第四章 时序逻辑设计:触发器、寄存器、计数器、分频器、状态机基础
各位同学,欢迎来到第四章。这一章我们聊聊时序逻辑。说白了,就是带“记忆”的电路。
组合逻辑大家应该熟悉了,输入一变,输出立马跟着变。但实际工程里,哪有这么简单?信号要同步,数据要暂存,状态要跳转——这些都得靠时序逻辑。我个人习惯把时序逻辑比作“数字电路的心跳”,没有它,整个系统就是一盘散沙。
4.1 触发器:时序逻辑的最小单元
触发器,英文叫 Flip-Flop,简称 FF。它是时序逻辑里最基础、最核心的元件。FPGA 里用的最多的就是 D 触发器。
D 触发器有个特点:在时钟上升沿(或下降沿)到来时,它把输入 D 的值“拍”到输出 Q 上。其他时间,Q 保持不变。这就是“记忆”的本质。
我在项目中遇到过一个问题:一个同事写的代码里,信号赋值没用时钟沿,结果仿真时波形乱跳,板子跑起来完全不对。后来一查,就是没搞清楚组合逻辑和时序逻辑的区别。嗯,这里要特别注意。
核心要点:D 触发器的输出 Q 只在时钟沿变化,其他时间保持。这是所有时序逻辑的基础。
Verilog 里描述一个 D 触发器很简单:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
注意这里用的是 非阻塞赋值 <=,不是 =。为什么?因为时序逻辑里,多个赋值要同时发生,不能有先后顺序。你想想看,如果两个触发器级联,用阻塞赋值就会出问题。
4.2 寄存器:多个触发器打包
寄存器其实就是一组 D 触发器。8 位寄存器就是 8 个 D 触发器并排,共享同一个时钟和复位。说白了,就是“批量处理”。
FPGA 里到处都是寄存器。比如你要存一个 16 位的计数器值,那就得用 16 个触发器。我刚开始做设计时,总觉得寄存器就是“存数据”的,后来才发现,它还能做延迟、做同步、做流水线——功能多着呢。
举个例子,跨时钟域同步:
reg sig_a_d1;
reg sig_a_d2;
always @(posedge clk_b) begin
sig_a_d1 <= sig_a;
sig_a_d2 <= sig_a_d1;
end
这就是用两级寄存器做同步,消除亚稳态。我当年第一次做跨时钟域设计时,没加这玩意儿,结果板子偶尔跑飞,查了三天才找到原因。避坑指南:跨时钟域信号,一定要做同步处理。
4.3 计数器:时序逻辑的“节拍器”
计数器,说白了就是“数数”。但你别小看它,雷达信号处理里,计数器无处不在。脉冲积累、距离门控制、FFT 地址生成——哪样离得开计数器?
一个简单的 8 位计数器:
reg [7:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 8'd0;
else if (cnt == 8'd255)
cnt <= 8'd0;
else
cnt <= cnt + 1'b1;
end
这里有个细节:计数器清零的条件。我见过有人用 cnt == 8'd256 做清零,结果仿真时永远清不了零。为什么?因为 8 位计数器最大值是 255,256 根本不会出现。这种低级错误,我年轻时也犯过。
实战技巧:计数器清零时,建议用“等于最大值”作为条件,而不是“等于目标值+1”。这样更直观,也更容易维护。
在弹载雷达里,计数器经常用来产生各种控制信号。比如,每 1000 个时钟周期产生一个脉冲,用来触发 AD 采样。这种场景,计数器就是“节拍器”。
4.4 分频器:时钟的“减速器”
分频器,就是把高频时钟变成低频时钟。FPGA 里分频有两种方式:
- 偶数分频:用计数器翻转即可
- 奇数分频:需要两个计数器,一个上升沿触发,一个下降沿触发,然后组合
偶数分频最简单。比如 4 分频:
reg clk_div4;
reg [1:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 2'd0;
clk_div4 <= 1'b0;
end
else if (cnt == 2'd1) begin
cnt <= 2'd0;
clk_div4 <= ~clk_div4;
end
else
cnt <= cnt + 1'b1;
end
注意,这里计数器计到 1 就翻转,不是计到 3。为什么?因为 4 分频需要 2 个时钟周期高电平、2 个时钟周期低电平,所以每 2 个时钟翻转一次。嗯,这个逻辑要理清楚。
警告:FPGA 内部尽量不要用分频产生的时钟作为全局时钟。因为分频时钟的占空比和相位可能不稳定,容易导致时序问题。建议用 PLL 或 MMCM 生成多频率时钟。
我记得有一次做项目,为了省资源,直接用分频器产生 10MHz 时钟给 ADC 用。结果 ADC 采样数据老是出错,查了半天才发现是分频时钟抖动太大。后来换成 PLL,问题就解决了。避坑指南:关键时钟,一定要用 PLL。
4.5 状态机:时序逻辑的“大脑”
状态机,英文叫 Finite State Machine,FSM。它是数字系统里最灵活、最强大的设计方法。说白了,就是“根据当前状态和输入,决定下一步做什么”。
状态机分两种:
- Moore 型:输出只取决于当前状态
- Mealy 型:输出取决于当前状态和输入
我个人习惯用 Moore 型,因为输出稳定,不容易出现毛刺。但 Mealy 型响应更快,适合对延迟敏感的场景。
一个简单的状态机例子——雷达脉冲控制:
localparam IDLE = 2'd0;
localparam TX = 2'd1;
localparam RX = 2'd2;
localparam WAIT = 2'd3;
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 = start ? TX : IDLE;
TX: next_state = tx_done ? RX : TX;
RX: next_state = rx_done ? WAIT : RX;
WAIT: next_state = wait_done ? IDLE : WAIT;
default: next_state = IDLE;
endcase
end
// 输出逻辑
always @(*) begin
tx_en = 1'b0;
rx_en = 1'b0;
case (state)
TX: tx_en = 1'b1;
RX: rx_en = 1'b1;
default: ;
endcase
end
这里用了三段式状态机。为什么用三段?因为组合逻辑和时序逻辑分开,代码清晰,综合结果也好。我见过有人把状态机写成一坨 always,结果状态跳转乱成一锅粥。避坑指南:状态机一定要用三段式,这是工程规范。
实战经验:状态机设计时,一定要考虑“非法状态”。如果状态机跑飞了,能不能回到正常状态?建议加一个 default 分支,回到初始状态。这叫“看门狗”思想。
在弹载雷达里,状态机用来控制整个信号处理流程:发射、接收、脉冲压缩、MTI、CFAR……每个阶段都是一个状态。状态机设计得好,整个系统就稳如泰山。
4.6 本章小结
这一章我们讲了时序逻辑的四个基本元件:
| 元件 | 作用 | 典型应用 |
|---|---|---|
| 触发器 | 存储 1 位数据 | 同步、延迟、寄存器 |
| 寄存器 | 存储多位数据 | 数据缓存、流水线 |
| 计数器 | 计数、定时 | 脉冲积累、地址生成 |
| 分频器 | 产生低频时钟 | 时钟生成(建议用 PLL) |
| 状态机 | 控制逻辑 | 流程控制、协议解析 |
这些元件,说白了就是 FPGA 设计的“砖块”。你把这些砖块用好,就能搭出任何复杂的系统。我做了十几年雷达信号处理,回过头来看,最基础的东西往往最重要。
下一章,我们会讲同步设计与异步设计。到时候我会分享一个我当年踩过的坑——异步复位导致的数据丢失。嗯,那是个好故事。
课后练习:设计一个 8 位计数器,每计到 100 时产生一个脉冲,同时清零重新计数。用三段式状态机实现一个“发射-接收-处理”的雷达脉冲控制流程。
好了,这一章就到这里。有什么问题,欢迎在课程群里讨论。记住,FPGA 设计没有捷径,多写代码、多仿真、多调试,才是正道。