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:
- Write 0xA5 to UNLOCK. A 5 s window opens.
- Write the new value to CHEM, VOUT or SHIP. It is applied provisionally (VOUT) or held pending (CHEM, SHIP).
- 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.