基本结构
Verilog程序的基本结构如下:
1 | module MUX21a(a, b, s, y); |
上面是一个2选1多路选择器的Verilog描述。
具体来说,一个Verilog程序即为一个模块,所以以modle
开始,以endmodule
结束。同时模块具有端口列表以及端口列表声明(这两部分可以合并到端口列表中)。
随后则是Verilog程序正式逻辑功能部分。其可以用下面的形式表示:
数据类型
总的来说,Verilog中的数据类型可以被分为两大类:网线类(net)、变量类(variable)
Verilog 最常用的 2 种数据类型就是线网(wire)与寄存器(reg),其余类型可以理解为这两种数据类型的扩展或辅助。
因连续赋值语句和过程赋值语句的激活特点不同,故赋值目标特点也不同,前者不需要保存,后者需要保存,因此规定两种数据类型,net型用于连续赋值的赋值目标或门原语的输出,且仿真时不需要分配内存空间,variable用于过程赋值的赋值目标,且仿真时需要分配内存空间。
将一个信号定义成net型还是varible型,由以下两方面决定
- 使用何种赋值语句对该信号进行赋值,
- 如果是连续赋值或门原语赋值或例化语句赋值,则定义成net型(wire);
- 如果是过程赋值则定义成variable型(reg)。
- 对于端口信号来说,
- input信号和inout信号必须定义成net型(wire)的;
- output信号可以是net型的也可以是variable型(reg)的,决定于如何对其赋值(同a)。
网线类数据
网线类中的数据类型包含如下类型:
wire | 线类(使用最多,其他很少使用)。wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。 |
---|---|
wand | 线与 |
wor | 线或 |
tri | 三态 |
triand | 三态与 |
trior | 三态或 |
tri0 | 下拉电阻 |
tri1 | wire |
trireg | 电容性线网 |
supply0 | 地 |
supply1 | 电源 |
变量类型
Verilog语言中的逻辑值有四种:
1:逻辑1,高电平,数字1
0:逻辑0,低电平,数字0
x:不确定
z:高阻态
变量类中的数据类型包含如下类型:
reg | 寄存器型(使用最多,其他很少使用)。寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。 |
---|---|
integer | 整形 |
real | 实数型 |
time | 时间型 |
realtime | 实时间型 |
例如在 always 块中,寄存器可能被综合成边沿触发器,在组合逻辑中可能被综合成 wire 型变量。寄存器不需要驱动源,也不一定需要时钟信号。在仿真时,寄存器的值可在任意时刻通过赋值操作进行改写。
变量位宽
当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。例如:
1 | reg [3:0] counter ; //声明4bit位宽的寄存器counter |
Verilog 支持可变的向量域选择,例如:
1 | reg [31:0] data1 ; |
对信号重新进行组合成新的向量时,需要借助大括号。例如:
1 | wire [31:0] temp1, temp2 ; |
整数,实数,时间寄存器变量
整形
整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数。例如:
1 | reg [31:0] data1 ; |
实数
实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。例如:
1 | real data1 ; |
时间
Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。例如:
1 | time current_time ; |
数组
在 Verilog 中允许声明 reg, wire, integer, time, real 及其向量类型的数组。
数组维数没有限制。线网数组也可以用于连接实例模块的端口。数组中的每个元素都可以作为一个标量或者向量,以同样的方式来使用,形如:**<数组名>[<下标>]**。对于多维数组来讲,用户需要说明其每一维的索引。例如:
1 | integer flag [7:0] ; //8个整数组成的数组 |
常量
参数用来表示常量,用关键字 parameter 声明,只能赋值一次。例如:
1 | parameter data_width = 10'd32 ; |
字符串
字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。
字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。例如,为存储字符串 “run.runoob.com”, 需要 14*8bit 的存储单元:
1 | reg [0: 14*8-1] str ; |
总结
总的来说:
- 针对大部分的程序、wire和reg变量类型就够了。其他类型一般用的并不是很多,除非是比较高层的硬件程序设计。
- 要区分wire和reg的根本区别,即wire表示硬件单元之间的物理连线;reg表示存储单元。
- 因此在实际电路设计中,input和inout类型必须用wire型;output可以定义为reg,也可以定义为wire。
- assign语句中变量需要定义成wire型,使用wire必须搭配assign。
操作符
Verilog中的操作符与其他语言基本类似:
由于Verilog也具有整形等数据类型,因此也分逻辑操作符(如逻辑与&&)和按位逻辑操作符(如逻辑或||)。
比较关系符中,其结果可能是逻辑真(1),逻辑假(0),不确定(x)。
比较关系符中,除了经典的
==
和!=
,还有===
(case等于)以及!==
(case不等)。后两者中,返回结果只能是0或1,,对于x、z认为是确定的值,参加比较。Verilog中还有一些特殊的操作符,如拼接和复制拼接。
1
2
3
4Y= {4’b1001, 2’b11}; %100111
Y= {4{2’b01}}; %01010101
Y= {{4{2’b01}}, 2’b11};
赋值语句
赋值语句分为两种:连续赋值和过程赋值。
连续赋值
连续赋值语句是 Verilog 数据流建模的基本语句,用于对 wire 型变量进行赋值。例如:
1 | assign LHS_target = RHS_expression ; |
LHS(left hand side) 指赋值操作的左侧,RHS(right hand side)指赋值操作的右侧。
assign 为关键词,任何已经声明 wire 变量的连续赋值语句都是以 assign 开头,例如:
1 | wire Cout, A, B ; |
需要说明的是:
- LHS_target 必须是一个标量或者线型向量,而不能是寄存器类型。
- RHS_expression 的类型没有要求,可以是标量或线型或存器向量,也可以是函数调用。
- 只要 RHS_expression 表达式的操作数有事件发生(值的变化)时,RHS_expression 就会立刻重新计算,同时赋值给 LHS_target。
Verilog 还提供了另一种对 wire 型赋值的简单方法,即在 wire 型变量声明的时候同时对其赋值。wire 型变量只能被赋值一次,因此该种连续赋值方式也只能有一次。例如下面赋值方式和上面的赋值例子的赋值方式,效果都是一致的。
1 | wire A, B ; |
连续性赋值总是处于激活状态,任何操作数的改变都会影响表达式的结果
过程赋值
过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型。
连续性赋值总是处于激活状态,任何操作数的改变都会影响表达式的结果;过程赋值只有在语句执行的时候,才会起作用。这是连续性赋值与过程性赋值的区别。
Verilog 过程赋值包括 2 种语句:阻塞赋值与非阻塞赋值。
阻塞赋值
阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。
阻塞赋值语句使用等号 = 作为赋值符。
前面的仿真中,initial 里面的赋值语句都是用的阻塞赋值。
非阻塞赋值
非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。
非阻塞赋值语句使用小于等于号 <= 作为赋值符。
基本语句块
连续语句块
在Verilog模块中,除always语句块之外,assign语句赋值可以被看作是连续语句块。连续语句的特点即为连续语句块的特点。
- 语法上,有关键词“assign”来标识;
- 连续赋值语句不能出现在过程块中(initial/always);
- 连续赋值语句主要用来对组合逻辑进行建模以及线网数据间进行描述;
- 左侧被赋值的数据类型必须是线网型数据(wire)
- always语句中还可以使用if、case、for循环等语句,其功能更加强大。
过程语句块
过程语句块一般指always语句块。
其基本格式如下:
1 | always @(敏感信号条件表) |
例如:
1 | always @ (posedge CLK) |
特点:
- always语句本身不是单一的有意义的一条语句,而是和下面的语句一起构成一个语句块,称之为过程块;过程块中的赋值语句称过程赋值语句;
- 该语句块不是总处于激活状态,当满足激活条件时才能被执行,否则被挂起,挂起时即使操作数有变化,也不执行赋值,赋值目标值保持不变;
- 赋值目标必须是reg型的。
其中敏感信号条件表有两种类型:
- 边沿敏感
- 电平敏感
边沿敏感
边沿敏感是指当一个信号的上升或者下降沿到来时,语句块就被激活并开始执行。
其中,posedge
用来表示上升沿;negedge
用来表示下降沿。
例如:
1 | always @(posedge clk) |
电平敏感
电平敏感是指当信号列表中的人一个信号有变化时激活该语句块。
例如:
1 | always @(a, b, c) |
always语句块中如果有多条赋值语句必须将其用begin end包括起来,assign语句中没有begin end。
同时,上面操作符中提到的赋值操作符中有阻塞赋值和非阻塞赋值。所以这里的begin和end中的多条赋值语句采用不同的赋值符号也会进行不同的执行。如:
1 | begin |
当m=a*b 执行完才能执行y=m 。当m赋值完成后,才能执行y的赋值,y得到的是m的新值。
1 | begin |
m=a*b 和y=m并行执行 。m和y的赋值并行执行,y得到的是m的旧值。
值得注意的是:
- 设计组合电路时常用阻塞赋值;
- 设计时序电路时常用非阻塞赋值;
- 但不是绝对的。
- 不建议在一个always块中混合使用阻塞赋值和非阻塞赋值
完整案例
1 | module CNT4 (CLK,Q); |
基本语句
if语句
在用if语句设计“组合电路”时要注意,如果条件不完整,会综合出锁存器。
1 | % 使用else使分支完整 |
case语句
如果条件描述不完整,则会综合出寄存器;在设计组合电路时要注意使条件描述完整。加default语句可以使条件完整。如果条件描述完整也可以不加default语句。
1 | case (表达式) |
案例
1 | module MUX41 (a,b,c,d,s1,s0,y); |
描述风格
- 结构化描述(也称门级描述)(全部用门原语和底层模块调用)
- 数据流级描述(全部用assign语句)
- 行为级描述(全部用always语句配合if、case语句等)
实际描述是三种混合的,举例:用门级描述、数据流描述、行为描述分别设计数据选择器。
多路选择器的Verilog 门级描述
1 | module mux4_to_1 (out, i0, i1, i2, i3, s1, s0); |
多路选择器的Verilog 数据流描述
1 | module mux4_to_1 (out, i0, i1, i2, i3, s1, s0); |
多路选择器的Verilog 行为级描述
1 | module mux4_to_1(out, i0, i1, i2, i3, s1, s0); |
模块调用方法
模块1:
1 | module DFF(CLK,D,Q) |
调用模块1:
1 | module examp (clk,d,a,q) |
其中参数的传递方法也有两种,一种是具名参数传递,一种是顺序传递。
具名参数传递格式如下:
(.底层端口名1(外接信号名1),.底层端口名2(外接信号名2),…)
如:
1 | DFF dff1(.CLK(clk),.D(d1),.Q(q1)); |
顺序传递方法如下:
(外接信号名1,外接信号名2,…)
1 | DFF dff2(q1,d,q); |
原语调用
Verilog语言提供已经设计好的门,称为门原语(primitive,共12个),这些门可直接调用,不用再对其进行功能描述。
调用格式如下:
门原语名 实例名 (端口连接)
如:
1 | and (out, in1, in2); |
所有门原语如下:
- and:与
- or:或
- xor:异或
- nand:与非
- nor:或非
- xnor:同或
- not:非
- buf:缓冲器
- bufif1:控制端1有效缓冲器
- bufif0:控制端0有效缓冲器
- notif1:控制端1有效非门
- notif0:控制端0有效非门
1 | bufif1 b1 (out, in, ctrl); |
端口列表中前面是输出,中间是输入,最后是使能端,输出个数不限。