Embedded HandbookApps
All sections › III. Peripherals
STM32F103, STM32F407 · level intermediate · 49 min read
Open in the interactive handbook

19г. OBD-II: car diagnostics over CAN — modes, PIDs and trouble codes

Section 19 taught bxCAN to receive and send frames, section 19в — to assemble long messages from them over ISO-TP. Now comes the most practical use of CAN: the connector under the steering wheel of any modern car. Through it a scanner learns engine speed, temperature and vehicle speed, reads and clears trouble codes, and retrieves the vehicle identification number (VIN). All of this is OBD-II, and a device for it fits into an STM32F103 with a transceiver.

Here: the connector and the protocols, how a request and a response are built, which services there are (they are also called modes), how to find out what an ECU supports and convert bytes into rpm and degrees, how trouble codes are encoded and how the VIN arrives in three frames. At the end — a scan tool and an ECU simulator in C for bxCAN on the F103 and F407.

Sources: the texts of the SAE J1979 and ISO 15765-4 standards were not used, so everything was cross-checked against three open-source projects. Formulas and frame parsing — from python-obd 0.7.3 (obd/decoders.py, obd/commands.py, obd/protocols/protocol_can.py). Live responses — the ELM327 4.0.0 emulator (ELM327-emulator, the "car" scenario). The ECU side — the rusEFI firmware (firmware/controllers/can/obd2.cpp): it answers OBD requests by itself. The negative response codes are from the iso14229 library. Licenses: python-obd — GPL v2, ELM327-emulator — CC BY-NC-SA 4.0, rusEFI — GPL v3, iso14229 — MIT; their code is paraphrased here, not copied into the check. Our own scan tool and simulator are _verify/obd19g/obd19g.c, the run is _verify/obd19g/obd19g.py.

Terms used in the documentation

Term Meaning
OBD-II (on-board diagnostics) on-board diagnostics, second generation
DLC, J1962 connector the diagnostic connector
service, mode a service (mode): 01, 02, 03…
PID (parameter ID) the parameter number within a service
functional / physical request a request "to all ECUs" / a request to one ECU
ECU (electronic control unit) an electronic control unit
DTC (diagnostic trouble code) a trouble code
stored, pending, permanent DTC stored, unconfirmed, permanent code
MIL (malfunction indicator lamp) the "check engine" lamp
freeze frame a snapshot of parameters at the moment of the fault
VIN (vehicle identification number) the vehicle number, 17 characters
negative response, NRC a refusal and its reason code
flow control flow control (ISO-TP)

19г.1 The connector and the protocols

The J1962 connector has sixteen sockets in two rows. Five matter for CAN:

Socket What is on it
6 CAN-H
14 CAN-L
16 +12 V from the battery
4 chassis ground
5 signal ground

The other sockets are taken by older protocols and manufacturer pins. Over the years several protocols have gone through the same connector, and the ELM327 adapter knows all of them by number — python-obd stores these numbers in its classes:

No. Protocol
1, 2 SAE J1850 PWM and VPW
3 ISO 9141-2 (K-line)
4, 5 ISO 14230-4 (KWP, slow and fast init)
6 ISO 15765-4, CAN, 11 bits, 500 kbit/s
7 ISO 15765-4, CAN, 29 bits, 500 kbit/s
8 ISO 15765-4, CAN, 11 bits, 250 kbit/s
9 ISO 15765-4, CAN, 29 bits, 250 kbit/s

Protocols 1–5 are not CAN; K-line, for example, is a serial line at 10400 baud with a "wake-up" by address 0x33 at 5 baud (that is how it works in the emulator). From here on we talk only about CAN, most often protocol 6.

Who listens to whom. The scan tool sends a functional request with identifier 0x7DF — it is heard by all ECUs. Each ECU that has something to say answers with its own identifier from the range 0x7E8…0x7EF: the engine — 0x7E8, the transmission — 0x7E9 (that is how the addresses are assigned in the emulator). The ECU's physical address is 8 lower: 0x7E0…0x7E7; that is where the scan tool sends requests to a single ECU and flow control. rusEFI has the same two numbers: OBD_TEST_REQUEST 0x7DF and OBD_TEST_RESPONSE 0x7E8. With 29-bit identifiers the functional request is 0x18DB33F1, and the engine's response is 0x18DAF110: in the python-obd parser byte 0xDB means a functional request, 0xDA — a physical one, 0x33 — "all", 0xF1 — the scan tool.

Top left — the J1962 connector with sixteen sockets in two rows, highlighted are 4 and 5 (chassis and signal ground), 6 and 14 (CAN-H and CAN-L) and 16 (+12 V); below it the scan tool: an STM32F103 with bxCAN through a CAN transceiver to sockets 6 and 14, power from socket 16 through a regulator, 500 kbit/s timing for the F103 and F407 and a filter on 0x7E8 with mask 0x7F8. On the right — the exchange between the scan tool and the engine ECU: request 7DF 02 01 0C and response 7E8 04 41 0C 14 5F, that is 1303.75 rpm; the VIN request 7DF 02 09 02, the first frame 10 14 for 20 bytes, flow control 30 00 00 to address 7E0, two consecutive frames 21 and 22 and the resulting VIN WP0ZZZ99ZTS390000. At the bottom — how the two bytes of a trouble code turn into P0133, P0301 and U0100


19г.2 Request and response: services and refusals

An OBD request is a single ISO-TP frame (section 19в): a length byte, the service number and, if needed, the parameter number. The ECU's response starts with the service plus 0x40: to 01 — 0x41, to 09 — 0x49. Our frame is 8 bytes, as in rusEFI (the first byte is the length, then 0x40 + mode, the PID, the data); the unused bytes are filler (0x00 for us and for the emulator).

Service What it does Request Response starts with
01 current data 02 01 PID 41 PID
02 freeze frame 03 02 PID frame 42 PID frame
03 stored trouble codes 01 03 43
04 clear codes and freeze frame 01 04 44
07 pending codes 01 07 47
09 vehicle information: VIN 02 09 PID 49 PID
0A permanent codes 01 0A 4A

If the ECU cannot carry out a request, it answers negatively: 0x7F, the service number and a reason code. The codes are the same as in UDS; the names are taken from iso14229:

Code Reason Where it occurred
0x11 service not supported the simulator, for service 05
0x12 sub-function not supported the emulator for 0201, the simulator for 020C01
0x22 conditions not correct the simulator, for 04 with the engine running

To an unknown PID of service 01 rusEFI stays silent (the code says exactly that: "ignore unhandled PIDs"), and so does our simulator: nobody answered 01A6, and the scan tool learns this only from a timeout. That is why you first ask what the ECU supports.


19г.3 PIDs: what is supported and how to calculate

Support bitmaps. PID 0x00 of service 01 is not a parameter but 32 bits: which of PIDs 0x01…0x20 the ECU knows. The most significant bit is PID 0x01, the least significant is PID 0x20, and PID 0x20 is the next bitmap (0x21…0x40), then 0x40 (0x41…0x60). The emulator answered 0100 with bytes BE 3F A8 13; the C parser and python-obd both read 18 PIDs from them: 01 03 04 05 06 07 0B 0C 0D 0E 0F 10 11 13 15 1C 1F 20. The last one, 0x20, says: ask 0120 as well. The simulator has its own bitmap — 98 3B 80 01: 01 04 05 0B 0C 0D 0F 10 11 20. The transmission answered the same functional request with 00 08 00 00 — it knows only the vehicle speed (0x0D).

Formulas. The data bytes after the PID are called A, B, C, D. Ten common PIDs:

PID What Bytes Formula Example from the emulator Value
0x04 engine load 1 A·100/255, % FF 100 %
0x05 coolant 1 A − 40, °C 5F 55 °C
0x0B intake manifold pressure 1 A, kPa 26 38 kPa
0x0C engine speed 2 (256A + B)/4, rpm 14 5F 1303.75 rpm
0x0D vehicle speed 1 A, km/h 0A 10 km/h
0x0F intake air 1 A − 40, °C 39 17 °C
0x10 air flow rate 2 (256A + B)/100, g/s 18 1F 61.75 g/s
0x11 throttle 1 A·100/255, % 2B 16.863 %
0x2F fuel level 1 A·100/255, % AA 66.667 %
0x42 control module voltage 2 (256A + B)/1000, V 39 4B 14.667 V

Each formula was cross-checked against python-obd: there engine speed is unit 0x07 of the unit table (0.25 rpm per unit), air flow — 0x27 (0.01 g/s), voltage — 0x0B (0.001 V), percentages — v * 100.0 / 255.0, temperature — v - 40. The ECU side in rusEFI encodes the same way: it multiplies engine speed by ODB_RPM_MULT 4, adds ODB_TEMP_EXTRA 40 to temperature, multiplies the throttle by 2.55, air flow — by 100, voltage — by 1000.

The limits follow from the formulas: engine speed — up to 16383.75 rpm, temperature — from −40 to 215 °C, air flow — up to 655.35 g/s, voltage — up to 65.535 V.

Our own parser in C is one table: multiply the raw number, divide, shift. The value is stored in thousandths, without floating point — the F103 does not have it:

