« Любая разработка начинается только с появления отладочных средств .
Подобно тому, как альпинизм начинается с верёвок.»

В разработке электроники часто делают электронную плату и не добавляют даже один отладочный UART. При этом на плате заложено от двух до восьми CAN портов + еще вся остальная возможная периферия, вот только про UART никто не вспомнил. При этом сама PCB размером с авианосец. Как же отлаживать софт и железо в таких случаях? О том, что такое UART и зачем он нужен у меня есть отдельный текст. Надо как-то порешать эту проблему отладки без UART-а. Вот про это и будет текст.
Постановка задачи
Реализовать возможность передавать SHELL команды (CLI) по CAN шине.
"Когда есть CLI, то далее plug and play.
Когда нет CLI, то далее plug and pray"
Настроить ISO-TP на микроконтроллере и на PC. Написать на Си консольную Win утилиту CANshell , которая позволит посылать CLI команды на микроконтроллер и получать ответ от микроконтроллера. В качестве переходника с USB на CAN использовать преобразователь USB2CANFD_V1, как самый доступный, дешевый (600 RUR) и простой вариант USB-CAN переходника.
Позволить конфигурировать утилиту аргументами функции main. То есть передавать параметры работы из *.bat скрипта. Чтобы не терять время на ручное конфигурирование в консоли. В аргументах PC утилиты CANshell при запуске указывать ISO-TP адрес устройства на CAN шине к которому мы собираемся подключаться, задавать наш собственный ISOtp адрес, задавать номер COM порта на, котором примонтирован преобразователь USB-CAN USB2CANFD_V1, битовую скорость CAN шины, номер порта сокета.
# аргумента | Назначение аргумента |
1 | COM порт на котором подключен USB2CANFD_V1 |
2 | Битовая скорость CAN шины |
3 | Номер TCP порта через который будет проходить трафик shell-а |
4 | ISO-TP адрес приложения в сети (отправителя shell пакета). |
5 | ISO-TP адрес устройства на шине, с которым мы хотим общаться по CLI |
Утилита CANshell должна создавать TCP сокет, который будет посредником между TCP клиентом (TeraTerm) и CAN шиной.
Схема подключения

В чем сложность?
1) CAN пакеты очень маленькие.
В один CAN пакет помещается только 8 байт. При этом команды shell могут быть длиной вплоть до 150 байт и более. Ответ на CLI команды может быть и вовсе любой длины от 1 байта до бесконечности. Опять таки, надо как-то передавать массивы по частям ввиду того, что в один CAN-classic пакет помещаются только массивы размером до 8 байт. Классическое решение - забабахать протокол ISO-TP.
" CAN без ISO-TP - деньги на ветер. "
" Все протоколы — это функция memcpy между устройствами. "
2) Разделение текстовых потоков логирования
Вторая сложность в том, что утилита canShell должна как-то разделять два потока shell данных на одном мониторе. Свой shell и shell c удалённого ECU устройства, к которому мы подключаемся по CAN через ISO-TP.
Ключевая идея решения в том, что утилита CANshell запускает TCP сервер, открывает TCP сокет, который с одной стороны ассоциирован с протоколом ISO_TP, а с другой с TCP клиентом PuTTY. Номер сокета можно даже как-то генерировать из ISO-TP адреса отправителя и адреса назначения. Далее открываем Putty, подключаемся к сокету на CANshell и гоняем Shell трафик, как будто мы подключились по UART. Утилита CANshell сама разруливает аккуратное перекладывание байт из ISO-TP в сокет, и наоборот, из сокета в ISO-TP.

Таким образом у вас на LapTop-е будет два shell-а:
1--> собственный shell от утилиты CANshell
2--> shell какого-то удаленного устройства где-то в CAN сети.
Ничего нового. На самом деле именно так и работают почти все отладочные GDB cерверы (например OpenOCD). Те самые, что работают в паре с GDB клиентов при пошаговой отладке кода. Две утилиты работают в тандеме одновременно для решения одной задачи. В нашем случае задача - это организация CLI-шки по CAN-у.
3) Перенаправление лога
Как заставить ECU перенаправить лог в ISO-TP? По умолчанию устройство пишет логи в какой-то UART (или в пин SWO). Очень просто. Надо самой первой командой отправить команду tpw 1. Команда iso_tp_writer сконфигурирует ECU отправлять логи только в первый экземпляр протокола ISO-TP. То есть все функции LOG_ERROR(), LOG_INFO(), LOG_DEBUG() будут писать в ISO-TP_1. Далее утилита can_shell примет массивы и просто перекопирует их через свой TCP-сервер в сторону TCP-клиента. Проблема решилась сама собой.
Реализация
В качестве переходника USB-CAN я выбрал изделие USB2CANFD_V1. Прибор USB2CANFD_V1 хорош тем, что он дешевый и для него легко писать PC приложения. Управление происходит по последовательному COM порту и тривиальному протоколу, который называется SLCAN.

