XCP on classic CAN — what changes
The example builds the CAN image with the preset DebugCan; everything in
this chapter is what that preset changes compared to Debug (the TCP
image described in Integration: XCP on TCP).
The ST peripheral is called FDCAN; the SDK uses it in classic CAN
mode (8-byte frames, no bit-rate switching) — from here on: classic CAN.
Same and different
| Same as TCP | Different on CAN |
|---|---|
the engine sources and qfw_xcp.h | the transport codec compiled into the engine: qfw_xcptl_can.c instead of qfw_xcptl.c (selected by QFW_XCP_TRANSPORT=can, no framing on the wire, MAX_CTO = MAX_DTO = 8) |
| the clock port and the clock contract | the glue: glue/freertos_can/xcp_can_glue.c instead of glue/freertos_lwip/xcp_sdk_glue.c |
identity (project_name, epk), log, critical section, queue memory | the port: port/fdcan (FDCAN wrapper) in addition to port/clock; lwIP is not needed |
events: qfw_xcp_create_event / qfw_xcp_trigger_event, daqId in QInsight | the glue config: tQfwXcpCanGlueConfig (master_id, slave_id, extended_ids, bitrate_kbps) instead of port |
the three calls in freertos.c | the receive entry: qfw_xcp_rx_can_frame(data, dlc) instead of qfw_xcp_rx(bytes, len) |
| the variable rules and the address tools | #if defined(QFW_XCP_TRANSPORT_CAN) in the integration file selects the glue header and config |
One firmware image carries exactly one transport (the two engine variants collide on their symbols by design).
Build selection
cmake --preset DebugCan sets QFW_XCP_TRANSPORT=can (CMakePresets.json);
in your own project pass -DQFW_XCP_TRANSPORT=can at configure time. The
CMake snippet of the TCP chapter then adds
${QFW_XCP_GLUE_FREERTOS_CAN_SOURCES}, ${QFW_XCP_PORT_FDCAN_SOURCES},
the matching include directories and the definitions
QFW_XCP_TRANSPORT_CAN (for your integration file) and
${QFW_XCP_PORT_FDCAN_DEFINES} (= HAL_FDCAN_MODULE_ENABLED).
The FDCAN port and your CubeMX project
port/fdcan/fdcan_classic.c brings up FDCAN1 completely in code: FDCAN
kernel clock from HSE (8 MHz on the Nucleo, the bit-timing table is
computed for it), GPIO PD0 = FDCAN1_RX / PD1 = FDCAN1_TX (AF9), classic
frame format, message RAM layout (32-deep RX FIFO0, 16-deep TX FIFO), one
acceptance filter passing only the master id into RX FIFO0 (everything
else and all remote frames rejected), polled operation without interrupts.
API: FdcanClassic_Init(&cfg), FdcanClassic_Receive(data, &dlc),
FdcanClassic_Transmit(data, len), FdcanClassic_RecoverBusOff().
Supported bitrates: 125, 250, 500, 1000 kbit/s.
port/fdcan/hal/ contains the ST HAL FDCAN driver (STM32Cube FW_H7
V1.13.0, verbatim, BSD-3-Clause, see port/fdcan/ORIGIN.md). The example
does not enable FDCAN in its .ioc: the peripheral is brought up
entirely by FdcanClassic_Init (clock, GPIO, filter, message RAM), and the
HAL driver comes from port/fdcan/hal because CubeMX does not copy it into
Drivers/ for a peripheral that is not enabled. Follow the same pattern in
your project: leave FDCAN out of the .ioc, add
port/fdcan/fdcan_classic.c and port/fdcan/hal/stm32h7xx_hal_fdcan.c to
your target with the include directories and HAL_FDCAN_MODULE_ENABLED
(the variables QFW_XCP_PORT_FDCAN_SOURCES,
QFW_XCP_PORT_FDCAN_INCLUDE_DIRS, QFW_XCP_PORT_FDCAN_DEFINES from
cmake/qfw_xcp.cmake do exactly that, see the example's
CMakeLists.txt). This is the configuration verified on the NUCLEO-H723ZG
with QInsight.
Integration file and glue
The CAN branch of the example's app/xcp_server.c:
#if defined(QFW_XCP_TRANSPORT_CAN)
#include "xcp_can_glue.h"
#define QFW_XCP_CAN_MASTER_ID 0x200U /* QInsight default: master -> slave */
#define QFW_XCP_CAN_SLAVE_ID 0x201U /* slave -> master */
#define QFW_XCP_CAN_BITRATE_KBPS 500U
#endif
...
tQfwXcpCanGlueConfig config = {
.project_name = QFW_XCP_PROJECT_NAME,
.epk = QFW_XCP_EPK,
.master_id = QFW_XCP_CAN_MASTER_ID,
.slave_id = QFW_XCP_CAN_SLAVE_ID,
.extended_ids = false, /* 11-bit identifiers */
.bitrate_kbps = QFW_XCP_CAN_BITRATE_KBPS,
.log_level = 2U,
.get_clock_us = QfwXcpClockUs,
};
if (!QfwXcpCanGlue_Init(&config))
{
return false;
}
QfwXcpCanGlue_Init (glue/freertos_can/xcp_can_glue.c) creates the
recursive mutex, initializes the engine (3 queue segments), calls
FdcanClassic_Init with rx_id = master_id, tx_id = slave_id, and
creates the server task QfwXcpCanGlue (4 KB stack, priority idle+2). The
task loop runs every 1 ms:
drain RX FIFO0 -> qfw_xcp_rx_can_frame() per frame (one frame = one XCP packet, no framing)
qfw_xcp_poll() -> GlueTransmit -> FdcanClassic_Transmit (one call = one frame on slave_id, len 8, zero padded)
FdcanClassic_RecoverBusOff() true? -> qfw_xcp_on_transport_reset() (the only transport reset on CAN)
vTaskDelay(1)
There is no connection and no byte stream: a malformed frame is dropped by
the engine and nothing needs to be resynchronized; a full TX FIFO is clean
backpressure (transmit returns false, the engine retries the frame on
the next poll). The 32-deep RX FIFO holds more than 8 ms of a fully loaded
500 kbit/s bus, so the 1 ms cadence has margin.
Wiring (NUCLEO-H723ZG)
FDCAN1 uses PD0 = FDCAN1_RX and PD1 = FDCAN1_TX (AF9). The Nucleo has no CAN transceiver — connect an external 3.3 V transceiver (e.g. SN65HVD230, TJA1051T/3): its TXD to PD1, RXD to PD0, VCC 3.3 V, common GND, CANH/CANL to the bus with 120 Ω termination at both cable ends (the PCAN-USB side usually provides one).
QInsight side
Driver PEAK CAN Adapter (PCAN-USB;
bitrate=500000 (bit/s), deviceId = the ID of your adapter), protocol
XCP on CAN with masterId=0x200,
slaveId=0x201, extendedIds=false. Variables, events and daqId are
configured exactly as on TCP. Values longer than one frame are chained by
QInsight automatically: reads in pieces of at most 7 B (SHORT_UPLOAD,
then UPLOAD), writes as SET_MTA followed by DOWNLOADs of at most
6 B — an 8-byte value is read as 7+1 B and written as 6+2 B; with a
block-mode slave an 8-byte value is one UPLOAD burst of 7+1 B. The example project
Qenex_CAN_XCP_NucleoH723_Maps_FreeRTOS.qproj is described under
XCP on CAN on a NUCLEO-H723ZG.
Sizing and throughput
- Transmit queue: 2–4 segments (
QFW_XCP_QUEUE_MEM_SIZE(3)= 4632 B is the glue default). CAN carries at most 6 payload bytes per DAQ frame at ~4000 frames/s (500 kbit/s, whole bus) — a larger queue only deepens the backlog, it cannot raise the throughput. - DAQ capacity per frame: the identification field is 2 bytes (ODT +
DAQ number); with slave timestamps (
daqTimestamps=slave) the first ODT of a list additionally carries a DWORD timestamp → 2 B data in ODT0, 6 B in every other ODT. WithdaqTimestamps=masterall ODTs carry 6 B. - Overrun indication: a transmit queue overflow (easy to reach on CAN with an ambitious DAQ set) is reported per the XCP standard by setting the MSB of the ODT number in the first DTO after the overflow — QInsight shows the losses; whole frames are dropped, data is never corrupted.
- Not implemented (optional per the standard, unused by QInsight):
GET_SLAVE_ID,GET_DAQ_ID,SET_DAQ_ID(ERR_CMD_UNKNOWN).
Next
The engine contract — the rules behind the glue; Troubleshooting — CAN rows in the symptom table.

