Why primary mode exists
Lithium thionyl chloride cells are the ten-year battery. An ER14505 is AA sized, holds 2.4 Ah at 3.6 V, loses about one percent a year on the shelf, and works from minus fifty-five. Utility meters run on them for a decade. Nobody made a developer module that would take one, because the module would have a charger on it, and a charger and a primary cell in the same box is a liability.
The use case is real, though. A sensor on a decade cell still gets serviced; a technician plugs in USB, reads the state, updates the firmware. Snowbird's power path already runs the load from USB or solar without touching the battery. All primary mode has to do is make sure the battery is never touched at all.
Three blocks that have to fail
The chemistry jumper has a PRIMARY position. In it:
- Hardware. The jumper holds the charger's charge-enable pin inactive. No register write can override a pin.
- Firmware. The supervisor reads the jumper at boot and refuses
setChargeCurrent_mA()andsetChemistry()to anything rechargeable. The call returns an error naming the jumper. - BMS. The protector's overvoltage threshold is set just above the cell's open-circuit voltage, 3.7 V for a 3.6 V cell. Any current into the cell raises its voltage past the threshold and the protector opens the switch.
A charger bug, a firmware bug and a BMS misconfiguration all have to happen together before a primary cell sees charge current. We tested each layer with the other two disabled, on a battery simulator, and each one on its own stopped the current.
The dip
Pull an ER14505 out of a drawer after three years and put a load on it. The voltage drops, to 3.0 or 3.1 V on the cell we used, and climbs back to 3.6 V over ten to twenty seconds. That is passivation: a lithium chloride film grows on the lithium anode in storage and has to be broken down by current before the cell delivers properly.
To a fuel gauge, that dip is a nearly empty cell. The first time we powered a node with a shelf-aged cell, the gauge logged 12 % state of charge at boot, then counted up as the voltage recovered, which is not a thing a gauge should ever do.
The fix is in the supervisor's first-boot path. On the first power-up with a primary cell, it applies a 30 mA load for twenty seconds, watches the voltage recover, and only then initialises the gauge. The gauge sees a full cell and starts counting down from the capacity you set. pmic.depassivate() exposes the same step for a cell that has been swapped in the field. On a fresh cell from the factory the dip is a few tens of millivolts and the step is harmless.
The pulse problem
A bobbin cell is built for low current. The one we used is happy at 100 mA continuous and will sag hard at the 400 mA a LoRa transmit pulls for a second. Snowbird's 3.3 V regulator is a buck-boost and rides the cell down to 2.0 V, so the rail holds, but the cell does not like it and the gauge's current reading swings with each burst.
So the board carries a footprint on VBAT for a hybrid layer capacitor, the component the meter industry uses for exactly this. Fitted, the capacitor supplies the burst and the cell sees the average, under 100 mA. It is not populated by default, because most builds do not need it and it is a six-dollar part; the ten-year sensor build shows where it goes and what the cell voltage looks like with and without it.
Capacity is yours to set
A primary cell cannot be charged, so the gauge can only count out. It has to be told where it started. setCapacity_mAh(2400) is the number the time-to-empty is built on, and the state of charge is exactly as honest as that number and the cell's self-discharge, which at one percent a year is close enough to ignore.
Primary mode is on the Feather 1S, the Core 1S and the 18650 board. The chemistry page in the docs has the thresholds; the jumper is the only setting on the board that software cannot change, and that is the point.