Я собрал отладочную CAN сеть на основе сегментов из проводов и штекеров audio-jack.

В качестве целевой платформы я использовал учебно-тренировочную электронную плату JZ-F407VET6. На ней как раз два CAN порта.

TCP сервер я буду писать на чистом Си под Windows используя функции из программного компонента WinSock2. Написание на Си дает преимущество, что я могу пере использовать программный компонент протокола ISO-TP и прочие программные компоненты (FIFO), как в коде прошивки, так и в коде LapTop приложения.
Когда TCP сервер получает текст, он сразу посылает его в ISO-TP. На стороне микроконтроллера выстреливает callback о приеме массива. Всё его содержимое передается прямиком на вход парсера shell команд cli_process_data. Вот так просто и не затейливо команды попадают от PC в прошивку.
void iso_tp1_rx_done(n_indn_t* in_done){ LOG_DEBUG(ISO_TP,"ISO_TP1,%s",Iso15765_n_indn_ToStr(in_done)); if(N_OK==in_done->rslt){ #ifdef HAS_CLI IsoTpHandle_t* Node=IsoTpGetNode(1); if(Node) { Node->target_id = in_done->n_ai.n_sa; cli_process_data(Node->cli_num, in_done->msg, in_done->msg_sz); memset(in_done->msg,0,I15765_MSG_SIZE); } #endif } }
С ответом иначе. Трудность в том, что логи поступают в полностью асинхронном порядке. Лог может возникнуть даже без изначальной shell команды. То есть самопроизвольно. Чтобы перенаправить лог в ISOTP_1 оператор должен отдельной командой ( tpw 1 ) переключить логирование в ISO-TP1. В этом случае все логи будут писаться в очередь TxFIFO, которая ассоциирована с первым экземпляром ISOtp.
void iso_tp1_puts(void* stream_ptr, const char* str, int32_t len) { IsoTpHandle_t* Node = IsoTpGetNode(1); if(Node) { if(str) { if(len) { bool res = fifo_push_array(&Node->TxFifo, (uint8_t*)str, (uint32_t)len); if(!res) { Node->error_cnt++; } } } } } void iso_tp1_putc(void* stream_ptr, char ch) { IsoTpHandle_t* Node = IsoTpGetNode(1); if(Node) { bool res = fifo_push(&Node->TxFifo, (uint8_t)ch); if(!res) { Node->error_cnt++; } } }
Далее отдельная задача IsoTpTx по мере освобождения автомата ISOTP и появления свободного времени у процессора будет по частям извлекать из очереди порции данных и отправлять их по ISO-TP в сторону консольного PC приложения CANshell.
static bool iso15765_tx_next(IsoTpHandle_t* const Node) { bool res = false; res = iso15765_is_idle( &Node->instance); if( res) { uint32_t count = fifo_get_count(&Node->TxFifo); if(0 < count) { uint32_t txLen = 0; n_req_t isoTpFrame={0}; isoTpFrame.fr_fmt=CBUS_FR_FRM_STD; res = iso_tp_node_to_address_info( Node, &isoTpFrame.n_ai); res = iso_tp_node_to_proto_ctrl_info( Node, &isoTpFrame.n_pci); res = fifo_pull_array(&Node->TxFifo, isoTpFrame.msg, sizeof(isoTpFrame.msg), &txLen); if(res) { isoTpFrame.msg_sz = txLen; n_rslt ret = iso15765_send(&Node->instance, &isoTpFrame); res = iso15765_ret_to_res(ret); } } } return res; } bool iso_tp_tx_proc_one(uint8_t num) { bool res = false; IsoTpHandle_t* Node = IsoTpGetNode(num); if(Node) { LOG_PARN(ISO_TP, "IsoTp%u,ProcTx", num); res = iso15765_tx_next(Node); Node->spin++; } return res; }
На стороне консольного PC приложения CANshell принятые по ISO-TP данные перекладываются в очередь для сокета и отправляются TCP клиенту.
bool can_shell_iso_tp_rx_data(uint8_t num, uint8_t iso_num, const uint8_t* const data, uint16_t msg_sz) { bool res = false; if(data) { if(msg_sz) { SocketHandle_t * Socket = SocketGetNode(1); if(Socket) { res = fifo_push_array(&Socket->TxFifo, data, (uint32_t) msg_sz); log_debug_res(CAN_SHELL, res, "SocketTxFiFoPush"); } } } return res; } static bool iso_tp_rx_done(uint8_t iso_num, n_indn_t* in_done) { bool res = false; if(in_done) { LOG_INFO(ISO_TP, "ISO_TP_%u,MoveDone:%s", iso_num, Iso15765_n_indn_ToStr(in_done)); res = iso15765_ret_to_res(in_done->rslt); if(res) { IsoTpHandle_t *Node = IsoTpGetNode(iso_num); if(Node) { Node->target_id = in_done->n_ai.n_sa; Node->rx_done = true; #ifdef HAS_CAN_SHELL res = can_shell_iso_tp_rx_data(1, iso_num, in_done->msg, in_done->msg_sz); #endif } }else{ LOG_ERROR(ISO_TP, "MoveErr,Ret,%u=%s", in_done->rslt, Iso15765retToStr(in_done->rslt)); } } return res; }
Далее уже код TCP сервера обработки сокета передает данные на финишную прямую в сторону TCP клиента в PuTTY (или TeraTerm)
static bool socket_server_connected_tx_proc(SocketHandle_t *const Node) { bool res = false; uint32_t tx_len = fifo_get_count(&Node->TxFifo ); if(tx_len) { uint8_t TxPart[150] = {0}; uint32_t tx_size = 0 ; res= fifo_pull_array(&Node->TxFifo , TxPart, sizeof(TxPart), &tx_size); if(res) { int tx_done_cnt = send(Node->socket_remote, (char*) TxPart, (int) tx_size, 0 ); if(tx_done_cnt==tx_size) { res = true; } else { res = false ; int ret = 0; ret = WSAGetLastError(); LOG_ERROR(LG_SOCKET_SERVER, "SendFailedWithErrorCode,ErrCode:%d=%s", ret, WSAErrorToStr(ret)); } } } return res; }
Пока я отлаживал этот механизм я заметил, что утилита can_shell иногда захлебывается от набегающего потока траффика в ISO-TP. Происходят осечки в автомате приема. Выглядит это так. На стороне микроконтроллера ISO-TP трансивер выходит в состояние IDLE раньше, чем ISO-TP трансивер на стороне консольного приложения. И поэтому часть данных падает на пол. Мне пришлось разнести ISO-TP сессии во времени, чтобы оба экземпляра успели перезарядиться перед перемещением очередного пучка данных.
Испытание утилиты CANshell
Прежде всего надо понять на каком COM порте у Вас оказался переходник с USB на CAN USB2CANFD_V1. В моем случае это COM3. В сети устройств целевая платформа имеет ISOtp адрес 0xA.

Вот вы запустили утилиту CANshell. Аргументы позиционные, как в инструкциях ассемблера. Переходник c USB-CAN на порту COM3, CAN bitrate=500kBit/s, IsoTpLapTopAdd=0xC, IsoTpPCBAddr=0xA, TCPport=50003.
CANshell.exe can_shell_config 3 500000 0xC 0xA 50003
Сразу после пуска утилиты CANshell надо проверить, что в операционной системе Windows в самом деле заработал TCP сокет. Для этого надо выполнить команду netstat -ano | grep 50003 .

Отлично. Сервер работает. Теперь можно пробовать подключиться к TCP серверу внутри утилиты can_shell. Для этого надо открыть какой-нибудь TCP клиент. Вот хотя бы тот же PuTTY. Прописать локальный IP: ( 127.0.0.1 ). Прописать порт 50003 и нажать Open.

И вот я получил долгожданную виртуальную консоль отдельного устройства в CAN шине! Как будто по UART подключился. Можно отправлять shell команды, принимать ответ на них и глазами анализировать результат работы софта и железа.

Аналогично в качестве TCP клиента можно выбрать культовую утилиту TeraTerm. Выбираем File->New connection. Указываем локальный IP: 127.0.0.1 . Прописываем port: 50003 . Для удобства ставим локальное эхо.

Я подключился к TCP серверу. Сервер это заметил.

И появляется главная консоль управления устройством. Далее вы можете делать с устройством практически всё, что хотите.

Достоинства отладки прошивки по Shell через ISO-TP
++ Главное достоинство в том, что вам больше не нужен UART для использования CLI. На самом деле очень много электронных плат проектируют и производят совсем без UART. Но зато с CAN. К этому приходится приспосабливаться. Подстраивать свои средства диагностики и отладки прошивки к ограничениям, которые накладывает интерфейс CAN.
++ Вы можете отлаживать софт и железо при помощи CLI, в которой можно делать практически всё что угодно.
«При разработке прошивки UART‑CLI нужна, как воздух, как хлеб.»
++ Так как главный портал в устройство работает на LapTop-е, а клиент подключается со стороны, то в одной сети вы можете подключаться к TCP серверу хоть с мобильного телефона по WiFi. Надо лишь знать IP адрес LapTop-а. То есть вы можете отлаживать прошивку и железо по CLI со смартфона или планшетного компьютера.
Недостатки отладки прошивки по Shell через ISO-TP
-- Как вы и сами могли заметить, механизм canShell получился довольно сложным. Появились новые параметры: время между отправками пучков данных, separation time, количество CAN пакетов без подтверждения. Всё это добро надо как-то настраивать и подгонять параметры для каждого конкретного окружения и состояния топологии CAN сети. Нужна тщательная юстировка программы. Но это неизбежная плата за упрощение схемотехники. За счет ликвидации физического UART пришлось городить такой вот громоздкий программный механизм и настраивать его.
-- Если в механизме ISO-TP что-то сломается, то вы окажетесь отрубленными от устройства и отладки. Есть множество того, что может пойти не так. Вы можете отправить команду, а ответ не придет. Может заклинить автомат ISO-TP. Может заклинить сокет, может переполнится очередь приема или FIFO отправки, внутри MCU могут отрубиться настройки прерываний. В этом случае вам придется уже отлаживать весь этот механизм при помощи более простых средств: того же UART, GPIO или даже пошаговой GDB отладки. Но уже на отдельной учебно-тренировочной отладочной плате.
" Более сложный механизм можно отладить только менее сложным механизмом. "
--Для CANshell требуется дополнительный ресурсы прошивки. Это прежде всего RAM память для очередей, Flash память для ISO-TP стека. Это процессорное время на обслуживание протоколов. В случае отладки по UART-CLI требуемых ресурсов микроконтроллера было бы на два порядка меньше.
" Чем проще схемотехника, тем сложнее прошивка.
Чем сложнее схемотехника, тем проще прошивка. "
Итог
Удалость научиться пробрасывать по CAN произвольный текст от PC c ECU и обратно. Теперь можно отлаживать софт и железо ECU в составе собранных агрегатов. При чем все, что требуется это добавить в прошивку ECU перенаправление ISO-TP трафика в парсер CLIшных команд и просто подключить плату к CAN шине. Через аргументы утилиты can_shell вы можете выбирать из десятков узлов сети то конкретное устройство, которое вас интересует здесь и сейчас по его 8-битному ISO-TP адресу. Еще через утилиту CANshell вы можете отлаживать и непосредственно саму реализацию протокола ISO-TP. Своего рода интеграционный тест ISO-TP.
Можно и дальше развивать утилиту CANshell. Добавлять поддержку все новых и новых переходников c USB на CAN: CAN-bus-USB от Marathon, SYS TEC USB-CANmodul1, pcan-pro-x, USBCAN-II C, CAN-Hacker CH-P FDL2, canable v2, CANalyze , candleLight , canfox, CANTact Pro, kvaser , Vector, MKS CANable Pro, UCCBEmbedded , VSCOM USB-CAN Plus ISO и прочие.
Скачать утилиту CANshell вы можете тут.
https://github.com/aabzel/Artifacts/tree/main/can\_shell
Если есть пожелания к улучшению утилиты CANshell, то пишите в комментариях.
Словарь
Акроним | Расшифровка |
CLI | Command Line Interface |
CAN | controller area network |
URL | Uniform Resource Locator |
ISO | International Organization for Standardization |
ISO-TP | ISO- Transport Layer |
ECU | engine control unit |
UART | Universal Asynchronous Receiver/Transmitter |
SLCAN | Serial-Line CAN |
Источники
Вопросы:
--Как извне проверить, что консольное win приложение в самом деле проворачивает основной суперцикл программы и не зависло и не приостановлено? Что надо добавить в код? Ведь у консольных приложений нет Heart Beat LED-а, как в случае программирования микроконтроллера на PCB.
--Существует ли аналог сторожевого таймера (watchdog) для разработки консольных Windows приложений? Как можно автоматически перезагрузить зависшее консольное Windows приложение?
--Существует ли аналог утилиты Segger J-Scope для отладки консольных Windows приложений? Разве что ArtMoney.
--Зачем нужны номера TCP/UDP портов, если и так были и есть PID номера процесса в операционной системе? У некоторых процессов могут быть открыты два и более TCP порта.
--Как в OC Windows10 показать все занятые порты? Как проверить, что конкретный порт (например 50003) в самом деле занят или свободен? netstat -ano | grep 50003
netstat -ano | grep 50003
