1 /*
2  * Copyright 2022 Google LLC
3  * Copyright 2023 Microsoft Corporation
4  *
5  * SPDX-License-Identifier: Apache-2.0
6  *
7  * Emulator for SBS 1.1 compliant smart battery fuel gauge.
8  */
9 
10 #ifdef CONFIG_FUEL_GAUGE
11 #define DT_DRV_COMPAT sbs_sbs_gauge_new_api
12 #else
13 #define DT_DRV_COMPAT sbs_sbs_gauge
14 #endif /* CONFIG_FUEL_GAUGE */
15 
16 #include <zephyr/logging/log.h>
17 LOG_MODULE_REGISTER(sbs_sbs_gauge);
18 
19 #include <stdbool.h>
20 #include <stdint.h>
21 #include <zephyr/device.h>
22 #include <zephyr/devicetree.h>
23 #include <zephyr/drivers/emul.h>
24 #include <zephyr/drivers/i2c.h>
25 #include <zephyr/drivers/i2c_emul.h>
26 #include <zephyr/sys/byteorder.h>
27 #include <zephyr/drivers/emul_fuel_gauge.h>
28 #include <zephyr/drivers/fuel_gauge.h>
29 #include <zephyr/sys/util.h>
30 
31 #include "sbs_gauge.h"
32 
33 /** Run-time data used by the emulator */
34 struct sbs_gauge_emul_data {
35 	uint16_t mfr_acc;
36 	uint16_t remaining_capacity_alarm;
37 	uint16_t remaining_time_alarm;
38 	uint16_t mode;
39 	int16_t at_rate;
40 	/* Whether the battery cutoff or not */
41 	bool is_cutoff;
42 	/*
43 	 * Counts the number of times the cutoff payload has been sent to the designated
44 	 * register
45 	 */
46 	uint8_t cutoff_writes;
47 	struct {
48 		/* Non-register values associated with the state of the battery */
49 		/* Battery terminal voltage */
50 		uint32_t uV;
51 		/* Battery terminal current - Pos is charging, Neg is discharging */
52 		int uA;
53 	} batt_state;
54 };
55 
56 /** Static configuration for the emulator */
57 struct sbs_gauge_emul_cfg {
58 	/** I2C address of emulator */
59 	uint16_t addr;
60 	bool cutoff_support;
61 	uint32_t cutoff_reg_addr;
62 	uint16_t cutoff_payload[SBS_GAUGE_CUTOFF_PAYLOAD_MAX_SIZE];
63 };
64 
emul_sbs_gauge_maybe_do_battery_cutoff(const struct emul * target,int reg,int val)65 static void emul_sbs_gauge_maybe_do_battery_cutoff(const struct emul *target, int reg, int val)
66 {
67 	struct sbs_gauge_emul_data *data = target->data;
68 	const struct sbs_gauge_emul_cfg *cfg = target->cfg;
69 
70 	/* Check if this is a cutoff write */
71 	if (cfg->cutoff_support && reg == cfg->cutoff_reg_addr) {
72 		__ASSERT_NO_MSG(ARRAY_SIZE(cfg->cutoff_payload) > 0);
73 		/*
74 		 * Calculate the next payload element value for a battery cutoff.
75 		 *
76 		 * We thoroughly check bounds elsewhere, so we can be confident we're not indexing
77 		 * past the end of the array.
78 		 */
79 		uint16_t target_payload_elem_val = cfg->cutoff_payload[data->cutoff_writes];
80 
81 		if (target_payload_elem_val == val) {
82 			data->cutoff_writes++;
83 			__ASSERT_NO_MSG(data->cutoff_writes <= ARRAY_SIZE(cfg->cutoff_payload));
84 		} else {
85 			/* Wrong payload target value, reset cutoff sequence detection. */
86 			data->cutoff_writes = 0;
87 		}
88 
89 		if (data->cutoff_writes == ARRAY_SIZE(cfg->cutoff_payload)) {
90 			data->is_cutoff = true;
91 			data->cutoff_writes = 0;
92 		}
93 	}
94 	/* Not a cutoff write, reset payload counter  */
95 	else {
96 		data->cutoff_writes = 0;
97 	}
98 }
99 
emul_sbs_gauge_reg_write(const struct emul * target,int reg,int val)100 static int emul_sbs_gauge_reg_write(const struct emul *target, int reg, int val)
101 {
102 	struct sbs_gauge_emul_data *data = target->data;
103 
104 	LOG_INF("write %x = %x", reg, val);
105 	switch (reg) {
106 	case SBS_GAUGE_CMD_MANUFACTURER_ACCESS:
107 		data->mfr_acc = val;
108 		break;
109 	case SBS_GAUGE_CMD_REM_CAPACITY_ALARM:
110 		data->remaining_capacity_alarm = val;
111 		break;
112 	case SBS_GAUGE_CMD_REM_TIME_ALARM:
113 		data->remaining_time_alarm = val;
114 		break;
115 	case SBS_GAUGE_CMD_BATTERY_MODE:
116 		data->mode = val;
117 		break;
118 	case SBS_GAUGE_CMD_AR:
119 		data->at_rate = val;
120 		break;
121 	default:
122 		LOG_INF("Unknown write %x", reg);
123 		return -EIO;
124 	}
125 
126 	/*
127 	 * One of the above registers is always designated as a "cutoff" register, usually it's
128 	 * MANUFACTURER ACCESS, but not always.
129 	 */
130 	emul_sbs_gauge_maybe_do_battery_cutoff(target, reg, val);
131 
132 	return 0;
133 }
134 
emul_sbs_gauge_reg_read(const struct emul * target,int reg,int * val)135 static int emul_sbs_gauge_reg_read(const struct emul *target, int reg, int *val)
136 {
137 	struct sbs_gauge_emul_data *data = target->data;
138 
139 	switch (reg) {
140 	case SBS_GAUGE_CMD_MANUFACTURER_ACCESS:
141 		*val = data->mfr_acc;
142 		break;
143 	case SBS_GAUGE_CMD_REM_CAPACITY_ALARM:
144 		*val = data->remaining_capacity_alarm;
145 		break;
146 	case SBS_GAUGE_CMD_REM_TIME_ALARM:
147 		*val = data->remaining_time_alarm;
148 		break;
149 	case SBS_GAUGE_CMD_BATTERY_MODE:
150 		*val = data->mode;
151 		break;
152 	case SBS_GAUGE_CMD_AR:
153 		*val = data->at_rate;
154 		break;
155 	case SBS_GAUGE_CMD_VOLTAGE:
156 		*val = data->batt_state.uV / 1000;
157 		break;
158 	case SBS_GAUGE_CMD_CURRENT:
159 		*val = data->batt_state.uA / 1000;
160 		break;
161 	case SBS_GAUGE_CMD_AVG_CURRENT:
162 	case SBS_GAUGE_CMD_TEMP:
163 	case SBS_GAUGE_CMD_ASOC:
164 	case SBS_GAUGE_CMD_RSOC:
165 	case SBS_GAUGE_CMD_FULL_CAPACITY:
166 	case SBS_GAUGE_CMD_REM_CAPACITY:
167 	case SBS_GAUGE_CMD_NOM_CAPACITY:
168 	case SBS_GAUGE_CMD_AVG_TIME2EMPTY:
169 	case SBS_GAUGE_CMD_AVG_TIME2FULL:
170 	case SBS_GAUGE_CMD_RUNTIME2EMPTY:
171 	case SBS_GAUGE_CMD_CYCLE_COUNT:
172 	case SBS_GAUGE_CMD_DESIGN_VOLTAGE:
173 	case SBS_GAUGE_CMD_CHG_CURRENT:
174 	case SBS_GAUGE_CMD_CHG_VOLTAGE:
175 	case SBS_GAUGE_CMD_FLAGS:
176 	case SBS_GAUGE_CMD_ARTTF:
177 	case SBS_GAUGE_CMD_ARTTE:
178 	case SBS_GAUGE_CMD_AROK:
179 		/* Arbitrary stub value. */
180 		*val = 1;
181 		break;
182 	default:
183 		LOG_ERR("Unknown register 0x%x read", reg);
184 		return -EIO;
185 	}
186 	LOG_INF("read 0x%x = 0x%x", reg, *val);
187 
188 	return 0;
189 }
190 
emul_sbs_gauge_buffer_read(const struct emul * target,int reg,char * val)191 static int emul_sbs_gauge_buffer_read(const struct emul *target, int reg, char *val)
192 {
193 	char mfg[] = "ACME";
194 	char dev[] = "B123456";
195 	char chem[] = "LiPO";
196 	struct sbs_gauge_manufacturer_name *mfg_name = (struct sbs_gauge_manufacturer_name *)val;
197 	struct sbs_gauge_device_name *dev_name = (struct sbs_gauge_device_name *)val;
198 	struct sbs_gauge_device_chemistry *dev_chem = (struct sbs_gauge_device_chemistry *)val;
199 
200 	switch (reg) {
201 	case SBS_GAUGE_CMD_MANUFACTURER_NAME:
202 		mfg_name->manufacturer_name_length = sizeof(mfg);
203 		memcpy(mfg_name->manufacturer_name, mfg, mfg_name->manufacturer_name_length);
204 		break;
205 	case SBS_GAUGE_CMD_DEVICE_NAME:
206 		dev_name->device_name_length = sizeof(dev);
207 		memcpy(dev_name->device_name, dev, dev_name->device_name_length);
208 		break;
209 
210 	case SBS_GAUGE_CMD_DEVICE_CHEMISTRY:
211 		dev_chem->device_chemistry_length = MIN(sizeof(chem),
212 							sizeof(dev_chem->device_chemistry));
213 		memcpy(dev_chem->device_chemistry, chem, dev_chem->device_chemistry_length);
214 		break;
215 	default:
216 		LOG_ERR("Unknown register 0x%x read", reg);
217 		return -EIO;
218 	}
219 
220 	return 0;
221 }
222 
sbs_gauge_emul_transfer_i2c(const struct emul * target,struct i2c_msg * msgs,int num_msgs,int addr)223 static int sbs_gauge_emul_transfer_i2c(const struct emul *target, struct i2c_msg *msgs,
224 				       int num_msgs, int addr)
225 {
226 	/* Largely copied from emul_bmi160.c */
227 	struct sbs_gauge_emul_data *data;
228 	unsigned int val;
229 	int reg;
230 	int rc;
231 
232 	data = target->data;
233 
234 	__ASSERT_NO_MSG(msgs && num_msgs);
235 
236 	i2c_dump_msgs_rw(target->dev, msgs, num_msgs, addr, false);
237 	switch (num_msgs) {
238 	case 2:
239 		if (msgs->flags & I2C_MSG_READ) {
240 			LOG_ERR("Unexpected read");
241 			return -EIO;
242 		}
243 		if (msgs->len != 1) {
244 			LOG_ERR("Unexpected msg0 length %d", msgs->len);
245 			return -EIO;
246 		}
247 		reg = msgs->buf[0];
248 
249 		/* Now process the 'read' part of the message */
250 		msgs++;
251 		if (msgs->flags & I2C_MSG_READ) {
252 			switch (msgs->len) {
253 			case 2:
254 				rc = emul_sbs_gauge_reg_read(target, reg, &val);
255 				if (rc) {
256 					/* Return before writing bad value to message buffer */
257 					return rc;
258 				}
259 
260 				/* SBS uses SMBus, which sends data in little-endian format. */
261 				sys_put_le16(val, msgs->buf);
262 				break;
263 					/* buffer properties */
264 			case (sizeof(struct sbs_gauge_manufacturer_name)):
265 			case (sizeof(struct sbs_gauge_device_chemistry)):
266 				rc = emul_sbs_gauge_buffer_read(target, reg, (char *)msgs->buf);
267 				break;
268 			default:
269 				LOG_ERR("Unexpected msg1 length %d", msgs->len);
270 				return -EIO;
271 			}
272 		} else {
273 			/* We write a word (2 bytes by the SBS spec) */
274 			if (msgs->len != 2) {
275 				LOG_ERR("Unexpected msg1 length %d", msgs->len);
276 			}
277 			uint16_t value = sys_get_le16(msgs->buf);
278 
279 			rc = emul_sbs_gauge_reg_write(target, reg, value);
280 		}
281 		break;
282 	default:
283 		LOG_ERR("Invalid number of messages: %d", num_msgs);
284 		return -EIO;
285 	}
286 
287 	return rc;
288 }
289 
emul_sbs_fuel_gauge_set_battery_charging(const struct emul * target,uint32_t uV,int uA)290 static int emul_sbs_fuel_gauge_set_battery_charging(const struct emul *target, uint32_t uV, int uA)
291 {
292 	struct sbs_gauge_emul_data *data = target->data;
293 
294 	if (uV == 0 || uA == 0)
295 		return -EINVAL;
296 
297 	data->batt_state.uA = uA;
298 	data->batt_state.uV = uV;
299 
300 	return 0;
301 }
302 
emul_sbs_fuel_gauge_is_battery_cutoff(const struct emul * target,bool * cutoff)303 static int emul_sbs_fuel_gauge_is_battery_cutoff(const struct emul *target, bool *cutoff)
304 {
305 	struct sbs_gauge_emul_data *data = target->data;
306 
307 	__ASSERT_NO_MSG(cutoff != NULL);
308 
309 	*cutoff = data->is_cutoff;
310 
311 	return 0;
312 }
313 
314 static const struct fuel_gauge_emul_driver_api sbs_gauge_backend_api = {
315 	.set_battery_charging = emul_sbs_fuel_gauge_set_battery_charging,
316 	.is_battery_cutoff = emul_sbs_fuel_gauge_is_battery_cutoff,
317 };
318 
319 static const struct i2c_emul_api sbs_gauge_emul_api_i2c = {
320 	.transfer = sbs_gauge_emul_transfer_i2c,
321 };
322 
sbs_gauge_emul_reset(const struct emul * target)323 static void sbs_gauge_emul_reset(const struct emul *target)
324 {
325 	struct sbs_gauge_emul_data *data = target->data;
326 
327 	memset(data, 0, sizeof(*data));
328 }
329 
330 #ifdef CONFIG_ZTEST
331 #include <zephyr/ztest.h>
332 
333 /* Add test reset handlers in when using emulators with tests */
334 #define SBS_GAUGE_EMUL_RESET_RULE_BEFORE(inst)                                                     \
335 	sbs_gauge_emul_reset(EMUL_DT_GET(DT_DRV_INST(inst)));
336 
emul_sbs_gauge_reset_rule_after(const struct ztest_unit_test * test,void * data)337 static void emul_sbs_gauge_reset_rule_after(const struct ztest_unit_test *test, void *data)
338 {
339 	ARG_UNUSED(test);
340 	ARG_UNUSED(data);
341 
342 	DT_INST_FOREACH_STATUS_OKAY(SBS_GAUGE_EMUL_RESET_RULE_BEFORE)
343 }
344 ZTEST_RULE(emul_sbs_gauge_reset, NULL, emul_sbs_gauge_reset_rule_after);
345 #endif /* CONFIG_ZTEST */
346 
347 /**
348  * Set up a new SBS_GAUGE emulator (I2C)
349  *
350  * @param emul Emulation information
351  * @param parent Device to emulate (must use sbs_gauge driver)
352  * @return 0 indicating success (always)
353  */
emul_sbs_sbs_gauge_init(const struct emul * target,const struct device * parent)354 static int emul_sbs_sbs_gauge_init(const struct emul *target, const struct device *parent)
355 {
356 	ARG_UNUSED(parent);
357 
358 	sbs_gauge_emul_reset(target);
359 
360 	return 0;
361 }
362 
363 /*
364  * Main instantiation macro. SBS Gauge Emulator only implemented for I2C
365  */
366 #define SBS_GAUGE_EMUL(n)                                                                          \
367 	static struct sbs_gauge_emul_data sbs_gauge_emul_data_##n;                                 \
368 	static const struct sbs_gauge_emul_cfg sbs_gauge_emul_cfg_##n = {                          \
369 		.addr = DT_INST_REG_ADDR(n),                                                       \
370 		.cutoff_support = DT_PROP_OR(DT_DRV_INST(n), battery_cutoff_support, false),       \
371 		.cutoff_reg_addr = DT_PROP_OR(DT_DRV_INST(n), battery_cutoff_reg_addr, 0),         \
372 		.cutoff_payload = DT_PROP_OR(DT_DRV_INST(n), battery_cutoff_payload, {}),          \
373 	};                                                                                         \
374 	EMUL_DT_INST_DEFINE(n, emul_sbs_sbs_gauge_init, &sbs_gauge_emul_data_##n,                  \
375 			    &sbs_gauge_emul_cfg_##n, &sbs_gauge_emul_api_i2c,                      \
376 			    &sbs_gauge_backend_api)
377 
378 DT_INST_FOREACH_STATUS_OKAY(SBS_GAUGE_EMUL)
379