Skip to main content
emobility.academy
B.Tech / BEDiagnostics & cybersecurityIntermediate About 5 weeksKit + add-ons

OBD-Style Diagnostic Trouble Code (DTC) Manager with Freeze Frames

You build a diagnostic trouble code (DTC) manager for an EV control unit on the kit that detects faults, debounces them, stores codes with freeze-frame data in SPI flash and reports them over CAN. A PC tool reads and clears codes through a USB-CAN adapter.

Uses: SPI · CAN · GPIO

The problem

When an EV develops a fault, service engineers rely on stored trouble codes and snapshot data to find the cause. A control unit must detect faults reliably, avoid false codes from brief glitches, keep records through power loss and report them in a standard way.

Objectives

  • Define a set of DTCs for battery, sensor and communication faults.
  • Implement fault debouncing and a DTC status model with confirmed, pending and cleared states.
  • Store DTCs and freeze frames in SPI flash with timestamps from the RTC.
  • Report and clear DTCs over CAN using a simple request and response protocol.

What you need

From the EV Embedded Development Kit

  • STM32F103C8T6 microcontroller unit (Arm Cortex-M3)
  • 1S battery with connector and T-sense input
  • Analog potentiometer input
  • MPU6050 accelerometer and gyroscope
  • SPI flash memory
  • RTC with CR2032 backup cell
  • CAN bus interface
  • 16×2 LCD
  • RGB LEDs and a buzzer

Software

  • STM32CubeIDE
  • Embedded C with HAL drivers
  • Python with python-can
  • A serial terminal for debugging

Extra parts

  • A USB-CAN adapter — The PC acts as the diagnostic tester. The kit has one CAN node, so a second node is needed to send requests and receive responses.

This project uses the kit's lithium cell. Respect polarity, never short the terminals, and don't leave the cell charging unattended.

Key concepts

Diagnostic trouble code (DTC)
A code identifying a specific fault, such as a sensor out of range. OBD-II codes look like P0A80; this project uses a similar numbering for its own faults.
Debouncing and fault maturation
A fault must persist for a set time or number of checks before it is confirmed, so brief noise does not create a code.
Freeze frame
A snapshot of key values, such as voltage, temperature and potentiometer setting, captured when a fault is confirmed.
Non-volatile storage
SPI flash keeps codes through power loss. Writes must be organised to avoid corruption and limit wear.

