Hi uvm experts,
I am developing a UVM tb for a parameterized Digital FIR Filter. And would like some suggestions on how experienced UVM coders deal with parameterized DUT.
My I/Os are simple: clock, reset, sampled signal, FIR coefficients, and output signal. A quick example “pseudocode”
coefficient = {1,2,3,4} //4 taps
input = {1,1,1,1,...}
output = {1*1, 1*1+1*2, 1*1+1*2+1*3, 1*1+1*2+1*3+1*4,...} = {1,3,6,10,...}
However, the width and the number of taps should be parameterized.
I have pondered on how to implement a parameterized testbench, and have some questions and preliminary thoughts. I would like to raise them to the forum readers to help me see if they are appropriate. I am pretty new to UVM, so could be entirely off track.
1. dealing with parameterized drivers
Interfaces for parameterized DUT is hard to pass to systemverilog class and maintain using uvm_config_db, so I created a maximum footprint interface nested inside the real DUT interface. At the “uvm_test” level, I do uvm_config_db::set, to pass down the “cfg” variable with parameters.
interface dut_if #(inputWidth=8,outputWidth=10,coefWidth=7firLength=31) (input clk);
localparam adder_length = int'(firLength/2) + firLength%2;
logic reset;
logic [inputWidth-1:0] sample;
logic [outputWidth-1:0] result;
logic [coefWidth-1:0] coeff [adder_length-1:0];
max_width_if internal_if(.clk(clk)); // remember to take care of input clk!!!
assign reset = internal_if.reset;
assign sample = internal_if.sample[inputWidth-1:0];
genvar i;
generate
for(i=0;i<adder_length;i=i+1)
assign coeff[i] = internal_if.coeff[i][coefWidth-1:0];
endgenerate
assign internal_if.result = {{(`MAX_WIDTH-outputWidth){1'bz}},result[outputWidth-1:0]};
endinterface
and on the driver side, I do high impedance for the unused bits.
task run_phase(uvm_phase phase);
...
seq_item_port.get_next_item(req);
dut_max_width_vif0.coeff[1] = {{(`MAX_WIDTH-cfg.coef_width){1'bz}},req.coeff[1]};
dut_max_width_vif0.coeff[0] = {{(`MAX_WIDTH-cfg.coef_width){1'bz}},req.coeff[0]};
dut_max_width_vif0.sample = {{(`MAX_WIDTH-cfg.smp_width){1'bz}}, req.sample};
seq_item_port.item_done(req);
Is this a good coding style? the drivers/agents are seemingly reusable, but the test is not. For cases where I have a specific combination of coefficients that I desired to test, I can live with writing new tests. What if I just want a simple case where all inputs(signal samples + coefficients) are purely random? I cannot think of a way to make the test being reusable for different parameters.
2. variable lengths of transactions.
In the above case, the width of transaction entries change together with the DUT parameters. When I create a new sequencer, can I do something like:
sequencer = uvm_sequencer#(my_transaction#(10,10,3,4))::type_id::create("sequencer",this);
with a parameterized my_transaction class?
class my_transaction #(inputWidth,outputWidth,coefWidth,numOfTaps) extends uvm_sequence_items
Transactions never seems to be specifically instantiated from users perspective. Its type is passed to the sequencer, and the sequencer generates them 1 by 1. I am wondering if any methodology can use factory override to create different transactions??
3. deterministic sequences
For starters, we usually test on deterministic sequences instead pure random ones. If the DUT doesn’t work for deterministic ones, we probably don’t have to utilize UVM’s capability of randomizing with super elaborated test cases. To achieve that, I can only think of changing the sequences task body:
task body;
if(starting_phase!=null) starting_phase.raise_objection(this);
for (int i=1;i<=4;i++)
begin
req = my_transaction::type_id::create("req");
start_item(req);
if(!req.randomize())
`uvm_error("","Randomize failed");
// overwriting the randomized entry in req
req.sample = i;
finish_item(req);
end
if(starting_phase!=null) starting_phase.drop_objection(this);
endtask:body
But this requires me to rip out the sequence code for other sequences. Any suggestion to make the code more reusable?
thanks a lot in advance