LuxLibre

LuxLibre › Documentation › Firmware › Porting LuxLibre

Porting LuxLibre

Two kinds of port:

Either way, the result is only finished when the board has been measured and tested (FW-2, FW-8).


A new board

1. Copy the closest board

Start from When
boards/example-tiny1616-pwm/ ATtiny1616 switching the LED with PWM (FET drive or a linear regulator’s enable)
boards/example-ch32v003-buck/ CH32V003 with a regulated driver: PWM filtered into a current set point, an enable pin, an NTC
boards/flashka-keychain-v05/ ESP32-C3 with charger, RGB indicator, laser and console
boards/flashka-keychain-v06/ the same with several buttons on one pin (resistor ladder)
boards/flashka-keychain-v07/ the v0.6 pins and ladder with a stronger LED driver and high-drain cells; its header also describes the body, cells, LED flux and optic for the simulator (simulator.md)
boards/edco-v01/ the v0.7 power stage on a bare ESP32-C3FH4 chip with one digital button, an RGB indicator, no laser and no radio, in a round sealed tube; with its thermal model
cd firmware/boards
cp -r example-ch32v003-buck my-light

2. Fill in the header

core/lx_board.h includes your board.h and fills in a default for everything you leave out.

Identity and hardware present

Macro Meaning Default
LX_BOARD_NAME name printed by ver (required) none
LX_TICK_MS main loop period 10
LX_STORAGE_SIZE bytes of EEPROM or emulated EEPROM for settings (at least 256) 256
LX_HAS_CONSOLE text console (USB or UART) 0
LX_HAS_AUX_RGB indicator LED (RGB or single colour) 0
LX_HAS_LASER laser with hardware interlock 0
LX_HAS_SIDE side lights: a red and a UV LED (5 clicks from off; parameters SIDE_RED, SIDE_UV) 0
LX_HAS_COLOR colour beam: red, green and blue LEDs next to the white LED(s), behind the same optic (5 clicks from off on a board without side lights, 4 clicks from on switch white and colour, 2 clicks in the colour beam give the next colour; parameters COLOR_SET, COL_R, COL_G, COL_B) 0
LX_COLOR_FULL_MA, LX_COLOR_MAX_MA colour beam: each colour LED’s current at the sink’s full set point, and the most one colour LED may get (its rating, or less for heat) 1000, LX_COLOR_FULL_MA
LX_COLOR_SUM_MA colour beam: the most the three colour LEDs get together; a mix above it is scaled down (e.g. the limit of the converter feeding the sinks) 3 × LX_COLOR_MAX_MA
LX_COLOR_RAIL_MV, LX_COLOR_RAIL_EFF_PCT colour beam: the linear sinks hang off a regulated rail of this voltage, made by a converter of this efficiency; 0 = straight off the cell (the cell current is then the sum of the three LED currents). Used for BAT_MAX_MA and the charge estimate 0, 90
LX_HAS_CHARGER charger with status signals 0
LX_HAS_TEMP temperature sensor (NTC or the MCU’s own) 1
LX_BAT_CELLS cells in series 1
LX_BAT_OCV_MV the Li-ion cell’s own open-circuit curve for the charge estimate (BAT_CHEM 0): 11 values in mV per cell at 100, 90, … 0 %; for a cell whose curve differs from the built-in one (3.82 V at 50 %), e.g. a 3.6 V-nominal 14500 the built-in curve

Main channel and LED model (measure these, FW-2)

Macro Meaning How to get it
LX_OUT_MIN_Q16 output at level 1, as a fraction of full scale × 65535 the lowest stable output: for 10-bit PWM the smallest duty 1/1024 gives 64; for a current set point, 65535 × I_min / I_max (1 mA of 2.2 A gives 30) (LED-7)
LX_MAIN_MAX_MA LED current at level 150 (0 if unknown, e.g. FET drive); at most 80 % of the LED’s maximum current (LED-2) measure at level 150 with a full cell
LX_LED_VF0_MV, LX_LED_RS_MOHM LED voltage model V = VF0 + RS × I measure the hot LED’s voltage at two currents I1, I2: RS = (V2 - V1) / (I2 - I1), VF0 = V1 - RS × I1
LX_DRIVER_EFF_PCT driver efficiency for the cell-current estimate, 1-100 LED power / input power × 100 at a high level (for example 88); for a linear regulator use Vf at turbo / 4.2 V × 100, so that the turbo cell current is not underestimated

