Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 49 additions & 8 deletions crates/core/src/peripherals/fdcan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -257,6 +264,7 @@ impl Fdcan {
trace: VecDeque::new(),
bus_tx: None,
bus_rx: None,
pending_tx: VecDeque::new(),
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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);
}

Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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.
Expand Down
65 changes: 64 additions & 1 deletion crates/core/tests/fdcan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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"
);
}
4 changes: 2 additions & 2 deletions docs/boards/VALIDATION_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 |
Expand Down Expand Up @@ -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 acked 2026-06-24 (re-capture pending)**

## `esp32c3` — 🟢 silicon-verified

Expand Down
10 changes: 9 additions & 1 deletion validation/manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading