第四章:Verilog基础(下)——时序逻辑、状态机与Testbench
好,我们接着往下聊。上一章我们把组合逻辑讲透了,这一章轮到时序逻辑了。说白了,时序逻辑就是「带记忆」的电路。你想想看,如果所有逻辑都是组合的,那系统怎么记住当前的状态?怎么知道下一步该做什么?所以,时钟信号和触发器就成了数字系统的「心跳」和「记忆细胞」。
4.1 时序逻辑基础:always @(posedge clk)
在Verilog里,描述时序逻辑最核心的语法就是 always @(posedge clk)。这个块里的代码,只在时钟上升沿触发执行。我刚开始学的时候,总把它和组合逻辑的 always @(*) 搞混,后来踩过坑才真正理解——时序逻辑里赋值要用非阻塞赋值 <=,这是铁律。
核心原则:
- 组合逻辑:
always @(*)+ 阻塞赋值= - 时序逻辑:
always @(posedge clk)+ 非阻塞赋值<=
为什么会这样?因为非阻塞赋值能模拟触发器的行为——所有赋值在时钟沿同时采样、同时更新。如果用阻塞赋值,仿真结果可能对,但综合出来的电路会多出很多不必要的组合逻辑链,甚至产生竞争冒险。我曾经在一个项目里因为混用了赋值方式,导致仿真通过但板子跑起来数据全乱,查了整整两天才找到问题。
来看一个最简单的D触发器例子:
// 一个带同步复位的D触发器
module d_flip_flop (
input wire clk,
input wire rst_n,
input wire d,
output reg q
);
always @(posedge clk) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
endmodule
嗯,这里要注意:复位信号 rst_n 是低电平有效,这是业界惯例。我个人习惯在模块端口命名时,用 _n 后缀表示低有效信号,这样看代码一目了然。
4.2 状态机设计:FSM的三种写法
状态机是FPGA设计的灵魂。量化交易里的指令解析、订单管理、风险控制,说白了都是一堆状态机在跑。我设计过的最复杂的状态机有32个状态,用于处理交易所的FIX协议。
状态机分两种:Moore型(输出只与当前状态有关)和 Mealy型(输出与当前状态和输入都有关)。在交易系统中,我更喜欢用Moore型,因为输出稳定,不会因为输入毛刺而抖动。
标准的FSM写法分三段:
| 段落 | 功能 | 描述方式 |
|---|---|---|
| 第一段 | 状态转移 | 时序逻辑,always @(posedge clk) |
| 第二段 | 次态逻辑 | 组合逻辑,always @(*) |
| 第三段 | 输出逻辑 | 组合逻辑或时序逻辑 |
来看一个简单的交易状态机示例——一个限价单的状态管理:
// 三段式状态机:限价单状态管理
module order_fsm (
input wire clk,
input wire rst_n,
input wire new_order, // 新订单触发
input wire fill_done, // 成交完成
input wire cancel_req, // 取消请求
output reg order_active, // 订单活跃中
output reg order_filled, // 已成交
output reg order_canceled // 已取消
);
// 状态编码
localparam IDLE = 2'b00;
localparam PENDING = 2'b01;
localparam FILLED = 2'b10;
localparam CANCELED = 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
next_state = state; // 默认保持
case (state)
IDLE: if (new_order) next_state = PENDING;
PENDING: begin
if (fill_done) next_state = FILLED;
else if (cancel_req) next_state = CANCELED;
end
FILLED: next_state = FILLED; // 终态
CANCELED: next_state = CANCELED; // 终态
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑)
always @(*) begin
order_active = (state == PENDING);
order_filled = (state == FILLED);
order_canceled = (state == CANCELED);
end
endmodule
我的经验:三段式状态机虽然代码量稍大,但可读性和可维护性最好。在量化交易系统里,状态机经常需要修改——比如增加一个「部分成交」状态。三段式只需要改第二段和第三段,第一段基本不动,不容易引入bug。
4.3 状态编码的选择
状态编码不是随便选的。常用的有三种:
- 二进制编码:状态数少时用,比如4个状态用2位。省寄存器,但组合逻辑复杂。
- 独热码:每个状态用1位。比如4个状态用4位。组合逻辑简单,适合FPGA(LUT资源多)。
- 格雷码:相邻状态只有1位变化。适合跨时钟域的场景。
在交易系统中,如果状态机状态数少于16个,我通常用独热码。为什么?因为FPGA的查找表(LUT)对独热码的译码效率极高,而且时序更容易收敛。我曾经把一个32状态的二进制编码状态机改成独热码,时序余量从-200ps变成了+500ps,效果立竿见影。
避坑指南:千万不要用 casex 或 casez 来写状态机!我曾经接手过一个同事的代码,他用 casex 忽略了某些位,结果仿真和综合行为不一致,板子跑起来状态机乱跳。老老实实用 case,加上 default 分支,把未定义状态导向安全状态(比如IDLE)。
4.4 Testbench编写:让仿真替你说话
写Testbench,说白了就是给设计模块「喂数据、看结果」。我见过很多工程师花大量时间写RTL,却只花10分钟写Testbench,结果上板子一跑就炸。其实,好的Testbench能帮你节省80%的调试时间。
一个标准的Testbench结构:
// 测试上面那个D触发器
`timescale 1ns / 1ps
module tb_d_flip_flop;
reg clk;
reg rst_n;
reg d;
wire q;
// 实例化待测模块
d_flip_flop u_dut (
.clk (clk),
.rst_n (rst_n),
.d (d),
.q (q)
);
// 生成时钟:周期10ns,频率100MHz
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 测试激励
initial begin
// 初始化
rst_n = 0;
d = 0;
#20;
rst_n = 1; // 释放复位
// 测试1:d=1时,q在下一个时钟沿变成1
@(posedge clk);
d = 1;
@(posedge clk);
if (q !== 1) $display("ERROR: q should be 1");
// 测试2:d=0时,q在下一个时钟沿变成0
@(posedge clk);
d = 0;
@(posedge clk);
if (q !== 0) $display("ERROR: q should be 0");
// 测试3:复位测试
@(posedge clk);
rst_n = 0;
@(posedge clk);
if (q !== 0) $display("ERROR: reset failed");
#20;
$finish;
end
// 波形导出(用于GTKWave等工具查看)
initial begin
$dumpfile("tb_d_flip_flop.vcd");
$dumpvars(0, tb_d_flip_flop);
end
endmodule
写Testbench有几个要点:
- 时钟生成:用
forever循环,别用repeat写死,方便改频率。 - 自检机制:用
$display或$error自动比对结果,别靠肉眼看波形。 - 随机测试:用
$random生成随机激励,覆盖边界情况。 - 波形导出:VCD格式通用,GTKWave免费好用。
我的习惯:每个模块我都会写一个「冒烟测试」Testbench,只测最基本的功能。然后再写一个「压力测试」Testbench,用随机激励跑几万拍。在量化交易系统里,我还会加一个「异常测试」——比如同时收到成交和取消信号,看状态机怎么处理。这些测试帮我挡掉了至少5次线上事故。
4.5 实战建议:从仿真到上板
最后,我想分享一个实战流程。我每次开发一个交易算法模块,都会走这几步:
- 写RTL:先画状态转移图,再写代码。别上来就敲键盘。
- 写Testbench:至少覆盖正常路径、异常路径、边界条件。
- 仿真验证:用VCS或ModelSim跑,看波形,确认时序正确。
- 综合与时序分析:看报告,确保setup/hold满足,频率达标。
- 上板调试:用ILA(集成逻辑分析仪)抓内部信号,和仿真对比。
嗯,这一章内容不少。时序逻辑是FPGA设计的基石,状态机是控制逻辑的灵魂,Testbench是验证的保障。把这三点吃透了,你就能写出稳定可靠的交易加速模块。下一章,我们会深入FPGA内部结构,看看LUT、DSP、BRAM这些资源到底怎么用才高效。