Embedded HandbookApps
All sections › III. Peripherals
STM32F042, STM32F072, STM32G431 · level intermediate · 25 min read
Open in the interactive handbook

19ж. USB-CAN adapter and SocketCAN: candleLight, can-utils and a car diagnostics test bench

Sections 19–19е taught the STM32 to sit on the bus itself: receive frames, query the car over OBD-II and UDS, decode someone else's frames with a DBC. But it is more convenient to debug all of this from a computer: watch the frame stream, send requests by hand, record a drive log. For that you need a USB-CAN adapter — and that is almost always the same STM32 with USB and bxCAN on one board.

Here you will see how a computer talks to such an adapter. There are two ways: SLCAN text lines over a virtual COM port (we already wrote those ourselves in 198е) and the binary gs_usb protocol of the candleLight firmware, which the Linux kernel understands without configuration and turns into the network interface can0. We will take apart a gs_usb packet byte by byte, see how ip link … bitrate 500000 becomes the CAN_BTR register, and build a car diagnostics test bench: the adapter, the unit simulator from 19г and the can-utils programs.

Sources: the candleLight_fw source code (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 firmware for the STM32F042) and can-utils (lib.c, candump.c, cansend.c, isotpsend.c, slcand.c, calc-bit-timing) — open repositories github.com/candle-usb/candleLight_fw, github.com/normaldotcom/canable-fw and github.com/linux-can/can-utils; the interface setup lines are from the Klipper and rusEFI documentation. The packets are built from the real gs_usb.h header, the lines are parsed and printed by the real lib.c — gcc 12.2.0 in the stm-sys:1 container; both firmwares were built by arm-none-eabi-gcc 12.2.1 in stm-arm:1. License: candleLight_fw — MIT (Hubert Denkmair), canable-fw — MIT (Eric Evenchick), can-utils — GPL-2.0-only or BSD-3-Clause at your choice (can-calc-bit-timing — GPL-2.0-only only); third-party code in this section is short quotes naming the project, our own code is given in full in 19ж.7.

Terms used in the documentation

Term Meaning
SocketCAN the CAN bus in Linux as a network interface: can0, sockets, ip link
gs_usb the binary protocol of a USB-CAN adapter and the Linux kernel driver for it
host frame, echo_id a packet carrying a frame between the computer and the adapter; a number used to confirm transmission
echo frame the echo: the adapter returns a sent frame once it has handed it to the CAN controller
bulk endpoint a USB endpoint for bulk transfers
CDC ACM the USB "virtual COM port" class
bit timing: prop_seg, phase_seg1/2, SJW, BRP bit timing: bit segments in quanta, synchronization jump, prescaler
time quantum, sample point the time quantum; the bit sampling point
vcan a virtual CAN bus in the Linux kernel, with no hardware
DFU firmware update over USB

19ж.1 Two ways: SLCAN and gs_usb

An adapter is a small bridge: USB on one side, a CAN controller and a transceiver on the other. The whole question is which language it speaks to the computer.

SLCAN — ASCII lines over a virtual COM port (USB CDC). The frame 0x7DF with data 02 01 0C padded to 8 bytes looks like this: t7DF802010CCCCCCCCCCC plus CR — 22 characters. This is how canable-fw for the STM32F042 and our sniffer from 198е work. In Linux the line is turned into an interface by the slcand daemon from can-utils: slcand -o -c -f -s6 ttyUSB0 — open the channel (-o), close it on exit (-c), clear the error flags (-f), speed S6 — 500 kbit/s.

gs_usb — binary packets over two USB bulk endpoints. The first device to work this way was the USB2CAN adapter from Geschwister Schneider (a comment in gs_usb.h mentions it); the open implementation is the candleLight_fw firmware. The gs_usb module is in the Linux kernel: according to the firmware README, the adapter "works out of the box" in distributions like Ubuntu and immediately appears as can0.

