1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Copyright (c) 2023, Nikita Travkin <nikita@trvn.ru> |
4 | */ |
5 | |
6 | #include <linux/errno.h> |
7 | #include <linux/module.h> |
8 | #include <linux/platform_device.h> |
9 | #include <linux/power_supply.h> |
10 | #include <linux/property.h> |
11 | #include <linux/regmap.h> |
12 | #include <linux/slab.h> |
13 | #include <linux/delay.h> |
14 | #include <linux/interrupt.h> |
15 | #include <linux/extcon-provider.h> |
16 | #include <linux/mod_devicetable.h> |
17 | |
18 | /* Two bytes: type + subtype */ |
19 | #define PM8916_PERPH_TYPE 0x04 |
20 | #define PM8916_LBC_CHGR_TYPE 0x1502 |
21 | #define PM8916_LBC_BAT_IF_TYPE 0x1602 |
22 | #define PM8916_LBC_USB_TYPE 0x1702 |
23 | #define PM8916_LBC_MISC_TYPE 0x1802 |
24 | |
25 | #define PM8916_LBC_CHGR_CHG_OPTION 0x08 |
26 | #define PM8916_LBC_CHGR_PMIC_CHARGER BIT(7) |
27 | |
28 | #define PM8916_LBC_CHGR_CHG_STATUS 0x09 |
29 | |
30 | #define PM8916_INT_RT_STS 0x10 |
31 | |
32 | #define PM8916_LBC_USB_USBIN_VALID BIT(1) |
33 | |
34 | #define PM8916_LBC_CHGR_VDD_MAX 0x40 |
35 | #define PM8916_LBC_CHGR_VDD_SAFE 0x41 |
36 | #define PM8916_LBC_CHGR_IBAT_MAX 0x44 |
37 | #define PM8916_LBC_CHGR_IBAT_SAFE 0x45 |
38 | |
39 | #define PM8916_LBC_CHGR_TCHG_MAX_EN 0x60 |
40 | #define PM8916_LBC_CHGR_TCHG_MAX_ENABLED BIT(7) |
41 | #define PM8916_LBC_CHGR_TCHG_MAX 0x61 |
42 | |
43 | #define PM8916_LBC_CHGR_CHG_CTRL 0x49 |
44 | #define PM8916_LBC_CHGR_CHG_EN BIT(7) |
45 | #define PM8916_LBC_CHGR_PSTG_EN BIT(5) |
46 | |
47 | #define PM8916_LBC_CHGR_MIN_CURRENT 90000 |
48 | #define PM8916_LBC_CHGR_MAX_CURRENT 1440000 |
49 | |
50 | #define PM8916_LBC_CHGR_MIN_VOLTAGE 4000000 |
51 | #define PM8916_LBC_CHGR_MAX_VOLTAGE 4775000 |
52 | #define PM8916_LBC_CHGR_VOLTAGE_STEP 25000 |
53 | |
54 | #define PM8916_LBC_CHGR_MIN_TIME 4 |
55 | #define PM8916_LBC_CHGR_MAX_TIME 256 |
56 | |
57 | struct pm8916_lbc_charger { |
58 | struct device *dev; |
59 | struct extcon_dev *edev; |
60 | struct power_supply *charger; |
61 | struct power_supply_battery_info *info; |
62 | struct regmap *regmap; |
63 | unsigned int reg[4]; |
64 | bool online; |
65 | unsigned int charge_voltage_max; |
66 | unsigned int charge_voltage_safe; |
67 | unsigned int charge_current_max; |
68 | unsigned int charge_current_safe; |
69 | }; |
70 | |
71 | static const unsigned int pm8916_lbc_charger_cable[] = { |
72 | EXTCON_USB, |
73 | EXTCON_NONE, |
74 | }; |
75 | |
76 | enum { |
77 | LBC_CHGR = 0, |
78 | LBC_BAT_IF, |
79 | LBC_USB, |
80 | LBC_MISC, |
81 | }; |
82 | |
83 | static int pm8916_lbc_charger_configure(struct pm8916_lbc_charger *chg) |
84 | { |
85 | int ret = 0; |
86 | unsigned int tmp; |
87 | |
88 | chg->charge_voltage_max = clamp_t(u32, chg->charge_voltage_max, |
89 | PM8916_LBC_CHGR_MIN_VOLTAGE, chg->charge_voltage_safe); |
90 | |
91 | tmp = chg->charge_voltage_max - PM8916_LBC_CHGR_MIN_VOLTAGE; |
92 | tmp /= PM8916_LBC_CHGR_VOLTAGE_STEP; |
93 | chg->charge_voltage_max = PM8916_LBC_CHGR_MIN_VOLTAGE + tmp * PM8916_LBC_CHGR_VOLTAGE_STEP; |
94 | |
95 | ret = regmap_write(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_VDD_MAX, val: tmp); |
96 | if (ret) |
97 | goto error; |
98 | |
99 | chg->charge_current_max = min(chg->charge_current_max, chg->charge_current_safe); |
100 | |
101 | tmp = clamp_t(u32, chg->charge_current_max, |
102 | PM8916_LBC_CHGR_MIN_CURRENT, PM8916_LBC_CHGR_MAX_CURRENT); |
103 | |
104 | tmp = chg->charge_current_max / PM8916_LBC_CHGR_MIN_CURRENT - 1; |
105 | chg->charge_current_max = (tmp + 1) * PM8916_LBC_CHGR_MIN_CURRENT; |
106 | |
107 | ret = regmap_write(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_IBAT_MAX, val: tmp); |
108 | if (ret) |
109 | goto error; |
110 | |
111 | ret = regmap_write(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_CHG_CTRL, |
112 | PM8916_LBC_CHGR_CHG_EN | PM8916_LBC_CHGR_PSTG_EN); |
113 | if (ret) |
114 | goto error; |
115 | |
116 | return ret; |
117 | |
118 | error: |
119 | dev_err(chg->dev, "Failed to configure charging: %pe\n" , ERR_PTR(ret)); |
120 | return ret; |
121 | } |
122 | |
123 | static int pm8916_lbc_charger_get_property(struct power_supply *psy, |
124 | enum power_supply_property psp, |
125 | union power_supply_propval *val) |
126 | { |
127 | struct pm8916_lbc_charger *chg = power_supply_get_drvdata(psy); |
128 | |
129 | switch (psp) { |
130 | case POWER_SUPPLY_PROP_ONLINE: |
131 | val->intval = chg->online; |
132 | return 0; |
133 | |
134 | case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX: |
135 | val->intval = chg->charge_voltage_max; |
136 | return 0; |
137 | |
138 | case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: |
139 | val->intval = chg->charge_current_max; |
140 | return 0; |
141 | |
142 | default: |
143 | return -EINVAL; |
144 | }; |
145 | } |
146 | |
147 | static int pm8916_lbc_charger_set_property(struct power_supply *psy, |
148 | enum power_supply_property prop, |
149 | const union power_supply_propval *val) |
150 | { |
151 | struct pm8916_lbc_charger *chg = power_supply_get_drvdata(psy); |
152 | |
153 | switch (prop) { |
154 | case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: |
155 | chg->charge_current_max = val->intval; |
156 | return pm8916_lbc_charger_configure(chg); |
157 | default: |
158 | return -EINVAL; |
159 | } |
160 | } |
161 | |
162 | static int pm8916_lbc_charger_property_is_writeable(struct power_supply *psy, |
163 | enum power_supply_property psp) |
164 | { |
165 | switch (psp) { |
166 | case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT: |
167 | return true; |
168 | default: |
169 | return false; |
170 | } |
171 | } |
172 | |
173 | static enum power_supply_property pm8916_lbc_charger_properties[] = { |
174 | POWER_SUPPLY_PROP_ONLINE, |
175 | POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX, |
176 | POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT, |
177 | }; |
178 | |
179 | static irqreturn_t pm8916_lbc_charger_state_changed_irq(int irq, void *data) |
180 | { |
181 | struct pm8916_lbc_charger *chg = data; |
182 | unsigned int tmp; |
183 | int ret; |
184 | |
185 | ret = regmap_read(map: chg->regmap, reg: chg->reg[LBC_USB] + PM8916_INT_RT_STS, val: &tmp); |
186 | if (ret) |
187 | return IRQ_HANDLED; |
188 | |
189 | chg->online = !!(tmp & PM8916_LBC_USB_USBIN_VALID); |
190 | extcon_set_state_sync(edev: chg->edev, EXTCON_USB, state: chg->online); |
191 | |
192 | power_supply_changed(psy: chg->charger); |
193 | |
194 | return IRQ_HANDLED; |
195 | } |
196 | |
197 | static int pm8916_lbc_charger_probe_dt(struct pm8916_lbc_charger *chg) |
198 | { |
199 | struct device *dev = chg->dev; |
200 | int ret = 0; |
201 | unsigned int tmp; |
202 | |
203 | ret = device_property_read_u32(dev, propname: "qcom,fast-charge-safe-voltage" , val: &chg->charge_voltage_safe); |
204 | if (ret) |
205 | return ret; |
206 | if (chg->charge_voltage_safe < PM8916_LBC_CHGR_MIN_VOLTAGE) |
207 | return -EINVAL; |
208 | |
209 | chg->charge_voltage_safe = clamp_t(u32, chg->charge_voltage_safe, |
210 | PM8916_LBC_CHGR_MIN_VOLTAGE, PM8916_LBC_CHGR_MAX_VOLTAGE); |
211 | |
212 | tmp = chg->charge_voltage_safe - PM8916_LBC_CHGR_MIN_VOLTAGE; |
213 | tmp /= PM8916_LBC_CHGR_VOLTAGE_STEP; |
214 | ret = regmap_write(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_VDD_SAFE, val: tmp); |
215 | if (ret) |
216 | return ret; |
217 | |
218 | ret = device_property_read_u32(dev, propname: "qcom,fast-charge-safe-current" , val: &chg->charge_current_safe); |
219 | if (ret) |
220 | return ret; |
221 | if (chg->charge_current_safe < PM8916_LBC_CHGR_MIN_CURRENT) |
222 | return -EINVAL; |
223 | |
224 | chg->charge_current_safe = clamp_t(u32, chg->charge_current_safe, |
225 | PM8916_LBC_CHGR_MIN_CURRENT, PM8916_LBC_CHGR_MAX_CURRENT); |
226 | |
227 | chg->charge_current_max = chg->charge_current_safe; |
228 | |
229 | tmp = chg->charge_current_safe / PM8916_LBC_CHGR_MIN_CURRENT - 1; |
230 | ret = regmap_write(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_IBAT_SAFE, val: tmp); |
231 | if (ret) |
232 | return ret; |
233 | |
234 | /* Disable charger timeout. */ |
235 | ret = regmap_write(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_TCHG_MAX_EN, val: 0x00); |
236 | if (ret) |
237 | return ret; |
238 | |
239 | return ret; |
240 | } |
241 | |
242 | static const struct power_supply_desc pm8916_lbc_charger_psy_desc = { |
243 | .name = "pm8916-lbc-chgr" , |
244 | .type = POWER_SUPPLY_TYPE_USB, |
245 | .properties = pm8916_lbc_charger_properties, |
246 | .num_properties = ARRAY_SIZE(pm8916_lbc_charger_properties), |
247 | .get_property = pm8916_lbc_charger_get_property, |
248 | .set_property = pm8916_lbc_charger_set_property, |
249 | .property_is_writeable = pm8916_lbc_charger_property_is_writeable, |
250 | }; |
251 | |
252 | static int pm8916_lbc_charger_probe(struct platform_device *pdev) |
253 | { |
254 | struct device *dev = &pdev->dev; |
255 | struct pm8916_lbc_charger *chg; |
256 | struct power_supply_config psy_cfg = {}; |
257 | int ret, len, irq; |
258 | unsigned int tmp; |
259 | |
260 | chg = devm_kzalloc(dev, size: sizeof(*chg), GFP_KERNEL); |
261 | if (!chg) |
262 | return -ENOMEM; |
263 | |
264 | chg->dev = dev; |
265 | |
266 | chg->regmap = dev_get_regmap(dev: pdev->dev.parent, NULL); |
267 | if (!chg->regmap) |
268 | return -ENODEV; |
269 | |
270 | len = device_property_count_u32(dev, propname: "reg" ); |
271 | if (len < 0) |
272 | return len; |
273 | if (len != 4) |
274 | return dev_err_probe(dev, err: -EINVAL, |
275 | fmt: "Wrong amount of reg values: %d (4 expected)\n" , len); |
276 | |
277 | irq = platform_get_irq_byname(pdev, "usb_vbus" ); |
278 | if (irq < 0) |
279 | return irq; |
280 | |
281 | ret = devm_request_threaded_irq(dev, irq, NULL, thread_fn: pm8916_lbc_charger_state_changed_irq, |
282 | IRQF_ONESHOT, devname: "pm8916_lbc" , dev_id: chg); |
283 | if (ret) |
284 | return ret; |
285 | |
286 | ret = device_property_read_u32_array(dev, propname: "reg" , val: chg->reg, nval: len); |
287 | if (ret) |
288 | return ret; |
289 | |
290 | ret = regmap_bulk_read(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_PERPH_TYPE, val: &tmp, val_count: 2); |
291 | if (ret) |
292 | goto comm_error; |
293 | if (tmp != PM8916_LBC_CHGR_TYPE) |
294 | goto type_error; |
295 | |
296 | ret = regmap_bulk_read(map: chg->regmap, reg: chg->reg[LBC_BAT_IF] + PM8916_PERPH_TYPE, val: &tmp, val_count: 2); |
297 | if (ret) |
298 | goto comm_error; |
299 | if (tmp != PM8916_LBC_BAT_IF_TYPE) |
300 | goto type_error; |
301 | |
302 | ret = regmap_bulk_read(map: chg->regmap, reg: chg->reg[LBC_USB] + PM8916_PERPH_TYPE, val: &tmp, val_count: 2); |
303 | if (ret) |
304 | goto comm_error; |
305 | if (tmp != PM8916_LBC_USB_TYPE) |
306 | goto type_error; |
307 | |
308 | ret = regmap_bulk_read(map: chg->regmap, reg: chg->reg[LBC_MISC] + PM8916_PERPH_TYPE, val: &tmp, val_count: 2); |
309 | if (ret) |
310 | goto comm_error; |
311 | if (tmp != PM8916_LBC_MISC_TYPE) |
312 | goto type_error; |
313 | |
314 | ret = regmap_read(map: chg->regmap, reg: chg->reg[LBC_CHGR] + PM8916_LBC_CHGR_CHG_OPTION, val: &tmp); |
315 | if (ret) |
316 | goto comm_error; |
317 | if (tmp != PM8916_LBC_CHGR_PMIC_CHARGER) |
318 | dev_err_probe(dev, err: -ENODEV, fmt: "The system is using an external charger\n" ); |
319 | |
320 | ret = pm8916_lbc_charger_probe_dt(chg); |
321 | if (ret) |
322 | dev_err_probe(dev, err: ret, fmt: "Error while parsing device tree\n" ); |
323 | |
324 | psy_cfg.drv_data = chg; |
325 | psy_cfg.of_node = dev->of_node; |
326 | |
327 | chg->charger = devm_power_supply_register(parent: dev, desc: &pm8916_lbc_charger_psy_desc, cfg: &psy_cfg); |
328 | if (IS_ERR(ptr: chg->charger)) |
329 | return dev_err_probe(dev, err: PTR_ERR(ptr: chg->charger), fmt: "Unable to register charger\n" ); |
330 | |
331 | ret = power_supply_get_battery_info(psy: chg->charger, info_out: &chg->info); |
332 | if (ret) |
333 | return dev_err_probe(dev, err: ret, fmt: "Unable to get battery info\n" ); |
334 | |
335 | chg->edev = devm_extcon_dev_allocate(dev, cable: pm8916_lbc_charger_cable); |
336 | if (IS_ERR(ptr: chg->edev)) |
337 | return PTR_ERR(ptr: chg->edev); |
338 | |
339 | ret = devm_extcon_dev_register(dev, edev: chg->edev); |
340 | if (ret < 0) |
341 | return dev_err_probe(dev, err: ret, fmt: "failed to register extcon device\n" ); |
342 | |
343 | ret = regmap_read(map: chg->regmap, reg: chg->reg[LBC_USB] + PM8916_INT_RT_STS, val: &tmp); |
344 | if (ret) |
345 | goto comm_error; |
346 | |
347 | chg->online = !!(tmp & PM8916_LBC_USB_USBIN_VALID); |
348 | extcon_set_state_sync(edev: chg->edev, EXTCON_USB, state: chg->online); |
349 | |
350 | chg->charge_voltage_max = chg->info->voltage_max_design_uv; |
351 | ret = pm8916_lbc_charger_configure(chg); |
352 | if (ret) |
353 | return ret; |
354 | |
355 | return 0; |
356 | |
357 | comm_error: |
358 | return dev_err_probe(dev, err: ret, fmt: "Unable to communicate with device\n" ); |
359 | |
360 | type_error: |
361 | return dev_err_probe(dev, err: -ENODEV, fmt: "Device reported wrong type: 0x%X\n" , tmp); |
362 | } |
363 | |
364 | static const struct of_device_id pm8916_lbc_charger_of_match[] = { |
365 | { .compatible = "qcom,pm8916-lbc" , }, |
366 | {} |
367 | }; |
368 | MODULE_DEVICE_TABLE(of, pm8916_lbc_charger_of_match); |
369 | |
370 | static struct platform_driver pm8916_lbc_charger_driver = { |
371 | .driver = { |
372 | .name = "pm8916-lbc" , |
373 | .of_match_table = pm8916_lbc_charger_of_match, |
374 | }, |
375 | .probe = pm8916_lbc_charger_probe, |
376 | }; |
377 | module_platform_driver(pm8916_lbc_charger_driver); |
378 | |
379 | MODULE_DESCRIPTION("pm8916 LBC driver" ); |
380 | MODULE_AUTHOR("Nikita Travkin <nikita@trvn.ru>" ); |
381 | MODULE_LICENSE("GPL" ); |
382 | |