1 概述
近两年来,米联客在提供可靠的 FPGA 硬件板卡,以及丰富的 FPGA 应用方案方面,做出了很大的进步。但是米联客的课程文档能算是最棒的吗?是初学者最佳选择的 FPGA 教程吗?显示不是。
笔者最近一直在思考这个问题。因此在公司内部会议上对新版本教程提出了非常高的要求。目前我们已经完成了,课程大纲的制定,相关的课程编写已经开始,并且一定以高标准,高质量的水平制作出来。我相信这一版本教程肯定给广大 FPGA 读者带来更好的体验。
当 然对于目前市场上鱼龙混杂的电子类书籍我也有吐槽的,精品太少了,内容重复,没有新意。我甚至怀疑很多书籍是水平没多高的人写的,所以写不出深度,写不出新的名堂。作为读者的你们,可以多翻翻自己买的 FPGA书籍,看看里面有多少含金量。
米联客目前没有出书的计划,因为我自知我们的教程内容还谈不上经典,所以电子教程会继续提供大家免费阅读和学习,学习的答疑在米联客 FPGA 社区 www.uisrc.com 答疑版块进行。这里也感谢广大支持和使用米联客。
学习 FPGA 编程,一定要搞清楚是时序逻辑电路,什么是组合逻辑电路。我们编写 FPGA 的代码尽量使用时序逻辑,因为时序逻辑电路,更有利用控制时序上的同步,这本身和目前的 FPGA 是以 LUT 查找表的方式实现的逻辑关系有关,而且也和 FPGA 逻辑规模越来越大有关系。大规模的组合逻辑,不利于时序的控制。
2 组合逻辑代码
还记得前面使用 LUT 资源实现的加法器不?其实那个就是组合逻辑代码,只是你还不认识这种代码。这节课的代码我们稍加改变,增加了 always @(*)模块,在 always @(*) 模块里面的代码最终会被 VIVADO 软件编译成组合逻辑电路。always @(*)中”*”符号表示了对所有信号敏感,也就是电路只要有任何的输入改变,输出就会改变。输入到输出除了电路上的延迟是瞬间完成输出的。
module ADDER(
input [5:0]A,
input [5:0]B,
output [6:0]Q
);
reg [6:0 ]Q=7'd0;
always @(*)begin
Q = A + B;
end
endmodule
| 看下代码翻译出来的原理图,如下图所示。
再看下仿真结果如下图,对于代码和仿真文件的编写再后面的章节还有具体讲解,读者先根据我的教程学习,这节课主要掌握组合逻辑电路和时序逻辑电路及代码的差异。
再给出仿真代码,仿真代码的专业描述叫做 test bench,就是通过一些测试量的输入,看代码在仿真条件下是否满足功能要求。编写复杂并且正确的测试文件,很多时候比实现具体的功能代码都复杂。如果你从事的 IC 设计或者验证领域,就必须熟练掌握并且编写 test bench 文件。IC 验证设计,仿真文件的编写可以使用 system C 会比用纯Verilog 更方便。我们做应用开发的,一般不需要编写那么复杂的仿真文件。很多时候我们还会直接用芯片厂家提供的仿真模型,这样大大降低了我们自己开发的难度。
module adder_tb;
reg [5:0]A;
reg [5:0]B;
wire [6:0]Q;
ADDER ADDER_inst(
.A(A),
.B(B),
.Q(Q)
);
initial begin
#100;
# 10 A = 1; B = 1;
# 5 A = 2; B = 3;
# 2 A = 3; B = 4;
# 10 A = 1; B = 2;
end
| 从仿真中也验证了前面我说的内容,对于组合逻辑,除了电路固有的延迟外,输入到输出的反应是非常迅速的。但是如果你觉得速度越快越好,那你就错了,因为如果用纯组合逻辑去写大规模的代码,速度反而快不起来。因为随着组合逻辑的代码规模变大,走线边长等因素,电路会越来越难以满足时序的要求。
3 时序逻辑代码
下面是一段基于时序逻辑的代码也是使用 always 模块,时序逻辑代码在 always @(posedge clk)的括号内的敏感信号,一般是时钟上升沿和时钟下降沿,而以时钟上升沿最常用, always 模块中的代码会在每个时钟沿执行一次。以下是以时序逻辑实现的加法器的代码。
module ADDER(
input clk,
input rstn,
input [5:0]A,
input [5:0]B,
output [6:0]Q
);
reg [6:0 ]Q = 7'd0;
always @(posedge clk)begin
if(!rstn)begin
Q <= 7'd0;
end
else begin
Q <= A + B;
end
end
endmodule
| 以下是翻译出来的原理图,可以看到蓝色的部分是多出来的电路,增加的 D 触发器的部分电路。由于 D 触发器的 CE 为 1 始终有效,所以每个时钟的上升沿,D 触发器都会执行一次。
仿真代码的编写,在仿真代码中,我们得确保输入的 A 和 B 是基于时钟同步的,只有这样才能确保时序的同步,否则也是无法保证执行结果正确,这就是时序逻辑的特点,我们得确保和时钟同步。
module adder_tb;
reg clk;
reg rstn;
reg [5:0]A;
reg [5:0]B;
wire [6:0]Q;
ADDER ADDER_inst(
.clk(clk),
.rstn(rstn),
.A(A),
.B(B),
.Q(Q)
);
initial begin
#100;
clk = 0;
A = 6'd0;
B = 6'd0;
rstn = 1'b0;
#100;
@(posedge clk); rstn = 1'b1;
@(posedge clk); A = 1; B = 1;
@(posedge clk); A = 2; B = 3;
@(posedge clk); A = 3; B = 4;
@(posedge clk); A = 1; B = 2;
end
always #10 clk = ~clk;
endmodule
| 以下是仿真结果。
时序逻辑电路,里面实际上也是也是有组合逻辑电路,如下图红框所示:
如果简单看我们的仿真代码,仿真代码结果是没问题的,但是我们刚才使用的是 Run Behavioral Simulation 仿真,这种仿真不能看出电路的延迟,我们看下增加电路延迟后的仿真,这样要选择 Run Post-Synthesis Functional Simulation。如下图所示:
仿真结果如下图所示,对于初学 FPGA 的人来说,是不是有点神奇?输出结果是晚于输入一个时钟周期。没错,以下的执行结果才是最接近真实的执行结果的。通过我们前面的红色框图的组合逻辑知道,输入 A 和输入 B 的值改变后:
但是我们仍然又疑问,说好的每个时钟上升沿代码才执行一次代码呢?通过上面的分析我们已经得出结论,逻辑部分,也就是在查找表部分是逻辑时序,除了电路固有的从延迟,是瞬间执行的,但是后面的 D 触发器需要在下一个时钟才能把数据输出。
这个是不是有点烧脑?
4 时序逻辑实现计数器
我们再看一个计数器的仿真,加深下理解时序逻辑电路代码,为了能够让代码简单分析,去掉了复位,以及使用 2 位的计数器,这样的电路方便我们分析。
module counter(
input clk,
output [1:0]Q
);
reg [1:0 ]Q = 2'd0;
always @(posedge clk)begin
Q <= Q + 1'b1;
end
endmodule
| 以下是 VIVADO 根据代码翻译出来的电路图:
通过查看 LUT 的属性,可以查到 LUT 的初值,这个初值是用于查找表实现逻辑电路的。
LUT1 的查表值为 2’h1 查表如下
LUT2 的查表值为 4’h6 查表如下
计数器时序逻辑真值表如下图:
上表中,D 触发器的 Q 是在 D 端数据到达后下一个时钟上升沿输出,本课程的内容理解了对于后面我们分析代码的时序会有很大的帮助。
|