379 lines
10 KiB
C
379 lines
10 KiB
C
/*
|
|
* 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 <zephyr/logging/log.h>
|
|
LOG_MODULE_REGISTER(sbs_sbs_gauge);
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <zephyr/device.h>
|
|
#include <zephyr/devicetree.h>
|
|
#include <zephyr/drivers/emul.h>
|
|
#include <zephyr/drivers/i2c.h>
|
|
#include <zephyr/drivers/i2c_emul.h>
|
|
#include <zephyr/sys/byteorder.h>
|
|
#include <zephyr/drivers/emul_fuel_gauge.h>
|
|
#include <zephyr/drivers/fuel_gauge.h>
|
|
#include <zephyr/sys/util.h>
|
|
|
|
#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 <zephyr/ztest.h>
|
|
|
|
/* 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)
|