STM32G474 CAN FD Configuration and Validation with Jetson Xavier
This guide walks through a complete CAN FD setup on STM32G474 (FDCAN1/2/3) using STM32CubeMX and MDK-ARM, adds minimal runtime code for UART logging, periodic scheduling, and error recovery, and verifies operation with a Jetson Xavier running SocketCAN. The example targets 500 kbit/s nominal, 2 Mbit/s data phase with BRS enabled. LPUART1 is used for high-speed logging via ST-LINK VCP.
Hardware notes:
- Use transceivers that support CAN FD at ≥2 Mbit/s.
- Connect LPUART1 to host PC through ST-LINK (VCP) or a USB-UART bridge.
- Assign FDCAN pins per your board (examples below use PD0/PD1 for FDCAN1).
Project Creation in STM32CubeMX
- MCU selection: STM32G474VETx.
- Debug: System Core > SYS > Debug = Serial Wire (SWD).
- Clock: System Core > RCC > HSE = Crystal/Ceramic Resonator.
- TIM6 (100 µs tick): Timers > TIM6 > Activated, Prescaler = 160-1, Period = 100-1; enable TIM6 interrupt.
- LPUART1: Connectivity > LPUART1 > Asynchronous, disable "Overrun" and "DMA on RX Error". Set 2,000,000 baud, 8-N-1.
- FDCAN1: Connectivity > FDCAN1
- Mode = FD, FD mode with bit rate switching = Enabled
- Auto Retransmission = Enabled, Transmit Pause = Enabled
- Nominal timing (arbitration) = 500 kbit/s; example: 160 MHz / 4 / (1 + 63 + 16); sample point ≈ 0.8
- Data timing = 2 Mbit/s; sample point ≈ 0.75
- RX filters: set a few or max (e.g., 28 STD, 8 EXT) as needed
- Enable FDCAN1 interrupt line 0
- Reassign pins to board pins (e.g., PD0/PD1)
- Repeat comparable settings for FDCAN2 and FDCAN3 with appropriate pins.
- Project Manager
- Toolchain/IDE = MDK-ARM
- Heap = 0x1000, Stack = 0x1000 (or larger)
- Code Generator: "Copy only the necessary library files" and "Generate peripheral initialization as a pair of .c/.h files per peripheral"
- Generate and open the project.
MDK-ARM configuraton (ST-LINK by default):
- Flash Download: enable "Reset and Run".
- Packs tab: uncheck unneeded Packs if desired.
UART Printf Retarget (LPUART1)
Enable standard I/O printing through LPUART1 by providing a _write implementation:
#include <unistd.h>
#include "main.h"
extern UART_HandleTypeDef hlpuart1;
int _write(int fd, const char *buf, int len)
{
(void)fd;
HAL_UART_Transmit(&hlpuart1, (uint8_t*)buf, (uint16_t)len, 0xFFFF);
return len;
}
100 µs Time Base (TIM6)
Use a periodic flag for light-weight scheduling. The example below starts TIM6 in interrupt mode and toggles a flag in the callback.
/* Start timer */
HAL_TIM_Base_Start_IT(&htim6);
volatile uint8_t g_tick100us = 0;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM6) {
g_tick100us = 1;
}
}
Rationale: At 64-byte frames in BRS mode, keeping a ~500 µs spacing between frames is conservative. A 100 µs tick gives a simple retry window.
FDCAN Filters, Notifications, and TDC
The helper below configures a controller to accept all standard and extended frames into RX FIFO0, enables notifications for RX FIFO0 new message and bus-off, and turns on Tx Delay Compensation for BRS.
static void CANFD_Config(FDCAN_HandleTypeDef *h)
{
FDCAN_FilterTypeDef flt = {0};
/* Accept all 11-bit IDs */
flt.IdType = FDCAN_STANDARD_ID;
flt.FilterIndex = 0;
flt.FilterType = FDCAN_FILTER_MASK;
flt.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
flt.FilterID1 = 0x000; /* ID mask */
flt.FilterID2 = 0x000; /* accept all */
if (HAL_FDCAN_ConfigFilter(h, &flt) != HAL_OK) {
Error_Handler();
}
/* Accept all 29-bit IDs */
flt.IdType = FDCAN_EXTENDED_ID;
flt.FilterIndex = 0;
flt.FilterType = FDCAN_FILTER_MASK;
flt.FilterConfig = FDCAN_FILTER_TO_RXFIFO0;
flt.FilterID1 = 0x00000000U; /* ID mask */
flt.FilterID2 = 0x00000000U; /* accept all */
if (HAL_FDCAN_ConfigFilter(h, &flt) != HAL_OK) {
Error_Handler();
}
/* Global filter: reject remote frames, accept matching */
if (HAL_FDCAN_ConfigGlobalFilter(h, FDCAN_REJECT, FDCAN_REJECT,
FDCAN_FILTER_REMOTE, FDCAN_FILTER_REMOTE) != HAL_OK) {
Error_Handler();
}
/* Notifications */
if (HAL_FDCAN_ActivateNotification(h, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0) != HAL_OK) {
Error_Handler();
}
if (HAL_FDCAN_ActivateNotification(h, FDCAN_IT_BUS_OFF, 0) != HAL_OK) {
Error_Handler();
}
/* Tx Delay Compensation for BRS */
HAL_FDCAN_ConfigTxDelayCompensation(h, h->Init.DataPrescaler * h->Init.DataTimeSeg1, 0);
HAL_FDCAN_EnableTxDelayCompensation(h);
HAL_FDCAN_Start(h);
}
Call this after each MX_FDCANx_Init().
Bus-Off Recovery
Reinitialize and reapply configuration if the controller enters bus-off. Keep ISR logic minimal.
void HAL_FDCAN_ErrorStatusCallback(FDCAN_HandleTypeDef *h, uint32_t it)
{
(void)it;
if (h->Instance == FDCAN1) {
MX_FDCAN1_Init();
CANFD_Config(&hfdcan1);
} else if (h->Instance == FDCAN2) {
MX_FDCAN2_Init();
CANFD_Config(&hfdcan2);
} else if (h->Instance == FDCAN3) {
MX_FDCAN3_Init();
CANFD_Config(&hfdcan3);
}
}
Bus-off can occur if CANH/CANL are shorted or node timing is incompatible.
Receive Path and UART Dump
Map DLC to payload length for printing. Avoid heavy prints in ISR for production; they are shown here for demonstration only.
static const uint8_t kDlcBytes[16] = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64
};
static inline uint8_t dlc_to_len(uint32_t dlc_field)
{
/* HAL encodes DLC in bits 16..19 */
return kDlcBytes[(dlc_field >> 16) & 0xF];
}
static FDCAN_RxHeaderTypeDef g_rxh;
static uint8_t g_rxbuf[64];
void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *h, uint32_t it)
{
if ((it & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) == 0) {
return;
}
if (HAL_FDCAN_GetRxMessage(h, FDCAN_RX_FIFO0, &g_rxh, g_rxbuf) != HAL_OK) {
return;
}
const uint8_t n = dlc_to_len(g_rxh.DataLength);
const char brs = (g_rxh.BitRateSwitch == FDCAN_BRS_ON) ? 'B' : '-';
const char esi = (g_rxh.ErrorStateIndicator == FDCAN_ESI_ACTIVE) ? 'E' : '-';
printf("0x%08lX, %02u, %c, %c:", (unsigned long)g_rxh.Identifier, (unsigned)n, brs, esi);
for (uint8_t i = 0; i < 64; ++i) {
printf(" %02X", g_rxbuf[i]);
}
printf("\r\n");
}
Transmit with BRS/ESI and 100 µs Retry Window
A generic transmit helper attempts to queue a frame. On failure, it caches the frame for a single retry on a later 100 µs tick.
#include <string.h>
typedef struct {
uint8_t pending;
FDCAN_TxHeaderTypeDef hdr;
uint8_t data[64];
} RetryCache;
static RetryCache s_retry1 = {0};
static RetryCache s_retry2 = {0};
static RetryCache s_retry3 = {0};
static void canfd_send_or_cache(FDCAN_HandleTypeDef *h, RetryCache *rc,
uint32_t can_id, uint32_t dlc_enum, const uint8_t *payload)
{
FDCAN_TxHeaderTypeDef th = {0};
th.Identifier = can_id;
th.IdType = (can_id < 0x800U) ? FDCAN_STANDARD_ID : FDCAN_EXTENDED_ID; /* heuristic, not universally valid */
th.TxFrameType = FDCAN_DATA_FRAME;
th.DataLength = dlc_enum; /* e.g., FDCAN_DLC_BYTES_64 */
th.ErrorStateIndicator = FDCAN_ESI_ACTIVE;
th.BitRateSwitch = FDCAN_BRS_ON;
th.FDFormat = FDCAN_FD_CAN;
th.TxEventFifoControl = FDCAN_NO_TX_EVENTS;
th.MessageMarker = 0;
if (HAL_FDCAN_AddMessageToTxFifoQ(h, &th, (uint8_t*)payload) != HAL_OK) {
rc->pending = 1;
rc->hdr = th;
memcpy(rc->data, payload, dlc_to_len(dlc_enum));
}
}
static void canfd_retry_cached(FDCAN_HandleTypeDef *h, RetryCache *rc)
{
if (!rc->pending) return;
if (HAL_FDCAN_AddMessageToTxFifoQ(h, &rc->hdr, rc->data) == HAL_OK) {
rc->pending = 0;
}
}
Scheduler example: send three frames every ~500 µs, with a 10 ms cycle, stopping after 6000 frames total.
int main(void)
{
/* ... HAL_Init, clocks, MX_* init calls ... */
MX_LPUART1_Init();
MX_FDCAN1_Init();
MX_FDCAN2_Init();
MX_FDCAN3_Init();
CANFD_Config(&hfdcan1);
CANFD_Config(&hfdcan2);
CANFD_Config(&hfdcan3);
HAL_TIM_Base_Start_IT(&htim6);
static uint8_t tx1[64], tx2[64], tx3[64];
for (uint8_t i = 0; i < 64; ++i) {
tx1[i] = i;
tx2[i] = i;
tx3[i] = i;
}
uint32_t tick_cnt = 0;
uint32_t half_ms_slots = 0;
uint32_t sent_groups = 0; /* 6 frames per 10 ms */
while (1) {
if (g_tick100us) {
g_tick100us = 0;
++tick_cnt; /* 100 us resolution */
/* retry once per 100 us window */
canfd_retry_cached(&hfdcan1, &s_retry1);
canfd_retry_cached(&hfdcan2, &s_retry2);
canfd_retry_cached(&hfdcan3, &s_retry3);
if ((tick_cnt % 5U) == 0U) { /* every 500 us */
++half_ms_slots;
/* decorate payload with a counter */
tx1[0] = (uint8_t)((half_ms_slots >> 8) & 0xFF);
tx1[1] = (uint8_t)(half_ms_slots & 0xFF);
switch (half_ms_slots) {
case 1:
canfd_send_or_cache(&hfdcan1, &s_retry1, 0x123U, FDCAN_DLC_BYTES_64, tx1);
canfd_send_or_cache(&hfdcan1, &s_retry1, 0x124U, FDCAN_DLC_BYTES_64, tx1);
canfd_send_or_cache(&hfdcan1, &s_retry1, 0x125U, FDCAN_DLC_BYTES_64, tx1);
break;
case 4:
canfd_send_or_cache(&hfdcan1, &s_retry1, 0x12345678U, FDCAN_DLC_BYTES_64, tx1);
break;
case 5:
canfd_send_or_cache(&hfdcan1, &s_retry1, 0x12345679U, FDCAN_DLC_BYTES_64, tx1);
break;
case 6:
canfd_send_or_cache(&hfdcan1, &s_retry1, 0x1234567AU, FDCAN_DLC_BYTES_64, tx1);
break;
case 20: /* 10 ms period */
half_ms_slots = 0;
++sent_groups;
break;
default:
break;
}
if (sent_groups >= 1000U) {
/* stop after 1000 cycles (~10 s, 6 frames per cycle) */
while (1) { /* idle */ }
}
}
}
}
}
Notes:
- DataLength is an encoded DLC enum (e.g., FDCAN_DLC_BYTES_64), not the raw byte count.
- The can_id < 0x800 test is just a convenience; do not rely on it for general protocols.
- The HAL TX FIFO can hold multiple frames; bursts of up to three frames work well in this example.
Jetson Xavier (SocketCAN) Setup
Bring up CAN FD with the same nominal and data timing. Example for can0 and can1:
#!/bin/sh
modprobe can
modprobe can_raw
modprobe mttcan
ip link set can0 down
ip link set can0 type can bitrate 500000 sample-point 0.8 dbitrate 2000000 dsample-point 0.75 fd on restart-ms 100
ip link set can0 up
ifconfig can0 txqueuelen 1000
ip link set can1 down
ip link set can1 type can bitrate 500000 sample-point 0.8 dbitrate 2000000 dsample-point 0.75 fd on restart-ms 100
ip link set can1 up
ifconfig can1 txqueuelen 1000
Check status:
ip -details -statistics link show can1
Example output (values will vary):
6: can1: <NOARP,UP,LOWER_UP,ECHO> mtu 72 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 1000
link/can promiscuity 0
can <FD> state ERROR-ACTIVE (berr-counter tx 0 rx 0) restart-ms 100
bitrate 500000 sample-point 0.800
tq 25 prop-seg 31 phase-seg1 32 phase-seg2 16 sjw 1
mttcan: tseg1 2..255 tseg2 0..127 sjw 1..127 brp 1..511 brp-inc 1
dbitrate 2000000 dsample-point 0.750
dtq 25 dprop-seg 6 dphase-seg1 7 dphase-seg2 5 dsjw 1
mttcan: dtseg1 1..31 dtseg2 0..15 dsjw 1..15 dbrp 1..15 dbrp-inc 1
clock 38400000
re-started bus-errors arbit-lost error-warn error-pass bus-off
0 0 0 0 0 0
RX: bytes packets errors dropped overrun mcast
0 0 0 0 0 0
TX: bytes packets errors dropped carrier collsns
0 0 0 0 0 0
Capturing with candump
Connect STM32 FDCAN1 to Xavier can1 (or adjust as wired). Then:
candump -ta -x can1 > log_fd.dat
Sample lines while the STM32 example runs (B = BRS, E = ESI):
(1710000000.100000) can1 RX B - 123 [64] 00 00 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
(1710000000.100500) can1 RX B - 124 [64] 00 00 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
(1710000000.101000) can1 RX B - 125 [64] 00 00 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
(1710000000.101500) can1 RX B E 12345678 [64] 00 00 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
Inetrpretation:
- The ID sequence and payloads should repeat with a ~500 µs spacing within each 10 ms cycle.
- The flags reflect BRS (B) and ESI (E). ESI can vary depending on the sender’s error state configuration.
Xavier TX Script (FD, BRS+ESI)
The "3" after "##" enables FD with BRS and ESI (per can-utils). This script emits six 64-byte FD frames every 10 ms.
#!/bin/sh
while true; do
cansend can1 18FF0001##3.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16
cansend can1 18FF0002##3.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16
cansend can1 18FF0003##3.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16
cansend can1 18FF0004##3.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16
cansend can1 18FF0005##3.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16
cansend can1 18FF0006##3.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16.01.02.03.04.05.06.07.08.09.10.11.12.13.14.15.16
sleep 0.01
done
You can observe received frames on the STM32 through the LPUART1 logs (as implemented in the RX callback) or with candidump on Xavier.
Notes on Classic CAN
If you need classical CAN (non-FD):
- In CubeMX, set the mode to Classic (not FD Master/Slave) for the instance.
- Keep the nominal timing at 500 kbit/s for both arbitration and (single) data phase (FD data timing is unused).