static const obd_pid_info pid_table[] = {
    /* PID  байт  умножить  делить  сдвиг   единица  что это */
    { 0x04u, 1u, 100000,   255,     0,     "%",     "нагрузка двигателя" },   /* A·100/255 */
    { 0x05u, 1u, 1000,     1,     -40,     "°C",    "охлаждающая жидкость" }, /* A − 40 */
    { 0x0Bu, 1u, 1000,     1,       0,     "кПа",   "давление во впуске" },   /* A */
    { 0x0Cu, 2u, 250,      1,       0,     "об/мин","обороты" },              /* (256A + B)/4 */
    { 0x0Du, 1u, 1000,     1,       0,     "км/ч",  "скорость" },             /* A */
    { 0x0Fu, 1u, 1000,     1,     -40,     "°C",    "воздух на впуске" },     /* A − 40 */
    { 0x10u, 2u, 10,       1,       0,     "г/с",   "расход воздуха" },       /* (256A + B)/100 */
    { 0x11u, 1u, 100000,   255,     0,     "%",     "дроссель" },             /* A·100/255 */
    { 0x2Fu, 1u, 100000,   255,     0,     "%",     "уровень топлива" },      /* A·100/255 */
    { 0x42u, 2u, 1,        1,       0,     "В",     "напряжение блока" },     /* (256A + B)/1000 */
};

The table was checked exhaustively: all 256 values of the seven one-byte PIDs and all 65536 values of the three two-byte ones — 198400 frames — went through the C parser and through the python-obd decoders. Engine speed, air flow, voltage and temperatures matched exactly; for percentages C discards everything beyond thousandths, and the difference is no more than 0.00098 percent.


19г.4 Trouble codes and freeze frame

How a code is encoded. Two bytes. The upper two bits are the system: 00 — P (engine and transmission), 01 — C (chassis), 10 — B (body), 11 — U (network). The next two bits are the first digit, 0…3. The remaining 12 bits are three hexadecimal digits. That is how parse_dtc in python-obd parses it, and so does our own parser:

void obd_dtc_str(uint16_t code, char out[6])
{
    static const char hex[] = "0123456789ABCDEF";
    out[0] = "PCBU"[code >> 14];                /* биты 15:14 — система */
    out[1] = (char)('0' + ((code >> 12) & 3u)); /* биты 13:12 — 0…3 */
    out[2] = hex[(code >> 8) & 0xFu];
    out[3] = hex[(code >> 4) & 0xFu];
    out[4] = hex[code & 0xFu];
    out[5] = '\0';
}

Bytes 01 33 are P0133, 03 01 — P0301, C1 00 — U0100. python-obd keeps the code descriptions in its own table: P0301 — misfire in the first cylinder, P0420 — low catalyst efficiency, U0100 — lost communication with the engine ECU.

Three lists of codes. Service 03 — stored codes: they are why the lamp is lit. 07 — pending: a fault has been noticed but not confirmed yet. 0A — permanent. How many codes are stored and whether the lamp is lit is told by PID 0x01: in the first byte of the response the most significant bit is the lamp, the lower seven are the number of codes. The simulator with three codes answered 41 01 83 … — the lamp is lit, 3 codes; the emulator with the same three codes gave the same 83.

The "how many codes" byte. Here the two open-source projects disagree. python-obd assumes that on CAN the number of codes comes right after 0x43, and truncates the response by it. The ELM327 4.0.0 emulator does not send this byte:

Who Response to 03 What python-obd read
simulator (with the count byte) 43 03 01 33 03 01 C1 00 — 8 bytes, two frames P0133, P0301, U0100
emulator (without it) 43 01 33 03 01 C1 00 — 7 bytes, one frame P3303

python-obd took 0x01 as the number of codes and built a nonexistent P3303 from the next two bytes. For the emulator's response to 07 (47 04 20) it returned an empty list. The standard was not consulted here, so our own parser understands both forms: with the count byte the response length is always even (service, count, two bytes per code), without it — odd.

        r->kind = OBD_R_DTC;
        r->count_byte = (n % 2u) == 0u;
        first = r->count_byte ? 2u : 1u;

This way the C parser read P0133, P0301 and U0100 from both responses.

Freeze frame. When the ECU stores a code, it also remembers the parameters at that moment. Service 02 asks for them the same way as 01, only with the frame number after the PID: 02 0C 00 — engine speed in freeze frame 0. The emulator answered 020200 with 42 02 00 00 00 — the bytes after the frame number are zero, there is no freeze frame. The simulator answered 42 02 00 01 33 — captured on P0133, and at that time it was 2000 rpm, 90 °C and 60 km/h. python-obd sends service 02 without the frame number and expects the data right after the PID, so in the check the frame number was removed by hand before its decoder.

Clearing. Service 04 clears the codes and the freeze frame — that is exactly how the CLEAR_DTC command is described in python-obd: "Clear DTCs and Freeze data". The emulator answered 44, and after that answered 03 with 43 00, and PID 0x01 showed 00: the lamp went out. Our simulator, with the engine running, answers with the refusal 7F 04 22 and clears only at zero engine speed; it does not touch the permanent codes (0A) — P0133 remained after clearing. The emulator cleared them too.


19г.5 VIN, ISO-TP and ELM327

The VIN does not fit into a frame. The response to 0902 is service 0x49, PID 0x02, the number of records 1 and 17 characters: 20 bytes. That is how python-obd describes it — b"0902", 22 (with two header bytes) and encoded_string(17). The response comes in three ISO-TP frames:

Who Frame What it is
scan tool 7DF: 02 09 02 functional request
ECU 7E8: 10 14 49 02 01 57 50 30 first frame: length 0x014 = 20 bytes, the first 6
scan tool 7E0: 30 00 00 flow control: send everything, no gaps
ECU 7E8: 21 5A 5A 5A 39 39 5A 54 consecutive frame 1: 7 bytes
ECU 7E8: 22 53 33 39 30 30 30 30 consecutive frame 2: 7 bytes

6 + 7 + 7 = 20: VIN WP0ZZZ99ZTS390000. The simulator's frames matched the emulator's frames byte for byte. The key point here is where the scan tool sends flow control: not to the functional 0x7DF, but to the physical address of the ECU that answered, 0x7E8 − 8 = 0x7E0. The ECU waits for it exactly there; without it the second and third frames will not come. A physical request 0902 to 0x7E0 produced the same three frames.

ELM327. Many scanners work through an ELM327 adapter — a chip that accepts text commands over a serial port (or Bluetooth) and talks to CAN by itself. A session with the emulator, as a scanner app runs it:

> ATZ        ELM327 v1.5
> ATE0       OK
> ATH1       OK
> ATSP6      OK
> ATDP       AUTO, ISO 15765-4 (CAN 11/500)
> ATRV       13.1V
> 010C       7E8 04 41 0C 14 5F
> 0902       7E8 10 14 49 02 01 57 50 30
             7E8 21 5A 5A 5A 39 39 5A 54
             7E8 22 53 33 39 30 30 30 30
> ATH0       OK
> 0902       014
             0: 49 02 01 4D 41 54
             1: 34 30 33 30 39 36 42
             2: 4E 4C 30 30 30 30 30

ATZ — reset, ATE0 — do not echo the command, ATH1 — show identifiers, ATSP6 — protocol 6, ATDP — which protocol is selected, ATRV — the voltage at the connector. A request is just the hexadecimal digits of the service and PID; the adapter takes care of ISO-TP, flow control and filler itself. With ATH0 the adapter assembles a long response itself: the first line is the length (014), then numbered pieces. In the second request the emulator returned a different VIN from its list — the format does not change because of that.

python-obd loses VIN characters. Its decode_encoded_string trims the edges of the string with a strip call using the set b'\x00' b'\x01' b'\x02' b'\\x00' …. The last elements are written with a double backslash, so the set ended up with the characters \, x and the digits 0, 1, 2. WP0ZZZ99ZTS390000 became WP0ZZZ99ZTS39 — 13 characters of 17; for a VIN starting with 1, the beginning is cut off as well. That is why of the 43 simulator responses that python-obd parsed, 41 matched the C parser — two mismatches, both VINs.


19г.6 A scan tool and a simulator on the STM32

Timing. OBD on CAN is 500 kbit/s (or 250). On the F103 the APB1 bus runs at 36 MHz, and with 8…25 quanta per bit exactly 500 kbit/s is given by five dividers: 3, 4, 6, 8 and 9 — that is 24, 18, 12, 9 and 8 quanta per bit. We take the same option as in section 19.4: divider 9, a 250 ns quantum, a bit of 1 + 6 + 1 = 8 quanta, 2 µs, sample point (1 + 6)/8 = 87.5 %. The BTR word is 0x00050008. For 250 kbit/s — divider 18 with the same quanta. On the F407 APB1 is 42 MHz, there are three options (dividers 4, 6 and 7); we take 6 and 14 quanta: 1 + 11 + 2, sample point 85.7 %, BTR 0x001A0005. _Static_assert checks all of this at build time.

