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(bat->regmap, bat->reg + PM8916_BMS_VM_BMS_FIFO_REG_0_LSB,
121			       &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(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, sizeof(*bat), GFP_KERNEL);
157	if (!bat)
158		return -ENOMEM;
159
160	bat->dev = dev;
161
162	bat->regmap = dev_get_regmap(pdev->dev.parent, NULL);
163	if (!bat->regmap)
164		return -ENODEV;
165
166	ret = device_property_read_u32(dev, "reg", &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, pm8916_bms_vm_fifo_update_done_irq,
175					IRQF_ONESHOT, "pm8916_vm_bms", bat);
176	if (ret)
177		return ret;
178
179	ret = regmap_bulk_read(bat->regmap, bat->reg + PM8916_PERPH_TYPE, &tmp, 2);
180	if (ret)
181		goto comm_error;
182
183	if (tmp != PM8916_BMS_VM_TYPE)
184		return dev_err_probe(dev, -ENODEV, "Device reported wrong type: 0x%X\n", tmp);
185
186	ret = regmap_write(bat->regmap, 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(bat->regmap, 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(bat->regmap, 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(bat->regmap,
199			   bat->reg + PM8916_BMS_VM_EN_CTL, PM8916_BMS_ENABLED);
200	if (ret)
201		goto comm_error;
202
203	ret = regmap_bulk_read(bat->regmap,
204			       bat->reg + PM8916_BMS_VM_S3_S7_OCV_DATA0, &tmp, 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(dev, &pm8916_bms_vm_battery_psy_desc, &psy_cfg);
216	if (IS_ERR(bat->battery))
217		return dev_err_probe(dev, PTR_ERR(bat->battery), "Unable to register battery\n");
218
219	ret = power_supply_get_battery_info(bat->battery, &bat->info);
220	if (ret)
221		return dev_err_probe(dev, ret, "Unable to get battery info\n");
222
223	platform_set_drvdata(pdev, bat);
224
225	return 0;
226
227comm_error:
228	return dev_err_probe(dev, ret, "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(bat->regmap,
243			   bat->reg + PM8916_SEC_ACCESS, PM8916_SEC_MAGIC);
244	if (ret)
245		goto error;
246	ret = regmap_write(bat->regmap,
247			   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(bat->regmap,
265			       bat->reg + PM8916_BMS_VM_S3_S7_OCV_DATA0, &tmp, 2);
266
267	bat->last_ocv_time = ktime_get_seconds();
268	bat->last_ocv = tmp * 300;
269
270	ret = regmap_write(bat->regmap,
271			   bat->reg + PM8916_SEC_ACCESS, PM8916_SEC_MAGIC);
272	if (ret)
273		goto error;
274	ret = regmap_write(bat->regmap,
275			   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