4、Verilog基础(下):时序逻辑(时钟/复位)、状态机设计、仿真与Testbench
好,咱们接着聊。上一章我们把组合逻辑和基本语法捋了一遍,这一章才是真正让FPGA“活起来”的关键——时序逻辑。说白了,就是让电路有“记忆”,能按着节拍一步步干活。
我个人习惯把时序逻辑比作一支军队:时钟是司令的鼓点,复位是集合号,而状态机就是作战计划。鼓点一响,全军按计划行动。没有时钟,电路就是一盘散沙;没有复位,上电后谁都不知道自己在哪。
核心要点:时序逻辑 = 组合逻辑 + 寄存器(存储单元)。寄存器在时钟沿采样数据,这就是“时序”二字的由来。
4.1 时钟与复位——设计的“心跳”与“归零键”
先说说时钟。在Verilog里,我们通常用 always @(posedge clk) 来描述时序逻辑。这个 posedge 就是时钟的上升沿。为什么用上升沿?因为信号在电平中间跳变最稳定,采样不容易出错。
我在项目中遇到过一个问题:两个模块用了不同相位的时钟,结果数据老是丢。查了半天,原来是跨时钟域没处理好。所以,同一个时钟域内,所有寄存器必须对同一个时钟沿敏感,这是铁律。
// 标准时序逻辑模板
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0; // 异步复位,低电平有效
else
q <= d; // 时钟上升沿采样
end
这里有个细节:复位。我建议用异步复位、同步释放的方式。为什么?纯异步复位容易受毛刺影响,纯同步复位又可能错过复位窗口。我曾经在一个高速设计中吃过这个亏——复位信号刚好在时钟沿附近变化,导致部分寄存器复位了,部分没复位,整个状态机跑飞了。
我的习惯:复位信号 rst_n 用低电平有效,这样符合大多数FPGA器件的内部结构。而且,复位逻辑里一定要把未用到的状态也覆盖到,否则综合时会给你“惊喜”。
4.2 状态机设计——有限状态机(FSM)实战
状态机是时序逻辑的“灵魂”。说白了,就是让电路记住“现在在干什么”,然后根据输入决定“下一步干什么”。
我常用的状态机分三种:Moore型(输出只跟当前状态有关)、Mealy型(输出跟当前状态和输入都有关),以及三段式状态机。我个人强烈推荐三段式,因为它把“状态跳转”、“状态寄存”、“输出逻辑”分得清清楚楚,代码可读性强,也容易维护。
你想想看,如果所有逻辑揉在一个always块里,出错了你找半天都找不到。三段式就不一样,每个部分各司其职。
// 三段式状态机示例:一个简单的序列检测器(检测"101")
// 第一段:状态跳转(组合逻辑)
always @(*) begin
next_state = current_state;
case (current_state)
S0: if (din) next_state = S1;
S1: if (!din) next_state = S2;
S2: if (din) next_state = S3;
S3: next_state = S0;
default: next_state = S0;
endcase
end
// 第二段:状态寄存(时序逻辑)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= S0;
else
current_state <= next_state;
end
// 第三段:输出逻辑(组合逻辑)
assign dout = (current_state == S3) ? 1'b1 : 1'b0;
注意:状态编码时,一定要用 default 把未定义状态引回到安全状态。我曾经见过一个设计,状态机跑到了非法状态,结果输出乱跳,整机复位才恢复。这就是“状态机锁死”,很要命的。
状态编码方式上,我建议小状态机(少于8个状态)用独热码(One-hot),因为译码简单、速度快;大状态机用二进制码,节省寄存器资源。别问我怎么选——看你的资源余量和速度要求。
4.3 仿真与Testbench——不仿真的设计都是耍流氓
好,代码写完了,直接上板子?千万别。我见过太多人“代码写完直接下载”,结果板子冒烟或者没反应。仿真,是验证设计的最后一道防线。
Testbench 说白了就是“给设计喂激励,看它拉什么屎”。它不需要综合,所以你可以用很多高级语法,比如 initial、repeat、$display 等。
// 一个简单的Testbench示例
module tb_fsm_detector();
reg clk, rst_n, din;
wire dout;
// 实例化被测试模块
fsm_detector u_fsm (
.clk (clk),
.rst_n (rst_n),
.din (din),
.dout (dout)
);
// 生成时钟:周期10ns
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 测试激励
initial begin
rst_n = 0;
din = 0;
#20 rst_n = 1; // 复位释放
// 输入序列:1 0 1 0 1 1 0 ...
#10 din = 1;
#10 din = 0;
#10 din = 1; // 此时dout应该变高
#10 din = 0;
#10 din = 1; // 再次检测到
#10 din = 1;
#10 din = 0;
#50 $finish;
end
// 监视输出
initial begin
$monitor("Time=%0t, din=%b, dout=%b", $time, din, dout);
end
endmodule
这里我有个习惯:先写Testbench,再写设计代码。这叫“测试驱动设计”。你想想看,如果你先写设计,再写测试,很容易被自己的设计思路带偏,测不出隐藏bug。反过来,你先想好“我要测什么”,再写设计,思路会清晰很多。
避坑指南:我曾经在仿真时发现波形对了,但上板就是不行。后来发现是Testbench里用了 #1 这种延迟,而实际电路里没有这种“理想延迟”。记住:Testbench里的延迟只用于仿真,不代表实际硬件行为。真正的时序要靠时钟沿来驱动。
4.4 本章知识体系
下面这张图,是我自己总结的时序逻辑设计流程。你照着这个思路走,基本不会出大错。
嗯,这一章的内容就这些。时钟、复位、状态机、仿真,这四个东西是FPGA设计的“四梁八柱”。你把这四个点吃透了,后面学什么高速接口、图像处理,都会轻松很多。
记住一句话:时序逻辑设计,就是跟时间打交道。你尊重时间,时间就给你稳定的电路。你糊弄时间,时间就在板子上给你颜色看。
最后送大家一句话:写代码前先画状态转移图,仿真前先列测试用例。这两步做好了,你的设计成功率至少提高50%。
可可精品资料库