硬件逻辑设计(Verilog)复习知识点
- FPGA设计方法和Verilog基本知识
- 状态机(FSM)设计
- 实验提示
- Verilog设计方法
- Verilog 基础语法
- Verilog数值表示
- Verilog数据类型
- Verilog表达式
- Verilog编译指令
- Verilog连续赋值
- Verilog时延
- Verilog过程结构
- Verilog过程赋值
- Verilog时序控制
- Verilog语句块
- Verilog条件语句
- Verilog多路分支语句
- Verilog循环语句
- Verilog过程连续赋值
- Verilog模块与端口
- Verilog模块例化
- Verilog带参数例化
- Verilog函数
- Verilog任务
- Verilog状态机
- Verilog竞争与冒险
- Verilog书写规范
- Verilog仿真激励
- Verilog流水线
- 8月3日作业-FPGA设计方法和Verilog基本知识.doc
- 8月5日作业-FSM设计.doc
- Verilog教程的1.4到6.7节内容
推荐学习平台:
https://www.runoob.com/w3cnote/verilog-tutorial.html
中文,一个中文的Verilog教程网站。
https://hdlbits.01xz.net/wiki/Main_Page
英文,在线作答、编译的学习Verilog的网站,题目很多,内容丰富。非常适合Verilog初学者。
FPGA设计方法和Verilog基本知识
名词缩写
缩略语 | 英文全称 | 中文全称 |
---|---|---|
FPGA | Field Programmable Gate Array | 现场可编程逻辑门阵列 |
DSP | Digital Signal Processor | 数字信号处理器 |
CPU | Central Processing Unit | 中央处理器 |
GPU | Graphics Processing Unit | 图形处理器 |
NPU | Neural Processing Unit | 神经网络处理器 |
ASIC | Application Specific Integrated Circuit | 专用集成电路 |
HDL | Hardware (High-level) Description Language | 硬件描述语言 |
LUT | Lookup Table | 查找表 |
RTL | Register Transfer Level | 寄存器传输级 |
EDA | Electronic Design Automation | 电子设计自动化 |
IP | Intellectual Property | 知识产权 |
FPGA与ASIC
FPGA的优点
- 更快的上市时间:FPGA不需要掩模和其他制造步骤,这使得它们能够更快地进入市场。
- 无前期NRE成本:与ASIC设计相关的前期非重发性设计成本(NRE)在FPGA设计中不存在。
- 降低小规模生产的单位成本:对于小规模生产,FPGA的成本较低。
- 缩短设计周期:FPGA的设计周期较短,因为软件可以处理许多布线、布局和时序问题。
- 小型化:FPGA可以根据设计规范进行小型化。
- 现场可重编程能力:FPGA具有远程上传新比特流的能力,实现了现场可重编程。
FPGA的缺点
- 较低的时钟频率:由于FPGA的位移元件和连线布局较复杂,其时钟频率通常比ASIC低。
- 较高的功耗:FPGA的逻辑电路中存在可编程逻辑单元,导致功耗较高。
- 大规模应用的较高成本:尽管FPGA的初始成本可能较低,但在大量使用时,其单片成本可能比ASIC高。
适用于FPGA的场景
FPGA适合的应用场景:包括通信与网络、数字信号处理、汽车与航天、工业自动化、高性能计算、智能物联网。
-
汽车与航天:FPGA在汽车电子系统和航天器中执行控制和数据处理任务,包括车载娱乐系统、发动机控制单元、自动驾驶系统和航空电子设备等。
-
工业自动化:在工业自动化领域,FPGA用于控制系统、机器人控制和自动检测调试等任务,提高生产效率和产品质量。
-
高性能计算:FPGA能够加速科学计算、密码学和人工智能等计算密集型任务,实现高性能的并行计算。
-
智能物联网(IoT):随着物联网的快速发展,FPGA在智能家居、智能城市和智能农业等领域中的应用越来越普遍,实现感知、数据处理、通信和安全等功能。
这些优缺点表明,FPGA和ASIC各有其适用场景。FPGA的快速设计和可重编程特性使其非常适合于快速变化的市场需求和原型设计,而ASIC则因其高性能和低功耗特性在需要高性能和长期稳定运行的应用中表现更佳。
Verilog开发过程
- 需求分析:工作人员需要对用户提出的功能要求进行分析理解,做出电路系统的整体规划,形成详细的技术指标,确定初步方案。例如,要设计一个电子屏,需要考虑供电方式、工作频率、产品体积、成本、功耗等,电路实现采用 ASIC 还是选用 FPGA/CPLD 器件等。
- 功能划分:分析了用户的电路需求后,就可以进行逻辑功能的总体设计,设计整个电路的功能、接口和总体结构,考虑功能模块的划分和设计思路,各子模块的接口和时序(包括接口时序和内部信号的时序)等,向项目组成员合理分配子模块设计任务。
- HDL描述:采用任意的文本编辑器,或专用的 HDL 编辑环境,对所需求的数字电路进行设计建模,保存为 .v 文件。
- 功能仿真:首先对建模文件进行编译,对模型电路进行功能上的仿真验证,查找设计的逻辑错误并修正。
- 逻辑综合:在标准单元库和特定的设计约束的基础上,将设计的高层次描述(Verilog 建模)转换为门级网表的过程。逻辑综合的目的是产生物理电路门级结构,并在逻辑、时序上进行一定程度的优化,寻求逻辑、面积、功耗的平衡,增强电路的可测试性。
- 布局布线:根据逻辑综合出的网表与约束文件,利用厂家提供的各种基本标准单元库,对门级电路进行布局布线。至此,已经将 Verilog 设计的数字电路,设计成由标准单元库组成的数字电路。
- 时序仿真:布局布线后,电路模型中已经包含了时延信息。利用在布局布线中获得的精确参数,用仿真软件验证电路的时序。单元器件的不同、布局布线方案都会给电路的时序造成影响,严重时会出现错误。出错后可能就需要重新修改 RTL(寄存器传输级描述,即 Verilog 初版描述),重复后面的步骤。这样的过程可能反复多次,直至错误完全排除。
- 下载、生产:完成上面所有步骤后,就可以通过开发工具将设计的数字电路目标文件烧入 FPGA/CPLD 芯片中,然后在电路板上进行调试、验证。
基本逻辑门
非门
真值表:
输入A | 输出Y |
---|---|
0(低电平) | 1(高电平) |
1(高电平) | 0(低电平) |
与非门
真值表:与非门的功能相当于先执行AND操作,然后取反。
输入A | 输入B | 输出Y |
---|---|---|
0 | 0 | 1 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
或非门
真值表:或非门的功能相当于先执行OR操作,然后取反。
输入A | 输入B | 输出Y |
---|---|---|
0 | 0 | 1 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 0 |
异或门
真值表:异或门的功能是当输入不相同时输出为1,相同时输出为0。
输入A | 输入B | 输出Y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
例题
【问题】请解释为什么两输入与门在使用CMOS工艺实现比两输入与非门使用的晶体管还要多?
【答】
CMOS逻辑门通常采用互补结构,即每个逻辑门由一对PMOS和NMOS晶体管组成,以实现低功耗和高噪声容限。对于两输入与门,由于需要直接实现与逻辑功能,可能需要更复杂的电路设计来确保两个输入同时为高电平时输出才为高电平。相比之下,两输入与非门可以通过实现非逻辑后再取反来间接实现与逻辑,这可能在某些CMOS实现中更为直接和高效,从而减少了所需的晶体管数量。
两输入与门需要在两输入与非门+非门实现,因而晶体管更多。如下图所示,左侧为两输入与非门,包含P1、P2、N1、N2四个晶体管,右侧是非门,包含两个晶体管。
组合逻辑与时序逻辑
组合逻辑
组合逻辑的输出仅依赖于当前的输入,与之前的输入或输出状态无关。这意味着,只要输入不变,输出也不会改变。组合逻辑电路通常由逻辑门(如AND、OR、NOT等)构成,用于实现各种逻辑功能,如编码、解码、多路选择等。
组合逻辑是指其输出仅依赖于当前输入信号的逻辑电路,不包含存储元件,如与门、或门和非门等。
时序逻辑
时序逻辑的输出不仅依赖于当前的输入,还依赖于电路之前的状态。时序逻辑电路具有存储元件(如触发器、寄存器等),能够保存之前的状态信息。这使得时序逻辑电路能够实现更为复杂的逻辑功能,如计数器、时序控制器等。
时序逻辑则包含存储元件(如触发器),其输出不仅依赖于当前的输入信号,还依赖于历史状态,因此具有记忆功能。
FPGA的结构(以Xilinx器件为例)
图中共有16个可配置逻辑块CLB(Configurable Logic Block),呈矩阵排布。围绕在CLB周围丰富的行列走线为布线池,用于衔接各个CLB以及相关资源,在FPGA芯片四周的小矩形以及延伸出去的短线,是FPGA和外部芯片接口的IO块的示意。
Xilinx FPGA的结构主要由可编程I/O(输入/输出单元)、可配置逻辑块(CLB)、布线资源等组成。这些组成部分共同协作,实现了FPGA的灵活性和可编程性。下面将详细解释每个组成部分的功能和它们在FPGA中的作用。
1)IO块(输入/输出单元)
功能:支持不同的IO引脚配置,包括IO标准、单端或差分、电压转换速率和输出强度、上拉或下拉电阻、数控阻抗(DCI)。
作用:可编程I/O允许用户根据需要配置FPGA的输入输出接口,以满足不同的应用需求。
2)CLB(可配置逻辑块)
功能:实现各种逻辑功能的电路,是Xilinx FPGA的基本逻辑单元。每个可配置逻辑块包含2个Slice,每个Slice包含查找表、寄存器、进位链和多个多数选择器。
作用:CLB是FPGA中实现用户逻辑的核心部分,通过配置Slice中的资源,可以构建复杂的数字电路。
3)布线池
功能:包括全局性的专用布线资源,用于完成器件内部的全局时钟和全局复位/置位的布线。
作用:布线资源允许FPGA内部的逻辑资源块、时钟处理单元、BLOCK RAM、DSP和接口模块等资源相互通信,协调合作,完成所需功能。
阻塞赋值与非阻塞赋值
Verilog中的阻塞赋值与非阻塞赋值的主要区别在于赋值操作的执行方式和时间上的处理。
阻塞赋值
阻塞赋值使用等号(=)表示,其执行过程是顺序进行的,即一条阻塞赋值语句的执行会立即影响下一条语句的执行情况和结果。在同一个always进程中,如果一条阻塞赋值语句没有执行完毕,那么下一条语句将无法开始执行,因此阻塞赋值语句的执行是顺序的,具有阻塞性。
非阻塞赋值
非阻塞赋值则使用小于等于号(<=)表示,其执行过程允许其他的Verilog语句同时进行操作。在时钟上升沿到来时,非阻塞赋值执行的是保存右侧表达式的值到左侧变量或信号中,但不会立即更新,而是在模块的末尾或下一个时钟周期才会生效。这种赋值方式允许在同一时钟周期内对多个变量进行赋值操作,而不会相互干扰,因此非阻塞赋值的执行是并行的。
编程例题
3-8译码器
【问题】下面是case语句编写的3-8译码器电路,将横线上的语句补上,使程序形成完整功能。
module decoder38 (sel,csout);
input [2:0] sel;//补充1
output [7:0] csout;//补充2
reg[7:0] csout;
always@(/*补充3*/sel,csout/*补充3*/)
begin
case(/*补充4*/sel/*补充4*/)
3'b000:csout=8'b11111110;
3'b001:csout=8'b11111101;
3'b010:csout=8'b11111011;
3'b011:csout=8'b11110111;
3'b100:csout=8'b11101111;
3'b101:csout=8'b11011111;
3'b110:csout=8'b10111111;
3'b111:csout=8'b01111111;
default:csout=8'b11111110;
endcase
end
endmodule
4位4输入最大值检测电路
【问题】设计一个4位4输入最大数值检测电路,其中,a、b、c和d为4位二进制数,输出为max。
module max_in4(a,b,c,d,max);
input [3:0]a,b,c,d;
output [3:0]max;
// 代码补充到下面
reg [3:0] max;
always @(a or b or c or d) begin
max = a;
if (b > max) max = b;
if (c > max) max = c;
if (d > max) max = d;
end
// 代码补充到上面
endmodule
另一种:
module max_in4(a,b,c,d,max);
input [3:0] a, b, c, d;
output [3:0] max;
wire [3:0] max1 = (a >= b) ? a : b;
wire [3:0] max2 = (c >= d) ? c : d;
assign max = (max1 > max2) ? max1 : max2;
endmodule
状态机(FSM)设计
有限状态机
概念
有限状态机(Finite State Machine, FSM)简称状态机,是用来表示系统中的有限个状态以及这些状态之间的转移和动作的模型。
优点
-
高效的顺序控制模型
克服了纯硬件数字系统顺序方式控制不灵活的缺点,在其运行方式上类似于控制灵活和方便的CPU,是高速高效控制的首选。
-
容易利用现成的EDA工具进行优化设计
- 状态机构建简单,设计方案相对固定,使用HDL综合器可以发挥其强大的优化功能;
- 性能良好的综合器都具备许多可控或自动优化状态机的功能。
-
稳定性能
状态机容易构成良好的同步时序逻辑模块,可用于解决大规模逻辑电路设计中的竞争和冒险现象。
-
高速性能
在高速通信和高速控制方面,状态机更具有其巨大的优势,一个状态机的功能类似于CPU的功能。
-
高可靠性能
-
状态机是由纯硬件电路构成,不存在CPU运行软件过程中许多固有的缺陷;
-
状态机的设计中能使用各种容错技术;
-
当状态机进入非法状态并从中跳出,进入正常状态的时间短暂,对系统的危害不大。
-
摩尔(Moore)型状态机和米勒(Mealy)型状态机
摩尔(Moore)型状态机
状态机的输出只由当前状态决定。也就是由当前的状态决定输出,而与此时的输入无关,输入只决定状态机的状态改变,不影响电路最终的输出。
米勒(Mealy)型状态机
状态机的输出不但与当前状态有关,还与当前输入值有关。即当前的输入和当前的状态共同决定当前的输出。
状态机设计例题
序列检测
【问题】以101的非重叠序列检测为例,分别使用摩尔(Moore)型状态机和米勒(Mealy) 型状态机进行状态机设计。
非重叠序列检测是指检测到的多组“101”序列之间是非重叠、独立的,比如序列110101进行“101”的非重叠序列检测,输出检测结果为000100。而重叠序列检测值检测到的多组“101”序列之间可以重叠,即第一组“101”和第二组“101”之间可以有重叠部分,第一组“101”序列的最后一个1是第二组“101”序列的第一个1,比如序列110101进行“101”的重叠序列检测,输出检测结果为000101。
【答】
摩尔(Moore)型状态机的状态设计:
状态名称 | 状态含义 |
---|---|
IDLE | 初始状态,检测到1则去S1(1),否则保持IDEL |
S_1 | 检测到第一个数字“1”,检测到0则去S2(10),否则保持S1(1) |
S_2 | 检测到前两个数字“10”,检测到1则去S3(101),否则返回IDLE |
S_3 | 检测到序列“101”,检测到1则去S1(1),否则返回IDLE |
摩尔(Moore)型状态机的状态转移图(需标明状态转移条件、状态输出等信息):
应该规定输入为X、输出为Y,并标注出来。
米勒(Mealy)型状态机的状态设计:
状态名称 | 状态含义 |
---|---|
IDLE | 初始状态,检测到1则去S1并输出y=0,否则保持IDEL |
S_1 | 检测到第一个数字“1”,检测到0则去S2并输出y=0,否则保持S1 |
S_2 | 检测到前两个数字“10”,检测到1则去IDLE并输出y=1,否则返回IDLE并输出y=0 |
米勒(Mealy)型状态机的状态转移图(需标明状态转移条件、状态输出等信息):
应该规定输入为X、输出为Y,并标注出来。
可乐机
【问题】
可乐机每次可以投入1枚1元或者1枚0.5元硬币,每瓶可乐卖2.5元钱。如果刚好投入2.5元,则售货机出一瓶可乐,如果投入3元还要找零0.5元。
输入信号:
sys_clk_n:时钟50MHz;
sys_rst_n:复位,低电平有效;
money_one:投币输入,高电平表示投入一元,低电平表示没有投币;
money_half:投币输入,高电平表示投入0.5元,低电平表示没有投币;
输出信号:
cola_out:可乐输出,高电平表示掉落一瓶可乐,低电平表示没有可乐掉落;
money_out:硬币输出,高电平表示找零0.5元,低电平表示无需找零。
题目要求:
(1)在下表中填写FSM的状态设计(摩尔(Moore)型状态机或米勒(Mealy)型状态机二选一均可),并画出状态转移图。
(2)补全该状态机的三段式Verilog代码。
【答】
(1)摩尔型状态机设计如下:
状态名称 | 状态含义 | 状态编码 |
---|---|---|
IDLE | 初始状态,检测到0.5元则去S_0_5,检测到1元则去S_1,否则保持IDEL | 000 |
S_0_5 | 投入0.5元,检测到0.5元则去S_1,检测到1元则去S_1_5 | 001 |
S_1 | 投入1元,检测到0.5元则去S_1_5,检测到1元则去S_2 | 010 |
S_1_5 | 投入1.5元,检测到0.5元则去S_2,检测到1元则去S_2_5 | 011 |
S_2 | 投入2元,检测到0.5元则去S_2_5,检测到1元则去S_3 | 100 |
S_2_5 | 投入2.5元,出可乐,并返回IDLE | 101 |
S_3 | 投入3元,出可乐,找0.5元,并返回IDLE | 110 |
输入、输出编码如下:
输入编码 | |
---|---|
未投入硬币 | 00 |
投入0.5元 | 01 |
投入1元 | 10 |
输出编码 | |
---|---|
无可乐,无硬币 | 00 |
有可乐,无硬币 | 10 |
有可乐,有硬币 | 11 |
状态转移图:
(2)状态机的三段式Verilog代码如下:
module cola_fsm(
input sys_clk,
input sys_rst_n,
input money_half,
input money_one,
output reg cola_out,
output reg money_out
);
parameter IDLE = 3’b000,
parameter S_0_5 = 3’b001,
parameter S_1 = 3’b010,
parameter S_1_5 = 3’b011,
parameter S_2 = 3’b100,
parameter S_2_5 = 3’b101,
parameter S_3 = 3’b110,
reg [2:0]state, nstate;
// 此段描述状态寄存器
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)begin
state <= IDLE;
end
state<=nstate;
end
// 此段描述下状态计算逻辑
always@(money_one , money_half) begin
nstate = state;
case (state) //假设每次都只能投一个硬币
IDLE: if(money_one == 1’b1)
nstate = S_1;
else if(money_half == 1’b1)
nstate = S_0_5;
S_0_5: if(money_one == 1’b1)
nstate = S_1_5;
else if(money_half == 1’b1)
nstate = S_1;
S_1: if(money_one == 1’b1)
nstate = S_2;
else if(money_half == 1’b1)
nstate = S_1_5;
S_1_5: if(money_one == 1’b1)
nstate = S_2_5;
else if(money_half == 1’b1)
nstate = S_2;
S_2: if(money_one == 1’b1)
nstate = S_3;
else if(money_half == 1’b1)
nstate = S_2_5;
S_2_5: nstate = IDLE;
S_3: nstate = IDLE;
endcase
end
// 此段描述状态输出
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
money_out<= 0;
cola_out<= 0;
end
else begin
case (state) //假设每次都只能投一个硬币
IDLE:
begin
money_out<= 0;
cola_out<= 0;
end
S_0_5:
begin
money_out<= 0;
cola_out<= 0;
end
S_1:
begin
money_out<= 0;
cola_out<= 0;
end
S_1_5:
begin
money_out<= 0;
cola_out<= 0;
end
S_2:
begin
money_out<= 0;
cola_out<= 0;
end
S_2_5:
begin
money_out<= 0;
cola_out<= 1;
end
S_3:
begin
money_out<= 1;
cola_out<= 1;
end
endcase
end
end
endmodule
注意:
务必理解题目要求,能自己设计FSM,考试时会对题目进行微调。
自动售货机:不用考虑投币之后不买需要退币的情况。
如果需要考虑,建议增加一个专门的输入信号表示进行退币,而不要使用rst_n信号进行退币。或者使用超时机制进行自动退币操作。
在编写可乐机的Testbench时,建议投币信号和出货信号建议保持一个时钟周期高电平脉冲,一般不会连续投币或连续出货。
实验提示
1、仿真时注意设置Testbench时钟周期为20ns(50MHz),与FPGA晶振保持一致。
2、if else或case不完整,导致latch产生。可以查看Vivado Message窗口看是否有Warning提示。
3、always@(*)内部描述的是组合逻辑,使用阻塞赋值;always@(posedge clk)描述的是时序逻辑(寄存器),使用非阻塞赋值。
4、设计仅使用单一时钟。
5、务必先仿真正确再上FPGA。仿真与FPGA不一致时,一是查看管脚约束是否正确;二是查看Vivado Message窗口看是否有关于代码的Warning提示。
1:begin和end要匹配
2:模块实例化的端口要准确
3:8月5日的FSM作业撰写Testbench对撰写的FSM设计进行仿真验证。
参考资料:
https://www.runoob.com/w3cnote/verilog-fsm.html
以下内容来自菜鸟教程,引用供方便学习使用,如有侵权,可联系删除。
Verilog设计方法
设计方法
Verilog 的设计多采用自上而下的设计方法(top-down)。即先定义顶层模块功能,进而分析要构成顶层模块的必要子模块;然后进一步对各个模块进行分解、设计,直到到达无法进一步分解的底层功能块。这样,可以把一个较大的系统,细化成多个小系统,从时间、工作量上分配给更多的人员去设计,从而提高了设计速度,缩短了开发周期。
设计流程
Verilog 的设计流程,一般包括以下几个步骤:
需求分析
工作人员需要对用户提出的功能要求进行分析理解,做出电路系统的整体规划,形成详细的技术指标,确定初步方案。例如,要设计一个电子屏,需要考虑供电方式、工作频率、产品体积、成本、功耗等,电路实现采用 ASIC 还是选用 FPGA/CPLD 器件等。
功能划分
正确地分析了用户的电路需求后,就可以进行逻辑功能的总体设计,设计整个电路的功能、接口和总体结构,考虑功能模块的划分和设计思路,各子模块的接口和时序(包括接口时序和内部信号的时序)等,向项目组成员合理分配子模块设计任务。
文本描述
可以用任意的文本编辑器,也可以用专用的 HDL 编辑环境,对所需求的数字电路进行设计建模,保存为 .v 文件。
功能仿真(前仿真)
对建模文件进行编译,对模型电路进行功能上的仿真验证,查找设计的错误并修正。
此时的仿真验证并没有考虑到信号的延迟等一些 timing 因素,只是验证逻辑上的正确性。
逻辑综合
综合(synthesize),就是在标准单元库和特定的设计约束的基础上,将设计的高层次描述(Verilog 建模)转换为门级网表的过程。逻辑综合的目的是产生物理电路门级结构,并在逻辑、时序上进行一定程度的优化,寻求逻辑、面积、功耗的平衡,增强电路的可测试性。
但不是所有的 Verilog 语句都是可以综合成逻辑单元的,例如时延语句。
布局布线
根据逻辑综合出的网表与约束文件,利用厂家提供的各种基本标准单元库,对门级电路进行布局布线。至此,已经将 Verilog 设计的数字电路,设计成由标准单元库组成的数字电路。
时序仿真(后仿真)
布局布线后,电路模型中已经包含了时延信息。利用在布局布线中获得的精确参数,用仿真软件验证电路的时序。单元器件的不同、布局布线方案都会给电路的时序造成影响,严重时会出现错误。出错后可能就需要重新修改 RTL(寄存器传输级描述,即 Verilog 初版描述),重复后面的步骤。这样的过程可能反复多次,直至错误完全排除。
FPGA/CPLD 下载或 ASIC 制造工艺生产
完成上面所有步骤后,就可以通过开发工具将设计的数字电路目标文件下载到 FPGA/CPLD 芯片中,然后在电路板上进行调试、验证。
如果要在 ASIC 上实现,则需要制造芯片。一般芯片制造时,也需要先在 FPGA 板卡上进行逻辑功能的验证。
Verilog 基础语法
格式
Verilog 是区分大小写的。
格式自由,可以在一行内编写,也可跨多行编写。
每个语句必须以分号为结束符。空白符(换行、制表、空格)都没有实际的意义,在编译阶段可忽略。例如下面两中编程方式都是等效的。
不换行(不推荐)
实例
wire [1:0] results ;assign results = (a == 1'b0) ? 2'b01 : (b==1'b0) ? 2'b10 : 2'b11 ;
换行(推荐)
实例
wire [1:0] results ;
assign results = (a == 1'b0) ? 2'b01 :
(b==1'b0) ? 2'b10 :
2'b11 ;
注释
Verilog 中有 2 种注释方式:
用 // 进行单行注释:
reg [3:0] counter ; // A definition of counter register
用 /* 与 */ 进行跨行注释:
wire [11:0] addr ;
/*
Next are notes with multiple lines.
Codes here cannot be compiled.
*/
assign addr = 12'b0 ;
标识符与关键字
标识符(identifier)可以是任意一组字母、数字、$ 符号和 _(下划线)符号的合,但标识符的第一个字符必须是字母或者下划线,不能以数字或者美元符开始。
另外,标识符是区分大小写的。
关键字是 Verilog 中预留的用于定义语言结构的特殊标识符。
Verilog 中关键字全部为小写。
实例
reg [3:0] counter ; //reg 为关键字, counter 为标识符
input clk; //input 为关键字,clk 为标识符
input CLK; //CLK 与 clk是 2 个不同的标识符
Verilog数值表示
数值种类
Verilog HDL 有下列四种基本的值来表示硬件电路中的电平逻辑:
- 0:逻辑 0 或 "假"
- 1:逻辑 1 或 "真"
- x 或 X:未知
x 意味着信号数值的不确定,即在实际电路里,信号可能为 1,也可能为 0。 - z 或 Z:高阻
z 意味着信号处于高阻状态,常见于信号(input, reg)没有驱动时的逻辑结果。例如一个 pad 的 input 呈现高阻状态时,其逻辑值和上下拉的状态有关系。上拉则逻辑值为 1,下拉则为 0 。
整数数值表示方法
数字声明时,合法的基数格式有 4 中,包括:十进制('d 或 'D),十六进制('h 或 'H),二进制('b 或 'B),八进制('o 或 'O)。数值可指明位宽,也可不指明位宽。
指明位宽
实例
4'b1011 // 4bit 数值
32'h3022_c0de // 32bit 的数值
其中,下划线 _ 是为了增强代码的可读性。
不指明位宽
一般直接写数字时,默认为十进制表示,例如下面的 3 种写法是等效的:
实例
counter = 'd100 ; //一般会根据编译器自动分频位宽,常见的为32bit
counter = 100 ;
counter = 32'h64 ;
负数表示
通常在表示位宽的数字前面加一个减号来表示负数。例如:
-6'd15
-15
-15 在 5 位二进制中的形式为 5'b10001, 在 6 位二进制中的形式为 6'b11_0001。
需要注意的是,减号放在基数和数字之间是非法的,例如下面的表示方法是错误的:
4'd-2 //非法说明
实数表示方法
实数表示方法主要有两种方式:
十进制:
30.123
6.0
3.0
0.001
科学计数法:
1.2e4 //大小为12000
1_0001e4 //大小为100010000
1E-3 //大小为0.001
字符串表示方法
字符串是由双引号包起来的字符队列。字符串不能多行书写,即字符串中不能包含回车符。Verilog 将字符串当做一系列的单字节 ASCII 字符队列。例如,为存储字符串 "www.runoob.com", 需要 14*8bit 的存储单元。例如:
实例
reg [0: 14*8-1] str ;
initial begin
str = "www.runoob.com";
end
Verilog数据类型
Verilog 最常用的 2 种数据类型就是线网(wire)与寄存器(reg),其余类型可以理解为这两种数据类型的扩展或辅助。
线网(wire)
wire 类型表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动。如果没有驱动元件连接到 wire 型变量,缺省值一般为 "Z"。举例如下:
实例
wire interrupt ;
wire flag1, flag2 ;
wire gnd = 1'b0 ;
线网型还有其他数据类型,包括 wand,wor,wri,triand,trior,trireg 等。这些数据类型用的频率不是很高,这里不做介绍。
寄存器(reg)
寄存器(reg)用来表示存储单元,它会保持数据原有的值,直到被改写。声明举例如下:
实例
reg clk_temp;
reg flag1, flag2 ;
例如在 always 块中,寄存器可能被综合成边沿触发器,在组合逻辑中可能被综合成 wire 型变量。寄存器不需要驱动源,也不一定需要时钟信号。在仿真时,寄存器的值可在任意时刻通过赋值操作进行改写。例如:
实例
reg rstn ;
initial begin
rstn = 1'b0 ;
#100 ;
rstn = 1'b1 ;
end
向量
当位宽大于 1 时,wire 或 reg 即可声明为向量的形式。例如:
实例
reg [3:0] counter ; //声明4bit位宽的寄存器counter
wire [32-1:0] gpio_data; //声明32bit位宽的线型变量gpio_data
wire [8:2] addr ; //声明7bit位宽的线型变量addr,位宽范围为8:2
reg [0:31] data ; //声明32bit位宽的寄存器变量data, 最高有效位为0
对于上面的向量,我们可以指定某一位或若干相邻位,作为其他逻辑使用。例如:
实例
wire [9:0] data_low = data[0:9] ;
addr_temp[3:2] = addr[8:7] + 1'b1 ;
Verilog 支持可变的向量域选择,例如:
实例
reg [31:0] data1 ;
reg [7:0] byte1 [3:0];
integer j ;
always@* begin
for (j=0; j<=3;j=j+1) begin
byte1[j] = data1[(j+1)*8-1 : j*8];
//把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]
end
end
Verillog 还支持指定 bit 位后固定位宽的向量域选择访问。
[bit+: width] : 从起始 bit 位开始递增,位宽为 width。
[bit-: width] : 从起始 bit 位开始递减,位宽为 width。
实例
//下面 2 种赋值是等效的
A = data1[31-: 8] ;
A = data1[31:24] ;
//下面 2 种赋值是等效的
B = data1[0+ : 8] ;
B = data1[0:7] ;
对信号重新进行组合成新的向量时,需要借助大括号。例如:
实例
wire [31:0] temp1, temp2 ;
assign temp1 = {byte1[0][7:0], data1[31:8]}; //数据拼接
//格外关注这种方式:
//格外关注这种方式:
assign temp2 = {32{1'b0}}; //赋值32位的数值0
整数,实数,时间寄存器变量
整数,实数,时间等数据类型实际也属于寄存器类型。
整数(integer)
整数类型用关键字 integer 来声明。声明时不用指明位宽,位宽和编译器有关,一般为32 bit。reg 型变量为无符号数,而 integer 型变量为有符号数。例如:
实例
reg [31:0] data1 ;
reg [3:0] byte1 [7:0]; //数组变量,后续介绍
integer j ; //整型变量,用来辅助生成数字电路
always@* begin
for (j=0; j<=3;j=j+1) begin
byte1[j] = data1[(j+1)*8-1 : j*8];
//把data1[7:0]…data1[31:24]依次赋值给byte1[0][7:0]…byte[3][7:0]
end
end
此例中,integer 信号 j 作为辅助信号,将 data1 的数据依次赋值给数组 byte1。综合后实际电路里并没有 j 这个信号,j 只是辅助生成相应的硬件电路。
是不是所有的情况中,都是这样的?也即,是否integer变量始终是辅助生成相应的硬件电路?
实数(real)
实数用关键字 real 来声明,可用十进制或科学计数法来表示。实数声明不能带有范围【什么意思?】,默认值为 0。如果将一个实数赋值给一个整数,则只有实数的整数部分会赋值给整数。例如:
实例
real data1 ;
integer temp ;
initial begin
data1 = 2e3 ;
data1 = 3.75 ;
end
initial begin
temp = data1 ; //temp 值的大小为3
end
时间(time)
Verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。其宽度一般为 64 bit,通过调用系统函数 $time 获取当前仿真时间。例如:
实例
time current_time ;
initial begin
#100 ;
current_time = $time ; //current_time 的大小为 100
end
数组
在 Verilog 中允许声明 reg, wire, integer, time, real 及其向量类型的数组。
数组维数没有限制。线网数组也可以用于连接实例模块的端口。数组中的每个元素都可以作为一个标量或者向量,以同样的方式来使用,形如:<数组名>[<下标>]。对于多维数组来讲,用户需要说明其每一维的索引。例如:
实例
integer flag [7:0] ; //8个整数组成的数组
reg [3:0] counter [3:0] ; //由4个4bit计数器组成的数组
wire [7:0] addr_bus [3:0] ; //由4个8bit wire型变量组成的数组
wire data_bit[7:0][5:0] ; //声明1bit wire型变量的二维数组
reg [31:0] data_4d[11:0][3:0][3:0][255:0] ; //声明4维的32bit数据变量数组
下面显示了对数组元素的赋值操作:
实例
flag [1] = 32'd0 ; //将flag数组中第二个元素赋值为32bit的0值
counter[3] = 4'hF ; //将数组counter中第4个元素的值赋值为4bit 十六进制数F,等效于counter[3][3:0] = 4'hF,即可省略宽度;
assign addr_bus[0] = 8'b0 ; //将数组addr_bus中第一个元素的值赋值为0
assign data_bit[0][1] = 1'b1; //将数组data_bit的第1行第2列的元素赋值为1,这里不能省略第二个访问标号,即 assign data_bit[0] = 1'b1; 是非法的。
data_4d[0][0][0][0][15:0] = 15'd3 ; //将数组data_4d中标号为[0][0][0][0]的寄存器单元的15~0bit赋值为3
虽然数组与向量的访问方式在一定程度上类似,但不要将向量和数组混淆。向量是一个单独的元件,位宽为 n;数组由多个元件组成,其中每个元件的位宽为 n 或 1。它们在结构的定义上就有所区别。
存储器
存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为。例如:
实例
reg membit[0:255] ; //256bit的1bit存储器
reg [7:0] mem[0:1023] ; //1Kbyte存储器,位宽8bit
mem[511] = 8'b0 ; //令第512个8bit的存储单元值为0
注意:在Verilog中,靠近变量类型的,代表位宽,后面的代表数量。
参数
参数用来表示常量,用关键字 parameter 声明,只能赋值一次。例如:
实例
parameter data_width = 10'd32 ;
parameter i=1, j=2, k=3 ;
parameter mem_size = data_width * 10 ;
但是,通过实例化的方式,可以更改参数在模块中的值(指的应该是实例化模块的时候)。此部分以后会介绍。
局部参数用 localparam 来声明,其作用和用法与 parameter 相同,区别在于它的值不能被改变。所以当参数只在本模块中调用时,可用 localparam 来说明。
字符串
字符串保存在 reg 类型的变量中,每个字符占用一个字节(8bit)。因此寄存器变量的宽度应该足够大,以保证不会溢出。
字符串不能多行书写,即字符串中不能包含回车符。如果寄存器变量的宽度大于字符串的大小,则使用 0 来填充左边的空余位;如果寄存器变量的宽度小于字符串大小,则会截去字符串左边多余的数据。例如,为存储字符串 "run.runoob.com", 需要 14*8bit 的存储单元:
实例
reg [0: 14*8-1] str ;
initial begin
str = "run.runoob.com";
end
有一些特殊字符在显示字符串中有特殊意义,例如换行符,制表符等。如果需要在字符串中显示这些特殊的字符,则需要在前面加前缀转义字符 \ 。例如下表所示:
转义字符 | 显示字符 |
---|---|
\n | 换行 |
\t | 制表符 |
%% | % |
\ | \ |
\" | " |
\ooo | 1到3个8进制数字字符 |
其实,在 SystemVerilog(主要用于 Verilog 仿真的编程语言)语言中,已经可以直接用关键字 string 来表示字符串变量类型,这为 Verilog 的仿真带来了极大的便利。有兴趣的学者可以简单学习下 SystemVerilog。
Verilog表达式
表达式
表达式由操作符和操作数构成,其目的是根据操作符的意义得到一个计算结果。表达式可以在出现数值的任何地方使用。例如:
实例
a^b ; //a与b进行异或操作
address[9:0] + 10'b1 ; //地址累加
flag1 && flag2 ; //逻辑与操作
操作数
操作数可以是任意的数据类型,只是某些特定的语法结构要求使用特定类型的操作数(例如,always块中不能对wire型变量赋值,assign中不能对reg型变量赋值。)。
操作数可以为常数,整数,实数,线网,寄存器,时间,位选,域选,存储器及函数调用等。
实例
module test;
//实数
real a, b, c;
c = a + b ;
//寄存器
reg [3:0] cprmu_1, cprmu_2 ;
always @(posedge clk) begin
cprmu_2 = cprmu_1 ^ cprmu_2 ;
end
//函数
reg flag1 ;
flag = calculate_result(A, B);
//非法操作数
reg [3:0] res;
wire [3:0] temp;
always@ (*)begin
res = cprmu_2 – cprmu_1 ;
//temp = cprmu_2 – cprmu_1 ; //不合法,always块里赋值对象不能是wire型
end
endmodule
操作符
Verilog 中提供了大约 9 种操作符,分别是算术、关系、等价、逻辑、按位、归约、移位、拼接、条件操作符。
大部分操作符与 C 语言中类似。同类型操作符之间,除条件操作符从右往左关联,其余操作符都是自左向右关联(可以这样理解:相当于括号是在右边)。圆括号内表达式优先执行。例如下面每组的 2 种写法都是等价的。
//自左向右关联,两种写法等价
A+B-C ;
(A+B)-C ;
//自右向左关联,两种写法等价,结果为 B、D 或 F
A ? B : C ? D : F ;
A ? B : (C ? D : F) ;
//自右向左关联,两种写法不等价
(A ? B : C) ? D : F ; //结果 D 或 F
A ? B : C ? D : F ; //结果为 B、D 或 F
不同操作符之间,优先级是不同的。下表列出了操作符优先级从高至低的排列顺序。当没有圆括号时,Verilog 会根据操作符优先级对表达式进行计算。为了避免由操作符优先级导致的计算混乱,在不确定优先级时,建议用圆括号将表达式区分开来。
操作符 | 操作符号 | 优先级 |
---|---|---|
单目运算 | + - ! ~ | 最高 |
乘、除、取模 | * / % | |
加减 | + - | |
移位 | << >> | |
关系 | < <= > >= | |
等价 | == != === !=== | |
归约 | & ~& | |
^ ~^ | ||
| ~| | ||
逻辑 | && | |
|| | ||
条件 | ?: | 最低 |
算术操作符
算术操作符包括单目操作符和双目操作符。
双目操作符对 2 个操作数进行算术运算,包括乘(*)、除(/)、加(+)、减(-)、求幂(**)、取模(%)。
实例
reg [3:0] a, b;
reg [4:0] c ;
a = 4'b0010 ;
b = 4'b1001 ;
c = a+b; //结果为c=b'b1011
c = b/a; //结果为c=4,取整
如果操作数某一位为 X,则计算结果也会全部出现 X。例如:
实例
b = 4'b100x ;
c = a+b ; //结果为c=4'bxxxx
对变量进行声明时,要根据变量的操作符对变量的位宽进行合理声明,不要让结果溢出。上述例子中,相加的 2 个变量位宽为 4bit,那么结果寄存器变量位宽最少为 5bit。否则,高位将被截断,导致结果高位丢失。无符号数乘法时,结果变量位宽应该为 2 个操作数位宽之和【为什么?】。
2'b11 * 2'b11 = 4'b1001
3'b111 * 3'b111 = 6'b110001
4'b1111 * 4'b1111 = 8'b11100001
猜测是可以归纳证明。
实例
reg [3:0] mula ;
reg [1:0] mulb;
reg [5:0] res ;
mula = 4'he ;
mulb = 2'h3 ;
res = mula * mulb ; //结果为res=6'h2a, 数据结果没有丢失位数
+ 和 - 也可以作为单目操作符来使用,表示操作数的正负性。此类操作符优先级最高。
-4 //表示负4
+3 //表示正3
负数表示时,可以直接在十进制数字前面增加一个减号 -,也可以指定位宽。因为负数使用二进制补码来表示,不指定位宽来表示负数,编译器在转换时,会自动分配位宽,从而导致意想不到的结果。例如:
实例
mula = -4'd4 ;
mulb = 2 ;
res = mula * mulb ; //计算结果为res=-6'd8, 即res=6'h38,正常
res = mula * (-'d4) ; //(4的32次幂-4) * 2, 结果异常
/*为什么会出现这样的异常呢?*/
关系操作符
关系操作符有大于(>),小于(<),大于等于(>=),小于等于(<=)。
关系操作符的正常结果有 2 种,真(1)或假(0)。
如果操作数中有一位为 x 或 z,则关系表达式的结果为 x。
实例
A = 4 ;
B = 3 ;
X = 3'b1xx ;
A > B //为真
A <= B //为假
A >= Z //为X,不确定
等价操作符
等价操作符包括逻辑相等(==),逻辑不等(!=),全等(===),非全等(!==)。
等价操作符的正常结果有 2 种:为真(1)或假(0)。
逻辑相等/不等操作符不能比较 x 或 z,当操作数包含一个 x 或 z,则结果为不确定值。
全等比较时,如果按位比较有相同的 x 或 z,返回结果也可以为 1,即全等比较可比较 x 或 z。所以,全等比较的结果一定不包含 x【要么是0,要么是1。】。举例如下:
实例
A = 4 ;
B = 8'h04 ;
C = 4'bxxxx ;
D = 4'hx ;
A == B //为真
A == (B + 1) //为假
A == C //为X,不确定
A === C //为假,返回值为0
C === D //为真,返回值为1
逻辑操作符
逻辑操作符主要有 3 个:&&(逻辑与), ||(逻辑或),!(逻辑非)。
逻辑操作符的计算结果是一个 1bit 的值,0 表示假,1 表示真,x 表示不确定。
如果一个操作数不为 0,它等价于逻辑 1;如果一个操作数等于 0,它等价于逻辑 0。如果它任意一位为 x 或 z,它等价于 x。
如果任意一个操作数包含 x,逻辑操作符运算结果不一定为 x。
例如:A = 3;C = 2'b1x ;
A || C 为真,因为A为真。
逻辑操作符的操作数可以为变量,也可以为表达式。例如:
实例
A = 3;
B = 0;
C = 2'b1x ;
A && B // 为假
A || B // 为真
! A // 为假
! B // 为真
A && C // 为X,不确定
A || C // 为真,因为A为真
//下面这个式子怎么可能为真呢???
(A==2) && (! B) //为真,此时第一个操作数为表达式
按位操作符
按位操作符包括:取反(~),与(&),或(|),异或(^),同或(~^)。
按位操作符对 2 个操作数的每 1bit 数据进行按位操作。
如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。
取反操作符只有一个操作数,它对操作数的每 1bit 数据进行取反操作。
下图给出了按位操作符的逻辑规则。
&(与) | 0 | 1 | x | |(或) | 0 | 1 | x | |
---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 1 | x | |
1 | 0 | 1 | x | 1 | 1 | 1 | 1 | |
x | 0 | x | x | x | x | 1 | x |
^(异或) | 0 | 1 | x | ~^(同或) | 0 | 1 | x | |
---|---|---|---|---|---|---|---|---|
0 | 0 | 1 | x | 0 | 1 | 0 | x | |
1 | 1 | 0 | x | 1 | 0 | 1 | x | |
x | x | x | x | x | x | x | x |
A = 4'b0101 ;
B = 4'b1001 ;
C = 4'bx010 ;
~A //4'b1010
A & B //4'b0001
A | B //4'b1101
A^B //4'b1100
A ~^ B //4'b0011
B | C //4'b1011
B&C //4'bx000
归约操作符
归约操作符包括:归约与(&),归约与非(~&)【与完再非】,归约或(|),归约或非(~|)【或完再非】,归约异或(^),归约同或(~^)。
归约操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。
逻辑操作符、按位操作符和归约操作符都使用相同的符号表示,因此有时候容易混淆。区分这些操作符的关键是分清操作数的数目,和计算结果的规则。
A = 4'b1010 ;
&A ; //结果为 1 & 0 & 1 & 0 = 1'b0,可用来判断变量A是否全1
~|A ; //结果为 ~(1 | 0 | 1 | 0) = 1'b0, 可用来判断变量A是否为全0
^A ; //结果为 1 ^ 0 ^ 1 ^ 0 = 1'b0
移位操作符
移位操作符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>)。
移位操作符是双目操作符,两个操作数分别表示要进行移位的向量信号(操作符左侧)与移动的位数(操作符右侧)。
算术左移和逻辑左移时,右边低位会补 0。
逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。
实例
A = 4'b1100 ;
B = 4'b0010 ;
A = A >> 2 ; //结果为 4'b0011
A = A << 1; //结果为 4'b1000
A = A <<< 1 ; //结果为 4'b1000
C = B + (A>>>2); //结果为 2 + (-4/4) = 1, 4'b0001
拼接操作符
拼接操作符用大括号 {,} 来表示,用于将多个操作数(向量)拼接成新的操作数(向量),信号间用逗号隔开。
拼接符操作数必须指定位宽,常数的话也需要指定位宽。例如:
实例
A = 4'b1010 ;
B = 1'b1 ;
Y1 = {B, A[3:2], A[0], 4'h3 }; //结果为Y1='b1100_0011
Y2 = {4{B}, 3'd4}; //结果为 Y2=7'b111_1100
Y3 = {32{1'b0}}; //结果为 Y3=32h0,常用作寄存器初始化时匹配位宽的赋初值
条件操作符【从右往左关联】
条件表达式有 3 个操作符(是不是应该是操作数?),结构描述如下:
condition_expression ? true_expression : false_expression
计算时,如果 condition_expression 为真(逻辑值为 1),则运算结果为 true_expression;如果 condition_expression 为假(逻辑值为 0),则计算结果为 false_expression。
assign hsel = (addr[9:8] == 2'b0) ? hsel_p1 : hsel_p2 ;
//当信号 addr 高 2bit 为 0 时,hsel 赋值为 hsel_p1; 否则,将 hsel_p2 赋值给 hsel。
其实,条件表达式类似于 2 路(或多路)选择器,其描述方式完全可以用 if-else 语句代替。
当然条件操作符也能进行嵌套,完成一个多次选择的逻辑。例如:
实例
assign hsel = (addr[9:8] == 2'b00) ? hsel_p1 :
(addr[9:8] == 2'b01) ? hsel_p2 :
(addr[9:8] == 2'b10) ? hsel_p3 :
(addr[9:8] == 2'b11) ? hsel_p4 ;//后面似乎缺少false_expression?
关于为什么hsel_p4后面似乎缺少false_expression,我的想法是:
上面的表达式可表述为:
always @(*) begin
if (addr[9:8] == 2'b00)
hsel <= hsel_p1;
else if(addr[9:8] == 2'b01)
hsel <= hsel_p2;
else if(addr[9:8] == 2'b10)
hsel <= hsel_p3;
else if(addr[9:8] == 2'b11)
hsel <= hsel_p4;
end
(需注意,由于wire型变量不能在always块中赋值,此时hsel应该为reg型)
但是,必须注意代码中此时对组合逻辑用了非阻塞赋值,但其实是没有意义的,还是会表现为阻塞赋值。
举例予以说明:
测试文件:
module test( input clk, input rst_n, output reg a, output reg b, output reg c, output reg d, output reg aa, output reg bb, output reg cc, output reg dd ); always @(posedge clk or negedge rst_n)begin if(rst_n == 'b0)begin a <= 'b0; aa <= 'b0; end else begin a <= 'b1; aa <= a; end end always @(posedge clk or negedge rst_n)begin if(rst_n == 'b0)begin b = 'b0; bb = 'b0; end else begin b = 'b1; bb = b; end end always @(*)begin if(rst_n == 'b0)begin c = 'b0; cc = 'b0; end else begin c = 'b1; cc = c; end end always @(*)begin if(rst_n == 'b0)begin d <= 'b0; dd <= 'b0; end else begin d <= 'b1; dd <= d; end end endmodule
激励文件:
module tb; reg clk; reg rst_n; wire a; wire b; wire c; wire d; wire aa; wire bb; wire cc; wire dd; initial begin clk = 0; rst_n = 0; #100 rst_n = 1; end always #5 clk = ~clk; test test_inst( .clk (clk ), .rst_n (rst_n ), .a (a ), .b (b ), .c (c ), .d (d ), .aa (aa ), .bb (bb ), .cc (cc ), .dd (dd ) ); endmodule
仿真结果:
结论:
从波形图可以发现:
always @(*)
描述的是组合逻辑,不管里边用阻塞赋值(=)还是非阻塞赋值(<=),都是阻塞的效果。而always@(posedge clk or negedge rst_n)
是时序逻辑,其中阻塞赋值和非阻塞赋值是起作用的,阻塞赋值可以理解成一堆组合逻辑连接了一个D触发器的结构,非阻塞可以看成流水线结构。
由于addr[9:8]只有四种可能,因此虽然形式上是不完备的,但其实状态已经完备了。按理说也不会生成latch?
Verilog编译指令
以反引号`
开始的某些标识符是 Verilog 系统编译指令。
编译指令为 Verilog 代码的撰写、编译、调试等提供了极大的便利。
下面介绍下完整的 8 种编译指令,其中前 4 种使用频率较高。
`define
, `undef
`define
在编译阶段, `define
用于文本替换,类似于 C 语言中的 #define
。
一旦 `define
指令被编译,其在整个编译过程中都会有效。例如,在一个文件中定义:
`define DATA_DW 32
则在另一个文件中也可以直接使用 `DATA_DW
。
`define S $stop;
//用`S来代替系统函数$stop; (包括分号)
`define WORD_DEF reg [31:0]
//可以用`WORD_DEF来声明32bit寄存器变量
`undef
`undef
用来取消之前的宏定义,例如:
`define DATA_DW 32
……
reg [DATA_DW-1:0] data_in ;
……
`undef DATA_DW
条件编译指令
`ifdef
, `ifndef
, `elsif
, `else
, `endif
这些属于条件编译指令。例如下面的例子中,如果定义了 MCU51,则使用第一种参数说明;如果没有定义 MCU、定义了 WINDOW,则使用第二种参数说明;如果 2 个都没有定义,则使用第三种参数说明。
`ifdef MCU51
parameter DATA_DW = 8 ;
`elsif WINDOW
parameter DATA_DW = 64 ;
`else
parameter DATA_DW = 32 ;
`endif
`elsif
, `else
编译指令对于 `ifdef
指令是可选的,即可以只有 `ifdef
和 `endif
组成一次条件编译指令块。
当然,也可用 `ifndef
来设置条件编译,表示如果没有相关的宏定义,则执行相关语句。
下面例子中,如果定义了 WINDOW,则使用第二种参数说明。如果没有定义 WINDOW,则使用第一种参数说明。
实例
`ifndef WINDOW
parameter DATA_DW = 32 ;
`else
parameter DATA_DW = 64 ;
`endif
`include
使用 `include
可以在编译时将一个 Verilog 文件内嵌到另一个 Verilog 文件中,作用类似于 C 语言中的 #include
结构。该指令通常用于将全局或公用的头文件包含在设计文件里。
文件路径既可以使用相对路径,也可以使用绝对路径。
`include "../../param.v"
`include "header.v"
`timescale
在 Verilog 模型中,时延有具体的单位时间表述,并用 timescale
编译指令将时间单位与实际时间相关联。
该指令用于定义时延、仿真的单位和精度,格式为:
`timescale time_unit / time_precision
time_unit 表示时间单位,time_precision 表示时间精度,它们均是由数字以及单位 s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成。时间精度可以和时间单位一样,但是时间精度大小不能超过时间单位大小,例如下面例子中,输出端 Z 会延迟 5.21ns 输出 A&B 的结果。
实例
`timescale 1ns/100ps //时间单位为1ns,精度为100ps,合法
//`timescale 100ps/1ns //不合法
module AndFunc(Z, A, B);
output Z;
input A, B ;
assign #5.207 Z = A & B
endmodule
在编译过程中, `timescale
指令会影响后面所有模块中的时延值,直至遇到另一个 `timescale
指令或 `resetall
指令。
由于在 Verilog 中没有默认的 `timescale
,如果没有指定 `timescale
,Verilog 模块就有会继承前面编译模块的 `timescale
参数。有可能导致设计出错。
如果一个设计中的多个模块都带有 `timescale
时,模拟器总是定位在所有模块的最小时延精度上,并且所有时延都相应地换算为最小时延精度,时延单位并不受影响。例如:
实例
`timescale 10ns/1ns
module test;
reg A, B ;
wire OUTZ ;
initial begin
A = 1;
B = 0;
# 1.28 B = 1;
# 3.1 A = 0;
end
AndFunc u_and(OUTZ, A, B) ;
endmodule
在模块 AndFunc 中,5.207 对应 5.21ns。
在模块 test 中,1.28 对应 13ns,3.1 对应 31ns。
但是,当仿真 test 时,由于 AndFunc 中的最小精度为 100ps,因此 test 中的时延精度将进行重新调整。13ns 将对应 130*100ps,31ns 将对应 310*100ps。仿真时,时延精度也会使用 100ps。仿真时间单位大小没有影响。
如果有并行子模块,子模块间的 `timescale
并不会相互影响。
例如在模块 test 中再例化一个子模块 OrFunc。仿真 test 时,OrFunc 中的 #5.207 延时依然对应 52ns。
实例
//子模块:
`timescale 10ns/1ns //时间单位为1ns,精度为100ps,合法
module OrFunc(Z, A, B);
output Z;
input A, B ;
assign #5.207 Z = A | B
endmodule
//顶层模块:
`timescale 10ns/1ns
module test;
reg A, B ;
wire OUTZ ;
wire OUTX ;
initial begin
A = 1;
B = 0;
# 1.28 B = 1;
# 3.1 A = 0;
end
AndFunc u_and(OUTZ, A, B) ;
OrFunc u_and(OUTX, A, B) ;
endmodule
此例中,仿真 test 时,OrFunc 中的 #5.207 延时依然对应 52ns。(时间单位本来就不会相互影响,此处是不是应该说是精度不会变才对?)
`timescale
的时间精度设置是会影响仿真时间的。时间精度越小,仿真时占用内存越多,实际使用的仿真时间就越长。所以如果没有必要,应尽量将时间精度设置的大一些。
`default_nettype
该指令用于为隐式的线网变量指定为线网类型,即将没有被声明的连线定义为线网类型。
`default_nettype wand
该实例定义的缺省的线网为线与类型。因此,如果在此指令后面的任何模块中的连线没有说明,那么该线网被假定为线与类型。
wand、wor型数据
除了最常用的net数据类型wire类型外,还有几种其他的net类型。
wand 和 wor 类型用于将基本逻辑门插入到电路中。wand 相当于插入一个与门,wor 类型则相当于插入一个或门(顾名思义)。
当使用 wand 和 wor 类型时,必须对信号进行多次赋值,这样做是因为每次赋值都代表底层逻辑门的一个输入。
下面的 verilog 代码展示了如何将 wand 和 wor 类型与 assign 关键字一起使用。wor a; //声明wor类型 wand b; //声明wand类型 wire c, d, e, f; //逻辑门的输入 //创建一个输入为c、d的或门 assign a = c; assign a = d; //创建一个输入为e、f的与门 assign b = e; assign b = f;
因为与门跟或门都可以轻松使用assign的组合逻辑实现,因此,并不推荐实践中使用 wor 和 wand 类型。
`default_nettype none
该实例定义后,将不再自动产生 wire 型变量。
例如下面第一种写法编译时不会报 Error,第二种写法编译将不会通过。
实例
//Z1 无定义就使用,系统默认Z1为wire型变量,有 Warning 无 Error
module test_and(
input A,
input B,
output Z);
assign Z1 = A & B ;
endmodule
实例
//Z1无定义就使用,由于编译指令的存在,系统会报Error,从而检查出书写错误
`default_nettype none
module test_and(
input A,
input B,
output Z);
assign Z1 = A & B ;
endmodule
`resetall
该编译器指令将所有的编译指令重新设置为缺省值。
`resetall
可以使得缺省连线类型为线网类型。
当 `resetall
加到模块最后时,可以将当前的 `timescale
取消防止进一步传递,只保证当前的 `timescale
在局部有效,避免 `timescale
的错误继承。
`celldefine
, `endcelldefine
这两个程序指令用于将模块标记为单元模块,他们包含模块的定义。例如一些与、或、非门,一些 PLL 单元,PAD 模型,以及一些 Analog IP 等。
实例
`celldefine
module (
input clk,
input rst,
output clk_pll,
output flag);
……
endmodule
`endcelldefine
`unconnected_drive
, `nounconnected_drive
在模块实例化中,出现在这两个编译指令间的任何未连接的输入端口,为正偏电路状态或者为反偏电路状态。
`unconnected_drive pull1
. . .
/ *在这两个程序指令间的所有未连接的输入端口为正偏电路状态(连接到高电平) * /
`nounconnected_drive
`unconnected_drive pull0
. . .
/ *在这两个程序指令间的所有未连接的输入端口为反偏电路状态(连接到低电平) * /
`nounconnected_drive
Verilog连续赋值
assign
连续赋值语句是 Verilog 数据流建模的基本语句,用于对 wire 型变量进行赋值。:
格式如下
assign LHS_target = RHS_expression ;
LHS(left hand side) 指赋值操作的左侧,RHS(right hand side)指赋值操作的右侧。
assign 为关键词,任何已经声明 wire 变量的连续赋值语句都是以 assign 开头,例如:
wire Cout, A, B ;
assign Cout = A & B ; //实现计算A与B的功能
需要说明的是:
- LHS_target 必须是一个标量或者线型向量,而不能是寄存器类型。【直接简单讲,就是wire】
- RHS_expression 的类型没有要求,可以是标量或线型或存器向量,也可以是函数调用。
- 只要 RHS_expression 表达式的操作数有事件发生(值的变化)时,RHS_expression 就会立刻重新计算,同时赋值给 LHS_target。
Verilog 还提供了另一种对 wire 型赋值的简单方法,即在 wire 型变量声明的时候同时对其赋值。wire 型变量只能被赋值一次,因此该种连续赋值方式也只能有一次。例如下面赋值方式和上面的赋值例子的赋值方式,效果都是一致的。
【原因:不能多驱】
wire A, B ;
wire Cout = A & B ;
全加器
下面采用数据流描述方式,来设计一个 1bit 全加器。
设 Ai,Bi,Ci 分别为被加数、加数和相邻低位的进位数,So, Co 分别为本位和与向相邻高位的进位数。
真值表如下:
Input | Output | |||
---|---|---|---|---|
Ci | Ai | Bi | So | Co |
0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 0 |
0 | 1 | 1 | 0 | 1 |
1 | 0 | 0 | 1 | 0 |
1 | 0 | 1 | 0 | 1 |
1 | 1 | 0 | 0 | 1 |
1 | 1 | 1 | 1 | 1 |
全加器的表达式为:
So = Ai ⊕ Bi ⊕ Ci ;
Co = AiBi + Ci(Ai+Bi)
【上述公式是非常容易想懂的。】
rtl 代码(full_adder1.v)如下:
实例
module full_adder1(
input Ai, Bi, Ci,
output So, Co);
assign So = Ai ^ Bi ^ Ci ;
assign Co = (Ai & Bi) | (Ci & (Ai | Bi));
endmodule
当然,更为贴近加法器的代码描述可以为:
实例
module full_adder1(
input Ai, Bi, Ci
output So, Co);
assign {Co, So} = Ai + Bi + Ci ;
endmodule
testbench(test.sv)参考如下:
实例
`timescale 1ns/1ns
module test ;
reg Ai, Bi, Ci ;
wire So, Co ;
initial begin
{Ai, Bi, Ci} = 3'b0;
forever begin
#10 ;
{Ai, Bi, Ci} = {Ai, Bi, Ci} + 1'b1;
end
end
full_adder1 u_adder(
.Ai (Ai),
.Bi (Bi),
.Ci (Ci),
.So (So),
.Co (Co));
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
仿真结果如下:
源码下载
Download
Verilog时延
时延
连续赋值延时语句中的延时,用于控制任意操作数发生变化到语句左端赋予新值之间的时间延时。
时延一般是不可综合的。【什么意思?】
感觉意思是,时延只能在仿真中使用,无法在真实的设计中被EDA工具综合出来。
寄存器的时延也是可以控制的,这部分在时序控制里加以说明。
连续赋值时延一般可分为普通赋值时延、隐式时延、声明时延。
下面 3 个例子实现的功能是等效的,分别对应 3 种不同连续赋值时延的写法。
//普通时延,A&B计算结果延时10个时间单位赋值给Z
wire Z, A, B ;
assign #10 Z = A & B ;
//隐式时延,声明一个wire型变量时对其进行包含一定时延的连续赋值。
wire A, B;
wire #10 Z = A & B;
//声明时延,声明一个wire型变量是指定一个时延。因此对该变量所有的连续赋值都会被推迟到指定的时间。除非门级建模中,一般不推荐使用此类方法建模。
wire A, B;
wire #10 Z ;
assign Z =A & B
时延的惯性
在上述例子中,A 或 B 任意一个变量发生变化,那么在 Z 得到新的值之前,会有 10 个时间单位的时延。如果在这 10 个时间单位内,即在 Z 获取新的值之前,A 或 B 任意一个值又发生了变化,那么计算 Z 的新值时会取 A 或 B 当前的新值。所以称之为惯性时延,即信号脉冲宽度小于时延时,对输出没有影响。
因此仿真时,时延一定要合理设置,防止某些信号不能进行有效的延迟。
对一个有延迟的与门逻辑进行时延仿真。
实例
module time_delay_module(
input ai, bi,
output so_lose, so_get, so_normal);
assign #20 so_lose = ai & bi ;
assign #5 so_get = ai & bi ;
assign so_normal = ai & bi ;
endmodule
testbench 参考如下:
实例
`timescale 1ns/1ns
module test ;
reg ai, bi ;
wire so_lose, so_get, so_normal ;
initial begin
ai = 0 ;
#25 ; ai = 1 ;
#35 ; ai = 0 ; //60ns
#40 ; ai = 1 ; //100ns
#10 ; ai = 0 ; //110ns
end
initial begin
bi = 1 ;
#70 ; bi = 0 ;
#20 ; bi = 1 ; //90ns
end
time_delay_module u_wire_delay(
.ai (ai),
.bi (bi),
.so_lose (so_lose),
.so_get (so_get),
.so_normal (so_normal));
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
仿真结果如下:
信号 so_normal 为正常的与逻辑。
由于所有的时延均大于 5ns,所以信号 so_get 的结果为与操作后再延迟 5ns 的结果。
信号 so_lose 前一段是与操作后再延迟 20ns 的结果。
由于信号 ai 第二个高电平持续时间小于 20ns,so_lose 信号会因惯性时延而漏掉对这个脉冲的延时检测,所以后半段 so_lose 信号仍然为 0。
源码下载
Download
Verilog过程结构
过程结构语句有 2 种,initial 与 always 语句。它们是行为级建模的 2 种基本语句。
一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。
这些语句在模块间并行执行,与其在模块的前后顺序没有关系。
但是initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。
每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。
initial语句
initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。
如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。
如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。
initial 理论上来讲是不可综合的,多用于初始化、信号检测等。
对上一节代码稍作修改,进行仿真,代码如下。
实例
`timescale 1ns/1ns
module test ;
reg ai, bi ;
initial begin
ai = 0 ;
#25 ; ai = 1 ;
#35 ; ai = 0 ; //absolute 60ns
#40 ; ai = 1 ; //absolute 100ns
#10 ; ai = 0 ; //absolute 110ns
end
initial begin
bi = 1 ;
#70 ; bi = 0 ; //absolute 70ns
#20 ; bi = 1 ; //absolute 90ns
end
//at proper time stop the simulation
initial begin
forever begin
#100;
//$display("---gyc---%d", $time);
if ($time >= 1000) begin
$finish ;
end
end
end
endmodule
仿真结果如下:
可以看出,2 个 initial 进程语句分别给信号 ai,bi 赋值时,相互间并没有影响。
信号 ai,bi 的值按照赋值顺序依次改变,所以 initial 内部语句也可以看做是顺序执行。
always语句
与 initial 语句相反,always 语句是重复执行的。always 语句块从 0 时刻开始执行其中的行为语句;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。
由于循环执行的特点,always 语句多用于仿真时钟的产生,信号行为的检测等。
下面用 always 产生一个 100MHz 时钟源,并在 1010ns 时停止仿真代码如下。
代码如下:
实例
`timescale 1ns/1ns
module test ;
parameter CLK_FREQ = 100 ; //100MHz
parameter CLK_CYCLE = 1e9 / (CLK_FREQ * 1e6) ; //switch to ns
reg clk ;
initial clk = 1'b0 ; //clk is initialized to "0"
always # (CLK_CYCLE/2) clk = ~clk ; //generating a real clock by reversing
always begin
#10;
if ($time >= 1000) begin
$finish ;
end
end
endmodule
仿真结果如下:
可见,时钟周期是我们想要得到的 100MHz。而且仿真在 1010ns 时停止。
源码下载
Download
Verilog过程赋值
过程性赋值是在 initial 或 always 语句块里的赋值,赋值对象是寄存器、整数、实数等类型。
这些变量在被赋值后,其值将保持不变,直到重新被赋予新值。
连续性赋值总是处于激活状态,任何操作数的改变都会影响表达式的结果;过程赋值只有在语句执行的时候,才会起作用。这是连续性赋值与过程性赋值的区别。
Verilog 过程赋值包括 2 种语句:阻塞赋值与非阻塞赋值。
阻塞赋值
阻塞赋值属于顺序执行,即下一条语句执行前,当前语句一定会执行完毕。
阻塞赋值语句使用等号 = 作为赋值符。
前面的仿真中,initial 里面的赋值语句都是用的阻塞赋值。
非阻塞赋值
非阻塞赋值属于并行执行语句,即下一条语句的执行和当前语句的执行是同时进行的,它不会阻塞位于同一个语句块中后面语句的执行。
非阻塞赋值语句使用小于等于号 <= 作为赋值符。
利用下面代码,对阻塞、非阻塞赋值进行仿真,来说明 2 种过程赋值的区别。
实例
`timescale 1ns/1ns
module test ;
reg [3:0] ai, bi ;
reg [3:0] ai2, bi2 ;
reg [3:0] value_blk ;
reg [3:0] value_non ;
reg [3:0] value_non2 ;
initial begin
ai = 4'd1 ; //(1)
bi = 4'd2 ; //(2)
ai2 = 4'd7 ; //(3)
bi2 = 4'd8 ; //(4)
#20 ; //(5)
//non-block-assigment with block-assignment
ai = 4'd3 ; //(6)
bi = 4'd4 ; //(7)
value_blk = ai + bi ; //(8)
value_non <= ai + bi ; //(9)
//non-block-assigment itself
ai2 <= 4'd5 ; //(10)
bi2 <= 4'd6 ; //(11)
value_non2 <= ai2 + bi2 ; //(12)
end
//stop the simulation
always begin
#10 ;
if ($time >= 1000) $finish ;
end
endmodule
仿真结果如下:
语句(1)-(8)都是阻塞赋值,按照顺序执行。
20ns 之前,信号 ai,bi 值改变。由于过程赋值的特点【只有在语句执行的时候,才会起作用】,value_blk = ai + bi 并没有执行到,所以 20ns 之前,value_blk 值为 X(不确定状态)。
20ns 之后,信号 ai,bi 值再次改变。执行到 value_blk = ai + bi,信号 value_blk 利用信号 ai,bi 的新值得到计算结果 7。
语句(9)-(12)都是非阻塞赋值,并行执行。
首先,(9)-(12)虽然都是并发执行,但是执行顺序也是在(8)之后,所以信号 value_non = ai + bi 计算是也会使用信号 ai,bi 的新值,结果为 7。
其次,(10)-(12)是并发执行,所以 value_non2 = ai2 + bi2 计算时,并不关心信号 ai2,bi2 的最新非阻塞赋值结果。即 value_non2 计算时使用的是信号 ai2,bi2 的旧值,结果为 4'hF。
使用非阻塞赋值避免竞争冒险
上述仿真代码只是为了让读者更好的理解阻塞赋值与非阻塞赋值的区别。实际 Verilog 代码设计时,切记不要在一个过程结构中混合使用阻塞赋值与非阻塞赋值。两种赋值方式混用时,时序不容易控制,很容易得到意外的结果。
更多时候,在设计电路时,always 时序逻辑块中多用非阻塞赋值,always 组合逻辑块中多用阻塞赋值;在仿真电路时,initial 块中一般多用阻塞赋值。
如下所示,为实现在时钟上升沿交换 2 个寄存器值的功能,在 2 个 always 块中使用阻塞赋值。
实例
always @(posedge clk) begin
a = b ;
end
always @(posedge clk) begin
b = a;
end
因为 2 个 always 块中的语句是同时进行的,但是 a=b 与 b=a 是无法判定执行顺序的,这就造成了竞争的局面。
但不管哪个先执行(和编译器等有关系),不考虑 timing 问题时,他们执行顺序总有先后,最后 a 与 b 的值总是相等的。没有达到交换 2 个寄存器值的效果。
但是,如果在 always 块中使用非阻塞赋值,则可以避免上述竞争冒险的情况。
如下所示,2 个 always 块中语句并行执行,赋值操作右端操作数使用的是上一个时钟周期的旧值,此时 a<=b 与 b<=a 就可以相互不干扰的执行,达到交换寄存器值的目的。
实例
always @(posedge clk) begin
a <= b ;
end
always @(posedge clk) begin
b <= a;
end
当然,利用下面代码也可以实现交换寄存器值的功能,但是显然不如在 always 块中直接用非阻塞赋值简单直观。
实例
always @(posedge clk) begin
temp = a ;
a = b ;
b = temp ;
end
关于竞争与冒险的更多内容,参见Verilog竞争与冒险。
源码下载
Download
Verilog时序控制
Verilog 提供了 2 大类时序控制方法:时延控制和事件控制。事件控制主要分为边沿触发事件控制与电平敏感事件控制。
时延控制
基于时延的时序控制出现在表达式中,它指定了语句从开始执行到执行完毕之间的时间间隔。
时延可以是数字、标识符或者表达式。
根据在表达式中的位置差异,时延控制又可以分为常规时延与内嵌时延。
常规时延
遇到常规延时时,该语句需要等待一定时间,然后将计算结果赋值给目标信号。
格式为:#delay procedural_statement,例如:
reg value_test ;
reg value_general ;
#10 value_general = value_test ;
该时延方式的另一种写法是直接将井号 # 独立成一个时延执行语句,例如:
#10 ;
value_ single = value_test ;
内嵌时延
遇到内嵌延时时,该语句先将计算结果保存,然后等待一定的时间后赋值给目标信号。
内嵌时延控制加在赋值号之后。例如:
reg value_test ;
reg value_embed ;
value_embed = #10 value_test ;
需要说明的是,这 2 种时延控制方式的效果是有所不同的。
当延时语句的赋值符号右端是常量时,2 种时延控制都能达到相同的延时赋值效果。
当延时语句的赋值符号右端是变量时,2 种时延控制可能会产生不同的延时赋值效果。
例如下面仿真代码:
实例
`timescale 1ns/1ns
module test ;
reg value_test ;
reg value_general, value_embed, value_single ;
//signal source
initial begin
value_test = 0 ;
#25 ; value_test = 1 ;
#35 ; value_test = 0 ; //absolute 60ns
#40 ; value_test = 1 ; //absolute 100ns
#10 ; value_test = 0 ; //absolute 110ns
end
//(1)general delay control
initial begin
value_general = 1;
#10 value_general = value_test ; //10ns, value_test=0
#45 value_general = value_test ; //55ns, value_test=1
#30 value_general = value_test ; //85ns, value_test=0
#20 value_general = value_test ; //105ns, value_test=1
end
//(2)embedded delay control
initial begin
value_embed = 1;
value_embed = #10 value_test ; //0ns, value_test=0
value_embed = #45 value_test ; //10ns, value_test=0
value_embed = #30 value_test ; //55ns, value_test=1
value_embed = #20 value_test ; //85ns, value_test=0
end
//(3)single delay control
initial begin
value_single = 1;
#10 ;
value_single = value_test ; //10ns, value_test=0
#45 ;
value_single = value_test ; //55ns, value_test=1
#30 ;
value_single = value_test ; //85ns, value_test=0
#20 ;
value_single = value_test ; //105ns, value_test=1
end
always begin
#10;
if ($time >= 150) begin
$finish ;
end
end
endmodule
仿真结果如下,由图可知:
(1)一般延时的两种表达方式执行的结果都是一致的。
(2)一般时延赋值方式:遇到延迟语句后先延迟一定的时间,然后将当前操作数赋值给目标信号,并没有"惯性延迟"的特点,不会漏掉相对较窄的脉冲。
(3)内嵌时延赋值方式:遇到延迟语句后,先计算出表达式右端的结果,然后再延迟一定的时间,赋值给目标信号。【会出现“惯性延迟”的特点】
下面分析下内嵌延时的赋值过程:
value_embed = #10 value_test ; //0ns, value_test=0
0ns 时,执行此延时语句。
先将 0 赋值给信号 value_embed, 延迟 10ns 输出为 0;
value_embed = #45 value_test ; //10ns, value_test=0
10ns 时,执行此延时语句。
由于此时 value_test 仍然为 0,所以 value_embed 值不变。
即到 55ns 时,value_embed 值仍然保持为 0。
value_embed = #30 value_test ; //55ns, value_test=1
同理,55ns 时,value_test 值为 1,将其赋值给 value_embed 并延迟 30ns 输出。
所以 85ns 时,value_embed 输出为 1。
value_embed = #20 value_test ; //85ns, value_test=0
同理,105ns 时,value_embed 输出为 0。
零延迟控制
零延迟控制可以保证带零延迟控制的语句将在执行时刻相同的多条语句中最后执行,避免竞争。但如果存在多条带零延迟控制的语句,他们之间的执行顺序与不一定。
边沿触发事件控制
在 Verilog 中,事件是指某一个 reg 或 wire 型变量发生了值的变化。
基于事件触发的时序控制又主要分为以下几种。
一般事件控制
事件控制用符号 @
表示。
语句执行的条件是信号的值发生特定的变化。
关键字 posedge 指信号发生边沿正向跳变,negedge 指信号发生负向边沿跳变,未指明跳变方向时,则 2 种情况的边沿变化都会触发相关事件。例如:
实例
//信号clk只要发生变化,就执行q<=d,双边沿D触发器模型
always @(clk) q <= d ;
//在信号clk上升沿时刻,执行q<=d,正边沿D触发器模型
always @(posedge clk) q <= d ;
//在信号clk下降沿时刻,执行q<=d,负边沿D触发器模型
always @(negedge clk) q <= d ;
//立刻计算d的值,并在clk上升沿时刻赋值给q,不推荐这种写法
q = @(posedge clk) d ;
命名事件控制
用户可以声明 event(事件)类型的变量,并触发该变量来识别该事件是否发生。命名事件用关键字 event 来声明,触发信号用 -> 表示。例如:
实例
event start_receiving ; always @( posedge clk_samp) begin -> start_receiving ; //采样时钟上升沿作为时间触发时刻 end always @(start_receiving) begin data_buf = {data_if[0], data_if[1]} ; //触发时刻,对多维数据整合 end
上面的实例不够明显,命名事件控制这个讲的比较好:
敏感列表
当多个信号或事件中任意一个发生变化都能够触发语句的执行时,Verilog 中使用"或"表达式来描述这种情况,用关键字 or
连接多个事件或信号。这些事件或信号组成的列表称为"敏感列表"。当然,or
也可以用逗号 ,
来代替。例如:
实例
//带有低有效复位端的D触发器模型
always @(posedge clk or negedge rstn) begin
//always @(posedge clk , negedge rstn) begin
//也可以使用逗号陈列多个事件触发
if(! rstn)begin
q <= 1'b ;
end
else begin
q <= d ;
end
end
当组合逻辑输入变量很多时,那么编写敏感列表会很繁琐。此时,更为简洁的写法是 @*
或 @(*)
,表示对语句块中的所有输入变量的变化都是敏感的。例如:
实例
always @(*) begin
//always @(a, b, c, d, e, f, g, h, i, j, k, l, m) begin
//两种写法等价
assign s = a? b+c : d ? e+f : g ? h+i : j ? k+l : m ;
end
电平敏感事件控制
前面所讨论的事件控制都是需要等待信号值的变化或事件的触发,使用 @+敏感列表 的方式来表示的。
Verilog 中还支持使用电平作为敏感信号来控制时序,即后面语句的执行需要等待某个条件为真。Verilog 中使用关键字 wait 来表示这种电平敏感情况。例如:
实例
initial begin
wait (start_enable) ; //等待 start 信号
forever begin
//start信号使能后,在clk_samp上升沿,对数据进行整合
@(posedge clk_samp) ;
data_buf = {data_if[0], data_if[1]} ;
end
end
源码下载
Download
Verilog语句块
Verilog 语句块提供了将两条或更多条语句组成语法结构上相当于一条一句的机制。主要包括两种类型:顺序块和并行块。
顺序块
顺序块用关键字 begin 和 end 来表示。
顺序块中的语句是一条条执行的。当然,非阻塞赋值除外。
顺序块中每条语句的时延总是与其前面语句执行的时间相关。
在本节之前的仿真中,initial 块中的阻塞赋值,都是顺序块的实例。
并行块
并行块有关键字 fork 和 join 来表示。
并行块中的语句是并行执行的,即便是阻塞形式的赋值。
并行块中每条语句的时延都是与块语句开始执行的时间相关。【意思是,每个# x都是从第一句开始延时x个时间单位。】
顺序块与并行块的区别显而易见,下面用仿真说明。
仿真代码如下:
实例
`timescale 1ns/1ns
module test ;
reg [3:0] ai_sequen, bi_sequen ;
reg [3:0] ai_paral, bi_paral ;
reg [3:0] ai_nonblk, bi_nonblk ;
//============================================================//
//(1)Sequence block
initial begin
#5 ai_sequen = 4'd5 ; //at 5ns
#5 bi_sequen = 4'd8 ; //at 10ns
end
//(2)fork block
initial fork
#5 ai_paral = 4'd5 ; //at 5ns
#5 bi_paral = 4'd8 ; //at 5ns
join
//(3)non-block block
initial fork
#5 ai_nonblk <= 4'd5 ; //at 5ns
#5 bi_nonblk <= 4'd8 ; //at 5ns
join
endmodule
仿真结果如下:
如图所示,顺序块顺序执行,第 10ns 时,信号 bi_sequen 才赋值为 8。
而并行块,ai_paral 与 bi_paral 的赋值是同时执行的,所以均在 5ns 时被赋值。
而非阻塞赋值,也能达到和并行块同等的赋值效果。
嵌套块
顺序块和并行块还可以嵌套使用。
仿真代码如下:
实例
`timescale 1ns/1ns
module test ;
reg [3:0] ai_sequen2, bi_sequen2 ;
reg [3:0] ai_paral2, bi_paral2 ;
initial begin
ai_sequen2 = 4'd5 ; //at 0ns
fork
#10 ai_paral2 = 4'd5 ; //at 10ns
#15 bi_paral2 = 4'd8 ; //at 15ns
join
#20 bi_sequen2 = 4'd8 ; //at 35ns
end
endmodule
仿真结果如下:
并行块语句块内是并行执行,所以信号 ai_paral2 和信号 bi_paral2 分别在 10ns, 15ns 时被赋值。而并行块中最长的执行时间为 15ns,所以顺序块中的信号 bi_sequen2 在 35ns 时被赋值。
命名块
我们可以给块语句结构命名。
命名的块中可以声明局部变量,通过层次名引用的方法对变量进行访问。
仿真代码如下:
实例
`timescale 1ns/1ns
module test;
initial begin: runoob //命名模块名字为runoob,分号不能少
integer i ; //此变量可以通过test.runoob.i 被其他模块使用
i = 0 ;
forever begin
#10 i = i + 10 ;
end
end
reg stop_flag ;
initial stop_flag = 1'b0 ;
always begin : detect_stop
if ( test.runoob.i == 100) begin //i累加10次,即100ns时停止仿真
$display("Now you can stop the simulation!!!");
stop_flag = 1'b1 ;
end
#10 ;
end
endmodule
仿真结果如下:
命名的块也可以被禁用,用关键字 disable 来表示。
disable 可以终止命名块的执行,可以用来从循环中退出、处理错误等。
与 C 语言中 break 类似,但是 break 只能退出当前所在循环,而 disable 可以禁用设计中任何一个命名的块。
仿真代码如下:
实例
`timescale 1ns/1ns
module test;
initial begin: runoob_d //命名模块名字为runoob_d
integer i_d ;
i_d = 0 ;
while(i_d<=100) begin: runoob_d2
# 10 ;
if (i_d >= 50) begin //累加5次停止累加
disable runoob_d3.clk_gen ;//stop 外部block: clk_gen
disable runoob_d2 ; //stop 当前block: runoob_d2
end
i_d = i_d + 10 ;
end
end
reg clk ;
initial begin: runoob_d3
while (1) begin: clk_gen //时钟产生模块
clk=1 ; #10 ;
clk=0 ; #10 ;
end
end
endmodule
仿真结果如下:
由图可知,信号 i_d 累加到 50 以后,便不再累加,以后 clk 时钟也不再产生。
可见,disable 退出了当前的 while 块。
需要说明的是,disable 在 always 或 forever 块中使用时只能退出当前回合,下一次语句还是会在 always 或 forever 中执行。因为 always 块和 forever 块是一直执行的,此时的 disable 有点类似 C 语言中的 continue 功能。
源码下载
Download
Verilog条件语句
条件语句
条件(if)语句用于控制执行语句要根据条件判断来确定是否执行。
条件语句用关键字 if 和 else 来声明,条件表达式必须在圆括号中。
条件语句使用结构说明如下:
if (condition1) true_statement1 ;
else if (condition2) true_statement2 ;
else if (condition3) true_statement3 ;
else default_statement ;
- if 语句执行时,如果 condition1 为真,则执行 true_statement1 ;如果 condition1 为假,condition2 为真,则执行 true_statement2;依次类推。
- else if 与 else 结构可以省略,即可以只有一个 if 条件判断和一组执行语句 ture_statement1 就可以构成一个执行过程。
- else if 可以叠加多个,不仅限于 1 或 2 个。
- ture_statement1 等执行语句可以是一条语句,也可以是多条。如果是多条执行语句,则需要用 begin 与 end 关键字进行说明。
下面代码实现了一个 4 路选择器的功能。
实例
module mux4to1(
input [1:0] sel ,
input [1:0] p0 ,
input [1:0] p1 ,
input [1:0] p2 ,
input [1:0] p3 ,
output [1:0] sout//注意wire型也是可以用于输出的
);
reg [1:0] sout_t ;
always @(*) begin
if (sel == 2'b00)
sout_t = p0 ;
else if (sel == 2'b01)
sout_t = p1 ;
else if (sel == 2'b10)
sout_t = p2 ;
else
sout_t = p3 ;
end
assign sout = sout_t ;
endmodule
testbench 代码如下:
实例
`timescale 1ns/1ns
module test ;
reg [1:0] sel ;
wire [1:0] sout ;
initial begin
sel = 0 ;
#10 sel = 3 ;
#10 sel = 1 ;
#10 sel = 0 ;
#10 sel = 2 ;
end
mux4to1 u_mux4to1 (
.sel (sel),
.p0 (2'b00), //path0 are assigned to 0
.p1 (2'b01), //path1 are assigned to 1
.p2 (2'b10), //path2 are assigned to 2
.p3 (2'b11), //path3 are assigned to 3
.sout (sout));
//finish the simulation
always begin
#100;
if ($time >= 1000) $finish ;
end
endmodule
仿真结果如下。
由图可知,输出信号与选择信号、输入信号的状态是相匹配的。
事例中 if 条件每次执行的语句只有一条,没有使用 begin 与 end 关键字。但如果是 if-if-else 的形式,即便执行语句只有一条,不使用 begin 与 end 关键字也会引起歧义。
例如下面代码,虽然格式上加以区分,但是 else 对应哪一个 if 条件,是有歧义的。
实例
if(en)
if(sel == 2'b1)
sout = p1s ;
else
sout = p0 ;
当然,编译器一般按照就近原则,使 else 与最近的一个 if(例子中第二个 if)相对应。
但显然这样的写法是不规范且不安全的。
所以条件语句中加入 begin 与 and 关键字就是一个很好的习惯。
例如上述代码稍作修改,就不会再有书写上的歧义。
实例
if(en) begin
if(sel == 2'b1) begin
sout = p1s ;
end
else begin
sout = p0 ;
end
end
源码下载
Download
Verilog多路分支语句
case 语句是一种多路条件分支的形式,可以解决 if 语句中有多个条件选项时使用不方便的问题。
case 语句
case 语句格式如下:
case(case_expr)
condition1 : true_statement1 ;
condition2 : true_statement2 ;
……
default : default_statement ;
endcase
case 语句执行时,如果 condition1 为真,则执行 true_statement1 ; 如果 condition1 为假,condition2 为真,则执行 true_statement2;依次类推。如果各个 condition 都不为真,则执行 default_statement 语句。
default 语句是可选的,且在一个 case 语句中不能有多个 default 语句。
条件选项可以有多个,不仅限于 condition1、condition2 等,而且这些条件选项不要求互斥。虽然这些条件选项是并发比较的,但执行效果是谁在前且条件为真谁被执行。
ture_statement1 等执行语句可以是一条语句,也可以是多条。如果是多条执行语句,则需要用 begin 与 end 关键字进行说明。
case 语句支持嵌套使用。
下面用 case 语句代替 if 语句实现了一个 4 路选择器的功能。仿真结果与 testbench 可参考条件语句一章,两者完全一致。
实例
module mux4to1(
input [1:0] sel ,
input [1:0] p0 ,
input [1:0] p1 ,
input [1:0] p2 ,
input [1:0] p3 ,
output [1:0] sout);
reg [1:0] sout_t ;
always @(*)
case(sel)
2'b00: begin
sout_t = p0 ;
end
2'b01: sout_t = p1 ;
2'b10: sout_t = p2 ;
default: sout_t = p3 ;
endcase
assign sout = sout_t ;
endmodule
case 语句中的条件选项表单式不必都是常量,也可以是 x 值或 z 值。
当多个条件选项下需要执行相同的语句时,多个条件选项可以用逗号分开,放在同一个语句块的候选项中。
但是 case 语句中的 x 或 z 的比较逻辑是不可综合的,所以一般不建议在 case 语句中使用 x 或 z 作为比较值。
例如,对 4 路选择器的 case 语句进行扩展,举例如下:
实例
case(sel)
2'b00: sout_t = p0 ;
2'b01: sout_t = p1 ;
2'b10: sout_t = p2 ;
2'b11: sout_t = p3 ;
2'bx0, 2'bx1, 2'bxz, 2'bxx, 2'b0x, 2'b1x, 2'bzx :
sout_t = 2'bxx ;
2'bz0, 2'bz1, 2'bzz, 2'b0z, 2'b1z :
sout_t = 2'bzz ;
default: $display("Unexpected input control!!!");
endcase
casex/casez 语句
casex、 casez 语句是 case 语句的变形,用来表示条件选项中的无关项。
casex 用 "x" 来表示无关值,casez 用问号 "?" 来表示无关值。
两者的实现的功能是完全一致的,语法与 case 语句也完全一致。
但是 casex、casez 一般是不可综合的,多用于仿真。
例如用 casez 语句来实现一个 4bit 控制端的 4 路选择选择器。
实例
module mux4to1(
input [3:0] sel ,
input [1:0] p0 ,
input [1:0] p1 ,
input [1:0] p2 ,
input [1:0] p3 ,
output [1:0] sout);
reg [1:0] sout_t ;
always @(*)
casez(sel)
4'b???1: sout_t = p0 ;
4'b??1?: sout_t = p1 ;
4'b?1??: sout_t = p2 ;
4'b1???: sout_t = p3 ;
default: sout_t = 2'b0 ;
endcase
assign sout = sout_t ;
endmodule
源码下载
Download
Verilog循环语句
Verilog 循环语句有 4 种类型,分别是 while,for,repeat,和 forever 循环。循环语句只能在 always 或 initial 块中使用,但可以包含延迟表达式。
while循环
while 循环语法格式如下:
while (condition) begin
…
end
while 循环中止条件为 condition 为假。
如果开始执行到 while 循环时 condition 已经为假,那么循环语句一次也不会执行。
当然,执行语句只有一条时,关键字 begin 与 end 可以省略。
下面代码执行时,counter 执行了 11 次。
实例
`timescale 1ns/1ns
module test ;
reg [3:0] counter ;
initial begin
counter = 'b0 ;
while (counter<=10) begin
#10 ;
counter = counter + 1'b1 ;
end
end
//stop the simulation
always begin
#10 ; if ($time >= 1000) $finish ;
end
endmodule
仿真结果如下:
for 循环
for 循环语法格式如下:
for(initial_assignment; condition ; step_assignment) begin
…
end
initial_assignment 为初始条件。
condition 为终止条件,condition 为假时,立即跳出循环。
step_assignment 为改变控制变量的过程赋值语句,通常为增加或减少循环变量计数。
一般来说,因为初始条件和自加操作等过程都已经包含在 for 循环中,所以 for 循环写法比 while 更为紧凑,但也不是所有的情况下都能使用 for 循环来代替 while 循环。
下面 for 循环的例子,实现了与 while 循环中例子一样的效果。需要注意的是,i = i + 1 不能像 C 语言那样写成 i++ 的形式,i = i -1 也不能写成 i -- 的形式。
实例
// for 循环语句
integer i ;
reg [3:0] counter2 ;
initial begin
counter2 = 'b0 ;
for (i=0; i<=10; i=i+1) begin
#10 ;
counter2 = counter2 + 1'b1 ;
end
end
repeat 循环
repeat 循环语法格式如下:
repeat (loop_times) begin
…
end
repeat 的功能是执行固定次数的循环,它不能像 while 循环那样用一个逻辑表达式来确定循环是否继续执行。repeat 循环的次数必须是一个常量、变量或信号。如果循环次数是变量信号,则循环次数是开始执行 repeat 循环时变量信号的值。即便执行期间,循环次数代表的变量信号值发生了变化,repeat 执行次数也不会改变。
下面 repeat 循环例子,实现了与 while 循环中的例子一样的效果。
实例
// repeat 循环语句
reg [3:0] counter3 ;
initial begin
counter3 = 'b0 ;
repeat (11) begin //重复11次
#10 ;
counter3 = counter3 + 1'b1 ;
end
end
下面 repeat 循环例子,实现了连续存储 8 个数据的功能:
实例
always @(posedge clk or negedge rstn) begin
j = 0 ;
if (!rstn) begin
repeat (8) begin
buffer[j] <= 'b0 ; //没有延迟的赋值,即同时赋值为0
j = j + 1 ;
end
end
else if (enable) begin
repeat (8) begin
@(posedge clk) buffer[j] <= counter3 ; //在下一个clk的上升沿赋值
j = j + 1 ;
end
end
end
仿真结果如下图。
由图可知,rstn 拉高时,buffer 的 8 个向量同时赋值为 0。
第二个时钟周期后,buffer 依次被 counter3 赋值,实现了连续存储 8 个数据的功能。
【此处必须注意为什么是依次赋值呢?】
前文中提到,事件控制用符号 @ 表示。
语句执行的条件是信号的值发生特定的变化。
因此,buffer[j] <= counter3 ;
必须是在clk
上升沿才能触发一次,这也就导致本应并行【HDL中的循环相当于硬件复制】的repeat (8)
块变成了每次触发执行一次。
【从这个例子可以看出,HDL中,always块并不是满足其条件就会立刻执行一次,而是如果always块执行完了,等下次满足条件时,会再进入一次,如果在执行过程中,只会继续进行,而不会理会条件。】
【那么,always块中还有rst条件,这个条件也是必须在执行完了后才会因为满足条件而执行吗?有空应该做个仿真。】
forever 循环
forever 循环语法格式如下:
forever begin
…
end
forever 语句表示永久循环,不包含任何条件表达式,一旦执行便无限的执行下去,系统函数 $finish 可退出 forever。
forever 相当于 while(1) 。
通常,forever 循环是和时序控制结构配合使用的。
例如,使用 forever 语句产生一个时钟:
实例
reg clk ;
initial begin
clk = 0 ;
forever begin
clk = ~clk ;
#5 ;
end
end
例如,使用 forever 语句实现一个时钟边沿控制的寄存器间数据传输功能:
实例
reg clk ;
reg data_in, data_temp ;
initial begin
forever @(posedge clk) data_temp = data_in ;
end
注:在Verilog中,forever语句无法被综合,主要用于产生各种仿真激励。
源码下载
Download
Verilog过程连续赋值
过程连续赋值是过程赋值的一种。这种赋值语句能够替换其他所有 wire 或 reg 的赋值,改写了 wire 或 reg 型变量的当前值。
与过程赋值不同的是,过程连续赋值的表达式能被连续的驱动到 wire 或 reg 型变量中,即过程连续赋值发生作用时,右端表达式中任意操作数的变化都会引起过程连续赋值语句的重新执行。
过程连续性赋值主要有 2 种,assign-deassign 和 force-release。
assign, deassign
assign(过程赋值操作)与 deassign (取消过程赋值操作)表示第一类过程连续赋值语句。赋值对象只能是寄存器或寄存器组,而不能是 wire 型变量。
赋值过程中对寄存器连续赋值,寄存器中的值被保留直到被重新赋值。
例如,一个带复位端的 D 触发器可以用下面代码描述:
实例
module dff_normal(
input rstn,
input clk,
input D,
output reg Q
);
always @(posedge clk or negedge rstn) begin
if(!rstn) begin //Q = 0 after reset effective
Q <= 1'b0 ;
end
else begin
Q <= D ; //Q = D at posedge of clock
end
end
endmodule
下面,用 assign 与 deassign 改写,完成相同的功能。
即在复位信号为 0 时,Q 端被 assign 语句赋值,始终输出为 0。
复位信号为 1 时,Q 端被 deassign 语句取消赋值,在时钟上升沿被重新赋值。
实例
module dff_assign(
input rstn,
input clk,
input D,
output reg Q
);
always @(posedge clk) begin
Q <= D ; //Q = D at posedge of clock
end
always @(negedge rstn) begin
if(!rstn) begin
assign Q = 1'b0 ; //change Q value when reset effective
end
else begin //cancel the Q value overlay,
deassign Q ; //and Q remains 0-value until the coming of clock posedge
end
end
endmodule
force, release
force (强制赋值操作)与 release(取消强制赋值)表示第二类过程连续赋值语句。
使用方法和效果,和 assign 与 deassign 类似,但赋值对象可以是 reg 型变量,也可以是 wire 型变量。
因为是无条件强制赋值,一般多用于交互式调试过程,不要在设计模块中使用。
-
当 force 作用在寄存器上时,寄存器当前值被覆盖;release 时该寄存器值将继续保留强制赋值时的值。之后,该寄存器的值可以被原有的过程赋值语句改变。
-
当 force 作用在线网上时,线网值也会被强制赋值。但是,一旦release 该线网型变量,其值马上变为原有的驱动值。
为直观的观察两种类型变量强制赋值的区别,利用第一节中的计数器 counter10 作为设计模块,testbench 设计如下。
实例
`timescale 1ns/1ns
module test ;
reg rstn ;
reg clk ;
reg [3:0] cnt ;
wire cout ;
counter10 u_counter (
.rstn (rstn),
.clk (clk),
.cnt (cnt),
.cout (cout));
initial begin
clk = 0 ;
rstn = 0 ;
#10 ;
rstn = 1'b1 ;
wait (test.u_counter.cnt_temp == 4'd4) ;
@(negedge clk) ;
force test.u_counter.cnt_temp = 4'd6 ;
force test.u_counter.cout = 1'b1 ;
#40 ;
@(negedge clk) ;
release test.u_counter.cnt_temp ;
release test.u_counter.cout ;
end
initial begin
clk = 0 ;
forever #10 clk = ~ clk ;
end
//finish the simulation
always begin
#1000;
if ($time >= 1000) $finish ;
end
endmodule // test
module counter10(
input rstn,
input clk,
output [3:0] cnt,
output cout);
reg [3:0] cnt_temp ;
always@(posedge clk or negedge rstn) begin
if(! rstn)begin
cnt_temp <= 4'b0 ;
end
else if (cnt_temp==4'd9) begin
cnt_temp <=4'b000;
end
else begin
cnt_temp <= cnt_temp + 1'b1 ;
end
end
assign cout = (cnt_temp==4'd9) ;
assign cnt = cnt_temp ;
endmodule // counter10
仿真结果如下。
由图可知,在 cnt_temp 等于 4 时(80ns), cnt_temp 被强制赋值为 6,cout 被强制赋值为 1。
release 时(120ns), cnt_temp 为寄存器类型,仍然保持原有值不变,直到时钟上升沿对其进行加法赋值操作,值才变为 7 。
而 120ns 时,由于 cout 是线网型变量,其值不能保存。原码 counter10 模型中存在驱动语句: assign cout = (cnt_temp==4'd9)
,所以 cout 值变为 0 。
源码下载
Download
Verilog模块与端口
结构建模方式有 3 类描述语句: Gate(门级)例化语句,UDP (用户定义原语)例化语句和 module (模块) 例化语句。
本次主要讲述使用最多的模块级例化语句。
模块
模块是 Verilog 中基本单元的定义形式,是与外界交互的接口。
模块格式定义如下:
module module_name
#(parameter_list)
(port_list) ;
Declarations_and_Statements ;
endmodule
模块定义必须以关键字 module 开始,以关键字 endmodule 结束。
模块名,端口信号,端口声明和可选的参数声明等,出现在设计使用的 Verilog 语句(图中 Declarations_and_Statements)之前。
模块内部有可选的 5 部分组成,分别是变量声明,数据流语句,行为级语句,低层模块例化及任务和函数,如下图表示。这 5 部分出现顺序、出现位置都是任意的。但是,各种变量都应在使用之前声明。变量具体声明的位置不要求,但必须保证在使用之前的位置。
前面大多数仿真代码都会用到 module 声明,大家可以自行参考,这里不再做具体举例。下面介绍端口时,再做详细的仿真。
端口
端口是模块与外界交互的接口。对于外部环境来说,模块内部是不可见的,对模块的调用只能通过端口连接进行。
端口列表
模块的定义中包含一个可选的端口列表,一般将不带类型、不带位宽的信号变量罗列在模块声明里【不能这样说吧?一般所有的输入输出端口都会罗列】。下面是一个 PAD 模型的端口列表:
module pad(
DIN, OEN, PULL,
DOUT, PAD);
一个模块如果和外部环境没有交互,则可以不用声明端口列表。例如之前我们仿真时 test.sv 文件中的 test 模块都没有声明具体端口。
module test ; //直接分号结束
...... //数据流或行为级描述
endmodule
端口声明
(1) 端口信号在端口列表中罗列出来以后,就可以在模块实体中进行声明了。
根据端口的方向,端口类型有 3 种: 输入(input),输出(output)和双向端口(inout)。
input、inout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。
output 可以声明为 wire 或 reg 数据类型。
上述例子中 pad 模块的端口声明,在 module 实体中就可以表示如下:
实例
//端口类型声明
input DIN, OEN ;
input [1:0] PULL ; //(00,01-dispull, 11-pullup, 10-pulldown)
inout PAD ; //pad value
output DOUT ; //pad load when pad configured as input
//端口数据类型声明
wire DIN, OEN ;
wire [1:0] PULL ;
wire PAD ;
reg DOUT ;
(2) 在 Verilog 中,端口隐式的声明为 wire 型变量,即当端口具有 wire 属性时,不用再次声明端口类型为 wire 型。但是,当端口有 reg 属性时,则 reg 声明不可省略。
上述例子中的端口声明,则可以简化为:
实例
//端口类型声明
input DIN, OEN ;
input [1:0] PULL ;
inout PAD ;
output DOUT ;
reg DOUT ;
(3) 当然,信号 DOUT 的声明完全可以合并成一句:
output reg DOUT ;
(4) 还有一种更简洁且常用的方法来声明端口,即在 module 声明时就陈列出端口及其类型。reg 型端口要么在 module 声明时声明,要么在 module 实体中声明,例如以下 2 种写法是等效的。
实例
module pad(
input DIN, OEN ,
input [1:0] PULL ,
inout PAD ,
output reg DOUT
);
module pad(
input DIN, OEN ,
input [1:0] PULL ,
inout PAD ,
output DOUT
);
reg DOUT ;
inout 端口仿真
对包含有 inout 端口类型的 pad 模型进行仿真。pad 模型完整代码如下:
实例
module pad(
//DIN, pad driver when pad configured as output
//OEN, pad direction(1-input, o-output)
input DIN, OEN ,
//pull function (00,01-dispull, 10-pullup, 11-pulldown)
input [1:0] PULL ,
inout PAD ,
//pad load when pad configured as input
output reg DOUT
);
//input:(not effect pad external input logic), output: DIN->PAD
assign PAD = OEN? 'bz : DIN ;
//input:(PAD->DOUT)
always @(*) begin
if (OEN == 1) begin //input
DOUT = PAD ;
end
else begin
DOUT = 'bz ;
end
end
//use tristate gate in Verilog to realize pull up/down function
bufif1 puller(PAD, PULL[0], PULL[1]);
endmodule
testbench代码如下:
实例
`timescale 1ns/1ns
module test ;
reg DIN, OEN ;
reg [1:0] PULL ;
wire PAD ;
wire DOUT ;
reg PAD_REG ;
assign PAD = OEN ? PAD_REG : 1'bz ; //
initial begin
PAD_REG = 1'bz ; //pad with no dirve at first
OEN = 1'b1 ; //input simulation
#0 ; PULL = 2'b10 ; //pull down
#20 ; PULL = 2'b11 ; //pull up
#20 ; PULL = 2'b00 ; //dispull
#20 ; PAD_REG = 1'b0 ;
#20 ; PAD_REG = 1'b1 ;
#30 ; OEN = 1'b0 ; //output simulation
DIN = 1'bz ;
#15 ; DIN = 1'b0 ;
#15 ; DIN = 1'b1 ;
end
pad u_pad(
.DIN (DIN) ,
.OEN (OEN) ,
.PULL (PULL) ,
.PAD (PAD) ,
.DOUT (DOUT)
);
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
endmodule // test
仿真结果如下:
仿真结果分析如下:
当 PAD 方向为 input 且没有驱动时,pull 功能能通过 PAD 的值而体现。
前 60ns 内,PAD 的驱动端 PAD_REG 为 z, 可认为没有驱动,所以开始时 PULL=2, 下拉,PAD值为 0; 20ns 时,PULL=3,上拉,PAD 值为 1;
40ns 时,PULL=0,没有 pull 功能,PAD 值输入为 z。
60ns~100ns 后,PAD 的驱动端 PAD_REG 开始正常驱动。此时相当于 PAD 直接与 PAD_REG 相连,所以 PAD 值与其驱动值保持一致。
以上分析,PAD 方向都是 input,所有输出端 DOUT 与 PAD 值保持一致。
当 PAD 方向为 output 时,即 120ns 时 OEN= 0,PAD 值与输入端 DIN 值保持一致。
源码下载
Download
Verilog模块例化
在一个模块中引用另一个模块,对其端口进行相关连接,叫做模块例化。模块例化建立了描述的层次。信号端口可以通过位置或名称关联,端口连接也必须遵循一些规则。
命名端口连接
这种方法将需要例化的模块端口与外部信号按照其名字进行连接,端口顺序随意,可以与引用 module 的声明端口顺序不一致,只要保证端口名字与外部信号匹配即可。
下面是例化一次 1bit 全加器的例子:
实例
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b0 : 1'b1),
.So (so_bit0),
.Co (co_temp[0]));
如果某些输出端口并不需要在外部连接,例化时可以悬空不连接,甚至删除。一般来说,input 端口在例化时不能删除【但可以悬空】,否则编译报错,output 端口在例化时可以悬空或删除。例如:
实例
//output 端口 Co 悬空
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b0 : 1'b1),
.So (so_bit0),
.Co ());
//output 端口 Co 删除
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b0 : 1'b1),
.So (so_bit0));
顺序端口连接
这种方法将需要例化的模块端口按照模块声明时端口的顺序与外部信号进行匹配连接,位置要严格保持一致。例如例化一次 1bit 全加器的代码可以改为:
full_adder1 u_adder1(
a[1], b[1], co_temp[0], so_bit1, co_temp[1]);
虽然代码从书写上可能会占用相对较少的空间,但代码可读性降低,也不易于调试。有时候在大型的设计中可能会有很多个端口,端口信号的顺序时不时的可能也会有所改动,此时再利用顺序端口连接进行模块例化,显然是不方便的。所以平时,建议采用命名端口方式对模块进行例化。
端口连接规则
输入端口
模块例化时,从模块外部来讲, input 端口可以连接 wire 或 reg 型变量。这与模块声明是不同的,从模块内部来讲,input 端口必须是 wire 型变量。
【注意:模块内部中,input、inout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。】
输出端口
模块例化时,从模块外部来讲,output 端口必须连接 wire 型变量【对于例化所在的上层模块来说,这相当于输入信号】。
这与模块声明是不同的,从模块内部来讲,output 端口可以是 wire 或 reg 型变量。
输入输出端口
模块例化时,从模块外部来讲,inout 端口必须连接 wire 型变量。这与模块声明是相同的。
悬空端口
模块例化时,如果某些信号不需要与外部信号进行连接交互,我们可以将其悬空,即端口例化处保留空白即可,上述例子中有提及。
output 端口正常悬空时,我们甚至可以在例化时将其删除。
input 端口正常悬空时,悬空信号的逻辑功能表现为高阻状态(逻辑值为 z)。但是,例化时一般不能将悬空的 input 端口删除,否则编译会报错,例如:
实例
//下述代码编译会报Warning
full_adder4 u_adder4(
.a (a),
.b (b),
.c (),
.so (so),
.co (co));
实例
//如果模块full_adder4有input端口c,则下述代码编译是会报Error
full_adder4 u_adder4(
.a (a),
.b (b),
.so (so),
.co (co));
一般来说,建议 input 端口不要做悬空处理,无其他外部连接时赋值其常量,例如:
实例
full_adder4 u_adder4(
.a (a),
.b (b),
.c (1'b0),
.so (so),
.co (co));
位宽匹配
当例化端口与连续信号位宽不匹配时,端口会通过无符号数的右对齐【高位补z】或截断方式【去掉高位】进行匹配。
假如在模块 full_adder4 中,端口 a 和端口 b 的位宽都为 4bit,则下面代码的例化结果会导致:u_adder4.a = {2'bzz, a[1:0]}
, u_adder4.b = b[3:0]
。
实例
full_adder4 u_adder4(
.a (a[1:0]), //input a[3:0]
.b (b[5:0]), //input b[3:0]
.c (1'b0),
.so (so),
.co (co));
端口连接信号类型
连接端口的信号类型可以是,1)标识符,2)位选择,3)部分选择,4)上述类型的合并,5)用于输入端口的表达式。
当然,信号名字可以与端口名字一样,但他们的意义是不一样的,分别代表的是 2 个模块内的信号。
用 generate 进行模块例化
当例化多个相同的模块时,一个一个的手动例化会比较繁琐。用 generate 语句进行多个模块的重复例化,可大大简化程序的编写过程。
重复例化 4 个 1bit 全加器组成一个 4bit 全加器的代码如下:
实例
module full_adder4(
input [3:0] a , //adder1
input [3:0] b , //adder2
input c , //input carry bit
output [3:0] so , //adding result
output co //output carry bit
);
wire [3:0] co_temp ;
//第一个例化模块一般格式有所差异,需要单独例化
full_adder1 u_adder0(
.Ai (a[0]),
.Bi (b[0]),
.Ci (c==1'b1 ? 1'b1 : 1'b0),
.So (so[0]),
.Co (co_temp[0]));
genvar i ;
generate
for(i=1; i<=3; i=i+1) begin: adder_gen
full_adder1 u_adder(
.Ai (a[i]),
.Bi (b[i]),
.Ci (co_temp[i-1]), //上一个全加器的溢位是下一个的进位
.So (so[i]),
.Co (co_temp[i]));
end
endgenerate
assign co = co_temp[3] ;
endmodule
其中,genvar 是一个在生成块中使用的局部变量,用于迭代生成构造。它与 integer 类型类似,但只能在 generate 块内使用。
生成的模块名称应该是adder_gen[i]。【推测】
testbench 如下:
实例
`timescale 1ns/1ns
module test ;
reg [3:0] a ;
reg [3:0] b ;
//reg c ;
wire [3:0] so ;
wire co ;
//简单驱动
initial begin
a = 4'd5 ;
b = 4'd2 ;
#10 ;
a = 4'd10 ;
b = 4'd8 ;
end
full_adder4 u_adder4(
.a (a),
.b (b),
.c (1'b0), //端口可以连接常量
.so (so),
.co (co));
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
endmodule // test
仿真结果如下,可知 4bit 全加器工作正常:
层次访问
每一个例化模块的名字,每个模块的信号变量等,都使用一个特定的标识符进行定义。在整个层次设计中,每个标识符都具有唯一的位置与名字。
Verilog 中,通过使用一连串的 . 符号对各个模块的标识符进行层次分隔连接,就可以在任何地方通过指定完整的层次名对整个设计中的标识符进行访问。
层次访问多见于仿真中。
例如,有以下层次设计,则叶单元、子模块和顶层模块间的信号就可以相互访问。
实例
//u_n1模块中访问u_n3模块信号:
a = top.u_m2.u_n3.c ;
//u_n1模块中访问top模块信号
if (top.p == 'b0) a = 1'b1 ;
//top模块中访问u_n4模块信号
assign p = top.u_m2.u_n4.d ;
前面章节的仿真中,或多或少的也进行过相关的层次访问。例如《过程连续赋值》一节中,在顶层仿真激励 test 模块中使用了如下语句:
wait (test.u_counter.cnt_temp == 4'd4) ;
源码下载
Download
Verilog带参数例化
当一个模块被另一个模块引用例化时,高层模块可以对低层模块的参数值进行改写。这样就允许在编译时将不同的参数传递给多个相同名字的模块,而不用单独为只有参数不同的多个模块再新建文件。
参数覆盖有 2 种方式:1)使用关键字 defparam,2)带参数值模块例化。
defparam语句
可以用关键字 defparam 通过模块层次调用的方法,来改写低层次模块的参数值。
例如对一个单口地址线和数据线都是 4bit 宽度的 ram 模块的 MASK 参数进行改写:
实例
//instantiation
defparam u_ram_4x4.MASK = 7 ;
ram_4x4 u_ram_4x4
(
.CLK (clk),
.A (a[4-1:0]),
.D (d),
.EN (en),
.WR (wr), //1 for write and 0 for read
.Q (q) );
ram_4x4 的模型如下:
实例
module ram_4x4
(
input CLK ,
input [4-1:0] A ,
input [4-1:0] D ,
input EN ,
input WR , //1 for write and 0 for read
output reg [4-1:0] Q );
parameter MASK = 3 ;
reg [4-1:0] mem [0:(1<<4)-1] ;
always @(posedge CLK) begin
if (EN && WR) begin
mem[A] <= D & MASK;
end
else if (EN && !WR) begin
Q <= mem[A] & MASK;
end
end
endmodule
对此进行一个简单的仿真,testbench 编写如下:
实例
`timescale 1ns/1ns
module test ;
parameter AW = 4 ;
parameter DW = 4 ;
reg clk ;
reg [AW:0] a ;
reg [DW-1:0] d ;
reg en ;
reg wr ;
wire [DW-1:0] q ;
//clock generating
always begin
#15 ; clk = 0 ;
#15 ; clk = 1 ;
end
initial begin
a = 10 ;
d = 2 ;
en = 'b0 ;
wr = 'b0 ;
repeat(10) begin
@(negedge clk) ;
en = 1'b1;
a = a + 1 ;
wr = 1'b1 ; //write command
d = d + 1 ;
end
a = 10 ;
repeat(10) begin
@(negedge clk) ;
a = a + 1 ;
wr = 1'b0 ; //read command
end
end // initial begin
//instantiation
defparam u_ram_4x4.MASK = 7 ;
ram_4x4 u_ram_4x4
(
.CLK (clk),
.A (a[AW-1:0]),
.D (d),
.EN (en),
.WR (wr), //1 for write and 0 for read
.Q (q)
);
//stop simulation
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
endmodule // test
仿真结果如下:
图中黄色部分,当地址第一次为 c 时写入数据 4, 当第二次地址为 c 时读出数据为 4;可知此时 ram 行为正确,且 MASK 不为 3。 因为 ram 的 Q 端 bit2 没有被屏蔽。
当第一次地址为 1 时写入数据为 9,第二次地址为 1 时读出的数据却是 1,因为此时 MASK 为 7,ram 的 Q 端信号 bit3 被屏蔽。由此可知,MASK 参数被正确改写。
【上述仿真结果中,似乎证明repeat(10)只是执行了9次,不太对劲,有时间了应该做个仿真看看。】
带参数模块例化
第二种方法就是例化模块时,将新的参数值写入模块例化语句,以此来改写原有 module 的参数值。
例如对一个地址和数据位宽都可变的 ram 模块进行带参数的模块例化:
实例
ram #(.AW(4), .DW(4))
u_ram
(
.CLK (clk),
.A (a[AW-1:0]),
.D (d),
.EN (en),
.WR (wr), //1 for write and 0 for read
.Q (q)
);
ram 模型如下:
实例
module ram
#( parameter AW = 2 ,
parameter DW = 3 )
(
input CLK ,
input [AW-1:0] A ,
input [DW-1:0] D ,
input EN ,
input WR , //1 for write and 0 for read
output reg [DW-1:0] Q
);
reg [DW-1:0] mem [0:(1<<AW)-1] ;
always @(posedge CLK) begin
if (EN && WR) begin
mem[A] <= D ;
end
else if (EN && !WR) begin
Q <= mem[A] ;
end
end
endmodule
仿真时,只需在上一例的 testbench 中,将本次例化的模块 u_ram 覆盖掉 u_ram_4x4, 或重新添加之即可。
仿真结果如下。由图可知,ram 模块的参数 AW 与 DW 均被改写为 4, 且 ram 行为正确。
区别与建议
(1) 和模块端口实例化一样,带参数例化时,也可以不指定原有参数名字,按顺序进行参数例化,例如 u_ram 的例化可以描述为:
ram #(4, 4) u_ram (......) ;
(2) 当然,利用 defparam 也可以改写模块在端口声明时声明的参数,利用带参数例化也可以改写模块实体中声明的参数。例如 u_ram 和 u_ram_4x4 的例化分别可以描述为:
实例
defparam u_ram.AW = 4 ;
defparam u_ram.DW = 4 ;
ram u_ram(......);
ram_4x4 #(.MASK(7)) u_ram_4x4(......);
(3) 那能不能混合使用这两种模块参数改写的方式呢?当然能!前提是所有参数都是模块在端口声明时声明的参数或参数都是模块实体中声明的参数,例如 u_ram 的声明还可以表示为(模块实体中参数可自行实验验证):
实例
defparam u_ram.AW = 4 ;
ram #(.DW(4)) u_ram (......); //也只有我这么无聊才会实验这种写法
(4) 那如果一个模块中既有在模块在端口声明时声明的参数,又有在模块实体中声明的参数,那这两种参数还能同时改写么?例如在 ram 模块中加入 MASK 参数,模型如下:
实例
module ram
#( parameter AW = 2 ,
parameter DW = 3 )
(
input CLK ,
input [AW-1:0] A ,
input [DW-1:0] D ,
input EN ,
input WR , //1 for write and 0 for read
output reg [DW-1:0] Q );
parameter MASK = 3 ;
reg [DW-1:0] mem [0:(1<<AW)-1] ;
always @(posedge CLK) begin
if (EN && WR) begin
mem[A] <= D ;
end
else if (EN && !WR) begin
Q <= mem[A] ;
end
end
endmodule
此时再用 defparam 改写参数 MASK 值时,编译报 Error:
实例
//都采用defparam时会报Error
defparam u_ram.AW = 4 ;
defparam u_ram.DW = 4 ;
defparam u_ram.MASK = 7 ;
ram u_ram (......);
//模块实体中parameter用defparam改写也会报Error
defparam u_ram.MASK = 7 ;
ram #(.AW(4), .DW(4)) u_ram (......);
重点来了!!!如果你用带参数模块例化的方法去改写参数 MASK 的值,编译不会报错,MASK 也将被成功改写!
ram #(.AW(4), .DW(4), .MASK(7)) u_ram (......);
【简单点,直接用module_name #(.param1(value1), .param2(value2))
的方式好了。】
可能的解释为,在编译器看来,如果有模块在端口声明时的参数,那么实体中的参数将视为 localparam 类型,使用 defparam 将不能改写模块实体中声明的参数。
也可能和编译器有关系,大家也可以在其他编译器上实验。
(5)建议,对已有模块进行例化并将其相关参数进行改写时,不要采用 defparam 的方法。除了上述缺点外,defparam 一般也不可综合。
(6)而且建议,模块在编写时,如果预知将被例化且有需要改写的参数,都将这些参数写入到模块端口声明之前的地方(用关键字井号 # 表示)。这样的代码格式不仅有很好的可读性,而且方便调试。
源码下载
Download
Verilog函数
在 Verilog 中,可以利用任务(关键字为 task)或函数(关键字为 function),将重复性的行为级设计进行提取,并在多个地方调用,来避免重复代码的多次编写,使代码更加的简洁、易懂。
函数
函数只能在模块中定义,位置任意,并在模块的任何地方引用,作用范围也局限于此模块。函数主要有以下几个特点:
- 不含有任何延迟、时序或时序控制逻辑
- 至少有一个输入变量
- 只有一个返回值,且没有输
- 不含有非阻塞赋值语句
- 函数可以调用其他函数,但是不能调用任务
Verilog 函数声明格式如下:
function [range-1:0] function_id ;//注意这里有个分号
input_declaration ;
other_declaration ;
procedural_statement ;
endfunction
函数在声明时,会隐式的声明一个宽度为 range、 名字为 function_id 的寄存器变量,函数的返回值通过这个变量进行传递。当该寄存器变量没有指定位宽时,默认位宽为 1。
函数通过指明函数名与输入变量进行调用。函数结束时,返回值被传递到调用处。
函数调用格式如下:
function_id(input1, input2, …);
下面用函数实现一个数据大小端转换的功能。
当输入为 4'b0011 时,输出可为 4'b1100。例如:
实例
module endian_rvs
#(parameter N = 4)
(
input en, //enable control
input [N-1:0] a ,
output [N-1:0] b
);
reg [N-1:0] b_temp ;
always @(*) begin
if (en) begin
b_temp = data_rvs(a);
end
else begin
b_temp = 0 ;
end
end
assign b = b_temp ;
//function entity
function [N-1:0] data_rvs ;
input [N-1:0] data_in ;
parameter MASK = 32'h3 ;
integer k ;
begin
for(k=0; k<N; k=k+1) begin
data_rvs[N-k-1] = data_in[k] ;
end
end
endfunction
endmodule
函数在声明时,也可以在函数名后面加一个括号,将 input 声明包起来。
例如上述大小端声明函数可以表示为:
function [N-1:0] data_rvs(
input [N-1:0] data_in
......
) ;
常数函数
常数函数是指在仿真开始之前,在编译期间就计算出结果为常数的函数。常数函数不允许访问全局变量或者调用系统函数,但是可以调用另一个常数函数。
这种函数能够用来引用复杂的值,因此可用来代替常量。
例如下面一个常量函数,可以来计算模块中地址总线的宽度:
实例
parameter MEM_DEPTH = 256 ;
reg [logb2(MEM_DEPTH)-1: 0] addr ; //可得addr的宽度为8bit
function integer logb2;
input integer depth ;
//256为9bit,我们最终数据应该是8,所以需depth=2时提前停止循环
for(logb2=0; depth>1; logb2=logb2+1) begin
depth = depth >> 1 ;
end
endfunction
automatic 函数
在 Verilog 中,一般函数的局部变量是静态的,即函数的每次调用,函数的局部变量都会使用同一个存储空间。若某个函数在两个不同的地方同时并发的调用,那么两个函数调用行为同时对同一块地址进行操作,会导致不确定的函数结果。
Verilog 用关键字 automatic 来对函数进行说明,此类函数在调用时是可以自动分配新的内存空间的,也可以理解为是可递归的。因此,automatic 函数中声明的局部变量不能通过层次命名进行访问,但是 automatic 函数本身可以通过层次名进行调用。
下面用 automatic 函数,实现阶乘计算:
实例
wire [31:0] results3 = factorial(4);
function automatic integer factorial ;
input integer data ;
integer i ;
begin
factorial = (data>=2)? data * factorial(data-1) : 1 ;
end
endfunction // factorial
下面是加关键字 automatic 和不加关键字 automatic 的仿真结果。
由图可知,信号 results3 得到了我们想要的结果,即 4 的阶乘。
而信号 results_noauto 值为 1,不是可预知的正常结果,这里不再做无用分析。
数码管译码
上述中涉及的相关函数知识似乎并没有体现出函数的优越性。下面设计一个 4 位 10 进制的数码管译码器,来说明函数可以简化代码的优点。
每位数码显示端有 8 个光亮控制端(如图中 a-g 所示),可以用来控制显示数字 0-9 。
而数码管有 4 个片选(如图中 1-4),用来控制此时哪一位数码显示端应该选通,即应该发光。倘若在很短的时间内,依次对 4 个数码显示端进行片选发光,同时在不同片选下给予不同的光亮控制(各对应 4 位十进制数字),那么在肉眼不能分辨的情况下,就达到了同时显示 4 位十进制数字的效果。
下面,我们用信号 abcdefg 来控制光亮控制端,用信号 csn 来控制片选,4 位 10 进制的数字个十百千位分别用 4 个 4bit 信号 single_digit, ten_digit, hundred_digit, kilo_digit 来表示,则一个数码管的显示设计可以描述如下:
实例
module digital_tube
(
input clk ,
input rstn ,
input en ,
input [3:0] single_digit ,
input [3:0] ten_digit ,
input [3:0] hundred_digit ,
input [3:0] kilo_digit ,
output reg [3:0] csn , //chip select, low-available
output reg [6:0] abcdefg //light control
);
reg [1:0] scan_r ; //scan_ctrl
always @ (posedge clk or negedge rstn) begin
if(!rstn)begin
csn <= 4'b1111;
abcdefg <= 'd0;
scan_r <= 3'd0;
end
else if (en) begin
case(scan_r)
2'd0:begin
scan_r <= 3'd1;
csn <= 4'b0111; //select single digit
abcdefg <= dt_translate(single_digit);
end
2'd1:begin
scan_r <= 3'd2;
csn <= 4'b1011; //select ten digit
abcdefg <= dt_translate(ten_digit);
end
2'd2:begin
scan_r <= 3'd3;
csn <= 4'b1101; //select hundred digit
abcdefg <= dt_translate(hundred_digit);
end
2'd3:begin
scan_r <= 3'd0;
csn <= 4'b1110; //select kilo digit
abcdefg <= dt_translate(kilo_digit);
end
endcase
end
end
/*------------ translate function -------*/
function [6:0] dt_translate;
input [3:0] data;
begin
case(data)
4'd0: dt_translate = 7'b1111110; //number 0 -> 0x7e
4'd1: dt_translate = 7'b0110000; //number 1 -> 0x30
4'd2: dt_translate = 7'b1101101; //number 2 -> 0x6d
4'd3: dt_translate = 7'b1111001; //number 3 -> 0x79
4'd4: dt_translate = 7'b0110011; //number 4 -> 0x33
4'd5: dt_translate = 7'b1011011; //number 5 -> 0x5b
4'd6: dt_translate = 7'b1011111; //number 6 -> 0x5f
4'd7: dt_translate = 7'b1110000; //number 7 -> 0x70
4'd8: dt_translate = 7'b1111111; //number 8 -> 0x7f
4'd9: dt_translate = 7'b1111011; //number 9 -> 0x7b
endcase
end
endfunction
endmodule
仿真结果如下。
由图可知,片选、译码等信号,均符合设计。实际中,4 位数字应当在一定的时间内保持不变,而片选信号不停的循环扫描,数码管才能给肉眼呈现一种静态显示的效果。
如果译码器设计没有使用函数 dt_translate,则在每个 case 选项里对信号 abcdefg 进行赋值时,还需要对 single_digit,ten_digit, hundred_digit, kilo_digit 进行判断。这些判断语句又会重复 4 次。虽然最后综合出的实际硬件电路可能是一样的,但显然使用函数后的代码更加的简洁、易读。
源码下载
Download
Verilog任务
任务与函数的区别
和函数一样,任务(task)可以用来描述共同的代码段,并在模块内任意位置被调用,让代码更加的直观易读。函数一般用于组合逻辑的各种转换和计算,而任务更像一个过程,不仅能完成函数的功能,还可以包含时序控制逻辑。下面对任务与函数的区别进行概括:
比较点 | 函数 | 任务 |
---|---|---|
输入 | 函数至少有一个输入,端口声明不能包含 inout 型 | 任务可以没有或者有多个输入,且端口声明可以为 inout 型 |
输出 | 函数没有输出 | 任务可以没有或者有多个输出 |
返回值 | 函数至少有一个返回值 | 任务没有返回值 |
仿真时刻 | 函数总在零时刻就开始执行 | 任务可以在非零时刻执行 |
时序逻辑 | 函数不能包含任何时序控制逻辑 | 任务不能出现 always 语句,但可以包含其他时序控制,如延时语句 |
调用 | 函数只能调用函数,不能调用任务 | 任务可以调用函数和任务 |
书写规范 | 函数不能单独作为一条语句出现,只能放在赋值语言的右端 | 任务可以作为一条单独的语句出现语句块中 |
任务
任务声明
任务在模块中任意位置定义,并在模块内任意位置引用,作用范围也局限于此模块。
模块内子程序出现下面任意一个条件时,则必须使用任务而不能使用函数。
- 子程序中包含时序控制逻辑,例如延迟,事件控制等
- 没有输入变量
- 没有输出或输出端的数量大于 1
Verilog 任务声明格式如下:
task task_id ;
port_declaration ;
procedural_statement ;
endtask
任务中使用关键字 input、output 和 inout 对端口进行声明。input 、inout 型端口将变量从任务外部传递到内部,output、inout 型端口将任务执行完毕时的结果传回到外部。
进行任务的逻辑设计时,可以把 input 声明的端口变量看做 wire 型,把 output 声明的端口变量看做 reg 型。但是不需要用 reg 对 output 端口再次说明。
对 output 信号赋值时也不要用关键字 assign。为避免时序错乱,建议 output 信号采用阻塞赋值。
例如,一个带延时的异或功能 task 描述如下:
实例
task xor_oper_iner;
input [N-1:0] numa;
input [N-1:0] numb;
output [N-1:0] numco ;
//output reg [N-1:0] numco ; //无需再注明 reg 类型,虽然注明也可能没错
#3 numco = numa ^ numb ;
//assign #3 numco = numa ^ numb ; //不用assign,因为输出默认是reg
endtask
任务在声明时,也可以在任务名后面加一个括号,将端口声明包起来。
上述设计可以更改为:
实例
task xor_oper_iner(
input [N-1:0] numa,
input [N-1:0] numb,
output [N-1:0] numco ) ;
#3 numco = numa ^ numb ;
endtask
任务调用
任务可单独作为一条语句出现在 initial 或 always 块中,调用格式如下:
task_id(input1, input2, …,outpu1, output2, …);
任务调用时,端口必须按顺序对应。
输入端连接的模块内信号可以是 wire 型,也可以是 reg 型。任务输出端连接的模块内信号要求一定是 reg 型,这点需要注意【与例化子模块是不一样的,例化子模块时,子模块的输出在本模块中应该为wire型变量】。
对上述异或功能的 task 进行一个调用,完成对异或结果的缓存。
实例
module xor_oper
#(parameter N = 4)
(
input clk ,
input rstn ,
input [N-1:0] a ,
input [N-1:0] b ,
output [N-1:0] co );
reg [N-1:0] co_t ;
always @(*) begin //任务调用
xor_oper_iner(a, b, co_t);
end
reg [N-1:0] co_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
co_r <= 'b0 ;
end
else begin
co_r <= co_t ; //数据缓存【为了实现同步输出】
end
end
assign co = co_r ;
/*------------ task -------*/
task xor_oper_iner;
input [N-1:0] numa;
input [N-1:0] numb;
output [N-1:0] numco ;
#3 numco = numa ^ numb ; //阻塞赋值,易于控制时序
endtask
endmodule
对上述异或功能设计进行简单的仿真,testbench 描述如下。
激励部分我们使用简单的 task 进行描述,激励看起来就更加的清晰简洁。
其实,task 最多的应用场景还是应用于 testbench 中进行仿真。task 在一些编译器中也不支持综合。
实例
`timescale 1ns/1ns
module test ;
reg clk, rstn ;
initial begin
rstn = 0 ;
#8 rstn = 1 ;
forever begin
clk = 0 ; # 5;
clk = 1 ; # 5;
end
end
reg [3:0] a, b;
wire [3:0] co ;
initial begin
a = 0 ;
b = 0 ;
sig_input(4'b1111, 4'b1001, a, b);
sig_input(4'b0110, 4'b1001, a, b);
sig_input(4'b1000, 4'b1001, a, b);
end
task sig_input ;
input [3:0] a ;
input [3:0] b ;
output [3:0] ao ;
output [3:0] bo ;
@(posedge clk) ;
ao = a ;
bo = b ;
endtask ; // sig_input
xor_oper u_xor_oper
(
.clk (clk ),
.rstn (rstn ),
.a (a ),
.b (b ),
.co (co ));
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
endmodule // test
仿真结果如下。
由图可知,异或输出逻辑结果正确,相对于输入有 3ns 的延迟。
且连接信号 a,b,co_t 与任务内部定义的信号 numa,numb,numco 状态也保持一致。
任务操作全局变量
因为任务可以看做是过程性赋值,所以任务的 output 端信号返回时间是在任务中所有语句执行完毕之后。
任务内部变量也只有在任务中可见,如果想具体观察任务中对变量的操作过程,需要将观察的变量声明在模块之内、任务之外,可谓之"全局变量"。
例如有以下 2 种尝试利用 task 产生时钟的描述方式。
实例
//way1 to decirbe clk generating, 不可行
task clk_rvs_iner ;
output clk_no_rvs ;
# 5 ; clk_no_rvs = 0 ;
# 5 ; clk_no_rvs = 1 ;
endtask
reg clk_test1 ;
always clk_rvs_iner(clk_test1);
//way2: use task to operate global varialbes to generating clk, 可行
reg clk_test2 ;
task clk_rvs_global ;
# 5 ; clk_test2 = 0 ;
# 5 ; clk_test2 = 1 ;
endtask // clk_rvs_iner
always clk_rvs_global;
仿真结果如下。
第一种描述方式,虽然任务内部变量会有赋值 0 和赋值 1 的过程操作,但中间变化过程并不可见,最后输出的结果只能是任务内所有语句执行完毕后输出端信号的最终值。所以信号 clk_test1 值恒为 1,此种方式产生不了时钟。
第二种描述方式,虽然没有端口信号,但是直接对"全局变量"进行过程操作,因为该全局变量对模块是可见的,所以任务内信号翻转的过程会在信号 clk_test2 中体现出来。
automatic 任务
和函数一样,Verilog 中任务调用时的局部变量都是静态的。可以用关键字 automatic 来对任务进行声明,那么任务调用时各存储空间就可以动态分配,每个调用的任务都各自独立的对自己独有的地址空间进行操作,而不影响多个相同任务调用时的并发执行。
如果一任务代码段被 2 处及以上调用,一定要用关键字 automatic 声明。
当没有使用 automatic 声明任务时,任务被 2 次调用,可能出现信号间干扰,例如下面代码描述:
实例
task test_flag ; input [3:0] cnti ; input en ; output [3:0] cnto ; if (en) cnto = cnti ; endtask reg en_cnt ; reg [3:0] cnt_temp ; initial begin en_cnt = 1 ; cnt_temp = 0 ; #25 ; en_cnt = 0 ; end always #10 cnt_temp = cnt_temp + 1 ; reg [3:0] cnt1, cnt2 ; always @(posedge clk) test_flag(2, en_cnt, cnt1); //task(1) always @(posedge clk) test_flag(cnt_temp, !en_cnt, cnt2);//task(2)
仿真结果如下。
en_cnt 为高时,任务 (1) 中信号 en 有效, cnt1 能输出正确的逻辑值;
此时任务 (2) 中信号 en 是不使能的,所以 cnt2 的值被任务 (1) 驱动的共用变量 cnt_temp 覆盖。
en_cnt 为低时,任务 (2) 中信号 en 有效,所以任务 (2) 中的信号 cnt2 能输出正确的逻辑值;而此时信号 cnt1 的值在时钟的驱动下,一次次被任务 (2) 驱动的共用变量 cnt_temp 覆盖。
【这个仿真的结果是有点奇怪的,有时间了应做个仿真看看,把task中变量的值都拉出来看看,最好再看看生成的RTL。】
可见,任务在两次并发调用中,共用存储空间,导致信号相互间产生了影响。
其他描述不变,只在上述 task 声明时加入关键字 automatic。
task automatic test_flag ;
此时仿真结果如下。
en_cnt 为高时,任务 (1) 中信号 cnt1 能输出正确的逻辑值,任务 (2) 中信号 cnt2 的值为 X;
en_cnt 为低时,任务 (2) 中信号 cnt2 能输出正确的逻辑值,任务 (1) 中信号 cnt1 的值为 X;
可见,任务在两次并发调用中,因为存储空间相互独立,信号间并没有产生影响。
源码下载
Download
Verilog状态机
有限状态机(Finite-State Machine,FSM),简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。状态机不仅是一种电路的描述工具,而且也是一种思想方法,在电路设计的系统级和 RTL 级有着广泛的应用。
状态机类型
Verilog 中状态机主要用于同步时序逻辑的设计,能够在有限个状态之间按一定要求和规律切换时序电路的状态。状态的切换方向不但取决于各个输入值,还取决于当前所在状态。 状态机可分为 2 类:Moore 状态机和 Mealy 状态机。
Moore 型状态机
Moore 型状态机的输出只与当前状态有关,与当前输入无关。
输出会在一个完整的时钟周期内保持稳定,即使此时输入信号有变化,输出也不会变化。输入对输出的影响要到下一个时钟周期才能反映出来。这也是 Moore 型状态机的一个重要特点:输入与输出是隔离开来的。
Mealy 型状态机
Mealy 型状态机的输出,不仅与当前状态有关,还取决于当前的输入信号。
Mealy 型状态机的输出是在输入信号变化以后立刻发生变化,且输入变化可能出现在任何状态的时钟周期内。因此,同种逻辑下,Mealy 型状态机输出对输入的响应会比 Moore 型状态机早一个时钟周期。
状态机设计流程
根据设计需求画出状态转移图,确定使用状态机类型,并标注出各种输入输出信号,更有助于编程。一般使用最多的是 Mealy 型 3 段式状态机,下面用通过设计一个自动售卖机的具体实例来说明状态机的设计过程。
实例:自动售卖机
自动售卖机的功能描述如下:
饮料单价 2 元,该售卖机只能接受 0.5 元、1 元的硬币。考虑找零和出货。投币和出货过程都是一次一次的进行,不会出现一次性投入多币或一次性出货多瓶饮料的现象。每一轮售卖机接受投币、出货、找零完成后,才能进入到新的自动售卖状态。
该售卖机的工作状态转移图如下所示,包含了输入、输出信号状态。
其中,coin = 1 代表投入了 0.5 元硬币,coin = 2 代表投入了 1 元硬币。
状态机设计:3 段式(推荐)
状态机设计如下:
(0) 首先,根据状态机的个数确定状态机编码。利用编码给状态寄存器赋值,代码可读性更好。
(1) 状态机第一段,时序逻辑,非阻塞赋值,传递寄存器的状态。
(2) 状态机第二段,组合逻辑,阻塞赋值,根据当前状态和当前输入,确定下一个状态机的状态。
(3) 状态机第三代,时序逻辑,非阻塞赋值,因为是 Mealy 型状态机,根据当前状态和当前输入,确定输出信号。
实例
// vending-machine
// 2 yuan for a bottle of drink
// only 2 coins supported: 5 jiao and 1 yuan
// finish the function of selling and changing
module vending_machine_p3 (
input clk ,
input rstn ,
input [1:0] coin , //01 for 0.5 jiao, 10 for 1 yuan
output [1:0] change ,
output sell //output the drink
);
//machine state decode
parameter IDLE = 3'd0 ;
parameter GET05 = 3'd1 ;
parameter GET10 = 3'd2 ;
parameter GET15 = 3'd3 ;
//machine variable
reg [2:0] st_next ;
reg [2:0] st_cur ;
//(1) state transfer
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
st_cur <= 'b0 ;
end
else begin
st_cur <= st_next ;
end
end
//(2) state switch, using block assignment for combination-logic
//all case items need to be displayed completely
always @(*) begin
//st_next = st_cur ;//如果条件选项考虑不全,可以赋初值消除latch
case(st_cur)
IDLE:
case (coin)
2'b01: st_next = GET05 ;
2'b10: st_next = GET10 ;
default: st_next = IDLE ;
endcase
GET05:
case (coin)
2'b01: st_next = GET10 ;
2'b10: st_next = GET15 ;
default: st_next = GET05 ;
endcase
GET10:
case (coin)
2'b01: st_next = GET15 ;
2'b10: st_next = IDLE ;
default: st_next = GET10 ;
endcase
GET15:
case (coin)
2'b01,2'b10:
st_next = IDLE ;
default: st_next = GET15 ;
endcase
default: st_next = IDLE ;
endcase
end
//(3) output logic, using non-block assignment
reg [1:0] change_r ;
reg sell_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
else if ((st_cur == GET15 && coin ==2'h1)
|| (st_cur == GET10 && coin ==2'd2)) begin
change_r <= 2'b0 ;
sell_r <= 1'b1 ;
end
else if (st_cur == GET15 && coin == 2'h2) begin
change_r <= 2'b1 ;
sell_r <= 1'b1 ;
end
else begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
end
assign sell = sell_r ;
assign change = change_r ;
endmodule
testbench 设计如下。仿真中模拟了 4 种情景,分别是:
case1 对应连续输入 4 个 5 角硬币;case2 对应 1 元 - 5 角 - 1 元的投币顺序;case3 对应 5 角 - 1 元 - 5 角的投币顺序;case4 对应连续 3 个 5 角然后一个 1 元的投币顺序。
实例
`timescale 1ns/1ps
module test ;
reg clk;
reg rstn ;
reg [1:0] coin ;
wire [1:0] change ;
wire sell ;
//clock generating
parameter CYCLE_200MHz = 10 ; //
always begin
clk = 0 ; #(CYCLE_200MHz/2) ;
clk = 1 ; #(CYCLE_200MHz/2) ;
end
//motivation generating
reg [9:0] buy_oper ; //store state of the buy operation
initial begin
buy_oper = 'h0 ;
coin = 2'h0 ;
rstn = 1'b0 ;
#8 rstn = 1'b1 ;
@(negedge clk) ;
//case(1) 0.5 -> 0.5 -> 0.5 -> 0.5
#16 ;
buy_oper = 10'b00_0101_0101 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
//case(2) 1 -> 0.5 -> 1, taking change
#16 ;
buy_oper = 10'b00_0010_0110 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
//case(3) 0.5 -> 1 -> 0.5
#16 ;
buy_oper = 10'b00_0001_1001 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
//case(4) 0.5 -> 0.5 -> 0.5 -> 1, taking change
#16 ;
buy_oper = 10'b00_1001_0101 ;
repeat(5) begin
@(negedge clk) ;
coin = buy_oper[1:0] ;
buy_oper = buy_oper >> 2 ;
end
end
//(1) mealy state with 3-stage
vending_machine_p3 u_mealy_p3 (
.clk (clk),
.rstn (rstn),
.coin (coin),
.change (change),
.sell (sell)
);
//simulation finish
always begin
#100;
if ($time >= 10000) $finish ;
end
endmodule // test
仿真结果如下:
由图可知,代表出货动作的信号 sell 都能在投币完毕后正常的拉高,而代表找零动作的信号 change 也都能根据输入的硬币场景输出正确的是否找零信号。
状态机修改:2 段式【将输出改成阻塞赋值,有可能导致竞争与冒险,不好】
将 3 段式状态机 2、3 段描述合并,其他部分保持不变,状态机就变成了 2 段式描述。
修改部分如下:
实例
//(2) state switch, and output logic
//all using block assignment for combination-logic
reg [1:0] change_r ;
reg sell_r ;
always @(*) begin //all case items need to be displayed completely
case(st_cur)
IDLE: begin
change_r = 2'b0 ;
sell_r = 1'b0 ;
case (coin)
2'b01: st_next = GET05 ;
2'b10: st_next = GET10 ;
default: st_next = IDLE ;
endcase // case (coin)
end
GET05: begin
change_r = 2'b0 ;
sell_r = 1'b0 ;
case (coin)
2'b01: st_next = GET10 ;
2'b10: st_next = GET15 ;
default: st_next = GET05 ;
endcase // case (coin)
end
GET10:
case (coin)
2'b01: begin
st_next = GET15 ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
2'b10: begin
st_next = IDLE ;
change_r = 2'b0 ;
sell_r = 1'b1 ;
end
default: begin
st_next = GET10 ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
endcase // case (coin)
GET15:
case (coin)
2'b01: begin
st_next = IDLE ;
change_r = 2'b0 ;
sell_r = 1'b1 ;
end
2'b10: begin
st_next = IDLE ;
change_r = 2'b1 ;
sell_r = 1'b1 ;
end
default: begin
st_next = GET15 ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
endcase
default: begin
st_next = IDLE ;
change_r = 2'b0 ;
sell_r = 1'b0 ;
end
endcase
end
将上述修改的新模块例化到 3 段式的 testbench 中即可进行仿真,结果如下:
由图可知,出货信号 sell 和 找零信号 change 相对于 3 段式状态机输出提前了一个时钟周期,这是因为输出信号都是阻塞赋值导致的。
如图中红色圆圈部分,输出信号都出现了干扰脉冲,这是因为输入信号都是异步的,而且输出信号是组合逻辑输出,没有时钟驱动。
实际中,如果输入信号都是与时钟同步的,这种干扰脉冲是不会出现的。如果是异步输入信号,首先应当对信号进行同步。
状态机修改:1 段式(慎用)
将 3 段式状态机 1、 2、3 段描述合并,状态机就变成了 1 段式描述。
修改部分如下:
实例
//(1) using one state-variable do describe
reg [1:0] change_r ;
reg sell_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
st_cur <= 'b0 ;
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
else begin
case(st_cur)
IDLE: begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
case (coin)
2'b01: st_cur <= GET05 ;
2'b10: st_cur <= GET10 ;
endcase
end
GET05: begin
case (coin)
2'b01: st_cur <= GET10 ;
2'b10: st_cur <= GET15 ;
endcase
end
GET10:
case (coin)
2'b01: st_cur <= GET15 ;
2'b10: begin
st_cur <= IDLE ;
sell_r <= 1'b1 ;
end
endcase
GET15:
case (coin)
2'b01: begin
st_cur <= IDLE ;
sell_r <= 1'b1 ;
end
2'b10: begin
st_cur <= IDLE ;
change_r <= 2'b1 ;
sell_r <= 1'b1 ;
end
endcase
default: begin
st_cur <= IDLE ;
end
endcase // case (st_cur)
end // else: !if(!rstn)
end
将上述修改的新模块例化到 3 段式的 testbench 中即可进行仿真,结果如下:
由图可知,输出信号与 3 段式状态机完全一致。
1 段式状态机的缺点就是许多种逻辑糅合在一起,不易后期的维护。当状态机和输出信号较少时,可以尝试此种描述方式。
状态机修改:Moore 型
如果使用 Moore 型状态机描述售卖机的工作流程,那么还需要再增加 2 个状态编码,用以描述 Mealy 状态机输出时的输入信号和状态机状态。
3 段式 Moore 型状态机描述的自动售卖机 Verilog 代码如下:
实例
module vending_machine_moore (
input clk ,
input rstn ,
input [1:0] coin , //01 for 0.5 jiao, 10 for 1 yuan
output [1:0] change ,
output sell //output the drink
);
//machine state decode
parameter IDLE = 3'd0 ;
parameter GET05 = 3'd1 ;
parameter GET10 = 3'd2 ;
parameter GET15 = 3'd3 ;
// new state for moore state-machine
parameter GET20 = 3'd4 ;
parameter GET25 = 3'd5 ;
//machine variable
reg [2:0] st_next ;
reg [2:0] st_cur ;
//(1) state transfer
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
st_cur <= 'b0 ;
end
else begin
st_cur <= st_next ;
end
end
//(2) state switch, using block assignment for combination-logic
always @(*) begin //all case items need to be displayed completely
case(st_cur)
IDLE:
case (coin)
2'b01: st_next = GET05 ;
2'b10: st_next = GET10 ;
default: st_next = IDLE ;
endcase
GET05:
case (coin)
2'b01: st_next = GET10 ;
2'b10: st_next = GET15 ;
default: st_next = GET05 ;
endcase
GET10:
case (coin)
2'b01: st_next = GET15 ;
2'b10: st_next = GET20 ;
default: st_next = GET10 ;
endcase
GET15:
case (coin)
2'b01: st_next = GET20 ;
2'b10: st_next = GET25 ;
default: st_next = GET15 ;
endcase
GET20: st_next = IDLE ;
GET25: st_next = IDLE ;
default: st_next = IDLE ;
endcase // case (st_cur)
end // always @ (*)
// (3) output logic,
// one cycle delayed when using non-block assignment
reg [1:0] change_r ;
reg sell_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
else if (st_cur == GET20 ) begin
sell_r <= 1'b1 ;
end
else if (st_cur == GET25) begin
change_r <= 2'b1 ;
sell_r <= 1'b1 ;
end
else begin
change_r <= 2'b0 ;
sell_r <= 1'b0 ;
end
end
assign sell = sell_r ;
assign change = change_r ;
endmodule
将上述修改的 Moore 状态机例化到 3 段式的 testbench 中即可进行仿真,结果如下:
由图可知,输出信号与 Mealy 型 3 段式状态机相比延迟了一个时钟周期,这是因为进入到新增加的编码状态机时需要一个时钟周期的时延。此时,输出再用非阻塞赋值就会导致最终的输出信号延迟一个时钟周期。这也属于 Moore 型状态机的特点。
输出信号赋值时,用阻塞赋值,则可以提前一个时钟周期。
【这是因为,使用非阻塞赋值时,输出的always块运行时, st_cur 的值还维持在上一排,虽然很快 st_cur 被赋值,但此时always块已经用旧的值判断了这一拍的输出。】
输出逻辑修改如下。
实例
// (3.2) output logic, using block assignment
reg [1:0] change_r ;
reg sell_r ;
always @(*) begin
change_r = 'b0 ;
sell_r = 'b0 ; //not list all condition, initializing them
if (st_cur == GET20 ) begin
sell_r = 1'b1 ;
end
else if (st_cur == GET25) begin
change_r = 2'b1 ;
sell_r = 1'b1 ;
end
end
输出信号阻塞赋值的仿真结果如下:
由图可知,输出信号已经和 3 段式 Mealy 型状态机一致。
源码下载
Download
Verilog竞争与冒险
产生原因
数字电路中,信号传输与状态变换时都会有一定的延时。
- 在组合逻辑电路中,不同路径的输入信号变化传输到同一点门级电路时,在时间上有先有后,这种先后所形成的时间差称为竞争(Competition)。
- 由于竞争的存在,输出信号需要经过一段时间才能达到期望状态,过渡时间内可能产生瞬间的错误输出,例如尖峰脉冲。这种现象被称为冒险(Hazard)。
- 竞争不一定有冒险,但冒险一定会有竞争。
例如,对于给定逻辑 \(F = A \& A'\),电路如左下图所示。
理想情况下输入与输出的关系为:
由于反相器电路的存在,信号 A' 传递到与门输入端的时间相对于信号 A 会滞后(门电路的输入到输出是一定会有时间延迟的),这就可能导致与门最后的输出结果 F 会出现干扰脉冲。如右下图所示。
其实实际硬件电路中,只要门电路各个输入端延时不同,就有可能产生竞争与冒险。
例如一个简单的与门,输入信号源不一定是同一个信号变换所来,由于硬件工艺、其他延迟电路的存在,也可能产生竞争与冒险,如下图所示。
判断方法
代数法
在逻辑函数表达式中,若某个变量同时以原变量和反变量两种形式出现, 就具备了竞争条件。去掉其余变量(也就是将其余变量取固定值 0 或 1),留下有竞争能力的变量,如果表达式为 F=A+A',就会产生 0 型冒险(F应该为1而实际却为0);如果表达式为 F=AA', 就会产生 1 型冒险。
表达式 F=AB+A'C,当 B=C=1 时,F=A+A',在A发生跳变时,可能出现 0 型冒险。
表达式F=(A+B)(A'+C),当B=C=0 时,F=AA',在A发生跳变时,可能出现 1 型冒险
卡诺图法
有两个相切的卡诺圈,并且相切处没有其他卡诺圈包围,可能会出现竞争与冒险现象。
例如左下图所存在竞争与冒险,右下图则没有。
其实,卡诺图本质上还是对逻辑表达式的一个分析,只是可以进行直观的判断。
例如,左上图逻辑表达式可以简化为 Y = A'B' + AC,当 B=0 且 C=1 时,此逻辑表达式又可以表示为 Y = A' + A。所以肯定会存在竞争与冒险。
右上图逻辑表达式可以简化为 Y = A'B' + AB,显然 B 无论等于 1 还是 0,此式都不会化简成 Y = A' + A。所以此逻辑不存在竞争与冒险。
需要注意的是,卡诺图是首尾相临的。如下图所示,虽然看起来两个卡诺圈并没有相切,但实际上,m6 与 m4 也是相邻的,所以下面卡诺图所代表的数字逻辑也会产生竞争与冒险。
观察法
根据电路图,观察输入变量到达输出端的多个路径上经过的门电路器件的数量是否不同,如果不同,则有可能尝试竞争-冒险现象
仿真与实验法
实际上逻辑电路级数多、输入变量多,判断竞争冒险非常复杂,广泛采用仿真和实际测量逻辑电路来观察是否存在竞争冒险的现象。
消除方法
接入滤波电容,消除毛刺的影响
利用电容的滤波作用,消去尖峰脉冲,简单易行,效果有保障,但增加输出电压的翻转时间(增加了输出电压波形的上升时间和下降时间),使波形变坏。
假设出现了 F=AA',产生了不应有的正向尖峰脉冲,那么再输出信号F前加一个电容后,这就会使得电容电压不可突变,当F中的电压发生突变时,由于电容的存在,将其变成一个充放电的过程,即电压突变就变成了一个缓慢的充放电过程,见下图。对于数字逻辑而言,高电平和低电平是一个允许的范围,所以这种方法对于逻辑工作而言,完全有保障,对于后级电路不会产生任何逻辑混乱
不足之处,因为电容的滤波作用,增加了输出电压波形的上升时间和下降时间,使波形变坏,见下图,虽然波形不好看了,但整体上影响不是很大。
引入选通脉冲,避开毛刺
在输入信号变化,并有可能出现竞争冒险,选用一个选通信号将输出门封锁,等到所有输入信号都变为稳态后,再去掉封锁输出信号。这样就避免了电路输出端出现瞬时尖峰脉冲。
该方法虽然消除了竞争-冒险,但也会带来一些电路的负担,或者说付出的成本。对选通控制信号的宽度(足够宽)和产生的时间(在C=0之前)严格要求,电路工作变复杂了。
修改逻辑设计,增加冗余项消除逻辑冒险
适当的逻辑表达式中增加一些冗余项,它们的增加不改变逻辑功能,但是可以消除互补信号产生的竞争-冒险现象。
例如:F=AB+A'C
由表达式得到卡诺图,两个卡诺圈相切处即可产生竞争-冒险现象 F=A+A' (相切处B=1,C=1),那怎样增加冗余项呢?
在相切处多画一个卡诺圈,将切点给覆盖掉,得到F=AB+A'C+BC,多了BC并不影响结果,此时当B=C=1时,F=A+A'+1=1。这意味这不管B、C怎么变,F恒为1,这样竞争-冒险就消失了。但电路会相对变得复杂。
使用时钟同步电路,利用触发器进行打拍延迟
同步电路信号的变化都发生在时钟边沿。对于触发器的 D 输入端,只要毛刺不出现在时钟的上升沿并且不满足数据的建立和保持时间,就不会对系统造成危害,因此可认为 D 触发器的 D 输入端对毛刺不敏感。 利用此特性,在时钟边沿驱动下,对一个组合逻辑信号进行延迟打拍,可消除竞争冒险。
延迟一拍时钟时,会一定概率的减少竞争冒险的出现。实验表明,最安全的打拍延迟周期是 3 拍,可有效减少竞争冒险的出现。
当然,最终还是需要根据自己的设计需求,对信号进行合理的打拍延迟。
为说明对信号进行打拍延迟可以消除竞争冒险,我们建立下面的代码模型。
实例
module competition_hazard
(
input clk ,
input rstn ,
input en ,
input din_rvs ,
output reg flag
);
wire condition = din_rvs & en ; //combination logic
always @(posedge clk or negedge !rstn) begin
if (!rstn) begin
flag <= 1'b0 ;
end
else begin
flag <= condition ;
end
end
endmodule
testbench 描述如下:
实例
`timescale 1ns/1ns
module test ;
reg clk, rstn ;
reg en ;
reg din_rvs ;
wire flag_safe, flag_dgs ;
//clock and rstn generating
initial begin
rstn = 1'b0 ;
clk = 1'b0 ;
#5 rstn = 1'b1 ;
forever begin
#5 clk = ~clk ;
end
end
initial begin
en = 1'b0 ;
din_rvs = 1'b1 ;
#19 ; en = 1'b1 ;
#1 ; din_rvs = 1'b0 ;
end
competition_hazard u_dgs
(
.clk (clk ),
.rstn (rstn ),
.en (en ),
.din_rvs (din_rvs ),
.flag (flag_dgs ));
initial begin
forever begin
#100;
if ($time >= 1000) $finish ;
end
end
endmodule // test
仿真结果如下:
由图可知,信号 condition 出现了一个尖峰脉冲,这是由于信号 din_rvs 与信号 en 相对于模块内部时钟都是异步的,所以到达内部门电路时的延时是不同的,就有可能造成竞争冒险。
虽然最后的仿真结果 flag 一直为 0,似乎是我们想要的结果。但是实际电路中,这个尖峰脉冲在时间上非常靠近时钟边沿,就有可能被时钟采集到而产生错误结果。
下面我们对模型进行改进,增加打拍延时【对输入打拍】的逻辑,如下:
实例
module clap_delay
(
input clk ,
input rstn ,
input en ,
input din_rvs ,
output reg flag
);
reg din_rvs_r ;
reg en_r ;
always @(posedge clk or !rstn) begin
if (!rstn) begin
din_rvs_r <= 1'b0 ;
en_r <= 1'b0 ;
end
else begin
din_rvs_r <= din_rvs ;
en_r <= en ;
end
end
wire condition = din_rvs_r & en_r ;
always @(posedge clk or negedge !rstn) begin
if (!rstn) begin
flag <= 1'b0 ;
end
else begin
flag <= condition ;
end
end // always @ (posedge clk or negedge !rstn)
endmodule
将此模块例化到上述 testbench 中,得到如下仿真结果。
由图可知,信号 condition 没有尖峰脉冲的干扰了,仿真结果中 flag 为 0 也如预期。
其实,输入信号与时钟边沿非常接近的情况下,时钟对输入信号的采样也存在不确定性,但是不会出现尖峰脉冲的现象。对输入信号多打 2 拍,是更好的处理方式,对竞争与冒险有更好的抑制作用。
采用格雷码计数器
递加的多 bit 位计数器,计数值有时候会发生多个 bit 位的跳变。
例如计数器变量 counter 从 5 计数到 6 时, 对应二进制数字为 4'b101 到 4'b110 的转换。因为各 bit 数据位的延时,counter 的变换过程可能是: 4'b101 -> 4'b111 -> 4'b110。如果有以下逻辑描述,则信号 cout 可能出现短暂的尖峰脉冲,这显然是与设计相悖的。
cout = counter[3:0] == 4'd7 ;
而格雷码计数器,计数时相邻的数之间只有一个数据 bit 发生了变化,所以能有效的避免竞争冒险。
好在 Verilog 设计时,计数器大多都是同步设计。即便计数时存在多个 bit 同时翻转的可能性,但在时钟驱动的触发器作用下,只要信号间满足时序要求,就能消除掉 100% 的竞争与冒险。
小结
一般来说,为消除竞争冒险,增加滤波电容和逻辑冗余,都不是 Verilog 设计所考虑的。
计数采用格雷码计数器,大多数也是应用在高速时钟下减少信号翻转率来降低功耗的场合。
利用触发器在时钟同步电路下对异步信号进行打拍延时,是 Verilog 设计中经常用到的方法。
除此之外,为消除竞争冒险,Verilog 编码时还需要注意一些问题,详见下一小节。
源码下载
Download
Verilog书写规范
在编程时多注意以下几点,也可以避免大多数的竞争与冒险问题。
- 时序电路建模时,用非阻塞赋值。
- 组合逻辑建模时,用阻塞赋值。
- 在同一个 always 块中建立时序和组合逻辑模型时,用非阻塞赋值。
- 在同一个 always 块中不要既使用阻塞赋值又使用非阻塞赋值。
- 不要在多个 always 块中为同一个变量赋值。
- 避免 latch 产生。
下面,对以上注意事项逐条分析。
时序电路建模时,用非阻塞赋值
前面讲述非阻塞赋值时就陈述过,时序电路中非阻塞赋值可以消除竞争冒险。
例如下面代码描述,由于无法确定 a 与 b 阻塞赋值的操作顺序,就有可能带来竞争冒险。
【 2 个 always 块中的语句是同时进行的,但是 a=b 与 b=a 是无法判定执行顺序的,这就造成了竞争的局面。
但不管哪个先执行(和编译器等有关系),不考虑 timing 问题时,他们执行顺序总有先后,最后 a 与 b 的值总是相等的。没有达到交换 2 个寄存器值的效果。】
always @(posedge clk) begin
a = b ;
b = a ;
end
而使用非阻塞赋值时,赋值操作是同时进行的,所以就不会带来竞争冒险,如以下代码描述。
always @(posedge clk) begin
a <= b ;
b <= a ;
end
组合逻辑建模时,用阻塞赋值
例如,我们想实现 C = A&B, F=C&D 的组合逻辑功能,用非阻塞赋值语句如下。
两条赋值语句同时赋值,F <= C & D 中使用的是信号 C 的旧值,所以导致此时的逻辑是错误的,F 的逻辑值不等于 A&B&D。
而且,此时要求信号 C 具有存储功能,但不是时钟驱动,所以 C 可能会被综合成锁存器(latch),导致竞争冒险。
always @(*) begin
C <= A & B ;
F <= C & D ;
end
对代码进行如下修改,F = C & D 的操作一定是在 C = A & B 之后,此时 F 的逻辑值等于 A&B&D,符合设计。
always @(*) begin
C = A & B ;
F = C & D ;
end
在同一个 always 块中建立时序和组合逻辑模型时,用非阻塞赋值
虽然时序电路中可能涉及组合逻辑,但是如果赋值操作使用非阻塞赋值,仍然会导致如规范 1 中所涉及的类似问题。
例如在时钟驱动下完成一个与门的逻辑功能,代码参考如下。
实例
always @(posedge clk or negedge rst_n)
if (!rst_n) begin
q <= 1'b0;
end
else begin
q <= a & b; //即便有组合逻辑,也不要写成:q = a & b
end
end
在同一个 always 块中不要既使用阻塞赋值又使用非阻塞赋值
always 涉及的组合逻辑中,既有阻塞赋值又有非阻塞赋值时,会导致意外的结果,例如下面代码描述。
此时信号 C 阻塞赋值完毕以后,信号 F 才会被非阻塞赋值,仿真结果可能正确。
但如果 F 信号有其他的负载,F 的最新值并不能马上传递出去,数据有效时间还是在下一个触发时刻。此时要求 F 具有存储功能,可能会被综合成 latch,导致竞争冒险。
always @(*) begin
C = A & B ;
F <= C & D ;
end
如下代码描述,仿真角度看,信号 C 被非阻塞赋值,下一个触发时刻才会有效。而 F = C & D 虽然是阻塞赋值,但是信号 C 不是阻塞赋值,所以 F 逻辑中使用的还是 C 的旧值。
always @(*) begin
C <= A & B ;
F = C & D ;
end
下面分析假如在时序电路里既有阻塞赋值,又有非阻塞赋值会怎样,代码如下。
假如复位端与时钟同步,那么由于复位导致的信号 q 为 0,是在下一个时钟周期才有效。
而如果是信号 a 或 b 导致的 q 为 0,则在当期时钟周期内有效。
如果 q 还有其他负载,就会导致 q 的时序特别混乱,显然不符合设计需求。
实例
always @(posedge clk or negedge rst_n)
if (!rst_n) begin //假设复位与时钟同步
q <= 1'b0;
end
else begin
q = a & b;
end
end
需要说明的是,很多编译器都支持这么写,上述的分析也都是建立在仿真角度上。实际中如果阻塞赋值和非阻塞赋值混合编写,综合后的电路时序将是错乱的,不利于分析调试。
不要在多个 always 块中为同一个变量赋值【禁止多驱】
与 C 语言有所不同,Verilog 中不允许在多个 always 块中为同一个变量赋值。此时信号拥有多驱动端(Multiple Driver),是禁止的。当然,也不允许 assign 语句为同一个变量进行多次连线赋值。 从信号角度来讲,多驱动时,同一个信号变量在很短的时间内进行多次不同的赋值结果,就有可能产生竞争冒险。
从语法来讲,很多编译器检测到多驱动时,也会报 Error。
避免 latch 产生
锁存器、触发器、寄存器
锁存器(Latch),是电平触发的存储单元,数据存储的动作取决于输入时钟(或者使能)信号的电平值。仅当锁存器处于使能状态时,输出才会随着数据输入发生变化。
当使能时,输出信号随输入信号变化,就像通过了缓冲器;不使能时,输出信号被锁存。激励信号的任何变化,都将直接引起锁存器输出状态的改变,很有可能会因为瞬态特性不稳定而产生振荡现象。
锁存器示意图如下:
触发器(flip-flop),是边沿敏感的存储单元,数据存储的动作(状态转换)由某一信号的上升沿或者下降沿进行同步的(限制存储单元状态转换在一个很短的时间内)。
触发器示意图如下:
寄存器(register),在 Verilog 中用来暂时存放参与运算的数据和运算结果的变量。一个变量声明为寄存器时,它既可以被综合成触发器,也可能被综合成 Latch,甚至是 wire 型变量。但是大多数情况下我们希望它被综合成触发器,但是有时候由于代码书写问题,它会被综合成不期望的 Latch 结构。
Latch 的主要危害有:
- 输入状态可能多次变化,容易产生毛刺,增加了下一级电路的不确定性;
- 在大部分 FPGA 的资源中,可能需要比触发器更多的资源去实现 Latch 结构;
- 锁存器的出现使得静态时序分析变得更加复杂。
Latch 多用于门控时钟(clock gating)的控制,一般设计时,我们应当避免 Latch 的产生。
if 结构不完整
组合逻辑中,不完整的 if - else 结构,会产生 latch。
例如下面的模型,if 语句中缺少 else 结构,系统默认 else 的分支下寄存器 q 的值保持不变,即具有存储数据的功能,所以寄存器 q 会被综合成 latch 结构。
实例
module module1_latch1(
input data,
input en ,
output reg q) ;
always @(*) begin
if (en) q = data ;
end
endmodule
避免此类 latch 的方法主要有 2 种,一种是补全 if-else 结构,或者对信号赋初值。
例如,上面模型中的always语句,可以改为以下两种形式:
实例
// 补全条件分支结构
always @(*) begin
if (en) q = data ;
else q = 1'b0 ;
end
//赋初值
always @(*) begin
q = 1'b0 ;
if (en) q = data ; //如果en有效,改写q的值,否则q会保持为0
end
但是在时序逻辑中,不完整的 if - else 结构,不会产生 latch,例如下面模型。
这是因为,q 寄存器具有存储功能,且其值在时钟的边沿下才会改变,这正是触发器的特性。
实例
module module1_ff(
input clk ,
input data,
input en ,
output reg q) ;
always @(posedge clk) begin
if (en) q <= data ;
end
endmodule
在组合逻辑中,当条件语句中有很多条赋值语句时,每个分支条件下赋值语句的不完整也是会产生 latch。
其实对每个信号的逻辑拆分来看,这也相当于是 if-else 结构不完整,相关寄存器信号缺少在其他条件下的赋值行为。例如:
实例
module module1_latch11(
input data1,
input data2,
input en ,
output reg q1 ,
output reg q2) ;
always @(*) begin
if (en) q1 = data1 ;
else q2 = data2 ;
end
endmodule
这种情况也可以通过补充完整赋值语句或赋初值来避免 latch。例如:
实例
always @(*) begin
//q1 = 0; q2 = 0 ; //或在这里对 q1/q2 赋初值
if (en) begin
q1 = data1 ;
q2 = 1'b0 ;
end
else begin
q1 = 1'b0 ;
q2 = data2 ;
end
end
case 结构不完整
case 语句产生 Latch 的原理几乎和 if 语句一致。在组合逻辑中,当 case 选项列表不全且没有加 default 关键字,或有多个赋值语句不完整时,也会产生 Latch。例如:
实例
module module1_latch2(
input data1,
input data2,
input [1:0] sel ,
output reg q ) ;
always @(*) begin
case(sel)
2'b00: q = data1 ;
2'b01: q = data2 ;
endcase
end
endmodule
当然,消除此种 latch 的方法也是 2 种,将 case 选项列表补充完整,或对信号赋初值。
补充完整 case 选项列表时,可以罗列所有的选项结果,也可以用 default 关键字来代替其他选项结果。
例如,上述 always 语句有以下 2 种修改方式。
实例
always @(*) begin
case(sel)
2'b00: q = data1 ;
2'b01: q = data2 ;
default: q = 1'b0 ;
endcase
end
always @(*) begin
case(sel)
2'b00: q = data1 ;
2'b01: q = data2 ;
2'b10, 2'b11 :
q = 1'b0 ;
endcase
end
原信号赋值或判断
在组合逻辑中,如果一个信号的赋值源头有其信号本身,或者判断条件中有其信号本身的逻辑,则也会产生 latch。因为此时信号也需要具有存储功能,但是没有时钟驱动。此类问题在 if 语句、case 语句、问号表达式中都可能出现,例如:
实例
//signal itself as a part of condition
reg a, b ;
always @(*) begin
if (a & b) a = 1'b1 ; //a -> latch
else a = 1'b0 ;
end
//signal itself are the assigment source
reg c;
wire [1:0] sel ;
always @(*) begin
case(sel)
2'b00: c = c ; //c -> latch
2'b01: c = 1'b1 ;
default: c = 1'b0 ;
endcase
end
//signal itself as a part of condition in "? expression"
wire d, sel2;
assign d = (sel2 && d) ? 1'b0 : 1'b1 ; //d -> latch
避免此类 Latch 的方法,就只有一种,即在组合逻辑中避免这种写法,信号不要给信号自己赋值,且不要用赋值信号本身参与判断条件逻辑。
例如,如果不要求立刻输出,可以将信号进行一个时钟周期的延时再进行相关逻辑的组合。上述第一个产生 Latch 的代码可以描述为:
实例
reg a, b ;
reg a_r ;
always (@posedge clk)
a_r <= a ;
always @(*) begin
if (a_r & b) a = 1'b1 ; //there is no latch
else a = 1'b0 ;
end
敏感信号列表不完整
如果组合逻辑中 always@() 块内敏感列表没有列全,该触发的时候没有触发,那么相关寄存器还是会保存之前的输出结果,因而会生成锁存器。
这种情况,把敏感信号补全或者直接用 always@(*) 即可消除 latch。
小结
总之,为避免 latch 的产生,在组合逻辑中,需要注意以下几点:
- if-else 或 case 语句,结构一定要完整
- 不要将赋值信号放在赋值源头,或条件判断中
- 敏感信号列表建议多用 always@(*)
源码下载
Download
Verilog仿真激励
Verilog 代码设计完成后,还需要进行重要的步骤,即逻辑功能仿真。仿真激励文件称之为 testbench,放在各设计模块的顶层,以便对模块进行系统性的例化调用进行仿真。
毫不夸张的说,对于稍微复杂的 Verilog 设计,如果不进行仿真,即便是经验丰富的老手,99.9999% 以上的设计都不会正常的工作。不能说仿真比设计更加的重要,但是一般来说,仿真花费的时间会比设计花费的时间要多。有时候,考虑到各种应用场景,testbench 的编写也会比 Verilog 设计更加的复杂。所以,数字电路行业会具体划分设计工程师和验证工程师。
下面,对 testbench 做一个简单的学习。
testbench 结构划分
testbench 一般结构如下:
其实 testbench 最基本的结构包括信号声明、激励和模块例化。
根据设计的复杂度,需要引入时钟和复位部分。当然更为复杂的设计,激励部分也会更加复杂。根据自己的验证需求,选择是否需要自校验和停止仿真部分。
当然,复位和时钟产生部分,也可以看做激励,所以它们都可以在一个语句块中实现。也可以拿自校验的结果,作为结束仿真的条件。
实际仿真时,可以根据自己的个人习惯来编写 testbench,这里只是做一份个人的总结。
testbench 仿真举例
前面的章节中,已经写过很多的 testbench。其实它们的结构也都大致相同。
下面,我们举一个数据拼接的简单例子,对 testbench 再做一个具体的分析。
一个 2bit 数据拼接成 8bit 数据的功能模块描述如下:
实例
module data_consolidation
(
input clk ,
input rstn ,
input [1:0] din , //data in
input din_en ,
output [7:0] dout ,
output dout_en //data out
);
// data shift and counter
reg [7:0] data_r ;
reg [1:0] state_cnt ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
state_cnt <= 'b0 ;
data_r <= 'b0 ;
end
else if (din_en) begin
state_cnt <= state_cnt + 1'b1 ; //数据计数
data_r <= {data_r[5:0], din} ; //数据拼接
end
else begin
state_cnt <= 'b0 ;
end
end
assign dout = data_r ;
// data output en
reg dout_en_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
dout_en_r <= 'b0 ;
end
//计数为 3 且第 4 个数据输入时,同步输出数据输出使能信号
else if (state_cnt == 2'd3 & din_en) begin
dout_en_r <= 1'b1 ;
end
else begin
dout_en_r <= 1'b0 ;
end
end
//这里不直接声明dout_en为reg变量,而是用相关寄存器对其进行assign赋值
assign dout_en = dout_en_r;
endmodule
对应的 testbench 描述如下,增加了文件读写的语句:
实例
`timescale 1ns/1ps
//============== (1) ==================
//signals declaration
module test ;
reg clk;
reg rstn ;
reg [1:0] din ;
reg din_en ;
wire [7:0] dout ;
wire dout_en ;
//============== (2) ==================
//clock generating
real CYCLE_200MHz = 5 ; //
always begin
clk = 0 ; #(CYCLE_200MHz/2) ;
clk = 1 ; #(CYCLE_200MHz/2) ;
end
//============== (3) ==================
//reset generating
initial begin
rstn = 1'b0 ;
#8 rstn = 1'b1 ;
end
//============== (4) ==================
//motivation
int fd_rd ;
reg [7:0] data_in_temp ; //for self check
reg [15:0] read_temp ; //8bit ascii data, 8bit \n
initial begin
din_en = 1'b0 ; //(4.1)
din = 'b0 ;
open_file("../tb/data_in.dat", "r", fd_rd); //(4.2)
wait (rstn) ; //(4.3)
# CYCLE_200MHz ;
//read data from file
while (! $feof(fd_rd) ) begin //(4.4)
@(negedge clk) ;
$fread(read_temp, fd_rd);
din = read_temp[9:8] ;
data_in_temp = {data_in_temp[5:0], din} ;
din_en = 1'b1 ;
end
//stop data
@(posedge clk) ; //(4.5)
#2 din_en = 1'b0 ;
end
//open task
task open_file;
input string file_dir_name ;
input string rw ;
output int fd ;
fd = $fopen(file_dir_name, rw);
if (! fd) begin
$display("--- iii --- Failed to open file: %s", file_dir_name);
end
else begin
$display("--- iii --- %s has been opened successfully.", file_dir_name);
end
endtask
//============== (5) ==================
//module instantiation
data_consolidation u_data_process
(
.clk (clk),
.rstn (rstn),
.din (din),
.din_en (din_en),
.dout (dout),
.dout_en (dout_en)
);
//============== (6) ==================
//auto check
reg [7:0] err_cnt ;
int fd_wr ;
initial begin
err_cnt = 'b0 ;
open_file("../tb/data_out.dat", "w", fd_wr);
forever begin
@(negedge clk) ;
if (dout_en) begin
$fdisplay(fd_wr, "%h", dout);
end
end
end
always @(posedge clk) begin
#1 ;
if (dout_en) begin
if (data_in_temp != dout) begin
err_cnt = err_cnt + 1'b1 ;
end
end
end
//============== (7) ==================
//simulation finish
always begin
#100;
if ($time >= 10000) begin
if (!err_cnt) begin
$display("-------------------------------------");
$display("Data process is OK!!!");
$display("-------------------------------------");
end
else begin
$display("-------------------------------------");
$display("Error occurs in data process!!!");
$display("-------------------------------------");
end
#1 ;
$finish ;
end
end
endmodule // test
仿真结果如下。由图可知,数据整合功能的设计符合要求:
testbench 具体分析
信号声明
testbench 模块声明时,一般不需要声明端口。因为激励信号一般都在 testbench 模块内部,没有外部信号。
声明的变量应该能全部对应被测试模块的端口。当然,变量不一定要与被测试模块端口名字一样。但是被测试模块输入端对应的变量应该声明为 reg 型【应该wire也行吧?】,如 clk,rstn 等,输出端对应的变量应该声明为 wire 型,如 dout,dout_en。
时钟生成
生成时钟的方式有很多种,例如以下两种生成方式也可以借鉴。
实例
initial clk = 0 ;
always #(CYCLE_200MHz/2) clk = ~clk;
initial begin
clk = 0 ;
forever begin
#(CYCLE_200MHz/2) clk = ~clk;
end
end
需要注意的是,利用取反方法产生时钟时,一定要给 clk 寄存器赋初值。
利用参数的方法去指定时间延迟时,如果延时参数为浮点数,该参数不要声明为 parameter 类型。例如实例中变量 CYCLE_200MHz 的值为 2.5。如果其变量类型为 parameter,最后生成的时钟周期很可能就是 4ns。当然,timescale 的精度也需要提高,单位和精度不能一样,否则小数部分的时间延迟赋值也将不起作用。
复位生成
复位逻辑比较简单,一般赋初值为 0,再经过一段小延迟后,复位为 1 即可。
这里大多数的仿真都是用的低有效复位。
激励部分
激励部分该产生怎样的输入信号,是根据被测模块的需要来设计的。
本次实例中:
-
(4.1) 对被测模块的输入信号进行一个初始化,防止不确定值 X 的出现。激励数据的产生,我们需要从数据文件内读入。
-
(4.2) 处利用一个 task 去打开一个文件,只要指定文件存在,就可以得到一个不为 0 的句柄信号 fp_rd。fp_rd 指定了文件数据的起始地址。
-
(4.3) 的操作是为了等待复位后,系统有一个安全稳定的可测试状态。
-
(4.4) 开始循环读数据、给激励。在时钟下降沿送出数据,是为了被测试模块能更好的在上升沿采样数据。
利用系统任务 $fread ,通过句柄信号 fd_rd 将读取的 16bit 数据变量送入到 read_temp 缓存。
输入数据文件前几个数据截图如下。因为 $fread 只能读取 2 进制文件,所以输入文件的第一行对应的 ASCII 码应该是 330a,所以我们想要得到文件里的数据 3,应该取变量 read_temp 的第 9 到第 8bit 位的数据。
信号 data_in_temp 是对输入数据信号的一个紧随的整合,后面校验模块会以此为参考,来判断仿真是否正常,模块设计是否正确。
-
(4.5) 选择在时钟上升沿延迟 2 个周期后停止输入数据,是为了被测试模块能够正常的采样到最后一个数据使能信号,并对数据进行正常的整合。
当数据量相对较少时,可以利用 Verilog 中的系统任务 $readmemh 来按行直接读取 16 进制数据。保持文件 data_in.dat 内数据和格式不变,则该激励部分可以描述为:
实例
reg [1:0] data_mem [39:0] ;
reg [7:0] data_in_temp ; //for self check
integer k1 ;
initial begin
din_en = 1'b0 ;
din = 'b0 ;
$readmemh("../tb/data_in.dat", data_mem);
wait (rstn) ;
# CYCLE_200MHz ;
//read data from file
for(k1=0; k1<40; k1=k1+1) begin
@(negedge clk) ;
din = data_mem[k1] ;
data_in_temp = {data_in_temp[5:0], din} ;
din_en = 1'b1 ;
end
//stop data
@(posedge clk) ;
#2 din_en = 1'b0 ;
end
模块例化
这里利用 testbench 开始声明的信号变量,对被测试模块进行例化连接。
自校验
如果设计比较简单,完全可以通过输入、输出信号的波形来确定设计是否正确,此部分完全可以删除。如果数据很多,有时候拿肉眼观察并不能对设计的正确性进行一个有效判定。此时加入一个自校验模块,会大大增加仿真的效率。
实例中,我们会在数据输出使能 dout_en 有效时,对输出数据 dout 与参考数据 read_temp(激励部分产生)做一个对比,并将对比结果置于信号 err_cnt 中。最后就可以通过观察 err_cnt 信号是否为 0 来直观的对设计的正确性进行判断。
当然如实例中所示,我们也可以将数据写入到对应文件中,利用其他方式做对比。
结束仿真
如果我们不加入结束仿真部分,仿真就会无限制的运行下去,波形太长有时候并不方便分析。Verilog 中提供了系统任务 $finish 来停止仿真。
停止仿真之前,可以将自校验的结果,通过系统任务 $display 在终端进行显示。
文件读写选项
用于打开文件的系统任务 $fopen 格式如下:
fd = $fopen("<name_of_file>", "mode")
和 C 语言类似,打开方式的选项 "mode" 意义如下:
mode | 含义 |
---|---|
r | 只读打开一个文本文件,只允许读数据。 |
w | 只写打开一个文本文件,只允许写数据。如果文件存在,则原文件内容会被删除。如果文件不存在,则创建新文件。 |
a | 追加打开一个文本文件,并在文件末尾写数据。如果文件如果文件不存在,则创建新文件。 |
rb | 只读打开一个二进制文件,只允许读数据。 |
wb | 只写打开或建立一个二进制文件,只允许写数据。 |
ab | 追加打开一个二进制文件,并在文件末尾写数据。 |
r+ | 读写打开一个文本文件,允许读和写 |
w+ | 读写打开或建立一个文本文件,允许读写。如果文件存在,则原文件内容会被删除。如果文件不存在,则创建新文件。 |
a+ | 读写打开一个文本文件,允许读和写。如果文件不存在,则创建新文件。读取文件会从文件起始地址的开始,写入只能是追加模式。 |
rb+ | 读写打开一个二进制文本文件,功能与 "r+" 类似。 |
wb+ | 读写打开或建立一个二进制文本文件,功能与 "w+" 类似。 |
ab+ | 读写打开一个二进制文本文件,功能与 "a+" 类似。 |
源码下载
Download
Verilog流水线
硬件描述语言的一个突出优点就是指令执行的并行性。多条语句能够在相同时钟周期内并行处理多个信号数据。
但是当数据串行输入时,指令执行的并行性并不能体现出其优势。而且很多时候有些计算并不能在一个或两个时钟周期内执行完毕,如果每次输入的串行数据都需要等待上一次计算执行完毕后才能开启下一次的计算,那效率是相当低的。流水线就是解决多周期下串行数据计算效率低的问题。
流水线
流水线的基本思想是:把一个重复的过程分解为若干个子过程,每个子过程由专门的功能部件来实现。将多个处理过程在时间上错开,依次通过各功能段,这样每个子过程就可以与其他子过程并行进行。
假如一个洗衣店内洗衣服的过程分为 4 个阶段:取衣、洗衣、烘干、装柜。每个阶段都需要半小时来完成,则洗一次衣服需要 2 小时。
考虑最差情况,洗衣店内只有一台洗衣机、一台烘干机、一个衣柜。如果每半小时送来一批要洗的衣服,每次等待上一批衣服洗完需要 2 小时,那么洗完 4 批衣服需要的时间就是 8 小时。
图示如下:
对这个洗衣店的装备进行升级,一共引进 4 套洗衣服的装备,工作人员也增加到 4 个,每个人负责一个洗衣阶段。所以每批次的衣服,都能够及时的被相同的人放入到不同的洗衣机内。由于时间上是错开的,每批次的衣服都能被相同的人在不同的设备与时间段(半小时)内洗衣、烘干和装柜。图示如下。
可以看出,洗完 4 批衣服只需要 3 个半小时,效率明显提高。
其实,在 2 小时后第一套洗衣装备已经完成洗衣过程而处于空闲状态,如果此时还有第 5 批衣服的送入,那么第一套设备又可以开始工作。依次类推,只要衣服批次不停的输入,4 台洗衣设备即可不间断的完成对所有衣服的清洗过程。且除了第一批次洗衣时间需要 2 小时,后面每半小时都会有一批次衣服清洗完成。
衣服批次越多,节省的时间就越明显。假如有 N 批次衣服,需要的时间为 (4+N) 个半小时。
当然,升级后洗衣流程也有缺点。设备和工作人员的增加导致了投入的成本增加,洗衣店内剩余空间也被缩小,工作状态看起来比较繁忙。
和洗衣服过程类似,数据的处理路径也可以看作是一条生产线,路径上的每个数字处理单元都可以看作是一个阶段,会产生延时。
流水线设计就是将路径系统的分割成一个个数字处理单元(阶段),并在各个处理单元之间插入寄存器来暂存中间阶段的数据。被分割的单元能够按阶段并行的执行,相互间没有影响。
流水线设计能够提高数据的吞吐率,即提高数据的处理速度。
流水线设计的缺点就是,各个处理阶段都需要增加寄存器保存中间计算状态,而且多条指令并行执行,势必会导致功耗增加。
下面,设计一个乘法器,并对是否采用流水线设计进行对比。
一般乘法器设计
前言
也许有人会问,直接用乘号 * 来完成 2 个数的相乘不是更快更简单吗?
如果你有这个疑问,说明你对硬件描述语言的认知还有所不足。就像之前所说,Verilog 描述的是硬件电路,直接用乘号完成相乘过程,编译器在编译的时候也会把这个乘法表达式映射成默认的乘法器,但其构造不得而知。
例如,在 FPGA 设计中,可以直接调用 IP 核来生成一个高性能的乘法器。在位宽较小的时候,一个周期内就可以输出结果,位宽较大时也可以流水输出。在能满足要求的前提下,可以谨慎的用 * 或直接调用 IP 来完成乘法运算。
但乘法器 IP 也有很多的缺陷,例如位宽的限制,未知的时序等。尤其使用乘号,会为数字设计的不确定性埋下很大的隐瞒。
很多时候,常数的乘法都会用移位相加的形式实现,例如:
实例
A = A<<1 ; //完成A * 2
A = (A<<1) + A ; //对应A * 3
A = (A<<3) + (A<<2) + (A<<1) + A ; //对应A * 15
用一个移位寄存器和一个加法器就能完成乘以 3 的操作。但是乘以 15 时就需要 3 个移位寄存器和 3 个加法器(当然乘以 15 可以用移位相减的方式)。
有时候数字电路在一个周期内并不能够完成多个变量同时相加的操作。所以数字设计中,最保险的加法操作是同一时刻只对 2 个数据进行加法运算,最差设计是同一时刻对 4 个及以上的数据进行加法运算。
如果设计中有同时对 4 个数据进行加法运算的操作设计,那么此部分设计就会有危险,可能导致时序不满足。
此时,设计参数可配、时序可控的流水线式乘法器就显得有必要了。
设计原理
和十进制乘法类似,计算 13 与 5 的相乘过程如下所示:
由此可知,被乘数按照乘数对应 bit 位进行移位累加,便可完成相乘的过程。
假设每个周期只能完成一次累加,那么一次乘法计算时间最少的时钟数恰好是乘数的位宽。所以建议,将位宽窄的数当做乘数,此时计算周期短。
乘法器设计
考虑每次乘法运算只能输出一个结果(非流水线设计),设计代码如下。
实例
module mult_low
#(parameter N=4,
parameter M=4)
(
input clk,
input rstn,
input data_rdy , //数据输入使能
input [N-1:0] mult1, //被乘数
input [M-1:0] mult2, //乘数
output res_rdy , //数据输出使能
output [N+M-1:0] res //乘法结果
);
//calculate counter
reg [31:0] cnt ;
//乘法周期计数器
wire [31:0] cnt_temp = (cnt == M)? 'b0 : cnt + 1'b1 ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
cnt <= 'b0 ;
end
else if (data_rdy) begin //数据使能时开始计数
cnt <= cnt_temp ;
end
else if (cnt != 0 ) begin //防止输入使能端持续时间过短
cnt <= cnt_temp ;
end
else begin
cnt <= 'b0 ;
end
end
//multiply
reg [M-1:0] mult2_shift ;
reg [M+N-1:0] mult1_shift ;
reg [M+N-1:0] mult1_acc ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
mult2_shift <= 'b0 ;
mult1_shift <= 'b0 ;
mult1_acc <= 'b0 ;
end
else if (data_rdy && cnt=='b0) begin //初始化
//【注意!!!,&&的运算优先级仅高于?:,因此,会先计算 cnt=='b0的值,再与data_rdy相与,也就是说,必须data_rdy==1且cnt=='b0时,才能进入初始化,注意!!!】
mult1_shift <= {{(N){1'b0}}, mult1} << 1 ; //【这里的N应该是M吧?】
mult2_shift <= mult2 >> 1 ;
mult1_acc <= mult2[0] ? {{(N){1'b0}}, mult1} : 'b0 ; //【这里的N应该是M吧?】
end
else if (cnt != M) begin
mult1_shift <= mult1_shift << 1 ; //被乘数乘2
mult2_shift <= mult2_shift >> 1 ; //乘数右移,方便判断
//判断乘数对应为是否为1,为1则累加
mult1_acc <= mult2_shift[0] ? mult1_acc + mult1_shift : mult1_acc ;
end
else begin
mult2_shift <= 'b0 ;
mult1_shift <= 'b0 ;
mult1_acc <= 'b0 ;
end
end
//results
reg [M+N-1:0] res_r ;
reg res_rdy_r ;
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
res_r <= 'b0 ;
res_rdy_r <= 'b0 ;
end
else if (cnt == M) begin
res_r <= mult1_acc ; //乘法周期结束时输出结果
res_rdy_r <= 1'b1 ;
end
else begin
res_r <= 'b0 ;
res_rdy_r <= 'b0 ;
end
end
assign res_rdy = res_rdy_r;
assign res = res_r;
endmodule
testbench
实例
`timescale 1ns/1ns
module test ;
parameter N = 8 ;
parameter M = 4 ;
reg clk, rstn;
//clock
always begin
clk = 0 ; #5 ;
clk = 1 ; #5 ;
end
//reset
initial begin
rstn = 1'b0 ;
#8 ; rstn = 1'b1 ;
end
//no pipeline
reg data_rdy_low ;
reg [N-1:0] mult1_low ;
reg [M-1:0] mult2_low ;
wire [M+N-1:0] res_low ;
wire res_rdy_low ;
//使用任务周期激励
task mult_data_in ;
input [M+N-1:0] mult1_task, mult2_task ;
begin
wait(!test.u_mult_low.res_rdy) ; //not output state
@(negedge clk ) ;
data_rdy_low = 1'b1 ;
mult1_low = mult1_task ;
mult2_low = mult2_task ;
@(negedge clk ) ;
data_rdy_low = 1'b0 ;
wait(test.u_mult_low.res_rdy) ; //test the output state
end
endtask
//driver
initial begin
#55 ;
mult_data_in(25, 5 ) ;
mult_data_in(16, 10 ) ;
mult_data_in(10, 4 ) ;
mult_data_in(15, 7) ;
mult_data_in(215, 9) ;
end
mult_low #(.N(N), .M(M))
u_mult_low
(
.clk (clk),
.rstn (rstn),
.data_rdy (data_rdy_low),
.mult1 (mult1_low),
.mult2 (mult2_low),
.res_rdy (res_rdy_low),
.res (res_low));
//simulation finish
initial begin
forever begin
#100;
if ($time >= 10000) $finish ;
end
end
endmodule // test
仿真结果如下。
由图可知,输入的 2 个数据在延迟 4 个周期后,得到了正确的相乘结果。算上中间送入数据的延迟时间,计算 4 次乘法大约需要 20 个时钟周期。
【以图中第一个例子为例,data_rdy有效后第一个时钟上升沿,cnt=0,开始运算,直到运算5个上升沿,cnt=4,此时将结果以非阻塞赋值的形式给res_r,而在赋值完成后,阻塞赋值给res,从而输出结果。因此,计算需要4个始终周期(对应此时M=4,需要累加4次),输出结果需要1个周期,故第M+1个周期才能输出结果,而此结果又需输出1个周期,也即第6个周期方可开始计算下一个运算。】
流水线乘法器设计
下面对乘法执行过程的中间状态进行保存,以便流水工作,设计代码如下。
单次累加计算过程的代码文件如下(mult_cell.v ):
实例
module mult_cell
#(parameter N=4,
parameter M=4)
(
input clk,
input rstn,
input en,
input [M+N-1:0] mult1, //被乘数
input [M-1:0] mult2, //乘数
input [M+N-1:0] mult1_acci, //上次累加结果
output reg [M+N-1:0] mult1_o, //被乘数移位后保存值
output reg [M-1:0] mult2_shift, //乘数移位后保存值
output reg [N+M-1:0] mult1_acco, //当前累加结果
output reg rdy );
always @(posedge clk or negedge rstn) begin
if (!rstn) begin
rdy <= 'b0 ;
mult1_o <= 'b0 ;
mult1_acco <= 'b0 ;
mult2_shift <= 'b0 ;
end
else if (en) begin
rdy <= 1'b1 ;
mult2_shift <= mult2 >> 1 ;
mult1_o <= mult1 << 1 ;
if (mult2[0]) begin
//乘数对应位为1则累加
mult1_acco <= mult1_acci + mult1 ;
end
else begin
mult1_acco <= mult1_acci ; //乘数对应位为1则保持
end
end
else begin
rdy <= 'b0 ;
mult1_o <= 'b0 ;
mult1_acco <= 'b0 ;
mult2_shift <= 'b0 ;
end
end
endmodule
顶层例化
多次模块例化完成多次累加,代码文件如下(mult_man.v ):
实例
module mult_man
#(parameter N=4,
parameter M=4)
(
input clk,
input rstn,
input data_rdy ,
input [N-1:0] mult1,
input [M-1:0] mult2,
output res_rdy ,
output [N+M-1:0] res );
wire [N+M-1:0] mult1_t [M-1:0] ;
wire [M-1:0] mult2_t [M-1:0] ;
wire [N+M-1:0] mult1_acc_t [M-1:0] ;
wire [M-1:0] rdy_t ;
//第一次例化相当于初始化,不能用 generate 语句
mult_cell #(.N(N), .M(M))
u_mult_step0
(
.clk (clk),
.rstn (rstn),
.en (data_rdy),
.mult1 ({{(M){1'b0}}, mult1}),
.mult2 (mult2),
.mult1_acci ({(N+M){1'b0}}),
//output
.mult1_acco (mult1_acc_t[0]),
.mult2_shift (mult2_t[0]),
.mult1_o (mult1_t[0]),
.rdy (rdy_t[0]) );
//多次模块例化,用 generate 语句
genvar i ;
generate
for(i=1; i<=M-1; i=i+1) begin: mult_stepx
mult_cell #(.N(N), .M(M))
u_mult_step
(
.clk (clk),
.rstn (rstn),
.en (rdy_t[i-1]),
.mult1 (mult1_t[i-1]),
.mult2 (mult2_t[i-1]),
//上一次累加结果作为下一次累加输入
.mult1_acci (mult1_acc_t[i-1]),
//output
.mult1_acco (mult1_acc_t[i]),
.mult1_o (mult1_t[i]), //被乘数移位状态传递
.mult2_shift (mult2_t[i]), //乘数移位状态传递
.rdy (rdy_t[i]) );
end
endgenerate
assign res_rdy = rdy_t[M-1];
assign res = mult1_acc_t[M-1];
endmodule
testbench
将下述仿真描述添加到非流水乘法器设计例子的 testbench 中,即可得到流水式乘法运算的仿真结果。
2 路数据为不间断串行输入,且带有自校验模块,可自动判断乘法运算结果的正确性。
实例
reg data_rdy ;
reg [N-1:0] mult1 ;
reg [M-1:0] mult2 ;
wire res_rdy ;
wire [N+M-1:0] res ;
//driver
initial begin
#55 ;
@(negedge clk ) ;
data_rdy = 1'b1 ;
mult1 = 25; mult2 = 5;
#10 ; mult1 = 16; mult2 = 10;
#10 ; mult1 = 10; mult2 = 4;
#10 ; mult1 = 15; mult2 = 7;
mult2 = 7; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 1; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 15; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 3; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 11; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 4; repeat(32) #10 mult1 = mult1 + 1 ;
mult2 = 9; repeat(32) #10 mult1 = mult1 + 1 ;
end
//对输入数据进行移位,方便后续校验
//【个人认为这跟移位没关系,而是因为每个数进去后都需要4个周期才能输出,因此做了4个周期的延迟,第4个周期刚好是mult1_ref[3]*mult2_ref[3],用这个值与乘法器的值做校验】
reg [N-1:0] mult1_ref [M-1:0];
reg [M-1:0] mult2_ref [M-1:0];
always @(posedge clk) begin
mult1_ref[0] <= mult1 ;
mult2_ref[0] <= mult2 ;
end
genvar i ;
generate
for(i=1; i<=M-1; i=i+1) begin
always @(posedge clk) begin
mult1_ref[i] <= mult1_ref[i-1];
mult2_ref[i] <= mult2_ref[i-1];
end
end
endgenerate
//自校验
reg error_flag ;
always @(posedge clk) begin
# 1 ;
if (mult1_ref[M-1] * mult2_ref[M-1] != res && res_rdy) begin
error_flag <= 1'b1 ;
end
else begin
error_flag <= 1'b0 ;
end
end
//module instantiation
mult_man #(.N(N), .M(M))
u_mult
(
.clk (clk),
.rstn (rstn),
.data_rdy (data_rdy),
.mult1 (mult1),
.mult2 (mult2),
.res_rdy (res_rdy),
.res (res));
仿真结果
前几十个时钟周期的仿真结果如下。
由图可知,仿真结果判断信号 error_flag 一直为 0,表示乘法设计正确。
数据在时钟驱动下不断串行输入,乘法输出结果延迟了 4 个时钟周期后,也源源不断的在每个时钟下无延时输出,完成了流水线式的工作。
【好奇吧?为什么这下变成4个周期就输出了?
注意看generate中,i最大为M-1,当i=M-1时,在mult_cell.v中,mult1_acco直接对外非阻塞赋值输出最终结果,没有像非流水线一样,在另一个always块中判断cnt==M后再输出,这样节省了一个周期。】
【个人认为,非流水线中采用一样的方法,可使周期缩短到4个。】
相对于一般不采用流水线的乘法器,乘法计算效率有了很大的改善。
但是,流水线式乘法器使用的寄存器资源也大约是之前不采用流水线式的 4 倍。
所以,一个数字设计,是否采用流水线设计,需要从资源和效率两方面进行权衡。
源码下载
Download