Loading

数字验证——VCS使用

一,基础介绍

VCS用来编译仿真verilog/systemverilog,先将HDL源文件转化为C文件,在linux下编译和链接生成可执行文件,运行可执行文件即可得到仿真结果。

  • 编译命令格式 :vcs sourcefile [compile_time_option] (编译选项用来控制编译过程)
  • 执行仿真命令格式:./simv [run_time_option] (simv为可执行文件)

常用编译选项(更详细参考 VCS 编译选项 - 知乎 (zhihu.com)):

  • +neg_tchk:使能时序检查中的负延时
  • +notimingcheck:不进行时序检查
  • +sdf_nocheck_celltype:SDF反标时不检查SDF文件中的celltype的一致性
  • +v2k:支持Verilog-2001标准
  • -f <filename>:指定文件列表的文件名,文件中可包括源代码文件的路径和名称,也可以包括编译选项参数
  • -R:在编译之后立即执行产生的可执行文件
  • -v <filename>:指定verilog库文件
  • -l <filename>:指定记录VCS编译和运行信息的log文件名
  • -o <name>:指定编译生成的可执行文件的名称,默认是simv
  • -gui:仿真开始后启动DVE图形界面
  • -verdi:使用verdi图形界面
  • -debug_all:用于产生debug所需的文件

在复杂设计的VCS使用中,经常将需要编译的源文件写进一个verilog.f文件,然后用-f选项指定文件进行编译。

同时,也可以把编译过程写成makefile脚本,提高程序的复用性,提高效率:

#一种参考运行vcs的makefile脚本
.PHONY:com sim clean OUTPUT
= adder_top VCS = vcs -sverilog +v2k -timescale=1ns/1ns \ -debug_all \ -o ${OUTPUT} \ -l compile.log \ SIM = ./${OUTPUT} -l run.log com: ${VCS} -f verilog_file.f sim: ${SIM} clean: rm -rf ./csrc *.daidir *.log simv* *.key

执行make com、make sim、make clean即可执行编译、仿真、清除指令。

二,VCS testbench搭建

简单的testbench常常是initial给波形,always给时钟,然后使能信号看输出,但这种方法效率很低,目前通用的是uvm(Universal Verification Method,通用验证方法学)。对于个人开发者或者学生而言,需要快速搭建testbench用于vcs对自己的设计进行验证。

1,verilog testbench基本语法

完整的testbench测试文件结构为:

`timescale 仿真单位/仿真精度

module Test_bench();//通常无输入无输出

信号或变量声明定义
逻辑设计中输入对应 reg 型
逻辑设计中输出对应 wire 型
使用 initialalways 语句产生激励
例化待测试模块
监控和比较输出响应

endmodule

 对应的testbench典型架构如下:

 

 (1)声明一个没有输入输出的模块作为顶层

一般命名方法是在被测模块名后加上_tb

module <module_name> ();
    //在这里写testbench
endmodule 

(2)声明仿真单位和精度

<unit_time> 指定时间的单位,<resolution>则指定时间精度。

<resolution> 表示精度,可以使用小数来指定 verilog 代码中的延时。例如,如果设计人员想要 10.5ns 的延迟,就可以简单地写为 #10.5。因此,编译指令中的 <resolution> 决定了可以实现最小时间的步长(即精度)。

`timescale <unit_time> / <resolution>

(3)信号或者变量定义

parameter X=20;  //把常量定义为参数的形式方便全局修改

//reg define
reg A;
reg B;

//wire define
wire [3:0] C;

//通常initial或者always语句块中的变量定义为reg类型,在assign或者用于例化模块名的信号定义成wire类型

(4)例化被测模块

example_design dut (
    .clock (clk),
    .reset (reset),
    .a     (in_a),
    .b     (in_b),
    .q     (out_q)
);
//其中clk、reset、in_a等信号在(3)中需要定义

(5)生成时钟和复位信号

