«ISO‑TP — младший брат TCP»
Пролог
В программировании МК часто надо работать с CAN. CAN может передавать только по 8 байт, однако любая задача требует передавать массивы и большего размера, например 512 байт. Чтобы распетлять это ограничение люди придумали протокол ISO-TP. Задача ISO-TP гарантированно передать массивы байт размером от 1 до 4095 байт. Буквально одной строчкой в коде вызовом функции.
Поэтому поверх интерфейса CAN обычно работает протокол ISO-TP. Поверх ISO-TP обычно UDS. Реализацию протокола ISO-TP можно взять с из github. В частности из репозитория разработчики-еноты. Для определенности назовем этот код Енотовский драйвер. Попробует разобраться работает ли реализация протокола ISO-TP от енотов.
Про то, что такое ISO-TP можно почитать тут. Если коротко, то ISO-TP - это протокол передачи больших массивов до 4k байт порциями по 8 байт (как раз для CAN classic сетей).
Постановка задачи
Проверить реализацию протокола из репозитория Разработчики-Еноты. В самом ли деле там опубликован работающий программный компонент ISO-TP протокола?
Что надо из оборудования?
Как водится, электронная плата в производстве, поэтому отлаживать прошивки придется на LapTop PC. Чтобы оснастить PC CAN-ом придется примонтировать какой-н переходник с USB на CAN.
В качестве переходника с USB на CAN можно использовать широко распространённый и дешевый CAN-трансивер USB2CANFD_V1 с Aliexpress.

Чтобы соединить несколько PC я изготовил CAN-harness, буквально из Audio-Jack-ов согласно этой методичке.

Определения
up-time — время с момента подачи питания на программу. Обычно исчисляется в миллисекундах. Или это время с момента пуска программы (или прошивки).
payload — полезные данные, которые надо отправлять.
супер цикл — тот код, который крутится внутри while(1){... } функции main().
Массив (array) — это упорядоченный набор однотипных элементов, объединенных под одним именем и доступных по индексу. Массив занимает непрерывный интервал памяти. В нашем случае — массив байт.
FIFO (queue) — абстрактный тип данных с дисциплиной доступа к элементам «первый пришёл — первый вышел» (FIFO, англ. first in, first out). Добавление элемента (принято обозначать словом enqueue — поставить в очередь) возможно лишь в конец очереди, выборка — только из начала очереди (что принято называть словом dequeue — убрать из очереди), при этом выбранный элемент из очереди удаляется.
Реализация
Весь программный компонент енотовского драйвера состоит всего-навсего из 4х функций.
n_rslt iso15765_init(iso15765_t* instance);
n_rslt iso15765_send(iso15765_t* instance, n_req_t* frame);
n_rslt iso15765_enqueue(iso15765_t* instance, canbus_frame_t* frame);
n_rslt iso15765_process(iso15765_t* instance);
Всё очень просто. Перед использованием надо вызывать функцию iso15765_init. Функцию iso15765_process надо периодически вызывать в супер цикле, желательно с высокой частотой. При приеме CAN пакета надо вызывать iso15765_enqueue. При отправке пакета надо вызывать iso15765_send.
В инициализационную структура надо добавить callback функции для получения миллисекундного up-time времени, функцию для отправки пакета, функцию для индикации об ошибке и функцию об индикации успешного приема данных. Еще надо указать количество блоков без подтверждения, время между блоками и тип адресации. Также можно увеличить размер максимально возможного I15765_MSG_SIZE пакета вплоть до 4094 байт.
После этого можно пользоваться программным компонентом ISO15765-2 library.
Зависимости
Драйвер ISO-TP от енотов требует их же енотовскую реализацию универсальной очереди, которая у них называется токеном iqueue.
i_status iqueue_init(iqueue_t* _queue, uint32_t _max_elements,
size_t _element_size, void* _storage);
i_status iqueue_enqueue(iqueue_t* _queue, void* _element);
i_status iqueue_dequeue(iqueue_t* _queue, void* _element);
i_status iqueue_size(iqueue_t* _queue, size_t* _size);
i_status iqueue_advance_next(iqueue_t* _queue);
void* iqueue_get_next_enqueue(iqueue_t* _queue);
void* iqueue_dequeue_fast(iqueue_t* _queue);
Внутри реализации ISO-TP в очередь iqueue складируются приходящие с улицы CAN-пакеты. Максимальное количество элементов в очереди принятых пакетов определяется константой I15765_QUEUE_ELMS.

