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