This UVM register model example puts every RAL piece together for one small design. It shows how a register model is written for the design, how it is placed in a register environment with an adapter and a predictor, and how a test uses it to write, read, check and update the design registers.

10 min read | Intermediate Level

What You'll Learn

  • Write a register model for a design with control, timer and status registers
  • Build a register environment with an adapter and a predictor
  • Connect the register model to an APB agent in the top environment
  • Use write(), read(), set(), predict(), mirror() and update() in a test
  • Read the log and waveform to see what each register call does on the bus

Design

The design represents a traffic light controller that is configured by writing into its registers through an APB interface.

  • The ctl register has fields to enable the module (mod_en), blink yellow (bl_yellow), blink red (bl_red) and select a timing profile.
  • The two timer registers store the time between transitions from one light to the next. The profile bit chooses between the two timers, which is useful for peak and off-peak hours.
  • The stat register is read-only and returns the current state of the design: yellow, red or green.
register model example design

This is not a complete design, because the goal is only to show how its registers are accessed through a UVM register model. All module ports belong to the APB protocol.


module traffic (  input          pclk,
                  input          presetn,
                  input [31:0]   paddr,
                  input [31:0]   pwdata,
                  input          psel,
                  input          pwrite,
                  input          penable,

                  // Outputs
                  output [31:0]  prdata);

   reg [3:0]      ctl_reg;    // profile, blink_red, blink_yellow, mod_en RW
   reg [1:0]      stat_reg;   // state[1:0] 
   reg [31:0]     timer_0;    // timer_g2y[31:20], timer_r2g[19:8], timer_y2r[7:0] RW
   reg [31:0]     timer_1;    // timer_g2y[31:20], timer_r2g[19:8], timer_y2r[7:0] RW

   reg [31:0]     data_in;
   reg [31:0]     rdata_tmp;

   // Set all registers to default values
   always @ (posedge pclk) begin
      if (!presetn) begin
         data_in <= 0;
         ctl_reg  <= 0; 
         stat_reg <= 0; 
         timer_0  <= 32'hcafe_1234; 
         timer_1  <= 32'hface_5678;
      end
   end

   // Capture write data
   always @ (posedge pclk) begin
      if (presetn & psel & penable) 
         if (pwrite) 
            case (paddr)
               'h0   : ctl_reg <= pwdata;
               'h4   : timer_0 <= pwdata;
               'h8   : timer_1 <= pwdata;
               'hc   : stat_reg <= pwdata;
            endcase
   end

   // Provide read data
   always @ (penable) begin
      if (psel & !pwrite) 
         case (paddr)
            'h0 : rdata_tmp <= ctl_reg;
            'h4 : rdata_tmp <= timer_0;
            'h8 : rdata_tmp <= timer_1;
            'hc : rdata_tmp <= stat_reg;
         endcase
   end

   assign prdata = (psel & penable & !pwrite) ? rdata_tmp : 'hz;

endmodule

The RTL accepts a write to address 'hc and updates stat_reg, even though the register is described as read-only. The test below writes to this address on purpose, to show how the register model handles a read-only register.

Interface

The interface holds the APB signals. It is passed to the testbench as a virtual interface, so that the driver can drive values into the design. Clocking blocks and modports are left out to keep things simple, although they are recommended in a real project.


interface bus_if (input pclk);
   logic [31:0]   paddr;
   logic [31:0]   pwdata;
   logic [31:0]   prdata;
   logic          pwrite;
   logic          psel;
   logic          penable;
   logic          presetn;
endinterface

Register Model Example

The register model below describes the design registers. In real projects, a script usually generates it from a register specification such as IP-XACT. The model is built in three layers:

  • Registers: ral_cfg_ctl, ral_cfg_stat and ral_cfg_timer extend uvm_reg and configure their fields in build().
  • Register block: ral_block_traffic_cfg creates the registers and adds them to its address map at offsets 0x0, 0x4, 0x8 and 0xc.
  • Top block: ral_sys_traffic contains the cfg block and adds its map as a submap at offset 0.

