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.
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
- The J1962 connector: CAN-H — 6, CAN-L — 14, +12 V — 16, grounds — 4 and 5; ELM327 protocol 6 is ISO 15765-4, 11 bits, 500 kbit/s.
- Functional request — 0x7DF, responses — 0x7E8…0x7EF, physical addresses — 0x7E0…0x7E7; with 29 bits — 0x18DB33F1 and 0x18DAF110.
- Response = service + 0x40; refusal — 0x7F, service, reason (0x11, 0x12, 0x22).
- PID bitmaps 0x00, 0x20, 0x40 — 32 bits each, the most significant bit is the first PID of the group.
- Engine speed (256A + B)/4, vehicle speed A, temperatures A − 40, percentages A·100/255, air flow (256A + B)/100, voltage (256A + B)/1000 — cross-checked with python-obd on all 198400 raw values.
- A trouble code is 2 bytes: 0x0133 → P0133, 0xC100 → U0100.
- The VIN is 20 bytes, three ISO-TP frames; flow control goes to 0x7E0.
- 500 kbit/s on the F103: divider 9, 8 quanta, 87.5 %; on the F407: 6 and 14, 85.7 %. The core of the scan tool and simulator is 2518 bytes of code.
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".