platform/x86:dell-laptop: Add knobs to change battery charge settings

The Dell BIOS allows you to set custom charging modes, which is useful
in particular for extending battery life. This adds support for tweaking
those various settings from Linux via sysfs knobs. One might, for
example, have their laptop plugged into power at their desk the vast
majority of the time and choose fairly aggressive battery-saving
settings (eg, only charging once the battery drops below 50% and only
charging up to 80%). When leaving for a trip, it would be more useful
to instead switch to a standard charging mode (top off at 100%, charge
any time power is available). Rebooting into the BIOS to change the
charging mode settings is a hassle.

For the Custom charging type mode, we reuse the common
charge_control_{start,end}_threshold sysfs power_supply entries. The
BIOS also has a bunch of other charging modes (with varying levels of
usefulness), so this also adds a 'charge_type' sysfs entry that maps the
standard values to Dell-specific ones.

This work is based on a patch by Perry Yuan <perry_yuan@dell.com> and
Limonciello Mario <Mario_Limonciello@Dell.com> submitted back in 2020.

Hans de Goede:
s/charge_type/charge_types/ since charge_types_show() used the new
charge_types power-supply property output format.

Signed-off-by: Andres Salomon <dilinger@queued.net>
Reviewed-by: Pali Rohár <pali@kernel.org>
Link: https://lore.kernel.org/r/20240820033005.09e03af1@5400
Reviewed-by: Hans de Goede <hdegoede@redhat.com>
Signed-off-by: Hans de Goede <hdegoede@redhat.com>
This commit is contained in:
Andres Salomon 2024-08-20 03:30:05 -04:00 committed by Hans de Goede
parent c34068c8ed
commit ab58016c68
3 changed files with 318 additions and 0 deletions

View File

@ -49,6 +49,7 @@ config DELL_LAPTOP
default m
depends on DMI
depends on BACKLIGHT_CLASS_DEVICE
depends on ACPI_BATTERY
depends on ACPI_VIDEO || ACPI_VIDEO = n
depends on RFKILL || RFKILL = n
depends on DELL_WMI || DELL_WMI = n

View File

