/* * Copyright 2022 Google LLC * Copyright 2023 Microsoft Corporation * * SPDX-License-Identifier: Apache-2.0 * * Emulator for SBS 1.1 compliant smart battery fuel gauge. */ #ifdef CONFIG_FUEL_GAUGE #define DT_DRV_COMPAT sbs_sbs_gauge_new_api #else #define DT_DRV_COMPAT sbs_sbs_gauge #endif /* CONFIG_FUEL_GAUGE */ #include LOG_MODULE_REGISTER(sbs_sbs_gauge); #include #include #include #include #include #include #include #include #include #include #include #include "sbs_gauge.h" /** Run-time data used by the emulator */ struct sbs_gauge_emul_data { uint16_t mfr_acc; uint16_t remaining_capacity_alarm; uint16_t remaining_time_alarm; uint16_t mode; int16_t at_rate; /* Whether the battery cutoff or not */ bool is_cutoff; /* * Counts the number of times the cutoff payload has been sent to the designated * register */ uint8_t cutoff_writes; struct { /* Non-register values associated with the state of the battery */ /* Battery terminal voltage */ uint32_t uV; /* Battery terminal current - Pos is charging, Neg is discharging */ int uA; } batt_state; }; /** Static configuration for the emulator */ struct sbs_gauge_emul_cfg { /** I2C address of emulator */ uint16_t addr; bool cutoff_support; uint32_t cutoff_reg_addr; uint16_t cutoff_payload[SBS_GAUGE_CUTOFF_PAYLOAD_MAX_SIZE]; }; static void emul_sbs_gauge_maybe_do_battery_cutoff(const struct emul *target, int reg, int val) { struct sbs_gauge_emul_data *data = target->data; const struct sbs_gauge_emul_cfg *cfg = target->cfg; /* Check if this is a cutoff write */ if (cfg->cutoff_support && reg == cfg->cutoff_reg_addr) { __ASSERT_NO_MSG(ARRAY_SIZE(cfg->cutoff_payload) > 0); /* * Calculate the next payload element value for a battery cutoff. * * We thoroughly check bounds elsewhere, so we can be confident we're not indexing * past the end of the array. */ uint16_t target_payload_elem_val = cfg->cutoff_payload[data->cutoff_writes]; if (target_payload_elem_val == val) { data->cutoff_writes++; __ASSERT_NO_MSG(data->cutoff_writes <= ARRAY_SIZE(cfg->cutoff_payload)); } else { /* Wrong payload target value, reset cutoff sequence detection. */ data->cutoff_writes = 0; } if (data->cutoff_writes == ARRAY_SIZE(cfg->cutoff_payload)) { data->is_cutoff = true; data->cutoff_writes = 0; } } /* Not a cutoff write, reset payload counter */ else { data->cutoff_writes = 0; } } static int emul_sbs_gauge_reg_write(const struct emul *target, int reg, int val) { struct sbs_gauge_emul_data *data = target->data; LOG_INF("write %x = %x", reg, val); switch (reg) { case SBS_GAUGE_CMD_MANUFACTURER_ACCESS: data->mfr_acc = val; break; case SBS_GAUGE_CMD_REM_CAPACITY_ALARM: data->remaining_capacity_alarm = val; break; case SBS_GAUGE_CMD_REM_TIME_ALARM: data->remaining_time_alarm = val; break; case SBS_GAUGE_CMD_BATTERY_MODE: data->mode = val; break; case SBS_GAUGE_CMD_AR: data->at_rate = val; break; default: LOG_INF("Unknown write %x", reg); return -EIO; } /* * One of the above registers is always designated as a "cutoff" register, usually it's * MANUFACTURER ACCESS, but not always. */ emul_sbs_gauge_maybe_do_battery_cutoff(target, reg, val); return 0; } static int emul_sbs_gauge_reg_read(const struct emul *target, int reg, int *val) { struct sbs_gauge_emul_data *data = target->data; switch (reg) { case SBS_GAUGE_CMD_MANUFACTURER_ACCESS: *val = data->mfr_acc; break; case SBS_GAUGE_CMD_REM_CAPACITY_ALARM: *val = data->remaining_capacity_alarm; break; case SBS_GAUGE_CMD_REM_TIME_ALARM: *val = data->remaining_time_alarm; break; case SBS_GAUGE_CMD_BATTERY_MODE: *val = data->mode; break; case SBS_GAUGE_CMD_AR: *val = data->at_rate; break; case SBS_GAUGE_CMD_VOLTAGE: *val = data->batt_state.uV / 1000; break; case SBS_GAUGE_CMD_CURRENT: *val = data->batt_state.uA / 1000; break; case SBS_GAUGE_CMD_AVG_CURRENT: case SBS_GAUGE_CMD_TEMP: case SBS_GAUGE_CMD_ASOC: case SBS_GAUGE_CMD_RSOC: case SBS_GAUGE_CMD_FULL_CAPACITY: case SBS_GAUGE_CMD_REM_CAPACITY: case SBS_GAUGE_CMD_NOM_CAPACITY: case SBS_GAUGE_CMD_AVG_TIME2EMPTY: case SBS_GAUGE_CMD_AVG_TIME2FULL: case SBS_GAUGE_CMD_RUNTIME2EMPTY: case SBS_GAUGE_CMD_CYCLE_COUNT: case SBS_GAUGE_CMD_DESIGN_VOLTAGE: case SBS_GAUGE_CMD_CHG_CURRENT: case SBS_GAUGE_CMD_CHG_VOLTAGE: case SBS_GAUGE_CMD_FLAGS: case SBS_GAUGE_CMD_ARTTF: case SBS_GAUGE_CMD_ARTTE: case SBS_GAUGE_CMD_AROK: /* Arbitrary stub value. */ *val = 1; break; default: LOG_ERR("Unknown register 0x%x read", reg); return -EIO; } LOG_INF("read 0x%x = 0x%x", reg, *val); return 0; } static int emul_sbs_gauge_buffer_read(const struct emul *target, int reg, char *val) { char mfg[] = "ACME"; char dev[] = "B123456"; char chem[] = "LiPO"; struct sbs_gauge_manufacturer_name *mfg_name = (struct sbs_gauge_manufacturer_name *)val; struct sbs_gauge_device_name *dev_name = (struct sbs_gauge_device_name *)val; struct sbs_gauge_device_chemistry *dev_chem = (struct sbs_gauge_device_chemistry *)val; switch (reg) { case SBS_GAUGE_CMD_MANUFACTURER_NAME: mfg_name->manufacturer_name_length = sizeof(mfg); memcpy(mfg_name->manufacturer_name, mfg, mfg_name->manufacturer_name_length); break; case SBS_GAUGE_CMD_DEVICE_NAME: dev_name->device_name_length = sizeof(dev); memcpy(dev_name->device_name, dev, dev_name->device_name_length); break; case SBS_GAUGE_CMD_DEVICE_CHEMISTRY: dev_chem->device_chemistry_length = MIN(sizeof(chem), sizeof(dev_chem->device_chemistry)); memcpy(dev_chem->device_chemistry, chem, dev_chem->device_chemistry_length); break; default: LOG_ERR("Unknown register 0x%x read", reg); return -EIO; } return 0; } static int sbs_gauge_emul_transfer_i2c(const struct emul *target, struct i2c_msg *msgs, int num_msgs, int addr) { /* Largely copied from emul_bmi160.c */ struct sbs_gauge_emul_data *data; unsigned int val; int reg; int rc; data = target->data; __ASSERT_NO_MSG(msgs && num_msgs); i2c_dump_msgs_rw(target->dev, msgs, num_msgs, addr, false); switch (num_msgs) { case 2: if (msgs->flags & I2C_MSG_READ) { LOG_ERR("Unexpected read"); return -EIO; } if (msgs->len != 1) { LOG_ERR("Unexpected msg0 length %d", msgs->len); return -EIO; } reg = msgs->buf[0]; /* Now process the 'read' part of the message */ msgs++; if (msgs->flags & I2C_MSG_READ) { switch (msgs->len) { case 2: rc = emul_sbs_gauge_reg_read(target, reg, &val); if (rc) { /* Return before writing bad value to message buffer */ return rc; } /* SBS uses SMBus, which sends data in little-endian format. */ sys_put_le16(val, msgs->buf); break; /* buffer properties */ case (sizeof(struct sbs_gauge_manufacturer_name)): case (sizeof(struct sbs_gauge_device_chemistry)): rc = emul_sbs_gauge_buffer_read(target, reg, (char *)msgs->buf); break; default: LOG_ERR("Unexpected msg1 length %d", msgs->len); return -EIO; } } else { /* We write a word (2 bytes by the SBS spec) */ if (msgs->len != 2) { LOG_ERR("Unexpected msg1 length %d", msgs->len); } uint16_t value = sys_get_le16(msgs->buf); rc = emul_sbs_gauge_reg_write(target, reg, value); } break; default: LOG_ERR("Invalid number of messages: %d", num_msgs); return -EIO; } return rc; } static int emul_sbs_fuel_gauge_set_battery_charging(const struct emul *target, uint32_t uV, int uA) { struct sbs_gauge_emul_data *data = target->data; if (uV == 0 || uA == 0) { return -EINVAL; } data->batt_state.uA = uA; data->batt_state.uV = uV; return 0; } static int emul_sbs_fuel_gauge_is_battery_cutoff(const struct emul *target, bool *cutoff) { struct sbs_gauge_emul_data *data = target->data; __ASSERT_NO_MSG(cutoff != NULL); *cutoff = data->is_cutoff; return 0; } static const struct fuel_gauge_emul_driver_api sbs_gauge_backend_api = { .set_battery_charging = emul_sbs_fuel_gauge_set_battery_charging, .is_battery_cutoff = emul_sbs_fuel_gauge_is_battery_cutoff, }; static const struct i2c_emul_api sbs_gauge_emul_api_i2c = { .transfer = sbs_gauge_emul_transfer_i2c, }; static void sbs_gauge_emul_reset(const struct emul *target) { struct sbs_gauge_emul_data *data = target->data; memset(data, 0, sizeof(*data)); } #ifdef CONFIG_ZTEST #include /* Add test reset handlers in when using emulators with tests */ #define SBS_GAUGE_EMUL_RESET_RULE_BEFORE(inst) \ sbs_gauge_emul_reset(EMUL_DT_GET(DT_DRV_INST(inst))); static void emul_sbs_gauge_reset_rule_after(const struct ztest_unit_test *test, void *data) { ARG_UNUSED(test); ARG_UNUSED(data); DT_INST_FOREACH_STATUS_OKAY(SBS_GAUGE_EMUL_RESET_RULE_BEFORE) } ZTEST_RULE(emul_sbs_gauge_reset, NULL, emul_sbs_gauge_reset_rule_after); #endif /* CONFIG_ZTEST */ /** * Set up a new SBS_GAUGE emulator (I2C) * * @param emul Emulation information * @param parent Device to emulate (must use sbs_gauge driver) * @return 0 indicating success (always) */ static int emul_sbs_sbs_gauge_init(const struct emul *target, const struct device *parent) { ARG_UNUSED(parent); sbs_gauge_emul_reset(target); return 0; } /* * Main instantiation macro. SBS Gauge Emulator only implemented for I2C */ #define SBS_GAUGE_EMUL(n) \ static struct sbs_gauge_emul_data sbs_gauge_emul_data_##n; \ static const struct sbs_gauge_emul_cfg sbs_gauge_emul_cfg_##n = { \ .addr = DT_INST_REG_ADDR(n), \ .cutoff_support = DT_PROP_OR(DT_DRV_INST(n), battery_cutoff_support, false), \ .cutoff_reg_addr = DT_PROP_OR(DT_DRV_INST(n), battery_cutoff_reg_addr, 0), \ .cutoff_payload = DT_PROP_OR(DT_DRV_INST(n), battery_cutoff_payload, {}), \ }; \ EMUL_DT_INST_DEFINE(n, emul_sbs_sbs_gauge_init, &sbs_gauge_emul_data_##n, \ &sbs_gauge_emul_cfg_##n, &sbs_gauge_emul_api_i2c, \ &sbs_gauge_backend_api) DT_INST_FOREACH_STATUS_OKAY(SBS_GAUGE_EMUL)