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

关键点:组合逻辑里不能出现反馈环路。我曾经在调试一个图像边缘检测模块时,不小心把输出又绕回了输入,结果仿真波形像心电图一样乱跳。查了两天才发现是组合逻辑环路。

⚠ 避坑指南:组合逻辑中,always块内必须使用阻塞赋值 =。如果你用了非阻塞赋值 <=,综合工具会报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 三段式状态机模板

三段式把状态机的三个功能拆开:

  1. 第一段:时序逻辑,状态跳转
  2. 第二段:组合逻辑,次态判断
  3. 第三段:时序逻辑,输出产生
// 三段式状态机示例
// 第一段:状态寄存器
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
💡 个人经验:第三段输出用时序逻辑,可以避免输出毛刺。我在做ISP的自动白平衡状态机时,第二段组合输出导致控制信号有毛刺,摄像头画面偶尔闪一下。改成第三段时序输出后,问题彻底解决。

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%。

🔑 核心要点:乒乓操作的本质是用面积(多一块存储)换带宽(连续处理)。在ISP流水线中,几乎每个模块前后都会用到乒乓RAM。

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
⚠ 注意:流水线寄存器插入后,输出结果会延迟几个时钟周期。如果你的设计有反馈环路(比如IIR滤波器),必须小心处理延迟匹配。我曾经在做一个去噪算法时,忘了补偿流水线延迟,结果反馈回来的数据和当前数据对不上,图像出现了鬼影。

4.5 本章知识体系总览

下面这张图把本章的核心概念串起来了。你可以把它当作一个「设计决策树」:

FPGA设计核心概念体系 Verilog 硬件描述 组合逻辑 时序逻辑 状态机 阻塞 vs 非阻塞赋值 三段式状态机 乒乓操作 消除组合逻辑环路 输出无毛刺 数据流无缝衔接 流水线寄存器插入 最终目标:更高频率 × 更高吞吐率 代价:延迟 + 面积 收益:频率提升

这张图从Verilog描述出发,分出了组合逻辑、时序逻辑和状态机三条路。每条路都有对应的设计技巧和注意事项。最终,这些技巧都汇聚到流水线寄存器插入这个核心方法上——它是提升FPGA设计性能的终极武器。

好了,这一章的内容就到这里。记住:写Verilog不是写C语言,你脑子里要时刻有一张「硬件电路图」。组合逻辑是线,时序逻辑是触发器,状态机是控制器,乒乓操作是数据调度,流水线是性能加速器。把这五样东西玩明白了,你的FPGA设计水平会上一个大台阶。