2024 · Vector India · Embedded C / RTOS
Body control module
Four independent controllers, one shared pair of wires, and a hard requirement that the safety message always goes first. A distributed real-time system small enough to reason about completely.
- Year
- 2024
- Context
- Vector India programme
- Language
- Embedded C / C++
- Bus
- CAN 2.0A, 500 kbit/s
- Nodes
- 4 + heartbeat
01What it had to do
A body control module is the node in a car that owns everything which is not powertrain: lighting, wipers, power windows, central locking, courtesy lamps. The awkward part is not any single one of those functions - each is a small state machine. It is that they all share one wire, they all have deadlines, and some of them matter far more than others.
A door-unlock request after a crash detection cannot wait behind a courtesy-lamp fade. So the design problem is arbitration and scheduling, not features: how do several independent controllers share a single bus without a central scheduler, and still guarantee that the important message goes first?
- Four cooperating nodes on one CAN segment, each owning a subsystem and each free to transmit whenever the bus is idle.
- Hard priority ordering that survives simultaneous transmission, without a bus master and without a scheduling table.
- Deterministic behaviour under concurrent load - the worst case must be reasoned about, not measured and hoped for.
- Graceful degradation: a node that drops off the bus must not take the others with it.
02How CAN arbitration actually resolves it
CAN solves this in the physical layer rather than in software, and it is the most elegant
thing I met during this project. The bus is wired-AND: a 0 is
dominant and a 1 is recessive, so if any node
drives 0 while others drive 1, the bus reads 0.
Every node that wants to talk starts transmitting its identifier at the same time, most significant bit first, and reads the bus back while it writes. The moment a node writes a recessive 1 and reads a dominant 0, it knows someone with a lower identifier is also talking - so it stops immediately and becomes a receiver. No collision, no retransmit, no lost data. The winner never even knows there was a contest.
The consequence for design is that the identifier is the priority. Assigning message IDs stops being bookkeeping and becomes the real-time schedule.
Three nodes, one bus, one winner
All three start transmitting on the same idle bus. Watch where each one drops out.
Blue marks a dominant 0 held on the bus. Red marks the bit where a node wrote 1, read 0, and withdrew. Lighting loses at bit 2, wipers at bit 3, and the door lock transmits its frame untouched - which is exactly the ordering the vehicle needs.
03Message map
Because the identifier is the priority, the ID map is the first thing designed and the last thing changed. Lower number wins, so safety-relevant frames were allocated the bottom of the range and comfort functions the top.
| ID | Frame | Producer | Period | Why there |
|---|---|---|---|---|
0x0C2 | Central lock / crash unlock | Door module | event | Occupant egress after impact - nothing outranks it |
0x1A4 | Wiper mode + speed | Wiper ECU | 50 ms | Visibility; driver notices latency immediately |
0x212 | Interior lamp state | Lighting ECU | 100 ms | Comfort - a late frame is invisible to the occupant |
0x2F0 | Window position | Window ECU | event | Anti-pinch handled locally, so the bus frame is advisory |
0x3A0 | Node heartbeat | all | 200 ms | Lowest priority by design; loss is detected, not prevented |
04Control logic
Each subsystem is an explicit finite state machine in Embedded C, driven from an RTOS task at a fixed period. Nothing blocks, nothing sleeps inside a transition, and every state is entered from exactly one place, which is what makes the behaviour reviewable.
Wiper state machine
Off, intermittent, low, high, and a one-shot wipe. The intermittent delay is a counter reloaded on entry rather than a timer callback, so the mode can change mid-interval without leaking a pending event.
Lighting
Courtesy lamp fade, footwell, and door-ajar handling. Fades are computed from a tick counter so a missed frame produces a shorter fade rather than a stuck lamp.
Power window
Stall current is sampled in the ISR and reverses the motor locally. The task only publishes the resulting position, so bus latency can never sit inside the safety path.
Central locking
Arbitrates key fob, interior switch and crash input against each other, with crash unlock latching until it is explicitly cleared.
05What went wrong, and what it taught me
- Priority inversion via the ID map. The first allocation gave the heartbeat frame a low ID because it was written first. Under load the heartbeats crowded out real traffic. The fix was not code - it was renumbering, which is exactly the point: on CAN, the schedule lives in the identifiers.
- A node that lost arbitration every cycle. A periodic frame kept colliding with a higher-priority periodic frame at the same phase, so it starved. Offsetting the transmit phase by a few milliseconds fixed it - a scheduling problem wearing a bus-problem costume.
- Debugging concurrency without a debugger you can trust. Halting on a breakpoint stops one node while the bus and the other nodes carry on, so the state you stop in is not the state that failed. Most real progress came from bus tracing and from counters that could be read without pausing anything.
- Fault handling is a design input, not a phase. Bus-off recovery, missing heartbeats and stuck inputs had to be in the state machines from the beginning; retrofitting them would have meant redrawing every diagram.
This is the project that made distributed systems concrete for me before I ever wrote a line of network code. Arbitration, priority, starvation, partial failure and the difficulty of observing a running system without disturbing it are all the same problems I now deal with in Linux servers - only here they were happening on a pair of twisted wires, at a scale small enough to hold entirely in your head.