type: design status: current summary: Reference for the monitor transport as implemented: L4 DebugMonitor and U3 shared-memory attach, calls, detach, and sleep interaction.
Tag Monitor Interface
This document describes the monitor interface implemented by the tag firmware and host tools. It covers both supported target paths:
- STM32L4 / L432: legacy DebugMonitor and
DCRDRtransport. - STM32U3 / U375: shared-memory attach and shared-memory RPC transport.
This is a reference for the current implementation. Open work on the transport is in the shared contracts worklist.
Common Model
The monitor is the SWD-accessible control path used by host tools to inspect a tag, read/write protobuf RPC packets, run tests, download logs, and manage state.
Both target families share these pieces:
include/monitor.h: monitor opcodes, U3 shared-memory ABI, and firmware monitor entry point declarations.embedded/tags/common/core/src/handlers.c: common monitor state, protobuf buffer storage, timeout helpers, and target-path selection.embedded/tags/common/core/src/handlersL4.c: STM32L4 transport.embedded/tags/common/core/src/handlersU3.c: STM32U3 transport.embedded/tags/common/core/src/monitor.c: protobuf request handling.host/libraries/tagcore/tagmonitor.cc: host-side attach, call, and detach logic.
The common firmware handler keeps monitor protobuf evaluation out of interrupt
context. Interrupts latch the request and signal the main thread; the main
thread calls monitorServicePending(), runs proto_eval(), and publishes the
response length.
DebugMonitor_IRQn must run at a ChibiOS kernel-callable priority. The shared
startup hook sets it to TAG_DEBUG_MONITOR_PRIORITY, defaulting to 8.
Opcodes
The monitor opcodes are shared:
TAG_MONITORINFO: L4 metadata query.MONITORSTART: start an L4 monitor session.MONITORSTOP: detach.PROTOBUF: process a protobuf request already written into the shared protobuf buffer.
TAG_MONITORINFO operands are:
MONITORVERSIONMONITORBUFMONITORBUFSIZETAGSHASTR
On U3, attach metadata is read from the shared monitor block instead of using
TAG_MONITORINFO.
Attachment State
Firmware exposes two related predicates:
bool monitorIsAttached(void);
bool isMonitorEnabled(void);
monitorIsAttached() means the target-specific monitor session is active.
isMonitorEnabled() is broader, and differs by transport:
/* STM32L4: vector catch is the attach mechanism. */
return MONCONNECTED || monitorIsAttached() || monitorAttachGraceActive();
/* STM32U3 (TAG_MONITOR_MAILBOX): the mailbox is the only session truth. */
return monitorIsAttached() || monitorAttachGraceActive();
MONCONNECTED is the DEMCR.VC_CORERESET attach hint set by the host. This is
important for the STM32L4 path because the host sets vector catch before it has
completed MONITORSTART. Runtime sleep code uses isMonitorEnabled() so a tag
does not enter standby while a host is still in the early attach/info phase.
The L4 transport also treats recent TAG_MONITORINFO traffic as a short attach
grace so metadata reads can bridge reliably into MONITORSTART.
The U3 path must not consult MONCONNECTED: it is an attach hint, not session
truth, and the host does not always clear it. A host that resumes the core
after a timed-out monitor call leaves VC_CORERESET set, so a U3 tag that
trusted it would refuse every sleep for the rest of the boot. MONCONNECTED is
still read by monitorResetRecoveryActive(), where the question really is
whether a debugger connected under reset. See
decision 0006
for the fault this caused and its measurement.
STM32L4 Path
The L4 path is the legacy DebugMonitor transport. It uses:
DEMCR.MON_ENto enable DebugMonitor.DEMCR.MON_REQandDEMCR.MON_PENDas request/pending bits.DEMCR.VC_CORERESETas the firmware-visible monitor attach hint.DCRDRas both request and response mailbox.
L4 Attach
The host AttachL4() sequence is:
- Read
DEMCR. - Enable debug and write
DEMCRwithMON_EN | VC_CORERESET, clearingMON_REQandMON_PEND. - Release reset.
- Clear halt by writing
DHCSR. - Call
TAG_MONITORINFOto read: - monitor version;
- protobuf buffer address;
- SHA string address;
- protobuf buffer size.
- Call
MONITORSTART.
During this metadata phase, monitorIsAttached() is still false because the
firmware session has not started yet. MONCONNECTED is therefore the only
host-provided signal that standby must be suppressed. Each TAG_MONITORINFO
request also arms a short target-side attach grace, covering hosts that briefly
drop the vector-catch hint between metadata reads and MONITORSTART.
L4 Calls
For a normal L4 monitor call, the host:
- Checks that
MON_REQandMON_PENDare clear; if either is still set,Call()logs an error and fails the call rather than waiting. - Writes
(operand << 8) | operationtoDCRDR. - Sets
MON_EN | MON_PEND | MON_REQ | VC_CORERESETinDEMCR. - Polls
MON_REQuntil firmware clears it. - Reads the result from
DCRDR.
The target DebugMon_Handler reads DCRDR, decodes the operation, and handles
commands as follows:
TAG_MONITORINFO: starts or refreshes the short attach grace, immediately writes metadata toDCRDR, and clearsMON_REQ.MONITORSTART: starts the monitor session, writes success toDCRDR, clearsMON_REQ, and signals the main thread.MONITORSTOP: writes success, stops the session, and clears DebugMonitor control/request state. The host treats posting this request as sufficient for detach because the target may enter terminal low power immediately afterward.PROTOBUF: latches the protobuf request length, arms the timeout, signals the main thread, and returns later throughmonitorServicePending().
monitorServicePending() runs proto_eval() in thread context, writes the
encoded response length to DCRDR, clears MON_REQ, and rearms the 60-second L4
watchdog (L4_MONITOR_TIMEOUT_S) while attached.
A protobuf call also writes the request into the protobuf buffer before step 1.
The buffer belongs to the pending request until the firmware clears MON_REQ,
and the main thread can take a second or more to reach it (Set RTC runs inside
proto_eval(); a self-test runs straight after it is accepted). So
TagMonitor::Rpc() first waits, up to 2.5 s, for MON_REQ and MON_PEND to
clear (WaitL4MonitorIdle()), and fails without writing if they do not. A
request that an earlier, failed call left pending on the tag is therefore
never overwritten.
Known firmware defect. A PROTOBUF that reaches DebugMon_Handler while
another is still pending is dropped with monitorClearRequest(), which clears
the pending request's MON_REQ too. The host then reads a reply that was never
written, and the pending request is never served. With the wait above, the
host does not send that second request; the firmware fix is recorded in
embedded/tags/TODO.md.
L4 Detach And Timeout
MONITORSTOP clears VC_CORERESET, MON_EN, MON_PEND, and MON_REQ, then
stops the monitor session.
The host does not require a post-stop DCRDR read on L4 detach. Once the stop
request is posted, firmware owns cleanup and may enter terminal low power before
another SWD transaction can complete.
The L4 attach grace is 10 seconds. Once MONITORSTART completes, the L4
watchdog is a 60-second request/session watchdog. If it fires while
monitor_enabled is true, it posts EVT_MONITOR_TIMEOUT; the main-thread
service path stops the session.
STM32U3 Path
The U3 path avoids using DCRDR and DebugMonitor as the runtime transport.
Instead it uses:
- a fixed shared RAM block at
MONITOR_SHARED_ADDR; - a reset-vector-halt attach handshake;
- an external IRQ used as a software kick;
- a target-side heartbeat watchdog.
The shared block ABI is defined in include/monitor.h:
#define MONITOR_SHARED_ADDR 0x20000000U
#define MONITOR_SHARED_SIZE 0x40U
#define MONITOR_REQUEST_MAGIC 0xC0FFEE00U
#define MONITOR_CONNECTED_MAGIC 0xDEADBEEFU
#define MONITOR_SHARED_ABI_VERSION 1U
The fixed block contains:
request: detached/requested/connected state;- ABI and debug version;
- protobuf buffer address and size;
- SHA string address;
- command, result, and status words;
host_activityheartbeat word;watchdog_ticksdiagnostics;path_magicidentifying the U3 path.
Normal firmware RAM is moved above MONITOR_SHARED_SIZE so C startup does not
erase the host-written request word.
U3 Attach
The host AttachU3() sequence is:
- Enable debug and reset vector catch without enabling the L4 DebugMonitor request path.
- Release reset and wait for the core to halt at the reset vector.
- Clear the shared block.
- Write
MONITOR_REQUEST_MAGICtomonitor_shared.request. - Clear any stale monitor kick IRQ pending bit.
- Resume the core.
- Poll the shared block until:
request == MONITOR_CONNECTED_MAGIC; andMONITOR_SHARED_FLAG_SESSION_READYis set.- Validate ABI version, metadata addresses, buffer size, and SHA address.
- Read the SHA string directly from target memory.
Target startup has two phases:
monitorSharedEarlyInit()runs before ChibiOS startup. It detectsMONITOR_REQUEST_MAGIC, publishes fixed metadata, setshost_activity, and writesMONITOR_CONNECTED_MAGIC.monitorSharedSessionStart()runs after ChibiOS startup. It enables the reserved kick IRQ, starts the monitor watchdog, setsMONITOR_SHARED_FLAG_SESSION_READY, and marks the session idle.
U3 Calls
For a normal U3 monitor call, the host:
- Reads the shared block and verifies the target is connected.
- Verifies the command/status words are idle.
- Writes result/status/command words in the shared block.
- Clears and then pends the reserved monitor kick IRQ through NVIC
ICPR/ISPR. - Polls shared
statusuntil it becomesMONITOR_STATUS_DONE, then readsresult.
The U3 kick IRQ handler:
- sets
host_activitywhenever the sharedrequestword is non-zero; - treats
request == 0as a stale kick/detach case; - handles
MONITORSTOPby stopping the session and clearing the shared state; - handles an exact
get_statusprotobuf request directly whenpState->safeis false, using cached runtime state and the last main-loop timestamp; - handles
PROTOBUFby latching the request length, setting status pending, clearing command, and signaling the main thread; - reports busy/not-attached/bad-command through the shared status word.
The U3 status fast path exists only to keep host status polling responsive while
the main thread is inside a guarded long operation. It intentionally omits
thread-context work done by the normal status path, including RTC reads, ADC
measurement, debug-log draining, diagnostics, and storage-derived totals. The
reported millis field is the timestamp last captured by the main loop, so host
clock-error displays can show stale-time drift during the busy interval.
monitorServicePending() runs proto_eval() in main-thread context, writes
the result length to monitor_shared.result, sets status done, clears command,
and rearms the heartbeat watchdog.
U3 Detach And Heartbeat
MONITORSTOP is explicit detach. The target stops the session and clears the
shared request word to 0. The host treats request == 0 as detach completion.
U3 also has target-side disappearance detection. The heartbeat is any monitor
call: the kick IRQ sets host_activity = 1 whenever request is non-zero. The
host writes host_activity = 1 directly only while polling for attach. The
target watchdog:
- increments
watchdog_ticks; - disconnects immediately if
requestis no longer connected; - clears
host_activityand rearms if host activity was seen; - disconnects on the next quiet heartbeat if
host_activityremains clear.
The heartbeat period is MONITOR_HEARTBEAT_PERIOD_S, currently 5 seconds. The
effective disconnect latency is one to two heartbeat periods after the last
host monitor call, depending on timer phase, so a host that makes no call for
5 to 10 seconds is treated as gone.
Main-Thread Service
Both target paths funnel protobuf evaluation into the same model:
- Interrupt context records a pending request and signals
tpMain. - The main loop calls
monitorServicePending(). monitorServicePending()runsproto_eval().- The target-specific response mailbox is completed:
- L4 writes response length to
DCRDRand clearsMON_REQ. - U3 writes response length/status into the shared block.
- Any returned monitor work bits are then posted as normal ChibiOS events.
This keeps protobuf parsing, storage access, and tag command execution out of the interrupt handler.
Sleep And Reset Interaction
Monitor attachment is part of the low-power contract:
- Terminal sleep uses
isMonitorEnabled()and must not be entered during early L4 attach or during an active monitor session. - On targets built with
TAG_MONITOR_RESET_RECOVERY(IMUTagNand and IMUTagNandBmp581), reset recovery distinguishes a monitor attach reset from field power loss usingMONCONNECTED, an external reset at boot, andDHCSR.C_DEBUGEN; it does not read the shared request word. - Runtime code that only needs to know whether an RPC session is active should
use
monitorIsAttached(). - Runtime code that must avoid sleeping while a host is trying to attach should
use
isMonitorEnabled().
MONCONNECTED remains a narrow attach hint: it is DEMCR.VC_CORERESET. It is
not the U3 session truth. U3 session truth is the shared request word.
A host must keep talking to an attached tag. On L4 the session ends when
the 60 s watchdog above runs out without a request, and an IDLE tag then
enters terminal sleep, after which every SWD access fails (the base reports
status 0x81, or the USB transfer times out) until a reset.
UIUCTag images up to and including fw-v0.6 end the session after 5 s, not
60 s. Where RANGE_MULTIPLIER was defined (UIUCTag and the BitPresTag
variants) the session raised MSI to 24 MHz and wrote TIM2's prescaler to
compensate; TIM2 is the free-running ChibiOS system timer and a prescaler
write only takes effect at an update event it never reaches, so the tick ran
12x fast for the whole session. Measured on a UIUCTag breakout (fw-v0.6,
2.496 V): 2.9 mA attached, then 0.16 uA about 4.6 s after the last request;
400 ms polls kept it attached for 30 s. Later images do not change the clock
for the session (only data_logAck() raises it, around one page of a
download); the same breakout then stayed attached through 15 s of quiet and
drew 433 uA attached. Hosts should keep the gap between requests well under
5 s for the tags already in the field: Tag Programmer polls every 400 ms, and
attaches again at the start of a session if the tag does not answer.
Current Target Split
STM32L4 targets, including PresTag:
- use
handlersL4.c; - use
TAG_MONITORINFOfor attach metadata; - use
DCRDRand DebugMonitor request bits for RPC calls; - need
MONCONNECTEDand the L4 attach grace to suppress standby during early attach.
STM32U3 targets, IMUTagNand and IMUTagNandBmp581:
- use
handlersU3.c; - use shared memory for attach metadata;
- use the reserved external IRQ as the runtime kick;
- use the shared request word as the attached-state truth;
- use heartbeat activity to detect host disappearance.
Validation Notes
Useful regression checks:
- L4/PresTag attach can complete all early
TAG_MONITORINFOcalls. - L4
MONITORSTART, protobuf calls, andMONITORSTOPclear request bits; L4 detach does not require a post-stop response read. - U3 attach reaches
MONITOR_CONNECTED_MAGICandSESSION_READY. - U3 protobuf calls transition status idle -> pending -> done.
- U3
MONITORSTOPclearsrequestto 0. - U3 timeout clears the shared session after missing heartbeat activity.
- On L4, standby entry is suppressed while
MONCONNECTED,monitorIsAttached(), or the L4 attach grace is active. On U3,isMonitorEnabled()ignoresMONCONNECTEDand there is no attach grace, so onlymonitorIsAttached()suppresses sleep.