Day 6The reusable testbench
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 data is an object:
command_transactionwithdo_copy,do_compareandconvert2string, and the scoreboard compares objects, not fields - The stimulus is asked for instead of written:
randomize(),constraint,dist, and the histogram that says whether the distribution does what it promises - And two things are left stuck together that do not belong together: the
testeris structure and stimulus at once, and theenvnames the monitor and the driver one by one
The day 6 recap has to name the two separations the unit makes, because there are two and they get confused: the agent separates the structure and the sequence separates the stimulus. Both are the same operation —taking something out of the component tree— applied to different things. The third bullet is the one to leave said in the student's own words: today, to test three stimuli and their combinations, six test classes are needed. That grows factorially, and it is the reason the unit exists.
Agenda
Day 6 · ≈ 4 h 45 · unit 7 · the reusable testbench
- Agents
- Callbacks
- Sequences
By the end of the day you can:
- Say what an agent with
is_active = UVM_PASSIVEbuilds and what it does not - Explain why a
uvm_sequencedoes not show up inprint_topology() - Remove the last hard-wired line of a test, with
default_sequencethroughuvm_config_db
The first two are rows 13 and 14 of the closing self-assessment. The 14th is the
question thrown on day 1, when the UVM class diagram showed up, and it gets
answered today: uvm_object is the data and uvm_component is the
structure.
The third one is what closes the unit and the day: when that line goes, no line
of the test names a component from inside the env, and only then is the
testbench reusable for one agent. Coordinating two is day 7.
Agents
The problem: a testbench that cannot be copied
- The
envwe left in the transactions works, but it is a welded block: seven components and fiveconnect()written by hand - Every one of those components exists for a single reason: it drives or
watches the same interface, the
vtalu_bfm - Ask what happens if the design brings two VTALU: you have to copy the seven objects, the five connections, and rename everything
- Copying and pasting structure is exactly what we had been avoiding since the
env - UVM puts a name on the solution:
uvm_agent
The question that orders the whole section: what is the natural unit of reuse in a testbench? It is not the component, and it is not the env. It is the interface. Everything that knows how to speak one protocol gets packed together, and that box gets instantiated once for every interface of the design. That is the agent, and it is nothing more. If the group comes from RTL, the analogy works on its own: the agent is to the testbench what a parameterizable module is to RTL. Nobody instantiates a FIFO by copying its registers one at a time.
Agents
What is inside an agent
- A
uvm_agentis one moreuvm_component: same tree, same phases, same factory - What changes is what it puts inside, always the same:
- a sequencer, which hands out stimulus
- a driver, which turns it into signals of the BFM
- the monitors, which watch the bus and publish transactions
- And it exposes outwards two
uvm_analysis_port: the one that publishes commands and the one that publishes results - The scoreboard and the coverage do not go inside: they are analysis, and they live in the
envhanging off those two ports - From this section on every piece uses its base class:
uvm_driver #(T),uvm_sequencer #(T)anduvm_monitorinstead of a plainuvm_component
There is a decision here that is worth saying out loud because the UVM Primer takes
it the other way round: it puts the scoreboard and the coverage inside the agent. We do not.
The reason is practical: the day you put a passive agent on an interface that
somebody else drives, you do not want a second scoreboard thrown in for free. The agent is
the protocol layer; the analysis belongs to the env, which is the one that knows what is being
verified.
The other reason is that if the scoreboard lives inside, the two analysis ports that
the agent exposes have nobody to serve, and half the design ends up decorative.
About the last bullet, because somebody is going to open code/u7/agents and see it:
up to the transactions the monitors extended uvm_component; from here on they extend
uvm_monitor. And uvm_monitor does not add a single line of behaviour — it is
uvm_component under another name. What it adds is intent: anybody who opens
the testbench knows what that class does without reading it, and print_topology() shows it
as a monitor.
It is the same economy as uvm_env: half the value of UVM is not in the
code of the library, it is in everybody using the same names for the same
things. uvm_driver and uvm_sequencer do bring things —the seq_item_port and the
arbitration—; uvm_monitor, uvm_env and uvm_agent are almost pure vocabulary.
Agents
The jump: from the tester to the sequence
- In the put and get ports we split the stimulus in two: the
testerchose what to send, thedriverknew how - We joined them with
uvm_put_port/uvm_get_portand auvm_tlm_fifoin the middle: three components in the tree and twoconnect() - UVM already brings that done, and with a name: the
uvm_sequencer - The
driverstops being a genericuvm_componentand becomesuvm_driver #(T), which already comes with aseq_item_port - And the
testerdisappears: auvm_sequencereplaces it, and that is not a component — it is an object, it is not in the tree and it does not get connected
This is the hardest jump of the course, and the reason is that there are two changes on
the same line. It is worth separating them.
Change one, structural: the FIFO is called a sequencer and comes with the driver.
Change two, conceptual: the stimulus generator stops being a component.
A component is built once in build_phase and lives the whole simulation;
a sequence is created, runs, finishes and gets thrown away — and you can run another one
right after on the same sequencer. That is what lets a test
combine stimuli without touching the structure, and that is what the sequences
section is all about.
The question to throw at the group: why does the driver of the passive agent not exist,
but the monitor does? Because driving is optional, watching is not.
Agents
The handshake: get_next_item() / item_done()
- The driver no longer does
get(): it doesget_next_item(), which blocks until the sequence has something - When it has finished driving the signals, it calls
item_done() - That second call is what the
put/getpair did not have: it is the driver saying "that is it, send me the next one" - Without
item_done()the sequence hangs forever in itsfinish_item(), and the simulation stops advancing without a single error message - The pair always goes complete, and in that order
code/u7/agents/tb_classes/driver.svh · #run_phase task run_phase(uvm_phase phase);
command_transaction command;
forever begin : command_loop
seq_item_port.get_next_item(command);
bfm.send_op(command.A, command.B, command.op);
seq_item_port.item_done();
end : command_loop
endtask : run_phaseel archivo entero, 27 líneas
class driver extends uvm_driver #(command_transaction);
`uvm_component_utils(driver)
virtual vtalu_bfm bfm;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
vtalu_agent_config cfg;
// The BFM is not asked for on its own: ask for the agent config, and it comes from there.
if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
`uvm_fatal("DRIVER", "Failed to get agent config")
bfm = cfg.bfm;
endfunction : build_phase
task run_phase(uvm_phase phase);
command_transaction command;
forever begin : command_loop
seq_item_port.get_next_item(command);
bfm.send_op(command.A, command.B, command.op);
seq_item_port.item_done();
end : command_loop
endtask : run_phase
endclass : driverForgetting item_done() is mistake number one of the first week, and the
symptom is misleading: no error, no warning, the simulation simply
stops advancing in time and the objection never gets dropped. Verilator has no
timeout by default: it keeps spinning until you kill it.
The trick to remember it: get_next_item / item_done is a loan, not
a copy. The sequence lends you the item and waits for you to
give it back. put/get was a delivery: the tester dropped the command and moved on.
And out of that comes the other useful difference: since the sequence still holds the
handle, the driver can write the result inside it and the sequence reads it when it
comes back from finish_item(). The sequences section uses that.
Agents
The driver, now a uvm_driver
uvm_driver #(command_transaction)brings theseq_item_portfor free: it is not declared nor instantiated withnew()- The
build_phaseno longer looks for the BFM straight in theconfig_db: it asks the agent config for it - The
run_phaseis three lines: ask for the item,bfm.send_op(...), say it is done. The driver does not touch a wire - The protocol stays where we left it in interfaces and BFM: inside the BFM. That is why the driver of an agent is so short, and why changing protocol does not touch it
code/u7/agents/tb_classes/driver.svhclass driver extends uvm_driver #(command_transaction);
`uvm_component_utils(driver)
virtual vtalu_bfm bfm;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
vtalu_agent_config cfg;
// The BFM is not asked for on its own: ask for the agent config, and it comes from there.
if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
`uvm_fatal("DRIVER", "Failed to get agent config")
bfm = cfg.bfm;
endfunction : build_phase
task run_phase(uvm_phase phase);
command_transaction command;
forever begin : command_loop
seq_item_port.get_next_item(command);
bfm.send_op(command.A, command.B, command.op);
seq_item_port.item_done();
end : command_loop
endtask : run_phase
endclass : driverIt is worth opening the driver of the put and get ports alongside and counting lines: they are almost
the same. The only thing that changed is where the item comes from —before a uvm_get_port,
now the seq_item_port— and that there is an item_done() at the end.
The question worth asking: why is the driver so short? Because the protocol
is not here. It is in the BFM since interfaces and BFM, and this class only translates
transaction into call. That is the division of labour that makes an AXI
agent have the same twenty-line driver.
And the corollary, which is the one from the bus appendix: when the protocol changes,
the BFM and the monitor change. The driver, the agent, the env and the test never find out.
Agents
The sequencer gets declared with a typedef
uvm_sequenceris a parameterized class: it is parameterized with the type of item it is going to hand out- There is no need to extend it for anything: it is used as is
- A
typedefin the package gives it a short name, and that name is the one used by the agent and thecreate()
typedef uvm_sequencer #(command_transaction) sequencer;
- The
command_transactionof the transactions already extendsuvm_sequence_item, so it does not have to be touched: that is why we chose that base class and notuvm_transaction - The
typedefgoes after the`includeof the transaction and before the driver and the agent
This is the moment when the decision from the transactions gets cashed in. If that day the
transaction had extended uvm_transaction, today this typedef would not compile:
uvm_sequencer #(T) requires that T derive from uvm_sequence_item.
It is worth saying explicitly: it is the first time in the course that a decision from
an earlier section enables —or breaks— the next one. That is exactly what
"adaptable code" means and it is not a phrase out of a manual.
And in case somebody tries it: extending uvm_sequencer to add things to it is
almost always a sign that it belonged in the sequence. The sequencer is an arbiter, not
a place to put logic.
Agents
The minimal sequence
- A
uvm_sequence #(T)has a taskbody(): that is all there is to write - Every item goes between
start_item()andfinish_item() start_item()blocks until the sequencer gives us the turn;finish_item()blocks until the driver calleditem_done()- The
randomize()goes between the two: that way the constraints get resolved at the moment the item is about to be handed over, not before - This is the direct translation of the
testerof the transactions, and nothing more than that — the sequences section is about what can be done in here
code/u7/agents/tb_classes/command_sequence.svh · #class-and-bodyclass command_sequence extends uvm_sequence #(command_transaction);
`uvm_object_utils(command_sequence)
function new(string name = "command_sequence");
super.new(name);
endfunction : new
task body();
command_transaction command;
// All transactions come out of the factory, the directed ones too:
// a new() would skip add_test's set_type_override.
command = command_transaction::type_id::create("command");
start_item(command);
command.op = rst_op;
finish_item(command);
repeat (1000) begin : random_loop
command = command_transaction::type_id::create("command");
start_item(command);
// The randomize() goes BETWEEN start_item and finish_item: that way the
// constraints are solved when the item is about to be handed over, and
// not before. Today it makes no difference; the day a constraint
// depends on the DUT state, it will.
if (!command.randomize()) `uvm_fatal("COMMAND SEQ", "randomize() failed")
finish_item(command);
end : random_loop
// Directed: the multiplier overflow, which 1000 random operations might
// never touch. It comes out of the factory like the others, but since it
// is not randomized, add_transaction's constraint does not run.
command = command_transaction::type_id::create("command");
start_item(command);
command.op = mul_op;
command.A = 8'hFF;
command.B = 8'hFF;
finish_item(command);
endtask : bodyel archivo entero, 45 líneas
// The run_phase of the u6/transactions tester, turned into the body() of a sequence.
// Same stimulus, same count, same directed cases.
//
// What changed: it is no longer a uvm_component, it is not in the tree, it
// connects to nothing and it does not raise the objection -- the test that starts it does.
class command_sequence extends uvm_sequence #(command_transaction);
`uvm_object_utils(command_sequence)
function new(string name = "command_sequence");
super.new(name);
endfunction : new
task body();
command_transaction command;
// All transactions come out of the factory, the directed ones too:
// a new() would skip add_test's set_type_override.
command = command_transaction::type_id::create("command");
start_item(command);
command.op = rst_op;
finish_item(command);
repeat (1000) begin : random_loop
command = command_transaction::type_id::create("command");
start_item(command);
// The randomize() goes BETWEEN start_item and finish_item: that way the
// constraints are solved when the item is about to be handed over, and
// not before. Today it makes no difference; the day a constraint
// depends on the DUT state, it will.
if (!command.randomize()) `uvm_fatal("COMMAND SEQ", "randomize() failed")
finish_item(command);
end : random_loop
// Directed: the multiplier overflow, which 1000 random operations might
// never touch. It comes out of the factory like the others, but since it
// is not randomized, add_transaction's constraint does not run.
command = command_transaction::type_id::create("command");
start_item(command);
command.op = mul_op;
command.A = 8'hFF;
command.B = 8'hFF;
finish_item(command);
endtask : body
endclass : command_sequenceTwo things worth marking in the code, because they are the ones that get copied wrong.
First: the transaction comes out of type_id::create(), the reset one and the
directed multiplication one too. It is the same rule as the transactions —the factory
only finds out about what goes through create()— and here it matters again because the
add_test is still around.
Second: randomize() between start_item and finish_item is not a whim of
style. Randomizing before works just the same today, but it breaks the day a constraint
depends on the state of the DUT or on the previous response: by then the value
had already been chosen. Randomizing late is the correct habit and it costs nothing
to adopt it from day one.
Agents
is_active: active and passive
uvm_agentbrings a fieldis_activeof typeuvm_active_passive_enum, with two values:UVM_ACTIVEandUVM_PASSIVE- Active: the sequencer and the driver get built. The agent drives the interface
- Passive: they do not get built. The agent only watches an interface that somebody else drives
- The monitors and the two analysis ports get built always: a passive agent keeps feeding scoreboard and coverage
- It is read with
get_is_active()and decided in thebuild_phase, with anif
The honest question is "and what do I want an agent that drives nothing for?". Three answers from the field, in order of frequency: One, output interfaces. A port the DUT drives and you only verify does not need a driver, and the passive agent gives you the monitor and the coverage for free. Two, integration. When the block you verified becomes part of a chip, the interface stops being yours: the block next door drives it. The same agent, with one different line in the config, still works. That is the real return on having built an agent. Three, the one from the example: living with stimulus that is not yours — a legacy module, a reference model, a generator from another team.
Agents
And it can be seen
bash run.sh +TOPOLOGYprints the tree of components UVM built
clase_agent_h vtalu_agent
command_ap uvm_analysis_port
command_monitor_h command_monitor
driver_h driver
seq_item_port uvm_seq_item_pull_port
result_ap uvm_analysis_port
result_monitor_h result_monitor
sequencer_h uvm_sequencer
seq_item_export uvm_seq_item_pull_imp
modulo_agent_h vtalu_agent
command_ap uvm_analysis_port
command_monitor_h command_monitor
result_ap uvm_analysis_port
result_monitor_h result_monitor
- Same class, two instances, and one has four components fewer
- There too are the
seq_item_portand theseq_item_exportthat the driver and the sequencer bring already fitted, without anybody declaring them — against the put and get ports, where theuvm_put_porthad to be declared andnew()ed print_topology()is besides the debug tool for when aconnect()points at a component that does not exist
This tree is printed by the library, not by the course, and that is the whole point: up to here
the hierarchy was a diagram on a slide, and now it is an output you can
grep. It is worth running it live — it is the print_topology() of the reporting
section, now with something worth looking at.
The two things to make them see, side by side: clase_agent_h
has driver_h and sequencer_h, and modulo_agent_h does not. That is
is_active in practice — the same class type, instantiated twice, and one
built two objects fewer. Both monitors are in both, because watching
is never optional.
And the one that is not there: there is no sequence in this tree. A sequence is a
uvm_object, not a uvm_component, so it does not live in the topology. It is the
question thrown out on day 1 with the class diagram, and here it can be answered
pointing at the screen.
Agents
The config object of the agent
- The agent needs two pieces of data from outside: which BFM and whether it is active
- Both travel together in a configuration object, which is the public interface of the agent
- It is a bare class, not a
uvm_object: that is why the constructor can demand both pieces of data, and whoever forgets one does not compile - A
uvm_objectis created withtype_id::create(), which only takes aname: the guarantee of the constructor is lost - The field
is_activegoesprotectedwith a getter: nobody changes it after the agent has been built
code/u7/agents/tb_classes/vtalu_agent_config.svh// The agent config is NOT a uvm_object: it is a plain class. That is why the
// constructor can DEMAND both values, and whoever forgets one does not compile.
// A uvm_object is built with type_id::create(), which only takes a name, and
// that guarantee is lost.
class vtalu_agent_config;
virtual vtalu_bfm bfm;
// protected + getter: nobody changes it after the agent was built.
protected uvm_active_passive_enum is_active;
function new(virtual vtalu_bfm bfm, uvm_active_passive_enum is_active);
this.bfm = bfm;
this.is_active = is_active;
endfunction : new
function uvm_active_passive_enum get_is_active();
return is_active;
endfunction : get_is_active
endclass : vtalu_agent_configHere the course departs from the custom of the industry on purpose, and it is worth
saying so: most projects do class agent_config extends uvm_object
and register it in the factory, so it can be overridden. It is a legitimate decision and
it has its reason. The downside is the one in the bullet: you lose the constructor
that forces you.
What is not negotiable is the underlying idea, and it is the one to leave behind: every
level of hierarchy receives its configuration through an object. If a component
needs five pieces of data, that is not five uvm_config_db::get() scattered around: it is
a config. The day you add the sixth piece of data, you touch one class and not six.
Notice that the driver and the two monitors also ask for the config, not the BFM: the
agent does not hand out handles by hand.
Agents
The agent class: build_phase
- It asks the
uvm_config_dbfor its config and copies theis_activeby hand: withoutsuper.build_phase(),uvm_agent's, the one that would read it, never runs - It builds sequencer and driver only if it is active; the monitors and the two analysis ports, always
- The analysis ports are ports: they get instantiated with
new(), not with the factory
code/u7/agents/tb_classes/vtalu_agent.svh · #class-and-buildclass vtalu_agent extends uvm_agent;
`uvm_component_utils(vtalu_agent)
vtalu_agent_config cfg;
sequencer sequencer_h;
driver driver_h;
command_monitor command_monitor_h;
result_monitor result_monitor_h;
uvm_analysis_port #(command_transaction) command_ap;
uvm_analysis_port #(result_transaction) result_ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
`uvm_fatal("AGENT", "Failed to get agent config")
is_active = cfg.get_is_active();
if (get_is_active() == UVM_ACTIVE) begin : estimulo
sequencer_h = sequencer::type_id::create("sequencer_h", this);
driver_h = driver::type_id::create("driver_h", this);
end : estimulo
command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
result_monitor_h = result_monitor::type_id::create("result_monitor_h", this);
command_ap = new("command_ap", this);
result_ap = new("result_ap", this);
endfunction : build_phaseel archivo entero, 43 líneas
class vtalu_agent extends uvm_agent;
`uvm_component_utils(vtalu_agent)
vtalu_agent_config cfg;
sequencer sequencer_h;
driver driver_h;
command_monitor command_monitor_h;
result_monitor result_monitor_h;
uvm_analysis_port #(command_transaction) command_ap;
uvm_analysis_port #(result_transaction) result_ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
`uvm_fatal("AGENT", "Failed to get agent config")
is_active = cfg.get_is_active();
if (get_is_active() == UVM_ACTIVE) begin : estimulo
sequencer_h = sequencer::type_id::create("sequencer_h", this);
driver_h = driver::type_id::create("driver_h", this);
end : estimulo
command_monitor_h = command_monitor::type_id::create("command_monitor_h", this);
result_monitor_h = result_monitor::type_id::create("result_monitor_h", this);
command_ap = new("command_ap", this);
result_ap = new("result_ap", this);
endfunction : build_phase
function void connect_phase(uvm_phase phase);
if (get_is_active() == UVM_ACTIVE)
driver_h.seq_item_port.connect(sequencer_h.seq_item_export);
command_monitor_h.ap.connect(command_ap);
result_monitor_h.ap.connect(result_ap);
endfunction : connect_phase
endclass : vtalu_agentA detail worth gold that nobody tells: the build_phase of uvm_agent that
the library brings only reads is_active from the resource pool. It is in
code/.uvm/src/comps/uvm_agent.svh, it can be opened in class.
Which means that in a testbench that calls super.build_phase(), a
uvm_config_db#(int)::set(this, "mi_agent", "is_active", UVM_PASSIVE) works
on its own, without a config object. Since this vtalu_agent does not call it,
that mechanism is switched off, and that is why we assign it by hand.
And here is where day 3 gets paid: uvm_agent and uvm_sequencer are the two
classes in the library that do real work in their build_phase —uvm_agent reads
is_active, and uvm_sequencer_param_base hooks up the response fifo
(seq/uvm_sequencer_param_base.svh:288)—. It is exactly the case the rule for out
there covers: if you extend either of the two and you write a build_phase, the
super is mandatory. You inherited from an intermediate class that does real
work, and skipping the super switches it off without a word. We can
skip it because we wrote the line that replaces it; whoever does not write that
line, cannot.
Both ways are valid. What is not valid is half of each one: putting
is_active in the config_db, not calling super.build_phase(), and then
wondering why the agent starts up active. It happens often.
Agents
The agent class: connect_phase
- Inside: the
seq_item_portof the driver against theseq_item_exportof the sequencer — a single line, and only if it is active - Outwards: the
apof each monitor gets connected to the analysis port of the agent - That second
connect()is the one that makes the agent a closed box: theenvnever touchescommand_monitor_h, it talks tocommand_ap - The mantra ports connect to exports still holds, with one more twist: a
port also connects to another port of the same type, outwards. That is
command_monitor_h.ap.connect(command_ap), and it is called port forwarding
Port forwarding is what confuses, and it is worth saying why it exists before
how it is written. The agent wants to offer outwards a piece of data produced by a child
of its own. It could expose the child —the env looking at agent.command_monitor_h.ap— and
that is where the encapsulation ends: the day the monitor changes name, the
env breaks.
The way out is for the agent to have its own analysis port and plug it into the
monitor's. From outside a single port is seen, command_ap, and inside there can be
whatever. It is exactly what a module does when it connects a port of its own to that of
a submodule — the same idea, with classes.
The detail that looks like it breaks the mantra: here a port connects to another port, not
to an export. It is not a capricious exception — the direction is still the same,
from the one who calls to the one who implements; the only thing that happens is that the agent is in
the middle and implements nothing, it only forwards.
Agents
How the env ends up
- Four lines of structure instead of seven objects and five connections
- The
envno longer knows there is a driver, or a sequencer, or a FIFO: it knows there is an agent and that it has two analysis ports - The scoreboard and the coverage stay just as in the analysis ports: they are subscribers, and they find out about nothing
code/u7/agents/env_un_agent.svh · #class-and-buildclass env extends uvm_env;
`uvm_component_utils(env)
vtalu_agent agent_h;
vtalu_agent_config agent_cfg_h;
scoreboard scoreboard_h;
coverage coverage_h;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
virtual vtalu_bfm bfm;
if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
`uvm_fatal("ENV", "Failed to get BFM")
agent_cfg_h = new(.bfm(bfm), .is_active(UVM_ACTIVE));
uvm_config_db#(vtalu_agent_config)::set(this, "agent_h*", "config", agent_cfg_h);
agent_h = vtalu_agent::type_id::create("agent_h", this);
scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
coverage_h = coverage::type_id::create("coverage_h", this);
endfunction : build_phase
function void connect_phase(uvm_phase phase);
agent_h.command_ap.connect(scoreboard_h.cmd_f.analysis_export);
agent_h.command_ap.connect(coverage_h.analysis_export);
agent_h.result_ap.connect(scoreboard_h.analysis_export);
endfunction : connect_phaseel archivo entero, 36 líneas
// ILLUSTRATION -- not compiled. The env of a SINGLE VTALU, to compare against
// the env.svh of the Transactions section.
class env extends uvm_env;
`uvm_component_utils(env)
vtalu_agent agent_h;
vtalu_agent_config agent_cfg_h;
scoreboard scoreboard_h;
coverage coverage_h;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
virtual vtalu_bfm bfm;
if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "bfm", bfm))
`uvm_fatal("ENV", "Failed to get BFM")
agent_cfg_h = new(.bfm(bfm), .is_active(UVM_ACTIVE));
uvm_config_db#(vtalu_agent_config)::set(this, "agent_h*", "config", agent_cfg_h);
agent_h = vtalu_agent::type_id::create("agent_h", this);
scoreboard_h = scoreboard::type_id::create("scoreboard_h", this);
coverage_h = coverage::type_id::create("coverage_h", this);
endfunction : build_phase
function void connect_phase(uvm_phase phase);
agent_h.command_ap.connect(scoreboard_h.cmd_f.analysis_export);
agent_h.command_ap.connect(coverage_h.analysis_export);
agent_h.result_ap.connect(scoreboard_h.analysis_export);
endfunction : connect_phase
endclass : env- That is the
envof a single VTALU. With two, it changes less than it looks
This is the slide for making the comparison live: open the env.svh of transactions
alongside. The difference is not that it is shorter —which it also is—, it is what
disappeared: the env of transactions named command_f, which means it knew the
internal mechanism of how the stimulus reached the driver. This one does not name
it. That is encapsulation measured objectively: count the names that
the class above has to know.
If the group is doing well on time, the question is worth it: what would have to
change in this env to go from a FIFO to a sequencer? Nothing. It is already
inside the agent.
Agents
Two agents, one single string
- The two agents execute the same line:
uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg) - And yet they have to receive different objects
- The second argument of the
set()resolves it: the scope, which is the path of the component that is going to read "clase_agent_h*"reaches that agent and everything hanging off it — that is why the driver and the monitors find the same config without anybody passing it to them- The
thisof theset()is the starting point of that path: theenv
Here is the classic trap of the section, and it is one of the ones that never fail. With a single
agent, everybody writes "*" in the scope and it works. The day the
second one shows up, the second set() overwrites the first and both agents get the
same config: the same BFM and the same is_active. With the order in env.svh —the
class first and the module after— the one left standing is the passive one, so both
start up passive, with no driver and no sequencer, and the test's seq.start() lands
on a null handle. With the order reversed they would be two drivers, but driving
the same interface. What is invariant, and what to take away: the last set()
wins, and both agents end up identical. The error does not show up in build_phase.
The asterisk matters: "clase_agent_h*" with an asterisk reaches the children;
"clase_agent_h" without an asterisk, only the agent. If you write it without an asterisk, the
agent finds its config and the driver does not — uvm_fatal in build_phase, which at
least is an honest error.
Short rule to take away: the scope of the set() is a path, not a label.
Agents
When the config_db does not find: +UVM_CONFIG_DB_TRACE
$ bash run.sh +UVM_CONFIG_DB_TRACE
UVM_INFO ... Configuration 'config' (type vtalu_agent_config) set by env_h
UVM_INFO ... Configuration 'config' read by
accessor=uvm_test_top.env_h.clase_agent_h.driver_h
UVM_INFO ... Configuration 'config' read by
accessor=uvm_test_top.env_h.modulo_agent_h.command_monitor_h
- The
config_dbis a database by strings: when it fails, it fails in silence or with auvm_fatalthat does not say why +UVM_CONFIG_DB_TRACEmakes UVM print everyset()and everyget(), with the complete hierarchical path of the component that asked for it- It works for the two questions one asks: did the
set()reach this component? and which components actually exist? - It is a plusarg, not code: nothing has to be touched or recompiled
This is the tool that tests was missing, when we said that
get(null, "*", ...) "does not fail: it lies". With the trace on, lying becomes
difficult: you see who put what and who read it.
The use almost nobody discovers on their own is the second one: since every get() prints the
path of the component that called it, the output is a census of the tree written by the
library. If a component does not show up, it was not built. The exercise of the day
uses exactly that to grade itself, and that is why it cannot be cheated: the
student does not write those lines.
A close relative worth naming: +UVM_OBJECTION_TRACE, for the other classic
hang —the simulation that does not finish because somebody did not drop their objection—.
Agents
The example: two VTALU and the legacy module
- The case that justifies all of this: two sources of stimulus have to be compared on the same design
- The
topinstantiates two VTALU and twovtalu_bfm - The first one is driven by our agent, in
UVM_ACTIVE - The second one is driven by
vtalu_tester_module, an ordinary module without one line of UVM, and we hang an agent on it inUVM_PASSIVEto watch it - Two scoreboards and two coverages: that is what answers which of the two stimuli covers more
code/u7/agents/top.sv · #topmodule top;
import uvm_pkg::*;
import vtalu_pkg::*;
`include "vtalu_macros.svh"
`include "uvm_macros.svh"
// The VTALU driven by the class testbench
vtalu_bfm clase_bfm ();
vtalu clase_dut (
.A(clase_bfm.A), .B(clase_bfm.B), .op(clase_bfm.op_set),
.clk(clase_bfm.clk), .reset_n(clase_bfm.reset_n), .start(clase_bfm.start),
.done(clase_bfm.done),
.ovf(clase_bfm.ovf), .result(clase_bfm.result)
);
// The VTALU driven by the legacy module
vtalu_bfm modulo_bfm ();
vtalu modulo_dut (
.A(modulo_bfm.A), .B(modulo_bfm.B), .op(modulo_bfm.op_set),
.clk(modulo_bfm.clk), .reset_n(modulo_bfm.reset_n), .start(modulo_bfm.start),
.done(modulo_bfm.done),
.ovf(modulo_bfm.ovf), .result(modulo_bfm.result)
);
vtalu_tester_module stim_module (modulo_bfm);
initial begin
// The top knows nothing about agents: it leaves the two interfaces with a
// name and walks away. Handing them out is the test's job.
uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "clase_bfm", clase_bfm);
uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "modulo_bfm", modulo_bfm);
run_test();
end
endmodule : topel archivo entero, 35 líneas
module top;
import uvm_pkg::*;
import vtalu_pkg::*;
`include "vtalu_macros.svh"
`include "uvm_macros.svh"
// The VTALU driven by the class testbench
vtalu_bfm clase_bfm ();
vtalu clase_dut (
.A(clase_bfm.A), .B(clase_bfm.B), .op(clase_bfm.op_set),
.clk(clase_bfm.clk), .reset_n(clase_bfm.reset_n), .start(clase_bfm.start),
.done(clase_bfm.done),
.ovf(clase_bfm.ovf), .result(clase_bfm.result)
);
// The VTALU driven by the legacy module
vtalu_bfm modulo_bfm ();
vtalu modulo_dut (
.A(modulo_bfm.A), .B(modulo_bfm.B), .op(modulo_bfm.op_set),
.clk(modulo_bfm.clk), .reset_n(modulo_bfm.reset_n), .start(modulo_bfm.start),
.done(modulo_bfm.done),
.ovf(modulo_bfm.ovf), .result(modulo_bfm.result)
);
vtalu_tester_module stim_module (modulo_bfm);
initial begin
// The top knows nothing about agents: it leaves the two interfaces with a
// name and walks away. Handing them out is the test's job.
uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "clase_bfm", clase_bfm);
uvm_config_db#(virtual vtalu_bfm)::set(null, "*", "modulo_bfm", modulo_bfm);
run_test();
end
endmodule : top- The module calls the very same
bfm.send_op()as the driver: it receives the interface through its port instead of through a virtual interface, but the protocol it executes is exactly the same code - That is what makes the comparison fair: both stimuli speak identically to the DUT. The only thing being compared is what each one sends, not how
This top.sv is the one that closes the argument of the section, and it is worth reading
looking for what did not change: there are two VTALU, two interfaces, two different
config_db, and the vtalu_tester_module is still the same old Verilog module
as always. Nobody touched it so it could live with UVM.
That is the real scenario the section comes to solve, and it is worth naming it like this:
you arrive at a project where there is already a stimulus written in Verilog that works and
nobody is going to rewrite. With a passive agent you can watch it —scoreboard,
coverage, assertions— without asking anybody's permission or touching a line of it.
The technical detail that makes the comparison fair, and that has to be pointed at with a
finger: the module calls the same bfm.send_op() as the driver. It receives the
interface through a port instead of through a virtual interface, but the protocol is the
same code. If each one drove the bus its own way, comparing coverages would
not mean anything.
Agents
The test, and what is still missing
- The
testtakes the two BFM out of theconfig_db, builds theenv_configand passes it down - Then it starts the sequence by hand, on the sequencer of the active agent:
seq = command_sequence::type_id::create("seq");
seq.start(env_h.clase_agent_h.sequencer_h);
- It works, but look at what that line does: the test goes across the hierarchy to reach the sequencer, and with that it knows the inside of the env again
- Everything we have just encapsulated leaks out in one line
- The sequences section fixes it: the sequence is handed to the sequencer through the
config_db, and the test stops knowing where it is
code/u7/agents/tb_classes/dual_test.svh · #build-and-run function void build_phase(uvm_phase phase);
virtual vtalu_bfm clase_bfm, modulo_bfm;
env_config env_config_h;
// The top left the two interfaces loose; the test packs them up.
if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "clase_bfm", clase_bfm))
`uvm_fatal("DUAL TEST", "Failed to get clase_bfm")
if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "modulo_bfm", modulo_bfm))
`uvm_fatal("DUAL TEST", "Failed to get modulo_bfm")
env_config_h = new(.clase_bfm(clase_bfm), .modulo_bfm(modulo_bfm));
uvm_config_db#(env_config)::set(this, "env_h*", "config", env_config_h);
env_h = env::type_id::create("env_h", this);
endfunction : build_phase
// +TOPOLOGY prints the component tree. It is how to SEE that the passive
// agent built neither a sequencer nor a driver.
function void end_of_elaboration_phase(uvm_phase phase);
if ($test$plusargs("TOPOLOGY")) uvm_root::get().print_topology();
endfunction : end_of_elaboration_phase
task run_phase(uvm_phase phase);
command_sequence seq;
phase.raise_objection(this);
// The only thing left wired by hand: the test reaches across the
// hierarchy to get to the sequencer. The sequences section takes it out of here.
seq = command_sequence::type_id::create("seq");
seq.start(env_h.clase_agent_h.sequencer_h);
#500;
phase.drop_objection(this);
endtask : run_phaseel archivo entero, 46 líneas
class dual_test extends uvm_test;
`uvm_component_utils(dual_test);
env env_h;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
virtual vtalu_bfm clase_bfm, modulo_bfm;
env_config env_config_h;
// The top left the two interfaces loose; the test packs them up.
if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "clase_bfm", clase_bfm))
`uvm_fatal("DUAL TEST", "Failed to get clase_bfm")
if (!uvm_config_db#(virtual vtalu_bfm)::get(this, "", "modulo_bfm", modulo_bfm))
`uvm_fatal("DUAL TEST", "Failed to get modulo_bfm")
env_config_h = new(.clase_bfm(clase_bfm), .modulo_bfm(modulo_bfm));
uvm_config_db#(env_config)::set(this, "env_h*", "config", env_config_h);
env_h = env::type_id::create("env_h", this);
endfunction : build_phase
// +TOPOLOGY prints the component tree. It is how to SEE that the passive
// agent built neither a sequencer nor a driver.
function void end_of_elaboration_phase(uvm_phase phase);
if ($test$plusargs("TOPOLOGY")) uvm_root::get().print_topology();
endfunction : end_of_elaboration_phase
task run_phase(uvm_phase phase);
command_sequence seq;
phase.raise_objection(this);
// The only thing left wired by hand: the test reaches across the
// hierarchy to get to the sequencer. The sequences section takes it out of here.
seq = command_sequence::type_id::create("seq");
seq.start(env_h.clase_agent_h.sequencer_h);
#500;
phase.drop_objection(this);
endtask : run_phase
endclass : dual_testThis is the slide to close the day with if there is no time for the
summary: it leaves an open question and a concrete promise.
And it is honest: seq.start(env_h.clase_agent_h.sequencer_h) is real code that
works and that is seen in many testbenches. It is not "wrong". It is simply the
last thing in the testbench that is still hard-wired, and it is the one the section
that follows knocks over.
If somebody asks why we do not do it right from the start: because to understand the
default_sequence you first have to have suffered writing the start() by hand.
Agents
What this section does differently
- The book does not use a sequencer here. In The UVM Primer the agent of this
section still has the
testerand theuvm_tlm_fifoinside, and the sequencer only shows up with sequences — where besides thetesterdisappears - We build the agent complete in one go: it is the way it is written today, and it makes the sequences section be only about sequences
- The scoreboard and the coverage stay outside the agent, against what the UVM Primer does. A passive agent should not drag a scoreboard along
- The config of the agent is not a
uvm_object: it is a bare class, so that the constructor forces you. The industry usually makes it auvm_object - The protocol lives in the BFM, the same as in the UVM Primer. Up to Verilator 5.051 this
could not be done: an interface task called through a virtual interface did not
propagate. It was fixed in 5.052 —
docs/en/verilator.md
That the material says where it departs from its sources is not a detail of
tidiness: it is what lets the student read the UVM Primer afterwards without
believing that one of the two is broken.
And along the way it leaves the lesson most used at work: the structure of a
UVM testbench is not fixed by the standard, it is fixed by custom. IEEE 1800.2 does not say
anywhere that the scoreboard goes in the env. It says what a uvm_agent is and
nothing else. Everything else is convention — good, but convention.
Agents
Summary of the unit
- An agent packs everything that knows how to speak one interface: sequencer, driver and monitors, wired inside a single time
- It gets instantiated once per interface, and it is configured with a configuration object, not with loose handles
is_activedecides whether, besides watching, it drives- The
envwent from seven components wired by hand to one agent and two subscribers - The
uvm_config_dbstops being a global mailbox: the scope is a path in the tree, and with more than one agent it is what makes each one receive its own - One single thing was left hard-wired: the test still looks for the sequencer by hand to start the sequence. That is the sequences section
Close of the section and of the biggest jump of the course. It is worth saying where they ended up standing: with today's material, the student can read the testbench of any UVM project and recognise the structure. An agent per interface, a config per level, analysis in the env. It is literally 80 % of what they will see on their first day. What is missing —sequences— is what they will write on their first day. That is why they go together on the same day and in this order: first the house, then what happens inside.
Callbacks
The third hook
- On day 1 we promised three ways of writing a new test without touching the env: constraints, factory override and callbacks. Two are done
- The three are the same idea at different heights: the constraint changes values, the override changes a whole class, the callback changes what a class does at one point
- The case that separates them: you are handed a VIP agent —from another team, or bought— and you need the driver to send a broken piece of data once in a while
- With an override you have to rewrite the whole driver to change one line. With a callback you hook that line and nothing else
- And there is a difference that is not a matter of style: callbacks stack. The override picks one class; the callbacks all run, every one of them, in order
This half section exists because unit 1 promises three hooks, and without it the course delivers two. Fifteen minutes are enough: the mechanics are small and what has to be left behind is the criterion for when it gets used. The criterion, said in one line: override when the what changes, callback when the when changes. If the new class differs from the old one in several methods, it is an override. If it is the same class with an addition at one point, it is a callback — and above all if the original class is not yours and you cannot touch it. The key word in all of this is VIP. In a real project half of the agents come from outside, with their licence and their support, and editing them is not an option: the next version wipes your change. The callback is the extension point the one who wrote the VIP left on purpose. And that they stack is what makes it possible for two engineers to hook different things into the same driver without stepping on each other. With overrides, the second one wins.
Callbacks
The hook: one class, one empty virtual method
code/u7/callbacks/tb_classes/driver_callback.svh · #driver_callbackclass driver_callback extends uvm_callback;
`uvm_object_utils(driver_callback)
function new(string name = "driver_callback");
super.new(name);
endfunction : new
// A task, not a function: a callback that only edits the transaction could
// be a function, but one that adds delay needs to consume time. Making the
// hook a task costs nothing and leaves both open.
virtual task pre_send(driver drv, command_transaction cmd);
endtask : pre_send
endclass : driver_callbackel archivo entero, 68 líneas
// The hook itself. A callback is NOT a component: it does not live in the tree
// and it has no phases. It is an object that hangs off a component, and the
// component decides where -- and whether -- it gets called.
//
// The base class is empty on purpose: a testbench with no callbacks registered
// runs exactly as it did before, at the cost of one empty virtual call.
typedef class driver;
class driver_callback extends uvm_callback;
`uvm_object_utils(driver_callback)
function new(string name = "driver_callback");
super.new(name);
endfunction : new
// A task, not a function: a callback that only edits the transaction could
// be a function, but one that adds delay needs to consume time. Making the
// hook a task costs nothing and leaves both open.
virtual task pre_send(driver drv, command_transaction cmd);
endtask : pre_send
endclass : driver_callback
// --------------------------------------------------------------------------
// Error injection: flip one bit of A, once every `cada` transactions.
//
// This is the classic use -- and the reason it is worth showing is what it does
// NOT do to the scoreboard. See run.sh.
class flip_bit_cb extends driver_callback;
`uvm_object_utils(flip_bit_cb)
int unsigned cada = 8; // one out of every N
int unsigned vistas;
function new(string name = "flip_bit_cb");
super.new(name);
endfunction : new
virtual task pre_send(driver drv, command_transaction cmd);
vistas++;
if (vistas % cada != 0) return;
`uvm_info("FLIP_BIT_CB", $sformatf("A: %02h -> %02h", cmd.A, cmd.A ^ 8'h01),
UVM_MEDIUM)
cmd.A ^= 8'h01;
endtask : pre_send
endclass : flip_bit_cb
// --------------------------------------------------------------------------
// Timing injection: hold the bus idle for a couple of clocks before driving.
//
// It is here to make one point that the factory cannot make: callbacks STACK.
// Both of these are registered on the same driver, and both run, in the order
// they were added. A factory override picks one class and only one.
class jitter_cb extends driver_callback;
`uvm_object_utils(jitter_cb)
int unsigned ciclos = 2;
function new(string name = "jitter_cb");
super.new(name);
endfunction : new
virtual task pre_send(driver drv, command_transaction cmd);
repeat (ciclos) @(posedge drv.bfm.clk);
endtask : pre_send
endclass : jitter_cb- A
uvm_callbackis not a component: it is not in the tree, it has no phases and it does not show up inprint_topology()— just like a sequence - The base method is empty on purpose: with nobody registered, the testbench runs exactly as before
- It is a
taskand not afunctionbecause a delay callback has to be able to consume time. One that only edits the transaction could be a function
It is worth stopping on the fact that it is a normal class, extending uvm_callback, which
extends uvm_object. There is no magic: the only thing uvm_callback does is give it
a place in the pool UVM keeps per component instance.
The typedef class driver; at the top of the file is the boring and
necessary part: the callback receives the driver, and the driver registers the callback, so
one of the two has to be forward-declared. In a real testbench
this lives in its own file for that very reason.
If somebody asks why the base method is empty instead of abstract:
because uvm_do_callbacks walks the whole queue, and an empty queue has to
be legal. The empty base class is what makes "no callbacks" the normal case
and not an exception.
Callbacks
Two lines in the driver, and not one more
code/u7/callbacks/tb_classes/driver.svh · #register-cbclass driver extends uvm_driver #(command_transaction);
`uvm_component_utils(driver)
// Declares "this component accepts callbacks of this type". Without it the
// add() below still hooks up and the callback still runs, but with a CBUNREG
// warning and without the type check: outside the contract, and nearly silent.
`uvm_register_cb(driver, driver_callback)
virtual vtalu_bfm bfm;el archivo entero, 35 líneas
// The Agents driver with the hook added. Diff against ../agents/tb_classes:
// two lines -- `uvm_register_cb and `uvm_do_callbacks. Nothing else moves.
class driver extends uvm_driver #(command_transaction);
`uvm_component_utils(driver)
// Declares "this component accepts callbacks of this type". Without it the
// add() below still hooks up and the callback still runs, but with a CBUNREG
// warning and without the type check: outside the contract, and nearly silent.
`uvm_register_cb(driver, driver_callback)
virtual vtalu_bfm bfm;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
vtalu_agent_config cfg;
if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
`uvm_fatal("DRIVER", "Failed to get agent config")
bfm = cfg.bfm;
endfunction : build_phase
task run_phase(uvm_phase phase);
command_transaction command;
forever begin : command_loop
seq_item_port.get_next_item(command);
// Every callback registered on this driver, in the order they were
// added. With none registered this is a loop over an empty queue.
`uvm_do_callbacks(driver, driver_callback, pre_send(this, command))
bfm.send_op(command.A, command.B, command.op);
seq_item_port.item_done();
end : command_loop
endtask : run_phase
endclass : drivercode/u7/callbacks/tb_classes/driver.svh · #run_phase task run_phase(uvm_phase phase);
command_transaction command;
forever begin : command_loop
seq_item_port.get_next_item(command);
// Every callback registered on this driver, in the order they were
// added. With none registered this is a loop over an empty queue.
`uvm_do_callbacks(driver, driver_callback, pre_send(this, command))
bfm.send_op(command.A, command.B, command.op);
seq_item_port.item_done();
end : command_loop
endtask : run_phaseel archivo entero, 35 líneas
// The Agents driver with the hook added. Diff against ../agents/tb_classes:
// two lines -- `uvm_register_cb and `uvm_do_callbacks. Nothing else moves.
class driver extends uvm_driver #(command_transaction);
`uvm_component_utils(driver)
// Declares "this component accepts callbacks of this type". Without it the
// add() below still hooks up and the callback still runs, but with a CBUNREG
// warning and without the type check: outside the contract, and nearly silent.
`uvm_register_cb(driver, driver_callback)
virtual vtalu_bfm bfm;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
vtalu_agent_config cfg;
if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
`uvm_fatal("DRIVER", "Failed to get agent config")
bfm = cfg.bfm;
endfunction : build_phase
task run_phase(uvm_phase phase);
command_transaction command;
forever begin : command_loop
seq_item_port.get_next_item(command);
// Every callback registered on this driver, in the order they were
// added. With none registered this is a loop over an empty queue.
`uvm_do_callbacks(driver, driver_callback, pre_send(this, command))
bfm.send_op(command.A, command.B, command.op);
seq_item_port.item_done();
end : command_loop
endtask : run_phase
endclass : driver`uvm_register_cbdeclares the type/callback pair. Without it theadd()hooks up all the same and the callback **runs** — with aUVM_WARNING CBUNREGlost in the log, and without the type check oradd_by_name`uvm_do_callbacksis the extension point: it walks the queue of that instance of driver, in order
The diff against the driver of the Agents section is exactly two lines, and
it is worth showing it with a live diff: diff code/u7/agents/tb_classes/driver.svh code/u7/callbacks/tb_classes/driver.svh. Everything else —the get_next_item, the
send_op, the item_done— is the same.
The failure mode of the missing uvm_register_cb is not the one you expect, and it
is worth telling it with the source in hand because half the tutorials get it
backwards: uvm_callback.svh:745 reports a UVM_WARNING CBUNREG and carries
on — the add() puts the callback into m_base_inst.m_pool all the same
(:781-785), and `uvm_do_callbacks reads that pool without consulting the
registry (:964-1006). Which means: without the macro the callback runs, and what
you lose is the type check and add_by_name by derived type. That is worse than an
error: it is a warning among a thousand lines, and the testbench ends up outside
the contract without anyone noticing.
That is why the run.sh of the example does not check the macro but the effect: it
counts how many times the callback ran and fails if it is zero. An example that
"passes" without having injected anything proves nothing, whatever the reason it
came unhooked.
Where to put the `uvm_do_callbacks is the real design decision, and it is the
same one taken by whoever writes a VIP: every hook point is a promise of
forward compatibility. That is why VIPs have three or four, not thirty.
Callbacks
Putting them in: the test, and nothing but the test
code/u7/callbacks/tb_classes/inject_test.svh · #hooking-the-cb // end_of_elaboration and not build: the driver is a grandchild of the env,
// and build_phase runs top-down -- when this test's build_phase returns, the
// driver does not exist yet.
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
flip_h = flip_bit_cb::type_id::create("flip_h");
jitter_h = jitter_cb::type_id::create("jitter_h");
// Both on the same driver, and both run: the queue is per component
// instance, in insertion order. The passive agent has no driver, so there
// is nothing to hook there.
uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, jitter_h);
uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, flip_h);
if ($test$plusargs("CALLBACK_TRACE"))
uvm_callbacks #(driver, driver_callback)::display(env_h.clase_agent_h.driver_h);
endfunction : end_of_elaboration_phaseel archivo entero, 36 líneas
// The same test as ../agents, with two callbacks hung on the driver.
//
// Note what this class does NOT touch: not the env, not the agent, not the
// driver, not the sequence. That is the whole promise of the third hook -- and
// it is the same promise as the factory override, one level lower: the override
// swaps a class, the callback changes what one class does at one point.
class inject_test extends dual_test;
`uvm_component_utils(inject_test)
flip_bit_cb flip_h;
jitter_cb jitter_h;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
// end_of_elaboration and not build: the driver is a grandchild of the env,
// and build_phase runs top-down -- when this test's build_phase returns, the
// driver does not exist yet.
function void end_of_elaboration_phase(uvm_phase phase);
super.end_of_elaboration_phase(phase);
flip_h = flip_bit_cb::type_id::create("flip_h");
jitter_h = jitter_cb::type_id::create("jitter_h");
// Both on the same driver, and both run: the queue is per component
// instance, in insertion order. The passive agent has no driver, so there
// is nothing to hook there.
uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, jitter_h);
uvm_callbacks #(driver, driver_callback)::add(env_h.clase_agent_h.driver_h, flip_h);
if ($test$plusargs("CALLBACK_TRACE"))
uvm_callbacks #(driver, driver_callback)::display(env_h.clase_agent_h.driver_h);
endfunction : end_of_elaboration_phase
endclass : inject_test- The test does not touch the env, nor the agent, nor the driver, nor the sequence. Just like the override, one level further down
end_of_elaboration_phaseand notbuild_phase: the driver is a grandchild of the env, and when thebuild_phaseof the test finishes it does not exist yet- The two callbacks go on the same instance and both run, in the order in which they were added
bash run.sh +CALLBACK_TRACE prints the queue of the driver
The phase trap is the one that takes somebody's whole afternoon: uvm_callbacks::add
needs the handle of the component, and the tree is built top
down. In the build_phase of the test, env_h exists —it has just created it— but
env_h.clase_agent_h is null. end_of_elaboration_phase runs with the whole tree
assembled, which is exactly what is needed.
The alternative that is sometimes seen is add_by_name("*driver_h", cb, this), with a
pattern instead of a handle. It is useful when you do not want to go across the hierarchy, and
it has the same phase problem.
And the passive agent does not show up in either of the two lines for a reason that is obvious
once said: it has no driver. Callbacks hang off instances, not off
types, so "the driver of the active agent" is a concrete address in the
tree.
Callbacks
The flipped bit the scoreboard does not see
cd code/u7/callbacks && bash run.sh
- The
flip_bit_cbflips a bit ofAin one out of every eight transactions. The scoreboard says nothing, and both runs close at 0UVM_ERROR - It is not a hole in the checker: it is the proof that the monitor watches the wire, not what the driver thought it was going to send
- A monitor that rebuilt the transaction from the driver —or that copied it from the sequencer— would have given PASS on data the DUT never saw
- What can be seen of the
jitter_cbis the time: same stimulus, more cycles
| The hook | Changes | How many at a time |
|---|---|---|
| Constraint | the values | as many as you like |
| Factory override | the whole class | one |
| Callback | what a class does at one point | as many as you like |
This is the slide of the half section, and the exercise of thinking is worth more than the
code: "if I inject an error and the scoreboard does not scream, is the scoreboard wrong?"
Let the group argue about it for a minute before answering.
The answer is no, and it is one of the most important things in the course: the
scoreboard predicts from what the monitor saw on the bus. If the driver
sends A^1, the DUT computes with A^1, the monitor sees A^1 and the prediction gives
A^1. All consistent. The only testbench this callback would break is
one with a monitor that does not watch the signals — and that testbench was wrong before
the callback ever existed.
Out of that comes the rule we already saw in Agents and that now has a demonstration:
the monitor watches the wire, always. An injection callback is, besides a
tool, the cheapest test there is for finding out whether your monitor does it.
To inject an error the scoreboard does have to catch, you have to break
the DUT, not the stimulus — which is exactly what the +BUG=1 of the capstone does.
Sequences
The only thing left hard-wired
- The agents left the testbench encapsulated: one
agentper interface, the analysis in theenv, and nobody naming somebody else's components - Except for one line, the one in the
run_phaseof the test:
seq.start(env_h.clase_agent_h.sequencer_h);
- The test goes across the hierarchy to reach the sequencer, and along the way it gets to know
the inside of the
envwe have just closed - And there is something bigger behind it: that
command_sequenceis one class doing three things — resetting, sending a thousand random operations and one directed - This section is about what happens inside
body(), and about how a sequence gets started without wiring it
The sentence that orders the whole section: stimulus is not structure.
A component is built once, before the simulation starts, and it
stays there until the end. Stimulus changes from test to test, and sometimes it changes
inside the same test. Transactions, agents and sequences are the same operation repeated
on three different things: separating the data (transaction), separating the
structure (agent), separating the stimulus (sequence).
The question to throw at the group before going on: with the command_sequence of the
agents, how many classes are needed to try three stimuli and their combinations?
Six, and it grows factorially. In the UVM Primer that is called explosion of tester
classes.
Sequences
An object, not a component
- The
uvm_sequenceris auvm_component: it is in the tree, it has a parent, it has phases, thebuild_phaseof the agent builds it - The
uvm_sequenceis auvm_object: it is not in the tree, it has no parent, it has no phases, and it gets created with a single argument — just like the transactions - Practical consequence: it gets created, runs, finishes and is thrown away. You can run another one right after, or two at once, without touching the structure
There is a detail here that confuses and is worth getting ahead of, because they are going to see it in the
log: even though the sequence is not in the tree, UVM reports it hanging off the
sequencer, with an @@ in the middle:
uvm_test_top.env_h.clase_agent_h.sequencer_h@@full_seq.random_seq
That does NOT mean it is a child of the sequencer. The @@ is precisely the mark that
what follows is a sequence and not a component: to the left, the path of the
sequencer it runs on; to the right, the path of nested sequences. If
somebody looks for sequencer_h.full_seq with uvm_root::get().find() they are never going to
find it.
The other consequence, the one used every day: since it is not a component, a
sequence can have configurable fields that get changed between the create()
and the start(). That is what full_seq.count = 200 does at the end of the
unit, and with a component it could not be done: by the time you want to change it, the
build_phase has already gone by.
Sequences
The smallest sequence there is
code/u7/sequences/tb_classes/reset_sequence.svh// The smallest sequence there is: a single item.
//
// It is a uvm_object, not a uvm_component: it is not in the tree, it has no
// parent, it has no phases. It gets created, it runs, and it gets thrown away.
class reset_sequence extends uvm_sequence #(command_transaction);
// `uvm_object_utils, NOT `uvm_component_utils: the constructor takes a single
// argument and there is no parent to pass.
`uvm_object_utils(reset_sequence)
function new(string name = "reset_sequence");
super.new(name);
endfunction : new
// body() is a TASK, not a function: it blocks inside. Nobody calls it by
// hand; UVM calls it when somebody starts the sequence.
task body();
command_transaction command;
command = command_transaction::type_id::create("command");
start_item(command); // blocks until the sequencer gives us our turn
command.op = rst_op; // only now do we decide what to send
finish_item(command); // blocks until the driver calls item_done()
endtask : body
endclass : reset_sequence- It extends
uvm_sequence #(T), parameterized with the item it is going to send - It gets registered with
`uvm_object_utils— **not** with`uvm_component_utils - Constructor with a single argument:
name, withoutparent - All the work lives in
body(), which is a task: it can block - Nobody calls
body()by hand: UVM calls it when somebody starts the sequence
The two typing mistakes everybody makes the first time, and which give messages that do not help:
`uvm_component_utilsinstead of`uvm_object_utils. The error talks about a constructor with two arguments and nobody relates it to the macro.function body()instead oftask body(). The declaration compiles, and then thestart_item()inside does not, because a function cannot block. And a design one worth marking: thecommandis declared insidebody(), not as a field of the class. In the UVM Primer it is a field. If it is a field, two runs of the same sequence share the handle, and the day somebody starts the same sequence twice in parallel you have two threads writing the same object. And a protocol one, which is what makes this sequence exist: the first item of every test is a reset. The VTALU starts up withreset_nat 0 and never raisesdone; the driver stays in itswhile (bfm.done == 0)and the sequence, in the firstfinish_item(). There is no error: the simulation stops advancing. It is the same symptom as the forgottenitem_done(), with another cause, and it is worth showing it once in class by deleting thereset_sequencefromfull_sequence.
Sequences
The handshake, now from the sequence side
- The same four calls as the agents, looking at the other lane:
start_item()blocks the sequence until the sequencer gives it the turn, andfinish_item()blocks it until theitem_done()of the driver - Simulation time advances only in the amber bar: filling the item does not cost a clock edge
- Two threads, four calls and no
#delayin the middle
Confusion number one of the section: believing that finish_item() comes back when the
item was delivered. No: it comes back when the driver called item_done(), that is
when the operation finished in the DUT. It is the difference between "I sent it" and
"it is done", and it is what makes the Fibonacci sequence possible.
Out of that comes as well why the #500 that the tester of the transactions
had at the end disappeared: it was not stimulus, it was a patch so the objection would not drop
before its time. With finish_item() the stimulus no longer gets cut in half. Careful,
honesty: the last result can still end up uncompared, because the
objection gets dropped as soon as start() comes back and the result_monitor publishes one edge
later. It can be seen in the log of the add_test run with +UVM_VERBOSITY=UVM_HIGH: 1001
commands, 1000 comparisons.
If somebody asks about the non-blocking cousin: there is try_next_item(), which
comes back right away with null if the sequencer has nothing. It is the same
get() / try_get() pair from the communication between threads and it is useful when the protocol forces you to drive
the bus even when there is no stimulus — idle cycles, refresh of a DRAM. The VTALU does not
need it: between one operation and the next the signals can stay still.
Sequences
What happens between start_item() and finish_item()
code/u7/sequences/tb_classes/random_sequence.svh · #body task body();
command_transaction command;
repeat (count) begin : random_loop
// Through the factory, same as in the Transactions section: that is what lets
// add_test change the TYPE without touching a line of this sequence.
command = command_transaction::type_id::create("command");
start_item(command);
// Late randomization: the values are picked AFTER getting the
// sequencer's turn, not when the object was created.
if (!command.randomize())
`uvm_fatal("RANDOM SEQUENCE", "randomize() failed")
finish_item(command);
end : random_loop
endtask : bodyel archivo entero, 31 líneas
class random_sequence extends uvm_sequence #(command_transaction);
`uvm_object_utils(random_sequence)
// The length is a field, not a new class. Since the sequence is an object and
// not a component, whoever starts it can change it before start().
int unsigned count = 1000;
function new(string name = "random_sequence");
super.new(name);
endfunction : new
task body();
command_transaction command;
repeat (count) begin : random_loop
// Through the factory, same as in the Transactions section: that is what lets
// add_test change the TYPE without touching a line of this sequence.
command = command_transaction::type_id::create("command");
start_item(command);
// Late randomization: the values are picked AFTER getting the
// sequencer's turn, not when the object was created.
if (!command.randomize())
`uvm_fatal("RANDOM SEQUENCE", "randomize() failed")
finish_item(command);
end : random_loop
endtask : body
endclass : random_sequence- When
start_item()comes back, the sequence already has the turn of the sequencer: nobody else is going to beat it to the driver - Only then do the fields get filled, or the randomize happen. That is late randomization: the values get chosen at the last possible moment
- It is useful because by then it is already known what state the DUT is in, and a constraint can depend on that
randomize()still goes with itselse: it returns 0 and aborts nothing — it is the same thing they saw in the Constrained random unit
The honest question: "and what if I randomize before start_item()?". It works. On a
small testbench you do not notice the difference.
You notice it when the sequence has a thousand items queued up and a constraint depends on
something that changes during the run —an address that has already been written, a buffer
that has filled up, the previous result—. If you randomized everything at the beginning,
you decided with old information.
The other reason, more practical: between start_item() and finish_item() is where UVM
calls pre_do() and mid_do(), which are the hooks people use to
inject errors without touching the original sequence. If you fill the item before, those
hooks have nothing to do.
And the hook back to day 5: this is where the randomize() with {} of the coverage
closure goes. The directed case gets asked for at the point of use, without writing a new
class. The slide that follows is exactly that, with the Verilator trap included.
Sequences
Directed late randomization, and the Verilator hole
start_item(command);
command.data.constraint_mode(0); // 1. I switch off the dist
if (!command.randomize() with {A inside {[1:10]};}) // 2. I ask for the directed one
`uvm_fatal("SEQ", "randomize() failed")
finish_item(command);
- Measured with Verilator 5.052, on a class with the constraints of
command_transaction, 100 randomizations of each form:
with {op == mul_op} 100 of 100 op has no dist
with {A inside {[1:10]}} 1 of 100 A does have one
constraint_mode(0) + with {A inside ...} 100 of 100 the workaround
- The rule, in one line: if the
with {}touches a field that has adist, switch that constraint off first - It is not the language, it is the tool:
repro-dist-with.svincode/verilator/
It is the same limitation they already saw in the Constrained random unit, but here
it shows up in the place where they are really going to use it, so it is worth measuring it
again against the real transaction.
What the measurement adds on top of what docs/en/verilator.md says: the trigger is
not "there is a dist in the class". It is the with {} restricting a field that has a
dist. Asking for op == mul_op works perfectly, even though A and B have dist in the
same resolution. That is why the sequences of the section do not run into this: none of them
uses with {}.
And the workaround is not a concession: for a directed case, switching off the distribution
constraint is the right thing anyway. The dist is there so the random hits the
edges; if you already know which value you want, it has nothing to contribute.
constraint_mode(0) is per object, not per class, and the object gets thrown away after
finish_item(): there is no need to remember to switch it back on.
Sequences
The item comes back with the result inside
code/u7/sequences/tb_classes/driver.svh · #run_phase task run_phase(uvm_phase phase);
command_transaction command;
shortint unsigned alu_result;
forever begin : command_loop
seq_item_port.get_next_item(command); // blocks until there is an item
bfm.send_op(command.A, command.B, command.op, alu_result);
command.result = alu_result; // the way back
seq_item_port.item_done(); // unblocks the finish_item()
end : command_loop
endtask : run_phaseel archivo entero, 29 líneas
class driver extends uvm_driver #(command_transaction);
`uvm_component_utils(driver)
virtual vtalu_bfm bfm;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
vtalu_agent_config cfg;
// The BFM is not asked for on its own: ask for the agent config, and it comes from there.
if (!uvm_config_db#(vtalu_agent_config)::get(this, "", "config", cfg))
`uvm_fatal("DRIVER", "Failed to get agent config")
bfm = cfg.bfm;
endfunction : build_phase
task run_phase(uvm_phase phase);
command_transaction command;
shortint unsigned alu_result;
forever begin : command_loop
seq_item_port.get_next_item(command); // blocks until there is an item
bfm.send_op(command.A, command.B, command.op, alu_result);
command.result = alu_result; // the way back
seq_item_port.item_done(); // unblocks the finish_item()
end : command_loop
endtask : run_phase
endclass : driver- The driver writes
command.resultbefore callingitem_done() - The sequence still holds the handle to that same object: when
finish_item()comes back, the result is there - It is the way back, and that is why
command_transactiongains a fieldresultin this unit — the only change to the transaction since it was created - It breaks MOOCOW on purpose: the driver modifies an object it did not create. It is agreed between the two parties, and it is the only place in the testbench where it is done
This is the subtlest point of the section and it is worth saying it slowly: there is
no return channel at all. There is no port, there is no FIFO. There is a shared
handle, and the two parties agreed that the driver writes and the
sequence reads, and that the safe moment to read is after finish_item().
If somebody asks "wasn't the rule to clone?": yes, that is the rule of the transactions, and this is the documented exception. UVM has besides a formal
mechanism for this —the REQ/RSP pair of uvm_sequence #(REQ, RSP) with
put_response() on the driver side and get_response() on the sequence side—
that almost nobody uses, because writing the result into the request is enough in the
vast majority of cases. It is worth naming so they recognise it if they see it.
An implementation detail that does matter: the send_op reads bfm.result on the
same edge on which it sees done high. One edge later the DUT has already started the
next operation.
Sequences
The formal way back: get_response()
// The sequence declares TWO types, and the response is no longer the request
class fib_sequence extends uvm_sequence #(command_transaction, result_transaction);
start_item(cmd);
finish_item(cmd);
get_response(rsp); // blocks until the driver answers
// And the driver, on the other side:
seq_item_port.get_next_item(req);
rsp = result_transaction::type_id::create("rsp");
rsp.set_id_info(req); // without this the response does not find its sequence
seq_item_port.item_done(rsp); // or item_done() now and put_response(rsp) later
- The shared handle of the previous slide holds as long as the driver is blocking and serves one at a time. The course's one is
- A pipelined driver calls
item_done()as soon as it pushes the command and the response arrives N cycles later: by then the sequence has already sent another item, and writing intoreq.resultwrites to the wrong item set_id_info(req)copies the sequence and transaction id onto the response. It is what makesget_response()know who to answer- The other two cases where it is needed: a VIP that clones the item, and a protocol with more than one response per request
This is the half slide that avoids a poor answer in an interview. The
question sounds like "how do you give the data back to the sequence?", and answering "I
write the result field into the item" is right for this driver and fails on
any real bus. It is worth saying both halves together.
The reasoning to leave behind: the shared handle trick is not a
technique, it is a consequence of the driver being blocking. The condition is
written in the get_next_item()/item_done() of the previous slide — as long as the
item_done() is after having the response, there is a single item in flight and
the handle is enough. The day somebody moves that item_done() up to
gain throughput, the testbench keeps compiling and starts lying.
The other failure mode, more treacherous: a VIP that clones the item between the
sequence and the driver. There the driver writes into its copy, the sequence reads the
original, and the result that arrives is always the previous one. There is no error, there is no
warning: there is a scoreboard shifted by one.
And the set_id_info() detail, which is the one that makes this fail silently the
first time: if it is missing, the response is sent anyway and get_response() waits
forever. A test that "hangs after the first transaction" and uses REQ/RSP is this
bug until proven otherwise.
Sequences
Fibonacci: when the stimulus depends on the result
code/u7/sequences/tb_classes/fibonacci_sequence.svh · #body task body();
byte unsigned n_menos_2 = 0;
byte unsigned n_menos_1 = 1;
command_transaction command;
command = command_transaction::type_id::create("command");
start_item(command);
command.op = rst_op;
finish_item(command);
`uvm_info("FIBONACCI", "Fib(01) = 000", UVM_MEDIUM)
`uvm_info("FIBONACCI", "Fib(02) = 001", UVM_MEDIUM)
// Up to 14 and no further: Fib(14) = 233 and Fib(15) = 377, which does not
// fit in the 8 bits of A and B.
for (int ff = 3; ff <= 14; ff++) begin : fib_loop
command = command_transaction::type_id::create("command");
start_item(command);
command.A = n_menos_2;
command.B = n_menos_1;
command.op = add_op;
finish_item(command); // returns when the driver called item_done()
n_menos_2 = n_menos_1;
n_menos_1 = command.result; // the driver wrote it inside the item
`uvm_info("FIBONACCI", $sformatf("Fib(%02d) = %03d", ff, n_menos_1),
UVM_MEDIUM)
end : fib_loop
endtask : bodyel archivo entero, 41 líneas
// Fibonacci with the VTALU adder. Every addition needs the RESULT of the previous
// one: it is the case that forces reading the item back, and that with the tester
// of the Transactions section could not be written without giving the monitor a handle.
class fibonacci_sequence extends uvm_sequence #(command_transaction);
`uvm_object_utils(fibonacci_sequence)
function new(string name = "fibonacci_sequence");
super.new(name);
endfunction : new
task body();
byte unsigned n_menos_2 = 0;
byte unsigned n_menos_1 = 1;
command_transaction command;
command = command_transaction::type_id::create("command");
start_item(command);
command.op = rst_op;
finish_item(command);
`uvm_info("FIBONACCI", "Fib(01) = 000", UVM_MEDIUM)
`uvm_info("FIBONACCI", "Fib(02) = 001", UVM_MEDIUM)
// Up to 14 and no further: Fib(14) = 233 and Fib(15) = 377, which does not
// fit in the 8 bits of A and B.
for (int ff = 3; ff <= 14; ff++) begin : fib_loop
command = command_transaction::type_id::create("command");
start_item(command);
command.A = n_menos_2;
command.B = n_menos_1;
command.op = add_op;
finish_item(command); // returns when the driver called item_done()
n_menos_2 = n_menos_1;
n_menos_1 = command.result; // the driver wrote it inside the item
`uvm_info("FIBONACCI", $sformatf("Fib(%02d) = %03d", ff, n_menos_1),
UVM_MEDIUM)
end : fib_loop
endtask : body
endclass : fibonacci_sequence- Every addition needs the result of the previous one: without a way back this cannot be written
finish_item()comes back after the ALU has finished, socommand.resultis already valid- It is the same
env, the same agent, the same driver and the same BFM as the random test. Only the sequence changes
Fibonacci is not there to teach Fibonacci: it is there to force the case where the
stimulus N+1 depends on the result N. A test like that with the tester of the transactions
is impossible without giving the tester a handle to the monitor, that is without breaking
the separation we had been building.
Numbers for the blackboard: 0 1 1 2 3 5 8 13 21 34 55 89 144 233. It stops at 233
because the next one is 377 and A and B are 8 bits. Having the student see why the
for goes up to 14 and not up to 20 is worth more than the whole sequence.
And one that is seen in the real run: the whole test takes 500 units of
time, against the 43,000 of the random test. Thirteen operations. A well-written directed
test is cheap; what is expensive is the random, and that is why it runs at night.
Sequences
Sequences that call sequences
- A sequence can start others: it is the way of composing stimulus without duplicating code
- The
command_sequenceof the agents, split into three pieces that now get combined however you like - The sequencer arbitrates: whatever it receives, it hands the driver one item at a time
The idea of the slide is that a sequence is not a special layer: it is an object with
a body(), and inside a body() another sequence can be started just the way
the first one is started. There is no nesting limit nor a different class for
"sequence that calls sequences".
The practical consequence, which is the one that counts: stimulus gets composed like
functions. A small sequence —"write the four registers", "send a maximum
multiplication"— gets written once and afterwards goes into any
scenario. It is exactly what in the conventional testbench was done by copying and
pasting blocks of the tester.
The last bullet is the one that avoids the most common misunderstanding: that three sequences
are composed does not mean that their items get mixed on the bus. The
sequencer hands over one at a time, and if two sequences compete on the same
sequencer there is an arbitration policy deciding. With sequences nested
on a single sequencer, the order is the one the body() says.
Sequences
full_sequence: three pieces, one single sequencer
code/u7/sequences/tb_classes/full_sequence.svh · #body task body();
reset_sequence reset_seq;
random_sequence random_seq;
maxmult_sequence maxmult_seq;
reset_seq = reset_sequence::type_id::create("reset_seq");
random_seq = random_sequence::type_id::create("random_seq");
maxmult_seq = maxmult_sequence::type_id::create("maxmult_seq");
random_seq.count = count;
// get_sequencer() returns the sequencer start() handed us: the children run
// on the same one. The second argument declares them CHILDREN -- without it
// they compete with us in the arbitration instead of inheriting our turn.
reset_seq.start(get_sequencer(), this);
random_seq.start(get_sequencer(), this);
maxmult_seq.start(get_sequencer(), this);
endtask : bodyel archivo entero, 32 líneas
// The command_sequence of the agents section, split into three pieces that can now be
// combined at will. This sequence sends no item of its own: it only
// starts the other ones.
class full_sequence extends uvm_sequence #(command_transaction);
`uvm_object_utils(full_sequence)
int unsigned count = 1000;
function new(string name = "full_sequence");
super.new(name);
endfunction : new
task body();
reset_sequence reset_seq;
random_sequence random_seq;
maxmult_sequence maxmult_seq;
reset_seq = reset_sequence::type_id::create("reset_seq");
random_seq = random_sequence::type_id::create("random_seq");
maxmult_seq = maxmult_sequence::type_id::create("maxmult_seq");
random_seq.count = count;
// get_sequencer() returns the sequencer start() handed us: the children run
// on the same one. The second argument declares them CHILDREN -- without it
// they compete with us in the arbitration instead of inheriting our turn.
reset_seq.start(get_sequencer(), this);
random_seq.start(get_sequencer(), this);
maxmult_seq.start(get_sequencer(), this);
endtask : body
endclass : full_sequenceget_sequencer()returns the sequencer thatstart()handed to this sequence: the daughters run on the same one- The second argument,
this, declares them daughters. Without it they compete with the mother in the arbitration instead of inheriting her turn random_seq.count = countis the advantage of the sequence being an object: it gets configured between thecreate()and thestart()- To run two in parallel,
fork/joinaround thestart(), and the sequencer interleaves the items
get_sequencer() is the accessor of IEEE 1800.2; the field is called m_sequencer and
the UVM Primer uses it directly. Both forms work — the get_ one is the one worth
teaching, because m_ is the UVM convention for "this is internal".
And if somebody has already read production code they are going to ask about p_sequencer: it is a
typed handle to the sequencer, which shows up when you use
`uvm_declare_p_sequencer(mi_sequencer). It is only useful if you extended
uvm_sequencer to put something of your own in it, because m_sequencer is of type
uvm_sequencer_base and does not see those fields. Since in this course the sequencer is a
bare typedef, p_sequencer contributes nothing.
The second argument of the start() is the one almost nobody puts in and that almost always
matters: without a parent, if mother and daughter ask for the turn at the same time the sequencer
treats them as two independent sequences. With a parent, the daughter inherits priority and
context.
If they ask about the order with fork/join: the default arbitration is
UVM_SEQ_ARB_FIFO, that is order of arrival. It gets changed with set_arbitration() of the
sequencer, and there are six modes, one of them with weights.
Sequences
Starting a sequence, form 1: start()
code/u7/sequences/tb_classes/full_test.svh · #run_phase task run_phase(uvm_phase phase);
full_sequence full_seq;
full_seq = full_sequence::type_id::create("full_seq");
// THE TEST raises the objection, not the sequence: the sequence knows
// nothing about phases, and has to be able to run inside another sequence.
phase.raise_objection(this);
full_seq.start(sequencer_h); // does not return until body() finished
phase.drop_objection(this);
endtask : run_phaseel archivo entero, 20 líneas
// Same stimulus as the dual_test of the agents section, now in three sequences.
class full_test extends base_test;
`uvm_component_utils(full_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
task run_phase(uvm_phase phase);
full_sequence full_seq;
full_seq = full_sequence::type_id::create("full_seq");
// THE TEST raises the objection, not the sequence: the sequence knows
// nothing about phases, and has to be able to run inside another sequence.
phase.raise_objection(this);
full_seq.start(sequencer_h); // does not return until body() finished
phase.drop_objection(this);
endtask : run_phase
endclass : full_teststart(sequencer)hands the sequence the sequencer it is going to run on and does not come back untilbody()has finished- The test raises the objection before and drops it after: the run lasts exactly as long as the stimulus lasts
- It is explicit and reads top to bottom. For a test that runs a single sequence, it is what suits
- And it still has the problem of the first slide:
sequencer_hcame out ofenv_h.clase_agent_h.sequencer_h
start() does three things, in this order: it saves the sequencer (it is what
get_sequencer() returns afterwards), it calls pre_body() / body() / post_body(), and it
comes back.
The detail to underline: the objection is raised by the test, not by the
sequence. The sequence knows nothing about phases. That is on purpose — the same
sequence has to be able to run inside another sequence, where raising an
objection would make no sense, and in fact that is what full_sequence does with its
three daughters.
The sequencer_h is resolved by base_test in end_of_elaboration_phase, not in
build_phase: during the build_phase of the test the agent has not built its
sequencer yet. It is the same reason why the connections go in connect_phase.
Sequences
Form 2: default_sequence through uvm_config_db
code/u7/sequences/tb_classes/default_seq_test.svh · #build_phase function void build_phase(uvm_phase phase);
full_sequence full_seq;
// super.build_phase() of base_test, which is a class of OURS: it builds the
// env. What the course never calls is uvm_component's build_phase.
super.build_phase(phase);
full_seq = full_sequence::type_id::create("full_seq");
full_seq.count = 200;
// WITHOUT this line main_phase ends at t=0: nobody raises the objection,
// and a phase without an objection lasts zero. The test PASSES with no stimulus.
full_seq.set_automatic_phase_objection(1);
// The instance name carries the "_phase" suffix: that is the phase the
// sequencer will start it in. Notice the test no longer names the sequencer
// as a handle: it names it as a PATH, and that is a config, not code.
uvm_config_db#(uvm_sequence_base)::set(
this, "env_h.clase_agent_h.sequencer_h.main_phase", "default_sequence",
full_seq);
endfunction : build_phaseel archivo entero, 32 líneas
// The same stimulus as full_test, WITHOUT run_phase: the sequencer itself starts
// the sequence when the phase begins.
class default_seq_test extends base_test;
`uvm_component_utils(default_seq_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
full_sequence full_seq;
// super.build_phase() of base_test, which is a class of OURS: it builds the
// env. What the course never calls is uvm_component's build_phase.
super.build_phase(phase);
full_seq = full_sequence::type_id::create("full_seq");
full_seq.count = 200;
// WITHOUT this line main_phase ends at t=0: nobody raises the objection,
// and a phase without an objection lasts zero. The test PASSES with no stimulus.
full_seq.set_automatic_phase_objection(1);
// The instance name carries the "_phase" suffix: that is the phase the
// sequencer will start it in. Notice the test no longer names the sequencer
// as a handle: it names it as a PATH, and that is a config, not code.
uvm_config_db#(uvm_sequence_base)::set(
this, "env_h.clase_agent_h.sequencer_h.main_phase", "default_sequence",
full_seq);
endfunction : build_phase
endclass : default_seq_test- The test has no
run_phase: the sequencer starts the sequence on its own, when the phase begins - The instance name carries the suffix
_phase:"...sequencer_h.main_phase"means "in themain_phaseof that sequencer" - It can be configured per instance (as here, with
uvm_config_db #(uvm_sequence_base)) or per type (uvm_config_db #(uvm_object_wrapper)+get_type()). If both are there the one with the higher precedence wins, and at equal precedence the lastset() - The sequencer stopped being a handle and became a path: that is already configuration, not code
The trap, and it is one of the ones that give no warning. Without the line
set_automatic_phase_objection(1), nobody raises the objection of the main_phase.
And a phase without an objection finishes as soon as it starts.
What was tried with Verilator 5.052: the run finishes at t=0, prints the Report
Summary with 0 UVM_ERROR / 0 UVM_FATAL, and exits with code 0. That is: the test
PASSES without having sent a single piece of stimulus. It is in code/u7/sequences as
no_objection_test, so it can be run in class alongside the good one.
It is exactly the same kind of bug as the new() that eats the override of the transactions: it does not break, it lies. And in a regression of a thousand tests, a test that passes
in zero seconds is one nobody looks at.
Fine detail: main_phase is the one to use, not run_phase, but not because
run_phase does not work — uvm_task_phase::traverse calls
start_phase_sequence() on every task phase (base/uvm_task_phase.svh:121), so
"…sequencer_h.run_phase" works just as well. The reason is one of coexistence: the
run_phase runs in parallel with the schedule reset → configure → main → shutdown,
and hooking onto main_phase puts the sequence inside that schedule instead of
alongside it.
Sequences
When to use which
seq.start(sqr) |
default_sequence |
|
|---|---|---|
| Where it is written | run_phase of the test |
build_phase of the test, or the top |
| Objection | the test handles it | set_automatic_phase_objection(1) |
| It reads | top to bottom | you have to go looking for it |
| Several sequences in order | trivial | one per phase |
| Changing it without recompiling | no | yes, it is a config |
Knows the inside of the env |
yes, through the handle | no, only the path |
| Reusable third-party agent | not always possible | it is the intended mechanism |
- For the tests of the course:
start(), because it shows - For an agent you hand over to another team:
default_sequence, because it lets them change the stimulus without touching your code
In the industry both live side by side and the choice is almost always political, not
technical: start() when the one writing the test is the owner of the testbench,
default_sequence when the testbench belongs to somebody else.
A third way worth naming without developing: the same uvm_config_db from
the top module, with null as the context. There the stimulus gets chosen from the command
line and the testbench does not even get recompiled. That is how the big
regressions work.
And the row that matters for the arc of the section is the second to last: default_sequence
is what finishes closing the env that the agents encapsulated.
Sequences
The `uvm_do macro and why the course does not use it
`uvm_do(command) // one line
command = command_transaction::type_id::create("command"); // four
start_item(command);
if (!command.randomize()) `uvm_fatal("SEQ", "randomize() failed")
finish_item(command);
`uvm_dodoes exactly those four things: create,start_item, randomize,finish_item- What it hides: the
randomize()without an else, so a constraint without a solution goes by in silence - What it prevents: filling fields by hand between the two calls — that is, everything on the previous slides
- What it breaks: the error message points at the expanded macro, not at your code
- There is a whole family (
`uvm_do_with,`uvm_create,`uvm_send) and the same argument holds for all of them
It is the same discussion as the `uvm_field_* macros of the transactions, and the
same conclusion: they save typing and charge you in debug.
It is worth being fair: `uvm_do_with is genuinely convenient for a one-line inline
constraint, and you are going to see it in every testbench in the world. You have to
know how to read it. What the course does not do is teach it first: whoever learns with
the macro does not know what happens between start_item() and finish_item(), and that is the whole
section.
A rule of thumb for work: read them all, write the explicit ones.
Sequences
The override still works, and no sequence found out
code/u7/sequences/tb_classes/add_test.svh// The same add_test as the Transactions section, word for word. No sequence finds
// out: random_sequence still asks the factory for a command_transaction,
// and the factory hands it back an add_transaction.
class add_test extends full_test;
`uvm_component_utils(add_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction : new
function void build_phase(uvm_phase phase);
command_transaction::type_id::set_type_override(add_transaction::get_type());
super.build_phase(phase);
endfunction : build_phase
endclass : add_test- It is the same
add_testas the transactions, word for word random_sequencestill createscommand_transaction::type_id::create(); the factory hands it back anadd_transaction- Measured result: 1000 operations, all
add_op, 0UVM_ERROR maxmult_sequencestill sendsmul_opunderadd_test— because it does not randomize, and the constraint only acts onrandomize()
It is the best proof that the separation worked: changing the data type did not
require touching the stimulus, and changing the stimulus does not require touching the data
type. Two independent axes, which in the transactions were stuck together and in the agents
still shared a class.
And the last bullet is the review of the trap of the transactions, now in a place
where it can be seen running: the factory picks the TYPE, the constraints only act on
randomize().
Sequences
Two sequences on the same sequencer: the arbitration
- Up to here every sequencer has had one sequence at a time. In a real testbench there are several in parallel on the same driver —background traffic, a directed test, an error injector— and somebody has to hand out the turns
fork
fondo_seq.start(sqr); // priority 100, the default one
urgente_seq.start(sqr, null, 500); // the third argument is the priority
join
The mode, in sqr.set_arbitration(...) |
How it hands out |
|---|---|
UVM_SEQ_ARB_FIFO |
order of arrival, and the priority is not looked at (default) |
UVM_SEQ_ARB_STRICT_FIFO |
the priority rules; at equal priority, order of arrival |
UVM_SEQ_ARB_WEIGHTED |
at random, with the priority as the weight |
UVM_SEQ_ARB_RANDOM |
at random, all equal |
UVM_SEQ_ARB_STRICT_RANDOM |
at random, but only among the highest priority ones |
UVM_SEQ_ARB_USER |
user_priority_arbitration(), you write it |
- The default ignores the priority: putting a 500 in and not changing the mode is the mistake of this slide, and nobody gives you a warning
The question that orders the slide is "who decides which item goes into the driver?",
and until today the answer was "there is nobody to decide about". The sequencer arbitrates
per item, not per sequence: two sequences in a fork do not take turns in blocks,
they interleave item by item. It is worth saying because intuition says the opposite.
The mistake to leave engraved is in the last bullet and it is out of a manual: the
priority gets written, it gets run, and nothing happens — because UVM_SEQ_ARB_FIFO does not
look at it. Priority without set_arbitration(UVM_SEQ_ARB_STRICT_FIFO) is an
expensive comment.
UVM_SEQ_ARB_STRICT_FIFO is the one used 90 % of the time, and the name
confuses: strict is about the priority, not about the FIFO. It first orders by
priority and only within each level does it respect the order of arrival.
And the one almost nobody uses but is worth naming, because it explains what
UVM_SEQ_ARB_USER exists for: there are protocols where the turn depends on the state of the DUT —do
not send a write if the FIFO of the DUT is full—. That is not a priority, it is
a function, and that is what the user mode is for. The cousin of that idea is
is_relevant(), which keeps a sequence out of the arbitration until it
says itself that it is ready.
Sequences
lock() and grab(): when the arbitration is not enough
- There are scenarios that cannot be interleaved: a configuration sequence, an atomic burst, a read-modify-write. If another sequence slips an item in the middle, the scenario stopped being the one you wrote
task body();
grab(); // cuts ahead of everyone and closes the door
... the items of the scenario, with nobody in between ...
unlock(); // ungrab() is the same method, under another name
endtask
lock()queues up: it waits its turn like anybody else and only then closes the door.grab()cuts in: it goes ahead of everyone, even of the higher priority ones- Both are released with
unlock(). Without theunlock(), if the sequence is still alive the sequencer stays closed forever and without a single error; if itsbody()finishes, UVM takes it away and shoutsSEQFINERR - The rule:
grab()for reset and error recovery —what cannot wait—,lock()for everything else, and theunlock()in the samebody()
The difference between the two is one single word and it is worth saying it like this: lock is
polite, grab is rude. Both end up with the sequencer all to yourself; what
changes is whether you wait your turn or take it.
When it is really needed, because abusing this is a classic: only when the
scenario loses its meaning if something gets interleaved. A read-modify-write on a
register is the canonical example —if another transaction goes by between the read and the write,
the value you write is old—. Sending ten items in a row does not
need a lock: a sequence already guarantees that.
The two endings of the missing unlock() have to be told together, because one
warns you and the other does not. If the sequence holding the lock finishes its
body(), uvm_sequencer_base::remove_sequence_from_queues
(seq/uvm_sequencer_base.svh:1258-1267) takes the lock away from it and reports a
UVM_ERROR SEQFINERR — "should not finish before locks … are removed": annoying, but
with a name on it. The silent case is the other one: the sequence is still alive and
blocked on something else, and there the sequencer stays closed without anybody
saying a word. That is the one that sends you looking on the wrong side: the
simulation does not advance and the +UVM_OBJECTION_TRACE says nothing odd, because
the objection is fine — the one that is stuck is the driver, waiting for an item that
is never going to arrive. The tool that shows it is +UVM_TIMEOUT and then reading
who has the lock.
And the detail that saves an afternoon: grab() does not interrupt the item that is in
flight. It cuts in at the next arbitration, not in the middle of a handshake — which
is exactly what one wants.
Sequences
The testbench, complete
- It is the diagram drawn today in any verification project, with the names used today
- Everything inside the
envis built once and does not change. What changes between one test and another is the cloud of sequences at the top
The diagram shows one agent, which is the unit of reuse; in code/u7/sequences there are
two, the active one and the passive one of the agents, and the bottom one is identical except that
it has neither sequencer nor driver.
It is worth doing the count out loud: the conventional testbench was an initial
of a hundred lines that drove signals. This one has test, env, agent, sequencer,
driver, two monitors, coverage and scoreboard, and every piece can be replaced
without touching the others. And the three layers at the top —sequences, agent, analysis—
change for different reasons: the stimulus changes per test, the structure per
design, the analysis per verification plan.
Sequences
Summary of the unit · the sequence and the item
- The stimulus came out of the component tree: it is a
uvm_object, not auvm_component, and that is why it gets created, configured, run and thrown away - Everything happens inside
body(), betweenstart_item()andfinish_item(): that is where the item gets filled, that is where therandomize()goes, and that is wherepre_doandmid_dohook on - The driver writes the result inside the item before
item_done(): it is the way back, and it is what makes a stimulus that reacts possible - Two ways of starting: explicit
start(sequencer), ordefault_sequencethroughuvm_config_db— which besides takes the last hard-wired line out of the testbench
The first half of the summary is the mechanism: what a sequence is, where the
randomize() lives, and which way the result comes back. Anyone who got lost in
the unit catches up here.
Sequences
Summary of the unit · how they compose
- Sequences compose: one calls others, in series or with
fork/join - And when there is more than one sequencer, the one that composes is a virtual sequence, which comes just as day 7 starts ↪ day 7
- With two sequences on the same sequencer the one who hands out is the sequencer:
priority and
set_arbitration(...), andlock()/grab()for the scenario that cannot be interleaved - With this the testbench of the course is complete —it is missing one single piece, tomorrow's— and it is the way it is written today in the industry
Close of day 6. None of the pieces we did see is magic: we built them all by hand —the
observer of talking to several objects, the FIFO of put and get, the tester of
transactions— before UVM
handed them to us done. That is the reason the course goes in this order and does not start
with the uvm_agent.
And the best place to go on reading is code/.uvm/src/: by this point
uvm_sequence_base.svh can be opened and understood.
What is left for tomorrow is the virtual sequence, and it is left out of today
on purpose: it is only understood with this unit's testbench closed, and the
morning of day 7 is where the student looks at a new DUT and has to decide which
structure it deserves.
Review · Day 6
1 of 10 · is_active
An agent in UVM_PASSIVE, what does its build_phase build?
- The monitors and the analysis ports; the driver and the sequencer, no
- Nothing: a passive agent is an empty shell
- Everything the same as an active one, but without connecting the driver to the sequencer
- Only the sequencer, so it can receive sequences from another agent of the same env
The monitors, always; the sequencer and the driver are left at
null— watching is never optional: a passive agent goes on feeding scoreboard and coverage. What gets skipped is what drives the interface, because there somebody else is already driving it.
Review · Day 6
2 of 10 · The handshake of the driver
The driver calls get_next_item(), drives the signals and forgets the item_done(). What happens?
- Compilation error: UVM demands the complete pair
- The sequencer hands over the next item just the same, with a warning
- The item gets discarded and the scoreboard reports a mismatch on the next comparison
- The sequence stays waiting in
finish_item()and does not advance any more
It hangs inside
finish_item(), and without saying anything — it is the classic mistake of the first week, and the symptom misleads: there is no error and no warning, time stops advancing and the objection never gets dropped.get_next_item()is a loan;item_done()is giving it back.
Review · Day 6
3 of 10 · Scope of the config_db
The env instantiates two agents and does both set() with the scope "*". What does each one receive?
- Both fail with
uvm_fatal: the string"config"is duplicated - Each one receives its own, in order of creation
- Both receive the same one: the second
set()overwrites the first - The first receives its config and the second is left with
cfg == null
Both receive the same thing — the
uvm_config_dbdoes not match by order nor by type: it matches by path. With"*"both entries describe the same components, so the last one wins. The scope is a path in the tree, not a label.
Review · Day 6
4 of 10 · The sequencer
typedef uvm_sequencer #(command_transaction) sequencer; compiles without touching the transaction of the transactions. Why?
- Because it is registered in the factory with
`uvm_object_utils - Because
uvm_sequenceraccepts anyuvm_object - Because the driver does the
$castinternally - Because
command_transactionextendsuvm_sequence_item
Because of the base class we chose in transactions —
uvm_sequencer #(T)requires thatTderive fromuvm_sequence_item. If that day the transaction had extended a plainuvm_transaction, thistypedefwould not compile today. A decision of one section enabling the next.
Review · Day 6
5 of 10 · super.build_phase()
Instead of the config object, you put is_active straight into the uvm_config_db. In this course the agent starts up active all the same. Why?
- Because
is_activeisprotectedand theconfig_dbcannot write it - Because the
config_dbdoes not accept enumerated types, onlyintandstring - Because
is_activeis fixed in the constructor andbuild_phasearrives late - Because the one who reads it is
uvm_agent::build_phase, and nobody callssuper
That mechanism is switched off —
uvm_agent::build_phaselooks foris_activein the resource pool (it is incode/.uvm/src/comps/uvm_agent.svh, it can be opened). Withoutsuper.build_phase()that line never runs, and nobody warns you. Both ways are valid; what does not work is half of each one.
Review · Day 6
6 of 10 · Object, not component
What is the practical difference of a uvm_sequence being a uvm_object and not a uvm_component?
- That it gets created, runs and is thrown away
- That it cannot be registered in the factory nor overridden
- That it cannot have
randfields nor constraints - That UVM builds it in
build_phase, like any other class of the tree
It gets created, runs and is thrown away — you can start several, one after the other, on the same sequencer. A component is built once in
build_phaseand lives to the end. That is why the stimulus cannot be a component: it changes from test to test, and sometimes within the same test. Out of that comes as well that it can be configured between thecreate()and thestart(), the wayfull_seq.count = 200does.
Review · Day 6
7 of 10 · start_item()
start_item(command) has just come back. What is it that this guarantees?
- That the driver has already received the item and is driving the signals
- That the sequencer gave the turn to this sequence
- That the
randomize()has already been resolved with the constraints of the class - That the objection of the phase is already raised by the sequencer
You have the turn —nobody else is going to beat you to the driver— and you have not handed anything over yet — and that is why the
randomize()goes after: it is the last possible moment to choose the values, when you already know what state the DUT is in. That is late randomization, and it is wherepre_do()andmid_do()hook on.
Review · Day 6
8 of 10 · The way back
At what moment does command.result have a value that can be read?
- When
finish_item()came back, because the driver wrote it before callingitem_done() - As soon as
start_item()came back - When the
result_monitorpublishes it through its analysis port - Never: to receive a response you have to use the REQ/RSP pair of
uvm_sequence #(REQ, RSP)
After
finish_item()— there is no return channel at all: there is a shared handle and an agreement between the two parties. The REQ/RSP pair exists and is the formal mechanism, but almost nobody uses it: writing the result into the request is enough. This is what makes Fibonacci possible.
Review · Day 6
9 of 10 · default_sequence
You configure a default_sequence through uvm_config_db and forget the set_automatic_phase_objection(1). What happens?
-
uvm_fatalinbuild_phase: the sequence does not find the sequencer - It runs just the same: when there is a
default_sequence, the sequencer raises the objection on its own - The
main_phasefinishes at t=0 and the test passes with 0 errors - The simulation hangs waiting for an objection nobody drops
It passes in zero seconds without sending a single piece of stimulus, and it lies — a phase without an objection finishes as soon as it starts. It is the same kind of bug as the
new()that eats the override: it does not break, it deceives. And in a regression of a thousand tests, the one that passes in zero seconds is one nobody looks at.
Review · Day 6
10 of 10 · Sub-sequences
full_sequence starts its daughters with reset_seq.start(get_sequencer(), this). What is the second argument for?
- To pass it the sequencer, because
get_sequencer()only returns the type - To declare the daughter a sub-sequence of the mother
- So the daughter runs in a separate thread, in parallel with the mother
- To register the daughter in the factory under the name of the mother, so it can be overridden
It declares the mother-daughter relationship — without it, the sequencer treats the two as independent sequences and they compete for the turn. With it, the daughter inherits the mother's turn and her priority in the arbitration. And for the parallel that is not enough: a
fork/joinaround thestart()is needed.
Exercise · Day 6 · 1 of 3
The agent that only watches
cd code/ejercicios/d6-agents && bash run.sh
- The
envhas a single agent. Add the passive one on the second VTALU - And make
is_activeactually count: today the agent builds driver and sequencer always - It is graded with
+UVM_CONFIG_DB_TRACE: the census of the tree is written by UVM, not by you
The exercise puts together the three things of the section that can be done wrong without the
compiler saying anything: is_active, the hierarchical scope of the set(), and the
connect_phase of the analysis ports.
The trap almost everybody is going to find is the scope: with "*" in both
set(), the second overwrites the first and both agents end up with the same
config — same BFM and same is_active. The
checker catches it by crossing the counts — the module sends 200 operations and the
sequence more than a thousand, so if both agents see similar numbers it means they
are watching the same interface.
And the shortcut of putting both in UVM_PASSIVE so it "passes" does not work either:
the checker demands that clase_agent_h still have a driver.
Exercise · Day 6 · 2 of 3
A sequence that only multiplies
cd code/ejercicios/d6-sequences && bash run.sh
- Write
mult_sequenceand themult_testthat starts it - The driver, the agent and the
envare already there: adding stimulus does not touch the structure, which is precisely what the section came to demonstrate - It is graded crosswise against what the
chequeosees on the bus, not against your count
It is the same exercise as day 2 and day 3, for the third time and with the final
tool: "I want it to only multiply". On day 2 it was solved by adding virtual,
on day 3 with a factory override, and today by writing twenty lines of
body() without touching a single class of the testbench. It is worth having them compare the
three: that is the whole story of the course in one exercise.
The trap they are going to find is the first one of the section: if the sequence does not
send a rst_op as the first item, the VTALU starts up with reset_n at 0, done
never rises and the driver stays in the first finish_item(). The run.sh has
a +UVM_TIMEOUT set so that finishes with a message instead of hanging.
Exercise · Day 6 · 3 of 3
Three planted bugs, and none of them looks like the others
cd code/ejercicios/d6-debug && bash run.sh
| File | How it shows up | |
|---|---|---|
| 1 | driver.svh |
it hangs: it dies on [PH_TIMEOUT] |
| 2 | default_seq_test.svh |
it ends at t=0 and says PASS |
| 3 | random_sequence.svh |
it lies in green: add_test passes with the wrong stimulus |
- The checker says which of the three is still broken. Which line it is, is the exercise
It is the only exercise of the course where there is nothing to write: there is
something to find. And it is the one that looks most like your first month on the
job, where nobody hands you a file with a hole — they hand you a testbench that
does not work.
The idea that orders the three rows, and worth saying before letting them go:
the failure modes of a UVM testbench do not look like each other. The one
that hangs shows up at once and costs nothing. The other two pass in green,
and those are the ones that cost weeks.
All three are in the debug appendix table, and reading it is allowed: the list
exists precisely so that the second time it takes them five minutes.
The third one deserves its own comment because it breaks the reflex the course
has been training: +TOPOLOGY is the right tool for *"the tree is not the one
you drew"*… and here it is not enough, because what got built wrong is a
uvm_object and the tree only shows uvm_component. The only way to see it is
to look at what went out on the bus.