Skip to content
Mohamed Rizwan
All projects

01 / Sep 2026/ Personal project

In progress

CAN 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
SHIFT ECUGEARBOX ECUD3 40ACKCAN 0x18F00500

ID 0x18F00500  DLC 8

  1. 0
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 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
Fig. 01Gear status frame, CAN ID 0x18F00500, 8 bytes, J1939-style

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

CAN high / CAN lowP · 0 km/hEngineShift ECUGearboxreceived · checksum OKBrakesDashboard

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

src/gear_state.cThe interlocks, straight out of the firmware. Every branch cites the requirement it implements, and the host test suite runs the same table.
/* 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;
}
src/can_protocol.cThe gear status frame: byte layout, the checksum over bytes 0 to 6, and the rolling counter that catches a repeated or dropped frame.
/* 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.cFreshness: a counter that does not advance by one is a gap, and 250 ms of silence raises a timeout fault instead of acting on stale data.
/* 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

CAN Vehicle Network Gear Controller characteristics
ParameterValue
Gear status CAN ID0x18F00500 (29-bit)
Frame length8 bytes
Rolling counter0 to 15, wraps
Broadcast period (requirement)20 ms +/- 2 ms
Shift response (requirement)within 50 ms
Bus-loss timeout250 ms to FAULT_TIMEOUT
Shift hysteresis>= 3.0 km/h