# BQ25896 charger

The chip that charges a Kode Dot, picks its power source, boosts 5 V for the header and switches it off: every register, and the driver over them.

## What it is

The **BQ25896** from Texas Instruments decides where a Kode Dot's power comes
from and what happens to the battery. It does four jobs:

1. It charges the cell from USB-C.
2. It feeds the board from the cable, the battery, or both, and keeps that
   supply, `SYS`, at a usable voltage.
3. It runs backwards: from the cell it makes 5 V on its input line, `VBUS`,
   which reaches the expansion header. This is the boost, also called OTG.
4. It holds the switch between the battery and the board, the **BATFET**.
   Opening it is the deepest off the hardware has, **ship mode**: the chip
   then draws 12 µA from the cell and nothing else runs.

It accepts 3.9 to 14 V in and charges at up to 3 A. Datasheet: TI SLUSC76C.

## How it is connected

```mermaid
flowchart LR
  subgraph dot["On the Kode Dot"]
    ao["AO_Power<br/>on the ESP32-P4"] -- "I2C, 0x6B" --> bq["BQ25896"]
    bot["bottom button"] -- "QON" --> bq
    bq -- "SYS" --> board["the board"]
    bq -- "BAT, through the BATFET" --> cell["1000 mAh cell"]
    bq -- "VBUS, boost" --> hdr["expansion header"]
  end
  usb["USB-C cable"] -- "VBUS" --> bq
  class usb ext
  class ao ao
  class bq chip
  class cell chip
```

*The charger on the Kode Dot*

The chip answers at `0x6B` on the shared I2C bus (`KBSP_I2C_ADDR_CHARGER`).
**AO_Power** owns it: nothing else in the system writes to it. Software
reaches it through AO_Power's requests, `kode_power_charge()`,
`kode_power_charge_limit()` and `kode_power_boost()`, and AO_Power publishes
a reading every 2 s. Ship mode has one door, the console verb `ship_mode`;
the facade has no call for it. The calls `kode_power_sleep()` and
`kode_power_off()` rest the Kode Dot through AO_LowPower and never reach the
charger (`sdk/kaos/lowpower`).

### The wiring

The wiring decides more than the firmware does:

- **The cable decides charging and boosting.** A circuit that senses USB
  power drives the charge-enable pin `CE` and, through transistor Q5, the
  boost pin `OTG`. With a cable in, charging is allowed and the boost is
  physically impossible. On battery alone, the reverse. The P4 drives neither
  pin.
- **The input limit is a resistor.** R58, 470 Ω on the `ILIM` pin, sets the
  input current ceiling. A USB chip on the board drives `PSEL`, which tells
  the BQ25896 whether a computer or a wall adapter is plugged in.
- **The temperature comes from the pack.** R65 (5.23 kΩ) and R74 (30.1 kΩ)
  form the divider on the `TS` pin, with the pack's 10 kΩ thermistor on pin 2
  of J7, the battery connector. This is the only temperature the charger sees.
- **The bottom button is `QON`.** Pressing it wakes a Kode Dot in ship mode;
  holding it 16 to 23 s with no cable power-cycles the whole board.
- **`PG`, `STAT` and `INT` go nowhere the firmware reads.** AO_Power polls
  the status over I2C instead.

### The boot policy

At every boot AO_Power writes one policy, each line read back, from the
cell's numbers in `kbsp_cell.h`. The chip keeps its registers when the P4 is
reflashed, so the policy is written every time, never assumed:

