第三章 时序逻辑与状态机:让FPGA拥有“记忆”
各位同学,欢迎来到第三章。前两章我们聊了组合逻辑,说白了就是“输入一变,输出立马变”。但现实世界哪有这么简单?你想想看,一个按键按下去,CPU需要判断是“按下”还是“抖动”,这需要时间概念。FPGA要处理图像,一帧一帧的数据流,没有“记忆”怎么行?
这一章,我们就来聊聊FPGA的“记忆”是怎么实现的。触发器、寄存器、状态机、计数器……这些是时序逻辑的基石。我个人习惯把时序逻辑比作“有节拍的舞蹈”,时钟就是那个节拍器。好了,我们开始吧。
3.1 触发器与寄存器:存储的基本单元
先说触发器。FPGA里最常用的就是D触发器。它的核心功能就一句话:在时钟上升沿(或下降沿)到来时,把输入D的值“锁存”到输出Q上。其他时间,Q保持不变。
为什么需要这个?因为组合逻辑的输出有毛刺、有竞争冒险。触发器就像个“采样器”,只在时钟边沿那一刻采样,把干净的数据存下来。我在项目中遇到过,一个图像边缘检测算法,没用寄存器打拍,结果输出全是噪点。后来加了三级寄存器同步,问题就解决了。
多个D触发器并在一起,就是寄存器。比如8位寄存器,就是8个D触发器共享同一个时钟。在Verilog里,我们通常这样写:
// 一个简单的8位寄存器
reg [7:0] data_reg;
always @(posedge clk) begin
data_reg <= data_in; // 非阻塞赋值,注意!
end
这里有个关键点:非阻塞赋值“<=”。我刚开始学的时候,总搞不清“=”和“<=”的区别。简单说:在时序逻辑里,永远用“<=”。它表示“在时钟沿到来时,同时更新所有寄存器”。如果用“=”,可能会产生意想不到的竞争。
3.2 同步复位 vs 异步复位
复位,就是把寄存器清零或置位。FPGA里有两种复位方式:同步复位和异步复位。很多新手会纠结选哪个。我直接说结论:我个人习惯,能用同步复位就用同步复位。
为什么?看代码:
// 同步复位
always @(posedge clk) begin
if (rst_n) begin
data_reg <= 8'b0;
end else begin
data_reg <= data_in;
end
end
// 异步复位
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
data_reg <= 8'b0;
end else begin
data_reg <= data_in;
end
end
异步复位的好处是“立即生效”,不需要等时钟沿。但问题也在这里:复位信号释放时,如果刚好不在时钟沿附近,容易导致寄存器进入亚稳态。我曾经在一个项目中,用了异步复位,结果板子偶尔上电后状态不对。查了两天才发现是复位释放时序有问题。
嗯,这里还要提一句:现在很多FPGA厂商(比如Xilinx)推荐使用同步复位,因为内部资源对同步复位有优化。你想想看,跟着厂商走,一般没错。
3.3 有限状态机(FSM)设计
状态机,是时序逻辑的“大脑”。图像处理里,状态机无处不在:摄像头配置、数据流控制、算法流水线调度……说白了,状态机就是“根据当前状态和输入,决定下一个状态和输出”。
FSM分两种:Moore型和Mealy型。Moore型的输出只取决于当前状态,Mealy型的输出还取决于输入。我个人偏好Moore型,因为输出更稳定,时序容易收敛。但Mealy型有时更节省状态数。
看一个简单的例子:检测串行数据中的“101”序列。
// 三段式状态机写法(推荐)
module fsm_101 (
input clk,
input rst_n,
input din,
output reg dout
);
// 状态编码
localparam IDLE = 2'b00,
S1 = 2'b01,
S2 = 2'b10,
S3 = 2'b11;
reg [1:0] state, next_state;
// 第一段:状态转移
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 = din ? S1 : IDLE;
S1: next_state = din ? S1 : S2;
S2: next_state = din ? S3 : IDLE;
S3: next_state = din ? S1 : S2;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
dout <= 1'b0;
else if (state == S2 && din == 1'b1)
dout <= 1'b1;
else
dout <= 1'b0;
end
endmodule
这里我用了三段式状态机。为什么推荐三段式?因为:
- 第一段:时序逻辑,负责状态更新。简单明了。
- 第二段:组合逻辑,负责计算下一个状态。容易调试。
- 第三段:时序逻辑,负责输出。输出没有毛刺。
我在项目中见过有人用一段式(全写在一个always里),代码是短了,但后期维护简直是噩梦。尤其是状态多了以后,根本分不清是组合逻辑还是时序逻辑的问题。
下面我用一张图来展示状态机的核心逻辑:
这张图展示了状态机的核心:当前状态和输入经过组合逻辑,算出下一状态;时钟沿到来时,下一状态变成新的当前状态。如此循环。
3.4 计数器与分频器
计数器,是状态机的一种特例。它就是个“数数”的电路。但别小看它,图像处理里,行计数、场计数、像素计数,全得靠它。
最简单的计数器:
reg [7:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 8'b0;
else if (cnt == 8'd255)
cnt <= 8'b0;
else
cnt <= cnt + 1'b1;
end
分频器,本质就是计数器。比如你想从50MHz得到1Hz的时钟,那就数50,000,000个周期,然后翻转输出。但注意:在FPGA里,尽量不要用分频后的时钟作为全局时钟。为什么?因为分频时钟的占空比、抖动都不如PLL生成的时钟好。
我曾经在一个项目中,直接用计数器分频做像素时钟,结果图像边缘有重影。后来换成PLL生成时钟,问题就没了。所以我的建议是:需要不同频率的时钟,用PLL或MMCM,别自己分频。计数器用来做使能信号更合适。
好了,这一章的内容就到这里。时序逻辑是FPGA设计的核心,触发器是基础,状态机是骨架,计数器是工具。把这些搞明白,你就能处理大部分图像加速中的控制逻辑了。
可可精品资料库