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 | 1–31 (line 0 = "no interrupt") |
1–31 |
| 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:
- Programs each CPU's interrupt mask (
IMASK[cpu], offset0x40 + 4*cpu) to enable the lines it cares about. - Reads the pending register (
IPR,0x04) — set by external sources. - On taking an interrupt trap, the controller auto-clears the corresponding pending (or force) bit (no explicit acknowledge write needed).
- Forces an interrupt for software self-IPI via the force registers
(
IFR0/IFORCE[cpu]). - Releases secondary CPUs from power-down by writing
MPSTATlow 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+kclears force bitk. - The lower 16 bits are write-1-to-set: a 1 in bit
ksets force bitk.
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)ORs1 << EIRQinto 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 frompending_ & IMASK[cpu] & 0xFFFF0000, clears its bit, and writes its full indexN ∈ [16,31]intoEID[cpu](irqmp.cpp:107). If none is pending it writesEID[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
0x100–0x108, read-as-zero. RTEMS'leon3_counter_initializeprobesTSTAMP 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¶
- Traps and interrupts —
PSR.PILgating andenter_trap. - Peripheral system § IRQ wiring —
the
IrqBridgeadapter. - APBUART § Extended-IRQ delivery.