Step-by-step guide

  1. 1

    Define the DTC list and rules

    Write a table of 8 to 12 faults: cell over-voltage, under-voltage, over-temperature, T-sense open or shorted, throttle (potentiometer) out of range, MPU6050 not responding, excessive tilt, CAN bus-off and RTC invalid. For each give a code, detection condition, maturation time, healing condition and freeze-frame contents. Use one code range per subsystem, such as battery, sensors and communication, so new codes can be added later without renumbering, and describe each code in plain words.

    Checkpoint: A reviewed DTC table with codes, conditions and timings.

  2. 2

    Configure the board

    Turn on DIP switches for the battery, potentiometer, MPU6050, SPI flash, RTC, CAN, LCD and LED blocks. In CubeMX configure the ADC with DMA, I2C for the MPU6050, SPI for the flash, the RTC with LSE and CAN at 500 kbit/s. Connect the USB-CAN adapter to the screw terminal and make sure the bus is terminated with 120 ohms at both ends. Handle the cell carefully and check polarity.

    Checkpoint: All peripherals initialise and a test CAN frame is received on the PC.

  3. 3

    Implement fault monitors

    Write a monitor function per fault, called from a 10 ms scheduler, that returns pass, fail or not tested. Check the MPU6050 by reading WHO_AM_I and check the RTC by a valid-time flag. Detect CAN bus-off from the bxCAN error status register. Keep monitors independent so each can be tested alone. Write a short test for each monitor that creates its fault and records the result, so later changes can be rechecked quickly.

    Checkpoint: Each monitor reports fail when its fault is created, such as turning the potentiometer to an end stop or unplugging the T-sense lead.

  4. 4

    Implement the DTC state machine

    For each DTC keep a counter that rises on fail and falls on pass. Set pending when the first fail occurs, confirmed when the counter reaches its threshold and healed after a number of passing cycles. Use status bits similar to the UDS DTC status byte, such as test failed, pending and confirmed. Show active codes on the LCD and the red LED.

    Checkpoint: Brief glitches do not confirm a DTC, but a sustained fault does after the defined time.

  5. 5

    Store DTCs and freeze frames

    On confirmation, capture a freeze frame with voltage, temperature, potentiometer value, tilt and RTC timestamp. Write the record to SPI flash with a sequence number and CRC, using two alternating sectors so a power cut during writing never loses all records. On start-up, read back the latest valid records. Count how often each sector is erased and estimate the flash lifetime at the expected fault rate, to show the design will not wear the memory out.

    Checkpoint: DTCs and freeze frames survive power cycling, and a record corrupted on purpose is detected by its CRC.

  6. 6

    Define the CAN diagnostic protocol

    Define request and response IDs, for example 0x7E0 for requests and 0x7E8 for responses. Define services: read number of DTCs, read DTC list, read a freeze frame and clear DTCs. Keep each response within 8 bytes or split it across several frames with a frame counter. Configure a bxCAN filter so the controller receives only the request ID. Include the negative response format and the meaning of each error code.

    Checkpoint: A protocol document with byte layouts for each request and response.

  7. 7

    Implement CAN handling

    Receive requests in the CAN RX interrupt with HAL_CAN_GetRxMessage, queue them and answer from the main loop using HAL_CAN_AddTxMessage. Handle unknown requests with a negative response code. Protect DTC data shared between the monitor scheduler and the CAN handler from simultaneous access. Check what happens when requests arrive faster than responses can be sent, and make sure the queue cannot overflow silently. Log every request and response over UART during development to make debugging easier.

    Checkpoint: The PC receives correct responses for every service.

  8. 8

    Build the PC tester tool

    Write a Python script with python-can that sends requests through the USB-CAN adapter and prints DTCs with descriptions and decoded freeze frames. Add a clear command that asks for confirmation before sending. Log all frames for the report. Keep the DTC descriptions in a single table shared by the tool and the report, so they never disagree. Add a polling mode that reads the DTC count every second so you can watch codes appear live while you create faults on the kit.

    Checkpoint: The tool reads, displays and clears DTCs from the kit.

  9. 9

    Test every fault end to end

    Create each fault, confirm the DTC appears with the right freeze frame, cycle power, read it again, heal the fault and clear it. Test timing: the confirmation delay should match the table. Test that clearing works and that codes return if the fault is still present. Record each test as a short table entry with the action taken, expected result and observed result, so the test log can be included directly in the report.

    Checkpoint: A test log showing pass for every DTC in every scenario.

  10. 10

    Document the system

    Write the report: DTC table, state machine, storage design, CAN protocol, tester tool and test results. Explain how it relates to OBD-II and UDS, and what a production system adds, such as standard service IDs and multi-frame transport. Include the DTC table, state diagrams, the flash record layout and CAN message layouts as figures. Add a short user guide explaining how to read and clear codes with the PC tool, and list known limitations, such as single-frame responses only.

    Checkpoint: A report and a live demo reading codes from the kit.

Testing & validation

  • Every DTC confirms after its defined maturation time and not on brief glitches.
  • Records survive power cycling and corrupted records are detected.
  • All diagnostic services work from the PC tester.
  • Clearing removes codes and active faults set them again.

Expected results

A working DTC manager on the kit with debounced fault detection, non-volatile freeze-frame storage and a CAN diagnostic interface read by a PC tool, similar in structure to a production ECU's diagnostic stack.

Take it further

  • Move to standard UDS services 0x19 and 0x14 over ISO-TP.
  • Add an occurrence counter and ageing of healed DTCs.
  • Add an HC-05 on the Bluetooth header for a phone-based reader.
  • Store a short pre-fault history buffer, not just a single freeze frame.

Viva and review questions

Why not set a DTC the moment a check fails?

Sensors and buses have brief glitches. A maturation counter confirms only faults that persist, which avoids false codes and unnecessary service visits.

How does your storage survive a power cut during a write?

Records carry a sequence number and CRC and are written alternately to two sectors, so the previous valid copy always remains if a write is interrupted.

Why did you need a USB-CAN adapter?

CAN needs at least two nodes to acknowledge frames. The kit has one CAN node, so the adapter acts as the tester and the second node.

Related EV projects

All 100 EV projects