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/timekeeping.h> |
16 | #include <linux/mod_devicetable.h> |
17 | |
18 | #define PM8916_PERPH_TYPE 0x04 |
19 | #define PM8916_BMS_VM_TYPE 0x020D |
20 | |
21 | #define PM8916_SEC_ACCESS 0xD0 |
22 | #define PM8916_SEC_MAGIC 0xA5 |
23 | |
24 | #define PM8916_BMS_VM_STATUS1 0x08 |
25 | #define PM8916_BMS_VM_FSM_STATE(x) (((x) & 0b00111000) >> 3) |
26 | #define PM8916_BMS_VM_FSM_STATE_S2 0x2 |
27 | |
28 | #define PM8916_BMS_VM_MODE_CTL 0x40 |
29 | #define PM8916_BMS_VM_MODE_FORCE_S3 (BIT(0) | BIT(1)) |
30 | #define PM8916_BMS_VM_MODE_NORMAL (BIT(1) | BIT(3)) |
31 | |
32 | #define PM8916_BMS_VM_EN_CTL 0x46 |
33 | #define PM8916_BMS_ENABLED BIT(7) |
34 | |
35 | #define PM8916_BMS_VM_FIFO_LENGTH_CTL 0x47 |
36 | #define PM8916_BMS_VM_S1_SAMPLE_INTERVAL_CTL 0x55 |
37 | #define PM8916_BMS_VM_S2_SAMPLE_INTERVAL_CTL 0x56 |
38 | #define PM8916_BMS_VM_S3_S7_OCV_DATA0 0x6A |
39 | #define PM8916_BMS_VM_BMS_FIFO_REG_0_LSB 0xC0 |
40 | |
41 | /* Using only 1 fifo is broken in hardware */ |
42 | #define PM8916_BMS_VM_FIFO_COUNT 2 /* 2 .. 8 */ |
43 | |
44 | #define PM8916_BMS_VM_S1_SAMPLE_INTERVAL 10 |
45 | #define PM8916_BMS_VM_S2_SAMPLE_INTERVAL 10 |
46 | |
47 | struct pm8916_bms_vm_battery { |
48 | struct device *dev; |
49 | struct power_supply *battery; |
50 | struct power_supply_battery_info *info; |
51 | struct regmap *regmap; |
52 | unsigned int reg; |
53 | unsigned int last_ocv; |
54 | time64_t last_ocv_time; |
55 | unsigned int vbat_now; |
56 | }; |
57 | |
58 | static int pm8916_bms_vm_battery_get_property(struct power_supply *psy, |
59 | enum power_supply_property psp, |
60 | union power_supply_propval *val) |
61 | { |
62 | struct pm8916_bms_vm_battery *bat = power_supply_get_drvdata(psy); |
63 | struct power_supply_battery_info *info = bat->info; |
64 | int supplied; |
65 | |
66 | switch (psp) { |
67 | case POWER_SUPPLY_PROP_STATUS: |
68 | supplied = power_supply_am_i_supplied(psy); |
69 | |
70 | if (supplied < 0 && supplied != -ENODEV) |
71 | return supplied; |
72 | else if (supplied && supplied != -ENODEV) |
73 | val->intval = POWER_SUPPLY_STATUS_CHARGING; |
74 | else |
75 | val->intval = POWER_SUPPLY_STATUS_DISCHARGING; |
76 | return 0; |
77 | |
78 | case POWER_SUPPLY_PROP_HEALTH: |
79 | if (bat->vbat_now < info->voltage_min_design_uv) |
80 | val->intval = POWER_SUPPLY_HEALTH_DEAD; |
81 | else if (bat->vbat_now > info->voltage_max_design_uv) |
82 | val->intval = POWER_SUPPLY_HEALTH_OVERVOLTAGE; |
83 | else |
84 | val->intval = POWER_SUPPLY_HEALTH_GOOD; |
85 | return 0; |
86 | |
87 | case POWER_SUPPLY_PROP_VOLTAGE_NOW: |
88 | val->intval = bat->vbat_now; |
89 | return 0; |
90 | |
91 | case POWER_SUPPLY_PROP_VOLTAGE_OCV: |
92 | /* |
93 | * Hardware only reliably measures OCV when the system is off or suspended. |
94 | * We expose the last known OCV value on boot, invalidating it after 180 seconds. |
95 | */ |
96 | if (ktime_get_seconds() - bat->last_ocv_time > 180) |
97 | return -ENODATA; |
98 | |
99 | val->intval = bat->last_ocv; |
100 | return 0; |
101 | |
102 | default: |
103 | return -EINVAL; |
104 | } |
105 | } |
106 | |
107 | static enum power_supply_property pm8916_bms_vm_battery_properties[] = { |
108 | POWER_SUPPLY_PROP_STATUS, |
109 | POWER_SUPPLY_PROP_VOLTAGE_NOW, |
110 | POWER_SUPPLY_PROP_VOLTAGE_OCV, |
111 | POWER_SUPPLY_PROP_HEALTH, |
112 | }; |
113 | |
114 | static irqreturn_t pm8916_bms_vm_fifo_update_done_irq(int irq, void *data) |
115 | { |
116 | struct pm8916_bms_vm_battery *bat = data; |
117 | u16 vbat_data[PM8916_BMS_VM_FIFO_COUNT]; |
118 | int ret; |
119 | |
120 | ret = regmap_bulk_read(map: bat->regmap, reg: bat->reg + PM8916_BMS_VM_BMS_FIFO_REG_0_LSB, |
121 | val: &vbat_data, PM8916_BMS_VM_FIFO_COUNT * 2); |
122 | if (ret) |
123 | return IRQ_HANDLED; |
124 | |
125 | /* |
126 | * The VM-BMS hardware only collects voltage data and the software |
127 | * has to process it to calculate the OCV and SoC. Hardware provides |
128 | * up to 8 averaged measurements for software to take in account. |
129 | * |
130 | * Just use the last measured value for now to report the current |
131 | * battery voltage. |
132 | */ |
133 | bat->vbat_now = vbat_data[PM8916_BMS_VM_FIFO_COUNT - 1] * 300; |
134 | |
135 | power_supply_changed(psy: bat->battery); |
136 | |
137 | return IRQ_HANDLED; |
138 | } |
139 | |
140 | static const struct power_supply_desc pm8916_bms_vm_battery_psy_desc = { |
141 | .name = "pm8916-bms-vm" , |
142 | .type = POWER_SUPPLY_TYPE_BATTERY, |
143 | .properties = pm8916_bms_vm_battery_properties, |
144 | .num_properties = ARRAY_SIZE(pm8916_bms_vm_battery_properties), |
145 | .get_property = pm8916_bms_vm_battery_get_property, |
146 | }; |
147 | |
148 | static int pm8916_bms_vm_battery_probe(struct platform_device *pdev) |
149 | { |
150 | struct device *dev = &pdev->dev; |
151 | struct pm8916_bms_vm_battery *bat; |
152 | struct power_supply_config psy_cfg = {}; |
153 | int ret, irq; |
154 | unsigned int tmp; |
155 | |
156 | bat = devm_kzalloc(dev, size: sizeof(*bat), GFP_KERNEL); |
157 | if (!bat) |
158 | return -ENOMEM; |
159 | |
160 | bat->dev = dev; |
161 | |
162 | bat->regmap = dev_get_regmap(dev: pdev->dev.parent, NULL); |
163 | if (!bat->regmap) |
164 | return -ENODEV; |
165 | |
166 | ret = device_property_read_u32(dev, propname: "reg" , val: &bat->reg); |
167 | if (ret < 0) |
168 | return -EINVAL; |
169 | |
170 | irq = platform_get_irq_byname(pdev, "fifo" ); |
171 | if (irq < 0) |
172 | return irq; |
173 | |
174 | ret = devm_request_threaded_irq(dev, irq, NULL, thread_fn: pm8916_bms_vm_fifo_update_done_irq, |
175 | IRQF_ONESHOT, devname: "pm8916_vm_bms" , dev_id: bat); |
176 | if (ret) |
177 | return ret; |
178 | |
179 | ret = regmap_bulk_read(map: bat->regmap, reg: bat->reg + PM8916_PERPH_TYPE, val: &tmp, val_count: 2); |
180 | if (ret) |
181 | goto comm_error; |
182 | |
183 | if (tmp != PM8916_BMS_VM_TYPE) |
184 | return dev_err_probe(dev, err: -ENODEV, fmt: "Device reported wrong type: 0x%X\n" , tmp); |
185 | |
186 | ret = regmap_write(map: bat->regmap, reg: bat->reg + PM8916_BMS_VM_S1_SAMPLE_INTERVAL_CTL, |
187 | PM8916_BMS_VM_S1_SAMPLE_INTERVAL); |
188 | if (ret) |
189 | goto comm_error; |
190 | ret = regmap_write(map: bat->regmap, reg: bat->reg + PM8916_BMS_VM_S2_SAMPLE_INTERVAL_CTL, |
191 | PM8916_BMS_VM_S2_SAMPLE_INTERVAL); |
192 | if (ret) |
193 | goto comm_error; |
194 | ret = regmap_write(map: bat->regmap, reg: bat->reg + PM8916_BMS_VM_FIFO_LENGTH_CTL, |
195 | PM8916_BMS_VM_FIFO_COUNT << 4 | PM8916_BMS_VM_FIFO_COUNT); |
196 | if (ret) |
197 | goto comm_error; |
198 | ret = regmap_write(map: bat->regmap, |
199 | reg: bat->reg + PM8916_BMS_VM_EN_CTL, PM8916_BMS_ENABLED); |
200 | if (ret) |
201 | goto comm_error; |
202 | |
203 | ret = regmap_bulk_read(map: bat->regmap, |
204 | reg: bat->reg + PM8916_BMS_VM_S3_S7_OCV_DATA0, val: &tmp, val_count: 2); |
205 | if (ret) |
206 | goto comm_error; |
207 | |
208 | bat->last_ocv_time = ktime_get_seconds(); |
209 | bat->last_ocv = tmp * 300; |
210 | bat->vbat_now = bat->last_ocv; |
211 | |
212 | psy_cfg.drv_data = bat; |
213 | psy_cfg.of_node = dev->of_node; |
214 | |
215 | bat->battery = devm_power_supply_register(parent: dev, desc: &pm8916_bms_vm_battery_psy_desc, cfg: &psy_cfg); |
216 | if (IS_ERR(ptr: bat->battery)) |
217 | return dev_err_probe(dev, err: PTR_ERR(ptr: bat->battery), fmt: "Unable to register battery\n" ); |
218 | |
219 | ret = power_supply_get_battery_info(psy: bat->battery, info_out: &bat->info); |
220 | if (ret) |
221 | return dev_err_probe(dev, err: ret, fmt: "Unable to get battery info\n" ); |
222 | |
223 | platform_set_drvdata(pdev, data: bat); |
224 | |
225 | return 0; |
226 | |
227 | comm_error: |
228 | return dev_err_probe(dev, err: ret, fmt: "Unable to communicate with device\n" ); |
229 | } |
230 | |
231 | static int pm8916_bms_vm_battery_suspend(struct platform_device *pdev, pm_message_t state) |
232 | { |
233 | struct pm8916_bms_vm_battery *bat = platform_get_drvdata(pdev); |
234 | int ret; |
235 | |
236 | /* |
237 | * Due to a hardware quirk the FSM doesn't switch states normally. |
238 | * Instead we unlock the debug registers and force S3 (Measure OCV/Sleep) |
239 | * mode every time we suspend. |
240 | */ |
241 | |
242 | ret = regmap_write(map: bat->regmap, |
243 | reg: bat->reg + PM8916_SEC_ACCESS, PM8916_SEC_MAGIC); |
244 | if (ret) |
245 | goto error; |
246 | ret = regmap_write(map: bat->regmap, |
247 | reg: bat->reg + PM8916_BMS_VM_MODE_CTL, PM8916_BMS_VM_MODE_FORCE_S3); |
248 | if (ret) |
249 | goto error; |
250 | |
251 | return 0; |
252 | |
253 | error: |
254 | dev_err(bat->dev, "Failed to force S3 mode: %pe\n" , ERR_PTR(ret)); |
255 | return ret; |
256 | } |
257 | |
258 | static int pm8916_bms_vm_battery_resume(struct platform_device *pdev) |
259 | { |
260 | struct pm8916_bms_vm_battery *bat = platform_get_drvdata(pdev); |
261 | int ret; |
262 | unsigned int tmp; |
263 | |
264 | ret = regmap_bulk_read(map: bat->regmap, |
265 | reg: bat->reg + PM8916_BMS_VM_S3_S7_OCV_DATA0, val: &tmp, val_count: 2); |
266 | |
267 | bat->last_ocv_time = ktime_get_seconds(); |
268 | bat->last_ocv = tmp * 300; |
269 | |
270 | ret = regmap_write(map: bat->regmap, |
271 | reg: bat->reg + PM8916_SEC_ACCESS, PM8916_SEC_MAGIC); |
272 | if (ret) |
273 | goto error; |
274 | ret = regmap_write(map: bat->regmap, |
275 | reg: bat->reg + PM8916_BMS_VM_MODE_CTL, PM8916_BMS_VM_MODE_NORMAL); |
276 | if (ret) |
277 | goto error; |
278 | |
279 | return 0; |
280 | |
281 | error: |
282 | dev_err(bat->dev, "Failed to return normal mode: %pe\n" , ERR_PTR(ret)); |
283 | return ret; |
284 | } |
285 | |
286 | static const struct of_device_id pm8916_bms_vm_battery_of_match[] = { |
287 | { .compatible = "qcom,pm8916-bms-vm" , }, |
288 | {} |
289 | }; |
290 | MODULE_DEVICE_TABLE(of, pm8916_bms_vm_battery_of_match); |
291 | |
292 | static struct platform_driver pm8916_bms_vm_battery_driver = { |
293 | .driver = { |
294 | .name = "pm8916-bms-vm" , |
295 | .of_match_table = pm8916_bms_vm_battery_of_match, |
296 | }, |
297 | .probe = pm8916_bms_vm_battery_probe, |
298 | .suspend = pm8916_bms_vm_battery_suspend, |
299 | .resume = pm8916_bms_vm_battery_resume, |
300 | }; |
301 | module_platform_driver(pm8916_bms_vm_battery_driver); |
302 | |
303 | MODULE_DESCRIPTION("pm8916 BMS-VM driver" ); |
304 | MODULE_AUTHOR("Nikita Travkin <nikita@trvn.ru>" ); |
305 | MODULE_LICENSE("GPL" ); |
306 | |