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
truewhen the transport accepted the whole segment; - return
falsewhen 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 returnstrue(segment dropped, the master sees the connection go away, the server task recovers toaccept); the bare-metal glue never writes partially — it queues the whole segment with onetcp_writeor returnsfalse(which is why it requiresTCP_SND_BUFof at least one segment); - never block for long: a blocking send inside
transmitstalls 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 same | Changes |
|---|---|
qfw_xcp.h, the engine sources, cmake/qfw_xcp.cmake | the transport calls (lwip_* → your stack) |
the four callbacks (transmit, get_clock, crit_enter/leave, log) and the queue memory | how 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 above | how 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.c | the 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 outsidecmake/qfw_xcp.cmakesays(dev)). The same provenance is ininclude/qfw_xcp_version.h(QFW_XCP_SDK_*). Quote the string in every support request.- EPK (
epkin the glue config) is the XCP identification string of your software version, returned to the master byGET_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.

