第3章 CHAPTER 3 Verilog硬件描述语言 硬件描述语言(Hardware Description Language,HDL)是一种国际上流行的描述数字电路和系统的语言,可以在EDA工具的支持下,快速实现设计者的设计意图。 常用的硬件描述语言有Verilog HDL和VHDL两种。本章介绍Verilog语言的语法和使用规则。 3.1硬件描述语言概述 Verilog HDL是由GDA(Gateway Design Automation)公司的Philip R.Moorby于1983年首创的,最初只设计了一个仿真与验证工具,之后又陆续开发了相关的故障模拟与时序分析工具。1985年Moorby推出商用仿真器VerilogXL,获得了巨大的成功,从而使得Verilog HDL迅速得到推广应用。1989年CADENCE公司收购了GDA公司,Verilog HDL成为该公司的独家专利。1990年CADENCE公司公开发表了Verilog HDL,成立OVI(Open Verilog International)组织,并推动Verilog HDL的发展。IEEE于1995年制定了Verilog HDL的IEEE标准,即Verilog HDL13641995,2001年发布了Verilog HDL13642001,目前已发布Verilog HDL 2003。 VHDL是VHSIC Hardware Description Language的缩写,其中VHSIC是Very High Speed Integrated Circuit的缩写,美国国防部为解决项目的多个承包人的信息交换困难和设计维修困难的问题,提出了VHDL构想,由TI、IBM和INTERMETRICS公司完成,并于1987年作为IEEE标准,即IEEE Std 10761987[LRM87],后来又进行一些修改,成为新的标准版本,即IEEE Std 10761993[LRM93]。 VHDL和Verilog HDL这两种语言的主要功能差别并不大,它们的描述能力也类似,相比于Verilog HDL,只是VHDL的系统描述能力稍强,而Verilog HDL的底层描述能力则更强。 3.1.1硬件描述语言特点 硬件描述语言(HDL)有不同于其他软件语言的特点: (1) 功能的灵活性。HDL支持设计者从开关、门级、RTL、行为级等不同抽象层次对电路进行描述,并支持不同抽象层次描述的电路组合为一个电路模型,HDL支持系统的层次化设计,支持元件库和功能模块的可重用设计。用HDL设计数字电路系统是一种贯穿于设计、仿真和综合的方法。 (2) HDL支持高层次的设计抽象,可应用于设计复杂的数字电路系统。HDL设计和传统的原理图输入方法的关系如同高级语言和汇编语言。原理图输入的可控性好、实现效率高,比较直观,但在设计大规模CPLD/FPGA时显得很烦琐,有时甚至无法理解。而设计者使用HDL进行设计,可以在非常抽象的层次上对电路进行描述,将烦琐的实现细节交由EDA工具辅助完成,实现“自顶向下”的层次化设计,缩短开发周期。 (3) HDL设计可不依赖厂商和器件,移植性好。设计者在设计时,只需在寄存器传输级(RTL级)对电路系统的功能和结构用HDL进行描述,电路系统如需实现在不同器件上,也不用重复设计,只需选择相应FPGA/CPLD芯片的综合、布局布线的库函数,由相应的设计工具对设计描述进行重新转换即可。 3.1.2层次化设计 随着现代控制、通信等电子行业的发展,数字电路复杂度也越来越高。集成电路制造业和EDA工具的快速发展,使复杂数字系统的设计实现成为可能。复杂系统的设计必然要使用层次化、结构化的设计方法,其设计思想就是“自顶向下”,即“化繁为简,逐步实现”,在数字系统的功能指标和端口基础上,将系统分解成多个子模块构成,然后对各个子模块作进一步分解,直到将模块分解到适中的实现复杂度或者可使用的EDA元件库中已有的基本元件实现为止,在设计的后期将各子模块组合起来构成一个系统。自顶向下设计示意图如图31所示。 图31自顶向下设计示意图 本章介绍Verilog语言,将按照“先框架,再细节”的模式,即先介绍Verilog HDL程序的基本结构,然后介绍常用的语法,最后进行一些数字系统设计练习。 3.2Verilog HDL程序的基本结构 Verilog语言作为一种用于设计数字系统的工具,可以完成以下功能: (1) 描述数字系统的逻辑功能。 (2) 描述多个数字系统模块之间的连接,组合成为一个系统。 (3) 建立测试激励信号文件,在仿真环境中,对设计好的系统进行调试验证。 根据对电路描述的抽象程度不同,Verilog语言描述有四个层次的模型类型。 (1) 系统级或算法级: 这是Verilog语言支持的最高抽象级别,设计者对系统行为进行描述,关注算法的实现,不关心具体的硬件实现细节,几乎可以使用Verilog语言提供的所有语句。 (2) 寄存器传输级(RTL): 通过描述模块内部状态转移的情况来表征该逻辑单元的功能,设计者关注数据的处理及其如何在线网上、寄存器间的传递。 (3) 逻辑级: 调用已设计好的逻辑级门电路的基本单元(原语),如与门、或门、异或门等,描述逻辑门之间的连接,以实现逻辑功能。 (4) 开关级: 这是Verilog语言支持的最低抽象层次,通过描述器件中的晶体管、存储节点及其互连来设计模块。 上述四个抽象级别的特性、描述方法和相关的问题在表31中给出。 表31Verilog HDL的抽象等级 模型特性描述说明 系统级 功能模型利用两类过程语句表征: (1) initial语句: 常用于建立行为(仿真)模型,只运行一次; (2) always: 用于行为描述和RTL级编码,可持续运行。 具体内容见3.4节不是所有的行为模型都是可综合的 例: always (A or B or C or D or Sel) begin case (Sel) 2`b00: Z = A; 2`b01: Z = B; 2`b10: Z = C; 2`b11: Z = D; default: Z = 1`bx; endcase end 注意case语句与ifelse if语句的区别 RTL级 典型的RTL为逻辑综合目的,可以描述组合电路的数据运算,也可描述在时钟沿之间组合逻辑的运行。数据流和行为结构连续赋值是数据流模型的基本结构,其中的表达式可利用大多数运算符。连续赋值在每个仿真周期会重新估值连续赋值中时间延迟将被综合工具忽略 例: module Mux2_1 (A,B,Sel,Out1); output Out1; input A,B,Sel; wire N1,N2; assign N1=(A & Sel); assign N2=(B & ~Sel); assign Out1=(N1 | N2); endmodule 可用assign out1=(A & Sel)|(B &~Sel); 隐含的连续赋值提供更简练的编码 续表 模型特性描述说明 逻辑级 库、宏单元Verilog语言中,逻辑级直接利用预先定义的门电路原语构筑系统,逻辑级模型含有行为仿真时序信息,但只适应小系统的应用,对多数系统设计而言太详尽并费时任何逻辑级模块都是可综合的 例: module AND_OR(A,B,C,D,Z); input A,B,C,D; output Z; wire SIG1,SIG2; and (SIG1,A,B); and (SIG2,C,D); or (Z,SIG1,SIG2); … endmodule; 任何延时规定,综合时将被忽略 开关级CMOS开关电路用FPGA实现数字系统,一般不采用开关级描述 一般来说,设计的抽象程度越高,设计的灵活性就越好,和工艺的无关性就越高,随着抽象程度降低,设计的灵活性和工艺的无关性变差,可移植性变差。 3.2.1模块结构分析 下面通过一个简单的Verilog HDL程序来分析Verilog HDL程序的基本结构。 例31设计一个半加器,如图32所示。 图32半加器模块图 module halfadder(A,B,CO,S); input A,B; output S,CO; wire S,CO; assign S = A ^B; assign CO= A & B; endmodule 从例31可以看到 (1) 程序文件位于关键字module和endmodule之间,module、endmodule是关键词; (2) 每个模块必须由一个模块名进行标识,如halfadder; (3) 模块的输入端口是A与B,是相加的两个加数,输出端口是S与CO,S是相加的和,CO是向高位的进位; (4) 第5行、第6行语句描述了模块的功能; (5) 模块中的每一条语句都以分号(;)结束,在模块末尾endmodule后不加分号。 模块(module)是Verilog HDL设计的基本功能单元,模块可以是一个元件,也可以是多个低层次模块的组合。一般而言,模块包含以下信息: 端口名的模块声明、I/O端口声明、各类型变量声明、模块功能说明和模块结尾。模块结构如表32所示。 表32Verilog HDL模块结构 代码行描述举例 1module模块名(端口1,端口2,端口3,…,端口n); module halfadder(A,B,CO,S); 2输入/输出端口声明; input A,B; output S,CO; 3wire、reg等各类型变量声明; wire S,CO; 4模块 功能说明数据流语句(assign); 低层模块例化; always、initial语句; 任务(task)和函数(function) assign S = A ^B; assign CO= A & B; 5endmodule endmodule 1. 端口名的模块声明 module 模块名(端口1,端口2,端口3,…,端口n); 为便于工程管理,模块命名一般应和其功能相关,如halfadder(半加器)、adder(加法器)、top(顶层模块)、testbench(测试模块)等。命名的字符应符合Verilog HDL对字符串的规定。 端口是模块和外界进行信息交互的接口,有些模块与外界无信息交互,则无端口列表,例如包含了待测模块和激励信号等完整的测试模块,可声明为 module testbench(); 如有信息交互,则注意在括号中各端口应用逗号隔开。对于外界环境来说,模块内部是一个“黑盒子”,对模块的调用(例化)都是通过对端口的操作进行的。 2. I/O端口声明 所有声明的端口都必须说明其端口类型、位宽等信息。根据信号的方向,端口类型有三类: (1) 输入端口: 声明为input [width-1:0]端口名1,端口名2,…,端口名n。 (2) 输出端口: 声明为output [width-1:0]端口名1,端口名2,…,端口名n。 (3) 输入/输出端口: 声明为inout [width-1:0]端口名1,端口名2,…,端口名n。 如果输入、输出端口无位宽的说明,系统将默认位宽为1。 例32 input[7:0] data_in; //一个名为data_in位宽为8位的输入数据 output S,CO; //两个名为S和CO位宽为1位的输出数据 3. 数据类型说明 对端口的信号、模块内部使用变量的数据类型说明详见3.3.2节。Verilog HDL常用两大类数据类型: (1) 线网类型(net type): 表示Verilog HDL结构化元件间的物理连线。它的值由驱动元件的值决定。如果没有驱动元件连接到线网,线网的默认值为高阻值z。 (2) 寄存器类型(register type): 表示一个抽象的数据存储单元,它只能在always语句和initial语句中被赋值。寄存器类型变量的默认值为不确定值x。 4. 模块功能说明 这是模块中最重要的部分,常有四类方法可以选用以完成模块编辑功能的表述,简要介绍如下: (1) 用连续赋值语句assign进行数据流建模。 例33 assign a=b & c; //描述了一个二输入的与门 (2) 对已定义好的元件进行调用。 例34halfadder u1(a,b,s,co); //调用半加器,例化名是u1 (3) 用结构说明语句always、initial、task和function进行行为级描述。 例35 always @ (posedge clk) //描述了一个D触发器 begin Q<= d; end 例36 initial //产生信号a,b的波形 begin a<= 0; b<= 0; #10 begin a<= 1; b<=1; end end 例37 task writeburst; //定义一个任务writeburst input [7:0] wdata; … endtask … writeburst(123); //调用任务 例38 function max(a,b)//定义一个函数max,求出a,b两数的最大值 … endfunction assign c = max(data_1,data_2); //函数的调用,将data_1,data_2的最大值赋给c assign、always、initial、task和function的语法和应用详见3.4节。 5. 模块结尾 在每个模块的末尾用endmodule结束,其后不加分号。 从上面的分析可以看出,每个Verilog HDL模块实现特定的功能,其中module、模块名和endmodule这三部分是模块必需的,其余如端口列表、端口声明、数据类型说明、assign描述、always、initial、task和function等结构说明语句根据设计要求选用。 Verilog HDL模块可分为两种类型: 一种是功能模块,用来描述某种电路系统结构和功能,以综合或者提供仿真模型为目的; 另一种是测试模块,为功能模块的测试提供信号源激励、输出数据监测。3.6节中将进行介绍。 3.2.2模块的实例化 Verilog HDL支持层次化设计,按“自顶向下”的思路,大型的数字电路的设计可分解成多个小型模块的设计,在每个小模块的设计实现后,用顶层模块调用低层模块的方式实现整体系统功能。模块调用(也称模块实例化)的基本格式为 <模块名> <例化名>(<端口列表 >); 根据被调用的低层模块端口与在上层模块的连接端口的不同,有两种实例化方法: (1) 按端口顺序连接:低层模块定义时声明的端口顺序与上层模块相应的连接端口顺序保持一致。 格式: 模块名例化名(PORT_1,PORT_2,…,PORT_N); (2) 按端口名称连接,被调用的低层模块和上层模块是通过端口名称进行连接的。 格式: 模块名 例化名(.port_1(PORT_1),.port_2(PORT_2),…,.port_n(PORT_N)); 其中,port_1,port_2,…,port_n为被调用模块设计声明的各个端口; PORT_1,PORT_2,…,PORT_N为上一层模块调用时对应端口名称。 这种连接端口的顺序可以是任意的,只要保证上层模块的端口名和被调用模块端口的对应即可,如果被调用模块有不需要连接的端口,该端口悬空,则可以将此端口忽略或者写成.port_n()。 例39通过调用半加器模块和或门模块来实现一位全加器。 (1) 使用电路板的元件装配与Verilog模块例化进行类比,清楚理解模块名,例化上层模块的对应端口以及低层模块设计时声明的端口,如表33所示。 表33Verilog语言调用和电路板元件装配的类比关系 Verilog语言的模块调用电路板(PCB)元件装配 已设计好的模块: (1) 半加器(halfadder) (2) 或门 已制造好的元件: (1) 异或门7486 7486 (2) 与门7408 7408 (3) 或门7432 7432 续表 Verilog语言的模块调用电路板(PCB)元件装配 实现的一位全加器: 装配好的电路板(局部) 说明: (1) 半加器模块中标识的A、B、CO、S和或门A、B、OUT是设计该模块时声明的输入、输出端口,可以类比于实现全加器的异或门、与门和或门电路元件相应的端口。 (2) 一位全加器中的a,b,co_temp1,s_temp,……是上层模块调用低层模块时的连接端口,可以类比于PCB中的上述元件之间相互的连接。 (3) 一位全加器图中的U1、U2、U3称为半加器、或门等低层模块的例化名,可以类比于PCB中的电路元件,如异或门7486、与门7408和或门7432。 (4) 在Verilog中,逻辑级例化名是可选的(如U3),而用户定义的模块例化时必须指定名字(如U1、U2)。 由于全加器的输出与输入信号有如下关系: S = A B co_in Co_out = A & B + A & co_in + B & co_in = A & B + co_in & (A B) 所以,一位全加器可以调用两个半加器模块和一个或门模块来实现,连接方式以电路板的元件装配来做类比,帮助理解。 (2) 调用设计好的低层模块,搭建上层系统。 半加器设计见例31。 module halfadder(A,B,CO,S); … endmodule 如果采用第一种按模块端口顺序连接的方法例化模块则全加器写成 module fulladder(a,b,co_in,co_out,s); … //调用半加器模块两次,例化名分别为u1,u2 halfadder u1(a,b, co_temp1, s_temp); halfadder u2(s_temp,co_in, co_temp2, s); //调用两输入与门,例化名为u3 and2 u3(co_temp1,co_temp2,co_out); endmodule 如果采用第二种按模块端口名称连接,则 halfadder u1(.A(a),.B(b),.CO(co_temp1),.S(s_temp)); halfadder u2(.A(s_temp),.B(co_in),.CO(co_temp2),.S(s)); 如果没有co_in输入,则半加器u2的调用可写为 halfadder u2(.A(s_temp),.CO(co_temp2),.S(s)); 或 halfadder u2(.A(s_temp),.B(),.CO(co_temp2),.S(s)); 按端口名称连接方式较按端口顺序连接有以下优点: (1) 在被调用模块有较多引脚时,根据端口名字进行信号连接,可避免因记错端口顺序而出错。 (2) 在被调用模块的端口顺序发生变化时,只要端口名字含义不变,模块调用就可以不更改调整。 3.3Verilog HDL词法、数据类型和运算符 本节讨论Verilog HDL的基本词法约定、数据类型和常用运算符,这些是组成Verilog语言的基本元素,是后续学习的基础。 3.3.1词法约定 Verilog HDL中的基本词法约定与C语言类似,可以有空白、注释、分隔符、数字、字符串、标识符和关键字等。 1. 注释 为加强程序的可读性和文档管理,设计程序中应适当地加入注释内容。注释有两种方式: 单行注释和多行注释。 (1) 单行注释以“//”开始,只能写在一行中。 (2) 多行注释以“/*”开始,以“*/”结束,注释的内容可以跨越多行。 例310 单行注释: assign c= a+b;//c等于a,b的和 多行注释: assign c= a+b;/* c等于a,b的和,本语句可综合成一个加法器,实现加法的组合逻辑*/ 2. 数字和字符串 数字的表达方式: <位宽>`<进制><数值> 说明: (1) <位宽>: 用十进制表示的数字位数,如果默认,则位宽由具体机器系统决定(至少为32位)。 (2) <进制>: 可以表示二进制(b或B)、八进制(o或O)、十进制(d或D)、十六进制(h或H)。默认为十进制。 (3) <数值>: 可以是所选进制内的任意有效数字,包括不定值x和高阻态z。当<数值>位宽大于指定的大小时,截去高位。 例311 8`b11001100//位宽为8的二进制数,`b表示二进制 6`O23 //位宽为6的八进制数,`O表示八进制 `hff23 //十六进制数,采用机器的默认位宽 123 //十进制数123,采用机器的默认位宽 2`b1101 //表示的是2`b01,因为当数值大于指定的大小时,截去高位 4`b110x //四位二进制数,最低位为不定值x 6`o1x //位宽为六位的八进制,其值的二进制表示为6`b001xxx 16`h1z0x /*位宽为16位的十六进制数,其值的二进制表示为 16`b0001zzzz0000xxxx */ 在书写过程中,可在数字之间使用下画线“_”对数字进行分隔,以增加数字的可读性,下画线不能作为数字的首字符,下画线在编译阶段将被忽略,如8`b1100_1100。 字符串是双引号内的字符序列,字符串不能分成多行书写。如"INTERNAL ERROR"和"REACHED->HERE"等。 3. 标识符 标识符(identifier)用于定义模块名、端口名、连线、信号名等。标识符可以是任意一组字母、数字、符号$和_(下画线)符号的组合,但标识符的第一个字符必须是字母或者下画线,字符数不能多于1024个。此外,标识符区分大小写。 例312 adder data_in state,State //这两个标识符是不同的 2and,&write //非法格式 4. 空白符 空白符由空格、制表符和换行符定义而成,除了出现在字符串中,Verilog HDL中的空白符仅用于分隔标识符,在编译阶段被忽略。 5. 关键字 Verilog语言内部已经使用的词称为关键字或保留字。关键字必须使用小写字母,说明见附表B。 3.3.2数据类型 Verilog HDL中共有19种数据类型。Verilog HDL允许信号具有逻辑值和强度值,以尽可能反映真实硬件电路的工作情况。逻辑值有0、1、x、z。其中,x表示未初始化或者未知的逻辑值; z表示高阻状态。逻辑强度值从最强到最弱分为几种强度等级。 类似于C语言,数据类型也有常量和变量之分。在程序运行中,其数值不能改变的量称为常量; 数值可以改变的量称为变量。下面对常用的变量数据类型wire型、reg型、memory型和常量数据类型parameter型进行介绍。其他数据类型的详细情况,请查阅Verilog HDL的相关手册。 1. 线网型(wire) wire型是连线型(net)中最常用的数据类型,它表示硬件单元之间的连接,常用于表示以assign为关键字的组合逻辑。 格式: wire [width-1 :0]变量名1,变量名2,…,变量名n; 说明: (1) [width-1:0]指明了变量的位宽,缺省此项时默认变量位宽为1; (2) wire为关键字; (3) wire数据默认值是z; (4) 模块输入、输出信号的类型默认为wire型。 例313 wire [7:0] a ,b; //位宽为8的wire型变量a和b wire c; //wire型变量c,位宽为1; wire [3:1] data; //位宽为3的wire型变量data,分别为data[3],data[2],data[1] 2. 寄存器型(reg) 寄存器是数据存储单元的抽象,寄存器中的数据可以保存,直到被赋值语句赋予新的值。reg型变量只能在initial语句和always语句中被赋值。reg的默认值是不定值x。 格式: reg[width-1:0]变量名1,变量名2,…,变量名n; 例314 reg [7:0] b,c; //两个位宽为8的寄存器变量b和c reg a; //寄存器变量a,位宽为1 reg[3:1] d; //位宽为3的寄存器变量d,由d[3],d[2],d[1]组成 3. 存储器型(memory) memory型数据常用于寄存器文件、ROM和RAM建模等,是寄存器型的二维数组形式,它是将reg型变量进行地址扩展而得到的,一般格式为 reg[n-1 : 0] 存储器名[N-1 : 0];//定义位宽为n,深度为N的寄存器组 例315 reg[7:0] mem[255 : 0]; //每个寄存器位宽为8,共有256个寄存器的存储器组 对一组存储单元进行读写,必须指定该单元的地址。例如,对例315的mem寄存器的第200个存储单元进行读写操作,格式为 men[200]= 0; //对存储器mem的第200个存储单元赋值0 要注意的是,虽然memory型数据是将reg型变量进行地址扩展而得到的,但是memory和reg型数据有很大区别。 例316 reg mem [N-1 : 0]; //N个一位的寄存器组men reg [N-1:0] a; //一个N位的寄存器变量a 4. 参数型(parameter) 在Verilog HDL中可以使用parameter为关键词,指定一个标识符(即名字)来代表一个常量,参数的定义常用在信号位宽定义、延迟时间定义等位置,增加程序的可读性,方便程序的更改。 格式: parameter标识符1=表达式1,标识符2=表达式2,…,标识符n=表达式n; 表达式可以是常数,也可以是以前定义过的标识符。 例317 parameter width = 8; //定义了一个常数参数 input[width-1:0] data_in; //输入信号data_in的位宽为8 parameter a=1,b=3; //定义了两个常数参数 parameter c=a+b; //c的值是前面定义的a,b的和 3.3.3运算符 运算符按照功能分为表34所列的类型,表34也总结了运算符的优先级关系。 表34运算符的分类和优先级 分类运算运算符操作数优先级 逻辑/按位运算符 算术运算符 移位运算符 关系运算符 等价运算符 按位/缩减运算符 逻辑运算符 条件运算符 拼接运算符 双目运算符(或,与) 单目运算符(非) 乘,除,取模 加,减 移位 关系 等价 缩减 逻辑 条件 拼接 |, &, ^2 ~1 *, /, %2 +, -2 <<, >>2 <, <=, >, >=2 ==, !=, ===, !==2 &, ~&1 ^, ~^1 |, ~|1 &&2 ‖2 !1 ? : 3 {,} {,{ }}≥2 最高 最低 根据参加运算的操作数数目,运算符可分为: (1) 单目运算符: 对一个操作数进行操作的运算符,例如clock =~clock; (2) 双目运算符: 对两个操作数进行运算的运算符,例如a = b & c; (3) 三目运算符: 对三个操作数进行运算的运算符,例如 D_out = condition ? D_in1 : D_in2。 下面简要介绍常用的运算符。 1. 算术运算符 算术运算符有加法(+)、减法(-)、乘法(*)、除法(/)和取模(%)。 例318设a= 4`b0101,b = 4`b0010。 a + b //a和b相加,等于4`b0111 a * b //a 和b 相乘,等于4`b1010 a / b //a 除以b ,等于 4`b0010,余数部分舍弃,取整 a % b //a 对b 取模,即求a,b相除的余数部分,结果等于1 在算术运算时,如果有一个操作数为不定值x,则运算结果全部为不定值x。 2. 逻辑运算符 逻辑运算符有逻辑与(&&)、逻辑或(‖)、逻辑非(!)。 说明: (1) &&和‖是双目运算符, !是单目运算符。 (2) 逻辑运算符的计算结果是逻辑假(0)、逻辑真(1)、不确定(x)3种情况1位的值。 (3) 当操作数为具体数值时: ①操作数不等于0,则等价于逻辑真(1); ②操作数等于0,则等价于逻辑假(0); ③操作数的任何一位为不确定值x或者高阻态z,则等价于不确定值x。 例319设a=2,b=0。 a && b //等于0,相当于(逻辑1 && 逻辑0) a ‖ b //等于1,相当于(逻辑1 ‖ 逻辑0) !a //等于0,相当于逻辑1取反 (a==3)&&(b==0) /*等于0,相当于两个表达式是否成立(为真),即如果a=3成立,则(a==3)为逻辑1,否则为逻辑0*/ x && a //等于x,相当于(x && 逻辑1) 3. 按位运算符 按位运算符有取反(~)、与(&)、或(|)、异或(^)和同或(~^,^~)。 说明: (1) 取反运算是单目运算符,其余是双目运算符。 (2) 按位运算对操作数中的每一位进行按位操作,如果两个数的位宽不相同,系统先将两个操作数右对齐,较短的操作数左端补0,然后再按位运算。 (3) 注意按位运算和逻辑运算的差别,逻辑运算结果是一个1位的逻辑值,按位运算产生一个与较长位宽操作数等宽的数值。 例320设a= 4`b0011, b=4`b1010, c=3`b011, d = 4`b11x0。 ~a //按位取反,结果等于4`b1100 b & c //按位与运算,结果等于4`b0010 a ^~ d //按位同或运算,结果等于4`b00x0 a & b //按位与运算,结果等于4`b0010 a & & b //逻辑与运算,等价于 1 && 1,结果等于1 4. 关系运算符 关系运算符包括大于(>)、小于(<)、大于或等于(>=)以及小于或等于(<=)。 在运算中: ①如果表达式成立,运算结果是真(1); ②如果表达式不成立,运算结果为假(0); ③如果操作数中某一位是不确定的,则表达式的结果是x。 例321设a = 4`b1010,b=4`b0001,c= 4`b1xz0。 a > b//结果等于逻辑1 a < b //结果等于逻辑0 a >= b //结果等于逻辑1 a <= c //结果等于逻辑值x 13 - a > b /*由于算术运算优先级较高,先进行13-a的计算,得到3,再和b进行比较,结果等于逻辑值1*/ 13 - (a>b) /*由于括号表明了关系运算的优先级,a>b成立,结果是真值为1,所以算术结果等于12*/ 5. 等式运算符 等式运算符包括4种: 逻辑等(==),逻辑不等(!=),case等(===),case不等(!==)。 说明: (1) 如果两个操作数位宽不等,则先对两个操作数右对齐,用0填充较短数的左边。 (2) 逻辑等(==)和逻辑不等(!=)中,如果两操作数中某一位是不确定的,则返回值是x; 如果两个数相同,则返回逻辑1; 如果不相同,则返回逻辑0。 例322设a = 4`b1010,b= 4`b1100,d = 4`b101x。 a == b; //逻辑等,结果为逻辑值0 a != b //结果为逻辑1 a == d //结果为逻辑x (3) case等(===)、case不等(!==)与逻辑等式运算符不同,在对两个操作数进行逐位比较时,即使有x,z位,也要进行精确比较,只有在二者完全相等的情况下结果为1,否则为0,case等式运算符的结果不可能为x。 例323设a = 4`b1010, b = 4`b1xzz, c = 4`b1xzz, d = 4`b1xzx。 a === b //结果为逻辑值0 b === c //结果为逻辑值1(两个数每一位都相同,包括x,z) b === d //结果为逻辑值0 (最低的一位不同) b !== d //结果为逻辑值1 6. 缩减运算符 缩减运算符包括缩减与(&)、缩减与非(~&)、缩减或(|)、缩减或非(~|)、缩减异或(^)和缩减同或(~^)。 这类操作符将对操作数由左向右进行操作,它们的运算规则和按位操作符相同。注意,缩减运算符只有一个操作数,按位运算符有两个操作数。 例324设a= 4`b1010。 &a //结果是1 & 0 & 1 & 0 = 0 |a //结果是1 | 0 | 1 | 0 = 1 ^a //结果是 1 ^ 0 ^ 1 ^ 0 = 0 可以看到,缩减异或、缩减同或可以产生一个向量的奇偶校验位。 7. 移位运算符 移位运算符有右移(>>)、左移(<<)。右移(>>)、左移(<<)将操作数向右、向左移动指定的位数,空出的位置用0补足。 例325设a = 4`b1010。 b=a >>1; //右移1位,结果是b=4`b0101 b=a <<2; //左移2位,结果是b=4`b1000 使用移位运算符可以将乘法转换成移位相加来完成,还可以进行移位寄存器的移位操作等,这在具体设计中有很多应用。 8. 拼接运算符 位拼接运算符{}可以将两个或多个操作数的某些位拼接起来成为一个操作数。进行拼接的每个操作数必须是确定位宽的,因为系统进行拼接时必须要确定拼接结果的位宽。 拼接运算时,将需要拼接的操作数按照顺序罗列出来,其间用逗号隔开,操作数的类型可以是线网变量、寄存器、向量线网或寄存器、有确定位宽的常数等。 例326设a = 1`b1,b = 3`b101,c = 4`b1010。 X={a,b,c}//结果是8`b11011010 Y={a,b,2`b01} //结果是6`b110101 Z={b[1:0],c[1],c[0]} //结果是4`b0110 位拼接可以使用重复操作、嵌套的方式来简化表达式。 例327 {1`b0,3{1`b1}}=4`b0111 {1`b0,{3{2`b01}}=7`b0010101 9. 条件运算符 条件运算符是一个三目运算符,格式为 条件表达式?表达式1 : 表达式2 判断过程是首先计算条件表达式: ①如果条件表达式为真,则计算表达式1的值; ②如果条件表达式为假,则计算表达式2的值; ③如果表达式为不确定x,且表达式1和表达式2的值不相等,则输出结果为不确定值x。 例328assign c=a>b? a: b //如果a大于b(即(a>b)为真),c=a; 反之,c=b 3.4Verilog HDL行为语句 本节重点介绍Verilog语言的常用行为级建模编程语句。 (1) 赋值语句,包括过程赋值和连续赋值,注意理解阻塞赋值和非阻塞赋值; (2) 顺序块和并行块语句; (3) 过程模块的结构说明语句,即always语句、initial语句、task语句、function语句; (4) 条件语句: ifelse语句和case语句; (5) 循环语句: for,forever,repeat,while; (6) 命令语句: 系统任务和系统函数,以及编译预处理命令。 3.4.1赋值语句 Verilog HDL赋值语句中,赋值符号左边是赋值目标,右边是表达式。常用赋值方式有过程赋值和连续赋值两种。 过程赋值语句的更新对象是寄存器、整数、实数等,这些类型变量在被赋值后,可以保持不变,直到赋值进程又被触发,变量才被赋予新值。过程赋值常出现在initial和always语句内。过程赋值方式有两种: 阻塞赋值和非阻塞赋值。它们在功能和特点上有很大不同。 连续赋值语句中,任何一个操作数的变化都会重新计算赋值表达式,重新进行赋值。 1. 过程赋值——阻塞赋值 阻塞赋值操作符用“=”表示。 例329 always @(posedge clk)//当时钟上升沿到来时,触发always块执行 begin a=b+1; c=a; end 当上面的always块被触发执行时,先求解b+1的值,将结果赋给a; 然后再执行将a的值赋给c的操作; 最后a和c的值都是b+1。 图33综合的参考电路结构 综合出的参考电路结构如图33所示。 可以看到: (1) 阻塞赋值的执行期间不允许其他Verilog HDL语句的执行干扰,必须是阻塞赋值完成后,才进行下一条语句的执行。 (2) 赋值一旦完成,等号左边的变量值立刻发生变化(如例329的a和c)。 (3) 使用阻塞赋值可能会得到意想不到的结果,如例329,可能设计者希望得到两个触发器,现在却只得到了一个。 2. 过程赋值——非阻塞赋值 非阻塞赋值操作符用“<=”表示。 例330 always @(posedge clk)//当时钟上升沿到来时,触发always块执行 begin a <= b+1; //语句1 c <= a; //语句2 end 执行时,根据时钟上升沿来到时,采样到a和b的值,并计算b+1的值,在always块结束之前,将b+1和a的值分别赋给a和c,最后a的值是b+1,c的值是时钟上升沿采样的a的值。 综合出的参考电路结构如图34所示。 图34综合的参考电路结构 可以看到: (1) 非阻塞赋值的符号(<=)与小于或等于运算符相同,但是这二者的意义是完全不一样的,在使用中应根据使用环境、相关语句含义进行区分。 (2) 非阻塞赋值在赋值开始时计算表达式右边的值,到了本次仿真周期结束时才更新被赋值变量(即赋值不立刻生效)。非阻塞赋值允许块中其他语句的同时执行。 (3) 在同一个顺序块中,非阻塞赋值表达式的书写顺序不影响赋值的结果。 3. 连续赋值语句 连续赋值常用于数据流行为建模。在连续赋值中,常以assign为关键词。 assign赋值语句执行将数值赋给线网,可以完成逻辑级描述,也可从更高的抽象角度对线网电路进行描述,多用于组合逻辑电路的描述。连续赋值操作符是“=”。 语句格式: assign 赋值目标线网 = 表达式; 例331 assign a=b | c; //描述的是两输入的或门 assign {c,sum[3:0]}=a[3:0] + b[3:0] + c_in; //描述一个加法器 assign c=max(a,b); //调用了求最大值的函数,将函数返回值赋给c 说明: (1) 式子左边的“赋值目标线网”只能是线网变量,而不是寄存器变量。 (2) 式子右边表达式的操作数可以是线网,可以是寄存器,也可以是函数。 (3) 一旦等式右边任何一个操作数发生变化,右边的表达式就会立刻被重新计算,再进行一次新的赋值。 (4) assign可以使用条件运算符进行条件判断后赋值,例如: assign data_out=sel? a : b;/*如果sel等于1,将a赋给data_out,否则将b赋给data_out,这实现了一个二选一的选择器电路描述*/ 4. 过程赋值与连续赋值的区别 在Verilog HDL程序编写过程中,要注意过程赋值与连续赋值的区别,避免出现问题。表35列出了二者的区别。 表35过程赋值与连续赋值的区别 过 程 赋 值连 续 赋 值 无关键字(过程连续赋值除外)关键字assign 用“=”和“<=”赋值只能用“=”赋值 只能出现在initial和always语句中不能出现在initial和always语句中 用于驱动寄存器用于驱动网线 3.4.2顺序块和并行块语句 Verilog HDL中使用块语句将多条语句组合成一条复合语句。块语句分为顺序块语句和并行块语句。 1. 顺序块 顺序块中的语句按书写顺序执行,由beginend标识。顺序块的格式为 begin 执行语句1; 执行语句2; ︙ end 或 begin 块名 块内变量、参数定义; 执行语句1; 执行语句2; ︙ end 说明: (1) 块名是可选的,是一个块的标识名。 (2) 块内可以根据需要定义变量,声明参数,但这些内容只能在块内使用,类似于“局部变量”和“局部声明”。 (3) 顺序块内的语句是按照语句的书写顺序执行的。在仿真开始时执行第一条语句,后面语句开始的执行时间和前一个语句的执行时间是相关的,如有延时,延时也是相对于前一个语句执行完的仿真时间而言的。当块内的最后一条语句执行完,才跳出该顺序块。 2. 并行块 并行块中的语句并行执行,由forkjoin标识。并行块的格式如下: fork 执行语句1; 执行语句2; ︙ join 或 fork 块名 块内变量、参数定义语句; 执行语句1; 执行语句2; ︙ join 说明: (1) 块名、块内声明语句的理解与顺序块相同。 (2) 并行块的语句是同时执行的,可以将每一条语句看成一个独立的进程,语句的书写顺序不会影响语句的执行结果。应注意避免并行块中的多条语句对同一个变量进行改变,否则可能会引起竞争。 (3) 块内每条语句的起始执行时间是相同的,当块中的执行时间最长的语句执行完,跳出并行块的执行。 3. 顺序块和并行块程序执行过程的区别 下面通过例子来说明顺序块和并行块语句的执行过程。 例332 顺序块: begin s=0; #2 s=1; #2 s=0; #3 s=1; #1 s=0; end 并行块: fork s=0; #2 s=1; #4 s=0; #7 s=1; #8 s=0; join 在仿真中,“#”后的数字表示仿真时间,在顺序块中是通过顺序累加得到当前仿真时间,而并行块中是用绝对时间表示仿真时间。例332中的顺序块和并行块完成了对变量s相同的赋值过程,假设块语句都从仿真时刻0开始执行,实现的赋值过程如表36所示。 以上两个程序都产生了如图35所示的波形。 表36顺序块和并行块执行结果 仿真时刻s的数值 00 21 40 71 80 图35仿真波形 3.4.3结构说明语句 Verilog语言中的过程模块常使用四种结构的说明语句: always语句、initial语句、task语句和function语句。 一个模块中可以有多个initial和always语句。每个initial块和always块代表一个独立工作的单元,不管这两种语句在模块中书写的先后顺序,在仿真一开始就同时开始执行。initial语句只能执行一次,always语句只要满足触发条件,就不断地重复执行,直到仿真过程结束。task语句和function语句定义后,可以在模块的多处进行调用。 在较大的系统中,常需要在不同的地方实现相似的功能、操作,为了程序的简洁、易懂,在Verilog语言中,可以用任务和函数的形式描述这些相似的功能模块,上一层模块可以根据需要对任务和函数进行调用。任务和函数都必须在模块内部进行定义和调用,其作用范围只局限于定义它们的模块内部。熟练编写、使用任务和函数的能力是设计大型系统的基础。 下面对这四种语句分别进行介绍。 1. initial语句 语句格式: initial begin 语句1; 语句2; ︙ end 说明: (1) 一个模块中可以包含多个initial语句,所有的initial语句都同时从0时刻开始并行执行,但是只能执行一次。 (2) initial语句常用于测试文本中对信号的初始化,生成输入仿真波形、监测信号变化等。 (3) 也可以使用fork…join对语句进行组合。 例333 reg [1:0] a,b; reg c; initial //第一个initial语句 begin a=1; b=0; #10 begin a=2; b=3; end //10个单位时间后,对a,b进行再次赋值 #10 begin a=0; b=2; end //20个单位时间后,对a,b进行再次赋值 end initial //第二个initial语句,只有一条执行语句,不需要顺序块语句begin-end c=1; 执行结果如表37所示。 表37仿真执行结果 仿真时刻(以单位时间为基准)变量值 0a=1, b=0, c = 1(信号初始化不占用仿真时间) 10a=2, b=3, c = 1 20a=0, b=2, c = 1 2. always语句 语句格式: always @ <触发事件> 语句或语句组; 说明: (1) always的触发事件可以是控制信号变化、时钟边沿跳变等。always块的触发控制信号可以是一个,也可以是多个,其间用or连接,例如: always @ (posedge clock or posedge reset) /*当clock上升沿或者reset上升沿时,触发always模块执行块中的语句*/ always @ (a or b or c or d) /*当a,b,c,d 四个信号的任意一个电平发生变化时,触发always模块*/ (2) 只要always的触发事件产生一次,always就会执行一次。在整个仿真过程中,如果触发事件不断产生,则always中的语句将被反复执行。 例334 reg q; always @ (posedge clock) //当时钟上升沿到来时,将d的值赋给q q<=d; (3) 一个模块中可以有多个always语句,每个always语句只要有相应的触发事件产生,对应的语句就执行,这与各个always语句书写的前后顺序无关。 例335 always @ (posedge clk) //第一个always语句,当时钟上升沿来到时触发执行 if(rst) counter <= 4`b0000; //当复位信号等于1,计数器counter置0 else counter <= counter + 1; //当复位信号等于0,对输入时钟上升沿进行计数 always @ (counter) //第二个always语句,只要counter发生变化就触发执行 $display ("the counter is = %d",counter); //显示计数器counter的值 … 通过使用两个always块,程序实现了一个对时钟计数到16的有同步复位控制的计数器及其仿真器显示。这两个always模块只要其触发条件成立,就执行各自相关的操作,而两个always块的书写顺序上的先后,不会影响执行结果,体现了Verilog语言描述的“并行性”。 3. 任务(task) 语句格式: task 任务名; <输入输出端口声明>; <任务中数据类型声明>; 语句1; 语句2; … endtask 说明: (1) 任务定义在关键字task和endtask之间,任务中可以包括延迟、时序控制和事件触发等时间控制语句。任务只有在被调用时才执行。 (2) 任务调用与变量的传递。 格式: 任务名(端口1,端口2,…,端口n); 任务的输入/输出都是局部的寄存器,执行完之后才返回结果。 (3) 当任务被调用时,任务被激活。同时,一个任务可以调用其他任务或函数。 例336通过定义,调用任务,将数据送入fifo中。 module top; … //例化一个fifo fifoctlr_cc u1 (.clock_in(clockin), .read_enable_in(read_enable), …); /*以下定义第一个任务writeburst,功能是完成写入一个8位的数据到fifo中的任务*/ task writeburst;//定义任务writeburst input [7:0] wdata; //此task中局部信号 begin always @(posedge clockin) begin write_enable=#2 1; //将写控制信号置1,有效 write_data=#2 wdata; //接收wdata数据,将其传送到fifo的write_data端口 end end endtask initial begin writeburst(128); //调用任务writeburst,将数值128送入fifo … end endmodule (4) 任务也可以没有参数的输入,只完成执行操作。 例337将输入信号的与和或的结果分别输出。 module result(data_in1,data_in2,data_out1,data_out2); input data_in1,data_in2; output data_out1,data_out2; reg data_out1,data_out2; task example; //定义任务example begin data_out1 <= data_in1 & data_in2; data_out2 <= data_in1 | data_in2; end endtask always @ (data_in1 or data_in2) example; //调用任务,任务没有输入参数 endmodule 4. 函数(function) 语句格式: function <返回值的位宽,类型说明> 函数名; <输入端口与类型说明>; <局部变量说明>; begin 语句; end endfunction 说明: (1) 函数定义在关键字function和endfunction之间,函数的目的是返回一个用于表达式的值。 例338 function[3:0] max; //函数名为max,作用: 返回两个数中的最大值 input[3:0] a,b; begin if (a>b) max=a; else max=b; end endfunction 函数的定义使得在模块中定义了一个和函数同名、位宽相同的寄存器类型变量,如例338中的max。如果函数的返回值的位宽缺省时,这个变量位宽为1。 (2) 函数的调用是以函数名同名的寄存器变量作为表达式的操作数来进行调用,并根据函数输入数据的要求,携带、传送数据。如对例338中函数的调用可写成 c=max(10,5); //运行结果c=10 5. 任务和函数的比较 (1) 任务和函数的定义和引用都应位于模块内部,而不是一个独立的模块。 (2) 函数不能启动任务,任务可以调用函数或其他任务。 (3) 任务可以没有输入变量或有任意类型的I/O变量,而函数允许有输入变量且至少有一个,输出则由函数名自身担当。 (4) 函数通过函数名返回一个值,任务名本身没有值,只是实现某种操作,传递数值通过I/O端口实现。 (5) 函数还可以出现在连续赋值语句的右端表达式中。 (6) 任务可以用于组合电路、时序电路的描述; 函数只能用于组合电路的描述,函数的定义不能包含任何的时间控制语句。 3.4.4条件语句 1. if语句 if语句和它的变化形式是条件语句的常见形式,用于判断给定的条件是否满足(为真),根据判定结果,执行相应的操作。 语句格式: (1) if(表达式) <语句>; (2) if(表达式) <语句1>; else <语句2>; (3) if(表达式1) <语句1>; else if(表达式2) <语句2>; ︙ else if(表达式n) <语句n>; else <语句n+1>; 说明: (1) if后面的表达式,可以是逻辑表达式或关系表达式。如果表达式的值是真(1),执行紧接在后的语句; 如果是假(0),执行else后的语句。 (2) if后的表达式还可以是操作数。如果操作数是0、x、z,等价于逻辑假,反之为逻辑真。下式是一种表达式的简化写法: if(reset)等价于if(reset ==1) (3) else不能作为单独的语句使用,必须与if语句配对使用。 (4) 如果if和else后有多个执行语句,可以用beginend块将其整合在一起。 例339 if(a > b) begin data_out1 <=a; data_out2 <=b; end else begin data_out1 <=b; data_out2 <=a; end (5) if语句可以嵌套使用,但是在嵌套使用过程中,应注意与if配对的else语句。通常,else与最近的if语句配对。 例340 if(a >b) //第一个if语句 if ( c ) //第二个if语句 data_out <= c+1; else //第二个if语句的配对else data_out <= a+1; else //第一个if语句的配对else data_out <=b; 特别是当ifelse数目不一致、if嵌套使用等情况时,可使用beginend块,如同算术表达式的括号一样,确定ifelse的配对关系,避免逻辑描述错误。 if(a > b) begin if( ) 执行语句; else 执行语句; end else… (6) 如果不正确使用else,可能会生成不需要的锁存器。 例341 always @ (a or b)//当a和b的数值发生变化时,触发此always块 begin if(a) data_out <=a; end 如果设计者的设计意图是,当a不为0时,data_out赋值为a,否则赋值为b。但例341的描述是: 在a等于0时,没有else语句描述分支的操作,data_out的值将锁定为a。 (7) ifelse表达了一个条件选择的设计意图,它与条件操作符有重要的区别: 条件操作符可以出现在一个表达式中,而这个表达式可以使用在过程赋值中或者连续赋值中,可进行行为建模,也可以进行逻辑级建模。 ifelse只能出现在always、initial块语句,或函数、任务中,一般只能在行为建模中使用。 2. case语句 ifelse语句提供了选择操作,如果选项数目较多,使用会很不方便,当if条件是同一个表达式时,而case是一种多分支选择语句,使用它就很简便。 语句格式: case(控制表达式) 分支表达式1: 语句1; 分支表达式2: 语句2; ︙ 分支表达式n: 语句n; default: 默认语句; endcase 控制表达式常表示为控制信号的某些位,分支表达式表述的是这些控制信号的具体状态值。语句执行时,先计算case后的控制表达式,然后将得到的值与后面的分支表达式的值进行比较,当控制信号的值与某分支表达式的值相等时,执行该分支表达式后的语句; 如果没有匹配的分支表达式,则执行default后的默认语句。 case语句的作用类似于多路选择器。用case语句可以容易、简洁地实现四选一、八选一、十六选一等电路描述。 例342用case语句实现四选一电路。 module mux4(clk,rst,data_in1,data_in2, data_in3,data_in4,select,data_out); input[3:0] data_in1,data_in2; input[3:0] data_in3,data_in4; input[1:0] select; input clk,rst; output[3:0] data_out; reg [3:0] data_out; always @ (posedge clk) if(rst) data_out <=4`b0000; else case(select) 2`b00 : data_out <=data_in1; //如果select=2`b00,输出data_in1的值 2`b01 : data_out <=data_in2; //如果select=2`b01,输出data_in2的值 2`b10 : data_out <=data_in3; //如果select=2`b10,输出data_in3的值 2`b11 : data_out <=data_in4; //如果select=2`b11,输出data_in4的值 default : $display("the control signal is invalid"); endcase endmodule 说明: (1) 每个分支表达式的值必须是互不相同的,否则,将产生同一个控制表达式的值有多种执行语句,从而产生矛盾。 (2) case语句执行中,逐位对控制表达式的值和分支表达式的值进行比较,每一位的值可能是0、1、x、z。如果二者的位宽不一致,将用0加在数值左端的方法调整使二者位宽相等。如果多个不同的状态值有相同的执行语句,可以用逗号将各个状态隔开。 例343 case(select) 2`b00 : data_out <=data_in1; 2`b01 : data_out <=data_in2; 2`b10 : data_out <=data_in3; 2`b11 : data_out <=data_in4; //如果select中有不确定位x,输出值是x 2`b0x,2`bx0,2`b1x,2`bx1,2`bxx: data_out <=4`bxxxx; //如果select中有高阻态位,输出值是z 2`b0z,2`bz0,2`b1z,2`bz1,2`bzz: data_out <=4`bzzzz; default : $display("the control signal is invalid"); endcase (3) default语句是可选的,但是一个case语句中只能有一个default选项。一般而言,建议在case语句中加入default分支,以避免因分支表达式未能对控制表达式的所有状态进行穷举而生成不需要的锁存器。如例343的四选一行为描述中没有default选项,如果有效控制信号中出现了x或z数值,输出端将不会产生相应的变化,原数据锁存。 (4) case语句为表达式值存在不定值x和高阻值z位的情况提供了逐位比较和执行对应语句的操作。 case语句还有两种变形,关键字为casez和casex。这可以对比较中的不关心的值进行忽略。其中,casex将条件表达式或者分支表达式中的不定态x的位视为不关心的位,casez则将高阻态z视为不关心的位。这样,设计者就可以根据具体要求,只对信号的某些位进行比较。具体请查阅Verilog HDL相关手册。 5.1.1节将分析ifelse语句和case语句在使用上带来的不同的综合结果。 3.4.5循环语句 循环语句只能在initial、always块中使用。Verilog HDL中有for、while、forever和repeat四种循环语句,它们的语法规则类似于C语言的循环语句。下面分别进行介绍。 1. for语句 语句格式: for(表达式1; 表达式2; 表达式3)语句; 说明: (1) 表达式1是初始条件表达式,表达式2是循环终止条件,表达式3是改变循环控制变量的赋值语句。 (2) 语句执行过程: 步骤1: 求解表达式1; 步骤2: 求解表达式2,如果其值为真(非0),执行for语句中内嵌的语句组,然后执行步骤3; 如果为假(等于0),结束循环,执行for语句后的操作; 步骤3: 求解表达式3,得到新的循环控制变量的值,转到步骤2继续执行。 例344用for语句对存储器组进行初始化。 reg[7:0] my_memory[511:0]; //定义一个寄存器型数组,共有512个变量,每个变量的位宽为8 integer i; initial begin for(i=0; i<512; i=i+1) //把数组中所有变量赋0值 my_memory[i]<= 8`b0; end 2. while语句 语句格式: while(条件表达式)语句; 说明: 语句执行过程先求解条件表达式的值,如果值为真(等于1),执行内嵌的执行语句(组),否则结束循环。如果一开始就不满足条件表达式,则循环一次都不执行。 例345用while语句求从1加到100的值,加法完成后打印结果。 module count(clk, data_out); input clk; output[12:0] data_out; reg [12:0] data_out; integer j; initial //data_out和 j赋初值为0 begin data_out=0; j=0; end always @ (posedge clk) begin while(j<=100) /*如果j小于或等于100,则执行循环内容,执行100次后,即j大于100后,跳出循环*/ begin data_out=data_out+j; j=j+1; end $display ("the sum is %d,j= %d",data_out,j); end endmodule 在内嵌语句中应该包含使循环控制变量变化的语句,如例345的j=j+1;如果没有此类语句,循环控制变量的值始终不变,循环将永不结束。 3. forever语句 语句格式: forever语句; 说明: forever表示永久循环,无条件地无限次执行其后的语句,相当于while(1),直到遇到系统任务$finish或$stop,如果需要从循环中退出,可以使用disable。 forever不能独立写在程序中,必须写在initial块中。 例346使用forever语句生成一个周期为20个时间单位的时钟信号。 reg clock; initial begin clock=0; forever #10 clock=~clock; end 这段程序常用于编写的测试程序中。 4. repeat语句 语句格式: repeat(表达式)语句; 说明: repeat语句执行其表达式所确定的固定次数的循环操作,其表达式通常是常数,也可以是一个变量,或者一个信号,如果是变量或者信号,循环次数是循环开始时刻变量或信号的值,而不是循环执行期间的值。 例347用加法和移位操作来完成两个4位数值的乘法运算。 module mux(data_in1,data_in2,data_out); input [3:0] data_in1,data_in2; output[7:0] data_out; reg [7:0] data_out; reg[7:0] data_in1_shift,data_in2_shift; initial begin data_in1_shift=data_in1; data_in2_shift=data_in2; data_out=0; repeat(4) begin if(data_in2_shift[0]) data_out=data_out + data_in1_shift; data_in1_shift=data_in1_shift << 1; data_in2_shift=data_in2_shift >> 1; end end endmodule 3.4.6系统任务和系统函数 Verilog HDL的系统任务和系统函数主要用于仿真,标准的系统任务和系统函数提供了显示、文件输入/输出、时间标度和仿真控制等各种功能。系统任务和系统函数前都有一个标志符$加以确认,执行中如果有返回值为系统函数,否则为系统任务。 下面简单介绍几个常用的系统任务和系统函数,包括模块信息的屏幕显示$display、信号的动态监控$monitor、暂停$stop、结束仿真$finish、数据读入$readmemb和$readmemh、文件打开$fopen以及文件关闭$fclose等。这些操作在系统的调试和测试过程中非常有用。其他系统任务和系统函数请参阅Verilog HDL手册。 1. $display $display用于变量、字符串、表达式的屏幕显示,格式如下: $display(p1,p2,…,pn); 其中,p1,p2,…,pn可以是字符串、变量名、表达式等,它的应用类似于C语言的printf。 说明: $display可以根据显示格式的要求显示字符串、数值的内容,常用的显示格式说明见表38。 Verilog语言提供了一些特殊的字符,可以对显示格式进行调整,如表39所示。 表38$display常用显示格式 格式显 示 结 果 %h或%H以十六进制格式输出 %d或% D以十进制格式输出 %o或% O以八进制格式输出 %b或%B以二进制格式输出 %c或%C以ASCII格式输出 %s或%S显示字符串 %t或%T显示当前时间 %f或%F输出十进制形式的实数 表39特殊字符 字符显 示 结 果 \n换行 \t横向跳格,输出到下一个输出区 \\显示字符\ %%显示百分符号% 例348 $display("TESTED COMPLETE PN SEQUENCE rolling over to test again. "); 显示结果如下: TESTED COMPLETE PN SEQUENCE rolling over to test again. $display("a=%d , b=%2.2f",a ,b);//数值的显示,设a=5,b=2.345 显示结果如下: a=5 ,b=2.35 $display("hello \nworld); //特殊字符的显示 显示结果如下: hello world 2. $monitor $monitor函数提供了对信号变化进行监控的功能,格式为 $monitor(p1,p2,…,pn); 其中,p1,p2,…,pn可以是字符串、变量、表达式、时间函数$time等。 说明: (1) 在整个仿真过程中,在任意一个时刻,只要监测的一个或多个变量发生变化,就会启动$monitor函数,输出这一时刻的数值情况。 (2) $monitor函数一般书写在initial块中,即只需调用一次$monitor函数,在整个仿真过程中都有效,这与$display不同。 (3) 在仿真过程中,如果在源程序中调用了多个$monitor函数,只有最后一个调用有效。 (4) Verilog语言提供了两个用于控制监控函数的系统任务$monitoron和$monitoroff。其中,$monitoron用于启动监控任务; $monitoroff用于关闭监控任务。在默认情况下,仿真开始时即启动了监控任务,在多模块联合调试时,为在适当的时候对各个模块的信号进行监控,就有必要将不需要的信号监控用$monitoroff关闭,而用$monitoron打开需要的信号监控。 3. $stop和$finish $stop和$finish常用于文本编写中的测试,它们的格式分别为 $stop; $finish; 说明: $stop暂停仿真,进入一种交互模式,将控制权交给用户; $finish结束仿真过程,返回主操作系统。 例349 initial begin #100 begin a=1;b=1;end #100 $stop; //在200ns时暂停仿真,交由用户控制 #200 $finish; //在400ns时,退出仿真 end 4. $readmemb和$readmemh $readmemb和$readmemh提供了将文件中的数据读到存储器阵列中的有效手段,这两个任务可以完成读取二进制或十六进制的数据。格式如下: $readmemb("文件名",存储器名,起始地址,终止地址); $readmemh("文件名",存储器名,起始地址,终止地址); 其中,文件名、存储器名是必须有的,起始地址、终止地址是可选项,这两类地址信息用十进制数表示。 说明: (1) “文件名”是被读取文件的ASCII码形式,还可以增加该文件的位置信息,例如“c:/test/my_project/simulus.dat”。 (2) 文件中的内容中只允许有空白(包括空格、换行)、Verilog注释行、数据地址(为十六进制格式)、二进制数或十六进制数,数中不能有位宽、进制的说明,对$readmemb而言,应为二进制数值; 对$readmemh而言,应为十六进制数值。 例350 module test(); reg[1:0] my_mem[7:0]; //定义了8个宽度为2的存储器组 integer i; initial begin //将位于D:/test目录下的my_data.dat中的数据读入my_mem中 $readmemb("D:/ test/my_data.dat",my_mem); for(i=0;i<8;i=i+1) $display("my_mem[%d]=%b",i,my_mem[i]); end endmodule 其中,my_data.dat的内容如下: 00 01 10 11 00 01 10 11 执行结果如下: memory[0]=00 memory[1]=01 memory[2]=10 memory[3]=11 memory[4]=00 memory[5]=01 memory[6]=10 memory[7]=11 (3) 数据文件中可以用@<地址>将数据存入存储器的指定位置,地址用十六进制数表示,@和<地址>间不能有空格。 例351如果例350中的my_data.dat为: @1 00 00 @6 1x z1 或 @1 00 00 @6 1x z1 执行结果如下: my_mem[0]=xx my_mem[1]=00 my_mem[2]=00 my_mem[3]=xx my_mem[4]=xx my_mem[5]=xx my_mem[6]=1x my_mem[7]=z1 一般情况下,数据文件中指定的地址空间应该在存储器定义的空间范围之内,否则将提示出错信息。数据文件中的数据可以包括x、z,存储器未赋值的位置默认为x。 (4) 语句中的起始地址和终止地址的不同定义对数据装载有如下影响: ① 未指定终止地址,执行时,将数据从指定的起始地址开始载入,如果数据文件的数据个数多于存储器可以装载的单元个数,则数据存入直到该存储器的结束地址为止; 反之,未能赋值的存储器单元的值默认为x。 ② 如果任务中指定了起始地址和终止地址,在执行中,将数据从起始位置开始载入,直到该指定的结束地址。如果指定的地址空间超出了定义的存储器空间,将提示出错信息。 ③ 如果在数据文件和任务定义中都给出了地址信息,那么数据文件中指定的地址必须在任务定义中的地址声明范围之内,否则执行中将提示出错信息。 5. $fopen和$fclose Verilog语言支持将仿真结果输出到指定的文件中,使用系统函数$fopen打开一个可以写入数据的文件; 再使用系统任务$fclose将前面打开的文件关闭。$fopen格式为 <file_descriptor>=$fopen("文件名"); 说明: $fopen返回的file_descriptor是一个32位(bit)的整数,称为无符号多通道描述符,该描述符每次只有一位被设置成1,其余位为0,表示一个独立的输出文件通道,如果文件不能正常打开,描述符的值是0。最低位(第0位)置1表示标准输出,第1位被置1表示第一个被打开的文件,第2位被置1表示第二个被打开的文件,依次类推。 例352 integer file_descriptor1,file_descriptor2,file_descriptor3; initial begin file_descriptor1=$fopen("file1.out"); file_descriptor2=$fopen("file2.out"); file_descriptor3=$fopen("file3.out"); $display("file_descriptor1=%h",file_descriptor1); $display("file_descriptor2=%h",file_descriptor2); $display("file_descriptor3=%h",file_descriptor3); end endmodule 执行结果如下: file_descriptor1=00000002 file_descriptor2=00000004 file_descriptor3=00000008 当要关闭打开的文件,采用如下格式: $fclose; 说明: 用$fclose将某文件关闭后,不能再写入,无符号多通道描述符的相应位设置为0,下次$fopen的调用可以再使用这一位。 3.4.7编译预处理命令 Verilog语言和C语言一样提供了一些特殊的命令,在进行Verilog语言综合时,综合工具首先对这些特殊命令进行“预处理”,然后将得到的结果和源程序一起再进行通常的编译处理。 编译预处理指令前有一个标志符“`”(反引号)加以确认,其作用范围是: 命令定义后直到本文件结束或其他命令替代或取消该命令为止。 接下来介绍常用的`define、`ifdef、`elsif、`else、`endif、`include、`timescale等命令,其余的如`default_nettype、`resetall、`unconnected_drive、`nounconnected_drive、`celldefine和`endcelldefine等命令请查阅Verilog HDL手册。 1. 宏定义命令`define 宏定义`define指定一个宏名来代表一个字符串。 语句格式: `define宏名字符串 说明: (1) 为与变量名区别,建议使用大写字符定义宏名。 (2) 在源文件中引用已定义的宏名时,必须在宏名前加“`”。 (3) 宏定义在预编译时只将宏名和字符串进行简单的置换,不作语法检查,如有错,只在宏展开后的源程序编译时才报错。 (4) 宏定义不是Verilog HDL语句,不必在末尾加分号,如果加了分号,则分号也将作为宏定义的字符串的内容,进行置换。 (5) `define命令可以出现在模块定义里,也可以出现在模块定义外。宏定义的有效范围是宏定义命令后到源文件结束。 (6) 宏定义可嵌套使用。 例353 `define ADD a+b //用a+b表示ADD字符串 assign c=`ADD; //在引用宏名时,前加"`",即assign c=a+b; `define ADD1 a+b `define ADD2 `ADD1+d assign data_out=`ADD2; //等同于data_out=a+b+d; `define data "the c=%b "//用the c=%b表示字符串data 2. 条件编译命令`ifdef、`elsif、`else和`endif 一般情况下,源程序的所有行都进行编译,但在一些特定的应用场合下,对源程序中满足指定编译条件的语句才进行编译,或者满足某条件时,对一组语句进行编译,当条件不满足时编译另一组语句。 语句格式: `ifdef 宏名 程序段1; `elsif 程序段2; `else 程序段3; `endif 其中,`else和`elsif命令对于`ifdef命令是可选的。 3. 文件包含命令`include 文件包含是指一个源文件可以将另外一个源文件的内容全部“复制”过来,作为一个源程序进行编译。可用`include来实现文件包含。 语句格式: `include "文件名" 说明: (1) 该语句可以出现在Verilog HDL程序的任何地方。编译时,`include "文件名"这一行文字由被包含的文件内容全部代替。文件名中还可以指明该文件存放的路径名。设计中可将常用的宏定义、任务、函数组成一个文件,用`include命令包含到源文件中。 (2) 一个`include只能包含一个文件,如果要包含多个文件,需要写多个`include命令。如果源文件top.v包含source1,而source1.v又需要用到source2.v的内容,则可以将source1.v和source2.v用`include包含到源文件中,但是source2.v应出现在source1.v之前,即 `include "source2.v" `include "source1.v" 例354 (1) 将系统设计需要的宏定义写在一个模块my_define.v中。 `define GENERIC_MULTP2_32X32 `define IC_1W_8KB `define RAMB16 (2) 上层模块top.v的编译中使用my_define.v。 `include "my_define.v" module top( ); … endmodule 编译预处理后,成为如下文件: `define GENERIC_MULTP2_32X32 `define IC_1W_8KB `define RAMB16 module top( ); … endmodule 4. 时间尺度命令`timescale 在Verilog HDL模型中,所有时延都用单位时间表述。使用`timescale命令将单位时间与实际时间相关联。用于定义仿真时间、延迟时间的单位和时延精度。 语句格式: `timescale时间单位/时间精度 说明: (1) 时间单位是指时间和延迟的测量单位,时间精度是指仿真过程中延迟值进位取整的精度,时间精度应该小于或等于时间单位。时间单位和时间精度由值1、10、和100以及单位秒(s)、毫秒(ms,即10-3s)、微秒(μs,即10-6s)、纳秒(ns,即10-9s)、皮秒(ps,即10-12s)等组成。该命令末尾没有分号。 (2) `timescale命令在模块说明外部出现,并且影响后面所有的时延值,直至遇到另一个`timescale命令。当一个设计中的多个模块带有自身的`timescale编译命令时,仿真的时间单位与精度采用所有模块的最小时延精度,并且所有时延都相应地换算为最小时延精度。 例355 `timescale 1ns/100ps //表示时间单位为1ns,时间精度为100ps module testbench(); … initial begin a=0; b=0; //在0时刻,a=0,b=0 #10 begin a=4; b=2; end //在0+10*1=10ns时,a=4;b=2 #20 begin a=2; b=3; end //在10+20*1=30ns时,a=2;b=3 end endmodule 3.4.8Verilog HDL可综合设计 用硬件描述语言进行程序设计的最终目的是进行硬件实现,在硬件描述语言中,许多基于仿真的语句虽然符合语法规则,但不能用硬件来实现,即不能映射到硬件逻辑电路单元。如果要最终实现硬件设计,则必须写出可以综合的程序。 Verilog HDL允许用户在不同的抽象层次上对电路进行建模,这些层次从逻辑级、寄存器传输级、算法级直至系统级。因此同一个电路可以有多种不同的描述方式,但不是每一种描述都可以综合。事实上,Verilog HDL原本是被设计成一种仿真语言,而不是一种用于“综合”的语言。结果导致有些语句只满足语法,而无法映射到具体逻辑元件结构,是不可综合的,例如initial语句、时间声明、系统任务和系统函数等。同样,也不存在用于寄存器传输级综合的Verilog HDL标准子集。 由于存在这些问题,不同的“综合”系统所支持的Verilog HDL综合子集不同。由于在Verilog HDL中不存在单个对象来表示锁存器或触发器,所以每一种综合工具都会提供不同的机制以实现锁存器或触发器的建模,因此各种综合工具都定义了自己的Verilog HDL可综合子集以及自己的建模方式。这一局限给设计者造成了严重的障碍,因为设计者不仅需要理解Verilog HDL,还必须理解特定综合工具的建模方式,才能编写出可综合的模型。 表310列出了可被绝大多数综合工具支持的可综合语句。 表310Verilog HDL可综合的运算符、数据类型和语句列表 语句可综合说明 运 算 符 逻辑/按位运算符 算术运算符 移位运算符 关系运算符 按位/缩减运算符 逻辑运算符 条件运算符 拼接运算符 |, ~, &支持 /, %受限支持在/和%运算中必须是除以或模2的幂次方 *, +, -支持 <<, >>支持 <,<=,>,>=支持 &, ~&支持 ^, ~^支持 |, ~|支持 &&支持 ? : 支持 { }支持 数 据 类 型 网线数据类型wire支持 寄存器数据类型reg, integer支持综合工具把integer综合成32位的寄存器型数据 存储器型受限支持仅支持一维限定性数组 语 句 连续赋值语句assign支持赋值语句的左边是wire型,右边是reg、integer或wire型 过程赋值语句=, <=支持一般是在always块内,采用非阻塞赋值 顺序块语句beginend支持 并行块语句forkjoin支持 always支持 结构说明语句function支持函数内循环变量的循环次数、步长和范围必须固定; 不能进行函数递归调用; 函数体内不能包含任何的时间控制语句; 函数不能启动任务 task支持任务内循环变量的循环次数、步长和范围必须固定; 不能进行任务递归调用; 任务体内不能包含任何的时间控制语句; 任务可以启动其他任务和函数 条件语句if…else if支持 case支持 for受限支持循环次数、步长和范围必须固定 循环语句repeat受限支持循环次数、步长和范围必须固定 while受限支持循环次数、步长和范围必须固定 3.5Verilog HDL设计举例 本节通过一些可综合的Verilog HDL设计实例介绍如何运用Verilog语言对组合逻辑电路和时序逻辑电路进行描述,并重点介绍有限状态机设计。 3.5.1组合电路设计 用assign语句对wire型变量进行赋值,综合后的结果是组合逻辑电路。 用always@ (敏感信号表),即电平敏感的always块描述的电路综合后的结果也可以是组合逻辑电路。此时,always块内赋值语句左边的变量是reg或integer型,块中要避免组合反馈回路。在生成组合逻辑的always块中被赋值的所有信号必须都在always@ (敏感信号表)的敏感电平列表中列出,否则在综合时将会为没有列出的信号隐含地产生一个透明的锁存器,这时综合后的电路已不是纯组合电路了,自Verilog 2001版本开始,敏感信号表可以用(*)代替,由*运算符自动识别所有敏感变量。 1. 编码器和译码器 编码器和译码器是常见的组合逻辑电路。组合逻辑可以使用连续赋值语句assign,也可以使用always语句。在Verilog HDL设计中,它们有相似的设计思路,现以译码器为例讨论用Verilog HDL设计组合电路。 例356BCD码将十进制数字转换为二进制,每一个十进制的数字(0~9)都对应着一个四位的二进制码,按照表311的转换关系,设计数字系统,其输出驱动信号至七段LED以显示相应信息。 表311十进制数与BCD码的转换关系 十进制数8421BCD码 data_in[3:0]输出到七段LED的数据 data_out[6:0](共阴/共阳)七段LED图例 000000111111/1000000 100010000110/1111001 200101011011/0100100 300111001111/0110000 401001100110/0011001 501011101101/0010010 601101111101/0000010 701110000111/1111000 810001111111/0000000 910011100111/0011000 module bin2bcd (data_in ,EN ,data_out ); input [3:0] data_in; input EN; //系统使能信号 output [6:0] data_out; reg [6:0] data_out; always @(data_in or EN )//当data_in 或EN变化时触发always模块,可用always @ (*) begin data_out = {7{1`b0}}; if (EN == 1) begin case (data_in) //根据共阳接法译码 4`b0000:data_out [6:0]=7`b1000000; 4`b0001:data_out [6:0]=7`b1111001; 4`b0010:data_out [6:0]=7`b0100100; 4`b0011:data_out [6:0]=7`b0110000; 4`b0100:data_out [6:0]=7`b0011001; 4`b0101:data_out [6:0]=7`b0010010; 4`b0110:data_out [6:0]=7`b0000010; 4`b0111:data_out [6:0]=7`b1111000; 4`b1000:data_out [6:0]=7`b0000000; 4`b1001:data_out [6:0]=7`b0011000; default:data_out [6:0]={7{1`b0}}; endcase end end endmodule 2. 数据选择器 在数字信号的传输过程中,常需要从一组输入数据中选择一个输出,这就需要设计数据选择器或多路开关的逻辑电路。 例357设计一个数据选择器,功能描述: 在选择信号SEL、使能信号EN的控制下,从输入信号IN0、IN1、IN2、IN3中选择输出值OUT,见表312。 表312数据选择器真值表 ENSELOUT 1 2`b00OUT=IN0 2`b01OUT=IN1 2`b10OUT=IN2 2`b11OUT=IN3 0任何值OUT=0 `define width 8 module mux(EN,IN0,IN1,IN2,IN3,SEL,OUT ); input EN; input [`width-1:0] IN0,IN1,IN2,IN3; input [1:0] SEL; output [`width-1:0] OUT; reg [`width-1:0] OUT; always @(SEL or EN or IN0 or IN1 or IN2 or IN3 )//或always @ (*) begin if (EN==0) OUT={8{1`b0}}; else case (SEL) 2`b00:OUT=IN0; 2`b01:OUT=IN1; 2`b10:OUT=IN2; 2`b11:OUT=IN3; default:OUT={8{1`b0}}; endcase end endmodule 也可以去掉always块而写成: wire[width-1:0] OUT; OUT= (EN==0)?8`b0:(SEL==2`b00) ? IN0 :(SEL==2`b01) ? IN1 :(SEL==2`b10) ? IN2 :(SEL==2`b11) ? IN3 :8`b0; 3. 数值比较器 数值比较器是数字系统常用的比较两个数大小的逻辑电路,一位的数值比较器逻辑关系较为简单,可以用原理图设计方法或调用Verilog语言的逻辑门等完成。随着比较的数值的位数增加,如果仍用逻辑门搭建电路则较复杂,但采用Verilog HDL来描述这一电路还是很容易的。程序通过改变define宏定义的位宽值,可以被综合工具综合成不同位宽的比较器。 例358设计比较器电路,实现两个多位数的比较,并将结果显示如下: 当a>b时置a_great为1,其余输出端为0; 当a=b时置a_equal_b为1,其余输出端为0; 当a<b时置b_great为1,其余输出端为0。 `define width 8//定义比较数值的位宽 module compare(a,b,a_great,a_equal_b,b_great); input[`width-1:0] a,b; output a_great,a_equal_b,b_great; reg a_great,a_equal_b,b_great; always @ (a or b ) //如果a或者b任意一个发生变化,触发执行以下操作,可用always @ (*) begin if(a > b) a_great=1; else a_great <=0; if(a==b) a_equal_b <=1; else a_equal_b <=0; if(a < b) b_great <=1; else b_great <=0; end endmodule 3.5.2时序电路设计 对时序电路建模时,always语句必须用时钟信号、复位或置位信号的边沿控制触发,如用always @ (posedge clock)或always @ (negedge clock)块描述的电路就可综合为同步时序逻辑电路。在always语句中对时序电路建模时一般采用非阻塞赋值。 1. 移位寄存器 当寄存器存储的代码能够在移位脉冲的作用下依次左移或右移,就构成了移位寄存器,移位寄存器不仅可以存储代码,而且可以用来实现数据的串行到并行的转换、数值的运算以及数据的处理等。 例359设计由边沿触发结构的D触发器组成的4位移位寄存器,如图36所示。 图36移位寄存器结构图 分析: 当时钟Clock上升沿来到时,如果输入端的数据已经稳定,所有触发器的输出端按照输入端的状态翻转,加到寄存器输入端D的值被存入D触发器,按照图36的结构,相当于移位寄存器中原有的代码依次向右移了一位。经过4个周期时钟信号后,串行输入的4位代码全部移入了移位寄存器中,如果在4个触发器的输出端引出输出代码,就完成了串行到并行的转换。 module shift_flop(D,CLK,Q); input D; input CLK; output [3:0] Q; reg [3:0] Q; always @ (posedge clk) begin Q[0] <=D; Q[1] <=Q[0]; Q[2] <=Q[1]; Q[3] <=Q[2]; end endmodule 2. 计数器 计数器是数字电路中使用广泛的时序电路,常用于脉冲计数,以控制程序执行时间,还可用于分频、定时、产生节拍脉冲等。 例360设计一个带异步复位的十六进制计数器。 module counter_16(clk,reset,counter_data); input clk,reset; reg [3:0] counter_data; output always @ (posedge clk or posedge reset); if(reset==1) counter_data <=4`b0000; //当复位信号为1时,计数器置位为0 else counter_data <=counter_data + 1; endmodule 3. 分频器 例361设有一个50MHz的时钟源,设计分频电路得到秒脉冲时钟信号。 分析: 根据设计要求,可知分频系数应为49999999。 module divider50m(inclk,outclk); input inclk; output outclk; reg outclk; reg [25:0] counter; always @ ( posedge inclk ) begin if ( count==49999999 ) count <=0; else count <=count + 1; end always @ ( count ) begin if ( count==49999999 ) outclk <=1; else outclk <=0; end endmodule 3.5.3数字系统设计 在第1章中已指出数字系统是多个分层次嵌套的有限状态机组成,常分解成数据通道和控制单元两部分。数字系统的控制单元通常用传统的有限状态机或时钟模式的时序电路来建模。每个控制步骤可被看作一种状态,即现态,实现时该状态由一个状态寄存器来保存,与每一控制步骤相关的转移条件(即输入)确定了它将要转换的状态(即次态)。在每个时钟周期,状态寄存器中都要填入由现态和输入决定的下一个状态(次态)。 有限状态机根据其输出的逻辑关系可分为两类,当时序逻辑的输出是输入信号和当前状态的函数,且当前状态和输入信号决定了下一状态,称为Mealy状态机; 当系统的输出只是当前状态的函数,称为Moore状态机。电路结构图分别如图37和图38所示。 图37Mealy状态机结构图 图38Moore状态机结构图 除了根据输出信号的产生方式划分外,状态机还可以根据状态编码方式划分。常用的编码方式有二进制顺序编码、格雷码、随机码、一位有效(onehot)编码等。 用Verilog语言描述有限状态机可使用多种风格。不同的风格会极大地影响电路性能。通常有3种描述方式: 单always块、双always块和三always块。 单always块把组合逻辑和时序逻辑用同一个时序always块描述,其输出是寄存器输出,无毛刺,但这种方式会产生多余的触发器,代码难以修改和调试,应该尽量避免使用。 双always块大多用于描述Mealy状态机和组合输出的Moore状态机,时序always块描述当前状态逻辑,组合逻辑always块描述下一状态(次态)逻辑并给输出赋值。这种方式结构清晰,综合后的时序性能好,资源占用少,节省面积。但组合逻辑输出往往会有毛刺,当输出向量作为时钟信号时,这些毛刺会对电路产生致命的影响。 三always块大多用于同步Mealy状态机,两个时序always块分别用来描述当前状态(现态)逻辑和对输出赋值,组合always块用于产生下一状态。这种方式的状态机也是寄存器输出,输出无毛刺,并且代码比单always块清晰易读,但是面积大于双always块。 先看两个双always块的Mealy机和Moore机实例,然后通过一个数字系统的设计来学习状态机的设计。 例362Mealy型状态机。 … always @(posedge clk) state <=next_state; always @(state or in1 or in2) begin case (state) 2`d0: begin out <=in1 & in2; //输出 if (in1) next_state <=1; //下一状态确定 else next_state <=2; end 2`d1: begin out <=~in2; if (in2) next_state <=2; else next_state <=3; end endcase end Mealy型状态机的系统输出反映系统当前状态和系统的输入。 例363Moore型状态机。 … always @(posedge clk) state=next_state; always @ (*) begin case (state) 2`d0: begin out=1; //输出 if (in1) next_state=1; //下一状态确定 else next_state=2; end 2`d1: begin if (in2) next_state=0; else next_state=3; end endcase end Moore型状态机的系统输出反映系统当前状态,与系统的输入无关。 例364用状态机设计交通灯控制器。由一条主干道和一条支干道的汇合点形成十字交叉路口,主干道为东西向,支干道为南北向。为确保车辆安全、迅速地通行,在交叉道口的每个入口处设置了红、绿、黄3色信号灯。 要求: (1) 主干道绿灯亮时,支干道红灯亮,反之亦然; 主干道每次放行35s,支干道每次放行25s。每次由绿灯变为红灯的过程中,黄灯亮作为过渡,时间为5s。 (2) 能实现正常的倒计时显示功能。 (3) 能实现总体清零功能: 计数器由初始状态开始计数,对应状态的指示灯亮。 (4) 能实现特殊状态的功能显示: 进入特殊状态时,东西、南北路口均显示红灯状态。 根据要求,交通灯的状态转换如表313所示。 表313交通灯控制器状态转换表 状态主 干 道支 干 道时间/s 0红灯亮红灯亮 1绿灯亮红灯亮35 2黄灯亮红灯亮5 3红灯亮绿灯亮25 4红灯亮黄灯亮5 交通灯控制器系统框图如图39所示,包括置数模块、计数模块、译码器模块和主控制器模块。置数模块将交通灯的点亮时间预置到置数电路中,计数模块以秒为单位倒计时。当计数值减为零时,主控电路改变输出状态,电路进入下一个状态的倒计时。为了简化设计使结构清晰,将置数模块、计数模块和译码器模块视作整个系统的数据通道,与主控制器模块构成了“数据通道+控制器”的系统结构。因为将定时计数器划归到了数据通道,使得控制器的状态数大大减少,主控制部分可以按照有限状态机设计。下面设计主控制模块。 图39交通灯控制系统框图 根据对设计要求的分析,主控制单元的输入信号有: (1) 时钟clock。 (2) 复位清零信号reset(reset=1表示系统复位)。 (3) 紧急状态输入信号sensor1(sensor1=1表示进入紧急状态)。 (4) 定时计数器的输入信号sensor2(由sensor2[2]、sensor2[1]和sensor2[0]三位组成,该信号为高电平时,分别表示35s、5s、25s的计时完成)。 输出信号有: (1) 主干道控制信号(red1, yellow1, green1)。 (2) 支干道的控制信号(red2, yellow2, green2)。 (3) 控制状态信号state(输出到定时计数器,分别进行35s、25s、5s计时)。 主控制单元的状态转移图如图310所示。 图310交通灯控制器状态转移图 注: 未标出的信号灯值为0,表示该信号灯关闭。 module traffic_control (clock, reset, sensor1, sensor2, red1, yellow1, green1, red2, yellow2, green2); input clock, reset, sensor1, sensor2; output red1, yellow1, green1, red2, yellow2, green2; //定义各个状态 parameter st0=0, st1=1, st2=2, st3=3,st4=4; reg [2:0] state, nxstate; reg red1, yellow1, green1, red2, yellow2, green2; //状态更新 always @(posedge clock) begin if (!reset) state=st1; else state=nxstate; end //根据当前状态和输入,计算下一个状态和输出 always @(state or sensor1 or sensor2) begin case (state) //依据状态转移图,完成状态跳转 st0: begin //状态0 if(sensor1) begin nxstate=st0; red1=1`b1; yellow1=1`b0; green1=1`b0; red2=1`b1; yellow2=1`b0; green2=1`b0; end else begin red1=1`b0; yellow1=1`b0; green1=1`b1; red2=1`b1; yellow2=1`b0; green2=1`b0; nxstate=st1; end end st1: begin //状态1 if(sensor1) begin red1=1`b1; yellow1=1`b0; green1=1`b0; red2=1`b1; yellow2=1`b0; green2=1`b0; nxstate=st0; end else if(sensor2[2]) begin red1=1`b0; yellow1=1`b1; green1=1`b0; red2=1`b1; yellow2=1`b0; green2=1`b0; nxstate=st2; end end st2: begin //状态2 if(sensor1) begin red1=1`b1; yellow1=1`b0; green1=1`b0; red2=1`b1; yellow2=1`b0; green2=1`b0; nxstate=st0; end else if(sensor2[1]) begin red1=1`b1; yellow1=1`b0; green1=1`b0; red2=1`b0; yellow2=1`b0; green2=1`b1; nxstate=st3; end end st3: begin //状态3 if(sensor1) begin red1=1`b1; yellow1=1`b0; green1=1`b0; red2=1`b1; yellow2=1`b0; green2=1`b0; nxstate=st0; end else if(sensor2[0]) begin red1=1`b1; yellow1=1`b0; green1=1`b0; red2=1`b0; yellow2=1`b1; green2=1`b0; nxstate=st4; end end st4: begin //状态4 if(sensor1) begin red1=1`b1; yellow1=1`b0; green1=1`b0; red2=1`b1; yellow2=1`b0; green2=1`b0; nxstate=st0; end else if(sensor2[1]) begin red1=1`b0; yellow1=1`b0; green1=1`b1; red2=1`b1; yellow2=1`b0; green2=1`b0; nxstate=st1; end end endcase end endmodule 3.5.4数码管扫描显示电路 单个数码管显示器包括7个LED灯和1个小圆点,需要8个I/O口来进行控制。采用这种控制方式,当使用多个数码管进行显示时,每个数码管都将需要8个I/O口。在实际应用中,为了减少FPGA芯片I/O口的使用数量,一般会采用分时复用的扫描显示方案进行数码管驱动。以4个数码管显示为例,采用扫描显示方案进行驱动时,4个数码管的8个段码并接在一起,再用4个I/O口分别控制每个数码管的公共端,动态点亮数码管。这样只用12个I/O口就可以实现4个数码管的显示控制,比静态显示方式的32个I/O口数量大大减少。 如图311所示,在最右端的数码管上显示“3”时,并接的段码信号为“01100001”,4个公共端的控制信号为“1110”。这种控制方式采用分时复用的模式轮流点亮数码管,在同一时间只会点亮一个数码管,数码管扫描显示电路的时序如图312所示。分时复用的扫描显示利用了人眼的视觉暂留特性,如果公共端控制信号的刷新速度足够快,人眼就不会区分出LED的闪烁,认为4个数码管是同时点亮的。 图311数码管扫描显示电路 图312数码管扫描显示电路时序图 分时复用的数码管显示电路模块含有4个控制信号,即an3、an2、an1和an0,以及与控制信号一致的输出段码信号sseg。控制信号的刷新频率必须足够快才能避免闪烁感,但也不能太快,以免影响数码管的开关切换,最佳工作频率为1000Hz左右。在设计中,利用一个18位二进制计数器对系统输入时钟进行分频得到所需工作频率,分频器高两位用来作为控制信号,例如an[0]的刷新频率为(50×106/216)Hz,约等于800Hz。4位数码管动态扫描显示电路的Verilog实现代码见例365。 例3654位数码管动态扫描显示电路的Verilog HDL描述。 module scan_led_disp (input clk,reset, input [7:0] in3,in2,inl,in0, output reg [3:0] an, output reg[7:0]sseg ); localparam N : 18; //对输入50MHz时钟进行分频(50 MHz/216) reg[N-1:0] regN; ahays @ (posedge clk,posedge reset) if (reset) regN<= 0; else regN<: regN+1, always @ * case (regN[N-l:N-2]) 2`b00: begin an=4`b1110; sseg=in0; end 2`b01: begin an:4`b1101; sseg:in1; end 2`b10: begin an:4`b1011; sseg:in2; end default: begin an=4`b0111; sseg=in3; end endcase endmodule 当采用分时复用电路,在七段式数码管上显示十六进制数字时,还需要4个译码电路,另外一个更好的选择是首先输出多路十六进制数据然后将其译码。这种方案只需要一个译码电路,使四选一数据选择器的位宽从8位降为5位(4位十六进制数和1位小数点)。实现代码见例366。除clock和reset信号之外,输入信号包括4个4位十六进制数hex3、hex2、hex1、hex0和p_in中的4位小数点。 例3664位十六进制数的数码管动态显示电路Verilog HDL描述。 module scan_led_hex_disp (input clk, reset, input [3:0] hex3, hex2,hex1,hex0, input [3:0] dp_in, output reg [3:0] an, output reg [7:0]sseg ); localparam N=18; //对输入50MHz时钟进行分频(50 MHz/216) reg[N-1:0] regN; reg[3:0] hex_in; always @ (posedge clk,posedge reset) if ( reset) regN<= 0; else regN<=regN+1; always @* case (regN[N-l:N-2]) 2`b00: begin an=4`b1110; hex_in=hex0; dp=dp_in[0]; end 2`b01: begin an=4`b1101; hex_in=hex1; dp=dp_in[l]; end 2`b10: begin an=4`bl011; hex_in=hex2; dp=dp_int[2]; end default: begin an=4`b0111; hex_in=hex3; dp=dp_in[3]; end endcase always @* begin case ( hex_in) 4`h0:sseg[6:0]=7`b1000000; 4`h1:sseg[6:0]=7`b1111001; 4`h2:sseg[6:0]=7`b0100100; 4`h3:sseg[6:0]=7`b0110000; 4`h4:sseg[6:0]=7`b0011001; 4`h5:sseg[6:0]=7`b0010010; 4`h6:sseg[6:0]=7`b0000010; 4`h7:sseg[6:0]=7`b1111000; 4`h8:sseg[6:0]=7`b0000000; 4`h9:sseg[6:0]=7`b0011000; 4`ha:sseg[6:0]=7`b0001000, 4`hb:sseg[6:0]=7`b0000011; 4`hc:sseg[6:0]=7`b1000110; 4`hd:sseg[6:0]=7`b0100001; 4`he:sseg[6:0]=7`b0000110; default: sseg[6:0]=7`b0001110; //4`hf endcase sseg[7]=dp; end endmodule 实际可在FPGA的电路中验证该设计,把8位开关数据作为两个4位无符号数据的输入,并使两个数据相加,将其结果显示在四位七段式数码管上。实现代码见例367。 例3674位十六进制数的数码管动态显示测试。 module scan_led_hex_disp_test (input clk, input[7:0] sw, output [3:0] an, output [7:0]sseg ); wire [3:0] a,b; wire [7:0] sum; assign a=sw [3:0]; assign b=sw [7:4]; assign sum={4`b0,a}+{4`b0, b}; //实例化4位十六进制数动态显示模块 scan_led_hex_disp_scan_led_disp_unit (.clk(clk), .reset(1`b0), .hex3(sum[7:4]),.hex2(sum[3:0]),.hexl(b),.hex0(a), .dp_in(4`b1011),.an(an),.sseg(sseg)); endmodule 许多时序逻辑电路一般工作在相对较低的频率,就像分时复用数码管电路中的使能脉冲一样。这可以通过使用计数器来产生只有一个时钟周期的使能信号。在这个电路中使用的是18位计数器: localparam N=18; reg [N-l:0] regN; 考虑计数器的位数,仿真这种电路需要消耗大量的计算时间(218个时钟周期为一个周期)。因为主要工作在于分时复用那段代码,大部分模拟时间被浪费了。更高效的方法是使用一个较小的计数器进行仿真,可以通过修改常量声明来实现: localparam N=4; 这样就只需要24个时钟周期为一个仿真周期,节省了大量时间,并且可以更好地观察关键操作。 最好定义参数N,而不是将其设置为一个常量,在仿真与综合时可以方便修改代码。同时在实例化过程中,也可以对于仿真和综合设置不同的值。 3.5.5LED通用异步收发电路设计 通用异步收发(UART)是一种通用串行数据总线,用于异步通信。UART能实现双向通信,在嵌入式设计中,它常用于主机与辅助设备通信。UART是异步串行通信口的总称,包括RS232、RS449、RS423、RS422和RS485等各种异步串行通信接口标准规范和总线标准规范,它规定了通信口的电气特性、传输速率、连接特性和接口的机械特性等内容,实际上是属于通信网络中的物理层(最底层)的概念,与通信协议没有直接关系。在本案例中采用的是RS232通信协议。UART传输时序如图313所示。 图313UART传输时序 (1) 发送数据过程: 空闲状态,线路处于高电平; 当收到发送数据指令后,拉低电平一个数据位的时间(如图313起始位的时间); 接着数据按低位到高位依次发送,数据发送完毕,接着发送奇偶校验位和停止位(停止位为高电平),一帧数据发送结束(本案例中,LED电路仅处于接收状态,关于发送功能暂时不讨论)。 (2) 接收数据过程: 空闲状态,电路处于高电平; 当检测到电路的下降沿,说明电路有数据传输,按照约定的波特率从低位到高位接收数据,数据接收完毕; 接着接收并比较奇偶校验位是否正确,如果正确,则通知接收端设备准备接收数据或存入缓存。 此案例中波特率默认设置为115200b/s,时钟频率默认为50MHz,过采样率为16倍波特率,因为分频数至少为2,所以时钟频率必须至少为32倍波特率。Rst是输入系统的异步复位信号,在同步逻辑中利用之前,它必须同步到时钟域clk_rx,可以利用一个简单的固化亚稳态的方法实现。此案例主要涉及模块如图314所示。 图314uartled组成框图 差分时钟由IBUFGDS缓冲把信号带进FPGA,通过BUFG分布到低偏斜的内部时钟网线上。按钮的复位由IBUF缓冲,并馈送到meta_harden模块以减少其将成为亚稳态的风险,被“净化”的复位将提供其余模块。第二个按钮信号由IBUF缓冲,馈送到meta_harden模块,被同步的信号提供给led_ctl模块。uart_rx捕获由IBUF缓冲rxd_pin的串行数据,将它存放到模块输出端的rx_data 8位总线上。信号rx_data_rdy持续一个时钟的脉冲为高。在这个设计中,来自uart_rx的帧错误信号没有利用。认定rx_data_rdy对led_ctl模块的作用,它捕获接收的字符,保持它在LED上显示。btn_clk_rx交换LED的最高和最低有效位。最后,led_ctl模块的输出通过8个OBUF驱动封装引脚。 时钟频率CLOCK_RATE和波特率BAUD_RATE都是通用类参数,前者以Hz规定,后者以波特率规定。前者的数值用于在推敲和编译期间计算与时钟频率有关的各种设置。后者的数值用于在推敲和编译期间分频时钟频率产生被选的波特率。 uart_rx模块固化进入的串行信号防止亚稳态,传递此信号到uart_rx_ctl模块,同时进入的200MHz时钟信号被分频到16倍的波特率。uart_rx_ctl模块是一个状态机,空闲的IDLE状态等待进入信号的下降沿,在检测到这个沿之后,它开始计数8个baud_x16_en的事件,查看固化的串行输入网线。时间延迟采样应该接近串行位的中点。如果是低电平,认为是起始位,并继续进行收集数据位; 否则,认为串行线的变低数值是毛刺,返回IDLE状态。在串行线上采集数据并进入保持寄存器,当所有的数据被采集,在使能信号出现后或停止位的中央串行线再一次采样,一旦停止位被采集,则捕获的数据以并行的方式提供rx_data,rx_data_rdy持续一个时钟周期,表示新数据已经到达。在最后采样的事件中不是高电平,帧错误信号frm_err表示帧错误。 例368uart_led.v通信总线模块设计。 `timescale 1ns/1ps module uart_led ( //Write side inputs inputclk_pin_p,//来自引脚的时钟输入 input clk_pin_n, //差分对 input rst_pin, //来自引脚的高电平复位 input btn_pin, //高低位变换按钮 input rxd_pin, //直接来自引脚的RS232 RXD output [7:0] led_pins); //8 LED输出 parameter BAUD_RATE = 115_200; parameter CLOCK_RATE = 200_000_000; //BUFG的输出 wire clk_rx; wire rst_clk_rx; //同步复位 wire btn_clk_rx; //同步按钮 //Between uart_rx and led_ctl wire [7:0] rx_data; //uart_rx的数据输出 wire rx_data_rdy; //uart_rx的数据准备输出 clk_core clk_core_inst ( .clk_in1_p (clk_pin_p), .clk_in1_n (clk_pin_n), .clk_out1 (clk_rx)); meta_harden meta_harden_rst_i0 ( .clk_dst (clk_rx), .rst_dst (1`b0), //复位固化器上无复位 .signal_src (~rst_pin),//针对EG01板卡复位的极性添加,如果复位不需要反相,则为rst_pin .signal_dst (rst_clk_rx)); //输入按钮 meta_harden meta_harden_btn_i0 ( .clk_dst (clk_rx), .rst_dst (rst_clk_rx), .signal_src (btn_pin), .signal_dst (btn_clk_rx)); uart_rx #( .CLOCK_RATE (CLOCK_RATE), .BAUD_RATE (BAUD_RATE)) ) uart_rx_i0 ( .clk_rx (clk_rx), .rst_clk_rx (rst_clk_rx), .rxd_i (rxd_pin), .rxd_clk_rx (), .rx_data_rdy (rx_data_rdy), .rx_data (rx_data), .frm_err () ); led_ctl led_ctl_i0 ( .clk_rx (clk_rx), .rst_clk_rx (rst_clk_rx), .btn_clk_rx (btn_clk_rx), .rx_data (rx_data), .rx_data_rdy (rx_data_rdy), .led_o (led_pins)); endmodule 例369uart_rx.v异步通信接收模块设计。 这是UART接收机的顶层,将同步rxd_pin的准稳态固化器、产生适合x16位使能的波特率产生器和UART自身的控制器放在一起组成一个子模块。波特率产生器生成N选1的脉冲,N由波特率和系统时钟频率确定,此信号使能uart_rx_ctl模块中所有的触发器,对于波特率和系统频率的所有合理的组合,只要N>2,uart_rx_ctl模块中所有的路径都是多周期的。 `timescale 1ns/1ps module uart_rx ( //Write side inputs input clk_rx, //时钟输入 input rst_clk_rx, //高电平有效复位,与clk_rx同步 input rxd_i, //直接来自焊盘的RS232 RXD引脚 output rxd_clk_rx, //同步于clk_rx的RXD引脚 output [7:0] rx_data, //8位数据输出,在rx_datardy插入后有效 output rx_data_rdy, //rx_data的准备信号 output frm_err ); //未检测到STOP位 parameter BAUD_RATE=115_200; //波特率 parameter CLOCK_RATE=50_000_000; wire baud_x16_en; //对uart_rx_ctl触发器N选1使能 /* 将RXD引脚同步到clk_rx时钟域。因为RXD随采样时钟缓慢变化,一个简单的准稳态固化器就足够 */ meta_harden meta_harden_rxd_i0 ( .clk_dst (clk_rx), .rst_dst (rst_clk_rx), .signal_src (rxd_i), .signal_dst (rxd_clk_rx) ); uart_baud_gen # ( .BAUD_RATE (BAUD_RATE), .CLOCK_RATE (CLOCK_RATE) ) uart_baud_gen_rx_i0 ( .clk (clk_rx), .rst (rst_clk_rx), .baud_x16_en (baud_x16_en)); uart_rx_ctl uart_rx_ctl_i0 ( .clk_rx (clk_rx), .rst_clk_rx (rst_clk_rx), .baud_x16_en (baud_x16_en), .rxd_clk_rx (rxd_clk_rx), .rx_data_rdy (rx_data_rdy), .rx_data (rx_data), .frm_err (frm_err)); endmodule 例370uart_baud_gen.v波特率发生模块设计。 这个模块产生一个16x波特使能,当系统时钟频率和波特率的参数确定时,以16倍波特率产生这个信号。 //局部参数 //OVERSAMPLE_RATE: 过采样率——16 x BAUD_RATE //DIVIDER: 每个baud_x16_en的时钟数 //CNT_WIDTH: 计数器宽度 //说明: //1) 分频器必须至少为2 (因此CLOCK_RATE必须至少为32x BAUD_RATE) `timescale 1ns/1ps module uart_baud_gen ( //写端输入 inputclk,//时钟输入 input rst, //高电平有效复位,与clk同步 output baud_x16_en //过采样波特率使能 ); //************************************************************************* //常数函数 //************************************************************************* //产生基2对数的上限,即位数 //要求持有N个值,大小为clogb2(N)的向量将持有值为0~N-1 function integer clogb2; input [31:0] value; reg [31:0] my_value; begin my_value = value - 1; for (clogb2 = 0; my_value > 0; clogb2 = clogb2 + 1) my_value = my_value >> 1; end endfunction //参数定义 parameter BAUD_RATE=57_600; //波特率 parameter CLOCK_RATE=50_000_000; //过采样波特率是波特率的16倍 localparam OVERSAMPLE_RATE=BAUD_RATE * 16; //分频数是CLOCK_RATE / OVERSAMPLE_RATE的舍入 //所以在整数除法之前,加1/2的过采样率 localparam DIVIDER=(CLOCK_RATE+OVERSAMPLE_RATE/2) / OVERSAMPLE_RATE; //计数器重新加载数值为DIVIDER-1 localparam OVERSAMPLE_VALUE=DIVIDER - 1; //要求的计算器宽度为DIVIDER的基2对数的上限 localparam CNT_WID=clogb2(DIVIDER); reg [CNT_WID-1:0] internal_count; regbaud_x16_en_reg; wire [CNT_WID-1:0] internal_count_m_1; //减1计数 assign internal_count_m_1=internal_count-1`b1; //从DIVIDER-1到0计数, 当internal_count=0时设置baud_x16_en_reg //信号baud_x16_en_reg必须来自触发器(因为它是一个模块的输出) //当下一个计数为1(即internal_count_m_1为0)时,安排来设置它 always @(posedge clk) begin if (rst) begin internal_count <=OVERSAMPLE_VALUE; baud_x16_en_reg <=1`b0; end else begin baud_x16_en_reg<=(internal_count_m_1 == {CNT_WID{1`b0}}); //OVERSAMPLE_VALUE重复计数至0 if (internal_count=={CNT_WID{1`b0}}) begin internal_count<=OVERSAMPLE_VALUE; end else//internal_count不为0 begin internal_count<=internal_count_m_1; end end end assign baud_x16_en=baud_x16_en_reg; endmodule 例371led_ctl.v LED主控模块程序设计。 module led_ctl ( //写端输入 inputclk_rx,//时钟输入 input rst_clk_rx, //高电平有效复位,与clk_rx同步 input btn_clk_rx, //高低位引脚变换按钮 input [7:0] rx_data, //8位数据输出——当rx_data_rdy有效时 input rx_data_rdy, //rx_data的准备信号 output reg [7:0] led_o); // LED输出 reg old_rx_data_rdy; reg [7:0] char_data; always @(posedge clk_rx) begin if (rst_clk_rx) begin old_rx_data_rdy <=1`b0; char_data<=8`b0; led_o<=8`b0; end else begin //获取边沿检测时rx_data_rdy的值 old_rx_data_rdy <=rx_data_rdy; //如果rx_data_rdy为上升沿,捕获rx_data的值 if (rx_data_rdy && !old_rx_data_rdy) begin char_data <=rx_data; end //输出正常的数据以及高低位变换后的数据 if (btn_clk_rx) led_o <={char_data[3:0],char_data[7:4]}; else led_o <=char_data; end end endmodule 例372分析下列uart_rx_ctrl.v程序的状态机描述。 module uart_rx_ctl (//写输入 inputclk_rx, //输入时钟 inputrst_clk_rx, //高电平有效复位——同步于clk_rx inputbaud_x16_en,//16x 过采样使能 inputrxd_clk_rx, //RS232 RXD引脚——同步clk_rx之后 output reg [7:0] rx_data, //8位数据输出,当rx_data_rdyc插入时有效 output reg rx_data_rdy,//为rx_data的Ready信号 output reg frm_err );//STOP 位不被检测 localparam //State encoding for main FSM IDLE= 2`b00, START= 2`b01, DATA= 2`b10, STOP= 2`b11; reg [1:0]state; //主状态机 reg [3:0]over_sample_cnt; //过采样计数器——每位16 reg [2:0]bit_cnt; //We Rx的位计数器 wire over_sample_cnt_done; //处于一位的中间 wire bit_cnt_done; //这是最后一个数据位 always @(posedge clk_rx) //主状态机 begin if (rst_clk_rx) state <=IDLE; elsebegin if (baud_x16_en) begin case (state) IDLE: begin //检测到rxd_clk_rx变低,转换到START状态 if (!rxd_clk_rx) state <=START; end //IDLE state START: begin //在1/2位周期之后,重新确认START状态 if (over_sample_cnt_done) if (!rxd_clk_rx)//非毛刺的状态位 state <=DATA; else //毛刺被拒绝 state <=IDLE; end //START state DATA: begin //一旦最后一位已接收,对stop停止位检验 if (over_sample_cnt_done && bit_cnt_done) state <=STOP; end//DATA state STOP: begin//返回idle if (over_sample_cnt_done)//过采样计数完成 state <= IDLE; end endcase end end end //过采样计数器:在IDEL状态检测到起始条件rxd_clk_rx为0,预加到7,这时处于第一位中间 //在确认START状态,并在所有数据位之间,预加到15 always @(posedge clk_rx) begin if (rst_clk_rx) over_sample_cnt<=4`d0; elsebegin if (baud_x16_en)begin if (!over_sample_cnt_done) over_sample_cnt <=over_sample_cnt-1`b1; else if ((state==IDLE) && !rxd_clk_rx) over_sample_cnt <=4`d7; else if (((state==START) && !rxd_clk_rx) || (state == DATA)) over_sample_cnt <=4`d15; end end end assign over_sample_cnt_done=(over_sample_cnt==4`d0); //关于接收跟踪哪一位,当确认起始条件时,设置为0,在整个DATA状态增量 always @(posedge clk_rx) begin if (rst_clk_rx) bit_cnt<=3`b0; elsebegin if (baud_x16_en) begin if (over_sample_cnt_done)begin if (state==START) bit_cnt <=3`d0; else if (state==DATA) bit_cnt <=bit_cnt+1`b1; end end end end assign bit_cnt_done = (bit_cnt == 3`d7); //捕获数据,产生rdy信号,一旦捕获最后一位数据,rdy信号将被产生, //甚至STOP还没有确认,它就被插入,维持一个位周期(16个baud_x16_en 周期) always @(posedge clk_rx) begin if (rst_clk_rx) begin rx_data<=8`b0000_0000; rx_data_rdy <=1`b0; end else if (baud_x16_en && over_sample_cnt_done) if (state==DATA) begin rx_data[bit_cnt] <=rxd_clk_rx; rx_data_rdy <=(bit_cnt==3`d7); end else rx_data_rdy <=1`b0; end //帧错误产生,一旦帧位被假设采样,产生持续一个baud_x16_en 周期 always @(posedge clk_rx) begin if (rst_clk_rx) frm_err <=1`b0; else if (baud_x16_en) if ((state==STOP) && over_sample_cnt_done && !rxd_clk_rx) frm_err <=1`b1; else frm_err <=1`b0; end endmodule 请参考5.3.4节中双触发器技术,编写meta_harden.v。 最后还包括一个时钟IP模块clk_core,用于生成要求的时钟信号。 在第4章将会利用上述程序进行uart_led的设计,并应用仿真程序验证程序的功能是否正确,时序约束后,设计实现达到性能的要求,位流文件下载到硬件验证结果的正确。 3.6Testbench文件与设计 在采用HDL进行电路设计时,仿真是设计过程中不可或缺的环节。通过仿真能对设计的正确性进行验证,并及时发现问题和调整设计。针对较复杂的设计,要获得较好的工作效率,常用的方式是先由测试者编写测试平台(Testbench),再在仿真工具上运行来进行验证。仿真过程一般包括以下工作: (1) 产生仿真激励(波形); (2) 将仿真的输入激励加入被测试模块端口并观测其输出响应; (3) 将被测模块的输出与期望值进行比较,验证设计的正确性。 1. 测试平台的搭建 Verilog语言描述的模块中,其中功能模块可以完成特定的电路功能描述,如前面讨论的半加器、全加器模块; 测试模块描述变化的测试信号和监视输出信号,通过观察被测试模块的输出信号情况,对模块进行调试和验证。 测试平台的建立有两种模式,二者的不同之处在于模块的驱动设计。在设计中可以根据具体情况选择测试平台。 (1) 测试模块是顶层模块,它直接调用功能模块。这是一种较常用的测试平台,框图如图315所示。 图315测试平台框图1 例373用图315的测试平台对例31的半加器进行测试。 `timescale 1ns/100ps//指明1个时间单位是1ns,其精度是100ps module testbench; //测试模块 reg a,b; //激励信号名字可以和半加器模块一样,但数据类型不同 wire co,s; //实例化半加器,半加器各端口和testbench的端口相连 halfadder u1 ( .A(a),.B(b), .CO(co), .S(s)); //产生各种可能的输入信号组合进行测试 initial begin a = 0; b = 0; #10 begin a=1;b=1;end #10 begin a=1;b=0;end #10 begin a=0;b=1;end #10 begin a=0;b=0;end #10 $stop; //暂停仿真 end endmodule 对测试平台而言,输入端口a、b是寄存器型变量,如同信号发生器的输出端口,通过在初始化语句(initial)中对a、b进行赋值,驱动半加器的输入端口。输出端口co、s为线网型,所以名称相同的输入和输出类型与设计模块的类型正好相反。 (2) 将测试模块和设计模块分别设计完成,然后在一个虚拟的顶层模块中进行调用,将相应端口进行连接。 例374用图316的测试平台对例31描述的半加器进行测试。 图316测试平台框图2 第一步: 编写针对半加器的测试模块。 `timescale 1ns/100ps module testhalfadder(a,b,co,s); input co,s; //注意这里的模块输入输出信号方向与半加器模块刚好相反 output a,b; //激励信号 reg a; reg b; //输出 wire s; wire co; initial begin a = 0; b = 0; #10 begin a=1;b=1;end #10 begin a=1;b=0;end #10 begin a=0;b=1;end #10 begin a=0;b=0;end #10 $stop; end endmodule 第二步: 实例化半加器及测试模块,建立两个模块在同一个层次上的连接。 module testbench; //这个顶层模块没有输入、输出端口 //实例化测试模块 testhalfadder u1 (.co(co),.s(s),.a(a),.b(b)); //实例化半加器模块 halfadder u2 (.A(a),.B(b),.S(s),.CO(co)); endmodule 2. Testbench的时钟产生方法 测试文件中时钟波形的设计是最基本的设计,常利用initial、always、forever、assign等语句产生时钟信号。下面分别介绍产生周期性时钟和具有相移的时钟的方法。 (1) 要产生周期性的时钟方波,多采用always和initial结合的方式,其中initial进行初始赋值。 例375产生周期为20ns的时钟。 `timescale 1ns/100ps module Gen_clock1 (clock1); output clock1; reg clock1; parameter T=20; initial clock1=0; always # (T/2) clock1=~clock1; endmodule 波形如图317所示。 图317周期性的时钟 利用forever同样可以产生图317所示的周期为20ns的时钟。 例376 `timescale 1ns/100ps … Initial begin clock=0; forever #10 clock2=~clock2; end endmodule (2) 采用always语句产生高低电平持续时间不同的时钟。 例377 module Gen_clock3 (clock3); output clock3; reg clock3; always begin # 4 clock3=0; //延时4个单位时间后,clock3赋值0 # 6 clock3=1; //延时4个单位时间后,clock3赋值1 end endmodule 波形如图318所示,高电平持续时间4,低电平持续时间6,初始值为不确定x。 图318高低电平持续时间不同的时钟 例378利用forever也能产生如图318的时钟波形。 … Forever begin # 4 clock4=1; # 6 clock4=0; end end endmodule (3) 利用前面的时钟产生模块,再通过添加连续赋值语句assign语句,就可以得到具有相移的时钟。 例379 module Gen_clock1 (clock_pshift,clock1); output clock_pshift,clock1; reg clock1; wire clock_pshift; parameter T=20; parameter pshift=2; initial clock1=0; always # (T/2) clock1=~clock1; assign #PSHIFT clock_pshift=clock1; endmodule 波形如图319所示,clock1是周期为20的时钟,clock_pshift是由clock1相移而来的。 图319相移时钟 3. Testbench的描述方法 在测试平台上,除了时钟,还有多个输入信号值需要描述。随着数字电路系统的复杂性增加,测试时间可能占到设计总时间的70%,测试的“完备性”对于降低设计的功能模块在使用中的风险起到了很大作用。下面介绍几种Testbench的描述方法。 (1) 输入信号取值数据量较少时可以使用initial语句对输入信号的变化进行穷举式的描述。 `timescale 1ns/100ps module testbench(); //定义输入、输出端口 reg 输入激励端口罗列; wire 输出端口罗列; … //例化被测功能模块 … //输入端加入激励信号 initial begin #延迟时间 begin 输入激励信号赋值; end #延迟时间 begin 输入激励信号赋值; end … end endmodule 例380 `timescale 1ns / 100ps//定义时间单位、时间精度 … Initial begin enable=0; #4enable=1; //延时4个时间单元后,enable赋值为1 #10enable=0; //延时10个时间单元后,enable赋值为0 #5enable=1; //延时5个时间单元后,enable赋值为1 end 产生的波形如图320所示。 图320特定值的序列波形 (2) 激励数据量较多时,可以通过编写、调用任务和函数完成重复性操作,减少程序书写工作量。 例381 `timescale 1ns / 100ps module testbench; … //定义任务,以备重复调用,驱动被测试模块的输入端口 //进行一个字节数据的写入 task writeburst; input [7:0] wdata; begin @(posedge clockin) begin write_enable=#2 1; write_data=#2 wdata; end end endtask //可以调用低层任务,构筑更复杂的任务操作 //进行多个字节(本例中为128字节)的数据写入 task writeburst128; begin writeburst(128); writeburst(129); writeburst(130); writeburst(131); writeburst(132); writeburst(133); writeburst(134); writeburst(135); … end … //调用任务,进行较多测试数据的输入 Initial begin writeburst128; … end endmodule (3) 如果输入的激励信号是视频码流等难以用手工进行输入的数据,就可以采用将需要输入的测试数据作为一个数据文件放于某文件目录下,调用系统函数读入数据,完成测试激励的输入,也可将得到的大量结果数据写到指定的文件中,以备后续分析。 例382 `timescale 1ns / 100ps module testbench; integer i; //例化被测功能模块 example u1(data_in,…); //定义一个寄存器组 reg [width1-1:0] my_memory[width2-1:0]; //将数据文件中的值读入某寄存器中 initial begin $readmemb("mydata .dat", my_memory); … end //使用数组中的数据作为输入激励 always @ (posedge clk) begin data_in <=my_memory[i] i=<i+1; … end initial begin //打开一个文件,准备接收仿真的输出数据 file_descriptor= $fopen("simulus.dat"); … $fwrite(file_descriptor , "%b\n",result);//将仿真数据写入输出文件 … $fclose(file_descriptor); end 本章小结 本章对Verilog语言的基本结构、基本语法进行了分类阐述,并采用Verilog语言进行电路设计举例,同时对测试文件的理解和设计进行了介绍。语言的学习应该在设计实践中去不断提高、体会。有以下学习建议: (1) 明确设计目标后才开始编程。 任务的分析、分解是整个设计工作的核心和基础。在未充分理解设计目标、未完成任务的分析、分解时,就忙于编写代码,往往会做无用功,反而耽误开发进度,“磨刀不误砍柴工”。 (2) 用硬件电路系统的思想来编写HDL。 首先要充分理解HDL语句和硬件电路的关系。HDL就是在描述一个电路,每完成一段程序,就应当对生成的电路有一些大体上的了解; 必须理解硬件“同时工作”的含义,在程序中描述的功能块往往是同时工作的,通常不会因为书写的顺序来决定其工作顺序(这和C语言是不同的)。 (3) 理解HDL的可综合性。 HDL程序如果只用于仿真,那么几乎所有的HDL语句、函数、编程方法都可以使用。如果需要将文本描述转化为硬件实现,则必须保证程序“可综合”(即文本可以被综合工具转化成硬件电路)。不可综合的HDL语句在综合时将被忽略或者报错。“所有的HDL描述都可以用于仿真,但不是所有的HDL描述都能用硬件实现”。 (4) 语法掌握贵在精,不在多。 30%的基本HDL语句就可以完成95%以上的电路设计,很多生僻的语句容易产生兼容性问题,也不利于其他人阅读和修改。学习中不需要花太多时间学全部的语句,而是着重理解常用的基本语句语法及其对应的硬件电路特点。本章只介绍了用Verilog HDL设计常用的语法,其他语法,读者可根据需要参考相关的手册。 习题 3.1主要的HDL语言是哪两种? 3.2Verilog HDL语言的特点是什么? 3.3定义以下Verilog变量: (1) 一个名为data_in的8位向量线网; (2) 一个名称为MEM1的存储器,含有128个数据,每个数据位宽为8位; (3) 一个名为data_out的16位寄存器,其第15位为最低位。 3.4设A=4`b1010,B=4`b0011,C=1`b1,则下式运算结果是什么? (1) &A (2) ^A (3) A>>1 (4) {A,B[0],C} (5) A & B (6) A ^B (7) A<B 3.5设计一个时钟,要求: (1) 可以对小时、分、秒进行计数; (2) 可以显示当前时间; (3) 可以校对当前时间; (4) 可以设置闹钟。 用“自顶向下”的设计思路分析系统,画出系统的模块组成情况(不必用语句进行具体设计)。 3.6有一个模块名为my_module,其输入、输出端口情况如题3.6图所示,试写出模块的定义、端口列表和端口定义(不必写出模块的内部语句)。 题3.6图 3.7在下面的initial块中,每条语句在什么时刻开始执行?A、B、C、D在仿真过程中和仿真结束时的值是什么? initial begin A=1`b0; B=1`b1; C=2`b10; D=4`b1100; #10 begin A=1`b1;B=1`b0;end #15 begin C= #5 2`b01;end #10 begin D=#7 {A,B,C}; end end 3.8定义一个长度为256、位宽为2的寄存器型数组,用for语句对该数组进行初始化,要求把所有的偶元素初始化为0,所有的奇元素初始化为1。 3.9如果将例362中的非阻塞赋值改为阻塞赋值,程序实现的功能是否会发生改变?为什么? 3.10设计一个移位函数,输入是一个32位的数data和一个左移、右移的控制信号shift_ctrl,其输出是一个32位的数。 3.11设计一个连加函数,输入的是起始数值和终止数值,输入和输出数据的位宽可由参数设定。 3.12定义一个任务,该任务能计算出一个8位变量的偶校验位作为该任务的输出,计算结束后,经过三个时钟周期将该校验位赋给任务的输出。 3.13设计一个周期为40个时间单位的时钟信号,其占空比为20%,使用always、initial块进行设计,设初始时刻时钟信号为0。 3.14为什么应该尽量避免按照例365的方法用一个always模块来描述Moore型状态机? 3.15设计例364中交通灯控制器的数据通道部分,并结合主控制模块的程序完成整个系统设计。 3.16用case、ifelse、assign语句分别设计四选一多路选择器,比较各种实现方式的特点。设本选择器的被选数据输入为A、B、C、D,采用参数法定义这四个数据的位数,选择信号设为sel,输出信号设为data_sel,其关系如下表。 选择信号sel[1:0]输出信号data_sel[width-1:0] 2`b00 data_sel[width-1:0]=A[width-1:0] 2`b01 data_sel[width-1:0]=B[width-1:0] 2`b10 data_sel[width-1:0]=C[width-1:0] 2`b11 data_sel[width-1:0]=D[width-1:0] 编写激励模块,对四选一选择器模块进行测试。