This example examines a common situation where the same physical register may need to be accessed from 2 registers within the same address map. UVM does not support this scenario directly and so here we create 2 registers within the address map and see how we can keep one updated when the other is accessed. For clarity, the registers have RW and RO access modes, so when the RW register is written, it is necessary to ensure that the mirrored value of the RO register is also updated.
Register Access Flow and Callbacks
When a register is accessed, there are a number of callbacks that get executed - the diagram, below, shows the order of callback invocation for a register write operation. Note the relationship between the callbacks - when a register is accessed, certain callbacks are invoked, but since a register is comprised of fields, each field access is also associated with callbacks. The rightmost column represents the user definable callbacks - we will be using the post_predict callback in this example.
Each callback is invoked with a reference to the register transaction item that represents the register access, an object of type uvm_reg_itemwhich allows the code in the callback to respond to or modify the transaction.
We start by considering a very simple register model consisting of just 2 registers, a RW and an RO register:
class my_reg_Rb extends uvm_reg; `uvm_object_utils(my_reg_Rb) rand uvm_reg_field F1; function new(string name = "my_reg_type"); super.new(.name(name), .n_bits(8), .has_coverage(UVM_NO_COVERAGE)); endfunction virtual function void build(); F1 = uvm_reg_field::type_id::create("F1"); F1.configure(this, 8, 0, "RO", 0, 8'h0, 1, 0, 1); endfunction endclass
Note that even though this register is declared as "RO", there is no harm declaring the field as rand since the UVM code itself will set rand_mode(0) if the register access mode does not allow write access. A similar declaration is made for the RW register, declared as my_reg_Ra and the register model which extends uvm_reg_block contains the address map declaration to assign the physical addresses as well as to associate the registers with an interface:
class my_blk extends uvm_reg_block; `uvm_object_utils(my_blk) rand my_reg_Ra Ra; rand my_reg_Rb Rb; function new(string name = "my_blk_type"); super.new(.name(name), .has_coverage(UVM_NO_COVERAGE)); endfunction virtual function void build(); rw_reg_cb rw_reg_cb_h; this.default_map = create_map( .name ("default_map" ), .base_addr('h1000 ), .n_bytes (4 ), .endian (UVM_LITTLE_ENDIAN) ); // build and configure the registers Ra = my_reg_Ra::type_id::create("Ra", null, get_full_name()); Rb = my_reg_Rb::type_id::create("Rb", null, get_full_name()); Ra.build(); Rb.build(); Ra.configure(.blk_parent(this), .regfile_parent(null), .hdl_path("")); Rb.configure(.blk_parent(this), .regfile_parent(null), .hdl_path("")); default_map.add_reg(Ra, .rights("RW"), .offset('h0100)); default_map.add_reg(Rb, .rights("RO"), .offset('h0200)); ... endfunction endclass
We shall complete the register block declaration once we've considered how to create the callback and write the code for the post_predictmethod. Referring back to the diagram above, we see that the post_predict call back is invoked as part of the uvm_reg_cbs call back object. This means we need to take several steps:
- Create a class that extends from uvm_reg_cbs
- Overwrite the method post_predict
- Create a call back object in the register model
- Associate the callback with the RW register
The code for post_predict is quite simple - it gets called after the register it is associated with (RW) gets its predict executed, i.e. when its mirrored value is updated, so this is a good place to then update the other (RO) register's mirrored value.
class rw_reg_cb extends uvm_reg_cbs; `uvm_object_utils(rw_reg_cb) my_reg_Rb m_ro_reg_cb; ... virtual function void post_predict( input uvm_reg_field fld , input uvm_reg_data_t previous , inout uvm_reg_data_t value , input uvm_predict_e kind , input uvm_path_e path , input uvm_reg_map map ); if (kind != UVM_PREDICT_WRITE) return; if (!m_ro_reg_cb.F1.predict( .value (value), .be ( -1), .kind (UVM_PREDICT_READ), .path (path), .map (map) )) `uvm_error(get_name(), "Register predict failed") endfunction endclass
The call back invokes the predict method on the other register's field. If it had more than one field, predict would need to be called for each. So the effect here is to update the RO register.
The final step, then, is to add the callback to the register model and associate it with the RW register:
rw_reg_cb_h = rw_reg_cb::type_id::create("rw_reg_cb_h"); rw_reg_cb_h.m_ro_reg_cb = Rb; uvm_callbacks#(uvm_reg_field, rw_reg_cb)::add(Ra.F1, rw_reg_cb_h);
A simple sequence will allow us to test the registers:
... task body(); uvm_status_e status; uvm_reg_data_t data; if (!uvm_config_db#(my_blk)::get(get_sequencer(), "", "model", model)) `uvm_fatal("TESTSEQ", "Failed to get register model from config db") // RW access model.Ra.write(status, 'h5a, .parent(this)); assert(status == UVM_IS_OK) else `uvm_error("TESTSEQ", "Failed to access register (write)") // Registers Ra and Rb share the same physical register via // 2 addresses and with different access modes `uvm_info("CBACCESS", $sformatf("Ra = %0x, Rb = %0x", model.Ra.get(), model.Rb.get()), UVM_MEDIUM ); // RO access model.Rb.read(status, data, .parent(this)); assert(status == UVM_IS_OK) else `uvm_error("TESTSEQ", "Failed to access register (read)") if (data != 'h5a) `uvm_error( "TESTSEQ", $sformatf("Expected value = %0h, actual = %0h", 'h5a, data ) ) endtask ...
Working code for this tutorial can be found on EDA Playground.