From 20916c509343e01c043d5ba4a71d6d7ad85358f5 Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:55:19 +0200 Subject: [PATCH 1/3] fix(fdcan): model asynchronous TX so TXBRP polling blocks (#336) TXBRP now stays asserted from the TXBAR write until the next tick(), matching real M_CAN behavior where the bit clears only after the frame leaves the node on the physical bus. The completion flags (TXBTO, IR.TFE, IR.TC) are posted in drain_pending_tx(), called at the start of every tick(). Previously, request_tx() resolved everything synchronously: firmware polling `while (TXBRP & 1) {}` never blocked, so the simulator false-passed code that called NVIC_SystemReset before its CAN frame was actually transmitted (the udslib #88 failure class). Frame delivery to loopback/bus/tx_frames is also deferred to tick() so the frame and the completion flags are always consistent. The new txbrp_stays_set_until_tick_then_txbto_is_set integration test covers the issue directly. Existing tests updated to tick once after TXBAR before inspecting completion state. --- crates/core/src/peripherals/fdcan.rs | 57 ++++++++++++++++++++---- crates/core/tests/fdcan.rs | 65 +++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/crates/core/src/peripherals/fdcan.rs b/crates/core/src/peripherals/fdcan.rs index 460e19431..8d57cf75a 100644 --- a/crates/core/src/peripherals/fdcan.rs +++ b/crates/core/src/peripherals/fdcan.rs @@ -186,11 +186,18 @@ pub struct Fdcan { txbcf: u32, txbtie: u32, txbcie: u32, - /// TX FIFO put/get indices (mod 3). With instant completion the - /// fill level is always 3 free, but the indices advance per TX as + /// TX FIFO put/get indices (mod 3). The indices advance per TX as /// pinned on silicon (TXFQS = 0x0001_0103 after one send). txfq_put: u32, txfq_get: u32, + /// Frames queued by `request_tx` awaiting tick-deferred completion. + /// Each entry carries the buffer bit-mask and the decoded frame so + /// that completion (TXBRP→0, TXBTO, IR flags) is applied in the + /// next `tick()` rather than synchronously on the TXBAR write. + /// This models the real M_CAN: TXBRP stays asserted until the + /// frame is actually placed on the bus (arbitration + bit time). + #[serde(skip)] + pending_tx: VecDeque<(u32, CanFrame)>, ckdiv: u32, optr: u32, /// Protocol has left INIT at least once — flips PSR from its reset @@ -257,6 +264,7 @@ impl Fdcan { trace: VecDeque::new(), bus_tx: None, bus_rx: None, + pending_tx: VecDeque::new(), } } @@ -324,8 +332,10 @@ impl Fdcan { } fn txfqs(&self) -> u32 { - // Instant completion: the queue never stays full, TFFL = 3. - (self.txfq_put << 16) | (self.txfq_get << 8) | FIFO_DEPTH + // TFFL reflects free slots: depth minus in-flight pending frames. + let pending = self.pending_tx.len() as u32; + let free = FIFO_DEPTH.saturating_sub(pending); + (self.txfq_put << 16) | (self.txfq_get << 8) | free } fn psr(&self) -> u32 { @@ -456,14 +466,28 @@ impl Fdcan { let Some(frame) = self.decode_tx_element(idx as usize) else { continue; }; - // Instant completion: pending request resolves this write. + // Assert the pending bit — it stays set until tick() delivers + // the frame and posts the completion flags. Firmware polling + // `while (TXBRP & 1) {}` will spin for at least one tick, + // matching real M_CAN behavior (TXBRP clears only after the + // frame has left the node on the physical bus). + self.txbrp |= bit; + self.pending_tx.push_back((bit, frame)); + } + } + + /// Complete all queued TX frames: deliver each frame to the bus or + /// loopback receiver, then post the completion flags (TXBRP→0, + /// TXBTO, IR.TFE / IR.TC). Called from `tick()` so the completion + /// is always at least one tick after the TXBAR write. + fn drain_pending_tx(&mut self) { + while let Some((bit, frame)) = self.pending_tx.pop_front() { self.txbrp &= !bit; self.txbto |= bit; self.txfq_put = (self.txfq_put + 1) % FIFO_DEPTH; self.txfq_get = self.txfq_put; - // TFE always (queue drains immediately); TC only for - // TXBTIE-enabled buffers — capture13 pinned TC clear with - // TXBTIE = 0. + // TFE always; TC only for TXBTIE-enabled buffers — + // capture13 pinned TC clear with TXBTIE = 0. self.ir |= IR_TFE; if self.txbtie & bit != 0 { self.ir |= IR_TC; @@ -633,6 +657,11 @@ impl Peripheral for Fdcan { } fn tick(&mut self) -> PeripheralTickResult { + // Complete any pending TX frames queued by request_tx(). This + // is the earliest point at which TXBRP can be cleared and + // IR.TC/TFE posted — one tick after the TXBAR write, so firmware + // polling `while (TXBRP & 1) {}` actually blocks. + self.drain_pending_tx(); // Drain the interconnect into the receiver. if let Some(rx) = self.bus_rx.take() { while let Ok(frame) = rx.try_recv() { @@ -739,6 +768,10 @@ mod tests { let mut dev = Fdcan::new(); enter_loopback(&mut dev); wr(&mut dev, REG_TXBAR, 0x1); + // TX is asynchronous: TXBRP stays set until the next tick. + assert_ne!(rd(&dev, REG_TXBRP), 0, "pending before tick"); + assert_eq!(rd(&dev, REG_TXBTO), 0, "not complete before tick"); + dev.tick(); // Every value below is the capture13 post-TX read. assert_eq!(rd(&dev, REG_TXBRP), 0); assert_eq!(rd(&dev, REG_TXBTO), 0x1); @@ -765,6 +798,8 @@ mod tests { let mut dev = Fdcan::new(); enter_loopback(&mut dev); wr(&mut dev, REG_TXBAR, 0x1); + // Trace events are posted by drain_pending_tx inside tick(). + dev.tick(); let trace = dev.trace_snapshot("fdcan1"); assert_eq!(trace.len(), 2); @@ -785,6 +820,7 @@ mod tests { enter_loopback(&mut dev); wr(&mut dev, REG_TXBTIE, 0x1); wr(&mut dev, REG_TXBAR, 0x1); + dev.tick(); assert_ne!(rd(&dev, REG_IR) & IR_TC, 0); } @@ -793,6 +829,7 @@ mod tests { let mut dev = Fdcan::new(); enter_loopback(&mut dev); wr(&mut dev, REG_TXBAR, 0x1); + dev.tick(); let ir = rd(&dev, REG_IR); assert_ne!(ir, 0); wr(&mut dev, REG_IR, ir); @@ -804,6 +841,7 @@ mod tests { let mut dev = Fdcan::new(); enter_loopback(&mut dev); wr(&mut dev, REG_TXBAR, 0x1); + dev.tick(); wr(&mut dev, REG_RXF0A, 0x0); // capture13: F0PI = 1, F0GI = 1, F0FL = 0. assert_eq!(rd(&dev, REG_RXF0S), 0x0001_0100); @@ -838,6 +876,7 @@ mod tests { wr(&mut dev, RAM_BASE + 0x280, 0x0102_0304); wr(&mut dev, REG_CCCR, 0xA2); wr(&mut dev, REG_TXBAR, 0x1); + dev.tick(); let r0 = rd(&dev, RAM_BASE + 0xB0); assert_ne!(r0 & (1 << 30), 0, "XTD"); assert_eq!(r0 & 0x1FFF_FFFF, 0x1ABC_DEF0 & 0x1FFF_FFFF); @@ -849,9 +888,11 @@ mod tests { let mut dev = Fdcan::new(); enter_loopback(&mut dev); wr(&mut dev, REG_TXBAR, 0x1); + dev.tick(); // Reload buffer 0 with a different payload, send again. wr(&mut dev, RAM_BASE + 0x280, 0x1111_2222); wr(&mut dev, REG_TXBAR, 0x1); + dev.tick(); assert_eq!(rd(&dev, REG_TXFQS), 0x0002_0203); assert_eq!(rd(&dev, REG_RXF0S) & 0x7F, 2); // Second element at SRAMCAN + 0xB0 + 72. diff --git a/crates/core/tests/fdcan.rs b/crates/core/tests/fdcan.rs index 8d9f5008a..2a5eaa5f3 100644 --- a/crates/core/tests/fdcan.rs +++ b/crates/core/tests/fdcan.rs @@ -19,6 +19,8 @@ const RAM: u64 = 0x800; const REG_CCCR: u64 = 0x018; const REG_RXF0S: u64 = 0x090; const REG_TXBAR: u64 = 0x0CC; +const REG_TXBRP: u64 = 0x0C8; +const REG_TXBTO: u64 = 0x0D4; fn rd(dev: &Fdcan, offset: u64) -> u32 { Peripheral::read_u32(dev, offset).unwrap() @@ -50,6 +52,10 @@ fn fdcan_nodes_exchange_classic_can_frames_over_can_bus() { start(&mut node_b); wr(&mut node_a, REG_TXBAR, 0x1); + // TX is asynchronous: node_a must tick first so drain_pending_tx + // sends the frame to the bus channel, then the bus propagates it + // to node_b on the subsequent ticks. + node_a.tick(); can_bus.tick().unwrap(); node_b.tick(); @@ -82,6 +88,8 @@ fn fdcan_can_fd_frames_preserve_fd_metadata_and_64_byte_payload() { start(&mut node_b); wr(&mut node_a, REG_TXBAR, 0x1); + // TX is asynchronous: tick node_a first to push the frame onto the bus. + node_a.tick(); can_bus.tick().unwrap(); node_b.tick(); @@ -114,10 +122,65 @@ fn transmitter_echo_is_contained_to_the_bus_broadcast() { wr(&mut node_a, RAM + 0x27C, 1 << 16); start(&mut node_a); wr(&mut node_a, REG_TXBAR, 0x1); - assert_eq!(rd(&node_a, REG_RXF0S) & 0x7F, 0, "no rx before bus tick"); + assert_eq!(rd(&node_a, REG_RXF0S) & 0x7F, 0, "no rx before node tick"); + // Tick node_a first so drain_pending_tx sends the frame to the bus + // channel; then bus propagates the broadcast; then node_a drains + // its own RX (the echo arrives on this second tick). + node_a.tick(); can_bus.tick().unwrap(); node_a.tick(); assert_eq!(rd(&node_a, REG_RXF0S) & 0x7F, 1, "broadcast echo"); wr(&mut node_a, 0x094, 0); assert_eq!(rd(&node_a, REG_RXF0S) & 0x7F, 0); } + +/// Issue #336: TXBRP must stay asserted until tick() completes the +/// transmission. Firmware that polls `while (TXBRP & 1) {}` should +/// actually block in simulation, preventing false-pass of code that +/// resets before the frame reaches the bus (the udslib #88 pattern). +#[test] +fn txbrp_stays_set_until_tick_then_txbto_is_set() { + // Register offsets used in this test only. + const REG_TEST: u64 = 0x010; + + let mut dev = Fdcan::new(); + + // Enter loopback using the same sequence as capture13: CCCR.INIT + + // CCCR.CCE + CCCR.TEST, then TEST.LBCK (bit 4), then leave INIT. + wr(&mut dev, REG_CCCR, 0x3); // INIT | CCE + wr(&mut dev, REG_CCCR, 0xA3); // + TEST | MON + wr(&mut dev, REG_TEST, 1 << 4); // TEST.LBCK + wr(&mut dev, REG_CCCR, 0xA2); // leave INIT, keep TEST | MON + + wr(&mut dev, RAM + 0x278, 0x100 << 18); // TX buf 0: std ID 0x100 + wr(&mut dev, RAM + 0x27C, 1 << 16); // DLC 1 + + // Write TXBAR — frame is queued as pending, TXBRP asserted. + wr(&mut dev, REG_TXBAR, 0x1); + + // TXBRP must be non-zero immediately after the TXBAR write. + assert_ne!( + rd(&dev, REG_TXBRP) & 0x1, + 0, + "TXBRP must be set before tick (frame in flight)" + ); + // TXBTO must still be zero — completion not yet posted. + assert_eq!( + rd(&dev, REG_TXBTO) & 0x1, + 0, + "TXBTO must be clear before tick" + ); + + // One tick — drain_pending_tx delivers the frame and posts flags. + dev.tick(); + + // Completion flags are now visible to the firmware. + assert_eq!(rd(&dev, REG_TXBRP) & 0x1, 0, "TXBRP must clear after tick"); + assert_ne!(rd(&dev, REG_TXBTO) & 0x1, 0, "TXBTO must set after tick"); + // Loopback path: frame must have reached RX FIFO0 on the same tick. + assert_eq!( + rd(&dev, REG_RXF0S) & 0x7F, + 1, + "loopback frame in FIFO0 after tick" + ); +} From d31eeb8faa02f21f35db11c6dd8ce59796b87303 Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:54:53 +0200 Subject: [PATCH 2/3] chore: merge main, regenerate validation status --- docs/boards/VALIDATION_STATUS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/boards/VALIDATION_STATUS.md b/docs/boards/VALIDATION_STATUS.md index e55ec685e..024576911 100644 --- a/docs/boards/VALIDATION_STATUS.md +++ b/docs/boards/VALIDATION_STATUS.md @@ -9,7 +9,7 @@ Machine-generated from `validation/manifest.yaml`. CI regenerates this on every |-------|------|----------------------|--------------|--------| | `nrf52840` | 🟢 silicon-verified | 2026-06-17 | 2026-06-23 | ⚠ drift acked 2026-06-23 (re-capture pending) | | `seeed-xiao-nrf52840-sense` | 🟢 silicon-verified | 2026-06-17 | 2026-06-23 | ⚠ drift acked 2026-06-23 (re-capture pending) | -| `stm32h563` | 🟢 silicon-verified | 2026-06-22 | 2026-06-22 | ✅ fresh | +| `stm32h563` | 🟢 silicon-verified | 2026-06-22 | 2026-06-23 | ✖ DRIFT — model 2026-06-23 > capture; RE-CAPTURE | | `esp32c3` | 🟢 silicon-verified | 2026-06-17 | 2026-06-20 | ⚠ drift acked 2026-06-20 (re-capture pending) | | `nucleo-l476rg` | 🟢 silicon-verified | 2026-06-20 | 2026-06-20 | ✅ fresh | | `nucleo-l073rz` | 🟢 silicon-verified | 2026-06-20 | 2026-06-20 | ✅ fresh | @@ -43,7 +43,7 @@ Machine-generated from `validation/manifest.yaml`. CI regenerates this on every - Silicon: **2026-06-22** on STLINK-V3 (USB 0483:374e, NUCLEO-H563ZI, dapdirect AP1 recipe) — FLASH program-behaviour live-diff run on the board 2026-06-22 (drives real program/erase over SWD): write buffer (NSSR.WBNE) accumulates a 16-byte quad-word, commits + sets EOP only on completion; a misaligned quad-word raises INCERR alone and commits nothing; program-over-not-erased is permitted and ANDs the bits (no PGSERR); flags clear via NSCCR (0x30), not by writing NSSR. The sim H5 flash error-flag + read-while-write fidelity gates were CORRECTED to match this capture (earlier datasheet model was wrong on all four points). Prior MMIO/reset diff (h563_mmio_diff + h563_parity_diff + h563_class_diff, 0 divergence) still holds. - offline (CI): h563_conformance (5 tests vs frozen 2026-06-10..12 captures) - offline (CI): h563_mmio_diff::{h563_mmio_sim_only,h563_parity_sim_only,h563_class_sim_only} -- Drift status: **✅ fresh** +- Drift status: **✖ DRIFT — model 2026-06-23 > capture; RE-CAPTURE** ## `esp32c3` — 🟢 silicon-verified From 8f6288f2d0727d11824aeb1f16cd1ca028336252 Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:14:37 +0200 Subject: [PATCH 3/3] validation: ack stm32h563 drift for fdcan async-TX timing change --- docs/boards/VALIDATION_STATUS.md | 4 ++-- validation/manifest.yaml | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/boards/VALIDATION_STATUS.md b/docs/boards/VALIDATION_STATUS.md index 024576911..b6507c645 100644 --- a/docs/boards/VALIDATION_STATUS.md +++ b/docs/boards/VALIDATION_STATUS.md @@ -9,7 +9,7 @@ Machine-generated from `validation/manifest.yaml`. CI regenerates this on every |-------|------|----------------------|--------------|--------| | `nrf52840` | 🟢 silicon-verified | 2026-06-17 | 2026-06-23 | ⚠ drift acked 2026-06-23 (re-capture pending) | | `seeed-xiao-nrf52840-sense` | 🟢 silicon-verified | 2026-06-17 | 2026-06-23 | ⚠ drift acked 2026-06-23 (re-capture pending) | -| `stm32h563` | 🟢 silicon-verified | 2026-06-22 | 2026-06-23 | ✖ DRIFT — model 2026-06-23 > capture; RE-CAPTURE | +| `stm32h563` | 🟢 silicon-verified | 2026-06-22 | 2026-06-23 | ⚠ drift acked 2026-06-24 (re-capture pending) | | `esp32c3` | 🟢 silicon-verified | 2026-06-17 | 2026-06-20 | ⚠ drift acked 2026-06-20 (re-capture pending) | | `nucleo-l476rg` | 🟢 silicon-verified | 2026-06-20 | 2026-06-20 | ✅ fresh | | `nucleo-l073rz` | 🟢 silicon-verified | 2026-06-20 | 2026-06-20 | ✅ fresh | @@ -43,7 +43,7 @@ Machine-generated from `validation/manifest.yaml`. CI regenerates this on every - Silicon: **2026-06-22** on STLINK-V3 (USB 0483:374e, NUCLEO-H563ZI, dapdirect AP1 recipe) — FLASH program-behaviour live-diff run on the board 2026-06-22 (drives real program/erase over SWD): write buffer (NSSR.WBNE) accumulates a 16-byte quad-word, commits + sets EOP only on completion; a misaligned quad-word raises INCERR alone and commits nothing; program-over-not-erased is permitted and ANDs the bits (no PGSERR); flags clear via NSCCR (0x30), not by writing NSSR. The sim H5 flash error-flag + read-while-write fidelity gates were CORRECTED to match this capture (earlier datasheet model was wrong on all four points). Prior MMIO/reset diff (h563_mmio_diff + h563_parity_diff + h563_class_diff, 0 divergence) still holds. - offline (CI): h563_conformance (5 tests vs frozen 2026-06-10..12 captures) - offline (CI): h563_mmio_diff::{h563_mmio_sim_only,h563_parity_sim_only,h563_class_sim_only} -- Drift status: **✖ DRIFT — model 2026-06-23 > capture; RE-CAPTURE** +- Drift status: **⚠ drift acked 2026-06-24 (re-capture pending)** ## `esp32c3` — 🟢 silicon-verified diff --git a/validation/manifest.yaml b/validation/manifest.yaml index 6e86b5af7..a5ce196b2 100644 --- a/validation/manifest.yaml +++ b/validation/manifest.yaml @@ -91,7 +91,15 @@ boards: # fidelity gates (default off). The erase/program *behavioural* live-diff # (writes real flash over SWD) WAS run on 2026-06-22 and the model corrected # to match silicon (write-buffer/EOP/INCERR/NSCCR; see result above). - drift_ack: 2026-06-22 + # 2026-06-24: fdcan.rs changed for the async-TX fix (#336/#342) — request_tx + # now queues into pending_tx and drain_pending_tx delivers on the next tick, + # so TXBRP stays asserted from the TXBAR write until the following tick + # (matches real M_CAN; prevents the udslib #88 reset-before-TX false-pass). + # TX-completion *timing* change, NOT a register-reset-value change: the prior + # MMIO/parity/class reset diffs (0 divergence) and the 2026-06-22 FLASH + # behavioural capture still hold. Live re-diff of FDCAN TX timing over SWD is + # tracked as a follow-up (no H563 probe attached this session). + drift_ack: 2026-06-24 models: - crates/core/src/peripherals/rcc.rs - crates/core/src/peripherals/gpio.rs