4、时序逻辑设计:触发器、寄存器、计数器、分频器、状态机基础
说实话,时序逻辑是FPGA设计的灵魂。组合逻辑只管当前输入,但时序逻辑能记住过去的状态。你想想看,交易所的协议解析,哪一步不需要记住前面收到了几个字节、当前在哪个状态?没有时序逻辑,一切都白搭。
我个人习惯把时序逻辑分成几个层次来理解。最底层是触发器,往上堆成寄存器,再往上就是计数器、分频器这些功能模块,最后是状态机这个集大成者。咱们一层层剥开来看。
4.1 触发器——时序逻辑的细胞
触发器,说白了就是能存1位数据的单元。FPGA里最常用的是D触发器。它的核心行为很简单:每个时钟上升沿,把输入D的值锁存到输出Q上。
我在项目中遇到过一个问题:两个模块之间跨时钟域传数据,直接连D触发器就出错了。为什么?因为D触发器对建立时间和保持时间有要求。信号变化太快,触发器就进入亚稳态,输出既不是0也不是1,像个墙头草一样摇摆不定。
// 一个标准的D触发器描述
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
注意这里的 non-blocking assignment (<=)。我刚开始学的时候老用阻塞赋值 =,结果仿真波形一塌糊涂。记住:时序逻辑用非阻塞赋值,这是铁律。
避坑指南:我曾经在同一个always块里混用阻塞和非阻塞赋值,综合出来的电路和仿真完全对不上。花了整整两天才定位到问题。从此以后,我坚持一个原则——时序逻辑只用非阻塞赋值,组合逻辑只用阻塞赋值,绝不混用。
4.2 寄存器——多位触发器的集合
寄存器就是多个D触发器并排站。8位寄存器就是8个触发器,16位就是16个。在FPGA里,寄存器是分布在各处的,不像ASIC那样有专门的寄存器阵列。
交易所协议解析里,寄存器用得最多的场景是:缓存一个数据包、保存当前解析到的偏移量、记录校验和累加值。比如解析UDP包时,我需要一个16位寄存器来暂存源端口号。
reg [15:0] src_port; // 16位寄存器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
src_port <= 16'd0;
else if (data_valid && (byte_cnt == 0))
src_port[15:8] <= data_in; // 高字节
else if (data_valid && (byte_cnt == 1))
src_port[7:0] <= data_in; // 低字节
end
你想想看,如果不用寄存器,你怎么记住第一个字节是什么?组合逻辑可没有记忆能力。
4.3 计数器——时序逻辑的节拍器
计数器本质上就是寄存器加加法器。每个时钟周期加1,加到某个值就归零。就这么简单,但用处太大了。
我做过一个项目,需要精确测量两个脉冲之间的时间差。用了一个32位计数器,时钟是200MHz,精度5纳秒。结果测出来和示波器对得上,心里那个爽。
reg [7:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 8'd0;
else if (cnt == 8'd99) // 计数到99归零
cnt <= 8'd0;
else
cnt <= cnt + 1'b1;
end
经验之谈:计数器宽度要留够余量。我曾经算出来最大需要1000,就用了10位计数器(最大1023)。结果需求变更,需要计到1200,只能改代码重新综合。现在我的习惯是:多留2位,反正FPGA里寄存器多的是。
4.4 分频器——从高频到低频
分频器就是特殊的计数器。FPGA开发板上通常只有一个晶振,比如50MHz。但你的设计可能需要25MHz、10MHz、1Hz等各种时钟。怎么办?分频。
偶数分频最简单:计数器计到N/2-1就翻转输出。比如50MHz分频到25MHz,计到1就翻转。
reg clk_div2;
reg [1:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 2'd0;
clk_div2 <= 1'b0;
end else begin
cnt <= cnt + 1'b1;
if (cnt == 2'd1) begin
clk_div2 <= ~clk_div2;
cnt <= 2'd0;
end
end
end
奇数分频稍微麻烦点,需要同时用上升沿和下降沿触发。我建议新手直接用PLL(锁相环)做分频,Xilinx和Altera都有现成的IP核,参数一配就行,比自己写的分频器稳定得多。
核心要点:分频得到的时钟,和原始时钟不是同源的。跨时钟域时一定要做同步处理。我见过有人直接把分频时钟当全局时钟用,结果时序收敛不了,整个项目重做。
4.5 状态机——时序逻辑的指挥官
状态机是时序逻辑的集大成者。它把组合逻辑和时序逻辑完美结合,用来控制复杂的流程。交易所协议解析,本质上就是一个巨大的状态机。
我个人习惯用三段式状态机。为什么?因为可读性强,维护方便,综合出来的电路也清晰。
// 三段式状态机示例
// 第一段:状态转移
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 ? HEADER : IDLE;
HEADER: next_state = header_done ? PAYLOAD : HEADER;
PAYLOAD:next_state = payload_done ? CHECKSUM : PAYLOAD;
CHECKSUM:next_state = IDLE;
default:next_state = IDLE;
endcase
end
// 第三段:输出逻辑(组合逻辑)
always @(*) begin
case (state)
IDLE: data_valid = 1'b0;
HEADER: data_valid = 1'b0;
PAYLOAD: data_valid = 1'b1;
CHECKSUM:data_valid = 1'b0;
default: data_valid = 1'b0;
endcase
end
我在解析金融交易协议时,状态机通常有十几个状态:等待同步头、解析消息类型、解析消息长度、解析消息体、校验和验证等等。每个状态对应一个处理阶段,清晰得很。
避坑指南:我曾经写过一个两段式状态机,把次态逻辑和输出逻辑混在一起。结果后来要加一个新状态,改了一处忘了改另一处,仿真通过但上板就跑飞。从那以后,我坚持三段式,虽然代码多几行,但心里踏实。
4.6 本章知识体系
下面这张图是我画的时序逻辑知识体系,你看一眼就能明白各个概念之间的关系。
从这张图可以看得很清楚:触发器是最基本的单元,多个触发器组成寄存器,寄存器加上加法器就是计数器,计数器的一种特殊用法就是分频器。而状态机,则是把这些东西全部整合起来,形成完整的控制逻辑。
嗯,时序逻辑这块内容就这些。说白了就是记住过去、控制未来。你在做交易所协议解析时,每个数据包的解析过程,本质上都是在和时序逻辑打交道。把触发器、寄存器、计数器、分频器、状态机这五样东西吃透了,FPGA设计的大门就算真正迈进去了。
总结一下:时序逻辑的核心是「记忆」。触发器记1位,寄存器记多位,计数器记数值,分频器记周期,状态机记状态。层层递进,缺一不可。
公众号:蓝海资料掘金营,微信deep3321