Skip to main content

esp_p4_eth/
clic.rs

1//! ESP32-P4 HP-CPU interrupt routing — INTERRUPT_CORE0 matrix + CLIC.
2//!
3//! See `reference_p4_clic_irq_routing.md` for the full hardware map. Routing a
4//! peripheral interrupt to the CPU is a two-step process:
5//!
6//! 1. Pick a CPU INT line `N` ∈ 1..31 and program the matrix register for the
7//!    peripheral source: `INTERRUPT_CORE0_<peripheral>_INT_MAP_REG = N`.
8//! 2. Configure CLIC entry `N + CLIC_EXT_INTR_NUM_OFFSET` (= `N + 16`):
9//!    ATTR (level/edge, mode), CTL (priority), IE (enable). Clear pending via
10//!    `CLIC_INT_IP` byte. Threshold register gates global priority.
11//!
12//! Constants and bit positions cross-checked against IDF v5.3.5
13//! `components/soc/esp32p4/include/soc/clic_reg.h` and
14//! `components/soc/esp32p4/include/soc/interrupt_core0_reg.h`.
15//!
16//! All MMIO writes are gated to riscv32 — the file is host-compilable so the
17//! pure address-arithmetic and byte-packing helpers can be unit-tested on the
18//! development host. On non-riscv32 targets every public function that would
19//! touch MMIO is a no-op.
20
21#[cfg(target_arch = "riscv32")]
22use core::ptr::{read_volatile, write_volatile};
23
24const INTERRUPT_CORE0_BASE: usize = 0x500D_6000;
25/// `INTERRUPT_CORE0_SYSTIMER_TARGET0_INT_MAP_REG` — bits[5:0] = CLIC index.
26pub const INT_MAP_SYSTIMER_TARGET0: *mut u32 = (INTERRUPT_CORE0_BASE + 0xD4) as *mut u32;
27/// `INTERRUPT_CORE0_SBD_INT_MAP_REG` — main EMAC/GMAC DMA interrupt
28/// (Synopsys DesignWare bus driver). Source ID 92 = offset 0x170.
29pub const INT_MAP_EMAC_SBD: *mut u32 = (INTERRUPT_CORE0_BASE + 0x170) as *mut u32;
30
31#[cfg(target_arch = "riscv32")]
32const CLIC_BASE: usize = 0x2080_0000;
33#[allow(dead_code)]
34const CLIC_CTRL_BASE: usize = 0x2080_1000;
35#[cfg(target_arch = "riscv32")]
36const CLIC_INT_THRESH_REG: *mut u32 = (CLIC_BASE + 0x08) as *mut u32;
37
38/// External interrupts in the CLIC index space start at 16 — peripheral CPU
39/// INT line `N` (1..31) lives at CLIC entry `N + 16`.
40pub const CLIC_EXT_INTR_NUM_OFFSET: u8 = 16;
41
42/// Trigger types for CLIC ATTR.TRIG bits[2:1].
43#[repr(u8)]
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum Trigger {
46    LevelPositive = 0b00,
47    EdgePositive = 0b01,
48    LevelNegative = 0b10,
49    EdgeNegative = 0b11,
50}
51
52// ---------------------------------------------------------------------------
53// Pure-data helpers (host-testable). All MMIO wrappers below build on these.
54// ---------------------------------------------------------------------------
55
56/// CLIC entry index for a given peripheral CPU INT line. External interrupts
57/// start at offset 16, so CPU line `N` maps to CLIC entry `N + 16`.
58#[inline]
59pub const fn clic_idx_for_cpu_line(cpu_int_n: u8) -> u8 {
60    cpu_int_n + CLIC_EXT_INTR_NUM_OFFSET
61}
62
63/// Byte offset of CLIC control word for entry `idx`, relative to
64/// `CLIC_CTRL_BASE`. Each entry is 4 bytes: `[IP, IE, ATTR, CTL]`.
65#[inline]
66pub const fn clic_ctrl_offset(idx: u8) -> usize {
67    (idx as usize) * 4
68}
69
70/// Pack the CLIC ATTR byte: MODE = 11 (machine), TRIG = `trigger`, SHV = 0.
71/// MODE in the top 2 bits, TRIG in bits[2:1], SHV in bit 0.
72#[inline]
73pub const fn clic_attr_byte(trigger: Trigger) -> u8 {
74    (3u8 << 6) | ((trigger as u8) << 1)
75}
76
77/// Pack the CLIC CTL byte: priority occupies the top NLBITS = 3 bits. Higher
78/// is more important; the bottom 5 bits are reserved/zero.
79#[inline]
80pub const fn clic_ctl_byte(priority: u8) -> u8 {
81    (priority & 0x07) << 5
82}
83
84/// Value to write to `INTERRUPT_CORE0_<peripheral>_INT_MAP_REG`. The register
85/// accepts a 6-bit CLIC index — clamps the input to its valid range.
86#[inline]
87pub const fn int_map_value(clic_idx: u8) -> u32 {
88    (clic_idx & 0x3F) as u32
89}
90
91/// Value to write to the CLIC global threshold register: `thresh` byte
92/// occupies bits[31:24]. `0x00` = pass everything; `0xFF` = block all.
93#[inline]
94pub const fn clic_threshold_value(thresh: u8) -> u32 {
95    (thresh as u32) << 24
96}
97
98// ---------------------------------------------------------------------------
99// Raw pointer helpers — only meaningful on riscv32. On host the addresses
100// are still computed correctly but never dereferenced.
101// ---------------------------------------------------------------------------
102
103#[allow(dead_code)]
104#[inline]
105fn clic_word_ptr(idx: u8) -> *mut u32 {
106    (CLIC_CTRL_BASE + clic_ctrl_offset(idx)) as *mut u32
107}
108
109#[inline]
110fn clic_byte_ip(idx: u8) -> *mut u8 {
111    (CLIC_CTRL_BASE + clic_ctrl_offset(idx)) as *mut u8
112}
113
114#[inline]
115fn clic_byte_ie(idx: u8) -> *mut u8 {
116    (CLIC_CTRL_BASE + clic_ctrl_offset(idx) + 1) as *mut u8
117}
118
119#[inline]
120fn clic_byte_attr(idx: u8) -> *mut u8 {
121    (CLIC_CTRL_BASE + clic_ctrl_offset(idx) + 2) as *mut u8
122}
123
124#[inline]
125fn clic_byte_ctl(idx: u8) -> *mut u8 {
126    (CLIC_CTRL_BASE + clic_ctrl_offset(idx) + 3) as *mut u8
127}
128
129// ---------------------------------------------------------------------------
130// MMIO wrappers — riscv32-only.
131// ---------------------------------------------------------------------------
132
133/// Route a peripheral interrupt source to CLIC entry `clic_idx` (16..47 for
134/// external peripherals — that's `cpu_int_line + 16`). `int_map_reg` is the
135/// absolute address of the matrix register for the source (e.g.
136/// [`INT_MAP_SYSTIMER_TARGET0`], [`INT_MAP_EMAC_SBD`]). Writing 0 unmaps
137/// the source.
138///
139/// Note: the value written to INTERRUPT_CORE0_*_INT_MAP_REG is the CLIC
140/// index directly, NOT just the CPU INT line number. IDF's
141/// `intr_matrix_route(src, n)` does `interrupt_clic_ll_route(core, src,
142/// n + RV_EXTERNAL_INT_OFFSET)` and that final value is what lands in the
143/// matrix register.
144#[inline]
145pub fn route_to_clic(_int_map_reg: *mut u32, _clic_idx: u8) {
146    #[cfg(target_arch = "riscv32")]
147    unsafe {
148        write_volatile(_int_map_reg, int_map_value(_clic_idx));
149    }
150}
151
152/// Convenience wrapper for the SYSTIMER TARGET0 source.
153#[inline]
154pub fn route_systimer_target0(clic_idx: u8) {
155    route_to_clic(INT_MAP_SYSTIMER_TARGET0, clic_idx);
156}
157
158/// Convenience wrapper for the EMAC main DMA / SBD source.
159#[inline]
160pub fn route_emac_sbd(clic_idx: u8) {
161    route_to_clic(INT_MAP_EMAC_SBD, clic_idx);
162}
163
164/// Configure a CLIC entry for one peripheral CPU INT line. `priority` is the
165/// CTL byte top-3-bit value (NLBITS=3) — use `0..=7`. Sets level-positive
166/// trigger, machine mode, IE = 1, no selective vectoring (SHV = 0).
167pub fn enable_cpu_int(_cpu_int_n: u8, _priority: u8) {
168    #[cfg(target_arch = "riscv32")]
169    {
170        let idx = clic_idx_for_cpu_line(_cpu_int_n);
171        let prio_byte = clic_ctl_byte(_priority);
172        let attr_byte = clic_attr_byte(Trigger::LevelPositive);
173        unsafe {
174            // Disable while reconfiguring to avoid spurious fires.
175            write_volatile(clic_byte_ie(idx), 0);
176            // Clear any latched IP.
177            write_volatile(clic_byte_ip(idx), 0);
178            write_volatile(clic_byte_attr(idx), attr_byte);
179            write_volatile(clic_byte_ctl(idx), prio_byte);
180            write_volatile(clic_byte_ie(idx), 1);
181        }
182    }
183}
184
185/// Disable a CLIC entry (clears IE byte). Source mapping in INTERRUPT_CORE0
186/// is left in place.
187#[inline]
188pub fn disable_cpu_int(_cpu_int_n: u8) {
189    #[cfg(target_arch = "riscv32")]
190    {
191        let idx = clic_idx_for_cpu_line(_cpu_int_n);
192        unsafe { write_volatile(clic_byte_ie(idx), 0) };
193    }
194}
195
196/// Clear pending bit on a CLIC entry. Most level-triggered sources auto-clear
197/// when the upstream peripheral deasserts, but this is useful as a fence /
198/// for edge sources.
199#[inline]
200pub fn clear_pending(_cpu_int_n: u8) {
201    #[cfg(target_arch = "riscv32")]
202    {
203        let idx = clic_idx_for_cpu_line(_cpu_int_n);
204        unsafe { write_volatile(clic_byte_ip(idx), 0) };
205    }
206}
207
208/// Read the raw CLIC control word for diagnostics.
209#[inline]
210pub fn read_ctrl_word(_cpu_int_n: u8) -> u32 {
211    #[cfg(target_arch = "riscv32")]
212    {
213        let idx = clic_idx_for_cpu_line(_cpu_int_n);
214        return unsafe { read_volatile(clic_word_ptr(idx)) };
215    }
216    #[cfg(not(target_arch = "riscv32"))]
217    0
218}
219
220/// Set the CLIC global threshold (bits[31:24]). CPU responds when an int's
221/// CTL ≥ threshold. `0x00` = pass everything; `0xFF` = block all.
222#[inline]
223pub fn set_threshold(_thresh: u8) {
224    #[cfg(target_arch = "riscv32")]
225    unsafe {
226        write_volatile(CLIC_INT_THRESH_REG, clic_threshold_value(_thresh));
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    /// IDF baseline `interrupt_core0_reg.h`: external interrupts start at
235    /// CLIC entry 16. If this drifts, every IRQ routing computation is off.
236    #[test]
237    fn clic_ext_intr_num_offset_is_sixteen() {
238        assert_eq!(CLIC_EXT_INTR_NUM_OFFSET, 16);
239    }
240
241    /// Verified against IDF `interrupt_core0_reg.h`:
242    /// `DR_REG_INTERRUPT_CORE0_BASE = 0x500D6000` and
243    /// `INTERRUPT_CORE0_SYSTIMER_TARGET0_INT_MAP_REG = base + 0xD4`.
244    #[test]
245    fn int_map_systimer_target0_address_matches_idf() {
246        assert_eq!(INT_MAP_SYSTIMER_TARGET0 as usize, 0x500D_60D4);
247    }
248
249    /// Verified against IDF: `INTERRUPT_CORE0_SBD_INT_MAP_REG = base + 0x170`.
250    /// Source ID 92 in the matrix corresponds to the GMAC SBD aggregate.
251    #[test]
252    fn int_map_emac_sbd_address_matches_idf() {
253        assert_eq!(INT_MAP_EMAC_SBD as usize, 0x500D_6170);
254    }
255
256    #[test]
257    fn clic_idx_for_cpu_line_is_offset_by_sixteen() {
258        assert_eq!(clic_idx_for_cpu_line(0), 16);
259        assert_eq!(clic_idx_for_cpu_line(1), 17);
260        assert_eq!(clic_idx_for_cpu_line(2), 18);
261        // 31 is the highest legal CPU INT line — yields CLIC index 47.
262        assert_eq!(clic_idx_for_cpu_line(31), 47);
263    }
264
265    #[test]
266    fn clic_ctrl_offset_is_four_bytes_per_entry() {
267        assert_eq!(clic_ctrl_offset(0), 0);
268        assert_eq!(clic_ctrl_offset(1), 4);
269        assert_eq!(clic_ctrl_offset(17), 17 * 4);
270        assert_eq!(clic_ctrl_offset(255), 255 * 4);
271    }
272
273    /// Each CLIC entry is `[IP, IE, ATTR, CTL]` in increasing-address order.
274    /// A swap here silently breaks every interrupt setup.
275    #[test]
276    fn clic_byte_offsets_are_ip_ie_attr_ctl_in_order() {
277        let idx = 17;
278        let ip = clic_byte_ip(idx) as usize;
279        let ie = clic_byte_ie(idx) as usize;
280        let attr = clic_byte_attr(idx) as usize;
281        let ctl = clic_byte_ctl(idx) as usize;
282        let word = clic_word_ptr(idx) as usize;
283        assert_eq!(ip, word);
284        assert_eq!(ie, word + 1);
285        assert_eq!(attr, word + 2);
286        assert_eq!(ctl, word + 3);
287    }
288
289    /// CLIC entry 17 (SYSTIMER alarm) lives at `0x2080_1000 + 17*4 = 0x2080_1044`.
290    #[test]
291    fn clic_word_address_for_systimer_entry_matches_layout() {
292        assert_eq!(clic_word_ptr(17) as usize, 0x2080_1044);
293        // Entry 18 (EMAC SBD) is the next one.
294        assert_eq!(clic_word_ptr(18) as usize, 0x2080_1048);
295    }
296
297    #[test]
298    fn clic_attr_byte_machine_mode_level_positive_is_0xc0() {
299        // MODE = 11 → top 2 bits = 11000000.
300        // TRIG = 00 (level positive) → bits[2:1] = 00.
301        // SHV = 0 → bit 0 = 0.
302        assert_eq!(clic_attr_byte(Trigger::LevelPositive), 0b1100_0000);
303    }
304
305    #[test]
306    fn clic_attr_byte_encodes_each_trigger_distinctly() {
307        let lp = clic_attr_byte(Trigger::LevelPositive);
308        let ep = clic_attr_byte(Trigger::EdgePositive);
309        let ln = clic_attr_byte(Trigger::LevelNegative);
310        let en = clic_attr_byte(Trigger::EdgeNegative);
311        // All four must differ — no aliasing.
312        assert_ne!(lp, ep);
313        assert_ne!(lp, ln);
314        assert_ne!(lp, en);
315        assert_ne!(ep, ln);
316        assert_ne!(ep, en);
317        assert_ne!(ln, en);
318        // MODE bits and SHV bit must be the same across triggers.
319        let mode_shv_mask = !0b0000_0110;
320        assert_eq!(lp & mode_shv_mask, ep & mode_shv_mask);
321        assert_eq!(lp & mode_shv_mask, ln & mode_shv_mask);
322        assert_eq!(lp & mode_shv_mask, en & mode_shv_mask);
323    }
324
325    #[test]
326    fn clic_ctl_byte_packs_priority_into_top_three_bits() {
327        // Priority 0 → 0b000_00000.
328        assert_eq!(clic_ctl_byte(0), 0b0000_0000);
329        // Priority 1 → top 3 bits = 001 → 0b001_00000 = 0x20.
330        assert_eq!(clic_ctl_byte(1), 0b0010_0000);
331        // Priority 7 → top 3 bits = 111 → 0xE0.
332        assert_eq!(clic_ctl_byte(7), 0b1110_0000);
333    }
334
335    #[test]
336    fn clic_ctl_byte_clamps_priority_to_three_bits() {
337        // 8 has bit 3 set, which falls outside NLBITS=3 — must wrap to 0.
338        assert_eq!(clic_ctl_byte(8), 0);
339        // 0xFF clamps to 0b111 → 0xE0.
340        assert_eq!(clic_ctl_byte(0xFF), 0b1110_0000);
341        // The bottom 5 bits of CTL must remain zero.
342        for p in 0..=255u8 {
343            assert_eq!(
344                clic_ctl_byte(p) & 0b0001_1111,
345                0,
346                "priority {} leaked into reserved bottom bits",
347                p
348            );
349        }
350    }
351
352    #[test]
353    fn int_map_value_writes_clic_index_in_low_six_bits() {
354        // For CPU line 1 → CLIC index 17, the matrix register receives 17.
355        // This catches the prior 2026-04-26 bug where we wrote `cpu_int_n`
356        // (= 1) instead of `clic_idx_for_cpu_line(1)` (= 17) and the
357        // peripheral never reached the CPU.
358        assert_eq!(int_map_value(17), 17);
359        assert_eq!(int_map_value(18), 18);
360        // Top bits ignored: even if upstream packs flags, only bits[5:0]
361        // hit the register.
362        assert_eq!(int_map_value(0xC0 | 17), 17);
363        // 6-bit max stays inside the field.
364        assert_eq!(int_map_value(0x3F), 0x3F);
365    }
366
367    #[test]
368    fn clic_threshold_value_packs_threshold_byte_into_high_byte() {
369        // 0x00 → pass everything → low 24 bits stay zero.
370        assert_eq!(clic_threshold_value(0x00), 0x0000_0000);
371        // 0xFF → block all → high byte = 0xFF.
372        assert_eq!(clic_threshold_value(0xFF), 0xFF00_0000);
373        // Any byte goes only into bits[31:24].
374        for t in 0..=255u8 {
375            let v = clic_threshold_value(t);
376            assert_eq!(v & 0x00FF_FFFF, 0, "threshold {:#x} leaked", t);
377            assert_eq!(v >> 24, t as u32);
378        }
379    }
380}