| Field | Written | Why |
| --- | --- | --- |
| `watchdog` | 0, off | The chip's watchdog otherwise resets the charge current after 40 s |
| `conv_rate` | 0 | The ADC runs one conversion per poll, not continuously (see Gotchas) |
| `en_hiz` | 0 | The input is used |
| `chg_config` | 1 | Charging is allowed |
| `otg_config` | 0 | The boost is off until an app asks for it |
| `boost_lim` | code 0, 0.5 A | 0.5 A out draws about 0.9 A from a cell at 3.0 V; the chip's 1.4 A default would exceed the pack's limit |
| `bhot` | code 1, about 55 °C | The pack's own limit, below the chip's 60 °C default |
| `vreg_mv` | 4192 | Charge voltage, 8 mV under the pack's nominal |
| `ichg_ma` | 512 | Fast charge at 0.5 C for the 1000 mAh pack |
| `iprechg_ma`, `iterm_ma` | 64 | The chip's floor |

When the Kode Dot rests, AO_Power writes `conv_rate` back to 0 on every round, in
case a bench tool left the ADC continuous. While it sleeps, the minute's
reading needs no conversion: the status registers and the gauge are read as
they are, so a cable that appeared is seen fresh (`sdk/kaos/power`).

The pack takes charge between 10 and 45 °C, a narrower window than the
chip's. AO_Power reads the temperature fault at every poll and holds charging
off while it reads anything but normal. A boost request is refused below
3600 mV (`KBSP_CELL_BOOST_MIN_MV`), and a running boost is stopped below
3300 mV (`KBSP_CELL_BOOST_STOP_MV`) or on a boost fault.

### The driver is one table

`kdrv_bq25896.c` describes every field of REG00 to REG14 as a row: register,
shift, width, offset, step, the codes it accepts, and whether it is
read-write, read-only or self-clearing. `kdrv_bq25896_get()` and
`kdrv_bq25896_set()` take a field and a value in its unit (mV, mA, °C, a
code).

A `set` does three things:

1. It rounds to the field's step and refuses a value out of range.
2. It does a masked read-modify-write, so the other fields of the register keep their bits.
3. It reads the register back.

A self-clearing bit is written and not checked, since it returns to 0 by itself. The driver holds no policy at all: what to write is AO_Power's decision. Three
things are typed calls because a table cannot express them: the ADC block, the
fault register and ship mode.

## Specifications

### The board

| Thing | Value |
| --- | --- |
| Address | `0x6B`. REG14 bits 5:3 read `000`, the BQ25896's part number; `kdrv_bq25896_init()` refuses any other |
| Input current ceiling | 850 mA measured with the input limit in force. R58 should give about 755 mA; **TBD**: why the two differ |
| Boost output | 5.0038 V measured from the battery, 4.998 V programmed. Starts in 28 ms. 267 mV of ripple at no load in PFM, gone with `pfm_otg_dis` |
| Charge profile | 512 mA fast, 64 mA pre-charge, 4.192 V, ends below 64 mA |
| Charge current an app may ask for | 64 to 896 mA (`KBSP_CELL_ICHG_MAX_MA`) |
| `TS` at room temperature | 56 to 59 % of `REGN` with a pack fitted, about 28 °C. About 85 % with no pack |
| Ship mode | 12 µA from the cell. Leaves on USB power, the bottom button held 0.9 to 1.3 s, or a register reset |

### The settings registers

The registers `0x00` to `0x0A`: settings. `[7]` is
bit 7, `[5:0]` bits 5 to 0. Reset values are the datasheet's, after a real
power-on; a reflash of the P4 does not reset them.

