An event driven kernel in machine code for the MSP430FR5969, with its own assembler, a cycle counting simulator and a charge balance taken from the data sheet.
Everything here is built from scratch: the assembler, the simulator and the kernel itself. No vendor toolchain, no SDK. Whatever can be checked against an independent implementation is checked against one.
The target is a temperature logger that samples once an hour, keeps five years of readings in FRAM and is read out over the serial port once a month. It spends 99.99 percent of its life asleep, which makes the idle current the only number that really matters.
| Part | Scope | State |
|---|---|---|
| Assembler | 3 instruction formats, all addressing modes, 20 emulated instructions | 38 of 38 against mspdebug |
| Simulator | cycle counting, Timer_A0, RTC_B, LPM3.5 with reset, ADC12, eUSCI, FRAM cache | 13 of 13 against mspdebug |
| Energy model | data sheet SLAS704G, no estimates | complete |
| Kernel | 2224 bytes, tickless, ADC driver, serial interface | runs in the simulator |
| Hardware | MSP-EXP430FR5969 LaunchPad | not yet measured |
sh run-all-checks.shEleven stages, all green. sh check-status.sh prints one line per stage if
you only want to know what passes.
Requirements: python3, plus mspdebug and msp430mcu for the two cross
checks against an independent implementation.
| mean current | 0.257 µA |
| of which idle | 5508 of 5556 µAs, or 99.1 percent |
| storage reach | 44032 samples, 1835 days, 5.02 years |
| CR2032, calculated | 97 years |
| CR2032, in practice | 10 to 15 years, limited by the cell |
The last two lines are the point. A CR2032 loses about one percent of its 220 mAh per year to self discharge, which works out at 251 nA. The circuit draws about as much as the cell loses on its own, so the cell gives up long before the electronics do. Below roughly a quarter of a microamp, saving current stops buying runtime.
LFXTDRIVE in CSCTL4 sets the drive level of the 32 kHz crystal
oscillator, and it resets to level 3, the most expensive one. Waking from
LPM3.5 is a reset, so the field comes back at level 3 every single time.
| Level | Load capacitance | Oscillator | LPM3.5 total |
|---|---|---|---|
| 0 | 3.7 pF | 180 nA | 250 nA |
| 1 | 6.0 pF | 185 nA | 255 nA |
| 2 | 9.0 pF | 225 nA | 295 nA |
| 3 | 12.5 pF | 330 nA | 400 nA |
Of the 250 nA the data sheet quotes for LPM3.5, 180 nA is this oscillator alone. The LaunchPad schematic gives Y4 as a 7.0 pF part, which needs level 1 at most. The kernel never wrote the field, so it ran at level 3 and paid 145 nA for nothing, more than a third of the whole budget.
The kernel now finds the lowest level the crystal tolerates at cold start,
records it in FRAM and applies it on every start, with a short check against
a note that no longer holds. check-crystal.py measures the result:
0.402 µA down to 0.257 µA.
| Kernel design | sleep paths, tickless timing, calling convention, why LPM3.5 ends in a reset |
| Energy | the model, the crystal, what optimisation gained and where it stopped |
| Serial readout | the button window, the commands, the full dump |
| Storage | one byte per sample, position as timestamp, gap marking |
| Hardware | power supply, jumpers, what the board adds |
| Bugs found | six real defects, none of which showed up in normal operation |
| Part survey | why this chip and not one of the twenty alternatives |
| File | Contents |
|---|---|
kernel.s |
the kernel |
assembler.py |
assembler, emits machine code and TI-Text |
simulator.py |
cycle counting simulator with peripherals and charge balance |
energy.py |
energy model from the data sheet |
decode.py |
reads the sample store, converts raw values to degrees |
benchmark.py |
kernel in the simulator, forecasts, comparisons |
parts.py |
part comparison for this workload |
storage.py |
reach and resolution of the sample store |
battery.py, radioisotope.py |
power source comparisons |
addresses.py |
memory map, read from the assembler symbol table |
profiler.py, profiler-detail.py |
cycles per routine and per instruction |
trace.py |
single steps through the kernel |
check-*.py |
the checks; run-all-checks.sh runs the eleven that gate |
check-status.sh runs only the eleven stages of the suite. The other
check-*.py files are diagnostic tools that print what happened rather than
pass or fail, and they are not part of the gate.
Verification on real hardware. The simulator works from data sheet
figures. Whether reality follows is a question for EnergyTrace on the actual
board. Three assumptions are worth checking first: the button on P4.5, the
baud rate modulation value 0x92, and the crystal startup time.
Board leakage. The supercapacitor sits in its own power domain and is disconnected from the factory, the debugger can be isolated at J13, and the target current is measurable at J9. What remains in the target domain is the chip, the crystal, two LEDs on driven pins and two buttons. That should be clean, but it has not been measured.
Clock source at run time. The crystal runs continuously, even when only short waits are pending, where the VLO would be cheaper. Switching between them founders on the crystal startup time of several hundred milliseconds.
MIT, see LICENSE.