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 TCPDifferent on CAN
the engine sources and qfw_xcp.hthe 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 contractthe 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 memorythe 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 QInsightthe glue config: tQfwXcpCanGlueConfig (master_id, slave_id, extended_ids, bitrate_kbps) instead of port
the three calls in freertos.cthe 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. With daqTimestamps=master all 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.