[PATCH v2 1/2] platform/x86:dell-laptop: Add knobs to change battery charge settings

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



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 (only charging once the battery drops to 50% and only charging
up to 80%). When leaving for a trip, they might want to switch those
settings to charge to 100% and charge any time power is available;
rebooting into the BIOS to change those 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 adds a new Dell-specific sysfs entry called
'charge_type' that's documented in sysfs-class-power-dell (and looks a
lot like sysfs-class-power-wilco).

This work is based on a patch by Perry Yuan <perry_yuan@xxxxxxxx> and
Limonciello Mario <Mario_Limonciello@xxxxxxxx> submitted back in 2020:
https://lore.kernel.org/all/20200729065424.12851-1-Perry_Yuan@xxxxxxxx/
Both of their email addresses bounce, so I'm assuming they're no longer
with the company. I've reworked most of the patch to make it smaller and
cleaner.

Signed-off-by: Andres Salomon <dilinger@xxxxxxxxxx>
---
Changes in v2, based on extensive feedback from Pali Rohár <pali@xxxxxxxxxx>:
    - code style changes
    - change battery write API to use token->value instead of passed value
    - stop caching current mode, instead querying SMBIOS as needed
    - drop the separate list of charging modes enum
    - rework the list of charging mode strings
    - query SMBIOS for supported charging modes
    - split dell_battery_custom_set() up
---
 .../ABI/testing/sysfs-class-power-dell        |  31 ++
 drivers/platform/x86/dell/Kconfig             |   1 +
 drivers/platform/x86/dell/dell-laptop.c       | 288 ++++++++++++++++++
 drivers/platform/x86/dell/dell-smbios.h       |   7 +
 4 files changed, 327 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-class-power-dell

diff --git a/Documentation/ABI/testing/sysfs-class-power-dell b/Documentation/ABI/testing/sysfs-class-power-dell
new file mode 100644
index 000000000000..ef72c5f797ce
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-power-dell
@@ -0,0 +1,31 @@
+What:		/sys/class/power_supply/<supply_name>/charge_type
+Date:		July 2024
+KernelVersion:	6.11
+Contact:	linux-pm@xxxxxxxxxxxxxxx
+Description:
+		Select the charging algorithm to use for the (primary)
+		battery.
+
+		Standard:
+			Fully charge the battery at a moderate rate.
+		ExpressCharge™:
+			Quickly charge the battery using fast-charge
+			technology. This is harder on the battery than
+			standard charging and may lower its lifespan.
+		Primarily AC Use:
+			Users who primarily operate the system while
+			plugged into an external power source can extend
+			battery life with this mode.
+		Adaptive:
+			Automatically optimize battery charge rate based
+			on typical usage.
+		Custom:
+			Use the charge_control_* properties to determine
+			when to start and stop charging. Advanced users
+			can use this to drastically extend battery life.
+
+		Access: Read, Write
+		Valid values:
+			      "standard", "express", "primarily_ac",
+			      "adaptive", "custom"
+
diff --git a/drivers/platform/x86/dell/Kconfig b/drivers/platform/x86/dell/Kconfig
index 85a78ef91182..02405793163c 100644
--- a/drivers/platform/x86/dell/Kconfig
+++ b/drivers/platform/x86/dell/Kconfig
@@ -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
diff --git a/drivers/platform/x86/dell/dell-laptop.c b/drivers/platform/x86/dell/dell-laptop.c
index 6552dfe491c6..aae9a95fb188 100644
--- a/drivers/platform/x86/dell/dell-laptop.c
+++ b/drivers/platform/x86/dell/dell-laptop.c
@@ -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,18 @@ static bool force_rfkill;
 static bool micmute_led_registered;
 static bool mute_led_registered;
 