The path of the frame 7DF 02 01 0C from the computer to the bus in two ways. Top, gs_usb: the cansend and candump programs, the SocketCAN interface can0 with the 16-byte can_frame structure, the gs_usb driver in the kernel, two USB bulk endpoints 0x02 and 0x81 to an STM32F042 or F072 adapter with candleLight_fw firmware and bxCAN, the transceiver; device 1d50:606f, class 0xFF, packet up to 32 bytes, timing set by a BITTIMING request. In the middle, the gs_host_frame packet byte by byte: echo_id 00 00 00 00, can_id DF 07 00 00, length 8 and channel 0, data 02 01 0C and five CC bytes, timestamp CC D0 12 00, that is 1 233 100 microseconds; 20 bytes from the computer and 24 with a timestamp from the adapter, a frame received from the bus has echo_id 0xFFFFFFFF. Bottom, SLCAN: cansend on slcan0, the slcand daemon, USB CDC with identifier ad50:60c4, STM32F042 with canable-fw, the same frame as a 22-character line, no frame acknowledgement

How they differ in practice:

SLCAN (canable-fw) gs_usb (candleLight_fw)
USB CDC, ad50:60c4 own class 0xFF, 1d50:606f
Frame 0x7DF, 8 bytes 22 characters 20 bytes to the adapter, 24 back (with timestamp)
Bus speed 9 presets: S0…S8 any timing via a BITTIMING request
Transmit confirmation none an echo with the same echo_id
Timestamp none 32 bits, microseconds
Linux slcand by hand gs_usb driver, the interface appears by itself
Windows its own .inf (windows-driver/cantact.inf) WCID descriptors, no driver needed
Firmware flash, F042 28632 bytes of code 23256 bytes of code

About confirmation, the canable-fw README says it directly: the firmware gives no response to any command. You send a line and do not know whether the frame went out. With gs_usb every sent frame comes back as an echo, and the Linux driver knows that the frame was handed to the controller.


19ж.2 gs_usb: descriptors and control requests

An adapter with candleLight_fw is a USB device with identifier 1d50:606f (USBD_VID and USBD_PID_FS in config.h, the same for all boards). The board names differ: "canable gs_usb", "candleLight USB to CAN adapter" and so on; to tell several identical adapters apart, the README advises using the serial number from lsusb.

There are two interfaces. Interface zero is gs_usb itself: class, subclass and protocol 0xFF ("vendor-specific"), two bulk endpoints — 0x81 to the computer and 0x02 from it, 32 bytes each (64 in the CAN FD build). Interface one is DFU (class 0xFE): through it the firmware is updated with dfu-util without a BOOT jumper. Plus Microsoft WCID descriptors with "WINUSB" — Windows installs WinUSB by itself.

Everything except frames goes as control requests to the interface. The numbers are from enum gs_usb_breq:

Request Number What it carries
HOST_FORMAT 0 byte order (candleLight does not support it — always little-endian)
BITTIMING 1 timing: prop_seg, phase_seg1, phase_seg2, sjw, brp — 20 bytes
MODE 2 channel START or RESET and mode flags — 8 bytes
BT_CONST 4 what the controller can do: flags, clock, timing limits — 40 bytes
DEVICE_CONFIG 5 number of channels and versions — 12 bytes

For bxCAN the firmware answers BT_CONST like this (src/can/bxcan.c): the clock is CAN_CLOCK_SPEED from config.h (48 MHz on F042 and F072 boards, 42 MHz on the F407), tseg1 1…16, tseg2 1…8, sjw up to 4, brp 1…1024. Flags: listen-only, loopback, one-shot without retransmission, hardware timestamp, blinking for identification, padding packets to 32 bytes, bus error reporting, state reading, bus-off recovery; some boards also have a terminator and a hardware filter.


19ж.3 The gs_host_frame packet byte by byte

A frame in both directions is one structure from 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;

Next comes a union: 8 data bytes, 8 bytes plus the timestamp_us timestamp, or 64 bytes for CAN FD. We built this structure from the header itself in the container and printed sizeof and offsetof:

Offset Bytes Field What it holds
0 4 echo_id frame number from the computer; 0xFFFFFFFF — received from the bus
4 4 can_id identifier and flags: 0x80000000 — 29-bit, 0x40000000 — remote request, 0x20000000 — error
8 1 can_dlc length
9 1 channel channel number
10 1 flags overflow, CAN FD, BRS, ESI
11 1 reserved 0
12 8 data data
20 4 timestamp_us timestamp, if enabled

