Verilog RTL 代码设计新手上路
对于使用硬件描述语言(HDL)来设计集成电路逻辑而言,一个有经验的集成电路逻辑设计人员会拥有以下的能力来完成设计。
目前成熟的EDA工具都是将RTL(寄存器传输级)层次的硬件描述语言转换为实际的电路。所谓RTL的含义就是D触发器之间穿插着组合逻辑。对于组合逻辑,我们只需描述它输入和输出的关系表达式(用if-else和case语句)不必深究到底用怎样的逻辑门来实现,而对于D触发器,则是一定要做到心中有数,就是哪些变量会生成D触发器。
边沿捕获电路的RTL结构 | EDA工具提取出的边沿捕获电路RTL结构 |
举例来说,一个在CLK时钟驱动下,对输入信号IN进行上跳沿捕获的电路,其电路RTL结构如左图所示,它的Verilog代码如下所示,另外,EDA工具进行编译之后,会提取出RTL结构如右图所示,我们可以观察EDA工具解析出的RTL是否和我们预想的一致,这是一种重要的验证手段。对于Quartus工具,Tools -> Netlist Viewer -> RTL Viewer
// module top, 边沿捕获器代码, module top( CLK , // input clock IN , // input OUT ); // output input CLK; input IN; output OUT; reg d1R, d2R; // 电路中的D触发器输出端 reg OUT; // 组合逻辑输出信号,作为输出端口 // 生成移位寄存的D触发器,我们要明确,下面代码的写法会生成d1R和d2R两个D触发器。 // 并且d1R和d2R级联构成IN信号的移位寄存器。这是EDA工具不会篡改的事实。 always @ (posedge CLK) begin d1R <= IN ; d2R <= d1R ; end // 判断上跳沿的组合逻辑,我们要明确,下面的代码是一个以d1R、d2R信号作为输入, // 以OUT信号作为输出的组合逻辑,并且代码中指出了OUT信号与d1R、d2R信号的逻辑 // 关系(好比是真值表),但是究竟用怎样的逻辑门实现(好比是卡诺图的画圈) // 那是EDA工具要关心的事情。 always @ (d1R or d2R) begin if((d2R == 0)&&(d1R == 1)) // 新值为1,旧值为0,跳变发生 OUT = 1'b1; else OUT = 1'b0; end endmodule // endmodule top
// 顶层模块 module top( IN , OUT0 , OUT1 ); input [4-1:0] IN ; output[4-1:0] OUT0; output[4-1:0] OUT1; wire [4-1:0] W_add0_out; wire [4-1:0] W_add1_out; // 第1次实例化, 加法器子模块 add U0_add( .IN0 (IN ), .IN1 (4'h1 ), .OUT (W_add0_out)); // 第2次实例化, 加法器子模块 add U1_add( .IN0 (IN ), .IN1 (4'h2 ), .OUT (W_add1_out)); // 端口接线互联 assign OUT0 = W_add0_out; assign OUT1 = W_add1_out; endmodule // 加法器子模块 module add( IN0 , IN1 , OUT ); input [4-1:0] IN0; input [4-1:0] IN1; output[4-1:0] OUT; reg [4-1:0] OUT; always @ (IN0 or IN1) begin OUT = IN0 + IN1; end endmodule
子模块的实例化和互联 |
MUX21 符号 | MUX21 时序图 |
// module top, 选择器(mux)的代码, module top( IN0 , // input 1 IN1 , // input 2 SEL , // select OUT ); // out data parameter WL = 16; // 输入输出数据信号位宽 input [WL-1:0] IN0, IN1;// 选择器的两个输入数据信号 input SEL; // 通道选通的控制信号 output[WL-1:0] OUT; // 选择器的输入数据信号 reg [WL-1:0] OUT; // 生成组合逻辑的代码 always @ (IN0 or IN1 or SEL) begin if(SEL) // SEL为1 选择输入1 OUT = IN1; else // SEL为0 选择输入0 OUT = IN0; end endmodule // endmodule top
做一个4选1的mux,并且进行波形仿真 和2选1的mux对比,观察资源消耗的变化
2X2交叉开关 符号 |
// module top, a 2x2 crossbar switch circuit module top( IN0 , // input 1 IN1 , // input 2 SEL0 , // select the output0 source SEL1 , // select the output1 source OUT0 , // output data 0 OUT1 ); // output data 1 parameter WL = 16; input [WL-1:0] IN0, IN1; input SEL0, SEL1; output[WL-1:0] OUT0, OUT1; reg [WL-1:0] OUT0, OUT1; // get the OUT0 always @ (IN0 or IN1 or SEL0) begin if(SEL0) OUT0 = IN1; else OUT0 = IN0; end // get the OUT1 always @ (IN0 or IN1 or SEL1) begin if(SEL1) OUT1 = IN1; else OUT1 = IN0; end endmodule // endmodule top
编写一个4X4路交叉开关的RTL,然后编译,看RTL View 比较2x2与4x4之间消耗资源的区别。通过对比资源,你有什么结论? 返回顶部
4输入优先编码器时序图 |
// module top, 4 input priority encoder with zero input check module top( IN , // input OUT ); // output input [3:0] IN; output[2:0] OUT; reg [2:0] OUT; // get the OUT always @ (IN) begin if(IN[3]) // 第一优先 OUT = 3'b011; else if(IN[2]) // 第二优先 OUT = 3'b010; else if(IN[1]) // 第三优先 OUT = 3'b001; else if(IN[0]) // 第四优先 OUT = 3'b000; else // 什么都没有检测到 OUT = 3'b111; // 输出值可自定义,不和上面的输出值混淆即可 end endmodule
数字电路课本中的3-8译码器就是多路译码器中的一种。
// module top, 4 input priority encoder with zero input check module top( IN , // input OUT ); // output input [2:0] IN; output[7:0] OUT; reg [7:0] OUT; // get the OUT always @ (IN) begin case(IN) 3'b000: OUT = 8'b0000_0001; 3'b001: OUT = 8'b0000_0010; 3'b010: OUT = 8'b0000_0100; 3'b011: OUT = 8'b0000_1000; 3'b100: OUT = 8'b0001_0000; 3'b101: OUT = 8'b0010_0000; 3'b110: OUT = 8'b0100_0000; 3'b111: OUT = 8'b1000_0000; // full case 不需要写default,否则一定要有default endcase end endmodule
加法器是很常用的电路,主要常用的形式包括
输入和输出数据都是无符号的整数,常用于计数器累加和计算地址序号
无符号加法器时序图 |
module top( IN1 , IN2 , OUT ); input[3:0] IN1, IN2; output[4:0] OUT; reg[4:0] OUT; always@(IN1 or IN2) begin // 生成组合逻辑的always 块 OUT = IN1 + IN2; end endmodule
输入和输出数据都是2补码形式的有符号数,常用于数字信号处理电路
module top( IN1 , IN2 , OUT ); input signed [3:0] IN1, IN2; output signed [4:0] OUT; reg signed [4:0] OUT; always@(IN1 or IN2) begin // 生成组合逻辑的always 块 OUT = IN1 + IN2; end endmodule
补码加法器时序图 |
从组合逻辑门的层面而言,补码加法器和无符号加法器的电路逻辑是有较大区别的,是由于EDA工具检测到了"signed"关键字,所以才生成了补码加法器的电路逻辑。 注意补码加法器的输入数据和对应结果在时间上同样是有延迟的,这是组合逻辑的延迟。(请估算一下延迟长度) 波形图上的数据是带负号的,这是通过选择信号-属性-Radix为 有符号的十进制 来观察的结果 请思考,对于同样的二进制比特数据,我们可以用不同的Radix观察它,这说明了什么? 请思考,如果输出的位数和输出的位数相同,那么加法结果在什么情况下是正确的,什么情况下是错误的?
把加法器的输出信号改成4比特位宽,编译,波形仿真。观察输出结果,观察输出结果在什么时候是正确的?。 把加法器的输入信号改成8比特位宽,编译,波形仿真。观察加法器的输出延迟,和4比特输入位宽的情况对比,你有什么结论,为什么?
输入和输出都添加了流水线D触发器的无符号加法器,其RTL结构如图所示,点击图片可以放大观察
流水线加法器RTL结构图 |
module top( IN1 , IN2 , CLK , OUT ); input [3:0] IN1, IN2; input CLK; output [4:0] OUT; reg [3:0] in1_d1R, in2_d1R; reg [4:0] adder_out, OUT; always@(posedge CLK) begin // 生成D触发器的always块 in1_d1R <= IN1; in2_d1R <= IN2; OUT <= adder_out; end always@(in1_d1R or in2_d1R) begin // 生成组合逻辑的always 块 adder_out = in1_d1R + in2_d1R; end endmodule
流水线无符号加法器时序图 |
目测一下,和不带流水线的加法器相比,毛刺的时间长度有变化么,为什么? 观察,输入数据和其对应的结果在一个时钟周期么?
不改变流水线的级数,把加法器的输入信号改成8比特位宽,编译,波形仿真,和不带流水线的情况对比一下,你有什么结论? 在8比特输入位宽的情况下,在输入上再添加一级流水线,观察编译和仿真的结果,你有什么结论?
写在其它内容之前,先要强调的是,乘法器是一种奢侈品会消耗大量的组合电路逻辑资源,一定要慎重使用。尤其是对于芯片内部没有硬件乘法器的FPGA芯片,需要更加慎重。 为什么会消耗如此多的资源呢,请回想一下我们在草稿纸上手工计算多位数乘法的过程,我们需要做很多次的加法,用电路计算乘法也是类似的。
乘法器的代码和加法器类似,有无符号,有符号的乘法器,另外还可以添加流水线。
//////////////////// 无符号的乘法器 ///////////////////////// module top( IN1 , IN2 , OUT ); input [3:0] IN1, IN2; output [7:0] OUT; reg [7:0] OUT; always@(IN1 or IN2) begin // 生成组合逻辑的always 块 OUT = IN1 * IN2; end endmodule //////////////////// 有符号的2补码乘法器 ///////////////////////// module top( IN1 , IN2 , OUT ); input signed[3:0] IN1, IN2; output signed [7:0] OUT; reg signed[7:0] OUT; always@(IN1 or IN2) begin // 生成组合逻辑的always 块 OUT = IN1 * IN2; end endmodule
补码乘法器时序图 |
A*3 = A*2 + A = (A << 1) + A
计数器是最为常用的时序电路之一,计数器在数字电路里面的作用,就如C程序中的for循环一样
最简单的计数器只有一个CLK信号和一个计数值Q信号,在实际应用中的计数器可能会出现较多的变化,例如下图所示的各种端口,完成不同的逻辑功能,列举如下:
带有多种控制信号的计数器RTL图 |
//////////////////// 计数器代码 ///////////////////////// module top( RST , // 异步复位, 高有效 CLK , // 时钟,上升沿有效 EN , // 输入的计数使能,高有效 CLR , // 输入的清零信号,高有效 LOAD , // 输入的数据加载使能信号,高有效 DATA , // 输入的加载数据信号 CNTVAL, // 输出的计数值信号 OV );// 计数溢出信号,计数值为最大值时该信号为1 input RST , CLK , EN , CLR , LOAD ; input [3:0] DATA ; output [3:0] CNTVAL; output OV; reg [3:0] CNTVAL, cnt_next; reg OV; // 电路编译参数,最大计数值 parameter CNT_MAX_VAL = 9; // 组合逻辑,生成cnt_next // 计数使能最优先,清零第二优先,加载第三优先 always @(EN or CLR or LOAD or DATA or CNTVAL) begin if(EN) begin // 使能有效 if(CLR) begin // 清零有效 cnt_next = 0; end else begin // 清零无效 if(LOAD) begin // 加载有效 cnt_next = DATA; end else begin // 加载无效,正常计数 // 使能有效,清零和加载都无效,根据当前计数值计算下一值 if(CNTVAL < CNT_MAX_VAL) begin // 未计数到最大值, 下一值加1 cnt_next = CNTVAL + 1'b1; end else begin // 计数到最大值,下一计数值为0 cnt_next = 0; end end // else LOAD end // else CLR end // if EN else begin // 使能无效,计数值保持不动 cnt_next = CNTVAL; end // else EN end // 时序逻辑 更新下一时钟周期的计数值 // CNTVAL 会被编译为D触发器 always @ (posedge CLK or posedge RST) begin if(RST) CNTVAL <= 0; else CNTVAL <= cnt_next; end // 组合逻辑,生成OV always @ (CNTVAL) begin if(CNTVAL == CNT_MAX_VAL) OV = 1; else OV = 0; end endmodule
下图是计数器代码的波形仿真时序图,从图中的计数值CNTVAL的改变可以看出信号的优先关系
带有多种控制信号的计数器仿真波形图 |
请完成以下设计实验,编译电路并且进行波形仿真。
有限状态机(Finite State Machine)同样是数字电路设计中非常常用的模块,其在EDA设计中的地位等同于C语言中的If-else语句。
状态机 的 状态转移图和RTL结构 |
当前状态 | 输入 | 次态 |
---|---|---|
ST_0_CENT | CENT1IN==0 | ST_0_CENT |
ST_0_CENT | CENT1IN==1 | ST_1_CENT |
ST_1_CENT | CENT1IN==0 | ST_1_CENT |
ST_1_CENT | CENT1IN==1 | ST_2_CENT |
ST_2_CENT | CENT1IN==0 | ST_2_CENT |
ST_2_CENT | CENT1IN==1 | ST_3_CENT |
ST_3_CENT | Donot care | ST_0_CENT |
当前状态 | 输出 TINOUT |
---|---|
ST_0_CENT | 0 |
ST_1_CENT | 0 |
ST_2_CENT | 0 |
ST_3_CENT | 1 |
//////////////////// 三段式状态机代码 ///////////////////////// module test_rtl( CLK , // clock RST , // reset CENT1IN , // input 1 cent coin TINOUT ); // output 1 tin cola input CLK ; input RST ; input CENT1IN ; output TINOUT ; parameter ST_0_CENT = 0; parameter ST_1_CENT = 1; parameter ST_2_CENT = 2; parameter ST_3_CENT = 3; reg [2-1:0]stateR ; reg [2-1:0]next_state ; reg TINOUT ; // calc next state always @ (CENT1IN or stateR) begin case (stateR) ST_0_CENT :begin if(CENT1IN) next_state = ST_1_CENT ; else next_state = ST_0_CENT; end ST_1_CENT :begin if(CENT1IN) next_state = ST_2_CENT ; else next_state = ST_1_CENT; end ST_2_CENT :begin if(CENT1IN) next_state = ST_3_CENT ; else next_state = ST_2_CENT; end ST_3_CENT :begin next_state = ST_0_CENT; end endcase end // calc output always @ (stateR) begin if(stateR == ST_3_CENT) TINOUT = 1'b1; else TINOUT = 1'b0; end // state DFF always @ (posedge CLK or posedge RST)begin if(RST) stateR <= ST_0_CENT; else stateR <= next_state; end endmodule
对于代码编写的规范的状态机,EDA工具可以检测出来,例如Quartus中,编译代码之后,可以在Tools-Netlist Viewers-State Machine Viewer 里面看到你写的状态机的状态转移图和表达式。这对你调试电路非常有帮助。
Quartus提取的状态机视图 |
设计一个用于识别2进制序列“1011”的状态机
两种移位寄存器RTL结构图 |
//////////////////// 串入并出移位寄存器 ///////////////////////// module top( RST , // 异步复位, 高有效 CLK , // 时钟,上升沿有效 EN , // 输入数据串行移位使能 IN , // 输入串行数据 OUT ); // 并行输出数据 input RST, CLK, EN; input IN; output[3:0] OUT; reg [3:0] shift_R; assign OUT[3:0] = shift_R[3:0]; // 时序逻辑 根据输入使能进行串行移位 // shift_R 会被编译为D触发器 always @ (posedge CLK or posedge RST) begin if(RST) shift_R[3:0] <= 0; else if(EN) begin // 串行移位的使能有效 shift_R[3:1] <= shift_R[2:0]; shift_R[0] <= IN; end else begin // 使能无效保持不动 shift_R[3:0] <= shift_R[3:0]; end end // always endmodule
串行输入并行输出移位寄存器波形 |
设计一个如本节“电路描述”部分的“带加载使能和移位使能的并入串出”的移位寄存器,电路的RTL结构图如“电路描述”部分的RTL结构图所示。
地址宽度,数据宽度均参数化的双口RAM,另外带有RAM内容初始化的仿真功能(注意这只是一个仿真用的功能)
使用了以下的编译命令(注意这是一种编译命令!虽然它们是写在注释里面的,因为Verilog语言本身是不包含编译控制的语法的,这种写法不推荐广泛使用,只有必要时采用,因为严格来说这不是一种规范的做法)来包裹行为仿真的代码
// synopsys translate_off // the code doNOT get into synthesis // synopsys translate_on
上面两个编译命令中的Verilog代码不参加电路转换,但是可以参加RTL的仿真。本模块在其中添加了RAM内容初始化的代码,这样做是为了避免进行RTL仿真时从RAM中读出未知的“X”数据。但是,请注意,这仅仅是用于仿真,实际的FPGA中的RAM,在上电以后,未向其写入数据之前,RAM中存储单元的数值具体是什么要看厂家的芯片手册。
这种双口RAM的读写可以是异步的,即写入时钟和读出时钟可以是异步的,所以可以使用这种RAM跨越时钟域或是构建一个FIFO(也是用了跨越时钟域的)
// module top, a dual port RAM // instancing and define parameter of dpram module module top( WE , // write enable WCLK , // write clock RCLK , // read clock WA , // write address RA , // read address WD , // write data RD ); // read data parameter DATAWL = 8; parameter ADDRWL = 8; parameter C2Q = 2; input WE, WCLK, RCLK; input [ADDRWL -1:0] WA, RA; input [DATAWL -1:0] WD; output [DATAWL -1:0] RD; dpram U0_dpram( .WE (WE ), // write enable .WCLK (WCLK ), // write clock .RCLK (RCLK ), // read clock .WA (WA ), // write address .RA (RA ), // read address .WD (WD ), // write data .RD (RD )); // read data defparam U0_dpram.DATAWL = DATAWL; defparam U0_dpram.ADDRWL = ADDRWL; defparam U0_dpram.C2Q = C2Q ; endmodule // endmodule top module dpram( WE , // write enable WCLK , // write clock RCLK , // read clock WA , // write address RA , // read address WD , // write data RD ); // read data // external set param parameter DATAWL = 0; parameter ADDRWL = 0; parameter C2Q = 0; input WE, WCLK, RCLK; input [ADDRWL -1:0] WA, RA; input [DATAWL -1:0] WD; output [DATAWL -1:0] RD; reg[DATAWL-1:0] RD; reg[DATAWL-1:0] mem [(1 << ADDRWL)-1:0]; always @ (posedge WCLK) begin if(WE) mem[WA] <= #C2Q WD; end always @ (posedge RCLK) begin RD <= #C2Q mem[RA]; end // ###################################### // synopsys translate_off // ###################################### // the code below this line will NOT take part into synthesis // they are only needed by RTL simulation // task DumpDpRAM, get the content of RAM[addr] task DumpDpRAM; input [ADDRWL-1 :0] addr ; output [DATAWL-1 :0] content ; begin content = mem[addr]; end // task begin endtask // task DumpDpRAM // task RAMInit, initialize the RAM content task RAMInit; integer i; reg[DATAWL-1:0] initData; begin initData = 'hAAAA; // initData = (1 << DATAWL) - 1; for( i = 0; i < (1 << ADDRWL); i = i + 1) mem[i] = initData; end endtask initial begin RAMInit(); $display("module dpram().RAMInit()called @ %0d", $time); end // ###################################### // synopsys translate_on // ###################################### // the code below this line will take part in synthesis endmodule // module dptbram()
在Altera中的ROM是使用MemoryBlock实现的,因为MemoryBlock拥有内容初始化的能力。本代码的最终编译成为一个FPGA片内的RAM需要以下的支持
// module top, a synchronized rom module top( CLK , // clock RA , // read address RD ); // read data input CLK; input [6 :0] RA; output [12 :0] RD; reg [12 :0] RD; always @ (posedge CLK) case(RA) 7 'd 0 : RD = #1 13'd 0 ; // 0x0 7 'd 1 : RD = #1 13'd 101 ; // 0x65 7 'd 2 : RD = #1 13'd 201 ; // 0xC9 7 'd 3 : RD = #1 13'd 301 ; // 0x12D ... ... ... 7 'd 123 : RD = #1 13'd 8176 ; // 0x1FF0 7 'd 124 : RD = #1 13'd 8181 ; // 0x1FF5 7 'd 125 : RD = #1 13'd 8185 ; // 0x1FF9 7 'd 126 : RD = #1 13'd 8189 ; // 0x1FFD 7 'd 127 : RD = #1 13'd 8190 ; // 0x1FFE endcase endmodule
使用多次例化语法生成的寄存器组RTL结构图 |
// module top, example for, 'for' and 'generate' module top( CLK , RST , IN1 , OUT1 ); parameter DWL = 8; parameter reg_len = 3; input CLK; input RST; input [DWL-1:0] IN1; output [DWL-1:0] OUT1; reg [DWL-1:0] in1R[reg_len-1:0]; wire [DWL-1:0] in1R_inW[reg_len-1:0]; integer idx; always @ (posedge CLK or posedge RST) begin if(RST) begin for(idx = 0; idx < reg_len; idx = idx +1) begin in1R[idx] <= 0; end end else begin for(idx = 0; idx < reg_len; idx = idx +1) begin in1R[idx] <= in1R_inW[idx]; end end end genvar i_g; generate for (i_g =0; i_g < reg_len; i_g=i_g+1) begin: MULT_INST wire [DWL-1:0] logic_inW; if(i_g == 0) begin assign logic_inW = IN1; end else begin assign logic_inW = in1R[i_g-1]; end param_logic U_pl( .IN (logic_inW ), .OUT (in1R_inW[i_g] )); defparam U_pl.DWL = DWL; defparam U_pl.INST_P1 = i_g; end endgenerate assign OUT1 = in1R[reg_len-1]; endmodule module param_logic( IN , OUT ); parameter DWL = 0; parameter INST_P1 = 0; input [DWL-1:0] IN ; output [DWL-1:0] OUT ; assign OUT = IN + INST_P1; endmodule