The scan tool's filter. Responses come from 0x7E8…0x7EF — eight identifiers that differ in the three lower bits. One 32-bit filter with mask 0x7F8 passes exactly them (section 19.5); the IDE and RTR bits in the mask force them to be compared as well — only standard data frames get through:

        /* прибор: 32-битная маска — 0x7E8…0x7EF, только стандартные кадры данных */
        CAN1->FS1R |= 1u;
        CAN1->FM1R &= ~1u;
        CAN1->sFilterRegister[0].FR1 = OBD_RSP_ID(0) << 21;
        CAN1->sFilterRegister[0].FR2 = (0x7F8u << 21) | CAN_RI0R_IDE | CAN_RI0R_RTR;

The ECU simulator needs 0x7DF and its own 0x7E0 — that is a list of 16-bit filters, like the CANopen node in section 19б.

The scan tool (obd_tester_step) asks for engine speed, vehicle speed and temperature in a round. It waits 150 ms for a response — the same as the default wait of the iso14229 client (UDS_CLIENT_DEFAULT_P2_MS); no response — next PID. Received frames go to an ISO-TP reassembler, each of the eight ECUs has its own; on a first frame the scan tool immediately sends flow control to f.id - 8u.

The ECU simulator (obd_ecu_step) is for the bench, without a car: two boards, two transceivers, a bus with terminators at the ends. It answers services 01, 02, 03, 04, 07, 09 and 0A, builds the support bitmaps itself from its table of values, and sends the refusals 0x11, 0x12 and 0x22. The default values in the run are the same as the emulator's, so that the frames can be compared.

What came out. The core obd19g.c (ISO-TP, parsing, simulator) and the bxCAN link obd19g_bxcan.c were built with arm-none-eabi-gcc (container stm-arm:1, -Os) for the F103 and F407 without warnings. The core is 2518 bytes of code on both; the bxCAN link is 743 bytes on the F103 and 747 on the F407 plus 584 bytes of RAM (eight ISO-TP reassemblers of 70 bytes each and the values). The ECU simulator takes 1008 bytes of RAM: PID values, the freeze frame and three code lists.

The same obd19g.c was built with gcc on the PC (container stm-sys:1) with -Wall -Wextra -Werror -pedantic: the scan tool, the engine simulator, the transmission simulator and the engine simulator on 29-bit identifiers exchanged data over an in-memory bus, 33 requests. All frames are recorded in _verify/obd19g/obd19g_host.txt. python-obd, with its ISO 15765-4 parser, assembled the same frames into 43 responses and matched the C parser in 41 (the two mismatches are the VINs from 19г.5). Of the 15 responses that the emulator also has, 12 matched byte for byte, 3 — services 03, 07, 0A — differ in the code count byte (19г.4). The request on 29-bit identifiers 0x18DB33F1 got a response from 0x18DAF110 with the same 1303.75 rpm.


19г.7 The full code of the scan tool and the simulator

All the code of the section is three of our own files: the header obd19g.h, the core obd19g.c (ISO-TP, request, parsing, ECU simulator) and obd19g_bxcan.c (the bxCAN registers of the F103 and F407). Below they are given in full and in order, except for two pieces of obd19g.c already shown: the pid_table table (19г.3) — it sits right after obd_pid_info — and obd_dtc_str() (19г.4) — it sits after obd_pid_name(). There is no third-party code.

The check builds both files for the F103 and F407 into object files, without linking. CMSIS_F1 is the Device/Include directory of the Keil STM32F1xx_DFP 2.4.1 pack (it contains stm32f10x.h), CMSIS_CORE is CMSIS/Core/Include from CMSIS 5.9.0 (github.com/ARM-software/CMSIS_5), CMSIS_F4 is the directory Drivers/CMSIS/Device/ST/STM32F4xx/Include of the STM32F4xx_DFP 2.17.1 pack:

F1="-mcpu=cortex-m3 -mthumb -DSTM32F10X_MD -I$CMSIS_F1 -I$CMSIS_CORE"
F4="-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard -DSTM32F407xx -I$CMSIS_F4 -I$CMSIS_CORE"
for d in F1 F4; do eval fl=\$$d
  for f in obd19g obd19g_bxcan; do
    arm-none-eabi-gcc -std=c11 -Os -Wall -Wextra -Werror -ffunction-sections -fdata-sections \
      $fl -I. -c $f.c -o ${d}_$f.o
  done
done

The scan tool has no main() of its own: the board must set up the clock, SysTick at 1 ms, call obd_can_init(0) (the simulator — obd_can_init(1)) and in a loop call obd_tester_step(ms) or obd_ecu_step(&ecu). Clock and SysTick are as in 19в.7; the check does not do such a build.

The header. OBD addresses for 11 and 29 bits, the frame, the ISO-TP states, the parse result and the ECU simulator:

/* Раздел 19г — OBD-II поверх CAN: прибор (тестер), имитатор блока и маленький ISO-TP.
 * Без железа: кадр — структура; связь с bxCAN — obd19g_bxcan.c. */
#ifndef OBD19G_H
#define OBD19G_H
#include <stdint.h>

#define OBD_FUNC_ID      0x7DFu                 /* функциональный запрос: «всем блокам» */
#define OBD_REQ_ID(n)    (0x7E0u + (n))         /* физический запрос блоку n = 0…7 */
#define OBD_RSP_ID(n)    (0x7E8u + (n))         /* ответ блока n */
#define OBD_FUNC_ID29    0x18DB33F1u            /* то же для 29-битных идентификаторов */
#define OBD_RSP_ID29(a)  (0x18DAF100u | (a))    /* ответ блока с адресом a (0x10 — двигатель) */
#define OBD_REQ_ID29(a)  (0x18DA00F1u | ((uint32_t)(a) << 8))
#define OBD_PAD          0x00u                  /* чем заполнять свободные байты кадра */
#define OBD_MSG_MAX      64u                    /* длиннее ответов OBD здесь не бывает */

typedef struct {
    uint32_t id;
    uint8_t ext;                                /* 1 — 29-битный идентификатор */
    uint8_t len;
    uint8_t d[8];
} obd_frame;

/* ── ISO-TP: одиночный, первый, последующий кадр и управление потоком ── */
typedef struct {
    uint8_t buf[OBD_MSG_MAX];
    uint16_t len, pos;
    uint8_t sn;
} isotp_tx;

typedef struct {
    uint8_t buf[OBD_MSG_MAX];
    uint16_t len, pos;
    uint8_t sn, busy;
} isotp_rx;

int isotp_start(isotp_tx *t, const uint8_t *msg, uint16_t n, obd_frame *out);
int isotp_next(isotp_tx *t, obd_frame *out);
int isotp_feed(isotp_rx *r, const obd_frame *in, obd_frame *fc);

/* ── разбор ответа ── */
enum { OBD_R_NONE, OBD_R_VALUE, OBD_R_PIDS, OBD_R_DTC, OBD_R_VIN, OBD_R_NEG, OBD_R_CLEARED, OBD_R_STATUS };

typedef struct {
    uint8_t kind, mode, pid;
    int32_t milli;                              /* значение × 1000 */
    const char *unit;
    uint32_t bitmap;
    uint8_t ndtc, nrc, mil, count_byte;
    uint16_t dtc[16];
    char vin[18];
} obd_result;

int obd_request(uint8_t mode, uint8_t pid, int with_pid, obd_frame *out);
int obd_decode(const uint8_t *m, uint16_t n, obd_result *r);
void obd_dtc_str(uint16_t code, char out[6]);
const char *obd_pid_name(uint8_t pid);

/* ── имитатор блока ── */
typedef struct {
    uint32_t req_id, rsp_id;
    uint8_t ext;
    uint8_t pid_len[0x60];                      /* 0 — PID не поддержан */
    uint8_t pid_data[0x60][4];
    uint8_t freeze[0x60][4];                    /* стоп-кадр: снимок в момент ошибки */
    uint16_t freeze_dtc;
    uint16_t stored[8], pending[8], permanent[8];
    uint8_t nstored, npending, npermanent;
    const char *vin;
    isotp_tx tx;
    uint8_t wait_fc;
} obd_ecu;

void ecu_set(obd_ecu *e, uint8_t pid, uint8_t n, uint8_t a, uint8_t b, uint8_t c, uint8_t d);
int ecu_rx(obd_ecu *e, const obd_frame *in, obd_frame *out, int max);

#endif

The frame. Build-time checks and a frame template: 8 bytes, the tail is filler:

/* Раздел 19г — OBD-II: запросы, разбор ответов, коды ошибок, имитатор блока и ISO-TP в миниатюре. */
#include "obd19g.h"
#include <stddef.h>

_Static_assert(sizeof(obd_frame) == 16u, "кадр: идентификатор, флаги и 8 байт");
_Static_assert(OBD_RSP_ID(7) == 0x7EFu, "ответы блоков — 0x7E8…0x7EF");
_Static_assert(OBD_RSP_ID(0) - OBD_REQ_ID(0) == 8u, "ответ = запрос + 8");