@ -22,11 +22,13 @@
#include <linux/io.h>
#include <linux/rfkill.h>
#include <linux/power_supply.h>
#include <linux/sysfs.h>
#include <linux/acpi.h>
#include <linux/mm.h>
#include <linux/i8042.h>
#include <linux/debugfs.h>
#include <linux/seq_file.h>
#include <acpi/battery.h>
#include <acpi/video.h>
#include "dell-rbtn.h"
#include "dell-smbios.h"
@ -99,6 +101,20 @@ static bool force_rfkill;
static bool micmute_led_registered;
static bool mute_led_registered;
struct battery_mode_info {
int token;
const char *label;
};
static const struct battery_mode_info battery_modes[] = {
{ BAT_PRI_AC_MODE_TOKEN, "Trickle" },
{ BAT_EXPRESS_MODE_TOKEN, "Fast" },
{ BAT_STANDARD_MODE_TOKEN, "Standard" },
{ BAT_ADAPTIVE_MODE_TOKEN, "Adaptive" },
{ BAT_CUSTOM_MODE_TOKEN, "Custom" },
};
static u32 battery_supported_modes;
module_param(force_rfkill, bool, 0444);
MODULE_PARM_DESC(force_rfkill, "enable rfkill on non whitelisted models");
@ -353,6 +369,32 @@ static const struct dmi_system_id dell_quirks[] __initconst = {
{ }
};
/* -1 is a sentinel value, telling us to use token->value */
#define USE_TVAL ((u32) -1)
static int dell_send_request_for_tokenid(struct calling_interface_buffer *buffer,
u16 class, u16 select, u16 tokenid,
u32 val)
{
struct calling_interface_token *token;
token = dell_smbios_find_token(tokenid);
if (!token)
return -ENODEV;
if (val == USE_TVAL)
val = token->value;
dell_fill_request(buffer, token->location, val, 0, 0);
return dell_send_request(buffer, class, select);
}
static inline int dell_set_std_token_value(struct calling_interface_buffer *buffer,
u16 tokenid, u32 value)
{
return dell_send_request_for_tokenid(buffer, CLASS_TOKEN_WRITE,
SELECT_TOKEN_STD, tokenid, value);
}
/*
* Derived from information in smbios-wireless-ctl:
*
@ -2183,6 +2225,271 @@ static struct led_classdev mute_led_cdev = {
.default_trigger = "audio-mute",
};
static int dell_battery_set_mode(const u16 tokenid)
{
struct calling_interface_buffer buffer;
return dell_set_std_token_value(&buffer, tokenid, USE_TVAL);
}
static int dell_battery_read(const u16 tokenid)
{
struct calling_interface_buffer buffer;
int err;
err = dell_send_request_for_tokenid(&buffer, CLASS_TOKEN_READ,
SELECT_TOKEN_STD, tokenid, 0);
if (err)
return err;
if (buffer.output[1] > INT_MAX)
return -EIO;
return buffer.output[1];
}
static bool dell_battery_mode_is_active(const u16 tokenid)
{
struct calling_interface_token *token;
int ret;
ret = dell_battery_read(tokenid);
if (ret < 0)
return false;
token = dell_smbios_find_token(tokenid);
/* token's already verified by dell_battery_read() */
return token->value == (u16) ret;
}
/*
* The rules: the minimum start charging value is 50%. The maximum
* start charging value is 95%. The minimum end charging value is
* 55%. The maximum end charging value is 100%. And finally, there
* has to be at least a 5% difference between start & end values.
*/
#define CHARGE_START_MIN 50
#define CHARGE_START_MAX 95
#define CHARGE_END_MIN 55
#define CHARGE_END_MAX 100
#define CHARGE_MIN_DIFF 5
static int dell_battery_set_custom_charge_start(int start)
{
struct calling_interface_buffer buffer;
int end;
start = clamp(start, CHARGE_START_MIN, CHARGE_START_MAX);
end = dell_battery_read(BAT_CUSTOM_CHARGE_END);
if (end < 0)
return end;
if ((end - start) < CHARGE_MIN_DIFF)
start = end - CHARGE_MIN_DIFF;
return dell_set_std_token_value(&buffer, BAT_CUSTOM_CHARGE_START,
start);
}
static int dell_battery_set_custom_charge_end(int end)
{
struct calling_interface_buffer buffer;
int start;
end = clamp(end, CHARGE_END_MIN, CHARGE_END_MAX);
start = dell_battery_read(BAT_CUSTOM_CHARGE_START);
if (start < 0)
return start;
if ((end - start) < CHARGE_MIN_DIFF)
end = start + CHARGE_MIN_DIFF;
return dell_set_std_token_value(&buffer, BAT_CUSTOM_CHARGE_END, end);
}
static ssize_t charge_types_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
ssize_t count = 0;
int i;
for (i = 0; i < ARRAY_SIZE(battery_modes); i++) {
bool active;
if (!(battery_supported_modes & BIT(i)))
continue;
active = dell_battery_mode_is_active(battery_modes[i].token);
count += sysfs_emit_at(buf, count, active ? "[%s] " : "%s ",
battery_modes[i].label);
}
/* convert the last space to a newline */
if (count > 0)
count--;
count += sysfs_emit_at(buf, count, "\n");
return count;
}
static ssize_t charge_types_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t size)
{
bool matched = false;
int err, i;
for (i = 0; i < ARRAY_SIZE(battery_modes); i++) {
if (!(battery_supported_modes & BIT(i)))
continue;
if (sysfs_streq(battery_modes[i].label, buf)) {
matched = true;
break;
}
}
if (!matched)
return -EINVAL;
err = dell_battery_set_mode(battery_modes[i].token);
if (err)
return err;
return size;
}
static ssize_t charge_control_start_threshold_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
int start;
start = dell_battery_read(BAT_CUSTOM_CHARGE_START);
if (start < 0)
return start;
if (start > CHARGE_START_MAX)
return -EIO;
return sysfs_emit(buf, "%d\n", start);
}
static ssize_t charge_control_start_threshold_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t size)
{
int ret, start;
ret = kstrtoint(buf, 10, &start);
if (ret)
return ret;
if (start < 0 || start > 100)
return -EINVAL;
ret = dell_battery_set_custom_charge_start(start);
if (ret)
return ret;
return size;
}
static ssize_t charge_control_end_threshold_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
int end;
end = dell_battery_read(BAT_CUSTOM_CHARGE_END);
if (end < 0)
return end;
if (end > CHARGE_END_MAX)
return -EIO;
return sysfs_emit(buf, "%d\n", end);
}
static ssize_t charge_control_end_threshold_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t size)
{
int ret, end;
ret = kstrtouint(buf, 10, &end);
if (ret)
return ret;
if (end < 0 || end > 100)
return -EINVAL;
ret = dell_battery_set_custom_charge_end(end);
if (ret)
return ret;
return size;
}
static DEVICE_ATTR_RW(charge_control_start_threshold);
static DEVICE_ATTR_RW(charge_control_end_threshold);
static DEVICE_ATTR_RW(charge_types);
static struct attribute *dell_battery_attrs[] = {
&dev_attr_charge_control_start_threshold.attr,
&dev_attr_charge_control_end_threshold.attr,
&dev_attr_charge_types.attr,
NULL,
};
ATTRIBUTE_GROUPS(dell_battery);
static int dell_battery_add(struct power_supply *battery,
struct acpi_battery_hook *hook)
{
/* this currently only supports the primary battery */
if (strcmp(battery->desc->name, "BAT0") != 0)
return -ENODEV;
return device_add_groups(&battery->dev, dell_battery_groups);
}
static int dell_battery_remove(struct power_supply *battery,
struct acpi_battery_hook *hook)
{
device_remove_groups(&battery->dev, dell_battery_groups);
return 0;
}
static struct acpi_battery_hook dell_battery_hook = {
.add_battery = dell_battery_add,
.remove_battery = dell_battery_remove,
.name = "Dell Primary Battery Extension",
};
static u32 __init battery_get_supported_modes(void)
{
u32 modes = 0;
int i;
for (i = 0; i < ARRAY_SIZE(battery_modes); i++) {
if (dell_smbios_find_token(battery_modes[i].token))
modes |= BIT(i);
}
return modes;
}
static void __init dell_battery_init(struct device *dev)
{
battery_supported_modes = battery_get_supported_modes();
if (battery_supported_modes != 0)
battery_hook_register(&dell_battery_hook);
}
static void dell_battery_exit(void)
{
if (battery_supported_modes != 0)
battery_hook_unregister(&dell_battery_hook);
}
static int __init dell_init(void)
{
struct calling_interface_token *token;
@ -2219,6 +2526,7 @@ static int __init dell_init(void)
touchpad_led_init(&platform_device->dev);
kbd_led_init(&platform_device->dev);
dell_battery_init(&platform_device->dev);
dell_laptop_dir = debugfs_create_dir("dell_laptop", NULL);
debugfs_create_file("rfkill", 0444, dell_laptop_dir, NULL,
@ -2293,6 +2601,7 @@ fail_backlight:
if (mute_led_registered)
led_classdev_unregister(&mute_led_cdev);
fail_led:
dell_battery_exit();
dell_cleanup_rfkill();
fail_rfkill:
platform_device_del(platform_device);
@ -2311,6 +2620,7 @@ static void __exit dell_exit(void)
if (quirks && quirks->touchpad_led)
touchpad_led_exit();
kbd_led_exit();
dell_battery_exit();
backlight_device_unregister(dell_backlight_device);
if (micmute_led_registered)
led_classdev_unregister(&micmute_led_cdev);

View File

@ -33,6 +33,13 @@
#define KBD_LED_AUTO_50_TOKEN 0x02EB
#define KBD_LED_AUTO_75_TOKEN 0x02EC
#define KBD_LED_AUTO_100_TOKEN 0x02F6
#define BAT_PRI_AC_MODE_TOKEN 0x0341
#define BAT_ADAPTIVE_MODE_TOKEN 0x0342
#define BAT_CUSTOM_MODE_TOKEN 0x0343
#define BAT_STANDARD_MODE_TOKEN 0x0346
#define BAT_EXPRESS_MODE_TOKEN 0x0347
#define BAT_CUSTOM_CHARGE_START 0x0349
#define BAT_CUSTOM_CHARGE_END 0x034A
#define GLOBAL_MIC_MUTE_ENABLE 0x0364
#define GLOBAL_MIC_MUTE_DISABLE 0x0365
#define GLOBAL_MUTE_ENABLE 0x058C