Skip to content

Commit

Permalink
fuel_gauge: Add battery cutoff support
Browse files Browse the repository at this point in the history
Many fuel gauge ICs offer a battery cutoff/shipping mode functionality that
cutoff charge from the battery. This is often useful for preserving battery
charge on devices while in storage.

Add battery cutoff support to the fuel gauge API with a generic default SBS
driver showing an example of support in tests.

Signed-off-by: Aaron Massey <aaronmassey@google.com>
  • Loading branch information
aaronemassey committed Aug 21, 2023
1 parent 45a4177 commit 4df450a
Show file tree
Hide file tree
Showing 15 changed files with 385 additions and 6 deletions.
9 changes: 9 additions & 0 deletions doc/hardware/peripherals/fuel_gauge.rst
Expand Up @@ -23,6 +23,15 @@ or present-time current/voltage.
Properties are fetched using a client allocated array of :c:struct:`fuel_gauge_get_property`. This
array is then populated by values as according to its `property_type` field.

Battery Cutoff
==============

Many fuel gauges embedded within battery packs expose a register address that when written to with a
specific payload will do a battery cutoff. This battery cutoff is often referred to as ship, shelf,
or sleep mode due to its utility in reducing battery drain while devices are stored or shipped.

The fuel gauge API exposes battery cutoff with the :c:func:`fuel_gauge_battery_cutoff` function.

Caching
=======

Expand Down
9 changes: 9 additions & 0 deletions drivers/fuel_gauge/fuel_gauge_syscall_handlers.c
Expand Up @@ -70,3 +70,12 @@ static inline int z_vrfy_fuel_gauge_get_buffer_prop(const struct device *dev,
}

#include <syscalls/fuel_gauge_get_buffer_prop_mrsh.c>

static inline int z_vrfy_fuel_gauge_battery_cutoff(const struct device *dev)
{
Z_OOPS(Z_SYSCALL_DRIVER_FUEL_GAUGE(dev, battery_cutoff));

return z_impl_fuel_gauge_battery_cutoff(dev);
}

#include <syscalls/fuel_gauge_battery_cutoff_mrsh.c>
70 changes: 70 additions & 0 deletions drivers/fuel_gauge/sbs_gauge/emul_sbs_gauge.c
Expand Up @@ -16,13 +16,17 @@
#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"

