Capstone: an APB peripheral with a register map

Build a GPIO-and-timer peripheral on APB from a register map written once. Decode, read mux, write enables and status bits are all named from the same table, with a write-1-to-clear status and an interrupt line.

Lesson
R8.1
Time
40 min
Runs on
Your browser
Check
5 questions

A GPIO-and-timer block had its register map in a spreadsheet and its RTL written from memory. Two status bits were swapped between them. The driver enabled the timer interrupt at bit 0, as the map said, and the interrupt fired when pin 0 rose instead.

Event (IRQEN = TMR only)The map saysShipped RTLRTL that names the map
Pin 0 risesSTATUS 0x02, irq stays 0STATUS 0x01, irq rises at edge 12STATUS 0x02, irq stays 0
The timer expiresSTATUS 0x01, irq risesSTATUS 0x02, irq stays 0STATUS 0x01, irq rises at edge 24
Write 0x01 to STATUSTMR clears, irq fallsnothing clears: STATUS still 0x02STATUS 0x00, irq falls at edge 29

Measured on the lab bench. The protocol checker reports 0 rules broken for both designs: every wire was legal.

One table, read four ways

A peripheral is a function (pins, a timer) behind a register map: a table of addresses, fields and access types that software reads and writes. The map is the agreement between the hardware, the driver and the testbench. Write it once, then derive everything from it.

OffsetRegisterAccessFieldsReset
0x00DIRread/write[7:0] 1 drives the pin from OUT0x00
0x04OUTread/write[7:0] the value driven0x00
0x08INread-only[7:0] the pins, through a two-flop synchronizerpins
0x0cCTRLread/write[0] EN: the timer runs0
0x10PERIODread/write[7:0] cycles from one timer event to the next0x00
0x14STATUSwrite-1-to-clear[0] TMR timer expired, [1] PIN pin 0 rose0
0x18IRQENread/write[0] TMR, [1] PIN: which STATUS bits drive irq0

The brief's register map. Fields sit in byte 0 and change only when PSTRB[0] is 1.

In the RTL the table becomes constants: one localparam per address and one per bit position. Then each part of the completer reads a row of it.

Decode, write enables, read mux and event bits, each derived from the map. A number typed from memory is where a swap hides.
localparam logic [7:0] A_DIR = 8'h00, A_OUT = 8'h04, A_IN = 8'h08, A_CTRL = 8'h0c,
                       A_PERIOD = 8'h10, A_STATUS = 8'h14, A_IRQEN = 8'h18;
localparam int TMR = 0, PIN = 1;          // bit positions in STATUS and IRQEN

wire known = (a == A_DIR) || (a == A_OUT) || /* ... one term per row */ (a == A_IRQEN);
if (wr && a == A_PERIOD) period_q <= pwdata[7:0];   // a write enable per writable row
case (a) A_STATUS: prdata[1:0] = status_q; /* ... a read arm per row */ endcase
if (tmr_ev) status_d[TMR] = 1'b1;                    // an event sets its named bit
The peripheral behind the completerArrows show values. The completer decodes the address once; the register rows feed the GPIO, the timer and the interrupt; events come back into STATUS.
The peripheral behind the completerArrows show values. The completer decodes the address once; the register rows feed the GPIO, the timer and the interrupt; events come back into STATUS. component APB completer decode from the map;act at completion component DIR, OUT, CTRL,PERIOD, IRQEN read/write rows component GPIO pins sync2 on the inputs;pin 0 rise component Timer counts to PERIOD(R2.7) component STATUS write-1-to-clear;events set bits output irq registered OR ofSTATUS & IRQEN write enablesDIR, OUTEN, PERIODPIN eventTMR eventenabled bits
  1. Program

    A write to PERIOD completes; its write enable, named from the map, loads the register. A write to CTRL starts the timer.

    Follow the calls, values and waits
    1. data write PERIOD: APB completer → DIR, OUT, CTRL, PERIOD, IRQEN
    2. data write PERIOD: DIR, OUT, CTRL, PERIOD, IRQEN → Timer
  2. Event

    At the last cycle of a period the timer raises its event. The event sets status_d[TMR], the bit the map calls TMR.

    Follow the calls, values and waits
    1. data timer event: Timer → STATUS
  3. Interrupt

    When an enabled STATUS bit is 1, irq rises one edge later. It stays high until software writes 1 to that bit.

    Follow the calls, values and waits
    1. data timer event: STATUS → irq
Read the diagram as text
  • APB completer: decode from the map; act at completion
  • DIR, OUT, CTRL, PERIOD, IRQEN: read/write rows
  • GPIO pins: sync2 on the inputs; pin 0 rise
  • Timer: counts to PERIOD (R2.7)
  • STATUS: write-1-to-clear; events set bits
  • irq: registered OR of STATUS & IRQEN
  • APB completer → DIR, OUT, CTRL, PERIOD, IRQEN: write enables
  • DIR, OUT, CTRL, PERIOD, IRQEN → GPIO pins: DIR, OUT
  • DIR, OUT, CTRL, PERIOD, IRQEN → Timer: EN, PERIOD
  • GPIO pins → STATUS: PIN event
  • Timer → STATUS: TMR event
  • STATUS → irq: enabled bits

The completer of R3.5 in front, the timer of R2.7 and the pins of R3.5.1 behind.

An access type is one line of RTL

Access typeA write doesThe line
read/writestores the written bitsif (wr && a == A_OUT) out_q <= pwdata[7:0];
read-onlynothing, or an error: the brief decidesbad = ... || (pwrite && a == A_IN);
write-1-to-clearclears each bit written 1, leaves bits written 0status_d = status_d & ~pwdata[1:0];
set by hardwarenothing: an event sets the bitif (tmr_ev) status_d[TMR] = 1'b1;

Write-1-to-clear (W1C) lets software clear one status bit without a read first, so it never clears an event it has not seen.

The order of the two STATUS lines matters. The clear comes first and the events second, so an event at the same edge as its clear wins. Measured with the timer firing every cycle, a clear of TMR leaves it at 1. Losing an event is worse than seeing it twice.

