LuxLibre

LuxLibre › Documentation › LuxLibre firmware

LuxLibre firmware

On this page: Features · Why C++17 · Architecture · Building · Flashing · The console · Parameters · Tests · More

One firmware for many flashlights. The same core code runs on an 8-bit ATtiny1616, a RISC-V CH32V003 that costs about $0.13 in quantity, an ESP32-C3, and on a PC, where a simulated flashlight (battery, LED, heat) runs the unit and behaviour tests.

Port MCU Status
host PC (Linux, macOS, Windows with WSL) simulator and tests; every change is tested here
attiny1616 Microchip ATtiny1616 builds and links (about 11.4 KB of 16 KB flash); not yet tested on hardware
ch32v003 WCH CH32V003 builds and links (about 11.8 KB of 15.75 KB flash); not yet tested on hardware
esp32c3 Espressif ESP32-C3 (FlashKa v0.5, v0.6 and v0.7) experimental: written for ESP-IDF 5.x and checked against its API, CI build configured (allowed to fail until it has passed once); not yet compiled with ESP-IDF or tested on hardware

Contents: features · why C++17 · architecture · building · flashing · the console · parameters · tests · more

Features

Why C++17

The firmware had to run smoothly on very small chips, give fine control over the hardware, and still be usable in ten years. The options:

Language Runs on 16 KB AVR and RV32E Same code on a PC simulator Fine hardware control Long-term support
C yes yes yes excellent, but no namespaces, no compile-time computation, macros everywhere
C++17 (embedded subset) yes yes yes excellent: ISO standard, GCC and Clang on every MCU family
Rust experimental for 8-bit AVR and RV32E yes yes very good on 32-bit chips (the FlashKa v0.5 firmware is Rust)
MicroPython, Arduino too large or too slow for 16 KB parts partly limited good for prototypes

LuxLibre uses C++17 with strict limits: no heap allocation, no exceptions, no run-time type information, no virtual functions (on AVR their tables would live in the 2 KB of RAM), no standard containers in the core. What it does use: namespaces, constexpr to compute lookup tables at compile time (levels, NTC tables), strong enums and static_assert checks of board settings. The result compiles to about 11-12 KB on both small MCUs and runs unchanged on the PC.

Architecture

flowchart LR
    board["boards/NAME/board.h<br/>pins, limits, defaults"] --> cfg["core/lx_board.h<br/>fills in defaults"]
    cfg --> core
    subgraph core["core/ (portable C++17)"]
        light["lx_light<br/>main loop"] --> input["lx_input<br/>gestures"]
        light --> ui["lx_ui<br/>states + binding table"]
        light --> thermal["lx_thermal"]
        light --> battery["lx_battery"]
        light --> aux["lx_aux"]
        light --> console["lx_console"]
        ui --> ramp["lx_ramp<br/>levels"]
        light --> storage["lx_storage"]
        params["lx_params<br/>+ lx_params_list.h"]
    end
    core --> hal["hal/lx_hal.h<br/>HAL interface"]
    hal --> ports["ports/: host (simulator), attiny1616, ch32v003, esp32c3"]

How the light decides its output

requested level (UI) -> min(thermal cap, low-voltage cap, cell-current cap, USB cap)
                     -> smoothing (SMOOTH_MS per level)
                     -> ramp::power_of_level()  (150 geometric levels between LX_OUT_MIN_Q16 and full scale)
                     -> hal::set_main(power)     (PWM duty, current set point, or both)

Storage layout

Block Contents Written when
Parameters “LX”, hash of the parameter list, values, CRC-16 save, factory reset
Calibration BAT_CAL, THM_OFFSET, CRC-16, in its own 64-byte page with every parameter save; survives the factory reset and a changed parameter list
State ring 8 slots: lockout, memory level, CRC-8 when the light goes off after a change (spreads the wear)

Building

You need make and a C++ compiler for the PC; the cross-compilers only for the MCUs you build.

# Ubuntu 24.04 (on Windows: install WSL with Ubuntu and use the same commands)
sudo apt install g++ make gcc-avr binutils-avr avr-libc gcc-riscv64-unknown-elf python3-matplotlib

cd firmware
make test                                   # unit and behaviour tests on the PC
make sim                                    # build/luxsim, the simulator
make docs                                   # regenerate parameters.md and user-interface.md
make tiny1616 BOARD=example-tiny1616-pwm    # build/tiny1616-example-tiny1616-pwm.hex
make ch32v003 BOARD=example-ch32v003-buck   # build/ch32v003-example-ch32v003-buck.bin

The ESP32-C3 port builds with ESP-IDF 5.x:

cd firmware/ports/esp32c3
idf.py set-target esp32c3
idf.py -DLX_BOARD=flashka-keychain-v05 build     # or flashka-keychain-v06, flashka-keychain-v07, edco-v01

Details and the current limitations of that port: ports/esp32c3/README.md.

Flashing

MCU Programmer Command
ATtiny1616 UPDI: a USB-serial adapter with a resistor (“serial UPDI”) or a dedicated UPDI programmer pymcuprog write -t uart -u /dev/ttyUSB0 -d attiny1616 -f build/tiny1616-<board>.hex --erase, and once per chip the brown-out fuse: make tiny1616-fuses UPDI_PORT=/dev/ttyUSB0
CH32V003 WCH-LinkE on the one-wire SWIO pin minichlink -w build/ch32v003-<board>.bin flash -b
ESP32-C3 USB (built-in USB serial/JTAG) idf.py -p <port> flash

Before the first flash of a new board, do the bring-up checks in testing.md: a current-limited supply and a check that every output stays off during reset (DRV-7).

The console

Boards with LX_HAS_CONSOLE (the FlashKa: over USB) accept text commands, one per line. Any serial terminal works.

Command Does
help list the commands
ver firmware version and board name
status state, output and requested level, estimated cell current, temperature, cell voltage, charge %, thermal and low-voltage caps
list all parameters with their values
get NAME one parameter
set NAME VALUE change a parameter (refused outside its range)
save store the parameters (and the calibration)
defaults parameters back to defaults, calibration kept (not saved until save)
defaults all the same, calibration included
level N / off switch the light to level N (1-150) or off; refused in lockout
reboot restart the MCU
wdtest watchdog test: moon on, then the firmware stops; the watchdog must restart the MCU (DRV-8)

An example session:

> ver
LuxLibre 0.5.0 / FlashKa Keychain v0.6
ok
> get THM_LIMIT
THM_LIMIT=50
ok
> set THM_LIMIT 47
THM_LIMIT=47
ok
> save
ok

The console stays awake for 30 s after the last input; while USB is connected the light does not sleep and the main LED is capped at CHG_MAXLVL (it is also a charger connection).

Parameters

Every user setting is one line in core/lx_params_list.h: name (at most 10 characters), default, range, unit and description. The list generates the C++ enum, the defaults table, the console names and parameters.md. Boards change defaults with LX_DEF_<NAME> in their header. Ranges keep every setting safe (FW-5): for example CHG_TMIN cannot go below 0 °C.

Tests

make test runs three programs:

CI runs these on every push, plus the ATtiny1616, CH32V003 and ESP32-C3 builds (the ESP32-C3 job may fail without failing the run until it has passed once) and a check that the generated documents are up to date (.github/workflows/firmware.yml).

More