static void frame_init(obd_frame *f, uint32_t id, uint8_t ext)
{
    f->id = id;
    f->ext = ext;
    f->len = 8u;                                /* кадр OBD всегда из 8 байт, хвост — заполнитель */
    for (unsigned i = 0; i < 8u; i++) {
        f->d[i] = OBD_PAD;
    }
}

ISO-TP in miniature. isotp_start() puts a single message into a frame or the first frame of a long one, isotp_next() produces the consecutive frames of 7 bytes, isotp_feed() reassembles what was received and asks for flow control to be sent. The transmitter ignores the block size and the gap from flow control, and the receiver asks for everything in a row without gaps: messages here are no longer than 64 bytes.

int isotp_start(isotp_tx *t, const uint8_t *msg, uint16_t n, obd_frame *out)
{
    if (n == 0u || n > OBD_MSG_MAX) {
        return -1;
    }
    for (uint16_t i = 0; i < n; i++) {
        t->buf[i] = msg[i];
    }
    t->len = n;
    if (n <= 7u) {                              /* одиночный кадр: PCI = 0x0L */
        out->d[0] = (uint8_t)n;
        for (uint16_t i = 0; i < n; i++) {
            out->d[1 + i] = msg[i];
        }
        t->pos = n;
        return 0;                               /* больше кадров не будет */
    }
    out->d[0] = (uint8_t)(0x10u | (n >> 8));    /* первый кадр: 0x1L LL + 6 байт */
    out->d[1] = (uint8_t)n;
    for (uint16_t i = 0; i < 6u; i++) {
        out->d[2 + i] = msg[i];
    }
    t->pos = 6u;
    t->sn = 1u;
    return 1;                                   /* ждём управление потоком */
}

int isotp_next(isotp_tx *t, obd_frame *out)
{
    if (t->pos >= t->len) {
        return 0;
    }
    out->d[0] = (uint8_t)(0x20u | (t->sn & 0x0Fu));  /* последующий: 0x2N + 7 байт */
    t->sn++;
    for (uint16_t i = 0; i < 7u && t->pos < t->len; i++) {
        out->d[1 + i] = t->buf[t->pos++];
    }
    return 1;
}

/* 1 — сообщение собрано; 2 — нужно послать управление потоком (fc); 0 — ждём; -1 — ошибка. */
int isotp_feed(isotp_rx *r, const obd_frame *in, obd_frame *fc)
{
    uint8_t pci = in->d[0] >> 4;

    if (pci == 0u) {
        r->len = in->d[0] & 0x0Fu;
        if (r->len == 0u || r->len > 7u) {
            return -1;
        }
        for (uint16_t i = 0; i < r->len; i++) {
            r->buf[i] = in->d[1 + i];
        }
        r->busy = 0u;
        return 1;
    }
    if (pci == 1u) {
        r->len = (uint16_t)(((in->d[0] & 0x0Fu) << 8) | in->d[1]);
        if (r->len < 8u || r->len > OBD_MSG_MAX) {
            return -1;
        }
        for (uint16_t i = 0; i < 6u; i++) {
            r->buf[i] = in->d[2 + i];
        }
        r->pos = 6u;
        r->sn = 1u;
        r->busy = 1u;
        fc->d[0] = 0x30u;                       /* «продолжай»: */
        fc->d[1] = 0x00u;                       /* блок без ограничения, */
        fc->d[2] = 0x00u;                       /* пауза между кадрами 0 мс */
        return 2;
    }
    if (pci == 2u && r->busy) {
        if ((in->d[0] & 0x0Fu) != (r->sn & 0x0Fu)) {
            r->busy = 0u;
            return -1;                          /* потерян кадр */
        }
        r->sn++;
        for (uint16_t i = 0; i < 7u && r->pos < r->len; i++) {
            r->buf[r->pos++] = in->d[1 + i];
        }
        if (r->pos == r->len) {
            r->busy = 0u;
            return 1;
        }
        return 0;
    }
    return -1;
}

The request and the PID description. A request is always a single frame to 0x7DF. In the file, the obd_pid_info description is followed by the table of formulas from 19г.3:

int obd_request(uint8_t mode, uint8_t pid, int with_pid, obd_frame *out)
{
    frame_init(out, OBD_FUNC_ID, 0u);
    out->d[0] = with_pid ? 2u : 1u;             /* одиночный кадр: длина 1 или 2 */
    out->d[1] = mode;
    out->d[2] = with_pid ? pid : OBD_PAD;
    return 0;
}

typedef struct {
    uint8_t pid, bytes;
    int32_t mul, div, offset;
    const char *unit, *name;
} obd_pid_info;
static const obd_pid_info *pid_find(uint8_t pid)
{
    for (size_t i = 0; i < sizeof pid_table / sizeof pid_table[0]; i++) {
        if (pid_table[i].pid == pid) {
            return &pid_table[i];
        }
    }
    return NULL;
}

const char *obd_pid_name(uint8_t pid)
{
    const obd_pid_info *p = pid_find(pid);
    return p ? p->name : "";
}

Parsing the response. obd_decode() recognizes the 7F refusal, the support bitmaps, the lamp state, the freeze frame, values by the table, the three code lists (with the count byte and without), clearing and the VIN — 17 characters after 49 02 01:

static uint32_t be(const uint8_t *p, unsigned n)
{
    uint32_t v = 0;
    for (unsigned i = 0; i < n; i++) {
        v = (v << 8) | p[i];
    }
    return v;
}

int obd_decode(const uint8_t *m, uint16_t n, obd_result *r)
{
    const obd_pid_info *p;
    uint16_t first;

    r->kind = OBD_R_NONE;
    if (n < 1u) {
        return -1;
    }
    r->mode = m[0];
    if (m[0] == 0x7Fu && n >= 3u) {             /* отрицательный ответ: 7F, служба, причина */
        r->kind = OBD_R_NEG;
        r->mode = m[1];
        r->nrc = m[2];
        return 0;
    }
    switch (m[0]) {
    case 0x41u:                                 /* служба 01 + 0x40 */
    case 0x42u:                                 /* служба 02: после PID — номер стоп-кадра */
        first = (m[0] == 0x42u) ? 3u : 2u;
        if (n < first + 1u) {
            return -1;
        }
        r->pid = m[1];
        if ((m[1] & 0x1Fu) == 0u && m[0] == 0x41u && n >= 6u) {
            r->kind = OBD_R_PIDS;               /* 0x00, 0x20, 0x40: карта поддержанных */
            r->bitmap = be(&m[2], 4u);
            return 0;
        }
        if (m[1] == 0x01u && m[0] == 0x41u && n >= 6u) {
            r->kind = OBD_R_STATUS;
            r->mil = m[2] >> 7;                 /* лампа «проверь двигатель» */
            r->ndtc = m[2] & 0x7Fu;             /* сколько кодов сохранено */
            return 0;
        }
        if (m[1] == 0x02u && n >= first + 2u) { /* код, из-за которого снят стоп-кадр */
            r->kind = OBD_R_DTC;
            r->dtc[0] = (uint16_t)be(&m[first], 2u);
            r->ndtc = r->dtc[0] != 0u;          /* 00 00 — стоп-кадра нет */
            return 0;
        }
        p = pid_find(m[1]);
        if (!p || n < first + p->bytes) {
            return -1;
        }
        r->kind = OBD_R_VALUE;
        r->unit = p->unit;
        r->milli = (int32_t)be(&m[first], p->bytes) * p->mul / p->div + p->offset * 1000;
        return 0;
    case 0x43u:                                 /* 03 — сохранённые */
    case 0x47u:                                 /* 07 — неподтверждённые */
    case 0x4Au:                                 /* 0A — постоянные */
        /* На CAN после службы идёт байт «сколько кодов»; длина тогда чётная.
           Без него (так отвечают некоторые адаптеры и эмуляторы) длина нечётная. */
        r->kind = OBD_R_DTC;
        r->count_byte = (n % 2u) == 0u;
        first = r->count_byte ? 2u : 1u;
        r->ndtc = 0u;
        for (uint16_t i = first; i + 1u < n && r->ndtc < 16u; i += 2u) {
            uint16_t c = (uint16_t)be(&m[i], 2u);
            if (c != 0u) {
                r->dtc[r->ndtc++] = c;
            }
        }
        return 0;
    case 0x44u:                                 /* 04 — коды стёрты */
        r->kind = OBD_R_CLEARED;
        return 0;
    case 0x49u:                                 /* 09 — сведения об автомобиле */
        r->pid = n > 1u ? m[1] : 0u;
        if (r->pid == 0x00u && n >= 6u) {
            r->kind = OBD_R_PIDS;
            r->bitmap = be(&m[2], 4u);
            return 0;
        }
        if (r->pid == 0x02u && n >= 20u) {      /* 49 02 01 + 17 знаков */
            for (unsigned i = 0; i < 17u; i++) {
                r->vin[i] = (char)m[3 + i];
            }
            r->vin[17] = '\0';
            r->kind = OBD_R_VIN;
            return 0;
        }
        return -1;
    default:
        return -1;
    }
}

ECU simulator: responses. The support bitmaps are built from the table of values, code lists are sent with the count byte, a refusal is three bytes: 7F, service, code:

