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
16 changes: 10 additions & 6 deletions crates/core/src/boot/esp32c3_rom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,12 +439,16 @@ pub fn build_rom_boot_machine<C: crate::Cpu, F: FnOnce(crate::cpu::RiscV) -> C>(
// C3 SYSTIMER_TARGET0 routes through the interrupt matrix on source
// 37 (TARGET1/2 at 38/39), unlike the S3's 57; the FreeRTOS tick
// alarm fires on that source.
Box::new(
crate::peripherals::esp32s3::systimer::Systimer::new_with_source_legacy_tick(
160_000_000,
37,
),
),
//
// Walk-free (C3 SYSTIMER batch): scheduler mode. The free-running
// counter advances lazily (write-path `sync_to` + the OP-update snapshot
// pulling the bus-published `CycleClock`, both to the batch-start
// cycle), and alarms fire as scheduled events at their exact expiry
// cycle, delivered through the C3 interrupt matrix by
// `apply_event_result`'s C3 routing arm. This un-pins SYSTIMER from the
// per-cycle walk; delivery stays cycle-identical to the legacy walk at
// a given tick interval (differential-gated).
Box::new(crate::peripherals::esp32s3::systimer::Systimer::new_with_source(160_000_000, 37)),
);
// RTC_CNTL main timer (0x6000_8000): the free-running slow-clock counter
// the IDF reads via rtc_time_get (set TIME_UPDATE @0x0C bit31 to latch,
Expand Down
1 change: 1 addition & 0 deletions crates/core/src/bus/from_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ impl SystemBus {
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down
18 changes: 18 additions & 0 deletions crates/core/src/bus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,17 @@ pub struct SystemBus {
/// that stops asserting drops out at the next tick boundary (≤ one
/// `peripheral_tick_interval` — the same bound as the write path).
esp32c3_asserted_sources: [u64; 2],
/// C3 matrix sources asserted by SCHEDULER-driven peripherals (currently
/// the SYSTIMER alarm once migrated off the walk). The per-cycle walk
/// rebuilds `esp32c3_asserted_sources` from scratch each tick and skips
/// scheduler-driven peripherals, so their level would drop every tick;
/// this bitmap is re-derived from `Peripheral::matrix_irq_sources` at the
/// event path (`apply_event_result`) and the walk-tick aggregation, and
/// OR-ed with `esp32c3_asserted_sources` in `recompute_esp32c3_irq_lines`.
/// Same level semantics (a source that stops asserting drops out at the
/// next re-derivation), so delivery matches the legacy walk cycle-for-cycle
/// at a given tick interval.
esp32c3_sched_asserted_sources: [u64; 2],
/// ESP32-S3 interrupt routing is present only when the S3 interrupt matrix
/// peripheral is registered. Cached separately from C3's RISC-V routing so
/// each chip model owns its own interrupt abstraction.
Expand Down Expand Up @@ -2527,6 +2538,7 @@ impl SystemBus {
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down Expand Up @@ -2584,6 +2596,7 @@ impl SystemBus {
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down Expand Up @@ -5029,6 +5042,7 @@ peripherals:
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down Expand Up @@ -5110,6 +5124,7 @@ peripherals:
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down Expand Up @@ -5342,6 +5357,7 @@ peripherals:
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down Expand Up @@ -5573,6 +5589,7 @@ peripherals:
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down Expand Up @@ -5658,6 +5675,7 @@ peripherals:
esp32c3_interrupt_core0_idx: None,
esp32c3_irq_cache: None,
esp32c3_asserted_sources: [0; 2],
esp32c3_sched_asserted_sources: [0; 2],
esp32s3_irq_routing: false,
esp32s3_intmatrix_idx: None,
flash_models_ops: false,
Expand Down
189 changes: 187 additions & 2 deletions crates/core/src/bus/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ impl SystemBus {
}
}
self.esp32c3_asserted_sources = asserted;
// Re-derive scheduler-driven peripheral levels (SYSTIMER once migrated
// off the walk) so their level-sensitive matrix IRQ persists across
// walk ticks and de-asserts the tick after firmware clears it.
self.refresh_esp32c3_sched_sources();

if self.esp32c3_irq_cache.is_some() {
self.recompute_esp32c3_irq_lines();
Expand Down Expand Up @@ -515,6 +519,17 @@ impl SystemBus {
for &src in source_ids {
route_source(src);
}
// Scheduler-driven peripheral levels (SYSTIMER off the walk) — refreshed
// into the persistent bitmap above.
let sched = self.esp32c3_sched_asserted_sources;
for (word, bits) in sched.iter().enumerate() {
let mut bits = *bits;
while bits != 0 {
let bit = bits.trailing_zeros();
route_source(word as u32 * 64 + bit);
bits &= !(1u64 << bit);
}
}
for (addr, src) in FROM_CPU {
let from_cpu = self
.esp32c3_system_idx
Expand Down Expand Up @@ -562,8 +577,13 @@ impl SystemBus {
}
};

for (word, &bits) in self.esp32c3_asserted_sources.iter().enumerate() {
let mut bits = bits;
for word in 0..self.esp32c3_asserted_sources.len() {
// Union of walk-emitted level sources (rebuilt each tick) and
// scheduler-driven peripheral level sources (re-derived from
// `matrix_irq_sources`), so a SYSTIMER migrated off the walk keeps
// its level-sensitive alarm IRQ routed.
let mut bits =
self.esp32c3_asserted_sources[word] | self.esp32c3_sched_asserted_sources[word];
while bits != 0 {
let bit = bits.trailing_zeros();
route_source(word as u32 * 64 + bit);
Expand All @@ -579,6 +599,34 @@ impl SystemBus {
self.riscv_irq_lines = mask;
}

/// Re-derive the C3 matrix sources asserted by SCHEDULER-driven peripherals
/// (skipped by the per-cycle walk) from their live level
/// (`Peripheral::matrix_irq_sources`). Rebuilt from scratch — level
/// semantics — so a source that stops asserting (e.g. after the SYSTIMER
/// alarm's INT_CLR) drops out on the next re-derivation. Called from the
/// event path (`Machine::apply_event_result`, exact-cycle delivery) and the
/// walk-tick aggregation (steady-state persistence + de-assert). No-op
/// unless C3 routing is active. Does NOT recompute — the caller decides
/// when to fold this into `riscv_irq_lines`.
pub(crate) fn refresh_esp32c3_sched_sources(&mut self) {
if !self.esp32c3_irq_routing {
return;
}
let mut asserted = [0u64; 2];
for p in &self.peripherals {
if !p.dev.uses_scheduler() {
continue;
}
for src in p.dev.matrix_irq_sources() {
let idx = (src / 64) as usize;
if idx < asserted.len() {
asserted[idx] |= 1u64 << (src % 64);
}
}
}
self.esp32c3_sched_asserted_sources = asserted;
}

fn aggregate_esp32s3_explicit_irqs(&mut self, source_ids: &[u32]) {
// Rebuild the per-core routed pending bitmap as a faithful LEVEL
// reflection of the sources asserting THIS tick — set while a source
Expand Down Expand Up @@ -909,3 +957,140 @@ mod walk_free_campaign {
);
}
}

/// Walk-free C3 SYSTIMER batch — the interrupt-matrix ROUTING identity.
///
/// A SYSTIMER migrated off the per-cycle walk delivers its alarm as a scheduled
/// event; the C3 routing arm (`Machine::apply_event_result` → this module's
/// `refresh_esp32c3_sched_sources` + `recompute_esp32c3_irq_lines`) must route
/// that level to `riscv_irq_lines` EXACTLY as the legacy walk did when the
/// SYSTIMER re-emitted source 37 every tick (`aggregate_esp32c3_irqs`). This
/// pins that equivalence at the bus level (the OLED-lab gate proves it
/// end-to-end through the real FreeRTOS tick).
#[cfg(all(test, feature = "event-scheduler"))]
mod c3_systimer_matrix_routing {
use crate::bus::SystemBus;
use crate::peripherals::esp32s3::systimer::Systimer;
use crate::Peripheral;
use labwired_config::{ChipDescriptor, SystemManifest};
use std::path::PathBuf;

const SYSTIMER_BASE: u64 = 0x6002_3000;
const INTMATRIX: u64 = 0x600C_2000;
const SYSTIMER_TARGET0_SOURCE: u64 = 37;
const LINE: u32 = 5;

/// Build a devkit C3 bus (real `interrupt_core0` → INTC cache), enable C3
/// routing, swap the declarative SYSTIMER stub for a real scheduler-driven
/// `Systimer`, and route SYSTIMER_TARGET0 (source 37) → CPU line 5.
fn setup() -> SystemBus {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let chip = ChipDescriptor::from_file(root.join("../../configs/chips/esp32c3.yaml"))
.expect("load esp32c3 chip yaml");
let manifest =
SystemManifest::from_file(root.join("../../configs/systems/esp32c3-devkit.yaml"))
.expect("load esp32c3-devkit system yaml");
let mut bus = SystemBus::from_config(&chip, &manifest).expect("build c3 devkit bus");

// Enable the RISC-V interrupt routing (the ROM-boot path sets this; the
// from_config bus does not) and rebuild the INTC cache.
bus.esp32c3_irq_routing = true;
bus.refresh_peripheral_index();

// Swap the declarative SYSTIMER stub for the real scheduler model and
// hand it the bus clock (as `add_peripheral` would).
let idx = bus
.find_peripheral_index_by_name("systimer")
.expect("devkit bus carries a systimer");
let mut dev = Systimer::new_with_source(160_000_000, SYSTIMER_TARGET0_SOURCE as u32);
dev.attach_cycle_clock(bus.cycle_clock.clone());
bus.peripherals[idx].dev = Box::new(dev);
bus.refresh_peripheral_index();

// Route source 37 → line 5, priority 1, threshold 1, line enabled.
bus.write_u32(INTMATRIX + SYSTIMER_TARGET0_SOURCE * 4, LINE)
.unwrap();
bus.write_u32(INTMATRIX + 0x114 + (LINE as u64) * 4, 1)
.unwrap();
bus.write_u32(INTMATRIX + 0x194, 1).unwrap();
bus.write_u32(INTMATRIX + 0x104, 1 << LINE).unwrap();

// Arm TARGET0 in target mode at 3 SYSTIMER ticks, enable its IRQ.
bus.write_u32(SYSTIMER_BASE + 0x64, 1).unwrap(); // INT_ENA bit0
bus.write_u32(SYSTIMER_BASE + 0x1C, 0).unwrap(); // TARGET0_HI
bus.write_u32(SYSTIMER_BASE + 0x20, 3).unwrap(); // TARGET0_LO
bus.write_u32(SYSTIMER_BASE + 0x50, 1).unwrap(); // COMP0_LOAD
let conf = bus.read_u32(SYSTIMER_BASE).unwrap();
bus.write_u32(SYSTIMER_BASE, conf | (1 << 24)).unwrap(); // TARGET0_WORK_EN
bus
}

fn systimer_mut(bus: &mut SystemBus) -> &mut Systimer {
let idx = bus.find_peripheral_index_by_name("systimer").unwrap();
bus.peripherals[idx]
.dev
.as_any_mut()
.unwrap()
.downcast_mut::<Systimer>()
.unwrap()
}

/// The scheduler routing arm and the legacy walk aggregation produce the
/// SAME `riscv_irq_lines` for the SAME SYSTIMER level.
#[test]
fn scheduler_routing_matches_walk_routing_for_same_level() {
let mut bus = setup();

// Advance the SYSTIMER past the target so the alarm latches
// pending && int_ena → the model asserts matrix source 37.
systimer_mut(&mut bus).sync_to(10_000);
assert_eq!(
systimer_mut(&mut bus).matrix_irq_sources(),
vec![SYSTIMER_TARGET0_SOURCE as u32],
"armed+fired SYSTIMER must assert matrix source 37"
);

// Scheduler routing arm (what `apply_event_result` runs on the C3 bus).
bus.refresh_esp32c3_sched_sources();
bus.recompute_esp32c3_irq_lines();
let scheduler_lines = bus.riscv_irq_lines;
assert_eq!(
scheduler_lines,
1 << LINE,
"scheduler routing must assert the routed CPU line for source 37"
);

// Legacy walk routing reference: source 37 re-emitted by the walk. Must
// land on the identical line mask.
bus.aggregate_esp32c3_irqs(&[SYSTIMER_TARGET0_SOURCE as u32]);
assert_eq!(
bus.riscv_irq_lines, scheduler_lines,
"walk aggregation and scheduler routing must produce identical riscv_irq_lines"
);
}

/// Clearing the SYSTIMER level (INT_CLR) de-asserts the routed line on the
/// next re-derivation — same level semantics as the walk (which stops
/// re-emitting the source the tick after INT_CLR).
#[test]
fn clearing_level_deasserts_routed_line() {
let mut bus = setup();
systimer_mut(&mut bus).sync_to(10_000);
bus.refresh_esp32c3_sched_sources();
bus.recompute_esp32c3_irq_lines();
assert_eq!(bus.riscv_irq_lines, 1 << LINE);

// INT_CLR bit0 clears the pending latch → level drops.
bus.write_u32(SYSTIMER_BASE + 0x6C, 1).unwrap();
assert!(
systimer_mut(&mut bus).matrix_irq_sources().is_empty(),
"after INT_CLR the SYSTIMER asserts no matrix source"
);
bus.refresh_esp32c3_sched_sources();
bus.recompute_esp32c3_irq_lines();
assert_eq!(
bus.riscv_irq_lines, 0,
"routed line must de-assert once the SYSTIMER level clears"
);
}
}
32 changes: 30 additions & 2 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,19 @@ pub trait Peripheral: std::fmt::Debug + Send {
/// its legacy walk path (`uses_scheduler() == false`), so hand-built
/// buses that bypass `add_peripheral` keep the old exact semantics.
fn attach_cycle_clock(&mut self, _clock: CycleClock) {}

/// Walk-free plan (ESP32-C3): the interrupt-matrix source IDs this
/// peripheral is asserting RIGHT NOW (level-sensitive). The per-cycle walk
/// normally re-emits a level source every tick via `PeripheralTickResult::
/// explicit_irqs`, and the bus rebuilds the C3 asserted-source bitmap from
/// that each tick. A scheduler-driven peripheral is skipped by the walk, so
/// the bus re-derives its live level from this method instead
/// (`SystemBus::refresh_esp32c3_sched_sources`, called from the event path
/// and the walk-tick aggregation). Default empty — only scheduler-driven
/// peripherals that raise C3 matrix IRQs override it.
fn matrix_irq_sources(&self) -> Vec<u32> {
Vec::new()
}
}

/// Trait representing the system bus
Expand Down Expand Up @@ -1797,8 +1810,23 @@ impl<C: Cpu> Machine<C> {
self.bus.pend_irq_for_event(irq, &mut fallthrough);
}
}
for irq in &result.explicit_irqs {
self.bus.pend_irq_for_event(*irq, &mut fallthrough);
// ESP32-C3 interrupt-matrix routing arm — beside the Cortex-M
// `system_exception` arm below (B1's SysTick path). C3 peripheral IRQs
// do NOT go through an NVIC; they assert a LEVEL-sensitive matrix source
// that the per-cycle walk normally re-emits each tick. A scheduler-
// driven SYSTIMER is skipped by the walk, so the event path owns its
// level: re-derive every scheduler-driven peripheral's live matrix
// sources (`matrix_irq_sources`) and fold them into `riscv_irq_lines`
// here, at the exact firing cycle. `pend_irq_for_event` (NVIC) would
// mis-route a matrix source ID as a Cortex-M exception on RISC-V, so the
// NVIC path is taken only on non-C3 buses.
if self.bus.esp32c3_irq_routing {
self.bus.refresh_esp32c3_sched_sources();
self.bus.recompute_esp32c3_irq_lines();
} else {
for irq in &result.explicit_irqs {
self.bus.pend_irq_for_event(*irq, &mut fallthrough);
}
}
// Phase 2B.3b: route DMA signals exactly as the legacy tick path does.
if !result.dma_signals.is_empty() {
Expand Down
Loading
Loading