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}