BKPT LabsDOCS/VIEWALYZER RECORDER/TRANSPORTS AND SNAPSHOTS VIEWALYZER · FIRMWARE RECORDER
VIEWALYZER · RECORDER API

Transports and snapshots

The recorder writes one packet stream; VA_TRANSPORT picks how it leaves the chip, and the host's connection file (.vacf) names the matching reader. This page pairs them up and covers the post-mortem ring.

Which transport

VA_TRANSPORT NEEDS ON THE TARGET HOST TRANSPORT (.VACF) THROUGHPUT WHEN
RAM_BUFFER (default)8 KB ring + 2 KB metadata table, descriptors and service state by default; any Cortex-Mstlink-rambuf, jlink-rambufLimited by the probe's SWD memory reads and the SWD clock; the throughput/ examples measure it per probeEvery board; no pins beyond SWD; the only choice on M0/M0+/M23
ARM_ITMAn ITM and the SWO pin wired to the probe (Cortex-M3/M4/M7/M33)stlink-swo, jlink-swoThe SWO rate: 2 MHz is 200 KB/s, a STLINK-V3 takes up to 24 MHzHighest bandwidth, and the only transport that also carries hardware trace (PC samples, exception trace, data watches) on the same pin
JLINK_RTTSEGGER RTT (SEGGER_RTT.c in the build)stlink-rtt, jlink-rttSimilar to the RAM bufferProjects that already use RTT for logging
CUSTOM_TRANSPORTA byte pipe of your own (UART, USB CDC, radio) and core/viewalyzer_cobs.cserial, udpYour link's rateNo debug probe at run time, or a remote target

Every probe transport also takes --target-device, --speed-khz and, with several probes attached, --stlink-serial or --jlink-serial.

RAM buffer

#define VA_TRANSPORT    RAM_BUFFER
#define VA_RAMBUF_SIZE  16384u
#define VA_RAMBUF_MODE  VA_RAMBUF_MODE_DROP     /* DROP | BLOCK | WRAP */

The probe reads the event ring over SWD while the core runs. Default DROP-mode capture uses RAM metadata: supply the matching ELF to a compatible ViewAlyzer-RS host. The host reads current definitions from RAM and requests a checkpoint, so attachment does not need periodic full setup bundles. With VA_METADATA=0, ring discovery can also use a RAM scan:

{
  "transport": "stlink-rambuf",
  "target-device": "STM32G474RE",
  "speed-khz": 24000,
  "rambuf-scan-start": "0x20000000",
  "rambuf-scan-size": 131072
}
$ viewalyzer-cli capture --config nucleo_g474_rambuf.vacf --elf build/rambuf/Nucleo_G474_VA.elf --no-reset --output run.vadb --duration 10

Modes: DROP loses the newest packet when the ring is full (the sequence byte tells the host how many); BLOCK spins in the writer until the probe drains, which keeps every event at the cost of timing; WRAP overwrites the oldest, turning the ring into a post-mortem window (below). Metadata defaults off in BLOCK/WRAP modes and with a snapshot tee. On cached cores, place the ring, control blocks and metadata in probe-visible, non-cacheable memory using VA_RAMBUF_ATTRIBUTES and matching linker/MPU configuration.

ITM over SWO

#define VA_TRANSPORT  ARM_ITM
#define VA_ITM_PORT   1                          /* port 0 stays free for printf */

The host programs the ITM, the TPIU and the trace clock through the probe on connect (init-swo, on by default), so the firmware does not touch the trace registers. The two sides must agree on the core clock and the SWO rate:

{
  "transport": "stlink-swo",
  "target-device": "STM32G474RE",
  "cpu-clock-hz": 170000000,
  "swo-freq-hz": 2000000,
  "itm-port": 1
}

