CAN 2.0B, CAN-FD, ISO-TP segmentation, and UDS diagnostic services — the complete
automotive communication stack. Weeks 1–5 build deep CAN peripheral knowledge on STM32.
Weeks 6–9 add ISO-TP transport and UDS flashing services, culminating in Capstone 4:
a CAN-based UDS firmware update node wired directly to the Phase 3 bootloader.
Bosch CAN specification 2.0B (free PDF)The original spec. Read Part A (standard frame) and Part B (extended frame) in full during Week 1. Everything else references this.
CSS Electronics CAN bus guide (free, kvaser.com)The clearest free introduction to CAN bus. Read before the Bosch spec to build mental model first.
AN3407 — STM32 bxCAN filter configuration (free, st.com)Every STM32F4/F1/L4 CAN filter question is answered here. Read before Week 3.
Vector UDS introduction (free, vector.com/knowhow)The most accessible free UDS overview. Read at the start of Week 6.
Automotive Embedded Systems Handbook — Navet & Simonot-Lion (~€60)CAN bus chapters provide the systems-level context: scheduling, bus load, error confinement. Optional but valuable.
python-can (github.com/hardbyte/python-can) and python-udsoncan (github.com/pylessard/python-udsoncan)Your PC-side test tools throughout this phase. Install both before Week 1.
// additional hardware for this phase
STM32G431RB or STM32G474RE Nucleo — already from Phase 3FDCAN peripheral required for Week 4. If you used STM32F4 in Phases 1–2, now is the time to switch.
Second STM32 Nucleo board — any family (~€12–18)Acts as the second CAN node. You need two nodes to form a bus.
Canable USB CAN adapter (~€15–20)Connects to your PC. Works out of the box with python-can on Linux as a socketcan interface.
SN65HVD230 CAN transceivers ×2 (~€2 each)3.3V, ISO 11898 compliant. One per STM32 node. Do not connect CAN TX/RX pins directly without a transceiver.
120Ω termination resistors ×2One at each end of the bus. Without proper termination you will see signal reflections and random error frames.
// daily schedule template (mon–fri)
08:00 – 09:30 Spec reading — Bosch CAN spec or Vector UDS guideCAN and UDS are spec-driven. Reading the actual spec is not optional.
09:30 – 13:00 STM32 implementation — peripheral config, driver code, frame handlingEvery session produces a runnable artefact. No throwaway code.
14:00 – 16:00 python-can / python-udsoncan — PC-side test scriptsWrite a Python test for every STM32 feature you implement. This is your integration test suite.
16:00 – 17:30 Oscilloscope / logic analyser analysis — verify bit timing, frame structure, error statesNever trust that it works without seeing it on a probe. CAN misbehaviour is invisible without hardware tools.
Saturday Full bus test: both nodes + PC adapter. Verify all frames flow correctly at both standard and FD data rates.
CSS Electronics CAN bus guide — complete (kvaser.com)Read this first. Builds the mental model before the Bosch spec adds precision.
Bosch CAN spec 2.0B — Part A (standard frame) and Part B (extended frame)Free PDF. Read every field description. Draw the frame structure on paper.
CAN frame structure — standard (11-bit ID)
SOF — 1 dominant bit, synchronises all nodes to frame start
Arbitration — 11-bit ID + RTR bit. Lower ID = higher priority. Arbitration is non-destructive: losing node backs off and retries.This is the key insight: CAN bus priority is purely determined by the message ID.
Control — IDE (0 = standard frame), r0 (reserved), DLC (0–8 bytes of data)
Data — 0–8 bytes payload
CRC — 15-bit polynomial + delimiter
ACK — any receiver drives dominant to acknowledge. No ACK = error frame.If your node transmits and sees no ACK: either no other node is listening, or the bus is disconnected.
EOF — 7 recessive bits, end of frame
Bit stuffing — the rule that catches most beginners
After 5 consecutive bits of the same polarity, the transmitter automatically inserts a stuff bit of the opposite polarity. Receivers strip these bits transparently.This means a CAN frame on the wire is longer than the sum of its fields. Oscilloscope measurements must account for stuff bits when checking bit timing.
Bit stuffing errors (a 6th consecutive same-polarity bit) are one of the 5 CAN error types and increment the transmit error counter (TEC).A continuous string of identical bits (e.g. a floating bus line) causes massive stuffing errors and drives the node to bus-off.
Daily exercises
MonDraw the full CAN 2.0B standard frame bit-by-bit on paper. Label every field. Calculate the minimum frame length (0 data bytes) and maximum frame length (8 data bytes) including worst-case stuff bits.
TueArbitration exercise: given three nodes transmitting simultaneously with IDs 0x100, 0x0FF, 0x101 — which wins? Trace through bit-by-bit. Verify your answer against the Bosch spec definition.
WedSet up python-can with your Canable adapter. Install socketcan on Linux (sudo ip link set can0 up type can bitrate 500000). Send and receive a frame between python-can and candump.If you see no frames: check termination resistors. A CAN bus without 120Ω termination at both ends is unreliable.
ThuUse candump to capture 100 frames from a known-good source (e.g. an OBD-II port, or simulate with python-can). Decode each field manually from the raw hex. Identify the arbitration IDs in use.
FriOscilloscope exercise: probe your CAN bus (CANH and CANL differentially). Identify SOF, the arbitration field, ACK slot, and EOF visually. Measure the bit time at 500 kbps. Verify it matches 2µs.
SatError frame exercise: disconnect termination from one end of the bus. Send frames. Observe error frames on the oscilloscope. Reconnect. Observe recovery. Document with screenshots.
STM32 RM — bxCAN chapter: initialisation, bit timing, mailboxes, FIFOs, interruptsThis is the primary source. Read every register description before writing driver code.
AN3407: bxCAN filter configuration in depthFilters are the most confusing part of bxCAN. Read this before Week 3.
bxCAN architecture
TX: 3 TX mailboxes. Each holds one pending frame. Peripheral picks the highest-priority (lowest ID) mailbox to transmit next.You write to a free mailbox and request transmission. The peripheral handles arbitration.
RX: 2 FIFOs (FIFO0 and FIFO1), each 3 frames deep. Filters assign incoming frames to one of the FIFOs.If no filter matches: frame is discarded at hardware level. This is a feature, not a bug.
Interrupts: TX complete (per mailbox), RX pending (per FIFO), error (bus-off, error warning, error passive).Enable error interrupts from day one. Silent bus-off is how CAN nodes disappear mysteriously.
bxCAN bit timing calculation
/* Bit timing for 500 kbps on STM32F446 with APB1 = 45 MHz
Prescaler = 5 → TQ clock = 45MHz / 5 = 9 MHz
Bit time = 1 / 500kbps = 2000 ns = 18 TQ at 9 MHz
Segments: SyncSeg=1 + PropSeg+PhaseSeg1=11 + PhaseSeg2=6 = 18 TQ
Sample point = (1 + 11) / 18 = 66.7% (target: 75–80%) */
CAN_InitTypeDef can_init = {
.Prescaler = 5, /* APB1 / Prescaler = TQ clock */
.Mode = CAN_MODE_NORMAL,
.SyncJumpWidth = CAN_SJW_1TQ,
.TimeSeg1 = CAN_BS1_11TQ,
.TimeSeg2 = CAN_BS2_6TQ,
.TimeTriggeredMode= DISABLE,
.AutoBusOff = ENABLE, /* auto recover from bus-off */
.AutoWakeUp = DISABLE,
.AutoRetransmission = ENABLE,
.ReceiveFifoLocked = DISABLE,
.TransmitFifoPriority = DISABLE,
};
/* Verify: use an online CAN bit timing calculator to cross-check.
bittiming.can-wiki.info is the standard tool. */
MonConfigure bxCAN in loopback mode (no external hardware). Transmit a frame and receive it back in the same MCU. Verify data integrity. Loopback is the fastest way to verify peripheral configuration before connecting hardware.
TueCalculate bit timing for 250 kbps, 500 kbps, and 1 Mbps for your APB1 clock. Use bittiming.can-wiki.info to verify. Write a bit timing config struct for each rate.Understanding the calculation prevents hours of mysterious communication failures.
WedConnect two STM32 boards via SN65HVD230 transceivers and 120Ω termination. Board A transmits a heartbeat every 100ms (ID 0x100, 8 bytes of counter data). Board B receives and logs over UART. Verify with python-can monitoring.
ThuImplement a TX queue: a ring buffer of CAN frames, filled from application code, drained by the TX complete interrupt. Prevents frame drops when the application tries to send faster than the bus can carry.
FriError state exercise: drive the TEC above 127 (error passive threshold) by creating an error condition (disconnect CANH). Monitor ESR register. Observe TEC climbing. Reconnect, observe recovery. Log every state transition.
SatBus utilisation measurement: send frames at maximum rate. Measure actual bus utilisation with python-can's timing. Verify you understand the overhead: 47 bits minimum per 8-byte frame + stuff bits + IFG.
Week 3 — CAN filters and message routing
Filtering
filter banksmask modelist modeFIFO assignmentextended ID filters32-bit vs 16-bitmessage routingdispatch table
Reading
AN3407 — STM32 bxCAN filter configuration (read in full)The 4 filter modes (32/16-bit, mask/list) are explained with examples. This is the clearest source.
STM32 RM — CAN filter section (cross-reference with AN3407)AN3407 tells you what to do. The RM tells you exactly which register bits to set.
The 4 filter modes
32-bit mask: one filter = one ID + one mask. ID bits where mask=1 must match exactly. Mask=0 = don't care.Most flexible. Use to accept a range of IDs: e.g. accept all IDs 0x100–0x1FF with ID=0x100, mask=0x700.
32-bit list: one filter = two exact IDs. Must match one exactly.Use when you need to accept exactly 2 specific IDs regardless of range.
16-bit mask: one filter = two (ID+mask) pairs, each 16-bit. Accepts 2 ranges.Efficient for standard 11-bit ID filtering — fits two filter definitions in one bank.
16-bit list: one filter = four exact IDs, each 16-bit.Use when you need exactly 4 standard IDs. Maximum density.
MonBuild a multi-node simulation: Node A sends 5 different IDs (0x100, 0x200, 0x300, 0x7DF, 0x7E0). Node B filters: accept only 0x100–0x1FF in FIFO0 and 0x7DF/0x7E0 in FIFO1. Verify with python-can that exactly the right frames arrive in each FIFO.
TueImplement a message dispatch table in C: an array of {id, mask, handler_fn} structs. On frame receipt, iterate the table and call the first matching handler. This is the architecture you'll use in Weeks 6–9 for UDS routing.
WedExtended 29-bit ID exercise: configure a 32-bit mask filter for extended frames. Verify that standard and extended filters must be configured separately (IDE bit in filter register).A common mistake: configuring a standard-ID filter, then transmitting extended frames and wondering why nothing arrives.
ThuFilter exhaustion exercise: STM32F4 has 28 filter banks shared between CAN1 and CAN2 (14 each by default). What happens when you configure more filters than available banks? Implement a filter manager that tracks bank usage and returns an error when exhausted.
Fripython-can filter test: use python-can's can.Bus(can_filters=[...]) to add software-side filtering on the PC. Verify that hardware and software filters produce identical results on the same traffic.
SatDocument your filter configuration for the final project: what IDs will the bootloader node need to accept? (UDS request IDs, ISO-TP flow control). Design the filter config now — you'll use it in Week 7.
AN5348 — STM32 FDCAN peripheral setup (free, st.com)Covers message RAM configuration, TDC, filter setup. Read before touching the peripheral.
ISO 11898-1:2015 — CAN-FD spec (expensive — use Vector's free overview instead)Vector's "CAN FD — The basic idea" whitepaper (vector.com) is a sufficient free alternative.
CAN-FD vs CAN 2.0B — key differences
Payload: up to 64 bytes (CAN 2.0B: 8 bytes). DLC 9–15 map to 12/16/20/24/32/48/64.The DLC encoding is non-linear above 8. DLC=9 means 12 bytes, not 9. Know the table.
Dual bitrate: arbitration phase at nominal rate (e.g. 500 kbps), data phase at higher rate (e.g. 2 Mbps) after the BRS (Bit Rate Switch) bit.BRS=1 in the control field triggers the switch. The receiver knows to switch at the same bit.
FDF bit: replaces the r0 reserved bit in CAN 2.0B. FDF=1 identifies a CAN-FD frame to legacy receivers (which will see it as an error).CAN-FD and CAN 2.0B cannot coexist on the same bus. Your entire bus must be FD-capable.
TDC (Transmitter Delay Compensation): at high data rates, the propagation delay through the transceiver is significant relative to the bit time. TDC compensates.Enable TDC for data rates above 1 Mbps. Wrong TDC setting = bit errors at high rate.
MonConfigure FDCAN peripheral in internal loopback mode. Send a 64-byte frame at 2 Mbps data rate. Verify all 64 bytes received correctly. Compare with an 8-byte CAN 2.0B frame — measure the transmission time difference.
TueCalculate FDCAN bit timing for 250k/1M, 500k/2M, and 1M/4M (nominal/data) for your APB clock. Use bittiming.can-wiki.info. Verify sample points are at 75–80% for nominal and 70–80% for data phase.
WedTDC calibration: enable TDC, sweep the offset value (0–127). At each setting, transmit 100 frames and count errors. Find the offset that gives zero errors. Document the optimal value for your transceiver.
ThuConnect two STM32G4 boards, both configured for FDCAN. Send a 48-byte frame at 2 Mbps data rate. Capture with oscilloscope — you should see the BRS bit followed by shorter data-phase bits. Verify on scope.
Fripython-can CAN-FD exercise: configure your Canable adapter for CAN-FD (canfd=True in python-can). Send a 64-byte FD frame from Python and receive it on STM32. Verify payload integrity.Not all Canable firmware supports CAN-FD. Check your firmware version first.
SatThroughput comparison: calculate theoretical throughput for CAN 2.0B at 1 Mbps (max 8 bytes) vs CAN-FD at 500k/2M (max 64 bytes). Measure actual throughput with python-can's timing. Document overhead.
Week 5 — python-can, bus monitoring, and node design
Receive: msg = bus.recv(timeout=1.0) — returns None on timeout
Monitor: for msg in bus: print(msg) — equivalent to candump
Periodic transmit: task = bus.send_periodic(msg, period=0.1) — sends every 100ms
CAN node architecture patterns
Heartbeat: every node periodically broadcasts a status frame. Absence of heartbeat = node fault.Pattern: ID = 0x700 + node_id, 1 byte payload: 0x05=operational, 0x7F=pre-operational
Producer/consumer: nodes publish data on fixed IDs, interested nodes filter and consume. No point-to-point addressing needed.Scales well. Adding a new consumer does not require changing the producer.
Request/response: requester sends on request ID, responder replies on response ID. Fundamental pattern for UDS.UDS request: 0x7DF (broadcast) or 0x600+node_id. Response: 0x580+node_id.
Multi-node simulation with python-can
#!/usr/bin/env python3
"""Simulate a multi-node CAN bus — send heartbeats and monitor responses"""
import can, time, threading
bus = can.Bus(interface='socketcan', channel='can0', bitrate=500000)
def heartbeat_task(node_id: int, period: float):
"""Broadcast heartbeat every `period` seconds"""
msg = can.Message(
arbitration_id = 0x700 + node_id,
data = [0x05], # operational
is_extended_id = False
)
task = bus.send_periodic(msg, period)
return task
def monitor(duration: float):
"""Log all frames for `duration` seconds"""
deadline = time.monotonic() + duration
while time.monotonic() < deadline:
msg = bus.recv(timeout=0.1)
if msg:
node = msg.arbitration_id - 0x700 if msg.arbitration_id >= 0x700 else 0
print(f" [{msg.timestamp:.3f}] ID=0x{msg.arbitration_id:03X} "
f"DLC={msg.dlc} DATA={msg.data.hex()}")
# Start heartbeats for two simulated nodes
t1 = heartbeat_task(node_id=1, period=0.1)
t2 = heartbeat_task(node_id=2, period=0.25)
print("Monitoring bus for 10 seconds...")
monitor(duration=10.0)
t1.stop()
t2.stop()
bus.shutdown()
Daily exercises
MonBuild a three-node system: Node A (STM32, sends sensor data every 50ms), Node B (STM32, sends actuator status every 100ms), Node PC (python-can, monitors and logs). Verify all frames on all nodes.
TueImplement heartbeat monitoring on the STM32: track the last received time for each expected node ID. If a heartbeat is missed for 500ms: flag a node fault. Test by disconnecting Node A and verifying Node B detects the absence.
WedBus load measurement: use python-can's timing to calculate bus utilisation at 250 kbps, 500 kbps, and 1 Mbps with your current frame set. Understand at what utilisation the bus becomes unreliable (typically >70%).
ThuRequest/response pattern: implement a simple request/response on the STM32. PC sends a request (ID 0x600), STM32 replies with sensor data (ID 0x580) within 10ms. Measure response latency with python-can timestamps.
FriDesign the Capstone 4 node architecture: define all the CAN IDs your bootloader node will use (UDS request ID, response ID, ISO-TP functional address). Write this down now — you will not change it later.
SatWireshark integration: capture CAN frames using Wireshark with the socketcan interface on Linux. Verify you can decode frames. The ISOTP and UDS dissectors will be essential in Weeks 7–9.
Week 6 — ISO-TP transport layer (ISO 15765-2)
Transport
single framefirst frameconsecutive frameflow controlblock sizeSTminISO-TP state machinepython-isotp
Why ISO-TP is necessary
CAN 2.0B carries a maximum of 8 bytes per frame. A firmware image is hundreds of kilobytes.
ISO-TP (ISO 15765-2) is the transport layer that fragments large messages across multiple
CAN frames and reassembles them at the receiver. UDS messages are carried inside ISO-TP.
You cannot implement UDS without implementing ISO-TP first.
ISO-TP frame types
Single Frame (SF): PCI byte [0x0n] where n=length. Payload ≤ 7 bytes. Entire message in one CAN frame.
First Frame (FF): PCI bytes [0x1h, 0xll] where hl=total length (12-bit). First 6 bytes of payload follow.
Consecutive Frame (CF): PCI byte [0x2n] where n=sequence number 1–F (wraps). Up to 7 bytes of payload.
Flow Control (FC): PCI byte [0x3n] where n=flow status (0=ContinueToSend, 1=Wait, 2=Overflow). Plus block size and STmin.
Flow control parameters
Block Size (BS): number of consecutive frames the sender may transmit before waiting for another FC. BS=0 = send everything without pause.For firmware update: BS=0 is simplest. The receiver handles everything and sends one FC at the start.
STmin (Separation Time minimum): minimum time between consecutive frames in milliseconds (0x00–0x7F) or microseconds (0xF1–0xF9).STmin=0: sender transmits as fast as possible. Set higher if receiver needs processing time between frames.
Timeout N_Bs: receiver must send FC within 1000ms of receiving FF. If N_Bs exceeded: sender aborts transmission.
ISO-TP TX state machine
typedef enum {
ISOTP_TX_IDLE,
ISOTP_TX_SEND_FF, /* send first frame, wait for FC */
ISOTP_TX_WAIT_FC, /* waiting for flow control from receiver */
ISOTP_TX_SEND_CF, /* sending consecutive frames */
ISOTP_TX_COMPLETE,
ISOTP_TX_ERROR,
} ISOTP_TxState_t;
typedef struct {
ISOTP_TxState_t state;
const uint8_t *payload;
uint32_t total_len;
uint32_t bytes_sent;
uint8_t sn; /* sequence number 1–F */
uint8_t bs; /* block size from FC */
uint8_t stmin_ms; /* separation time from FC */
uint32_t fc_timeout; /* N_Bs timer */
uint8_t frames_in_block; /* frames sent since last FC */
} ISOTP_TxCtx_t;
/* Call from CAN TX complete interrupt or application loop */
void ISOTP_TX_Tick(ISOTP_TxCtx_t *ctx) {
switch (ctx->state) {
case ISOTP_TX_SEND_FF: {
uint8_t ff[8] = {0};
ff[0] = 0x10 | ((ctx->total_len >> 8) & 0x0F);
ff[1] = ctx->total_len & 0xFF;
memcpy(&ff[2], ctx->payload, 6);
CAN_Transmit(ISOTP_TX_ID, ff, 8);
ctx->bytes_sent = 6;
ctx->fc_timeout = HAL_GetTick() + 1000; /* N_Bs = 1000ms */
ctx->state = ISOTP_TX_WAIT_FC;
break;
}
case ISOTP_TX_WAIT_FC:
if (HAL_GetTick() > ctx->fc_timeout) {
ctx->state = ISOTP_TX_ERROR; /* N_Bs timeout */
}
break;
case ISOTP_TX_SEND_CF: {
uint8_t cf[8] = {0};
cf[0] = 0x20 | (ctx->sn & 0x0F);
uint32_t remaining = ctx->total_len - ctx->bytes_sent;
uint8_t chunk = (remaining > 7) ? 7 : (uint8_t)remaining;
memcpy(&cf[1], ctx->payload + ctx->bytes_sent, chunk);
CAN_Transmit(ISOTP_TX_ID, cf, 8);
ctx->bytes_sent += chunk;
ctx->sn = (ctx->sn + 1) & 0x0F;
ctx->frames_in_block++;
if (ctx->bytes_sent >= ctx->total_len) {
ctx->state = ISOTP_TX_COMPLETE;
} else if (ctx->bs > 0 && ctx->frames_in_block >= ctx->bs) {
ctx->frames_in_block = 0;
ctx->fc_timeout = HAL_GetTick() + 1000;
ctx->state = ISOTP_TX_WAIT_FC;
}
break;
}
default: break;
}
}
Daily exercises
MonImplement ISO-TP TX for messages ≤ 7 bytes (single frame only). Test with python-isotp receiving. Verify PCI byte is correct and payload arrives complete.
TueImplement ISO-TP TX for multi-frame messages (FF + CF). Hard-code BS=0, STmin=0. Test with python-isotp receiving a 200-byte message. Verify all bytes arrive in order.
WedImplement ISO-TP RX: receive FF, send FC (BS=0, STmin=0), receive all CF, reassemble payload. Test with python-isotp sending a 500-byte message. Verify reassembly is correct.
ThuAdd flow control handling to TX: parse FC frames, respect BS and STmin. Test with python-isotp configured to send FC with BS=5, STmin=10ms. Verify your transmitter pauses after every 5 CFs.
FriTimeout handling: simulate N_Bs timeout (don't send FC after FF). Verify transmitter aborts after 1000ms and sets error state. Simulate N_Cr timeout (stop sending CFs mid-transfer). Verify receiver detects and aborts.
SatTransfer a full 64 kB buffer over ISO-TP and measure throughput at 500 kbps. Compare against theoretical maximum. Document the overhead from FF/FC handshake and STmin.
Week 7 — UDS diagnostic services
UDS
0x10 session control0x27 security access0x34 request download0x36 transfer data0x37 transfer exit0x31 routine controlNRC codesP2 / P2* timing
Reading
Vector UDS introduction — complete (free, vector.com/knowhow)Read before implementing any service. The free Vector doc covers all six flashing services with examples.
python-udsoncan documentation (udsoncan.readthedocs.io)Your test client. Read the API for each service you implement on the STM32 side.
The six firmware update services
0x10 DiagnosticSessionControl — switch to programming session (0x02)Default session (0x01): normal operation. Programming session (0x02): enables flashing services.
MonImplement 0x10 DiagnosticSessionControl. Accept sub-functions 0x01 (default) and 0x02 (programming). Reject all others with NRC 0x12. Test with python-udsoncan: client.change_session(services.DiagnosticSessionControl.Session.programmingSession).
TueImplement 0x27 SecurityAccess. Seed: use STM32 hardware RNG. Key: XOR seed with a secret constant (you'll replace this with HMAC in Week 8). Implement lockout: after 3 wrong keys, deny access for 10 seconds (NRC 0x37).
WedImplement 0x31 RoutineControl for flash erase (routineId 0xFF00). On startRoutine: call your Phase 3 Flash_EraseSector(). Poll until complete. Return success or generalProgrammingFailure.
ThuImplement 0x34 RequestDownload and 0x36 TransferData. 0x34 validates address and size, returns maxBlockLen. 0x36 calls Flash_Write() with each block, verifies sequence counter, returns NRC 0x73 on wrong counter.
FriImplement 0x37 RequestTransferExit. Compute CRC32 over written image. Compare against client-provided checksum. On match: call BootFlags_CommitBank(). On mismatch: NRC 0x72, erase the bank.
SatFull UDS sequence test with python-udsoncan: session → security → erase → download → transfer (1 kB test image) → exit. Verify each step returns a positive response. Log every request/response.
MonRun flash_can.py against your Week 7 UDS stub. Verify the full sequence completes. Capture with Wireshark — decode each ISO-TP frame and each UDS service. Verify the dissector shows the correct service ID and sub-functions.
TueP2/P2* timing: verify your STM32 responds within P2 (50ms) for fast services (session, security). For slow services (erase, transfer exit): send a pending response (NRC 0x78 — requestCorrectlyReceivedResponsePending) and complete within P2* (5s).NRC 0x78 is how ECUs say "I received your request, still working, don't timeout." Essential for flash erase which can take 200ms+.
WedS3 session keep-alive: if the client is silent for more than S3 (5s), the ECU must exit programming session automatically. Implement S3 timer in the bootloader. Verify: start session, wait 6s with no messages, verify ECU is back in default session.
ThuReplace XOR key with HMAC-SHA256: seed = 8 bytes from STM32 hardware RNG. Key = HMAC-SHA256(seed, shared_secret). Implement using STM32 HMAC peripheral (available on H7/U5) or software (mbedTLS or a single-file HMAC). Update python client to match.
FriApp-triggered update: application sets update_pending in backup SRAM and calls NVIC_SystemReset(). Python client detects the bootloader banner over CAN (or polls for UDS session availability) and initiates the update automatically.
Sat">50-cycle soak test: loop the python flasher 50 times, alternating between two firmware versions. Verify: correct version every time, boot counter resets, rollback works when you deliberately flash a corrupt image at cycle 25.
0x85 ControlDTCSettings — disable DTC recording at session entry, re-enable at exitPrevents spurious DTCs being logged during the programming session (e.g. missing message faults)
0x28 CommunicationControl — disable normal application comms during programmingStops the application from interfering with bootloader CAN traffic
0x19 ReadDTCInformation — read fault codes stored by the bootloaderImplement for: failed update attempts, CRC mismatches, flash errors, bus-off events
0x3E TesterPresent — already handled by S3 keep-alive logicSub-function 0x80: suppressPosRspMsgIndicationBit — client sends TesterPresent with no response expected
Full sequence: 50-cycle soak, rollback on corrupt image, S3 timeout
Phase 4 capstone — deliverable checklist
CAN-FD bus with two STM32 nodes + USB adapter, 120Ω termination — oscilloscope-verified bit timingScope trace in README showing BRS transition at correct bit boundary
ISO-TP layer: TX and RX state machines, flow control, timeouts, unit tested on hostpython-isotp integration test transferring 64 kB payload with 100% integrity
UDS service handler: all six flashing services, NRC table, session state machine, P2/P2*/S3 timerspython-udsoncan test suite covers every positive and negative response
Security access with STM32 hardware RNG seed and HMAC-based key derivationLockout after 3 wrong keys with 10-second delay enforced on STM32 side
Phase 3 bootloader integration: flash driver, dual-bank A/B, CRC verification, rollback — all reused unchangedNo modifications to Phase 3 bootloader logic — only transport layer replaced
flash_can.py: complete Python UDS flash client with progress bar, timing, error handlingUsage: python flash_can.py --channel can0 firmware.fwu
Wireshark capture of a complete flash session: ISO-TP frames decoded, UDS services visible.pcapng file committed to GitHub repo
Unit tests: ISO-TP framing, UDS service parser, CRC — ≥85% branch coverage, CI badgeCeedling on host. Tests run without STM32 hardware.
What this capstone demonstrates to a hiring engineer
The complete automotive firmware update stack: CAN-FD physical layer → ISO-TP transport → UDS application layer → flash management. This is what production ECU bootloaders implement. A candidate who has built this from scratch is immediately productive on any automotive embedded team.Most junior candidates know CAN in theory. Very few have implemented ISO-TP and UDS from scratch and have a working flash session to show for it.
Phase 5 (professional readiness) adds MISRA-C compliance, full test coverage, and portfolio polish to what you've built in Phases 3 and 4. The technical work is done here.