文章目录
记录一下《UVM实战(卷I)》学习笔记。
1 验证平台的组成
1.1 简单验证平台
- driver:模拟DUT的真实使用情况,给DUT施加各种激励,包括正常异常激励,不同模式激励。
- scoreboard:也被称为 checker 。根据DUT的输出判断DUT行为是否与预期符合。
- monitor:收集DUT的输出并将它们传递给scoreboard。
- reference model:给出预期的结果。
1.2 典型UVM验证平台
引入 agent 和 sequence 概念。
2 按照书本例程使用 Modelsim 搭建仿真环境
2.1 最简单的验证平台
参考文章:UVM学习-仿真环境的搭建
在上述参考文章的基础上,我们自己要去学习具体的原理,首先要了解Modelsim仿真流程及相关命令。
首先按照课本编写好相关代码:
待测设计DUT:这里按照个人代码风格抄了一遍。
//file name: dut.v
module dut(
input I_sys_clk,
input I_reset_n,
input [7:0] I_rxd ,
input I_rx_dv ,
output reg [7:0] O_txd ,
output reg O_tx_en
);
always @(posedge I_sys_clk or negedge I_reset_n)
begin
if(~I_reset_n)
begin
O_txd <= 8'b0;
O_tx_en <= 1'b0;
end
else
begin
O_txd <= I_rxd;
O_tx_en <= I_rx_dv;
end
end
endmodule
UVM是一个库,库中所有东西都是使用类(class)实现。验证平台中所有组件应该派生自UVM中的类。
类有函数(function)和任务(task),通过函数和任务可以完成driver输出激励,monitor监测,参考模型计算,scoreboard比较功能。
类有成员变量,通过成员变量可以控制类的行为。
添加driver类:
//file name: my_driver.sv
`ifndef MY_DRIVER__SV
`define MY_DRIVER__SV
class my_driver extends uvm_driver;
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
endfunction //new()
extern virtual task main_phase(uvm_phase phase);
endclass //my_driver extends uvm_driver
task my_driver::main_phase(uvm_phase phase);
top_tb.I_rxd <= 8'd0;
top_tb.I_rx_dv <= 1'b0;
while(!top_tb.I_reset_n)
@(posedge top_tb.I_sys_clk);
for (int i = 0; i < 256; i++) begin
@(posedge top_tb.I_sys_clk)
top_tb.I_rxd <= $urandom_range(0, 255);
top_tb.I_rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge top_tb.I_sys_clk);
top_tb.I_rx_dv <= 1'b0;
endtask
`endif
uvm_info宏的功能与verilog的display类似,但比display更强大。
第一个字符串参数把打印的信息归类,第二个字符串参数是需要打印的信息,第三个参数是冗余级别,有UVM_LOW、UVM_MEDIUM、UVM_HIGH。UVM默认只显示UVM_MEDIUM或者UVM_LOW的信息。
添加testbench的顶层:
//file name:top_tb.sv
`timescale 1ns/1ps
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "my_driver.sv"
module top_tb();
parameter T = 10;
reg I_sys_clk;
reg I_reset_n;
reg [7:0] I_rxd ;
reg I_rx_dv ;
wire[7:0] O_txd ;
wire O_tx_en ;
dut my_dut(
.I_sys_clk(I_sys_clk),
.I_reset_n(I_reset_n),
.I_rxd (I_rxd ),
.I_rx_dv (I_rx_dv ),
.O_txd (O_txd ),
.O_tx_en (O_tx_en )
);
initial begin
my_driver drv;
drv = new("drv", null);
drv.main_phase(null);
$finish();
end
initial begin
I_sys_clk <= 1'b1;
I_reset_n <= 1'b0;
#(T*10);
I_reset_n <= 1'b1;
end
always #(T/2) I_sys_clk <= ~I_sys_clk;
endmodule
按以往仿真FPGA设计的流程, 在modelsim新建工程然后添加这3个文件。
直接全部编译,是会报错的:
# Compile of top_tb.sv was successful.
# Compile of dut.v was successful.
# Compile of my_driver.sv failed with 2 errors.
# 3 compiles, 1 failed with 2 errors.
使用命令对my_driver.sv单独编译打印报错信息:vlog ./demo0/my_driver.sv
报这个错的原因是 uvm_driver 类的定义没有包含进来,而头文件的包含是在 top_tb.sv 中完成的,所以我们需要指定头文件的路径才能包含进来。vlog 中的 +incdir+<directory>
指令指定include文件路径。关于这个选项官方文档见《modelsim_se_reg.pdf》p817:
注:moselsim文档路径:[Modelsim安装路径]/docs/pdfdocs/
vlog +incdir+C:/software/FPGA/modeltech64_10.5/verilog_src/uvm-1.1d/src C:/software/FPGA/modeltech64_10.5/verilog_src/uvm-1.1d/src/uvm_pkg.sv ./demo0/dut.v ./demo0/top_tb.sv
当然,绝对路径比较长,可以先将路径设为变量
set UVM_PATH C:/software/FPGA/modeltech64_10.5/verilog_src/uvm-1.1d/src
set SRC_PATH ./demo0
vlog +incdir+$UVM_PATH $UVM_PATH/uvm_pkg.sv $SRC_PATH/dut.v $SRC_PATH/top_tb.sv
接下来添加wave信号,运行仿真命令或者在work库右击top_tb→simulate without optimization,进行仿真:
注:如果优化直接把DUT优化掉了,因为输出空载。
vsim -novopt -sv_lib C:/software/FPGA/modeltech64_10.5/uvm-1.1d/win64/uvm_dpi work.top_tb
仿真结果:
将上述过程汇总在一个.do文件中,方便后续更新调用
文件名:demo0.do
cd D:/prj/uvm_prj/demo0
vlib work
set UVM_PATH C:/software/FPGA/modeltech64_10.5/verilog_src/uvm-1.1d/src
set SRC_PATH .
vlog +incdir+$UVM_PATH $UVM_PATH/uvm_pkg.sv $SRC_PATH/dut.v $SRC_PATH/top_tb.sv
vsim -novopt -sv_lib C:/software/FPGA/modeltech64_10.5/uvm-1.1d/win64/uvm_dpi work.top_tb
add wave -position insertpoint sim:/top_tb/*
run 2us
因为do文件放在源文件相同路径,所以路径稍微改了一下。
将调用do文件的操作用批处理命令实现:
文件名:demo0_do.bat
vsim -do demo0.do
2.2 加入factory机制
引入 UVM 的 factory 机制 自动创建一个类的实例并调用其中的函数和任务。
my_driver.sv:
`ifndef MY_DRIVER__SV
`define MY_DRIVER__SV
class my_driver extends uvm_driver;
`uvm_component_utils(my_driver);
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
`uvm_info("my_driver", "new is called", UVM_LOW);
endfunction //new()
extern virtual task main_phase(uvm_phase phase);
endclass //my_driver extends uvm_driver
task my_driver::main_phase(uvm_phase phase);
`uvm_info("my_driver", "main_phase is called", UVM_LOW);
top_tb.I_rxd <= 8'd0;
top_tb.I_rx_dv <= 1'b0;
while(!top_tb.I_reset_n)
@(posedge top_tb.I_sys_clk);
for (int i = 0; i < 256; i++) begin
@(posedge top_tb.I_sys_clk)
top_tb.I_rxd <= $urandom_range(0, 255);
top_tb.I_rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge top_tb.I_sys_clk);
top_tb.I_rx_dv <= 1'b0;
endtask
`endif
top_tb.sv:
`timescale 1ns/1ps
`include "uvm_macros.svh"
import uvm_pkg::*;
`include "my_driver.sv"
module top_tb();
parameter T = 10;
reg I_sys_clk;
reg I_reset_n;
reg [7:0] I_rxd ;
reg I_rx_dv ;
wire[7:0] O_txd ;
wire O_tx_en ;
dut my_dut(
.I_sys_clk(I_sys_clk),
.I_reset_n(I_reset_n),
.I_rxd (I_rxd ),
.I_rx_dv (I_rx_dv ),
.O_txd (O_txd ),
.O_tx_en (O_tx_en )
);
initial begin
// my_driver drv;
// drv = new("drv", null);
// drv.main_phase(null);
run_test("my_driver");
// $finish();
end
initial begin
I_sys_clk <= 1'b1;
I_reset_n <= 1'b0;
#(T*10);
I_reset_n <= 1'b1;
end
always #(T/2) I_sys_clk <= ~I_sys_clk;
endmodule
demo0.do的路径改一下,如果改了do文件名字则批处理文件也要对应,改好之后执行,结果如下:
可以看到,run_test(“my_driver”); 语句完成了 my_driver 的实例化并调用了 main_phase task。但是没有驱动数据,这与作者说的符合。
笔记:
- factory机制的实现被集成在了一个宏中:uvm_component_utils。这个宏将 my_driver 类登记在 UVM 的一张表中,这张表是实现 factory 功能的基础。
- 所有派生自uvm_component及其派生类的类都应该使用uvm_component_utils宏注册。
- run_test(“my_driver”); 语句创建实例并调用 main_phase 。
2.3 加入 Objection 机制
UVM中通过objection机制来控制验证平台的关闭。
在每个phase中,UVM会检查是否有 objection 被提起(raise_objection),如果有,那么等待这 objection 被撤销(drop_objection)后停止仿真;如果没有,则马上结束当前phase。
所以虽然 top_tb 没有 $finish ,但是没有 raise_objection, 所以会马上结束当前 phase,还没来得及驱动信号输出。
加入 objection 机制,my_driver.sv:
`ifndef MY_DRIVER__SV
`define MY_DRIVER__SV
class my_driver extends uvm_driver;
`uvm_component_utils(my_driver);
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
`uvm_info("my_driver", "new is called", UVM_LOW);
endfunction //new()
extern virtual task main_phase(uvm_phase phase);
endclass //my_driver extends uvm_driver
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW);
top_tb.I_rxd <= 8'd0;
top_tb.I_rx_dv <= 1'b0;
while(!top_tb.I_reset_n)
@(posedge top_tb.I_sys_clk);
for (int i = 0; i < 256; i++) begin
@(posedge top_tb.I_sys_clk)
top_tb.I_rxd <= $urandom_range(0, 255);
top_tb.I_rx_dv <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge top_tb.I_sys_clk);
top_tb.I_rx_dv <= 1'b0;
phase.drop_objection(this);
endtask
`endif
修改do文件运行批处理程序,仿真结果:
笔记:
- raise_objection和drop_objection总是成对出现。
- raise_objection语句必须在main_phase中第一个消耗仿真时间的语句之前。 如$display语句是不消耗仿真时间的, 这些语句可以放在raise_objection之前, 但是类似@( posedge top.clk) 等语句是要消耗仿真时间的。
2.4 加入 virtual interface
上诉代码信号驱动使用的是绝对路径,大大降低平台可移植性。
避免绝对路径的两个方法:
- 使用宏定义。修改路径只需要修改宏定义。
- 使用interface。
可以在 top_tb 定义 interface 然后实例化,但是 my_driver 是一个类,不可以在类中使用这种方式声明 interface。在类中使用的是 virtual interface。
my_driver.sv:
`ifndef MY_DRIVER__SV
`define MY_DRIVER__SV
class my_driver extends uvm_driver;
virtual my_if vif;
`uvm_component_utils(my_driver);
function new(string name = "my_driver", uvm_component parent = null);
super.new(name, parent);
`uvm_info("my_driver", "new is called", UVM_LOW);
endfunction //new()
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase); // 因为在其父类的build_phase中执行了一些必要的操
作, 这里必须显式地调用并执行它。
`uvm_info("my_driver", "build_phase is called", UVM_LOW);
if(!uvm_config_db#(virtual my_if)::get(this, "", "vif", vif)) // config_db
`uvm_fatal("my_driver", "virtual interface must be set for vif!!!")
endfunction
extern virtual task main_phase(uvm_phase phase);
endclass //my_driver extends uvm_driver
task my_driver::main_phase(uvm_phase phase);
phase.raise_objection(this);
`uvm_info("my_driver", "main_phase is called", UVM_LOW);
vif.data <= 8'b0;
vif.valid <= 1'b0;
while(!vif.rst_n)
@(posedge vif.clk);
for(int i = 0; i < 256; i++)begin
@(posedge vif.clk);
vif.data <= $urandom_range(0, 255);
vif.valid <= 1'b1;
`uvm_info("my_driver", "data is drived", UVM_LOW);
end
@(posedge vif.clk);
vif.valid <= 1'b0;
phase.drop_objection(this);
endtask
`endif
top_tb.sv 增加:
// 模块内增加
initial begin
uvm_config_db#(virtual my_if)::set(null, "uvm_test_top", "vif", input_if);
end
// 模块外增肌
interface my_if(input clk, input rst_n);
logic [7:0] data ;
logic valid;
endinterface
- build_phase
与main_phase一样, build_phase也是UVM中内建的一个phase。
在build_phase中主要通过config_db的set和get操作来传递一些数据,以及实例化成员变量等。
build_phase与main_phase不同的一点在于,build_phase是一个函数phase,而main_phase是一个任务phase,build_phase是不消耗仿真时间的。build_phase总是在仿真时间($time函数打印出的时间)为0时执行。 - config_db
set和get函数都有四个参数, 这两个函数的第三个参数必须完全一致。 set函数的第四个参数表示要将哪个interface 通过config_db传递给my_driver, get函数的第四个参数表示把得到的interface传递给哪个my_driver的成员变量。- set:可以简单地理解成是“寄信”
- get: 可以简单地理解成是“收信”
仿真结果: