The engine contract

Everything in this chapter is enforced by the engine's design, not by the compiler. Getting it wrong shows up as rare corruption, not as an error code. The shipped glues (glue/freertos_lwip, glue/freertos_can, glue/baremetal_lwip_raw) honour all of it; keep it when you change them or write your own.

Contexts: rx and poll belong together

qfw_xcp_rx() (or qfw_xcp_rx_can_frame()) and qfw_xcp_poll() must run in the same context — one task — or you serialize them yourself. The FreeRTOS glues do both in their server task; the bare-metal glue in the main loop (the lwIP recv callback and QfwXcpTcpRawGlue_Poll run in one context). qfw_xcp_trigger_event() is the only engine function designed to be called from elsewhere (your data tasks). qfw_xcp_create_event() is an init-time call.

Critical section — must be reentrant

crit_enter / crit_leave protect the engine's shared state (event list, DAQ tables, transmit queue). Engine code paths enter the section while already holding it, so the pair must tolerate nesting: with FreeRTOS a recursive mutex (xSemaphoreCreateRecursiveMutexStatic in the glue) — a plain mutex deadlocks. The section is held briefly (queue push/pop, list updates); keep your own work out of it. The log callback may be invoked inside it, so logging must not block. NULL for both callbacks is allowed only when every engine call, including every qfw_xcp_trigger_event, comes from one single context — that is how the bare-metal glue works: the engine runs in the main loop without a critical section and events are triggered from the same loop, never from an interrupt (see Integration without an RTOS).

Triggering events

qfw_xcp_trigger_event copies the DAQ variables of the event into the transmit queue under the critical section, and applies STIM data before that. From another task: fine with the recursive mutex. From an ISR: not with a FreeRTOS mutex — trigger from the task the ISR wakes. The transmission itself happens later, in the server task's poll, never in the triggering task.

transmit — never block, report backpressure honestly

Called from the poll context, outside the critical section, with one framed transport segment (TCP: one or more LEN+CTR messages back to back — write the bytes to the stream as they are). Rules:

  • return true when the transport accepted the whole segment;
  • return false when it accepted nothing (send buffer full) — the engine keeps the segment and retries it on the next poll; qfw_xcp_is_backpressured() is true meanwhile;
  • a partial write is your problem: the remainder must follow or the TCP stream desynchronizes. The FreeRTOS glue retries a stalled remainder for up to 2 s (GLUE_SEND_STALL_LIMIT_MS), then shuts the socket down and returns true (segment dropped, the master sees the connection go away, the server task recovers to accept); the bare-metal glue never writes partially — it queues the whole segment with one tcp_write or returns false (which is why it requires TCP_SND_BUF of at least one segment);
  • never block for long: a blocking send inside transmit stalls command processing and STIM for everything else.

get_clock — monotonic, microseconds, 64 bit

Used for DAQ timestamps. Must be monotonic across the whole run, 1 µs resolution, never wrap — that is why the clock port widens the DWT counter to 64 bit and needs the periodic Clock64_Update from one writer.

Transmit queue memory

You provide the transmit queue: queue_mem (8-byte aligned, e.g. a static uint64_t[]) and queue_mem_size ≥ QFW_XCP_QUEUE_MEM_SIZE(2). Size it with QFW_XCP_QUEUE_MEM_SIZE(n), n = number of 1480-byte segments; one segment is always the open accumulation buffer, so 2 is the floor. Both TCP glues use 6 (GLUE_QUEUE_SEGMENTS), the CAN glue 3. More segments = more DAQ data buffered across a slow transport; an overflow under load is reported to the master as a transport counter gap (TCP) or the overrun flag (CAN), never as corrupted data.

Log callback

log(level, msg, user) receives one complete line without a newline. The levels follow the XCPlite convention (not the XCP standard): 1 error, 2 warning, 3 info, 4 trace, 5–6 debug; config.log_level gates them at runtime (0 = silent even with a callback). The engine itself never calls printf; the glue's GlueLog prints [XCP E|W|I|T|D] … through printf, which the example retargets to the ST-LINK virtual COM port. The callback may run inside the critical section — a UART with a FIFO is fine, semihosting is not.

Init failure reasons

qfw_xcp_init returns false (and QfwXcpGlue_Init passes it through) when: the config was not initialized with QFW_XCP_CONFIG_DEFAULT (magic / struct-size guard = header and engine sources do not belong together), project_name / epk / transmit / get_clock missing, only one of crit_enter / crit_leave set, queue_mem missing or smaller than QFW_XCP_QUEUE_MEM_SIZE(2), a second init, or the protocol layer refusing the identity strings.

Connection state

qfw_xcp_is_connected() is true between a successful CONNECT and a DISCONNECT or qfw_xcp_on_transport_reset(). The engine does not act on a lost master beyond that: DAQ sampling stops, pending STIM data is no longer applied, memory keeps its last contents. Any application reaction (return of stimulated variables to the internal model, safe outputs) belongs into your task, polling this function. Both TCP glues turn a vanished master into a reset through keepalive (~11 s); the CAN glue can only rely on a regular DISCONNECT or bus-off.

Your own glue (another stack or RTOS)

The engine does not care what drives it. The package ships two FreeRTOS glues (lwIP sockets, FDCAN) and one without an RTOS (lwIP RAW API); for another TCP/IP stack, another RTOS or another byte transport, write your own — glue/freertos_lwip/xcp_sdk_glue.c (230 lines) is the reference, glue/baremetal_lwip_raw/xcp_tcp_raw_glue.c for an integration without threads. What stays and what changes:

Stays the sameChanges
qfw_xcp.h, the engine sources, cmake/qfw_xcp.cmakethe transport calls (lwip_* → your stack)
the four callbacks (transmit, get_clock, crit_enter/leave, log) and the queue memoryhow the critical section is implemented (a recursive mutex of your RTOS)
the three engine entries qfw_xcp_rx / qfw_xcp_poll / qfw_xcp_on_transport_reset and the contract abovehow the server task is created and how it waits (a 1–10 ms receive timeout, or a queue with timeout), or a poll from the main loop without a task
your integration file and the three calls in freertos.cthe glue's config struct and Qfw…Glue_Init name, if you want them different

Minimum body of a server task:

for (;;) {
    /* on a new connection: qfw_xcp_on_transport_reset(); */
    n = receive_with_timeout(buf, sizeof buf, 1 /* ms */);
    if (n > 0 && !qfw_xcp_rx(buf, n)) { reset_connection(); }   /* false = stream desynchronized */
    (void)qfw_xcp_poll();                                        /* responses + DAQ out */
    /* connection lost: close, qfw_xcp_on_transport_reset(), back to accept */
}

qfw_xcp_on_transport_reset() after every connect and disconnect is what makes the master's next CONNECT start from a clean state.

The version string

  • qfw_xcp_version() returns "QFW-XCP <semver> (<git>)" — the SDK version and the git revision of the package the engine sources came from (a plain source build outside cmake/qfw_xcp.cmake says (dev)). The same provenance is in include/qfw_xcp_version.h (QFW_XCP_SDK_*). Quote the string in every support request.
  • EPK (epk in the glue config) is the XCP identification string of your software version, returned to the master by GET_ID. QInsight does not evaluate it; it is informative. Recommended: your firmware version, at most 31 characters, no spaces or colons (e.g. V100, fw-2.3.1).
  • There is no license mode: the engine runs without a time limit and needs no key. Your use of the sources is governed by the QENEX Software License Agreement in LICENSES/.

Next

Variable addresses for QInsight — from the ELF to the QInsight project.