可以在initial块中为时钟和复位信号编写代码,然后使用延时运算符来实现信号状态的变化。以使用 forever 关键字在仿真期间持续运行时钟信号。使用此语法将每 1 ns 进行一次反转,从而实现500MHz 的时钟频率----选择此频率纯粹是为了实现快速仿真。

//生成时钟信号
initial begin
    clk = 1'b0;
    forever #1 clk = ~clk;
end
 
//生成复位信号
initial begin
   reset = 1'b1;
    #10
   reset = 1'b0;
end

下面为其他常用的时钟和复位设计:

/*----------------------------------------------------------------
时钟激励产生方法一: 50%占空比时钟
----------------------------------------------------------------*/
parameter ClockPeriod=10;
initial
    begin
        clk_i=0;
        forever
        #(ClockPeriod/2) clk_i=~clk_i;
    
    end
/*----------------------------------------------------------------
时钟激励产生方法二: 50%占空比时钟
----------------------------------------------------------------*/
initial
    begin
        clk_i=0;
        always #(ClockPeriod/2) clk_i=~clk_i;
    end
/*----------------------------------------------------------------
时钟激励产生方法四:产生固定数量的时钟脉冲
----------------------------------------------------------------*/
initial
    begin
        clk_i=0;
        repeat(6)
        #(ClockPeriod/2) clk_i=~clk_i;
    end
/*----------------------------------------------------------------
时钟激励产生方法五:产生非占空比为 50%的时钟
----------------------------------------------------------------*/
initial
    begin
        clk_i=0;
        forever
                begin
                    #((ClockPeriod/2)-2) clk_i=0;
                    #((ClockPeriod/2)+2) clk_i=1;
                end
    end
/*----------------------------------------------------------------
复位信号产生方法一:异步复位
----------------------------------------------------------------*/
initial
    begin
        rst_n_i=1;
        #100;
        rst_n_i=0;
        #100;
        rst_n_i=1;
    end
/*----------------------------------------------------------------
复位信号产生方法二:同步复位
----------------------------------------------------------------*/
initial
    begin
        rst_n_i=1;
        @(negedge clk_i)
        rst_n_i=0;
        #100; //固定时间复位
        repeat(10) @(negedge clk_i); //固定周期数复位
        @(negedge clk_i)
        rst_n_i=1;
    end
/*----------------------------------------------------------------
复位信号产生方法三:复位任务封装
----------------------------------------------------------------*/
task reset;
    input [31:0] reset_time; //复位时间可调,输入复位时间
    RST_ING=0; //复位方式可调,低电平或高电平
        begin
            rst_n=RST_ING; //复位中
            #reset_time; //复位时间
            rst_n_i=~RST_ING; //撤销复位,复位结束
        end
endtask

(6)文本输入输出

reg [a:0] data_mem [0:b];  //定义位宽为(a+1)深度为(b+1)的存储器
$readmemb/$readmemh("<读入文件名>",<存储器名>);
$readmemb/$readmemh("<读入文件名>",<存储器名>,<起始地址>);
$readmemb/$readmemh("<读入文件名>",<存储器名>,<起始地址>,<结束地址>);

$readmemb
/*------------------------------------------------------------------------*\
  读取二进制数据,读取文件内容只能包含:空白位置,注释行,二进制数
  数据中不能包含位宽说明和格式说明,每个数字必须是二进制数字。
\*------------------------------------------------------------------------*/

$readmemh
/*------------------------------------------------------------------------*\
  读取十六进制数据,读取文件内容只能包含:空白位置,注释行,十六进制数
  数据中不能包含位宽说明和格式说明,每个数字必须是十六进制数字.
\*------------------------------------------------------------------------*/
//==========================================================================
//==    输出txt文件
//==========================================================================
integer fp_write;                                  //定义
    initial  begin
        begin
            fp_write = $fopen("output.txt");  //打开输出文件
            begin
                $fwrite(fp_write, "\n%h", output_data); //写入数据16进制
                #(`clk_period);
            end
        end
        $fclose(fp_write);   //关闭文件,不可少
end

