FreeRTOS firmware for an STM32 USB-to-CAN SLCAN bridge with optional cellular telemetry.
Developed for the ATUv1.0 board (STM32L476), but designed with hardware-independent application layers. See Porting & CubeMX Integration.
- USB CDC Virtual COM Port: Native compatibility with standard SLCAN (Lawicel) tools like
python-can, SavvyCAN, and BUSMASTER. - Optional Cellular Telemetry: Streams CAN frames over a UDP socket using a SIM7600 modem.
- Modular Design: Cellular telemetry is completely optional. Set
BOARD_HAS_MODEMto0inapp_config.hto fully compile out all modem drivers, queues, and FreeRTOS tasks to run as a dedicated, lightweight USB-to-CAN bridge. - Auto-Baud Detection: Sweeps all 9 supported bitrates (500K → 250K → 125K → 1M → 100K → 50K → 20K → 10K → 800K) in listen-only mode on startup or via custom
Acommand to automatically lock onto active bus traffic. - Data Batching & DMA: Uses DMA for non-blocking UART transmissions and batches packets to optimize network usage.
- Modem Resync & LED Signals: Automatically synchronizes baud rates with the modem on boot to prevent desyncs and provides simple, clear status LED feedback.
- Fully Static Memory: 100% statically allocated FreeRTOS resources—no dynamic heap usage.
The firmware's hardware initialization, HAL drivers, and middleware stacks are generated using STM32CubeMX. The App/ directory contains all custom, hardware-independent application code. Other directories (Core/, Drivers/, Middlewares/, USB_DEVICE/) contain the autogenerated CubeMX code templates and should generally not be modified directly (unless you know what you're doing).
CANbridge/
├── ATUv1.0.ioc CubeMX project file
├── CMakeLists.txt
├── CMakePresets.json
├── startup_stm32l476xx.s CubeMX-generated
├── STM32L476XX_FLASH.ld CubeMX-generated
│
├── App/ ← Main application directory
│ ├── app_config.h Compile-time configuration (bitrates, APN/IP parameters, stack sizes)
│ ├── autobaud.c/h Blocking bitrate scan in listen-only mode
│ ├── can_bridge.c/h FreeRTOS task creation and orchestration
│ ├── can_driver.c/h bxCAN HAL wrapper with FreeRTOS queue integration
│ ├── can_timing.c/h Bit-timing parameter calculator (any APB1 frequency)
│ ├── slcan.c/h SLCAN protocol parser and encoder
│ ├── modem.c/h SIM7600 cellular modem driver and high-speed data loop
│ ├── panda_encode.c/h comma.ai 16-byte packed Panda binary frame encoder
│ ├── usb_serial.c/h USB CDC ring-buffer isolation layer
│ └── util_ringbuffer.c/h SPSC byte ring buffer
│
├── Core/ CubeMX-generated (main.c, FreeRTOSConfig.h, ISR handlers)
├── Drivers/ CubeMX-generated (STM32L4 HAL + CMSIS)
├── Middlewares/ CubeMX-generated (FreeRTOS, STM32 USB Device Library)
└── USB_DEVICE/ CubeMX-generated + modified
└── App/
└── usbd_cdc_if.c CDC callbacks hook into usb_serial via USER CODE blocks
usbd_cdc_if.c and main.c are the only CubeMX-generated files with manual additions. The hooks are placed inside USER CODE BEGIN/END blocks and survive CubeMX regeneration.
Five statically allocated FreeRTOS tasks and four static communication queues orchestrate data flow:
| Task | Priority | Role |
|---|---|---|
CAN_RX |
4 | Pulls frames from bxCAN driver and forwards them to SLCAN and Modem queues |
CAN_TX |
4 | Drains the Driver TX Queue and feeds them directly into bxCAN hardware mailboxes |
SLCAN |
3 | Handles USB CDC character parsing, command decoding, and frame formatting |
MODEM |
2 | Manages cellular modem power sequencing, auto-baud resync, registration, socket, and batching |
LED |
1 | Manages system status and CAN activity LED feedback patterns |
The data pipelines operate asynchronously across thread-safe static queues to guarantee zero frame loss:
-
Data Transmission (Host ➔ CAN Bus):
- USB Host writes SLCAN ASCII commands to the Virtual COM Port.
- USB CDC Driver loads raw bytes into the
usb_serialRX Ring Buffer. SLCANTask processes bytes, parses Lawicel commands, and generates structured CAN frames.- CAN frames are dispatched to the thread-safe Driver TX Queue.
CAN_TXTask drains the queue and populates the bxCAN Hardware Mailboxes as they become available.- bxCAN peripheral transmits the frames onto the physical CAN Bus.
-
Data Reception & Local Bridging (CAN Bus ➔ USB CDC):
- Incoming frames on the CAN Bus trigger the bxCAN RX FIFO0 interrupt.
- The ISR extracts the frame and pushes it to the thread-safe Driver RX Queue.
CAN_RXTask drains the queue and forwards frames to the SLCAN TX Queue.SLCANTask formats the frames into Lawicel-compliant ASCII strings and writes them to theusb_serialTX Ring Buffer.- The USB CDC peripheral transmits data from the ring buffer back to the USB Host.
-
Asymmetric Cellular Telemetry Pipeline (CAN Bus ➔ Remote UDP Server):
- Incoming frames on the CAN Bus trigger the
CAN_RXtask. - The
CAN_RXtask forwards the frame to the thread-safe Modem TX Queue (net_tx_queue). MODEMTask drains the queue and serializes CAN frames into packed 16-byte Panda binary frames.- Encodings are batched into
tx_batch_buf(416-byte maximum capacity). - If the batch reaches the
MODEM_UDP_FLUSH_BYTESlimit (400 bytes) OR if a 25ms timeout expires, a preemptive transmission is initiated. - The batch is dispatched via a non-blocking UART4 DMA TX channel to the SIM7600 cellular modem, and the
MODEMtask yields CPU execution efficiently using direct task notifications. - The SIM7600 modem streams the UDP packet blindly at 921600 baud through the cellular network to the Remote UDP Server.
- Incoming frames on the CAN Bus trigger the
| Command | Description |
|---|---|
Sn |
Set standard bitrate: S0=10K S1=20K S2=50K S3=100K S4=125K S5=250K S6=500K S7=800K S8=1M |
O |
Open CAN channel in Normal mode |
L |
Open CAN channel in Listen-Only (silent) mode |
l |
Open CAN channel in Loopback mode |
C |
Close CAN channel |
tIIILDD... |
Transmit standard data frame (11-bit ID) |
TIIIIIIIILDD... |
Transmit extended data frame (29-bit ID) |
rIIIL |
Transmit standard Remote Transmission Request (RTR) frame |
RIIIIIIIIL |
Transmit extended Remote Transmission Request (RTR) frame |
F |
Read status / error flags (Warning / Passive / Bus-off state flags) |
V |
Read hardware version |
v |
Read firmware version |
N |
Read hardware serial number |
Z0 / Z1 |
Disable / enable 16-bit millisecond timestamps on received frames |
ACK = \r, NACK = \x07. All commands are terminated with \r.
| Command | Description |
|---|---|
A |
Trigger an on-demand synchronous passive auto-baud scan (channel must be closed). Scans active traffic across all 9 supported bitrates. Returns An\r (where n is the detected bitrate code 0-8) on success, or NACK (\x07) if no active bus bitrate is found. |
The firmware features a single, unified configuration and control namespace ($CONFIG) utilizing standard space-separated, bash-style CLI arguments. Changes can be updated in memory, read back in a clean shell format, and committed persistently to internal Flash NVS.
| Argument | Syntax / Values | Description |
|---|---|---|
| None | $CONFIG |
Reads and prints the current active runtime configuration from memory. |
--apn |
--apn=internet |
Updates Cellular Context APN string in memory (max 31 chars). |
--ip |
--ip=34.101.176.84 |
Updates Remote UDP Server IP/Domain string in memory (max 63 chars). |
--rport |
--rport=1338 |
Updates Remote target UDP port in memory. |
--lport |
--lport=8888 |
Updates Local modem source UDP port in memory. |
--auto |
--auto=0|1 |
Configures cellular bridge auto-connect on boot (1 = auto, 0 = off/disabled). |
--canscan |
--canscan=0|1 |
Configures CAN boot-time auto-scanning (1 = scan, 0 = start with default speed). |
--canbaud |
--canbaud=0..8 |
Sets default CAN bitrate index (0–8, matching standard Lawicel index). |
--save |
--save |
Commits all active memory settings persistently to the STM32 Flash NVS page. |
--reset |
--reset |
Reverts all settings in memory back to compile-time defaults. |
Multiple arguments can be combined and executed in a single command line (e.g. $CONFIG --apn=hologram --auto=0 --save which updates settings in memory, commits them to Flash NVS, and returns $OK\r\n on success).
The firmware provides highly optimized, event-driven status LED visual telemetry.
- LED1 (Cellular & Network Status):
- OFF (Dark): Power-on sequencing, re-probing, or auto-baud synchronization in progress.
- Fast Blinking (100ms): Active GSM network registration, APN attachment, or UDP tunnel negotiation.
- Solid ON: Socket connected, transparent high-speed telemetry streaming active.
- Triple-Pulse Heartbeat (Heartbeat flash + pause): Connection dropped, UDP socket failure, or data mode fault cooldown.
- LED2 (CAN Status / Activity):
- OFF (Dark): CAN bus is closed/idle.
- Fast Blinking (100ms): Auto-baud scan or search in progress.
- Solid ON: CAN bus is open and active.
- Flicker (50ms toggle): Pin toggles on active transmit/receive activity, and automatically restores to the persistent state (
ONwhen open,OFFwhen closed).
All software tuning is centralized in app_config.h.
| Define | Default | Description |
|---|---|---|
CAN_SCAN_ENABLED |
1 |
Enable/disable boot-time auto-baud sweep |
CAN_SCAN_DWELL_MS |
300 |
Passive listening time (ms) spent per bitrate during scan |
DEFAULT_CAN_BITRATE |
CAN_BITRATE_250K |
Default bitrate if an O command is received without prior Sn setup |
CAN_RX_QUEUE_LEN |
512 |
Driver RX queue depth (frames) |
CAN_TX_QUEUE_LEN |
512 |
Driver TX queue depth (frames) |
SLCAN_TX_QUEUE_LEN |
512 |
SLCAN TX queue depth (frames) |
NET_TX_QUEUE_LEN |
64 |
Cellular telemetry queue depth (frames) |
BOARD_HAS_LEDS |
1 |
Enable/disable status LED visual feedback compile-time |
DEFAULT_MODEM_APN |
"internet" |
Context APN for cellular network registration |
DEFAULT_MODEM_REMOTE_IP |
"35.197.154.247" |
Target server destination IP address for UDP streaming |
DEFAULT_MODEM_REMOTE_PORT |
1338 |
Destination port for UDP streaming |
MODEM_AT_BAUDRATE |
115200 |
Baud rate for AT command configuration phase |
MODEM_DATA_BAUDRATE |
921600 |
Baud rate for high-speed transparent data streaming |
MODEM_UDP_FLUSH_BYTES |
400 |
MTU batching size threshold (bytes) before flushing |
HW_VERSION_STR |
"1010" |
Hardware version string returned by V command |
FW_VERSION_STR |
"0101" |
Firmware version string returned by v command |
USB ring-buffer sizes are defined in usb_serial.c (USB_RX_BUF_SIZE = 2048 bytes, USB_TX_BUF_SIZE = 8192 bytes).
If you only require the local USB-to-CAN bridge functionality and do not have a cellular modem on your board, you can check out our dedicated, lightweight release tag:
git checkout slcan-onlyAlternatively, you can run the master branch and simply set BOARD_HAS_MODEM to 0 in app_config.h to cleanly compile out the cellular modules.
The application layer (App/) is hardware-independent. To port this codebase to another STM32 microcontroller or integrate it into a freshly generated CubeMX project, follow this step-by-step guide:
Configure the hardware peripherals in CubeMX exactly as follows:
- System Core → SYS: Set the Timebase Source to
TIM6(or any basic timer other than SysTick) to prevent conflicts between the HAL delay utility and FreeRTOS. - Connectivity → CAN1 (or bxCAN instance):
- Set the operating mode to Master (or Normal/Listen-Only/Loopback).
- Under NVIC Settings, enable the
CAN1 TX,CAN1 RX0, andCAN1 SCE(Status Change Error) interrupts. Interrupt priority should be set to5or lower to prevent kernel conflicts.
- Connectivity → USB_OTG_FS:
- Set Mode to Device_Only.
- Middleware → USB_DEVICE:
- Select class for IP: Communication Device Class (Virtual Port Com).
- In
USB_DEVICE/App/usbd_cdc_if.h, set bothAPP_RX_DATA_SIZEandAPP_TX_DATA_SIZEto2048to define the underlying transmit/receive packet allocation arrays (UserRxBufferFSandUserTxBufferFS) for the USB CDC middleware.
- Middleware → FREERTOS:
- Set Interface to CMSIS_V2 (the codebase utilizes CMSIS-RTOS v2 APIs such as
osThreadNewandosPriorityNormal). - Under Config parameters, ensure Use Static Allocation is set to Enabled.
- Set Interface to CMSIS_V2 (the codebase utilizes CMSIS-RTOS v2 APIs such as
Copy the entire App/ directory into your new project's root folder.
Add the App/ files and include paths to your CMake build configuration:
# Add App sources to the executable target
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
App/usb_serial.c
App/util_ringbuffer.c
App/can_driver.c
App/can_timing.c
App/autobaud.c
App/slcan.c
App/can_bridge.c
App/nvs.c
App/app_cmd.c
)
# Add App include directories
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
App
)Inject the usb_serial callbacks into the CubeMX-generated USB_DEVICE/App/usbd_cdc_if.c file inside the marked USER CODE sections:
- Under
USER CODE BEGIN INCLUDE(line 24-26):#include "usb_serial.h"
- In
CDC_Receive_FS()insideUSER CODE BEGIN 6:/* Hand received data over to the custom RX ring buffer */ usb_serial_rx_callback(Buf, *Len); USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); USBD_CDC_ReceivePacket(&hUsbDeviceFS); return (USBD_OK);
- In
CDC_TransmitCplt_FS()insideUSER CODE BEGIN 13:/* Inform the ring buffer driver that transmission is complete */ usb_serial_tx_complete_callback();
Hook the main task orchestration in your Core/Src/main.c file:
- Under
USER CODE Includes:#include "can_bridge.h"
- Inside the
main()function underUSER CODE BEGIN 2(beforeosKernelInitialize()):/* Initialize static tasks and queues, then start the scheduler */ can_bridge_init();
- Let
osKernelStart()orvTaskStartScheduler()take over. The static CAN bridge tasks will execute automatically.
- To another bxCAN STM32 (F1/F2/F4/F7/L1): Simply update the HAL family header in
can_driver.c. No timing recalculations are needed as the APB1 clock frequency is read dynamically at runtime viaHAL_RCC_GetPCLK1Freq(). - To an FDCAN STM32 (G0/G4/H7/U5): Re-implement
can_driver.cagainst the FDCAN HAL API and adaptcan_timing.cto compute FDCAN register fields. The protocol, ring buffer, and orchestration modules are unchanged.