Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407 · уровень средний · 49 мин чтения
Открыть в интерактивном справочнике

19г. OBD-II: диагностика автомобиля по CAN — режимы, PID и коды ошибок

Раздел 19 научил bxCAN принимать и слать кадры, раздел 19в — собирать из них длинные сообщения по ISO-TP. Теперь — самое практичное применение CAN: разъём под рулём любой современной машины. Через него сканер узнаёт обороты, температуру и скорость, читает и стирает коды ошибок, достаёт номер кузова (VIN). Всё это — OBD-II, и прибор для него умещается в STM32F103 с трансивером.

Здесь — разъём и протоколы, как устроены запрос и ответ, какие есть службы (их ещё называют режимами), как узнать, что блок умеет, и перевести байты в обороты и градусы, как закодированы коды ошибок и как VIN приходит тремя кадрами. В конце — прибор и имитатор блока на C для bxCAN F103 и F407.

Опоры: тексты стандартов SAE J1979 и ISO 15765-4 не использовались, поэтому всё сверено с тремя открытыми проектами. Формулы и разбор кадров — по python-obd 0.7.3 (obd/decoders.py, obd/commands.py, obd/protocols/protocol_can.py). Живые ответы — эмулятор ELM327 4.0.0 (ELM327-emulator, сценарий «car»). Сторона блока — прошивка rusEFI (firmware/controllers/can/obd2.cpp): она сама отвечает на запросы OBD. Коды причин отказа — из библиотеки iso14229. Лицензии: python-obd — GPL v2, ELM327-emulator — CC BY-NC-SA 4.0, rusEFI — GPL v3, iso14229 — MIT; их код здесь пересказан, в проверку не скопирован. Свой прибор и имитатор — _verify/obd19g/obd19g.c, прогон — _verify/obd19g/obd19g.py.

Как это называется в документации

Оригинал Перевод
OBD-II (on-board diagnostics) бортовая диагностика, второе поколение
DLC, J1962 connector диагностический разъём
service, mode служба (режим): 01, 02, 03…
PID (parameter ID) номер параметра внутри службы
functional / physical request общий запрос «всем блокам» / запрос одному блоку
ECU (electronic control unit) электронный блок
DTC (diagnostic trouble code) код ошибки
stored, pending, permanent DTC сохранённый, неподтверждённый, постоянный код
MIL (malfunction indicator lamp) лампа «проверь двигатель»
freeze frame стоп-кадр: снимок параметров в момент ошибки
VIN (vehicle identification number) номер кузова, 17 знаков
negative response, NRC отрицательный ответ, код причины
flow control управление потоком (ISO-TP)

19г.1 Разъём и протоколы

Разъём J1962 — шестнадцать гнёзд в два ряда. Для CAN важны пять:

Гнездо Что на нём
6 CAN-H
14 CAN-L
16 +12 В от аккумулятора
4 корпус
5 сигнальная земля

Остальные гнёзда занимают старые протоколы и выводы изготовителя. Через один разъём за годы прошло несколько протоколов, и адаптер ELM327 знает их все под номерами — python-obd хранит эти номера в своих классах:

№ Протокол
1, 2 SAE J1850 PWM и VPW
3 ISO 9141-2 (K-line)
4, 5 ISO 14230-4 (KWP, медленный и быстрый старт)
6 ISO 15765-4, CAN, 11 бит, 500 кбит/с
7 ISO 15765-4, CAN, 29 бит, 500 кбит/с
8 ISO 15765-4, CAN, 11 бит, 250 кбит/с
9 ISO 15765-4, CAN, 29 бит, 250 кбит/с

Протоколы 1–5 — не CAN; K-line, например, — последовательная линия на 10400 бод с «пробуждением» адресом 0x33 на скорости 5 бод (так она устроена в эмуляторе). Дальше речь только о CAN, чаще всего это протокол 6.

Кто кого слушает. Прибор шлёт общий запрос с идентификатором 0x7DF — его слышат все блоки. Каждый блок, которому есть что ответить, отвечает своим идентификатором из ряда 0x7E8…0x7EF: двигатель — 0x7E8, коробка — 0x7E9 (так размечены адреса в эмуляторе). Физический адрес блока — на 8 меньше: 0x7E0…0x7E7, туда прибор шлёт запросы одному блоку и управление потоком. В rusEFI те же два числа: OBD_TEST_REQUEST 0x7DF и OBD_TEST_RESPONSE 0x7E8. На 29-битных идентификаторах общий запрос — 0x18DB33F1, ответ двигателя — 0x18DAF110: в разборщике python-obd байт 0xDB означает общий запрос, 0xDA — физический, 0x33 — «всем», 0xF1 — прибор.