void ecu_set(obd_ecu *e, uint8_t pid, uint8_t n, uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
    e->pid_len[pid] = n;
    e->pid_data[pid][0] = a;
    e->pid_data[pid][1] = b;
    e->pid_data[pid][2] = c;
    e->pid_data[pid][3] = d;
}

static uint32_t ecu_bitmap(const obd_ecu *e, uint8_t base)
{
    uint32_t map = 0;
    for (unsigned k = 1; k <= 32u; k++) {       /* PID base+1 — бит 31, base+32 — бит 0 */
        unsigned pid = base + k;
        int on = 0;
        if (k == 32u) {                         /* «есть следующая карта» — если выше что-то есть */
            for (unsigned q = pid + 1u; q < 0x60u; q++) {
                on |= e->pid_len[q] != 0u;
            }
        } else if (pid < 0x60u) {
            on = e->pid_len[pid] != 0u;
        }
        if (on) {
            map |= 1uL << (32u - k);
        }
    }
    return map;
}

static int ecu_send(obd_ecu *e, const uint8_t *msg, uint16_t n, obd_frame *out)
{
    frame_init(out, e->rsp_id, e->ext);
    e->wait_fc = (uint8_t)(isotp_start(&e->tx, msg, n, out) == 1);
    return 1;
}

static int ecu_dtc_list(obd_ecu *e, uint8_t sid, const uint16_t *list, uint8_t cnt, obd_frame *out)
{
    uint8_t m[2u + 2u * 8u];
    m[0] = sid;
    m[1] = cnt;                                 /* байт «сколько кодов» */
    for (uint8_t i = 0; i < cnt; i++) {
        m[2 + 2 * i] = (uint8_t)(list[i] >> 8);
        m[3 + 2 * i] = (uint8_t)list[i];
    }
    return ecu_send(e, m, (uint16_t)(2u + 2u * cnt), out);
}

static int ecu_negative(obd_ecu *e, uint8_t sid, uint8_t nrc, obd_frame *out)
{
    const uint8_t m[3] = { 0x7Fu, sid, nrc };
    return ecu_send(e, m, 3u, out);
}

ECU simulator: parsing the request. A flow control frame — send the rest; otherwise a single frame with service 01, 02, 03, 04, 07, 09 or 0A. To a functional request with an unknown service the ECU stays silent, to a physical one — 7F:

/* Возвращает число кадров ответа в out[]. */
int ecu_rx(obd_ecu *e, const obd_frame *in, obd_frame *out, int max)
{
    uint8_t m[OBD_MSG_MAX];
    const uint8_t *q = &in->d[1];
    uint8_t sid, pid, n;

    if (in->ext != e->ext || (in->id != e->req_id && in->id != (e->ext ? OBD_FUNC_ID29 : OBD_FUNC_ID))) {
        return 0;                               /* не нам */
    }
    if ((in->d[0] & 0xF0u) == 0x30u) {         /* управление потоком: досылаем остальное */
        int k = 0;
        if (in->id != e->req_id || !e->wait_fc) {
            return 0;
        }
        e->wait_fc = 0u;
        while (k < max) {
            frame_init(&out[k], e->rsp_id, e->ext);
            if (!isotp_next(&e->tx, &out[k])) {
                break;
            }
            k++;
        }
        return k;
    }
    n = in->d[0];
    if ((n & 0xF0u) != 0u || n == 0u || n > 7u) {
        return 0;                               /* запросы OBD — только одиночные кадры */
    }
    sid = q[0];
    pid = n > 1u ? q[1] : 0u;
    switch (sid) {
    case 0x01u:
    case 0x02u:
        if (pid >= 0x60u || (sid == 0x02u && n < 3u)) {
            return 0;
        }
        m[0] = (uint8_t)(sid + 0x40u);
        m[1] = pid;
        n = 2u;
        if (sid == 0x02u) {
            m[n++] = q[2];                      /* номер стоп-кадра, у нас один — 0 */
            if (pid == 0x02u) {
                m[n++] = (uint8_t)(e->freeze_dtc >> 8);
                m[n++] = (uint8_t)e->freeze_dtc;
                return ecu_send(e, m, n, out);
            }
            if (q[2] != 0u || e->pid_len[pid] == 0u || (pid & 0x1Fu) == 0u) {
                return ecu_negative(e, sid, 0x12u, out);   /* такого в стоп-кадре нет */
            }
            for (uint8_t i = 0; i < e->pid_len[pid]; i++) {
                m[n++] = e->freeze[pid][i];
            }
            return ecu_send(e, m, n, out);
        }
        if ((pid & 0x1Fu) == 0u && pid < 0x60u) {           /* 0x00, 0x20, 0x40 */
            uint32_t map = ecu_bitmap(e, pid);
            if (pid != 0u && e->pid_len[pid] == 0u) {
                return 0;
            }
            m[2] = (uint8_t)(map >> 24);
            m[3] = (uint8_t)(map >> 16);
            m[4] = (uint8_t)(map >> 8);
            m[5] = (uint8_t)map;
            return ecu_send(e, m, 6u, out);
        }
        if (pid == 0x01u) {                     /* лампа и число кодов — из списка */
            m[2] = (uint8_t)((e->nstored ? 0x80u : 0u) | e->nstored);
            m[3] = e->pid_data[1][1];
            m[4] = e->pid_data[1][2];
            m[5] = e->pid_data[1][3];
            return ecu_send(e, m, 6u, out);
        }
        if (e->pid_len[pid] == 0u) {
            return 0;                           /* не поддержан — молчим */
        }
        for (uint8_t i = 0; i < e->pid_len[pid]; i++) {
            m[n++] = e->pid_data[pid][i];
        }
        return ecu_send(e, m, n, out);
    case 0x03u:
        return ecu_dtc_list(e, 0x43u, e->stored, e->nstored, out);
    case 0x07u:
        return ecu_dtc_list(e, 0x47u, e->pending, e->npending, out);
    case 0x0Au:
        return ecu_dtc_list(e, 0x4Au, e->permanent, e->npermanent, out);
    case 0x04u:
        if (e->pid_data[0x0C][0] | e->pid_data[0x0C][1]) {
            return ecu_negative(e, sid, 0x22u, out);        /* двигатель крутится — не стираем */
        }
        e->nstored = 0u;                        /* стираются сохранённые, неподтверждённые */
        e->npending = 0u;                       /* и стоп-кадр; постоянные остаются */
        e->freeze_dtc = 0u;
        m[0] = 0x44u;
        return ecu_send(e, m, 1u, out);
    case 0x09u:
        m[0] = 0x49u;
        m[1] = pid;
        if (pid == 0x00u && e->vin) {
            m[2] = 0x40u;                       /* поддержан только 0x02 — VIN */
            m[3] = m[4] = m[5] = 0u;
            return ecu_send(e, m, 6u, out);
        }
        if (pid == 0x02u && e->vin) {
            m[2] = 1u;                          /* одна запись */
            for (unsigned i = 0; i < 17u; i++) {
                m[3 + i] = (uint8_t)e->vin[i];
            }
            return ecu_send(e, m, 20u, out);    /* 20 байт — три кадра */
        }
        return 0;
    default:
        if (in->id == e->req_id) {
            return ecu_negative(e, sid, 0x11u, out);        /* служба не поддержана */
        }
        return 0;                               /* на общий запрос — молчим */
    }
}

bxCAN: timing and filters. The timing numbers for the two chips and _Static_assert checks that will not let wrong dividers build. The scan tool gets a mask on 0x7E8…0x7EF, the simulator — a list of 0x7DF and 0x7E0:

/* Раздел 19г — OBD-II на bxCAN (F103 и F407): тайминг 500 кбит/с, фильтры, прибор и имитатор блока. */
#if defined(STM32F10X_MD)
#include "stm32f10x.h"
#define APB1_HZ 36000000u                       /* F103: APB1 = 72 МГц / 2 */
#define CAN_BRP 9u
#define CAN_TS1 6u
#define CAN_TS2 1u
#else
#include "stm32f4xx.h"
#define APB1_HZ 42000000u                       /* F407: APB1 = 168 МГц / 4 */
#define CAN_BRP 6u
#define CAN_TS1 11u
#define CAN_TS2 2u
#endif
#include "obd19g.h"

#define OBD_BITRATE 500000u
#define CAN_SJW     1u
#define QUANTA      (1u + CAN_TS1 + CAN_TS2)
#define WAIT_MS     150u                        /* сколько ждать ответа */

_Static_assert(APB1_HZ % (CAN_BRP * QUANTA) == 0u && APB1_HZ / (CAN_BRP * QUANTA) == OBD_BITRATE,
               "ровно 500 кбит/с");
_Static_assert(CAN_TS1 <= 16u && CAN_TS2 <= 8u && CAN_SJW <= CAN_TS2 && QUANTA >= 8u && QUANTA <= 25u,
               "поля BTR в допустимых пределах");
#if defined(STM32F10X_MD)
_Static_assert((1u + CAN_TS1) * 1000u / QUANTA == 875u, "F103: точка выборки 87,5 %");
#else
_Static_assert((1u + CAN_TS1) * 1000u / QUANTA == 857u, "F407: точка выборки 85,7 %");
#endif

