System architecture development for the RC safety system. Covering functional architecture, physical architecture, and software implementation with ESP32 and Arduino, including state machines and failsafe mechanisms.
Practice Project for Systems Engineering - Part 2
Part 1 was all whiteboards and sticky notes: we argued over requirements, carved a latency budget, and froze a mission that could be proven on a kitchen table. That was the left-hand descent of the V-model.
Part 2 begins the climb up the right-hand side. Here the abstractions acquire voltages, GPIO pins and FreeRTOS tasks. We will:
Pin every SMART requirement to a functional block.
You'll see a tight sense β estimate β decide β act pipeline that closes in 10 msβfast enough to intervene five times before the car has rolled a single wheel-diameter.Choose silicon and copper that can't mis-brake.
A 1 74HC157 mux makes sure the radio always has veto power if firmware dies.Split software into four clean layers.
The Application layer ships features; the Domain layer does the math; the Service layer adapts ports; HAL drivers and RTOS tasks sit at rock bottom. Every layer is unit-testable on a PC.Give the driver two eyes, not one.
A WildFire Nano AIO camera/VTx provides a 13-17 ms analogue feed for racing, while the ESP32 still pushes MJPEG over Wi-Fi for bench work. The two links share nothing but a battery, so congestion on one can't sink the other.
By the end of this part you'll have the full bill-of-materials, a layered codebase skeleton, and concrete verification hooksβall ready for the soldering iron. If Part 1 was the blueprint, Part 2 is the build manual. Let's zoom in.
DISCLAIMER: I haven't done extensive testing for the hardware pieces included in the project. Since this is an exercise, I "trusted" GPT o3 with the estimates for the latencies and such. If I were planning to carry on with this project, a whole verification and validation process would need to be done for those parts.
1 Functional Architecture - What the System Does
In classical control parlance this is a sense β estimate β decide β act pipeline. The feedback loop closes every 10 ms, giving the controller five fresh opportunities to intervene during the 25 ms it takes the car to travel 20 cm at 30 km hβ»ΒΉ.
| Block | Prime SMART reqs |
|---|---|
| Sensing | FR-4 |
| Estimation | FR-1a, FR-2 |
| Control | FR-1b, FR-2 |
| Telemetry | FR-3, FR-4 |
Anything that cannot be traced forward to a requirement,or backward to a verification step,gets chopped as scope-creep.
2 Physical Architecture - What the System Is
As with other parts of the series, selecting certain parts, such as the MCU, ESC, Servos, etc., must be carefully considered. Here, I'm "trusting" the LLM (GPT o3) to select for me, but if I were to build the system, I would refine this part in detail.
| Sub-assembly | Main parts & interfaces | Power domain |
|---|---|---|
| Chassis & drive | WLtoys 144001 buggy, HobbyWing QuicRun 16BL30 ESC, stock steering servo | Li-Po raw |
| Radio link | ELRS receiver (S.Bus 100 Hz) | 5 V BEC |
| Compute module | ESP32-S3-CAM dev-kit, on-board Wi-Fi/BLE, 8 MB PSRAM | 5 V buck |
| Safety-mux | 74HC157 quad 2-to-1 PWM mux + RC watchdog gate (GPIO feed from ESP32) | 5 V buck |
| Sensing frontend | β’ Hall sensor (rear-left wheel) β’ BMI270 IMU (SPI) |
3 V3 LDO |
| Vision frontend | OV2640 camera on ESP32 | 3 V3 |
| FPV link | Foxeer WildFire Nano VTx 25/200 mW + 600 TVL AIO cam (NTSC) | 5 V buck |
| Power chain | MP1584 buck (11 Vβ5 V) β AMS1117 (5 Vβ3 V3) | - |
| User I/O | RGB status LED, push-button mode select | 3 V3 |
| Test/Debug | SWO/UART header, 8-ch logic-analyser pin-out | 3 V3 |
Design intent
- Fail-silent hardware path. Radio PWM always has a physical lane to the ESC. The ESP32 takes over only while a watchdog-toggled GPIO keeps the mux selected; loss of toggling (<30 ms) reverts to radio.
- All life-critical loads share the main Li-Po railβif SmartDrive-XR dies, the driver regains stock control immediately.
- WildFire Nano gives a sub-10 ms FPV feed for racing; Wi-Fi MJPEG stays for bench work.
3 Software Architecture - Where Behaviour Lives in Time
3.1 Layered View
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application layer ("features") β
β β’ ABSController β’ FailsafeManager β’ LatencyProfiler β
β β’ FpvOsdManager β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Domain layer ("business logic") β
β β’ VehicleState β’ PwmShapeStrategy β’ ControlLaw (PID) β
β β’ StateEstimation β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Service layer ("ports & adapters") β
β β’ StateProvider β’ SpeedCommander β’ CsvLogger |
| β’ MjpegStreamer β’ FpvMonitor (RSSI) β’ **PwmMuxDriver** β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HAL + RTOS (drivers & tasks) β
β β’ imu_bmi270.c β’ hall_driver.c β’ esc_pwm_driver.c β
β β’ fpv_rssi_adc.c β’ pwm_mux_driver.c β
β β’ cam_dma.c β’ wifi_sta.c β’ rtos_task_mgr.c β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why this split?
- Application layer answers "What feature do I ship?"
- Domain layer answers "What maths / logic makes it correct?"
- Service layer answers "How do I talk to IO, network, storage?", using Port-and-Adapter (Hexagonal) pattern.
- HAL + RTOS hides silicon specifics, keeping everything above unit-testable on the host.
3.2 Core Design Patterns
| Pattern | Anchor module | Why it matters |
|---|---|---|
| Hexagonal | StateProvider, SpeedCommander, FpvMonitor |
Swap real drivers for mocks in CI |
| Strategy | PwmShapeStrategy |
Tune brake feel live over CLI |
| Command | BrakeCommand β driver queue |
Decouples 10 ms maths from Β΅s PWM |
| State Machine | FailsafeManager |
Formal RUN β DECEL β STOP β IDLE |
| Observer | event_bus.h |
Zero-copy pub-sub between tasks |
| DI (manual) | app_init() |
Tests inject mocks; prod wires HW |
3.3 RTOS Task Model (ESP-IDF v5 / FreeRTOS SMP)
| Task (core) | Period | Stack | Prio | Role |
|---|---|---|---|---|
sensor_task (1) |
10 ms | 4 kB | 15 | IMU + Hall β STATE_RAW |
estimator_task |
10 ms | 6 kB | 18 | Madgwick + Kalman + link-loss timer β VehicleState |
control_task |
10 ms | 4 kB | 20 | ABS PID (+ FailsafeManager) β BrakeCommand |
mux_feed_task (1) |
2 ms | 1 kB | 22 | Toggle GPIO watchdog for 74HC157 |
actuator_task |
10 ms | 3 kB | 21 | ESC PWM, safety heartbeat check |
cam_task (0) |
33 ms | 8 kB | 10 | DMA frame β PSRAM |
stream_task (0) |
asap | 8 kB | 12 | MJPEG over TCP |
fpv_monitor (0) |
100 ms | 2 kB | 11 | Sample VTx RSSI β EVENT_FPV_LOST |
osd_task (0) |
33 ms | 3 kB | 10 | Push speed/slip overlay to MAX7456 |
cli_task (0) |
event | 4 kB | 8 | UART shell & unit hooks |
Bench note: Added FPV tasks raise CPU utilisation to β 51 %/core, still well within head-room.
3.4 File-Tree & Build Targets
firmware/
βββ app/
β βββ abs_controller.c
β βββ failsafe_manager.c
β βββ latency_profiler.c
β βββ fpv_osd_manager.c
βββ domain/
β βββ vehicle_state.c
β βββ state_estimation.c
β βββ control_law_pid.c
β βββ pwm_shape_strategy.c
β βββ event_bus.c
βββ services/
β βββ state_provider/
β βββ speed_commander/
β βββ csv_logger.c
β βββ mjpeg_streamer.c
β βββ osd_bridge/
β βββ fpv_monitor.c
β βββ osd_bridge.c
βββ drivers/
β βββ imu_bmi270.c
β βββ hall_driver.c
β βββ esc_pwm_driver.c
β βββ fpv_rssi_adc.c
β βββ pwm_mux_driver.c/.h # GPIO toggle & self-test
βββ platform/
β βββ board_init.c
β βββ task_manager.c
βββ test/ # Unity + host_sim
βββ tools/ # log parser, CI
Build targets:
idf.py buildβ full firmwareidf.py build -DUNIT_TESTS=ONβ on-target Unity harnessmake host_simβ desktop binary with SDL graphing, for CIidf.py size-componentsβ memory budgeting gate
3.5 Configuration & Parameter Store
fpv:
mode: "analog" # analog | wifi | off
vtx_power: 25 # mW (toggle 25/200 via push-button)
osd: true
abs:
kp: 1.4
ki: 0.3
kd: 0.02
Compile-time pins via menuconfig; run-time tweaks via CLI (set fpv.mode wifi etc.) and persisted with Command + Memento.
3.6 Verification Hooks (Right Arm of the V)
| Artifact | Checks in CI / bench |
|---|---|
| Replay harness | Deterministic stop distance |
| Golden CSV | Brake 30 km hβ»ΒΉ β 0 β€ 1.0 m |
| Latency GPIO pair | Camera-to-Wi-Fi β€ 120 ms |
| FPV latency scope | IR-LED flash β goggles; 90 % < 15 ms |
| Static analysis | clang-tidy, radon < 10 CC |
| Mux watchdog scope | ill mux_feed_task; radio PWM path must be restored in < 30 ms. |
3.7 ABS Subsystem β from Raw Sensors to Brake Torque
Everything below can be dropped in as one self-contained section; it replaces the terse "Quick Tour" with a fully annotated, reader-friendly module description.
3.7.1 Data flow at a glance
3.7.2 Two nested control loops
| Loop | Period | Plant | Goal | Controller | Comment |
|---|---|---|---|---|---|
| Inner | 10 ms | rear wheel dynamics | ΞΌ (slip) β 0.15 | PID | |
| Outer | event-driven | vehicle state | safe states | 4-state machine | Guards against wind-up, link loss, zero-speed stall. |
State transitions:
RUN ββΊ DECEL (link lost OR driver pulls trigger)
DECEL ββΊ STOP (v_car < 0.3 m sβ»ΒΉ)
STOP ββΊ IDLE (brake_duty == 0 for 500 ms)
IDLE ββΊ RUN (driver reapplies throttle)
3.7.3 ESC as an electronic brake
No caliper, no servo; the ESC does it all.
| Pulse | Meaning (QuicRun "F/B" mode) |
|---|---|
| 1.70 ms | 100 % forward throttle |
| 1.50 ms | Neutral / coast |
| 1.30 ms | 100 % dynamic brake |
| < 1.30 ms | Reverse (disabled by clamping) |
Firmware constants:
#define ESC_NEUTRAL_US 1500
#define ESC_BRAKE_MAX_US 1300 // never go lower
SpeedCommander linearly interpolates between those limits; the pwm_mux_driver toggles its GPIO at 2 kHz. If the MCU crashes, the RC filter on the mux select pin discharges in β 25 ms and hardware flips back to the radio laneβmeeting FR-1b even with dead firmware.
3.7.4 Calibration checklist
- Set QuicRun to Forward/Brake (hold button 3 s, 1 LED flash).
- Radio end-point calibration: full-throttle β full-brake β neutral.
- Record neutral pulse with logic analyser β update
ESC_NEUTRAL_US. - Road test: log
ΞΌ_est, adjustPID.kpuntil damped (ΞΆ β 0.7).
3.7.5 Verification hooks
| Hook | What it proves |
|---|---|
| Logic-analyser on mux output | Pulse drops to 1.30 ms β€ 100 ms after Rx loss (FR-1b). |
| High-speed video + tape | 30 km hβ»ΒΉ β 0 in β€ 1.2 m (FR-2). |
| Replay harness | Deterministic stop distance on log playback. |
This richer ABS section now ties sensors, maths, hardware PWM and safety-mux into one coherent narrativeβreaders can replicate or audit every step.
3.8 The FPV Link: Seeing Through the Car's Eyes (and why it beats Wi-Fi in a race)
The WildFire Nano AIO module bundles sensor, NTSC encoder and 5.8 GHz VTx on one 4 g board. That saves you an extra ribbon cable and, more importantly, slices the latency pie:
| Segment | Typical Wi-Fi (ESP32 MJPEG) | WildFire analogue |
|---|---|---|
| Sensor β encode | 20 ms (JPEG) | 3 ms (direct CVBS) |
| PHY / air link | 50β80 ms (2.4 GHz) | 4β6 ms (5.8 GHz) |
| Decode β LCD | 15 ms (browser GPU) | 6β8 ms (goggle LCD) |
| Total | 75-120 ms | 13-17 ms |
Because the module is camera-internal, the ESP32 does nothing for the race-day video stream: zero CPU, zero DMA, zero risk. The ESP still provides a Wi-Fi MJPEG tap for pit-lane tuning; you simply choose which feed to watch.
Safety tie-in: fpv_monitor reads the WildFire RSSI pin through fpv_rssi_adc.c. If signal fades below -90 dBm for half a second the task raises EVENT_FPV_LOST, which the OSD blanks and the controller logs. The event can even trigger a gentle "pull over" profile if you like,just add a handler in FailsafeManager. Worst-case current draw at 200 mW is < 300 mA; a 470 Β΅F low-ESR cap right at the VTx pins stops brown-outs when the power amplifier keys up.
So you get laboratory-friendly Wi-Fi plus race-worthy analogue,each with its own latency-budget test hook,without burdening the microcontroller or poking extra holes in the ABS loop.
4 End-to-End Traceability - Kitchen-Table Proof
| Req | Functional blk | Physical element(s) | Software module(s) | Verification hook |
|---|---|---|---|---|
| FR-1a | Estimation | ELRS Rx PWM line + timer counter |
estimator_task (LinkLossTimer) |
Logic-analyser: > 120 ms gap raises EVENT_RX_LOST |
| FR-1b | Control | 74HC157 mux (sel GPIO) + QuicRun ESC | control_task, mux_feed_task |
Scope: pulse hits 1.30 ms β€ 100 ms after FR-1a |
| FR-2 | Control / Sense | Hall sensor + BMI270 IMU | estimator_task, PID |
Hi-speed video + pandas stop-distance script |
| FR-3 | Telemetry | OV2640 cam + ESP32 Wi-Fi | cam_task, stream_task |
LED-flash latency rig |
| FR-4 | Telemetry | SD-card logger | csv_logger |
pandas integrity check |
| FR-5 | Telemetry | WildFire Nano VTx | fpv_monitor, osd_task |
IR-scope: analogue latency |
| CO-1 | BOM | All parts | tools/bom.xlsx |
CI budget line |
5 Risks & Mitigations Snapshot
| Risk | Like. | Impact | Mitigation |
|---|---|---|---|
| Mux select line stuck HIGH (firmware bug) | Low | High | Power-on reset pulse; self-test toggles at 2 kHz |
| Wi-Fi congestion inflates MJPEG latency | Med | High | Auto channel scan + analogue fallback |
| Buck-converter noise corrupts IMU Z-gyro | Low | Med | Ξ -filter on 3 V3 rail + IMU ground-flood |
| Hall encoder misses pulses > 30 km hβ»ΒΉ | Med | Med | Oversample + de-bounce; add second magnet if track demands |
| VTx over-temp in enclosed shell | Med | Med | Therm-pad to chassis; auto power-back-off at 65 Β°C |
Β© 2026 Victor Retamal - Project Notes.