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 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:
- The chip runs from the cell. Its I2C side wakes above 2.4 V and stops below 2.2 V. With no battery fitted it does not answer at all, and AO_Power treats it as an optional part that is absent.
- The shunt is R75, 10 mΩ, in series with the cell on the
VBAT_SHUNTnet, read through a 100 Ω filter on each sense pin. The sense pins are wired so that charging reads positive, as the datasheet defines it. The current agrees with the charger's within one of its 50 mA steps and has the same sign: the gauge read +511 mA while the charger reported 500 mA of charge, measured on a Kode Dot. - The thermistor pin is not connected. The temperature register reads
0x00, which is −40 °C. The cell's temperature comes from the charger. - The alarm pin
FG_GPOUTreaches neither the P4 nor the expander. The expander's P5 is the IMU'sINT1line. Nothing in the firmware reads a gauge interrupt, and none of the three alarm sources would give a useful signal without a profile.
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:
- It takes the gauge's voltage and subtracts the current times 180 mΩ (
KBSP_CELL_R_INT_MOHM) to estimate the open-circuit voltage. - It smooths that with a moving average of weight 1/5.
- 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
| Thing | Value |
|---|---|
| Address | 0x64. VERSION reads 0xA0 asleep or awake; kdrv_cw2217_init() refuses anything else |
| Firmware byte | FW_VERSION reads 0x00 asleep. Awake, bits 7:6 are 01 for a CW2217; a Kode Dot reads 0x4A |
| Supply | From the cell. Wakes above 2.4 V, stops below 2.2 V. 0.5 µA in shutdown, 12 to 16 µA active |
| Cell voltage | 14 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 |
| Current | Signed 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 |
| Sign | Charging positive, as the datasheet defines it. AO_Power uses the value unchanged |
| Start-up | 900 to 1100 ms after the second wake write before ready reads 1 |
| Temperature | Pin 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 |
| Bus | Up to 400 kHz. Reads of one or two bytes both work |
| Profile | 80 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 profile | SOC 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.
| Reg | Reset | Name and meaning |
|---|---|---|
0x00 | 0xA0 | VERSION: the chip family, the same asleep and awake |
0x02-0x03 | 0 | VCELL: 14 bits, 312.5 µV per step |
0x04-0x05 | 0 | SOC: whole percent in the high byte, 1/256 % in the low byte. A high byte above 100 means no profile |
0x06 | 0 | TEMP: −40 °C + code / 2, from the thermistor pin |
0x08 | 0xF0 | CONFIG: SLEEP [7:6], RESTART [5:4], [3:0] reserved and kept 0. 0x30 then 0x00 wakes the chip. 0xF0, 0x30, 0x00 restarts it |
0x0A | 0x40 | INT_CONF: alarm enables [6:4] (percentage, hot, cold) and their status bits [2:0], cleared by writing 0 |
0x0B | 0x14 | UPDATE_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 |
0x0C | 0xAA | TEMP_MAX: −40 °C + code / 2, default 45 °C |
0x0D | 0x50 | TEMP_MIN: −40 °C + code / 2, default 0 °C |
0x0E-0x0F | 0 | CURRENT: 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.
| Reg | Reset | Name and meaning |
|---|---|---|
0x10-0x5F | 0 | The battery profile, 80 bytes. From CellWise's reference driver; the datasheet does not describe it |
0xA0-0xA1 | 0 | T_HOST: host-reported temperature, half degrees in the high byte. The low byte must change on every write or the chip ignores it |
0xA2 | 0 | USER_CONF: a free byte |
0xA4-0xA5 | 0 | CYCLECNT: charge cycles in sixteenths; clears on shutdown |
0xA6 | 0 | SOH: health, 0 to 100 %; 100 on entering active mode; clears on shutdown |
0xA7 | none | Readiness: bits 2 and 3 both set once the algorithm runs. From the reference driver, not in the datasheet |
0xA8-0xA9 | 0 | AVE_CUR: signed 16 s average, same scale and sign as CURRENT |
0xAB | 0 | FW_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.
| Field | Reg | Unit and range | Access |
|---|---|---|---|
version | 0x00 | code, 0 to 255 | RO |
vcell_mv | 0x02-0x03 | mV, 0 to 5120 | RO |
soc_x256 | 0x04-0x05 | 1/256 %, 0 to 65535 | RO |
temp_c2 | 0x06 | half degrees, −80 to 175 | RO |
en_soc_int, en_tmx_int, en_tmn_int | 0x0A bits 6, 5, 4 | bool | RW |
int_flags | 0x0A bits 2:0 | code, 0 to 7; write 0 to clear | RW |
update_flag | 0x0B bit 7 | bool | RO |
soc_alert_pct | 0x0B bits 6:0 | %, 0 to 127 | RW |
temp_max_c2, temp_min_c2 | 0x0C, 0x0D | half degrees, −80 to 175 | RW |
current_ma | 0x0E-0x0F | mA, ±5240 at 10 mΩ, charging positive | RO |
t_host_c2 | 0xA0-0xA1 | half degrees, −80 to 175 | RW |
user_conf | 0xA2 | code, 0 to 255 | RW |
cycles_x16 | 0xA4-0xA5 | 1/16 cycle, 0 to 65535 | RO |
soh_pct | 0xA6 | %, 0 to 100 | RO |
ready | 0xA7 | bool, bits 2 and 3 both set | RO |
ave_current_ma | 0xA8-0xA9 | mA, same range as current_ma | RO |
fw_version | 0xAB | code, 0 to 255 | RO |
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
| Call | What 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
| Thing | Value |
|---|---|
| Owner | AO_Power, components/kaos/power/src/power_ao.c; claims KBSP_RES_GAUGE |
| Poll | First at 1500 ms, then every 2000 ms |
| Gauge voltage accepted | Above 2000 mV; otherwise the charger's reading stays |
| Percentage | Open-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 |
| Console | gauge field= value= reads or writes a field, gauge alone dumps, bat_info dumps both power chips |
| Self-test | test 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 kodeOS | sleep(), 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
components/kdrv/power: the driver,kdrv_cw2217.c, beside the charger's.kdrv_cw2217.h: the fields, the sequences and the profile calls.components/kaos/power: AO_Power, the owner, with the poll and the percentage.