第3章 Verilog HDL 语法 视频讲解 3.1Verilog简介 在FPGA开发中使用最多的语言主要有两种: VHDL和Verilog。VHDL语法格式严谨,入门比较难。VHDL最初由美国国防部创建。Verilog对软件爱好者来说有种似曾相识的感觉,不易被爱好者排斥,初看起来还以为是C语言编程,其实它起源于C语言,语法格式灵活,容易掌握,因此被大多数FPGA工程师所喜爱。本章只对Verilog语法内容进行介绍。 3.2数据类型 3.2.1常量 (1) 整数: 整数可以用二进制b或B、八进制o或O、十进制d或D、十六进h或H表示,例如, 8’b00001111表示8位位宽的二进制整数,4’ha表示4位位宽的十六进制整数。 (2) x和z: x代表不定值,z代表高阻值,例如,5’b00x11,第三位为不定值,3’b00z表示最低位为高阻值。 (3) 下画线: 在位数过长时可以用来分割位数,提高程序可读性,如8’b0000_1111。 (4) 参数parameter: parameter可以用标识符定义常量,运用时只使用标识符即可,以提高程序可读性及维护性,如parameter width=8,寄存器reg [width-1:0] a,即定义了8位宽度的寄存器。 (5) 参数的传递: 在一个模块中如果有定义参数,那么在其他模块调用此模块时可以传递参数,并可以修改参数,如下面程序所示,在module后用#()表示。 例如,定义如下调用模块: module rom module top(); #(wire[31:0] addr ; parameter depth =15,wire[15:0] data ; parameter width =8wire result ; )(rom input[depth-1:0] addr ,#( input[width-1:0] data ,.depth(32), output result .width(16) ); ) r1 endmodule( .addr(addr), .data(data), .result(result) ); endmodule parameter可以用于模块间的参数传递,而localparam仅用于本模块内使用,不能用于参数传递,而localparam多用于状态机状态的定义。 3.2.2变量 变量是指程序运行时可以改变其值的量。下面主要介绍几种常用的变量。 1. wire类型 wire 类型变量也叫网络类型变量,用于结构实体之间的物理连接,如门与门之间,不能存储值,用连续赋值语句assign赋值,定义为wire [n-1:0] a,其中n代表位宽,如定义“wire a,assign a=b”,是将b节点连接到连线a上。如图31所示,两个实体之间的连线即是wire类型变量。 2. reg类型 reg 类型变量也称为寄存器变量,可用来存储值,必须在always语句里使用。其定义为reg [n-1:0] a ,表示n位位宽的寄存器,如reg [7:0] a表示定义8位位宽的寄存器a。如下所示定义了寄存器q,生成的电路为时序逻辑,如图32所示为D触发器。 图31wire连线 图32D触发器 module top(d, clk, q); input d ; input clk ; outputreg q ; always@(posedge clk) begin q <= d ; end endmodule 也可以生成组合逻辑,如数据选择器,敏感信号没有时钟,定义了reg Mux,最终生成电路为组合逻辑,如图33所示。 图33数据选择器 module top(a, b, c, d, sel, Mux); inputa ; inputb ; inputc ; inputd ; input[1:0] sel ; outputreg Mux ; always@(sel or a or b or c or d) begin case(sel) 2'b00: Mux = a ; 2'b01: Mux = b ; 2'b10: Mux = c ; 2'b11: Mux = d ; endcase end endmodule 3. memory类型 可以用memory类型的变量来定义RAM和ROM等存储器,其结构为reg [n-1:0] 存储器名[m-1:0],含义为m个n位宽度的寄存器。例如,reg [7:0] ram [255:0]表示定义了256个8位寄存器,256是存储器的深度,8为数据宽度。 视频讲解 3.3运算符 3.3.1算术运算符 算术运算符包括“+”(加法运算符)、“-”(减法运算符)、“*”(乘法运算符)、“/”(除法运算符,如7/3 =2),“%”(取模运算符,也即求余数,如7%3=1,余数为1)。 3.3.2赋值运算符 赋值运算符分两种:“=”阻塞赋值和“<=”非阻塞赋值。阻塞赋值为执行完一条赋值语句,再执行下一条,可理解为顺序执行,而且赋值是立即执行; 非阻塞赋值可理解为并行执行,不考虑顺序,在always块语句执行完成后,变量才进行赋值。如下面的阻塞赋值: 激励文件代码如下: module top(din,a,b,c,clk); `timescale 1ns/1ns input din; module top_tb(); input clk; reg din ; outputreg a,b,c; reg clk ; always@(posedge clk)wire a,b,c ; begininitial a = din; begin b = a; din =0; c = b; clk =0; endforever endmodulebegin #({$random}%100) din =~din ; end end always#10 clk =~clk ; top t0(.din(din),.a(a),.b(b),.c(c),.clk(clk)); endmodule 仿真结果如图34所示,在clk的上升沿,a的值等于din,并立即赋给b,b的值赋给c。 图34阻塞赋值仿真波形 如果改为非阻塞赋值,则仿真结果如图35所示,在clk上升沿,a的值没有立即赋值给b,b为a原来的值,同样,c为b原来的值。 图35非阻塞赋值仿真波形 可以从两者的RTL视图看出明显不同,如图36和图37所示。 图36阻塞赋值RTL图 图37非阻塞赋值RTL图 注意: 一般情况下,在时序逻辑电路中使用非阻塞赋值,可避免仿真时出现竞争冒险现象; 在组合逻辑中使用阻塞赋值,执行赋值语句后立即改变; 在assign语句中必须用阻塞赋值。 3.3.3关系运算符 关系运算符用于表示两个操作数之间的关系,如a>b、a<b,多用于判断条件,例如: If (a>=b) q <=1'b1 ; else q <= 1'b0 ; 表示如果a的值大于或等于b的值,则q的值为1,否则q的值为0。 3.3.4逻辑运算符 逻辑运算符包括“&&”(两个操作数逻辑与)、“‖”(两个操作数逻辑或)和“!”(单个操作数逻辑非)。例如: if (a>b && c <d) 表示条件为a>b并且c<d,if (!a)表示条件为a的值不为1,也就是0。 3.3.5条件运算符 “?:”为条件判断,类似于if else,例如assign a = (i>8)?1’b1:1’b0 ,判断i的值是否大于8,如果大于8则a的值为1,否则为0。 3.3.6位运算符 位运算符包括“~”(按位取反)、“|”(按位或)、“^”(按位异或)、“&”(按位与)、“^~”(按位同或)。除了“~”只需要一个操作数外,其他几个都需要两个操作数,如a&b、a|b。具体应用3.4节中有讲解。 3.3.7移位运算符 图38优先级 移位运算符包括“<<”左移位运算符和“>>”右移位运算符,如a<<1表示向左移1位,a>>2表示向右移2位。 3.3.8拼接运算符 “{ }”为拼接运算符,用于将多个信号按位拼接起来,如{a[3:0],b[1:0]},将a的低4位和b的低2位拼接成6位数据。另外,{n{a[3:0]}}表示将n个a[3:0]拼接,{n{1’b0}}表示n位的0拼接,{8{1’b0}}表示8’b0000_0000。 3.3.9优先级 各种运算符的优先级如图38所示。 视频讲解 3.4组合逻辑 本节主要介绍组合逻辑,组合逻辑电路的特点是任意时刻的输出仅仅取决于输入信号,输入信号变化,输出立即变化,不依赖于时钟。 3.4.1与门 在Verilog中以“&”表示按位与,如c=a&b,表示在a和b都等于1时结果才为1,其真值表和RTL视图如图39所示。 图39与门真值表及RTL视图 代码实现和激励文件如下: module top(a, b, c); `timescale 1ns/1ns input a ; module top_tb(); input b ; reg a ; output c ; reg b ; wire c ; assign c = a & b ; initial endmodulebegin a =0; b =0; forever begin #({$random}%100) a =~a ; #({$random}%100) b =~b ; end end top t0(.a(a),.b(b),.c(c)); endmodule 仿真结果如图310所示。 图310与门仿真波形 如果a和b的位宽大于1,例如有定义“input [3:0] a,input [3:0]b”,那么a&b指a与b的对应位相与。如a[0]&b[0]、a[1]&b[1]。 3.4.2或门 在Verilog中以“|”表示按位或,如c = a|b ,表示在a和b都为0时结果才为0,其真值表和RTL视图如图311所示。 图311或门真值表与RTL视图 代码实现和激励文件如下: module top(a, b, c); `timescale 1ns/1ns input a ; module top_tb(); input b ; reg a ; output c ; reg b ; wire c ; assign c = a | b ; endmoduleinitial begin a =0; b =0; forever begin #({$random}%100) a =~a ; #({$random}%100) b =~b ; end end top t0(.a(a),.b(b),.c(c)); endmodule 仿真结果如图312所示。 图312或门仿真波形 3.4.3非门 在Verilog中以“~”表示按位取反,如b=~a,表示b等于a的相反数,其真值表和RTL视图如图313所示。 图313非门真值表及RTL视图 代码实现和激励文件如下: module top(a, b); `timescale 1ns/1ns inputa ; module top_tb(); output b ; reg a ; wire b ; assign b =~a ; endmoduleinitial begin a =0; forever begin #({$random}%100) a =~a ; end end top t0(.a(a),.b(b)); endmodule 仿真结果如图314所示。 图314非门仿真波形 3.4.4异或 在Verilog中以“^”表示异或,如c= a^b ,表示当a和b相同时,输出为0,其真值表和RTL视图如图315所示。 图315异或真值表及RTL视图 代码实现和激励文件如下: module top(a, b, c); `timescale 1ns/1ns input a ; module top_tb(); input b ; reg a ; output c ; reg b ; wire c ; assign c = a ^ b ; initial endmodulebegin a =0; b =0; forever begin #({$random}%100) a =~a ; #({$random}%100) b =~b ; end end top t0(.a(a),.b(b),.c(c)); endmodule 仿真结果如图316所示。 图316异或仿真波形 3.4.5比较器 在Verilog中,比较器以大于“>”、等于“==”、小于“<”、大于或等于“>=”、小于或等于“<=”和不等于“!=”表示。以大于举例,如c=a > b ,表示如果a大于b,那么c的值就为1,否则为0。其真值表和RTL视图如图317所示。 图317比较器真值表及RTL视图 代码实现和激励文件如下: module top(a, b, c); `timescale 1ns/1ns input a ; module top_tb(); input b ; reg a ; output c ; reg b ; wire c ; assign c = a > b ; initial endmodulebegin a =0; b =0; forever begin #({$random}%100) a =~a ; #({$random}%100) b =~b ; end end top t0(.a(a),.b(b),.c(c)); endmodule 仿真结果如图318所示。 图318比较器仿真波形 3.4.6半加器 半加器和全加器是算术运算电路中的基本单元,由于半加器不考虑从低位来的进位,所以称之为半加器,sum表示相加结果,count表示进位。其真值表和RTL视图如图319所示。 图319半加器真值表及RTL视图 代码实现和激励文件如下: module top(a, b, sum, count); `timescale 1ns/1ns input a ; module top_tb(); input b ; reg a ; output sum ; reg b ; output count ; wire sum ; wire count ; assign sum = a ^ b ; assign count = a & b ; initial begin endmodulea =0; b =0; forever begin #({$random}%100) a =~a ; #({$random}%100) b =~b ; end end top t0(.a(a),.b(b), .sum(sum),.count(count)); endmodule 仿真结果如图320所示。 图320半加器仿真波形 3.4.7全加器 全加器需要加上低位来的进位信号cin,真值表和RTL视图如图321所示。 图321全加器真值表及RTL视图 代码实现和激励文件如下: module top(cin, a, b, sum, count); `timescale 1ns/1ns input cin ; module top_tb(); input a ; reg a ; input b ; reg b ; output sum ; reg cin ; output count ; wire sum ; wire count ; assign{count,sum}= a + b + cin ; initial begin endmodulea =0; b =0; cin =0; forever begin #({$random}%100) a =~a ; #({$random}%100) b =~b ; #({$random}%100) cin =~cin ; end end top t0(.cin(cin),.a(a),.b(b), .sum(sum),.count(count)); endmodule 仿真结果如图322所示。 图322全加器仿真波形 3.4.8乘法器 乘法的表示也很简单,利用“*”即可,如a*b,乘法器的RTL视图如图323所示。 图323乘法器RTL视图 代码示例如下: module top(a, b, c); `timescale 1ns/1ns input[1:0] a ; module top_tb(); input[1:0] b ; reg[1:0]a ; output[3:0] c ; reg[1:0]b ; wire[3:0]c ; assign c = a * b ; endmoduleinitial begin a =0; b =0; forever begin #({$random}%100) a =~a ; #({$random}%100) b =~b ; end end top t0(.a(a),.b(b),.c(c)); endmodule 仿真结果如图324所示。 图324乘法器仿真波形 3.4.9数据选择器 在Verilog中经常会用到数据选择器,通过选择信号,选择不同的输入信号输出到输出端,四选一数据选择器,sel[1:0]为选择信号,a、b、c、d为输入信号,Mux为输出信号。其真值表如表31所示,RTL视图如图325所示。 表31数据选择器真值表 选 择 信 号输 入 信 号输 出 信 号 sel[0] sel[1] a b c d Mux 0 0 x x x x a 0 1 x x x x b 1 0 x x x x c 1 1 x x x x d 图325数据选择器RTL视图 代码实现和激励文件如下: module top(a, b, c, d, sel, Mux); `timescale 1ns/1ns inputa ; module top_tb(); inputb ; reg a ; inputc ; reg b ; inputd ; reg c ; reg d ; input[1:0] sel ; reg[1:0] sel ; wire Mux ; outputreg Mux ; initial begin always@(sel or a or b or c or d)a =0; beginb =0; case(sel)c =0; 2'b00: Mux = a ; d =0; 2'b01: Mux = b ; forever 2'b10: Mux = c ; begin 2'b11: Mux = d ; #({$random}%100) endcasea ={$random}%3; end#({$random}%100) b ={$random}%3; endmodule#({$random}%100) c ={$random}%3; #({$random}%100) d ={$random}%3; end end initial begin sel =2'b00; #2000 sel =2'b01; #2000 sel =2'b10; #2000 sel =2'b11; end top t0(.a(a),.b(b),.c(c),.d(d),.sel(sel), .Mux(Mux)); endmodule 仿真结果如图326所示。 图326数据选择器仿真波形 3.4.1038译码器 38译码器是一个很常用的器件,其真值表如表32所示,根据A2、A1和A0的值,得出不同的结果。其RTL视图如图327所示。 表3238译码器真值表 输入 输出 A2 A1 A0 Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7 0 0 0 0 1 1 1 1 1 1 1 0 0 1 1 0 1 1 1 1 1 1 0 1 0 1 1 0 1 1 1 1 1 0 1 1 1 1 1 0 1 1 1 1 1 0 0 1 1 1 1 0 1 1 1 1 0 1 1 1 1 1 1 0 1 1 1 1 0 1 1 1 1 1 1 0 1 1 1 1 1 1 1 1 1 1 1 0 图32738译码器RTL视图 代码实现和激励文件如下: module top(addr, decoder); `timescale 1ns/1ns input[2:0] addr ; module top_tb(); outputreg[7:0] decoder ; reg[2:0] addr ; wire[7:0] decoder ; always@(addr) begininitial case(addr)begin 3'b000: decoder =8'b1111_1110; addr =3'b000; 3'b001: decoder =8'b1111_1101; #2000 addr =3'b001; 3'b010: decoder =8'b1111_1011; #2000 addr =3'b010; 3'b011: decoder =8'b1111_0111; #2000 addr =3'b011; 3'b100: decoder =8'b1110_1111; #2000 addr =3'b100; 3'b101: decoder =8'b1101_1111; #2000 addr =3'b101; 3'b110: decoder =8'b1011_1111; #2000 addr =3'b110; 3'b111: decoder =8'b0111_1111; #2000 addr =3'b111; endcaseend endtop t0(.addr(addr),.decoder(decoder)); endmoduleendmodule 仿真结果如图328所示。 图32838译码器仿真结果 3.4.11三态门 在Verilog中,经常会用到双向I/O,需要用到三态输出电路,如bio = en? din: 1’bz;,其中en为使能信号,用于打开关闭三态输出电路,双向I/O电路RTL视图如图329所示,如下程序介绍了怎样实现两个双向I/O的对接。 图329三态输出电路 RTL视图 module top(en, din, dout, bio); `timescale 1ns/1ns input din ; module top_tb(); input en ; reg en0 ; output dout ; reg din0 ; inout bio ; wire dout0 ; reg en1 ; assign bio = en? din :1'bz; reg din1 ; assign dout = bio ; wire dout1 ; wire bio ; endmoduleinitial begin din0 =0; din1 =0; forever begin #({$random}%100) din0 =~din0 ; #({$random}%100) din1 =~din1 ; end end initial begin en0 =0; en1 =1; #100000 en0 =1; en1 =0; end top t0(.en(en0),.din(din0),.dout(dout0),.bi o(bio)); top t1(.en(en1),.din(din1),.dout(dout1),.bi o(bio)); endmodule 仿真结果如图330所示,当en0为0、en1为1时,1通道打开,双向I/O bio就等于1通道的din1,1通道向外发送数据,0通道接收数据,dout0等于bio; 当en0为1、en1为0时,0通道打开,双向I/O bio就等于0通道的din0,0通道向外发送数据,1通道接收数据,dout1等于bio。 图330三态门仿真波形 3.5时序逻辑 组合逻辑电路在逻辑功能上的特点是任意时刻的输出仅仅取决于当前时刻的输入,与电路原来的状态无关。而时序逻辑电路在逻辑功能上的特点是任意时刻的输出不仅取决于当前的输入信号,还取决于电路原来的状态。下面以一个典型的时序逻辑电路为例进行分析。 3.5.1D触发器 D触发器在时钟的上升沿或下降沿存储数据,输出与时钟跳变之前输入信号的状态相同。 代码实现和激励文件如下: module top(d, clk, q); `timescale 1ns/1ns input d ; module top_tb(); input clk ; reg d ; outputreg q ; reg clk ; always@(posedge clk)wire q ; begininitial q <= d ; begin endd =0; endmoduleclk =0; forever begin #({$random}%100) d =~d ; end end always#10 clk =~clk ; top t0(.d(d),.clk(clk),.q(q)); endmodule D触发器的RTL视图如图331所示。 图331D触发器的RTL视图 仿真结果如图332所示,可以看到在t0时刻时,d的值为0,则q的值也为0; 在t1时刻d发生了变化,值为1,那么q相应也发生了变化,值变为1。可以看到,在t0~t1的一个时钟周期内,无论输入信号d的值如何变化,q的值是保持不变的,也就是有存储的功能,保存的值为在时钟的跳变沿时d的值。 图332D触发器仿真波形 3.5.2两级D触发器 软件是按照两级D触发器的模型进行时序分析的,具体可以分析在同一时刻两个D触发器输出的数据有何不同,其RTL视图如图333所示。 图333两级D触发器的RTL视图 代码实现和激励文件如下: module top(d, clk, q, q1); `timescale 1ns/1ns input d ; module top_tb(); input clk ; reg d ; outputreg q ; reg clk ; outputreg q1 ; wire q ; always@(posedge clk)wire q1 ; begininitial q <= d ; begin endd =0; always@(posedge clk)clk =0; beginforever q1 <= q ; begin end#({$random}%100) endmoduled =~d ; end end always#10 clk =~clk ; top t0(.d(d),.clk(clk),.q(q),.q1(q1)); endmodule 仿真结果如图334所示,可以看到,在t0时刻,d为0,q输出为0; 在t1时刻,q随着d的数据变化而变化,而此时钟跳变之前q的值仍为0,那么q1的值仍为0; 在t2时刻,时钟跳变前q的值为1,则q1的值相应为1,q1相对于q落后一个周期。 图334两级D触发器仿真波形 3.5.3带异步复位D触发器 异步复位是指独立于时钟,一旦异步复位信号有效,就触发复位操作。这个功能在写代码时会经常用到,用于给信号复位和初始化,其RTL视图如图335所示。 图335带异步复位D触发器的RTL视图 代码如下,注意要把异步复位信号放在敏感列表里。如果是低电平复位,即为negedge; 如果是高电平复位,则是posedge。 module top(d, rst, clk, q); `timescale 1ns/1ns input d ; module top_tb(); input rst ; reg d ; input clk ; reg rst ; outputreg q ; reg clk ; always@(posedge clk or negedge rst)wire q ; begin if(rst ==1'b0)initial q <=0; begin elsed =0; q <= d ; clk =0; endforever endmodulebegin #({$random}%100) d =~d ; end end initial begin rst =0; #200 rst =1; end always#10 clk =~clk ; top t0(.d(d),.rst(rst),.clk(clk),.q(q)); endmodule 仿真结果如图336所示,可以看到,在复位信号之前,虽然输入信号d数据有变化,但由于正处于复位状态,输入信号q始终为0,在复位之后q的值就正常了。 图336带异步复位D触发器仿真波形 3.5.4带异步复位同步清零D触发器 前面讲到异步复位独立于时钟操作,而同步清零则是在同步时钟信号下操作的,当然也不仅限于同步清零,也可以是其他的同步操作,其RTL视图如图337所示。 图337带异步复位同步清零D触发器的RTL视图 不同于异步复位,同步操作不能把信号放到敏感列表里。代码如下: module top(d, rst, clr, clk, q); `timescale 1ns/1ns input d ; module top_tb(); input rst ; reg d ; input clr ; reg rst ; input clk ; reg clr ; outputreg q ; reg clk ; always@(posedge clk or negedge rst)wire q ; begininitial if(rst ==1'b0)begin q <=0; d =0; else if(clr ==1'b1)clk =0; q <=0; forever elsebegin q <= d ; #({$random}%100) endd =~d ; endmoduleend end initial begin rst =0; clr =0; #200 rst =1; #200 clr =1; #100 clr =0; end always#10 clk =~clk ; top t0(.d(d),.rst(rst),.clr(clr),.clk(clk), .q(q)); endmodule 仿真结果如图338所示,可以看到clr信号拉高后,q没有立即清零,而是在下一个clk上升沿之后执行清零操作,也就是clr同步于clk。 图338带异步复位同步清零D触发器仿真波形 3.5.5移位寄存器 移位寄存器是指在每个时钟脉冲到来时,向左或向右移动一位,移位寄存器结构如图339所示,由于D触发器的特性,数据输出同步于时钟边沿,每个时钟来临,每个D触发器的输出Q等于前一个D触发器输出的值,从而实现移位的功能。 图339移位寄存器结构 代码实现和激励文件如下: module top(d, rst, clk, q); `timescale 1ns/1ns input d ; module top_tb(); input rst ; reg d ; input clk ; reg rst ; outputreg[7:0] q ; reg clk ; always@(posedge clk or negedge rst)wire[7:0] q ; begin if(rst ==1'b0)initial q <=0; begin elsed =0; q <={q[6:0], d}; //向左移位clk =0; //q <= {d, q[7:1]} ; //向右移位forever endbegin endmodule#({$random}%100) d =~d ; end end initial begin rst =0; #200 rst =1; end always#10 clk =~clk ; top t0(.d(d),.rst(rst),.clk(clk),.q(q)); endmodule 仿真结果如图340所示。可以看到,复位之后,数据随clk上升沿左移一位。 图340移位寄存器仿真波形 3.5.6单口RAM 单口RAM的写地址与读地址共用一个地址,代码如下,其中reg [7:0] ram [63:0]定义了64个8位宽度的数据,addr_reg可以将读地址延迟一周期之后将数据送出。 module top `timescale 1ns/1ns (module top_tb(); input[7:0] data,reg[7:0] data ; input[5:0] addr,reg[5:0] addr ; input wr,reg wr ; input clk,reg clk ; output[7:0] q wire[7:0] q ; ); initial begin reg[7:0] ram[63:0]; data =0; reg[5:0] addr_reg; //地址寄存器addr =0; wr =1; always@(posedge clk)clk =0; beginend if(wr)//写使能always#10 clk =~clk ; ram[addr]<= data; always@(posedge clk) begin addr_reg <= addr; data <= data +1'b1; endaddr <= addr +1'b1; assign q = ram[addr_reg];//读数据end endmoduletop t0(.data(data), .addr(addr), .clk(clk), .wr(wr), .q(q)); endmodule 仿真结果如图341所示。可以看到,q的输出与写入的数据一致。 图341单口RAM仿真波形 3.5.7伪双口RAM 伪双口RAM的读写地址是独立的,可以随机选择写或读地址,同时进行读写操作,代码如下: module top `timescale 1ns/1ns (module top_tb(); input[7:0] data,reg[7:0] data ; input[5:0] write_addr,reg[5:0] write_addr ; input[5:0] read_addr,reg[5:0] read_addr ; input wr,reg wr ; input rd,reg clk ; input clk,reg rd ; outputreg[7:0] q wire[7:0] q ; ); initial begin reg[7:0] ram[63:0]; data =0; reg[5:0] addr_reg;//地址寄存器write_addr =0; read_addr =0; always@(posedge clk)wr =0; beginrd =0; if(wr)//写使能clk =0; ram[write_addr]<= data; #100 wr =1; if(rd)//读使能#20 rd =1; q <= ram[read_addr]; end endalways#10 clk =~clk ; always@(posedge clk) endmodulebegin if(wr) begin data <= data +1'b1; write_addr <= write_addr +1'b1; if(rd) read_addr <= read_addr +1'b1; end end top t0(.data(data), .write_addr(write_addr), .read_addr(read_addr), .clk(clk), .wr(wr), .rd(rd), .q(q)); endmodule 仿真结果如图342所示。可以看到,在rd有效时,对读地址进行操作,读出数据。 图342伪双口RAM仿真波形 3.5.8真双口RAM 真双口RAM有两套控制线和数据线,允许两个系统对其进行读写操作,代码如下: module top `timescale 1ns/1ns (module top_tb(); input[7:0] data_a, data_b,reg[7:0] data_a, data_b ; input[5:0] addr_a, addr_b,reg[5:0] addr_a, addr_b ; input wr_a, wr_b,reg wr_a, wr_b ; input rd_a, rd_b,reg rd_a, rd_b ; input clk,reg clk ; outputreg[7:0] q_a, q_b wire[7:0] q_a, q_b ; ); initial begin reg[7:0] ram[63:0];//声明 ram data_a =0; data_b =0; //端口 A addr_a =0; always@(posedge clk)addr_b =0; beginwr_a =0; if(wr_a)//写wr_b =0; beginrd_a =0; ram[addr_a]<= data_a; rd_b =0; q_a <= data_a ; clk =0; end#100 wr_a =1; if(rd_a)#100 rd_b =1; //读end q_a <= ram[addr_a]; always#10 clk =~clk ; endalways@(posedge clk) begin if(wr_a) //端口 B begin always@(posedge clk)data_a <= data_a +1'b1; beginaddr_a <= addr_a +1'b1; if(wr_b)//写end beginelse ram[addr_b]<= data_b; begin q_b <= data_b ; data_a <=0; endaddr_a <=0; if(rd_b)end //读end q_b <= ram[addr_b]; always@(posedge clk) endbegin if(rd_b) begin endmoduleaddr_b <= addr_b +1'b1; end else addr_b <=0; end top t0(.data_a(data_a),.data_b(data_b), .addr_a(addr_a),.addr_b(addr_b ), .wr_a(wr_a),.wr_b(wr_b), .rd_a(rd_a),.rd_b(rd_b), .clk(clk), .q_a(q_a),.q_b(q_b)); endmodule 仿真结果如图343所示。 图343真双口RAM仿真波形 3.5.9单口ROM ROM是用来存储数据的,可以按照下列代码形式初始化ROM,但用这种方法处理大容量的ROM时就比较麻烦,建议用FPGA自带的ROM IP核实现,并添加初始化文件。 代码实现和激励文件如下: module top `timescale 1ns/1ns (module top_tb(); input[3:0] addr,reg[3:0] addr ; input clk,reg clk ; outputreg[7:0] q wire[7:0] q ; ); initial reg[7:0] rom [15:0]; //申明 rom begin always@(addr)addr =0; beginclk =0; case(addr)end 4'd0: rom[addr]=8'd15; always#10 clk =~clk ; 4'd1: rom[addr]=8'd24; always@(posedge clk) 4'd2: rom[addr]=8'd100; begin 4'd3: rom[addr]=8'd78; addr <= addr +1'b1; 4'd4: rom[addr]=8'd98; end 4'd5: rom[addr]=8'd105; top t0(.addr(addr), 4'd6: rom[addr]=8'd86; .clk(clk), 4'd7: rom[addr]=8'd254; .q(q)); 4'd8: rom[addr]=8'd76; endmodule 4'd9: rom[addr]=8'd35; 4'd10: rom[addr]=8'd120; 4'd11: rom[addr]=8'd85; 4'd12: rom[addr]=8'd37; 4'd13: rom[addr]=8'd19; 4'd14: rom[addr]=8'd22; 4'd15: rom[addr]=8'd67; endcase end always@(posedge clk) begin q <= rom[addr]; end endmodule 仿真结果如图344所示。 图344单口ROM仿真波形 3.5.10有限状态机 在Verilog中经常会用到有限状态机来处理相对复杂的逻辑,设定好不同的状态,根据触发条件跳转到对应的状态,在不同的状态下做相应的处理。有限状态机主要用到always及case语句。下面以一个四状态的有限状态机举例说明,如图345所示。 图345状态机跳转 在程序中设计了8位的移位寄存器。在Idle状态下,判断shift_start信号是否为高,如果为高,进入Start状态,在Start状态延迟100个周期,进入Run状态,进行移位处理,如果shift_stop信号有效,则进入Stop状态,在Stop状态,将q的值清零,再跳转到Idle状态。 Mealy有限状态机的输出不仅与当前状态有关,也与输入信号有关,在RTL视图中会与输入信号有连接。Mealy有限状态机的RTL视图如图346所示,其实现代码如下: 图346Mealy有限状态机的RTL视图 module top ( input shift_start, input shift_stop, input rst, input clk, input d, outputreg[7:0] q ); parameter Idle =2'd0;//初始状态 parameter Start =2'd1;//开始状态 parameter Run=2'd2;//运行状态 parameter Stop =2'd3;//停止状态 reg[1:0] state ; reg[4:0] delay_cnt ;//延迟计数 always@(posedge clk or negedge rst) begin if(!rst)begin state <= Idle ; delay_cnt <=0; q <=0; end else case(state) Idle :begin if(shift_start) state <= Start ; end Start :begin if(delay_cnt ==5'd99)begin delay_cnt <=0; state <= Run ; end else delay_cnt <= delay_cnt +1'b1; end Run:begin if(shift_stop) state <= Stop ; else q <={q[6:0], d}; end Stop :begin q <=0; state <= Idle ; end default: state <= Idle ; endcase end endmodule Moore有限状态机的输出只与当前状态有关,与输入信号无关,输入信号只影响状态的改变,不影响输出,比如对delay_cnt和q的处理,只与当前状态有关。Moore有限状态机的RTL视图如图347所示,其实现代码如下: 图347Moore有限状态机的RTL视图 module top ( input shift_start, input shift_stop, input rst, input clk, input d, outputreg[7:0] q ); parameter Idle =2'd0;//初始状态 parameter Start =2'd1;//开始状态 parameter Run=2'd2;//运行状态 parameter Stop =2'd3;//停止状态 reg[1:0] current_state ; reg[1:0] next_state ; reg[4:0] delay_cnt ;//延迟计数 //第一部分:状态转换 always@(posedge clk or negedge rst)begin if(!rst) current_state <= Idle ; else current_state <= next_state ; end //第二部分:组合逻辑,判断语句转换条件 always@(*)begin case(current_state) Idle :begin if(shift_start) next_state <= Start ; else next_state <= Idle ; end Start :begin if(delay_cnt ==5'd99) next_state <= Run ; else next_state <= Start ; end Run:begin if(shift_stop) next_state <= Stop ; else next_state <= Run ; end Stop : next_state <= Idle ; default:next_state <= Idle ; endcase end //第三部分:输出数据 always@(posedge clk or negedge rst) begin if(!rst) delay_cnt <=0; else if(current_state == Start) delay_cnt <= delay_cnt +1'b1; else delay_cnt <=0; end always@(posedge clk or negedge rst) begin if(!rst) q <=0; else if(current_state == Run) q <={q[6:0], d}; else q <=0; end endmodule 上面两个程序中用到了两种方式的写法。第一种是Mealy状态机,采用了一段式的写法,只用了一个always语句,所有的状态转移,判断状态转移条件,数据输出都在一个always语句里,缺点是如果状态太多,则会使整段程序显得冗长。第二种是Moore状态机,采用了三段式的写法,状态转移用了一个always语句,判断状态转移条件是组合逻辑,采用了一个always语句,数据输出也是单独的 always语句,这样写起来比较直观清晰,状态很多时也不会显得烦琐。 激励文件如下: `timescale 1ns/1ns module top_tb(); reg shift_start ; reg shift_stop ; reg rst ; reg clk ; reg d ; wire[7:0] q ; initial begin rst =0; clk =0; d =0; #200 rst =1; forever begin #({$random}%100) d =~d ; end end initial begin shift_start =0; shift_stop =0; #300 shift_start =1; #1000 shift_start =0; shift_stop =1; #50 shift_stop =0; end always#10 clk =~clk ; top t0 ( .shift_start(shift_start), .shift_stop(shift_stop), .rst(rst), .clk(clk), .d(d), .q(q) ); Endmodule 仿真结果如图348所示。 图348状态机仿真波形 3.6总结 本章介绍了组合逻辑以及时序逻辑中常用的模块,其中有限状态机较为复杂,但经常用到,希望大家能够深入理解,在编写代码时多运用、多思考,有利于快速提升水平。