(7)打印信息

$monitor      //仿真打印输出,打印出仿真过程中的变量,使其终端显示
/*------------------------------------------------------------------------*\
  $monitor($time,,,"clk=%d reset=%d out=%d",clk,reset,out);
\*------------------------------------------------------------------------*/

$display      //终端打印字符串,显示仿真结果等
/*------------------------------------------------------------------------*\
  $display(” Simulation start ! ");
  $display(” At time %t,input is %b%b%b,output is %b",$time,a,b,en,z);
\*------------------------------------------------------------------------*/

$time         //返回 64 位整型时间

$stime        //返回 32 位整型时间

$realtime     //实行实时模拟时间

2,快速搭建soc验证平台

待学习

三,用VCS对设计进行debug

1,在testbench中使用系统函数

这在testbench基本结构中已经有所介绍,在编写verilog模块的testbench时,可以在里面使用一些verilog的系统函数,在运行simv文件跑仿真时,进行一些控制。例如:

  • $time 代表当前的仿真时间。
  • $display 类似C语言的printf函数,仿真时在终端上打印一些信息,比如一些变量的值。
  • $monitor 和$display类似,不同的是$display在被调用的时候打印一些信息,$monitor可以自动监测变量,当变量值发生变化时,便打印出信息。
  • $stop 调用时使仿真产生一次中断。
  • $finish 调用时使仿真结束。
  • $readmemh $readmemb 用于存储器建模时的初始化,$readmemb以二进制数的形式写入,$readmemh以十六进制数写入。

一般而言使用$readmemb较多,下面看一个例子:

`timescale 1ns/10ps
module myrom (read_data, addr, read_en_);
    input read_en_;
    input [3:0] addr;
    output [3:0] read_data;
    reg [3:0] read_data;
    reg [3:0] mem [0:15];
    initial
        $readmemb ("my_rom_data", mem); //将my_rom_data文件里的数据写入mem rom中
    always @( addr or read_en_)
        if (! read_en_)
            read_data = mem[addr];
endmodule

2,使用dve进行debug

新增了一个 initial 块。表示如果在编译时,定义了 DUMP_VPD 这个宏,那么在仿真时,打开 $vcdpluson() 这个开关选项。

initial begin          
`ifdef DUMP_VPD
       $vcdpluson();