void obd_can_init(int ecu_mode)
{
    RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
#if defined(STM32F10X_MD)
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
    GPIOA->CRH = (GPIOA->CRH & ~0x000FF000u) | 0x000B8000u;  /* PA11 — вход с подтяжкой, PA12 — CAN_TX */
    GPIOA->ODR |= 1u << 11;
#else
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    GPIOA->MODER = (GPIOA->MODER & ~(0xFu << 22)) | (0xAu << 22);          /* PA11, PA12 — альтернативная */
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~(0xFFu << 12)) | (0x99u << 12);      /* AF9 — CAN1 */
#endif
    CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM;    /* режим настройки; сам выходит из «шина отключена» */
    while ((CAN1->MSR & CAN_MSR_INAK) == 0u) {
    }
    CAN1->BTR = ((CAN_SJW - 1u) << 24) | ((CAN_TS2 - 1u) << 20) | ((CAN_TS1 - 1u) << 16) | (CAN_BRP - 1u);

    CAN1->FMR |= CAN_FMR_FINIT;
    CAN1->FA1R &= ~1u;
    if (ecu_mode) {
        /* имитатор блока: список из 16-битных фильтров — 0x7DF и 0x7E0 */
        CAN1->FS1R &= ~1u;
        CAN1->FM1R |= 1u;
        CAN1->sFilterRegister[0].FR1 = (OBD_REQ_ID(0) << 21) | (OBD_FUNC_ID << 5);
        CAN1->sFilterRegister[0].FR2 = (OBD_REQ_ID(0) << 21) | (OBD_REQ_ID(0) << 5);
    } else {
        /* прибор: 32-битная маска — 0x7E8…0x7EF, только стандартные кадры данных */
        CAN1->FS1R |= 1u;
        CAN1->FM1R &= ~1u;
        CAN1->sFilterRegister[0].FR1 = OBD_RSP_ID(0) << 21;
        CAN1->sFilterRegister[0].FR2 = (0x7F8u << 21) | CAN_RI0R_IDE | CAN_RI0R_RTR;
    }
    CAN1->FFA1R &= ~1u;
    CAN1->FA1R |= 1u;
    CAN1->FMR &= ~CAN_FMR_FINIT;

    CAN1->MCR &= ~CAN_MCR_INRQ;                 /* на шину */
    while ((CAN1->MSR & CAN_MSR_INAK) != 0u) {
    }
}

Sending and receiving without interrupts. A free mailbox out of three, or a refusal; reception — by polling FIFO 0:

int obd_can_send(const obd_frame *f)
{
    uint32_t box;
    if (CAN1->TSR & CAN_TSR_TME0) {
        box = 0u;
    } else if (CAN1->TSR & CAN_TSR_TME1) {
        box = 1u;
    } else if (CAN1->TSR & CAN_TSR_TME2) {
        box = 2u;
    } else {
        return -1;                              /* все три ящика заняты */
    }
    CAN1->sTxMailBox[box].TDTR = f->len;
    CAN1->sTxMailBox[box].TDLR = (uint32_t)f->d[0] | ((uint32_t)f->d[1] << 8)
                               | ((uint32_t)f->d[2] << 16) | ((uint32_t)f->d[3] << 24);
    CAN1->sTxMailBox[box].TDHR = (uint32_t)f->d[4] | ((uint32_t)f->d[5] << 8)
                               | ((uint32_t)f->d[6] << 16) | ((uint32_t)f->d[7] << 24);
    CAN1->sTxMailBox[box].TIR = f->ext ? ((f->id << 3) | CAN_TI0R_IDE | CAN_TI0R_TXRQ)
                                       : ((f->id << 21) | CAN_TI0R_TXRQ);
    return 0;
}

int obd_can_read(obd_frame *f)
{
    uint32_t rir, lo, hi;
    if ((CAN1->RF0R & CAN_RF0R_FMP0) == 0u) {
        return 0;
    }
    rir = CAN1->sFIFOMailBox[0].RIR;
    f->ext = (uint8_t)((rir & CAN_RI0R_IDE) != 0u);
    f->id = f->ext ? (rir >> 3) : (rir >> 21);
    f->len = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0xFu);
    lo = CAN1->sFIFOMailBox[0].RDLR;
    hi = CAN1->sFIFOMailBox[0].RDHR;
    CAN1->RF0R |= CAN_RF0R_RFOM0;               /* освободить место в очереди */
    for (uint32_t i = 0; i < 4u; i++) {
        f->d[i] = (uint8_t)(lo >> (8u * i));
        f->d[i + 4u] = (uint8_t)(hi >> (8u * i));
    }
    return 1;
}

The scan tool. One request in flight, 150 ms for the response, engine speed, vehicle speed and temperature in a round; the values go to obd_live, in thousandths:

/* ── прибор: по кругу спрашивает обороты, скорость и температуру ── */
static const uint8_t poll_pids[] = { 0x0Cu, 0x0Du, 0x05u };
volatile int32_t obd_live[sizeof poll_pids];    /* значения × 1000; смотреть отладчиком */
static isotp_rx rxs[8];
static uint32_t sent_at, next_pid;
static int waiting;

void obd_tester_step(uint32_t now_ms)
{
    obd_frame f, fc;
    obd_result r;

    if (waiting && now_ms - sent_at >= WAIT_MS) {   /* ответа нет — следующий PID */
        waiting = 0;
        next_pid = (next_pid + 1u) % sizeof poll_pids;
    }
    if (!waiting) {
        obd_request(0x01u, poll_pids[next_pid], 1, &f);
        if (obd_can_send(&f) == 0) {
            sent_at = now_ms;
            waiting = 1;
        }
    }
    while (obd_can_read(&f)) {
        isotp_rx *rx = &rxs[f.id - OBD_RSP_ID(0)];  /* фильтр пропускает только 0x7E8…0x7EF */
        fc.id = f.id - 8u;                      /* управление потоком — на физический адрес */
        fc.ext = 0u;
        fc.len = 8u;
        for (uint32_t i = 3u; i < 8u; i++) {
            fc.d[i] = OBD_PAD;
        }
        switch (isotp_feed(rx, &f, &fc)) {
        case 2:
            (void)obd_can_send(&fc);
            break;
        case 1:
            if (obd_decode(rx->buf, rx->len, &r) == 0 && r.kind == OBD_R_VALUE
                && r.pid == poll_pids[next_pid]) {
                obd_live[next_pid] = r.milli;
                next_pid = (next_pid + 1u) % sizeof poll_pids;
                waiting = 0;
            }
            break;
        default:
            break;
        }
    }
}

The simulator on the board. Everything that arrives goes to ecu_rx(), the response frames go into mailboxes, waiting for a free one:

/* ── имитатор блока для стола: отвечает на 0x7DF и 0x7E0 ── */
void obd_ecu_step(obd_ecu *e)
{
    obd_frame in, out[4];
    while (obd_can_read(&in)) {
        int k = ecu_rx(e, &in, out, 4);
        for (int i = 0; i < k; i++) {
            while (obd_can_send(&out[i]) != 0) {
            }                                   /* ждём свободный ящик */
        }
    }
}

19г.8 Pitfalls

Clearing codes while driving. Service 04 is not a harmless button: it clears the codes and the freeze frame, and with them the trace of the fault that will be used to find it later. Never send 04 while the vehicle is moving and never build it into automatic polling. Do not rely on the ECU refusing by itself: our simulator answers 7F 04 22 with the engine running, but the emulator at 1303.75 rpm cleared everything and answered 44.

The scan tool is powered from socket 16. That is +12 V from the battery, not 3.3 V. Power the board only through a regulator. Poll with the ignition on: otherwise the ECU may stay silent, and the scan tool will see the same as our run did for an unsupported PID — silence.

Your own terminator on the car's bus. In a car the bus is already assembled, the 120 Ω resistors sit at its ends (section 19.2). The scan tool is an intermediate node: remove the terminator on the board with the jumper. On the bench, with the simulator, it is the opposite — both are needed.

Flow control went to 0x7DF. The first VIN frame arrived, then silence. The ECU waits for flow control at its physical address: response from 0x7E8 — so, to 0x7E0.

Codes with and without the count byte. On the emulator's response without the count byte, python-obd read P3303 instead of three real codes. Check the length: even — the count byte is there, odd — it is not.

Percentages in floating point. The F103 has no floating-point unit: thousandths and the integer table from 19г.3 give the same numbers with a precision of 0.001 and without a floating-point division library.

The wrong sample point. The firmware of the canable adapter (STM32F042, 48 MHz) uses divider 12 and quanta 1 + 4 + 3 for 500 kbit/s: the bit is the same 2 µs, but the sample point is 62.5 %. The adapter works, but it is not a model to follow: for your own board take the point from section 19.4 — 87.5 %, as in 19г.6.


19г.9 Summary

Full sources

obd19g.c — OBD-II requests, response parsing, trouble codes, the ECU simulator and ISO-TP in miniature.

