What is the best way to make register write/read sequences generic?

Hi all

Here is my first time on the forum! Thanks in advance for the help…

I’m currently busy with the integration of a register model into our UVM testbench.
I’ve been following the cookbook to understand how to do it the UVM way.

Now I’m at the step where I need to create the register write sequences that will be called from my test.

In the cookbook I see that each sequence is in a way “specialized” to a certain function.
The first example in the cookbook is as follows:

// Slave Unselect setup sequence
//
// Writes 0 to the slave select register
//
class slave_unselect_seq extends spi_bus_base_seq;
 
`uvm_object_utils(slave_unselect_seq)
 
function new(string name = "slave_unselect_seq");
  super.new(name);
endfunction
 
task body;
  super.body();
  spi_rm.ss_reg.write(status, 32'h0, .parent(this));
endtask: body
 
endclass: slave_unselect_seq

So this sequence is dedicated to a write of a value 0 to the ss_reg.

Looking at the other examples it seems that all of them have likewise dedicated functions.

It somehow means that if I want to have completely independent register writes (not combining different register writes in a single sequence) I should have write a sequence per register.

But I’d like to find a way to do that “generic”. No dedication to a specific function. Just from my test being able to write any value to any register without calling always a different sequence.

So we come to my question, what’s the best way to make it generic? I saw 2 possible solutions:

  1. create a generic sequence where we have a member of the class that would be a handle to a uvm_reg (X) and another one the value I need to write (Y). I would then simply do X.write(.,Y,.) in the sequence body.
  2. I could also imagine that instead of passing a handle I would pass a string and do a big case statement in the sequence (we might be doing that to connect to some specific APIs where a string is more convenient). The big case would be auto generated by the scripts that are parsing our register description file.

I might miss another obvious solution… so that’s why I come to you for help!

Thanks for the feedback!

In reply to jeanphi500:

Reading back my question… I would also want to know how this is typically handled.
I guess almost everyone had to work this question out.

If the sequence per register write is the way it usually is done, then I’ll probably try that way too :)

I don’t pretend to speak for the whole community in saying how it’s done, but I’m happy to share my thoughts. I’m more of an e user, so that might have colored my thinking somewhat, but I’m pretty sure the concepts apply to SV UVM as well.

You don’t want to have a sequence per register. It doesn’t make sense, since you don’t want to reimplement the functionality of uvm_reg’s read(…) and write(…) methods. What you want to have, is a sequence for each individual “task” you want to perform.

Let’s go back to the example you gave us from the cookbook. Concentrate on the sequence’s name which tells you that after you run this sequence, your DUT will unselect all slaves. The fact that it does it by writing a “0” to the select register is secondary. It could have just as well required 3 different writes to 3 different registers. What’s important is the “what” and not the “how”.

A typical task you want to do with a DUT is configure it in a certain way. Out of the multitude of configuration options you want to take just one:


class config_seq extends dut_reg_sequence;
  // configuration knobs your DUT has
  rand mode_t mode;
  rand op_t operation;
  // ...

  task body;
    super.body();
    // convert from the multitude of config knobs to register writes
    // ...
    some_reg.write(...);
    some_other_reg.write(...);
    // ...
  endtask: body
  
endclass

This sequence provides you an abstract view of your DUT. You know you can configure it certain ways, but as a test writer you might not care exactly how this happens (i.e. which registers get written). Moreover, should the configuration fields jump around to different registers, the sequences that use this sequence won’t care, because all of these details are encapsulated here. You’ll just need to update this sequence.

Another examples of a task for a communication device is transmitting a packet. What you typically care about is just giving such a sequence a data word and let it handle writing the appropriate registers in the appropriate order with the right timings:


class tx_seq extends dut_reg_sequence;
  rand byte data[];
  
  task body;
    super.body();
    foreach (data[i]) begin
      // poll some status register until the buffer is available
      // (this can be an own register sequence itself)
      // ...
      
      tx_buf.write(data[i]);
    end
  endtask: body
endclass

I am not familiar with the uvm reg layer, but if you want to do what you describe, cant you call

spi_rm.ss_reg.write(status, 32’h0, .parent(this));

directly from the test?