1 // SPDX-License-Identifier: GPL-2.0-or-later 2 /* 3 * ChromeOS EC driver for charge control 4 * 5 * Copyright (C) 2024 Thomas Weißschuh <linux@weissschuh.net> 6 */ 7 #include <acpi/battery.h> 8 #include <linux/container_of.h> 9 #include <linux/dmi.h> 10 #include <linux/lockdep.h> 11 #include <linux/mod_devicetable.h> 12 #include <linux/module.h> 13 #include <linux/mutex.h> 14 #include <linux/platform_data/cros_ec_commands.h> 15 #include <linux/platform_data/cros_ec_proto.h> 16 #include <linux/platform_device.h> 17 #include <linux/types.h> 18 19 #define EC_CHARGE_CONTROL_BEHAVIOURS (BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) | \ 20 BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE) | \ 21 BIT(POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE)) 22 23 /* 24 * Semantics of data *returned* from the EC API and Linux sysfs differ 25 * slightly, also the v1 API can not return any data. 26 * To match the expected sysfs API, data is never read back from the EC but 27 * cached in the driver. 28 * 29 * Changes to the EC bypassing the driver will not be reflected in sysfs. 30 * Any change to "charge_behaviour" will synchronize the EC with the driver state. 31 */ 32 33 struct cros_chctl_priv { 34 struct device *dev; 35 struct cros_ec_device *cros_ec; 36 struct acpi_battery_hook battery_hook; 37 struct power_supply *hooked_battery; 38 u8 cmd_version; 39 40 const struct power_supply_ext *psy_ext; 41 42 struct mutex lock; /* protects fields below and cros_ec */ 43 enum power_supply_charge_behaviour current_behaviour; 44 u8 current_start_threshold, current_end_threshold; 45 }; 46 47 static int cros_chctl_send_charge_control_cmd(struct cros_ec_device *cros_ec, 48 u8 cmd_version, struct ec_params_charge_control *req) 49 { 50 static const u8 outsizes[] = { 51 [1] = offsetof(struct ec_params_charge_control, cmd), 52 [2] = sizeof(struct ec_params_charge_control), 53 [3] = sizeof(struct ec_params_charge_control), 54 }; 55 56 struct { 57 struct cros_ec_command msg; 58 union { 59 struct ec_params_charge_control req; 60 struct ec_response_charge_control resp; 61 } __packed data; 62 } __packed buf = { 63 .msg = { 64 .command = EC_CMD_CHARGE_CONTROL, 65 .version = cmd_version, 66 .insize = 0, 67 .outsize = outsizes[cmd_version], 68 }, 69 .data.req = *req, 70 }; 71 72 return cros_ec_cmd_xfer_status(cros_ec, &buf.msg); 73 } 74 75 static int cros_chctl_configure_ec(struct cros_chctl_priv *priv) 76 { 77 struct ec_params_charge_control req = {}; 78 79 lockdep_assert_held(&priv->lock); 80 81 req.cmd = EC_CHARGE_CONTROL_CMD_SET; 82 83 switch (priv->current_behaviour) { 84 case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: 85 req.mode = CHARGE_CONTROL_NORMAL; 86 break; 87 case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: 88 req.mode = CHARGE_CONTROL_IDLE; 89 break; 90 case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: 91 req.mode = CHARGE_CONTROL_DISCHARGE; 92 break; 93 default: 94 return -EINVAL; 95 } 96 97 if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO && 98 !(priv->current_start_threshold == 0 && priv->current_end_threshold == 100)) { 99 req.sustain_soc.lower = priv->current_start_threshold; 100 req.sustain_soc.upper = priv->current_end_threshold; 101 } else { 102 /* Disable charging limits */ 103 req.sustain_soc.lower = -1; 104 req.sustain_soc.upper = -1; 105 } 106 107 return cros_chctl_send_charge_control_cmd(priv->cros_ec, priv->cmd_version, &req); 108 } 109 110 static int cros_chctl_psy_ext_get_prop(struct power_supply *psy, 111 const struct power_supply_ext *ext, 112 void *data, 113 enum power_supply_property psp, 114 union power_supply_propval *val) 115 { 116 struct cros_chctl_priv *priv = data; 117 118 switch (psp) { 119 case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD: 120 val->intval = priv->current_start_threshold; 121 return 0; 122 case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: 123 val->intval = priv->current_end_threshold; 124 return 0; 125 case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: 126 val->intval = priv->current_behaviour; 127 return 0; 128 default: 129 return -EINVAL; 130 } 131 } 132 133 static int cros_chctl_psy_ext_set_threshold(struct cros_chctl_priv *priv, 134 enum power_supply_property psp, 135 int val) 136 { 137 int ret; 138 139 if (val < 0 || val > 100) 140 return -EINVAL; 141 142 if (psp == POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD) { 143 /* Start threshold is not exposed, use fixed value */ 144 if (priv->cmd_version == 2) 145 priv->current_start_threshold = val == 100 ? 0 : val; 146 147 if (val < priv->current_start_threshold) 148 return -EINVAL; 149 priv->current_end_threshold = val; 150 } else { 151 if (val > priv->current_end_threshold) 152 return -EINVAL; 153 priv->current_start_threshold = val; 154 } 155 156 if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) { 157 ret = cros_chctl_configure_ec(priv); 158 if (ret < 0) 159 return ret; 160 } 161 162 return 0; 163 } 164 165 166 static int cros_chctl_psy_ext_set_prop(struct power_supply *psy, 167 const struct power_supply_ext *ext, 168 void *data, 169 enum power_supply_property psp, 170 const union power_supply_propval *val) 171 { 172 struct cros_chctl_priv *priv = data; 173 int ret; 174 175 guard(mutex)(&priv->lock); 176 177 switch (psp) { 178 case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD: 179 case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: 180 return cros_chctl_psy_ext_set_threshold(priv, psp, val->intval); 181 case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: 182 priv->current_behaviour = val->intval; 183 ret = cros_chctl_configure_ec(priv); 184 if (ret < 0) 185 return ret; 186 return 0; 187 default: 188 return -EINVAL; 189 } 190 } 191 192 static int cros_chctl_psy_prop_is_writeable(struct power_supply *psy, 193 const struct power_supply_ext *ext, 194 void *data, 195 enum power_supply_property psp) 196 { 197 return true; 198 } 199 200 #define DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(_name, ...) \ 201 static const enum power_supply_property _name ## _props[] = { \ 202 __VA_ARGS__, \ 203 }; \ 204 \ 205 static const struct power_supply_ext _name = { \ 206 .name = "cros-charge-control", \ 207 .properties = _name ## _props, \ 208 .num_properties = ARRAY_SIZE(_name ## _props), \ 209 .charge_behaviours = EC_CHARGE_CONTROL_BEHAVIOURS, \ 210 .get_property = cros_chctl_psy_ext_get_prop, \ 211 .set_property = cros_chctl_psy_ext_set_prop, \ 212 .property_is_writeable = cros_chctl_psy_prop_is_writeable, \ 213 } 214 215 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v1, 216 POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR 217 ); 218 219 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v2, 220 POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, 221 POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD 222 ); 223 224 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v3, 225 POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, 226 POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD, 227 POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD 228 ); 229 230 static int cros_chctl_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) 231 { 232 struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook); 233 234 if (priv->hooked_battery) 235 return 0; 236 237 priv->hooked_battery = battery; 238 return power_supply_register_extension(battery, priv->psy_ext, priv->dev, priv); 239 } 240 241 static int cros_chctl_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) 242 { 243 struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook); 244 245 if (priv->hooked_battery == battery) { 246 power_supply_unregister_extension(battery, priv->psy_ext); 247 priv->hooked_battery = NULL; 248 } 249 250 return 0; 251 } 252 253 static bool probe_with_fwk_charge_control; 254 module_param(probe_with_fwk_charge_control, bool, 0644); 255 MODULE_PARM_DESC(probe_with_fwk_charge_control, 256 "Probe the driver in the presence of the custom Framework EC charge control"); 257 258 static int cros_chctl_fwk_charge_control_versions(struct cros_ec_device *cros_ec) 259 { 260 if (!dmi_match(DMI_SYS_VENDOR, "Framework")) 261 return 0; 262 263 return cros_ec_get_cmd_versions(cros_ec, 0x3E03 /* FW_EC_CMD_CHARGE_LIMIT */); 264 } 265 266 static int cros_chctl_probe(struct platform_device *pdev) 267 { 268 struct device *dev = &pdev->dev; 269 struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent); 270 struct cros_ec_device *cros_ec = ec_dev->ec_dev; 271 struct cros_chctl_priv *priv; 272 int ret; 273 274 ret = cros_chctl_fwk_charge_control_versions(cros_ec); 275 if (ret < 0) 276 return ret; 277 if (ret > 0 && !probe_with_fwk_charge_control) { 278 dev_info(dev, "Framework charge control detected, preventing load\n"); 279 return -ENODEV; 280 } 281 282 priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); 283 if (!priv) 284 return -ENOMEM; 285 286 ret = devm_mutex_init(dev, &priv->lock); 287 if (ret) 288 return ret; 289 290 ret = cros_ec_get_cmd_versions(cros_ec, EC_CMD_CHARGE_CONTROL); 291 if (ret < 0) 292 return ret; 293 else if (ret & EC_VER_MASK(3)) 294 priv->cmd_version = 3; 295 else if (ret & EC_VER_MASK(2)) 296 priv->cmd_version = 2; 297 else if (ret & EC_VER_MASK(1)) 298 priv->cmd_version = 1; 299 else 300 return -ENODEV; 301 302 dev_dbg(dev, "Command version: %u\n", (unsigned int)priv->cmd_version); 303 304 priv->dev = dev; 305 priv->cros_ec = cros_ec; 306 307 if (priv->cmd_version == 1) 308 priv->psy_ext = &cros_chctl_psy_ext_v1; 309 else if (priv->cmd_version == 2) 310 priv->psy_ext = &cros_chctl_psy_ext_v2; 311 else 312 priv->psy_ext = &cros_chctl_psy_ext_v3; 313 314 priv->battery_hook.name = dev_name(dev); 315 priv->battery_hook.add_battery = cros_chctl_add_battery; 316 priv->battery_hook.remove_battery = cros_chctl_remove_battery; 317 318 priv->current_behaviour = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; 319 priv->current_start_threshold = 0; 320 priv->current_end_threshold = 100; 321 322 /* Bring EC into well-known state */ 323 scoped_guard(mutex, &priv->lock) 324 ret = cros_chctl_configure_ec(priv); 325 if (ret < 0) 326 return ret; 327 328 return devm_battery_hook_register(dev, &priv->battery_hook); 329 } 330 331 static const struct platform_device_id cros_chctl_id[] = { 332 { "cros-charge-control", 0 }, 333 {} 334 }; 335 336 static struct platform_driver cros_chctl_driver = { 337 .driver.name = "cros-charge-control", 338 .probe = cros_chctl_probe, 339 .id_table = cros_chctl_id, 340 }; 341 module_platform_driver(cros_chctl_driver); 342 343 MODULE_DEVICE_TABLE(platform, cros_chctl_id); 344 MODULE_DESCRIPTION("ChromeOS EC charge control"); 345 MODULE_AUTHOR("Thomas Weißschuh <linux@weissschuh.net>"); 346 MODULE_LICENSE("GPL"); 347