This document specifies the Interrupt Controller (RV_PLIC) functionality. This module conforms to the Comportable guideline for peripheral functionality. See that document for integration overview within the broader top level system.
The RV_PLIC module is designed to manage various interrupt sources from the peripherals. It receives interrupt events as either edge or level of the incoming interrupt signals (intr_src_i
) and can notify multiple targets.
The RV_PLIC is compatible with any RISC-V core implementing the RISC-V privilege specification.
Each interrupt source has a unique ID assigned based upon its bit position within the input intr_src_i
. ID ranges from 0 to N, the number of interrupt sources. ID 0 is reserved and represents no interrupt. The bit 0 of intr_src_i
shall be tied to 0 from the outside of RV_PLIC. The intr_src_i[i]
bit has an ID of i
. This ID is used when targets “claim” the interrupt and to “complete” the interrupt event.
Interrupt sources have configurable priority values. The maximum value of the priority is configurable through the localparam MAX_PRIO
in the rv_plic top-level module. For each target there is a threshold value (THRESHOLD0
for target 0). RV_PLIC notifies a target of an interrupt only if it‘s priority is strictly greater than the target’s threshold. Note this means an interrupt with a priority is 0 is effectively prevented from causing an interrupt at any target and a target can suppress all interrupts by setting it's threshold to the max priority value.
MAX_PRIO
parameter is most area contributing option in RV_PLIC. If MAX_PRIO
is big, then finding the highest priority in Process module may consume a lot of logic gates.
The Gateway observes incoming interrupt sources and converts them to a common interrupt format used internally by RV_PLIC. It can be parameterized to detect interrupts events on an edge (when the signal changes from 0 to 1) or level basis (where the signal remains at 1). The choice is a system-integration decision and can be configured via the design parameter LevelEdgeTrig
for each interrupt request.
When the gateway detects an interrupt event it raises the interrupt pending bit (IP
) for that interrupt source. When an interrupt is claimed by a target the relevant bit of IP
is cleared. A bit in IP
will not be reasserted until the target signals completion of the interrupt. Any new interrupt event between a bit in IP
asserting and completing that interrupt is ignored. In particular this means that for edge triggered interrupts if a new edge is seen after the source's IP
bit is asserted but before completion, that edge will be ignored (counting missed edges as discussed in the RISC-V PLIC specification is not supported).
Note that there is no ability for a level triggered interrupt to be cancelled. If the interrupt drops after the gateway has set a bit in IP
, the bit will remain set until the interrupt is completed. The SW handler should be conscious of this and check the interrupt still requires handling in the handler if this behaviour is possible.
Each target has a set of Interrupt Enable (IE0
for target 0) registers. Each bit in the IE0
registers controls the corresponding interrupt source. If an interrupt source is disabled for a target, then interrupt events from that source won‘t trigger an interrupt at the target. RV_PLIC doesn’t have a global interrupt disable feature.
“Claiming” an interrupt is done by a target reading the associated Claim/Completion register for the target (CC0
for target 0). The return value of the CC0
read represents the ID of the pending interrupt that has the highest priority. If two or more pending interrupts have the same priority, RV_PLIC chooses the one with lowest ID. Only interrupts that that are enabled for the target can be claimed. The target priority threshold doesn't matter (this only factors into whether an interrupt is signalled to the target) so lower priority interrupt IDs can be returned on a read from CC0
. If no interrupt is pending (or all pending interrupts are disabled for the target) a read of CC0
returns an ID of 0.
After an interrupt is claimed, the relevant bit of interrupt pending (IP
) is cleared, regardless of the status of the intr_src_i
input value. Until a target “completes” the interrupt, it won't be re-asserted if a new event for the interrupt occurs. A target completes the interrupt by writing the ID of the interrupt to the Claim/Complete register (CC0
for target 0). The write event is forwarded to the Gateway logic, which resets the interrupt status to accept a new interrupt event. The assumption is that the processor has cleaned up the originating interrupt event during the time between claim and complete such that intr_src_i[ID]
will have de-asserted (unless a new interrupt has occurred).
{ signal: [ { name: 'clk', wave: 'p...........' }, { name: 'intr_src_i[i]', wave: '01....0.1...', node:'.a....e.f...'}, { name: 'irq_o', wave: '0.1.0......1', node:'..b.d......h'}, { name: 'irq_id_o', wave: '=.=.=......=', data: ["0","i","0","i"] }, { name: 'claim', wave: '0..10.......', node:'...c........'}, { name: 'complete', wave: '0.........10', node:'..........g.'}, ], head:{ text: 'Interrupt Flow', tick: 0, }, }
In the example above an interrupt for source ID i
is configured as a level interrupt and is raised at a, this results in the target being notified of the interrupt at b. The target claims the interrupt at c (reading i
from it‘s Claim/Complete register) so irq_o
deasserts though intr_src_i[i]
remains raised. The SW handles the interrupt and it drops at e. However a new interrupt quickly occurs at f. As complete hasn’t been signaled yet irq_o
isn‘t asserted. At g the interrupt is completed (by writing i
to it’s Claim/Complete register) so at h irq_o
is asserted due to the new interrupt.
After reset, RV_PLIC doesn't generate any interrupts to any targets even if interrupt sources are set, as all priorities and thresholds are 0 by default and all IE
values are 0. Software should configure the above three registers.
PRIO0
.. PRIO31
registers are unique. So, only one of the targets shall configure them.
// Pseudo-code below void plic_init() { // Configure priority // Note that PRIO0 register doesn't affect as intr_src_i[0] is tied to 0. for (int i = 0; i < N_SOURCE; ++i) { *(PRIO + i) = value(i); } } void plic_threshold(tid, threshold) { *(THRESHOLD + tid) = threshold; } void plic_enable(tid, iid) { // iid: 0-based ID int offset = ceil(N_SOURCE / 32) * tid + (iid >> 5); *(IE + offset) = *(IE + offset) | (1 << (iid % 32)); }
If software receives an interrupt request, it is recommended to follow the steps shown below (assuming target 0 which uses CC0
for claim/complete).
CC0
register.CC0
register.CC0
It is possible to have multiple interrupt events claimed. If software claims one interrupt request, then the process module advertises any pending interrupts with lower priority unless new higher priority interrupt events occur. If a higher interrupt event occurs after previous interrupt is claimed, the RV_PLIC IP advertises the higher priority interrupt. Software may utilize an event manager inside a loop so that interrupt claiming and completion can be separated.
void interrupt_service() { uint32_t tid = /* ... */; uint32_t iid = *(CC + tid); if (iid == 0) { // Interrupt is claimed by one of other targets. return; } do { // Process interrupts... // ... // Finish. *(CC + tid) = iid; iid = *(CC + tid); } while (iid != 0); }
As a reference, default interrupt service routines are auto-generated for each IP, and are documented here.
The RV_PLIC in the top level is generated by topgen tool so that the number of interrupt sources may be different.