北航计算机组成原理课程设计-2020秋 【系列完结】Verilog或ISE高级特性与自动化测试

北航计算机学院-计算机组成原理课程设计-2020秋

PreProject-Verilog HDL 与 ISE

Verilog或ISE高级特性与自动化测试


本系列所有博客,知识讲解、习题以及答案均由北航计算机学院计算机组成原理课程组创作,解析部分由笔者创作,如有侵权联系删除。


从本节开始,课程组给出的教程中增添了很多视频讲解。为了避免侵权,本系列博客将不会搬运课程组的视频讲解,而对于文字讲解也会相应地加以调整,重点在于根据笔者自己的理解给出习题的解析。因此带来的讲解不到位敬请见谅。


编译预处理

Verilog HDL语言和C语言一样也提供了编译预处理的功能。所谓编译预处理,可以类比C语言中的#define等语句。例如:

#define BUFSIZE 256
char buf[`BUFSIZE];

Verilog HDL语言和C语言一样也提供了编译预处理的功能。在Verilog HDL语言中,为了和一般的语句相区别,这些预处理命令以符号"`“开头(位于主键盘左上角,其对应的上键盘字符为”~"。注意这个符号不同于单引号)。这些预处理命令的有效作用范围为定义命令之后到本文结束或到其他命令定义替代该命令之处。由于计组实验中用到的预处理命令不多,所以在此仅对`define, `timescale做相应介绍。

1. 宏定义`define

用一个指定的标识符(即名字)来代表一个字符串,它的一般形式为:

`define 标识符(宏名) 字符串(宏内容)

如:

`define signal string

它的作用是指定用标识符signal来代替string这个字符串,在编译预处理时,把程序中该命令以后所有的signal都替换成string。

例如:

`define WORDSIZE 8
module test reg[1:`WORDSIZE] data;
//相当于定义reg[1:8] data;
endmodule

注意在引用宏名时也要必须在宏名前加上符号"`"表明该名字是经过宏定义的名字。

2. 文件包含`include

所谓"文件包含"处理是一个源文件可以将另外一个源文件的全部内容包含进来,即将另外的文件包含到本文件之中。Verilog HDL语言提供了`include命令用来实现"文件包含"的操作。其一般形式为:

`include "文件名"

在编译的时候,需要对include命令进行"文件包含"预处理:将File2.v的全部内容复制插入到include "File2.v"命令出现的地方,即将File2.v被包含到File1.v中。在接着往下进行编译中,将"包含"以后的File1.v作为一个源文件单位进行编译。

例如:

// aaa.v
module aaa(a,b,out);
  input a, b;
  output out;
  wire out;
  assign out = a ^ b;
endmodule

// bbb.v
`include "aaa.v"
module bbb(c,d,e,out);
  input c,d,e;
  output out;
  wire out_a;
  wire out;
  aaa aaa(.a(c),.b(d),.out(out_a));
  assign out=e&out_a;
endmodule

在经过编译预处理后,文件bbb.v被处理为:

// bbb.v
module aaa(a,b,out);
  input a, b;
  output out;
  wire out;
  assign out = a ^ b;
endmodule
module bbb(c,d,e,out);
  input c,d,e;
  output out;
  wire out_a;
  wire out;
  aaa aaa(.a(c),.b(d),.out(out_a));
  assign out=e&out_a;
endmodule

关于"文件包含"处理的四点说明:

  1. 一个`include命令只能指定一个被包含的文件,如果要包含n个文件,要用n个`include命令。注意下面的写法是非法的`include"aaa.v"“bbb.v”

  2. `include命令可以出现在Verilog HDL源程序的任何地方,被包含文件名可以是相对路径名,也可以是绝对路径名。例如:`include"parts/count.v"

  3. 可以将多个`include命令写在一行,在`include命令行,只可以出空格和注释行。例如下面的写法是合法的。

    `include “fileB” `include “fileC” //including fileB and fileC

  4. 如果文件1包含文件2,而文件2要用到文件3的内容,则可以在文件1用两个`include命令分别包含文件2和文件3,而且文件3应出现在文件2之前。


补充:宏文件的引用

在具体代码实践中,我们定义的宏常常在多个文件中都需要使用。如果在每个文件中都采用复制粘贴的方式,那么在修改的时候就会有很大的工程量,所以我们常常单独开一个文件进行宏的定义,在其他文件中对这个“宏文件”进行引用。

// macro.v
`define WORDSIZE 8

// test1.v
`include "macro.v"

module test1 reg[1:`WORDSIZE] data;
  // ...
endmodule

// test2.v
`include "macro.v"

module test2 reg[1:`WORDSIZE] data;
  // ...
endmodule

macro.v是我们定义的一个“宏文件”,在其他需要使用到这个“宏文件”的文件头部,使用include "macro.v"就可以使用“宏文件”中定义的宏了。

3. 时间尺度`timescale

`timescale命令用来说明跟在该命令后的模块的时间单位和时间精度。使用`timescale命令可以在同一个设计里包含采用了不同的时间单位的模块。例如,一个设计中包含了两个模块,其中一个模块的时间延迟单位为纳秒(ns),另一个模块的时间延迟单位为皮秒(ps)。EDA工具仍然可以对这个设计进行仿真测试。

`timescale命令的格式如下:

`timescale[时间单位]/[时间精度]

例如:

`timescale 1ns/1ps;

在这个命令之后,模块中所有的时间值都表示是1ns的整数倍。这是因为在`timescale命令中,定义了时间单位是1ns。模块中的延迟时间可表达为带3位小数的实型数,因为`timescale命令定义时间精度为1ps。

4. 条件编译命令`ifdef, `else, `elsif, `endif, `ifndef

和C语言类似,这些条件编译编译指令用于包含Verilog HDL的可选行编译期间的源描述。 `ifdef编译器指令检查text_macro_name的定义,如果定义了text_macro_name,那么`ifdef指令后面的行被包含在内。如果未定义text_macro_name并且存在`else指令,则编译`else后的源描述。

`ifndef编译器指令检查text_macro_name的定义。如果未定义text_macro_name,则包含`ifndef指令后面的行。如果定义了text_macro_name并且存在`else指令,则编译`else后的源描述。如果`elsif指令存在(注意不是`else),编译器会检查text_macro_name的定义。如果定义存在,则包含`elsif指令后面的行。

`elseif指令等同于编译器指令序列`else `ifdef …`endif。该指令不需要相应的`endif指令。该指令必须以`ifdef或`ifndef指令开头。

例如:

module and_op (a, b, c);
output a;
input b, c;
`ifdef behavioral
  wire a = b & c;
`else
  and a1 (a,b,c);
`endif
endmodule

大家可以充分利用条件编译指令来灵活控制自己的代码。


系统任务

Verilog中还提供了很多系统任务,类似于C中的库函数,使用这些系统任务可以方便地进行测试。由于计组实验中用到的系统任务相对较少,所以在此仅对$display, ​$monitor, ​$readmemh进行介绍。

1. $display

格式:$display(p1,p2,…,pn);

这个系统任务的作用是用来输出信息,即将参数p2到pn按参数p1给定的格式输出。用法和C语言中的printf类似。下面用一个例子简单介绍其用法。

例如:

module disp;
    reg[4:0] a;
    reg[4:0] b;
    initial begin
        a = 10;
        b = 20;
        $display("a = %d,b = %d\n",a,b);
    end
endmodule

其输出结果为: a = 10,b = 20

其中%d是以十进制的形式输出,\n为换行符。

在此说明几种常用的输出格式:

输出格式说明
%h或%H以十六进制数的形式输出
%d或%D以十进制数的形式输出
%b或%B以二进制数的形式输出
%c或%C以ASCII码字符的形式输出
%s或%S以字符串的形式输出
2. $monitor

格式: $monitor(p1,p2,…,pn);

$monitor;

$monitoron;

$monitoroff;

任务$monitor提供了监控和输出参数列表中的表达式或变量值的功能。其参数列表中输出控制格式字符串和输出列表的规则和​$display中的一样。当启动带有一个或多个参数的​$monitor任务时,仿真器则建立一个处理机制,使得每当参数列表中变量或表达式的值发生变化时,整个参数列表中变量或表达式的值都将输出显示。如果同一时刻,两个或多个参数的值发生变化,则在该时刻只输出显示一次。

$monitoron和​$monitoroff任务的作用是通过打开和关闭监控标志来控制监控任务​$monitor的启动和停止,这样使得程序员可以很容易地控制​$monitor何时发生。其中​$monitoroff任务用于关闭监控标志,停止监控任务​$monitor,​$monitoron则用于打开监控标志,启动​$monitor监控任务。$monitor与​$display的不同处还在于​$monitor往往在initial块中调用,只要不调用​$monitoroff,$monitor便不间断地对所设定的信号进行监视。

3. $readmemh

格式: $readmemh(“<数据文件名>”,<存储器名>); ​$readmemh(“<数据文件名>”,<存储器名>,<起始地址>); $readmemh(“<数据文件名>”,<存储器名>,<起始地址>,<结束地址>);

功能: $readmemh函数会根据绝对/相对路径找到需要访问的文件,按照ascii的解码方式将文件字节流解码并读入容器。文件中的内容必须是十六进制数字0~f或是不定值x,高阻值z(字母大小写均可),不需要前导0x,不同的数用空格或换行隔开。假设存储器名为arr,起始地址为s,结束地址为d,那么文件中用空格隔开的数字会依次读入到arr[s],arr[s+1]…到arr[d]。假如数字的位数大于数组元素的位数,那么只有低位会被读入,剩下的高位会被忽略。

此系统任务用来从文件中读取数据到存储器中,类似于C语言中的fread函数。

例如:

module im;
    reg[31:0] im_reg[0:2047];
    initial begin
            $readmemh("code.txt",im_reg);
    end
endmodule

仿真后即可将code.txt中的内容读入im_reg存储器中。


层次化事件队列

下文内容需要本节前面知识的铺垫,请掌握前面内容再做拓展了解。(层次化事件队列是一个用于仿真硬件行为时所广泛使用的模型和规则,对其了解将有助于理解Verilog中阻塞和非阻塞赋值在仿真时行为差异的真正原因,此处对层次化事件队列仅做简要介绍,部分内容有删减,详情请参见《Verilog数字系统设计教程》第4版14.1-14.3 P194-p198)

在前面的视频内容中,我们通过实例分析了阻塞赋值与非阻塞赋值的差异,细心的同学可能注意到了,在Verilog的语法中,以begin-end为开头结尾的代码块被称作顺序块,也就是说从细节上理解-其中代码执行是按顺序进行的,但是实例中我们又称always块中的非阻塞赋值是“并发执行”的,从概念上似乎出现了矛盾。其实,“并发执行”并不是运行规则,而是运行规则作用后的外在效果,真正的规则实际上是Verilog代码运行时的层次化事件队列。

同学们应该清楚,Verilog是硬件描述语言,语句的执行顺序与C语言程序有很大差异。层次化事件队列是硬件仿真(Simulation)时,用于规定“不同事件执行的优先级关系”,在这里我们可以一般将一个事件理解为需要运行的一条语句(当然,有的语句由多个事件组成,例如非阻塞赋值需要被拆分为计算等号右边的值(RHS)和将结果赋予等号的左边变量(LHS),赋值事件在计算事件执行结束时才能加入队列。根据事件的优先级,Verilog将其分为4个队列**(队列间的优先级不同,从上到下优先级依次递减,只有当优先级高的队列中所有任务完成后,才会继续完成优先级较低的任务)**

  1. 动态事件队列(动态事件队列在队列内部执行顺序无硬性规定,但在同一个begin-end语句块中的语句应当严格按照源代码中的顺序执行;且多个非阻塞赋值应当按照语句执行顺序进行)
    • 阻塞赋值
    • 计算非阻塞赋值语句右边的表达式(RHS)
    • 连续赋值(如assign)
    • 执行display命令
    • ……
  2. 停止运行的时间队列(#0)(不推荐使用)
  3. 非阻塞事件队列:更新非阻塞赋值语句LHS(左边变量)的值。
  4. 监控事件队列(执行monitor,strobe命令)

在大致了解4个队列后,同学们不妨尝试回答下列几个问题,加深理解:

  1. 为何“阻塞与非阻塞的区别”视频教程的代码中,阻塞赋值与非阻塞赋值最终的结果存在不同(请对比两种赋值方式在更新等号左边变量操作(LHS)的优先级)?

    在这里插入图片描述

    在阻塞赋值的always块中,两项阻塞赋值同属动态事件队列,应严格按照书写在顺序块中的顺序执行,因此先执行将a的值赋给b_blocked,再执行将b_blocked的值赋给c_blocked,赋值过后三个变量的值都是a的值。

    在非阻塞赋值的always块中,两项非阻塞赋值都要分别经过计算右边表达式(RHS)和赋值给左边变量(LHS)两个过程,而两项RHS属于动态事件队列,两项LHS属于非阻塞事件队列,应当先执行两项RHS,再执行两项LHS。两项RHS执行过后,应当赋给左侧两个变量的值分别是a的初值和b_non_blocked的初值,因此赋值过后b_non_blocked和c_non_blocked的新值是不一样的。

  2. 有时在always语句块中,使用display、strobe、monitor三种输出语句会得到不同结果,这是为什么?(例如下面的代码,同一时刻的display和monitor所输出的值为何相差1?)

    在这里插入图片描述

    0 monitor out =  0
    20 display out =  0
    20 monitor out =  1
    60 display out =  1
    60 monitor out =  2
     ……
    

    display和monitor语句都会监测out变量的变化,但display属于动态事件队列,而monitor属于监控事件队列。在always块中,几个事件的执行优先级由高到低依次是:计算右侧表达式out + 1(RHS)、display、向左侧变量out赋值(LHS)、monitor。

    非阻塞赋值语句out <= out + 1的计算右侧表达式(RHS)属于动态事件队列,首先计算结果,但并没有赋值给左侧的变量,此时display执行,打印的是out的旧值,随后新值被赋给out,最后monitor执行,打印的是out的新值,因此在同一时间monitor打印的数值始终比display更新。

  3. 在一个always语句块中同时使用阻塞赋值与非阻塞赋值存在什么风险(提示:请结合同一事件队列中的任务顺序无硬性规定这一特点)?由这个结论,在书写语句块时应该遵循哪些规范?

    在同一always语句块中,若单纯使用阻塞赋值或单纯使用非阻塞赋值,其执行流程是一致的,由于同一个顺序块中要严格按照代码先后执行,一般不会出现赋值错误的问题。但如果混用阻塞赋值和非阻塞赋值,由于阻塞赋值属于动态事件队列,非阻塞赋值的RHS属于动态事件队列,而LHS属于优先级较低的非阻塞复制队列。在动态事件队列中,一条阻塞赋值语句和一条RHS的顺序无硬性规定,这就可能导致赋值的顺序不受源码约束,造成无法预料的错误。

    因此,在书写语句块时应注意不能混用阻塞赋值和非阻塞赋值。


DEFAULT NETTYPE

在我们的编写过程中,如果不对某个变量显式声明而直接使用,该变量将会被隐式创建为缺省类型

module test(input temp);
    wire [3:0] a;
    assign a=4'b1000;
    assign b=a;
endmodule            

打开仿真,会发现b的取值为0,是一个位宽为1的变量。

Verilog的默认缺省类型是wire,如果我们不对某个变量显式声明类型或显式定义,而直接使用,该变量将会被默认地设为wire类型。上述示例中temp和b都是wire类型。

如果在使用该变量之后再对该变量显式声明,则变量的类型以之后显式声明的类型为准。

module test(input wire[3:0] temp);
    wire [3:0] a;
    assign a=4'b1000;
    assign b=a;
    wire [3:0] b;
endmodule                

在这个示例中,b的值变成了4’b1000。在语法检查或仿真时,可以看到编译器产生了如下警告:

WARNING:HDLCompiler:35 - “test.v” Line N: <b> is already implicitly declared earlier.

这一语法特性容易造成一个问题:如果我们在对模块进行连接的时候,忘记对于某一个变量进行定义,或将变量名打错,如将alu打成aiu,则该变量将会默认被定义为1位宽的wire类型,造成意料之外的bug。

这时有一种解决方法:使用`default_nettype。

module test(input temp);
    wire [3:0] a;
    assign a=4'b1000;
    assign b=a;
endmodule
`default_nettype none            

`default_nettype用于设置缺省类型,在代码文件的任意位置加入`default_nettype宏,都可以使得该代码文件中所有变量的缺省类型改变。若代码中有两个以上的`default_nettype宏,则将会以最后一条为准。

若需要取消缺省类型,即若不显式声明类型就会报错,则应该使用`default_nettype none。以上的示例将会由于temp和b都未显式指定类型而报错。


函数

函数用关键词function声明,并用endfunction 结束,不允许输出端口声明(包括输出和双向端口) ,但可以有多个输入端口。函数只返回一个值到函数被调用的位置,并且在函数中返回值与函数名同名。

函数的定义如下所示:

function [range] function_id; input_declaration other_declarations procedural_statement endfunction

在使用函数时有以下几点需要注意:

  1. 函数定义只能在模块中完成,不能出现在过程块中;
  2. 函数至少要有一个输入端口;不能包含输出端口和双向端口;
  3. 在函数结构中, 不能使用任何形式的时间控制语句 (#、 wait 等) , 也不能使用 disable中止语句;
  4. 函数定义结构体中不能出现过程块语句(always 语句) ;
  5. 函数内部可以调用函数,但不能调用任务。

一个简单的例子:

module comb15 (A, B, CIN, S, COUT);
  input [3:0] A, B;
  input CIN;
  output [3:0] S;
  output COUT;

  wire [1:0] S0, S1, S2, S3;

  function signed [1:0] ADD;

    input A, B, CIN;

    reg S, COUT;

    begin
      S = A ^ B ^ CIN;
      COUT = (A&B) | (A&CIN) | (B&CIN);
      ADD = {COUT, S};
    end
  endfunction

  assign S0 = ADD (A[0], B[0], CIN),
  S1 = ADD (A[1], B[1], S0[1]),
  S2 = ADD (A[2], B[2], S1[1]),
  S3 = ADD (A[3], B[3], S2[1]),
  S = {S3[0], S2[0], S1[0], S0[0]},
  COUT = S3[1];
endmodule

在函数调用中,有下列几点需要注意:

  1. 函数调用可以在过程块中完成,也可以在 assign 这样的连续赋值语句中出现。
  2. 函数调用语句不能单独作为一条语句出现,只能作为赋值语句的右端操作数。

IVerilog与自动化测试

除了庞大的ISE进行CPU的仿真,我们还可以利用IVerilog这款轻量级的仿真软件运行我们verilog文件进行仿真。(注意⚠️:最终仿真模拟的结果依旧以ISE为准,IVerilog在某些情况下会与ISE产生不同的结果

IVerilog下载地址:点击这里

除了体量小速度快的特点,iverilog还可以在命令行中进行功能仿真,我们在Windows中利用bat批处理文件,或者linux中使用shell进行批处理文件的编写,利用几条命令,就可以完成verilog的仿真和其标准输出的转储操作。

以下是可能用到的命令:

  1. iverilog -o [target_file] source_file...

    将源文件编译为vvp格式的目标文件,利用vvp文件,可以启动verilog文件的功能仿真。

  2. vvp target_file

    运行vvp格式的目标文件,运行后vvp命令将输出功能仿真时产生的各个输出信息。此时可以使用管道或重定向保存输出信息,以便于我们进行下一步的对拍。

有关iverilog的其他命令可查看官网自行学习。

Iverilog在大部分情况下都与ISE的结果相同,但是在使用中发现,在always @*块中使用阻塞语句,将会导致二者的结果出现不同,因此,最好按照标准使用非阻塞语句。其他情况下基本二者行为一致,但测评依旧以ISE的输出为准。

补充:

我们在ISE中查看波形图,不是感到运行太慢、电脑吃不消,就是时不时的ISE会产生一些未知的BUG,在这里,我们推荐gtkwave和Scansion两款波形查看软件,他们同样体量较小,运行速度快,并且在多个平台都可运行,如果你是MacOS系统,不妨试一试他们。


Mars与自动化测试

Mars在我们平时的MIPS代码模拟运行时有着巨大的作用,同时,我们可以利用Mars的输出同我们的verilog工程文件的仿真输出结果进行对比,从而进行测试。为了使我们不依靠眼睛,利用代码自动化的进行测试,我们需要取出Mars在模拟运行中产生的输出信息,因此命令行成为了我们唯一的选择。

我们在Mars中点击Help或者按“F1”进入帮助页面后,选择选项卡的“MARS”->“Command”即可找到需要的所有命令行参数。

如下为常用命令行参数:

Option描述
a仅编译,不模拟。
ascii存储器或寄存器的内容被解释为ASCII码显示。(可选的选项是’dec’和’hex’)
bbrief - 不显示寄存器/内存地址和内容。
dbMIPS 延迟槽被开启。
dump在文件中转储内存内容。有三个可选的参数,例如:dump <segment> <format> <file>。目前支持的段为 .text.data。同样支持一个范围内的地址 (详见下面的 m-n)。当前支持的转储格式有BinaryHexTextBinaryTextAsciiText
ic显示指令计数,MIPS被执行的基础指令的个数。
mc设置内存配置。选项有1个参数,例如 mc <config>。参数 <config> 区分大小写,其可能的值为默认32位地址空间的Default,数据段位于地址0的32KB地址空间的CompactDataAtZero 或者文本段位于地址0的32KB地址空间的CompactTextAtZero
nc版权声明将不显示。用于重定向或管道程序输出。
n其中n是要模拟的执行步骤的最大整数计数。如果0、负数或未指定,则没有最大值。
$reg其中reg是寄存器的编号或名称(例如5、t3、f10),其内容将在运行结束时显示。偶数编号的浮点寄存器同时显示浮点和双精度。选项可以重复。注意:根据您的命令shell,您可能需要转义 ∗ ∗ 。 例 如 ∗ ∗ **。例如** * $t3*
reg_name其中reg_name是寄存器的名称(例如t3、f10),其内容将在运行结束时显示。偶数编号的浮点寄存器同时显示浮点和双精度。选项可以重复。不需要$。
m-n内存地址范围从mn,其内容将在运行结束时显示。mn可以是十进制或十六进制(以“0x”开头),m<=n,两者都必须在字边界上。选项可以重复。

我们将在以下讲解常用命令:

  1. java -jar mars.jar mc CompactDataAtZero a dump .text HexText hexcode.txt fibonacci.asm,将MIPS文件fibonacci.asm编译但不运行,选取其中的.text段生成HexText(即十六进制ASCII文件)写入hexcode.txt中。这在我们后续将代码和数据填入verilog中有很大的作用。

    mc为内存配置设置,CompactDataAtZero即为我们需要的内存配置。

    a为编译但不模拟。

    dump为导出内存信息,.text选择了导出的内存字段,HexText为十六进制的ASCII格式。

  2. java -jar mars.jar db mc CompactDataAtZero nc fibonacci.asm,将MIPS文件fibonacci.asm编译并运行。

    db为开启delay branch。

    nc为禁止显示版权信息,此处为了获得干净的输出信息。

若有其他需求可自行查找help中的信息。同时,可根据自己的需要修改Mars命令行参数和输出信息,在源代码中修改后编译打包即可运行。


至此,北航计算机组成原理课程设计前导学习Verilog有关的章节全部更新完毕了。后续的9个迭代开发的课程实验,学校网站在课程结束后隐藏了其入口,加之本人后续的学业安排,将不会继续更新具体的CPU设计章节。但我相信,入门Verilog之后,阅读计算机组成相关的书籍,亲手实践完成其CPU模块设计,应当会更加如鱼得水。

接下来,我将把学习和介绍的重点转向机器学习以及其硬件加速上。具体将分为三个模块更新博客:

  • 西瓜书(《机器学习》周志华)学习笔记

  • 深度学习框架使用和经典网络的了解和复现

  • Vivado硬件设计实现神经网络的硬件加速

如果您阅读到这里,那么由衷地感谢您对我的关注和支持,我将继续分享更多经验,希望大家共同进步!

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值