Interrupt Controller

Introduction

In On Interrupts I have outlined a few thoughts on the use of interrupts vs. alternative solutions implemented in the FPGA. Here we now assume that we actually need an interrupt-based solution, and we have more than one interrupt source.

Requirements

  • More than one interrupt source.

  • One input signal line per interrupt source, active high, edge detected.

  • A specific interrupt handler procedure per interrupt.

  • Use the existing RISC5 interrupt mechanism, ie. no changes allowed here.

  • Mimic the existing RISC5 interrupt processing, eg. regarding handler invocation, or signal polarity, hence existing solutions should work without changes, apart from “administrative” aspects such as handler installation and interrupt enabling and disabling, and of course the re-wiring of the interrupt signals at the hardware level.

Solution Concept

The RISC5 interrupt mechanism expects the interrupt handler invocation code – a branch instruction to the handler’s address – at address 4. The CPU loads address 4 into the program counter (PC)1, and consequently the handler invocation code into the instruction register (IR).

With no changes allowed to the RISC5 interrupt handling mechanism, there are two basic solution approaches to allow several interrupts, each with their own handler procedure:

  1. Store the handler invocation code at RAM address 4 just before activating the CPU’s interrupt line, or
  2. make the CPU load the handler invocation code from a different address outside the RAM address range.

Approach 1 would entail access arbitrage to the RAM, between the interrupt controller and the CPU, which would be possible by stalling the CPU while the controller stores the handler invocation code at address 4. Embedded Oberon’s CPU does not have an external stall signal line, but this could be added analogously to Project Oberon’s external stall signal used for the video circuitry’s RAM access.

Approach 2 means to add another term to the codebus de-muxer in RISC5Top.v, so that the handler invocation code is loaded similar to reading from the PROM during system startup.

I have chosen to use approach 2. It allows the interrupt controller to do do its fetch-and-store of the interrupt invocation code in parallel to the CPU’s current work, hence no CPU cycles are wasted. Also, introducing the external stall signal, as required for approach 1, would mean a few changes of the stall mechanism in the RISC5 CPU, and I didn’t want to go there.

A specific interrupt is invoked by this simple sequence of steps:

  1. Upon activating an interrupt input signal of the interrupt controller, the corresponding handler invocation code is read from a table and put into a buffer, from where it will be loaded by the CPU. Remember that the RISC5 interrupt mechanism can load the invocation code only from one specific address location. It does not have a notion of several interrupts and interrupt handlers.

  2. Activate the CPU interrupt signal line.

To enable this behaviour, the software needs to store the handler invocation code for each interrupt in said table upfront, and enable the interrupt (as well as the interrupt handling by the CPU, of course) – in-line with the preparation of the single RISC5 interrupt.

Implementation

A workable solution occurred to me when studying the timing diagram in the RISC5 Update document: the RISC5 interrupt mechanism in fact provides exactly the two signals I need, intAck and RTI.

I could use

  • intAck to do the codebus switch-over
  • RTI to signal the end of interrupt handling

In my RISC5Top.v:

