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