| Reg | Reset | Fields |
| --- | --- | --- |
| `0x00` | `0x08` | `EN_HIZ` [7], `EN_ILIM` [6], `IINLIM` [5:0] = 100 mA + 50 mA per code |
| `0x01` | `0x06` | `BHOT` [7:6], `BCOLD` [5], `VINDPM_OS` [4:0] = 100 mV per code |
| `0x02` | `0x11` | `CONV_START` [7], `CONV_RATE` [6], `BOOST_FREQ` [5], `ICO_EN` [4], `FORCE_DPDM` [1], `AUTO_DPDM_EN` [0] |
| `0x03` | `0x1A` | `BAT_LOADEN` [7], `WD_RST` [6], `OTG_CONFIG` [5], `CHG_CONFIG` [4], `SYS_MIN` [3:1] = 3.0 V + 100 mV per code, `MIN_VBAT_SEL` [0] |
| `0x04` | `0x20` | `EN_PUMPX` [7], `ICHG` [6:0] = 64 mA per code, 3008 mA at most |
| `0x05` | `0x13` | `IPRECHG` [7:4], `ITERM` [3:0], each 64 mA + 64 mA per code |
| `0x06` | `0x5E` | `VREG` [7:2] = 3.840 V + 16 mV per code, 4.608 V at most; `BATLOWV` [1]; `VRECHG` [0] |
| `0x07` | `0x9D` | `EN_TERM` [7], `STAT_DIS` [6], `WATCHDOG` [5:4], `EN_TIMER` [3], `CHG_TIMER` [2:1], `JEITA_ISET` [0] |
| `0x08` | `0x03` | `BAT_COMP` [7:5] = 20 mΩ per code, `VCLAMP` [4:2] = 32 mV per code, `TREG` [1:0] = 60 °C + 20 °C per code |
| `0x09` | `0x44` | `FORCE_ICO` [7], `TMR2X_EN` [6], `BATFET_DIS` [5], `JEITA_VSET` [4], `BATFET_DLY` [3], `BATFET_RST_EN` [2], `PUMPX_UP` [1], `PUMPX_DN` [0] |
| `0x0A` | `0x73` | `BOOSTV` [7:4] = 4.55 V + 64 mV per code, `PFM_OTG_DIS` [3], `BOOST_LIM` [2:0] |

### The status and reading registers

The registers `0x0B` to `0x14`: status, faults, thresholds, the ADC results and the part number.

| Reg | Reset | Fields |
| --- | --- | --- |
| `0x0B` | read only | `VBUS_STAT` [7:5]: 0 none, 1 USB host, 2 adapter, 7 boost. `CHRG_STAT` [4:3]: 0 off, 1 pre-charge, 2 fast, 3 done. `PG_STAT` [2], `VSYS_STAT` [0] |
| `0x0C` | read only, latching | `WATCHDOG_FAULT` [7], `BOOST_FAULT` [6], `CHRG_FAULT` [5:4] (1 input, 2 thermal shutdown, 3 timer), `BAT_FAULT` [3], `NTC_FAULT` [2:0] (2 warm, 3 cool, 5 cold, 6 hot) |
| `0x0D` | `0x12` | `FORCE_VINDPM` [7], `VINDPM` [6:0] = 2.6 V + 100 mV per code, 3.9 V at least |
| `0x0E` | measured | `THERM_STAT` [7], `BATV` [6:0] = 2.304 V + 20 mV per code |
| `0x0F` | measured | `SYSV` [6:0], same scale |
| `0x10` | measured | `TSPCT` [6:0] = 21 % + 0.465 % of `REGN` per code |
| `0x11` | measured | `VBUS_GD` [7], `VBUSV` [6:0] = 2.6 V + 100 mV per code |
| `0x12` | measured | `ICHGR` [6:0] = 50 mA per code |
| `0x13` | read only | `VDPM_STAT` [7], `IDPM_STAT` [6], `IDPM_LIM` [5:0] = 100 mA + 50 mA per code |
| `0x14` | `0x02` | `REG_RST` [7], `ICO_OPTIMIZED` [6], `PN` [5:3], `TS_PROFILE` [2], `DEV_REV` [1:0] |

### The codes

What the code of each coded field means.

| Field | Codes |
| --- | --- |
| `BHOT` | 0 = about 60 °C, 1 = 55 °C, 2 = 65 °C, 3 = off |
| `BCOLD` | 0 = about −10 °C, 1 = −20 °C |
| `WATCHDOG` | 0 off, 1 = 40 s, 2 = 80 s, 3 = 160 s |
| `CHG_TIMER` | 0 = 5 h, 1 = 8 h, 2 = 12 h, 3 = 20 h |
| `BOOST_LIM` | 0 to 6 = 0.5, 0.75, 1.2, 1.4, 1.65, 1.875, 2.15 A |
| `MIN_VBAT_SEL` | 0 = 2.9 V, 1 = 2.5 V |
| `BATLOWV` | 0 = 2.8 V, 1 = 3.0 V |
| `VRECHG` | 0 = 100 mV, 1 = 200 mV |