The header is 12 bytes; with classic CAN data — 20; with a timestamp — 24; a CAN FD frame — 76 and 80. The flags in can_id are the same as in SocketCAN's struct can_frame (which is 16 bytes, data at offset 8): the driver does not need to translate them.

The OBD "RPM" request — 7DF#02010CCCCCCCCCCC — on the USB wire. The line was parsed by parse_canframe from can-utils, the packet built from 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

Everything is little-endian: 0x7DF lies as DF 07 00 00. The echo is the same packet with the same echo_id 0 and a timestamp: CC D0 12 00 — 0x0012D0CC = 1233100 µs (the timestamps in the example are ours). The third line is the unit's response 7E8 04 41 0C 14 5F, the same as in 19г: echo_id 0xFFFFFFFF means "received from the bus", and the RPM is (0x14 × 256 + 0x5F) / 4 = 1303.75 RPM.

How this works in the firmware (src/can_common.c). CAN_SendFrame takes a packet from the queue from the computer, puts the frame into a bxCAN mailbox, sets the timestamp and sends the same packet back — that is the echo. For a received frame CAN_ReceiveFrame writes echo_id = GS_HOST_FRAME_ECHO_ID_RX, that is 0xFFFFFFFF. The timestamp is a TIM2 counter at 1 MHz overflowing at 0xFFFFFFFF: 2³² µs is 4294.97 s, so the timestamp wraps to zero every 71.6 minutes.

If the adapter has the packet padding flag enabled, a classic frame goes to the computer in a packet of exactly 32 bytes: according to a comment in usbd_gs_can.c, WinUSB works noticeably better this way.


19ж.4 Bit timing: from bitrate 500000 to CAN_BTR

In gs_usb the computer does not ask for "500 kbit/s" — it computes the timing itself and sends five numbers. The Linux kernel does the computation using the limits from BT_CONST. The same calculation is available in can-utils as the can-calc-bit-timing program, and its table has a line "gs_usb, bxcan, stm32f072" with a 48 MHz clock — the same limits as in the firmware. A run of can-calc-bit-timing -c 48000000 bxcan (the "v6.3" algorithm):

Speed Quantum, ns PrS PhS1 PhS2 SJW BRP Sample point CAN_BTR
1000000 62 5 6 4 2 3 75.0 % 0x013a0002
500000 125 6 7 2 1 6 87.5 % 0x001c0005
250000 250 6 7 2 1 12 87.5 % 0x001c000b
125000 500 6 7 2 1 24 87.5 % 0x001c0017

For 500 kbit/s: a prescaler of 6 gives a 125 ns quantum, a bit is 1 + 6 + 7 + 2 = 16 quanta, the sample point is after 14 of 16, 87.5 %. The BITTIMING request is five 4-byte numbers:

запрос BITTIMING 500000 (20 байт): 06000000 07000000 02000000 01000000 06000000

The firmware assembles them into the register in 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 does not distinguish prop_seg and phase_seg1 — it has a single TS1 segment, and the firmware adds them up: 6 + 7 − 1 = 12 in bits 16…19. TS2 = 2 − 1 in bits 20…22, SJW = 1 − 1 in bits 24…25, BRP = 6 − 1 in bits 0…9 (the positions are from the CMSIS header stm32f072xb.h). The result 0x001C0005 is exactly what can-calc-bit-timing printed in the CAN_BTR column. Our run of the same formula on the four rows of the table matched all four.

With SLCAN it is the other way round: the timing is hard-coded in the firmware, and the computer picks a number S0…S8. A non-standard speed (say, 33.3 kbit/s) cannot be set via S — for that slcand offers -b <btr>, the raw register value, and it is different for each adapter.


19ж.5 SocketCAN and can-utils

When a gs_usb adapter is plugged in, the interface is already there but down. Lines from the rusEFI documentation (java_console/readme.md) and 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

