An APB agent and the register adapter

Build the UVM agent for APB: a driver that runs the state machine and a monitor that reports where the data is valid. Put the register model on top through an adapter and a predictor. A twin monitor that reports one edge too early shows why the edge matters.

Lesson
P3.9
Time
35 min
Runs on
Verilator with UVM
Check
5 questions

A register test passed every write and failed every read with the value of the read before it. The monitor published each transfer at its setup edge, where PADDR is known and PRDATA is not yet. The predictor dutifully copied the stale value into the register model, and every mirror check blamed the design.

Two monitors on one busTwo reads and a write back to back, one wait state each, as the example runs them. The correct monitor reports at completion, its twin at setup.
PSELPREADYPRDATAPRDATADriverSETUP, then ACCESSCompleterone wait statemonitorat completionearly monat setup (broken)

A token flies to a monitor at the edge where it publishes a transfer.

012345678clkpselpenablepwritepaddr100110118preadyprdata05a41504234pslverr
  1. Cycle 0

    Setup of the read of 0x100. The early monitor publishes now, with PRDATA still 0 from before: the completer has not answered yet.

  2. Cycle 2

    The completion edge: PSEL, PENABLE and PREADY are all 1. The monitor publishes the read with 5a, the value the completer drove for this transfer.

  3. Cycle 3

    Setup of the read of 0x110. The early monitor publishes again, with 5a: the data of the previous read, still on PRDATA.

  4. Cycle 5

    Completion: the monitor publishes 41504234, the ID register.

  5. Cycle 6

    Setup of the write to 0x118. The early monitor publishes it as OKAY; nothing about the response is known yet.

  6. Cycle 8

    Completion with PSLVERR: 0x118 is unmapped. Only the monitor that waited sees the error.

Measured in the example's UVM run, 155 ns to 235 ns, sampled at each rising edge.

The agent on one page

The bench is the library's completer with one wait state per transfer, and an agent for its port. APB carries one transfer at a time, so the agent is simpler than P2.17's: no IDs, no per-channel processes, one item per transfer.

PartWhat it does here
apb_itemOne transfer: direction, address, data, strobes; the driver fills in the read data and the error
apb_driverSETUP for one cycle, ACCESS until PREADY, then item_done; straight back to SETUP when the next item is waiting
apb_monitorPublishes a transfer at the edge where PSEL, PENABLE and PREADY are all 1; at_setup makes the broken twin
apb_regsCTRL, STATUS, WCOUNT and ID as the completer has them, and GHOST at 0x118, which the hardware does not have
apb_adapterreg2bus makes one apb_item per register access; bus2reg turns PSLVERR into UVM_NOT_OK
uvm_reg_predictorUpdates the register model from what the monitor saw (explicit prediction)

The components of the example, top to bottom in the source.

The driver runs the state machine

The driver drives the setup cycle, raises PENABLE one edge later and waits for PREADY at each edge after that. At the completion edge it copies PRDATA and PSLVERR into the request and calls item_done. Then it asks for the next item with try_next_item, which lets the sequences settle and returns null only if none is waiting.

If an item is waiting, the driver goes straight to SETUP with PSEL still high, the ACCESS-to-SETUP transition of the state machine. The log shows it: the three RAM writes complete at 55, 85 and 115 ns, and each next transfer's setup is the edge right after. A driver that always returned to IDLE would add a cycle to every transfer.

The read data goes back in the request itself, so the adapter is built with provides_responses = 0. The register model reads the value from the item it sent once finish_item returns.

The monitor reports at completion

PRDATA and PSLVERR mean something only in the cycle where PSEL, PENABLE and PREADY are all high. Before that edge the completer may still be preparing the data, and with wait states it usually is. A monitor that publishes at setup has the address and the direction right, but the data belongs to whatever was on PRDATA before, and the response is always OKAY.

The example runs both monitors on the same wires. early mon publishes the read of 0x100 as 0, the read of 0x110 as 5a (the previous read), and the failed write to 0x118 as OKAY. The correct monitor publishes 5a, 41504234 and PSLVERR. Wait states make it worse: with one wait state the setup edge is two edges before the data is valid.

Registers on top: adapter and predictor

The register model reaches the bus through the agent's sequencer: set_sequencer(agent.sqr, adapter). The adapter is the only class that knows APB. It makes one item per register access and maps PSLVERR to UVM_NOT_OK, so a test sees the error as a status (U13 builds the same adapter in detail).

