4. Verilog基础与流水线设计:组合逻辑与时序逻辑、状态机、乒乓操作、流水线寄存器插入
各位同学,欢迎来到第四章。这一章是FPGA设计的「内功心法」。你可能会写Verilog,但能不能写出高性能、高稳定性的硬件,关键就看你对这几个概念的理解深度。
我个人习惯把这一章叫做「从能跑到能跑得快」的分水岭。咱们一个一个来拆解。
4.1 组合逻辑与时序逻辑:硬件的「瞬间」与「节拍」
先问大家一个问题:你写的Verilog代码,综合出来到底是「线」还是「触发器」?
这个问题想明白了,后面所有设计都不会跑偏。
4.1.1 组合逻辑:纯「线」的世界
组合逻辑的输出只取决于当前输入。没有记忆,没有时钟。信号来了,结果瞬间就出来——当然,这个「瞬间」实际上有门延迟。
典型的写法就是 assign 或者 always @(*)。
// 组合逻辑:一个简单的加法器
assign sum = a + b;
// 或者用 always 块
always @(*) begin
result = sel ? data_a : data_b;
end
关键点:组合逻辑里不能出现反馈环路。我曾经在调试一个图像边缘检测模块时,不小心把输出又绕回了输入,结果仿真波形像心电图一样乱跳。查了两天才发现是组合逻辑环路。
=。如果你用了非阻塞赋值 <=,综合工具会报warning,仿真行为也会和你预期的不一样。
4.1.2 时序逻辑:跟着时钟走
时序逻辑就不一样了。它靠时钟沿来「拍照」——只在时钟上升沿(或下降沿)才采样输入,更新输出。这就有了「记忆」。
典型写法是 always @(posedge clk)。
// 时序逻辑:一个D触发器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里有个细节:时序逻辑里必须用 非阻塞赋值 <=。为什么?因为硬件里所有触发器是并行更新的。你用阻塞赋值,仿真行为就会变成串行,和真实硬件对不上。
| 对比项 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 输出依赖 | 仅当前输入 | 当前输入 + 历史状态 |
| 时钟 | 不需要 | 需要时钟沿触发 |
| 赋值方式 | 阻塞赋值 = |
非阻塞赋值 <= |
| 综合结果 | 门电路、多路选择器 | 触发器、寄存器 |
| 典型应用 | 译码器、加法器 | 计数器、状态机 |
4.2 状态机:硬件的「大脑」
状态机是FPGA设计的核心控制单元。说白了,就是让硬件知道「现在该干什么,下一步该干什么」。
我建议初学者从 三段式状态机 入手。为什么?因为可读性强,时序好收敛,不容易出bug。
4.2.1 三段式状态机模板
三段式把状态机的三个功能拆开:
- 第一段:时序逻辑,状态跳转
- 第二段:组合逻辑,次态判断
- 第三段:时序逻辑,输出产生
// 三段式状态机示例
// 第一段:状态寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
// 第二段:次态逻辑(组合)
always @(*) begin
case (state)
IDLE: next_state = start ? WORK : IDLE;
WORK: next_state = done ? IDLE : WORK;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(时序)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_out <= 1'b0;
else if (state == WORK)
data_out <= 1'b1;
else
data_out <= 1'b0;
end
4.3 乒乓操作:让数据「无缝衔接」
乒乓操作是FPGA里非常经典的一种数据流控制技巧。它解决什么问题?说白了就是:让处理单元不空等。
想象一下:你有一个处理模块,处理一帧数据需要10ms。如果只用一块RAM,那这10ms里输入数据只能干等着。乒乓操作就是准备两块RAM,一块在「写」,一块在「读」,交替进行。
// 乒乓操作控制逻辑(简化版)
always @(posedge clk) begin
if (frame_start) begin
wr_flag <= ~wr_flag; // 切换写通道
end
end
// 写通道选择
assign wr_en_a = (wr_flag == 1'b0) ? wr_en : 1'b0;
assign wr_en_b = (wr_flag == 1'b1) ? wr_en : 1'b0;
// 读通道选择
assign rd_en_a = (wr_flag == 1'b1) ? rd_en : 1'b0;
assign rd_en_b = (wr_flag == 1'b0) ? rd_en : 1'b0;
这样做的好处很明显:数据流是连续的,没有间隙。处理模块的利用率接近100%。
4.4 流水线寄存器插入:让时钟「跑得更快」
这是本章的重头戏。很多同学问我:「为什么我的设计综合出来只能跑50MHz,别人的能跑200MHz?」
答案往往就在流水线寄存器插入上。
4.4.1 为什么需要流水线?
组合逻辑路径越长,延迟越大。如果一条路径的延迟超过了时钟周期,时序就违例了。解决思路很简单:在长路径中间插入寄存器,把一个大组合逻辑拆成几个小段。
举个例子,一个4级加法树:
// 无流水线:一个时钟周期内完成4级加法
always @(posedge clk) begin
sum <= a + b + c + d; // 综合后是3级加法器串联
end
// 插入流水线:分3级完成
reg [7:0] sum1, sum2;
always @(posedge clk) begin
sum1 <= a + b; // 第一级
sum2 <= c + d; // 第一级
end
always @(posedge clk) begin
sum <= sum1 + sum2; // 第二级
end
你看,原来一个时钟周期要做完3级加法,现在每级只做1级加法。路径延迟缩短了,最高频率自然就上去了。
4.4.2 流水线的代价
天下没有免费的午餐。流水线插入会带来两个代价:
- 延迟增加:数据从输入到输出需要多等几个时钟周期
- 面积增加:每级流水线都需要额外的寄存器
但在FPGA里,寄存器资源相对丰富,用少量寄存器换更高的工作频率,这笔账通常是划算的。
| 指标 | 无流水线 | 2级流水线 | 4级流水线 |
|---|---|---|---|
| 最大频率 | 100 MHz | 180 MHz | 250 MHz |
| 延迟 | 1 clk | 2 clk | 4 clk |
| 寄存器数 | 8 | 16 | 32 |
| 吞吐率 | 1 数据/clk | 1 数据/clk | 1 数据/clk |
4.5 本章知识体系总览
下面这张图把本章的核心概念串起来了。你可以把它当作一个「设计决策树」:
这张图从Verilog描述出发,分出了组合逻辑、时序逻辑和状态机三条路。每条路都有对应的设计技巧和注意事项。最终,这些技巧都汇聚到流水线寄存器插入这个核心方法上——它是提升FPGA设计性能的终极武器。
好了,这一章的内容就到这里。记住:写Verilog不是写C语言,你脑子里要时刻有一张「硬件电路图」。组合逻辑是线,时序逻辑是触发器,状态机是控制器,乒乓操作是数据调度,流水线是性能加速器。把这五样东西玩明白了,你的FPGA设计水平会上一个大台阶。