System76 laptops running open source EC firmware support configuring charging thresholds through ACPI methods. Expose this functionality through the standard sysfs entries charge_control_{start,end}_threshold. Signed-off-by: Tim Crawford <tcrawford@xxxxxxxxxxxx> --- drivers/platform/x86/system76_acpi.c | 153 +++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/drivers/platform/x86/system76_acpi.c b/drivers/platform/x86/system76_acpi.c index 06f6509980e2..b42c4a384ba9 100644 --- a/drivers/platform/x86/system76_acpi.c +++ b/drivers/platform/x86/system76_acpi.c @@ -18,8 +18,11 @@ #include <linux/leds.h> #include <linux/module.h> #include <linux/pci_ids.h> +#include <linux/power_supply.h> #include <linux/types.h> +#include <acpi/battery.h> + struct system76_data { struct acpi_device *acpi_dev; struct led_classdev ap_led; @@ -144,6 +147,151 @@ static int system76_set(struct system76_data *data, char *method, int value) return -1; } +#define BATTERY_THRESHOLD_INVALID 0xFF + +enum { + THRESHOLD_START, + THRESHOLD_END, +}; + +static ssize_t battery_get_threshold(int which, char *buf) +{ + struct acpi_object_list input; + union acpi_object param; + acpi_handle handle; + acpi_status status; + unsigned long long ret = BATTERY_THRESHOLD_INVALID; + + handle = ec_get_handle(); + if (!handle) + return -ENODEV; + + input.count = 1; + input.pointer = ¶m; + // Start/stop selection + param.type = ACPI_TYPE_INTEGER; + param.integer.value = which; + + status = acpi_evaluate_integer(handle, "GBCT", &input, &ret); + if (ACPI_FAILURE(status)) + return -EIO; + if (ret == BATTERY_THRESHOLD_INVALID) + return -EINVAL; + + return sprintf(buf, "%d\n", (int)ret); +} + +static ssize_t battery_set_threshold(int which, const char *buf, size_t count) +{ + struct acpi_object_list input; + union acpi_object params[2]; + acpi_handle handle; + acpi_status status; + unsigned int value; + int ret; + + handle = ec_get_handle(); + if (!handle) + return -ENODEV; + + ret = kstrtouint(buf, 10, &value); + if (ret) + return ret; + + if (value > 100) + return -EINVAL; + + input.count = 2; + input.pointer = params; + // Start/stop selection + params[0].type = ACPI_TYPE_INTEGER; + params[0].integer.value = which; + // Threshold value + params[1].type = ACPI_TYPE_INTEGER; + params[1].integer.value = value; + + status = acpi_evaluate_object(handle, "SBCT", &input, NULL); + if (ACPI_FAILURE(status)) + return -EIO; + + return count; +} + +static ssize_t charge_control_start_threshold_show( + struct device *dev, + struct device_attribute *attr, + char *buf) +{ + return battery_get_threshold(THRESHOLD_START, buf); +} + +static ssize_t charge_control_start_threshold_store( + struct device *dev, + struct device_attribute *attr, + const char *buf, + size_t count) +{ + return battery_set_threshold(THRESHOLD_START, buf, count); +} + +static DEVICE_ATTR_RW(charge_control_start_threshold); + +static ssize_t charge_control_end_threshold_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return battery_get_threshold(THRESHOLD_END, buf); +} + +static ssize_t charge_control_end_threshold_store(struct device *dev, + struct device_attribute *attr, const char *buf, size_t count) +{ + return battery_set_threshold(THRESHOLD_END, buf, count); +} + +static DEVICE_ATTR_RW(charge_control_end_threshold); + +static int system76_battery_add(struct power_supply *battery) +{ + // System76 EC only supports 1 battery + if (strcmp(battery->desc->name, "BAT0") != 0) + return -ENODEV; + + device_create_file(&battery->dev, &dev_attr_charge_control_start_threshold); + device_create_file(&battery->dev, &dev_attr_charge_control_end_threshold); + return 0; +} + +static int system76_battery_remove(struct power_supply *battery) +{ + device_remove_file(&battery->dev, &dev_attr_charge_control_start_threshold); + device_remove_file(&battery->dev, &dev_attr_charge_control_end_threshold); + return 0; +} + +static struct acpi_battery_hook system76_battery_hook = { + .add_battery = system76_battery_add, + .remove_battery = system76_battery_remove, + .name = "System76 Battery Extension", +}; + +static void system76_battery_init(void) +{ + acpi_handle handle; + + handle = ec_get_handle(); + if (handle && acpi_has_method(handle, "GBCT")) + battery_hook_register(&system76_battery_hook); +} + +static void system76_battery_exit(void) +{ + acpi_handle handle; + + handle = ec_get_handle(); + if (handle && acpi_has_method(handle, "GBCT")) + battery_hook_unregister(&system76_battery_hook); +} + // Get the airplane mode LED brightness static enum led_brightness ap_led_get(struct led_classdev *led) { @@ -545,6 +693,8 @@ static int system76_add(struct acpi_device *acpi_dev) return err; } + system76_battery_init(); + return 0; } @@ -554,6 +704,9 @@ static int system76_remove(struct acpi_device *acpi_dev) struct system76_data *data; data = acpi_driver_data(acpi_dev); + + system76_battery_exit(); + if (data->kb_color >= 0) device_remove_file(data->kb_led.dev, &kb_led_color_dev_attr); -- 2.31.1