The rate is bounded by the probe's receiver (2 MHz on a V2 ST-LINK and a J-Link over probe-rs, 24 MHz on a STLINK-V3) and must divide the core clock in whole numbers. When nobody drains the pin the ITM FIFO fills; the recorder detects the stall and drops rather than blocks, then re-arms from VA_TickOverflowCheck() once a host appears. Hardware trace (--dwt, --dwt-pc, --dwt-watch, the hardware-trace block) rides the same pin; see the CLI reference.

RTT

#define VA_TRANSPORT     JLINK_RTT
#define VA_RTT_CHANNEL   0
#define VA_CONFIGURE_RTT 1                       /* the recorder sizes the channel itself */
{ "transport": "stlink-rtt", "target-device": "STM32F446RE", "rtt-channel": 0 }

The host finds _SEGGER_RTT in RAM (or --rtt-address / --elf pins it) and drains the up-channel named ViewAlyzer, else the configured index. Works over an ST-LINK too: the RTT control block is ordinary memory.

Custom transport

Apply the transport and buffering definitions to every recorder translation unit.

#define VA_TRANSPORT CUSTOM_TRANSPORT
#define VA_TRANSPORT_BUFFERED 1

static uint32_t uart_send(const uint8_t *data, uint32_t length)
{
    return uart_tx_try_copy(data, length);
}

VA_RegisterTransportSend(uart_send);       /* before VA_Init */
VA_Init(SystemCoreClock);

Packets arrive COBS-framed and zero-delimited (core/viewalyzer_cobs.c does the framing); ship each buffer as is. On the host, the serial transport reads a port, udp listens on a socket; both expect COBS ("cobs": true by default):

Implement uart_tx_try_copy to copy as many bytes as its transmit queue can accept immediately and return that count. Call VA_Drain() regularly from an unmasked thread or main loop. The data pointer expires on return; DMA must use your own copied buffer. Buffered callback boundaries are byte-stream chunks, not packet or datagram boundaries. A datagram transport must assemble complete COBS frames before sending them. See Scheduling the drain for the FreeRTOS hook, Zephyr thread and stack requirements.

Direct custom callbacks run during serialized packet emission and must not call recorder APIs or services that trigger tracing. Buffered callbacks run outside the recorder critical section and may produce trace events.

{ "transport": "serial", "serial-port": "COM7", "baud": 921600 }
{ "transport": "udp", "udp-port": 17200 }

Buffered mode

VA_TRANSPORT_BUFFERED=1 stages whole packets in a RAM ring (VA_BUFFER_SIZE, a power of two of at least 64 bytes). Each VA_Drain() offers at most VA_DRAIN_MAX_BYTES bytes (default 256) and retains unaccepted data for the next call. ISR, interrupt-masked and overlapping drains do nothing. Custom writers must return promptly; a callback that blocks cannot be bounded by the recorder.

Applies to ITM, RTT and custom transports. Reserve the RTT up-channel exclusively for the recorder. Buffered RTT checks available space before writing, even if the channel has blocking or trimming flags. Do not reset or reconfigure the channel during a drain. RAM_BUFFER cannot be combined with buffered mode.

VA_GetBufferStats() returns queue occupancy and cumulative, saturating loss totals. DROP: string events report new losses when space becomes available, including with string tracing disabled. Buffered output is unsuitable for fused ETM/ITM timing correlation because transmission is delayed.

Scheduling the drain

Draining is application-owned. The recorder and its RTOS adapters do not create a drain task or install an idle hook that drains output. Trace hooks enqueue packets in buffered mode; a separate call to VA_Drain() services the transport.

CONFIGURATION WHO DRAINS THE OUTPUT APPLICATION CALLS VA_DRAIN()?
RAM_BUFFER (default)The host reads target RAM through the debug probeNo
ITM, RTT or custom with VA_TRANSPORT_BUFFERED=0Recorder calls write directly to the selected transportNo
ITM, RTT or custom with VA_TRANSPORT_BUFFERED=1An application loop, idle hook or service taskYes, regularly

Each call is bounded; call again to send more. VA_TickOverflowCheck() handles timestamp/setup housekeeping and does not replace VA_Drain(). Choose the service frequency for your event rate and transport capacity. If servicing falls behind, the staging ring fills and new packets are dropped.

