While reviewing the UVM reference manual, I came across a section titled "Traversal" in Chapter 29.4. Within this chapter, the term "uvm_visitor" caught my attention. This brought to mind a book I had purchased in the past, "Dive into Design Patterns" by Alexander Shvets, which also featured a chapter named "Visitor" under the section labeled "Behavioral Design Pattern." Despite my experience in various verification environments and online resources, I had never encountered the practical application of this pattern. I decided to learn this pattern and find a good use case for it.
Next step obviously was to search Google, the only relevant presentation I found can be found here. This presentation showed few use cases mainly:
Component Configuration Check Visitor
Reset and Clock Generation Check Visitor
Add Message and Improve The Reporting System
After testing the provided code and confirming its functionality, I really liked the potential of this pattern and I wanted to further explore it.
The subsequent action involved requesting ChatGpt to list some use cases for the UVM Visitor design pattern. From the various results provided, one particular use case stood out to me, and it was as follows:
Register model Configuration
Use the Visitor pattern to traverse a register model hierarchy.
Configure registers and their fields based on test requirements.
Validate and update register settings during different phases of testing.
As someone with good knowledge about the Register Abstraction Layer (RAL), as soon as I read this response, I recognized its potential and quickly wrote use case for it.
If you've found this topic intriguing so far, I encourage you to explore the following explanations about the Visitor pattern and the double dispatch method. These explanations are highly informative and will greatly enhance your comprehension of my code.
As mentioned above the objective is to configure the RAL based on various test requirements
Configure various types of secantious via visitor object
Initially, we will declare the "reg_cfg" interface class, along with a pure virtual function named "accept." This declaration will mandate that all classes implementing this interface must also implement the "accept" method
interface class reg_cfg; pure virtual function void accept (visitor v); endclass
Next all uvm_reg objects will implement this interface class. double dispatch technique means that we delegate the choice of which visitor method to activate to object it self.
refer to the highlighted line of code.
class port_cfg extends uvm_reg implements reg_cfg; rand uvm_reg_field adc_cfg; rand uvm_reg_field dac_cfg;
...
virtual function void accept (visitor v); v.visit_port_cfg(this); endfunction
...
endclass
class chip_cfg extends uvm_reg implements reg_cfg; rand uvm_reg_field pwr_cfg; rand uvm_reg_field prio_cfg;
...
virtual function void accept (visitor v); v.visit_chip_cfg(this); endfunction
...
endclass
Next the register block is been implemented, nothing special here
class chip_reg_block extends uvm_reg_block; rand port_cfg m_port_control; rand chip_cfg m_chip_control; uvm_reg_map reg_map;
...
virtual function void build(); m_port_control = port_cfg::type_id::create("m_port_control") ; ... m_chip_control = chip_cfg::type_id::create("m_chip_control"); ...
reg_map = create_map(...);
endfunction: build
endclass
Now, let's delve into the main part of this post where we implement the visitor class.
In this example, we can observe that the class "reg_cfg_scenario_a" extends the visitor class and modifies the desired value of registers based on a specific test scenario.
Subsequently, when you activate this class, each register will trigger the corresponding method within the visitor that aligns with that register's characteristics.
For instance, the "register port_cfg" will invoke the "visit_port_cfg" method, which, in turn, alters the "adc_cfg" field to 'hA and the "dac_cfg" field to 'hF.
Similarly, the "register chip_cfg" will trigger the "visit_chip_cfg" method, which modifies the "pwr_cfg" field to 'hB and the "prio_cfg" field to 'hC.
This approach allows us to create numerous "cfg" scenarios that extend the visitor class, catering to a variety of configuration requirements dictated by different test scenarios
virtual class visitor; pure virtual function void visit_port_cfg (port_cfg pc); pure virtual function void visit_chip_cfg (chip_cfg cc); endclass class reg_cfg_scenario_a extends visitor; virtual function void visit_port_cfg (port_cfg pc); pc.adc_cfg.set(64'hA); pc.dac_cfg.set(64'hF); endfunction virtual function void visit_chip_cfg (chip_cfg cc); cc.pwr_cfg.set(64'hB); cc.prio_cfg.set(64'hC); endfunction endclass
In order to enable the visitor pattern in the test, we must undertake the following steps.
Create the environment
In the run phase method declare queue of uvm_reg objects
Declare one of the visitor classes and construct it
Using RAL method get_registers get all registers and assign them to the queue
(Most important) Iterate through all uvm_reg queue items, perform a type casting to 'reg_cfg,' and trigger the 'accept' method for each of the registers.
class visitor_test extends uvm_test; chip_env m_chip_env; ...
// Step 1 m_chip_env=chip_env::type_id::create("m_chip_env", this); task run_phase (uvm_phase phase);
// Step 2
uvm_reg regs[$];
// Step 3 reg_cfg_scenario_a visit=new;
// Step 4 m_chip_env.m_chip_reg_block.get_registers(regs); ... // Step 5 foreach (regs[idx]) begin reg_cfg r; $cast(r, regs[idx]); r.accept(visit); end endtask endclass
Here is the output obtained by applying the 'convert2string' method to each of the registers both before and after applying the visitor pattern.
**************************************
Registers desired value before visit
RW m_port_control[3:0]=4'h0
RW m_port_control[7:4]=4'h0
RW m_chip_control[3:0]=4'h0
RW m_chip_control[7:4]=4'h0
**************************************
**************************************
Registers desired value after visit
RW m_port_control[3:0]=4'ha (Mirror: 4'h0)
RW m_port_control[7:4]=4'hf (Mirror: 4'h0)
RW m_chip_control[3:0]=4'hb (Mirror: 4'h0)
RW m_chip_control[7:4]=4'hc (Mirror: 4'h0)
**************************************
In this post, we delve into the powerful utility of the visitor design pattern for configuring registers based on different test cases. By extending a visitor class for each test case, we unlock the ability to customize register behavior uniquely for each scenario. The post takes you through the essential steps to set up your test environment, demonstrating the implementation of the accept method using an interface class. Towards the conclusion, we illustrate how to iterate through all registers, enabling each one to accept a visitor instance. This clever technique, known as double dispatch, ensures that the relevant visitor method is activated for each register instance, providing a comprehensive and adaptable solution to your register configuration needs.
full test bench could be found here
Comments