All projects
01 / Sep 2026/ Personal project
In progressCAN Vehicle Network Gear Controller
A transmission controller must never shift into an unsafe gear or act on a stale or corrupted CAN frame.
- Role
- Sole engineer: requirements, architecture, firmware, tests and CI.
- Stack
- Embedded C, STM32 Cortex-M4, FreeRTOS, CAN, Unity
- Context
- Personal project
ID 0x18F00500 DLC 8
- 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- B0
- Gear P, R, N, D1 to D8
- B1-2
- Road speed km/h x10, little-endian
- B3
- Throttle percent
- B4
- Fault flags invalid request, timeout
- B5
- Reserved 0x00
- B6
- Rolling counter 0 to 15
- B7
- Checksum over bytes 0-6
Interactive
Drive it yourself
No engineering background needed. Try to shift into reverse at speed.
Gear
P
Speed
0km/h
engine 800 rpm
How the car's controllers talk
With CAN: every controller shares the same two wires. Each message carries an ID, a counter and a checksum, so a damaged or missing message is caught instead of trusted.
What the controller did
You're in Park. Press the brake, then pull the lever to D to drive.
Message on the bus0x18F00500
00
gear
00
spd
00
spd
14
thr
00
flt
00
--
00
cnt
EB
sum
checksum OK
01
What I built
- 01A two-node gear-state controller for a simulated multi-speed transmission. Node A runs the shift scheduler and broadcasts gear status; node B acts as the transmission ECU, validates requests, applies interlocks and reports the actual gear.
- 02Safety interlocks written as requirements first: brake required to leave PARK, REVERSE blocked above 5.0 km/h, PARK blocked above 2.0 km/h, one forward gear per request.
- 03All decision logic (gear state machine, shift scheduler, CAN protocol) is pure C behind a thin HAL, so it runs and is tested on a normal PC.
02
Hardware
- 012x STM32 Nucleo-F446RE (ARM Cortex-M4)
- 022x CAN transceivers, 120 ohm termination at each end
- 038-channel USB logic analyzer on CAN_TX / CAN_RX
- 04ST-Link SWD for flashing and debug
03
Software
- 01Embedded C, FreeRTOS tasks for control, CAN comms and fault monitoring
- 02J1939-style frame encode/decode with rolling counter and checksum
- 03Unity + CMake/CTest host unit tests
- 04GitHub Actions: tests, cppcheck, ARM cross-compile
04
Validation
- 01Every software requirement carries an ID (SWR-xxx) referenced in the name of the test that verifies it, with a traceability matrix.
- 02Timing requirements SWR-030 and SWR-031 are verified on the bench with the logic analyzer; those captures are in progress.
05
Results
- 01Control logic verified by host-run unit tests on every change.
- 02Frames with a bad checksum are rejected; a counter gap is flagged as a dropped frame.
06
The code
/* SWR-002..SWR-007 */
bool gear_transition_allowed(const gear_ctx_t *ctx, gear_t requested)
{
if (ctx == 0) return false;
if (requested >= GEAR_COUNT) return false;
if (requested == ctx->current) return true;
/* SWR-005: neutral is always reachable. */
if (requested == GEAR_NEUTRAL) return true;
/* SWR-004: parking pawl protection. */
if (requested == GEAR_PARK) {
return ctx->speed_kph_x10 <= SPEED_PARK_LIMIT_X10;
}
/* SWR-003: reverse only at very low speed. */
if (requested == GEAR_REVERSE) {
if (ctx->speed_kph_x10 > SPEED_REVERSE_LIMIT_X10) return false;
/* SWR-002: leaving PARK needs the brake. */
if (ctx->current == GEAR_PARK && !ctx->brake_applied) return false;
return true;
}
if (gear_is_forward(requested)) {
/* SWR-002: leaving PARK needs the brake. */
if (ctx->current == GEAR_PARK) {
if (!ctx->brake_applied) return false;
return requested == GEAR_D1; /* pull away in first */
}
/* From reverse or neutral, engage first gear only. */
if (ctx->current == GEAR_REVERSE || ctx->current == GEAR_NEUTRAL) {
return requested == GEAR_D1;
}
/* SWR-007: single-step shifts between forward gears. */
int delta = (int)requested - (int)ctx->current;
return (delta == 1) || (delta == -1);
}
return false;
}/* Byte layout (SWR-020):
0 gear
1-2 speed km/h x10, little endian
3 throttle percent
4 faults
5 reserved (0)
6 rolling counter, low nibble (SWR-021)
7 checksum over bytes 0..6 (SWR-022)
*/
uint8_t can_checksum(const uint8_t *data, size_t len)
{
uint8_t sum = 0u;
if (data == 0) return 0u;
for (size_t i = 0u; i < len; ++i) {
sum = (uint8_t)(sum + data[i]);
}
return (uint8_t)(0xFFu - sum);
}
can_result_t can_encode_gear_status(const gear_status_t *in, uint8_t buf[CAN_DLC])
{
if (in == 0 || buf == 0) return CAN_ERR_NULL;
buf[0] = (uint8_t)in->gear;
buf[1] = (uint8_t)(in->speed_kph_x10 & 0xFFu);
buf[2] = (uint8_t)((in->speed_kph_x10 >> 8) & 0xFFu);
buf[3] = in->throttle_pct;
buf[4] = in->faults;
buf[5] = 0u;
buf[6] = (uint8_t)(in->counter & CAN_COUNTER_MAX);
buf[7] = can_checksum(buf, 7u);
return CAN_OK;
}/* src/can_protocol.c - a frame is only trusted if it is fresh and intact */
/* SWR-024: counter must advance by exactly one, modulo 16. */
if (ctx->have_last) {
uint8_t expected = (uint8_t)((ctx->last_counter + 1u) & CAN_COUNTER_MAX);
if (out->counter != expected) {
ctx->last_counter = out->counter;
return CAN_ERR_COUNTER_GAP;
}
}
/* SWR-032 */
void can_rx_tick(can_rx_ctx_t *ctx, uint32_t now_ms)
{
if (ctx == 0) return;
if (!ctx->have_last) return;
if ((now_ms - ctx->last_rx_ms) >= CAN_TIMEOUT_MS) {
ctx->faults |= (uint8_t)FAULT_TIMEOUT;
}
}07
Characteristics
| Parameter | Value |
|---|---|
| Gear status CAN ID | 0x18F00500 (29-bit) |
| Frame length | 8 bytes |
| Rolling counter | 0 to 15, wraps |
| Broadcast period (requirement) | 20 ms +/- 2 ms |
| Shift response (requirement) | within 50 ms |
| Bus-loss timeout | 250 ms to FAULT_TIMEOUT |
| Shift hysteresis | >= 3.0 km/h |