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