The first line sets the speed (rusEFI adds restart-ms 100 for restarting after bus-off), the second brings the interface up, the third shows the state, speed and counters. Klipper puts the fourth into the adapter interface configuration: a transmit queue of 128 frames.

Next come the can-utils programs. We built them from source in the container: 37 programs, zero warnings. The main ones:

How cansend understands a line was checked on the real parse_canframe from lib.c:

Line Result
7DF#02010CCCCCCCCCCC 0x7DF, 8 bytes
7DF#02.01.0C 0x7DF, 3 bytes — dots can be added for readability
18DB33F1#02010C 0x98DB33F1: eight digits — 29-bit, the 0x80000000 flag is set
7DF#R 0x400007DF: remote request (RTR), 0 bytes
7DF#02010CCCCCCCCCCCCC 0x7DF, 8 bytes: the ninth byte is silently dropped
7DF02010C 0 — error, no #

The unit's response in the two candump output formats — they are printed by snprintf_canframe and snprintf_long_canframe from the same lib.c (we converted the timestamp 1234567 µs into the log format):

candump -L: (0000000001.234567) can0 7E8#04410C145FCCCCCC
candump: "  can0       7E8   [8]  04 41 0C 14 5F CC CC CC"

A log line is exactly what cansend accepts after the interface name. The log can be read by eye, decoded with a DBC (19е) and replayed back onto the bus.


19ж.6 A car diagnostics test bench

On the desk, before the car, the bench is built from three parts:

  1. The adapter with candleLight_fw — a computer with SocketCAN and can-utils.
  2. A unit simulator — the firmware from 19г (engine and gearbox answer OBD-II) or the UDS bootloader from 19д on a second board with bxCAN; the device from 198р can be tested on the same simulator.
  3. The bus: two wires CANH and CANL and two 120 Ω resistors, preferably one at each end (as in Klipper's CANBUS.md). Often one is already fitted on the adapter or unit board.

An exchange on the bench: the log in one window, ISO-TP decoding in the second, the request in the third:

candump -L can0 > trip.log
isotpdump -s 7DF -d 7E8 can0
echo "01 0C" | isotpsend -s 7DF -d 7E8 -p CC:CC can0

-s is the identifier we send from, -d the one we expect the response from, -p — frame padding with byte 0xCC (as Linux does in 19в). A single-frame request can be sent to the functional address 0x7DF, but a long response (VIN, 0902) requires flow control to the unit's physical address — 0x7E0 for 0x7E8 (19в, 19г). So for long responses use -s 7E0.

Without hardware, Linux has a virtual bus. Lines from the cantools documentation (docs/index.rst) and the tools/enable_vcan0.sh script from iso14229:

sudo modprobe vcan
sudo ip link add dev vcan0 type vcan
sudo ip link set vcan0 up

And all the same programs work on vcan0 — if the kernel is built with them. The WSL2 kernel (6.6.87.2-microsoft-standard-WSL2) on which the run was done is built without the virtual bus, without ISO-TP and without the gs_usb driver:

# CONFIG_CAN_ISOTP is not set
# CONFIG_CAN_VCAN is not set
# CONFIG_CAN_GS_USB is not set
Error: Unknown device type.

The last line is the response to ip link add dev vcan0 type vcan. The built can-utils answered just as honestly: cansend and candump — "No such device", isotpsend — socket: Protocol not supported. So bus exchange was not run in this section; what could be checked without it was checked: the gs_usb packet bytes, the parsing and printing of lines by lib.c, the timing and the firmware builds.

Which board to choose. According to CMakeLists.txt, candleLight_fw builds for 16 boards: 6 on STM32F042 (canable, cantact, usb2can and others), 7 on STM32F072 (candleLight, FYSETC UCAN, DSD TECH SH-C30A and others), 1 on STM32F407 and 2 on STM32G0B1 with CAN FD (these two are disabled by default). We built two:

Target MCU Code, bytes Data / RAM Flash used
canable_fw STM32F042, 32 KB 23256 64 / 4136 70.97 %
candleLight_fw STM32F072, 128 KB 23368 64 / 4136 17.83 %
canable-fw (SLCAN) STM32F042, 32 KB 28632 1436 / 4288 —

candleLight_fw built without warnings; RAM is 4200 bytes out of 6 KB on the F042 (68.36 %). The files for flashing are 23320 and 23432 bytes. canable-fw built with its own Makefile, but with 11 warnings.

The STM32F103 is not suitable for such an adapter: the candleLight README states plainly that on it USB and CAN cannot work at the same time (shared memory — see 198е). On the F042 in small packages USB and CAN_TX are routed to the same pin — those are unusable too. Boards on the STM32G431 (CANable 2.0 and similar) with FDCAN are not yet supported by this project — they have their own firmware, which was not built here.


19ж.7 Full code of the gs_usb test bench

The section's own code is three files. gs_side.h and gs_side.c are the gs_usb side: the packet is built and parsed using the gs_usb.h header itself from candleLight_fw. usbcan19zh.c is the frame path "cansend line → USB packet → candump line" using lib.c from can-utils. There are two translation units because gs_usb.h and the can-utils headers each have their own macros with the same names, CAN_*_FLAG. Below are all three files in full. There is no adapter firmware of our own in this section: the board gets candleLight_fw or canable-fw as is.

The build uses the same commands as the run. CANDLELIGHT is a clone of github.com/candle-usb/candleLight_fw, CAN_UTILS is a clone of github.com/linux-can/can-utils. The test takes the lines from 125 kbit/s up from the can-calc-bit-timing output and prints CAN_BTR for each, and for 500 kbit/s also the bytes of the BITTIMING request:

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)