assign codebus = intack ? intout : (adr[20] == 1'b1) ? romout : inbus0;

I just needed to “pull out” intAck from module RISC5, represented in RISC5Top.v as intack in the above line. The CPU reads the interrupt invocation code from intout, with value 1 (ie. address 4) in the PC, hence determining the correct address for the branch instruction. It actually works. Yay.

RTI is “pulled out” as well, as input for the interrupt controller to determine when the handling of one interrupt is over, and the next can be initiated.

  • All interrupts signals into the interrupt controller can be activated at the same time, or one signal can become active while another interrupt is being handled. All interrupts are pended until they can be handled. Activating an already pended interrupt is a no-op, it just stays pended.

  • There are no explicit interrupt priorities, but all pending interrupts will be handled in a sequence with lowest interrupt number first.

  • This means that, say, interrupt 0 could prevent all other interrupts from being handled, if the corresponding interrupt input signal is asserted again during its handling. I do have a variant of the interrupt controller that prevents this continuous assertion, but the added complexity is probably not warranted in most cases.

  • An interrupt can also be triggered from software.

  • The current implementation has eight interrupt input signal lines. It could be any reasonable number. The design is parameterised for this.

  • To set up the table in the interrupt controller with the invocation codes for all interrupts to be used, standard memory mapped IO is used.

  • The interrupt controller adds one clock cycle to the total interrupt handling latency.

Interrupts.mod

Here’s the current API for the interrupt controller.

MODULE Interrupts;

  IMPORT SYSTEM;

  CONST
    (* IO addresses *)
    DataAdr = -172;
    CtrlAdr = -168;

    (* branch instruction *)
    BR = 0E7000000H;

    (* control data *)
    SNUM = 1; (* set handler number *)
    IEN  = 2; (* interrupt enable *)
    IDI  = 4; (* interrupt disable *)
    SWI  = 8; (* software trigger *)

    (* shift for control data *)
    DataShift = 8;

  PROCEDURE Install*(isr: PROCEDURE; no: INTEGER);
  BEGIN
    SYSTEM.PUT(CtrlAdr, SNUM + LSL(no, DataShift));
    SYSTEM.PUT(DataAdr, BR + SYSTEM.VAL(INTEGER, isr) DIV 4 - 2)
  END Install;

  PROCEDURE* Enable*(ints: SET);
  BEGIN
    SYSTEM.PUT(CtrlAdr, IEN + LSL(ORD(ints), DataShift))
  END Enable;

  PROCEDURE* Disable*(ints: SET);
  BEGIN
    SYSTEM.PUT(CtrlAdr, IDI + LSL(ORD(ints), DataShift))
  END Disable;

  PROCEDURE GetEnabled*(VAR en: INTEGER);
  BEGIN
    SYSTEM.GET(CtrlAdr, en)
  END GetEnabled;

  PROCEDURE Trigger*(no: INTEGER);
  BEGIN
    SYSTEM.PUT(CtrlAdr, SWI + LSL(ORD({no}), DataShift))
  END Trigger;
  
  PROCEDURE EnableCPU*;
  BEGIN
    SYSTEM.LDPSR(1)
  END EnableCPU;

END Interrupts.

Using the API, interrupt handlers are installed and enabled thusly (excerpt from Errors.mod):

MODULE Errors;
  (* ... *)
  PROCEDURE Install*;
  BEGIN
    SysCtrl.ResetErrorState;
    handlingError := FALSE;

    (* install trap and abort handlers *)
    Kernel.Install(SYSTEM.ADR(trap), 20H);
    Kernel.Install(SYSTEM.ADR(abort), 0);

    (* interrupt handlers *)
    (* watchdog *)
    Interrupts.Install(watchdog, WatchdogIntNum);
    IF Watchdog.Enabled() THEN Interrupts.Enable({WatchdogIntNum}) END;

    (* kill abort *)
    Interrupts.Install(killabort, KillIntNum);
    Interrupts.Enable({KillIntNum});

    (* stack overflow monitor *)
    Interrupts.Install(stackoverflow, StackOvflIntNum);
    Interrupts.Enable({StackOvflIntNum});

    (* not alive monitor *)
    (* triggered by software *)
    Interrupts.Install(notalive, NotAliveIntNum);
    Interrupts.Enable({NotAliveIntNum});

    (* enable interrupt handling at the CPU *)
    Interrupts.EnableCPU
  END Install;
END Errors.

Remarks regarding Enable and Disable:

  • In lieu of directly writing an enable/disable bitmap, the operations are separated.
  • Any bit set in the ints SET parameter of Enable enables the corresponding interrupt. Unset bits don’t matter, ie. you cannot disable an interrupt using this procedure.
  • Analogously, any bit set in the ints SET parameter of Disable disables the corresponding interrupt, unset bits don’t matter.
  • This allows one step enabling and disabling, without first reading back the current bitmap.
  • The enabled/disabled bitmap can be read using GetEnabled to get the status for whatever reason, eg. debugging.

  1. Actually, the value 1 is loaded into the PC, as the PC is 4-byte-aligned and omits the two least significant address bits. ↩︎