198р. Мини-проект: OBD-II сканер на STM32 — обороты, скорость, температура и коды ошибок
В разделе 19 bxCAN разобран изнутри, в 198е он стал сниффером — прибором, который только слушает шину. Здесь прибор впервые разговаривает с чужими блоками: карманный сканер втыкается в диагностический разъём машины, спрашивает у блока управления двигателем обороты, скорость, температуру охлаждающей жидкости и напряжение, читает коды ошибок и номер кузова (VIN) и показывает всё на маленьком дисплее. Одна кнопка листает экраны, а долгое нажатие стирает коды — с подтверждением и защитой от стирания на ходу.
Проверено всё, что можно проверить без машины. Ядро сканера (obd_core.c) собрано gcc на компьютере и прогнано
против модели двух блоков на шине в памяти: поездка на 72 с с пуском,
разгоном, круизом и остановкой, два кода у двигателя, три у коробки, VIN,
нажатия кнопки по сценарию и обрыв связи. Тот же опыт собран
arm-none-eabi-gcc для Cortex-M3 и прогнан в QEMU — вывод совпал строка в
строку. Модель на Python своим кодом пересчитала битовый тайминг, фильтр,
разбор кадров и загрузку шины. Прошивка для BluePill собрана с
-Werror; на плате и в машине она не запускалась.
Опоры: RM0008 (глава bxCAN: битовый тайминг, рис. 233, поля CAN_BTR,
фильтры в режиме маски, рис. 229, длина кадра, рис. 234) из пака Keil
STM32F1xx_DFP 2.4.1. Стандарты SAE J1979, J1962 и ISO 15765 платные и здесь не сверялись:
формулы PID и разбор кодов сверены с python-obd (decoders.py,
UnitsAndScaling.py), адреса блоков, функциональный адрес 0x7DF, разбивка
ответа на кадры и VIN — с elm327-emulator (obd_message.py, elm.py),
значения по умолчанию ISO-TP — с isotp-c и iso14229, команды SSD1306 — с
драйвером OLED.cpp из IronOS. Лицензия: python-obd — GPL-2.0,
elm327-emulator — CC BY-NC-SA 4.0, isotp-c и iso14229 — MIT, IronOS —
GPL-3.0; их код в справочник не скопирован, только сверены числа и форматы.
bxCAN — раздел 19, трансивер и терминаторы — 19.2, защита входа питания —
173, сниффер — 198е.
Как это называется в документации
Оригинал Перевод OBD-II, SAE J1979 service (mode) бортовая диагностика; служба (режим) запроса: 01 — текущие данные, 03 — коды, 04 — стереть, 09 — сведения о машине PID (parameter ID) номер параметра внутри службы 01 DTC (diagnostic trouble code) код неисправности, например P0133 MIL (malfunction indicator lamp) лампа «проверь двигатель» freeze frame стоп-кадр: параметры в момент появления кода functional / physical addressing запрос всем блокам (0x7DF) / одному блоку (0x7E0…0x7E7) ISO-TP (ISO 15765-2): single frame, first frame, consecutive frame, flow control транспорт поверх CAN: одиночный кадр, первый кадр, последующий кадр, управление потоком block size, STmin сколько кадров слать без нового FC; пауза между кадрами P2 timeout сколько ждать ответа блока J1962 connector диагностический разъём на 16 выводов load dump сброс нагрузки: выброс при отключении генератора под нагрузкой
198р.1 Что делает прибор и из чего он
Сканер питается от разъёма машины, сам находит блоки, которые отвечают на OBD-II, выбирает блок двигателя и опрашивает его по кругу. Экранов четыре: живые данные, коды ошибок, VIN и служебный (какие блоки ответили, какие PID они знают, счётчики). Пятый — вопрос перед стиранием кодов.
Перечень деталей — примерный, цены не приводятся:
| Что | Какое | Зачем |
|---|---|---|
| плата | BluePill, STM32F103C8 | bxCAN, I2C1, 64 КБ флеш-памяти |
| трансивер | модуль на SN65HVD230 | питание 3,3 В, как у микроконтроллера (19.2) |
| разъём | вилка OBD-II (J1962) с кабелем | выводы 4, 5, 6, 14, 16 |
| защита | предохранитель, диод, однонаправленный ограничитель | переполюсовка и выбросы (173) |
| питание | понижающий модуль 12 → 3,3 В | линейный стабилизатор здесь греется (198р.2) |
| дисплей | модуль 0,96″ на SSD1306, 128 × 64, I2C | 8 строк по 21 символу |
| кнопка | тактовая, на землю | подтяжка — внутренняя |
Выводы BluePill: CAN_RX и CAN_TX — PB8 и PB9 после переназначения
(CAN_REMAP = 10, как в 198е), I2C1 — PB6 и PB7, кнопка — PA0. Такт —
кварц 8 МГц × 9 = 72 МГц, APB1 — 36 МГц: от него тактуются bxCAN и I2C1.
198р.2 Разъём, питание и трансивер
Разъём J1962. Для CAN-диагностики нужны пять выводов: 16 — плюс аккумулятора, 4 — земля кузова, 5 — сигнальная земля, 6 — CAN-H, 14 — CAN-L. Это общеизвестная раскладка; со стандартом J1962 она не сверена.
Трансивер. Выбран SN65HVD230: он питается от 3,3 В, как и микроконтроллер (таблица в 19.2), и одной шины 3,3 В хватает на всю плату. TJA1050 питается от 5 В — понадобилась бы вторая шина питания, а его выход RXD шёл бы на вход микроконтроллера пятивольтовым уровнем (19.2: «согласователь уровней или толерантные выводы»). Терминатора 120 Ом в сканере нет: на шине машины их уже два, на концах линии (19.2), а сканер — ответвление.
Питание и защита. Порядок — как в 173.4: предохранитель, защита от переполюсовки (диод или транзистор), однонаправленный ограничитель после неё, накопительный конденсатор, затем стабилизатор. Ограничитель для машины выбирают, как сказано в 173.3: рабочее напряжение выше 24 В, чтобы он не открывался при запуске от внешней батареи на 24 В. Сброс нагрузки — выброс на доли секунды с энергией на порядки больше, чем гасит ограничитель у платы (173.3); его защищает цепь в генераторе, а мы только не даём ему сжечь стабилизатор. Чисел для сброса нагрузки здесь нет: со стандартом они не сверялись, это не проверено.
Стабилизатор — импульсный. Линейный падал бы на себе 14,4 − 3,3 = 11,1 В и при токе 50 мА (пример: ток сканера не измерялся) рассеивал бы 0,555 Вт.
198р.3 bxCAN: 500 кбит/с и фильтр на ответы
Скорость. По RM0008 (рис. 233) бит — это один квант синхронизации плюс TS1 + 1 и TS2 + 1 квантов, квант — (BRP + 1) тактов APB1. Поля узкие: TS1 — 4 бита (1…16 квантов), TS2 — 3 бита (1…8), BRP — 10 бит. От 36 МГц 500 кбит/с ровно дают 38 сочетаний, из них 6 — с точкой выборки от 80 до 90 %:
| Делитель | Квантов в бите | TS1 + TS2 | Точка выборки |
|---|---|---|---|
| 4 | 18 | 15 + 2 | 88,9 % |
| 4 | 18 | 14 + 3 | 83,3 % |
| 6 | 12 | 9 + 2 | 83,3 % |
| 8 | 9 | 7 + 1 | 88,9 % |
| 9 | 8 | 6 + 1 | 87,5 % |
| 12 | 6 | 4 + 1 | 83,3 % |
Взят первый: квантов больше всего (квант 111,1 нс), и прыжок подстройки
SJW можно дать в 2 кванта — в машине много блоков со своими кварцами.
Сниффер 198е брал 9 × 8 квантов (87,5 %) — тоже годится. Какую точку
выборки требует конкретная машина, без ISO 15765-4 не проверить.
В регистре это 0x011E0003:
CAN1->BTR = ((CAN_SJW - 1u) << 24) | ((CAN_TS2 - 1u) << 20) | ((CAN_TS1 - 1u) << 16) | (CAN_BRP - 1u);
Фильтр. Блоки отвечают на идентификаторах 0x7E8…0x7EF — восемь соседних, отличаются тремя младшими битами. Значит, хватает одного 32-битного фильтра в режиме маски (RM0008, рис. 229: STID — в битах 31…21, IDE — бит 2, RTR — бит 1). В маску попадают старшие 8 бит идентификатора и ещё IDE и RTR: 29-битные кадры и запросы данных не пройдут.
#define OBD_FR1 ((uint32_t)OBD_RESP_ID << 21)
#define OBD_FR2 (((uint32_t)OBD_RESP_MASK << 21) | 0x4u | 0x2u) /* IDE = 0 и RTR = 0 — тоже «должно совпасть» */
Выходит FR1 = 0xFD000000, FR2 = 0xFF000006. Опыт прогнал через тот же фильтр все 2048 стандартных идентификаторов: прошли ровно 8, от 0x7E8 до 0x7EF; из запросов данных и 29-битных кадров — ни одного. Остальной трафик машины до прерывания не доходит вовсе.
198р.4 Запросы OBD-II поверх ISO-TP
Запрос — один кадр: байт длины (PCI одиночного кадра, 0x0N), служба, номер PID. Кадр всегда полный, 8 байт; остаток добивается 0xAA — таким байтом добивает isotp-c, а elm327-emulator — нулями. Функциональный запрос уходит на 0x7DF (его слышат все блоки), физический — на 0x7E0…0x7E7, адрес ответа минус 8 (так адреса расписаны в elm327-emulator: двигатель 0x7E0/0x7E8, коробка 0x7E1/0x7E9).
Ответ длиннее 7 байт идёт составным. Первый кадр несёт 12 бит длины и 6 байт; сканер отвечает кадром управления потоком (FC) на физический адрес этого блока, и блок досылает последующие кадры по 7 байт с номерами 1, 2…:
} else if (type == 1u) { /* первый кадр: длина 12 бит, 6 байт данных */
obd_frame fc;
tp.len = (uint16_t)(((f->d[0] & 15u) << 8) | f->d[1]);
if (tp.len < 8u || tp.len > OBD_BUF || f->dlc < 8u) {
obd.isotp_err++;
return;
}
for (unsigned i = 0; i < 6u; i++)
tp.buf[i] = f->d[2u + i];
tp.got = 6;
tp.sn = 1;
tp.from = from;
tp.active = 1;
fc.id = from - 8u; /* управление потоком — на физический адрес этого ЭБУ */
fc.dlc = 8;
fc.d[0] = 0x30; /* FC: продолжай */
fc.d[1] = 0; /* размер блока 0 — всё без остановок */
fc.d[2] = 0; /* STmin 0 мс */
Начало опыта — первые 207 мс на шине, дословно из прогона (время в мс, идентификатор, 8 байт):
bus t= 1 7DF 02 01 00 AA AA AA AA AA
bus t= 3 7E8 06 41 00 88 18 00 01 AA
bus t= 5 7E9 06 41 00 00 08 00 00 AA
bus t=101 7E0 02 01 20 AA AA AA AA AA
bus t=103 7E8 06 41 20 00 00 00 01 AA
bus t=126 7E0 02 01 40 AA AA AA AA AA
bus t=128 7E8 06 41 40 40 00 00 00 AA
bus t=151 7E0 02 01 01 AA AA AA AA AA
bus t=153 7E8 06 41 01 82 07 65 00 AA
bus t=176 7E0 02 09 02 AA AA AA AA AA
bus t=178 7E8 10 14 49 02 01 57 50 30
bus t=179 7E0 30 00 00 AA AA AA AA AA
bus t=180 7E8 21 5A 5A 5A 39 39 5A 54
bus t=181 7E8 22 53 33 39 30 30 30 30
bus t=201 7DF 01 03 AA AA AA AA AA AA
bus t=203 7E8 06 43 02 01 33 04 20 AA
bus t=205 7E9 10 08 43 03 07 00 07 15
bus t=206 7E1 30 00 00 AA AA AA AA AA
bus t=207 7E9 21 C1 00 AA AA AA AA AA
Что здесь происходит:
- 0100 на 0x7DF — «какие PID 01…20 ты знаешь». Ответили двое: 0x7E8 — PID 01, 05, 0C, 0D и 20, 0x7E9 — только 0D. Блок двигателя — тот, кто знает обороты (0C): 0x7E8. Дальше сканер спрашивает только его, на 0x7E0: на 0x7DF скорость (0D) прислала бы и коробка — лишний ответ на каждый такой запрос.
- 0120 и 0140 — следующие диапазоны. Последний бит ответа 0100 (PID 20) говорит, что есть ответ на 0120, его последний бит (PID 40) — что есть
- PID 0x42 (напряжение) лежит в диапазоне 41…60: одной таблицы 0100 не хватило бы.
- 0101 — байт A = 0x82: старший бит — лампа MIL горит, младшие семь — два кода (разбор как в python-obd).
- 0902 — VIN: ответ 20 байт (
49 02 01и 17 символов) не лезет в кадр. Первый кадр10 14— длина 0x014, FC на 0x7E0 через 1 мс, затем кадры 21 и 22. СобраноWP0ZZZ99ZTS390000— номер из примера в elm327-emulator. - 03 на 0x7DF — коды. У двигателя два (одиночный кадр,
43 02 01 33 04 20), у коробки три — это 8 байт, составной ответ, FC уже на 0x7E1.
Формулы — те же, что в python-obd (таблица UAS: 0x07 — 0,25 об/мин на единицу, 0x09 — 1 км/ч, 0x0B — 0,001 В):
| PID | Что | Байты | Формула |
|---|---|---|---|
| 0C | обороты | A, B | (256·A + B) / 4 об/мин |
| 0D | скорость | A | A км/ч |
| 05 | охлаждающая жидкость | A | A − 40 °C |
| 42 | напряжение блока | A, B | (256·A + B) / 1000 В |
Код ошибки — два байта. Два старших бита — буква системы (P, C, B, U), следующие два — первая цифра 0…3, остальные 12 бит — три шестнадцатеричные цифры:
out[0] = sys[code >> 14];
out[1] = (char)('0' + ((code >> 12) & 3u)); /* следующие два бита: 0…3 */
out[2] = hex[(code >> 8) & 15u];
out[3] = hex[(code >> 4) & 15u];
out[4] = hex[code & 15u];
Опыт проверил семь кодов: 0x0133 — P0133, 0x0420 — P0420, 0x4123 — C0123 (пример из комментария python-obd), 0x8001 — B0001, 0xC100 — U0100, 0x1234 — P1234, 0x3FFF — P3FFF; модель на Python разобрала их так же.
198р.5 Опрос по кругу, экраны и кнопка
Ядро вызывается раз в миллисекунду. Запрос в полёте всегда один; новый
уходит в начале слота 25 мс, если прежний закончен. Физический запрос
кончается первым ответом или таймаутом 150 мс (столько по умолчанию ждёт
ответа клиент iso14229, UDS_CLIENT_DEFAULT_P2_MS). Функциональный ждёт
100 мс и собирает ответы всех блоков. Три таймаута подряд — надпись
NO LINK.
Порядок опроса — таблица из восьми слотов; PID, которых блок не знает, пропускаются:
static const uint8_t POLL[8] = {0x0C, 0x0D, 0x0C, 0x05, 0x0C, 0x0D, 0x0C, 0x42}; /* обороты — каждый второй слот */
Обороты меняются быстрее всего и идут каждые 50 мс (20 раз в секунду), скорость — каждые 100 мс, температура и напряжение — каждые 200 мс. Так и вышло в опыте: на участке 12…48 с между запросами 010C ровно 50 мс (720 запросов), 010D — 100 мс, 0105 и 0142 — по 200 мс.
Загрузка шины. 40 запросов и 40 ответов в секунду — 80 кадров. Кадр с 8 байтами — 111 бит с паузой (RM0008, рис. 234), в худшем случае вставки битов — 135. Это 1,78…2,16 % шины 500 кбит/с. Сканер почти не мешает машине; чаще спрашивать можно, но блок двигателя отвечает не мгновенно, а его время ответа без машины не измерить.
Экраны. Строка — 21 символ шрифта 5 × 7 (6 точек с промежутком, 126 из 128), строк восемь — по странице SSD1306 на строку. Экраны из опыта, дословно:
screen t=20000 accelerating
|OBD-II ECU 7E8 |
|RPM 1894 RPM |
|SPEED 44 KM/H |
|COOLANT 48 C |
|BATTERY 14.20 V |
|MIL ON DTC 2 |
| |
| |
screen t=5500 dtc
|STORED DTC: 5 |
|P0133 P0420 |
|P0700 P0715 |
|U0100 |
| |
| |
|HOLD 2S: CLEAR |
|PRESS: NEXT |
Надписи латиницей: в прошивке шрифт только на символы от пробела до Z — 59 знаков по 5 байт. На снимке разгона обороты отстают от «правды» поездки на 46 мс — меньше периода их опроса.
Дисплей не должен тормозить опрос. Весь экран 1024 байта по I2C на
400 кГц (9 бит на байт) — 23,1 мс ожидания в главном цикле. Поэтому за
вызов уходит один символ: адрес, байт 0x40 и 6 байт столбцов — 0,18 мс,
плюс раз на строку три команды позиции. Экран обновляется целиком за
168 мс, а опрос не сдвигается. Команды включения — префикс 0x00 для
команд и 0x40 для данных, адрес 0x3C, накачка 8D 14, мультиплекс A8
(высота минус 1), раскладка COM DA (0x02 или 0x12 в зависимости от
панели) — сверены с OLED.cpp из IronOS; с даташитом SSD1306 не сверены.
Кнопка и стирание кодов. Короткое нажатие листает экраны. Долгое (2 с, считается по удержанию, а не по отпусканию) на экране кодов открывает вопрос:
screen t=28200 confirm
|CLEAR ALL DTC? |
|ALSO FREEZE FRAME |
|AND READINESS DATA |
|IGNITION ON, |
|ENGINE OFF ONLY |
| |
|HOLD 2S: YES |
|PRESS: NO 5|
Ещё одно долгое нажатие в течение 5 с — «да», короткое — «нет», тишина
5 с — тоже «нет». Служба 04 стирает не только коды: в python-obd она
названа «Clear DTCs and Freeze data», а ответ 0101 — «Status since DTCs
cleared», то есть готовность систем самодиагностики считается от
стирания и тоже сбрасывается. А причина неисправности никуда не денется.
Поэтому сканер не стирает при работающем двигателе (последние обороты
больше нуля) и без связи — пишет ENGINE ON: NOT CLEAR.
198р.6 Проверка на компьютере: поездка на 72 секунды
Модель шины: кадр доходит за 1 мс, блок двигателя отвечает через 2 мс, коробка — через 4 мс; ответ приходит на 3 мс позже запроса. Сценарий:
| Время | Что происходит | Что показал сканер |
|---|---|---|
| 0…2 с | зажигание, двигатель стоит | нашёл 2 блока, VIN, 5 кодов |
| 2…10 с | пуск, холостой ход 800 об/мин | 4 с: 800 об/мин, 21 °C, 14,20 В |
| 10…30 с | разгон до 3000 об/мин и 90 км/ч, прогрев | 20 с: 1894 об/мин, 44 км/ч, 48 °C |
| 26…31,5 с | два долгих нажатия на ходу | 31,02 с: отказ ENGINE ON: NOT CLEAR |
| 30…40 с | круиз 2200 об/мин, 90 км/ч | 38 с: 2200, 90, 78 °C |
| 52 с | двигатель заглушен | — |
| 54…56,5 с | вопрос — и без ответа | 61,02 с: вопрос закрыт через 5 с |
| 62…67,5 с | вопрос, «да» | 67,03 с: оба блока ответили 44; кодов 0, MIL погасла |
| 69…71 с | блоки молчат | 69,45 с: NO LINK; 71,10 с: связь вернулась |
Каждое разобранное значение сверено с тем, что модель блока в этот момент
закодировала: обороты — 1388 ответов, скорость — 694, температура — 347,
напряжение — 346, расхождений ноль. NO LINK появился через 450 мс
тишины — три таймаута по 150 мс. Ошибок ISO-TP и лишних ответов нет.
Тот же опыт собран arm-none-eabi-gcc 12.2.1 для Cortex-M3 и запущен в
qemu-system-arm -M netduinoplus2 — вывод совпал с ПК строка в строку.
Модель на Python отдельно пересчитала тайминг, фильтр, коды, собрала
VIN и ответы коробки из кадров и сверила значения на снимках экрана с
«правдой» поездки.
Прошивка BluePill (obd_f103.c: такт, bxCAN, I2C1, кнопка, SysTick;
ядро то же) собрана arm-none-eabi-gcc -Os -Wall -Wextra -Werror:
4972 байта флеш-памяти и 680 байт ОЗУ. bxCAN, I2C и дисплей на
железе не запускались — CAN в QEMU не моделируется.
Как повторить в машине: сначала экран INFO — видно, кто ответил на 0100 и какие PID знает блок двигателя. Не ответил никто — проверить выводы 6 и 14 и скорость: бывают машины с 29-битными адресами (0x18DB33F1 — их функциональный адрес в elm327-emulator), под них фильтр и адреса другие.
198р.7 Полный код прошивки
Прошивка — шесть своих файлов: ядро без железа obd_core.h и
obd_core.c, работа с регистрами F103 obd_f103.c, старт
start_f103.c, шрифт font5x7.h и скрипт компоновщика f103.ld. Ниже
все они целиком. Куски из 198р.3…198р.5 здесь повторяются внутри своих
функций. Чужого кода нет.
Сборка — те же команды, что в проверке. 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):
for f in start_f103 obd_f103 obd_core; do
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Os -Wall -Wextra -Werror -std=c11 -ffreestanding \
-ffunction-sections -fdata-sections -DSTM32F10X_MD -I. -I$CMSIS_F1 -I$CMSIS_CORE -c $f.c -o $f.o
done
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb start_f103.o obd_f103.o obd_core.o \
-T f103.ld -nostdlib -Wl,--gc-sections -o fw.elf -lgcc
Заголовок ядра. Адреса и фильтр, все сроки, кадр и состояние прибора:
/* Раздел 198р «Мини-проект: OBD-II сканер» — ядро без железа.
* Запросы OBD-II поверх ISO-TP (одиночный кадр туда, одиночный или составной обратно),
* расписание опроса, разбор PID и кодов ошибок, кнопка и экраны 21 × 8 символов.
* Одно и то же ядро собирается gcc на ПК (против модели ЭБУ) и arm-none-eabi-gcc для F103. */
#ifndef OBD_CORE_H
#define OBD_CORE_H
#include <stdint.h>
#define OBD_FUNC_ID 0x7DFu /* функциональный адрес: «все ЭБУ, кто поддерживает OBD» */
#define OBD_RESP_ID 0x7E8u /* ответы 0x7E8…0x7EF, запрос к одному ЭБУ — ответ минус 8 */
#define OBD_RESP_MASK 0x7F8u /* 8 младших идентификаторов — в одном фильтре-маске */
/* фильтр bxCAN, 32 бита, режим маски (RM0008, рис. 229): STID в битах 31:21, IDE — бит 2, RTR — бит 1 */
#define OBD_FR1 ((uint32_t)OBD_RESP_ID << 21)
#define OBD_FR2 (((uint32_t)OBD_RESP_MASK << 21) | 0x4u | 0x2u) /* IDE = 0 и RTR = 0 — тоже «должно совпасть» */
#define OBD_PAD 0xAAu /* добивка кадра до 8 байт — как ISO_TP_FRAME_PADDING_VALUE в isotp-c */
#define OBD_SLOT_MS 25u /* один запрос в слот */
#define OBD_P2_MS 150u /* ждём ответ — как UDS_CLIENT_DEFAULT_P2_MS в iso14229 */
#define OBD_COLLECT_MS 100u /* функциональный запрос: собираем ответы всех ЭБУ */
#define OBD_LOST_N 3u /* столько таймаутов подряд — «NO LINK» */
#define OBD_DEBOUNCE_MS 20u
#define OBD_LONG_MS 2000u /* долгое нажатие */
#define OBD_CONFIRM_MS 5000u /* окно подтверждения стирания */
#define OBD_BUF 64u /* буфер сборки ISO-TP */
#define OBD_MAX_DTC 8u
#define OBD_MAX_ECU 8u
typedef struct {
uint32_t id;
uint8_t dlc;
uint8_t d[8];
} obd_frame;
enum { SCR_LIVE, SCR_DTC, SCR_VIN, SCR_INFO, SCR_CONFIRM, SCR_N };
typedef struct {
/* что знаем о машине */
uint32_t pids[3]; /* поддержка PID 01…20, 21…40, 41…60 (ответы 0100, 0120, 0140) */
uint16_t ecu_id[OBD_MAX_ECU];
uint8_t n_ecu;
uint16_t engine; /* ЭБУ двигателя: ответил 0100 и поддерживает 0C */
/* последние значения; have_* — пришёл ли хоть один ответ */
uint16_t rpm_raw; /* 256·A + B, обороты ×4 */
uint8_t speed, coolant_raw, have;
uint16_t volt_mv;
uint8_t mil, n_dtc_mil; /* из 0101: лампа и число кодов */
uint16_t dtc[OBD_MAX_DTC];
uint8_t n_dtc, dtc_read;
char vin[18];
/* счётчики */
uint32_t tx, rx, timeouts, nrc, extra, isotp_err, cleared;
uint8_t lost, screen, cleared_ok, refused;
} obd_state;
#define HAVE_RPM 1u
#define HAVE_SPEED 2u
#define HAVE_COOL 4u
#define HAVE_VOLT 8u
extern obd_state obd;
void obd_init(void);
void obd_rx(const obd_frame *f, uint32_t now); /* кадр, прошедший фильтр */
void obd_poll(uint32_t now, int btn_down); /* вызывать каждую миллисекунду */
int plat_can_send(const obd_frame *f); /* даёт платформа; 0 — кадр ушёл в ящик */
/* разбор — отдельно, чтобы сверить с моделью */
unsigned obd_dtc_str(uint16_t code, char out[6]);
int32_t obd_decode(uint8_t pid, const uint8_t *ab); /* 0C — об/мин ×4, 0D — км/ч, 05 — °C, 42 — мВ */
extern char obd_lines[8][22]; /* экран: 8 строк по 21 символу */
void obd_render(uint32_t now);
/* журнал для опыта на ПК: что ушло и что разобрано; на плате — пустые функции */
void obd_log_req(uint32_t now, uint16_t id, uint8_t svc, uint8_t pid);
void obd_log_val(uint32_t now, uint8_t pid, int32_t v);
#endif
Ядро: состояние. Один запрос в полёте (job), сборка составного
ответа (tp), таблица опроса и состояние кнопки:
/* Раздел 198р «Мини-проект: OBD-II сканер» — ядро: ISO-TP, запросы, разбор, кнопка, экраны. */
#include "obd_core.h"
obd_state obd;
char obd_lines[8][22];
static struct {
uint8_t busy, func, svc, pid, answered;
uint16_t to;
uint32_t t0;
} job; /* один запрос в полёте */
static struct {
uint8_t active, sn;
uint16_t from, len, got;
uint8_t buf[OBD_BUF];
} tp; /* сборка составного ответа ISO-TP */
static uint32_t next_slot;
static uint8_t step, poll_i, want_clear, reread, miss;
static const uint8_t POLL[8] = {0x0C, 0x0D, 0x0C, 0x05, 0x0C, 0x0D, 0x0C, 0x42}; /* обороты — каждый второй слот */
static struct {
uint8_t raw, stable, long_fired;
uint32_t t_change, t_press, t_confirm;
} btn;
void obd_init(void)
{
uint8_t *p = (uint8_t *)&obd;
for (unsigned i = 0; i < sizeof obd; i++)
p[i] = 0;
job.busy = 0;
tp.active = 0;
next_slot = 0;
step = poll_i = want_clear = reread = miss = 0;
btn.raw = btn.stable = btn.long_fired = 0;
btn.t_change = btn.t_press = btn.t_confirm = 0;
}
Ядро: запрос. Поддержан ли PID — по картам 0100/0120/0140; кадр запроса — всегда 8 байт с добивкой; физический запрос — на адрес двигателя минус 8:
static int supported(uint8_t pid)
{
if (pid == 0u || pid > 0x60u)
return pid == 0u;
return (obd.pids[(pid - 1u) / 32u] >> (31u - (pid - 1u) % 32u)) & 1u;
}
static void send(uint16_t id, uint8_t n, uint8_t b0, uint8_t b1, uint8_t b2)
{
obd_frame f;
f.id = id;
f.dlc = 8; /* кадр всегда полный: 8 байт с добивкой */
f.d[0] = n; /* PCI одиночного кадра: 0x0N, N — байт полезных данных */
f.d[1] = b0;
f.d[2] = b1;
f.d[3] = b2;
for (unsigned i = 1u + n; i < 8u; i++)
f.d[i] = OBD_PAD;
if (plat_can_send(&f) == 0)
obd.tx++;
}
static void request(uint32_t now, int func, uint8_t svc, uint8_t pid, int has_pid)
{
job.busy = 1;
job.func = (uint8_t)func;
job.svc = svc;
job.pid = pid;
job.answered = 0;
job.to = func ? (uint16_t)OBD_FUNC_ID : (uint16_t)(obd.engine - 8u);
job.t0 = now;
if (svc == 0x03u)
obd.n_dtc = 0;
send(job.to, has_pid ? 2u : 1u, svc, pid, 0);
obd_log_req(now, job.to, svc, has_pid ? pid : 0xFFu);
}
Ядро: формулы и коды. Значения хранятся целыми: обороты ×4, напряжение в милливольтах:
int32_t obd_decode(uint8_t pid, const uint8_t *ab)
{
switch (pid) {
case 0x0C: return (int32_t)((ab[0] << 8) | ab[1]); /* (256A + B) / 4 об/мин — храним ×4 */
case 0x0D: return ab[0]; /* A км/ч */
case 0x05: return (int32_t)ab[0] - 40; /* A − 40 °C */
case 0x42: return (int32_t)((ab[0] << 8) | ab[1]); /* (256A + B) / 1000 В — храним в мВ */
default: return 0;
}
}
unsigned obd_dtc_str(uint16_t code, char out[6])
{
static const char sys[4] = {'P', 'C', 'B', 'U'}; /* два старших бита: система */
static const char hex[16] = "0123456789ABCDEF";
out[0] = sys[code >> 14];
out[1] = (char)('0' + ((code >> 12) & 3u)); /* следующие два бита: 0…3 */
out[2] = hex[(code >> 8) & 15u];
out[3] = hex[(code >> 4) & 15u];
out[4] = hex[code & 15u];
out[5] = 0;
return 5;
}
static void finish(void)
{
job.busy = 0;
miss = 0;
obd.lost = 0;
}
Ядро: разбор сообщения. Отказ 7F, карты поддержки (ЭБУ двигателя —
тот, кто знает PID 0C), лампа и число кодов, значения, список кодов,
стирание и VIN:
/* целое сообщение от ЭБУ from: p[0] — ответ службы (запрос + 0x40) */
static void message(uint16_t from, const uint8_t *p, unsigned n, uint32_t now)
{
obd.rx++;
if (!job.busy) {
obd.extra++;
return;
}
if (p[0] == 0x7Fu && n >= 2u && p[1] == job.svc) { /* отрицательный ответ: 7F, служба, причина */
obd.nrc++;
if (!job.func)
finish();
return;
}
if (p[0] != (uint8_t)(job.svc + 0x40u) || (!job.func && from != obd.engine)) {
obd.extra++;
return;
}
job.answered = 1;
if (job.svc == 0x01u) {
if (n < 2u || p[1] != job.pid) {
obd.extra++;
return;
}
if ((job.pid == 0x00u || job.pid == 0x20u || job.pid == 0x40u) && n >= 6u) {
uint32_t bm = ((uint32_t)p[2] << 24) | ((uint32_t)p[3] << 16) | ((uint32_t)p[4] << 8) | p[5];
if (job.pid == 0x00u && obd.n_ecu < OBD_MAX_ECU)
obd.ecu_id[obd.n_ecu++] = from;
if (job.pid == 0x00u && obd.engine == 0u && (bm >> (31u - (0x0Cu - 1u))) & 1u)
obd.engine = from; /* ЭБУ двигателя — тот, кто знает обороты */
if (job.pid != 0x00u || from == obd.engine)
obd.pids[job.pid / 32u] = bm;
} else if (job.pid == 0x01u) {
obd.mil = p[2] >> 7; /* A7 — лампа MIL, A6…A0 — число кодов */
obd.n_dtc_mil = p[2] & 0x7Fu;
} else if (n >= 3u) {
int32_t v = obd_decode(job.pid, p + 2);
if (job.pid == 0x0Cu) { obd.rpm_raw = (uint16_t)v; obd.have |= HAVE_RPM; }
if (job.pid == 0x0Du) { obd.speed = (uint8_t)v; obd.have |= HAVE_SPEED; }
if (job.pid == 0x05u) { obd.coolant_raw = p[2]; obd.have |= HAVE_COOL; }
if (job.pid == 0x42u) { obd.volt_mv = (uint16_t)v; obd.have |= HAVE_VOLT; }
obd_log_val(now, job.pid, v);
}
} else if (job.svc == 0x03u) { /* 43, число кодов, по два байта на код */
for (unsigned i = 2; i + 1u < n; i += 2u) {
uint16_t c = (uint16_t)((p[i] << 8) | p[i + 1u]);
if (c != 0u && obd.n_dtc < OBD_MAX_DTC)
obd.dtc[obd.n_dtc++] = c;
}
obd.dtc_read = 1;
} else if (job.svc == 0x04u) {
obd.cleared++;
obd.cleared_ok = 1;
} else if (job.svc == 0x09u && n >= 20u && p[1] == 0x02u) {
for (unsigned i = 0; i < 17u; i++) /* 49 02 01, затем 17 символов VIN */
obd.vin[i] = (char)p[3u + i];
obd.vin[17] = 0;
}
if (!job.func)
finish();
}
Ядро: ISO-TP на приёме. Одиночный кадр — сразу в разбор; первый кадр — управление потоком на адрес этого ЭБУ; последующие — по номерам 1…15, 0, 1…:
void obd_rx(const obd_frame *f, uint32_t now)
{
uint8_t type = f->d[0] >> 4;
uint16_t from = (uint16_t)f->id;
if (type == 0u) { /* одиночный кадр */
unsigned n = f->d[0] & 15u;
if (n == 0u || n > 7u || n + 1u > f->dlc) {
obd.isotp_err++;
return;
}
message(from, f->d + 1, n, now);
} else if (type == 1u) { /* первый кадр: длина 12 бит, 6 байт данных */
obd_frame fc;
tp.len = (uint16_t)(((f->d[0] & 15u) << 8) | f->d[1]);
if (tp.len < 8u || tp.len > OBD_BUF || f->dlc < 8u) {
obd.isotp_err++;
return;
}
for (unsigned i = 0; i < 6u; i++)
tp.buf[i] = f->d[2u + i];
tp.got = 6;
tp.sn = 1;
tp.from = from;
tp.active = 1;
fc.id = from - 8u; /* управление потоком — на физический адрес этого ЭБУ */
fc.dlc = 8;
fc.d[0] = 0x30; /* FC: продолжай */
fc.d[1] = 0; /* размер блока 0 — всё без остановок */
fc.d[2] = 0; /* STmin 0 мс */
for (unsigned i = 3; i < 8u; i++)
fc.d[i] = OBD_PAD;
if (plat_can_send(&fc) == 0)
obd.tx++;
} else if (type == 2u) { /* последующий кадр: номер 1…15, 0, 1… */
if (!tp.active || from != tp.from || (f->d[0] & 15u) != tp.sn) {
obd.isotp_err++;
tp.active = 0;
return;
}
for (unsigned i = 1; i < 8u && tp.got < tp.len; i++)
tp.buf[tp.got++] = f->d[i];
tp.sn = (tp.sn + 1u) & 15u;
if (tp.got == tp.len) {
tp.active = 0;
message(from, tp.buf, tp.len, now);
}
}
}
Ядро: кнопка. Дребезг 20 мс; короткое нажатие — следующий экран, долгое (2 с, по удержанию) — вопрос о стирании и ответ на него:
static void on_short(void)
{
obd.refused = 0;
obd.screen = (obd.screen == SCR_CONFIRM) ? (uint8_t)SCR_DTC : (uint8_t)((obd.screen + 1u) % 4u);
}
static void on_long(uint32_t now)
{
if (obd.screen == SCR_DTC) {
obd.screen = SCR_CONFIRM;
obd.refused = 0;
obd.cleared_ok = 0;
btn.t_confirm = now;
} else if (obd.screen == SCR_CONFIRM) {
obd.screen = SCR_DTC;
if (obd.lost || ((obd.have & HAVE_RPM) && obd.rpm_raw > 0u))
obd.refused = 1; /* двигатель работает или связи нет — не стираем */
else
want_clear = 1;
}
}
static void button(uint32_t now, int down)
{
if ((uint8_t)(down != 0) != btn.raw) {
btn.raw = (uint8_t)(down != 0);
btn.t_change = now;
}
if (btn.raw != btn.stable && now - btn.t_change >= OBD_DEBOUNCE_MS) {
btn.stable = btn.raw;
if (btn.stable) {
btn.t_press = now;
btn.long_fired = 0;
} else if (!btn.long_fired) {
on_short();
}
}
if (btn.stable && !btn.long_fired && now - btn.t_press >= OBD_LONG_MS) {
btn.long_fired = 1; /* долгое — по удержанию, не по отпусканию */
on_long(now);
}
if (obd.screen == SCR_CONFIRM && now - btn.t_confirm >= OBD_CONFIRM_MS)
obd.screen = SCR_DTC;
}
Ядро: расписание и таймауты. Сначала опознание (0100, 0120, 0140,
0101, VIN, коды), потом опрос по кругу; obd_poll() зовётся раз в
миллисекунду:
static void schedule(uint32_t now)
{
if (step == 0u) { request(now, 1, 0x01, 0x00, 1); step = 1; return; }
if (step == 1u) { step = 2; if (obd.engine && supported(0x20)) { request(now, 0, 0x01, 0x20, 1); return; } }
if (step == 2u) { step = 3; if (obd.engine && supported(0x40)) { request(now, 0, 0x01, 0x40, 1); return; } }
if (step == 3u) { step = 4; if (obd.engine && supported(0x01)) { request(now, 0, 0x01, 0x01, 1); return; } }
if (step == 4u) { step = 5; if (obd.engine) { request(now, 0, 0x09, 0x02, 1); return; } }
if (step == 5u) { step = 6; request(now, 1, 0x03, 0, 0); return; }
if (!obd.engine) { /* никто не ответил на 0100 — начать сначала */
step = 0;
return;
}
if (want_clear) { want_clear = 0; reread = 2; request(now, 1, 0x04, 0, 0); return; }
if (reread == 2u) { reread = 1; request(now, 0, 0x01, 0x01, 1); return; }
if (reread == 1u) { reread = 0; request(now, 1, 0x03, 0, 0); return; }
for (unsigned k = 0; k < 8u; k++) { /* по кругу, пропуская то, чего ЭБУ не знает */
uint8_t pid = POLL[poll_i];
poll_i = (uint8_t)((poll_i + 1u) & 7u);
if (supported(pid)) {
request(now, 0, 0x01, pid, 1);
return;
}
}
}
void obd_poll(uint32_t now, int btn_down)
{
button(now, btn_down);
if (job.busy) {
uint32_t dt = now - job.t0;
if (job.func && dt >= OBD_COLLECT_MS) {
if (job.answered)
finish();
else
job.busy = 0;
}
if (job.busy && !job.func && dt >= OBD_P2_MS)
job.busy = 0;
if (!job.busy && !job.answered) { /* никто не ответил */
obd.timeouts++;
tp.active = 0;
if (++miss >= OBD_LOST_N)
obd.lost = 1;
}
}
if (!job.busy && (int32_t)(now - next_slot) >= 0) {
next_slot = ((int32_t)(now - next_slot) >= (int32_t)OBD_SLOT_MS) ? now + OBD_SLOT_MS : next_slot + OBD_SLOT_MS;
schedule(now);
}
}
Ядро: экраны. Четыре экрана и вопрос о стирании — в буфер 8 строк по 21 символу; на дисплей его выводит платформа:
/* ── экраны ─────────────────────────────────────────────────────────────── */
static void put(unsigned row, unsigned col, const char *s)
{
while (*s && col < 21u)
obd_lines[row][col++] = *s++;
}
static void num(unsigned row, unsigned end, int32_t v) /* число вправо до столбца end */
{
char b[12];
unsigned n = 0, neg = v < 0;
uint32_t u = neg ? (uint32_t)(-v) : (uint32_t)v;
do {
b[n++] = (char)('0' + u % 10u);
u /= 10u;
} while (u);
if (neg)
b[n++] = '-';
for (unsigned i = 0; i < n && end >= i; i++)
obd_lines[row][end - i] = b[i];
}
static void hexn(unsigned row, unsigned col, uint32_t v, unsigned n)
{
static const char hex[16] = "0123456789ABCDEF";
for (unsigned i = 0; i < n; i++)
obd_lines[row][col + i] = hex[(v >> (4u * (n - 1u - i))) & 15u];
}
void obd_render(uint32_t now)
{
for (unsigned r = 0; r < 8u; r++) {
for (unsigned c = 0; c < 21u; c++)
obd_lines[r][c] = ' ';
obd_lines[r][21] = 0;
}
if (obd.screen == SCR_LIVE) {
put(0, 0, "OBD-II");
if (obd.engine) {
put(0, 8, "ECU");
hexn(0, 12, obd.engine, 3);
}
put(1, 0, "RPM");
put(2, 0, "SPEED");
put(3, 0, "COOLANT");
put(4, 0, "BATTERY");
if (obd.have & HAVE_RPM) num(1, 13, obd.rpm_raw / 4u); else put(1, 11, "---");
if (obd.have & HAVE_SPEED) num(2, 13, obd.speed); else put(2, 11, "---");
if (obd.have & HAVE_COOL) num(3, 13, (int32_t)obd.coolant_raw - 40); else put(3, 11, "---");
if (obd.have & HAVE_VOLT) {
uint32_t cv = (obd.volt_mv + 5u) / 10u; /* до сотых вольта */
num(4, 10, (int32_t)(cv / 100u));
obd_lines[4][11] = '.';
obd_lines[4][12] = (char)('0' + cv / 10u % 10u);
obd_lines[4][13] = (char)('0' + cv % 10u);
} else {
put(4, 11, "---");
}
put(1, 15, "RPM");
put(2, 15, "KM/H");
put(3, 15, "C");
put(4, 15, "V");
put(5, 0, obd.mil ? "MIL ON" : "MIL OFF");
put(5, 9, "DTC");
num(5, 14, obd.n_dtc_mil);
} else if (obd.screen == SCR_DTC || obd.screen == SCR_CONFIRM) {
if (obd.screen == SCR_DTC) {
put(0, 0, "STORED DTC:");
num(0, 13, obd.n_dtc);
for (unsigned i = 0; i < obd.n_dtc && i < 8u; i++) {
char s[6];
obd_dtc_str(obd.dtc[i], s);
put(1u + i / 2u, (i & 1u) ? 7u : 0u, s);
}
if (obd.refused)
put(5, 0, "ENGINE ON: NOT CLEAR");
else if (obd.cleared_ok)
put(5, 0, "CLEARED");
put(6, 0, "HOLD 2S: CLEAR");
put(7, 0, "PRESS: NEXT");
} else {
uint32_t left = (OBD_CONFIRM_MS - (now - btn.t_confirm) + 999u) / 1000u;
put(0, 0, "CLEAR ALL DTC?");
put(1, 0, "ALSO FREEZE FRAME");
put(2, 0, "AND READINESS DATA");
put(3, 0, "IGNITION ON,");
put(4, 0, "ENGINE OFF ONLY");
put(6, 0, "HOLD 2S: YES");
put(7, 0, "PRESS: NO");
num(7, 20, (int32_t)left);
}
} else if (obd.screen == SCR_VIN) {
put(0, 0, "VIN");
put(2, 0, obd.vin[0] ? obd.vin : "---");
} else {
put(0, 0, "ECU");
for (unsigned i = 0; i < obd.n_ecu && i < 4u; i++)
hexn(0, 4u + 4u * i, obd.ecu_id[i], 3);
for (unsigned r = 0; r < 3u; r++) {
put(1u + r, 0, r == 0u ? "PID 01-20" : r == 1u ? "PID 21-40" : "PID 41-60");
hexn(1u + r, 11, obd.pids[r], 8);
}
put(4, 0, "TX");
num(4, 9, (int32_t)obd.tx);
put(4, 11, "RX");
num(4, 20, (int32_t)obd.rx);
put(5, 0, "TIMEOUT");
num(5, 9, (int32_t)obd.timeouts);
put(5, 11, "NRC");
num(5, 20, (int32_t)obd.nrc);
}
if (obd.lost && obd.screen != SCR_CONFIRM)
put(7, 13, "NO LINK");
}
F103: сроки, очередь и прерывание. Проверки делителей CAN и I2C при сборке; прерывание приёма складывает кадры в очередь на 16 и считает потерянные:
/* Раздел 198р «Мини-проект: OBD-II сканер»: BluePill (STM32F103C8), регистры через CMSIS, без HAL.
* PB8/PB9 — CAN_RX/CAN_TX после ремапа (CAN_REMAP = 10), трансивер SN65HVD230 на 3,3 В;
* PB6/PB7 — I2C1 SCL/SDA к дисплею SSD1306 128 × 64 (адрес 0x3C), 400 кГц;
* PA0 — кнопка на землю, внутренняя подтяжка вверх.
* Такт: кварц 8 МГц × 9 = 72 МГц, APB1 = 36 МГц — от него тактуются bxCAN и I2C1. */
#include "stm32f10x.h"
#include "obd_core.h"
#include "font5x7.h"
#define FCPU 72000000u
#define FCAN 36000000u /* APB1 */
#define CAN_BRP 4u /* 36 МГц / 4 = 9 МГц: квант 111 нс */
#define CAN_TS1 15u
#define CAN_TS2 2u
#define CAN_SJW 2u
_Static_assert(FCAN / (CAN_BRP * (1u + CAN_TS1 + CAN_TS2)) == 500000u
&& FCAN % (CAN_BRP * (1u + CAN_TS1 + CAN_TS2)) == 0u, "500 кбит/с ровно");
#define I2C_CCR (FCAN / (3u * 400000u)) /* быстрый режим, DUTY = 0: Tlow = 2·Thigh */
_Static_assert(I2C_CCR == 30u, "36 МГц / (3 · 30) = 400 кГц");
#define OLED 0x3Cu
static volatile uint32_t ms;
static volatile obd_frame rxq[16]; /* кадры из прерывания — в главный цикл */
static volatile uint32_t rxh, rxt, rx_lost;
void SysTick_Handler(void)
{
ms++;
}
void USB_LP_CAN1_RX0_IRQHandler(void)
{
while (CAN1->RF0R & CAN_RF0R_FMP0) {
uint32_t lo = CAN1->sFIFOMailBox[0].RDLR, hi = CAN1->sFIFOMailBox[0].RDHR;
if (rxh - rxt < 16u) {
volatile obd_frame *f = &rxq[rxh % 16u];
f->id = CAN1->sFIFOMailBox[0].RIR >> 21; /* фильтр пропускает только 11-битные 0x7E8…0x7EF */
f->dlc = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0xFu);
for (unsigned i = 0; i < 4u; i++) {
f->d[i] = (uint8_t)(lo >> (8u * i));
f->d[4u + i] = (uint8_t)(hi >> (8u * i));
}
rxh++;
} else {
rx_lost++;
}
CAN1->RF0R = CAN_RF0R_RFOM0; /* отпустить ящик очереди */
}
}
F103: отправка кадра. Свободный ящик из трёх; журнал на плате пустой:
int plat_can_send(const obd_frame *f)
{
uint32_t tsr = CAN1->TSR;
unsigned m = (tsr & CAN_TSR_TME0) ? 0u : (tsr & CAN_TSR_TME1) ? 1u : (tsr & CAN_TSR_TME2) ? 2u : 3u;
if (m == 3u)
return 1; /* все три ящика заняты */
CAN1->sTxMailBox[m].TDTR = f->dlc;
CAN1->sTxMailBox[m].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[m].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[m].TIR = (f->id << 21) | CAN_TI0R_TXRQ;
return 0;
}
void obd_log_req(uint32_t now, uint16_t id, uint8_t svc, uint8_t pid)
{
(void)now; (void)id; (void)svc; (void)pid;
}
void obd_log_val(uint32_t now, uint8_t pid, int32_t v)
{
(void)now; (void)pid; (void)v;
}
F103: bxCAN. Режим настройки с ограничением ожидания, делители, фильтр-маска из ядра, прерывание и выход на шину:
static int can_init(void)
{
uint32_t t;
CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_ABOM; /* из сна — в инициализацию; после bus-off — вернуться самому */
for (t = 100000u; !(CAN1->MSR & CAN_MSR_INAK); t--)
if (t == 0u)
return 1;
CAN1->BTR = ((CAN_SJW - 1u) << 24) | ((CAN_TS2 - 1u) << 20) | ((CAN_TS1 - 1u) << 16) | (CAN_BRP - 1u);
CAN1->FMR |= CAN_FMR_FINIT; /* фильтр 0: маска, 32 бита, 0x7E8…0x7EF → очередь 0 */
CAN1->FA1R &= ~1u;
CAN1->FS1R |= 1u;
CAN1->FM1R &= ~1u;
CAN1->sFilterRegister[0].FR1 = OBD_FR1;
CAN1->sFilterRegister[0].FR2 = OBD_FR2;
CAN1->FFA1R &= ~1u;
CAN1->FA1R |= 1u;
CAN1->FMR &= ~CAN_FMR_FINIT;
CAN1->IER = CAN_IER_FMPIE0;
NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
CAN1->MCR &= ~CAN_MCR_INRQ; /* ждёт 11 рецессивных бит на RX */
for (t = 100000u; CAN1->MSR & CAN_MSR_INAK; t--)
if (t == 0u)
return 2;
return 0;
}
F103: дисплей. I2C опросом и SSD1306: команды включения и один символ (6 столбцов) за вызов, строка за строкой:
/* ── I2C1 опросом: одна короткая посылка за вызов ─────────────────────── */
static int i2c_wait(uint32_t flag)
{
for (uint32_t t = 20000u; !(I2C1->SR1 & flag); t--)
if (t == 0u || (I2C1->SR1 & I2C_SR1_AF))
return 1;
return 0;
}
static int i2c_write(uint8_t ctrl, const uint8_t *p, unsigned n)
{
int err = 0;
I2C1->CR1 |= I2C_CR1_START;
err |= i2c_wait(I2C_SR1_SB);
I2C1->DR = OLED << 1;
err |= i2c_wait(I2C_SR1_ADDR);
(void)I2C1->SR2; /* чтение SR1, затем SR2 снимает ADDR */
I2C1->DR = ctrl; /* 0x00 — дальше команды, 0x40 — данные */
for (unsigned i = 0; i < n && !err; i++) {
err |= i2c_wait(I2C_SR1_TXE);
I2C1->DR = p[i];
}
err |= i2c_wait(I2C_SR1_BTF);
I2C1->CR1 |= I2C_CR1_STOP;
if (err)
I2C1->SR1 = 0; /* снять AF */
return err;
}
static const uint8_t OLED_INIT[] = {
0xAE, /* выключить */
0xD5, 0x80, /* делитель частоты */
0xA8, 0x3F, /* мультиплекс: 64 строки − 1 */
0xD3, 0x00, /* смещение 0 */
0x40, /* начальная строка 0 */
0x8D, 0x14, /* накачка заряда: включить — панель питается от 3,3 В */
0x20, 0x02, /* адресация по страницам */
0xA1, 0xC8, /* развёртка: зеркала по X и Y — «вверх ногами» не будет */
0xDA, 0x12, /* раскладка COM для 128 × 64 */
0x81, 0x8F, /* яркость */
0xD9, 0xF1, /* предзаряд */
0xDB, 0x40, /* уровень VCOMH */
0xA4, 0xA6, /* из памяти, без инверсии */
0xAF, /* включить */
};
static unsigned orow, ocol; /* какой символ экрана выводим следующим */
static void oled_step(void)
{
uint8_t g[6];
if (ocol == 0u) {
if (orow == 0u)
obd_render(ms); /* новый кадр — снимок экрана из ядра */
uint8_t pos[3] = {(uint8_t)(0xB0u | orow), 0x02, 0x10}; /* страница, столбец 2 */
i2c_write(0x00, pos, 3);
}
uint8_t c = (uint8_t)obd_lines[orow][ocol];
const uint8_t *gl = (c >= 0x20u && c <= 0x5Au) ? FONT5X7[c - 0x20u] : FONT5X7[0];
for (unsigned i = 0; i < 5u; i++)
g[i] = gl[i];
g[5] = 0;
i2c_write(0x40, g, 6); /* 8 байт на шине: ≈ 0,18 мс на 400 кГц */
if (++ocol == 21u) {
ocol = 0;
orow = (orow + 1u) & 7u;
}
}
F103: такт и главный цикл. 72 МГц от кварца; выводы, I2C 400 кГц, SysTick; в цикле — кадры в ядро, раз в миллисекунду — кнопка, расписание и символ на дисплей:
static void clock_72mhz(void)
{
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY)) {
}
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2;
RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)) {
}
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}
}
int main(void)
{
uint32_t last = 0;
clock_72mhz();
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN | RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN | RCC_APB1ENR_I2C1EN;
AFIO->MAPR = AFIO_MAPR_CAN_REMAP_REMAP2; /* PB8 RX, PB9 TX; SWJ_CFG = 000 — отладчик жив */
GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x4u | (0xBu << 4); /* PB8 вход, PB9 — AF двухтактный */
GPIOB->CRL = (GPIOB->CRL & ~0xFF000000u) | (0xFu << 24) | (0xFu << 28); /* PB6, PB7 — AF открытый сток */
GPIOA->CRL = (GPIOA->CRL & ~0xFu) | 0x8u; /* PA0 — вход с подтяжкой… */
GPIOA->ODR |= 1u; /* …вверх */
I2C1->CR2 = FCAN / 1000000u; /* FREQ = 36 */
I2C1->CCR = I2C_CCR_FS | I2C_CCR;
I2C1->TRISE = FCAN / 1000000u * 300u / 1000u + 1u; /* 300 нс быстрого режима: 11 */
I2C1->CR1 = I2C_CR1_PE;
SysTick->LOAD = FCPU / 1000u - 1u;
SysTick->VAL = 0;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;
i2c_write(0x00, OLED_INIT, sizeof OLED_INIT);
obd_init();
can_init();
for (;;) {
while (rxt != rxh) { /* кадры из очереди прерывания — в ядро */
obd_frame f;
f.id = rxq[rxt % 16u].id;
f.dlc = rxq[rxt % 16u].dlc;
for (unsigned i = 0; i < 8u; i++)
f.d[i] = rxq[rxt % 16u].d[i];
rxt++;
obd_rx(&f, ms);
}
if (ms != last) { /* раз в миллисекунду: кнопка, таймауты, расписание… */
last = ms;
obd_poll(last, !(GPIOA->IDR & 1u));
oled_step(); /* …и один символ на дисплей */
}
}
}
Старт. Копия .data, обнуление .bss, таблица векторов с SysTick
и прерыванием CAN (номер 20, общий с USB):
/* Мини-проект «OBD-II сканер» (раздел 198р): стартовый код BluePill.
* Два обработчика: SysTick (миллисекунды ядра) и приём CAN (IRQ 20, общий с USB — RM0008, таблица векторов). */
#include <stdint.h>
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);
void SysTick_Handler(void);
void USB_LP_CAN1_RX0_IRQHandler(void);
void Reset_Handler(void)
{
uint32_t *src = &_sidata, *dst = &_sdata;
while (dst < &_edata)
*dst++ = *src++;
for (dst = &_sbss; dst < &_ebss;)
*dst++ = 0;
main();
for (;;) {
}
}
void Default_Handler(void)
{
for (;;) {
}
}
typedef void (*vector_t)(void);
__attribute__((section(".isr_vector"), used))
const vector_t vectors[16 + 21] = {
[0] = (vector_t)&_estack, [1] = Reset_Handler, [2] = Default_Handler, [3] = Default_Handler,
[4] = Default_Handler, [5] = Default_Handler, [15] = SysTick_Handler,
[16 + 20] = USB_LP_CAN1_RX0_IRQHandler,
};
Шрифт. 59 знаков 5 × 7, от пробела до Z, по 5 байт-столбцов:
/* font5x7.h — раздел 198р: шрифт 5 × 7 для SSD1306, символы 0x20…0x5A (пробел … Z).
* Сгенерирован obd_model.py --font; столбец — байт, младший бит — верхняя точка (страница SSD1306). */
#ifndef FONT5X7_H
#define FONT5X7_H
#include <stdint.h>
static const uint8_t FONT5X7[0x5B - 0x20][5] = {
{0x00, 0x00, 0x00, 0x00, 0x00}, /* ' ' */
{0x00, 0x00, 0x5F, 0x00, 0x00}, /* '!' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '"' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '#' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '$' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '%' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '&' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* ''' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '(' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* ')' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '*' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '+' */
{0x00, 0x50, 0x30, 0x00, 0x00}, /* ',' */
{0x08, 0x08, 0x08, 0x08, 0x08}, /* '-' */
{0x00, 0x60, 0x60, 0x00, 0x00}, /* '.' */
{0x20, 0x10, 0x08, 0x04, 0x02}, /* '/' */
{0x3E, 0x51, 0x49, 0x45, 0x3E}, /* '0' */
{0x00, 0x42, 0x7F, 0x40, 0x00}, /* '1' */
{0x42, 0x61, 0x51, 0x49, 0x46}, /* '2' */
{0x21, 0x41, 0x45, 0x4B, 0x31}, /* '3' */
{0x18, 0x14, 0x12, 0x7F, 0x10}, /* '4' */
{0x27, 0x45, 0x45, 0x45, 0x39}, /* '5' */
{0x3C, 0x4A, 0x49, 0x49, 0x30}, /* '6' */
{0x01, 0x71, 0x09, 0x05, 0x03}, /* '7' */
{0x36, 0x49, 0x49, 0x49, 0x36}, /* '8' */
{0x06, 0x49, 0x49, 0x29, 0x1E}, /* '9' */
{0x00, 0x36, 0x36, 0x00, 0x00}, /* ':' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* ';' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '<' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '=' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '>' */
{0x02, 0x01, 0x51, 0x09, 0x06}, /* '?' */
{0x00, 0x00, 0x00, 0x00, 0x00}, /* '@' */
{0x7E, 0x09, 0x09, 0x09, 0x7E}, /* 'A' */
{0x7F, 0x49, 0x49, 0x49, 0x36}, /* 'B' */
{0x3E, 0x41, 0x41, 0x41, 0x22}, /* 'C' */
{0x7F, 0x41, 0x41, 0x22, 0x1C}, /* 'D' */
{0x7F, 0x49, 0x49, 0x49, 0x41}, /* 'E' */
{0x7F, 0x09, 0x09, 0x09, 0x01}, /* 'F' */
{0x3E, 0x41, 0x49, 0x49, 0x7A}, /* 'G' */
{0x7F, 0x08, 0x08, 0x08, 0x7F}, /* 'H' */
{0x00, 0x41, 0x7F, 0x41, 0x00}, /* 'I' */
{0x20, 0x40, 0x41, 0x3F, 0x01}, /* 'J' */
{0x7F, 0x08, 0x14, 0x22, 0x41}, /* 'K' */
{0x7F, 0x40, 0x40, 0x40, 0x40}, /* 'L' */
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, /* 'M' */
{0x7F, 0x04, 0x08, 0x10, 0x7F}, /* 'N' */
{0x3E, 0x41, 0x41, 0x41, 0x3E}, /* 'O' */
{0x7F, 0x09, 0x09, 0x09, 0x06}, /* 'P' */
{0x3E, 0x41, 0x51, 0x21, 0x5E}, /* 'Q' */
{0x7F, 0x09, 0x19, 0x29, 0x46}, /* 'R' */
{0x46, 0x49, 0x49, 0x49, 0x31}, /* 'S' */
{0x01, 0x01, 0x7F, 0x01, 0x01}, /* 'T' */
{0x3F, 0x40, 0x40, 0x40, 0x3F}, /* 'U' */
{0x1F, 0x20, 0x40, 0x20, 0x1F}, /* 'V' */
{0x3F, 0x40, 0x38, 0x40, 0x3F}, /* 'W' */
{0x63, 0x14, 0x08, 0x14, 0x63}, /* 'X' */
{0x03, 0x04, 0x78, 0x04, 0x03}, /* 'Y' */
{0x61, 0x51, 0x49, 0x45, 0x43}, /* 'Z' */
};
#endif
Скрипт компоновщика. 63 КБ флеш-памяти, 20 КБ ОЗУ и 2 КБ под стек:
/* Мини-проект «OBD-II сканер» (раздел 198р): BluePill, 64 КБ флеш-памяти, 20 КБ ОЗУ.
* Каждая строка отвечает на один вопрос: где лежит, где работает, откуда берутся начальные значения. */
ENTRY(Reset_Handler) /* с чего начинается программа — для отладчика и ELF */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 63K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM); /* вершина стека: стек растёт вниз от конца ОЗУ */
_stack_size = 0x800; /* сколько ОЗУ оставить под стек — проверяется в ASSERT */
SECTIONS
{
.isr_vector :
{
KEEP(*(.isr_vector)) /* таблица векторов — первой, с адреса 0x08000000 */
} > FLASH
.text :
{
*(.text .text.*) /* код */
*(.rodata .rodata.*) /* константы и строки */
. = ALIGN(4);
_etext = .;
} > FLASH
.data :
{
_sdata = .; /* где .data работает — адрес в ОЗУ (VMA) */
*(.data .data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH /* а хранится во флеш-памяти сразу за кодом (LMA) */
_sidata = LOADADDR(.data); /* откуда стартовый код копирует начальные значения */
.bss (NOLOAD) :
{
_sbss = .; /* нули: во флеш-памяти не хранятся вовсе */
*(.bss .bss.*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
ASSERT(_ebss + _stack_size <= _estack, "RAM too small for .data + .bss + stack")
}
198р.8 Грабли
На шине ничего нет. Без фильтра bxCAN не принимает ничего (19.5), а с фильтром на 0x7E8 без маски — только двигатель. Маска 0x7F8 пропускает все восемь адресов ответа.
Составной ответ обрывается после первого кадра. Блок ждёт FC. Его шлют на физический адрес блока (ответ минус 8), а не на 0x7DF.
Ответили два блока, в памяти каша. У ядра один буфер сборки: если два блока шлют составные ответы одновременно, второй первый кадр перезапишет первый. В опыте двигатель отвечает одиночным кадром, коробка составным, и всё сходится; для надёжности нужен буфер на каждый адрес.
Напряжение не показывается. PID 0x42 — в диапазоне 41…60, его поддержку сообщает ответ 0140, а о нём — последний бит 0120. Одной таблицы 0100 мало.
Обороты в четыре раза больше. 256·A + B — это обороты × 4 (UAS 0x07 — 0,25 об/мин на единицу).
Коды стёрлись на ходу. Служба 04 стирает и стоп-кадр, и готовность систем. Долгое нажатие с подтверждением и запрет при оборотах больше нуля.
Дисплей тормозит опрос. Весь буфер по I2C — 23,1 мс в главном цикле; по символу — 0,18 мс.
Сканер разряжает аккумулятор. Вывод 16 — прямо от аккумулятора; на многих машинах питание на нём есть и при выключенном зажигании (общеизвестно, не проверено). Сканер на ночь вынимают.
198р.9 Итоги
- Разъём J1962: 16 — +12 В, 4 и 5 — земля, 6 и 14 — CAN-H и CAN-L; трансивер SN65HVD230 на 3,3 В, без терминатора; питание — через предохранитель, диод и ограничитель (173), стабилизатор импульсный.
- bxCAN от 36 МГц: BRP 4, TS1 15, TS2 2, SJW 2 — 500 кбит/с ровно,
точка выборки 88,9 %,
BTR = 0x011E0003; одним фильтром-маской (FR1 0xFD000000, FR2 0xFF000006) проходят ровно 8 адресов 0x7E8…0x7EF. - ISO-TP: одиночный кадр туда, одиночный или составной обратно; FC — на физический адрес ответившего блока; VIN — 20 байт в трёх кадрах.
- Опрос: слот 25 мс, обороты каждые 50 мс, скорость 100 мс, температура и напряжение 200 мс; 80 кадров/с — 1,78…2,16 % шины.
- Опыт на 72 с: 2775 значений совпали с моделью блока, коды P0133 и P0420
у двигателя и три у коробки, стирание только с подтверждением и на
заглушенном двигателе,
NO LINKчерез 450 мс; ПК и QEMU — строка в строку. - Прошивка BluePill — 4972 байта флеш-памяти и 680 байт ОЗУ.
Дальше: раздел 198с «Мини-проект: приборная панель по CAN — обороты, скорость и температура из шины машины» — тот же разъём и та же шина, но без вопросов: машина сама рассылает обороты и скорость, их остаётся услышать.