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 int ret; 51 static const u8 outsizes[] = { 52 [1] = offsetof(struct ec_params_charge_control, cmd), 53 [2] = sizeof(struct ec_params_charge_control), 54 [3] = sizeof(struct ec_params_charge_control), 55 }; 56 57 ret = cros_ec_cmd(cros_ec, cmd_version, EC_CMD_CHARGE_CONTROL, req, 58 outsizes[cmd_version], NULL, 0); 59 60 if (ret < 0) 61 return ret; 62 63 return 0; 64 } 65 66 static int cros_chctl_configure_ec(struct cros_chctl_priv *priv) 67 { 68 struct ec_params_charge_control req = {}; 69 70 lockdep_assert_held(&priv->lock); 71 72 req.cmd = EC_CHARGE_CONTROL_CMD_SET; 73 74 switch (priv->current_behaviour) { 75 case POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO: 76 req.mode = CHARGE_CONTROL_NORMAL; 77 break; 78 case POWER_SUPPLY_CHARGE_BEHAVIOUR_INHIBIT_CHARGE: 79 req.mode = CHARGE_CONTROL_IDLE; 80 break; 81 case POWER_SUPPLY_CHARGE_BEHAVIOUR_FORCE_DISCHARGE: 82 req.mode = CHARGE_CONTROL_DISCHARGE; 83 break; 84 default: 85 return -EINVAL; 86 } 87 88 if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO && 89 !(priv->current_start_threshold == 0 && priv->current_end_threshold == 100)) { 90 req.sustain_soc.lower = priv->current_start_threshold; 91 req.sustain_soc.upper = priv->current_end_threshold; 92 } else { 93 /* Disable charging limits */ 94 req.sustain_soc.lower = -1; 95 req.sustain_soc.upper = -1; 96 } 97 98 return cros_chctl_send_charge_control_cmd(priv->cros_ec, priv->cmd_version, &req); 99 } 100 101 static int cros_chctl_psy_ext_get_prop(struct power_supply *psy, 102 const struct power_supply_ext *ext, 103 void *data, 104 enum power_supply_property psp, 105 union power_supply_propval *val) 106 { 107 struct cros_chctl_priv *priv = data; 108 109 switch (psp) { 110 case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD: 111 val->intval = priv->current_start_threshold; 112 return 0; 113 case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: 114 val->intval = priv->current_end_threshold; 115 return 0; 116 case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: 117 val->intval = priv->current_behaviour; 118 return 0; 119 default: 120 return -EINVAL; 121 } 122 } 123 124 static int cros_chctl_psy_ext_set_threshold(struct cros_chctl_priv *priv, 125 enum power_supply_property psp, 126 int val) 127 { 128 int ret; 129 130 if (val < 0 || val > 100) 131 return -EINVAL; 132 133 if (psp == POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD) { 134 /* Start threshold is not exposed, use fixed value */ 135 if (priv->cmd_version == 2) 136 priv->current_start_threshold = val == 100 ? 0 : val; 137 138 if (val < priv->current_start_threshold) 139 return -EINVAL; 140 priv->current_end_threshold = val; 141 } else { 142 if (val > priv->current_end_threshold) 143 return -EINVAL; 144 priv->current_start_threshold = val; 145 } 146 147 if (priv->current_behaviour == POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO) { 148 ret = cros_chctl_configure_ec(priv); 149 if (ret < 0) 150 return ret; 151 } 152 153 return 0; 154 } 155 156 157 static int cros_chctl_psy_ext_set_prop(struct power_supply *psy, 158 const struct power_supply_ext *ext, 159 void *data, 160 enum power_supply_property psp, 161 const union power_supply_propval *val) 162 { 163 struct cros_chctl_priv *priv = data; 164 int ret; 165 166 guard(mutex)(&priv->lock); 167 168 switch (psp) { 169 case POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD: 170 case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD: 171 return cros_chctl_psy_ext_set_threshold(priv, psp, val->intval); 172 case POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR: 173 priv->current_behaviour = val->intval; 174 ret = cros_chctl_configure_ec(priv); 175 if (ret < 0) 176 return ret; 177 return 0; 178 default: 179 return -EINVAL; 180 } 181 } 182 183 static int cros_chctl_psy_prop_is_writeable(struct power_supply *psy, 184 const struct power_supply_ext *ext, 185 void *data, 186 enum power_supply_property psp) 187 { 188 return true; 189 } 190 191 #define DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(_name, ...) \ 192 static const enum power_supply_property _name ## _props[] = { \ 193 __VA_ARGS__, \ 194 }; \ 195 \ 196 static const struct power_supply_ext _name = { \ 197 .name = "cros-charge-control", \ 198 .properties = _name ## _props, \ 199 .num_properties = ARRAY_SIZE(_name ## _props), \ 200 .charge_behaviours = EC_CHARGE_CONTROL_BEHAVIOURS, \ 201 .get_property = cros_chctl_psy_ext_get_prop, \ 202 .set_property = cros_chctl_psy_ext_set_prop, \ 203 .property_is_writeable = cros_chctl_psy_prop_is_writeable, \ 204 } 205 206 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v1, 207 POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR 208 ); 209 210 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v2, 211 POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, 212 POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD 213 ); 214 215 DEFINE_CROS_CHCTL_POWER_SUPPLY_EXTENSION(cros_chctl_psy_ext_v3, 216 POWER_SUPPLY_PROP_CHARGE_BEHAVIOUR, 217 POWER_SUPPLY_PROP_CHARGE_CONTROL_START_THRESHOLD, 218 POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD 219 ); 220 221 static int cros_chctl_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook) 222 { 223 struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook); 224 225 if (priv->hooked_battery) 226 return 0; 227 228 priv->hooked_battery = battery; 229 return power_supply_register_extension(battery, priv->psy_ext, priv->dev, priv); 230 } 231 232 static int cros_chctl_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook) 233 { 234 struct cros_chctl_priv *priv = container_of(hook, struct cros_chctl_priv, battery_hook); 235 236 if (priv->hooked_battery == battery) { 237 power_supply_unregister_extension(battery, priv->psy_ext); 238 priv->hooked_battery = NULL; 239 } 240 241 return 0; 242 } 243 244 static bool probe_with_fwk_charge_control; 245 module_param(probe_with_fwk_charge_control, bool, 0644); 246 MODULE_PARM_DESC(probe_with_fwk_charge_control, 247 "Probe the driver in the presence of the custom Framework EC charge control"); 248 249 static int cros_chctl_fwk_charge_control_versions(struct cros_ec_device *cros_ec) 250 { 251 if (!dmi_match(DMI_SYS_VENDOR, "Framework")) 252 return 0; 253 254 return cros_ec_get_cmd_versions(cros_ec, 0x3E03 /* FW_EC_CMD_CHARGE_LIMIT */); 255 } 256 257 static int cros_chctl_probe(struct platform_device *pdev) 258 { 259 struct device *dev = &pdev->dev; 260 struct cros_ec_dev *ec_dev = dev_get_drvdata(dev->parent); 261 struct cros_ec_device *cros_ec = ec_dev->ec_dev; 262 struct cros_chctl_priv *priv; 263 int ret; 264 265 ret = cros_chctl_fwk_charge_control_versions(cros_ec); 266 if (ret < 0) 267 return ret; 268 if (ret > 0 && !probe_with_fwk_charge_control) { 269 dev_info(dev, "Framework charge control detected, preventing load\n"); 270 return -ENODEV; 271 } 272 273 priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); 274 if (!priv) 275 return -ENOMEM; 276 277 ret = devm_mutex_init(dev, &priv->lock); 278 if (ret) 279 return ret; 280 281 ret = cros_ec_get_cmd_versions(cros_ec, EC_CMD_CHARGE_CONTROL); 282 if (ret < 0) 283 return ret; 284 else if (ret & EC_VER_MASK(3)) 285 priv->cmd_version = 3; 286 else if (ret & EC_VER_MASK(2)) 287 priv->cmd_version = 2; 288 else if (ret & EC_VER_MASK(1)) 289 priv->cmd_version = 1; 290 else 291 return -ENODEV; 292 293 dev_dbg(dev, "Command version: %u\n", (unsigned int)priv->cmd_version); 294 295 priv->dev = dev; 296 priv->cros_ec = cros_ec; 297 298 if (priv->cmd_version == 1) 299 priv->psy_ext = &cros_chctl_psy_ext_v1; 300 else if (priv->cmd_version == 2) 301 priv->psy_ext = &cros_chctl_psy_ext_v2; 302 else 303 priv->psy_ext = &cros_chctl_psy_ext_v3; 304 305 priv->battery_hook.name = dev_name(dev); 306 priv->battery_hook.add_battery = cros_chctl_add_battery; 307 priv->battery_hook.remove_battery = cros_chctl_remove_battery; 308 309 priv->current_behaviour = POWER_SUPPLY_CHARGE_BEHAVIOUR_AUTO; 310 priv->current_start_threshold = 0; 311 priv->current_end_threshold = 100; 312 313 /* Bring EC into well-known state */ 314 scoped_guard(mutex, &priv->lock) 315 ret = cros_chctl_configure_ec(priv); 316 if (ret < 0) 317 return ret; 318 319 return devm_battery_hook_register(dev, &priv->battery_hook); 320 } 321 322 static const struct platform_device_id cros_chctl_id[] = { 323 { "cros-charge-control", 0 }, 324 {} 325 }; 326 327 static struct platform_driver cros_chctl_driver = { 328 .driver.name = "cros-charge-control", 329 .probe = cros_chctl_probe, 330 .id_table = cros_chctl_id, 331 }; 332 module_platform_driver(cros_chctl_driver); 333 334 MODULE_DEVICE_TABLE(platform, cros_chctl_id); 335 MODULE_DESCRIPTION("ChromeOS EC charge control"); 336 MODULE_AUTHOR("Thomas Weißschuh <linux@weissschuh.net>"); 337 MODULE_LICENSE("GPL"); 338