Слева вверху — разъём J1962 из шестнадцати гнёзд в два ряда, выделены 4 и 5 (корпус и сигнальная земля), 6 и 14 (CAN-H и CAN-L) и 16 (+12 В); под ним прибор: STM32F103 с bxCAN через трансивер CAN к гнёздам 6 и 14, питание от гнезда 16 через стабилизатор, тайминг 500 кбит/с для F103 и F407 и фильтр 0x7E8 с маской 0x7F8. Справа — обмен прибора с блоком двигателя: запрос 7DF 02 01 0C и ответ 7E8 04 41 0C 14 5F, то есть 1303,75 об/мин; запрос VIN 7DF 02 09 02, первый кадр 10 14 на 20 байт, управление потоком 30 00 00 на адрес 7E0, два последующих кадра 21 и 22 и итог VIN WP0ZZZ99ZTS390000. Внизу — как два байта кода ошибки превращаются в P0133, P0301 и U0100


19г.2 Запрос и ответ: службы и отказы

Запрос OBD — один кадр ISO-TP (раздел 19в): байт длины, номер службы и, если нужно, номер параметра. Ответ блока начинается со службы плюс 0x40: на 01 — 0x41, на 09 — 0x49. Кадр у нас из 8 байт, как и у rusEFI (первый байт — длина, 0x40 + mode, PID, данные), свободные байты — заполнитель (у нас и у эмулятора — 0x00).

Служба Что делает Запрос Ответ начинается с
01 текущие данные 02 01 PID 41 PID
02 стоп-кадр 03 02 PID кадр 42 PID кадр
03 сохранённые коды ошибок 01 03 43
04 стереть коды и стоп-кадр 01 04 44
07 неподтверждённые коды 01 07 47
09 сведения об автомобиле: VIN 02 09 PID 49 PID
0A постоянные коды 01 0A 4A

Если блок не может выполнить запрос, он отвечает отрицательно: 0x7F, номер службы и код причины. Коды — те же, что в UDS; названия взяты из iso14229:

Код Причина Где встретился
0x11 служба не поддержана имитатор на службу 05
0x12 подфункция не поддержана эмулятор на 0201, имитатор на 020C01
0x22 условия не те имитатор на 04 при работающем двигателе

На неизвестный PID службы 01 rusEFI молчит (в коде так и написано: «ignore unhandled PIDs»), наш имитатор тоже: на 01A6 не ответил никто, и прибор узнаёт это только по таймауту. Поэтому сначала спрашивают, что блок умеет.


19г.3 PID: что поддержано и как считать

Карты поддержки. PID 0x00 службы 01 — не параметр, а 32 бита: какие из PID 0x01…0x20 блок знает. Старший бит — PID 0x01, младший — PID 0x20, а PID 0x20 — это следующая карта (0x21…0x40), дальше 0x40 (0x41…0x60). Эмулятор ответил на 0100 байтами BE 3F A8 13; C-разбор и python-obd одинаково прочли из них 18 PID: 01 03 04 05 06 07 0B 0C 0D 0E 0F 10 11 13 15 1C 1F 20. Последний, 0x20, говорит: спроси и 0120. У имитатора карта своя — 98 3B 80 01: 01 04 05 0B 0C 0D 0F 10 11 20. Коробка передач на тот же общий запрос ответила 00 08 00 00 — ей известна только скорость (0x0D).

Формулы. Байты данных после PID обозначают A, B, C, D. Десять частых PID:

PID Что Байт Формула Пример из эмулятора Значение
0x04 нагрузка двигателя 1 A·100/255, % FF 100 %
0x05 охлаждающая жидкость 1 A − 40, °C 5F 55 °C
0x0B давление во впуске 1 A, кПа 26 38 кПа
0x0C обороты 2 (256A + B)/4, об/мин 14 5F 1303,75 об/мин
0x0D скорость 1 A, км/ч 0A 10 км/ч
0x0F воздух на впуске 1 A − 40, °C 39 17 °C
0x10 расход воздуха 2 (256A + B)/100, г/с 18 1F 61,75 г/с
0x11 дроссель 1 A·100/255, % 2B 16,863 %
0x2F уровень топлива 1 A·100/255, % AA 66,667 %
0x42 напряжение блока 2 (256A + B)/1000, В 39 4B 14,667 В

