在学习FPGA使用Verilog HDL语言编程时,开始遇到时序逻辑和组合逻辑时概念一看就明白,但是实际使用时还是不清楚到底要用哪个。现在用就一个例子来体会一下这两者的区别。
首先先看组合逻辑和时序逻辑的定义。
看完以后还是感觉云里雾里搞不清楚,那么就不用管它了,直接用例子来说明。
在这里设计一个0---9计数器,clk为输入时钟信号,cin为计数有效信号,也就是说只有当cin为高电平时,计数器才计数一次。cout为计数进位信号,当计数值为9时,计数值再加1的话,就输出一个进位信号,同时计数值清零。q输出计数值,输出值的范围是0--9。这个计数器类似于数码管显示数字时每一个数码管的显示范围,每个数码管显示范围为0--9,当低位满10之后,向前一位进1,同时低位清零。
下面开始编写代码
首先定义输入输出端口
module bcd_counter(
input clk, //时钟
input rst_n, //复位
input cin, //计数使能
output cout, //进位输出
output [3:0] q //计数输出
);
endmodule
输入信号有三个 时钟 clk、复位 rst_n、计数使能 cin。
输出信号有两个 进位输出cout、计数值输出q,q的计数范围是0--9,所以q设置为4位计数器。
下面编写计数代码
reg [3:0] cnt;
//BCD码计数
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cnt <= 4'd0;
else if(cin == 1'b1) begin //计数使能信号有效时 计数器加1
if(cnt < 4'd9)
cnt <= cnt + 1'b1;
else
cnt <= 4'd0;
end
else
cnt <= cnt;
end
cnt存储计数值,复位后默认值为0,每次当cin为高电平时,计数值加1,当计数到9时,计数值清零。cin为低电平时,计数值保持不变。这样计数寄存器的值就在0-到9之间循环。
下面编写进位代码
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
当计数寄存器的值为9,同时计数使能信号为1时,说明已经计够10次了,需要进位一次,这时cout输出1。其余情况下输出为0。
最后将寄存器的值连接到输出端口上
assign q = cnt;
整体代码如下
module bcd_counter(
input clk, //时钟
input rst_n, //复位
input cin, //计数使能
output cout, //进位输出
output [3:0] q //计数输出
);
reg [3:0] cnt;
//BCD码计数
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cnt <= 4'd0;
else if(cin == 1'b1) begin //计数使能信号有效时 计数器加1
if(cnt < 4'd9)
cnt <= cnt + 1'b1;
else
cnt <= 4'd0;
end
else
cnt <= cnt;
end
//进位信号输出
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
assign q = cnt; //输出计数值
endmodule
代码的功能比较简单,一个always语句产生计数信号,一个always语句产生进位信号。
下面编写测试文件
`timescale 1ns/1ns
module bcd_counter_tb;
parameter T = 20;
reg sys_clk;
reg sys_rst_n;
reg cin;
wire cout;
wire [3:0] q;
bcd_counter bcd_counter0(
.clk (sys_clk), //时钟
.rst_n (sys_rst_n), //复位
.cin (cin), //计数使能
.cout (cout), //进位输出
.q (q) //计数输出
);
initial begin
sys_rst_n = 1'b0;
sys_clk = 1'b1;
#200;
sys_rst_n = 1'b1;
repeat(100) begin
cin <= 1'b0; //输出4个周期低电平
#(T * 4);
cin <= 1'b1; //输出1个周期高电平
#(T);
end
cin <= 1'b0;
#(200 * T);
$stop;
end
always #(T/2) sys_clk = ~sys_clk;
endmodule
测试文件产生一个时钟信号 sys_clk 和一个计数使能信号 cin,cin信号为4个时钟的低电平,然后1个时钟的高电平.也是说没5个时钟周期计数器就会计数一次。
下来仿真一下,看看输出波形。
放大波形看看cin和计数值关系
可以看到每个cin信号为高时,计数值加1,当计数值为9时,输出cout信号输出一个高脉冲。
再放大波形看看cout和cin的输出时序
可以看到当计数值为9时,cin信号出现高电平,cout延时一个时钟周期才输出的一个高电平。按照正常计数逻辑来说,当低位9再加1时,低位变为0,同时向高位进1,计数和进位是同时发生的。而这个却出现了计数和进位不同步的情况。出现这种情况是为什么呢?那就要分析分析代码中的进位信号。
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
进位的always语句执行块,是在时钟的上升沿或者复位信号的下降沿才会进入。
在波形中可以看出,在蓝色光标处,时钟的上升沿cin信号到来,计数值加1,此时计数值为9,同时cin信号为1,此时cout信号应该要输出高电平了,但是由于cout是由时序逻辑控制的,只有在时钟的上升边沿always语句才会执行,所以只有等到下一个时钟上升沿cout才有机会输出高电平。
说明cout信号通过时序逻辑来实现的话,会有延时,不符合设计的实时变化要求,那么就把cout的实现改成用组合逻辑来实现,看看是什么效果。
在代码中将cout改为组合逻辑
/*
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
cout <= 1'b0;
else if(cnt == 4'd9 && cin == 1'b1)
cout <= 1'b1;
else
cout <= 1'b0;
end
*/
将时序逻辑改为组合逻辑
assign cout = (cnt == 4'd9 && cin == 1'b1) ? 1'b1 : 1'b0;
将cout改为组合逻辑实现,当计数值cnt为9,同时cin为高电平时,cout值为1,否则cout值为0。
这样当cin和cnt的值由任何变化时,cout值也跟着会变化,不受到时钟上升沿的影响。
注意将cout由时序逻辑改为组合逻辑时,要在初始化中将cout由寄存器类型改为线网类型。
重新编译代码后,查看波形。
这时可以看到cout信号和cin信号会同时变为高电平,不会有一个时钟周期的延迟。
当计数值变为9时,cin由低电平变为高电平继续计数时,计数值清0,同时进位输出也变为高电平。符合设计的要求。
通过上面的例子可以看到,当输出信号需要实时跟随输入信号变化时,就必须用组合逻辑来实现。其余情况下用时序逻辑来实现。