The verilog testbench is fairly limited in functionality and the ethernet packet data structure is described using a module. Needless to say it is clunky and difficult to make enhancements. I figured anyone wishing to build a more complex packet processor, would need a more sophisticated testbench.
I decided to build one in System verilog, using the UVM class framework. Thanks in part to the EDA vendors, people don't believe you've built a testbench unless its in UVM.
Essentially I use a system verilog class to represent an ethernet 802.3 packet, build drivers and monitors to inject and watch for packets, also use a checker to verify the packets coming out (a self-checking testbench). UVM provides the class framework.
Here's the breakdown of the steps I followed.
Edited the demo_tb.v removing the configuration_tb and emac0_phy_tb instance, creating tb_uvm.v Moved the clock and reset generation blocks from configuration_tb.v to tb_uvm.v.
For UVM drivers and monitor to access signals, system verilog interfaces instances are created and stored in a UVM resource database (dB). This happens in lines 107 through 130.
The test itself "default_run_test" is launced in line 131.
1 //----------------------------------------------------- 2 // Systemverilog+UVM testbench to exercise a 3 // Xilinx coregen Virtex5 Trimac with a GMII interface 4 //----------------------------------------------------- 5 6 `timescale 1ps / 1ps 7 8 import uvm_pkg::*; 9 10 module tb_uvm; 11 12 reg reset; 13 14 // EMAC0 15 wire tx_client_clk_0; 16 wire [7:0] tx_ifg_delay_0; 17 wire rx_client_clk_0; 18 wire [15:0] pause_val_0; 19 wire pause_req_0; 20 21 // GMII wires 22 wire gmii_tx_clk_0; 23 wire gmii_tx_en_0; 24 wire gmii_tx_er_0; 25 wire [7:0] gmii_txd_0; 26 reg gmii_rx_clk_0; 27 wire gmii_rx_dv_0; 28 wire gmii_rx_er_0; 29 wire [7:0] gmii_rxd_0; 30 31 // Clock wires 32 reg host_clk; 33 reg gtx_clk; 34 reg refclk; 35 36 reg config_busy; 37 38 //--------------------------------------------------- 39 // Wire up Device Under Test 40 //--------------------------------------------------- 41 v5_emac_v1_8_example_design dut 42 ( 43 // Client Receiver Interface - EMAC0 44 .EMAC0CLIENTRXDVLD (), 45 .EMAC0CLIENTRXFRAMEDROP (), 46 .EMAC0CLIENTRXSTATS (), 47 .EMAC0CLIENTRXSTATSVLD (), 48 .EMAC0CLIENTRXSTATSBYTEVLD (), 49 50 // Client Transmitter Interface - EMAC0 51 .CLIENTEMAC0TXIFGDELAY (tx_ifg_delay_0), 52 .EMAC0CLIENTTXSTATS (), 53 .EMAC0CLIENTTXSTATSVLD (), 54 .EMAC0CLIENTTXSTATSBYTEVLD (), 55 56 // MAC Control Interface - EMAC0 57 .CLIENTEMAC0PAUSEREQ (pause_req_0), 58 .CLIENTEMAC0PAUSEVAL (pause_val_0), 59 60 // Clock wire - EMAC0 61 .GTX_CLK_0 (gtx_clk), 62 63 // GMII Interface - EMAC0 64 .GMII_TXD_0 (gmii_txd_0), 65 .GMII_TX_EN_0 (gmii_tx_en_0), 66 .GMII_TX_ER_0 (gmii_tx_er_0), 67 .GMII_TX_CLK_0 (gmii_tx_clk_0), 68 .GMII_RXD_0 (gmii_rxd_0), 69 .GMII_RX_DV_0 (gmii_rx_dv_0), 70 .GMII_RX_ER_0 (gmii_rx_er_0), 71 .GMII_RX_CLK_0 (gmii_rx_clk_0), 72 73 74 .REFCLK (refclk), 75 76 // Asynchronous Reset 77 .RESET (reset) 78 ); 79 80 //----------------------------------------------- 81 // Flow Control is unused in this demonstration 82 //----------------------------------------------- 83 assign pause_req_0 = 1'b0; 84 assign pause_val_0 = 16'b0; 85 86 // IFG stretching not used in demo. 87 assign tx_ifg_delay_0 = 8'b0; 88 89 //--------------------------------------------- 90 // Clock drivers 91 //-------------------------------------------- 92 93 // Drive GTX_CLK at 125 MHz 94 initial // drives gtx_clk at 125 MHz 95 begin 96 gtx_clk <= 1'b0; 97 #10000; 98 forever 99 begin 100 gtx_clk <= 1'b0; 101 #4000; 102 gtx_clk <= 1'b1; 103 #4000; 104 end 105 end 106 107 //Systemverilog interface instances to pass onto the 108 //uvm testbench drivers and monitors 109 gmii_mon_intf tx_mon_intf( .clk(gmii_tx_clk_0), 110 .data(gmii_txd_0), 111 .valid(gmii_tx_en_0), 112 .err(gmii_tx_er_0) ); 113 114 gmii_mon_intf rx_mon_intf( .clk(gmii_rx_clk_0), 115 .data(gmii_rxd_0), 116 .valid(gmii_rx_dv_0), 117 .err(gmii_rx_er_0) ); 118 119 gmii_drv_intf drv_intf( .clk(gmii_rx_clk_0), 120 .data(gmii_rxd_0), 121 .valid(gmii_rx_dv_0), 122 .err(gmii_rx_er_0), 123 .config_busy(config_busy) ); 124 125 initial 126 begin 127 //Store interface handles in resource db 128 uvm_resource_db #(virtual gmii_mon_intf )::set("tx_mon", "intf", tx_mon_intf,null); 129 uvm_resource_db #(virtual gmii_mon_intf )::set("rx_mon", "intf", rx_mon_intf,null); 130 uvm_resource_db #(virtual gmii_drv_intf )::set("drv", "intf", drv_intf,null); 131 run_test("default_run_test"); 132 end 133 134 // Drive refclk at 200MHz 135 initial 136 begin 137 refclk <= 1'b0; 138 #10000; 139 forever 140 begin 141 refclk <= 1'b1; 142 #2500; 143 refclk <= 1'b0; 144 #2500; 145 end 146 end 147 148 // drives gmii_rx_clk at 125 MHz for 1000Mb/s operation 149 initial 150 begin 151 gmii_rx_clk_0 <= 1'b0; 152 #20000; 153 forever 154 begin 155 #4000; 156 gmii_rx_clk_0 <= 1'b1; 157 #4000; 158 gmii_rx_clk_0 <= 1'b0; 159 end 160 end 161 162 // hostclk freq = 1/3 gtx_clk freq 163 initial 164 begin 165 host_clk <= 1'b0; 166 #2000; 167 forever 168 begin 169 host_clk <= 1'b1; 170 #12000; 171 host_clk <= 1'b0; 172 #12000; 173 end 174 end 175 176 //Reset 177 initial 178 begin 179 $display("timing checks invalid"); 180 181 reset <= 1'b1; 182 config_busy <= 0; 183 184 #200000 185 config_busy <= 1; 186 187 $display("reset design"); 188 189 reset <= 1'b1; 190 #4000000 191 reset <= 1'b0; 192 #200000; 193 194 $display("timing checks valid"); 195 #15000000; 196 #100000 ; 197 config_busy <= 0; 198 end 199 200 endmodule
So what are these system verilog interface thingys anyway ?
Lets take a look at the interface for the GMII driver. Lines 2 through 6 specify the port and direction, we'll be driving the data, valid, err signals, hence they are outputs. clk is an input, driven by the clock generator in the top level testbench, tb_uvm.v. The default clocking block specifies that the outputs and inputs are to be sampled with respect to the rising clock edge.
Lets take a look at the interface for the GMII driver. Lines 2 through 6 specify the port and direction, we'll be driving the data, valid, err signals, hence they are outputs. clk is an input, driven by the clock generator in the top level testbench, tb_uvm.v. The default clocking block specifies that the outputs and inputs are to be sampled with respect to the rising clock edge.
1 interface gmii_drv_intf ( 2 input clk, 3 output [7:0] data, 4 output valid, 5 output err, 6 input config_busy) ; 7 8 default clocking pclk @(posedge clk); 9 endclocking 10 11 endinterfaceIn a similar fashion, for a monitor we have (gmii_mon_intf.sv).
1 interface gmii_mon_intf ( 2 input clk, 3 input [7:0] data, 4 input valid, 5 input err) ; 6 7 default clocking pclk @(posedge clk); 8 endclocking 9 10 endinterface
Notice that a passive monitor does not drive any signals, so everything is an input.
There are many ways to describe interfaces, you may see the direction being set by modports as opposed to the direction being set in the interface port declaration list. So you can actually combine the driver and monitor in one interface and have modports distinguish between driver and monitor. I will defer stuff like that to post discussing interfaces.
At this point, building the hooks to access the RTL is finished. We jump over to the test side to define the packet, driver, monitor, checker and put everything together in a test.
The ethernet packet is defined in the class below. All data objects derive from uvm_sequence_item in UVM. I'll point out some nifty features of System verilog. A packet can have a variable length payload, from 46 to 1500 bytes. System verilog provides several features to transform a packet to a stream of bytes (remember thats what goes on the wire) and vice versa. Line 7 defines a queue of bytes, calling the pack() method on the packet will fill bytestream. The "real" packet fields are between lines 9 and 16. The data field on line 14 is variable length and so is described as a queue. The constraint on line 22, chooses a payload length and also sizes the data byte queue. The uvm_object_utils macros setup print,copy, pack and unpack methods. The built-in pack and unpack methods do not work for us, since we have a variable length field. The pack() and unpack() methods use the system verilog streaming operator, which allows us to do in one line what the UVM custom packing methods would take several lines to do. In UVM custom packing methods must be defined in do_pack() and do_unpack(). In another post I will compare the two ways.
1 //------------------------------------------------- 2 // Ethernet 802.3 packet 3 //------------------------------------------------- 4 class ethernet_pkt extends uvm_sequence_item ; 5 6 static int pkt_count = 0; 7 byte bytestream[$] ; 8 9 rand bit [55:0] preamble ; 10 rand byte sfd ; 11 rand bit [47:0] da; 12 rand bit [47:0] sa ; 13 rand bit [15:0] length; 14 rand byte data[$]; 15 rand bit [31:0] fcs; 16 rand bit [31:0] calc_fcs; 17 18 rand int unsigned payload_length ; 19 20 constraint preamble_c { preamble == 56'h55555555555555; } 21 constraint sfd_c { sfd == 8'hd5; } 22 constraint payload_length_c { 23 payload_length inside {[46:1500]}; 24 data.size() == payload_length; } 25 26 `uvm_object_utils_begin(ethernet_pkt) 27 `uvm_field_int( preamble, UVM_ALL_ON | UVM_NOPACK ); 28 `uvm_field_int( sfd, UVM_ALL_ON | UVM_NOPACK ); 29 `uvm_field_int( da, UVM_ALL_ON | UVM_NOPACK ); 30 `uvm_field_int( sa, UVM_ALL_ON | UVM_NOPACK ); 31 `uvm_field_int( length, UVM_ALL_ON | UVM_NOPACK | UVM_DEC ); 32 `uvm_field_queue_int( data, UVM_ALL_ON | UVM_NOPACK ); 33 34 `uvm_field_int( fcs, UVM_ALL_ON | UVM_NOPACK | UVM_NOCOMPARE ); 35 `uvm_field_int( payload_length, UVM_ALL_ON | 36 UVM_NOPACK | UVM_DEC | UVM_ABSTRACT | UVM_NOCOMPARE); 37 `uvm_field_int( calc_fcs, UVM_ALL_ON | 38 UVM_NOPACK | UVM_ABSTRACT | UVM_NOCOMPARE); 39 `uvm_object_utils_end 40 41 function new(string name="ethernet_pkt"); 42 super.new(name); 43 pkt_count++; 44 endfunction // new 45 46 function void post_randomize(); 47 length = payload_length ; 48 do_calc_fcs(); 49 fcs = calc_fcs ; 50 endfunction // void 51 52 task do_calc_fcs() ; 53 byte unsigned byte_array[]; 54 int unsigned num_bytes ; 55 bit [31:0] tmp ; 56 57 this.pack_bytes( byte_array ); 58 num_bytes = byte_array.size(); 59 60 calc_fcs = 32'hffffffff; 61 for (int i=8;i<num_bytes-4;i++) 62 calc_fcs = nextCRC32_D8( byte_array[i],calc_fcs); 63 //Invert 64 calc_fcs = ~calc_fcs ; 65 //Bit reverse 66 calc_fcs = { << {calc_fcs} }; 67 //Byte reverse 68 calc_fcs = { << 8{calc_fcs}}; 69 endtask 70 71 task pack(); 72 bytestream = { >> {preamble,sfd,da,sa,length,data,fcs}}; 73 endtask // pack 74 75 task unpack() ; 76 { >> {preamble,sfd,da,sa,length,data,fcs}} = bytestream ; 77 do_calc_fcs(); 78 endtask // unpack 79 80 function bit [31:0] nextCRC32_D8 (bit [7:0] Data, bit [31:0] crc); 81 82 reg [7:0] d; 83 reg [31:0] c; 84 reg [31:0] newcrc; 85 86 d = Data; 87 c = crc; 118 newcrc[29] = d[4] ^ c[27] ....... 119 newcrc[30] = c[28] ^ d[0] ^ .......... 120 newcrc[31] = c[29] ^ c[23] ^ d[2]; 121 122 return(newcrc); 123 124 endfunction // nextCRC32_D8 125 126 endclass
The gmii_driver class takes a packet (from a UVM sequencer), packs it into a bytstream and sends it on the wire. Line 12 shows the declaration of an interface handle, always preceded by the keyword virtual. In Line 20, we retrieve the gmii_drv_intf instance from the resource dB. The key lookup for this dB is defined by concatenating the first and second arguments. get_name() returns a string, that was used as the name argument when the driver object is created.
The driver, monitor and checker objects are created (or new'd) in a parent class. UVM suggests an environment class. For this example, the test class acts as the parent.
1 `ifndef __GMII_DRIVER__ 2 `define __GMII_DRIVER__ 3 4 import uvm_pkg::*; 5 6 `include "ethernet_pkt.sv" 7 8 class gmii_driver extends uvm_driver #(ethernet_pkt) ; 9 10 `uvm_component_utils(gmii_driver) 11 12 virtual gmii_drv_intf intf ; 13 14 function new(string name, uvm_component parent); 15 super.new(name,parent); 16 endfunction 17 18 function void build_phase( uvm_phase phase); 19 super.build_phase(phase); 20 uvm_resource_db #(virtual gmii_drv_intf)::read_by_name(this.get_name(), "intf", intf,null); 21 endfunction 22 23 function void connect_phase(uvm_phase phase) ; 24 super.connect_phase(phase); 25 endfunction 26 27 task main_phase(uvm_phase phase); 28 fork 29 begin 30 ethernet_pkt pkt ; 31 intf.valid = 0; 32 intf.data = 0; 33 intf.err = 0; 34 35 //Wait for reset to finish 36 wait ( intf.config_busy == 0 ); 37 wait ( intf.config_busy == 1 ); 38 wait ( intf.config_busy == 0 ); 39 40 forever 41 begin 42 //Get packet from sequencer 43 seq_item_port.get_next_item(pkt); 44 45 pkt.pack() ; 46 47 //Drive each byte from packed bytestream 48 foreach ( pkt.bytestream[i] ) 49 begin 50 @(intf.pclk); 51 intf.valid = 1 ; 52 intf.err = 0; 53 intf.data = pkt.bytestream[i] ; 54 end 55 @(intf.pclk); 56 intf.valid = 0; 57 58 seq_item_port.item_done(); 59 60 //Inter packet gap 61 repeat (200) @(intf.pclk) ; 62 63 end // forever begin 64 end // fork begin 65 join_none 66 endtask 67 68 endclass 69 70 `endif
The gmii monitor watches the gmii interface, builds an ethernet packet and
sends it up to higher layer for processing. In our case this is the checker. The communication is through the UVM analysis port declared in line 12 and new'd in line 20. All the action is defined in the UVM main_phase task.
1 `ifndef __GMII_MONITOR__ 2 `define __GMII_MONITOR__ 3 4 import uvm_pkg::*; 5 6 `include "ethernet_pkt.sv" 7 8 class gmii_monitor extends uvm_monitor ; 9 10 `uvm_component_utils(gmii_monitor) 11 12 uvm_analysis_port #(ethernet_pkt) mon_export ; 13 14 virtual gmii_mon_intf intf ; 15 int unsigned pkt_count ; 16 17 function new(string name, uvm_component parent); 18 super.new(name,parent); 19 pkt_count = 0; 20 mon_export = new("mon_export",this); 21 endfunction 22 23 function int unsigned get_pkt_count(); 24 return pkt_count ; 25 endfunction // int 26 27 function void build_phase( uvm_phase phase); 28 super.build_phase(phase); 29 uvm_resource_db #(virtual gmii_mon_intf)::read_by_name(this.get_name(), "intf", intf,null); 30 endfunction 31 32 function void connect_phase(uvm_phase phase) ; 33 super.connect_phase(phase); 34 endfunction 35 36 task main_phase(uvm_phase phase); 37 fork 38 int count ; 39 ethernet_pkt pkt ; 40 41 forever 42 begin 43 count = 0; 44 @(intf.pclk); 45 if ( intf.valid == 1 ) 46 begin 47 `uvm_info(this.get_name(),"SOP",UVM_NONE); 48 pkt = new("pkt"); 49 do 50 begin 51 pkt.bytestream.push_back(intf.data); 52 count = count + 1; 53 54 @(intf.pclk); 55 if ( intf.valid == 0 ) 56 begin 57 `uvm_info(this.get_name(),"******EOP",UVM_NONE); 58 pkt.unpack(); 59 mon_export.write( pkt ); 60 pkt = null ; 61 count = 0; 62 pkt_count++; 63 end 64 end 65 while ( count ); 66 end 67 end 68 join_none 69 endtask 70 71 endclass 72 73 `endif // `ifndef __GMII_MONITOR__
The gmii checker uses ethernet pkt object fifos to pick up packets from the Rx and Tx interface monitors.Then compares the packets. All the fields should match, once the source and destination fields are swapped, which is what the example design does.The communication fifos are defined in lines 10-11,with the fancy uvm_tlm_analysis_fifo name. (TLM = Transaction Level Model). The packet compare method gets created auto-magically in the ethernet packet class, with the UVM object util macro set.
1 `ifndef __GMII_CHECKER__ 2 `define __GMII_CHECKER__ 3 4 `include "ethernet_pkt.sv" 5 6 class gmii_checker extends uvm_agent ; 7 8 `uvm_component_utils(gmii_checker) 9 10 uvm_tlm_analysis_fifo #(ethernet_pkt) rx_mon_fifo ; 11 uvm_tlm_analysis_fifo #(ethernet_pkt) tx_mon_fifo ; 12 13 function new (string name="gmii_checker", uvm_component parent); 14 super.new(name,parent); 15 rx_mon_fifo = new("rx_mon_fifo",this); 16 tx_mon_fifo = new("tx_mon_fifo",this); 17 endfunction // new 18 19 function void build(); 20 super.build(); 21 endfunction // void 22 23 task run(); 24 ethernet_pkt rx_pkt ; 25 ethernet_pkt tx_pkt ; 26 bit [47:0] swp ; 27 fork 28 forever 29 begin 30 tx_mon_fifo.get(tx_pkt); 31 //The design swaps the source and destination addresses 32 //checker does the same. 33 swp = tx_pkt.da ; 34 tx_pkt.da = tx_pkt.sa ; 35 tx_pkt.sa = swp; 36 rx_mon_fifo.get(rx_pkt); 37 38 if ( tx_pkt.compare(rx_pkt) ) 39 begin 40 `uvm_info(this.get_name(),"**** PKT MATCH ******",UVM_NONE); 41 tx_pkt.print(); 42 end 43 else 44 `uvm_error(this.get_name(),"*** PKT MISMATCH *****"); 45 end 46 join_none 47 48 endtask // run 49 50 endclass 51 52 `endif // `ifndef __GMII_CHECKER__
UVM has the concept of stimulus sequences, which are essentially streams of uvm_sequence_items, in our case this would be a stream of ethernet packets. Nothing precludes you from sending a packet to a driver using another mechanism such as sytem verilog's built-in mailbox. Indeed for simple stimulus streams, sequences are overkill. The glue that gets a sequence into a driver is the sequencer, which you will see is defined in the test class. The body() task is what defines how the sequence is built.
1 `ifndef __DEFAULT_PKT_SEQUENCE__ 2 `define __DEFAULT_PKT_SEQUENCE__ 3 4 `include "ethernet_pkt.sv" 5 6 class default_pkt_sequence extends uvm_sequence #(ethernet_pkt); 7 8 int cnt = 10; 9 ethernet_pkt pkt ; 10 11 `uvm_object_utils_begin(default_pkt_sequence) 12 `uvm_field_int(cnt,UVM_ALL_ON); 13 `uvm_object_utils_end 14 15 function new(string name = "default_pkt_sequence"); 16 super.new(name); 17 endfunction // new 18 19 task body(); 20 repeat(cnt) 21 begin 22 pkt = new("pkt"); 23 start_item(pkt); 24 25 pkt.randomize() with { payload_length == 46; } ; 26 27 finish_item(pkt); 28 end 29 endtask // body 30 31 endclass 32 33 `endif 34
At the test, finally ! For this example, I've used the test to create all the testbench objects and glue stuff together. UVM purists will want a UVM agent that brings the driver+monitor+sequencer, then a UVM environment class that instantiates an agent, followed by a test class that instantiates the environment class.
The testbench objects are created in lines 35-40. The first argument in the create or new functions give each object its name. In the connect function on line 45, we get the sequencer to talk to the driver and the monitors to the checker. The main_phase starts the test. UVM uses an objection mechanism to keep a phase alive. Once all objects have dropped objections in a phase, that phase is terminated. You will notice that the driver, monitor and checker did not raise an objection. Without the test raising an objection in the main phase, the main phase would basically terminate immediately giving us a "do-nothing" test. For our simple case, the test is kept alive by waiting for the same number of packets that were sent in. I plan to discuss other methods, timeouts etc in a future post.
1 //------------------------------------------------- 2 // Test: Bringing everything together 3 //------------------------------------------------- 4 import uvm_pkg::*; 5 6 `include "gmii_monitor.sv" 7 `include "gmii_driver.sv" 8 `include "default_pkt_sequence.sv" 9 `include "gmii_checker.sv" 10 11 class default_run_test extends uvm_test ; 12 13 `uvm_component_utils(default_run_test) 14 15 gmii_monitor tx_mon ; 16 gmii_monitor rx_mon ; 17 gmii_driver drv ; 18 gmii_checker checker ; 19 20 uvm_sequencer #(ethernet_pkt) seqr ; 21 22 default_pkt_sequence seq ; 23 24 time countdown_interval ; 25 26 function new(string name, uvm_component parent); 27 super.new(name,parent); 28 drv_mbox = new(1); 29 `uvm_info("", "Called default_run_test::new", UVM_NONE); 30 countdown_interval = 1000ns; 31 endfunction: new 32 33 function void build_phase(uvm_phase phase); 34 super.build_phase(phase); 35 tx_mon = gmii_monitor::type_id::create("tx_mon",this); 36 rx_mon = gmii_monitor::type_id::create("rx_mon",this); 37 drv = gmii_driver::type_id::create("drv",this); 38 seqr = new("seqr",this); 39 seq = default_pkt_sequence::type_id::create("seq",this); 40 checker = new("checker",this); 41 42 uvm_top.enable_print_topology = 1; 43 endfunction 44 45 function void connect(); 46 drv.seq_item_port.connect(seqr.seq_item_export); 47 tx_mon.mon_export.connect( checker.tx_mon_fifo.analysis_export ); 48 rx_mon.mon_export.connect( checker.rx_mon_fifo.analysis_export ); 49 endfunction // void 50 51 task main_phase(uvm_phase phase); 52 ethernet_pkt pkt ; 53 int num_pkts = 3; 54 55 phase.raise_objection(this); 56 57 //Send pkt sequence 58 seq.start(seqr); 59 60 //Loop until all pkts Tx by DUT 61 while(tx_mon.get_pkt_count() < num_pkts ) 62 #(countdown_interval); 63 64 phase.drop_objection(this); 65 66 endtask // main_phase 67 68 endclassThat pretty much describes all the components of the testbench. The complete source code tarball for the testbench is on github.You can browse at : https://github.com/austinchipworks/sv_uvm_xil_emac_tb For a local copy: git clone https://github.com/austinchipworks/sv_uvm_xil_emac_tb