Каждая формула сверена с python-obd: обороты там — единица 0x07 таблицы единиц (0,25 об/мин на единицу), расход воздуха — 0x27 (0,01 г/с), напряжение — 0x0B (0,001 В), проценты — v * 100.0 / 255.0, температура — v - 40. Сторона блока в rusEFI кодирует так же: обороты умножает на ODB_RPM_MULT 4, к температуре прибавляет ODB_TEMP_EXTRA 40, дроссель умножает на 2,55, расход воздуха — на 100, напряжение — на 1000.

Пределы следуют из формул: обороты — до 16383,75 об/мин, температура — от −40 до 215 °C, расход воздуха — до 655,35 г/с, напряжение — до 65,535 В.

Свой разбор на C — одна таблица: сырое число умножить, разделить, сдвинуть. Значение хранится в тысячных, без плавающей точки — на F103 её нет:

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 */
};

Таблицу проверили перебором: все 256 значений семи однобайтовых PID и все 65 536 значений трёх двухбайтовых — 198 400 кадров — прошли через C-разбор и через декодеры python-obd. Обороты, расход, напряжение и температуры совпали точно; у процентов C отбрасывает всё дальше тысячных, и расхождение не больше 0,00098 процента.


19г.4 Коды ошибок и стоп-кадр

Как закодирован код. Два байта. Старшие два бита — система: 00 — P (двигатель и трансмиссия), 01 — C (шасси), 10 — B (кузов), 11 — U (связь). Следующие два бита — первая цифра, 0…3. Оставшиеся 12 бит — три шестнадцатеричные цифры. Так разбирает parse_dtc в python-obd, и так же свой разбор:

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';
}

Байты 01 33 — P0133, 03 01 — P0301, C1 00 — U0100. Описания кодов python-obd держит в своей таблице: P0301 — пропуски зажигания в первом цилиндре, P0420 — низкая эффективность катализатора, U0100 — потеря связи с блоком двигателя.

Три списка кодов. Служба 03 — сохранённые коды: из-за них горит лампа. 07 — неподтверждённые: ошибка замечена, но ещё не подтвердилась. 0A — постоянные. Сколько кодов сохранено и горит ли лампа, говорит PID 0x01: в первом байте ответа старший бит — лампа, младшие семь — число кодов. Имитатор с тремя кодами ответил 41 01 83 … — лампа горит, кодов 3; эмулятор с теми же тремя кодами — те же 83.

Байт «сколько кодов». Здесь два открытых проекта расходятся. python-obd считает, что на CAN сразу за 0x43 идёт число кодов, и по нему обрезает ответ. Эмулятор ELM327 4.0.0 этого байта не шлёт:

Кто Ответ на 03 Что прочёл python-obd
имитатор (с байтом числа) 43 03 01 33 03 01 C1 00 — 8 байт, два кадра P0133, P0301, U0100
эмулятор (без него) 43 01 33 03 01 C1 00 — 7 байт, один кадр P3303

python-obd принял 0x01 за число кодов и собрал из следующих двух байт несуществующий P3303. На ответ эмулятора на 07 (47 04 20) он вернул пустой список. Стандарт здесь не сверялся, поэтому свой разбор понимает оба вида: с байтом числа длина ответа всегда чётная (служба, число, по два байта на код), без него — нечётная.

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

Так C-разбор прочёл P0133, P0301 и U0100 из обоих ответов.

Стоп-кадр. Когда блок сохраняет код, он запоминает и параметры в этот момент. Служба 02 спрашивает их так же, как 01, только с номером кадра после PID: 02 0C 00 — обороты в стоп-кадре 0. Эмулятор на 020200 ответил 42 02 00 00 00 — байты после номера кадра нулевые, стоп-кадра нет. Имитатор ответил 42 02 00 01 33 — снят по P0133, и тогда было 2000 об/мин, 90 °C и 60 км/ч. python-obd шлёт службу 02 без номера кадра и данные ждёт сразу после PID, поэтому в проверке номер кадра перед его декодером убран вручную.

