«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 от DevCoons - решать Вам.
Словарь
Акроним | Расшифровка |
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? Чтобы банально проверить, есть ли физическая связь с узлом конкретного ISO-TP адреса?
Известны ли Вам другие более надежные open-source реализации протокола ISO-TP, написанные на языке программирования Си?
Существует ли аппаратная реализация протокола ISO-TP на Verilog HDL?