### The driver

| Call | What it does |
| --- | --- |
| `kdrv_bq25896_init()` | Probes `0x6B`, attaches the device to the bus, checks the part number. Writes nothing |
| `kdrv_bq25896_get(f, &v)` | One field in its unit. `v` is untouched on a failed read, so a bus error never becomes a number |
| `kdrv_bq25896_set(f, v)` | One field, masked, read back. `KODE_DENIED` for a read-only field or a value out of range, `KODE_ERROR` when the bus fails or the chip did not keep it |
| `kdrv_bq25896_field_info(f)`, `_field_by_name()` | Name, unit, range and step of a field; what the console's `pmic` prints |
| `kdrv_bq25896_adc_read()` | REG0E to REG12 in one transaction: one conversion, all five numbers |
| `kdrv_bq25896_faults(&latched, &live)` | REG0C read twice: the first read is the history since the last read, the second is now |
| `kdrv_bq25896_ship_mode(delayed)` | One write of REG09 with `BATFET_DIS` set and `BATFET_RST_EN` kept. On battery it never returns |
| `kdrv_bq25896_dump()` | Logs every register in hex with each field decoded |

## Gotchas

> **WARNING**
>
> **With no battery, every charging test fails.** Without the pack's
> thermistor, the divider holds `TS` at about 85 %, past the top of the ADC's
> range. The chip reads "cold" and allows neither charging nor the boost. Fit a
> pack before you judge a board.

> **WARNING**
>
> **A reflash leaves the chip as the last firmware set it.** Only a real
> power-on resets the registers. That includes the ADC: until a conversion
> runs, REG0E to REG12 read the bottom of their scales, and 2.304 V of battery
> looks exactly like a flat cell. Start a conversion (`conv_start`) and wait
> about 1.5 s before you trust a reading, which is what AO_Power does at boot.

> **WARNING**
>
> **The charge current falls back to 2048 mA after about 35 s.** The chip's
> watchdog starts with the first I2C write. When it expires, the chip resets
> `ICHG` with most other registers, and the board then charges at its 850 mA
> ceiling. Turn the watchdog off (`watchdog` = 0), as AO_Power does first, or
> kick it more often than every 40 s.

> **NOTE**
>
> **`otg_config` set does not mean the boost runs.** With a cable in, Q5 holds
> the `OTG` pin low and the bit does nothing; no register shows the pin. Read
> `vbus_stat`: 7 means the boost is running.

## Example code

Ask AO_Power to charge the cell at 256 mA instead of the 512 mA it writes at
boot. With the app running, type `pmic field="ichg_ma"` on the console: the
answer reads 256.

```c
/*
 * charge_limit.c - 2026-10-28 - from Kode with love
 *
 * Asks AO_Power, the owner of the Kode Dot's BQ25896, to charge the cell at
 * 256 mA, then keeps the app alive so the console stays up. Type
 * pmic field="ichg_ma" on the console and the value reads 256.
 */
#include "kode.h"

/* Bring the system up, ask for the new current, then idle. */
void app_main(void)
{
    kode_init();
    kode_power_charge_limit(256);

    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`](https://github.com/kodediy/kode-sdk/tree/main/components/kdrv/power): the driver, `kdrv_bq25896.c`, beside the gauge's.
- [`kdrv_bq25896.h`](https://github.com/kodediy/kode-sdk/blob/main/components/kdrv/power/include/kdrv_bq25896.h): every field, the ADC and fault structures, the calls.
- [`components/kaos/power`](https://github.com/kodediy/kode-sdk/tree/main/components/kaos/power): AO_Power, the owner, with the boot policy.
