You inherit a VIP whose sequences are written almost entirely in macros: uvm_do_with(req, {...}), uvm_do_on(sub, p_sequencer.apb_sqr), uvm_create followed by uvm_send. A test fails with an APB access to address 0, which no sequence asks for. There is no randomization error in the log, only one warning nobody had read.
To debug macro-based sequences, and to answer the interview question "what does uvm_do actually do?", you need to see through the macros to the calls underneath. This lesson expands each one, shows the IEEE 1800.2 forms, and gives the reasons many teams write the calls by hand.
A macro is text substitution: the preprocessor replaces uvm_do(item) with a block of code before the compiler sees anything. That block does exactly what you would write by hand: create the item through the factory, start_item, randomize, finish_item. For a sequence argument it creates the sequence and calls start(). The macro saves a few lines and hides several decisions, and the hidden decisions are where the bugs live.
Think of a pre-filled form letter. It is fast to send, but you cannot see what it says unless you open the template, and it says the same thing to everyone, including the paragraph you would have left out.
Macro (UVM 1.2 name) | For an item, the same as | For a sequence, the same as |
|---|---|---|
|
| the same: created, not started |
| create; | create; |
| as | as |
| as | as |
| as | as |
|
|
|
|
| randomize (with), then start |
Combinations such as uvm_do_on_pri_with exist too, one per mix of target, priority and constraints. Two details hide in every row: sub-sequences are started with call_pre_post set to 0, so their pre_body and post_body never run, and a failed randomize is only a warning.
In the IEEE 1800.2 library the family collapses to four macros with default arguments. This is uvm_do and the macro it uses, as the library defines them (comments removed):
// snippet: macros/uvm_sequence_defines.svh, IEEE 1800.2-2020 library
`define uvm_do(SEQ_OR_ITEM, SEQR=get_sequencer(), PRIORITY=-1, CONSTRAINTS={}) \
begin \
`uvm_create(SEQ_OR_ITEM, SEQR) \
`uvm_rand_send(SEQ_OR_ITEM, PRIORITY, CONSTRAINTS) \
end
`define uvm_rand_send(SEQ_OR_ITEM, PRIORITY=-1, CONSTRAINTS={}) \
begin \
uvm_sequence_base __seq; \
if ( SEQ_OR_ITEM.is_item() ) begin \
start_item(SEQ_OR_ITEM, PRIORITY);\
if ( ! SEQ_OR_ITEM.randomize() with CONSTRAINTS ) begin \
`uvm_warning("RNDFLD", "Randomization failed in uvm_rand_send action") \
end\
finish_item(SEQ_OR_ITEM, PRIORITY);\
end \
else if ( $cast( __seq, SEQ_OR_ITEM ) ) begin \
__seq.set_item_context(this,SEQ_OR_ITEM.get_sequencer()); \
if ( __seq.get_randomize_enabled() ) begin \
if ( ! SEQ_OR_ITEM.randomize() with CONSTRAINTS ) begin \
`uvm_warning("RNDFLD", "Randomization failed in uvm_rand_send action") \
end \
end \
__seq.start(__seq.get_sequencer(), this, PRIORITY, 0);\
end \
else begin \
`uvm_warning("NOT_SEQ_OR_ITEM", "Object passed uvm_rand_send appears to be neither a sequence or item." ) \
end \
endEvery claim in the table is visible here: late randomization between start_item and finish_item, the RNDFLD warning instead of a fatal, and the literal 0 for call_pre_post when a sequence is started. The one call the table does not cover, set_item_context(this, SEQR), records the parent sequence and the sequencer on the child before it is randomized, so the child's full name, its config_db scope and its factory path all hang under the parent.
// snippet: seq_lab_tb.sv, every form in one body
task body();
lab_item it;
lab_child_seq sub;
`uvm_do(it) // UVM 1.2 and 1800.2
`uvm_do_with(it, { addr == 8'h10; write == 1; }) // UVM 1.2; deprecated in 1800.2
`uvm_do(it, , , { addr == 8'h14; write == 0; }) // 1800.2 only: empty args take defaults
`uvm_create(it)
it.addr = 8'h0C; it.write = 1; it.data = 32'h1234;
`uvm_send(it) // no randomize: fields as assigned
`uvm_do(sub) // a sub-sequence: call_pre_post = 0
endtaskThe explicit equivalent of the uvm_do_with line is the pattern the DMA bench uses everywhere, with the return value checked:
// snippet: the uvm_do_with line, written out
it = lab_item::type_id::create("it");
start_item(it);
if (!it.randomize() with { addr == 8'h10; write == 1; })
`uvm_fatal("RAND", "randomize failed") // the macro only warns
finish_item(it);The lab sequence above (driver messages at UVM_MEDIUM):
UVM_INFO seq_lab_tb.sv(300) @ 35000: uvm_test_top.env.sqr@@mseq [MAC] uvm_do: WR a4=11d0b73a
UVM_INFO seq_lab_tb.sv(302) @ 55000: uvm_test_top.env.sqr@@mseq [MAC] uvm_do_with: WR 10=b41abcb1
UVM_INFO seq_lab_tb.sv(304) @ 75000: uvm_test_top.env.sqr@@mseq [MAC] uvm_do 1800.2: RD 14
UVM_INFO seq_lab_tb.sv(308) @ 95000: uvm_test_top.env.sqr@@mseq [MAC] create+send: WR 0c=00001234
UVM_INFO seq_lab_tb.sv(163) @ 95000: uvm_test_top.env.sqr@@mseq.sub [HOOK] sub.pre_start
UVM_INFO seq_lab_tb.sv(165) @ 95000: uvm_test_top.env.sqr@@mseq.sub [HOOK] sub.body
UVM_INFO seq_lab_tb.sv(167) @ 95000: uvm_test_top.env.sqr@@mseq.sub [HOOK] sub.post_startEach item form sends one transfer, 20 ns apart, exactly like the explicit calls.
create+send drives the assigned values: uvm_send does not randomize.
The sub-sequence started by uvm_do logs pre_start, body and post_start, but not pre_body or post_body: call_pre_post was 0.
Back to the opening. One line in the VIP read uvm_do_with(req, { addr == cfg.base + 8'h03; }), and someone had changed cfg.base so that the sum was misaligned. The item's word-alignment constraint made the solve impossible. The macro created a fresh item, whose fields were all zero, logged RNDFLD: Randomization failed in uvm_rand_send action as a warning, and sent it anyway: a read of address 0. The test failed far from the cause, on an unexpected read of CTRL.
The explicit code would have stopped at the line with a fatal and the item's state. That is the first of the reasons many teams ban the macros in new code:
A failed randomize is only a warning, and the item is sent anyway.
Sub-sequences skip `pre_body` and `post_body`, silently, because the macro passes call_pre_post = 0.
The target sequencer is implicit (get_sequencer()), which hides mistakes in virtual sequences.
You cannot step into them in most debuggers, and errors point at the macro line, not the failing call.
They are deprecated in IEEE 1800.2 except for four core forms, so a codebase that must build on both standards cannot use the extended forms.
Reading them remains essential: a great deal of production VIP still uses them.
uvm_do(cfg_seq) to start a configuration sub-sequence whose register writes are in pre_body. What happens, and what is the minimal fix?uvm_do starts sub-sequences with call_pre_post set to 0, so pre_body never runs and the registers are never written. The minimal fix is to move the writes into cfg_seq's body. Replacing the macro with cfg_seq.start(m_sequencer, this) also works, because call_pre_post then defaults to 1, but the sequence would still break the next time someone starts it with a macro.
uvm_create(it) followed by uvm_send(it), and uvm_do(it)?uvm_create plus uvm_send creates the item, lets you set fields by hand in between, and sends it without randomizing. uvm_do creates and randomizes it, then sends it. To set some fields by hand and randomize the rest, use uvm_create then uvm_rand_send_with, or, better, the explicit calls with randomize() with.
Only the one-argument forms that exist in both, such as uvm_do(x), uvm_create(x), uvm_send(x) and uvm_rand_send(x), and even the UVM 1.2 variants like uvm_do_with exist in 1800.2 only as deprecated wrappers. The 1800.2 multi-argument forms do not compile on UVM 1.2. The portable choice is the explicit start_item / randomize / finish_item and start() calls.
Next: with the mechanics in hand, the Virtual sequences topic coordinates several sequencers at once, from the DMA's register, stream and interrupt agents.