Стирание. Служба 04 стирает коды и стоп-кадр — так и описана команда CLEAR_DTC в python-obd: «Clear DTCs and Freeze data». Эмулятор ответил 44 и дальше на 03 — 43 00, а PID 0x01 показал 00: лампа погасла. Наш имитатор при работающем двигателе отвечает отказом 7F 04 22 и стирает только при нулевых оборотах; постоянные коды (0A) он не трогает — P0133 остался и после стирания. Эмулятор стёр и их.


19г.5 VIN, ISO-TP и ELM327

VIN не влезает в кадр. Ответ на 0902 — служба 0x49, PID 0x02, число записей 1 и 17 знаков: 20 байт. Так он и описан в python-obd — b"0902", 22 (с двумя байтами заголовка) и encoded_string(17). Ответ идёт тремя кадрами ISO-TP:

Кто Кадр Что это
прибор 7DF: 02 09 02 общий запрос
блок 7E8: 10 14 49 02 01 57 50 30 первый кадр: длина 0x014 = 20 байт, первые 6
прибор 7E0: 30 00 00 управление потоком: слать всё, без пауз
блок 7E8: 21 5A 5A 5A 39 39 5A 54 последующий кадр 1: 7 байт
блок 7E8: 22 53 33 39 30 30 30 30 последующий кадр 2: 7 байт

6 + 7 + 7 = 20: VIN WP0ZZZ99ZTS390000. Кадры имитатора совпали с кадрами эмулятора байт в байт. Главное здесь — куда прибор шлёт управление потоком: не на общий 0x7DF, а на физический адрес ответившего блока, 0x7E8 − 8 = 0x7E0. Блок ждёт его именно там; без него второй и третий кадры не придут. Физический запрос 0902 на 0x7E0 дал те же три кадра.

ELM327. Многие сканеры работают через адаптер ELM327 — микросхему, которая принимает текстовые команды по последовательному порту (или Bluetooth) и сама ходит на CAN. Сессия с эмулятором, как её ведёт приложение-сканер:

> 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 — сброс, ATE0 — не повторять команду эхом, ATH1 — показывать идентификаторы, ATSP6 — протокол 6, ATDP — какой протокол выбран, ATRV — напряжение на разъёме. Запрос — просто шестнадцатеричные цифры службы и PID; ISO-TP, управление потоком и заполнитель адаптер берёт на себя. С ATH0 адаптер собирает длинный ответ сам: первой строкой — длина (014), дальше — куски с номерами. Во втором запросе эмулятор выдал другой VIN из своего списка — формат от этого не меняется.

python-obd теряет знаки VIN. Его decode_encoded_string чистит края строки вызовом strip с набором b'\x00' b'\x01' b'\x02' b'\\x00' …. Последние элементы записаны с двойной обратной чертой, и в набор попали буквы \, x и цифры 0, 1, 2. Из WP0ZZZ99ZTS390000 вышло WP0ZZZ99ZTS39 — 13 знаков из 17; у VIN, который начинается с 1, отрезается и начало. Поэтому из 43 ответов имитатора, которые python-obd разобрал, совпали с C-разбором 41 — два расхождения, оба VIN.


19г.6 Прибор и имитатор на STM32

Тайминг. OBD на CAN — 500 кбит/с (или 250). У F103 шина APB1 — 36 МГц, и при 8…25 квантах на бит точно 500 кбит/с дают пять делителей: 3, 4, 6, 8 и 9 — это 24, 18, 12, 9 и 8 квантов на бит. Берём тот же вариант, что в разделе 19.4: делитель 9, квант 250 нс, бит из 1 + 6 + 1 = 8 квантов, 2 мкс, точка выборки (1 + 6)/8 = 87,5 %. Слово BTR — 0x00050008. Для 250 кбит/с — делитель 18 при тех же квантах. У F407 APB1 — 42 МГц, вариантов три (делители 4, 6 и 7); берём 6 и 14 квантов: 1 + 11 + 2, точка выборки 85,7 %, BTR 0x001A0005. Всё это _Static_assert проверяют при сборке.

