The APB peripheral capstone
Verify a register-programmed APB peripheral with the whole track's machinery, follow one byte from the virtual sequence to the comparator, and localise a planted design bug from the bench's report alone.
- Lesson
- U16.4
- Time
- 35 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
A checksum peripheral adds or xors the bytes software writes to it. Its block bench passes every test that runs in one mode. The first test that switches modes fails, but only after the switch, and CTRL still reads back the new mode. The report has to tell you where to look before you open the RTL.
| Phase | CTRL.mode | Coverage bin | Blocks | Comparator |
|---|---|---|---|---|
| 1 | add | rst->add | 1, 2 | Match |
| 2 | xor | add->xor | 3, 4 | RESULT 4a, expected 4; RESULT a4, expected 8 |
| 3 | xor | xor->xor | 5, 6 | RESULT 8a, expected 4; RESULT 24, expected 8 |
| 4 | add | xor->add | 7, 8 | Match |
Measured in the example below. Every xor block fails, CTRL reads back correctly, and the add blocks after xor pass: the engine never left add.
The peripheral and its test plan
The design has four registers. CTRL.mode (0x00) selects add or xor. Each write to DATA (0x04) feeds one byte, and RESULT (0x08) holds the checksum since CTRL was last written. IRQ.done (0x0C) is set after every fourth byte and cleared by writing 1. Writing CTRL clears the checksum and must take effect from the next byte.
| Requirement | Check | Coverage | Plan step |
|---|---|---|---|
| R1 add mode sums the bytes, mod 256 | Comparator: RESULT against the reference model | rst->add | Phase 1 |
| R2 xor mode xors the bytes | Comparator | xor->xor | Phase 3 |
| R3 a CTRL write takes the new mode from the next byte | Comparator | add->xor, xor->add | Phases 2 and 4 |
| R4 IRQ.done after every fourth byte, write 1 clears it | Service routine reads IRQ = 1 | Blocks serviced | Every block |
| R5 every result is read, none twice | End scan: PENDING, EXTRA | - | check_phase |
| R6 CTRL reads back what was written | mirror(UVM_CHECK) | - | End of the plan |
The integrated test plan. Each requirement has a check that fails loudly and, where it depends on configuration, a coverage bin that proves it was exercised.
R3 is the requirement a single-mode test cannot reach. Its two bins name changes of mode after traffic, so the plan runs four phases: add, xor, xor again and add. The first configuration after reset gets its own rst-> bins, because a mode chosen before any byte is not a reconfiguration. A goal bin at 0 is a hole in the plan, whatever the comparator says.
Who does what in the bench
| Component | Job here | From |
|---|---|---|
apb_agent | Active: sequencer, driver and monitor on one interface | U11.1, U13.2 |
chk_regs + apb_adapter | Register model with the burst-aware adapter | U13.1, U13.2 |
uvm_reg_predictor | Explicit prediction from the monitor; auto prediction off | U13.3 |
chk_model | Untimed reference model of the engine, written from the specification | U9.2 |
chk_predictor, chk_comparator | Predictor turns operations into expectations; comparator checks RESULT and scans for leftovers | U8.1, U7.3 |
chk_coverage | Blocks per mode transition | U15.1 |
chk_vseq | Configuration through the model, traffic through the agent, interrupt service | U10.1, U14.3 |
Everything is in chk_bench.svh; main.sv holds only the design and tb.
One choice holds the scoreboard together. The predictor does not listen to the monitor directly; it listens to the register predictor's reg_ap. That port publishes one register operation per observed transfer, in bus order, after the mirror has been updated. So when a DATA write arrives, the CTRL write before it has already set the mirrored mode, and no same-edge ordering question arises (U13.3).
One byte, end to end
- Traffic
The virtual sequence starts
chk_bytes_seqon the agent's sequencer. The fourth byte, 14, goes out as an APB write to DATA.More context
Block 3 is phase 2 of the plan: CTRL was written to xor first, then bytes 11, 12 and 13. CTRL reads back correctly in this run: "REG CTRL read-back mismatches: 0".
Follow the calls, values and waits
- data APB write 14 to DATA (0x04): chk_vseq → apb_agent driver
- data APB write 14 to DATA (0x04): apb_agent driver → chk_csr
- The design
The engine adds 11 + 12 + 13 + 14 = 4a, because its copy of the mode is still add. It sets IRQ.done.
- Observed and predicted
The monitor publishes the write; the register predictor updates the DATA register and passes the operation on through
reg_ap.More context
uvm_reg_predictorupdates the mirror first and only then writesreg_ap, so a subscriber always sees the mirror after the operation it is told about.Follow the calls, values and waits
- data APB write 14 to DATA (0x04): chk_csr → apb_monitor
- data APB write 14 to DATA (0x04): apb_monitor → uvm_reg_predictor
- Expected
The predictor feeds 14 to the reference model, which is in xor mode from the earlier CTRL write: 11 ^ 12 ^ 13 ^ 14 = 4. Block 3 expects 4, labelled add->xor.
More context
The model took the xor mode from the mirrored CTRL value when the CTRL write came through
reg_ap. Nothing here reads the design.Follow the calls, values and waits
- data reg op: DATA write 14: uvm_reg_predictor → chk_predictor + chk_model
- data block 3: expect 4: chk_predictor + chk_model → chk_comparator
- data block label add->xor: chk_predictor + chk_model → chk_coverage
- Waiting: chk_comparator — expectation 4 queued for block 3
- Checked655 ns (illustrative)
The sequence services the interrupt and reads RESULT. That read reaches the comparator through the same path: 4a, expected 4, a
UVM_ERROR [CMP]at 655 ns.More context
The predictor forwards a RESULT read on its second port,
act_ap; the arrow stands for both ports. The log line is "[CMP] block 3 (add->xor): RESULT 4a, expected 4".Follow the calls, values and waits
- data read RESULT (0x08): chk_vseq → apb_agent driver
- data read RESULT (0x08): apb_agent driver → chk_csr
- data RESULT read = 4a: chk_csr → apb_monitor
- data RESULT read = 4a: apb_monitor → uvm_reg_predictor
- data RESULT read = 4a: uvm_reg_predictor → chk_predictor + chk_model
- data RESULT read = 4a: chk_predictor + chk_model → chk_comparator
- Released: chk_comparator
Read the diagram as text
- chk_vseq: Phase 2: CTRL = xor, then four DATA bytes
- apb_agent driver: APB write of 14 to 0x04
- chk_csr: Engine still adds: RESULT 4a; IRQ.done = 1
- apb_monitor: Publishes each completed transfer
- uvm_reg_predictor: Predicts the register, then writes reg_ap
- chk_predictor + chk_model: Model xors: block 3 expects 4 (add->xor)
- chk_comparator: RESULT read 4a against 4: mismatch
- chk_coverage: add->xor: one more block
- chk_vseq → apb_agent driver: apb_item
- apb_agent driver → chk_csr: APB write
- chk_csr → apb_monitor: pins sampled
- apb_monitor → uvm_reg_predictor: ap.write(t)
- uvm_reg_predictor → chk_predictor + chk_model: reg_ap: DATA write 14
- chk_predictor + chk_model → chk_comparator: exp_ap, act_ap
- chk_predictor + chk_model → chk_coverage: block label
The expectation comes only from observed operations and the specification, never from the design's internal signals.
Every expectation therefore depends only on what was observed on the bus and on the specification. If the reference model and the design disagree, the report names the block and the configuration. The bench never looks inside the design to decide which one is right. The comparator checks results in order, because the bus serializes them. At the end, check_phase reports any expectation that was never read as PENDING, and a read with nothing waiting as EXTRA. Before the objection drops, the test waits a 100 ns quiet window and fails if any transfer arrives in it.
Design decisions behind the bench
An active agent with explicit prediction. The agent drives the bus at block level, but the register model learns only from the monitor. At chip level the processor drives APB; the same agent becomes passive and the scoreboard keeps working, because nothing in it depends on who started a transfer.
An untimed model fed by register operations. chk_model knows nothing about APB timing. It sees operations in the order the bus completed them, so it needs no clock. The same model would serve a test that loads the engine through a DMA port.
In-order checking, with accounting at the end. One APB bus completes one transfer at a time, so results come back in the order blocks finish, and a queue is enough. That assumption is stated in the comparator, and the end scan proves no expectation was left behind.
Coverage of transitions, not modes. Counting add and xor blocks would show full coverage for the xor-only plan. The bins name the change from the previous mode, because the requirement does.
Reading the report
The example runs the design as delivered. Its report gives the evidence; localise before you open the RTL.
Which check fired?
Four
[CMP]errors, all on xor blocks. No PENDING, no EXTRA, no IRQ error: every block was signalled, serviced and read once.Is the register path at fault?
REG CTRL read-back mismatches: 0. The design stores and returns the mode correctly, so the decoder and CTRL are fine.Which bins pass and fail?
rst->add and xor->add pass; add->xor and xor->xor fail. The design always computes add, whatever CTRL says.
What would explain that?
A copy of the mode taken once and never refreshed. In
chk_csr,opis loaded at the first byte whilefirstis 1, and a CTRL write never setsfirstagain.
| Component | What it contributed to this diagnosis |
|---|---|
| Comparator | Four mismatches, each with its block and transition label |
| End scan and quiet window | No PENDING or EXTRA: the failure is wrong data, not lost or late data |
| Interrupt service | 8 blocks signalled and cleared: the byte counter and IRQ work |
| Register model | CTRL read back with no mismatch: the configuration path works |
| Coverage | Every goal bin hit, so the passing bins mean something; failures sit on exactly the xor bins |
Each line of the bench report rules out a part of the design. Together they leave the engine's mode select.
A weaker plan would have hidden the bug. Measured on the same design, a plan that runs xor alone from reset passes: the first byte loads xor into op, and both blocks match. Its coverage line says rst->xor 2 and leaves both R3 bins at 0, which is the warning. An earlier version of this bench labelled that first configuration add->xor, after CTRL's reset value. It reported an R3 bin as hit by a plan that never reconfigured. A bin is only as good as its definition.
The fix, and why the mode copy exists at all
The fix is one line in the CTRL branch: first <= 1;, so the next byte reloads op. Loading op <= bus.pwdata[0] directly also works here.
A copy like op is common in real engines: it keeps a mode change from taking effect in the middle of a block. The specification must say when the copy is taken. The reference model encodes R3 as "from the next byte after a CTRL write", and that sentence is what the comparator enforced.
Try it
The four UVM_ERROR [CMP] lines come from the comparator; the bench report after them is printed by the test's report_phase. Read them in the order of the steps above.
Try it: add first <= 1; to the CTRL branch of chk_csr and run again: the report shows 8 matched blocks and PASS. Then put the bug back and add a test class that sets modes = '{1}: the run passes, and the coverage line shows why it should not be trusted.
UVM_ERROR chk_bench.svh(265) @ 655: uvm_test_top.env.cmp [CMP] block 3 (add->xor): RESULT 4a, expected 4 UVM_ERROR chk_bench.svh(265) @ 865: uvm_test_top.env.cmp [CMP] block 4 (add->xor): RESULT a4, expected 8 UVM_ERROR chk_bench.svh(265) @ 1105: uvm_test_top.env.cmp [CMP] block 5 (xor->xor): RESULT 8a, expected 4 UVM_ERROR chk_bench.svh(265) @ 1315: uvm_test_top.env.cmp [CMP] block 6 (xor->xor): RESULT 24, expected 8 BENCH REPORT at 1930 ns, 61 APB transfers REG CTRL read-back mismatches: 0 IRQ blocks signalled and cleared: 8 SB 8 blocks expected, 4 matched, 4 mismatched, 0 pending, 0 extra COV rst->add 2, rst->xor 0, add->add 0, add->xor 2, xor->xor 2, xor->add 2 FAIL: 4 UVM_ERROR reports UVM_ERROR : 4Read included file: chk_bench.svh
// chk_bench.svh: the UVM bench for the checksum peripheral. main.sv holds the design and tb.
// ---------------- APB agent (the bus from U13 and U14, packaged as an agent) ----------------
interface apb_if (input logic clk);
logic psel = 0, penable = 0, pwrite = 0;
logic [31:0] paddr = 0, pwdata = 0;
logic [31:0] prdata;
logic pready;
endinterface
class apb_item extends uvm_sequence_item;
`uvm_object_utils(apb_item)
bit write;
bit [31:0] addr, data;
function new(string name = "apb_item"); super.new(name); endfunction
endclass
class apb_adapter extends uvm_reg_adapter;
`uvm_object_utils(apb_adapter)
function new(string name = "apb_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 0;
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_item t = apb_item::type_id::create("t");
t.write = (rw.kind inside {UVM_WRITE, UVM_BURST_WRITE});
t.addr = rw.addr;
t.data = rw.data;
return t;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_item t;
if (!$cast(t, bus_item)) `uvm_fatal("NOTAPB", "bus2reg needs an apb_item")
rw.kind = t.write ? UVM_WRITE : UVM_READ;
rw.addr = t.addr;
rw.data = t.data;
rw.status = UVM_IS_OK; // this APB has no pslverr
endfunction
endclass
class apb_driver extends uvm_driver #(apb_item);
`uvm_component_utils(apb_driver)
virtual apb_if vif;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
task run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
@(negedge vif.clk); // setup phase
vif.psel <= 1; vif.penable <= 0;
vif.pwrite <= req.write; vif.paddr <= req.addr; vif.pwdata <= req.data;
@(negedge vif.clk); // access phase
vif.penable <= 1;
do @(posedge vif.clk); while (!vif.pready);
if (!req.write) req.data = vif.prdata;
@(negedge vif.clk);
vif.psel <= 0; vif.penable <= 0;
seq_item_port.item_done();
end
endtask
endclass
// Publishes every completed transfer, whoever started it.
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
virtual apb_if vif;
uvm_analysis_port #(apb_item) ap;
int n; // transfers observed
function new(string name, uvm_component parent); super.new(name, parent); ap = new("ap", this); endfunction
task run_phase(uvm_phase phase);
forever begin
@(posedge vif.clk);
if (vif.psel && vif.penable && vif.pready) begin
apb_item t = apb_item::type_id::create("t");
t.write = vif.pwrite;
t.addr = vif.paddr;
t.data = vif.pwrite ? vif.pwdata : vif.prdata;
n++;
ap.write(t);
end
end
endtask
endclass
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
uvm_sequencer #(apb_item) sqr;
apb_driver drv;
apb_monitor mon;
virtual apb_if vif;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
if (!uvm_config_db #(virtual apb_if)::get(this, "", "vif", vif)) `uvm_fatal("NOVIF", "no virtual interface")
mon = apb_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = uvm_sequencer #(apb_item)::type_id::create("sqr", this);
drv = apb_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
mon.vif = vif;
if (get_is_active() == UVM_ACTIVE) begin
drv.vif = vif;
drv.seq_item_port.connect(sqr.seq_item_export);
end
endfunction
endclass
// ---------------- Register model ----------------
class chk_ctrl extends uvm_reg;
`uvm_object_utils(chk_ctrl)
rand uvm_reg_field mode;
function new(string name = "chk_ctrl"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
mode = uvm_reg_field::type_id::create("mode");
mode.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
endfunction
endclass
class chk_data extends uvm_reg;
`uvm_object_utils(chk_data)
uvm_reg_field din;
function new(string name = "chk_data"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
din = uvm_reg_field::type_id::create("din");
din.configure(this, 8, 0, "WO", 0, 8'h00, 1, 0, 0);
endfunction
endclass
class chk_result extends uvm_reg;
`uvm_object_utils(chk_result)
uvm_reg_field acc;
function new(string name = "chk_result"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
acc = uvm_reg_field::type_id::create("acc");
acc.configure(this, 8, 0, "RO", 1, 8'h00, 1, 0, 0); // volatile: the engine changes it
endfunction
endclass
class chk_irq extends uvm_reg;
`uvm_object_utils(chk_irq)
uvm_reg_field done;
function new(string name = "chk_irq"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
done = uvm_reg_field::type_id::create("done");
done.configure(this, 1, 0, "W1C", 1, 1'b0, 1, 0, 0); // volatile: the engine sets it
endfunction
endclass
class chk_regs extends uvm_reg_block;
`uvm_object_utils(chk_regs)
rand chk_ctrl ctrl;
chk_data data;
chk_result result;
chk_irq irq;
function new(string name = "chk_regs"); super.new(name, UVM_NO_COVERAGE); endfunction
virtual function void build();
ctrl = chk_ctrl::type_id::create("ctrl"); ctrl.configure(this); ctrl.build();
data = chk_data::type_id::create("data"); data.configure(this); data.build();
result = chk_result::type_id::create("result"); result.configure(this); result.build();
irq = chk_irq::type_id::create("irq"); irq.configure(this); irq.build();
default_map = create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
default_map.add_reg(ctrl, 'h00, "RW");
default_map.add_reg(data, 'h04, "WO");
default_map.add_reg(result, 'h08, "RO");
default_map.add_reg(irq, 'h0C, "RW");
lock_model();
endfunction
endclass
// ---------------- Reference model: untimed, from the specification only ----------------
class chk_model;
bit mode; // 0 = add, 1 = xor
bit prev; // the mode before the last CTRL write
bit from_reset; // the last CTRL write was the first since reset
bit configured;
bit [7:0] acc;
int nbytes;
function void configure(bit m);
from_reset = !configured;
configured = 1;
prev = mode; mode = m; acc = 0; nbytes = 0;
endfunction
// Returns 1 when this byte completes a block of four.
function bit feed(bit [7:0] b);
acc = mode ? (acc ^ b) : (acc + b);
nbytes++;
return (nbytes % 4 == 0);
endfunction
function string label();
string from, to;
if (from_reset) from = "rst"; else if (prev) from = "xor"; else from = "add";
if (mode) to = "xor"; else to = "add";
return {from, "->", to};
endfunction
endclass
class chk_expect extends uvm_object;
`uvm_object_utils(chk_expect)
bit [7:0] value;
int block;
string label;
function new(string name = "chk_expect"); super.new(name); endfunction
endclass
// ---------------- Scoreboard: predictor and comparator ----------------
// The predictor listens to the register predictor's reg_ap: one register operation per
// observed transfer, in bus order, published after the mirror was updated. CTRL writes
// configure the model with the mirrored mode; DATA writes feed it; every fourth byte yields
// one expectation. RESULT reads go to the comparator as the actual values.
class chk_predictor extends uvm_subscriber #(uvm_reg_item);
`uvm_component_utils(chk_predictor)
chk_regs regs;
chk_model model = new;
uvm_analysis_port #(chk_expect) exp_ap;
uvm_analysis_port #(uvm_reg_item) act_ap;
int blocks;
function new(string name, uvm_component parent);
super.new(name, parent);
exp_ap = new("exp_ap", this);
act_ap = new("act_ap", this);
endfunction
function void write(uvm_reg_item t);
uvm_object e = t.get_element();
string name = e.get_name();
if (t.get_kind() == UVM_WRITE && name == "ctrl")
model.configure(regs.ctrl.mode.get_mirrored_value());
else if (t.get_kind() == UVM_WRITE && name == "data") begin
bit [7:0] b = t.get_value(0);
if (model.feed(b)) begin
chk_expect x = chk_expect::type_id::create("x");
x.value = model.acc;
x.block = ++blocks;
x.label = model.label();
exp_ap.write(x);
end
end
else if (t.get_kind() == UVM_READ && name == "result")
act_ap.write(t);
endfunction
endclass
`uvm_analysis_imp_decl(_exp)
`uvm_analysis_imp_decl(_act)
class chk_comparator extends uvm_scoreboard;
`uvm_component_utils(chk_comparator)
uvm_analysis_imp_exp #(chk_expect, chk_comparator) exp_in;
uvm_analysis_imp_act #(uvm_reg_item, chk_comparator) act_in;
chk_expect pending[$];
int n_exp, n_match, n_mismatch, n_extra;
function new(string name, uvm_component parent);
super.new(name, parent);
exp_in = new("exp_in", this);
act_in = new("act_in", this);
endfunction
function void write_exp(chk_expect x); pending.push_back(x); n_exp++; endfunction
function void write_act(uvm_reg_item t);
bit [7:0] got = t.get_value(0);
chk_expect x;
if (pending.size() == 0) begin
n_extra++;
`uvm_error("EXTRA", $sformatf("RESULT read %0h with no block waiting", got))
return;
end
x = pending.pop_front();
if (got == x.value) n_match++;
else begin
n_mismatch++;
`uvm_error("CMP", $sformatf("block %0d (%s): RESULT %0h, expected %0h", x.block, x.label, got, x.value))
end
endfunction
// End scan: every expectation must have been read.
function void check_phase(uvm_phase phase);
foreach (pending[i])
`uvm_error("PENDING", $sformatf("block %0d (%s): expected %0h was never read", pending[i].block, pending[i].label, pending[i].value))
endfunction
endclass
// ---------------- Coverage: blocks per mode transition (rst = first configuration) ----------------
class chk_coverage extends uvm_subscriber #(chk_expect);
`uvm_component_utils(chk_coverage)
int hits[string];
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void write(chk_expect t);
if (hits.exists(t.label)) hits[t.label]++; else hits[t.label] = 1;
endfunction
function int count(string bin);
if (hits.exists(bin)) return hits[bin];
return 0;
endfunction
endclass
class chk_env extends uvm_env;
`uvm_component_utils(chk_env)
apb_agent agent;
chk_regs regs;
apb_adapter adapter;
uvm_reg_predictor #(apb_item) reg_pred;
chk_predictor pred;
chk_comparator cmp;
chk_coverage cov;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
agent = apb_agent::type_id::create("agent", this);
regs = chk_regs::type_id::create("regs");
regs.build();
adapter = apb_adapter::type_id::create("adapter");
reg_pred = uvm_reg_predictor #(apb_item)::type_id::create("reg_pred", this);
pred = chk_predictor::type_id::create("pred", this);
cmp = chk_comparator::type_id::create("cmp", this);
cov = chk_coverage::type_id::create("cov", this);
endfunction
function void connect_phase(uvm_phase phase);
regs.default_map.set_sequencer(agent.sqr, adapter);
regs.default_map.set_auto_predict(0); // one prediction path: the monitor
reg_pred.map = regs.default_map;
reg_pred.adapter = adapter;
agent.mon.ap.connect(reg_pred.bus_in);
reg_pred.reg_ap.connect(pred.analysis_export);
pred.regs = regs;
pred.exp_ap.connect(cmp.exp_in);
pred.exp_ap.connect(cov.analysis_export);
pred.act_ap.connect(cmp.act_in);
endfunction
endclass
// ---------------- Stimulus ----------------
// Traffic: raw DATA writes on the agent's sequencer.
class chk_bytes_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(chk_bytes_seq)
bit [7:0] bytes[$];
function new(string name = "chk_bytes_seq"); super.new(name); endfunction
task body();
foreach (bytes[i]) begin
apb_item t = apb_item::type_id::create("t");
start_item(t);
t.write = 1; t.addr = 'h04; t.data = bytes[i];
finish_item(t);
end
endtask
endclass
// The virtual sequence: configuration through the register model, traffic through the agent,
// and interrupt service after every block of four bytes.
class chk_vseq extends uvm_sequence;
`uvm_object_utils(chk_vseq)
chk_regs regs;
uvm_sequencer #(apb_item) sqr;
bit modes[$];
int blocks_per_mode = 2;
bit skip_last_read;
int serviced;
function new(string name = "chk_vseq"); super.new(name); endfunction
task body();
uvm_status_e st;
uvm_reg_data_t v;
foreach (modes[p]) begin
regs.ctrl.write(st, modes[p], UVM_FRONTDOOR, null, this);
if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL write failed")
for (int blk = 0; blk < blocks_per_mode; blk++) begin
chk_bytes_seq traffic = chk_bytes_seq::type_id::create("traffic");
for (int k = 0; k < 4; k++) traffic.bytes.push_back(8'h10 * p + 4 * blk + k + 1);
traffic.start(sqr, this);
// service the interrupt: done must be set; read RESULT; clear done
regs.irq.read(st, v, UVM_FRONTDOOR, null, this);
if (st != UVM_IS_OK || v[0] != 1) `uvm_error("IRQ", "done not set after four bytes")
if (!(skip_last_read && p == modes.size() - 1 && blk == blocks_per_mode - 1)) begin
regs.result.read(st, v, UVM_FRONTDOOR, null, this);
if (st != UVM_IS_OK) `uvm_error("RESULT", "RESULT read failed")
end
regs.irq.write(st, 1, UVM_FRONTDOOR, null, this);
if (st != UVM_IS_OK) `uvm_error("IRQ", "clear failed")
serviced++;
end
end
endtask
endclass
// ---------------- The test: the integrated plan ----------------
class chk_test extends uvm_test;
`uvm_component_utils(chk_test)
chk_env env;
chk_vseq vseq;
bit modes[$] = '{0, 1, 1, 0}; // add, then xor, xor again, add: four transitions
bit skip_last_read;
function new(string name, uvm_component parent); super.new(name, parent); endfunction
function void build_phase(uvm_phase phase);
env = chk_env::type_id::create("env", this);
endfunction
task run_phase(uvm_phase phase);
uvm_status_e st;
int seen;
phase.raise_objection(this);
vseq = chk_vseq::type_id::create("vseq");
vseq.regs = env.regs;
vseq.sqr = env.agent.sqr;
vseq.modes = modes;
vseq.skip_last_read = skip_last_read;
vseq.start(null);
env.regs.ctrl.mirror(st, UVM_CHECK); // CTRL must read back what the model predicted
if (st != UVM_IS_OK) `uvm_error("CFG", "CTRL read failed")
seen = env.agent.mon.n; // quiet window: nothing more may arrive
#100;
if (env.agent.mon.n != seen) `uvm_error("QUIET", "transfers after the plan ended")
phase.drop_objection(this);
endtask
function void report_phase(uvm_phase phase);
uvm_report_server svr = uvm_report_server::get_server();
int errors = svr.get_severity_count(UVM_ERROR);
$display("BENCH REPORT at %0t ns, %0d APB transfers", $time, env.agent.mon.n);
$display("REG CTRL read-back mismatches: %0d", svr.get_id_count("RegModel"));
$display("IRQ blocks signalled and cleared: %0d", vseq.serviced);
$display("SB %0d blocks expected, %0d matched, %0d mismatched, %0d pending, %0d extra",
env.cmp.n_exp, env.cmp.n_match, env.cmp.n_mismatch, env.cmp.pending.size(), env.cmp.n_extra);
$display("COV rst->add %0d, rst->xor %0d, add->add %0d, add->xor %0d, xor->xor %0d, xor->add %0d",
env.cov.count("rst->add"), env.cov.count("rst->xor"), env.cov.count("add->add"),
env.cov.count("add->xor"), env.cov.count("xor->xor"), env.cov.count("xor->add"));
if (errors == 0) $display("PASS: no errors");
else $display("FAIL: %0d UVM_ERROR reports", errors);
endfunction
endclass
Check yourself
Remember
- Write the test plan as requirement, check, coverage bin and plan step; a requirement about a change needs transition bins.
- Build expectations from observed operations and the specification: here the register predictor's
reg_ap, which arrives in bus order after the mirror is updated. - End with an end scan and a quiet window, so a result that is never read, or arrives late, fails the test.
- Localise from the report first: which check fired, whether the register path reads back, and which coverage bins pass or fail.
Practice
- The interrupt that clears itself Verify · Hard