Прием
Принимаемые CAN-пакеты складируются в очередь, которая называется iqueue. Полный данные собираются как мозаика. При полном приеме передаваемого массива данных вызывается callback функция на которую указывает указатель indn.

После выхода из функции indn() принятые данные стираются из внутренней структуры. Поэтому обрабатывать результат надо внутри indn().
Отправка
Отправка пакетов происходит функцией iso15765_send.
n_rslt iso15765_send(iso15765_t* instance, n_req_t* frame);
Так как поле длинны данных составляет 12 бит, то чисто математически за одну ISO-TP сессию мы можем передать максимум 4096 байт. Однако по факту передается и принимается только 4094 байт.
Отладка
Вы наверное удивитесь, но чтобы протестировать протокол ISO-TP даже не нужна CAN-шина, USB-CAN переходники, кабели, провода, Wago клеммники, разъёмы и прочее. Сейчас объясню почему... Вы можете просто взять и определить в своей консольной программе (или прошивке) три независимых экземпляра протокола ISO-TP: ISO-TP1, ISO-TP2, ISO-TP3. Затем написать модульный тест, который передает массив от ISO-TP2 к ISO-TP3. Сконфигурировать callback2 функцию send на отправку не в CAN а сразу на приемную очередь соседнего ISO_TP3. Далее проверять, что переданный массив в 2 совпал с принятым в 3. Если CRC32 совпали, то тест можно считать успешным. Если получились разные CRC32, значит в механизме есть осечка. Вот так просто и не затейливо.

Убедившись, что алгоритмическая логика протокола ISO-TP работает можно выйти на уровень CAN шины и уже пробовать передавать данные через настоящие провода.
Чтобы проверить и отладить работу протокола ISO-TP я написал консольную утилиту CANcat. По аналогии с NetCat, только для CAN.

Я запустил CANcat в двух экземплярах Win процесса и передал массив от одного процесса в другой прямо про проводам CAN-шины. Можно заметить, что массив 0x3344556656565656565656 в самом деле достиг адресата. По протоколу ISO-TP.

Вот пример передачи массива 0x11223344556677889900aabbccddeeff от устройства 0xd к устройству 0xc.

Single frame тоже в обе стороны доставляется корректно.

Со стороны консольных утилит без отладочных логов передача данных выглядит так.

Модульные тесты можно вызывать прямо из CLI, реализованного поверх stdio внутри тестировочного консольного приложения.
3.025-->
3.149-->ta iso
8.390,+5520,196 I,[TEST] unit_test_find_key(),key1:iso,key2:
+-----+--------------------------+-----+
| No | name |index|
+-----+--------------------------+-----+
| 132 | iso_tp_types | 132 |
| 133 | iso_tp_sep_time | 133 |
| 134 | iso_tp_diag | 134 |
| 135 | iso_tp_2_3_send_sngl_frm | 135 |
| 136 | iso_tp_2_3_send | 136 |
| 137 | iso_tp_2_3_send_24 | 137 |
| 138 | iso_tp_2_3_send_jumbo | 138 |
| 139 | iso_tp_2_3_send_4094 | 139 |
+-----+--------------------------+-----+
8.461-->
14.699-->
14.827-->tr 137
cmd_unit_test_run() argc 1
196.212,+187822,197 I,[TEST] key [137]
************* Run test iso_tp_2_3_send_24 .137/187
196.226,+14,198 I,[IsoTp] test_iso_tp_send_x_y():3->2,TxSize:24 Byte,TxCrc32:0x8295A696
196.344,+118,199 I,[IsoTp] ISO_TP_3:Send,Addr:0x0b,Size:24 Byte
196.381,+37,200 I,[IsoTp] ISO_TP_2,RxFirstFrame MesgSize:24 Byte,AI:[Prio:0x0,Source:0xc,Target:0xb,Ext:0x0,Func:0x0,Type:0x1,],ProtoCtrlInfo:[PDU:FirstFrame,Fs:0,BS:0,SN:0,SepTime:0.000000 s,DatLen:24 Byte,],FrameFormat:Classic,
196.451,+70,201 I,[IsoTp] ISO_TP_2,ReceptionAvailable:RxSize:24,AI:[Prio:0x0,Source:0xc,Target:0xb,Ext:0x0,Func:0x0,Type:0x1,],ProtoCtrlInfo:[PDU:ConsecutiveFrame,Fs:0,BS:0,SN:1,SepTime:0.000000 s,DatLen:24 Byte,],FrameFormat:Classic,Payload:000102030405060708090A0B0C0D0E0F1011121314151617,Rslt:OK
196.479,+28,202 I,[IsoTp] RxCRC32,0x8295A696
196.482,+3,203 I,[IsoTp] CopyRxData,Max:4095
196.486,+4,204 W,[IsoTp] WaitRxEnd!...
196.502,+16,205 W,[IsoTp] RxDone!
196.507,+5,206 I,[IsoTp] RxEnd!
(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23)
196.515,+8,207 I,[IsoTp] DataMatch! Size:24 Byte
196.519,+4,208 I,[IsoTp] test_iso_tp_send_x_y():2->3,TxSize:24 Byte,TxCrc32:0x8295A696
196.636,+117,209 I,[IsoTp] ISO_TP_2:Send,Addr:0x0c,Size:24 Byte
196.658,+22,210 I,[IsoTp] ISO_TP_3,RxFirstFrame MesgSize:24 Byte,AI:[Prio:0x0,Source:0xb,Target:0xc,Ext:0x0,Func:0x0,Type:0x1,],ProtoCtrlInfo:[PDU:FirstFrame,Fs:0,BS:3,SN:0,SepTime:0.010000 s,DatLen:24 Byte,],FrameFormat:Classic,
196.728,+70,211 I,[IsoTp] ISO_TP_3,ReceptionAvailable:RxSize:24,AI:[Prio:0x0,Source:0xb,Target:0xc,Ext:0x0,Func:0x0,Type:0x1,],ProtoCtrlInfo:[PDU:ConsecutiveFrame,Fs:0,BS:3,SN:1,SepTime:0.010000 s,DatLen:24 Byte,],FrameFormat:Classic,Payload:000102030405060708090A0B0C0D0E0F1011121314151617,Rslt:OK
196.749,+21,212 I,[IsoTp] RxCRC32,0x8295A696
196.750,+1,213 I,[IsoTp] CopyRxData,Max:4095
196.752,+2,214 W,[IsoTp] WaitRxEnd!...
196.767,+15,215 W,[IsoTp] RxDone!
196.769,+2,216 I,[IsoTp] RxEnd!
(0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23)
196.773,+4,217 I,[IsoTp] DataMatch! Size:24 Byte
!OKTEST
197.084,+311,218 I,[TEST] TestDuration:863 ms=0.863 s=0.014383333 min
197.093,+9,219 I,[TEST] All 1 tests passed!
3:17-->
В целом программный компонент ISO-TP работает иcправно.
Достоинство данной реализации ISO-TP
Написанный Енотами драйвер ISO-TP позволяет создавать несколько экземпляров протокола ISO-TP. Это позволяет легко масштабировать драйвер и каждому экземпляру ISO-TP задавать отдельный CAN интерфейс или даже вовсе производить обмен по UART. Еноты в этом плане очень порадовали.
Написана на Cи. Это делает возможным использование кода в микроконтроллерных прошивках.
Лаконичность. Ядро протокола ISO-TP всего 1000 строк (вместе с очевидными комментариями).
Экземпляр ISO-TP может быть как передатчиком, так и приемником массивов. Нет нужды пересобирать программный компонент с другим конфигом, подобно тому как это можно встретить в других реализациях протокола ISO-TP.
Недостатки реализации в lib_iso15765
Сорцы lib_iso15765.h не собираются с GCC ключом компилятора -Werror=strict-prototypes
Экземпляры драйвера ISO-TP от енотов не могут одновременно передавать маccив и принимать массив. По очереди - да, одновременно - нет. Получается так, что прием массива просто перебивается отправкой массива. Как будто у отправки выше приоритет. Как только вызываешь функцию iso15765_send, так сразу драйвер забывает, что что-то принимал и полностью переключается на отправку массива.
Приемник ISO-TP никак не обрабатывает потерю соединения. Если во время отправки большого массива master отключается от электропитания и не будет посылать consecutive frame, то приемник все равно будет ждать consecutive frame до бесконечности.
-
Если в режиме CAN classic отправить single frame с чрезмерным количеством байт в 4x битном поле Data Len. Например вписать число от 8 ...... до 15, то приемник на той стороне в самом деле будет считать, что пришло 15 байт. Хотя на самом деле в single frame пакете ну никак не могло быть больше, чем 7 байт. Вот такие пирожки с капустой.

Данная реализация ISO-TP не фильтрует по ID. Если пришел пакет c ID для другого устройства, то данный код будет на него отвечать в любом случае. Если на CAN шине больше трёх устройств, то возникнет коллизия. Поэтому фильтрацию по ISO-TP адресам надо делать уже каким-то своим кодом-фантиком поверх данной скачанной реализации.
Данная реализация не может передать массивы ровно 4095 байт. По факту принимаются нули. При этом нормально принимается массив 4094 байт. Судя по исходникам 4095 — это максимальное значение для константы I15765_MSG_SIZE, однако оно по факту не работает.
В open-source коде нет никакой диагностики. Для printf-отладки нужны функции сериализаторы для каждого типа и каждой константы.
const char* IsoTpPduTypeToStr(const pci_type pdu_type) {
const char* name = "?";
switch(pdu_type) {
case N_PCI_T_SF : name = "Single"; break;
case N_PCI_T_FF : name = "First"; break;
case N_PCI_T_CF : name = "Consecutive"; break;
case N_PCI_T_FC : name = "FlowControl"; break;
default: name = "?"; break;
}
return name;
}
const char* IsoTpAddrInfoToStr(const n_ai_t* const Addr){
if(Addr) {
strcpy(lText, "");
snprintf(lText, sizeof(lText), "%sPrio:%u,", lText, Addr->n_pr); /* Network Address Priority */
snprintf(lText, sizeof(lText), "%sSource:%u,", lText, Addr->n_sa); /*Network Source Address*/
snprintf(lText, sizeof(lText), "%sTarget:%u,", lText, Addr->n_ta); /* Network Target Address */
snprintf(lText, sizeof(lText), "%sExt:%u,", lText, Addr->n_ae); /* Network Address Extension */
snprintf(lText, sizeof(lText), "%sFunc:%u,", lText, Addr->n_fa); /* Network Functional Address */
snprintf(lText, sizeof(lText), "%sType:%u,", lText, Addr->n_tt); /*Network Target Address type*/
}
return lText;
}
Чтобы можно было банально отлаживаться. Всю диагностику по факту пришлось писать самостоятельно. С нуля.
8. В коде от Енотов нет команд оболочки API для CLI. Это нужно для управления программным компонентов через SHELL.
#ifndef ISO_TP_COMMAND_H
#define ISO_TP_COMMAND_H
#ifdef __cplusplus
extern "C" {
#endif
#include "std_includes.h"
bool iso_tp_diag_command(int32_t argc, char* argv[]);
bool iso_tp_send_command(int32_t argc, char* argv[]);
bool iso_tp_compose_address_command(int32_t argc, char* argv[]);
#define ISO_TP_COMMANDS \
SHELL_CMD("iso_tp_diag", "tpd", iso_tp_diag_command, "IsoTpDiag"), \
SHELL_CMD("iso_tp_compose_addr", "tpca", iso_tp_compose_address_command, "IsoTpComposeAddress"), \
SHELL_CMD("iso_tp_send", "iso_tp", iso_tp_send_command, "IsoTpSend"),
#ifdef __cplusplus
}
#endif
#endif /* ISO_TP_COMMAND_H */
Пришлось также отдельно писать поддержку драйвера ISO-TP в интерфейсе командной строки. Чтобы запускать тесты и не собирать по 10...50 версий одной и той же утилиты с незначительными изменениями.
9. Нет тестов. Пришлось самостоятельно придумывать сценарии как для модульных так и для интеграционных тестов этого кода и накидывать их.
10. Реализация енотов использует оператор goto

вопреки запретам автомобильного стандарта MISRA C 2012. Оператор goto делает код не структурируемым и трудным к пониманию коллегами.

Также в коде енотов можно зарегистрировать множественные точки выхода из функций. Что тоже осуждается всяческими индустриальными стандартами программирования.

11. В енотовской реализации API универсальной очереди (iqueue) отсутствует классическая FIFOшная функция peek, чтобы просто посмотреть на первый элемент в очереди не извлекая его, подобно тому как это можно встретить во всех других реализациях очередей. Это осложняет тестирование и отладку.
12. Нет защиты от отправки ISO-TP массива нулевой длинны. Драйвер от Енотов преспокойно отправляет и принимает массивы нулевого размера. Нормально так... Да?
Итоги
Реализация протокола ISO-TP от разработчиков-енотов в самом деле как-то дышит, однако надо дорабатывать проверки, функционал, защиту от дугака, фильтрацию, диагностику, тесты и оболочку. Стараться не передавать массивы больше 4094 байта и нулевые массивы. Чисто теоретически, нынешнюю реализацию можно использовать для тривиальных use-case-ов и в тепличных условиях.
Выявлены многочисленные отступления от рекомендаций автомобильного программирования MISRA С, что, по меньшей мере, странно для автомобильного протокола ISO-TP.
Зато в работе IQUEUE мой набор модульных тестов проблем не обнаружил.
Утилита CANcat показала себя отличной лабораторией для тестирования и отладки CAN протоколов.
Использовать или нет эту реализацию ISO-TP - решать Вам.
Словарь
Акроним |
Расшифровка |
API |
application programming interface |
ISO |
International Organization for Standardization |
ISO-TP |
Транспортный протокол для адресной передачи массивов между устройствами на CAN-шине. |
TP |
Transport Protocol |
MTU |
Maximum transmission unit |
SF |
Single Frame |
UDS |
Unified Diagnostic Services |
MISRA |
Motor Industry Software Reliability Association |
PCI |
Protocol Control Information |
FC |
Flow Control |
CF |
Consecutive frame |
PDU |
Protocol Data Unit |
CAN |
Controller Area Network (ISO 11898) |
ISO15765-2 |
ISO-TP protocol |
API |
application programming interface |
CLI |
Command Line Interface |
AGPL |
AFFERO GENERAL PUBLIC LICENSE |
Ссылки
Название |
URL |
https://docs.google.com/spreadsheets/d/1yHserq9AY0wNc5kbwriT_orr5LVbfv4ktDXRSDwYiR8/edit?gid=0#gid=0 |
|
https://github.com/devcoons/iso15765-canbus/blob/master/doc/ISO-15765-2-2016.pdf |
|
Как собрать Си программу в OS Windows |
|
Вопросы
Какого максимального размера массив можно передать по протоколу ISO-TP за одну сессию? Согласно тому, что в пакета для размера выделено 12 бит можно передать 4096 байт.
Существует ли в CAN-сетях аналог утилиты ping? Чтобы банально проверить, есть ли физическая связь с узлом конкретного адреса?
Известны ли Вам другие более надежные open-source реализации протокола ISO-TP, написанные на языке программирования Си?