Day 8RAL, the golden model and the second capstone
The same slides as the deck, with the instructor notes inside the text and the complete code. If you are taking the course on your own, start here and keep the deck alongside for the reviews.
Yesterday we left…
Where the testbench stands
- The capstone handed in: an APB slave, its spec, and a whole testbench written from a blank sheet — monitor, driver, scoreboard, coverage and properties
- The scoreboard of that testbench predicts by hand the value of every register, and the coverage of the bus was written row by row
- And that is done the same way at work… until the DUT has sixty registers. That is when both things get modelled, and that is the whole of day 8
This day is optional and it goes after the wrap-up, and both things are on purpose. The course ends on day 7: whoever got that far wrote a complete UVM testbench from a blank sheet and lacks nothing to go to work. What is here is what gets added once that has happened. The recap has a concrete job on day 8: the three pieces of today are only understood as a replacement for something the student has just written by hand. RAL replaces the register-by-register prediction, the golden model in C replaces the whole prediction, and the second capstone shows where the pattern of the first stops working. Without the capstone done, all three are abstractions without a problem.
Agenda
Day 8 · ≈ 4 h · optional · what comes after the capstone
- RAL, unit 9 — the register model over the same APB
- The reference model in C, through DPI
- The second capstone: a FIFO with backpressure
By the end of the day you can:
- Read and write a register by name, without writing a single
PADDR, and say what the adapter and the predictor do - Plug in a reference model written in C, and prove it with a mutation
- Write a scoreboard for a DUT where a table is not enough
For whoever teaches it: in a company this is another half day, and it usually
gets taught when the group has real registers in the project. In a term it is the
optional unit, or the final assignment for those who want a high mark.
The three objectives are deliberately concrete and not "understand RAL": day 8
has no row in the 175b self-assessment, so the rubric the student takes away is
these three verbs and the eight-question review. The third is the one that pays off the most of the three and
the least expected — a FIFO has order and occupancy, and that does not fit in a
four-row table.
RAL
Unit 9 · optional · the register stops being an address
- Every chip has configuration registers, and a small SoC has two hundred
- Without RAL, the testbench writes them like this:
addr = 8'h04,wdata = ..., and the scoreboard keeps alogic [31:0]for each one - The day the architect moves
SCRATCHfrom0x04to0x24, you have to look for8'h04all over the testbench — and0x04was also the value of something else - RAL —Register Abstraction Layer— is the layer that puts a name on that:
model.SCRATCH.write(status, 'h1000_0000) - It is optional and it goes at the end on purpose: it is only understood once you have written the testbench it replaces
This unit is short and it is the last one, and both things are deliberate. RAL is the
piece of UVM most often taught without context —there are courses that give it on day
2— and that way it is not understood: it looks like one more API. With the capstone just done,
on the other hand, the student has the comparison inside their head. They wrote the
scoreboard that predicts CTRL; now I show them the library that does it on its own,
and also where it does not.
The number of 200 registers is not rhetorical. A PHY block or a memory
controller has that many, and the register spec arrives as a spreadsheet. The
next step, which this course does not take, is generating the model from that spreadsheet
with IP-XACT or SystemRDL: nobody writes 200 registers by hand. It is worth saying, because
otherwise the student walks away with the idea that RAL is a lot of typing.
And the argument about moving 0x04 to 0x24 is the one that convinces a
sceptic: it is not elegance, it is that the map gets written only once.
RAL
Three pieces, and only one knows what bus is underneath
| Piece | What it is | How many times it gets written |
|---|---|---|
uvm_reg_block |
the map: registers, fields, addresses | one per DUT |
uvm_reg_adapter |
translates access ↔ bus transaction | one per protocol |
uvm_reg_predictor |
what the monitor saw, said in registers | none: it comes done |
- The model does not know there is an APB underneath. You change the adapter and the same model drives AHB
- The adapter is the file that comes with a VIP you bought. It is twenty lines
- And underneath everything, without a single change: the driver, the monitor and the agent of the capstone
The table is the slide. That the third column says one per DUT, one per
protocol and none is the whole economic argument for RAL, and it is the one that
holds the decision up in a project meeting.
It is worth pointing at the last line with a finger: nothing the student wrote yesterday gets
replaced here. code/u9/ral/ compiles the interface, the transaction,
the driver, the monitor and the agent of d7-final/solucion/ exactly as they are, through
+incdir. RAL is a layer, not a different testbench — and it is the reason why
this unit fits into three files.
If somebody asks about the backdoor: it exists, it gets declared with add_hdl_path and it reads
the signal of the RTL without going through the bus. It does not go into the course because it depends on the
hierarchy support of the simulator, which is precisely what changes between
tools. The frontdoor —the one that uses the real bus— is the one you have to know.
RAL
The model: the table of the spec, in code
code/u9/ral/tb_classes/apb_reg_block.svh · #ctrl_reg// 0x00 -- CTRL. EN is an ordinary bit. CLR is not: writing a 1 clears ACC and
// OVF, and the bit always reads back 0.
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg)
rand uvm_reg_field EN;
rand uvm_reg_field CLR;
function new(string name = "ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
// configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
// is_rand, individually_accessible)
virtual function void build();
EN = uvm_reg_field::type_id::create("EN");
EN.configure(this, 1, 0, "RW", 0, 'h0, 1, 1, 0);
// "WC" is the name UVM has for exactly this: the write clears the field,
// the mirror drops to zero, and the read returns that zero. Modelling it as
// "RW" is the classic mistake, and it is what +MAL does. "WOC" also passes,
// for the wrong reason: any WO* access takes the field out of bit_bash and
// out of do_check, so nobody looks at it. See the RAL unit slides.
CLR = uvm_reg_field::type_id::create("CLR");
CLR.configure(this, 1, 1, $test$plusargs("MAL") ? "RW" : "WC", 0, 'h0, 1, 1, 0);
endfunction : build
endclass : ctrl_regel archivo entero, 149 líneas
// The register map of spec.md, written as a UVM register model.
//
// The table of the spec maps one to one onto this file, and that is the check
// worth doing out loud: four registers, six fields, four addresses. Nothing here
// knows that the bus is APB -- swap the adapter and the same model drives AHB.
//
// The access string of every field IS the specification. Get it wrong and the
// built-in sequences of the last file catch it; see the +MAL run of run.sh.
// 0x00 -- CTRL. EN is an ordinary bit. CLR is not: writing a 1 clears ACC and
// OVF, and the bit always reads back 0.
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg)
rand uvm_reg_field EN;
rand uvm_reg_field CLR;
function new(string name = "ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
// configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
// is_rand, individually_accessible)
virtual function void build();
EN = uvm_reg_field::type_id::create("EN");
EN.configure(this, 1, 0, "RW", 0, 'h0, 1, 1, 0);
// "WC" is the name UVM has for exactly this: the write clears the field,
// the mirror drops to zero, and the read returns that zero. Modelling it as
// "RW" is the classic mistake, and it is what +MAL does. "WOC" also passes,
// for the wrong reason: any WO* access takes the field out of bit_bash and
// out of do_check, so nobody looks at it. See the RAL unit slides.
CLR = uvm_reg_field::type_id::create("CLR");
CLR.configure(this, 1, 1, $test$plusargs("MAL") ? "RW" : "WC", 0, 'h0, 1, 1, 0);
endfunction : build
endclass : ctrl_reg
// 0x04 -- SCRATCH. A plain RW register. That every write to it also adds to ACC
// is a side effect the model does not know about, and does not need to.
class scratch_reg extends uvm_reg;
`uvm_object_utils(scratch_reg)
rand uvm_reg_field VALUE;
function new(string name = "scratch_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
virtual function void build();
VALUE = uvm_reg_field::type_id::create("VALUE");
VALUE.configure(this, 32, 0, "RW", 0, 'h0, 1, 1, 1);
endfunction : build
endclass : scratch_reg
// 0x08 -- ACC. Read-only, and volatile: the DUT changes it behind the model's
// back, on every write to SCRATCH. See the comment on set_compare() below.
class acc_reg extends uvm_reg;
`uvm_object_utils(acc_reg)
uvm_reg_field VALUE;
function new(string name = "acc_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
virtual function void build();
VALUE = uvm_reg_field::type_id::create("VALUE");
VALUE.configure(this, 32, 0, "RO", 1, 'h0, 1, 0, 1);
endfunction : build
endclass : acc_reg
// 0x0C -- STATUS. Two read-only bits, and both are volatile for the same reason
// as ACC: EN is a copy of CTRL.EN, and OVF turns on when the accumulator
// overflows -- neither of them happens on a write to STATUS.
class status_reg extends uvm_reg;
`uvm_object_utils(status_reg)
uvm_reg_field EN;
uvm_reg_field OVF;
function new(string name = "status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
virtual function void build();
EN = uvm_reg_field::type_id::create("EN");
EN.configure(this, 1, 0, "RO", 1, 'h0, 1, 0, 0);
OVF = uvm_reg_field::type_id::create("OVF");
OVF.configure(this, 1, 1, "RO", 1, 'h0, 1, 0, 0);
endfunction : build
endclass : status_reg
class apb_reg_block extends uvm_reg_block;
`uvm_object_utils(apb_reg_block)
rand ctrl_reg CTRL;
rand scratch_reg SCRATCH;
acc_reg ACC;
status_reg STATUS;
function new(string name = "apb_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction : new
// Not build_phase: a uvm_reg_block is a uvm_object, not a component. Nobody
// calls this for you -- the test does, right after the create().
virtual function void build();
// create_map(name, base_addr, n_bytes, endian, byte_addressing)
default_map = create_map("default_map", 'h0, 4, UVM_LITTLE_ENDIAN, 1);
CTRL = ctrl_reg::type_id::create("CTRL");
CTRL.configure(this, null, "");
CTRL.build();
default_map.add_reg(CTRL, CTRL_ADDR, "RW");
SCRATCH = scratch_reg::type_id::create("SCRATCH");
SCRATCH.configure(this, null, "");
SCRATCH.build();
default_map.add_reg(SCRATCH, SCRATCH_ADDR, "RW");
ACC = acc_reg::type_id::create("ACC");
ACC.configure(this, null, "");
ACC.build();
default_map.add_reg(ACC, ACC_ADDR, "RO");
STATUS = status_reg::type_id::create("STATUS");
STATUS.configure(this, null, "");
STATUS.build();
default_map.add_reg(STATUS, STATUS_ADDR, "RO");
// The honest line of the whole unit. ACC and STATUS have an address, but
// they are not registers: their value is produced by writes to OTHER
// addresses. A register model predicts "what I wrote is what I will read",
// and for these two that sentence is false, so mirror(UVM_CHECK) is turned
// off on them. Reads still update the mirror -- a read is a prediction --
// and the accumulator itself stays where it was in the capstone: in the
// scoreboard, which is the thing that does know how to add.
ACC.VALUE.set_compare(UVM_NO_CHECK);
STATUS.EN.set_compare(UVM_NO_CHECK);
STATUS.OVF.set_compare(UVM_NO_CHECK);
lock_model();
endfunction : build
endclass : apb_reg_block- A field gets declared with
configure(), and the argument that matters is the access string:"RW","RO","WC"… - That string is the specification. It is not documentation: it is what UVM uses to predict, and to generate the tests of The tests you did not write
configure(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible) — nine positional arguments, and it is the ugliest signature
in UVM. It is worth writing it on the blackboard once; afterwards the student copies it from the
example, which is what everybody does.
CLR is the field that justifies the slide. The spec of the capstone says "writing a
1 puts ACC and OVF at zero, and the bit always reads 0", and that is trap 2
of the small print — the one that makes the hand-written scoreboard fail. The access
that says exactly that is WC: the write clears the field, the mirror goes down
to zero, and the read returns that zero. W1C works just as well here.
And here is RAL's silent trap, which is worth more than the slide. WOC —the name
you write first, because it sounds like "write-only, clears"— also gives zero
errors, but it gives them for the wrong reason: uvm_reg_bit_bash_seq.svh:129-133
skips every field whose access starts with WO (Accellera's own comment says
"Ignore Write-only fields because you are not supposed to read them"), and
do_check takes it out of the comparison mask (uvm_reg.svh:2782-2788), so the
mirror(UVM_CHECK) does not look at it either. With WOC bit 1 of CTRL is never
tested and nobody prints anything; the only trace is in the timing, because the
bashing of CTRL takes four transfers less. Green is not checked, and a WO*
access is the cheapest way there is to switch off a check without noticing.
The complete list of accesses is in the LRM 1800.2, and there are twenty-five. You do not have to
know them by heart: you have to know that the list exists, and look there before
inventing a set_compare or a callback. Half of the bad register models
you come across are an RW field with patches on top.
RAL
The adapter: the only thing that knows there is an APB
code/u9/ral/tb_classes/apb_reg_adapter.svh · #reg2bus-and-bus2reg // Downstream: the model asks for a register access, this builds the bus item
// the sequencer will hand to the driver.
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_transaction t = apb_transaction::type_id::create("t");
t.write = (rw.kind == UVM_WRITE);
t.addr = rw.addr[7:0];
t.wdata = rw.data[31:0];
return t;
endfunction : reg2bus
// Upstream: a bus item comes back -- from the driver's response, or from the
// monitor when prediction is explicit -- and this fills the generic operation
// the model understands.
//
// PSLVERR is what makes status meaningful: an access to an unmapped address
// comes back UVM_NOT_OK, and the write()/read() call reports it.
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_transaction t;
if (!$cast(t, bus_item)) `uvm_fatal("REG ADAPTER", "Not an apb_transaction")
rw.kind = t.write ? UVM_WRITE : UVM_READ;
rw.addr = t.addr;
rw.data = t.write ? t.wdata : t.rdata;
rw.status = t.slverr ? UVM_NOT_OK : UVM_IS_OK;
endfunction : bus2regel archivo entero, 39 líneas
// The only class in the unit that knows the bus exists. Twenty lines, and they
// are the whole reason the register model is portable: a generic uvm_reg_bus_op
// in, an apb_transaction out.
//
// Written once per protocol, not once per project. This is the file you would
// get with a commercial APB VIP.
class apb_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(apb_reg_adapter)
function new(string name = "apb_reg_adapter");
super.new(name);
endfunction : new
// Downstream: the model asks for a register access, this builds the bus item
// the sequencer will hand to the driver.
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_transaction t = apb_transaction::type_id::create("t");
t.write = (rw.kind == UVM_WRITE);
t.addr = rw.addr[7:0];
t.wdata = rw.data[31:0];
return t;
endfunction : reg2bus
// Upstream: a bus item comes back -- from the driver's response, or from the
// monitor when prediction is explicit -- and this fills the generic operation
// the model understands.
//
// PSLVERR is what makes status meaningful: an access to an unmapped address
// comes back UVM_NOT_OK, and the write()/read() call reports it.
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_transaction t;
if (!$cast(t, bus_item)) `uvm_fatal("REG ADAPTER", "Not an apb_transaction")
rw.kind = t.write ? UVM_WRITE : UVM_READ;
rw.addr = t.addr;
rw.data = t.write ? t.wdata : t.rdata;
rw.status = t.slverr ? UVM_NOT_OK : UVM_IS_OK;
endfunction : bus2reg
endclass : apb_reg_adapterreg2busgoes down: the model asks for a generic access, this builds the transaction of the busbus2reggoes up: the transaction comes back —from the driver, or from the monitor— and this counts it in terms of an accessPSLVERRturns intoUVM_NOT_OK, and there thestatusof everywrite()stops being decorative
Twenty-two lines, and that is the whole file. It is worth saying the number out loud,
because RAL has a reputation for being heavy and the project-specific part is
this one.
The reg2bus / bus2reg symmetry is the one to fix: one function per
direction, and neither of the two has state. The uvm_reg_bus_op is the common
currency —kind, addr, data, status— and it is deliberately as flat as possible:
any register bus fits in there.
The const ref of the reg2bus comes from the signature of the base class and has to be copied
exactly. It works on Verilator 5.052, which was not obvious.
And a warning from the field: bus2reg is called by the predictor too, with the
item the monitor saw. An adapter that reads something from the driver —or that depends on
having built the transaction itself— works with automatic prediction and breaks
the day you switch to explicit. That is why both functions are pure.
RAL
Hooking it up: three lines and a connect_phase
code/u9/ral/tb_classes/ral_base_test.svh · #connect_phase function void connect_phase(uvm_phase phase);
// The three lines that turn a model into a testbench component: which
// sequencer carries the accesses, which adapter translates them, and where
// the truth about the DUT comes from.
model.default_map.set_sequencer(agent_h.sequencer_h, adapter);
model.default_map.set_auto_predict(0);
predictor.map = model.default_map;
predictor.adapter = adapter;
agent_h.ap.connect(predictor.bus_in);
endfunction : connect_phaseel archivo entero, 85 líneas
// The structure, and nothing else. It is the capstone's active agent with three
// objects on top: the register model, the adapter, and the predictor.
virtual class ral_base_test extends uvm_test;
`uvm_component_abstract_utils(ral_base_test)
apb_agent agent_h;
apb_agent_config agent_cfg_h;
apb_reg_block model;
apb_reg_adapter adapter;
// Parameterised by the BUS item, not by the register: the predictor's job is
// to turn what the monitor saw into a prediction on the model.
uvm_reg_predictor #(apb_transaction) predictor;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
virtual apb_if bfm;
if (!uvm_config_db#(virtual apb_if)::get(this, "", "bfm", bfm))
`uvm_fatal("RAL BASE TEST", "Failed to get bfm")
agent_cfg_h = new(.bfm(bfm), .is_active(UVM_ACTIVE));
uvm_config_db#(apb_agent_config)::set(this, "agent_h*", "config", agent_cfg_h);
agent_h = apb_agent::type_id::create("agent_h", this);
// A uvm_reg_block is a uvm_object: create() does not call build(), the way
// the component build_phase would. Forgetting this line leaves a model
// with zero registers and an error that says nothing.
model = apb_reg_block::type_id::create("model");
model.build();
adapter = apb_reg_adapter::type_id::create("adapter");
predictor = uvm_reg_predictor #(apb_transaction)::type_id::create("predictor", this);
endfunction : build_phase
function void connect_phase(uvm_phase phase);
// The three lines that turn a model into a testbench component: which
// sequencer carries the accesses, which adapter translates them, and where
// the truth about the DUT comes from.
model.default_map.set_sequencer(agent_h.sequencer_h, adapter);
model.default_map.set_auto_predict(0);
predictor.map = model.default_map;
predictor.adapter = adapter;
agent_h.ap.connect(predictor.bus_in);
endfunction : connect_phase
// The reset the driver does at time 0 happens on the wire; the model has to
// be told, or its mirror starts out as "unknown" instead of as the reset
// values of the spec.
task reset_model();
model.reset();
endtask : reset_model
// The model printed back as the table it came from. One line per field, in
// the order of the map, with the four things the spec says about it: where it
// lives, how wide it is, and how it is accessed.
//
// It is worth doing once out loud, because a register model is the only
// document in a project that is both the spec and the code -- and this is
// the diff between the two.
function void print_map();
uvm_reg regs[$];
uvm_reg_field fields[$];
model.get_registers(regs);
foreach (regs[i]) begin
fields.delete(); // get_fields() APPENDS: without this, register i
regs[i].get_fields(fields); // prints the fields of 0..i as well
foreach (fields[j])
`uvm_info("MAPA", $sformatf("%-8s @0x%02h %-7s [%0d+:%0d] %s",
regs[i].get_name(), regs[i].get_offset(model.default_map),
fields[j].get_name(), fields[j].get_lsb_pos(),
fields[j].get_n_bits(), fields[j].get_access()), UVM_NONE)
end
endfunction : print_map
function void start_of_simulation_phase(uvm_phase phase);
if ($test$plusargs("MAPA")) print_map();
endfunction : start_of_simulation_phase
endclass : ral_base_testset_sequenceris what makesmodel.CTRL.write()end up in the driver: the model sends items through the sequencer of the agent that already existedset_auto_predict(0)switches off the automatic prediction, and theuvm_reg_predictorreplaces it by listening to the analysis port of the monitor- And the
model.build()of thebuild_phase: auvm_reg_blockis auvm_object, not a component. Nobody calls it for you
The phase trap that takes everybody's first afternoon is the last
line of the slide. apb_reg_block::type_id::create("model") gives back an object
with zero registers, because build() here is an ordinary method that gets called by hand
—not the build_phase of a component, which UVM walks on its own—. The symptom is a
uvm_fatal from uvm_reg_map that talks about a null register and does not mention the
word build.
The second thing to point out is where the sequencer comes from: it is
agent_h.sequencer_h, the same one from the capstone, the same one the normal sequences
run on. RAL does not bring a sequencer of its own. That means a register
sequence and a traffic one compete for the same driver, and there the
priorities show up — which is a topic for another course, but it is worth them knowing it exists.
RAL
Prediction: who tells the model what happened
| Who predicts | What happens if the DUT does something else | |
|---|---|---|
| automatic | the write() call itself |
the model does not find out: it believes what it meant to send |
| explicit | the uvm_reg_predictor, from the monitor |
the model believes what happened on the wire |
- The automatic one is one line fewer and it is the one that comes up in the tutorials
- The explicit one is the one to use, and for the same reason the scoreboard of the capstone looks at the monitor and not at the driver
- And it is free: the monitor and its analysis port were already there
This is the judgement slide of the unit and it is worth not rushing it. With
set_auto_predict(1), model.CTRL.write(status, 'h1) updates the mirror at the
moment it gets called, before the bus has done anything. If the driver sends
the transfer wrong, or the DUT ignores it, the model goes on being convinced that
EN=1. It is exactly the design mistake the course has been attacking since day 1: the
one who checks cannot believe the one who drives.
With explicit prediction the path is the long and correct one: the driver drives,
the monitor sees the wire, the analysis port publishes it, the predictor calls
bus2reg and only then does the mirror change. It is the same drawing as the scoreboard, with
a piece of the library instead of a hand-written class.
The bonus, which can be seen in the example: since the predictor listens to the bus and not to the
calls, the model also finds out about the transfers somebody else made —
another sequence, another master, an ordinary module. Automatic prediction cannot
do that even in principle.
RAL
The whole path, from the call to the mirror
- The way down is written; the way up —monitor, analysis port and
bus2reg— was there - The
uvm_reg_predictoris not written: instantiate it and plug in the map - The dashed one is
set_auto_predict(1): the shortcut not to take
This is the figure worth having on screen while reading the connect_phase: every
one of the three lines of that function is an arrow of the drawing.
set_sequencer splices the way down, predictor.map/predictor.adapter give the
predictor the two things it needs, and agent_h.ap.connect(...) is the arrow that
goes up from the monitor.
The symmetry is the point: the adapter shows up twice, once per direction, and
it is the only piece of the drawing that knows there is an APB underneath. Swapping
it for an AHB one moves those two boxes and no other.
And the right half is not RAL: the monitor, the analysis port and the agent are the
ones from the capstone, without a change. Worth saying it with a finger on the
drawing, because it is what explains that the unit fits in three files.
The dashed shortcut is the trap of the unit. With set_auto_predict(1) that arrow
is enough for the mirror to change, so the bottom half of the drawing turns
decorative: the model believes what the test meant to send, not what happened.
RAL
The stimulus, without a single PADDR
code/u9/ral/tb_classes/ral_test.svh · #the-stimulus // --- the frontdoor -----------------------------------------------------
// write() puts the value on the wire through the sequencer. There is no
// second argument for the address: the address is in the map.
model.CTRL.write(status, 'h1); // EN = 1
model.CTRL.read(status, data);
`uvm_info("RAL", $sformatf("CTRL read back 0x%08h (status %s)", data, status.name()),
UVM_MEDIUM)
// --- prediction --------------------------------------------------------
// Nothing above updated the mirror by hand. The monitor saw the two
// transfers, the predictor turned them into predictions, and the model
// knows -- with set_auto_predict(0), which is the honest setting: what the
// model believes comes from the BUS, not from what the test meant to send.
if (model.CTRL.EN.get_mirrored_value() != 1)
`uvm_error("RAL", "the predictor did not update CTRL.EN")
`uvm_info("RAL", $sformatf("mirrored CTRL.EN = %0d, and nobody assigned it",
model.CTRL.EN.get_mirrored_value()), UVM_MEDIUM)el archivo entero, 82 líneas
// What the register layer buys, in ten lines of stimulus: not one PADDR, not one
// PWDATA, not one wait state. The same driver as the capstone is doing all of
// that underneath.
class ral_test extends ral_base_test;
`uvm_component_utils(ral_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
task run_phase(uvm_phase phase);
uvm_status_e status;
uvm_reg_data_t data;
phase.raise_objection(this);
reset_model();
// --- the frontdoor -----------------------------------------------------
// write() puts the value on the wire through the sequencer. There is no
// second argument for the address: the address is in the map.
model.CTRL.write(status, 'h1); // EN = 1
model.CTRL.read(status, data);
`uvm_info("RAL", $sformatf("CTRL read back 0x%08h (status %s)", data, status.name()),
UVM_MEDIUM)
// --- prediction --------------------------------------------------------
// Nothing above updated the mirror by hand. The monitor saw the two
// transfers, the predictor turned them into predictions, and the model
// knows -- with set_auto_predict(0), which is the honest setting: what the
// model believes comes from the BUS, not from what the test meant to send.
if (model.CTRL.EN.get_mirrored_value() != 1)
`uvm_error("RAL", "the predictor did not update CTRL.EN")
`uvm_info("RAL", $sformatf("mirrored CTRL.EN = %0d, and nobody assigned it",
model.CTRL.EN.get_mirrored_value()), UVM_MEDIUM)
// --- mirror(UVM_CHECK): the read that checks itself --------------------
model.SCRATCH.write(status, 'h1000_0000);
model.SCRATCH.mirror(status, UVM_CHECK);
// --- and the part RAL does not do --------------------------------------
// ACC changed because of the write to SCRATCH, at another address. The
// model never predicted that and never will: mirror(UVM_CHECK) is off on
// ACC (see the reg block). The read still updates the mirror, so after
// this line the model agrees with the DUT again.
model.ACC.read(status, data);
`uvm_info("RAL", $sformatf("ACC = 0x%08h -- predicted by the SCOREBOARD, not by the model",
data), UVM_MEDIUM)
if (data != 'h1000_0000)
`uvm_error("RAL", $sformatf("ACC read 0x%08h, expected 0x10000000", data))
// CLR is "WC": the write clears the accumulator, the mirror drops to zero
// and the bit reads 0, so the mirror(UVM_CHECK) right after really compares
// it -- which is the point of getting the access policy right.
model.CTRL.write(status, 'h3); // EN = 1, CLR = 1
model.CTRL.mirror(status, UVM_CHECK);
model.ACC.read(status, data);
if (data != 0) `uvm_error("RAL", $sformatf("after CLR, ACC = 0x%08h", data))
// STATUS.EN is a copy of CTRL.EN, and it got there through a transfer to a
// DIFFERENT address. Without the set_compare(UVM_NO_CHECK) of the reg
// block this line fails -- and the failure would be the model's fault, not
// the DUT's. Knowing which of the two is lying is the whole skill.
//
// The two UVM_WARNING it prints (UVM/FLD/GET_MIRRORED_VAL/VOL) are the
// library saying the same thing on its own: a volatile field's mirror is
// not evidence. They are the expected output of this line, not noise.
`uvm_info("RAL", "the two VOL warnings below are the point, not a problem",
UVM_MEDIUM)
model.STATUS.mirror(status, UVM_CHECK);
// --- the status that only a bus can give -------------------------------
// Every call returns a status, and it is not decoration: PSLVERR comes back
// through bus2reg as UVM_NOT_OK. A pure software model could not tell you
// that the slave refused the access -- it is the one thing in this file
// that could only come from the wire.
if (status != UVM_IS_OK)
`uvm_error("RAL", $sformatf("the mapped accesses came back %s", status.name()))
phase.drop_objection(this);
endtask : run_phase
endclass : ral_test- There is no address in any call: the address is in the map
- Nobody assigned the mirror.
get_mirrored_value()gives back 1 because the monitor saw the transfer and the predictor counted it - Underneath, the driver of the capstone is doing SETUP, ACCESS and the wait state of the read, the same as yesterday
bash run.sh · code/u9/ral/
It is worth running it live and looking at the log with grep MONITOR: for every line of
stimulus there is a complete APB transfer, with its address and its handshake,
done by the driver they wrote. It is the demonstration that RAL does not
replace the testbench, it drives it.
The line most worth discussing is model.CTRL.read(status, data) against
model.CTRL.mirror(status, UVM_CHECK). Both read from the DUT. The difference is
that mirror compares against what the model believed before the read, and if
it does not match it reports a uvm_error without anybody writing a check. It is a
register scoreboard in one word.
And the third form, which is not on the slide and is worth naming: set() +
update(). set() changes the desired value in the model, without touching the bus, and
update() writes only the registers whose desired value differs from the mirror.
It is how a whole block gets configured at the start of a test, and it is where RAL stops
being convenient and starts genuinely saving simulation time.
RAL
The tests you did not write
code/u9/ral/tb_classes/builtin_test.svh · #the-two-seqs rst_seq = uvm_reg_hw_reset_seq::type_id::create("rst_seq");
rst_seq.model = model;
rst_seq.start(null);
bash_seq = uvm_reg_bit_bash_seq::type_id::create("bash_seq");
bash_seq.model = model;
bash_seq.start(null);el archivo entero, 40 líneas
// The argument for RAL that survives a code review: tests you did not write.
//
// Both sequences below ship with uvm-core. They read the register model -- the
// addresses, the field positions, the access strings -- and generate the
// stimulus and the checks from it. Nothing here is specific to this DUT.
//
// uvm_reg_hw_reset_seq reads every register after reset and compares it
// against the reset value declared in configure()
// uvm_reg_bit_bash_seq writes and reads back every writable bit of every
// register, one at a time, in both directions
//
// Run it with +MAL and the register model declares CTRL.CLR as "RW" instead of
// "WC" -- one wrong word in one string. bit_bash catches it without anyone
// writing a test for CTRL.
class builtin_test extends ral_base_test;
`uvm_component_utils(builtin_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
task run_phase(uvm_phase phase);
uvm_reg_hw_reset_seq rst_seq;
uvm_reg_bit_bash_seq bash_seq;
phase.raise_objection(this);
reset_model();
rst_seq = uvm_reg_hw_reset_seq::type_id::create("rst_seq");
rst_seq.model = model;
rst_seq.start(null);
bash_seq = uvm_reg_bit_bash_seq::type_id::create("bash_seq");
bash_seq.model = model;
bash_seq.start(null);
phase.drop_objection(this);
endtask : run_phase
endclass : builtin_test- Both come from
uvm-core. They know nothing about this DUT: they read the model and generate the stimulus and the check out of it uvm_reg_hw_reset_seqreads everything after the reset and compares it with the reset value declareduvm_reg_bit_bash_seqwrites and reads every writable bit of every register, one by one, in both directions
This pair of sequences is the RAL argument that wins discussions, because it does not
appeal to taste: they are two real tests, free, that on a block of 200 registers
cover 80 % of the bugs of address decoding and of fields wired the wrong way round.
There are more in the library and it is worth naming them: uvm_reg_access_seq (writes through
the frontdoor and reads through the backdoor, and the other way round), uvm_reg_shared_access_seq for
registers in more than one map, and uvm_mem_walk_seq for memories. The ones that
need a backdoor stay out of the course for what was said before.
The implementation detail worth telling, because it explains why the
model has to be right: bit_bash does not skip the read-only fields — it writes
the bit and demands that they read back as they were —, and it does the same with the
reserved bits, which it treats as RO (uvm_reg_bit_bash_seq.svh:137-139, 149-152).
What it really skips is the WO* family, NOACCESS and the fields with
set_compare(UVM_NO_CHECK); individually_accessible it does not even look at. And
it uses the access string to know what to expect back. Which means the quality of the
test they give you is exactly the quality of the model you wrote.
RAL
And that is why the model is the spec
CTRL.CLR as "WC" 0 UVM_ERROR, and bit_bash did write bit 1
CTRL.CLR as "RW" 2 UVM_ERROR, from uvm_reg_bit_bash_seq
> Writing a 1 in bit #1 of register "model.CTRL" ...
yielded 'h0000000000000000 instead of 'h0000000000000002
- One word changed in a string, and a sequence nobody wrote catches it with the number of the bit
- It is trap 2 of the small print of the capstone —
CLRnever reads as 1— and there a whole scoreboard was needed to see it - The register model is the only document of a project that is the spec and the code at the same time
bash run.sh runs both versions and compares them
This is the slide that justifies the unit, and the run.sh of the example produces it
on its own: it runs builtin_test with the model right and with +MAL, which declares CLR
as RW, and shows the two numbers side by side.
The punchline to say out loud: in the capstone, that same trap cost writing
a scoreboard, modelling the autoclear by hand and discovering the bug with a message
of your own. Here it costs picking one word well, and the error message is written by
Accellera, with the number of the bit inside.
The honest downside, which has to be said so it does not sound like a brochure: if you
get it wrong the other way round —you declare RW a field the DUT implemented as RW
when the spec said something else— the model and the DUT agree on the
mistake and no sequence says anything. The register model verifies the DUT
against the spec written in the model, not against the real spec. That is why the
review of the model against the document is still human work, and that is why
stage 1 of the exercise prints the map so it can be compared by hand.
RAL
What RAL does not do, and it has to be said
code/u9/ral/tb_classes/apb_reg_block.svh · #no-check // The honest line of the whole unit. ACC and STATUS have an address, but
// they are not registers: their value is produced by writes to OTHER
// addresses. A register model predicts "what I wrote is what I will read",
// and for these two that sentence is false, so mirror(UVM_CHECK) is turned
// off on them. Reads still update the mirror -- a read is a prediction --
// and the accumulator itself stays where it was in the capstone: in the
// scoreboard, which is the thing that does know how to add.
ACC.VALUE.set_compare(UVM_NO_CHECK);
STATUS.EN.set_compare(UVM_NO_CHECK);
STATUS.OVF.set_compare(UVM_NO_CHECK);el archivo entero, 149 líneas
// The register map of spec.md, written as a UVM register model.
//
// The table of the spec maps one to one onto this file, and that is the check
// worth doing out loud: four registers, six fields, four addresses. Nothing here
// knows that the bus is APB -- swap the adapter and the same model drives AHB.
//
// The access string of every field IS the specification. Get it wrong and the
// built-in sequences of the last file catch it; see the +MAL run of run.sh.
// 0x00 -- CTRL. EN is an ordinary bit. CLR is not: writing a 1 clears ACC and
// OVF, and the bit always reads back 0.
class ctrl_reg extends uvm_reg;
`uvm_object_utils(ctrl_reg)
rand uvm_reg_field EN;
rand uvm_reg_field CLR;
function new(string name = "ctrl_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
// configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
// is_rand, individually_accessible)
virtual function void build();
EN = uvm_reg_field::type_id::create("EN");
EN.configure(this, 1, 0, "RW", 0, 'h0, 1, 1, 0);
// "WC" is the name UVM has for exactly this: the write clears the field,
// the mirror drops to zero, and the read returns that zero. Modelling it as
// "RW" is the classic mistake, and it is what +MAL does. "WOC" also passes,
// for the wrong reason: any WO* access takes the field out of bit_bash and
// out of do_check, so nobody looks at it. See the RAL unit slides.
CLR = uvm_reg_field::type_id::create("CLR");
CLR.configure(this, 1, 1, $test$plusargs("MAL") ? "RW" : "WC", 0, 'h0, 1, 1, 0);
endfunction : build
endclass : ctrl_reg
// 0x04 -- SCRATCH. A plain RW register. That every write to it also adds to ACC
// is a side effect the model does not know about, and does not need to.
class scratch_reg extends uvm_reg;
`uvm_object_utils(scratch_reg)
rand uvm_reg_field VALUE;
function new(string name = "scratch_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
virtual function void build();
VALUE = uvm_reg_field::type_id::create("VALUE");
VALUE.configure(this, 32, 0, "RW", 0, 'h0, 1, 1, 1);
endfunction : build
endclass : scratch_reg
// 0x08 -- ACC. Read-only, and volatile: the DUT changes it behind the model's
// back, on every write to SCRATCH. See the comment on set_compare() below.
class acc_reg extends uvm_reg;
`uvm_object_utils(acc_reg)
uvm_reg_field VALUE;
function new(string name = "acc_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
virtual function void build();
VALUE = uvm_reg_field::type_id::create("VALUE");
VALUE.configure(this, 32, 0, "RO", 1, 'h0, 1, 0, 1);
endfunction : build
endclass : acc_reg
// 0x0C -- STATUS. Two read-only bits, and both are volatile for the same reason
// as ACC: EN is a copy of CTRL.EN, and OVF turns on when the accumulator
// overflows -- neither of them happens on a write to STATUS.
class status_reg extends uvm_reg;
`uvm_object_utils(status_reg)
uvm_reg_field EN;
uvm_reg_field OVF;
function new(string name = "status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction : new
virtual function void build();
EN = uvm_reg_field::type_id::create("EN");
EN.configure(this, 1, 0, "RO", 1, 'h0, 1, 0, 0);
OVF = uvm_reg_field::type_id::create("OVF");
OVF.configure(this, 1, 1, "RO", 1, 'h0, 1, 0, 0);
endfunction : build
endclass : status_reg
class apb_reg_block extends uvm_reg_block;
`uvm_object_utils(apb_reg_block)
rand ctrl_reg CTRL;
rand scratch_reg SCRATCH;
acc_reg ACC;
status_reg STATUS;
function new(string name = "apb_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction : new
// Not build_phase: a uvm_reg_block is a uvm_object, not a component. Nobody
// calls this for you -- the test does, right after the create().
virtual function void build();
// create_map(name, base_addr, n_bytes, endian, byte_addressing)
default_map = create_map("default_map", 'h0, 4, UVM_LITTLE_ENDIAN, 1);
CTRL = ctrl_reg::type_id::create("CTRL");
CTRL.configure(this, null, "");
CTRL.build();
default_map.add_reg(CTRL, CTRL_ADDR, "RW");
SCRATCH = scratch_reg::type_id::create("SCRATCH");
SCRATCH.configure(this, null, "");
SCRATCH.build();
default_map.add_reg(SCRATCH, SCRATCH_ADDR, "RW");
ACC = acc_reg::type_id::create("ACC");
ACC.configure(this, null, "");
ACC.build();
default_map.add_reg(ACC, ACC_ADDR, "RO");
STATUS = status_reg::type_id::create("STATUS");
STATUS.configure(this, null, "");
STATUS.build();
default_map.add_reg(STATUS, STATUS_ADDR, "RO");
// The honest line of the whole unit. ACC and STATUS have an address, but
// they are not registers: their value is produced by writes to OTHER
// addresses. A register model predicts "what I wrote is what I will read",
// and for these two that sentence is false, so mirror(UVM_CHECK) is turned
// off on them. Reads still update the mirror -- a read is a prediction --
// and the accumulator itself stays where it was in the capstone: in the
// scoreboard, which is the thing that does know how to add.
ACC.VALUE.set_compare(UVM_NO_CHECK);
STATUS.EN.set_compare(UVM_NO_CHECK);
STATUS.OVF.set_compare(UVM_NO_CHECK);
lock_model();
endfunction : build
endclass : apb_reg_blockACCandSTATUShave an address, but they are not registers: their value gets produced by a write to another address- A model predicts "what I wrote is what I am going to read", and for these two that
sentence is false.
set_compare(UVM_NO_CHECK)says so in the model - The accumulator goes on being checked where it already was: in the scoreboard, which is the one that knows how to add
- The read updates the mirror all the same — a read is a prediction
If they take a single idea away from this unit, let it be this one: an address is not a
register. RAL models addressable storage, and it does it very well. It does not
model behaviour. The accumulator of the capstone —which adds only with EN=1, which
overflows, which gets cleared with CLR— is behaviour, and that lives in the
scoreboard, as it lived yesterday.
The failure mode of not saying it is the worst of all and it is an everyday one: the team puts
RAL in, switches on mirror(UVM_CHECK) over the whole map, and the regression fills up with
errors on the status registers. Since they are false positives, somebody switches the
check off entirely — and from then on the model verifies nothing and nobody
finds out. The difference between switching off three named fields and switching off the global
check is the whole difference.
The two UVM_WARNING the example prints —GET_MIRRORED_VAL/VOL— are the
library saying the same thing on its own account: the mirror of a volatile field is not
evidence. They are in the output on purpose.
RAL
Summary of the unit
- The model is the map of the spec in code, and the access string of each field is the part that verifies
- The adapter is the only thing that knows about the bus: twenty lines, once per protocol
- The prediction goes through the monitor, not through the call — the same rule as the scoreboard
- It gives you
hw_reset_seqandbit_bash_seqfor free, and it charges you knowing what is not a register - And all of this runs on Verilator 5.052, which until today nobody had shown
The close of the unit and of the technical course. The last line is not marketing: when
writing this unit it was first tested whether uvm_reg_adapter and the frontdoor
worked on Verilator, before writing a single slide, and the result is
measured in docs/en/verilator.md. Everything the unit uses works: the model, the
adapter, the uvm_reg_predictor, mirror(UVM_CHECK) and the two sequences of the
library.
For whoever goes on alone, the order of what is missing: uvm_reg_access_seq with
a backdoor, generating the model from IP-XACT or SystemRDL —which is how it
is really done—, and uvm_mem for memories. All three are extensions of what
they saw here, not new topics.
And the bridge to the exercise: the first thing they are going to be asked for in a project with
registers is exactly this, with the spreadsheet of the spec on the left-hand side of
the screen.
Exercise · Day 8 · 1 of 2
Modelling the register map
cd code/ejercicios/d8-ral && cat README.md
- It is literally the first thing you get asked for in a project with registers: they give you the table of the spec and you give back the model
- The DUT and the testbench are the ones from the capstone. The adapter, the predictor and the
tests come from the unit. The only thing you write is
apb_reg_block.svh - Four registers, six fields, forty lines
- The checker goes in stages: the map —printed and compared with the table, without simulating anything—, the accesses —the two sequences of the library— and what the model cannot predict
It is the shortest exercise of the fifteen and the one most like a real task from
the first weeks of a project. Forty lines, and half of them are copying and
pasting the pattern of CTRL.
The stage that teaches is stage 1, and for a reason of method: the model gets printed
—+UVM_TESTNAME=mapa_test— and gets compared line by line against the register
table of the exercise README, before simulating anything. It is what has to be done with a real register
model, where the typical mistake is not about UVM but an offset copied
wrong from a 200-row spreadsheet.
Stage 2 is where CLR falls: whoever declared it RW passes stage 1 without
a problem and finds out here, with the number of the bit, thanks to a test they did not
write. It is worth letting them fall there — it is the moment when the RAL argument
stops being an opinion.
And stage 3 is the one that separates: it asks you to switch off a check, which is the opposite of
what the course has been asking for the last seven days. The justification has to come
from them: STATUS.EN changed because of a write to another address, the model
cannot know it, and a false positive that gets let through ends up with somebody switching
the whole check off.
The reference model in C
The scoreboard that rewrites the spec
- The scoreboard of the course predicts in SystemVerilog: a
casewith the arithmetic of the VTALU. For five eight-bit operations it is perfect - For a DSP, a codec or a crypto engine, that arithmetic already exists, and in C: the algorithm team wrote it, and it is against that model that the spec was signed off
- Rewriting it in SystemVerilog is maintaining two models and spending the project debugging the difference between them. That is not verifying: it is having two specs
- The way out is not to rewrite it. Verilator compiles to C++, so the model gets linked inside the simulation: no socket, no separate process, no IPC
This is the technique that separates a course testbench from a production one, and the
argument is not about convenience: it is about authority. In a project with serious
arithmetic, the spec is not a PDF — it is a program in C that somebody ran against a thousand
vectors before the RTL existed. A scoreboard that reimplements that
program in SystemVerilog does not verify the DUT against the spec: it verifies the DUT
against the reading the verification engineer made of the spec, which is another thing.
The question worth throwing at the group before showing the code: "if the
C model and the scoreboard differ, who is right?". The uncomfortable answer
is that you do not know, and finding out costs days. With DPI the question does not
exist because there is only one model.
And the reason why this section is so cheap in this course: DPI is part of the LRM
and the commercial simulators ship it as a base feature —what leaves it out are the
free student editions—; what costs you in a commercial flow is the toolchain:
-sv_lib, compiling the .c separately, and remembering the library on every run.
Here the simulator compiles to C++ by design, so the bridge is one more file on the
command line and there is nothing to configure.
The reference model in C
import "DPI-C": one declaration on each side
code/u8/dpi/vtalu_pkg.sv · #the-imports // The reference model lives in vtalu_golden.c and is called from here. This
// is all that is needed on the SystemVerilog side: one declaration per
// function, with the LRM types that cross the boundary.
//
// input int -> int output int -> int *
// function -> returns task -> may consume time (not here)
//
// The name after "DPI-C" is the symbol the linker looks for, so it has to
// match the one in the .c letter by letter.
import "DPI-C" function int vtalu_golden(input int op, input int a,
input int b, output int ovf);
import "DPI-C" function void vtalu_golden_bug(input int on);el archivo entero, 63 líneas
package vtalu_pkg;
import uvm_pkg::*;
`include "uvm_macros.svh"
// The reference model lives in vtalu_golden.c and is called from here. This
// is all that is needed on the SystemVerilog side: one declaration per
// function, with the LRM types that cross the boundary.
//
// input int -> int output int -> int *
// function -> returns task -> may consume time (not here)
//
// The name after "DPI-C" is the symbol the linker looks for, so it has to
// match the one in the .c letter by letter.
import "DPI-C" function int vtalu_golden(input int op, input int a,
input int b, output int ovf);
import "DPI-C" function void vtalu_golden_bug(input int on);
typedef enum bit [2:0] {
no_op = 3'b000,
add_op = 3'b001,
sub_op = 3'b010,
and_op = 3'b011,
xor_op = 3'b100,
mul_op = 3'b101,
rst_op = 3'b111
} operation_t;
// Configs first: the driver and the monitors use them.
`include "env_config.svh"
`include "vtalu_agent_config.svh"
`include "command_transaction.svh"
`include "add_transaction.svh"
`include "result_transaction.svh"
// The sequencer is not extended: it is parameterized and given a name.
// It goes AFTER command_transaction and BEFORE the driver and the agent.
typedef uvm_sequencer #(command_transaction) sequencer;
// Sequences are uvm_object: they go before the tests that start them.
`include "reset_sequence.svh"
`include "random_sequence.svh"
`include "maxmult_sequence.svh"
`include "fibonacci_sequence.svh"
`include "full_sequence.svh"
`include "coverage.svh"
`include "scoreboard.svh"
`include "driver.svh"
`include "command_monitor.svh"
`include "result_monitor.svh"
`include "vtalu_agent.svh"
`include "env.svh"
`include "base_test.svh"
`include "full_test.svh"
`include "fibonacci_test.svh"
`include "add_test.svh"
`include "default_seq_test.svh"
`include "no_objection_test.svh"
endpackage : vtalu_pkgcode/u8/dpi/vtalu_golden.c · #the-golden/* op, a and b arrive as SystemVerilog `int` (32-bit signed). The result goes
* back as the return value and the borrow through a pointer, which is how an
* `output int` argument crosses the boundary. */
int vtalu_golden(int op, int a, int b, int *ovf) {
int r;
a &= 0xff;
b &= 0xff;
*ovf = 0;
switch (op) {
case ADD_OP: r = a + b; break;
/* 8-bit operands into a 16-bit result: a - b wraps, and the borrow is
* the second half of the answer the scoreboard has to check. */
case SUB_OP: r = a - b; *ovf = (a < b); break;
case AND_OP: r = a & b; break;
case XOR_OP: r = a ^ b; break;
case MUL_OP: r = mutar ? (a * b) & 0xff : a * b; break;
default: r = 0; break; /* no_op and rst_op do not answer */
}
return r & 0xffff;
}el archivo entero, 62 líneas
/* The VTALU reference model, in C. This is the whole point of the example:
* the scoreboard does NOT predict in SystemVerilog -- it asks this file.
*
* Why it matters: for a DUT with real arithmetic -- a DSP, a codec, a crypto
* engine -- the reference model already exists in C, written by the algorithm
* team, and it is the thing the specification was signed off against.
* Rewriting it in SystemVerilog means maintaining two models and debugging the
* difference between them, which is not verification.
*
* Verilator compiles to C++, so this file is linked straight into the
* simulation: no IPC, no socket, no FLI. One `import "DPI-C"` on the
* SystemVerilog side (vtalu_pkg.sv) and one function call on this side.
*
* The `extern "C"` guard is not decoration: Verilator hands user sources to
* the C++ compiler, and without it the symbol gets mangled and the link fails
* with an undefined reference that names a function that is obviously there.
*/
#include <svdpi.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Same opcodes as operation_t in vtalu_pkg.sv. Keeping the two in sync by hand
* is the tax of DPI, and it is the first thing to check when every comparison
* fails at once. */
enum { NO_OP = 0, ADD_OP = 1, SUB_OP = 2, AND_OP = 3, XOR_OP = 4, MUL_OP = 5, RST_OP = 7 };
/* A deliberate mutation, switched on with +GOLDEN_BUG. A scoreboard that never
* saw an error is not tested: this is what proves the DPI path is actually
* wired, and it is the same idea as the +BUG=1 of the day 7 capstone. */
static int mutar = 0;
void vtalu_golden_bug(int on) { mutar = on; }
/* op, a and b arrive as SystemVerilog `int` (32-bit signed). The result goes
* back as the return value and the borrow through a pointer, which is how an
* `output int` argument crosses the boundary. */
int vtalu_golden(int op, int a, int b, int *ovf) {
int r;
a &= 0xff;
b &= 0xff;
*ovf = 0;
switch (op) {
case ADD_OP: r = a + b; break;
/* 8-bit operands into a 16-bit result: a - b wraps, and the borrow is
* the second half of the answer the scoreboard has to check. */
case SUB_OP: r = a - b; *ovf = (a < b); break;
case AND_OP: r = a & b; break;
case XOR_OP: r = a ^ b; break;
case MUL_OP: r = mutar ? (a * b) & 0xff : a * b; break;
default: r = 0; break; /* no_op and rst_op do not answer */
}
return r & 0xffff;
}
#ifdef __cplusplus
}
#endif- The name after
"DPI-C"is the symbol the linker looks for: it has to match letter by letter with the one in the.c - An
input inttravels as anint; anoutput intarrives as anint *. Afunctioncannot consume time; a DPItaskcan, but only declaredcontextand calling back into anexport "DPI-C" task - The
.cgoes on theverilatorline as one more source. There is no DPI flag
It is worth reading the two declarations side by side, because the symmetry is the whole
mechanism: the one above is the promise and the one below is who keeps it, and the
compiler does not cross-check them — the one who joins them is the linker, at the end. Out of that comes the
number one failure mode of DPI, which is worth naming before it shows up: an
undefined reference to a function that is written, right there, in plain sight. Almost
always it is one of two things — the name does not match, or the file got compiled as
C++ and the symbol came out with a mangled name.
That last one is literally what happens here: Verilator hands the user's sources
to the C++ compiler, so the .c needs the extern "C" that is
in the file. Without that guard, the example does not link and the error does not say why.
The type table of the LRM is longer —there is svBitVecVal for wide vectors,
svOpenArrayHandle for arrays— and it is not needed here. The rule of thumb: if the
data fits in an int, the signature writes itself; if not, you look up annex H of the
1800 and copy it.
The reference model in C
The scoreboard: the prediction gets asked for, not written
code/u8/dpi/tb_classes/scoreboard.svh · #predict_result // The ONLY difference with the sequences scoreboard, and it is the whole
// section: there is no case with the arithmetic of the spec here. The
// prediction is asked of the C reference model, the one that signed it.
//
// What does not change: the monitor, the analysis port, the FIFO, the
// do_compare and the uvm_error. The golden model replaces SIX LINES -- the
// ones that predict -- and no other piece of the testbench finds out.
function result_transaction predict_result(command_transaction cmd);
result_transaction predicted;
int ovf;
predicted = result_transaction::type_id::create("predicted");
predicted.result = vtalu_golden(int'(cmd.op), int'(cmd.A), int'(cmd.B), ovf);
predicted.ovf = (ovf != 0);
return predicted;
endfunction : predict_resultel archivo entero, 58 líneas
class scoreboard extends uvm_subscriber #(result_transaction);
`uvm_component_utils(scoreboard);
uvm_tlm_analysis_fifo #(command_transaction) cmd_f;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
cmd_f = new("cmd_f", this);
endfunction : build_phase
// The ONLY difference with the sequences scoreboard, and it is the whole
// section: there is no case with the arithmetic of the spec here. The
// prediction is asked of the C reference model, the one that signed it.
//
// What does not change: the monitor, the analysis port, the FIFO, the
// do_compare and the uvm_error. The golden model replaces SIX LINES -- the
// ones that predict -- and no other piece of the testbench finds out.
function result_transaction predict_result(command_transaction cmd);
result_transaction predicted;
int ovf;
predicted = result_transaction::type_id::create("predicted");
predicted.result = vtalu_golden(int'(cmd.op), int'(cmd.A), int'(cmd.B), ovf);
predicted.ovf = (ovf != 0);
return predicted;
endfunction : predict_result
function void write(result_transaction t);
string data_str;
command_transaction cmd;
result_transaction predicted;
do
if (!cmd_f.try_get(cmd))
`uvm_fatal("SELF CHECKER", "Missing command in self checker")
while ((cmd.op == no_op) || (cmd.op == rst_op));
predicted = predict_result(cmd);
data_str = {
cmd.convert2string(),
" ==> Actual ",
t.convert2string(),
"/Predicted ",
predicted.convert2string()
};
if (!predicted.compare(t)) `uvm_error("SELF CHECKER", {"FAIL: ", data_str})
else `uvm_info("SELF CHECKER", {"PASS: ", data_str}, UVM_HIGH)
endfunction : write
endclass : scoreboard- It is the only difference from the scoreboard of the sequences. The monitor, the
analysis port, the FIFO, the
do_compareand theuvm_errordo not change - The golden model replaces the six lines of predicting, and no other piece of the testbench finds out there is C on the other side
- And the traffic goes both ways: the result comes back from C, and
vtalu_golden_bug(1)goes from SystemVerilog to change the state of the model
The count is worth doing out loud because it is the design argument of the whole
unit: one six-line function changed and nothing else did. That happens because
the scoreboard was already well divided up —predicting and comparing are two jobs, and
they were in two methods—, and it is the late reward for having separated
predict_result from write three days ago.
The int'(...) line is the one to point at: the UVM types do not cross the
border. On the other side there is no command_transaction, there are three integers. When the
data is an object —an image, a packet, a matrix— what gets passed is a
pointer or an open array, and the one who decides the format is the C model, not the
testbench.
And the vtalu_golden_bug is not an ornament: a scoreboard that has never seen an error
is not tested. run.sh runs the same test against a mutated model —the
multiplication truncated to eight bits— and demands that the scoreboard scream. If it does not
scream, the testbench is comparing against itself and nobody was going to find out. It is
the same mutation test the checker of the capstone does with +BUG=1, from the
other side of the wire.
The reference model in C
What you gain, what you pay
| You gain | You pay |
|---|---|
| One single model, the one that signed off the spec | the opcodes get synchronized by hand: one enum on each side |
| The model gets tested with the tools of C | an undefined reference does not say which of the two declarations is wrong |
| Arithmetic that in SystemVerilog is painful —floating point, matrices, crypto— | the C model runs in zero time: it cannot wait for an edge |
| Performance: it is a function call, not an IPC | a segfault in the model takes the whole simulation down with it |
- The rule for deciding: if the prediction already exists in C, DPI. If you write it
yourself from scratch and it is five lines, a
casein SystemVerilog and that is it - It runs:
make u8/dpi— two tests against the healthy model, and one against the mutated one
The table is what has to be left written down, and the row that gives the most discussion is the
first one on the right: the duplicated enum. It is real and it has no elegant solution
—the LRM does not share types between the two sides—, and it is the first thing to
look at when all the comparisons fail at once. If one in every
six fails, it is the DUT; if all of them fail, it is the mapping.
The third row on the right is the one that surprises and is worth getting ahead of: a DPI
function runs in zero time, so the C model cannot synchronize with the clock. That
is what import "DPI-C" task is for — with one clarification almost every tutorial
skips: C cannot block by itself. A DPI task consumes time only if it is declared
context and the C calls back into an export "DPI-C" task of SystemVerilog, which
is the one that waits for the edge. And there it is no longer a golden model, it is a
bus model, which is another topic.
The rule of the second-to-last bullet is the one to take away, because the risk of
this section is that somebody comes out wanting to do everything through DPI. The VTALU does not
justify DPI: five eight-bit operations get written in a case and read
better. The example exists so the technique has been seen when the DUT that does
justify it comes along, and that DUT arrives sooner than you think.
Review · Day 8
1 of 8 · The access string
CLR clears itself when you write a 1 to it. You declare it "WOC", and bit_bash and mirror(UVM_CHECK) give zero errors. What happened?
- Nothing:
WOCis the access that describes a field that clears on a write -
WOCis not in the LRM, so UVM treats it as a plainRW - Both skip
WO*accesses: that bit never got tested - The predictor leaves the mirror at
x, and a comparison againstxalways passes
Green is not checked —
uvm_reg_bit_bash_seq.svh:129-133skips every field whose access starts withWO("you are not supposed to read them"), anddo_checktakes it out of the comparison mask (uvm_reg.svh:2782-2788). The only trace is in the timing: bashingCTRLtakes four transfers less. The right access isWC(orW1C), and with it the bit does get bashed. AWO*access is the cheapest way there is of switching a check off without noticing.
Review · Day 8
2 of 8 · Automatic or explicit prediction
What changes between set_auto_predict(1) and hooking a uvm_reg_predictor to the monitor?
- With auto-predict the model believes what it meant to send, not what happened
- Nothing: the predictor is the internal implementation of auto-predict
- The predictor is faster: it does not build the bus transaction
- Auto-predict only works frontdoor, and the predictor works backdoor too
The one who checks cannot believe the one who drives — with
set_auto_predict(1),model.CTRL.write()updates the mirror at the moment of the call, before the bus has done anything: if the driver sends the transfer wrong, the model stays convinced. With the predictor, the mirror only changes once the monitor has seen the wire. It is the same drawing as the capstone scoreboard, with a library piece instead of a hand-written class. And it is free: the monitor and its analysis port were already there.
Review · Day 8
3 of 8 · volatile
STATUS changes on its own, without anybody writing it. What are you saying when you declare it volatile in configure()?
- That the mirror is not evidence: the value can change without going through the bus
- That UVM is going to re-read it from the DUT before every comparison, to be safe
- That the field drops out of the map and stops having an address
- That it has to be read backdoor, because the frontdoor does not get there in time
The mirror stops being evidence — a model predicts "what I wrote is what I am going to read", and for a
volatilefield that sentence is false. The twoUVM_WARNING GET_MIRRORED_VAL/VOLof the example are the library saying exactly that, and they are in the output on purpose. What does have to be checked gets checked where it is known: in the scoreboard, and in the model goesset_compare(UVM_NO_CHECK)on that field so that RAL does not invent an error.
Review · Day 8
4 of 8 · mirror() against read()
model.CTRL.read(status, data) and model.CTRL.mirror(status, UVM_CHECK). How do they differ?
-
readgoes over the bus andmirrorstays in the mirror, with no transfer at all -
mirrorwrites the mirror into the DUT, to leave the two of them equal -
readupdates the mirror andmirrordoes not touch it, so as not to hide an error - Both read from the DUT;
mirroralso compares against what the model believed
mirroris a register scoreboard in one word — both read from the DUT; the difference is thatmirrorcompares against what the model believed before the read, and if it does not match it reports auvm_errorwithout anybody writing a check. And a read is a prediction: both update the mirror afterwards.
Review · Day 8
5 of 8 · Who knows about the bus
What does the uvm_reg_block know about the APB?
- The base address and the width of
PADDR, which reach it through theuvm_reg_map - Nothing: the only one that knows about the bus is the adapter
- Everything: that is why there is one register model per protocol
- The SETUP and ACCESS timing, to predict the wait state of the read
The model does not know there is an APB underneath — swap the adapter and the same model drives an AHB. The adapter is twenty lines, once per protocol, and it is the file that comes with a VIP you bought. Watch out for one thing:
bus2regis called by the predictor too, with the item the monitor saw, so an adapter that depends on something the driver put there works in one direction and fails in the other.
Review · Day 8
6 of 8 · DPI and time
Can an import "DPI-C" function of the golden model wait for a clock edge?
- Yes, by putting a
#1inside the.c - No: a
functionruns in zero time - Yes, as long as the
.cis compiled with Verilator's timing support - Yes: the simulator suspends the C thread for the duration of the call
Zero time, like any
function— to consume time you needimport "DPI-C" task, and with a clarification almost every tutorial skips: C cannot block by itself. A DPI task consumes time only if it is declaredcontextand from the C it calls back into anexport "DPI-C" taskof SystemVerilog, which is the one that waits for the edge. And at that point it is no longer a golden model, it is a bus model.
Review · Day 8
7 of 8 · undefined reference
The .c is written and it compiles, and the link fails with undefined reference to the golden model function. What do you look at first?
- Whether the DPI flag is missing from the
verilatorcommand line - Whether the
.cis in its own-fand not mixed in with the.sv - The name, and the
extern "C": the symbol may have come out mangled - Whether the SystemVerilog
functionis declaredvirtualso the symbol gets exported
The compiler does not cross the two declarations: the one that joins them is the linker — hence an error about something that is written, right there, in plain sight. It is almost always one of two things: the name does not match letter for letter, or the file got compiled as C++ and the symbol came out mangled. That last one is literally the case here: Verilator hands the user's sources to the C++ compiler, so the
.cneeds itsextern "C". And there is no DPI flag: the.cgoes on theverilatorcommand line like any other source.
Review · Day 8
8 of 8 · The scoreboard that never shouted
The scoreboard with the golden model in C runs a thousand operations and reports not one. Is that enough?
- Yes: a thousand comparisons without a single difference is the definition of verified
- Yes, as long as the functional coverage closed at 100 % on top of that
- No, because both sides share the opcode
enumand cancel each other out - No: a scoreboard that has never seen an error is not tested
The model has to be broken on purpose —
vtalu_golden_bug(1)mutates the multiplication andrun.shdemands that the scoreboard shout. If it does not shout, the testbench is comparing against itself and nobody was going to find out: it is the same mutation test the capstone grader does with+BUG=1, from the other side of the wire. And the duplicatedenumis real but it is a different symptom: if every comparison fails at once, it is the mapping; if one in six fails, it is the DUT.
Exercise · Day 8 · 2 of 2
The reference model in C, and the two mutations that prove it
cd code/ejercicios/d8-dpi && bash run.sh
- The only file of the exercise is
vtalu_golden.c. Not one line of the testbench gets touched - Two operations are missing: the subtraction with its borrow —which goes out through a pointer— and the product with the mutation hook
- The checker runs three times: healthy DUT against your model, mutated DUT
(
VTALU_BUG=1), and your mutated model (+GOLDEN_BUG)
Stage 2 is what makes the exercise worth it, and it is worth explaining first:
against the healthy DUT, a model returning some fixed thing could pass by
accident if the stimulus were poor. With the DUT lying on bit 0, there is no way
of passing without having computed — it is the same mutation test as the
capstone, pointed at the model instead of at the scoreboard.
The subtraction is the one that trips people, and not because of the arithmetic:
a - b is one line. What gets forgotten is the second half of the answer,
the borrow, and it gets forgotten because it crosses the boundary in another way
—through a pointer, which is how a SystemVerilog output int travels—. The
scoreboard compares it, so without it every subtraction fails and no other
operation does: that pattern of failures is the one to learn to read.
And the trap you cannot see, which is solved in the skeleton on purpose so they
do not lose the hour there: without extern "C", Verilator hands the file to the
C++ compiler, the symbol comes out mangled, and the link fails with an
undefined reference to a function written two lines above. It is DPI's number
one failure mode with Verilator.
Capstone 2 · Day 8
The second final: a FIFO with backpressure
cd code/ejercicios/d8-fifo && cat spec.en.md
- For whoever has already handed in the APB. The protocol is simpler —there are no addresses, no wait states, no error response— and even so it is harder
- The APB one is a slave with no useful memory: an address and a value, and the scoreboard can be a table of four rows. A FIFO has order and occupancy, and both have to be carried
- The bug of
+BUG=1is not a data bug:almost_fullgoes up one place late. The data still comes out right and in order, so a scoreboard that only comparesrd_datapasses green - The small print: the flags describe the state before the edge,
rd_dataarrives one cycle late, writing with the FIFO full is not an error, and a simultaneous read makes room for the write
This is the exercise that separates whoever understood from whoever copied the pattern, and the
mechanism by which it separates is in the third bullet: the bug is in a control
output, not in the data path. A scoreboard that compares what
comes out of rd_data closes six of the seven rows of the plan and passes green
with the DUT broken. To see it the flags have to be predicted, and to predict the
flags the occupancy has to be modelled — that is, writing a reference model
with state, which is what the first capstone could not ask for.
It is worth saying why it goes second and not first: the APB is the DUT you run into in the
first year, and the table-address-value pattern is the right one there. This one teaches
when that pattern stops being any use, and that lesson is not understood without having used
the pattern before.
The trap that catches the most people is the order of updating the model: first look
at whether the read takes something out —that frees a place— and only then whether the write goes in.
The other way round, the simultaneous write with the FIFO full shows up as a lost piece of data
that the DUT never lost, and the student spends an hour looking for the bug in the RTL.
And the second one: the check_phase that claims data that never came out. Almost
always the test ended one cycle too early, and it is the drain_time of day 3
showing up where nobody expected it.