The LED model and the efficiency feed the cell current limit (BAT_MAX_MA, BAT-1) and the simulator. A wrong model gives a wrong limit, so measure.

Parameter defaults. Any parameter’s default can be set with LX_DEF_<NAME>, for example LX_DEF_THM_LIMIT, LX_DEF_TURBO_LVL, LX_DEF_BAT_MAX_MA, LX_DEF_UI_STYLE, LX_DEF_POCKET_S. The full list with ranges is in parameters.md. Set at least the thermal limit (from the measured sensor-to-surface difference, THM-1), the turbo level (below the full-scale current, which itself stays within 80 % of the LED’s maximum, LED-2), the cell current limit and the low-voltage thresholds for the chemistry (BAT-5).

3. Pins and port options

Each port reads its own macros; the defaults are those of the port’s example board.

Port Macros
ATtiny1616 LX_TINY_PWM_WO (TCA0 output: 0 = PB0 …), LX_TINY_BTN_PIN (PORTA pin, fully asynchronous pins wake from power-down), LX_TINY_TEMP_OFFSET_DC (internal sensor offset)
CH32V003 LX_CH32_BTN_PORT, LX_CH32_BTN_PORTSEL (EXTI code: 0 = PA, 2 = PC, 3 = PD), LX_CH32_BTN_PIN, LX_CH32_EN_PORT, LX_CH32_EN_PIN (driver enable), LX_CH32_NTC_ADC_CH (-1 = no NTC), LX_CH32_NTC_PORT, LX_CH32_NTC_PIN, LX_CH32_NTC_RFIX, LX_CH32_NTC_R25, LX_CH32_NTC_BETA, LX_CH32_VREFINT_MV
ESP32-C3 LX_ESP_GPIO_BTN (one button, low = pressed) or a resistor ladder (LX_ESP_BTN_LADDER_ADC_GPIO, LX_ESP_BTN_WAKE_GPIO, LX_ESP_BTN_MAIN_BELOW_MV, below), LX_ESP_GPIO_LED_PWM, LX_ESP_GPIO_LED_EN, LX_ESP_GPIO_LASER, LX_ESP_GPIO_RGB_R/G/B, LX_ESP_GPIO_SIDE_RED and LX_ESP_GPIO_SIDE_UV (with LX_HAS_SIDE; LX_ESP_SIDE_RED_ACTIVE_LOW / LX_ESP_SIDE_UV_ACTIVE_LOW = 1 for a switch that is on when the pin is low, e.g. a P-MOSFET on a strap pin), LX_ESP_GPIO_I2C_SDA and LX_ESP_GPIO_I2C_SCL (with LX_HAS_COLOR: an MCP4728 12-bit DAC at LX_ESP_COLOR_DAC_ADDR, default 0x60, sets the three colour sinks; its channels A, B, C = red, green, blue, code 4095 = LX_COLOR_FULL_MA; channel D is high while any colour LED is on, for the enable pin of the rail converter), LX_ESP_GPIO_CHG_STAT, LX_ESP_GPIO_CHG_DONE, LX_ESP_GPIO_CHG_SET, LX_ESP_LED_FULL_MA, LX_ESP_VBAT_DIV, LX_ESP_NTC_RFIX, LX_ESP_NTC_R25, LX_ESP_NTC_BETA

The CH32V003 main PWM is fixed on PD2 (TIM1 channel 1); the ATtiny1616 MCU measures its own supply as the cell voltage, so it must run directly from a single Li-ion cell.

Resistor-ladder buttons (ESP32-C3)

A resistor ladder puts several buttons on one pin: a pull-up resistor to the supply, the main button straight to ground and every other button to ground through its own resistor, so each press gives its own voltage. LuxLibre’s user interface uses one button (FW-3): on such a board the port reports “pressed” only for the main button and ignores the others. A board declares the ladder with three macros instead of LX_ESP_GPIO_BTN; the FlashKa Keychain v0.6 header (boards/flashka-keychain-v06/board.h) is the example.

Macro Meaning FlashKa v0.6
LX_ESP_BTN_LADDER_ADC_GPIO the pin that measures the ladder with ADC1 (GPIO0 to GPIO4 on the ESP32-C3) 3
LX_ESP_BTN_WAKE_GPIO a second pin on the same net, read as a digital input (low = some button); it wakes the chip from light sleep 9
LX_ESP_BTN_MAIN_BELOW_MV “pressed” = calibrated voltage below this, in mV 106

