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
47struct 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
58static 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
107static 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
114static 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
140static 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
148static 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
227comm_error:
228 return dev_err_probe(dev, err: ret, fmt: "Unable to communicate with device\n");
229}
230
231static 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
253error:
254 dev_err(bat->dev, "Failed to force S3 mode: %pe\n", ERR_PTR(ret));
255 return ret;
256}
257
258static 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
281error:
282 dev_err(bat->dev, "Failed to return normal mode: %pe\n", ERR_PTR(ret));
283 return ret;
284}
285
286static const struct of_device_id pm8916_bms_vm_battery_of_match[] = {
287 { .compatible = "qcom,pm8916-bms-vm", },
288 {}
289};
290MODULE_DEVICE_TABLE(of, pm8916_bms_vm_battery_of_match);
291
292static 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};
301module_platform_driver(pm8916_bms_vm_battery_driver);
302
303MODULE_DESCRIPTION("pm8916 BMS-VM driver");
304MODULE_AUTHOR("Nikita Travkin <nikita@trvn.ru>");
305MODULE_LICENSE("GPL");
306

source code of linux/drivers/power/supply/pm8916_bms_vm.c