GSUSB Device Stack
This document describes XRUSB’s GSUSB (Linux gs_usb) device class implementation: LibXR::USB::GsUsbClass<CanChNum>.
This device class targets USB-to-CAN usage in the Linux/SocketCAN ecosystem. It uses a Vendor Interface + Bulk IN/OUT transport model and implements the commonly used control plane and data plane of the gs_usb protocol.
Supported capabilities:
- Classic CAN: up to 8 bytes of data
- CAN FD (optional): up to 64 bytes of data; DLC mapping follows the FD table
- TX echo: echoes back
echo_idfor host-side TX buffer tracking - Optional hardware timestamp:
timestamp_us(4 bytes) appended to the end of the wire frame - Multi-channel: the number of channels is fixed at compile time by the template parameter
CanChNum
1. Class and construction
In GsUsbClass<CanChNum>, CanChNum is the compile-time CAN channel count (1..255). Channel indices are exposed as uint8_t. At construction, you must provide a list of channel object pointers whose count equals CanChNum.
This class supports both Classic CAN and FDCAN (with FD capability enabled) via different constructors.
1.1 Classic CAN constructor
GsUsbClass(std::initializer_list<LibXR::CAN*> cans,
Endpoint::EPNumber data_in_ep_num = EP1,
Endpoint::EPNumber data_out_ep_num = EP2,
size_t rx_queue_size = 32,
size_t echo_queue_size = 32,
LibXR::GPIO* identify_gpio = nullptr,
std::initializer_list<LibXR::GPIO*> termination_gpios = {},
LibXR::Database* database = nullptr);
Notes:
cans: list of Classic CAN pointers; the count must equalCanChNum- Default endpoint numbers are EP1 (IN) / EP2 (OUT) to satisfy constraints of some older kernels/drivers regarding endpoint layout
- The Linux
gs_usbdriver often matches devices via a VID:PID whitelist and may requirebInterfaceNumber == 0. It is recommended to place this class as the first interface in the configuration
1.2 FDCAN constructor (FD enabled)
GsUsbClass(std::initializer_list<LibXR::FDCAN*> fd_cans,
Endpoint::EPNumber data_in_ep_num = EP_AUTO,
Endpoint::EPNumber data_out_ep_num = EP_AUTO,
size_t rx_queue_size = 32,
size_t echo_queue_size = 32,
LibXR::GPIO* identify_gpio = nullptr,
std::initializer_list<LibXR::GPIO*> termination_gpios = {},
LibXR::Database* database = nullptr);
Notes:
fd_cans: list of FDCAN pointers; the count must equalCanChNum- If the driver does not restrict endpoint numbers,
EP_AUTOcan be used for automatic endpoint allocation - Whether FD is truly usable depends on both device-side FD support and whether the host enables FD mode for the channel via the control plane
2. USB interface and endpoints
2.1 Interface descriptor
GsUsbClass exposes one interface and does not use an IAD. The interface class/subclass/protocol are fixed to 0xFF/0xFF/0xFF (Vendor Specific), with 2 endpoints.
2.2 Bulk endpoints
- Bulk OUT: Host → Device (host sends CAN/FD frames; carries
echo_idfor TX echo) - Bulk IN: Device → Host (device reports RX frames/error frames, plus TX echo replies)
The maximum transfer length is determined by the underlying Endpoint implementation; configuration uses UINT16_MAX as an upper bound.
3. gs_usb wire format (data plane)
All Bulk data frames start with a fixed 12-byte header (little-endian):
struct WireHeader {
uint32_t echo_id; // Echo ID
uint32_t can_id; // CAN ID (with flags)
uint8_t can_dlc; // DLC
uint8_t channel; // Channel index
uint8_t flags; // GS_CAN_FLAG_*
uint8_t reserved;
}
Conventions:
- For RX frames reported by the device,
ECHO_ID_RX = 0xFFFFFFFFis used - If the host sends a frame with
echo_id != 0xFFFFFFFF, the device generates a corresponding TX echo event and sends it back
Payload and length:
- Classic CAN: payload is fixed at 8 bytes
- CAN FD: payload is fixed at 64 bytes
- Optional timestamp: if enabled for the channel, append 4 bytes
timestamp_us(microseconds, low 32 bits) after the payload
Total length:
- Classic:
12 + 8(or12 + 8 + 4) - FD:
12 + 64(or12 + 64 + 4)
3.1 DLC and FD mapping
- Classic:
can_dlcis capped at 8 (values above 8 are clamped to 8) - FD:
can_dlcis mapped to length0..64using the FD DLC mapping table
| DLC | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| LEN | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 20 | 24 | 32 | 48 | 64 |
4. Lifecycle: BindEndpoints / UnbindEndpoints
4.1 BindEndpoints
Initialization typically includes:
- Allocate/configure Bulk IN/OUT endpoints; generate and submit configuration descriptors (Interface + 2x Endpoint)
- Register endpoint callbacks:
- OUT complete: parse host wire frames
- IN complete: attempt to continue sending the next frame
- Reset runtime state and keep the OUT endpoint armed whenever possible (to avoid host “stalling” on sends)
- Register CAN RX callbacks (enqueue received frames/error frames and trigger transmission)
4.2 UnbindEndpoints
Teardown typically includes:
- Close endpoints and return them to
EndpointPool - Clear key state (including host_format negotiation state and per-channel enable flags)
5. Host → Device (Bulk OUT)
After receiving one OUT frame, the device:
- Parses
WireHeaderand validateschanneland the corresponding channel object - Determines Classic/FD by
flagsand checks whether the channel is enabled (e.g., FD requires both device and host to enable it) - Converts the wire frame into a CAN/FDCAN pack and submits it to the corresponding channel’s TX queue
- If
echo_id != 0xFFFFFFFF, enqueues an echo event to be sent back via Bulk IN - Re-arms OUT reception (when queue capacity allows)
6. Device → Host (Bulk IN)
Device-to-host traffic comes from two sources:
- TX echo (higher priority): used by the host to track transmit completion
- RX reports: Classic/FD frames received by the channels, plus optional error frames
Transmission policy:
- A send is started only when the IN endpoint is idle
- Dequeue order: echo first, then RX reports
- The IN complete callback triggers the next send attempt for continuous output
7. Control plane: Vendor Requests (gs_usb BREQ)
GsUsbClass does not implement standard Class Requests; all control-plane operations are done via Vendor Requests (gs_usb BREQ).
7.1 Reads (Device → Host)
Common read requests (per-channel or global) include:
- Bit timing constants / extended constants (FD capability)
- Device configuration (channel count, version, etc.)
- Timestamp (microseconds, low 32 bits)
- Termination state, per-channel error state
- USER_ID (from RAM or Database)
7.2 Writes (Host → Device)
Common write requests (per-channel) include:
HOST_FORMAT: host format negotiationBITTIMING: arbitration phase bit timingDATA_BITTIMING: data phase bit timing (FD)MODE: START/RESET and mode flags (loopback/listen-only/one-shot, etc.)- Error-frame reporting enable, Identify GPIO, termination control, USER_ID write
7.3 HOST_FORMAT requirement
The host must first complete format negotiation via HOST_FORMAT. If negotiation has not succeeded, some configuration requests (such as BITTIMING, MODE, etc.) are rejected to force the host to negotiate first.
8. Error frames and timestamps
8.1 Error frames (SocketCAN semantics)
Classic CAN error packs can be converted to SocketCAN-compatible error frames (e.g., CAN_ERR_FLAG). Whether they are reported is controlled by the per-channel error-frame enable flag.
8.2 Timestamps
When timestamps are enabled for a channel and a system timebase is available, the device appends timestamp_us to the end of the wire frame (4 bytes, microseconds, low 32 bits). Otherwise it is 0 or omitted (depending on the channel enable setting).
9. Examples
9.1 Classic CAN (2 channels)
using Dev = LibXR::USB::GsUsbClass<2>;
Dev gsusb({&can1, &can2},
/*in_ep*/LibXR::USB::Endpoint::EPNumber::EP1,
/*out_ep*/LibXR::USB::Endpoint::EPNumber::EP2,
/*rx_queue*/64,
/*echo_queue*/64,
/*identify*/&led_gpio,
/*termination*/{&term1_gpio, &term2_gpio},
/*db*/&db);
// USB device class list: {{&gsusb}}
// usb_dev.Init();
// usb_dev.Start();
9.2 FDCAN (1 channel)
using Dev = LibXR::USB::GsUsbClass<1>;
Dev gsusb({&fdcan1},
/*in_ep*/LibXR::USB::Endpoint::EPNumber::EP_AUTO,
/*out_ep*/LibXR::USB::Endpoint::EPNumber::EP_AUTO);
On Linux, the device is typically enumerated by the gs_usb driver as a SocketCAN interface (e.g., can0). You can then use ip link set can0 up type can bitrate ... or tools like cansend/candump for transmit/receive.