All work

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
CAN_H CAN_L 120Ω 120Ω Door module locks · windows Lighting indicators · lamps Dashboard cluster · telltales Gateway diagnostics arbitration 0 0 1 1 0 1 0 0 1 0 1 0 0 1 1 0 1 1 0 0 1 1 lower ID holds the bus loser backs off, retries, loses nothing
Four modules, one differential pair, 120 ohm at each end. Arbitration is decided bit by bit.

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.

Door lock moduleID 0x0C2 · central locking, crash unlock
00011000010
Wiper controllerID 0x1A4 · intermittent + auto rain
00110100100
Interior lightingID 0x212 · courtesy lamps, footwell
01000010010

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.

IDFrameProducerPeriodWhy there
0x0C2Central lock / crash unlockDoor moduleeventOccupant egress after impact - nothing outranks it
0x1A4Wiper mode + speedWiper ECU50 msVisibility; driver notices latency immediately
0x212Interior lamp stateLighting ECU100 msComfort - a late frame is invisible to the occupant
0x2F0Window positionWindow ECUeventAnti-pinch handled locally, so the bus frame is advisory
0x3A0Node heartbeatall200 msLowest priority by design; loss is detected, not prevented
Anti-pinch detection on the window motor is deliberately not a bus transaction. A trapped-object stall has to reverse the motor in single-digit milliseconds, and anything that depends on winning arbitration first cannot promise that. It runs in the local node's interrupt handler; the bus frame that follows is only telling the rest of the car what already happened.

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.

task / 10 ms

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.

task / 20 ms

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.

interrupt + task

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.

task / 50 ms

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.