File Index
Files and directories
Sign in
FILE

pm8916_bms_vm.c

File preview with syntax highlighting.

linux-6.19.14/drivers/power/supply/pm8916_bms_vm.c C
  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	ret = regmap_bulk_read(bat->regmap, bat->reg + PM8916_PERPH_TYPE, &tmp, 2);
171	if (ret)
172		goto comm_error;
173
174	if (tmp != PM8916_BMS_VM_TYPE)
175		return dev_err_probe(dev, -ENODEV, "Device reported wrong type: 0x%X\n", tmp);
176
177	ret = regmap_write(bat->regmap, bat->reg + PM8916_BMS_VM_S1_SAMPLE_INTERVAL_CTL,
178			   PM8916_BMS_VM_S1_SAMPLE_INTERVAL);
179	if (ret)
180		goto comm_error;
181	ret = regmap_write(bat->regmap, bat->reg + PM8916_BMS_VM_S2_SAMPLE_INTERVAL_CTL,
182			   PM8916_BMS_VM_S2_SAMPLE_INTERVAL);
183	if (ret)
184		goto comm_error;
185	ret = regmap_write(bat->regmap, bat->reg + PM8916_BMS_VM_FIFO_LENGTH_CTL,
186			   PM8916_BMS_VM_FIFO_COUNT << 4 | PM8916_BMS_VM_FIFO_COUNT);
187	if (ret)
188		goto comm_error;
189	ret = regmap_write(bat->regmap,
190			   bat->reg + PM8916_BMS_VM_EN_CTL, PM8916_BMS_ENABLED);
191	if (ret)
192		goto comm_error;
193
194	ret = regmap_bulk_read(bat->regmap,
195			       bat->reg + PM8916_BMS_VM_S3_S7_OCV_DATA0, &tmp, 2);
196	if (ret)
197		goto comm_error;
198
199	bat->last_ocv_time = ktime_get_seconds();
200	bat->last_ocv = tmp * 300;
201	bat->vbat_now = bat->last_ocv;
202
203	psy_cfg.drv_data = bat;
204	psy_cfg.fwnode = dev_fwnode(dev);
205
206	bat->battery = devm_power_supply_register(dev, &pm8916_bms_vm_battery_psy_desc, &psy_cfg);
207	if (IS_ERR(bat->battery))
208		return dev_err_probe(dev, PTR_ERR(bat->battery), "Unable to register battery\n");
209
210	ret = power_supply_get_battery_info(bat->battery, &bat->info);
211	if (ret)
212		return dev_err_probe(dev, ret, "Unable to get battery info\n");
213
214	irq = platform_get_irq_byname(pdev, "fifo");
215	if (irq < 0)
216		return irq;
217
218	ret = devm_request_threaded_irq(dev, irq, NULL, pm8916_bms_vm_fifo_update_done_irq,
219					IRQF_ONESHOT, "pm8916_vm_bms", bat);
220	if (ret)
221		return ret;
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");
~/ Read-only file preview.