The APB checker
Write the rules of an APB transfer as properties and bind them inside every completer. Break each rule once, read the edge it names, and learn what a checker on the wires cannot tell you about a register block.
- Lesson
- P3.8
- Time
- 30 min
- Runs on
- Verilator
- Check
- 5 questions
Firmware wrote 0x66 to a register at offset 0x1c and read back 0x00, while its neighbour at 0x20 held the 0x66. The requester had moved PADDR forward by one word during a wait state, and the completer took the address it saw at the completion edge. Weeks of directed tests had never stalled that peripheral twice; one property names the edge.
A token flies at each completion edge: the write data to the completer, the read data back.
- Cycle 0
Setup of the write: PSEL high, PENABLE low, PADDR = 0x1c, PWDATA = 0x66. From here to the completion edge every one of these must stay put.
- Cycle 2
The second access cycle, still waiting: PADDR is now 0x20. The property
stable_ctlfails at this edge, and the procedural checker prints stable at the same edge. - Cycle 3
PREADY is 1: the completion edge. The completer acts on what it sees now, so 0x66 lands at 0x20.
- Cycle 5
The read of 0x1c returns 0x00. The data was never written there.
- Cycle 7
The read of 0x20 returns 0x66: the neighbour took the write.
Measured on the lesson's bench (edges 3 to 10, move_in_wait on, the completer with two wait states). The rule names the edge where the address moved; the data shows it only two transfers later.
Ten names, twelve properties
The checker of this module has two forms, as in P2.16. apb_checker.sv is procedural: one always block at every edge that prints RULE <name> broken at edge N. apb_props.sv says the same sentences as concurrent assertions with the same names, so the two can be read side by side. APB needs almost no bookkeeping: one transfer at a time, no IDs, no bursts. A property only has to know the previous cycle, which $past and $stable give it.
| Rule | The sentence | Broken in the tour by | Fires at |
|---|---|---|---|
| setup | An access cycle follows a setup cycle or a waited access cycle | PSEL and PENABLE rising together (phase 1); PENABLE held into the next transfer (phase 2) | edges 16; 19 |
| enable_drop | PENABLE goes low after the completion edge | PENABLE held into the next transfer (phase 2) | edge 19 |
| stable | PSEL, PADDR, PWRITE, PPROT, PSTRB and the write data hold until PREADY; PENABLE stays high | PADDR moved in a wait (phase 3); PSEL and PENABLE dropped by a requester that gave up (phase 7) | edges 22; 54 |
| strb_read | Every PSTRB bit is low during a read | PSTRB = 1111 on a read (phase 4) | edges 24, 25 |
| setup_len | The setup phase lasts one cycle | a setup held for two cycles (phase 5) | edge 27 |
| pslverr (note) | PSLVERR is recommended LOW outside the completion cycle | a completer that raises it in setup and wait cycles (phase 6) | edges 29, 30 |
| timeout | A watchdog: PREADY within 16 access cycles | a completer that never answers (phase 7) | edge 48 |
| reset | No transfer while PRESETn is low | PSEL driven during reset (phase 8) | edge 55 |
| onehot | At most one PSELx at a time | not reachable inside one completer: bind it on the decoded bus with NSEL = 3 | |
| known | No x on a control signal while PSEL is high | the procedural checker only: Verilator has no x |
The rule table, measured by the tour below. At every edge the property and the procedural checker print together.
Two of the rows are not rules of the specification. timeout is a watchdog. APB lets a completer add any number of wait states, so only the system can say how many is too many; the 16 here is the bench's choice. The pslverr row is a recommendation: the specification makes PSLVERR meaningful only in the last cycle of a transfer and recommends, without requiring, that it is LOW elsewhere. Its copy counts notes, not failures.
Read apb_props.sv below the bench with this table beside it. The bind line in the bench puts one copy inside apb_periph, so every completer instance checks its own PSEL.
The sentence at every access edge
The setup sentence is the whole APB state machine in one line. An access cycle (PSEL and PENABLE high) must come straight after a setup cycle, or after an access cycle that was still waiting for PREADY. It is tried at every access edge, including every wait state, so a transfer with two waits is checked three times.
- Edge 3 · t=25
The first access at 25. The cycle before it was a setup (PSEL 1, PENABLE 0): pass.
- Edge 4 · t=35
The access at 35 follows an access that was still waiting (PREADY was 0 at 25): pass. This is the same transfer, one wait state later.
- Edge 6 · t=55
At 55, PSEL and PENABLE rise together. The cycle before was idle, so no setup came first: fail, at the access edge itself.
Read the lanes as text
- access @25, from edge 3 (t=25): passes at edge 3 (t=25).
- access @35, from edge 4 (t=35): passes at edge 4 (t=35).
- access @55, from edge 6 (t=55): fails at edge 6 (t=55).
Sampled values, edge 1 to 7: psel 0 1 1 1 0 1 0; penable 0 0 1 1 0 1 0; pready 0 0 0 1 0 1 0.
The program behind the lanes
The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of setup that can start only at that lane's edge, so every verdict and its edge are read from that copy's own output.
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic psel = 0, penable = 0, pready = 0;
setup: assert property (@(posedge clk) psel && penable |-> $past(psel && (!penable || !pready)));
initial begin
@(negedge clk); psel = 1; // setup at 15
@(negedge clk); penable = 1; // access at 25, PREADY low: a wait
@(negedge clk); pready = 1; // access at 35 completes
@(negedge clk); psel = 0; penable = 0; pready = 0; // idle at 45
@(negedge clk); psel = 1; penable = 1; pready = 1; // 55: PSEL and PENABLE rise together
@(negedge clk); psel = 0; penable = 0; pready = 0;
@(negedge clk);
$finish;
end
endmodule
Three attempts, one failure. $past is the previous sample, so the rule reads the state machine one cycle back.
The stable sentence uses |=>: it starts in a cycle where the transfer is not finished and checks the next one. Its failure is reported one edge after the attempt began, at the edge where the change is first visible.
psel && penable && !pready |=> $stable(paddr).- Edge 3 · t=25
The first wait cycle at 25 starts an attempt. It looks at the next edge.
- Edge 4 · t=35
At 35 PADDR is 0x104 instead of 0x100: the attempt from 25 fails here. The wait at 35 starts its own attempt.
- Edge 5 · t=45
At 45 PADDR is still 0x104, so the second attempt passes. One move, one failure: the rule names the edge of the change, not every edge after it.
Read the lanes as text
- wait @25, from edge 3 (t=25): fails at edge 4 (t=35).
- wait @35, from edge 4 (t=35): passes at edge 5 (t=45).
Sampled values, edge 1 to 6: psel 0 1 1 1 1 0; penable 0 0 1 1 1 0; pready 0 0 0 0 1 0; paddr 000 100 100 104 104 104.
The program behind the lanes
The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of stable_addr that can start only at that lane's edge, so every verdict and its edge are read from that copy's own output.
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic psel = 0, penable = 0, pready = 0;
logic [11:0] paddr = 0;
stable_addr: assert property (@(posedge clk) psel && penable && !pready |=> $stable(paddr));
initial begin
@(negedge clk); psel = 1; paddr = 12'h100; // setup at 15
@(negedge clk); penable = 1; // access at 25, a wait
@(negedge clk); paddr = 12'h104; // access at 35, a wait: the address moved
@(negedge clk); pready = 1; // access at 45 completes
@(negedge clk); psel = 0; penable = 0; pready = 0;
@(negedge clk);
$finish;
end
endmodule
Measured on a hand-driven bus: the address moves in the second wait cycle, as move_in_wait does in the tour.
What the wires cannot say
Three faults of the library's completer pass every rule above, because they are faults of the register block, not of the bus. A completer with act_every_cycle acts at every edge its PSEL is high: a waited read of the FIFO pops three words and returns one. The handshake is perfect, and only the register contents show the damage. With late_rdata, PRDATA arrives one cycle after the completion edge, so every read returns the value of the read before it. The wires obey every timing rule; only the data is wrong. And a write that gets PSLVERR may or may not have changed the register. The specification allows either, so no checker may call one of them wrong.
This is the same split as in P2.16. The checker names the edge where a rule broke. The scoreboard keeps a model of the registers and catches the data that is legal on the wires and wrong in value. P3.10 runs both over every fault of the library.
Where to bind decides what is checked. A copy inside apb_periph watches that completer's own PSELx, which is the right place to check a requester or a bridge from the completer side. To check that a decoder never selects two completers, bind one copy to the decoded bus with NSEL equal to the number of select lines. That is the only place onehot can fire.
Asked in interviews: APB checkers
Which properties would you write for APB?
An access follows a setup or a waited access, and setup lasts one cycle. PSEL, PADDR, PWRITE, PPROT, PSTRB and the write data hold from setup until PREADY, and PENABLE stays high. PENABLE drops after the completion edge. PSTRB is zero on reads, one PSELx is high at a time, and nothing is selected in reset. Add a watchdog on PREADY with the system's own limit.
Is PREADY low in the setup cycle a rule?
No. PREADY may take any value while PENABLE is low, so a completer with no wait states can tie it high. A checker that demands PREADY low in setup rejects legal designs.
Is PSLVERR outside the last cycle an error?
No. It is only meaningful in the cycle where PSEL, PENABLE and PREADY are all high; driving it low elsewhere is a recommendation. Report it as a note if you report it at all.
Why is a timeout not part of the protocol checker proper?
Because APB allows any number of wait states. How long is too long is a property of the system, so the watchdog's limit comes from the peripheral's documentation, not from the specification.
What can an APB checker never catch?
Faults that keep the handshake legal: a completer that acts more than once per transfer, read data that arrives a cycle late, a register that ignores strobes. Those show only in the data, which needs a register model or scoreboard.
Try it
Nine phases, one rule each, the property line and the RULE line at the same edge. Phase 0 is correct traffic with one wait state per transfer and PSEL held between them, and nothing fires. Phase 6 prints notes, not failures: a completer that raises PSLVERR early breaks a recommendation, and the requester, which reads PSLVERR only at the completion edge, sees OKAY. The register was not written either, because this completer changes nothing on an error.
Then write a sentence of your own. Copy strb_read into the bench as a property that PPROT[2] (instruction) is 0 on every write. Break it with apb_write(32'h100, 32'h1, 4'hf, 3'b100); and read the edges it fires at. It fires at the setup edge and at the completion edge, because it is tried at every selected cycle.
-- phase 0: correct traffic, back to back with one wait state e3-e5 WRITE 0x0100 data=0x00000011 strb=1111 waits=1 OKAY e6-e8 WRITE 0x0014 data=0x00000022 strb=1111 waits=1 OKAY e9-e11 READ 0x0100 data=0x00000011 waits=1 OKAY e12-e14 READ 0x0014 data=0x00000022 waits=1 OKAY -- phase 1: setup, PSEL and PENABLE rise together RULE setup broken at edge 16: PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it property setup failed at edge 16: PENABLE is 1 without a setup cycle before it e16-e16 WRITE 0x0100 data=0x00000033 strb=1111 waits=0 OKAY -- phase 2: enable_drop, PENABLE held into the next transfer e17-e18 WRITE 0x0100 data=0x00000044 strb=1111 waits=0 OKAY RULE setup broken at edge 19: PENABLE is 1 without a setup cycle (PSEL 1, PENABLE 0) before it RULE enable_drop broken at edge 19: PENABLE is still 1 in the cycle after a completed transfer property enable_drop failed at edge 19: PENABLE still 1 after a completed transfer property setup failed at edge 19: PENABLE is 1 without a setup cycle before it e19-e19 WRITE 0x0018 data=0x00000055 strb=1111 waits=0 OKAY -- phase 3: stable, PADDR moves during a wait state RULE stable broken at edge 22: PADDR changed from 0x1c to 0x20 inside a transfer property stable failed at edge 22: PSEL, PADDR, PWRITE, PPROT or PSTRB changed inside a transfer e20-e23 WRITE 0x001c data=0x00000066 strb=1111 waits=2 OKAY -- phase 4: strb_read, strobes on a read property strb_read failed at edge 24: PSTRB is 1111 during a read RULE strb_read broken at edge 24: PSTRB is 1111 during a read property strb_read failed at edge 25: PSTRB is 1111 during a read RULE strb_read broken at edge 25: PSTRB is 1111 during a read e24-e25 READ 0x0100 data=0x00000044 waits=0 OKAY -- phase 5: setup_len, a setup phase that lasts two cycles RULE setup_len broken at edge 27: the setup cycle was not followed by an access cycle property setup_len failed at edge 27: the setup cycle was not followed by an access cycle -- phase 6: PSLVERR outside the completion cycle (a recommendation) note pslverr at edge 29: PSLVERR is 1 outside the completion cycle NOTE pslverr at edge 29: PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW) note pslverr at edge 30: PSLVERR is 1 outside the completion cycle NOTE pslverr at edge 30: PSLVERR is 1 outside the completion cycle (ignored by the requester; recommended LOW) e29-e31 WRITE 0x0104 data=0x00000001 strb=1111 waits=1 OKAY -- phase 7: timeout, a completer that never answers RULE timeout broken at edge 48: no PREADY after 16 wait cycles property timeout failed at edge 48: no PREADY within 16 access cycles e32-e53 READ 0x0100 no PREADY after 20 wait cycles, the bench gives up RULE stable broken at edge 54: PSEL dropped before PREADY property stable failed at edge 54: PENABLE dropped before PREADY property stable failed at edge 54: PSEL, PADDR, PWRITE, PPROT or PSTRB changed inside a transfer -- phase 8: reset, PSEL high while PRESETn is low property reset failed at edge 55: PSEL is 1 in reset RULE reset broken at edge 55: PSEL is 1 while PRESETn is low properties: 11 failure(s), 2 note(s) checker: 7 writes, 4 reads, 28 wait edges, 10 rules broken, 2 notesRead 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
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: apb_tasks.svh
// apb_tasks.svh: the requester side of the bench, as tasks for the stimulus (read-only).
// apb_write(addr, data, strb, prot) and apb_read(addr, prot) run one transfer and leave the bus
// IDLE; q_write / q_read queue transfers and apb_run() sends the queue back to back with PSEL held
// high (ACCESS goes straight to SETUP). Each transfer prints one log line at its completion edge:
// e3-e4 WRITE 0x0100 data=0x11223344 strb=1111 waits=0 OKAY
// (setup edge - completion edge). The result of a read is in prdata_q and pslverr_q; the results
// of a queue are in rdv[i] and errv[i]. `cycle` counts rising edges from 0.
int cycle = -1;
always @(posedge pclk) cycle = cycle + 1;
bit quiet = 0;
// Requester bugs, for the broken twins:
bit move_in_wait = 0; // PADDR moves by 4 in the first wait cycle
bit enable_stuck = 0; // PENABLE stays 1 into the next queued transfer (no setup cycle)
bit strb_on_read = 0; // PSTRB = 1111 on reads
bit skip_setup = 0; // PSEL and PENABLE rise together
int give_up = 24; // the bench stops waiting after this many wait cycles
logic [31:0] prdata_q = 0;
logic pslverr_q = 0;
int waits_q = 0;
bit stuck_now = 0;
bit q_wr [0:63];
logic [31:0] q_addr [0:63], q_data [0:63], rdv [0:63];
logic [3:0] q_strb [0:63];
logic [2:0] q_prot [0:63];
bit errv [0:63];
int qn = 0;
// A text waveform for the trace blocks: one line per rising edge with the values sampled there.
bit watch = 0;
int wave_edge = -1;
always @(posedge pclk) begin
wave_edge = wave_edge + 1;
`ifdef APB_DECODER
if (watch) $display("wave e%0d: psel=%b pselx=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
wave_edge, psel, pselx, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);
`else
if (watch) $display("wave e%0d: psel=%b penable=%b pwrite=%b paddr=%h pwdata=%h pstrb=%b pready=%b prdata=%h pslverr=%b",
wave_edge, psel, penable, pwrite, paddr, pwdata, pstrb, pready, prdata, pslverr);
`endif
end
// One transfer: SETUP for one cycle, then ACCESS until PREADY. last = 0 keeps PSEL high for the
// next transfer.
task automatic apb_xfer(input bit wr, input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb,
input logic [2:0] prot, input bit last);
int s_edge, w;
paddr = addr; pwrite = wr; pwdata = wr ? data : 32'h0;
pstrb = wr ? strb : (strb_on_read ? 4'hf : 4'h0);
pprot = prot; psel = 1;
s_edge = cycle + 1;
if (skip_setup || stuck_now) penable = 1;
else begin
penable = 0;
@(posedge pclk); #1 penable = 1;
end
stuck_now = 0;
w = 0;
@(posedge pclk);
while (pready !== 1'b1 && w < give_up) begin
w++;
#1 if (move_in_wait && w == 1) paddr = addr + 4;
@(posedge pclk);
end
prdata_q = prdata; pslverr_q = (pslverr === 1'b1); waits_q = w;
#1;
if (!quiet) begin
if (w >= give_up) $display("e%0d-e%0d %0s 0x%04h no PREADY after %0d wait cycles, the bench gives up", s_edge, cycle, wr ? "WRITE" : "READ ", addr[15:0], w);
else if (wr) $display("e%0d-e%0d WRITE 0x%04h data=0x%08h strb=%b waits=%0d %0s", s_edge, cycle, addr[15:0], data, strb, w, pslverr_q ? "PSLVERR" : "OKAY");
else $display("e%0d-e%0d READ 0x%04h data=0x%08h waits=%0d %0s", s_edge, cycle, addr[15:0], prdata_q, w, pslverr_q ? "PSLVERR" : "OKAY");
end
if (last) begin
psel = 0; penable = 0; pwrite = 0; pstrb = 0;
end else begin
penable = enable_stuck ? 1 : 0;
stuck_now = enable_stuck;
end
endtask
task automatic apb_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf, input logic [2:0] prot = 3'b000);
apb_xfer(1, addr, data, strb, prot, 1);
endtask
task automatic apb_read(input logic [31:0] addr, input logic [2:0] prot = 3'b000);
apb_xfer(0, addr, 0, 0, prot, 1);
endtask
// The queue: transfers sent back to back by apb_run().
task automatic q_write(input logic [31:0] addr, input logic [31:0] data, input logic [3:0] strb = 4'hf, input logic [2:0] prot = 3'b000);
q_wr[qn] = 1; q_addr[qn] = addr; q_data[qn] = data; q_strb[qn] = strb; q_prot[qn] = prot; qn++;
endtask
task automatic q_read(input logic [31:0] addr, input logic [2:0] prot = 3'b000);
q_wr[qn] = 0; q_addr[qn] = addr; q_data[qn] = 0; q_strb[qn] = 0; q_prot[qn] = prot; qn++;
endtask
task automatic apb_run();
for (int i = 0; i < qn; i++) begin
apb_xfer(q_wr[i], q_addr[i], q_data[i], q_strb[i], q_prot[i], i == qn - 1);
rdv[i] = prdata_q; errv[i] = pslverr_q;
end
qn = 0;
endtask
task automatic idle(input int n);
repeat (n) @(posedge pclk);
#1;
endtask
// The completer's RAM, by bus address (the decoder bench picks the completer from bits 13:12).
function automatic logic [7:0] ram_byte(input int unsigned a);
`ifdef APB_DECODER
case (a[13:12]) 2'd1: ram_byte = p1.mem[a[7:0]]; 2'd2: ram_byte = p2.mem[a[7:0]]; default: ram_byte = p0.mem[a[7:0]]; endcase
`else
ram_byte = p0.mem[a[7:0]];
`endif
endfunction
// dump(addr, n): n bytes from addr, one 32-bit word per group, lane 0 (the lowest address) first.
task automatic dump(input int unsigned addr, input int n);
string s;
for (int a = addr & ~3; a < addr + n; a += 4) begin
if ((a - (addr & ~3)) % 16 == 0) begin
if (a != (addr & ~3)) $display("%0s", s);
s = $sformatf("0x%04h ", a[15:0]);
end
s = {s, $sformatf(" %h %h %h %h ", ram_byte(a), ram_byte(a + 1), ram_byte(a + 2), ram_byte(a + 3))};
end
$display("%0s", s);
endtask
// fifo_fill(n, first): put n words first, first+1, ... in completer 0's FIFO before the run.
task automatic fifo_fill(input int n, input logic [31:0] first);
for (int i = 0; i < n; i++) p0.fifo[(p0.fhead + p0.fcount + i) % 16] = first + i;
p0.fcount = p0.fcount + n;
endtask
// regs(): completer 0's registers as one line.
task automatic regs();
$display("regs: CTRL=0x%08h STATUS=%0d WCOUNT=%0d", p0.ctrl, p0.fcount, p0.wcount);
endtask
Read included file: apb_props.sv
`timescale 1ns/1ps
// apb_props.sv: the rules of an APB interface as properties (read-only in this lesson).
// One spec sentence, one property, named like the rule of apb_checker.sv it mirrors. NSEL is the
// number of select lines the copy watches (1 inside a completer, 3 on a decoded bus). A property
// reports with $display, so a failure never stops the run; the PSLVERR sentence is a
// recommendation, so its copy counts notes, not failures.
module apb_props #(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 edge_n = -1, fails = 0, notes = 0;
always @(posedge pclk) edge_n++;
wire sel = (psel != 0);
// Sentence: PSEL is low while PRESETn is low (no transfer during reset).
reset_sel: assert property (@(posedge pclk) !presetn |-> !sel)
else begin fails = fails + 1; $display("property reset failed at edge %0d: PSEL is 1 in reset", edge_n); end
// Sentence: an access cycle (PSEL and PENABLE high) follows a setup cycle or a waited access cycle.
setup: assert property (@(posedge pclk) disable iff (!presetn) sel && penable |-> $past(sel && (!penable || !pready)))
else begin fails = fails + 1; $display("property setup failed at edge %0d: PENABLE is 1 without a setup cycle before it", edge_n); end
// Sentence: the setup phase lasts one cycle; the next cycle is the access phase.
setup_len: assert property (@(posedge pclk) disable iff (!presetn) sel && !penable |=> sel && penable)
else begin fails = fails + 1; $display("property setup_len failed at edge %0d: the setup cycle was not followed by an access cycle", edge_n); end
// Sentence: from setup until PREADY, the select, address, direction, protection, strobes and the
// write data stay unchanged, and PENABLE stays high once it has risen.
stable_ctl: assert property (@(posedge pclk) disable iff (!presetn) sel && (!penable || !pready) |=> $stable({psel, paddr, pwrite, pprot, pstrb}))
else begin fails = fails + 1; $display("property stable failed at edge %0d: PSEL, PADDR, PWRITE, PPROT or PSTRB changed inside a transfer", edge_n); end
stable_data: assert property (@(posedge pclk) disable iff (!presetn) sel && pwrite && (!penable || !pready) |=> $stable(pwdata))
else begin fails = fails + 1; $display("property stable failed at edge %0d: PWDATA changed inside a write", edge_n); end
stable_en: assert property (@(posedge pclk) disable iff (!presetn) sel && penable && !pready |=> penable)
else begin fails = fails + 1; $display("property stable failed at edge %0d: PENABLE dropped before PREADY", edge_n); end
// Sentence: at the end of a transfer PENABLE goes low.
enable_drop: assert property (@(posedge pclk) disable iff (!presetn) sel && penable && pready |=> !penable)
else begin fails = fails + 1; $display("property enable_drop failed at edge %0d: PENABLE still 1 after a completed transfer", edge_n); end
// Sentence: the requester drives every PSTRB bit low during a read.
strb_read: assert property (@(posedge pclk) disable iff (!presetn) sel && !pwrite |-> pstrb == 0)
else begin fails = fails + 1; $display("property strb_read failed at edge %0d: PSTRB is %b during a read", edge_n, pstrb); end
// Sentence: one PSELx at a time (a decoded bus).
onehot: assert property (@(posedge pclk) disable iff (!presetn) $onehot0(psel))
else begin fails = fails + 1; $display("property onehot failed at edge %0d: PSELx is %b", edge_n, psel); end
// A watchdog, not a spec sentence: PREADY within TIMEOUT access cycles of a setup.
timeout: assert property (@(posedge pclk) disable iff (!presetn) sel && !penable |=> ##[0:TIMEOUT-1] pready)
else begin fails = fails + 1; $display("property timeout failed at edge %0d: no PREADY within %0d access cycles", edge_n, TIMEOUT); end
// Recommendation: PSLVERR is driven low outside the completion cycle. A note, not a failure.
pslverr_low: assert property (@(posedge pclk) disable iff (!presetn) pslverr |-> sel && penable && pready)
else begin notes = notes + 1; $display("note pslverr at edge %0d: PSLVERR is 1 outside the completion cycle", edge_n); end
final $display("properties: %0d failure(s), %0d note(s)", fails, notes);
endmodule
Check yourself
Remember
- APB's checker is short because APB is: one transfer at a time, no IDs, no bursts; the properties only need the previous cycle, through
$pastand$stable. - An access follows a setup or a waited access; setup lasts one cycle; everything the requester drives holds until PREADY; PENABLE drops after completion; PSTRB is zero on reads.
- PREADY in setup and PSLVERR outside completion are not rules: one is a don't-care, the other a recommendation. A checker that enforces them rejects legal designs.
- A timeout is a watchdog with the system's limit, because APB allows any number of wait states.
- Faults that keep the handshake legal (acting twice, late read data, ignored strobes) show only in the data. Bind the checker inside each completer and keep a register model beside it.