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

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 нет

Слева — четыре вида кадров ISO-TP побайтно: одиночный с тетрадой 0 и длиной L и семью байтами данных, первый с тетрадой 1 и 12-битной длиной и шестью байтами данных, следующий с тетрадой 2 и номером SN и семью байтами, кадр управления с тетрадой 3 и состоянием FS, байтами BS и STmin и пятью байтами дополнения. Справа — обмен из прогона: тестер шлёт на 0x7DF одиночный запрос 02 09 02, двигатель отвечает с 0x7E8 первым кадром 10 14 и данными 49 02 01 57 50 30, тестер с 0x7E0 шлёт кадр управления 30 08 01, двигатель — два следующих кадра 21 и 22 на 2,223 и 4,223 мс. Внизу — итоги прогона: 100 байт за 28 мс, 4095 байт за 1,17 с, ошибки при отсутствии кадра управления и при пропаже следующих кадров

Обмен «запрос 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() — сообщения, можно пустую. Сама программа вызывает:

Ниже — тестер на 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 Итоги

Дальше: раздел 19г «OBD-II: диагностика автомобиля по CAN — режимы, PID и коды ошибок» — что спрашивать у машины поверх этого транспорта.

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