Skip to content

Overview

The firmware is one Arduino-ESP32 binary that wakes up four FreeRTOS tasks, each owning a clearly-defined slice of the runtime. Nothing in here is exotic — the load-bearing decisions are heap budgets, mutex ownership, and a handful of sequencing rules. Get those right and the rest is widgets.

The runtime, in one picture

                    ┌────────────────────────────────────────┐
                    │              taskUI  (core 1)          │
                    │  lv_task_handler · widget tick · gesture│
                    │  owns g_lvglMutex                      │
                    └──┬──────────────┬────────────────┬─────┘
                       │              │                │
                reads  │       reads  │       reads    │ tick / draw
                       ▼              ▼                ▼
              ┌─────────────┐ ┌──────────────┐ ┌──────────────┐
              │ SignalStore │ │  ErrorStore  │ │  PageManager │
              │  portMUX    │ │  portMUX     │ │  LVGL trees  │
              └──────▲──────┘ └──────▲───────┘ └──────────────┘
                     │ writes        │ writes
     ┌───────────────┴────────────┐  │
     │                            │  │
┌──────┴─────────┐         ┌────────┴──┴────────┐         ┌──────────────────┐
│ taskCAN core 0 │         │   taskUSB  core 0   │         │   taskBLE core 0 │
│ TWAI → decode  │         │ JSON lines · 10 Hz  │         │ NimBLE GATT      │
│ ECU profiles   │         │ PUT_CONFIG · scan   │         │ telemetry mirror │
└────────┬───────┘         └─────────┬───────────┘         └────────┬─────────┘
       │                           │                              │
       ▼                           ▼                              ▼
   CAN bus                  Web Serial (tuner)                Mobile app

Tasks on core 0 handle I/O — CAN, USB, BLE. Core 1 is reserved for the UI loop so a stalled host link cannot cost a frame. Cross-task state lives in the two stores (SignalStore, ErrorStore) and is guarded by portMUX_TYPE — RMW under a critical section, no LOG / no ALLOC / no LOCK inside it.

Where to look next

Three rules that explain most of the source

Invariants worth memorising
Heap order
Anything contiguous > 16 KB must be reserved before lv_init. The post-init largest free block drops to ~13–18 KB on a no-PSRAM WROOM.
LVGL ownership
Any lv_* call from outside taskUI must hold g_lvglMutex. taskUI never needs to take it — it owns the lock by being the only writer.
Store critical sections
SignalStore and ErrorStore mutate under portMUX_TYPE. No LOG, no ALLOC, no LOCK inside portENTER_CRITICAL.

Reference shortcuts

  • Pinout — GPIO contract for the reference build.
  • Build flags — compile-time switches (panel, CAN baud, BLE on/off).
  • Signals — every signal the firmware knows about, with units and source.
  • Dev setup — PlatformIO env, clang-format gate, monorepo wiring.