FreeRTOS idle hook

Enable FreeRTOS's existing idle callback in FreeRTOSConfig.h:

#define configUSE_IDLE_HOOK 1

In an application source file that includes ViewAlyzer.h:

void vApplicationIdleHook(void)
{
    VA_Drain();
}

FreeRTOS calls this hook automatically when the idle task runs. If your application already defines it, add the drain call to that implementation; keep a single hook definition. The hook and custom send callback must not block.

Idle draining depends on spare CPU time. If higher-priority tasks keep the CPU busy, use an application service task with enough scheduled execution time to handle the trace rate. Call VA_Drain() from that task's normal unmasked loop; there is no requirement to use idle. Do not call it from kernel trace hooks, ISRs or critical sections.

Zephyr and bare metal

For Zephyr, enable CONFIG_VIEWALYZER_BUFFERED=y with ITM or RTT and call VA_Drain() regularly from an application thread. The module does not create a worker. For bare metal, put the call in the main loop after initialization. In both cases, the caller must be outside an ISR with interrupts unmasked. Keep unrelated application work and housekeeping in the loop as needed.

Stack ownership

VA_Drain() and the custom send callback use the calling task's stack. With FreeRTOS idle draining, budget that work in the idle task; with a service task, budget it there. Packet construction and trace hooks still consume stack in producer contexts, so buffering does not remove producer stack costs.

Size stacks for the selected features, name/log limits, callback and compiler build settings, and check high-water marks under load in Debug and Release. The G474 UART example allocates 128 StackType_t words (512 bytes on that Cortex-M4) to idle, including buffered mode. That is an example allocation, not a universal recorder minimum or an amount to add to every task. FreeRTOS's configMINIMAL_STACK_SIZE uses words; ensure a custom static idle-stack allocation and its reported size agree.

VA_DRAIN_MAX_BYTES limits output work per call; it does not set a task stack size. VA_BUFFER_SIZE allocates the staging ring in static RAM, separately from these stack requirements.

Custom transports use one static COBS encoding workspace, shared by serialized packet emission. It occupies VA_MAX_PACKET_SIZE + VA_MAX_PACKET_SIZE / 254 + 2 bytes (111 bytes with default limits), separately from the staging ring. Packet builders still use the producer's stack; a larger log limit increases the string builder's stack requirement. Buffered drains send reserved ring spans directly and do not allocate a second payload copy on the caller's stack.

Post-mortem snapshots

Two ways to keep the last events across a crash:

  • VA_RAMBUF_MODE_WRAP: the live RAM buffer itself overwrites the oldest packets, so it always holds the most recent window.
  • VA_SNAPSHOT=1: a second ring (VA_SNAPSHOT_SIZE, with VA_SNAPSHOT_SETUP_SIZE reserved for the names) alongside any live transport.

In both cases call VA_SnapshotFreeze() from the fault or assert handler; the ring stops and the window is preserved through the handler, a reset loop, or a halted core. The host reads it without resetting the target:

$ viewalyzer-cli snapshot --config nucleo_g474_rambuf.vacf --elf firmware.elf --output crash.vadb

The envelope reports ring (post-mortem or live-drop), events, window_bytes, discarded_packets, wrapped, frozen; the recording opens like any other, with the last seconds before the freeze. viewalyzer-cli reset resets the target afterwards.

Sizing the transport

A packet is 7 bytes for an ISR edge or a task switch (type, sequence, id, 32-bit timestamp), 11 for a value, 12 plus the text for a marker. As a rule of thumb at 100 KB/s: a 1 kHz task switch rate with a few values per millisecond fits in a RAM buffer; a 10 kHz ISR with entry and exit traced (140 KB/s by itself) does not, and belongs on the SWO pin or should be sampled. The capture summary's lost_events and the sequence gaps tell you when the transport fell behind; VA_RAMBUF_MODE_BLOCK trades timing for completeness while you find out where.