3. Verilog基础与流水线设计:组合逻辑与时序逻辑、阻塞与非阻塞赋值、流水线设计思想、一个简单的加法器流水线实例
各位同学,欢迎来到第三章。这一章可以说是整个FPGA设计的「内功心法」。很多新手写Verilog,代码能跑,但综合出来的电路要么跑不快,要么逻辑混乱。说白了,就是没搞懂组合逻辑和时序逻辑的区别,也没弄明白阻塞和非阻塞赋值到底该怎么用。
我个人习惯,在开始一个新项目前,一定会先画一张「数据流图」。这张图决定了你的设计是「组合逻辑」还是「时序逻辑」。嗯,我们先从最基础的概念说起。
3.1 组合逻辑与时序逻辑
组合逻辑,说白了就是「输入一变,输出立马变」。它没有记忆功能,输出只取决于当前的输入。比如一个简单的与门、或门,或者一个加法器。
时序逻辑,则是有「记忆」的。它的输出不仅取决于当前输入,还取决于之前的状态。最典型的代表就是触发器(Flip-Flop)。
我在项目中遇到过一个问题:一个同事写了一个计数器,用了组合逻辑的写法,结果仿真没问题,上板子后计数乱跳。为什么?因为组合逻辑对毛刺太敏感了。你想想看,一个组合逻辑的输出,只要输入有抖动,输出就会跟着抖。而时序逻辑只在时钟沿采样,天然就带了一个「滤波器」。
核心区别一句话总结:
- 组合逻辑:没有时钟,输出随输入变
- 时序逻辑:有时钟,输出只在时钟沿更新
我们来看一个简单的例子。这是一个组合逻辑的加法器:
// 组合逻辑加法器
module comb_adder (
input [7:0] a,
input [7:0] b,
output [8:0] sum
);
assign sum = a + b;
endmodule
再看一个时序逻辑的加法器(带寄存器输出):
// 时序逻辑加法器
module seq_adder (
input clk,
input [7:0] a,
input [7:0] b,
output reg [8:0] sum
);
always @(posedge clk) begin
sum <= a + b;
end
endmodule
看到了吗?区别就在于那个 always @(posedge clk)。时序逻辑的加法结果,只在时钟上升沿才被锁存到 sum 里。这能有效过滤掉输入信号的毛刺。
3.2 阻塞与非阻塞赋值
这块是新手最容易踩坑的地方。我曾经见过一个老工程师,写了十年Verilog,还在 always 块里混用阻塞和非阻塞赋值,结果综合出来的电路跟他想的完全不一样。
阻塞赋值(=):说白了就是「先算完,再往下走」。它是顺序执行的,就像C语言一样。阻塞赋值一般用于组合逻辑的 always 块。
非阻塞赋值(<=):它是「大家一起算,算完再一起赋值」。所有非阻塞赋值在同一个时钟沿是并行执行的。非阻塞赋值一般用于时序逻辑的 always 块。
避坑指南:
我曾经在一个项目中,把非阻塞赋值用在了组合逻辑里。结果仿真波形看起来是对的,但综合工具报了一堆警告,最后生成的电路面积大了三倍。从那以后,我给自己定了个规矩:
- 组合逻辑 always 块:用阻塞赋值(=)
- 时序逻辑 always 块:用非阻塞赋值(<=)
这个规矩,我建议你也记住。
我们来看一个对比的例子。假设我们要交换两个寄存器的值:
// 错误写法:用阻塞赋值交换
always @(posedge clk) begin
a = b; // a 先被赋值为 b
b = a; // b 被赋值为 a,但此时 a 已经是 b 的值了
end
// 结果:a 和 b 都变成了 b 原来的值,交换失败!
// 正确写法:用非阻塞赋值交换
always @(posedge clk) begin
a <= b; // 先计算 b 的值
b <= a; // 先计算 a 的值
end
// 结果:a 得到 b 的值,b 得到 a 的值,完美交换!
为什么会这样?因为非阻塞赋值在时钟沿来临时,会先「读取」所有右边的值,然后再「写入」左边的寄存器。这就保证了交换操作的原子性。
3.3 流水线设计思想
流水线,说白了就是「把一个大任务拆成多个小任务,每个小任务用一级寄存器隔开」。这样做的目的是提高时钟频率。
你想想看,一个复杂的组合逻辑,从输入到输出可能要经过很多级门电路。门电路越多,延迟就越大。如果这个延迟超过了时钟周期,那你的电路就跑不起来。
流水线的思路就是:在组合逻辑的中间插入寄存器,把长路径切成短路径。这样每级路径的延迟都变小了,时钟频率就能提上去。
流水线的代价:
- 增加了延迟(Latency):数据需要多几个时钟周期才能输出
- 增加了面积:每级流水线都需要额外的寄存器
- 但换来了更高的吞吐量(Throughput):每个时钟都能输出一个结果
我在做量化交易系统时,对延迟极其敏感。但即便如此,我们仍然会在某些关键路径上使用流水线。为什么?因为如果不做流水线,时钟频率上不去,整个系统的吞吐量反而更低。这是一个「延迟 vs 吞吐量」的权衡。
3.4 一个简单的加法器流水线实例
好了,理论说完了,我们来看一个实际的例子。假设我们要做一个 4 级流水线的加法器,把 8 个 8 位数相加。
先画一张流水线结构图:
好,现在我们来看代码实现。这个例子把 8 个 8 位数两两相加,经过 4 级流水线得到最终结果。
// 4级流水线加法器
module pipeline_adder (
input clk,
input rst_n,
input [7:0] data_in [0:7], // 8个输入数据
output reg [11:0] result // 最终结果
);
// 第1级流水线:输入寄存器
reg [7:0] d0 [0:7];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < 8; i++) d0[i] <= 8'd0;
end else begin
for (int i = 0; i < 8; i++) d0[i] <= data_in[i];
end
end
// 第2级流水线:第1次加法(4个加法器并行)
reg [8:0] s1 [0:3];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < 4; i++) s1[i] <= 9'd0;
end else begin
for (int i = 0; i < 4; i++) s1[i] <= d0[2*i] + d0[2*i+1];
end
end
// 第3级流水线:流水线寄存器 + 第2次加法(2个加法器并行)
reg [8:0] s2 [0:1];
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < 2; i++) s2[i] <= 9'd0;
end else begin
for (int i = 0; i < 2; i++) s2[i] <= s1[2*i] + s1[2*i+1];
end
end
// 第4级流水线:输出寄存器 + 最后一次加法
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
result <= 12'd0;
end else begin
result <= s2[0] + s2[1];
end
end
endmodule
个人经验:
我在写流水线时,习惯给每一级都加上寄存器。这样虽然多用了几个触发器,但时序收敛会容易很多。尤其是在做高频交易系统时,时钟频率动辄 500MHz 以上,每一级组合逻辑的延迟都必须严格控制。
另外,我建议你在仿真时,把每一级流水线的中间结果都拉出来看看。这样能快速定位问题。我曾经就因为少写了一级寄存器,导致数据错位,查了整整两天才找到原因。
好了,这一章的内容就到这里。记住三个核心点:组合逻辑 vs 时序逻辑、阻塞 vs 非阻塞赋值、流水线的思想。这些是FPGA设计的基石,搞懂了它们,后面的路就好走了。
公众号:蓝海资料掘金营,微信deep3321