A write to the read-only IN register needs a decision. The APB specification lets a completer refuse a transfer with PSLVERR or support no errors at all. This brief refuses: a driver that writes IN has a bug, and an error shows it at once.

A timer event, the interrupt and a write-1-to-clearIRQEN enables TMR only. tmr_ev is the timer's event; w1c marks the ACCESS cycle of the write of 0x01 to STATUS. Values in the cycle after edges 21 to 29.
012345678clktmr_evw1cstatus000100irq
  1. Cycle 1

    After edge 22 the counter is in the last cycle of its period, so tmr_ev is 1.

  2. Cycle 2

    Edge 23 sets STATUS bit 0, TMR. The interrupt register has not seen it yet.

  3. Cycle 3

    Edge 24: irq rises, one edge after STATUS, because it is the registered OR of STATUS and IRQEN.

  4. Cycle 6

    The write of 0x01 to STATUS is in its ACCESS cycle. Edge 28 is its completion edge.

  5. Cycle 7

    Edge 28 clears TMR. irq is still 1: it follows STATUS one edge later.

  6. Cycle 8

    Edge 29: irq falls. The line stayed high from the event until software cleared its bit.

Measured on the lab bench with the corrected design. The log prints e24 irq=1 and e29 irq=0.

The interrupt is a level: it stays high while any enabled bit is set. A flop on it keeps glitches off a wire that leaves the block, at the cost of one cycle.

The lab: derive it from the table

Design brief
PortDirectionWidthMeaning
pclkinput1APB clock
presetninput1APB reset, asynchronous, active-low
pselinput1APB select
penableinput1APB access phase
pwriteinput1APB write
paddrinput[31:0]APB address
pwdatainput[31:0]APB write data
pstrbinput[3:0]APB write strobes
pprotinput[2:0]APB protection
preadyoutput1APB ready
prdataoutput[31:0]APB read data
pslverroutput1a write to IN or any unmapped address: PSLVERR, no change
gpio_ininput[7:0]the pins, asynchronous to pclk
gpio_outoutput[7:0]the pin values driven out
gpio_oeoutput[7:0]the pin output enables
irqoutput1registered: 1 while an enabled STATUS bit is 1
Reset
PRESETn asynchronous, active-low; every register 0
Latency
no wait states; every effect at the completion edge; irq one edge after STATUS
Parameters
Registers: the map above; fields in byte 0, written only when PSTRB[0] is 1. Timer: while EN is 1, an event every PERIOD cycles (0 and 1: every cycle)
Corner cases
an event and its clear at the same edge (the event wins); a write of 0 to STATUS; PSTRB[0] at 0

The bench drives the pads so a driven pin reads back on IN. It enables TMR only, makes pin 0 rise, runs the timer for one event and clears STATUS. Run the shipped design: irq rises at edge 12 for the pin and never for the timer.

Bits typed from memory

    if (tmr_ev) status_d[1] = 1'b1;
    if (pin_ev) status_d[0] = 1'b1;

The timer sets PIN and the pin sets TMR. Lint: TMR and PIN are unused.

Bits named from the map

    if (tmr_ev) status_d[TMR] = 1'b1;
    if (pin_ev) status_d[PIN] = 1'b1;

STATUS matches the map: irq rises at edge 24 and falls at edge 29.

Press Lint before you read the code. Verilator 5.052 reports UNUSEDPARAM for TMR and PIN: the map's names exist and nothing uses them. Replace the numbers by the names and run again. Synthesis does not change: 249 cells, 55 flops and 16 levels of logic on this flow, for both.

Asked in interviews
  1. How do you implement write-one-to-clear?

    Next status = status AND NOT the written data, so bits written 1 clear and bits written 0 stay. Then OR in the events of the same cycle, so an event that coincides with its clear is kept.

  2. How do you keep a register map and the RTL consistent?

    Write the map once and derive the rest: constants for addresses and bit positions, or code generated from a machine-readable map. The driver headers and the testbench model come from the same source.

  3. How is an interrupt line built?

    Status bits are set by events and cleared by software; an enable register masks them. The line is the OR of status AND enable, registered, and it stays high until every enabled bit is cleared.

Try it

IN reads 0xa5: pins 3 to 0 are driven with 0x5, pins 7 to 4 come from outside as 0xa. The pin event sets bit 0 and irq rises at edge 12, with only TMR enabled. The timer event sets bit 1, which no enable selects, and the clear of bit 0 leaves it set.

With the bits named, STATUS reads 0x02 after the pin and 0x01 after the timer. The log gains e24 irq=1 and e29 irq=0 and loses the two early irq lines; the last STATUS read is 0x00.

RTLIcarus Verilog · in your browser · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e3-e4 WRITE 0x00000018 data=0x00000001 strb=1111 waits=0 OKAY e5-e6 WRITE 0x00000000 data=0x0000000f strb=1111 waits=0 OKAY e7-e8 WRITE 0x00000004 data=0x00000005 strb=1111 waits=0 OKAY e11-e12 READ 0x00000008 data=0x000000a5 waits=0 OKAY e12 irq=1 e13-e14 READ 0x00000014 data=0x00000001 waits=0 OKAY e15-e16 WRITE 0x00000014 data=0x00000003 strb=1111 waits=0 OKAY e17 irq=0 e17-e18 WRITE 0x00000010 data=0x00000003 strb=1111 waits=0 OKAY e19-e20 WRITE 0x0000000c data=0x00000001 strb=1111 waits=0 OKAY e23-e24 WRITE 0x0000000c data=0x00000000 strb=1111 waits=0 OKAY e25-e26 READ 0x00000014 data=0x00000002 waits=0 OKAY e27-e28 WRITE 0x00000014 data=0x00000001 strb=1111 waits=0 OKAY e29-e30 READ 0x00000014 data=0x00000002 waits=0 OKAY e31-e32 WRITE 0x00000008 data=0x000000ff strb=1111 waits=0 ERROR checker: 9 writes, 4 reads, 0 wait edges, 0 rules broken, 0 notes

Original design · 249 cells · 55 flip-flops · 0 latches · 16 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic pclk = 0, presetn = 0;
  always #5 pclk = ~pclk;                 // rising edges at 5, 15, 25, ...
  logic        psel = 0, penable = 0, pwrite = 0;
  logic [31:0] paddr = 0, pwdata = 0;
  logic [3:0]  pstrb = 0;
  logic [2:0]  pprot = 0;
  logic        pready, pslverr;
  logic [31:0] prdata;
  logic [7:0] gpio_out, gpio_oe, ext = 8'ha0;  // ext: what the outside world drives on the pins
  logic       irq;
  wire  [7:0] gpio_in = (gpio_oe & gpio_out) | (~gpio_oe & ext);   // the pads: a driven pin reads back
  logic       irq_seen = 0;
  always @(posedge pclk) begin #1;          // one line whenever irq changes (unless quiet)
    if (irq !== irq_seen && !quiet) $display("e%0d irq=%b", cycle, irq);
    irq_seen = irq;
  end
  gpio_timer dut (.pclk(pclk), .presetn(presetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
      .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .prdata(prdata), .pslverr(pslverr), .gpio_in(gpio_in), .gpio_out(gpio_out), .gpio_oe(gpio_oe), .irq(irq));
  apb_checker chk (.pclk(pclk), .presetn(presetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
                   .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .pslverr(pslverr));
  `include "rtl_apb_tasks.svh"
  initial begin

    repeat (2) @(posedge pclk); #1 presetn = 1;   // reset released after edge 1
    @(posedge pclk); #1;                          // the first transfer may start after edge 2
    apb_write(32'h18, 32'h01);              // IRQEN: TMR only (bit 0 in the map)
    apb_write(32'h00, 32'h0f);              // DIR: pins 3..0 are driven
    apb_write(32'h04, 32'h05);              // OUT: pin 0 rises
    idle(2);
    apb_read(32'h08);                       // IN
    apb_read(32'h14);                       // STATUS
    apb_write(32'h14, 32'h03);              // clear both bits
    apb_write(32'h10, 32'h03);              // PERIOD: a timer event every 3 cycles
    apb_write(32'h0c, 32'h01);              // CTRL: the timer runs
    idle(2);
    apb_write(32'h0c, 32'h00);              // CTRL: and stops
    apb_read(32'h14);                       // STATUS
    apb_write(32'h14, 32'h01);              // clear TMR: write 1 to bit 0
    apb_read(32'h14);
    apb_write(32'h08, 32'hff);              // IN is read-only: refused
    repeat (3) @(posedge pclk);
    $finish(0);
  end
endmodule
Read included file: apb_checker.sv
`timescale 1ns/1ns
// apb_checker.sv: the rules of an APB interface, checked at every rising edge (read-only).
//   reset        PSEL is 1 while PRESETn is low;
//   known        PSEL is x, or a control signal, a strobed write byte or PREADY in access is x;
//   setup        PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it;
//   setup_len    a setup cycle not followed by an access cycle;
//   stable       PSEL, PADDR, PWRITE, PPROT, PSTRB or the write data changed inside a transfer,
//                or PSEL or PENABLE dropped before PREADY;
//   enable_drop  PENABLE still 1 in the cycle after a completed transfer;
//   strb_read    PSTRB not zero during a read;
//   onehot       more than one PSELx at once;
//   timeout      no PREADY after TIMEOUT wait cycles (a liveness watchdog, not a protocol rule).
// A NOTE (not a broken rule): PSLVERR is 1 outside the completion cycle, which the spec allows
// and recommends against (IHI 0024E 3.4).
module apb_checker #(parameter int NSEL = 1, parameter int TIMEOUT = 16) (
  input logic            pclk,
  input logic            presetn,
  input logic [NSEL-1:0] psel,
  input logic            penable,
  input logic            pwrite,
  input logic [31:0]     paddr,
  input logic [31:0]     pwdata,
  input logic [3:0]      pstrb,
  input logic [2:0]      pprot,
  input logic            pready,
  input logic            pslverr);
  int broken = 0, notes = 0, writes = 0, reads = 0, waits = 0, edge_n = -1, stall = 0;
  logic            p_any = 0, p_en = 0, p_rdy = 0, p_wr = 0;
  logic [NSEL-1:0] p_sel = 0;
  logic [31:0]     p_addr = 0, p_wdata = 0;
  logic [3:0]      p_strb = 0;
  logic [2:0]      p_prot = 0;
  task automatic report(input string rule, input string msg);
    broken++;
    $display("RULE %0s broken at edge %0d: %0s", rule, edge_n, msg);
  endtask
  task automatic note(input string rule, input string msg);
    notes++;
    $display("NOTE %0s at edge %0d: %0s", rule, edge_n, msg);
  endtask
  always @(posedge pclk) begin
    logic any, en, rdy, bytes_x;
    edge_n = edge_n + 1;
    any = (|psel) === 1'b1; en = (penable === 1'b1); rdy = (pready === 1'b1);
    if (presetn !== 1'b1) begin
      if (any) report("reset", "PSEL is 1 while PRESETn is low");
      any = 0; en = 0;
    end else begin
      bytes_x = 0;
      for (int l = 0; l < 4; l++) if (pstrb[l] === 1'b1 && (^pwdata[8*l +: 8]) === 1'bx) bytes_x = 1;
      if ((^psel) === 1'bx) report("known", "PSEL is x");
      else if (any && (^{paddr, pwrite, penable, pstrb, pprot}) === 1'bx) report("known", "a control signal is x while PSEL is 1");
      else if (any && pwrite === 1'b1 && bytes_x) report("known", "a strobed byte of PWDATA is x");
      else if (any && en && pready === 1'bx) report("known", "PREADY is x in an access cycle");
      if (any && en && !(p_any && (!p_en || !p_rdy)))
        report("setup", "PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it");
      if (p_any && !p_en && !(any && en))
        report("setup_len", "the setup cycle was not followed by an access cycle");
      if (p_any && (!p_en || !p_rdy) && any) begin
        if (psel !== p_sel) report("stable", $sformatf("PSEL changed from %b to %b inside a transfer", p_sel, psel));
        if (paddr !== p_addr) report("stable", $sformatf("PADDR changed from 0x%0h to 0x%0h inside a transfer", p_addr, paddr));
        if (pwrite !== p_wr) report("stable", "PWRITE changed inside a transfer");
        if (pprot !== p_prot) report("stable", "PPROT changed inside a transfer");
        if (pstrb !== p_strb) report("stable", "PSTRB changed inside a transfer");
        if (p_wr && pwdata !== p_wdata) report("stable", $sformatf("PWDATA changed from 0x%0h to 0x%0h inside a transfer", p_wdata, pwdata));
        if (p_en && !en) report("stable", "PENABLE dropped before PREADY");
      end
      if (p_any && p_en && !p_rdy && !any) report("stable", "PSEL dropped before PREADY");
      if (p_any && p_en && p_rdy && en) report("enable_drop", "PENABLE is still 1 in the cycle after a completed transfer");
      if (any && pwrite === 1'b0 && pstrb !== 4'b0000) report("strb_read", $sformatf("PSTRB is %b during a read", pstrb));
      if ($countones(psel) > 1) report("onehot", $sformatf("PSELx is %b: two completers selected", psel));
      if (any && en && rdy) begin
        if (pwrite === 1'b1) writes++; else reads++;
        stall = 0;
      end else if (any && en) begin
        waits++; stall++;
        if (stall == TIMEOUT) report("timeout", $sformatf("no PREADY after %0d wait cycles", TIMEOUT));
      end
      if (pslverr === 1'b1 && !(any && en && rdy))
        note("pslverr", "PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW)");
    end
    p_any = any; p_en = en; p_rdy = rdy; p_sel = psel; p_addr = paddr; p_wr = (pwrite === 1'b1);
    p_wdata = pwdata; p_strb = pstrb; p_prot = pprot;
  end
  final $display("checker: %0d writes, %0d reads, %0d wait edges, %0d rules broken, %0d notes", writes, reads, waits, broken, notes);
endmodule
Read included file: rtl_apb_tasks.svh
// rtl_apb_tasks.svh: the requester side of the bench (read-only).
// apb_write(addr, data, strb) and apb_read(addr) run one transfer: SETUP for one cycle, ACCESS until
// PREADY, then the bus goes idle. Each transfer prints one line at its completion edge:
//   e3-e4 WRITE 0x00000000 data=0x0000005a strb=1111 waits=0 OKAY
// (setup edge - completion edge). A read leaves its data in prdata_q and its error in pslverr_q.
// quiet = 1 turns the log off; watch = 1 prints the bus at every edge.
// cycle is the number of the last rising edge; it updates after the edge's active events, so at an
// edge every reader (the tasks, the watch line) sees the same old value and names the edge cycle + 1.
int cycle = -1;
always @(posedge pclk) cycle <= cycle + 1;
bit quiet = 0, watch = 0;
logic [31:0] prdata_q = 0;
logic        pslverr_q = 0;
int          waits_q = 0, give_up = 24;
always @(posedge pclk) if (watch)
  $display("wave e%0d: psel=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
           cycle + 1, psel, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);

task automatic apb_xfer(input bit wr, input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb);
  int s_edge, w;
  paddr = addr; pwrite = wr; pwdata = wr ? data : 32'h0; pstrb = wr ? strb : 4'h0; pprot = 3'b000;
  psel = 1; penable = 0;                       // SETUP
  s_edge = cycle + 1;
  @(posedge pclk); #1 penable = 1;             // ACCESS
  w = 0;
  @(posedge pclk);
  while (pready !== 1'b1 && w < give_up) begin w++; @(posedge pclk); end
  prdata_q = prdata; pslverr_q = pslverr; waits_q = w;
  if (!quiet) begin
    if (wr && pslverr)  $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d ERROR", s_edge, cycle + 1, addr, data, strb, w);
    else if (wr)        $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d OKAY", s_edge, cycle + 1, addr, data, strb, w);
    else if (pslverr)   $display("e%0d-e%0d READ  0x%08h data=0x%08h waits=%0d ERROR", s_edge, cycle + 1, addr, prdata, w);
    else                $display("e%0d-e%0d READ  0x%08h data=0x%08h waits=%0d OKAY", s_edge, cycle + 1, addr, prdata, w);
  end
  #1 psel = 0; penable = 0; pwrite = 0; pstrb = 0; pwdata = 0;   // idle
endtask
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf);
  apb_xfer(1, addr, data, strb);
endtask
task automatic apb_read(input logic [31:0] addr);
  apb_xfer(0, addr, 32'h0, 4'h0);
endtask
task automatic idle(input int n);
  repeat (n) @(posedge pclk); #1;
endtask

Check yourself

Predict the outputThe corrected block has IRQEN = 0b11. The timer fires once and pin 0 rises, so STATUS is 0b11. Then software writes 0x01 to STATUS and reads it back. What does the bench print?
// gpio_timer: an APB4 peripheral with eight GPIO pins, an interval timer and one interrupt line.
// The register map, from the brief (every field below is named from this table):
//   0x00 DIR     read/write   [7:0] 1 drives the pin from OUT
//   0x04 OUT     read/write   [7:0] the value driven
//   0x08 IN      read-only    [7:0] the pins, through a two-flop synchronizer
//   0x0c CTRL    read/write   [0]   EN: the timer runs
//   0x10 PERIOD  read/write   [7:0] cycles from one timer event to the next (0 and 1: every cycle)
//   0x14 STATUS  write-1-to-clear  [0] TMR: the timer expired   [1] PIN: pin 0 rose
//   0x18 IRQEN   read/write   [0] TMR   [1] PIN: which STATUS bits drive irq
// Fields sit in byte 0 and change only when PSTRB[0] is 1. A write to IN, or any access to
// another address, ends with PSLVERR and changes nothing. No wait states.
module gpio_timer (
  input  logic        pclk,
  input  logic        presetn,     // active-low, asynchronous
  input  logic        psel,
  input  logic        penable,
  input  logic        pwrite,
  input  logic [31:0] paddr,
  input  logic [31:0] pwdata,
  input  logic [3:0]  pstrb,
  input  logic [2:0]  pprot,
  output logic        pready,
  output logic [31:0] prdata,
  output logic        pslverr,
  input  logic [7:0]  gpio_in,     // the pins, asynchronous to pclk
  output logic [7:0]  gpio_out,
  output logic [7:0]  gpio_oe,
  output logic        irq          // registered: 1 while an enabled STATUS bit is 1
);
  // The map as constants: addresses and bit positions.
  localparam logic [7:0] A_DIR = 8'h00, A_OUT = 8'h04, A_IN = 8'h08, A_CTRL = 8'h0c,
                         A_PERIOD = 8'h10, A_STATUS = 8'h14, A_IRQEN = 8'h18;
  localparam int TMR = 0, PIN = 1;

  logic [7:0] dir_q, out_q, period_q, cnt_q, s1_q, s2_q;
  logic       en_q, pin0_q;
  logic [1:0] status_q, irqen_q;

  // The bus (R3.5): no wait states, so a transfer completes in its first ACCESS cycle.
  assign pready = 1'b1;
  wire  [7:0] a    = paddr[7:0];
  wire  done = psel && penable;
  wire  known = (a == A_DIR) || (a == A_OUT) || (a == A_IN) || (a == A_CTRL)
             || (a == A_PERIOD) || (a == A_STATUS) || (a == A_IRQEN);
  wire  bad  = !known || (pwrite && a == A_IN);
  assign pslverr = done && bad;
  wire  wr   = done && !bad && pwrite && pstrb[0];   // a write that lands, at the completion edge

  // The read mux: one arm per row of the map.
  always_comb begin
    prdata = 32'd0;
    case (a)
      A_DIR:    prdata[7:0] = dir_q;
      A_OUT:    prdata[7:0] = out_q;
      A_IN:     prdata[7:0] = s2_q;
      A_CTRL:   prdata[0]   = en_q;
      A_PERIOD: prdata[7:0] = period_q;
      A_STATUS: prdata[1:0] = status_q;
      A_IRQEN:  prdata[1:0] = irqen_q;
      default:  prdata = 32'd0;
    endcase
  end

  // The events.
  wire tmr_ev = en_q && ({1'b0, cnt_q} + 9'd1 >= {1'b0, period_q});   // the last cycle of a period
  wire pin_ev = s2_q[0] && !pin0_q;                                   // pin 0 rose

  // STATUS: write 1 to clear; an event in the same cycle as its clear wins.
  logic [1:0] status_d;
  always_comb begin
    status_d = status_q;
    if (wr && a == A_STATUS) status_d = status_d & ~pwdata[1:0];
    if (tmr_ev) status_d[TMR] = 1'b1;
    if (pin_ev) status_d[PIN] = 1'b1;
  end

  always_ff @(posedge pclk or negedge presetn)
    if (!presetn) begin
      dir_q <= '0; out_q <= '0; en_q <= 1'b0; period_q <= '0; irqen_q <= '0;
      status_q <= '0; cnt_q <= '0; s1_q <= '0; s2_q <= '0; pin0_q <= 1'b0; irq <= 1'b0;
    end else begin
      if (wr && a == A_DIR)    dir_q    <= pwdata[7:0];
      if (wr && a == A_OUT)    out_q    <= pwdata[7:0];
      if (wr && a == A_CTRL)   en_q     <= pwdata[0];
      if (wr && a == A_PERIOD) period_q <= pwdata[7:0];
      if (wr && a == A_IRQEN)  irqen_q  <= pwdata[1:0];
      cnt_q    <= (!en_q || tmr_ev) ? 8'd0 : cnt_q + 8'd1;
      s1_q     <= gpio_in;                      // two-flop synchronizer (R6.2)
      s2_q     <= s1_q;
      pin0_q   <= s2_q[0];
      status_q <= status_d;
      irq      <= |(status_q & irqen_q);        // registered OR of the enabled bits
    end

  assign gpio_out = out_q;
  assign gpio_oe  = dir_q;
endmodule
`timescale 1ns/1ps
module tb;
  logic pclk = 0, presetn = 0;
  always #5 pclk = ~pclk;                 // rising edges at 5, 15, 25, ...
  logic        psel = 0, penable = 0, pwrite = 0;
  logic [31:0] paddr = 0, pwdata = 0;
  logic [3:0]  pstrb = 0;
  logic [2:0]  pprot = 0;
  logic        pready, pslverr;
  logic [31:0] prdata;
  logic [7:0] gpio_out, gpio_oe, ext = 8'ha0;  // ext: what the outside world drives on the pins
  logic       irq;
  wire  [7:0] gpio_in = (gpio_oe & gpio_out) | (~gpio_oe & ext);   // the pads: a driven pin reads back
  logic       irq_seen = 0;
  always @(posedge pclk) begin #1;          // one line whenever irq changes (unless quiet)
    if (irq !== irq_seen && !quiet) $display("e%0d irq=%b", cycle, irq);
    irq_seen = irq;
  end
  gpio_timer dut (.pclk(pclk), .presetn(presetn), .psel(psel), .penable(penable), .pwrite(pwrite), .paddr(paddr),
      .pwdata(pwdata), .pstrb(pstrb), .pprot(pprot), .pready(pready), .prdata(prdata), .pslverr(pslverr), .gpio_in(gpio_in), .gpio_out(gpio_out), .gpio_oe(gpio_oe), .irq(irq));
  `include "rtl_apb_tasks.svh"
  initial begin

    repeat (2) @(posedge pclk); #1 presetn = 1;   // reset released after edge 1
    @(posedge pclk); #1;                          // the first transfer may start after edge 2
    quiet = 1;
    apb_write(32'h18, 32'h03);              // IRQEN: TMR and PIN
    apb_write(32'h10, 32'h02);              // PERIOD 2
    apb_write(32'h0c, 32'h01);              // the timer runs ...
    apb_write(32'h0c, 32'h00);              // ... and stops
    apb_write(32'h00, 32'h01);              // DIR: pin 0 driven
    apb_write(32'h04, 32'h01);              // OUT: pin 0 rises
    idle(4);
    apb_write(32'h14, 32'h01);              // write 1 to STATUS bit 0
    idle(1);
    apb_read(32'h14);
    $display("status=%b irq=%b", prdata_q[1:0], irq);
    repeat (3) @(posedge pclk);
    $finish(0);
  end
endmodule
Read included file: apb_checker.sv
`timescale 1ns/1ns
// apb_checker.sv: the rules of an APB interface, checked at every rising edge (read-only).
//   reset        PSEL is 1 while PRESETn is low;
//   known        PSEL is x, or a control signal, a strobed write byte or PREADY in access is x;
//   setup        PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it;
//   setup_len    a setup cycle not followed by an access cycle;
//   stable       PSEL, PADDR, PWRITE, PPROT, PSTRB or the write data changed inside a transfer,
//                or PSEL or PENABLE dropped before PREADY;
//   enable_drop  PENABLE still 1 in the cycle after a completed transfer;
//   strb_read    PSTRB not zero during a read;
//   onehot       more than one PSELx at once;
//   timeout      no PREADY after TIMEOUT wait cycles (a liveness watchdog, not a protocol rule).
// A NOTE (not a broken rule): PSLVERR is 1 outside the completion cycle, which the spec allows
// and recommends against (IHI 0024E 3.4).
module apb_checker #(parameter int NSEL = 1, parameter int TIMEOUT = 16) (
  input logic            pclk,
  input logic            presetn,
  input logic [NSEL-1:0] psel,
  input logic            penable,
  input logic            pwrite,
  input logic [31:0]     paddr,
  input logic [31:0]     pwdata,
  input logic [3:0]      pstrb,
  input logic [2:0]      pprot,
  input logic            pready,
  input logic            pslverr);
  int broken = 0, notes = 0, writes = 0, reads = 0, waits = 0, edge_n = -1, stall = 0;
  logic            p_any = 0, p_en = 0, p_rdy = 0, p_wr = 0;
  logic [NSEL-1:0] p_sel = 0;
  logic [31:0]     p_addr = 0, p_wdata = 0;
  logic [3:0]      p_strb = 0;
  logic [2:0]      p_prot = 0;
  task automatic report(input string rule, input string msg);
    broken++;
    $display("RULE %0s broken at edge %0d: %0s", rule, edge_n, msg);
  endtask
  task automatic note(input string rule, input string msg);
    notes++;
    $display("NOTE %0s at edge %0d: %0s", rule, edge_n, msg);
  endtask
  always @(posedge pclk) begin
    logic any, en, rdy, bytes_x;
    edge_n = edge_n + 1;
    any = (|psel) === 1'b1; en = (penable === 1'b1); rdy = (pready === 1'b1);
    if (presetn !== 1'b1) begin
      if (any) report("reset", "PSEL is 1 while PRESETn is low");
      any = 0; en = 0;
    end else begin
      bytes_x = 0;
      for (int l = 0; l < 4; l++) if (pstrb[l] === 1'b1 && (^pwdata[8*l +: 8]) === 1'bx) bytes_x = 1;
      if ((^psel) === 1'bx) report("known", "PSEL is x");
      else if (any && (^{paddr, pwrite, penable, pstrb, pprot}) === 1'bx) report("known", "a control signal is x while PSEL is 1");
      else if (any && pwrite === 1'b1 && bytes_x) report("known", "a strobed byte of PWDATA is x");
      else if (any && en && pready === 1'bx) report("known", "PREADY is x in an access cycle");
      if (any && en && !(p_any && (!p_en || !p_rdy)))
        report("setup", "PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it");
      if (p_any && !p_en && !(any && en))
        report("setup_len", "the setup cycle was not followed by an access cycle");
      if (p_any && (!p_en || !p_rdy) && any) begin
        if (psel !== p_sel) report("stable", $sformatf("PSEL changed from %b to %b inside a transfer", p_sel, psel));
        if (paddr !== p_addr) report("stable", $sformatf("PADDR changed from 0x%0h to 0x%0h inside a transfer", p_addr, paddr));
        if (pwrite !== p_wr) report("stable", "PWRITE changed inside a transfer");
        if (pprot !== p_prot) report("stable", "PPROT changed inside a transfer");
        if (pstrb !== p_strb) report("stable", "PSTRB changed inside a transfer");
        if (p_wr && pwdata !== p_wdata) report("stable", $sformatf("PWDATA changed from 0x%0h to 0x%0h inside a transfer", p_wdata, pwdata));
        if (p_en && !en) report("stable", "PENABLE dropped before PREADY");
      end
      if (p_any && p_en && !p_rdy && !any) report("stable", "PSEL dropped before PREADY");
      if (p_any && p_en && p_rdy && en) report("enable_drop", "PENABLE is still 1 in the cycle after a completed transfer");
      if (any && pwrite === 1'b0 && pstrb !== 4'b0000) report("strb_read", $sformatf("PSTRB is %b during a read", pstrb));
      if ($countones(psel) > 1) report("onehot", $sformatf("PSELx is %b: two completers selected", psel));
      if (any && en && rdy) begin
        if (pwrite === 1'b1) writes++; else reads++;
        stall = 0;
      end else if (any && en) begin
        waits++; stall++;
        if (stall == TIMEOUT) report("timeout", $sformatf("no PREADY after %0d wait cycles", TIMEOUT));
      end
      if (pslverr === 1'b1 && !(any && en && rdy))
        note("pslverr", "PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW)");
    end
    p_any = any; p_en = en; p_rdy = rdy; p_sel = psel; p_addr = paddr; p_wr = (pwrite === 1'b1);
    p_wdata = pwdata; p_strb = pstrb; p_prot = pprot;
  end
  final $display("checker: %0d writes, %0d reads, %0d wait edges, %0d rules broken, %0d notes", writes, reads, waits, broken, notes);
endmodule
Read included file: rtl_apb_tasks.svh
// rtl_apb_tasks.svh: the requester side of the bench (read-only).
// apb_write(addr, data, strb) and apb_read(addr) run one transfer: SETUP for one cycle, ACCESS until
// PREADY, then the bus goes idle. Each transfer prints one line at its completion edge:
//   e3-e4 WRITE 0x00000000 data=0x0000005a strb=1111 waits=0 OKAY
// (setup edge - completion edge). A read leaves its data in prdata_q and its error in pslverr_q.
// quiet = 1 turns the log off; watch = 1 prints the bus at every edge.
// cycle is the number of the last rising edge; it updates after the edge's active events, so at an
// edge every reader (the tasks, the watch line) sees the same old value and names the edge cycle + 1.
int cycle = -1;
always @(posedge pclk) cycle <= cycle + 1;
bit quiet = 0, watch = 0;
logic [31:0] prdata_q = 0;
logic        pslverr_q = 0;
int          waits_q = 0, give_up = 24;
always @(posedge pclk) if (watch)
  $display("wave e%0d: psel=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
           cycle + 1, psel, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);

task automatic apb_xfer(input bit wr, input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb);
  int s_edge, w;
  paddr = addr; pwrite = wr; pwdata = wr ? data : 32'h0; pstrb = wr ? strb : 4'h0; pprot = 3'b000;
  psel = 1; penable = 0;                       // SETUP
  s_edge = cycle + 1;
  @(posedge pclk); #1 penable = 1;             // ACCESS
  w = 0;
  @(posedge pclk);
  while (pready !== 1'b1 && w < give_up) begin w++; @(posedge pclk); end
  prdata_q = prdata; pslverr_q = pslverr; waits_q = w;
  if (!quiet) begin
    if (wr && pslverr)  $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d ERROR", s_edge, cycle + 1, addr, data, strb, w);
    else if (wr)        $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d OKAY", s_edge, cycle + 1, addr, data, strb, w);
    else if (pslverr)   $display("e%0d-e%0d READ  0x%08h data=0x%08h waits=%0d ERROR", s_edge, cycle + 1, addr, prdata, w);
    else                $display("e%0d-e%0d READ  0x%08h data=0x%08h waits=%0d OKAY", s_edge, cycle + 1, addr, prdata, w);
  end
  #1 psel = 0; penable = 0; pwrite = 0; pstrb = 0; pwdata = 0;   // idle
endtask
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf);
  apb_xfer(1, addr, data, strb);
endtask
task automatic apb_read(input logic [31:0] addr);
  apb_xfer(0, addr, 32'h0, 4'h0);
endtask
task automatic idle(input int n);
  repeat (n) @(posedge pclk); #1;
endtask
Spot the bugEach option is the clear term in if (wr && a == A_STATUS) status_d = ...;. Both bits are set; the bench writes 0x01, then 0x00, then 0x02, reading STATUS after each. Which one fails?
Read included file: apb_checker.sv
`timescale 1ns/1ns
// apb_checker.sv: the rules of an APB interface, checked at every rising edge (read-only).
//   reset        PSEL is 1 while PRESETn is low;
//   known        PSEL is x, or a control signal, a strobed write byte or PREADY in access is x;
//   setup        PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it;
//   setup_len    a setup cycle not followed by an access cycle;
//   stable       PSEL, PADDR, PWRITE, PPROT, PSTRB or the write data changed inside a transfer,
//                or PSEL or PENABLE dropped before PREADY;
//   enable_drop  PENABLE still 1 in the cycle after a completed transfer;
//   strb_read    PSTRB not zero during a read;
//   onehot       more than one PSELx at once;
//   timeout      no PREADY after TIMEOUT wait cycles (a liveness watchdog, not a protocol rule).
// A NOTE (not a broken rule): PSLVERR is 1 outside the completion cycle, which the spec allows
// and recommends against (IHI 0024E 3.4).
module apb_checker #(parameter int NSEL = 1, parameter int TIMEOUT = 16) (
  input logic            pclk,
  input logic            presetn,
  input logic [NSEL-1:0] psel,
  input logic            penable,
  input logic            pwrite,
  input logic [31:0]     paddr,
  input logic [31:0]     pwdata,
  input logic [3:0]      pstrb,
  input logic [2:0]      pprot,
  input logic            pready,
  input logic            pslverr);
  int broken = 0, notes = 0, writes = 0, reads = 0, waits = 0, edge_n = -1, stall = 0;
  logic            p_any = 0, p_en = 0, p_rdy = 0, p_wr = 0;
  logic [NSEL-1:0] p_sel = 0;
  logic [31:0]     p_addr = 0, p_wdata = 0;
  logic [3:0]      p_strb = 0;
  logic [2:0]      p_prot = 0;
  task automatic report(input string rule, input string msg);
    broken++;
    $display("RULE %0s broken at edge %0d: %0s", rule, edge_n, msg);
  endtask
  task automatic note(input string rule, input string msg);
    notes++;
    $display("NOTE %0s at edge %0d: %0s", rule, edge_n, msg);
  endtask
  always @(posedge pclk) begin
    logic any, en, rdy, bytes_x;
    edge_n = edge_n + 1;
    any = (|psel) === 1'b1; en = (penable === 1'b1); rdy = (pready === 1'b1);
    if (presetn !== 1'b1) begin
      if (any) report("reset", "PSEL is 1 while PRESETn is low");
      any = 0; en = 0;
    end else begin
      bytes_x = 0;
      for (int l = 0; l < 4; l++) if (pstrb[l] === 1'b1 && (^pwdata[8*l +: 8]) === 1'bx) bytes_x = 1;
      if ((^psel) === 1'bx) report("known", "PSEL is x");
      else if (any && (^{paddr, pwrite, penable, pstrb, pprot}) === 1'bx) report("known", "a control signal is x while PSEL is 1");
      else if (any && pwrite === 1'b1 && bytes_x) report("known", "a strobed byte of PWDATA is x");
      else if (any && en && pready === 1'bx) report("known", "PREADY is x in an access cycle");
      if (any && en && !(p_any && (!p_en || !p_rdy)))
        report("setup", "PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it");
      if (p_any && !p_en && !(any && en))
        report("setup_len", "the setup cycle was not followed by an access cycle");
      if (p_any && (!p_en || !p_rdy) && any) begin
        if (psel !== p_sel) report("stable", $sformatf("PSEL changed from %b to %b inside a transfer", p_sel, psel));
        if (paddr !== p_addr) report("stable", $sformatf("PADDR changed from 0x%0h to 0x%0h inside a transfer", p_addr, paddr));
        if (pwrite !== p_wr) report("stable", "PWRITE changed inside a transfer");
        if (pprot !== p_prot) report("stable", "PPROT changed inside a transfer");
        if (pstrb !== p_strb) report("stable", "PSTRB changed inside a transfer");
        if (p_wr && pwdata !== p_wdata) report("stable", $sformatf("PWDATA changed from 0x%0h to 0x%0h inside a transfer", p_wdata, pwdata));
        if (p_en && !en) report("stable", "PENABLE dropped before PREADY");
      end
      if (p_any && p_en && !p_rdy && !any) report("stable", "PSEL dropped before PREADY");
      if (p_any && p_en && p_rdy && en) report("enable_drop", "PENABLE is still 1 in the cycle after a completed transfer");
      if (any && pwrite === 1'b0 && pstrb !== 4'b0000) report("strb_read", $sformatf("PSTRB is %b during a read", pstrb));
      if ($countones(psel) > 1) report("onehot", $sformatf("PSELx is %b: two completers selected", psel));
      if (any && en && rdy) begin
        if (pwrite === 1'b1) writes++; else reads++;
        stall = 0;
      end else if (any && en) begin
        waits++; stall++;
        if (stall == TIMEOUT) report("timeout", $sformatf("no PREADY after %0d wait cycles", TIMEOUT));
      end
      if (pslverr === 1'b1 && !(any && en && rdy))
        note("pslverr", "PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW)");
    end
    p_any = any; p_en = en; p_rdy = rdy; p_sel = psel; p_addr = paddr; p_wr = (pwrite === 1'b1);
    p_wdata = pwdata; p_strb = pstrb; p_prot = pprot;
  end
  final $display("checker: %0d writes, %0d reads, %0d wait edges, %0d rules broken, %0d notes", writes, reads, waits, broken, notes);
endmodule
Read included file: rtl_apb_tasks.svh
// rtl_apb_tasks.svh: the requester side of the bench (read-only).
// apb_write(addr, data, strb) and apb_read(addr) run one transfer: SETUP for one cycle, ACCESS until
// PREADY, then the bus goes idle. Each transfer prints one line at its completion edge:
//   e3-e4 WRITE 0x00000000 data=0x0000005a strb=1111 waits=0 OKAY
// (setup edge - completion edge). A read leaves its data in prdata_q and its error in pslverr_q.
// quiet = 1 turns the log off; watch = 1 prints the bus at every edge.
// cycle is the number of the last rising edge; it updates after the edge's active events, so at an
// edge every reader (the tasks, the watch line) sees the same old value and names the edge cycle + 1.
int cycle = -1;
always @(posedge pclk) cycle <= cycle + 1;
bit quiet = 0, watch = 0;
logic [31:0] prdata_q = 0;
logic        pslverr_q = 0;
int          waits_q = 0, give_up = 24;
always @(posedge pclk) if (watch)
  $display("wave e%0d: psel=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
           cycle + 1, psel, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);

task automatic apb_xfer(input bit wr, input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb);
  int s_edge, w;
  paddr = addr; pwrite = wr; pwdata = wr ? data : 32'h0; pstrb = wr ? strb : 4'h0; pprot = 3'b000;
  psel = 1; penable = 0;                       // SETUP
  s_edge = cycle + 1;
  @(posedge pclk); #1 penable = 1;             // ACCESS
  w = 0;
  @(posedge pclk);
  while (pready !== 1'b1 && w < give_up) begin w++; @(posedge pclk); end
  prdata_q = prdata; pslverr_q = pslverr; waits_q = w;
  if (!quiet) begin
    if (wr && pslverr)  $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d ERROR", s_edge, cycle + 1, addr, data, strb, w);
    else if (wr)        $display("e%0d-e%0d WRITE 0x%08h data=0x%08h strb=%b waits=%0d OKAY", s_edge, cycle + 1, addr, data, strb, w);
    else if (pslverr)   $display("e%0d-e%0d READ  0x%08h data=0x%08h waits=%0d ERROR", s_edge, cycle + 1, addr, prdata, w);
    else                $display("e%0d-e%0d READ  0x%08h data=0x%08h waits=%0d OKAY", s_edge, cycle + 1, addr, prdata, w);
  end
  #1 psel = 0; penable = 0; pwrite = 0; pstrb = 0; pwdata = 0;   // idle
endtask
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf);
  apb_xfer(1, addr, data, strb);
endtask
task automatic apb_read(input logic [31:0] addr);
  apb_xfer(0, addr, 32'h0, 4'h0);
endtask
task automatic idle(input int n);
  repeat (n) @(posedge pclk); #1;
endtask
ConceptThe shipped design passed the protocol checker with 0 rules broken and synthesized cleanly. What would have found the swap before a driver did?
ConceptA driver writes 0xff to IN, a read-only register. What must the completer do?
Read the waveformIn the measured trace, at which edge does irq fall after the write-1-to-clear?
Clearing TMR
012345678w1cstatus000100irq

Values in the cycle after edges 21 to 29, left to right.

Remember

  • Write the register map once and derive everything from it: localparams for addresses and bit positions, one decode term, write enable and read arm per row.
  • Each access type is one line: read/write stores, read-only refuses or ignores as the brief says, write-1-to-clear is status AND NOT data.
  • Put the events after the clear, so an event at the same edge as its clear is kept.
  • The interrupt is a registered OR of STATUS AND IRQEN: a level that stays high until software clears every enabled bit.