Skip to content
Snowbird Power Systems

Documentation

Register map

The I2C register map, version 1.2, at address 0x36, for anyone who would rather not use the library.

The bus

Every Snowbird board is a 7-bit I2C device at 0x36 (0x37 with the ADDR jumper closed), standard and fast mode, 3.3 V logic. Registers are addressed by a one-byte pointer; multi-byte registers are little-endian and auto-increment, so a sixteen-bit value is one write of the pointer followed by a two-byte read.

This is register map version 1.2. The Arduino, C and MicroPython libraries are generated from the same YAML file that produced this page, so the two cannot disagree.

Registers

Address Name Size Access Description
0x00 WHO_AM_I 1 R Always 0x5B
0x01 CAPS 1 R Bits 0–2 board id (1 Feather 1S, 2 Core 1S, 3 18650, 4 Pack, 5 Lite 1S); bits 3–4 cell count minus one; bit 5 BMS; bit 6 BLE; bit 7 balance
0x02 FW_VERSION 3 R Major, minor, patch
0x03 MAP_VERSION 2 R Major, minor: 1, 2
0x10 STATUS 1 R Bits 0–1 input source (0 none, 1 USB, 2 solar); bits 2–4 charge state (0 idle, 1 precharge, 2 CC, 3 CV, 4 done, 5 held); bit 5 PG; bit 6 5 V enabled; bit 7 BLE writes enabled
0x11 FAULTS 2 R Bitmask, the same bit order as faults() in the library
0x12 SOC 1 R State of charge, %
0x13 VBAT 2 R Pack voltage, mV
0x14 IBAT 2 R Signed current, mA, positive into the cell
0x15 TEMP 2 R Thermistor temperature, 0.01 °C signed; 0x7FFF if open
0x16 TTE 2 R Time to empty, minutes; 0xFFFF while charging
0x17 TTF 2 R Time to full, minutes; 0xFFFF while discharging
0x18 INPUT_SRC 1 R 0 none, 1 USB, 2 solar; bits 4–5 USB current limit (0 0.5 A, 1 1.5 A, 2 3 A)
0x19 PSOLAR 2 R Panel power, mW
0x1A EHARVEST 2 R Harvested today, mWh
0x1B VIN 2 R Input voltage, mV
0x1C TBOARD 2 R Supervisor temperature, 0.01 °C
0x20 CELL1 2 R Cell 1 voltage, mV (Pack)
0x21 CELL2 2 R Cell 2 voltage, mV (Pack)
0x22 CELL3 2 R Cell 3 voltage, mV (Pack)
0x23 CELL4 2 R Cell 4 voltage, mV (Pack)
0x24 IMBALANCE 2 R Highest minus lowest cell, mV (Pack)
0x30 CHEM 1 RW* 0 Li-ion, 1 LiPo, 2 LiHV, 3 LiFePO4, 4 LiSOCl2. Refused above the jumper's ceiling
0x31 CAPACITY 2 RW mAh
0x32 ICHG 2 RW Requested charge current, mA. Reads back the applied value
0x33 V5_EN 1 RW Bit 0 enable the 5 V rail. Reads back the combined state with EN5V
0x34 TEMP_LIMITS 4 RW Charge min, charge max, discharge min, discharge max, °C signed bytes
0x35 VOUT 2 RW* 3.3 V rail setting, mV, 1800 to 3300 where the jumper permits
0x36 SOC_LOW 1 RW Threshold for the SocLow event, %
0x37 ADV_INTERVAL 2 RW BLE advertising interval, ms, 1000 to 5000
0x38 NTC_BETA 2 RW Thermistor B value
0x40 EVENT_MASK 2 RW Which events pulse INT and notify over BLE
0x41 EVENT_FLAGS 2 R, clear on read Events since the last read
0x42 INT_CFG 1 RW Bit 0 latch INT low until EVENT_FLAGS is read
0x50 UNLOCK 1 W Write 0xA5 to open the 5 s window
0x51 CONFIRM 1 W Write 0x5A to commit a pending starred write
0x52 SHIP 1 W* Write 0x01 for ship mode, 0x02 for sleep
0x53 BLE_WRITES 1 RW Bit 0 enable BLE configuration writes
0x5F RESET 1 W Write 0xF0 for factory defaults

Registers marked RW* or W* take effect only inside an unlock-and-confirm sequence. Reading a starred register returns the committed value, not a pending one, until CONFIRM is written.

Unknown addresses read 0x00 and ignore writes. Registers marked Pack return 0xFFFF on other boards.

Unlock and confirm

Writes that can take down the host or overcharge a cell are guarded:

  1. Write 0xA5 to UNLOCK. A 5 s window opens.
  2. Write the new value to CHEM, VOUT or SHIP. It is applied provisionally (VOUT) or held pending (CHEM, SHIP).
  3. Write 0x5A to CONFIRM within the window. The value is committed.

If CONFIRM does not arrive, the window closes and the register reverts. A VOUT change that browns out the host therefore reverts on its own, because the host is no longer there to confirm it.

From a bare I2C master

Read the state of charge and the voltage with two transactions. On a Raspberry Pi:

from smbus2 import SMBus, i2c_msg

with SMBus(1) as bus:
    assert bus.read_byte_data(0x36, 0x00) == 0x5B
    soc = bus.read_byte_data(0x36, 0x12)
    lo, hi = bus.read_i2c_block_data(0x36, 0x13, 2)
    print(f"{soc} %  {lo | hi << 8} mV")

Set the chemistry to LiFePO4 and a 3000 mAh capacity:

    bus.write_byte_data(0x36, 0x50, 0xA5)           # unlock
    bus.write_byte_data(0x36, 0x30, 3)              # LiFePO4, pending
    bus.write_byte_data(0x36, 0x51, 0x5A)           # confirm
    bus.write_i2c_block_data(0x36, 0x31, [0xB8, 0x0B])   # 3000 mAh

From a shell with i2c-tools:

i2cget -y 1 0x36 0x12          # SOC
i2cget -y 1 0x36 0x13 w        # VBAT, little-endian word

A Linux host with the fuel gauge in the device tree is a snowbird driver in the power_supply class; the overlay is in the firmware repository and exposes capacity, voltage and status under /sys/class/power_supply/snowbird.

Versioning

MAP_VERSION is major.minor. A minor bump adds registers and never moves or resizes an existing one, so a library built for 1.1 reads a 1.2 board correctly and does not know about the new registers. A major bump may change layouts and the library refuses to talk to a board whose major differs from its own, with Err::MapMismatch.

The register map YAML, with every field's bit layout and the generated C header, is published with each firmware release; see Firmware updates.