Prediction here is explicit: set_auto_predict(0), and the monitor feeds a uvm_reg_predictor. The mirror then follows what happened on the bus, including accesses the model did not start. The GHOST write shows the other side. The access returns UVM_NOT_OK, and the register model skips the prediction with the warning PREDICT_NOK, so the mirror keeps its old value.

Whether the hardware changed on that error is the completer's choice. The specification allows either, and this completer changes nothing. WCOUNT reads 4 at the end: three RAM writes and CTRL, and not GHOST.

Asked in interviews: APB agents
  1. When does an APB monitor publish a transfer?

    At the edge where PSEL, PENABLE and PREADY are all high. Only there are PRDATA and PSLVERR valid; at setup the data and the response are not known yet.

  2. How does an APB driver do back-to-back transfers?

    After the completion edge it asks for the next item without waiting. If one is ready it drives the next setup at once with PSEL still high; if not, it drops PSEL and goes idle.

  3. What does the register adapter do with PSLVERR?

    bus2reg sets the status to UVM_NOT_OK, so the test sees the error as the status of write or read. With explicit prediction the model then skips predicting that register.

  4. Why use explicit prediction with a monitor?

    The mirror then follows what the bus did, not what the model asked for. Accesses from other masters, a firmware model or a second sequence update it too.

  5. Does get_next_item or get suit an APB driver?

    get_next_item with item_done, because APB carries one transfer at a time and the response goes back in the same item. get with put is for buses that keep several items in flight.

Try it

Read the log by time. Each transfer appears twice, early mon at its setup edge and monitor two edges later at its completion. The test lines are the register calls returning: the write of CTRL, its read-back, the ID, the failed write to GHOST with its PREDICT_NOK warning, WCOUNT and the mirror check.

Then break it. Connect the early monitor to the predictor instead (early.ap.connect(pred.bus_in); in place of the agent's monitor) and run again. The mirror now takes the stale data, so ctrl.mirror(check) reports a mismatch as a UVM_ERROR.

RUNVerilator + UVM 2020.3.1 ยท simulator service
Edit the code, then run it
Expected among the UVM messages: 35 ns early mon write 0x020 data=0x000000a0 OKAY 55 ns monitor write 0x020 data=0x000000a0 OKAY 65 ns early mon write 0x024 data=0x000000a1 OKAY 85 ns monitor write 0x024 data=0x000000a1 OKAY 95 ns early mon write 0x028 data=0x000000a2 OKAY 115 ns monitor write 0x028 data=0x000000a2 OKAY 125 ns early mon write 0x100 data=0x0000005a OKAY 145 ns monitor write 0x100 data=0x0000005a OKAY 146 ns test ctrl.write(5a): UVM_IS_OK, mirror 5a 155 ns early mon read 0x100 data=0x00000000 OKAY 175 ns monitor read 0x100 data=0x0000005a OKAY 176 ns test ctrl.read: UVM_IS_OK, 5a 185 ns early mon read 0x110 data=0x0000005a OKAY 205 ns monitor read 0x110 data=0x41504234 OKAY 206 ns test id.read: UVM_IS_OK, 41504234 215 ns early mon write 0x118 data=0x00000001 OKAY 235 ns monitor write 0x118 data=0x00000001 PSLVERR UVM_WARNING uvm/reg/uvm_reg.svh(1663) @ 235: reporter [PREDICT_NOK] status UVM_NOT_OK; skip prediction. 236 ns test ghost.write(1): UVM_NOT_OK 245 ns early mon read 0x108 data=0x41504234 OKAY 265 ns monitor read 0x108 data=0x00000004 OKAY 266 ns test wcount.read: UVM_IS_OK, 4 275 ns early mon read 0x100 data=0x00000004 OKAY 295 ns monitor read 0x100 data=0x0000005a OKAY 296 ns test ctrl.mirror(check): UVM_IS_OK UVM_WARNING : 1 UVM_ERROR : 0
Read included file: apb_periph.sv
`timescale 1ns/1ns
// apb_periph.sv: an APB4 completer (read-only). Offsets are paddr[11:0]:
//   0x000-0x0ff  RAM, 256 bytes, written lane by lane under PSTRB
//   0x100 CTRL   read/write, byte-strobed
//   0x104 STATUS read-only: the number of words in the FIFO
//   0x108 WCOUNT read-only: counts completed writes (a zero-strobe write counts too)
//   0x10c FIFO   a write pushes PWDATA, a read pops the oldest word (0 when empty)
//   0x110 ID     read-only, 0x41504234 ("APB4")
// With err_on (the default) the completer answers PSLVERR for an unmapped offset, an unaligned
// PADDR, a write to a read-only register and, with priv_ctrl, an unprivileged access to CTRL;
// a transfer that gets PSLVERR changes nothing here (IHI 0024E 3.4 allows either choice).
// Switches (set from the stimulus):
//   waits           wait states in every access (PREADY low for that many access cycles)
//   ready_in_setup  drive PREADY high outside the access phase (a don't-care: the spec allows it)
//   hang            never raise PREADY
//   err_on, priv_ctrl
// Broken twins:
//   act_every_cycle acts at every edge PSEL is high, not once at completion
//   late_rdata      PRDATA registered at completion, so it arrives one cycle late
//   err_early       PSLVERR during setup and wait cycles, low at completion
//   ignore_strobes  writes every lane of a RAM word or CTRL
//   ignore_psel     decodes PADDR and acts in access cycles without looking at its PSEL
module apb_periph #(parameter NAME = "p0") (
  input  logic        pclk,
  input  logic        presetn,
  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);
  int waits = 0;
  bit ready_in_setup = 0, hang = 0, err_on = 1, priv_ctrl = 0, trace = 0;
  bit act_every_cycle = 0, late_rdata = 0, err_early = 0, ignore_strobes = 0, ignore_psel = 0;

  logic [7:0]  mem [0:255];
  logic [31:0] ctrl = 0, wcount = 0;
  logic [31:0] fifo [0:15];
  int          fhead = 0, fcount = 0;
  int          wcnt = 0;                  // wait cycles spent in this access
  logic [31:0] prdata_q = 0;
  initial for (int i = 0; i < 256; i++) mem[i] = 0;

  localparam int RAM = 0, CTRL = 1, STATUS = 2, WCOUNT = 3, FIFO = 4, ID = 5, NONE = 6;
  function automatic int region(input logic [11:0] a);
    if (a < 12'h100) region = RAM;
    else if (a[11:2] == 10'h040) region = CTRL;
    else if (a[11:2] == 10'h041) region = STATUS;
    else if (a[11:2] == 10'h042) region = WCOUNT;
    else if (a[11:2] == 10'h043) region = FIFO;
    else if (a[11:2] == 10'h044) region = ID;
    else region = NONE;
  endfunction
  function automatic bit bad(input logic wr, input logic [11:0] a, input logic [2:0] prot);
    int r;
    r = region(a);
    bad = (r == NONE) || (a[1:0] != 0) || (wr && (r == STATUS || r == WCOUNT || r == ID))
          || (priv_ctrl && r == CTRL && !prot[0]);
  endfunction
  function automatic logic [31:0] value(input logic [11:0] a);
    case (region(a))
      RAM:     value = {mem[{a[7:2], 2'd3}], mem[{a[7:2], 2'd2}], mem[{a[7:2], 2'd1}], mem[{a[7:2], 2'd0}]};
      CTRL:    value = ctrl;
      STATUS:  value = fcount;
      WCOUNT:  value = wcount;
      FIFO:    value = fcount > 0 ? fifo[fhead] : 32'h0;
      ID:      value = 32'h41504234;
      default: value = 32'h0;
    endcase
  endfunction

  wire sel    = ignore_psel ? 1'b1 : (psel === 1'b1);
  wire access = sel && (penable === 1'b1);
  wire err    = err_on && bad(pwrite, paddr[11:0], pprot);
  assign pready  = hang ? 1'b0 : (!access ? (ready_in_setup ? 1'b1 : 1'b0) : (wcnt >= waits));
  wire done   = access && pready;
  assign pslverr = err_early ? (sel && !done && err) : (done && err);
  // The read value is taken at the setup edge (PSEL 1, PENABLE 0) and held on PRDATA until the next
  // read, the way most register blocks do it (the bus multiplexor decides who is heard);
  // late_rdata registers it at completion instead.
  logic [31:0] rdata_r = 0;
  always @(posedge pclk) if (sel && penable !== 1'b1 && pwrite === 1'b0) rdata_r <= value(paddr[11:0]);
  assign prdata  = late_rdata ? prdata_q : rdata_r;
  wire act    = act_every_cycle ? sel : done;

  always @(posedge pclk or negedge presetn) begin
    if (!presetn) begin
      ctrl <= 0; wcount <= 0; wcnt <= 0;
    end else begin
      wcnt <= (access && !pready) ? wcnt + 1 : 0;
      if (done && !pwrite) prdata_q <= value(paddr[11:0]);
      if (act && !err) begin
        if (pwrite) begin
          wcount <= wcount + 1;
          case (region(paddr[11:0]))
            RAM:  for (int l = 0; l < 4; l++) if (pstrb[l] || ignore_strobes) mem[{paddr[7:2], 2'b00} + l] <= pwdata[8*l +: 8];
            CTRL: for (int l = 0; l < 4; l++) if (pstrb[l] || ignore_strobes) ctrl[8*l +: 8] <= pwdata[8*l +: 8];
            FIFO: if (fcount < 16) begin fifo[(fhead + fcount) % 16] <= pwdata; fcount <= fcount + 1; end
            default: ;
          endcase
          if (trace) $display("  %0s e%0d: write 0x%03h <= 0x%08h strb=%b", NAME, ($time - 5) / 10, paddr[11:0], pwdata, pstrb);
        end else begin
          if (region(paddr[11:0]) == FIFO && fcount > 0) begin fhead <= (fhead + 1) % 16; fcount <= fcount - 1; end
          if (trace) $display("  %0s e%0d: read 0x%03h -> 0x%08h", NAME, ($time - 5) / 10, paddr[11:0], value(paddr[11:0]));
        end
      end else if (act && trace) $display("  %0s e%0d: error, %0s 0x%03h ignored", NAME, ($time - 5) / 10, pwrite ? "write" : "read", paddr[11:0]);
    end
  end
endmodule

Check yourself

Predict the outputA plain program applies both monitors' rules to the nine cycles of the opening trace: a read publishes the low byte of PRDATA, a write publishes its response. What line is printed?
module tb;
  // Nine cycles of the bus as the monitors sample them, one per rising edge: a read of 0x100,
  // a read of 0x110 and a write of 0x118, back to back, one wait state each.
  localparam logic [8:0]  PSEL    = 9'b111111111;
  localparam logic [8:0]  PENABLE = 9'b011011011;
  localparam logic [8:0]  PREADY  = 9'b001001001;
  localparam logic [8:0]  PWRITE  = 9'b000000111;
  localparam logic [8:0]  PSLVERR = 9'b000000001;
  localparam logic [71:0] PRDATA  = {8'h00, 8'h5a, 8'h5a, 8'h5a, 8'h34, 8'h34, 8'h34, 8'h34, 8'h34}; // low byte only
  string early = "", mon = "", what;
  int k;
  initial begin
    for (int c = 0; c < 9; c++) begin
      k = 8 - c;                              // cycle c is bit 8 - c: the first cycle on the left
      if (!PWRITE[k]) what = $sformatf(" %02h", PRDATA[8 * k +: 8]);
      else if (PSLVERR[k]) what = " PSLVERR";
      else what = " OKAY";
      if (PSEL[k] && !PENABLE[k]) early = {early, what};             // the broken twin: at the setup edge
      if (PSEL[k] && PENABLE[k] && PREADY[k]) mon = {mon, what};     // the monitor: at the completion edge
    end
    $display("early:%0s | monitor:%0s", early, mon);
  end
endmodule
ConceptWhy does this monitor publish a transfer at the edge where PSEL, PENABLE and PREADY are all high, and not at the setup edge?
ConceptThe write to GHOST returns PSLVERR. With explicit prediction from the monitor, what happens to the register model?
ConceptWhy does the driver call try_next_item right after item_done, instead of always dropping PSEL and calling get_next_item?
Read the waveformAt which cycle does the correct monitor publish the read of 0x110, and with what data?
The read of 0x110
012345pselpenablepaddr100110preadyprdata05a41504234

The first six cycles of the opening trace.

Remember

  • An APB monitor publishes at the edge where PSEL, PENABLE and PREADY are all 1: the only edge where PRDATA and PSLVERR are valid.
  • A monitor that reports at setup gets the address right and the data wrong: the previous read's value, and OKAY for every response.
  • The driver follows the state machine: SETUP for one cycle, ACCESS until PREADY; with the next item waiting it goes straight to SETUP with PSEL held high.
  • The adapter is the only APB-aware part of the register layer; it maps PSLVERR to UVM_NOT_OK, and the register model skips predicting a failed access.
  • Explicit prediction makes the mirror follow what the monitor saw, which is why the monitor's sampling edge decides whether the mirror is right.