diff --git a/README.md b/README.md index 591c041a943e2b6ab3eaa71dc8eab0c83949add5..ae8b386ed303e22693e35df6cab9cea73661156a 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,19 @@ This module requires `cros_ec` and `cros_ec_lpcs` to be loaded and functional. ### LEDs - `/sys/class/leds/framework_laptop::kbd_backlight` + +### Fan Control + +This driver supports up to 4 fans, and creates a HWMON interface with the name `framework_laptop`. + +- `fan[1-4]_input` - Read fan speed in RPM (read-only) +- `fan[1-4]_target` - Set target fan speed in RPM + - read-write on the first fan, write-only on the others +- `fan[1-4]_fault` - Fan removed indicator (read-only) +- `fan[1-4]_alarm` - Fan stall indicator (read-only) +- `pwm[1-4]` - Fan speed control in percent 0-100 (write-only) +- `pwm[1-4]_enable` - Enable automatic fan control (write-only) + - Currently you can write anything to enable, but I recommend writing `2` in case the driver is updated to support disabling automatic fan control. + - Writing to the other interfaces will disable automatic fan control. +- `pwm[1-4]_min` - returns 0 (read-only) +- `pwm[1-4]_max` - returns 100 (read-only) diff --git a/framework_laptop.c b/framework_laptop.c index b848ebee578c08c909b212143f7ed56b92ea0cf2..acfa8ce3f34e7ccecf5baa21b53094a9f29273cc 100644 --- a/framework_laptop.c +++ b/framework_laptop.c @@ -37,6 +37,7 @@ static struct device *ec_device; struct framework_data { struct platform_device *pdev; struct led_classdev kb_led; + struct device *hwmon_dev; }; #define EC_CMD_CHARGE_LIMIT_CONTROL 0x3E03 @@ -257,6 +258,357 @@ static int framework_laptop_battery_remove(struct power_supply *battery) return 0; } +// --- fanN_input --- +// Read the current fan speed from the EC's memory +static ssize_t ec_get_fan_speed(u8 idx, u16 *val) +{ + if (!ec_device) + return -ENODEV; + + struct cros_ec_device *ec = dev_get_drvdata(ec_device); + + const u8 offset = EC_MEMMAP_FAN + 2 * idx; + + return ec->cmd_readmem(ec, offset, sizeof(*val), val); +} + +static ssize_t fw_fan_speed_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct sensor_device_attribute *sen_attr = to_sensor_dev_attr(attr); + + u16 val; + if (ec_get_fan_speed(sen_attr->index, &val) < 0) { + return -EIO; + } + + if (val == EC_FAN_SPEED_NOT_PRESENT || val == EC_FAN_SPEED_STALLED) { + return sysfs_emit(buf, "%u\n", 0); + } + + // Format as string for sysfs + return sysfs_emit(buf, "%u\n", val); +} + +// --- fanN_target --- +static ssize_t ec_set_target_rpm(u8 idx, u32 *val) +{ + int ret; + if (!ec_device) + return -ENODEV; + + struct cros_ec_device *ec = dev_get_drvdata(ec_device); + + struct ec_params_pwm_set_fan_target_rpm_v1 params = { + .rpm = *val, + .fan_idx = idx, + }; + + ret = cros_ec_cmd(ec, 1, EC_CMD_PWM_SET_FAN_TARGET_RPM, ¶ms, + sizeof(params), NULL, 0); + if (ret < 0) + return -EIO; + + return 0; +} + +static ssize_t ec_get_target_rpm(u8 idx, u32 *val) +{ + int ret; + if (!ec_device) + return -ENODEV; + + struct cros_ec_device *ec = dev_get_drvdata(ec_device); + + struct ec_response_pwm_get_fan_rpm resp; + + // index isn't supported, it should only return fan 0's target + + ret = cros_ec_cmd(ec, 0, EC_CMD_PWM_GET_FAN_TARGET_RPM, NULL, 0, &resp, + sizeof(resp)); + if (ret < 0) + return -EIO; + + *val = resp.rpm; + + return 0; +} + +static ssize_t fw_fan_target_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute *sen_attr = to_sensor_dev_attr(attr); + u32 val; + + int err; + err = kstrtou32(buf, 10, &val); + if (err < 0) + return err; + + if (ec_set_target_rpm(sen_attr->index, &val) < 0) { + return -EIO; + } + + return count; +} + +static ssize_t fw_fan_target_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct sensor_device_attribute *sen_attr = to_sensor_dev_attr(attr); + + // Only fan 0 is supported + if (sen_attr->index != 0) { + return -EINVAL; + } + + u32 val; + if (ec_get_target_rpm(sen_attr->index, &val) < 0) { + return -EIO; + } + + // Format as string for sysfs + return sysfs_emit(buf, "%u\n", val); +} + +// --- fanN_fault --- +static ssize_t fw_fan_fault_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct sensor_device_attribute *sen_attr = to_sensor_dev_attr(attr); + + u16 val; + if (ec_get_fan_speed(sen_attr->index, &val) < 0) { + return -EIO; + } + + // Format as string for sysfs + return sysfs_emit(buf, "%u\n", val == EC_FAN_SPEED_NOT_PRESENT); +} + +// --- fanN_alarm --- +static ssize_t fw_fan_alarm_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct sensor_device_attribute *sen_attr = to_sensor_dev_attr(attr); + + u16 val; + if (ec_get_fan_speed(sen_attr->index, &val) < 0) { + return -EIO; + } + + // Format as string for sysfs + return sysfs_emit(buf, "%u\n", val == EC_FAN_SPEED_STALLED); +} + +// --- pwmN_enable --- +static ssize_t ec_set_auto_fan_ctrl(u8 idx) +{ + int ret; + if (!ec_device) + return -ENODEV; + + struct cros_ec_device *ec = dev_get_drvdata(ec_device); + + struct ec_params_auto_fan_ctrl_v1 params = { + .fan_idx = idx, + }; + + ret = cros_ec_cmd(ec, 1, EC_CMD_THERMAL_AUTO_FAN_CTRL, ¶ms, + sizeof(params), NULL, 0); + if (ret < 0) + return -EIO; + + return 0; +} + +static ssize_t fw_pwm_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute *sen_attr = to_sensor_dev_attr(attr); + + // The EC doesn't take any arguments for this command, + // so we don't need to parse the buffer + // u32 val; + + // int err; + // err = kstrtou32(buf, 10, &val); + // if (err < 0) + // return err; + + if (ec_set_auto_fan_ctrl(sen_attr->index) < 0) { + return -EIO; + } + + return count; +} + +// --- pwmN --- +static ssize_t ec_set_fan_duty(u8 idx, u32 *val) +{ + int ret; + if (!ec_device) + return -ENODEV; + + struct cros_ec_device *ec = dev_get_drvdata(ec_device); + + struct ec_params_pwm_set_fan_duty_v1 params = { + .percent = *val, + .fan_idx = idx, + }; + + ret = cros_ec_cmd(ec, 1, EC_CMD_PWM_SET_FAN_DUTY, ¶ms, + sizeof(params), NULL, 0); + if (ret < 0) + return -EIO; + + return 0; +} + +static ssize_t fw_pwm_store(struct device *dev, struct device_attribute *attr, + const char *buf, size_t count) +{ + struct sensor_device_attribute *sen_attr = to_sensor_dev_attr(attr); + u32 val; + + int err; + err = kstrtou32(buf, 10, &val); + if (err < 0) + return err; + + if (ec_set_fan_duty(sen_attr->index, &val) < 0) { + return -EIO; + } + + return count; +} + +static ssize_t fw_pwm_min_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%i\n", 0); +} + +static ssize_t fw_pwm_max_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + return sysfs_emit(buf, "%i\n", 100); +} + +static ssize_t ec_count_fans(size_t *val) +{ + if (!ec_device) + return -ENODEV; + + struct cros_ec_device *ec = dev_get_drvdata(ec_device); + + u16 fans[EC_FAN_SPEED_ENTRIES]; + + int ret = ec->cmd_readmem(ec, EC_MEMMAP_FAN, sizeof(fans), fans); + if (ret < 0) + return -EIO; + + for (size_t i = 0; i < EC_FAN_SPEED_ENTRIES; i++) { + if (fans[i] == EC_FAN_SPEED_NOT_PRESENT) { + *val = i; + return 0; + } + } + + *val = EC_FAN_SPEED_ENTRIES; + return 0; +} + +#define FW_ATTRS_PER_FAN 8 + +// clang-format off +static SENSOR_DEVICE_ATTR_RO(fan1_input, fw_fan_speed, 0); // Fan Reading +static SENSOR_DEVICE_ATTR_RW(fan1_target, fw_fan_target, 0); // Target RPM (RW on fan 0 only) +static SENSOR_DEVICE_ATTR_RO(fan1_fault, fw_fan_fault, 0); // Fan Not Present +static SENSOR_DEVICE_ATTR_RO(fan1_alarm, fw_fan_alarm, 0); // Fan Stalled +static SENSOR_DEVICE_ATTR_WO(pwm1_enable, fw_pwm_enable, 0); // Set Fan Control Mode +static SENSOR_DEVICE_ATTR_WO(pwm1, fw_pwm, 0); // Set Fan Speed +static SENSOR_DEVICE_ATTR_RO(pwm1_min, fw_pwm_min, 0); // Min Fan Speed +static SENSOR_DEVICE_ATTR_RO(pwm1_max, fw_pwm_max, 0); // Max Fan Speed + +static SENSOR_DEVICE_ATTR_RO(fan2_input, fw_fan_speed, 1); +static SENSOR_DEVICE_ATTR_WO(fan2_target, fw_fan_target, 1); +static SENSOR_DEVICE_ATTR_RO(fan2_fault, fw_fan_fault, 1); +static SENSOR_DEVICE_ATTR_RO(fan2_alarm, fw_fan_alarm, 1); +static SENSOR_DEVICE_ATTR_WO(pwm2_enable, fw_pwm_enable, 1); +static SENSOR_DEVICE_ATTR_WO(pwm2, fw_pwm, 1); +static SENSOR_DEVICE_ATTR_RO(pwm2_min, fw_pwm_min, 1); +static SENSOR_DEVICE_ATTR_RO(pwm2_max, fw_pwm_max, 1); + +static SENSOR_DEVICE_ATTR_RO(fan3_input, fw_fan_speed, 2); +static SENSOR_DEVICE_ATTR_WO(fan3_target, fw_fan_target, 2); +static SENSOR_DEVICE_ATTR_RO(fan3_fault, fw_fan_fault, 2); +static SENSOR_DEVICE_ATTR_RO(fan3_alarm, fw_fan_alarm, 2); +static SENSOR_DEVICE_ATTR_WO(pwm3_enable, fw_pwm_enable, 2); +static SENSOR_DEVICE_ATTR_WO(pwm3, fw_pwm, 2); +static SENSOR_DEVICE_ATTR_RO(pwm3_min, fw_pwm_min, 2); +static SENSOR_DEVICE_ATTR_RO(pwm3_max, fw_pwm_max, 2); + +static SENSOR_DEVICE_ATTR_RO(fan4_input, fw_fan_speed, 3); +static SENSOR_DEVICE_ATTR_WO(fan4_target, fw_fan_target, 3); +static SENSOR_DEVICE_ATTR_RO(fan4_fault, fw_fan_fault, 3); +static SENSOR_DEVICE_ATTR_RO(fan4_alarm, fw_fan_alarm, 3); +static SENSOR_DEVICE_ATTR_WO(pwm4_enable, fw_pwm_enable, 3); +static SENSOR_DEVICE_ATTR_WO(pwm4, fw_pwm, 3); +static SENSOR_DEVICE_ATTR_RO(pwm4_min, fw_pwm_min, 3); +static SENSOR_DEVICE_ATTR_RO(pwm4_max, fw_pwm_max, 3); +// clang-format on + +static struct attribute + *fw_hwmon_attrs[(EC_FAN_SPEED_ENTRIES * FW_ATTRS_PER_FAN) + 1] = { + &sensor_dev_attr_fan1_input.dev_attr.attr, + &sensor_dev_attr_fan1_target.dev_attr.attr, + &sensor_dev_attr_fan1_fault.dev_attr.attr, + &sensor_dev_attr_fan1_alarm.dev_attr.attr, + &sensor_dev_attr_pwm1_enable.dev_attr.attr, + &sensor_dev_attr_pwm1.dev_attr.attr, + &sensor_dev_attr_pwm1_min.dev_attr.attr, + &sensor_dev_attr_pwm1_max.dev_attr.attr, + + &sensor_dev_attr_fan2_input.dev_attr.attr, + &sensor_dev_attr_fan2_target.dev_attr.attr, + &sensor_dev_attr_fan2_fault.dev_attr.attr, + &sensor_dev_attr_fan2_alarm.dev_attr.attr, + &sensor_dev_attr_pwm2_enable.dev_attr.attr, + &sensor_dev_attr_pwm2.dev_attr.attr, + &sensor_dev_attr_pwm2_min.dev_attr.attr, + &sensor_dev_attr_pwm2_max.dev_attr.attr, + + &sensor_dev_attr_fan3_input.dev_attr.attr, + &sensor_dev_attr_fan3_target.dev_attr.attr, + &sensor_dev_attr_fan3_fault.dev_attr.attr, + &sensor_dev_attr_fan3_alarm.dev_attr.attr, + &sensor_dev_attr_pwm3_enable.dev_attr.attr, + &sensor_dev_attr_pwm3.dev_attr.attr, + &sensor_dev_attr_pwm3_min.dev_attr.attr, + &sensor_dev_attr_pwm3_max.dev_attr.attr, + + &sensor_dev_attr_fan4_input.dev_attr.attr, + &sensor_dev_attr_fan4_target.dev_attr.attr, + &sensor_dev_attr_fan4_fault.dev_attr.attr, + &sensor_dev_attr_fan4_alarm.dev_attr.attr, + &sensor_dev_attr_pwm4_enable.dev_attr.attr, + &sensor_dev_attr_pwm4.dev_attr.attr, + &sensor_dev_attr_pwm4_min.dev_attr.attr, + &sensor_dev_attr_pwm4_max.dev_attr.attr, + + NULL, + }; + +static const struct attribute_group fw_hwmon_group = { + .attrs = fw_hwmon_attrs, +}; + +static const struct attribute_group *fw_hwmon_groups[] = { &fw_hwmon_group, + NULL }; + static struct acpi_battery_hook framework_laptop_battery_hook = { .add_battery = framework_laptop_battery_add, .remove_battery = framework_laptop_battery_remove, @@ -335,6 +687,27 @@ static int framework_probe(struct platform_device *pdev) } #endif + struct cros_ec_device *ec = dev_get_drvdata(ec_device); + if (ec->cmd_readmem) { + // Count the number of fans + size_t fan_count; + if (ec_count_fans(&fan_count) < 0) { + dev_err(dev, DRV_NAME ": failed to count fans.\n"); + return -EINVAL; + } + // NULL terminates the list after the last detected fan + fw_hwmon_attrs[fan_count * FW_ATTRS_PER_FAN] = NULL; + + data->hwmon_dev = hwmon_device_register_with_groups( + dev, DRV_NAME, NULL, fw_hwmon_groups); + if (IS_ERR(data->hwmon_dev)) + return PTR_ERR(data->hwmon_dev); + + } else { + dev_err(dev, DRV_NAME ": fan readings could not be enabled for this EC %s.\n", + FRAMEWORK_LAPTOP_EC_DEVICE_NAME); + } + battery_hook_register(&framework_laptop_battery_hook); return ret; @@ -342,8 +715,16 @@ static int framework_probe(struct platform_device *pdev) static int framework_remove(struct platform_device *pdev) { + struct framework_data *data; + + data = (struct framework_data *)platform_get_drvdata(pdev); + battery_hook_unregister(&framework_laptop_battery_hook); + // Make sure it's not null before we try to unregister it + if (data && data->hwmon_dev) + hwmon_device_unregister(data->hwmon_dev); + put_device(ec_device); return 0;