Фильтр прибора. Ответы приходят с 0x7E8…0x7EF — восемь идентификаторов, отличающихся тремя младшими битами. Один 32-битный фильтр с маской 0x7F8 пропускает ровно их (раздел 19.5); биты IDE и RTR в маске заставляют сравнивать и их — пройдут только стандартные кадры данных:

        /* прибор: 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;

Имитатору блока нужны 0x7DF и свой 0x7E0 — это список из 16-битных фильтров, как у узла CANopen в разделе 19б.

Прибор (obd_tester_step) по кругу спрашивает обороты, скорость и температуру. Ответ ждёт 150 мс — столько же по умолчанию ждёт клиент iso14229 (UDS_CLIENT_DEFAULT_P2_MS); нет ответа — следующий PID. Принятые кадры идут в сборщик ISO-TP, у каждого из восьми блоков свой; на первый кадр прибор сразу шлёт управление потоком на f.id - 8u.

Имитатор блока (obd_ecu_step) — для стола, без машины: две платы, два трансивера, шина с терминаторами на концах. Он отвечает на службы 01, 02, 03, 04, 07, 09 и 0A, сам строит карты поддержки из своей таблицы значений, шлёт отказы 0x11, 0x12 и 0x22. Значения по умолчанию в прогоне — те же, что у эмулятора, чтобы кадры можно было сравнить.

Что получилось. Ядро obd19g.c (ISO-TP, разбор, имитатор) и связь с bxCAN obd19g_bxcan.c собраны arm-none-eabi-gcc (контейнер stm-arm:1, -Os) для F103 и F407 без предупреждений. Ядро — 2518 байт кода на обоих; связь с bxCAN — 743 байта на F103 и 747 на F407 плюс 584 байта ОЗУ (восемь сборщиков ISO-TP по 70 байт и значения). Имитатор блока занимает 1008 байт ОЗУ: значения PID, стоп-кадр и три списка кодов.

Тот же obd19g.c собран gcc на ПК (контейнер stm-sys:1) с -Wall -Wextra -Werror -pedantic: прибор, имитатор двигателя, имитатор коробки и имитатор двигателя на 29-битных идентификаторах обменялись по шине в памяти, 33 запроса. Все кадры записаны в _verify/obd19g/obd19g_host.txt. python-obd своим разборщиком ISO 15765-4 собрал те же кадры в 43 ответа и совпал с C-разбором в 41 (два расхождения — VIN из 19г.5). Из 15 ответов, которые есть и у эмулятора, 12 совпали байт в байт, 3 — службы 03, 07, 0A — отличаются байтом числа кодов (19г.4). Запрос на 29-битных идентификаторах 0x18DB33F1 получил ответ 0x18DAF110 с теми же 1303,75 об/мин.


19г.7 Полный код прибора и имитатора

Весь код раздела — три своих файла: заголовок obd19g.h, ядро obd19g.c (ISO-TP, запрос, разбор, имитатор блока) и obd19g_bxcan.c (регистры bxCAN F103 и F407). Ниже они целиком и по порядку, кроме двух уже показанных кусков obd19g.c: таблицы pid_table (19г.3) — она стоит сразу за obd_pid_info — и obd_dtc_str() (19г.4) — она стоит за obd_pid_name(). Чужого кода нет.

Проверка собирает оба файла для F103 и F407 в объектные файлы, без компоновки. CMSIS_F1 — каталог Device/Include пака Keil STM32F1xx_DFP 2.4.1 (в нём stm32f10x.h), CMSIS_CORE — CMSIS/Core/Include из CMSIS 5.9.0 (github.com/ARM-software/CMSIS_5), CMSIS_F4 — каталог Drivers/CMSIS/Device/ST/STM32F4xx/Include пака STM32F4xx_DFP 2.17.1:

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

Своего main() у прибора нет: плата должна настроить такт, SysTick на 1 мс, вызвать obd_can_init(0) (имитатор — obd_can_init(1)) и в цикле звать obd_tester_step(ms) или obd_ecu_step(&ecu). Такт и SysTick — как в 19в.7; такую сборку проверка не делает.

Заголовок. Адреса OBD для 11 и 29 бит, кадр, состояния ISO-TP, результат разбора и имитатор блока:

/* Раздел 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

Кадр. Проверки при сборке и заготовка кадра: 8 байт, хвост — заполнитель:

/* Раздел 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 в миниатюре. isotp_start() кладёт в кадр одиночное сообщение или первый кадр длинного, isotp_next() выдаёт следующие кадры по 7 байт, isotp_feed() собирает принятое и просит послать управление потоком. Размер блока и паузу из управления потоком передатчик не смотрит, а приёмник просит всё подряд без пауз: сообщения здесь не длиннее 64 байт.

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;
}

Запрос и описание PID. Запрос — всегда одиночный кадр на 0x7DF. За описанием obd_pid_info в файле идёт таблица формул из 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 : "";
}

Разбор ответа. obd_decode() узнаёт отказ 7F, карты поддержки, состояние лампы, стоп-кадр, значения по таблице, три списка кодов (с байтом числа и без), стирание и VIN — 17 знаков после 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;
    }
}

Имитатор блока: ответы. Карты поддержки строятся из таблицы значений, списки кодов уходят с байтом числа, отказ — три байта 7F, служба, код:

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);
}

Имитатор блока: разбор запроса. Кадр управления потоком — дослать остальное; иначе одиночный кадр со службой 01, 02, 03, 04, 07, 09 или 0A. На общий запрос с неизвестной службой блок молчит, на физический — 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: тайминг и фильтры. Числа тайминга для двух кристаллов и _Static_assert, которые не дадут собрать неверные делители. Прибору — маска на 0x7E8…0x7EF, имитатору — список 0x7DF и 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) {
    }
}

Отправка и приём без прерываний. Свободный ящик из трёх или отказ; приём — опросом очереди 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;
}

Прибор. Один запрос в полёте, 150 мс на ответ, по кругу обороты, скорость и температура; значения — в obd_live, в тысячных:

/* ── прибор: по кругу спрашивает обороты, скорость и температуру ── */
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;
        }
    }
}