// Register definition for the register called "ctl"
class ral_cfg_ctl extends uvm_reg;
	rand uvm_reg_field mod_en;      // Enables the module
	rand uvm_reg_field bl_yellow;   // Blinks yellow
	rand uvm_reg_field bl_red;      // Blinks red
    rand uvm_reg_field profile;     // 1 : Peak, 0 : Off-Peak

	`uvm_object_utils(ral_cfg_ctl)

	function new(string name = "traffic_cfg_ctrl");
		super.new(name, 32, build_coverage(UVM_NO_COVERAGE));
	endfunction: new

  // Build all register field objects
  virtual function void build();
    this.mod_en     = uvm_reg_field::type_id::create("mod_en",,   get_full_name());
    this.bl_yellow  = uvm_reg_field::type_id::create("bl_yellow",,get_full_name());
    this.bl_red     = uvm_reg_field::type_id::create("bl_red",,   get_full_name());
    this.profile    = uvm_reg_field::type_id::create("profile",,  get_full_name());

    // configure(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible); 
    this.mod_en.configure(this, 1, 0, "RW", 0, 1'h0, 1, 0, 0);
    this.bl_yellow.configure(this, 1, 1, "RW", 0, 1'h0, 1, 0, 0);
    this.bl_red.configure(this, 1, 2, "RW", 0, 1'h0, 1, 0, 0);
    this.profile.configure(this, 1, 3, "RW", 0, 1'h0, 1, 0, 0);
  endfunction
endclass 

// Register definition for the register called "stat"
class ral_cfg_stat extends uvm_reg;
  uvm_reg_field state;    // Current state of the design
  
  `uvm_object_utils(ral_cfg_stat)
  function new(string name = "ral_cfg_stat");
    super.new(name, 32, build_coverage(UVM_NO_COVERAGE));
  endfunction

  virtual function void build();
    this.state = uvm_reg_field::type_id::create("state",, get_full_name());

    // configure(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible); 
    this.state.configure(this, 2, 0, "RO", 0, 1'h0, 0, 0, 0);
  endfunction
endclass

// Register definition for the register called "timer"
class ral_cfg_timer extends uvm_reg;
	uvm_reg_field timer;     // Time for which it blinks

	`uvm_object_utils(ral_cfg_timer)
	function new(string name = "traffic_cfg_timer");
		super.new(name, 32,build_coverage(UVM_NO_COVERAGE));
	endfunction

  virtual function void build();
     this.timer = uvm_reg_field::type_id::create("timer",,get_full_name());

    // configure(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible); 
     this.timer.configure(this, 32, 0, "RW", 0, 32'hCAFE1234, 1, 0, 1);
     this.timer.set_reset('h0, "SOFT");
  endfunction
endclass

// These registers are grouped together to form a register block called "cfg"
class ral_block_traffic_cfg extends uvm_reg_block;
	rand ral_cfg_ctl    ctrl;       // RW
	rand ral_cfg_timer  timer[2];   // RW
         ral_cfg_stat   stat;       // RO

	`uvm_object_utils(ral_block_traffic_cfg)

	function new(string name = "traffic_cfg");
		super.new(name, build_coverage(UVM_NO_COVERAGE));
	endfunction

  virtual function void build();
    this.default_map = create_map("", 0, 4, UVM_LITTLE_ENDIAN, 0);
    this.ctrl = ral_cfg_ctl::type_id::create("ctrl",,get_full_name());
    this.ctrl.configure(this, null, "");
    this.ctrl.build();
    this.default_map.add_reg(this.ctrl, `UVM_REG_ADDR_WIDTH'h0, "RW", 0);

     
    this.timer[0] = ral_cfg_timer::type_id::create("timer[0]",,get_full_name());
    this.timer[0].configure(this, null, "");
    this.timer[0].build();
    this.default_map.add_reg(this.timer[0], `UVM_REG_ADDR_WIDTH'h4, "RW", 0);

    this.timer[1] = ral_cfg_timer::type_id::create("timer[1]",,get_full_name());
    this.timer[1].configure(this, null, "");
    this.timer[1].build();
    this.default_map.add_reg(this.timer[1], `UVM_REG_ADDR_WIDTH'h8, "RW", 0);

    this.stat = ral_cfg_stat::type_id::create("stat",,get_full_name());
    this.stat.configure(this, null, "");
    this.stat.build();
    this.default_map.add_reg(this.stat, `UVM_REG_ADDR_WIDTH'hc, "RO", 0);
  endfunction 
endclass 

// The register block is placed in the top level model class definition
class ral_sys_traffic extends uvm_reg_block;
  rand ral_block_traffic_cfg cfg;

	`uvm_object_utils(ral_sys_traffic)
	function new(string name = "traffic");
		super.new(name);
	endfunction

	function void build();
      this.default_map = create_map("", 0, 4, UVM_LITTLE_ENDIAN, 0);
      this.cfg = ral_block_traffic_cfg::type_id::create("cfg",,get_full_name());
      this.cfg.configure(this, "tb_top.pB0");
      this.cfg.build();
      this.default_map.add_submap(this.cfg.default_map, `UVM_REG_ADDR_WIDTH'h0);
	endfunction
endclass

Each field is set up with configure(), whose arguments are listed in the comment above each call. For example, bl_yellow is a 1-bit field at bit position 1 with "RW" access and a reset value of 0. The timer is modeled as a single 32-bit field,.

Register Environment

The register environment puts the register model, the adapter and the predictor in one place. It can then be plugged into any testbench, and the top environment does not need to know how these parts are connected.

The adapter depends on the bus protocol, so a custom class reg2apb_adapter is derived from uvm_reg_adapter. Its two functions convert between the two worlds:

  • reg2bus() turns a register operation into an APB packet bus_pkt.
  • bus2reg() turns an observed APB packet back into a register operation.

class reg2apb_adapter extends uvm_reg_adapter;
   `uvm_object_utils (reg2apb_adapter)

   function new (string name = "reg2apb_adapter");
      super.new (name);
   endfunction

   virtual function uvm_sequence_item reg2bus (const ref uvm_reg_bus_op rw);
      bus_pkt pkt = bus_pkt::type_id::create ("pkt");
      pkt.write = (rw.kind == UVM_WRITE) ? 1: 0;
      pkt.addr  = rw.addr;
      pkt.data  = rw.data;
      `uvm_info ("adapter", $sformatf ("reg2bus addr=0x%0h data=0x%0h kind=%s", pkt.addr, pkt.data, rw.kind.name()), UVM_DEBUG) 
      return pkt; 
   endfunction

   virtual function void bus2reg (uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
      bus_pkt pkt;
      if (! $cast (pkt, bus_item)) begin
         `uvm_fatal ("reg2apb_adapter", "Failed to cast bus_item to pkt")
      end
   
      rw.kind = pkt.write ? UVM_WRITE : UVM_READ;
      rw.addr = pkt.addr;
      rw.data = pkt.data;
      `uvm_info ("adapter", $sformatf("bus2reg : addr=0x%0h data=0x%0h kind=%s status=%s", rw.addr, rw.data, rw.kind.name(), rw.status.name()), UVM_DEBUG)
   endfunction
endclass

// Register environment class puts together the model, adapter and the predictor
class reg_env extends uvm_env;
   `uvm_component_utils (reg_env)
   function new (string name="reg_env", uvm_component parent);
      super.new (name, parent);
   endfunction

   ral_sys_traffic                m_ral_model;         // Register Model
   reg2apb_adapter                m_reg2apb;           // Convert Reg Tx <-> Bus-type packets
   uvm_reg_predictor #(bus_pkt)   m_apb2reg_predictor; // Map APB tx to register in model
   my_agent                       m_agent;             // Agent to drive/monitor transactions

   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      m_ral_model          = ral_sys_traffic::type_id::create ("m_ral_model", this);
      m_reg2apb            = reg2apb_adapter :: type_id :: create ("m_reg2apb");
      m_apb2reg_predictor  = uvm_reg_predictor #(bus_pkt) :: type_id :: create ("m_apb2reg_predictor", this);

      m_ral_model.build ();
      m_ral_model.lock_model ();
      uvm_config_db #(ral_sys_traffic)::set (null, "uvm_test_top", "m_ral_model", m_ral_model);
   endfunction

   virtual function void connect_phase (uvm_phase phase);
      super.connect_phase (phase);
      m_apb2reg_predictor.map       = m_ral_model.default_map;
      m_apb2reg_predictor.adapter   = m_reg2apb;
   endfunction   
endclass

The predictor m_apb2reg_predictor needs the address map and the adapter. Its bus_in port is connected to the APB monitor in the top environment. Every observed APB transfer updates the mirrored value of the matching register.

APB Agent Example

AMBA APB is a bus protocol for low-bandwidth peripheral accesses, and it is often used to access control registers. The agent below is a simple model of an APB agent, enough for this example. It does not handle many features of the actual APB protocol.


// Declare a sequence_item for the APB transaction 
class bus_pkt extends uvm_sequence_item;
   rand bit [31:0]  addr;
   rand bit [31:0]  data;
   rand bit         write;

   `uvm_object_utils_begin (bus_pkt)
      `uvm_field_int (addr, UVM_ALL_ON)
      `uvm_field_int (data, UVM_ALL_ON)
      `uvm_field_int (write, UVM_ALL_ON)
   `uvm_object_utils_end

   function new (string name = "bus_pkt");
      super.new (name);
   endfunction
   
   constraint c_addr { addr inside {0, 4, 8};}
endclass

// Drives a given apb transaction packet to the APB interface
class my_driver extends uvm_driver #(bus_pkt);
   `uvm_component_utils (my_driver)

   bus_pkt  pkt;

   virtual bus_if    vif;

   function new (string name = "my_driver", uvm_component parent);
      super.new (name, parent);
   endfunction

   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      if (! uvm_config_db#(virtual bus_if)::get (this, "*", "bus_if", vif))
         `uvm_error ("DRVR", "Did not get bus if handle")
   endfunction

   virtual task run_phase (uvm_phase phase);
      bit [31:0] data;

      vif.psel <= 0;
      vif.penable <= 0;
      vif.pwrite <= 0;
      vif.paddr <= 0;
      vif.pwdata <= 0;
      forever begin
         seq_item_port.get_next_item (pkt);
         if (pkt.write)
            write (pkt.addr, pkt.data);
         else begin
            read (pkt.addr, data);
            pkt.data = data;
         end
         seq_item_port.item_done ();
      end
   endtask

   virtual task read (  input bit    [31:0] addr, 
                        output logic [31:0] data);
      vif.paddr <= addr;
      vif.pwrite <= 0;
      vif.psel <= 1;
      @(posedge vif.pclk);
      vif.penable <= 1;
      @(posedge vif.pclk);
      data = vif.prdata;
      vif.psel <= 0;
      vif.penable <= 0;
   endtask

   virtual task write ( input bit [31:0] addr,
                        input bit [31:0] data);
      vif.paddr <= addr;
      vif.pwdata <= data;
      vif.pwrite <= 1;
      vif.psel <= 1;
      @(posedge vif.pclk);
      vif.penable <= 1;
      @(posedge vif.pclk);
      vif.psel <= 0;
      vif.penable <= 0;
   endtask
endclass

// Monitors the APB interface for any activity and reports out
// through an analysis port
class my_monitor extends uvm_monitor;
   `uvm_component_utils (my_monitor)
   function new (string name="my_monitor", uvm_component parent);
      super.new (name, parent);
   endfunction

   uvm_analysis_port #(bus_pkt)  mon_ap;
   virtual bus_if                vif;

   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      mon_ap = new ("mon_ap", this);
      uvm_config_db #(virtual bus_if)::get (null, "uvm_test_top.*", "bus_if", vif);
   endfunction
   
   virtual task run_phase (uvm_phase phase);
      fork
         forever begin
            @(posedge vif.pclk);
            if (vif.psel & vif.penable & vif.presetn) begin
               bus_pkt pkt = bus_pkt::type_id::create ("pkt");
               pkt.addr = vif.paddr;
               if (vif.pwrite)
                  pkt.data = vif.pwdata;
               else
                  pkt.data = vif.prdata;
               pkt.write = vif.pwrite;
               mon_ap.write (pkt);
            end 
         end
      join_none
   endtask
endclass

// The agent puts together the driver, sequencer and monitor
class my_agent extends uvm_agent;
   `uvm_component_utils (my_agent)
   function new (string name="my_agent", uvm_component parent);
      super.new (name, parent);
   endfunction

   my_driver                  m_drvr;
   my_monitor                 m_mon;
   uvm_sequencer #(bus_pkt)   m_seqr; 

   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      m_drvr = my_driver::type_id::create ("m_drvr", this);
      m_seqr = uvm_sequencer#(bus_pkt)::type_id::create ("m_seqr", this);
      m_mon = my_monitor::type_id::create ("m_mon", this);
   endfunction

   virtual function void connect_phase (uvm_phase phase);
      super.connect_phase (phase);
      m_drvr.seq_item_port.connect (m_seqr.seq_item_export);
   endfunction
endclass

Testbench Environment

The top environment creates the APB agent and the register environment, and connects them in connect_phase():

  • The monitor's analysis port is connected to the predictor's bus_in port.
  • set_sequencer() tells the default map which sequencer and adapter to use for front-door accesses.

class my_env extends uvm_env;
   `uvm_component_utils (my_env)
   
   my_agent       m_agent;   
   reg_env        m_reg_env;
   
   function new (string name = "my_env", uvm_component parent);
      super.new (name, parent);
   endfunction
   
   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      m_agent = my_agent::type_id::create ("m_agent", this);
      m_reg_env = reg_env::type_id::create ("m_reg_env", this);
      uvm_reg::include_coverage ("*", UVM_CVR_ALL);
   endfunction

   // Connect analysis port of monitor with predictor, assign agent to register env
   // and set default map of the register env
   virtual function void connect_phase (uvm_phase phase);
      super.connect_phase (phase);
      m_reg_env.m_agent = m_agent;
      m_agent.m_mon.mon_ap.connect (m_reg_env.m_apb2reg_predictor.bus_in);
      m_reg_env.m_ral_model.default_map.set_sequencer(m_agent.m_seqr, m_reg_env.m_reg2apb);
   endfunction
   
endclass

Sequences

This sequence brings the DUT out of reset. It gets the virtual interface handle from uvm_config_db and drives the reset input directly. Register accesses can also be written inside a sequence like this, but here they are called from the test class to keep the example short.


class reset_seq extends uvm_sequence;
   `uvm_object_utils (reset_seq)
   function new (string name = "reset_seq");
      super.new (name);
   endfunction

   virtual bus_if    vif; 

   task body ();
      if (!uvm_config_db #(virtual bus_if) :: get (null, "uvm_test_top.*", "bus_if", vif)) 
         `uvm_fatal ("VIF", "No vif")

      `uvm_info ("RESET", "Running reset ...", UVM_MEDIUM);
      vif.presetn <= 0;
      @(posedge vif.pclk) vif.presetn <= 1;
      @ (posedge vif.pclk);
   endtask
endclass

Test

A base test builds the environment and applies reset in reset_phase(). Every other test extends it and only adds its own stimulus. The test reg_rw_test runs in main_phase(), which starts after the DUT is out of reset.


class base_test extends uvm_test;
   `uvm_component_utils (base_test)

   my_env         m_env;
   reset_seq      m_reset_seq;
   uvm_status_e   status;

   function new (string name = "base_test", uvm_component parent);
      super.new (name, parent);
   endfunction

   // Build the testbench environment, and reset sequence
   virtual function void build_phase (uvm_phase phase);
      super.build_phase (phase);
      m_env = my_env::type_id::create ("m_env", this);
      m_reset_seq = reset_seq::type_id::create ("m_reset_seq", this);
   endfunction
 
   // In the reset phase, apply reset
   virtual task reset_phase (uvm_phase phase);
      super.reset_phase (phase);
      phase.raise_objection (this);
      m_reset_seq.start (m_env.m_agent.m_seqr);
      phase.drop_objection (this);
   endtask
endclass

class reg_rw_test extends base_test;
   `uvm_component_utils (reg_rw_test)
   function new (string name="reg_rw_test", uvm_component parent);
      super.new (name, parent);
   endfunction

   // Note that main_phase comes after reset_phase, and is performed when
   // DUT is out of reset. "reset_phase" is already defined in base_test
   // and is always called when this test is started
   virtual task main_phase(uvm_phase phase);
      ral_sys_traffic   m_ral_model;
      uvm_status_e      status;
      int               rdata;

      phase.raise_objection(this);

      m_env.m_reg_env.set_report_verbosity_level (UVM_HIGH);
      
      // Get register model from config_db
      uvm_config_db#(ral_sys_traffic)::get(null, "uvm_test_top", "m_ral_model", m_ral_model);
      
      // Write 0xcafe_feed to the timer[1] register, and read it back
      m_ral_model.cfg.timer[1].write (status, 32'hcafe_feed);
      m_ral_model.cfg.timer[1].read (status, rdata);
      
      // Set 0xface as the desired value for timer[1] register
      m_ral_model.cfg.timer[1].set(32'hface);
      `uvm_info(get_type_name(), $sformatf("desired=0x%0h mirrored=0x%0h", m_ral_model.cfg.timer[1].get(), m_ral_model.cfg.timer[1].get_mirrored_value()), UVM_MEDIUM)
      
      // Predict that current value of timer[1] is 0xcafe_feed and check it is true
      m_ral_model.cfg.timer[1].predict(32'hcafe_feed);
      m_ral_model.cfg.timer[1].mirror(status, UVM_CHECK);
      
      // Set desired value of the field "bl_yellow" in register ctrl to 1
      // Then start bus transactions by calling "update" to update DUT with 
      // desired value
      m_ral_model.cfg.ctrl.bl_yellow.set(1);
      m_ral_model.cfg.update(status);

	  // Attempt to write into a RO register "stat" with some value
      m_ral_model.cfg.stat.write(status, 32'h12345678);
      phase.drop_objection(this);
   endtask

  // Before end of simulation, allow some time for unfinished transactions to
  // be over
  virtual task shutdown_phase(uvm_phase phase);
    super.shutdown_phase(phase);
    phase.raise_objection(this);
    #100ns;
    phase.drop_objection(this);
  endtask
endclass
Output
ncsim> run                                                                                                                                                                  
----------------------------------------------------------------                                                                                                            
CDNS-UVM-1.1d (15.20-s044)                                                                                                                                                  
(C) 2007-2013 Mentor Graphics Corporation                                                                                                                                   
(C) 2007-2013 Cadence Design Systems, Inc.
(C) 2006-2013 Synopsys, Inc.
(C) 2011-2013 Cypress Semiconductor Corp.
----------------------------------------------------------------
UVM_INFO @ 0: reporter [RNTST] Running test reg_rw_test...
UVM_INFO ./tb/my_env.sv(215) @ 0: uvm_test_top.m_env.m_agent.m_seqr@@m_reset_seq [RESET] Running reset ...
UVM_INFO ./tb/test_lib.sv(80) @ 110: uvm_test_top [reg_rw_test] desired=0xface mirrored=0xcafefeed

--- UVM Report catcher Summary ---

reg model example waves

The log and the waveform show what each register call does:

  • write() and read() (30 to 110): 32'hcafe_feed is written to timer[1] at address 8, and read back through prdata.
  • set() at 110: only the desired value changes, so the log shows desired=0xface while mirrored=0xcafefeed. Nothing happens on the bus.
  • predict() and mirror() (110 to 150): predict() sets the model value to 0xcafefeed, and mirror() reads address 8 again. The read data matches, so UVM_CHECK passes without any message.
  • update() (150 to 190): only ctrl has a desired value that differs from its mirrored value, so a single write of 2 (bit bl_yellow) goes to address 0.
  • Write to stat (from 190): the register model still sends the write to address 'hc on the bus. Because the field is read-only, the predictor does not change its mirrored value.

Quick Check

Q: Why does update() not write timer[1], even though set(32'hface) was called on it earlier?

Show Answer

predict() and mirror() were called after set(). Both update the desired value along with the mirrored value, so the desired value of timer[1] became 0xcafefeed again. With no difference left, update() skips it.

Where is This Structure Used?

Real projects follow the same layering: a generated register model, a register environment with an adapter and a predictor, and a bus agent connected in the top environment. When the register specification changes, only the generated model is replaced, and the rest of the testbench stays the same.

Common Mistakes

Mistake #1: Reset values that do not match the RTL

In this example, both timers use the same ral_cfg_timer class with a reset value of 32'hCAFE1234, but the RTL resets timer_1 to 32'hface_5678. The test above does not check reset values, but a built-in reset test such as uvm_reg_hw_reset_seq would report a mismatch for timer[1]. Registers with different reset values need their own class or a different reset in configure().

Mistake #2: Forgetting set_sequencer()

Without default_map.set_sequencer(), the register model has no sequencer or adapter for front-door accesses, and the first front-door write() or read() fails, because there is no path to the bus.

Mistake #3: Expecting update() after set() to always write

Wrong: calling set(), then mirror() or predict(), then update()
Right: call update() right after the set() calls that it should apply

Why? mirror() and predict() overwrite the desired value with the current value, so the change made by set() is lost.

Practice Exercise

Challenge: Program a Peak Profile

Write a few lines for main_phase() of a new test that enables the module, selects the peak profile and programs timer[0] with 32'h0102_0304, using set() and a single update() call. Then check the ctrl register in the design with mirror().

Hint

Set the fields mod_en and profile of ctrl, and the field timer of timer[0]. Calling update() on the cfg block writes every register whose desired value changed.

Solution

m_ral_model.cfg.ctrl.mod_en.set (1);
m_ral_model.cfg.ctrl.profile.set (1);
m_ral_model.cfg.timer[0].timer.set (32'h0102_0304);
m_ral_model.cfg.update (status);                  // Writes ctrl and timer[0]

m_ral_model.cfg.ctrl.mirror (status, UVM_CHECK);  // Reads ctrl and compares

The bus shows two writes: 0x9 to address 0 and 0x01020304 to address 4. The mirror() read returns 0x9, so the check passes.

Summary

In this tutorial, you learned:

  • A register model is built from register classes, a register block with an address map, and a top block
  • The register environment holds the model, a protocol-specific adapter and a predictor
  • The top environment connects the monitor to the predictor and gives the map its sequencer and adapter
  • write() and read() access the design, while set() only changes the desired value
  • predict() and mirror() update both the desired and mirrored values, and update() writes only registers whose values differ
  • The register model sends writes to read-only registers on the bus, but does not change their mirrored value
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5