SDK

CW2217 fuel gauge

The chip that measures a Kode Dot's battery voltage and current, how it is woken and read, and every register and driver call over it.

What it is

The CW2217 from CellWise measures the battery cell. It has a voltmeter for the cell, an ammeter that reads the voltage across a 10 mΩ resistor in series with the cell, and a thermistor input. On top of those sits an algorithm that turns the measurements into a charge percentage, a health figure and a count of charge cycles. Datasheet: CW2217EAAD-DS V1.0.

The algorithm needs an 80-byte description of the exact cell, the battery profile, which the chip maker supplies per cell model. No profile for the Kode Dot's cell exists, so none is loaded. The chip works as a voltmeter and an ammeter, and both are exact. The percentage, health, cycle count and average current registers carry no meaning. AO_Power computes the percentage itself from the cell's voltage curve.

How it is connected

flowchart LR
cell["1000 mAh cell"] -- "VBAT" --> cw["CW2217"]
cell -- "R75, 10 mOhm shunt" --> cw
p4["ESP32-P4<br/>AO_Power"] -- "I2C, 0x64" --> cw
cw -. "FG_GPOUT<br/>not routed" .-> p4
class cell chip
class cw chip
class p4 ao
The gauge on the Kode Dot

The CW2217 answers at 0x64 on the shared I2C bus (KBSP_I2C_ADDR_GAUGE). AO_Power owns it and claims it as KBSP_RES_GAUGE; nothing else writes to it. The charger, the BQ25896 (sdk/kdrv/bq25896), shares the same owner, because the two chips answer one question between them: the charger knows what the power path is doing, the gauge knows the cell.

The wiring

The wiring decides what the gauge can do:

Bring-up

kdrv_cw2217_init() probes 0x64 for 100 ms, attaches the device and checks that VERSION reads 0xA0. It writes nothing. The chip starts asleep, with CONFIG at 0xF0. AO_Power then calls kdrv_cw2217_wake(), which writes 0x30, waits 20 ms and writes 0x00, and returns at once.

The algorithm needs 900 to 1100 ms to come up, and until it does the chip answers FW_VERSION 0 and a cell voltage of 0 mV. Both look like readings. The driver does not wait, because an Active Object never waits. AO_Power polls the readiness byte on its own timer.

The poll and the percentage

AO_Power reads at 1.5 s after boot and then every 2 s (KODE_POWER_FIRST_MS, KODE_POWER_POLL_MS). Until ready reads 1 it uses the charger's voltage alone. After that the cell voltage is the gauge's, unless it reads 2000 mV or less, and the current comes from the gauge. Every poll publishes one POWER_EVT_SIG with the voltage, the current (into the cell positive, out negative) and the percentage.

The percentage is AO_Power's. It works in three steps:

  1. It takes the gauge's voltage and subtracts the current times 180 mΩ (KBSP_CELL_R_INT_MOHM) to estimate the open-circuit voltage.
  2. It smooths that with a moving average of weight 1/5.
  3. It reads the percentage off a 13-point curve in kbsp_cell.h.

The displayed value moves at most 2 points per reading. When the charger reports the charge as done, the value is 100.

The driver is one table

kdrv_cw2217.c describes every field as a row: register, kind, shift and width. kdrv_cw2217_get() and kdrv_cw2217_set() take a field and a value in its unit. A set refuses read-only rows and values out of range, does a masked read-modify-write so neighbouring bits keep their values, and reads the row back.

Four sequences are typed calls: wake, sleep, restart and loading a profile. The driver logs when the chip stops answering and when it returns, once each.

Specifications

The board