Expand All @@ -33,6 +37,13 @@ struct sbs_gauge_emul_data {
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 */
Expand All @@ -46,8 +57,46 @@ struct sbs_gauge_emul_data {
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;
Expand All @@ -74,6 +123,12 @@ static int emul_sbs_gauge_reg_write(const struct emul *target, int reg, int val)
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;
}

Expand Down Expand Up @@ -245,8 +300,20 @@ static int emul_sbs_fuel_gauge_set_battery_charging(const struct emul *target, u
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 = {
Expand Down Expand Up @@ -303,6 +370,9 @@ static int emul_sbs_sbs_gauge_init(const struct emul *target, const struct devic
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, \
Expand Down
57 changes: 53 additions & 4 deletions drivers/fuel_gauge/sbs_gauge/sbs_gauge.c
Expand Up @@ -11,10 +11,14 @@

#include "sbs_gauge.h"

#include <stdbool.h>
#include <stdint.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/fuel_gauge.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>

LOG_MODULE_REGISTER(sbs_gauge);

Expand Down Expand Up @@ -173,6 +177,25 @@ static int sbs_gauge_get_prop(const struct device *dev, struct fuel_gauge_get_pr
return rc;
}

static int sbs_gauge_do_battery_cutoff(const struct device *dev)
{
int rc;
const struct sbs_gauge_config *cfg = dev->config;

if (cfg->cutoff_cfg == NULL) {
return -ENOTSUP;
}

for (int i = 0; i < cfg->cutoff_cfg->payload_size; i++) {
rc = sbs_cmd_reg_write(dev, cfg->cutoff_cfg->reg, cfg->cutoff_cfg->payload[i]);
if (rc != 0) {
return rc;
}
}

return rc;
}

static int sbs_gauge_set_prop(const struct device *dev, struct fuel_gauge_set_property *prop)
{
int rc = 0;
Expand Down Expand Up @@ -203,7 +226,6 @@ static int sbs_gauge_set_prop(const struct device *dev, struct fuel_gauge_set_pr
rc = sbs_cmd_reg_write(dev, SBS_GAUGE_CMD_AR, prop->value.sbs_at_rate);
prop->value.sbs_at_rate = val;
break;

default:
rc = -ENOTSUP;
}
Expand Down Expand Up @@ -307,17 +329,44 @@ static const struct fuel_gauge_driver_api sbs_gauge_driver_api = {
.get_property = &sbs_gauge_get_props,
.set_property = &sbs_gauge_set_props,
.get_buffer_property = &sbs_gauge_get_buffer_prop,
.battery_cutoff = &sbs_gauge_do_battery_cutoff,
};

/* FIXME: fix init priority */
/* Concatenates index to battery config to create unique cfg variable name per instance. */
#define _SBS_GAUGE_BATT_CUTOFF_CFG_VAR_NAME(index) sbs_gauge_batt_cutoff_cfg_##index

/* Declare and define the battery config struct */
#define _SBS_GAUGE_CONFIG_INIT(index) \
static const struct sbs_gauge_battery_cutoff_config _SBS_GAUGE_BATT_CUTOFF_CFG_VAR_NAME( \
index) = { \
.reg = DT_INST_PROP(index, battery_cutoff_reg_addr), \
.payload = DT_INST_PROP(index, battery_cutoff_payload), \
.payload_size = DT_INST_PROP_LEN(index, battery_cutoff_payload), \
};

/* Conditionally defined battery config based on battery cutoff support */
#define SBS_GAUGE_CONFIG_DEFINE(index) \
COND_CODE_1(DT_INST_PROP_OR(index, battery_cutoff_support, false), \
(_SBS_GAUGE_CONFIG_INIT(index)), (;))

/* Conditionally get the battery config variable name or NULL based on battery cutoff support */
#define SBS_GAUGE_GET_BATTERY_CONFIG_NAME(index) \
COND_CODE_1(DT_INST_PROP_OR(index, battery_cutoff_support, false), \
(&_SBS_GAUGE_BATT_CUTOFF_CFG_VAR_NAME(index)), (NULL))

#define SBS_GAUGE_INIT(index) \
\
SBS_GAUGE_CONFIG_DEFINE(index); \
static const struct sbs_gauge_config sbs_gauge_config_##index = { \
.i2c = I2C_DT_SPEC_INST_GET(index), \
}; \
.cutoff_cfg = SBS_GAUGE_GET_BATTERY_CONFIG_NAME(index)}; \
\
DEVICE_DT_INST_DEFINE(index, &sbs_gauge_init, NULL, NULL, &sbs_gauge_config_##index, \
POST_KERNEL, CONFIG_FUEL_GAUGE_INIT_PRIORITY, \
&sbs_gauge_driver_api);

DT_INST_FOREACH_STATUS_OKAY(SBS_GAUGE_INIT)

#define CUTOFF_PAYLOAD_SIZE_ASSERT(inst) \
BUILD_ASSERT(DT_INST_PROP_LEN_OR(inst, battery_cutoff_payload, 0) <= \
SBS_GAUGE_CUTOFF_PAYLOAD_MAX_SIZE);
DT_INST_FOREACH_STATUS_OKAY(CUTOFF_PAYLOAD_SIZE_ASSERT)
25 changes: 25 additions & 0 deletions drivers/fuel_gauge/sbs_gauge/sbs_gauge.h
Expand Up @@ -47,8 +47,33 @@

#define SBS_GAUGE_DELAY 1000

/*
* Nearly all cutoff payloads are actually a singular value that must be written twice to the fuel
* gauge. For the case where it's a singular value that must only be written to the fuel gauge only
* once, retransmitting the duplicate write has no significant negative consequences.
*
* Why not devicetree: Finding the maximum length of all the battery cutoff payloads in a devicetree
* at compile-time would require labyrinthine amount of macro-batics.
*
* Why not compute at runtime: It's not worth the memory given having more than a single fuel gauge
* is rare, and most will have a payload size of 2.
*
* This is validated as a BUILD_ASSERT in the driver.
*/
#define SBS_GAUGE_CUTOFF_PAYLOAD_MAX_SIZE 2

struct sbs_gauge_battery_cutoff_config {
/* Size of the payload array */
size_t payload_size;
/* Array SMBus word values to write to cut off the battery */
uint32_t payload[SBS_GAUGE_CUTOFF_PAYLOAD_MAX_SIZE];
/* Register to write cutoff payload */
uint8_t reg;
};

struct sbs_gauge_config {
struct i2c_dt_spec i2c;
const struct sbs_gauge_battery_cutoff_config *cutoff_cfg;
};

#endif
29 changes: 29 additions & 0 deletions dts/bindings/fuel-gauge/battery-cutoff.yaml
@@ -0,0 +1,29 @@
#
# Copyright 2023 Google LLC
#
# SPDX-License-Identifier: Apache-2.0
#

description: |
Properties for fuel-gauges that may control battery cutoff, this is common in SBS-compliant or
similarly smart battery fuel gauges.
Note: These properties are to be used with meaningful defaults in fuel gauge ICs that can cut off
their associated battery from the system. See the default fuel gauge SBS Gauge compatible as an
example.
properties:
battery-cutoff-support:
description: |
Helper prop that indicates whether this device can cutoff the battery; this is also often
referred to as ship or sleep mode.
type: boolean
battery-cutoff-reg-addr:
description: |
Address of register to receive cutoff payload for battery cutoff.
type: int
battery-cutoff-payload:
description: |
Payload to write to cutoff battery register. This must be array of maximum 2 integers.
type: array
22 changes: 22 additions & 0 deletions dts/bindings/fuel-gauge/sbs,default-sbs-gauge.yaml
@@ -0,0 +1,22 @@
#
# Copyright 2023 Google LLC
#
# SPDX-License-Identifier: Apache-2.0
#

compatible: "sbs,default-sbs-gauge"

include: ["sbs,sbs-gauge-new-api.yaml", "battery-cutoff.yaml"]

description: |
Default generic smart battery fuel gauge driver. Includes support for battery cutoff if enabled.
This compatible is intended to be used with the abstract SBS Gauge compatible because it is
actuated by the SBS driver for SBS compliant fuel gauge ICs.
properties:
battery-cutoff-reg-addr:
# For SBS compliant fuel gauges this is usually "ManufactuerAccess"
default: 0x0
battery-cutoff-payload:
default: [0x0010, 0x0010]
21 changes: 21 additions & 0 deletions include/zephyr/drivers/emul_fuel_gauge.h
Expand Up @@ -34,6 +34,7 @@ extern "C" {
*/
__subsystem struct fuel_gauge_emul_driver_api {
int (*set_battery_charging)(const struct emul *emul, uint32_t uV, int uA);
int (*is_battery_cutoff)(const struct emul *emul, bool *cutoff);
};
/**
* @endcond
Expand Down Expand Up @@ -66,6 +67,26 @@ static inline int emul_fuel_gauge_set_battery_charging(const struct emul *target
return backend_api->set_battery_charging(target, uV, uA);
}

/**
* @brief Check if the battery has been cut off.
*
* @param target Pointer to the emulator structure for the fuel gauge emulator instance.
* @param cutoff Pointer to bool storing variable.
*
* @retval 0 If successful.
* @retval -ENOTSUP if not supported by emulator.
*/
static inline int emul_fuel_gauge_is_battery_cutoff(const struct emul *target, bool *cutoff)
{
const struct fuel_gauge_emul_driver_api *backend_api =
(const struct fuel_gauge_emul_driver_api *)target->backend_api;

if (backend_api->is_battery_cutoff == 0) {
return -ENOTSUP;
}
return backend_api->is_battery_cutoff(target, cutoff);
}

#ifdef __cplusplus
}
#endif
Expand Down

0 comments on commit 4df450a

Please sign in to comment.