+static const struct {
+	int token;
+	const char *label;
+} battery_modes[] = {
+	{ BAT_STANDARD_MODE_TOKEN, "standard" },
+	{ BAT_EXPRESS_MODE_TOKEN, "express" },
+	{ BAT_PRI_AC_MODE_TOKEN, "primarily_ac" },
+	{ 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");
 
@@ -2183,6 +2197,277 @@ static struct led_classdev mute_led_cdev = {
 	.default_trigger = "audio-mute",
 };
 
+static int dell_send_request_by_token_loc(struct calling_interface_buffer *buffer,
+					  u16 class, u16 select, int type,
+					  u32 val)
+{
+	struct calling_interface_token *token;
+
+	token = dell_smbios_find_token(type);
+	if (!token)
+		return -ENODEV;
+
+	/* -1 is a sentinel value, telling us to use token->value */
+	if (val == (u32)-1)
+		val = token->value;
+
+	dell_fill_request(buffer, token->location, val, 0, 0);
+	return dell_send_request(buffer, class, select);
+}
+
+static int dell_battery_set_mode(const int type)
+{
+	struct calling_interface_buffer buffer;
+
+	/* -1 means use the value from the token */
+	return dell_send_request_by_token_loc(&buffer, CLASS_TOKEN_WRITE,
+			SELECT_TOKEN_STD, type, -1);
+}
+
+static int dell_battery_read(const int type)
+{
+	struct calling_interface_buffer buffer;
+	int err;
+
+	err = dell_send_request_by_token_loc(&buffer, CLASS_TOKEN_READ,
+			SELECT_TOKEN_STD, type, 0);
+	if (err)
+		return err;
+
+	return buffer.output[1];
+}
+
+static bool dell_battery_mode_is_active(const int type)
+{
+	struct calling_interface_token *token;
+
+	token = dell_smbios_find_token(type);
+	if (!token)
+		return false;
+
+	return token->value == dell_battery_read(type);
+}
+
+/* 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;
+
+	if (start < CHARGE_START_MIN)
+		start = CHARGE_START_MIN;
+	else if (start > CHARGE_START_MAX)
+		start = CHARGE_START_MAX;
+
+	end = dell_battery_read(BAT_CUSTOM_CHARGE_END);
+	/* a failed read is okay */
+	if (end < 0)
+		end = CHARGE_END_MAX;
+	if ((end - start) < CHARGE_MIN_DIFF)
+		start = end - CHARGE_MIN_DIFF;
+
+	return dell_send_request_by_token_loc(&buffer, CLASS_TOKEN_WRITE,
+			SELECT_TOKEN_STD, BAT_CUSTOM_CHARGE_START, start);
+}
+
+static int dell_battery_set_custom_charge_end(int end)
+{
+	struct calling_interface_buffer buffer;
+	int start;
+
+	if (end < CHARGE_END_MIN)
+		end = CHARGE_END_MIN;
+	else if (end > CHARGE_END_MAX)
+		end = CHARGE_END_MAX;
+
+	start = dell_battery_read(BAT_CUSTOM_CHARGE_START);
+	/* a failed read is okay */
+	if (start < 0)
+		start = CHARGE_START_MIN;
+	if ((end - start) < CHARGE_MIN_DIFF)
+		end = start + CHARGE_MIN_DIFF;
+
+	return dell_send_request_by_token_loc(&buffer, CLASS_TOKEN_WRITE,
+			SELECT_TOKEN_STD, BAT_CUSTOM_CHARGE_END, end);
+}
+
+static ssize_t charge_type_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_type_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 (sysfs_streq(battery_modes[i].label, buf)) {
+			matched = true;
+			break;
+		}
+	}
+	if (!matched || !(battery_supported_modes & BIT(i)))
+		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;
+
+	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;
+
+	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;
+
+	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;
+
+	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_type);
+
+static struct attribute *dell_battery_attrs[] = {
+	&dev_attr_charge_control_start_threshold.attr,
+	&dev_attr_charge_control_end_threshold.attr,
+	&dev_attr_charge_type.attr,
+	NULL,
+};
+ATTRIBUTE_GROUPS(dell_battery);
+
+static int dell_battery_add(struct power_supply *battery,
+		struct acpi_battery_hook *hook)
+{
+	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 __exit 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 +2504,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 +2579,7 @@ static int __init dell_init(void)
 	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 +2598,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);
diff --git a/drivers/platform/x86/dell/dell-smbios.h b/drivers/platform/x86/dell/dell-smbios.h
index ea0cc38642a2..77baa15eb523 100644
--- a/drivers/platform/x86/dell/dell-smbios.h
+++ b/drivers/platform/x86/dell/dell-smbios.h
@@ -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
-- 
2.39.2




-- 
I'm available for contract & employment work, see:
https://spindle.queued.net/~dilinger/resume-tech.pdf





[Index of Archives]     [Linux Kernel Development]     [Linux USB Devel]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux