Skip to content

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.

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.

Two conditions, both load-bearing, both checked by [can_serve_peripheral]:

  1. 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 are EXTERN_EDGE, which latch and need explicit acknowledgement the level path does not do.
  2. It must be level 1. vectors.S implements 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.

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

enum RouteError {
NoSuchSource,
Unusable,
}

Why a route was refused.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

const fn can_serve_peripheral(cpu_int: u8) -> bool

Whether 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.

const fn is_extern_level(cpu_int: u8) -> bool

Whether 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.

const fn map_reg(source: u8) -> Option<u32>

The crossbar register for a peripheral source.

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.

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.

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

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.

As [route], and additionally: routing into the APP table before the APP CPU is running arms an interrupt nothing will service.

References: Core, RouteError

unsafe fn routed_to(source: u8) -> Option<u8>

Read back where a source currently points.

Reads a DPORT register. No side effects.

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.

const SOURCE_COUNT: u8;

ETS_MAX_INTR_SOURCE. Valid sources are 0..SOURCE_COUNT.