The boundary between the headers — 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);

The gs_usb side. gs_layout() prints sizeof and offsetof for the table in 19ж.3. gs_pack() and gs_unpack() convert a frame to USB bytes and back; only echo_id distinguishes an echo from a received frame. gs_bt_pack() builds the BITTIMING request and computes CAN_BTR using the can_drv_enable formula (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;
}

The frame path. Parsing six cansend lines, the request 7DF 02 01 0C to the adapter, the echo with a timestamp, the 7E8 response from the bus, printing in the two candump formats and the timing from the can-calc-bit-timing lines:

/* Раздел 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 Pitfalls

The echo arrived, but the frame is not on the bus. candleLight returns the echo when it has put the frame into a bxCAN mailbox, not when the frame has gone out on the bus and been acknowledged. The firmware README calls this a limitation: timestamps, one-shot mode and corner cases suffer. If there is nobody on the bus (no second node or terminator), the echo will still arrive, even though no one received the frame. Watch the error counters in ip -details -statistics.

Frames get reordered. Klipper in CANBUS_Troubleshooting.md describes frame reordering in the gs_usb driver of old kernels (fixed in Linux 6.6.0) and in old versions of candleLight_fw. For ISO-TP this is fatal: consecutive frames with mixed-up sequence numbers. The cure is to update the kernel and the firmware.

An extra byte in cansend is not reported. 7DF#02010CCCCCCCCCCCCC is nine bytes, but eight will go out: parse_canframe simply stops reading. Count the bytes yourself or write them with dots.

The kernel crashes when the adapter is unplugged. In Linux before 4.5 the gs_usb driver could crash the kernel when the device was disconnected (candleLight README). On old systems, do not unplug it while running.

The SLCAN speed is wrong. SLCAN has no timing calculation: S6 is 500 kbit/s only if the firmware thinks so. A non-standard speed is possible only via -b with the raw register of the specific adapter. With gs_usb the timing is computed by the kernel from the limits the adapter itself reported.

The timestamp jumps backwards. The 32-bit microsecond counter overflows every 71.6 minutes. In a drive log of a couple of hours this will happen once or twice — compare timestamps modulo 2³² or use the computer's time.

vcan "does not exist". Error: Unknown device type. from ip link add means that the kernel has no vcan module (as in the WSL2 kernel from the run), not an error in the command.


19ж.9 Summary


Next: section 19з "rusEFI: an open engine control unit on STM32 — sensors, CAN and diagnostics" — a real engine unit with open-source code that you can query from this test bench.

Open in the interactive handbook