4. Simulate, build, measure

cd firmware
make sim BOARD=my-light                     # the simulator with your header
build/luxsim-my-light --preset 18650 --set TURBO_S=0 "2C W600000"
make ch32v003 BOARD=my-light                # or: make tiny1616 BOARD=my-light

Then follow testing.md: bring-up with a current-limited supply, default-off check, flashing, calibration (BAT_CAL, THM_OFFSET), the measured values above, the thermal test, and a test report. Put the measured values back into the header and cite the report in a comment.

5. Share it

A pull request with the board folder, a short README for the board (what it is, where the hardware files are, the test report) and an entry in CHANGELOG.md (CONTRIBUTING.md).


A new MCU

What a port contains

firmware/ports/<mcu>/
    hal_<mcu>.cpp     the HAL functions (hal/lx_hal.h)
    main.cpp          lx::light::init(); for (;;) { hal::wait_tick(); lx::light::tick(); }
    startup.S, link.ld, <mcu>_regs.h    for bare-metal ports (vector table, stack, registers)
    port.mk           make rules, included by firmware/Makefile (or the vendor's build files)
    README.md         status, toolchain, flashing, limitations

Register definitions can come from the vendor’s headers, or be generated from the vendor’s machine-readable description (the ATtiny1616 port generates tiny1x_io.h from Microchip’s ATDF file with atdf_to_header.py). Keep the port free of vendor frameworks where possible: it makes the build reproducible and the binary small.

The HAL contract

The core calls these functions from the main loop only (no re-entrancy). Units: mV, deci-°C, ms, and Q16 output power (0-65535).

Function Must do
init() first drive every output that can make light or heat to off (DRV-7); then clocks, tick timer, PWM, ADC, storage, watchdog
watchdog_kick() reset the watchdog; called every tick (DRV-8)
reboot() software reset
millis() monotonic milliseconds, also advanced over sleep
wait_tick() idle in a low-power mode until the next LX_TICK_MS tick
sleep(max_ms, wake_on_button) deepest sleep that still keeps RAM: wake on a button press and/or after max_ms (0 = no timer); outputs off; return true if the button woke it
button_down() raw switch state (the core debounces)
battery_mv() pack voltage in mV, averaged if the ADC is noisy
temperature() deci-°C; -32768 if the board has no sensor; return values outside -40…150 °C for an open or shorted sensor so that the core detects the fault
usb_present(), charger_status() USB power and charger state (CHG_NONE if not supported)
set_main(power) 0 = driver disabled (enable pin off); 1-65535 = fraction of full scale, as PWM at 3 kHz or more, a current set point, or both (LED-6)
set_aux(r, g, b), set_laser(on), set_side(red, uv), set_color(red_ma, green_ma, blue_ma) optional outputs; no-ops if absent. set_side gets a duty of 0-255 per colour, never both at once; set_color gets the current of each colour LED in mA (at most LX_COLOR_MAX_MA, 0 = off); both must also be off after init()
set_charge(enable, allowed) called every 100 ms; enable = charge now; allowed = the cell temperature is inside the charge window. While allowed is false, no current may reach the cell, not even to detect USB power (CHG-3)
storage_read(), storage_write() LX_STORAGE_SIZE bytes; skip unchanged bytes to save wear; return false if the write failed. A reset in the middle of a write is detected by the core’s checksums (it falls back to an older state slot or to defaults); flash emulation with pages should keep the core’s blocks in separate pages (the core places the calibration 64 bytes below the state ring for that reason)
console_getc(), console_write() optional text console; may drop output when no host is connected

Rules for port code

Bring-up of a new port

  1. Build and check the size (avr-size, riscv64-unknown-elf-size), the vector table and the start-up code in the disassembly (objdump -d).
  2. On a test board with a current-limited supply: outputs off during reset and with a blank MCU, then the tick (blink the indicator at 1 Hz from a test build), the button, the ADC readings.
  3. Run the gesture checklist and protections from testing.md, including wdtest if the board has a console, and measure the sleep current.
  4. Add the build to CI (.github/workflows/firmware.yml) and write the port README with its status.

The existing ports are the best examples: ports/README.md.