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

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 link
gs_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.

Путь кадра 7DF 02 01 0C от компьютера к шине двумя способами. Вверху gs_usb: программы cansend и candump, интерфейс SocketCAN can0 со структурой can_frame в 16 байт, драйвер gs_usb в ядре, две массовые конечные точки USB 0x02 и 0x81 к адаптеру STM32F042 или F072 с прошивкой candleLight_fw и bxCAN, трансивер; устройство 1d50:606f, класс 0xFF, пакет до 32 байт, тайминг задаётся запросом BITTIMING. Посередине пакет gs_host_frame побайтно: echo_id 00 00 00 00, can_id DF 07 00 00, длина 8 и канал 0, данные 02 01 0C и пять байт CC, метка времени CC D0 12 00, то есть 1 233 100 микросекунд; 20 байт от компьютера и 24 с меткой от адаптера, у принятого с шины кадра echo_id равен 0xFFFFFFFF. Внизу SLCAN: cansend на slcan0, демон slcand, USB CDC с идентификатором ad50:60c4, STM32F042 с canable-fw, тот же кадр строкой из 22 символов, подтверждения кадра нет

Чем они отличаются на деле:

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 программ, ноль предупреждений. Главные:

Как 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 Стенд для автодиагностики

На столе, до машины, стенд собирается из трёх частей:

  1. Адаптер с candleLight_fw — компьютер с SocketCAN и can-utils.
  2. Имитатор блока — прошивка из 19г (двигатель и коробка отвечают на OBD-II) или загрузчик UDS из 19д на второй плате с bxCAN; прибор из 198р можно проверить на том же имитаторе.
  3. Шина: два провода CANH и CANL и два резистора по 120 Ом, лучше — по одному на каждом конце (так в CANBUS.md Klipper). Часто один уже стоит на плате адаптера или блока.

Обмен со стенда: журнал в одном окне, расшифровка 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 Итоги


Дальше: раздел 19з «rusEFI: открытый блок управления двигателем на STM32 — датчики, CAN и диагностика» — настоящий блок двигателя с открытым кодом, которого можно спросить с этого стенда.

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