19в. ISO-TP (ISO 15765-2): длинные сообщения поверх CAN
Раздел 19 довёл CAN до кадров и фильтров, 19б — до CANopen, где всё нужное укладывается в 8 байт кадра, а длинное значение SDO режет на сегменты по-своему. В автомобиле так нельзя: номер кузова (VIN) — 17 знаков, список кодов ошибок — десятки байт, блок прошивки — килобайты. Всё это ходит между тестером и блоками машины по ISO-TP — транспорту из стандарта ISO 15765-2. Он режет сообщение на кадры, нумерует их, а приёмник говорит передатчику, с какой скоростью слать.
Здесь — четыре вида кадров побайтно, управление потоком (BS и STmin), длина до 4095 байт и дальше, адреса диагностики в машине (0x7DF, 0x7E0, 0x7E8, 0x18DB33F1), дополнение кадра до 8 байт, тайм-ауты и что бывает, когда ответ не пришёл. В конце — открытая библиотека isotp-c поверх bxCAN STM32F103: как её подключить и сколько она занимает.
Опоры: стандарт ISO 15765-2 платный и здесь не сверялся. Поведение взято из живого
прогона библиотеки isotp-c v1.9.3 (лицензия MIT): две её копии —
тестер и блок двигателя — обменивались по модели шины CAN в памяти, все
кадры записаны с временем. Константы и правила сверены с тремя другими
реализациями: подсистемой ISO-TP Zephyr 4.4.2 (её описание и Kconfig
ссылаются на ISO 15765-2), заголовком linux/can/isotp.h из can-utils и
разбором кадров в python-obd 0.7.3; адреса OBD — ещё и с эмулятором
elm327-emulator. Длины кадров — по RM0090 и canframelen.c из can-utils.
Прогон — _verify/isotp19v/isotp19v.py, запись кадров —
_verify/isotp19v/isotp19v_host.txt. Лицензия isotp-c — MIT
(© Li Shen, Simon Cahill), canframelen.c — BSD-3-Clause или GPL-2.0.
Как это называется в документации
Оригинал Перевод ISO-TP, ISO 15765-2, DoCAN транспорт диагностики поверх CAN PCI (protocol control information) служебный байт (байты) в начале кадра SF, FF, CF, FC (single, first, consecutive, flow control frame) одиночный, первый, следующий кадр, кадр управления потоком SF_DL, FF_DL длина сообщения в одиночном и первом кадре SN (sequence number) номер следующего кадра, 4 бита FS: CTS, WAIT, OVFLW состояние потока: можно, жди, переполнение BS (block size) сколько CF до следующего FC STmin (separation time) наименьшая пауза между CF N_As, N_Ar, N_Bs, N_Cr тайм-ауты: передачи кадра, ожидания FC, ожидания CF functional / physical addressing функциональный (всем) и физический (одному) адрес normal, extended, fixed addressing адресация: обычная, расширенная, фиксированная (29 бит) padding дополнение кадра до 8 байт tester, ECU тестер (диагностический прибор), блок управления
19в.1 Почему 8 байт мало
В классическом кадре CAN данных от 0 до 8 байт (RM0090, рис. 347). Для датчика или команды приводу этого хватает. Диагностике — нет: ответ на запрос VIN — это 3 байта заголовка и 17 знаков, 20 байт. Длина сообщения в ISO-TP доходит до 4095 байт в обычной записи и до 32-битной — в расширенной; описание Zephyr прямо пишет «теоретически до четырёх гигабайт».
ISO-TP делает три вещи:
- режет сообщение на кадры и пишет в первый его длину, а в каждый следующий — номер;
- собирает на приёмной стороне и проверяет, что номера идут подряд;
- даёт приёмнику тормоз: тот отвечает кадром управления потоком — сколько кадров слать подряд и с какой паузой, или «подожди», или «не влезет».
Короткое сообщение (до 7 байт) уходит одним кадром без всяких ответов — так в прогоне ушёл двухбайтный запрос VIN. Длинное — первым кадром, потом кадр управления от приёмника, потом следующие.
19в.2 Четыре вида кадров побайтно
Первый байт кадра — PCI. Старшая тетрада — вид кадра, младшая —
длина, номер или состояние (python-obd так и берёт вид кадра:
frame.data[0] & 0xF0):
| Вид | Старшая тетрада | Что дальше | Данных в кадре |
|---|---|---|---|
| SF, одиночный | 0 | младшая тетрада — длина 1…7 | до 7 байт |
| FF, первый | 1 | 12 бит длины: младшая тетрада и байт 1 | 6 байт |
| CF, следующий | 2 | младшая тетрада — номер SN | 7 байт |
| FC, управление | 3 | младшая тетрада — FS, байт 1 — BS, байт 2 — STmin | нет |
Обмен «запрос VIN» из прогона (тестер спрашивает всех, отвечает двигатель; время — конец кадра на шине 500 кбит/с):
| Время | Идентификатор | Байты | Что это |
|---|---|---|---|
| 0,222 мс | 0x7DF | 02 09 02 CC CC CC CC CC | SF: 2 байта, сервис 09, параметр 02 |
| 1,223 мс | 0x7E8 | 10 14 49 02 01 57 50 30 | FF: длина 0x014 = 20, первые 6 байт |
| 1,445 мс | 0x7E0 | 30 08 01 CC CC CC CC CC | FC: можно, BS 8, STmin 1 мс |
| 2,223 мс | 0x7E8 | 21 5A 5A 5A 39 39 5A 54 | CF 1: ещё 7 байт |
| 4,223 мс | 0x7E8 | 22 53 33 39 30 30 30 30 | CF 2: последние 7 байт |
6 + 7 + 7 = 20 — сообщение собрано, тестер получил 20 байт, и они совпали с отправленными. Кадр управления тестер шлёт не на 0x7DF, а на физический адрес двигателя 0x7E0: функциональный запрос бывает только одиночным кадром (так сказано и в описании isotp-c), дальше разговор идёт один на один.
Номер SN в CF начинается с 1, растёт до F и переходит в 0. В прогоне сообщения из 4095 байт после CF с байтом 2F пришёл 20, затем 21 — приёмник isotp-c ждёт именно такой номер, иначе бросает сообщение с ошибкой «неверный номер».
Длина больше 4095. 12 бит длины FF хватает на 4095 байт. Для большего
сообщения FF пишет длину 0, а настоящую — в следующие 4 байта, старшим
вперёд. В прогоне сообщение из 4096 байт начиналось так:
10 00 00 00 10 00 01 08 — после 0x00001000 = 4096 в первом кадре
осталось место только на 2 байта данных. Сообщение из 4095 байт — ещё
в обычной записи: 1F FF … и 6 байт данных.
19в.3 Управление потоком: FS, BS и STmin
Кадр управления (FC) шлёт приёмник сразу после FF, а потом — после каждых BS кадров CF. В нём три поля.
FS — состояние: 0 — можно (CTS), 1 — жди (WAIT), 2 — переполнение
(OVFLW). WAIT передатчик терпит ограниченное число раз: в isotp-c по
умолчанию 1 (ISO_TP_MAX_WFT_NUMBER), в Zephyr — 10; дальше — отказ.
OVFLW значит «сообщение не влезет». В прогоне у тестера был буфер 4095
байт, а блок начал слать 4096: тестер ответил на FF кадром
32 00 00 CC CC CC CC CC, и передача кончилась ошибкой переполнения (−8)
у обеих сторон — через 0,444 мс, не дожидаясь ни одного CF.
BS — сколько CF слать до следующего FC; 0 — всё сообщение без остановок. При BS 8 сообщение из 100 байт — это FF, 14 CF (6 + 13 × 7 + 3) и 2 FC: после FF и после восьмого CF. Всего 17 кадров. 4095 байт — FF, 585 CF и 74 FC, 660 кадров.
STmin — наименьшая пауза между CF, один байт с двумя диапазонами. Как их понимает isotp-c (прогон её собственных функций):
| Байт STmin | Пауза |
|---|---|
| 0x00 | 0 |
| 0x01…0x7F | 1…127 мс: 0x01 — 1000 мкс, 0x14 — 20 000 мкс, 0x7F — 127 000 мкс |
| 0x80…0xF0 | не определено; isotp-c читает как 0 |
| 0xF1…0xF9 | 100…900 мкс: 0xF1 — 100, 0xF5 — 500, 0xF9 — 900 |
| 0xFA…0xFF | не определено; isotp-c читает как 0 |
Те же два диапазона и «зарезервировано» между ними записаны в isotp.h
Zephyr и в linux/can/isotp.h. Обратно, из микросекунд в байт, isotp-c
кодирует 500 мкс как 0xF5, 1500 мкс — как 0x01 (округляет вниз до
миллисекунд), 950 мкс — как 0x00, а 128 000 мкс, что уже не
помещается, — тоже как 0x00: просили паузу, а вышло «без паузы».
19в.4 Адреса в машине и дополнение кадра
У ISO-TP нет своего поля адреса — адрес задаёт идентификатор CAN. В диагностике легковых машин по OBD-II приняты два набора:
| Что | 11 бит | 29 бит |
|---|---|---|
| запрос всем блокам (функциональный) | 0x7DF | 0x18DB33F1 |
| запрос одному блоку (физический) | 0x7E0…0x7E7 | 0x18DAxxF1, xx — номер блока |
| ответ блока | запрос + 8: 0x7E8…0x7EF | 0x18DAF1xx |
0x7DF и 0x18DB33F1 как функциональные адреса записаны в elm327-emulator, пары «запрос — ответ» там же: двигатель 0x7E0/0x7E8, коробка 0x7E1/0x7E9, блок гибрида 0x7E2/0x7EA. python-obd узнаёт ответ по биту 0x08 в идентификаторе и берёт номер блока из трёх младших бит — отсюда 0x7E8…0x7EF.
29-битный идентификатор разложен по полям (схема из isotp.h Zephyr,
«фиксированная адресация по SAE J1939»):
| Биты | 28…26 | 25…24 | 23…16 | 15…8 | 7…0 |
|---|---|---|---|---|---|
| Поле | приоритет | EDP, DP | тип адреса | кому | от кого |
| 0x18DB33F1 | 6 | 0 | 0xDB — функциональный | 0x33 — всем | 0xF1 — тестер |
| 0x18DA10F1 | 6 | 0 | 0xDA — физический | 0x10 | 0xF1 |
python-obd разбирает эти байты так же: DB — функциональный, DA — физический, 0x33 — всем, 0xF1 — тестер. Прогон с 29-битными адресами дал тот же обмен, что и с 11-битными, только идентификаторы другие, а каждый кадр на 20 бит длиннее: 67 + 8n бит вместо 47 + 8n (RM0090, рис. 347, плюс 3 бита паузы) — VIN-ответ дошёл за те же 5 мс, кадры сдвинулись на десятки микросекунд.
Расширенная адресация (extended) кладёт адрес получателя в первый байт данных, а PCI сдвигается на байт: в SF остаётся 6 байт данных вместо 7 (так считает Zephyr: длина кадра минус 2). isotp-c её не поддерживает — в её коде нет ни поля, ни флага для этого байта. Стандарт описывает ещё смешанную адресацию; ни isotp-c, ни Zephyr отдельным режимом её не выделяют, и здесь она не разбирается.
Дополнение. isotp-c по умолчанию шлёт кадр ровно той длины, что нужна:
запрос VIN без дополнения в прогоне ушёл как 7DF [3] 02 09 02, FC —
как [3] 30 08 00. Блоки машин ждут кадр в 8 байт: AUTOSAR требует
длину 8 у SF, FC и последнего CF (так описан флаг
ISOTP_REQUIRE_RX_PADDING в Zephyr), rusEFI всегда отвечает на OBD-запрос
кадром в 8 байт. В isotp-c дополнение включают ISO_TP_FRAME_PADDING;
байт по умолчанию — 0xAA. Zephyr и Linux берут 0xCC: Zephyr — «как
рекомендует Bosch», Linux — чтобы не было вставленных битов.
Сколько вставленных битов на деле — кадр 0x7DF 02 09 02 и 5 байт
дополнения, подсчёт по правилам CAN, сверенный с canframelen.c из
can-utils:
| Дополнение | Вставлено бит | Длина кадра, бит |
|---|---|---|
| 0x00 | 13 | 124 |
| 0x55 | 5 | 116 |
| 0xAA | 5 | 116 |
| 0xCC | 6 | 117 |
| 0xFF | 14 | 125 |
Без вставок кадр — 111 бит. Нули и единицы дают больше всего вставок; 0xAA, 0x55 и 0xCC почти ничего к ним не добавляют — вставки остаются от идентификатора и CRC.
19в.5 Тайминги и тайм-ауты
Стандарт задаёт тайм-ауты, после которых сторона бросает сообщение. Zephyr ставит все три по умолчанию в 1000 мс со ссылкой на ISO 15765-2:
| Тайм-аут | Кто ждёт | Чего |
|---|---|---|
| N_As, N_Ar | передатчик кадра | пока драйвер CAN отправит кадр |
| N_Bs | передатчик сообщения | следующего FC |
| N_Cr | приёмник сообщения | следующего CF |
В isotp-c один общий тайм-аут ISO_TP_DEFAULT_RESPONSE_TIMEOUT_US
и для N_Bs, и для N_Cr, по умолчанию 100 000 мкс, то есть 100 мс;
N_As не отслеживается. Чтобы совпасть с 1000 мс, ставят 1 000 000. Что было в
прогоне:
| Сценарий | Итог | Когда |
|---|---|---|
| блок шлёт FF, тестера на шине нет | передатчик: ошибка −2, тайм-аут N_Bs | 1001 мс |
| то же с настройкой по умолчанию | передатчик: ошибка −2 | 101 мс |
| блок замолк после 5 CF из 14 | приёмник: ошибка −3, тайм-аут N_Cr | 1010 мс |
Лишняя миллисекунда — шаг опроса: isotp_poll() вызывалась раз в 1 мс, а
просрочку она замечает строго после срока. В последней строке последний
CF пришёл на 9,223 мс, срок вышел на 1009,223 мс, а заметили на 1010 мс.
Ни одна сторона не шлёт другой «я сдаюсь»: приёмник просто выбрасывает
недособранное, а передатчик ставит состояние «ошибка» — что делать
дальше, решает программа.
Шаг опроса решает скорость. isotp-c шлёт CF только из isotp_poll()
и не больше одного за вызов, а следующий — на первом вызове строго позже
конца паузы STmin. Прогон при BS 8 и STmin 1 мс:
| Сообщение | Опрос | Шаг CF | До приёма |
|---|---|---|---|
| 100 байт | 1 мс | 2000 мкс | 28 мс |
| 100 байт | 0,1 мс | 1100 мкс | 15,1 мс |
| 4095 байт | 1 мс | 2000 мкс | 1,17 с |
И без тормоза — BS 0, STmin 0:
| Сообщение | Опрос | Шаг CF | До приёма |
|---|---|---|---|
| 4095 байт | 1 мс | 1000 мкс | 586 мс |
| 4095 байт | 0,1 мс | 222…444 мкс | 130,9 мс |
| 4095 байт | 0,01 мс | 222…444 мкс | 130,77 мс |
222 мкс — это сам кадр из 8 байт: 111 бит по 2 мкс. При опросе раз в
миллисекунду 585 CF шли 586 мс — в 4,5 раза медленнее, чем позволяет шина.
444 мкс — там, где между двумя CF на шину встал FC тестера: при BS 0
isotp-c всё равно шлёт FC 30 00 00 после каждых 256 CF — счётчик блока
у неё 8-битный. В прогоне таких FC было два, передатчику они не мешают.
19в.6 isotp-c поверх bxCAN F103
Библиотека — один файл isotp.c и заголовки, без динамической памяти.
От программы ей нужны три функции: isotp_user_send_can() — отправить
кадр, isotp_user_get_us() — время в микросекундах, isotp_user_debug() —
сообщения, можно пустую. Сама программа вызывает:
isotp_init_link()— канал: свой идентификатор отправки и два буфера;isotp_send()/isotp_send_with_id()— отправить (второй — с другим идентификатором, например 0x7DF);isotp_on_can_message()— отдать библиотеке принятый кадр этого канала: идентификаторы она не смотрит, фильтровать — программе;isotp_poll()— продвинуть передачу и проверить тайм-ауты;isotp_receive()— забрать собранное сообщение.
Ниже — тестер на STM32F103C8 (файл isotp19v_f103.c): раз в секунду
спрашивает VIN у всех блоков и собирает ответ двигателя. Отправка кадра —
в свободный ящик bxCAN; если все три заняты, библиотека повторит на
следующем опросе. Идентификатор больше 0x7FF считается 29-битным —
isotp-c передаёт только число, а 11 или 29 бит, решает эта функция:
int isotp_user_send_can(const uint32_t id, const uint8_t *data, const uint8_t size)
{
uint32_t box = (CAN1->TSR & CAN_TSR_CODE) >> 24; /* номер свободного ящика */
uint32_t lo = 0, hi = 0;
if ((CAN1->TSR & (CAN_TSR_TME0 << box)) == 0u) {
return ISOTP_RET_NOSPACE; /* все три заняты: библиотека повторит */
}
for (uint32_t i = 0; i < size; i++) {
if (i < 4u) lo |= (uint32_t)data[i] << (8u * i);
else hi |= (uint32_t)data[i] << (8u * (i - 4u));
}
CAN1->sTxMailBox[box].TDTR = size;
CAN1->sTxMailBox[box].TDLR = lo;
CAN1->sTxMailBox[box].TDHR = hi;
CAN1->sTxMailBox[box].TIR = (id > 0x7FFu)
? ((id << 3) | CAN_TI0R_IDE | CAN_TI0R_TXRQ) /* 29 бит: EXID с бита 3 */
: ((id << 21) | CAN_TI0R_TXRQ); /* 11 бит: STID с бита 21 */
return ISOTP_RET_OK;
}
Три ящика bxCAN по умолчанию уходят не по порядку записи, а по идентификатору, при равных — ящик с меньшим номером первым (RM0090, 32.7.1). У всех CF одного сообщения идентификатор один, и CF, записанный в ящик 0 позже, обгонит записанный раньше в ящик 2 — приёмник увидит неверный номер. Бит TXFP переводит ящики в очередь по порядку записи — в RM0090 так и сказано: полезно для сегментированной передачи. Фильтр — один 32-битный в режиме маски: пропускает 0x7E8…0x7EF и только 11-битные:
CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_TXFP | CAN_MCR_ABOM; /* TXFP: ящики уходят в порядке записи */
CAN1->sFilterRegister[0].FR1 = ID_RESP << 21; /* 0x7E8, IDE = 0 */
CAN1->sFilterRegister[0].FR2 = (0x7F8u << 21) | CAN_RI0R_IDE; /* 0x7E8…0x7EF, только 11 бит */
Прерывание приёма только копирует кадр в кольцо на 16 кадров. Все вызовы
библиотеки — из главного цикла: для одного канала вызовы не должны
перекрываться (так требует описание isotp-c), а isotp_on_can_message()
из прерывания могла бы влезть посреди isotp_poll():
for (;;) {
while (tail != head) { /* кадры из прерывания — в библиотеку */
rx_frame *f = &ring[tail % 16u];
if (f->id == ID_RESP) {
isotp_on_can_message(&link, f->d, f->len);
}
tail++;
}
isotp_poll(&link); /* тайм-ауты и следующие CF */
isotp_poll() здесь зовётся на каждом проходе цикла, а не раз в
миллисекунду — иначе своя длинная передача пойдёт со скоростью опроса
(19в.5). Время для библиотеки — счёт миллисекунд SysTick плюс текущее
значение его счётчика.
Сборка arm-none-eabi-gcc 12.2 (-Os, -Wall -Wextra -Werror, с
ISO_TP_FRAME_PADDING, 0xCC, тайм-аут 1 000 000 мкс и
ISO_TP_NO_FORMATTED_ERRORS — без snprintf) прошла без предупреждений:
вся прошивка — 3288 байт флеш-памяти и 8620 байт ОЗУ, из них
8190 — два буфера по 4095 байт (приёмный буфер канала и копия ответа).
Сам isotp.c — 2063 байта кода, структура канала IsoTpLink — 96 байт.
Для сравнения, в isotp_config.h автор приводит 2091 байт для Cortex-M4
без форматированных сообщений. У F407 bxCAN с теми же регистрами (RM0090),
для него меняются тактирование, выводы и заголовок; эта сборка не
проверялась.
19в.7 Полный код тестера на F103
Здесь весь isotp19v_f103.c по порядку, кроме isotp_user_send_can():
она целиком приведена в 19в.6 и стоит в файле сразу за
isotp_user_get_us(). Вместе с isotp-c и скриптом компоновщика это вся
прошивка тестера. Из isotp-c нужны пять файлов: isotp.c, isotp.h,
isotp_config.h, isotp_defines.h, isotp_user.h —
github.com/SimonCahill/isotp-c, версия v1.9.3 (коммит 1fc19e2), без правок.
Сборка — та же команда, что в проверке. 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):
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -Os -std=c99 -Wall -Wextra -Werror \
-ffunction-sections -fdata-sections -DNDEBUG -DISO_TP_NO_FORMATTED_ERRORS \
-DISO_TP_FRAME_PADDING -DISO_TP_FRAME_PADDING_VALUE=0xCC \
-DISO_TP_DEFAULT_RESPONSE_TIMEOUT_US=1000000 -DSTM32F10X_MD \
-I$CMSIS_F1 -I$CMSIS_CORE -I. isotp.c isotp19v_f103.c \
-T f103.ld -nostartfiles --specs=nano.specs -Wl,--gc-sections -o f.elf
Начало файла и время. Делители CAN с проверкой при сборке, адреса
OBD, буферы канала и кольцо на 16 кадров. isotp_user_get_us() читает
счёт миллисекунд и счётчик SysTick; цикл повторяет чтение, если
прерывание SysTick успело сменить ms между двумя чтениями:
/* Раздел 19в — ISO-TP (isotp-c, MIT) поверх bxCAN STM32F103C8: тестер спрашивает блоки машины.
* PB8/PB9 — CAN_RX/CAN_TX после ремапа; кварц 8 МГц × 9 = 72 МГц, APB1 = 36 МГц;
* 500 кбит/с: 36 МГц / 4 = 9 МГц, 1 + 15 + 2 = 18 квантов, точка выборки 16/18.
* Прерывание приёма только складывает кадр в кольцо; isotp_on_can_message и isotp_poll
* вызываются из одного места — главного цикла: библиотека требует, чтобы вызовы
* для одного канала не перекрывались. */
#include "stm32f10x.h"
#include "isotp.h"
#define FCAN 36000000u
#define BRP 4u
#define TS1 15u
#define TS2 2u
_Static_assert(FCAN / (BRP * (1u + TS1 + TS2)) == 500000u, "500 кбит/с ровно");
#define ID_FUNC 0x7DFu /* функциональный запрос: всем блокам */
#define ID_REQ 0x7E0u /* физический запрос двигателю */
#define ID_RESP 0x7E8u /* ответ двигателя = 0x7E0 + 8 */
typedef struct { uint32_t id; uint8_t len; uint8_t d[8]; } rx_frame;
static IsoTpLink link;
static uint8_t t_tx[64], t_rx[4095];
static volatile uint32_t ms;
static rx_frame ring[16];
static volatile uint32_t head, tail;
void isotp_user_debug(const char *message, ...) { (void)message; }
void SysTick_Handler(void)
{
ms++;
}
/* микросекунды из счёта мс и текущего значения SysTick (он считает вниз от 71999) */
uint32_t isotp_user_get_us(void)
{
uint32_t m, v;
do {
m = ms;
v = SysTick->VAL;
} while (m != ms);
return m * 1000u + (71999u - v) / 72u;
}
Прерывание приёма. Разбирает очередь FIFO 0 до дна: идентификатор
(11 или 29 бит по биту IDE), длина, 8 байт данных — и освобождает ящик
очереди битом RFOM0. Кольцо не должно переполняться: при 16 непрочитанных
кадрах новый ложится поверх самого старого, а head не растёт.
void USB_LP_CAN1_RX0_IRQHandler(void)
{
while (CAN1->RF0R & CAN_RF0R_FMP0) {
rx_frame *f = &ring[head % 16u];
uint32_t rir = CAN1->sFIFOMailBox[0].RIR;
uint32_t lo = CAN1->sFIFOMailBox[0].RDLR, hi = CAN1->sFIFOMailBox[0].RDHR;
f->id = (rir & CAN_RI0R_IDE) ? (rir >> 3) : (rir >> 21);
f->len = (uint8_t)(CAN1->sFIFOMailBox[0].RDTR & 0xFu);
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));
}
CAN1->RF0R = CAN_RF0R_RFOM0;
if (head - tail < 16u) {
head++;
}
}
}
Такт 72 МГц и bxCAN. Кварц 8 МГц, умножение на 9, две задержки
флеш-памяти, APB1 делится на 2 — 36 МГц для bxCAN. В can_init(): ремап
CAN на PB8/PB9, режим настройки, делители, фильтр на 0x7E8…0x7EF,
прерывание по приёму и выход на шину:
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) {
}
}
static void can_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_CAN1EN;
AFIO->MAPR |= AFIO_MAPR_CAN_REMAP_REMAP2; /* CAN на PB8/PB9 */
GPIOB->CRH = (GPIOB->CRH & ~0xFFu) | 0x4u | (0xBu << 4); /* PB8 вход, PB9 альт. выход 50 МГц */
CAN1->MCR = CAN_MCR_INRQ | CAN_MCR_TXFP | CAN_MCR_ABOM; /* TXFP: ящики уходят в порядке записи */
while (!(CAN1->MSR & CAN_MSR_INAK)) {
}
CAN1->BTR = ((TS2 - 1u) << 20) | ((TS1 - 1u) << 16) | (BRP - 1u);
CAN1->FMR |= CAN_FMR_FINIT;
CAN1->FA1R &= ~1u;
CAN1->FS1R |= 1u; /* один 32-битный фильтр */
CAN1->FM1R &= ~1u; /* режим маски */
CAN1->sFilterRegister[0].FR1 = ID_RESP << 21; /* 0x7E8, IDE = 0 */
CAN1->sFilterRegister[0].FR2 = (0x7F8u << 21) | CAN_RI0R_IDE; /* 0x7E8…0x7EF, только 11 бит */
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;
while (CAN1->MSR & CAN_MSR_INAK) {
}
}
Главный цикл. Кадры из кольца — в библиотеку, isotp_poll() на каждом
проходе, готовое сообщение — в answer, раз в секунду — запрос VIN:
int main(void)
{
static const uint8_t vin_req[] = {0x09, 0x02};
static uint8_t answer[4095];
uint32_t n, next = 0;
clock_72mhz();
SysTick_Config(72000u); /* 1 мс */
can_init();
isotp_init_link(&link, ID_REQ, t_tx, sizeof t_tx, t_rx, sizeof t_rx);
for (;;) {
while (tail != head) { /* кадры из прерывания — в библиотеку */
rx_frame *f = &ring[tail % 16u];
if (f->id == ID_RESP) {
isotp_on_can_message(&link, f->d, f->len);
}
tail++;
}
isotp_poll(&link); /* тайм-ауты и следующие CF */
if (isotp_receive(&link, answer, sizeof answer, &n) == ISOTP_RET_OK) {
/* answer[0..n-1]: 49 02 01 и 17 знаков VIN */
}
if ((int32_t)(ms - next) >= 0) { /* раз в секунду — запрос VIN всем блокам */
next = ms + 1000u;
(void)isotp_send_with_id(&link, ID_FUNC, vin_req, sizeof vin_req);
}
}
}
Старт и таблица векторов. Без стартового файла из пакета: копия
.data, обнуление .bss, вызов main(). В таблице — стек, сброс, SysTick
(номер 15) и прерывание приёма CAN (16 + 20):
extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
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,
[15] = SysTick_Handler, [16 + 20] = USB_LP_CAN1_RX0_IRQHandler,
};
Скрипт компоновщика f103.ld — 64 КБ флеш-памяти, 20 КБ ОЗУ и 2 КБ под
стек, которые проверяет ASSERT:
/* Раздел 19в: STM32F103C8 — 64 КБ флеш-памяти, 20 КБ ОЗУ. */
ENTRY(Reset_Handler)
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
}
_estack = ORIGIN(RAM) + LENGTH(RAM);
_stack_size = 0x800;
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > FLASH
.text :
{
*(.text .text.*)
*(.rodata .rodata.*)
. = ALIGN(4);
} > FLASH
.data :
{
_sdata = .;
*(.data .data.*)
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
_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")
}
19в.8 Грабли
Блок машины молчит на запрос. Кадр ушёл короче 8 байт: isotp-c без
ISO_TP_FRAME_PADDING шлёт SF 02 09 02 длиной 3. Включить дополнение.
Длинный ответ не приходит, приходит только первый кадр. Тестер не прислал FC — фильтр не пропустил 0x7E8, или FC ушёл на 0x7DF. FC идёт на физический адрес блока (0x7E0 для 0x7E8). Без FC передатчик isotp-c бросил сообщение через 1001 мс с ошибкой −2.
CF идут вдвое медленнее STmin. При STmin 1 мс и опросе раз в 1 мс шаг CF в прогоне — 2000 мкс: следующий CF уходит на первом опросе строго позже паузы. Опрашивать чаще: при 0,1 мс шаг стал 1100 мкс.
Большой файл идёт со скоростью опроса. Один CF на вызов
isotp_poll(): 4095 байт при опросе раз в 1 мс — 586 мс, при 0,1 мс —
130,9 мс. Звать опрос в цикле или по освобождению ящика передачи.
Неверный номер на приёме при трёх ящиках. Без TXFP bxCAN может отправить CF не в том порядке. Включить TXFP.
Просили паузу 128 мс — получили 0. isotp-c кодирует всё, что больше 127 000 мкс, как 0x00. И 950 мкс — тоже как 0x00: между 900 мкс и 1 мс кода нет.
Тайм-аут 100 мс вместо 1000. В isotp-c по умолчанию 100 мс; блок,
которому для ответа нужно больше, покажется молчащим. Задать
ISO_TP_DEFAULT_RESPONSE_TIMEOUT_US.
Вызовы из прерывания и из цикла. isotp_on_can_message() в прерывании
и isotp_poll() в цикле работают с одной структурой канала без защиты.
Кадры — в кольцо, библиотеку — из одного места.
19в.9 Итоги
- ISO-TP режет сообщение на кадры по 8 байт: SF — до 7 байт, FF — длина и 6 байт, CF — номер и 7 байт, FC — ответ приёмника.
- Длина в FF — 12 бит, до 4095; длиннее — 0 и 32 бита следом
(
10 00 00 00 10 00для 4096). - FC: FS 0/1/2 (можно, жди, переполнение), BS, STmin 0x00…0x7F = 0…127 мс, 0xF1…0xF9 = 100…900 мкс.
- OBD-II: всем — 0x7DF или 0x18DB33F1, двигателю — 0x7E0, ответ — 0x7E8; 29 бит — 0x18DA<кому><от кого>.
- Кадры дополняют до 8 байт; 0xCC или 0xAA дают 5–6 вставленных бит против 13–14 у 0x00 и 0xFF.
- Тайм-ауты стандарта — 1000 мс; в isotp-c по умолчанию 100 мс.
- При BS 8 и STmin 1 мс: 100 байт — 17 кадров за 28 мс, 4095 байт — 660 кадров за 1,17 с при опросе раз в 1 мс.
- isotp-c на F103 — 2063 байта кода; тестер целиком — 3288 байт флеш-памяти.
Дальше: раздел 19г «OBD-II: диагностика автомобиля по CAN — режимы, PID и коды ошибок» — что спрашивать у машины поверх этого транспорта.