The post below is in Portuguese. We will translate critical announcements as the project gains an English-speaking audience.
ECU firmware running in CI — 100% testable without hardware
The safety-critical logic of the ECU firmware (trigger, fueling, ignition, knock, safety) is now a Rust no_std workspace that runs entirely under cargo test in CI. Zero hardware required to prove invariants.
The Proteus F7 hasn’t arrived yet (importing takes time), but that
didn’t have to stop what’s doable: practically all of the
safety-critical part of the firmware is pure math over tables and
states, and pure math can be proven with cargo test in CI.
Result: ecu-firmware/core became a no_std crate that composes the
ECU modules and runs 100+ unit + integration tests in CI on every
push, with no board needed.
What’s covered today
- 32-2 trigger decoder — Lost → Searching → Synced state machine, detects the wheel’s missing-tooth gap by the longest period between edges, syncs in 1-2 revolutions.
- 1D and 2D tables with bilinear interpolation — base of every map (VE, target AFR, advance, dwell, injector dead-time, ECT multiplier, knock threshold by RPM).
- Sensor conversions — NTC (ECT/IAT), MAP, TPS, Bosch LSU 4.9 lambda.
- PID controller — derivative-on-measurement + anti-windup (integral clamp). Used in AFR closed-loop and idle/boost.
- Speed-density fueling —
m_air = (MAP × V_cyl × VE) / (R × T_K), cold-start enrichment via ECT table, after-start linear decay, acceleration enrichment, injector dead-time compensated by Vbat. - Ignition — RPM × MAP advance with safety clamp, dwell calculated by Vbat with upper bound to avoid frying the coil.
- Knock detector — per-cylinder window with variable threshold by RPM band. Suggests spark advance retard on the next cycle.
- Safety state machine — over-rev and over-temp with hysteresis (warning → limp → cutoff), never returns from cutoff without a manual event.
- Cooperative watchdog — each subsystem (trigger, fueling, ignition, knock, safety) registers its own deadline. If any stops pinging, the supervisor detects it and trips fail-safe.
- CAN protocol — encode/decode of the 15 IDs in
docs/can-protocol.mdv0.1, with versioning embedded in byte 0.
Why this matters
Safety-critical work in solo dev with no embedded-senior code review is a recipe for disaster if you only discover bugs with the ECU in hand. Trigger decoder with a bug detects wrong sync → injector pulses off TDC → engine doesn’t start (or worse, fires the wrong cylinder and slams a piston into a valve). PID with broken anti-windup → permanent boost overshoot.
Each module is tested in CI against what’s supposed to happen (TDC aligned in the decoder, AFR converging in the PI, limp activating before cutoff). When the Proteus arrives, I won’t be debugging map logic on a bench with an oscilloscope — I’ll be validating the driver (which is purely hardware-side), not the algorithm.
CI
GitHub Actions workflow ecu-firmware.yml runs on every push under
ecu-firmware/: cargo fmt --check, cargo clippy -- -D warnings
(no warnings, no unwrap in production — clippy enforces),
cargo test. All on ubuntu-latest, no cross-compile yet — the
no_std core compiles on host for tests; cross to
thumbv7em-none-eabihf will come with the Proteus.
What’s missing
Drivers (Embassy STM32 HAL, USB CDC console, ADC, timers for injection / ignition scheduling), USB bootloader for flashing via Tuner, adapter harness for the original Delphi ECM. All of that depends on hardware in hand. The logic doesn’t.