19ж. USB-CAN адаптер и SocketCAN: candleLight, can-utils и стенд для автодиагностики
Разделы 19–19е учили STM32 самому сидеть на шине: принимать кадры, спрашивать машину по OBD-II и UDS, расшифровывать чужие кадры по DBC. Но отлаживать всё это удобнее с компьютера: смотреть поток кадров, слать запросы руками, писать журнал поездки. Для этого нужен USB-CAN адаптер — а это почти всегда тот же STM32 с USB и bxCAN на одной плате.
Здесь — как компьютер разговаривает с таким адаптером. Способов два:
текстовые строки SLCAN через виртуальный COM-порт (их мы уже писали сами в
198е) и двоичный протокол gs_usb прошивки candleLight, который ядро Linux
понимает без настройки и превращает в сетевой интерфейс can0. Разберём
пакет gs_usb побайтно, посмотрим, как ip link … bitrate 500000
становится регистром CAN_BTR, и соберём стенд для автодиагностики:
адаптер, имитатор блока из 19г и программы can-utils.
Опоры: исходники candleLight_fw (include/gs_usb.h, include/config.h,
src/usbd_gs_can.c, src/can/bxcan.c, src/can_common.c, README.md),
canable-fw (прошивка SLCAN для STM32F042) и can-utils (lib.c, candump.c,
cansend.c, isotpsend.c, slcand.c, calc-bit-timing) — открытые
репозитории github.com/candle-usb/candleLight_fw, github.com/normaldotcom/canable-fw
и github.com/linux-can/can-utils; строки настройки интерфейса — из документации
Klipper и rusEFI. Пакеты собраны по настоящему заголовку gs_usb.h,
строки разобраны и напечатаны настоящим lib.c — gcc 12.2.0 в контейнере
stm-sys:1; обе прошивки собраны arm-none-eabi-gcc 12.2.1 в stm-arm:1.
Лицензия: candleLight_fw — MIT (Hubert Denkmair), canable-fw — MIT (Eric
Evenchick), can-utils — GPL-2.0-only или BSD-3-Clause на выбор
(can-calc-bit-timing — только GPL-2.0-only); чужой код в разделе —
короткие цитаты с указанием проекта, свой — целиком в 19ж.7.
Как это называется в документации
Оригинал Перевод SocketCAN шина CAN в Linux как сетевой интерфейс: can0, сокеты,ip linkgs_usb двоичный протокол USB-CAN адаптера и драйвер ядра Linux для него host frame, echo_id пакет с кадром между компьютером и адаптером; номер для подтверждения отправки echo frame эхо: адаптер возвращает отправленный кадр, когда отдал его контроллеру CAN bulk endpoint конечная точка USB для массовых передач CDC ACM класс USB «виртуальный COM-порт» bit timing: prop_seg, phase_seg1/2, SJW, BRP битовый тайминг: отрезки бита в квантах, скачок синхронизации, делитель time quantum, sample point квант времени; точка выборки бита vcan виртуальная шина CAN в ядре Linux, без железа DFU обновление прошивки по USB
19ж.1 Два способа: SLCAN и gs_usb
Адаптер — это маленький мост: с одной стороны USB, с другой контроллер CAN и трансивер. Весь вопрос в том, каким языком он говорит с компьютером.
SLCAN — строки ASCII через виртуальный COM-порт (USB CDC). Кадр 0x7DF с
данными 02 01 0C и дополнением до 8 байт выглядит так:
t7DF802010CCCCCCCCCCC и CR — 22 символа. Так работает canable-fw для
STM32F042 и наш сниффер из 198е. В Linux строку превращает в интерфейс
демон slcand из can-utils: slcand -o -c -f -s6 ttyUSB0 — открыть канал
(-o), закрыть при выходе (-c), сбросить флаги ошибок (-f), скорость
S6 — 500 кбит/с.
gs_usb — двоичные пакеты по двум массовым конечным точкам USB. Первым
так работал адаптер USB2CAN фирмы Geschwister Schneider (о нём пишет
комментарий в gs_usb.h), открытая реализация — прошивка candleLight_fw.
Модуль gs_usb есть в ядре Linux: по словам README прошивки, адаптер
«работает из коробки» в дистрибутивах вроде Ubuntu и сразу появляется как
can0.
Чем они отличаются на деле:
| SLCAN (canable-fw) | gs_usb (candleLight_fw) | |
|---|---|---|
| USB | CDC, ad50:60c4 |
свой класс 0xFF, 1d50:606f |
| Кадр 0x7DF, 8 байт | 22 символа | 20 байт к адаптеру, 24 обратно (с меткой) |
| Скорость шины | 9 готовых: S0…S8 |
любой тайминг запросом BITTIMING |
| Подтверждение отправки | нет | эхо с тем же echo_id |
| Метка времени | нет | 32 бита, микросекунды |
| Linux | slcand вручную |
драйвер gs_usb, интерфейс сам |
| Windows | свой .inf (windows-driver/cantact.inf) |
дескрипторы WCID, драйвер не нужен |
| Флеш прошивки, F042 | 28 632 байта кода | 23 256 байт кода |
Про подтверждение README canable-fw пишет прямо: прошивка не даёт ответа ни на одну команду. Отправил строку — и не знаешь, ушёл ли кадр. У gs_usb каждый отправленный кадр возвращается эхом, и драйвер Linux знает, что кадр отдан контроллеру.
19ж.2 gs_usb: дескрипторы и управляющие запросы
Адаптер с candleLight_fw — это устройство USB с идентификатором
1d50:606f (USBD_VID и USBD_PID_FS в config.h, одни на все платы).
Имена у плат разные: «canable gs_usb», «candleLight USB to CAN adapter» и
так далее; несколько одинаковых адаптеров README советует различать по
серийному номеру из lsusb.
Интерфейсов два. Нулевой — сам gs_usb: класс, подкласс и протокол 0xFF
(«свой»), две массовые конечные точки — 0x81 к компьютеру и 0x02 от него,
по 32 байта (64 — в сборке с CAN FD). Первый — DFU (класс 0xFE): через
него прошивку обновляют dfu-util без перемычки BOOT. Плюс дескрипторы
Microsoft WCID с «WINUSB» — Windows сама ставит WinUSB.
Всё, кроме кадров, идёт управляющими запросами к интерфейсу. Номера — из
enum gs_usb_breq:
| Запрос | Номер | Что передаёт |
|---|---|---|
| HOST_FORMAT | 0 | порядок байтов (candleLight его не поддерживает — всегда little-endian) |
| BITTIMING | 1 | тайминг: prop_seg, phase_seg1, phase_seg2, sjw, brp — 20 байт |
| MODE | 2 | START или RESET канала и флаги режима — 8 байт |
| BT_CONST | 4 | что умеет контроллер: флаги, частота, пределы тайминга — 40 байт |
| DEVICE_CONFIG | 5 | число каналов и версии — 12 байт |
Для bxCAN прошивка отвечает на BT_CONST так (src/can/bxcan.c): частота —
CAN_CLOCK_SPEED из config.h (у плат на F042 и F072 — 48 МГц, у F407 —
42 МГц), tseg1 1…16, tseg2 1…8, sjw до 4, brp 1…1024. Флаги:
только слушать, петля, одна попытка без повтора, аппаратная метка
времени, мигание для опознания, дополнение пакетов до 32 байт, отчёт об
ошибках шины, чтение состояния, выход из bus-off; у части плат ещё
терминатор и аппаратный фильтр.
19ж.3 Пакет gs_host_frame побайтно
Кадр в обе стороны — одна структура из gs_usb.h (candleLight_fw, MIT):
struct gs_host_frame {
u32 echo_id;
u32 can_id;
u8 can_dlc;
u8 channel;
u8 flags;
u8 reserved;
Дальше — объединение: 8 байт данных, 8 байт и метка timestamp_us, либо 64
байта для CAN FD. Мы собрали эту структуру по самому заголовку в
контейнере и напечатали sizeof и offsetof:
| Смещение | Байт | Поле | Что в нём |
|---|---|---|---|
| 0 | 4 | echo_id |
номер кадра от компьютера; 0xFFFFFFFF — принят с шины |
| 4 | 4 | can_id |
идентификатор и флаги: 0x80000000 — 29 бит, 0x40000000 — запрос, 0x20000000 — ошибка |
| 8 | 1 | can_dlc |
длина |
| 9 | 1 | channel |
номер канала |
| 10 | 1 | flags |
переполнение, CAN FD, BRS, ESI |
| 11 | 1 | reserved |
0 |
| 12 | 8 | data |
данные |
| 20 | 4 | timestamp_us |
метка времени, если включена |
Заголовок — 12 байт; с данными классического CAN — 20; с меткой — 24; кадр
CAN FD — 76 и 80. Флаги в can_id те же, что у struct can_frame в
SocketCAN (у той — 16 байт, данные со смещения 8): драйверу не нужно их
переводить.
Запрос OBD «обороты» — 7DF#02010CCCCCCCCCCC — на проводе USB. Строку
разобрал parse_canframe из can-utils, пакет собран по gs_usb.h:
хост -> адаптер, echo_id 0 (20 байт): 00000000 DF070000 08000000 02010CCC CCCCCCCC
адаптер -> хост, эхо (24 байт): 00000000 DF070000 08000000 02010CCC CCCCCCCC CCD01200
адаптер -> хост, приём (24 байт): FFFFFFFF E8070000 08000000 04410C14 5FCCCCCC 87D61200
Всё little-endian: 0x7DF лежит как DF 07 00 00. Эхо — тот же пакет с тем
же echo_id 0 и меткой: CC D0 12 00 — 0x0012D0CC = 1 233 100 мкс (метки
в примере наши). Третья строка — ответ блока 7E8 04 41 0C 14 5F, тот же,
что в 19г: echo_id 0xFFFFFFFF значит «принят с шины», а обороты — (0x14 ×
256 + 0x5F) / 4 = 1303,75 об/мин.
Как это работает в прошивке (src/can_common.c). CAN_SendFrame берёт
пакет из очереди от компьютера, кладёт кадр в почтовый ящик bxCAN, ставит
метку и отправляет тот же пакет обратно — это и есть эхо. У принятого
кадра CAN_ReceiveFrame пишет echo_id = GS_HOST_FRAME_ECHO_ID_RX, то есть
0xFFFFFFFF. Метка — счётчик TIM2 на 1 МГц с переполнением на 0xFFFFFFFF:
2³² мкс — 4294,97 с, то есть метка обнуляется каждые 71,6 минуты.
Если адаптеру включили флаг дополнения пакетов, классический кадр уходит к
компьютеру пакетом ровно в 32 байта: по комментарию в usbd_gs_can.c, так
заметно лучше работает WinUSB.
19ж.4 Битовый тайминг: от bitrate 500000 до CAN_BTR
В gs_usb компьютер не просит «500 кбит/с» — он сам считает тайминг и шлёт
пять чисел. Считает ядро Linux по пределам из BT_CONST. Тот же расчёт
вынесен в can-utils программой can-calc-bit-timing, и в её таблице есть
строка «gs_usb, bxcan, stm32f072» с частотой 48 МГц — те же пределы, что в
прошивке. Прогон can-calc-bit-timing -c 48000000 bxcan (алгоритм «v6.3»):
| Скорость | Квант, нс | PrS | PhS1 | PhS2 | SJW | BRP | Точка выборки | CAN_BTR |
|---|---|---|---|---|---|---|---|---|
| 1 000 000 | 62 | 5 | 6 | 4 | 2 | 3 | 75,0 % | 0x013a0002 |
| 500 000 | 125 | 6 | 7 | 2 | 1 | 6 | 87,5 % | 0x001c0005 |
| 250 000 | 250 | 6 | 7 | 2 | 1 | 12 | 87,5 % | 0x001c000b |
| 125 000 | 500 | 6 | 7 | 2 | 1 | 24 | 87,5 % | 0x001c0017 |
Для 500 кбит/с: делитель 6 даёт квант 125 нс, бит — 1 + 6 + 7 + 2 = 16 квантов, точка выборки — после 14 из 16, 87,5 %. Запрос BITTIMING — пять чисел по 4 байта:
запрос BITTIMING 500000 (20 байт): 06000000 07000000 02000000 01000000 06000000
Прошивка складывает их в регистр в can_drv_enable:
uint32_t btr = FIELD_PREP(CAN_BTR_SJW, channel->bittiming.sjw - 1) |
FIELD_PREP(CAN_BTR_TS2, channel->bittiming.phase_seg2 - 1) |
FIELD_PREP(CAN_BTR_TS1, channel->bittiming.prop_seg + channel->bittiming.phase_seg1 - 1) |
FIELD_PREP(CAN_BTR_BRP, channel->bittiming.brp - 1);
bxCAN не различает prop_seg и phase_seg1 — у него один отрезок TS1, и
прошивка их складывает: 6 + 7 − 1 = 12 в битах 16…19. TS2 = 2 − 1 в битах
20…22, SJW = 1 − 1 в битах 24…25, BRP = 6 − 1 в битах 0…9 (позиции — из
заголовка CMSIS stm32f072xb.h). Итог 0x001C0005 — ровно то, что напечатал
can-calc-bit-timing в колонке CAN_BTR. Наш прогон той же формулы на
четырёх строках таблицы совпал со всеми четырьмя.
У SLCAN всё наоборот: тайминг зашит в прошивку, а компьютер выбирает
номер S0…S8. Нестандартную скорость (скажем, 33,3 кбит/с) через S
не задать — slcand для этого предлагает -b <btr>, сырое значение
регистра, и оно у каждого адаптера своё.
19ж.5 SocketCAN и can-utils
Когда адаптер с gs_usb воткнут, интерфейс уже есть, но выключен. Строки из
документации rusEFI (java_console/readme.md) и Klipper (docs/CANBUS.md):
sudo ip link set can0 type can bitrate 500000 restart-ms 100
sudo ip link set can0 up
ip -details -statistics link show can0
ip link set can0 txqueuelen 128
Первая строка задаёт скорость (restart-ms 100 rusEFI добавляет для
перезапуска после bus-off), вторая включает, третья показывает состояние,
скорость и счётчики. Четвёртую Klipper ставит в настройку интерфейса
адаптера: очередь отправки на 128 кадров.
Дальше — программы can-utils. Мы собрали их в контейнере из исходников: 37 программ, ноль предупреждений. Главные:
candump can0— смотреть поток;candump -L can0— писать журнал, который потом проигрываетcanplayer;cansend can0 7DF#02010CCCCCCCCCCC— отправить один кадр;cangen can0— залить шину случайными кадрами (проверка нагрузки);isotpsend,isotprecv— отправить и принять сообщение ISO-TP из 19в через сокет ISO-TP ядра;isotpdump— подслушать и расшифровать кадры ISO-TP на обычном сокете.
Как cansend понимает строку, проверили на самом parse_canframe из
lib.c:
| Строка | Результат |
|---|---|
7DF#02010CCCCCCCCCCC |
0x7DF, 8 байт |
7DF#02.01.0C |
0x7DF, 3 байта — точки можно ставить для читаемости |
18DB33F1#02010C |
0x98DB33F1: восемь цифр — 29-битный, взведён флаг 0x80000000 |
7DF#R |
0x400007DF: запрос (RTR), 0 байт |
7DF#02010CCCCCCCCCCCCC |
0x7DF, 8 байт: девятый байт молча отброшен |
7DF02010C |
0 — ошибка, нет # |
Ответ блока в двух видах вывода candump — их печатают snprintf_canframe
и snprintf_long_canframe из того же lib.c (метку 1 234 567 мкс мы
перевели в формат журнала):
candump -L: (0000000001.234567) can0 7E8#04410C145FCCCCCC
candump: " can0 7E8 [8] 04 41 0C 14 5F CC CC CC"
Строка журнала — это ровно то, что принимает cansend после имени
интерфейса. Журнал можно читать глазами, разбирать по DBC (19е) и
проигрывать обратно на шину.
19ж.6 Стенд для автодиагностики
На столе, до машины, стенд собирается из трёх частей:
- Адаптер с candleLight_fw — компьютер с SocketCAN и can-utils.
- Имитатор блока — прошивка из 19г (двигатель и коробка отвечают на OBD-II) или загрузчик UDS из 19д на второй плате с bxCAN; прибор из 198р можно проверить на том же имитаторе.
- Шина: два провода CANH и CANL и два резистора по 120 Ом, лучше —
по одному на каждом конце (так в
CANBUS.mdKlipper). Часто один уже стоит на плате адаптера или блока.
Обмен со стенда: журнал в одном окне, расшифровка ISO-TP во втором, запрос в третьем:
candump -L can0 > trip.log
isotpdump -s 7DF -d 7E8 can0
echo "01 0C" | isotpsend -s 7DF -d 7E8 -p CC:CC can0
-s — идентификатор, с которого шлём, -d — с которого ждём ответ, -p —
дополнение кадра байтом 0xCC (как у Linux в 19в). Одиночный запрос можно
слать на общий адрес 0x7DF, но длинный ответ (VIN, 0902) требует
управления потоком на физический адрес блока — 0x7E0 для 0x7E8 (19в, 19г).
Поэтому для длинных ответов -s 7E0.
Без железа в Linux есть виртуальная шина. Строки из документации
cantools (docs/index.rst) и скрипта tools/enable_vcan0.sh из iso14229:
sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set vcan0 up
И все те же программы работают на vcan0 — если ядро собрано с ними.
Ядро WSL2 (6.6.87.2-microsoft-standard-WSL2), в котором шёл прогон,
собрано без виртуальной шины, без ISO-TP и без драйвера gs_usb:
# CONFIG_CAN_ISOTP is not set
# CONFIG_CAN_VCAN is not set
# CONFIG_CAN_GS_USB is not set
Error: Unknown device type.
Последняя строка — ответ ip link add dev vcan0 type vcan. Собранные
can-utils ответили так же честно: cansend и candump — «No such device»,
isotpsend — socket: Protocol not supported. Поэтому обмен по шине в
этом разделе не запускался; проверено то, что можно без неё: байты пакета
gs_usb, разбор и печать строк lib.c, тайминг и сборка прошивок.
Какую плату брать. По CMakeLists.txt candleLight_fw собирается для 16
плат: 6 на STM32F042 (canable, cantact, usb2can и др.), 7 на STM32F072
(candleLight, FYSETC UCAN, DSD TECH SH-C30A и др.), 1 на STM32F407 и 2 на
STM32G0B1 с CAN FD (эти две по умолчанию выключены). Мы собрали две:
| Цель | МК | Код, байт | Данные / ОЗУ | Флеш занято |
|---|---|---|---|---|
canable_fw |
STM32F042, 32 КБ | 23 256 | 64 / 4136 | 70,97 % |
candleLight_fw |
STM32F072, 128 КБ | 23 368 | 64 / 4136 | 17,83 % |
| canable-fw (SLCAN) | STM32F042, 32 КБ | 28 632 | 1436 / 4288 | — |
candleLight_fw собралась без предупреждений; ОЗУ — 4200 байт из 6 КБ у F042 (68,36 %). Файлы для прошивки — 23 320 и 23 432 байта. canable-fw собралась со своим Makefile, но с 11 предупреждениями.
STM32F103 для такого адаптера не годится: README candleLight прямо пишет, что у него USB и CAN не могут работать одновременно (общая память — см. 198е). У F042 в маленьких корпусах USB и CAN_TX выведены на одну ножку — такие тоже непригодны. Платы на STM32G431 (CANable 2.0 и похожие) с FDCAN этот проект пока не поддерживает — у них своя прошивка, здесь она не собиралась.
19ж.7 Полный код стенда gs_usb
Свой код раздела — три файла. gs_side.h и gs_side.c — сторона gs_usb:
пакет собирается и разбирается по самому заголовку gs_usb.h из
candleLight_fw. usbcan19zh.c — путь кадра «строка cansend → пакет
USB → строка candump» на lib.c из can-utils. Единиц трансляции две,
потому что у gs_usb.h и у заголовков can-utils свои одноимённые макросы
CAN_*_FLAG. Ниже все три файла целиком. Своей прошивки адаптера в
разделе нет: на плату идёт candleLight_fw или canable-fw как есть.
Сборка — те же команды, что в прогоне. CANDLELIGHT — клон
github.com/candle-usb/candleLight_fw, CAN_UTILS — клон
github.com/linux-can/can-utils. Тест берёт из вывода can-calc-bit-timing
строки от 125 кбит/с и для каждой печатает CAN_BTR, а для 500 кбит/с —
ещё и байты запроса BITTIMING:
gcc -std=gnu11 -Wall -Wextra -Werror -I$CANDLELIGHT/include -c gs_side.c -o gs.o
gcc -std=gnu11 -Wall -Wextra -Wno-unused-parameter -Werror -I. -I$CAN_UTILS -I$CAN_UTILS/include \
usbcan19zh.c $CAN_UTILS/lib.c gs.o -o u
can-calc-bit-timing -c 48000000 bxcan > calc.txt
./u $(awk '$1 ~ /^[0-9]+$/ && $1 >= 125000 {print $1, $3, $4, $5, $6, $7}' calc.txt)
Граница между заголовками — gs_side.h:
/* Раздел 19ж: граница между заголовками candleLight (gs_side.c) и can-utils (usbcan19zh.c):
* у обоих свои макросы CAN_*_FLAG, поэтому это две единицы трансляции. */
#pragma once
#include <stddef.h>
#include <stdint.h>
void gs_layout(void);
size_t gs_pack(uint8_t *out, uint32_t echo_id, uint32_t can_id, uint8_t dlc,
const uint8_t *data, int with_ts, uint32_t ts_us);
int gs_unpack(const uint8_t *in, size_t len, uint32_t *echo_id, uint32_t *can_id,
uint8_t *dlc, uint8_t *data, uint32_t *ts_us);
size_t gs_bt_pack(uint8_t *out, uint32_t prop, uint32_t ps1, uint32_t ps2, uint32_t sjw, uint32_t brp,
uint32_t *btr);
Сторона gs_usb. gs_layout() печатает sizeof и offsetof для
таблицы 19ж.3. gs_pack() и gs_unpack() переводят кадр в байты USB и
обратно; эхо от принятого кадра отличает только echo_id. gs_bt_pack()
собирает запрос BITTIMING и по формуле can_drv_enable считает CAN_BTR
(19ж.4):
/* Раздел 19ж. Сторона gs_usb: раскладка struct gs_host_frame из заголовка
* candleLight_fw (include/gs_usb.h, MIT, Hubert Denkmair) — берём сам заголовок,
* а не пересказ. Собирается gcc в stm-sys:1 с флагами прошивки:
* -std=gnu11 -Wall -Wextra -Werror. Порядок байтов — little-endian, как у прошивки
* и у x86 (gs_usb.h: «candleLight ... exchanges the data in little endian»). */
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include "gs_usb.h"
#include "gs_side.h"
void gs_layout(void)
{
printf("sizeof gs_host_frame (заголовок) %zu\n", sizeof(struct gs_host_frame));
printf("offsetof echo_id %zu, can_id %zu, can_dlc %zu, channel %zu, flags %zu, reserved %zu, data %zu\n",
offsetof(struct gs_host_frame, echo_id), offsetof(struct gs_host_frame, can_id),
offsetof(struct gs_host_frame, can_dlc), offsetof(struct gs_host_frame, channel),
offsetof(struct gs_host_frame, flags), offsetof(struct gs_host_frame, reserved),
offsetof(struct gs_host_frame, classic_can));
struct gs_host_frame *f = NULL;
printf("struct_size classic_can %zu, classic_can_ts %zu, canfd %zu, canfd_ts %zu\n",
struct_size(f, classic_can, 1), struct_size(f, classic_can_ts, 1),
struct_size(f, canfd, 1), struct_size(f, canfd_ts, 1));
printf("sizeof host_config %zu, device_config %zu, device_mode %zu, bittiming %zu, bt_const %zu\n",
sizeof(struct gs_host_config), sizeof(struct gs_device_config), sizeof(struct gs_device_mode),
sizeof(struct gs_device_bittiming), sizeof(struct gs_device_bt_const));
printf("BREQ HOST_FORMAT %d, BITTIMING %d, MODE %d, BT_CONST %d, DEVICE_CONFIG %d\n",
GS_USB_BREQ_HOST_FORMAT, GS_USB_BREQ_BITTIMING, GS_USB_BREQ_MODE,
GS_USB_BREQ_BT_CONST, GS_USB_BREQ_DEVICE_CONFIG);
printf("echo_id приёма 0x%08X, флаги id EFF 0x%08X RTR 0x%08X ERR 0x%08X\n",
GS_HOST_FRAME_ECHO_ID_RX, CAN_EFF_FLAG, CAN_RTR_FLAG, CAN_ERR_FLAG);
}
/* Кадр хоста -> байты USB. with_ts: с меткой времени (так шлёт устройство при HW_TIMESTAMP). */
size_t gs_pack(uint8_t *out, uint32_t echo_id, uint32_t can_id, uint8_t dlc,
const uint8_t *data, int with_ts, uint32_t ts_us)
{
union { struct gs_host_frame f; uint8_t raw[80]; } u;
memset(&u, 0, sizeof u);
u.f.echo_id = echo_id;
u.f.can_id = can_id;
u.f.can_dlc = dlc;
u.f.channel = 0;
u.f.flags = 0;
memcpy(u.f.classic_can->data, data, dlc);
size_t len;
if (with_ts) {
u.f.classic_can_ts->timestamp_us = ts_us;
len = struct_size(&u.f, classic_can_ts, 1);
} else {
len = struct_size(&u.f, classic_can, 1);
}
memcpy(out, u.raw, len);
return len;
}
/* Байты USB -> поля. Возвращает 1, если это эхо нашего кадра, 0 — принятый с шины. */
int gs_unpack(const uint8_t *in, size_t len, uint32_t *echo_id, uint32_t *can_id,
uint8_t *dlc, uint8_t *data, uint32_t *ts_us)
{
union { struct gs_host_frame f; uint8_t raw[80]; } u;
memset(&u, 0, sizeof u);
memcpy(u.raw, in, len);
*echo_id = u.f.echo_id;
*can_id = u.f.can_id;
*dlc = u.f.can_dlc;
memcpy(data, u.f.classic_can->data, 8);
*ts_us = (len >= struct_size(&u.f, classic_can_ts, 1)) ? u.f.classic_can_ts->timestamp_us : 0;
return u.f.echo_id != GS_HOST_FRAME_ECHO_ID_RX;
}
/* Запрос BITTIMING (управляющий запрос 1): пять чисел по 4 байта, как в gs_usb.h.
* Регистр CAN_BTR прошивка собирает в src/can/bxcan.c (can_drv_enable) — повторяем
* ту же формулу: SJW-1 в биты 24..25, TS2-1 в 20..22, TS1-1 = prop+phase1-1 в 16..19, BRP-1 в 0..9. */
size_t gs_bt_pack(uint8_t *out, uint32_t prop, uint32_t ps1, uint32_t ps2, uint32_t sjw, uint32_t brp,
uint32_t *btr)
{
struct gs_device_bittiming bt = { .prop_seg = prop, .phase_seg1 = ps1, .phase_seg2 = ps2, .sjw = sjw, .brp = brp };
memcpy(out, &bt, sizeof bt);
*btr = ((sjw - 1u) << 24) | ((ps2 - 1u) << 20) | ((prop + ps1 - 1u) << 16) | (brp - 1u);
return sizeof bt;
}
Путь кадра. Разбор шести строк cansend, запрос 7DF 02 01 0C к
адаптеру, эхо с меткой, ответ 7E8 с шины, печать в двух видах candump
и тайминг по строкам can-calc-bit-timing:
/* Раздел 19ж. Путь кадра «строка cansend -> struct can_frame -> пакет gs_usb -> обратно
* -> строка candump». Строки разбирает и печатает настоящий lib.c из can-utils
* (GPL-2.0-only OR BSD-3-Clause), раскладку пакета даёт gs_usb.h из candleLight_fw (MIT)
* через gs_side.c. Шины здесь нет: в ядре WSL2 нет vcan (см. прогон), поэтому
* проверяется то, что можно без неё, — форматы и байты.
* Сборка (stm-sys:1): gcc -std=gnu11 -Wall -Wextra -Werror -I<can-utils> -I<can-utils>/include
* usbcan19zh.c <can-utils>/lib.c gs_side.o */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <linux/can.h>
#include "lib.h"
#include "gs_side.h"
static void hex(const char *what, const uint8_t *b, size_t n)
{
printf("%s (%zu байт):", what, n);
for (size_t i = 0; i < n; i++)
printf("%s%02X", (i % 4 == 0) ? " " : "", b[i]);
printf("\n");
}
static void try_parse(const char *s)
{
char buf[64];
union cfu cu;
strcpy(buf, s);
memset(&cu, 0, sizeof cu);
int mtu = parse_canframe(buf, &cu);
if (mtu == CAN_MTU)
printf("parse %-24s -> CAN_MTU %d, can_id 0x%08X, len %u%s\n", s, mtu, cu.cc.can_id, cu.cc.len,
(cu.cc.can_id & CAN_RTR_FLAG) ? ", RTR" : "");
else
printf("parse %-24s -> %d (ошибка разбора)\n", s, mtu);
}
int main(int argc, char **argv)
{
/* 1. Раскладка gs_usb и can_frame */
gs_layout();
printf("sizeof can_frame %zu, CAN_MTU %zu, can_frame.data offset %zu, EFF 0x%08X\n",
sizeof(struct can_frame), (size_t)CAN_MTU, offsetof(struct can_frame, data), CAN_EFF_FLAG);
/* 2. Разборщик строк cansend (lib.c: parse_canframe) */
try_parse("7DF#02010CCCCCCCCCCC");
try_parse("7DF#02.01.0C");
try_parse("18DB33F1#02010C");
try_parse("7DF#R");
try_parse("7DF#02010CCCCCCCCCCCCC");
try_parse("7DF02010C");
/* 3. Запрос 7DF 02 01 0C (дополнение 0xCC) от компьютера к адаптеру */
char line[64] = "7DF#02010CCCCCCCCCCC";
union cfu cu;
memset(&cu, 0, sizeof cu);
parse_canframe(line, &cu);
uint8_t pkt[80];
size_t n = gs_pack(pkt, 0u, cu.cc.can_id, cu.cc.len, cu.cc.data, 0, 0);
hex("хост -> адаптер, echo_id 0", pkt, n);
/* адаптер отправил кадр и вернул его тем же echo_id, с меткой времени */
n = gs_pack(pkt, 0u, cu.cc.can_id, cu.cc.len, cu.cc.data, 1, 1233100u);
hex("адаптер -> хост, эхо", pkt, n);
uint32_t echo, id, ts;
uint8_t dlc, data[8];
int is_echo = gs_unpack(pkt, n, &echo, &id, &dlc, data, &ts);
printf("разбор эха: echo_id %u -> %s\n", echo, is_echo ? "эхо (кадр ушёл)" : "принят с шины");
/* 4. Ответ блока 7E8 04 41 0C 14 5F (обороты, как у elm327-emulator в 19г) */
const uint8_t resp[8] = { 0x04, 0x41, 0x0C, 0x14, 0x5F, 0xCC, 0xCC, 0xCC };
n = gs_pack(pkt, 0xFFFFFFFFu, 0x7E8u, 8, resp, 1, 1234567u);
hex("адаптер -> хост, приём", pkt, n);
is_echo = gs_unpack(pkt, n, &echo, &id, &dlc, data, &ts);
printf("разбор приёма: echo_id 0x%08X -> %s, метка %u мкс\n", echo, is_echo ? "эхо" : "принят с шины", ts);
union cfu rx;
memset(&rx, 0, sizeof rx);
rx.cc.can_id = id;
rx.cc.len = dlc;
memcpy(rx.cc.data, data, dlc);
char s[256];
snprintf_canframe(s, sizeof s, &rx, 0);
printf("candump -L: (%010llu.%06llu) can0 %s\n", (unsigned long long)(ts / 1000000u),
(unsigned long long)(ts % 1000000u), s);
snprintf_long_canframe(s, sizeof s, &rx, CANLIB_VIEW_INDENT_SFF);
printf("candump: \" can0 %s\"\n", s);
printf("обороты: (0x%02X*256 + 0x%02X)/4 = %.2f об/мин\n", data[3], data[4], (data[3] * 256 + data[4]) / 4.0);
/* 5. Запрос BITTIMING и регистр CAN_BTR для строк can-calc-bit-timing */
for (int i = 1; i + 5 < argc; i += 6) {
unsigned long rate = strtoul(argv[i], NULL, 10);
uint32_t prop = (uint32_t)atoi(argv[i + 1]), ps1 = (uint32_t)atoi(argv[i + 2]);
uint32_t ps2 = (uint32_t)atoi(argv[i + 3]), sjw = (uint32_t)atoi(argv[i + 4]);
uint32_t brp = (uint32_t)atoi(argv[i + 5]), btr;
n = gs_bt_pack(pkt, prop, ps1, ps2, sjw, brp, &btr);
unsigned tq = 1u + prop + ps1 + ps2;
printf("bittiming %lu: tq %u, бит/с %lu, точка выборки %.1f %%, CAN_BTR 0x%08x\n", rate, tq,
48000000ul / (brp * tq), 100.0 * (1u + prop + ps1) / tq, btr);
if (rate == 500000)
hex("запрос BITTIMING 500000", pkt, n);
}
return 0;
}
19ж.8 Грабли
Эхо пришло, а кадра на шине нет. candleLight возвращает эхо, когда
положил кадр в почтовый ящик bxCAN, а не когда кадр ушёл на шину и
получил подтверждение. README прошивки называет это ограничением: страдают
метки времени, режим одной попытки и крайние случаи. Если на шине никого
нет (нет второго узла или терминатора), эхо всё равно придёт, хотя кадр
никто не принял. Смотрите счётчики ошибок в ip -details -statistics.
Кадры меняются местами. Klipper в CANBUS_Troubleshooting.md
описывает перестановку кадров в драйвере gs_usb старых ядер (исправлено в
Linux 6.6.0) и в старых версиях candleLight_fw. Для ISO-TP это смерть:
следующие кадры с перепутанными номерами. Лечится обновлением ядра и
прошивки.
Лишний байт в cansend не ругается. 7DF#02010CCCCCCCCCCCCC — девять
байт, а уйдёт восемь: parse_canframe просто перестаёт читать. Считайте
байты сами или пишите через точки.
Ядро падает при выдёргивании адаптера. В Linux до 4.5 драйвер gs_usb мог уронить ядро при отключении устройства (README candleLight). На старых системах — не дёргать на ходу.
Скорость SLCAN не та. У SLCAN нет расчёта тайминга: S6 — это 500
кбит/с только если прошивка так считает. Нестандартная скорость — только
через -b с сырым регистром конкретного адаптера. У gs_usb тайминг
считает ядро по пределам, которые сообщил сам адаптер.
Метка времени скачет назад. 32-битный счётчик микросекунд переполняется каждые 71,6 минуты. В журнале поездки на пару часов это случится раз или два — сравнивайте метки по модулю 2³² или берите время компьютера.
vcan «не существует». Error: Unknown device type. у ip link add
значит, что в ядре нет модуля vcan (как в ядре WSL2 из прогона), а не ошибку в
команде.
19ж.9 Итоги
- Адаптер USB-CAN — STM32 с USB и bxCAN. SLCAN — строки: кадр 0x7DF с 8 байтами — 22 символа, подтверждения нет. gs_usb — пакеты: 20 байт к адаптеру, 24 с меткой обратно, каждый отправленный кадр возвращается эхом.
gs_host_frame:echo_id(0),can_id(4), длина, канал, флаги (8…11), данные с 12, метка с 20.echo_id0xFFFFFFFF — кадр принят с шины. Флаги идентификатора — как в SocketCAN: 0x80000000, 0x40000000, 0x20000000.- candleLight_fw:
1d50:606f, класс 0xFF, конечные точки 0x81 и 0x02 по 32 байта, DFU и WCID; запросы HOST_FORMAT 0, BITTIMING 1, MODE 2, BT_CONST 4, DEVICE_CONFIG 5. - 500 кбит/с на 48 МГц: BRP 6, 16 квантов по 125 нс, точка выборки 87,5 %,
CAN_BTR= 0x001C0005 — совпало уcan-calc-bit-timingи формулы прошивки. - SocketCAN:
ip link set can0 type can bitrate 500000,up; can-utils — 37 программ,candump -Lпишет журнал в форматеcansend. - Прошивки: candleLight_fw для F042 — 23 256 байт кода (70,97 % флеша), для F072 — 23 368; canable-fw (SLCAN) — 28 632.
- vcan и ISO-TP в ядре WSL2 6.6.87.2 нет — обмен по шине не запускался.
Дальше: раздел 19з «rusEFI: открытый блок управления двигателем на STM32 — датчики, CAN и диагностика» — настоящий блок двигателя с открытым кодом, которого можно спросить с этого стенда.