`endif             
end

在linux命令行运行vcs时加上 +define+DUMP_VPD,则在仿真完成后,生成了 vcdplus.vpd 这个文件。这个文件记录了仿真过程中所有信号的波形,可以使用 dve 打开。通过 dve & 命令打开 dve, "&"的用途是后台打开dve,以免终端被占用。我们可以看到 dve 打开后界面为空白。File -> Open Database,选择 vpd 文件并打开,在Hierarchy 部分,可以查看顶层模块里面的子模块,右键 -> Add to Waves 查看对应模块的波形图。或者可直接使用命令: dve -vpd vcdplus.vpd & 后台打开 dve 并加载 vpd 文件。

 

四,验证覆盖率

在进行功能验证时,给设计添加激励信号,查看仿真结果,需要考虑覆盖率的问题。覆盖率分为代码覆盖率(code coverage)和功能覆盖率(function coverage)。

1.1功能覆盖率(user defined/用户自定义)

(1)作用:用于度量DUT中哪些功能或者特性被测试用例测试到;

(2)实现:使用sv等验证语言实现覆盖率模型(coverage model)或断言(assertion),并从大量的回归测试(regression)中采集覆盖数据(sample),然后进行统计数据的合并(merge),最后做覆盖率分析(analysis);

(3)侧重点:模块验证会更关心是否所有的功能都被触发,而系统验证会更关心是否耦合了各类工作场景;

(4)涉及点:sv功能覆盖率模型的实现,需要用到covergroup和coverpoint;

1.2代码覆盖率(仿真工具生成)

(1)分类:翻转覆盖率(toggle cov),行覆盖率(line cov),语句覆盖率(statement cov),分支覆盖率(branch cov),状态覆盖率(FSM cov);

(2)作用:通过对代码覆盖率的分析,可以较容易发现RTL中冗余的代码块,这种冗余的代码块可能来自于RTL实现逻辑的错误,或者验证计划不够完备;

(3)收集:由EDA工具在启用相应功能的选项之后自动化完成,不需要工程师开发额外的代码;

 

 对于代码覆盖率而言,通常需要考虑以下情况:

  • Line coverage :行覆盖率,检查语句是否被执行。
  • Toggle coverage:检查电路的每个节点是否都有 0 -> 1 和 1 -> 0 的跳变。这种检查通常会使仿真变慢很多。
  • conditional coverage:检查条件语句是否覆盖了所有的情况。 比如有时写了if 语句,没有写else语句。
  • FSM coverage: 状态机覆盖率,检查状态机所有的状态是否都到达过。
  • path coverage:在always语句块和initial语句块中,有时会使用 if ... else 和 case 语句,在电路结构上便会产生一系列的数据路径。检查这些路径的覆盖情况。

VCS在统计代码覆盖率的过程中,我们通常在编译和仿真命令上添加对应的开关选项,生成一个 .vdb文件记录覆盖率的情况。再使用dve打开该文件查看。

VCS有关覆盖率的常用选项如下:

  • -cm <coveragetype> :打开对应类型的覆盖率,例如 -cm cond+tgl+lin+fsm+path为统计上述所有覆盖率。可根据需要增减。
  • -cm_name:设置记录有覆盖率信息文件的名字。
  • -cm_dir:指定生成文件的目录。
  • -cm_log + filename.log:.log文件记录仿真过程中统计覆盖率的信息。用的比较少。
  • -cm_nocasedef: 在统计case语句的条件覆盖率时,不考虑default条件未达到的情况。
  • -cm_hier vcs_cov.cfg:通过.cfg文件(名字随便取)选择要查看覆盖率的模块/文件

“+”代表查看,“-”代表不查看。tree代表查看某个模块调用的子模块。

五,VCS后仿

当我们把所写的RTL进行的功能仿真通过之后,便输入到 Design Compiler工具中进行逻辑综合,逻辑综合的结果便是RTL代码转化为由与、或、非等门电路和触发器组成的电路,称为门级网表(netlist)。门级网表中便包含了电路的实际信息,例如逻辑门单元的扇入扇出系数,延迟等等。因此在逻辑综合完成之后,需要对网表再进行仿真验证,防止出现意想不到的错误。

在testbench中使用sdf_annotate()系统函数将sdf文件“反标”到设计中,第一个参数指定sdf文件,第二个参数指定反标到哪一层的module上,这里我们选择顶层文件。其他参数保持默认即可。

与前仿不一样的是后仿除了网表verilog文件,还需要工艺库verilog文件和sdf延时文件。

 

参考资料:

如何快速搭建模块验证平台 – 两位语言学家的相遇 (kellen.wang)

(12条消息) Verilog如何编写一个基础的Testbench_verilog中testbench怎么写_孤独的单刀的博客-CSDN博客

(12条消息) TestBench基本写法与语法详解_testbench怎么写_ZHE980121的博客-CSDN博客

(12条消息) Testbench编写常用语法和必备知识_青青豌豆的博客-CSDN博客

验证覆盖率及用仿真工具收集覆盖率(code cov/func cov) - _见贤_思齐 - 博客园 (cnblogs.com)

VCS入门教程(一) - 知乎 (zhihu.com)

VCS入门教程(二) - 知乎 (zhihu.com)

VCS入门教程(三) - 知乎 (zhihu.com)

VCS入门教程(四) - 知乎 (zhihu.com)

(11条消息) 使用VCS对电路进行后仿真_vcs 后仿_早睡身体好~的博客-CSDN博客

VCS - 三步仿真 - 知乎 (zhihu.com)

posted @ 2023-04-07 00:36  Haowen_Zhao  阅读(1423)  评论(0编辑  收藏  举报