soc_esp32::intr_map
Peripheral interrupt routing: which CPU interrupt a peripheral fires on.
The Xtensa core has 32 interrupt inputs. The ESP32 has 69 peripheral sources. A crossbar in DPORT decides which of the 32 each of the 69 lands on, and until a source is pointed at an interrupt the CPU is enabled for, that peripheral cannot interrupt anything — its interrupt-enable bits set happily, its status bits assert, and nothing happens.
Everything the kernel handled before this existed was internal to the core: Timer0 on CPU interrupt 6 and the software interrupt on 7. Neither goes through the crossbar, which is why a driver could be written, enabled, and silently never serviced.
Where a source points before you route it
Section titled “Where a source points before you route it”Reset value is 5’d16 — CPU interrupt 16, an internal timer no peripheral
can reach. So the default is not “unconnected” but “parked somewhere
harmless”, and [park] puts a source back rather than clearing to zero.
Clearing to zero would aim it at CPU interrupt 0, which is a perfectly
usable external interrupt and might belong to something else.
Which interrupts a peripheral may use
Section titled “Which interrupts a peripheral may use”Two conditions, both load-bearing, both checked by
[can_serve_peripheral]:
- The input must be
EXTERN_LEVEL. Six of the 32 are wired to the core’s own timers, software interrupts, NMI or profiling and are not connected to the crossbar at all; four more areEXTERN_EDGE, which latch and need explicit acknowledgement the level path does not do. - It must be level 1.
vectors.Simplements level 1; levels 2 through 5 land in the “unhandled higher-level interrupt” stub. Routing a peripheral to level 2 produces a fault at the first interrupt instead of a call to its handler.
That leaves 0–5, 8, 9, 12, 13, 17 and 18, minus 6 and 7 which fail the first test anyway. Rejecting the rest here is the point: the failure is otherwise a board that boots and then dies at the first interrupt.
Sources
Section titled “Sources”DPORT_PRO_MAC_INTR_MAP_REG is source 0 at DPORT_BASE + 0x104, and the
table is indexed by source number from there. Verified against four
independent defines in dport_reg.h rather than assumed from one:
| Source | Peripheral | Header says | 0x104 + 4n |
|---|---|---|---|
| 10 | SLC0 | 0x12C | 0x12C |
| 34 | UART | 0x18C | 0x18C |
| 47 | RMT | 0x1C0 | 0x1C0 |
| 49 | I2C_EXT0 | 0x1C8 | 0x1C8 |
Levels and types come from the ESP32’s core-isa.h; the count from
ETS_MAX_INTR_SOURCE.
enum Core { Pro, App,}Which core’s crossbar table a route goes in.
Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq
RouteError
Section titled “RouteError”enum RouteError { NoSuchSource, Unusable,}Why a route was refused.
Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq
Functions
Section titled “Functions”can_serve_peripheral
Section titled “can_serve_peripheral”const fn can_serve_peripheral(cpu_int: u8) -> boolWhether a peripheral routed here would actually be serviced.
See the module header: external-level and level 1, because those are the
interrupts vectors.S has a handler for.
is_extern_level
Section titled “is_extern_level”const fn is_extern_level(cpu_int: u8) -> boolWhether the core input is a plain level-triggered external interrupt.
Excludes the core’s own timers (6, 15, 16), software interrupts (7, 29), profiling (11), the NMI (14), and the four edge-triggered inputs (10, 22, 28, 30) whose latching this kernel does not acknowledge.
const fn level(cpu_int: u8) -> Option<u8>The interrupt level cpu_int fires at, or None if there is no such input.
map_reg
Section titled “map_reg”const fn map_reg(source: u8) -> Option<u32>The crossbar register for a peripheral source.
map_reg_on
Section titled “map_reg_on”const fn map_reg_on(core: Core, source: u8) -> Option<u32>The crossbar register for a source in one core’s table.
References: Core
unsafe fn park(source: u8) -> Result<(), RouteError>Put source back where reset left it, so it can no longer interrupt.
Safety
Section titled “Safety”Writes a DPORT crossbar register.
References: RouteError
unsafe fn route(source: u8, cpu_int: u8) -> Result<(), RouteError>Point source at cpu_int.
The caller still has to enable cpu_int on the core and enable the
peripheral’s own interrupt bits; this only decides where it lands.
Safety
Section titled “Safety”Writes a DPORT crossbar register. Two peripherals may share a CPU interrupt — the handler then has to work out which fired — but routing a source that another driver owns will steal its interrupts.
References: RouteError
route_on
Section titled “route_on”unsafe fn route_on(core: Core, source: u8, cpu_int: u8) -> Result<(), RouteError>[route], naming which core the source should land on.
The two tables are independent: routing in one leaves the other parked, so this is how an interrupt is aimed at a particular core rather than merely enabled.
Safety
Section titled “Safety”As [route], and additionally: routing into the APP table before the APP
CPU is running arms an interrupt nothing will service.
References: Core, RouteError
routed_to
Section titled “routed_to”unsafe fn routed_to(source: u8) -> Option<u8>Read back where a source currently points.
Safety
Section titled “Safety”Reads a DPORT register. No side effects.
Constants
Section titled “Constants”PARKED
Section titled “PARKED”const PARKED: u8;Where a source points until something routes it.
CPU interrupt 16 is an internal timer, so a parked source is inert rather than merely unconfigured.
SOURCE_COUNT
Section titled “SOURCE_COUNT”const SOURCE_COUNT: u8;ETS_MAX_INTR_SOURCE. Valid sources are 0..SOURCE_COUNT.