NVIC helpers
src/nvic.S is the minimal Cortex-M33 NVIC wrapper layer. It exists
because the NVIC has six different register banks for what is
conceptually "set/clear/enable/pending/priority for IRQ N", and
forgetting which is which is a great way to spend an afternoon staring
at a chip that won't IRQ.
Datasheet reference: ARMv8-M Architecture Reference Manual sec B11.1 (NVIC). RP2350 datasheet sec 3.2 lists the 52 external IRQ lines.
API
| Function | Inputs | Effect |
|---|---|---|
nvic_enable_irq |
r0 = irq_num |
ISER[irq>>5] |= 1 << (irq & 31) |
nvic_disable_irq |
r0 = irq_num |
ICER[irq>>5] |= 1 << (irq & 31) |
nvic_set_pending |
r0 = irq_num |
ISPR[irq>>5] |= 1 << (irq & 31) |
nvic_clear_pending |
r0 = irq_num |
ICPR[irq>>5] |= 1 << (irq & 31) |
nvic_set_priority |
r0 = irq_num, r1 = priority(0..15) |
IPR[irq] = priority << 4 |
nvic_install_handler |
r0 = irq_num, r1 = handler |
vec[16 + irq] = handler | 1 |
All clobber r0-r3 only. Caller-saved registers (r4-r11, sp,
lr) are preserved per the calling conventions.
IRQ numbers
RP2350 has 52 external IRQs (0..51). The canonical numbering is in
include/usb.inc, include/uart.inc, etc. -- each driver header
defines the IRQ constant it cares about (e.g. USBCTRL_IRQ = 14).
The list of names lives in the RP2350 datasheet sec 3.2; the most
commonly used:
0 TIMER0_IRQ_0 14 USBCTRL_IRQ 26 SIO_IRQ_BELL
4 TIMER1_IRQ_0 15 PIO0_IRQ_0 30 CLOCKS_IRQ
8 PWM_IRQ_WRAP_0 21 IO_IRQ_BANK0 33 UART0_IRQ
10 DMA_IRQ_0 25 SIO_IRQ_FIFO 36 I2C0_IRQ
39 TRNG_IRQ
Standard wiring pattern
The "install + enable" idiom every IRQ-using driver follows:
movs r0, #USBCTRL_IRQ @ which IRQ line
ldr r1, =usb_device_isr @ which handler
bl nvic_install_handler @ patches vec[16+14] = isr | 1
movs r0, #USBCTRL_IRQ
bl nvic_enable_irq @ NVIC_ISER bit on
After this, any USB event that ends up driving the USBCTRL line will
vector through vec[30] to usb_device_isr. The handler is entered
in Handler mode with MSP, and must return with a normal bx lr once
all sources are W1C'd in the peripheral.
If you also want to set a non-default priority (e.g. to allow another IRQ to preempt this one), do it between install and enable:
movs r0, #USBCTRL_IRQ
movs r1, #8 @ 0 = highest, 15 = lowest
bl nvic_set_priority
The scheduler (src/sched.S) is the only subsystem in the SDK that
relies on priorities for correctness; if you don't touch
nvic_set_priority, every IRQ is at priority 0 and they don't preempt
each other.
Why nvic_install_handler needs a RAM vector table
The handler-install function patches:
*VTOR + 0x40 + irq_num*4 = handler | 1
+0x40 skips the 16 ARMv8-M core vectors; *VTOR is read at runtime so
the function doesn't care where the table actually lives.
For SRAM-resident builds the table is at 0x20000000 (writable),
so the patch just works. For flash-resident builds the table
would otherwise live at 0x10000000 (XIP flash, read-only) and the
store silently fails. _reset in src/startup.S copies _vectors
into _ram_vectors (in .bss, 512-byte aligned) and points VTOR at
the copy before main runs, so handler installs work on both paths.
See boot.md for the full vector-relocation story.
Cycle costs
All five primitives are ~6-7 cycles in steady state (one ldr of the
base address from a literal pool, a couple of shifts to compute the
bit/index, one str). This matters when you're toggling an IRQ
enable in a hot path; if you only enable / install once at boot, the
cost is irrelevant.
| Function | Cycles |
|---|---|
nvic_enable_irq |
~6 |
nvic_disable_irq |
~6 |
nvic_set_pending |
~6 |
nvic_clear_pending |
~6 |
nvic_set_priority |
~7 |
nvic_install_handler |
~6 (one extra load for VTOR) |
See also
- boot.md: vector-table layout, RAM relocation, why
nvic_install_handlerneeds a writable table. - calling.md: ABI for handlers, scratch-register rules, W1C-the-peripheral convention.
- sched.md: uses
nvic_set_priorityextensively for the QV-style scheduler. examples/timer_alarm_demo.S,examples/usb_cdc_echo_demo.S,examples/sched_demo.S: worked examples of the install + enable pattern.