/* Раздел 19г — OBD-II: запросы, разбор ответов, коды ошибок, имитатор блока и ISO-TP в миниатюре. */
#include "obd19g.h"
#include <stddef.h>

_Static_assert(sizeof(obd_frame) == 16u, "кадр: идентификатор, флаги и 8 байт");
_Static_assert(OBD_RSP_ID(7) == 0x7EFu, "ответы блоков — 0x7E8…0x7EF");
_Static_assert(OBD_RSP_ID(0) - OBD_REQ_ID(0) == 8u, "ответ = запрос + 8");

static void frame_init(obd_frame *f, uint32_t id, uint8_t ext)
{
    f->id = id;
    f->ext = ext;
    f->len = 8u;                                /* кадр OBD всегда из 8 байт, хвост — заполнитель */
    for (unsigned i = 0; i < 8u; i++) {
        f->d[i] = OBD_PAD;
    }
}

/* ══ ISO-TP ═══════════════════════════════════════════════════════════ */

int isotp_start(isotp_tx *t, const uint8_t *msg, uint16_t n, obd_frame *out)
{
    if (n == 0u || n > OBD_MSG_MAX) {
        return -1;
    }
    for (uint16_t i = 0; i < n; i++) {
        t->buf[i] = msg[i];
    }
    t->len = n;
    if (n <= 7u) {                              /* одиночный кадр: PCI = 0x0L */
        out->d[0] = (uint8_t)n;
        for (uint16_t i = 0; i < n; i++) {
            out->d[1 + i] = msg[i];
        }
        t->pos = n;
        return 0;                               /* больше кадров не будет */
    }
    out->d[0] = (uint8_t)(0x10u | (n >> 8));    /* первый кадр: 0x1L LL + 6 байт */
    out->d[1] = (uint8_t)n;
    for (uint16_t i = 0; i < 6u; i++) {
        out->d[2 + i] = msg[i];
    }
    t->pos = 6u;
    t->sn = 1u;
    return 1;                                   /* ждём управление потоком */
}

int isotp_next(isotp_tx *t, obd_frame *out)
{
    if (t->pos >= t->len) {
        return 0;
    }
    out->d[0] = (uint8_t)(0x20u | (t->sn & 0x0Fu));  /* последующий: 0x2N + 7 байт */
    t->sn++;
    for (uint16_t i = 0; i < 7u && t->pos < t->len; i++) {
        out->d[1 + i] = t->buf[t->pos++];
    }
    return 1;
}

/* 1 — сообщение собрано; 2 — нужно послать управление потоком (fc); 0 — ждём; -1 — ошибка. */
int isotp_feed(isotp_rx *r, const obd_frame *in, obd_frame *fc)
{
    uint8_t pci = in->d[0] >> 4;

    if (pci == 0u) {
        r->len = in->d[0] & 0x0Fu;
        if (r->len == 0u || r->len > 7u) {
            return -1;
        }
        for (uint16_t i = 0; i < r->len; i++) {
            r->buf[i] = in->d[1 + i];
        }
        r->busy = 0u;
        return 1;
    }
    if (pci == 1u) {
        r->len = (uint16_t)(((in->d[0] & 0x0Fu) << 8) | in->d[1]);
        if (r->len < 8u || r->len > OBD_MSG_MAX) {
            return -1;
        }
        for (uint16_t i = 0; i < 6u; i++) {
            r->buf[i] = in->d[2 + i];
        }
        r->pos = 6u;
        r->sn = 1u;
        r->busy = 1u;
        fc->d[0] = 0x30u;                       /* «продолжай»: */
        fc->d[1] = 0x00u;                       /* блок без ограничения, */
        fc->d[2] = 0x00u;                       /* пауза между кадрами 0 мс */
        return 2;
    }
    if (pci == 2u && r->busy) {
        if ((in->d[0] & 0x0Fu) != (r->sn & 0x0Fu)) {
            r->busy = 0u;
            return -1;                          /* потерян кадр */
        }
        r->sn++;
        for (uint16_t i = 0; i < 7u && r->pos < r->len; i++) {
            r->buf[r->pos++] = in->d[1 + i];
        }
        if (r->pos == r->len) {
            r->busy = 0u;
            return 1;
        }
        return 0;
    }
    return -1;
}

/* ══ Запрос и разбор ══════════════════════════════════════════════════ */

int obd_request(uint8_t mode, uint8_t pid, int with_pid, obd_frame *out)
{
    frame_init(out, OBD_FUNC_ID, 0u);
    out->d[0] = with_pid ? 2u : 1u;             /* одиночный кадр: длина 1 или 2 */
    out->d[1] = mode;
    out->d[2] = with_pid ? pid : OBD_PAD;
    return 0;
}

typedef struct {
    uint8_t pid, bytes;
    int32_t mul, div, offset;
    const char *unit, *name;
} obd_pid_info;

static const obd_pid_info pid_table[] = {
    /* PID  байт  умножить  делить  сдвиг   единица  что это */
    { 0x04u, 1u, 100000,   255,     0,     "%",     "нагрузка двигателя" },   /* A·100/255 */
    { 0x05u, 1u, 1000,     1,     -40,     "°C",    "охлаждающая жидкость" }, /* A − 40 */
    { 0x0Bu, 1u, 1000,     1,       0,     "кПа",   "давление во впуске" },   /* A */
    { 0x0Cu, 2u, 250,      1,       0,     "об/мин","обороты" },              /* (256A + B)/4 */
    { 0x0Du, 1u, 1000,     1,       0,     "км/ч",  "скорость" },             /* A */
    { 0x0Fu, 1u, 1000,     1,     -40,     "°C",    "воздух на впуске" },     /* A − 40 */
    { 0x10u, 2u, 10,       1,       0,     "г/с",   "расход воздуха" },       /* (256A + B)/100 */
    { 0x11u, 1u, 100000,   255,     0,     "%",     "дроссель" },             /* A·100/255 */
    { 0x2Fu, 1u, 100000,   255,     0,     "%",     "уровень топлива" },      /* A·100/255 */
    { 0x42u, 2u, 1,        1,       0,     "В",     "напряжение блока" },     /* (256A + B)/1000 */
};

static const obd_pid_info *pid_find(uint8_t pid)
{
    for (size_t i = 0; i < sizeof pid_table / sizeof pid_table[0]; i++) {
        if (pid_table[i].pid == pid) {
            return &pid_table[i];
        }
    }
    return NULL;
}

const char *obd_pid_name(uint8_t pid)
{
    const obd_pid_info *p = pid_find(pid);
    return p ? p->name : "";
}

void obd_dtc_str(uint16_t code, char out[6])
{
    static const char hex[] = "0123456789ABCDEF";
    out[0] = "PCBU"[code >> 14];                /* биты 15:14 — система */
    out[1] = (char)('0' + ((code >> 12) & 3u)); /* биты 13:12 — 0…3 */
    out[2] = hex[(code >> 8) & 0xFu];
    out[3] = hex[(code >> 4) & 0xFu];
    out[4] = hex[code & 0xFu];
    out[5] = '\0';
}

static uint32_t be(const uint8_t *p, unsigned n)
{
    uint32_t v = 0;
    for (unsigned i = 0; i < n; i++) {
        v = (v << 8) | p[i];
    }
    return v;
}

int obd_decode(const uint8_t *m, uint16_t n, obd_result *r)
{
    const obd_pid_info *p;
    uint16_t first;

    r->kind = OBD_R_NONE;
    if (n < 1u) {
        return -1;
    }
    r->mode = m[0];
    if (m[0] == 0x7Fu && n >= 3u) {             /* отрицательный ответ: 7F, служба, причина */
        r->kind = OBD_R_NEG;
        r->mode = m[1];
        r->nrc = m[2];
        return 0;
    }
    switch (m[0]) {
    case 0x41u:                                 /* служба 01 + 0x40 */
    case 0x42u:                                 /* служба 02: после PID — номер стоп-кадра */
        first = (m[0] == 0x42u) ? 3u : 2u;
        if (n < first + 1u) {
            return -1;
        }
        r->pid = m[1];
        if ((m[1] & 0x1Fu) == 0u && m[0] == 0x41u && n >= 6u) {
            r->kind = OBD_R_PIDS;               /* 0x00, 0x20, 0x40: карта поддержанных */
            r->bitmap = be(&m[2], 4u);
            return 0;
        }
        if (m[1] == 0x01u && m[0] == 0x41u && n >= 6u) {
            r->kind = OBD_R_STATUS;
            r->mil = m[2] >> 7;                 /* лампа «проверь двигатель» */
            r->ndtc = m[2] & 0x7Fu;             /* сколько кодов сохранено */
            return 0;
        }
        if (m[1] == 0x02u && n >= first + 2u) { /* код, из-за которого снят стоп-кадр */
            r->kind = OBD_R_DTC;
            r->dtc[0] = (uint16_t)be(&m[first], 2u);
            r->ndtc = r->dtc[0] != 0u;          /* 00 00 — стоп-кадра нет */
            return 0;
        }
        p = pid_find(m[1]);
        if (!p || n < first + p->bytes) {
            return -1;
        }
        r->kind = OBD_R_VALUE;
        r->unit = p->unit;
        r->milli = (int32_t)be(&m[first], p->bytes) * p->mul / p->div + p->offset * 1000;
        return 0;
    case 0x43u:                                 /* 03 — сохранённые */
    case 0x47u:                                 /* 07 — неподтверждённые */
    case 0x4Au:                                 /* 0A — постоянные */
        /* На CAN после службы идёт байт «сколько кодов»; длина тогда чётная.
           Без него (так отвечают некоторые адаптеры и эмуляторы) длина нечётная. */
        r->kind = OBD_R_DTC;
        r->count_byte = (n % 2u) == 0u;
        first = r->count_byte ? 2u : 1u;
        r->ndtc = 0u;
        for (uint16_t i = first; i + 1u < n && r->ndtc < 16u; i += 2u) {
            uint16_t c = (uint16_t)be(&m[i], 2u);
            if (c != 0u) {
                r->dtc[r->ndtc++] = c;
            }
        }
        return 0;
    case 0x44u:                                 /* 04 — коды стёрты */
        r->kind = OBD_R_CLEARED;
        return 0;
    case 0x49u:                                 /* 09 — сведения об автомобиле */
        r->pid = n > 1u ? m[1] : 0u;
        if (r->pid == 0x00u && n >= 6u) {
            r->kind = OBD_R_PIDS;
            r->bitmap = be(&m[2], 4u);
            return 0;
        }
        if (r->pid == 0x02u && n >= 20u) {      /* 49 02 01 + 17 знаков */
            for (unsigned i = 0; i < 17u; i++) {
                r->vin[i] = (char)m[3 + i];
            }
            r->vin[17] = '\0';
            r->kind = OBD_R_VIN;
            return 0;
        }
        return -1;
    default:
        return -1;
    }
}

