3. Verilog基础与RTL设计:组合逻辑与时序逻辑、阻塞与非阻塞赋值、状态机设计(FSM)、同步与异步复位
好,咱们进入第三讲。这一章是RTL设计的基石,说白了就是FPGA工程师的“内功心法”。很多新手写代码,仿真能过,一上板子就挂,十有八九是这里的基本功没打牢。我自己带团队时,面试必问这几个概念,能讲清楚的人,代码质量通常不会差。
3.1 组合逻辑 vs 时序逻辑
这两个概念,我习惯用一个比喻来理解:组合逻辑就像一根导线,输入一变,输出立刻跟着变,没有记忆能力。时序逻辑就像一个小盒子,只在时钟沿到来时才把输入“抓”进去存起来,输出不会乱跳。
在Verilog里,组合逻辑用 assign 或者 always @(*) 来描述。时序逻辑则用 always @(posedge clk) 来描述。记住这个区分,能避免很多低级错误。
核心区别一句话:组合逻辑的输出只取决于当前输入;时序逻辑的输出还取决于之前的状态(靠时钟驱动)。
我在项目中遇到过一位同事,把计数器写成了组合逻辑,结果仿真波形看着对,综合后却出现了一大堆latch。嗯,这里要提醒大家:组合逻辑中如果条件分支不完整,综合工具会推断出锁存器(latch),这通常不是我们想要的。
3.1.1 组合逻辑示例
// 组合逻辑:全加器
module full_adder (
input a, b, cin,
output sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
3.1.2 时序逻辑示例
// 时序逻辑:带同步使能的8位计数器
module counter (
input clk,
input rst_n,
input en,
output reg [7:0] cnt
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 8'd0;
else if (en)
cnt <= cnt + 1'b1;
end
endmodule
3.2 阻塞赋值与非阻塞赋值
这是新手最容易踩的坑,没有之一。我当年刚学Verilog时,也在这个问题上吃过亏。仿真跑得欢,综合出来逻辑全乱套。
规则其实很简单,但需要刻在脑子里:
- 组合逻辑用阻塞赋值
=:因为它是“立即生效”的,符合组合逻辑的直通特性。 - 时序逻辑用非阻塞赋值
<=:因为它是“并行赋值”的,所有赋值在时钟沿同时发生,符合寄存器行为。
警告:千万不要在同一个 always 块里混用 = 和 <=。我曾经见过一个案例,工程师在时序逻辑里用了阻塞赋值,导致仿真和综合结果不一致,查了两天才找到原因。
3.2.1 为什么时序逻辑必须用非阻塞赋值?
你想想看,一个寄存器在时钟沿到来时,它的输入和输出是同时更新的。如果用阻塞赋值,就会变成“先赋值给第一个寄存器,再用更新后的值去赋值给第二个寄存器”,这就变成了串行行为,完全违背了硬件并行工作的本质。
// 错误示例:时序逻辑中使用阻塞赋值
always @(posedge clk) begin
a = b; // 先执行
c = a; // 再执行,此时a已经更新,c得到的是新a的值
end
// 综合后:a和c变成了同一个寄存器,逻辑错误!
// 正确示例:时序逻辑中使用非阻塞赋值
always @(posedge clk) begin
a <= b; // 同时采样
c <= a; // 同时采样,c得到的是a的旧值
end
// 综合后:两个独立的寄存器,行为正确
3.3 状态机设计(FSM)
状态机是数字设计的灵魂。以太网MAC层里,从数据包接收、CRC校验到帧间隙控制,几乎全是状态机在驱动。我个人习惯把状态机分成三段式来写,清晰又好维护。
3.3.1 三段式状态机模板
| 段 | 功能 | 使用赋值 |
|---|---|---|
| 第一段 | 状态转移(时序逻辑) | 非阻塞赋值 <= |
| 第二段 | 次态逻辑(组合逻辑) | 阻塞赋值 = |
| 第三段 | 输出逻辑(组合逻辑或时序逻辑) | 根据需求选择 |
// 三段式状态机示例:简单数据包接收
module fsm_example (
input clk,
input rst_n,
input sop, // 包起始
input eop, // 包结束
output reg data_valid
);
// 状态编码
localparam IDLE = 2'b00;
localparam RECV = 2'b01;
localparam DONE = 2'b10;
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 (sop) next_state = RECV;
RECV: if (eop) next_state = DONE;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
data_valid <= 1'b0;
else if (state == RECV)
data_valid <= 1'b1;
else
data_valid <= 1'b0;
end
endmodule
小技巧:状态编码尽量用 localparam 定义,别用魔法数字。另外,我习惯在 case 里加 default 分支,防止综合出latch。
3.4 同步复位 vs 异步复位
复位策略的选择,直接影响芯片的可靠性和面积。说白了,就是“什么时候复位”和“怎么复位”的问题。
- 同步复位:复位信号只在时钟沿有效。优点是抗毛刺能力强,综合后占用资源少。缺点是复位路径上需要组合逻辑,可能影响时序。
- 异步复位:复位信号立即生效,不依赖时钟。优点是复位速度快,不需要时钟就能把电路拉回初始态。缺点是对毛刺敏感,且存在“恢复时间”和“移除时间”的时序约束问题。
3.4.1 我推荐的做法:异步复位、同步释放
这是业界最常用的方案。既保留了异步复位“立即生效”的优点,又解决了异步复位容易引起亚稳态的问题。说白了,就是复位信号先经过两级同步器,再送给内部逻辑。
// 异步复位、同步释放电路
module rst_sync (
input clk,
input rst_async_n,
output rst_sync_n
);
reg rst_meta, rst_sync;
always @(posedge clk or negedge rst_async_n) begin
if (!rst_async_n) begin
rst_meta <= 1'b0;
rst_sync <= 1'b0;
end
else begin
rst_meta <= 1'b1;
rst_sync <= rst_meta;
end
end
assign rst_sync_n = rst_sync;
endmodule
注意:在 always @(posedge clk or negedge rst_n) 这种写法中,如果复位信号是异步的,一定要保证它满足恢复/移除时间约束。否则,寄存器可能进入亚稳态,导致整个系统行为不可预测。
3.5 本章知识体系图
下面这张图,是我自己总结的RTL设计核心脉络。你可以把它当作一张“思维导图”来用。
这张图把本章的四个核心知识点串在了一起。你写代码时,可以对照着检查:我这段逻辑是组合还是时序?赋值用对了吗?状态机结构清晰吗?复位可靠吗?
好了,这一章的内容就到这里。记住,RTL设计没有捷径,多写、多仿真、多上板验证,才是正道。