ThingValue
Address0x64. VERSION reads 0xA0 asleep or awake; kdrv_cw2217_init() refuses anything else
Firmware byteFW_VERSION reads 0x00 asleep. Awake, bits 7:6 are 01 for a CW2217; a Kode Dot reads 0x4A
SupplyFrom the cell. Wakes above 2.4 V, stops below 2.2 V. 0.5 µA in shutdown, 12 to 16 µA active
Cell voltage14 bits, 312.5 µV per step, 2.5 to 5.0 V. A conversion takes 5 ms. Accuracy ±7.5 mV at 25 °C and 4 V
CurrentSigned 16 bits, 52.4 mV full scale across the shunt. A conversion takes 10 ms. At 10 mΩ: 160 µA per step, ±5.24 A
SignCharging positive, as the datasheet defines it. AO_Power uses the value unchanged
Start-up900 to 1100 ms after the second wake write before ready reads 1
TemperaturePin not connected: temp_c2 reads −80, which is −40 °C. The profile on the chip would have to select a host-reported temperature to use one
BusUp to 400 kHz. Reads of one or two bytes both work
Profile80 bytes at 0x10 to 0x5F. None is loaded; the block reads all zero. A generic 4.2 V lithium-ion profile starts the algorithm and verifies byte for byte, but its percentage means nothing for this cell. The block lives in RAM that the cell powers
Percentage without a profileSOC reads 0xFEB3 and the health, cycle and average-current registers read 0

The measurement and control registers

The registers 0x00 to 0x0F: the readings, the configuration and the alarms. [7:6] is bits 7 and 6. A range such as 0x02-0x03 is two registers holding one number, high byte first.

RegResetName and meaning
0x000xA0VERSION: the chip family, the same asleep and awake
0x02-0x030VCELL: 14 bits, 312.5 µV per step
0x04-0x050SOC: whole percent in the high byte, 1/256 % in the low byte. A high byte above 100 means no profile
0x060TEMP: −40 °C + code / 2, from the thermistor pin
0x080xF0CONFIG: SLEEP [7:6], RESTART [5:4], [3:0] reserved and kept 0. 0x30 then 0x00 wakes the chip. 0xF0, 0x30, 0x00 restarts it
0x0A0x40INT_CONF: alarm enables [6:4] (percentage, hot, cold) and their status bits [2:0], cleared by writing 0
0x0B0x14UPDATE_FLAG [7] (a profile is loaded), percentage alarm threshold [6:0]. The alarm fires when the whole percent crosses the threshold; 101 to 127 fires on every change
0x0C0xAATEMP_MAX: −40 °C + code / 2, default 45 °C
0x0D0x50TEMP_MIN: −40 °C + code / 2, default 0 °C
0x0E-0x0F0CURRENT: signed, 52.4 mV × code / 32768 across the shunt

The profile and extended registers

The registers 0x10 to 0xAB: the battery profile, the host temperature and the figures that need a profile to mean anything.

RegResetName and meaning
0x10-0x5F0The battery profile, 80 bytes. From CellWise's reference driver; the datasheet does not describe it
0xA0-0xA10T_HOST: host-reported temperature, half degrees in the high byte. The low byte must change on every write or the chip ignores it
0xA20USER_CONF: a free byte
0xA4-0xA50CYCLECNT: charge cycles in sixteenths; clears on shutdown
0xA60SOH: health, 0 to 100 %; 100 on entering active mode; clears on shutdown
0xA7noneReadiness: bits 2 and 3 both set once the algorithm runs. From the reference driver, not in the datasheet
0xA8-0xA90AVE_CUR: signed 16 s average, same scale and sign as CURRENT
0xAB0FW_VERSION: 0x00 asleep; top two bits 01 for a CW2217, the rest the revision

The fields

The driver's table. The first column is the name the console's gauge command takes. RO is read-only.

FieldRegUnit and rangeAccess
version0x00code, 0 to 255RO
vcell_mv0x02-0x03mV, 0 to 5120RO
soc_x2560x04-0x051/256 %, 0 to 65535RO
temp_c20x06half degrees, −80 to 175RO
en_soc_int, en_tmx_int, en_tmn_int0x0A bits 6, 5, 4boolRW
int_flags0x0A bits 2:0code, 0 to 7; write 0 to clearRW
update_flag0x0B bit 7boolRO
soc_alert_pct0x0B bits 6:0%, 0 to 127RW
temp_max_c2, temp_min_c20x0C, 0x0Dhalf degrees, −80 to 175RW
current_ma0x0E-0x0FmA, ±5240 at 10 mΩ, charging positiveRO
t_host_c20xA0-0xA1half degrees, −80 to 175RW
user_conf0xA2code, 0 to 255RW
cycles_x160xA4-0xA51/16 cycle, 0 to 65535RO
soh_pct0xA6%, 0 to 100RO
ready0xA7bool, bits 2 and 3 both setRO
ave_current_ma0xA8-0xA9mA, same range as current_maRO
fw_version0xABcode, 0 to 255RO

