1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* |
3 | * max77976_charger.c - Driver for the Maxim MAX77976 battery charger |
4 | * |
5 | * Copyright (C) 2021 Luca Ceresoli |
6 | * Author: Luca Ceresoli <luca.ceresoli@bootlin.com> |
7 | */ |
8 | |
9 | #include <linux/i2c.h> |
10 | #include <linux/module.h> |
11 | #include <linux/power_supply.h> |
12 | #include <linux/regmap.h> |
13 | |
14 | #define MAX77976_DRIVER_NAME "max77976-charger" |
15 | #define MAX77976_CHIP_ID 0x76 |
16 | |
17 | static const char *max77976_manufacturer = "Maxim Integrated" ; |
18 | static const char *max77976_model = "MAX77976" ; |
19 | |
20 | /* -------------------------------------------------------------------------- |
21 | * Register map |
22 | */ |
23 | |
24 | #define MAX77976_REG_CHIP_ID 0x00 |
25 | #define MAX77976_REG_CHIP_REVISION 0x01 |
26 | #define MAX77976_REG_CHG_INT_OK 0x12 |
27 | #define MAX77976_REG_CHG_DETAILS_01 0x14 |
28 | #define MAX77976_REG_CHG_CNFG_00 0x16 |
29 | #define MAX77976_REG_CHG_CNFG_02 0x18 |
30 | #define MAX77976_REG_CHG_CNFG_06 0x1c |
31 | #define MAX77976_REG_CHG_CNFG_09 0x1f |
32 | |
33 | /* CHG_DETAILS_01.CHG_DTLS values */ |
34 | enum max77976_charging_state { |
35 | MAX77976_CHARGING_PREQUALIFICATION = 0x0, |
36 | MAX77976_CHARGING_FAST_CONST_CURRENT, |
37 | MAX77976_CHARGING_FAST_CONST_VOLTAGE, |
38 | MAX77976_CHARGING_TOP_OFF, |
39 | MAX77976_CHARGING_DONE, |
40 | MAX77976_CHARGING_RESERVED_05, |
41 | MAX77976_CHARGING_TIMER_FAULT, |
42 | MAX77976_CHARGING_SUSPENDED_QBATT_OFF, |
43 | MAX77976_CHARGING_OFF, |
44 | MAX77976_CHARGING_RESERVED_09, |
45 | MAX77976_CHARGING_THERMAL_SHUTDOWN, |
46 | MAX77976_CHARGING_WATCHDOG_EXPIRED, |
47 | MAX77976_CHARGING_SUSPENDED_JEITA, |
48 | MAX77976_CHARGING_SUSPENDED_THM_REMOVAL, |
49 | MAX77976_CHARGING_SUSPENDED_PIN, |
50 | MAX77976_CHARGING_RESERVED_0F, |
51 | }; |
52 | |
53 | /* CHG_DETAILS_01.BAT_DTLS values */ |
54 | enum max77976_battery_state { |
55 | MAX77976_BATTERY_BATTERY_REMOVAL = 0x0, |
56 | MAX77976_BATTERY_PREQUALIFICATION, |
57 | MAX77976_BATTERY_TIMER_FAULT, |
58 | MAX77976_BATTERY_REGULAR_VOLTAGE, |
59 | MAX77976_BATTERY_LOW_VOLTAGE, |
60 | MAX77976_BATTERY_OVERVOLTAGE, |
61 | MAX77976_BATTERY_RESERVED, |
62 | MAX77976_BATTERY_BATTERY_ONLY, // No valid adapter is present |
63 | }; |
64 | |
65 | /* CHG_CNFG_00.MODE values */ |
66 | enum max77976_mode { |
67 | MAX77976_MODE_CHARGER_BUCK = 0x5, |
68 | MAX77976_MODE_BOOST = 0x9, |
69 | }; |
70 | |
71 | /* CHG_CNFG_02.CHG_CC: charge current limit, 100..5500 mA, 50 mA steps */ |
72 | #define MAX77976_CHG_CC_STEP 50000U |
73 | #define MAX77976_CHG_CC_MIN 100000U |
74 | #define MAX77976_CHG_CC_MAX 5500000U |
75 | |
76 | /* CHG_CNFG_09.CHGIN_ILIM: input current limit, 100..3200 mA, 100 mA steps */ |
77 | #define MAX77976_CHGIN_ILIM_STEP 100000U |
78 | #define MAX77976_CHGIN_ILIM_MIN 100000U |
79 | #define MAX77976_CHGIN_ILIM_MAX 3200000U |
80 | |
81 | enum max77976_field_idx { |
82 | VERSION, REVISION, /* CHIP_REVISION */ |
83 | CHGIN_OK, /* CHG_INT_OK */ |
84 | BAT_DTLS, CHG_DTLS, /* CHG_DETAILS_01 */ |
85 | MODE, /* CHG_CNFG_00 */ |
86 | CHG_CC, /* CHG_CNFG_02 */ |
87 | CHGPROT, /* CHG_CNFG_06 */ |
88 | CHGIN_ILIM, /* CHG_CNFG_09 */ |
89 | MAX77976_N_REGMAP_FIELDS |
90 | }; |
91 | |
92 | static const struct reg_field max77976_reg_field[MAX77976_N_REGMAP_FIELDS] = { |
93 | [VERSION] = REG_FIELD(MAX77976_REG_CHIP_REVISION, 4, 7), |
94 | [REVISION] = REG_FIELD(MAX77976_REG_CHIP_REVISION, 0, 3), |
95 | [CHGIN_OK] = REG_FIELD(MAX77976_REG_CHG_INT_OK, 6, 6), |
96 | [CHG_DTLS] = REG_FIELD(MAX77976_REG_CHG_DETAILS_01, 0, 3), |
97 | [BAT_DTLS] = REG_FIELD(MAX77976_REG_CHG_DETAILS_01, 4, 6), |
98 | [MODE] = REG_FIELD(MAX77976_REG_CHG_CNFG_00, 0, 3), |
99 | [CHG_CC] = REG_FIELD(MAX77976_REG_CHG_CNFG_02, 0, 6), |
100 | [CHGPROT] = REG_FIELD(MAX77976_REG_CHG_CNFG_06, 2, 3), |
101 | [CHGIN_ILIM] = REG_FIELD(MAX77976_REG_CHG_CNFG_09, 0, 5), |
102 | }; |
103 | |
104 | static const struct regmap_config max77976_regmap_config = { |
105 | .reg_bits = 8, |
106 | .val_bits = 8, |
107 | .max_register = 0x24, |
108 | }; |
109 | |
110 | /* -------------------------------------------------------------------------- |
111 | * Data structures |
112 | */ |
113 | |
114 | struct max77976 { |
115 | struct i2c_client *client; |
116 | struct regmap *regmap; |
117 | struct regmap_field *rfield[MAX77976_N_REGMAP_FIELDS]; |
118 | }; |
119 | |
120 | /* -------------------------------------------------------------------------- |
121 | * power_supply properties |
122 | */ |
123 | |
124 | static int max77976_get_status(struct max77976 *chg, int *val) |
125 | { |
126 | unsigned int regval; |
127 | int err; |
128 | |
129 | err = regmap_field_read(field: chg->rfield[CHG_DTLS], val: ®val); |
130 | if (err < 0) |
131 | return err; |
132 | |
133 | switch (regval) { |
134 | case MAX77976_CHARGING_PREQUALIFICATION: |
135 | case MAX77976_CHARGING_FAST_CONST_CURRENT: |
136 | case MAX77976_CHARGING_FAST_CONST_VOLTAGE: |
137 | case MAX77976_CHARGING_TOP_OFF: |
138 | *val = POWER_SUPPLY_STATUS_CHARGING; |
139 | break; |
140 | case MAX77976_CHARGING_DONE: |
141 | *val = POWER_SUPPLY_STATUS_FULL; |
142 | break; |
143 | case MAX77976_CHARGING_TIMER_FAULT: |
144 | case MAX77976_CHARGING_SUSPENDED_QBATT_OFF: |
145 | case MAX77976_CHARGING_SUSPENDED_JEITA: |
146 | case MAX77976_CHARGING_SUSPENDED_THM_REMOVAL: |
147 | case MAX77976_CHARGING_SUSPENDED_PIN: |
148 | *val = POWER_SUPPLY_STATUS_NOT_CHARGING; |
149 | break; |
150 | case MAX77976_CHARGING_OFF: |
151 | case MAX77976_CHARGING_THERMAL_SHUTDOWN: |
152 | case MAX77976_CHARGING_WATCHDOG_EXPIRED: |
153 | *val = POWER_SUPPLY_STATUS_DISCHARGING; |
154 | break; |
155 | default: |
156 | *val = POWER_SUPPLY_STATUS_UNKNOWN; |
157 | } |
158 | |
159 | return 0; |
160 | } |
161 | |
162 | static int max77976_get_charge_type(struct max77976 *chg, int *val) |
163 | { |
164 | unsigned int regval; |
165 | int err; |
166 | |
167 | err = regmap_field_read(field: chg->rfield[CHG_DTLS], val: ®val); |
168 | if (err < 0) |
169 | return err; |
170 | |
171 | switch (regval) { |
172 | case MAX77976_CHARGING_PREQUALIFICATION: |
173 | *val = POWER_SUPPLY_CHARGE_TYPE_TRICKLE; |
174 | break; |
175 | case MAX77976_CHARGING_FAST_CONST_CURRENT: |
176 | case MAX77976_CHARGING_FAST_CONST_VOLTAGE: |
177 | *val = POWER_SUPPLY_CHARGE_TYPE_FAST; |
178 | break; |
179 | case MAX77976_CHARGING_TOP_OFF: |
180 | *val = POWER_SUPPLY_CHARGE_TYPE_STANDARD; |
181 | break; |
182 | case MAX77976_CHARGING_DONE: |
183 | case MAX77976_CHARGING_TIMER_FAULT: |
184 | case MAX77976_CHARGING_SUSPENDED_QBATT_OFF: |
185 | case MAX77976_CHARGING_OFF: |
186 | case MAX77976_CHARGING_THERMAL_SHUTDOWN: |
187 | case MAX77976_CHARGING_WATCHDOG_EXPIRED: |
188 | case MAX77976_CHARGING_SUSPENDED_JEITA: |
189 | case MAX77976_CHARGING_SUSPENDED_THM_REMOVAL: |
190 | case MAX77976_CHARGING_SUSPENDED_PIN: |
191 | *val = POWER_SUPPLY_CHARGE_TYPE_NONE; |
192 | break; |
193 | default: |
194 | *val = POWER_SUPPLY_CHARGE_TYPE_UNKNOWN; |
195 | } |
196 | |
197 | return 0; |
198 | } |
199 | |
200 | static int max77976_get_health(struct max77976 *chg, int *val) |
201 | { |
202 | unsigned int regval; |
203 | int err; |
204 | |
205 | err = regmap_field_read(field: chg->rfield[BAT_DTLS], val: ®val); |
206 | if (err < 0) |
207 | return err; |
208 | |
209 | switch (regval) { |
210 | case MAX77976_BATTERY_BATTERY_REMOVAL: |
211 | *val = POWER_SUPPLY_HEALTH_NO_BATTERY; |
212 | break; |
213 | case MAX77976_BATTERY_LOW_VOLTAGE: |
214 | case MAX77976_BATTERY_REGULAR_VOLTAGE: |
215 | *val = POWER_SUPPLY_HEALTH_GOOD; |
216 | break; |
217 | case MAX77976_BATTERY_TIMER_FAULT: |
218 | *val = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE; |
219 | break; |
220 | case MAX77976_BATTERY_OVERVOLTAGE: |
221 | *val = POWER_SUPPLY_HEALTH_OVERVOLTAGE; |
222 | break; |
223 | case MAX77976_BATTERY_PREQUALIFICATION: |
224 | case MAX77976_BATTERY_BATTERY_ONLY: |
225 | *val = POWER_SUPPLY_HEALTH_UNKNOWN; |
226 | break; |
227 | default: |
228 | *val = POWER_SUPPLY_HEALTH_UNKNOWN; |
229 | } |
230 | |
231 | return 0; |
232 | } |
233 | |
234 | static int max77976_get_online(struct max77976 *chg, int *val) |
235 | { |
236 | unsigned int regval; |
237 | int err; |
238 | |
239 | err = regmap_field_read(field: chg->rfield[CHGIN_OK], val: ®val); |
240 | if (err < 0) |
241 | return err; |
242 | |
243 | *val = (regval ? 1 : 0); |
244 | |
245 | return 0; |
246 | } |
247 | |
248 | static int max77976_get_integer(struct max77976 *chg, enum max77976_field_idx fidx, |
249 | unsigned int clamp_min, unsigned int clamp_max, |
250 | unsigned int mult, int *val) |
251 | { |
252 | unsigned int regval; |
253 | int err; |
254 | |
255 | err = regmap_field_read(field: chg->rfield[fidx], val: ®val); |
256 | if (err < 0) |
257 | return err; |
258 | |
259 | *val = clamp_val(regval * mult, clamp_min, clamp_max); |
260 | |
261 | return 0; |
262 | } |
263 | |
264 | static int max77976_set_integer(struct max77976 *chg, enum max77976_field_idx fidx, |
265 | unsigned int clamp_min, unsigned int clamp_max, |
266 | unsigned int div, int val) |
267 | { |
268 | unsigned int regval; |
269 | |
270 | regval = clamp_val(val, clamp_min, clamp_max) / div; |
271 | |
272 | return regmap_field_write(field: chg->rfield[fidx], val: regval); |
273 | } |
274 | |
275 | static int max77976_get_property(struct power_supply *psy, |
276 | enum power_supply_property psp, |
277 | union power_supply_propval *val) |
278 | { |
279 | struct max77976 *chg = power_supply_get_drvdata(psy); |
280 | int err = 0; |
281 | |
282 | switch (psp) { |
283 | case POWER_SUPPLY_PROP_STATUS: |
284 | err = max77976_get_status(chg, val: &val->intval); |
285 | break; |
286 | case POWER_SUPPLY_PROP_CHARGE_TYPE: |
287 | err = max77976_get_charge_type(chg, val: &val->intval); |
288 | break; |
289 | case POWER_SUPPLY_PROP_HEALTH: |
290 | err = max77976_get_health(chg, val: &val->intval); |
291 | break; |
292 | case POWER_SUPPLY_PROP_ONLINE: |
293 | err = max77976_get_online(chg, val: &val->intval); |
294 | break; |
295 | case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX: |
296 | val->intval = MAX77976_CHG_CC_MAX; |
297 | break; |
298 | case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: |
299 | err = max77976_get_integer(chg, fidx: CHG_CC, |
300 | MAX77976_CHG_CC_MIN, |
301 | MAX77976_CHG_CC_MAX, |
302 | MAX77976_CHG_CC_STEP, |
303 | val: &val->intval); |
304 | break; |
305 | case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: |
306 | err = max77976_get_integer(chg, fidx: CHGIN_ILIM, |
307 | MAX77976_CHGIN_ILIM_MIN, |
308 | MAX77976_CHGIN_ILIM_MAX, |
309 | MAX77976_CHGIN_ILIM_STEP, |
310 | val: &val->intval); |
311 | break; |
312 | case POWER_SUPPLY_PROP_MODEL_NAME: |
313 | val->strval = max77976_model; |
314 | break; |
315 | case POWER_SUPPLY_PROP_MANUFACTURER: |
316 | val->strval = max77976_manufacturer; |
317 | break; |
318 | default: |
319 | err = -EINVAL; |
320 | } |
321 | |
322 | return err; |
323 | } |
324 | |
325 | static int max77976_set_property(struct power_supply *psy, |
326 | enum power_supply_property psp, |
327 | const union power_supply_propval *val) |
328 | { |
329 | struct max77976 *chg = power_supply_get_drvdata(psy); |
330 | int err = 0; |
331 | |
332 | switch (psp) { |
333 | case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: |
334 | err = max77976_set_integer(chg, fidx: CHG_CC, |
335 | MAX77976_CHG_CC_MIN, |
336 | MAX77976_CHG_CC_MAX, |
337 | MAX77976_CHG_CC_STEP, |
338 | val: val->intval); |
339 | break; |
340 | case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: |
341 | err = max77976_set_integer(chg, fidx: CHGIN_ILIM, |
342 | MAX77976_CHGIN_ILIM_MIN, |
343 | MAX77976_CHGIN_ILIM_MAX, |
344 | MAX77976_CHGIN_ILIM_STEP, |
345 | val: val->intval); |
346 | break; |
347 | default: |
348 | err = -EINVAL; |
349 | } |
350 | |
351 | return err; |
352 | }; |
353 | |
354 | static int max77976_property_is_writeable(struct power_supply *psy, |
355 | enum power_supply_property psp) |
356 | { |
357 | switch (psp) { |
358 | case POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT: |
359 | case POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT: |
360 | return true; |
361 | default: |
362 | return false; |
363 | } |
364 | } |
365 | |
366 | static enum power_supply_property max77976_psy_props[] = { |
367 | POWER_SUPPLY_PROP_STATUS, |
368 | POWER_SUPPLY_PROP_CHARGE_TYPE, |
369 | POWER_SUPPLY_PROP_HEALTH, |
370 | POWER_SUPPLY_PROP_ONLINE, |
371 | POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT, |
372 | POWER_SUPPLY_PROP_CHARGE_CONTROL_LIMIT_MAX, |
373 | POWER_SUPPLY_PROP_INPUT_CURRENT_LIMIT, |
374 | POWER_SUPPLY_PROP_MODEL_NAME, |
375 | POWER_SUPPLY_PROP_MANUFACTURER, |
376 | }; |
377 | |
378 | static const struct power_supply_desc max77976_psy_desc = { |
379 | .name = MAX77976_DRIVER_NAME, |
380 | .type = POWER_SUPPLY_TYPE_USB, |
381 | .properties = max77976_psy_props, |
382 | .num_properties = ARRAY_SIZE(max77976_psy_props), |
383 | .get_property = max77976_get_property, |
384 | .set_property = max77976_set_property, |
385 | .property_is_writeable = max77976_property_is_writeable, |
386 | }; |
387 | |
388 | /* -------------------------------------------------------------------------- |
389 | * Entry point |
390 | */ |
391 | |
392 | static int max77976_detect(struct max77976 *chg) |
393 | { |
394 | struct device *dev = &chg->client->dev; |
395 | unsigned int id, ver, rev; |
396 | int err; |
397 | |
398 | err = regmap_read(map: chg->regmap, MAX77976_REG_CHIP_ID, val: &id); |
399 | if (err) |
400 | return dev_err_probe(dev, err, fmt: "cannot read chip ID\n" ); |
401 | |
402 | if (id != MAX77976_CHIP_ID) |
403 | return dev_err_probe(dev, err: -ENXIO, fmt: "unknown model ID 0x%02x\n" , id); |
404 | |
405 | err = regmap_field_read(field: chg->rfield[VERSION], val: &ver); |
406 | if (!err) |
407 | err = regmap_field_read(field: chg->rfield[REVISION], val: &rev); |
408 | if (err) |
409 | return dev_err_probe(dev, err: -ENXIO, fmt: "cannot read version/revision\n" ); |
410 | |
411 | dev_info(dev, "detected model MAX779%02x ver %u rev %u" , id, ver, rev); |
412 | |
413 | return 0; |
414 | } |
415 | |
416 | static int max77976_configure(struct max77976 *chg) |
417 | { |
418 | struct device *dev = &chg->client->dev; |
419 | int err; |
420 | |
421 | /* Magic value to unlock writing to some registers */ |
422 | err = regmap_field_write(field: chg->rfield[CHGPROT], val: 0x3); |
423 | if (err) |
424 | goto err; |
425 | |
426 | /* |
427 | * Mode 5 = Charger ON, OTG OFF, buck ON, boost OFF. |
428 | * Other modes are not implemented by this driver. |
429 | */ |
430 | err = regmap_field_write(field: chg->rfield[MODE], val: MAX77976_MODE_CHARGER_BUCK); |
431 | if (err) |
432 | goto err; |
433 | |
434 | return 0; |
435 | |
436 | err: |
437 | return dev_err_probe(dev, err, fmt: "error while configuring" ); |
438 | } |
439 | |
440 | static int max77976_probe(struct i2c_client *client) |
441 | { |
442 | struct device *dev = &client->dev; |
443 | struct power_supply_config psy_cfg = {}; |
444 | struct power_supply *psy; |
445 | struct max77976 *chg; |
446 | int err; |
447 | int i; |
448 | |
449 | chg = devm_kzalloc(dev, size: sizeof(*chg), GFP_KERNEL); |
450 | if (!chg) |
451 | return -ENOMEM; |
452 | |
453 | i2c_set_clientdata(client, data: chg); |
454 | psy_cfg.drv_data = chg; |
455 | chg->client = client; |
456 | |
457 | chg->regmap = devm_regmap_init_i2c(client, &max77976_regmap_config); |
458 | if (IS_ERR(ptr: chg->regmap)) |
459 | return dev_err_probe(dev, err: PTR_ERR(ptr: chg->regmap), |
460 | fmt: "cannot allocate regmap\n" ); |
461 | |
462 | for (i = 0; i < MAX77976_N_REGMAP_FIELDS; i++) { |
463 | chg->rfield[i] = devm_regmap_field_alloc(dev, regmap: chg->regmap, |
464 | reg_field: max77976_reg_field[i]); |
465 | if (IS_ERR(ptr: chg->rfield[i])) |
466 | return dev_err_probe(dev, err: PTR_ERR(ptr: chg->rfield[i]), |
467 | fmt: "cannot allocate regmap field\n" ); |
468 | } |
469 | |
470 | err = max77976_detect(chg); |
471 | if (err) |
472 | return err; |
473 | |
474 | err = max77976_configure(chg); |
475 | if (err) |
476 | return err; |
477 | |
478 | psy = devm_power_supply_register_no_ws(parent: dev, desc: &max77976_psy_desc, cfg: &psy_cfg); |
479 | if (IS_ERR(ptr: psy)) |
480 | return dev_err_probe(dev, err: PTR_ERR(ptr: psy), fmt: "cannot register\n" ); |
481 | |
482 | return 0; |
483 | } |
484 | |
485 | static const struct i2c_device_id max77976_i2c_id[] = { |
486 | { MAX77976_DRIVER_NAME, 0 }, |
487 | { }, |
488 | }; |
489 | MODULE_DEVICE_TABLE(i2c, max77976_i2c_id); |
490 | |
491 | static const struct of_device_id max77976_of_id[] = { |
492 | { .compatible = "maxim,max77976" }, |
493 | { }, |
494 | }; |
495 | MODULE_DEVICE_TABLE(of, max77976_of_id); |
496 | |
497 | static struct i2c_driver max77976_driver = { |
498 | .driver = { |
499 | .name = MAX77976_DRIVER_NAME, |
500 | .of_match_table = max77976_of_id, |
501 | }, |
502 | .probe = max77976_probe, |
503 | .id_table = max77976_i2c_id, |
504 | }; |
505 | module_i2c_driver(max77976_driver); |
506 | |
507 | MODULE_AUTHOR("Luca Ceresoli <luca.ceresoli@bootlin.com>" ); |
508 | MODULE_DESCRIPTION("Maxim MAX77976 charger driver" ); |
509 | MODULE_LICENSE("GPL v2" ); |
510 | |