/* ══ Имитатор блока ═══════════════════════════════════════════════════ */

void ecu_set(obd_ecu *e, uint8_t pid, uint8_t n, uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
    e->pid_len[pid] = n;
    e->pid_data[pid][0] = a;
    e->pid_data[pid][1] = b;
    e->pid_data[pid][2] = c;
    e->pid_data[pid][3] = d;
}

static uint32_t ecu_bitmap(const obd_ecu *e, uint8_t base)
{
    uint32_t map = 0;
    for (unsigned k = 1; k <= 32u; k++) {       /* PID base+1 — бит 31, base+32 — бит 0 */
        unsigned pid = base + k;
        int on = 0;
        if (k == 32u) {                         /* «есть следующая карта» — если выше что-то есть */
            for (unsigned q = pid + 1u; q < 0x60u; q++) {
                on |= e->pid_len[q] != 0u;
            }
        } else if (pid < 0x60u) {
            on = e->pid_len[pid] != 0u;
        }
        if (on) {
            map |= 1uL << (32u - k);
        }
    }
    return map;
}

static int ecu_send(obd_ecu *e, const uint8_t *msg, uint16_t n, obd_frame *out)
{
    frame_init(out, e->rsp_id, e->ext);
    e->wait_fc = (uint8_t)(isotp_start(&e->tx, msg, n, out) == 1);
    return 1;
}

static int ecu_dtc_list(obd_ecu *e, uint8_t sid, const uint16_t *list, uint8_t cnt, obd_frame *out)
{
    uint8_t m[2u + 2u * 8u];
    m[0] = sid;
    m[1] = cnt;                                 /* байт «сколько кодов» */
    for (uint8_t i = 0; i < cnt; i++) {
        m[2 + 2 * i] = (uint8_t)(list[i] >> 8);
        m[3 + 2 * i] = (uint8_t)list[i];
    }
    return ecu_send(e, m, (uint16_t)(2u + 2u * cnt), out);
}

static int ecu_negative(obd_ecu *e, uint8_t sid, uint8_t nrc, obd_frame *out)
{
    const uint8_t m[3] = { 0x7Fu, sid, nrc };
    return ecu_send(e, m, 3u, out);
}

/* Возвращает число кадров ответа в out[]. */
int ecu_rx(obd_ecu *e, const obd_frame *in, obd_frame *out, int max)
{
    uint8_t m[OBD_MSG_MAX];
    const uint8_t *q = &in->d[1];
    uint8_t sid, pid, n;

    if (in->ext != e->ext || (in->id != e->req_id && in->id != (e->ext ? OBD_FUNC_ID29 : OBD_FUNC_ID))) {
        return 0;                               /* не нам */
    }
    if ((in->d[0] & 0xF0u) == 0x30u) {         /* управление потоком: досылаем остальное */
        int k = 0;
        if (in->id != e->req_id || !e->wait_fc) {
            return 0;
        }
        e->wait_fc = 0u;
        while (k < max) {
            frame_init(&out[k], e->rsp_id, e->ext);
            if (!isotp_next(&e->tx, &out[k])) {
                break;
            }
            k++;
        }
        return k;
    }
    n = in->d[0];
    if ((n & 0xF0u) != 0u || n == 0u || n > 7u) {
        return 0;                               /* запросы OBD — только одиночные кадры */
    }
    sid = q[0];
    pid = n > 1u ? q[1] : 0u;
    switch (sid) {
    case 0x01u:
    case 0x02u:
        if (pid >= 0x60u || (sid == 0x02u && n < 3u)) {
            return 0;
        }
        m[0] = (uint8_t)(sid + 0x40u);
        m[1] = pid;
        n = 2u;
        if (sid == 0x02u) {
            m[n++] = q[2];                      /* номер стоп-кадра, у нас один — 0 */
            if (pid == 0x02u) {
                m[n++] = (uint8_t)(e->freeze_dtc >> 8);
                m[n++] = (uint8_t)e->freeze_dtc;
                return ecu_send(e, m, n, out);
            }
            if (q[2] != 0u || e->pid_len[pid] == 0u || (pid & 0x1Fu) == 0u) {
                return ecu_negative(e, sid, 0x12u, out);   /* такого в стоп-кадре нет */
            }
            for (uint8_t i = 0; i < e->pid_len[pid]; i++) {
                m[n++] = e->freeze[pid][i];
            }
            return ecu_send(e, m, n, out);
        }
        if ((pid & 0x1Fu) == 0u && pid < 0x60u) {           /* 0x00, 0x20, 0x40 */
            uint32_t map = ecu_bitmap(e, pid);
            if (pid != 0u && e->pid_len[pid] == 0u) {
                return 0;
            }
            m[2] = (uint8_t)(map >> 24);
            m[3] = (uint8_t)(map >> 16);
            m[4] = (uint8_t)(map >> 8);
            m[5] = (uint8_t)map;
            return ecu_send(e, m, 6u, out);
        }
        if (pid == 0x01u) {                     /* лампа и число кодов — из списка */
            m[2] = (uint8_t)((e->nstored ? 0x80u : 0u) | e->nstored);
            m[3] = e->pid_data[1][1];
            m[4] = e->pid_data[1][2];
            m[5] = e->pid_data[1][3];
            return ecu_send(e, m, 6u, out);
        }
        if (e->pid_len[pid] == 0u) {
            return 0;                           /* не поддержан — молчим */
        }
        for (uint8_t i = 0; i < e->pid_len[pid]; i++) {
            m[n++] = e->pid_data[pid][i];
        }
        return ecu_send(e, m, n, out);
    case 0x03u:
        return ecu_dtc_list(e, 0x43u, e->stored, e->nstored, out);
    case 0x07u:
        return ecu_dtc_list(e, 0x47u, e->pending, e->npending, out);
    case 0x0Au:
        return ecu_dtc_list(e, 0x4Au, e->permanent, e->npermanent, out);
    case 0x04u:
        if (e->pid_data[0x0C][0] | e->pid_data[0x0C][1]) {
            return ecu_negative(e, sid, 0x22u, out);        /* двигатель крутится — не стираем */
        }
        e->nstored = 0u;                        /* стираются сохранённые, неподтверждённые */
        e->npending = 0u;                       /* и стоп-кадр; постоянные остаются */
        e->freeze_dtc = 0u;
        m[0] = 0x44u;
        return ecu_send(e, m, 1u, out);
    case 0x09u:
        m[0] = 0x49u;
        m[1] = pid;
        if (pid == 0x00u && e->vin) {
            m[2] = 0x40u;                       /* поддержан только 0x02 — VIN */
            m[3] = m[4] = m[5] = 0u;
            return ecu_send(e, m, 6u, out);
        }
        if (pid == 0x02u && e->vin) {
            m[2] = 1u;                          /* одна запись */
            for (unsigned i = 0; i < 17u; i++) {
                m[3 + i] = (uint8_t)e->vin[i];
            }
            return ecu_send(e, m, 20u, out);    /* 20 байт — три кадра */
        }
        return 0;
    default:
        if (in->id == e->req_id) {
            return ecu_negative(e, sid, 0x11u, out);        /* служба не поддержана */
        }
        return 0;                               /* на общий запрос — молчим */
    }
}

Next: section 19д "UDS (ISO 14229): sessions, reading data, trouble codes and flashing through diagnostics".

Open in the interactive handbook