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 | 157 +++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/drivers/platform/x86/system76_acpi.c b/drivers/platform/x86/system76_acpi.c index 9e525b51a267..70d0490e23f5 100644 --- a/drivers/platform/x86/system76_acpi.c +++ b/drivers/platform/x86/system76_acpi.c @@ -18,8 +18,12 @@ #include <linux/leds.h> #include <linux/module.h> #include <linux/pci_ids.h> +#include <linux/power_supply.h> +#include <linux/sysfs.h> #include <linux/types.h> +#include <acpi/battery.h> + struct system76_data { struct acpi_device *acpi_dev; struct led_classdev ap_led; @@ -143,6 +147,154 @@ 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 sysfs_emit(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 struct attribute *system76_battery_attrs[] = { + &dev_attr_charge_control_start_threshold.attr, + &dev_attr_charge_control_end_threshold.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(system76_battery); + +static int system76_battery_add(struct power_supply *battery) +{ + // System76 EC only supports 1 battery + if (strcmp(battery->desc->name, "BAT0") != 0) + return -ENODEV; + + if (device_add_groups(&battery->dev, system76_battery_groups)) + return -ENODEV; + + return 0; +} + +static int system76_battery_remove(struct power_supply *battery) +{ + device_remove_groups(&battery->dev, system76_battery_groups); + 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) { @@ -581,6 +733,8 @@ static int system76_add(struct acpi_device *acpi_dev) if (err) goto error; + system76_battery_init(); + return 0; error: @@ -596,6 +750,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