Tag Firmware TODO
Before the next release (opened 2026-10-04/05)
Background: the ADC and power-floor investigation.
- ~~Requalify all five distributed targets.~~ Done 2026-10-08: all five
PASS against the
fw-v0.6release image, each measured from the published image rather than a local rebuild, each with its session logs attached to the release.
| Target | Result | Supply | Idle | Running |
|---|---|---|---|---|
IMUTagNandBmp581 |
PASS 10-07, storms included (results) | 3.693 V | 6.43 µA | 665.41 µA @ 400 Hz |
CompassTagAT25 |
PASS 10-06 (results) | 2.496 V | 0.21 µA | 1.95 / 1.9478 µA @ 30 s |
UIUCTag |
PASS 10-08 (results) | 2.496 V | 0.1572 µA | 0.5620 µA @ 300 s |
BitTag |
PASS 10-07, both log formats (results) | 2.496 V | 0.1185 µA | 0.4941 µA default |
PresTag |
PASS 10-07, first ever (results) | 2.496 V | 0.1118 µA | 0.3894 / 0.3908 µA @ 60 s |
No power regression. Running current moved +0.5% on IMUTagNandBmp581, −0.5% on CompassTagAT25 and −0.14% on BitTag against their previous qualified figures, on the same boards and supplies. IMUTag is the one that matters: it draws three orders of magnitude more than the rest, so it alone sets whether a deployment makes its battery life, and it is the part where image layout has moved idle current by 150x before. Its firmware contribution is isolated because the outgoing image was measured on the same board in the same session before flashing.
What the next requalification needs to know, learned doing this one:
- Pass the bounds for every target but IMUTag.
tag_release_check.pydefaults to a 100 µA sleep threshold, which is also the floor the run must clear, so a sub-100 µA run is failed as "not collecting".630ce14elets the bounds through; the defaults stay because IMUTag needs them. - The harness cannot drive BitTag, PresTag or UIUCTag, aborting on
Monitor attach failed: initial DEMCR read failed. Those were qualified by running the phases by hand. That is the harness aborting, not the tag: no run in any session reachedABORTED. - The harness checks neither calibration nor
tag-test. Close both by hand. Only CompassTag carries calibration, but no target's qualification includes a self-test unless one is run. -
Measure the outgoing image before flashing. One 120 s window separates the firmware's contribution from the board's, and without it IMUTag would have looked like a 16% idle regression that belonged to the board.
-
~~Find what lowered PresTag's resting floor.~~ Closed 2026-10-05: the firmware did not lower it. The September build
890a11bwas rebuilt and flashed to PresTag board B, which measured 0.1267 µA idle against 0.1341 µA for the same board ond41b5357-- a 5.8% spread, with the dearer figure on the current firmware. One board, two builds, no firmware effect. The bisection of890a11b..d41b5357is cancelled.
What remains is that September's 0.2810 µA is unreproducible on either
board under any build tried. It was anomalous when it was taken: BitTag
carries strictly more parts in sleep -- an ADXL362 in shutdown on top of
PresTag's RV-3028 and L432 -- and reads 0.1169 µA, so PresTag could not
legitimately rest at 2.4x BitTag. The remaining explanations are board
condition (surface leakage from flux residue, which is humidity- and
temperature-dependent; the session was taken on a hot day) and the session
itself. Neither is now worth chasing: treat 0.2810 µA as suspect data, not
as a superseded result. The lesson is the recording checklist, which now
demands a board UUID -- the September entry has none, so it cannot be
attributed to a board even in principle.
- Verify the stored-configuration write, at least part 1 of
the proposal --
verify and report, which is safe on a shared page. A qualification taken
against an unverified config write can report a pass for a tag running a
configuration nobody chose, which is worse than a failure because it looks
fine.
- Explain the 11% gap between PresTag's fitted I_rest (0.1122 µA) and its
measured IDLE (0.1261 µA). September had the two within 1.1%.
- Test flash_release.py's PEMPTY clearing on hardware. The conditional
clear has only ever reported "already clear"; the branch that writes the
register is unexercised.
Open work only. Delete an item when it is done; put what was learned in an investigation or decision record. Items marked found by reading code have not been reproduced on a tag.
Defects
- The L4 DebugMonitor handler can clear the MON_REQ of a request still
pending. The fix, and a move to the U3 mailbox if bursts remain, is
proposed in
l4-monitor-robustness.md.
Observed 2026-10-27 on a UIUCTag breakout (fw
7c508f46plus the session-clock fix), readingcommon/core/src/handlersL4.candmonitor.cafter bursts of base 0x81 errors during Synchronize Clock and Self-Test: - The handler.
DebugMon_Handlerlatches a PROTOBUF request (monitor_pending, DCRDR = 0) and leaves MON_REQ set until the main thread runs it inmonitorServicePending()at a cooperative point. A second PROTOBUF that arrives while one is pending takes theelsebranch,monitorClearRequest(), and clears the pending request's MON_REQ. The host then takes that request as done and reads a reply nobody wrote, andmonitorServicePending()later findsmonitor_pendingwith MON_REQ clear and drops the request. The clear is right when no session is open; while a request is pending it should leave MON_REQ alone. - Why a request stays pending. Synchronize Clock runs
SetTimeUnixSec()insideproto_eval()in the main thread: it sets the internal RTC, re-initialises it (tagRtcInit()), applies the clock correction, and writes the RV3028 over I2C, all while the host waits for MON_REQ to clear. Self-Test's tests run in the main thread straight after the request is accepted. - The 0x81s themselves.
tag-testshows the host's DEMCR reads failing at the SWD level (base 0x81) for most of the time the main thread spends on those operations: 172 failed reads inside one call's wait, about 330 in the run. The handler race does not explain an access failing outright. Explained and fixed (2026-10-27):DBGMCU_CR.DBG_SLEEPwas clear, so the idle thread's Sleep-mode WFI stopped HCLK and held off every SWD access while the main thread waited on I2C or a test delay, past the base's 25 WAIT retries. The host now setsDBG_SLEEPafterMONITORSTART(tagcore, both libraries); seeembedded/bases/design/investigations/2026-10-0x81-dbg-sleep.md. The handler race above is separate and still open. - An empty reply during a self-test. With the host change below in
place, the Rust
tag-test(ALL_PASSED) still logged oneGetStatus failed: Tag returned an empty or invalid acknowledgement: a call that completed with DCRDR = 0. Possible causes, not yet told apart:- the handler race above;
- a DEMCR read inside a 0x81 burst that came back without MON_REQ;
monitorServicePending()answering 0 becausemonitor_enabledwas false.
- Host side, done: tagcore's
TagMonitor::Rpc()now waits for an L4 tag's MON_REQ and MON_PEND to clear before writing the request buffer (WaitL4MonitorIdle(), andwait_l4_idle()in tagcore-rs), so a request a failed call left pending is no longer overwritten; qtmonitor pauses its polls for 1.5 s after Synchronize Clock and Self-Test. - The download clock switch's TIM2 prescaler writes do nothing.
fast_msi()/slow_msi()in the PresTag, BitPresTag and UIUCTagdatalog.craise MSI 12x around one page ofdata_logAck()and writeSTM32_ST_TIM->PSCto compensate. The prescaler is buffered until an update event, which ChibiOS's free-running 32-bit TIM2 does not reach, so the tick runs 12x fast for each page and system time jumps ahead by the excess. Harmless for a download so far; the same pattern in the monitor session made the 60 s watchdog fire after 5 s on UIUCTag (removed 2026-10-26, seedocs/shared/monitor-interface.md). Either drop thePSClines or make the change take effect without disturbing ChibiOS's counter. - A PresTag accepts SetRtc after it has been started, and hangs. The tag
must refuse a
set_rtc(Ack::PERM) once it has been started since its last reset; Tag Monitor never sends one then. On PresTag and PresTagRaw (TAG_STOP_RTC_TICKER), a start arms the stop-mode RTC ticker (tagStopRtcTickerInit()fromRunning(T_INIT)), which ends withtagRtcWriteLock()(RTC->WPR = 0xFF), as does everystopMilliseconds()in the run, and nothing unlocks it before the next reset. ChibiOS'srtc_lld_set_time()assumes the one-time unlock inrtc_lld_init()still holds: the locked RTC ignores its write toRTC_ISR.INIT, andrtc_enter_init()spins onINITFforever with BASEPRI raised, so the monitor is masked and the tag stays awake until reset. The tag may be IDLE again by then (after a stop and erase), so the check must be "started since reset", not the current state. The same lock silently drops the later ChibiOS RTC writes: a capture after Stop and erase still had the wake-up timer and alarm A enabled (RTC_CR0x4524), so an IDLE PresTag that was run without a reset since may keep waking; measure it. Seen 2026-10-26 on PresTag 92 (UID 2033305036415004003C0061, fw-v0.6), when the programmers set the clock after their final start-stop-erase:pc0x80062c0-c4,bfar= RTC_ISR, and a capture with the RTC clocked and correct butINITclear (captures/2026-10-26-prestag92-rtc-lock/). The programmers no longer start the tag. Fix: refuseset_rtcafter a start; unlock around the ChibiOS RTC writes (or stop relocking); boundrtc_enter_init()'s wait. Verify with the lifecycle and power checks: this is the stop-mode path. Found on the bench. - The state-marker log stops silently when full.
recordState()(common/core/src/persistent.c) stops writing oncesEpoch[]is full: 24 entries on L4, 20 on U3. The only notice is a scratchpadESLFrecord, and field builds have no scratchpad. So a tag that filled its marker log and then failed looks the same as one that simply stopped transitioning (Field Data Extraction, Gap 2). Fix: reserve the final slot for an overflow marker, or keep a dropped-transition count that a later write folds in. Verify: drive more thansEPOCH_SIZEtransitions on the bench and confirm the log shows the overflow. Found by reading code. - Resets and faults leave no marker. A hard fault, a watchdog reset or a
brown-out leaves nothing in the flash record, and the tag simply reappears
in an earlier state. Fix: latch the reset cause from
RCC_CSRat boot, and record a marker only when it is not an ordinary power-on or a monitor attach, so attach storms do not burn the log. Weigh the flash write against endurance on a 12 mAh cell first; this is an open question in Field Data Extraction. gd5fSectorErase()reports success without erasing on three paths: logical block out of range, mapping failure, and physical block out of range. A caller cannot tell "erased" from "silently skipped". Found by reading code.gd5fRead()does not invalidategd5f_cache_active. Onlygd5fProgramCacheLoad()andgd5fProgramExecuteCache()clear the flag. A read can therefore load a different page into the device cache register while the flag still claims the programmed page is resident. Found by reading code.- Unbounded hardware waits in the IMUTagNandBmp581 RTC LLD
(
IMUTagNandBmp581/src/hal_rtc_lld.c): theALRAWF,ALRBWFandWUTWFwaits, and thedo/whileinside a critical zone. Any of them hangs the tag if the bit never sets. Found by reading code. - Asymmetric wakeup-timer disable.
rtcSTM32SetPeriodicWakeup()waits forWUTWFwhen arming but not when disarming. It was exonerated as a cause of an idle fault, because compiling the wakeup timer out changed nothing, but the asymmetry is still there. The IMUTag family never callsenableTicker(), so it never arms the timer. - BitTagNG writes stray RAM into flash. Its 28-byte
t_storedconfigis an odd number of words, and on STM32L4FLASH_Program_Array()programs one word past the struct for an odd word count. The extra word lands in alignment padding. Found by reading code. - CompassTag and UIUCTag reattach paths are unchecked. After the
2026-10-02 rule that only true failures abort (see
the investigation),
PresTag needed
Running(T_CONT, POWERFAIL)to re-arm its sample ticker. CompassTag and UIUCTag were not re-tested and may need the same re-arm. - The
debug_logmodule prevents low-power entry on STM32U375 (1.71 mA). This is a fault in the module itself, separate from the flash-flag and layout faults; see the warning inIMUTagNandBmp581/project.mk. - IMUTagNandBmp581 RTC sync.
tag-start --set-rtcintermittently failed with "RTC sync failed while writing tag clock", and boots often reportedrtcInitializedAtBootandclockTrustedfalse. The I2C bus clear (0005) removed the stuck-bus cause. A single failure in 30 was still seen on 2026-09-07 (0007). Confirm whether a residual RV-3028 write fault remains.
STM32U375 low power
- Open question: which monitor predicate guards the terminal sleep.
tagPowerEnterStop3()refuses only whilemonitorIsAttached(). The retired Standby path, the idle hook and the RUNNING wait use the broaderisMonitorEnabled(), which on U3 also coversmonitorAttachGraceActive(). During an attach grace the tag can therefore enter Stop 3 but not returned STOP. Decide whether that is intended (STM32U375 Low Power). - Move the flash error-flag clear to where errors occur.
tagPowerClearFlashErrorFlags()runs before every terminal sleep and clears any latched error without reporting it. Clear and report the flags at the datalog flash operation that latched them, where the error can be attributed to an access. Measure idle before and after: this touches the power path. - Scratchpad retention through Stop 3 is unverified.
tagScratchRetain()(PWR_CR1_RRSB3) is called only fromtagPowerEnterStandby(), which is unused. Check whether the scratchpad survives the Stop 3 terminal sleep and the reset that follows it, and whether the retention bit is needed there. - Dead code in
pwr-u375.c.tagPowerRestoreClocksAfterStop3(),tagPowerRestoreFlashAfterStop3()andtagPowerPostStop3WakeEventI()are never called, andTAG_STM32U3_STOP3_CLEAR_WAKE_FLAGSis defined but never read. Remove them or document them as unused. This changes the image, so measure idle and run current after the change. - Standby bias for interrupt lines. On both IMUTagNand boards
WKUP1,LSM_TRG,LPS_DRDYandBMM_INT(andLMS_TRIG_2on v1) are left unbiased "while measuring whether interrupt pulldowns fight latched active-high outputs". The outcome was not recorded. Decide the bias and updateembedded/boards/IMUTagNandv*/cfg/board-customizations.jsonand the boards'standby-pins.md. - LPTIM-backed system timer. Not built; see the proposal.
- Validate
stopMilliseconds()on L432 hardware, if not already done: scope or log 1, 2, 10 and 100 ms delays and confirm none returns early, and confirm that monitor-attached behaviour useschThdSleepMilliseconds().
Restart recovery
- The monitor-attach recovery branch adopts retained state without
cross-checking the marker log. Nothing depends on that now, but if
pState->stateis ever wiped or corrupted, it will again outrank durable flash evidence. - Acquisition-phase sentinels are not implemented. Add retained sentinels
around sensor read, header write, external data write and cursor commit, so
recovery can decide whether the final pre-reset page is complete, abandoned
or should be hidden. Download timing would then combine
vddHeaderanchors, state markers for discontinuities, and the sentinel state. - Consider a full erase sweep when a run did not finish with clearly recoverable boundaries.
- Exercise checked flash reads on hardware. Reset a tag repeatedly during internal header writes, and verify that recovery and download stop at the last checked-readable header instead of entering the exception path. If an ECC-faulted double-word can be produced on a bench unit, check that a guarded read returns an ECC error and an unguarded read still takes the ordinary exception path.
- Host erase-progress fallback. Once deployed firmware reliably reports
Status.erase_sectors_total_plus_one, remove the IMUTag page-size fallback (kImuDataLogPageBytes) fromqtmonandqtprogram, so they no longer need to knowDATALOG_SAMPLES * sizeof(t_DataLog).
IMUTagNand and IMUTagNandBmp581 NAND bring-up
- Investigate the GD5F2GM7RE
B9hextreme-low-power command, and the wake sequence and timing needed before the shared NAND driver can use it (IMUTagNandBmp581). - Exercise the NAND erase path with a provisioned map present.
- Confirm the tag refuses to start collection when the NAND map is absent.
- Test page-cache write behaviour across NAND page boundaries.
- Verify that normal reprogramming and erase flows preserve calibration, the NAND map, and the dedicated configuration pages.
- Test behaviour with simulated or forced factory bad-block markers.
- Handle ECC status on NAND page reads.
- Provide a "bad page" marker or equivalent read result, so host download can skip unreadable pages instead of failing the whole download.
- Decide where bad-page observations live after runtime reads: a transient RAM table, the debug log only, a persisted table, or appended download metadata.
- Add a way to inspect bad-page information during download, by dumping the table or through a monitor or debug command.
- Confirm downloader behaviour when sparse pages are skipped: sample count, timestamp continuity, and log metadata.
- Keep first-boot map provisioning read-only with respect to NAND factory markers.
- Confirm that logical-to-physical map validation rejects erased, unsorted or out-of-range entries.
- Confirm that erase always checks physical bad-block markers before issuing a NAND block erase.
- Decide whether runtime program or erase failures retire blocks, report an error only, or both.
- Keep qtmonitor line-buffering behaviour under longer debug streams.
- Consider quieting duplicate NAND ID logs once bring-up stabilizes.
- Add host-side checks that distinguish an absent map, a bad NAND ID, an unreadable page, and erased or no-data cases.
Next release
The release after fw-v0.0.3. The fixes already landed, and how each was
verified, are in
the next-release investigation.
Remaining measurements for landed fixes
- PresTag run and idle current against its flash-log entry 0
(
captures/2026-10-02-prestag-d1/flash-log.md), for the mid-page halt fix. - CompassTag run and idle current against its entry 0
(
captures/2026-10-02-compasstag-d1), for the same fix. - IMUTagNandBmp581 currents for the external-reset-is-a-reattach image
(entry 2 of
captures/2026-10-02-imutag-nand-bmp581/flash-log.md).
Offline reconstruction layout
Specified in Offline Log Reconstruction, Decisions and plan and the records 0021, 0020 and 0017.
- Identity record (B1): add the compiler version. The build-options digest does not cover the toolchain: an image built on another machine had the same digest as a local build of the same source, but was 460 bytes smaller, with every code address shifted. Also add the GD5F logical block count.
- B1 on hardware: families other than PresTag, CompassTagAT25, UIUCTag and IMUTagNandBmp581 are not yet checked; BitTag remains.
- Session facts (B2), still to do: the effective sample settings as values
(sample period, ODR, ranges), and optionally the nanopb-encoded
Configbeside the raw struct, which removes the per-familyreadConfig()mapping from the offline path. Give new members fixed-width integer types (short-enum ABI), and check the larger struct still fits its page or region, especially on L4, wheresconfigshares.persistent. - Build-time layout descriptor (B3). Not built.
- Compile a small C program for each target, with that target's flags and
-fshort-enums. It emits a JSON descriptor:sizeofand member offsets for every stored struct, the same constants as B1, and the symbol addresses read from the linked ELF. - Install the descriptor beside the ELF and record its hash in the build manifest.
- Copy the
.protofiles, or aFileDescriptorSet, into the package. - Generate B1's record and the descriptor from the same definitions.
- Verify: for each fw-v0.0.3 target the descriptor agrees with the struct sizes and offsets established by hand in Offline Log Reconstruction, and a reproducible build regenerates it byte-identically.
Release qualification
Every item above changes a tag image, so this release needs measurement, not argument. The procedure is the release procedure.
- The current checks are the gate for this release. For each shipped
target, measure idle, running and finished current from a clean build of the
release commit, and record the numbers as the next release's baseline.
IMUTagNandBmp581 is still a prototype, with boards out for fab, so it is not
gated on
tag_release_check.pyfor this release. Run that check when the target ships. - Qualify the never-qualified targets first, then redo IMUTagNandBmp581
(agreed 2026-10-03). PresTag, CompassTagAT25 and UIUCTag have no power
qualification. BitTag's first was committed in 009bc134 against
its plan. IMUTagNandBmp581's
fw-v0.5-era measurement was of a locally built image and needs redoing against the release. - Each qualification updates the release notes, per Publishing the qualification in the release procedure: correct the "not bench-tested" sentence for each target as it is cleared.
- Functional checks beyond the gate: the CompassTag resume test, the
full-flash download test, and a
tag-captureof each target to confirm the identity record and session facts read back from the capture. - Make
tag_release_check.pycover every tag. Today it is a full gate only for IMUTagNandBmp581: power-configs/holds configs only for IMUTag and BitTag;--run-max-ua 760is sized for IMUTag at 400 Hz from 3.7 V;- the sample-count check reads only IMUTag's
lsm6.odr; - the life-cycle run is a fixed 60 s.
For other tags the life-cycle step is not meaningful. Its run bound is too
high for PresTag and BitTag to catch anything. CompassTag and UIUCTag
probably produce no downloadable row in 60 s, so they would fail on an
empty table. The fix needs:
- a config per tag (PresTag, CompassTag and UIUCTag have none);
- per-target defaults, chosen by --target, for the run-current bound,
taken from a known-good measurement;
- a run long enough to fill a downloadable page;
- a per-tag sample-count check.
- Tag the release and publish the package.
Host counterparts
These ship separately but are needed for the firmware changes to be useful.
tag-start --start-timeoutdoes not wait. Its help says "seconds to wait for the tag to leave IDLE after the start is accepted", but the poll loop inhost/commandline/tag-start.ccbreaks on the first failed status read:
read_ok = tag.GetStatus(status);
if (!read_ok) break; /* gives up; the remaining tries are not used */
A tag that sleeps straight after a start -- the normal case -- drops the
debug link, so the first read fails and --start-timeout 90 waits about zero
seconds. The flag is inoperative in exactly the situation it was added for,
and the tool reports State: not confirmed (last read: IDLE).
At minimum the help text is wrong. Whether the loop should instead keep
trying for the remaining timeout depends on something untested: whether the
monitor link recovers when the tag next wakes, without a re-attach. If it
does not -- hotplug does not work on this rig -- the current behaviour is
right and only the documentation needs fixing. Test that before changing the
loop. tag-stop has the same shape and exits 1 on a failed read.
This matters beyond tidiness: there is currently no command-line way to
confirm a tag reached RUNNING. Re-attaching to check resets the run, and
holding the link keeps isMonitorEnabled() true so the tag never sleeps.
That gap is what makes a UIUCTag run hard to qualify, because a download with
no samples cannot be distinguished from a run that never started.
- D1, offline rebuild: decoders exist for all five distributed targets (0018). Still to do: the fw-v0.0.3 layouts, and the GD5F logical block count in the identity record.
- D2: in
tagcore/recovery, read the identity record at the per-MCU address (add it toswdmcu). Check its magic, size and end entry (the record has no CRC), and use it to choose the loader and decoder. Without the record, fall back to hash or string identification. - D3, capture additions: the EEOffset over I2C for tags already deployed; NAND pages read through on-die ECC, with their status; internal-flash ECC faults.
- D4:
host/libraries/tagcore/legacy_bittag_rescue.ccshould takevddHeaderfrom the ELF instead of the hard-coded0x08007a68, and acceptBITTAG_LE. - D5: decoders should read CompassTag
temp10as signed for fw-v0.0.3 offline decoding, and read the newsconfigmembers by layout version.
Cleanup
- Move sensor orchestration out of
state_run.c. Some older tags still configure sensors directly fromstate_run.c, which crowds the acquisition state with device setup. The CompassTag family started a family-ownedsensors.c; the name is historical and means "how this family uses its sensors", not a reusable driver. When touching an olderstate_run.c, move sensor configuration, orientation transforms, calibration handling and sensor wake/sleep flow into a tag- or family-owned orchestration file with a clearer name. Reusable drivers stay undercommon/sensors. - RTC descriptors. The default RV3028 binding is still a weak board default
in common code. Tags with non-default RTC wiring should move the complete
TagRtcDevicedescriptor into tag or family board support, so that one place owns the register bus and the board power callbacks. That also removes the remaining globalrtcOn()/rtcOff()glue. - I2C backends: build one mixed development target that puts the software and hardware backends on different buses.
- BMM350 self-test enum. The monitor test table maps the BMM350 to the
legacy
RUN_MMC5633request until the protobuf test enum gains a BMM350-specific request and result.