Skip to content

IRQMP / IRQAMP — Interrupt controllers

GRLIB IRQMP (GR712RC §8 / GRLIB IP core) is the cross-bar that turns external interrupt assertions into per-CPU pending bitmaps. The GR740 ships a sibling IP core, IRQAMP (GR740 §16), modelled by a separate class. Both implement the common IInterruptController interface (src/interfaces/include/tero/iinterrupt_controller.hpp) so the IrqBridge and Emulator::sample_interrupts are SoC-agnostic.

Property IRQMP (GR712RC) IRQAMP (GR740)
Class peripherals::IrqMP peripherals::IrqAMP
Default MMIO base 0x80000200 0xFF904000
MMIO window size 0x100 0x200 (adds TIMESTAMP block)
CPUs serviced up to 4 (recipe uses 2) up to 4 (recipe uses 4)
IRQ lines 131 (line 0 = "no interrupt") 131
Source src/peripherals/src/irqmp.cpp src/peripherals/src/irqamp.cpp

Using it

Driver-visible model

From a guest's point of view the controller is a bank of word-aligned MMIO registers. The RTEMS LEON3 BSP:

  1. Programs each CPU's interrupt mask (IMASK[cpu], offset 0x40 + 4*cpu) to enable the lines it cares about.
  2. Reads the pending register (IPR, 0x04) — set by external sources.
  3. On taking an interrupt trap, the controller auto-clears the corresponding pending (or force) bit (no explicit acknowledge write needed).
  4. Forces an interrupt for software self-IPI via the force registers (IFR0/IFORCE[cpu]).
  5. Releases secondary CPUs from power-down by writing MPSTAT low bits.

Register map

All registers are 32-bit, word-aligned. Byte and half-word accesses return ErrorCode::AlignmentError (is_word_access guard). The per-CPU register arrays use a 4-byte stride (CpuRegStride).

Offset Name Access Description
0x000 ILR R/W Interrupt level register (priority bit per line). Stored, not yet used by delivery.
0x004 IPR R Interrupt pending register (shared asserted lines). Write ignored.
0x008 IFR0 R/W Force register, CPU 0.
0x00C ICR W (1-to-clear) Interrupt clear register. Reads as 0.
0x010 MPSTAT partial R/W Multiprocessor status (see below).
0x014 BROADCAST R/W Broadcast mask (which lines fan out to all CPUs as forced).
0x040 + 4*cpu IMASK[cpu] R/W Per-CPU interrupt mask (1 = enabled).
0x080 + 4*cpu IFORCE[cpu] R/W (high half W1C) Per-CPU force register.
0x0C0 + 4*cpu EID[cpu] R Extended interrupt ID for that CPU.

IRQAMP additionally exposes a TIMESTAMP block (read-as-zero):

Offset Name (IRQAMP) Behaviour
0x100 TSTAMP control Reads 0; writes accepted and dropped.
0x104 TSTAMP value Reads 0 — [31:27] == 0 tells RTEMS "no timestamp counter".
0x108 TSTAMP latch Reads 0; writes accepted and dropped.

MPSTAT layout

MPSTAT is read-mostly with a write-1-to-wake low half:

Bits Field Meaning Reset
31:28 NCPU Number of CPUs minus 1 num_cpus - 1
27 BA Broadcast available 1
19:16 EIRQ Extended-IRQ redirect level 12
15:0 STATUS Per-CPU power-down status / wake request depends on cores

Writing a 1 into STATUS bit i requests a wake of CPU i (drained by consume_mpstat_wake); this is how RTEMS releases secondary cores during SMP boot. The NCPU/BA/EIRQ fields are read-only — a write recomputes them from mpstat_reset() and keeps only the writable high bits (write_offset, irqmp.cpp:222).

Reset value

MPSTAT = ((num_cpus-1) << 28) | (1 << 27) | (12 << 16). With the GR712RC recipe's 2 CPUs that is 0x100C0000.

IFORCE / IFR write semantics

IFORCE[cpu] (and IFR0) use a clear-then-set protocol matching GRLIB hardware, so software can atomically toggle force state in one MMIO write:

  • The upper 16 bits of the written value are write-1-to-clear: a 1 in bit 16+k clears force bit k.
  • The lower 16 bits are write-1-to-set: a 1 in bit k sets force bit k.

In code (irqmp.cpp:248):

auto clear_mask = (value >> 16) & IforceReadMask;   // IforceReadMask = 0x0000FFFE
iforce_[cpu] &= ~clear_mask;                          // clear (high half)
iforce_[cpu] |=  value & IforceReadMask;              // set   (low half)

Reads of IFORCE[cpu] mask to 0x0000FFFE — only standard IRQs 1..15 are visible (bit 0 does not exist; bits 16..31 are extended IRQs that read as 0).

Extended IRQs (lines 16..31)

SPARC V8 PSR.PIL is 4 bits, so an interrupt above level 15 cannot be delivered directly. The controller redirects any masked pending line in [16, 31] to the level encoded in MPSTAT.EIRQ (default 12). This is how GR712RC UART1..5 (IRQ 17..21) and GR740 UART0/1 (IRQ 29/30) reach the CPU.

  • pending_mask(cpu) ORs 1 << EIRQ into the returned mask whenever any masked extended bit is set (irqmp.cpp:166).
  • On EIRQ-level acknowledge (acknowledge(cpu, 1 << EIRQ)) the controller pops the highest pending extended IRQ from pending_ & IMASK[cpu] & 0xFFFF0000, clears its bit, and writes its full index N ∈ [16,31] into EID[cpu] (irqmp.cpp:107). If none is pending it writes EID[cpu] = 0.