The driver computes the current in 64-bit microamps as code × 52.4 mV / (32768 × KBSP_CELL_RSENSE_MOHM) and rounds to the milliamp, away from zero. The table's range comes from the same constant. A write to t_host_c2 sets the high byte, then a low byte that alternates between 0 and 1, and reads nothing back.

The driver

CallWhat it does
kdrv_cw2217_init()Probes 0x64, attaches the device, checks VERSION. Writes nothing. KODE_ERROR when nothing answers, logged as a warning
kdrv_cw2217_present()True once init succeeded
kdrv_cw2217_wake()0x30, 20 ms, 0x00. Returns at once; poll ready
kdrv_cw2217_sleep()0x30, 20 ms, 0xF0. Clears health and cycles; the profile and its flag stay
kdrv_cw2217_reset()The datasheet's restart, 0xF0, 0x30, 0x00, then waits up to 3 s for ready. KODE_TIMEOUT if it never comes
kdrv_cw2217_get(f, &v)One field in its unit. v is untouched on a failed read
kdrv_cw2217_set(f, v)One field, masked, read back. KODE_DENIED for a read-only row or a value out of range, KODE_ERROR when the bus fails or the chip did not keep it
kdrv_cw2217_field_info(f), _field_by_name()Name, unit and range of a field; what the console's gauge prints
kdrv_cw2217_profile_load(profile, pct)Sleep, write the 80 bytes, set UPDATE_FLAG together with the threshold pct in the same write, disable the interrupts, wake, wait up to 5 s for ready, read the 80 bytes back. Blocks 1 to 2 s. KODE_ERROR when a byte differs, KODE_TIMEOUT when the algorithm never starts
kdrv_cw2217_profile_read(out)The 80 bytes as the chip holds them
kdrv_cw2217_dump()Logs every field, the raw CONFIG byte and the profile as five lines of 16 bytes

The owner

ThingValue
OwnerAO_Power, components/kaos/power/src/power_ao.c; claims KBSP_RES_GAUGE
PollFirst at 1500 ms, then every 2000 ms
Gauge voltage acceptedAbove 2000 mV; otherwise the charger's reading stays
PercentageOpen-circuit voltage = cell voltage − current × 180 mΩ, moving average of weight 1/5, 13-point curve, at most 2 points per reading, 100 when the charge is done
Consolegauge field= value= reads or writes a field, gauge alone dumps, bat_info dumps both power chips
Self-testtest component=gauge: identity, ready, firmware bits 01, cell voltage between 3000 and 4280 mV, within 100 mV of the charger's. When the charger delivers at least 200 mA, the current must agree with it to within 50 mA plus a quarter of the charger's figure
Never called by kodeOSsleep(), reset() and profile_load(): they exist to load a profile, and none exists for this cell

Gotchas

Example code

Ask AO_Power to dump the gauge. Open the serial log and look for the kdrv.cw2217 lines: ready=1, the cell voltage in vcell_mv, and a profile of 80 zero bytes.

/*
 * gauge_dump.c - 2026-10-28 - from Kode with love
 *
 * Waits for the CW2217 fuel gauge on the Kode Dot to come up, then asks
 * AO_Power, the chip's owner, to log every field. With a battery fitted, the
 * serial log shows one line of fields (vcell_mv, current_ma, ready and the
 * rest) and the five lines of the 80-byte battery profile.
 */
#include "kode_ao.h"
 
/* Bring the system up, give the gauge time to start, then ask for the dump. */
void app_main(void)
{
    kode_init();
 
    /* The gauge needs about a second after its wake before it means anything. */
    kode_delay_ms(3000);
    power_field_request(POWER_CHIP_GAUGE, POWER_OP_DUMP, 0U, 0);
 
    while (1) {
        kode_delay_ms(1000);
    }
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK