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.
A token flies to a monitor at the edge where it publishes a transfer.
- 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.
- 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.
- Cycle 3
Setup of the read of 0x110. The early monitor publishes again, with 5a: the data of the previous read, still on PRDATA.
- Cycle 5
Completion: the monitor publishes 41504234, the ID register.
- Cycle 6
Setup of the write to 0x118. The early monitor publishes it as OKAY; nothing about the response is known yet.
- 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.
| Part | What it does here |
|---|---|
| apb_item | One transfer: direction, address, data, strobes; the driver fills in the read data and the error |
| apb_driver | SETUP for one cycle, ACCESS until PREADY, then item_done; straight back to SETUP when the next item is waiting |
| apb_monitor | Publishes a transfer at the edge where PSEL, PENABLE and PREADY are all 1; at_setup makes the broken twin |
| apb_regs | CTRL, STATUS, WCOUNT and ID as the completer has them, and GHOST at 0x118, which the hardware does not have |
| apb_adapter | reg2bus makes one apb_item per register access; bus2reg turns PSLVERR into UVM_NOT_OK |
| uvm_reg_predictor | Updates 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
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.
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.
What does the register adapter do with PSLVERR?
bus2regsets the status toUVM_NOT_OK, so the test sees the error as the status ofwriteorread. With explicit prediction the model then skips predicting that register.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.
Does
get_next_itemorgetsuit an APB driver?get_next_itemwithitem_done, because APB carries one transfer at a time and the response goes back in the same item.getwithputis 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.
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 : 0Read 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
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.