Имитатор на плате. Всё, что пришло, — в ecu_rx(), ответные кадры — в ящики, ожидая свободный:

/* ── имитатор блока для стола: отвечает на 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 Грабли

Стирать коды на ходу. Служба 04 — не безобидная кнопка: она стирает коды и стоп-кадр, а вместе с ними — след неисправности, по которому её потом ищут. Никогда не шлите 04 во время движения и не встраивайте её в автоматический опрос. Не надейтесь, что блок откажет сам: наш имитатор при работающем двигателе отвечает 7F 04 22, а эмулятор при 1303,75 об/мин стёр всё и ответил 44.

Прибор питается от гнезда 16. Там +12 В аккумулятора, а не 3,3 В. Плату — только через стабилизатор. Опрашивать — при включённом зажигании: иначе блок может промолчать, и прибор увидит то же, что наш прогон на неподдержанный PID, — тишину.

Свой терминатор на машинную шину. В машине шина уже собрана, резисторы 120 Ом стоят на её концах (раздел 19.2). Прибор — промежуточный узел: терминатор на плате снять перемычкой. На столе, с имитатором, наоборот — нужны оба.

Управление потоком ушло на 0x7DF. Первый кадр VIN пришёл, дальше тишина. Блок ждёт управление потоком на своём физическом адресе: ответ 0x7E8 — значит, на 0x7E0.

Коды с байтом числа и без. python-obd на ответе эмулятора без байта числа прочёл P3303 вместо трёх настоящих кодов. Проверяйте длину: чётная — байт числа есть, нечётная — нет.

Проценты с плавающей точкой. На F103 нет модуля плавающей точки: тысячные и целочисленная таблица из 19г.3 дают те же числа с точностью 0,001 и без библиотеки деления с плавающей точкой.

Точка выборки не та. Прошивка адаптера canable (STM32F042, 48 МГц) ставит на 500 кбит/с делитель 12 и кванты 1 + 4 + 3: бит те же 2 мкс, но точка выборки 62,5 %. Адаптер работает, но это не образец: для своей платы берите точку из раздела 19.4 — 87,5 %, как в 19г.6.


19г.9 Итоги

Полные исходники

obd19g.c — запросы OBD-II, разбор ответов, коды ошибок, имитатор блока и ISO-TP в миниатюре.

/* Раздел 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;                               /* на общий запрос — молчим */
    }
}

Дальше: раздел 19д «UDS (ISO 14229): сеансы, чтение данных, ошибки и прошивка через диагностику».

Открыть в интерактивном справочнике