The RTEMS irqmp.c handler reads EID[cpu] first: non-zero = the extended IRQ to dispatch, zero = a standard IRQ at the EIRQ level. See the APBUART page for the UART side.


Internals / how it's modelled

IInterruptController API

Other peripherals and the runtime drive the controller through this interface (not through MMIO). The IrqBridge adapter (src/runtime/include/tero/runtime/emulator.hpp:55) wraps a peripheral's IInterruptSource::raise()/lower() into these calls:

void     external_assert(uint32_t bits) noexcept;   // raise()  → set pending bits
void     external_clear (uint32_t bits) noexcept;   // lower()  → clear pending bits
uint32_t pending_mask(uint32_t cpu) const noexcept; // effective mask for one CPU
void     acknowledge(uint32_t cpu, uint32_t bit) noexcept;  // auto-clear on trap delivery
bool     consume_mpstat_wake(uint32_t cpu) noexcept;        // drain SMP wake request
void     set_thread_safe(bool active) noexcept;             // engage the GatedMutex under MT

external_assert / external_clear

external_assert(bits) splits bits against the BROADCAST mask (irqmp.cpp:57): non-broadcast bits go into the shared pending vector (IPR); broadcast bits are OR'd into every per-CPU force register, so a broadcast line reaches all CPUs. external_clear(bits) clears the shared pending bits. Both notify ctx_.observer (on_irq_raised/on_irq_lowered).

pending_mask(cpu) — what the core sees

Emulator::sample_interrupts calls this once per quantum (emulator.cpp:1292):

mask  = pending_;                 // shared asserted lines
if (cpu == 0) mask |= ifr0_;      // CPU 0 also sees the legacy IFR0
mask |= iforce_[cpu];             // plus this CPU's force register
mask &= imask_[cpu];              // gate by this CPU's mask
if (EIRQ != 0 && (mask & 0xFFFF0000)) mask |= (1 << EIRQ);  // extended redirect

The runtime then scans bits 15..1 for the highest set level, compares it to PSR.PIL, and (if level > PIL and PSR.ET) injects a hardware trap with tt = 0x10 + level (emulator.cpp:1334). See Traps and interrupts.

acknowledge(cpu, bit) — auto-clear on trap delivery

Per GR712RC §8 / GR740 §16: when a processor takes an interrupt trap the controller automatically clears the corresponding bit. sample_interrupts invokes acknowledge() immediately before enter_trap (emulator.cpp:1339).

Force-precedence rule (irqmp.cpp:122): if the line was sourced from a force register (IFR0 for CPU 0, or IFORCE[cpu]), the force bit is cleared instead of the shared pending bit. Only when neither force register holds the bit is pending_ cleared. This mirrors real hardware, where a forced interrupt takes priority over an externally-asserted one on the same line:

if (iforce_[cpu] & bit)            { iforce_[cpu] &= ~bit; forced = true; }
if (cpu == 0 && (ifr0_ & bit))     { ifr0_ &= ~bit;       forced = true; }
if (!forced)                       { pending_ &= ~bit; }

The extended-IRQ ack path (described above) runs first when bit == (1 << EIRQ).

ICR — bulk write-1-clear

A write to ICR (0x0C) clears the named bits across pending_, ifr0_, and every iforce_[cpu] in one shot (irqmp.cpp:209), held under the controller's GatedMutex so it cannot interleave a concurrent acknowledge under MultiThread.

Thread safety

The fields are plain std::uint32_t; under ExecutionMode::MultiThread every concurrent access goes through a transient std::atomic_ref (the Inc-3 idiom, seq_cst), and the compound read-decide-write sequences (acknowledge, ICR, IFORCE clear-then-set) are serialised by a GatedMutex that is a no-op in SingleThread. The single-field bitwise RMWs on the raise/lower fast path stay lock-free. See ADR-001.

Reset state

reset() (irqmp.cpp:45) sets IPR=0, IFR0=0, all IFORCE[*]=0, all IMASK[*]=0 (everything masked), BROADCAST=0, EID[*]=0, wake_pending_=0, and MPSTAT to its computed reset value.


IRQAMP — the GR740 sibling

peripherals::IrqAMP (src/peripherals/src/irqamp.cpp) duplicates the classic IRQMP register subset (ILR / IPR / IFR0 / ICR / MPSTAT / BROADCAST and the per-CPU IMASK / IFORCE / EID blocks) with identical semantics — the pending_mask, acknowledge, extended-IRQ, broadcast, and clear-then-set logic are line-for-line the same. The differences:

  • TIMESTAMP block at 0x1000x108, read-as-zero. RTEMS' leon3_counter_initialize probes TSTAMP value [31:27]; finding zero, it treats the controller as having no high-resolution timestamp counter and falls through to its next counter source. Writes to the block are accepted and dropped (so optional RTEMS configuration writes never bus-error).
  • Larger MMIO window (0x200) to cover the TIMESTAMP block and reserve space for AMP / AHB-error-trap registers (modelled as read-as-zero stubs until a guest exercises them).
  • Default 4 CPUs (vs the IRQMP's recipe default of 2).

Modelling them as siblings rather than a base/derived pair is deliberate: GR740 behaviour must not perturb the GR712RC path even at the cost of duplicated register code.

Tests

  • tests/unit/test_irqmp.cpp — register-by-register coverage of the IFORCE clear-then-set semantics, pending_mask, external_assert, broadcast propagation, ICR write-1-clear, and MPSTAT writes that release a CPU.
  • tests/integration/test_rtems_* — exercise the controller through full RTEMS BSP boot (uniprocessor and SMP).

See also