One library, every board
The Arduino library (Snowbird Power in the Library Manager, snowbird-power on PlatformIO), the C library and the MicroPython package (snowbird) are generated from the same register map, so they expose the same calls with the same semantics. The examples below are Arduino; the MicroPython names are the same in snake_case.
On begin() the library reads the capability register and learns which board it is on. A call the board cannot honour returns Err::NotOnThisBoard rather than silently doing nothing. cellVoltage_mV(2) on a Snowbird Feather 1S is the usual way to meet it.
Setup
#include <Snowbird.h>
Snowbird pmic;
pmic.begin(Wire); // default address 0x36
pmic.begin(Wire, 0x37); // the address jumper moves it to 0x37
begin() returns false if no board answers. It does not change any setting.
Capabilities
| Call | Returns |
|---|---|
pmic.boardId() |
Board::Feather1S, Core1S, Cell18650, Pack, Lite1S |
pmic.cells() |
1, 2, 3 or 4 |
pmic.hasBMS() |
true on every board except the Snowbird Lite 1S |
pmic.hasBLE() |
true on every board except the Snowbird Lite 1S |
pmic.hasBalance() |
true on the Snowbird Pack |
pmic.firmwareVersion() |
semantic version string |
pmic.registerMapVersion() |
1.2 at this release |
Configuration
Settings are stored on the board and survive a host reflash and a power cycle.
setChemistry(Chem)
Chem::LiIon, Chem::LiPo, Chem::LiHV, Chem::LiFePO4, Chem::LiSOCl2. Sets the charge voltage, the cutoff and the BMS thresholds together. Refused with Err::AboveCeiling if the requested chemistry's voltage is above the chemistry jumper's ceiling. The jumper rule is in Chemistries.
setCapacity_mAh(uint16_t)
The cell or pack capacity. Drives the default charge current and the coulomb counter. Required for LiFePO4 and LiSOCl2, where the gauge cannot infer it from the voltage curve.
setChargeCurrent_mA(uint16_t)
Returns the value actually applied, which is the lowest of:
| Limit | Value |
|---|---|
| Request | what you asked for, rounded down to 10 mA |
| Chemistry ceiling | 1 C of the configured capacity |
| Hardware ceiling | 3 A |
| Input | what the source offers: 0.5, 1.5 or 3 A on USB-C, or the panel's MPPT point on solar |
Default without a call: 0.5 C. On a LiSOCl2 board the call returns 0 and sets Err::PrimaryCell.
set5V(bool)
Enables the 5 V rail, up to 2 A. Off by default. The EN5V pin, when pulled low, overrides this to off. is5VEnabled() reads back the combined state.
setTempLimits(TempLimits)
pmic.setTempLimits(TempLimits::charge(0, 45).discharge(-20, 60));
Narrower than the chemistry's default is accepted. Wider is refused with Err::AboveCeiling. The hardware layer enforces the ceilings whatever this says; see Safety.
setOutputVoltage(uint16_t mV, Confirm)
The 3.3 V rail can be set between 1.8 V and 3.3 V on boards whose voltage jumper permits it. Because the host usually runs from this rail, the call needs the unlock-and-confirm sequence:
pmic.unlock();
pmic.setOutputVoltage(1800, Snowbird::CONFIRM_PENDING); // applied provisionally
// ... host confirms it is still running ...
pmic.confirm(); // within 5 s, or the rail reverts
Telemetry
| Call | Returns | Notes |
|---|---|---|
soc() |
0 to 100 % | ModelGauge on Li-ion, LiPo, LiHV; coulomb counting on LiFePO4 and LiSOCl2 |
voltage_mV() |
pack voltage | |
current_mA() |
signed; positive into the cell | 1 mA resolution |
timeToEmpty() |
minutes, or UINT16_MAX while charging |
|
timeToFull() |
minutes, or UINT16_MAX while discharging |
|
cellVoltage_mV(n) |
one cell, n = 1 to 4 | Pack only |
imbalance_mV() |
highest minus lowest cell | Pack only |
batteryTemp_C() |
from the thermistor | NAN if the thermistor is open |
boardTemp_C() |
the supervisor's own sensor | |
inputSource() |
Input::None, USB, Solar |
|
inputCurrentLimit_mA() |
500, 1500 or 3000 on USB | |
solarPower_mW() |
panel power at the MPPT point | |
harvestedToday_mWh() |
since local midnight, or since boot if no clock was set | |
chargeState() |
Charge::Idle, Precharge, CC, CV, Done, Held |
Faults and events
faults() returns a bitmask that is set until the condition clears:
Fault f = pmic.faults();
if (f & Fault::OverVoltage) ...
// OverVoltage, UnderVoltage, OverCurrent, ShortCircuit, OverTemp, UnderTemp,
// NtcOpen, NtcShort, BmsTripped, InputOverVoltage, ReversedBattery, CellCountMismatch
Events are edge-triggered and arrive on the INT pin:
void onPower(Event e) { ... }
pmic.onEvent(Event::ChargeDone | Event::PowerLost | Event::TempHold | Event::BmsTrip, onPower);
// in loop(), or from the INT ISR:
pmic.poll();
The event set: ChargeDone, ChargeStart, PowerLost, PowerRestored, TempHold, TempCutoff, BmsTrip, SocLow (threshold set by setSocLowThreshold()), SolarDay, SolarNight. Reading events() clears the flags.
Power states
pmic.shipMode(); // opens the battery switch; draws 3 µA; wakes on USB or solar
pmic.sleep(); // outputs off, supervisor reporting; wakes on INT or I2C
shipMode() is the last thing a sketch should do before a board goes in a box. It needs the unlock sequence, because it also takes down the host.
MicroPython
from machine import I2C, Pin
from snowbird import Snowbird, Chem, Event
pmic = Snowbird(I2C(0, sda=Pin(4), scl=Pin(5)))
pmic.set_chemistry(Chem.LIFEPO4)
pmic.set_capacity_mah(3000)
def on_event(e):
print("event", e)
pmic.on_event(Event.CHARGE_DONE | Event.POWER_LOST, on_event)
print(pmic.soc(), pmic.voltage_mv(), pmic.time_to_empty())
print(pmic.cells(), pmic.has_bms(), pmic.has_ble())
pmic.poll() dispatches events; call it from the main loop or from a Pin.irq handler on the INT pin.
Errors
Every setter returns Err::Ok or one of:
| Error | Meaning |
|---|---|
Err::NotOnThisBoard |
The board does not have the hardware for this call |
Err::AboveCeiling |
The value exceeds a jumper or chemistry limit |
Err::PrimaryCell |
Charging is not possible on a LiSOCl2 board |
Err::Locked |
The call needs unlock() first |
Err::Timeout |
The board did not answer; check the bus |
pmic.lastError() returns the most recent one and pmic.errorString(err) a short description, for serial logs.
Without the library
The board is an ordinary I2C device at 0x36. The register map documents every register, so a bare I2C master in any language can read SOC and voltage with two transactions.