将陆续上传本人写的新书《自己动手写处理器》(尚未出版),今天是第六篇,我尽量每周四篇
2.3 Verilog HDL简介
本书实现的OpenMIPS处理器是使用Verilog HDL编写的,所以本章接下来的几节将介绍Verilog HDL的一些基本知识,包括语法、结构等。因为本书并不是一本讲授Verilog HDL的专门书籍,所以此处介绍的内容并不是Verilog HDL的全部,只是一些基础知识,以及在OpenMIPS处理器实现过程中会使用到的知识。读者如果对Verilog HDL有进一步了解的需求,可以参考相关书籍,这方面有许多非常优秀的书籍。笔者推荐《数字系统设计与Verilog HDL(第4版)》,本章关于Verilog
HDL的介绍也部分参考了该书。
Verilog HDL由GDA(Gateway Design Automation)公司的Phil Moorby于1983年首创,之后,Moorby又设计了Verilog-XL仿真器,Verilog-XL仿真器大获成功,也使得Verilog HDL得到推广使用。1989年,Cadence收购了GDA,1990年,Cadence公开发布了Verilog HDL,并成立了OVI(Open Verilog International)组织,专门负责Verilog HDL的发展。由于Verilog
HDL具有简洁、高效、易用、功能强等优点,逐渐为众多设计者所接受和喜爱。其发展经历了几个关键节点。
- 1995年,Verilog HDL成为IEEE标准,称为IEEE Standard 1364-1995(Verilog-1995)。
- 2001年,IEEE Standard 1364-2001(Verilog-2001)获得通过,其对Verilog-1995做了扩充和增强,另外,修改了一些语法结构,使之更易于使用。
- 2002年,为了使综合器输出的结果和基于IEEE Standard 1364-2001的仿真和分析工具的结果相一致,推出了IEEE 1364[1].1-2002标准,其对Verilog HDL的RTL级综合定义了一系列的建模准则。
- 2005年,Verilog HDL再次进行了更新,即IEEE Standard 1364-2005(Verilog-2005)。该版本只是对上一版本的细微修正。这个版本还包括了一个相对独立的新部分,即Verilog-AMS (Analog and Mixed-Signal),这个扩展使得传统的Verilog HDL可以对集成模拟和混合信号的系统进行建模。
Verilog HDL具有下述特点。
(1)Verilog HDL是在C语言的基础上发展而来的,就语法结构而言,Verilog HDL继承了C语言的很多语法结构,两者有许多相似之处。
(2)既适于可综合的电路设计,也可胜任电路与系统的仿真。
(3)能在多个层次上对所设计的系统加以描述,从开关级、门级、寄存器传输级(RTL)到行为级,都可以胜任,同时Verilog HDL不对设计规模施加任何限制。
(4)灵活多样的电路描述风格,可进行行为描述,也可进行结构描述;支持混合建模,一个设计中的各个模块可以在不同的设计层次上建模和描述。
(5)内置多种基本逻辑门,如and、or和nand等,可方便的进行门级结构描述;内置多种开关级元件,如pmos、nmos和cmos等,可进行开关级的建模。
(6)用户定义原语(UDP)创建的灵活性。用户定义的原语既可以是组合逻辑,也可以是时序逻辑;通过编程语言接口(PLI)机制可进一步扩展Verilog HDL语言的描述能力。
2.4 Verilog HDL中模块的结构
Verilog程序的基本设计单元是“模块”(Module),一个模块有其特定的结构,如图2-6所示。Verilog的模块完全定义在module与endmodule关键字之间,每个模块包括四个主要部分:模块声明、端口定义、数据类型说明和逻辑功能描述。
如下是一个实现32位加法器的模块。有两个输入信号in1、in2,两者相加的结果通过out输出。
module add32(in1, in2, out); // 模块声明 input in1, in2; // 端口定义,此处是输入端口 output out; // 端口定义,此处是输出端口 wire[31:0] in1, in2, out; // 数据类型说明,此处都是wire型 assign out = in1 + in2; // 逻辑功能描述 endmodule
下面结合该加法器的例子,对Module的基本结构进行说明。
1、模块声明
模块声明包括模块名字,以及输入、输出端口列表,其格式如下。
module 模块名(端口1, 端口2, 端口3……);
2、端口定义
明确说明模块端口的方向(输入、输出、双向等),其格式如下。
input 端口1, 端口2, 端口3 ……; // 输入端口 output 端口1, 端口2, 端口3 ……; // 输出端口 inout 端口1, 端口2, 端口3 ……; // 双向端口
3、数据类型说明
对模块中所有用到的信号(包括端口信号、节点信号等)都必须进行数据类型的定义。Verilog HDL提供了各种信号类型,下面是几种定义信号类型的例子。各数据类型的具体含义将在2.5.2节详述。
reg a; // 定义信号a的数据类型为reg型 wire[31:0] out ; // 定义信号out的数据类型为32位wire型
对于端口,可以将数据类型说明与端口定义放在一条语句中完成,于是,上文的32位加法器可以改为如下形式。
module add32(in1, in2, out); input wire[31:0] in1, in2; // 将端口定义与类型说明放在一条语句 output wire[31:0] out; assign out = in1 + in2; endmodule
对于端口,还可以将端口定义、数据类型说明都放在模块声明中,而不再放在模块内部,于是,上文的32位加法器还可以改为如下形式,更为简便。
// 将端口定义、数据类型说明放在模块声明中 module add32(input wire[31:0] in1, input wire[31:0] in2, output wire[31:0] out); assign out = in1 + in2; endmodule
4、逻辑功能描述
模块中最核心的部分就是逻辑功能描述,可以有多种方法在模块中描述和定义逻辑功能。几种基本方法如下,将在2.6节详述。
- 用assign持续赋值语句定义
- 用always过程块定义
- 调用元件(也称为元件例化)
2.5 Verilog HDL基本要素
2.5.1 常量
Verilog中的常量(Constant)有三种:整数、实数、字符串。在OpenMIPS的实现过程中只使用到了整数常量,所以,此处也仅介绍整数常量。其格式如图2-7所示。
一些整数常数的例子如下。
8'b11000101 // 宽度为8位的二进制数,数值为11000101,等于十进制的197 8'h8a // 宽度为8位的十六进制数,数值为8a,等于十进制的138 5'o27 // 宽度为5位的八进制数,数值为27,等于十进制的23 4'd10 // 宽度为4位的十进制数,数值为10
如果没有明确指明进制,那么默认是十进制。
2.5.2 变量声明与数据类型
Verilog中变量声明的格式如图2-8所示。只有数据类型、变量名是必要的,其他部分都可以省略。如果省略符号和位宽,那么根据数据类型设置为默认值。如果省略元素数,那么默认声明元素数为1。
数据类型可以是net型、variable型,分别介绍如下。
1、net型变量
net型相当于硬件电路中各种物理连接,其特点是输出的值紧跟输入值的变化而变化。net型变量包括多种类型,如表2-1所示。
本书在实现OpenMIPS处理器的时候只使用到了其中的wire类型。wire是最常用的net型变量,Verilog HDL模块中的输入、输出信号在没有明确指定数据类型时,都被默认为wire型。wire型信号可以用作任何表达式的输入,也可以用作assign语句和实例元件的输出,如前文中的32位加法器对out信号的赋值。对于综合器而言,wire型变量的取值可为0、1、X、Z,其中0表示低电平、逻辑0;1表示高电平、逻辑1;X表示不确定或未知的逻辑状态;Z表示高阻态。如果没有赋值,默认为高阻态Z。
2、variable型变量
variable型变量是可以保存上次写入数据的数据类型,一般对应硬件上的一个触发器或锁存器等存储元件,但并不是绝对的,在综合器综合的时候,会根据其被赋值的情况来具体确定是映射成连线还是映射为存储元件。variable型变量也包括多种类型,如表2-2所示。本书在实现OpenMIPS处理器的时候只使用到了其中的reg类型。
variable型变量必须在过程语句(initial或always)中实现赋值,这种赋值方式称为过程赋值。将在2.6节详述。
2.5.3 向量
图2-8变量声明格式中的位宽如果为1,那么对应的变量为标量,如果不为1,那么对应的变量为向量,默认为标量。向量的位宽用下面的形式定义。
[MSB : LSB]
冒号左边的数字表示向量的最高有效位MSB(Most Significant Bit),冒号右边的数字表示向量的最低有效位LSB(Least Significant Bit)。例如。
wire [3:0] bus; // 4位的wire型向量bus,其中bus[3]是最高位,bus[0]是最低位 reg [31:5] ra; // 27位的reg型向量ra,其中ra[31]是最高位,ra[5]是最低位 reg [0:7] rc; // 8位的reg型向量rc,其中rc[0]是最高位,rc[7]是最低位
向量有两种,一种是向量类向量,一种是标量类向量,可以使用关键字区分,如下。
wire vectored [7:0] databus; // 使用关键字vectored,表示是向量类向量 reg scalared [31:0] rega; // 使用关键字scalared,表示是标量类向量
如果没有明确指出,那么默认是标量类向量。本书也只用到了标量类向量,对标量类向量可以任意选中其中的一位或相邻几位,分别称为位选择(bit-select)和域选择(part-select)。例如。
A = rega[6]; // 位选择,将向量rega的其中一位赋值给变量A B = rega[5:2]; // 域选择,将向量rega的第5、4、3、2位赋值给变量B
在OpenMIPS的实现过程中,使用到了存储器,存储器可看做是二维的向量。如下就是一个存储器的定义,定义了一个深度为64,每个元素宽度为32bit的存储器。
reg [31:0] mem[63:0]; // mem是深度为64,字长为32bit的存储器
2.5.4 运算符
Verilog HDL中定义的运算符包括:算术运算符、逻辑运算符、位运算符、关系运算符、等式运算符、缩位运算符、移位运算符、条件运算符和位拼接运算符。详情如表2-3所示。
表2-3中的大部分运算都很好理解,本书不再详释,只做如下几点说明。
(1)等式运算符中的“==”与“===”的区别是:对于“==”运算,参与比较的两个操作数必须逐位相等,其结果才为1,如果某些值是不定态X或高阻态Z,那么得到的结果是不定值X;而对于“===”运算,则要求对参与运算的操作数中为不定态X或高阻态Z的位也进行比较,两个操作数必须完全一致,其结果才为1,否则结果为0。例如。
reg [4:0] a = 5'b11x01; reg [4:0] b = 5'b11x01;
针对上面的a、b,“a==b”的返回结果为X,而“a===b”的返回结果为1。
(2)缩位运算符与位运算的运算符号、逻辑运算法则都是一样的,但是缩位运算符是对单个操作数进行与、或、异或的递推运算,它放在操作数的前面,能够将一个矢量减为一个标量。例如。
reg [3:0] a; b = &a; // 等效于b = ((a[0] & a[1]) & a[2]) & a[3]
而位运算需要对两个操作数按对应位分别进行逻辑运算,例如。
wire [3:0] a = 4'b0011; wire [3:0] b = 4'b0101; 那么a&b = 4'b0001,a|b = 4'b0111
(3)位拼接运算符:用来将两个或多个信号的某些位拼接起来。其格式如下。
{比特序列0, 比特序列1,…… }
例如,在进行加法运算时,可将和与进位输出拼接在一起使用。
input [3:0] ina,inb; // 加法输入 output [3:0] sum; // 加法的和 output cout; // 进位 assign {cout, sum} = ina + inb; // 将和与进位拼接在一起
位拼接还可以用来重复信号的某些位,其格式如下。
{重复次数{被重复数据}}
利用上面的功能,可以实现对信号的符号扩展,例如。
//将Data的符号位进行扩展,s_data = {Data[7],Data[7],Data[7],Data[7],Data} wire [7:0] Data; wire [11:0] s_data; s_data = {{4{Data[7]}},Data};
(4)运算符的优先级如图2-9所示。
自己动手写处理器之第二阶段(2)——Verilog HDL简介