第三章 FPGA基础与Verilog语法回顾:组合逻辑与时序逻辑、阻塞赋值与非阻塞赋值、状态机设计基础、同步与异步复位
各位同学,欢迎来到第三章。这一章我们不讲新东西,而是把FPGA设计里最基础、也最容易踩坑的几个概念,彻底捋一遍。
说实话,我见过太多新手甚至工作两三年的工程师,在阻塞赋值和非阻塞赋值上翻车。状态机写成一团乱麻的也不在少数。所以这一章,咱们稳扎稳打,把地基打牢。
3.1 组合逻辑与时序逻辑:数字电路的两大基石
FPGA里所有的逻辑,归根结底就两种:组合逻辑和时序逻辑。
组合逻辑,说白了就是“输入一变,输出立马变”。没有记忆功能,不依赖时钟。比如一个与门、一个加法器,都是组合逻辑。
时序逻辑,则是有“记忆”的。它的输出不仅取决于当前输入,还取决于之前的状态。触发器(Flip-Flop)就是最典型的时序逻辑单元。
我个人的习惯是:能用时序逻辑解决的问题,尽量不用纯组合逻辑。为什么?因为组合逻辑容易产生毛刺,而且时序分析不好做。你想想看,一个复杂的组合逻辑路径,延迟可能很大,跑高频的时候很容易出问题。
核心区别速览:
- 组合逻辑:无时钟、无记忆、输出立即响应输入变化
- 时序逻辑:有时钟、有记忆、输出在时钟沿更新
在Verilog里,组合逻辑通常用 assign 连续赋值语句,或者 always @(*) 块来实现。时序逻辑则用 always @(posedge clk) 块来实现。
// 组合逻辑示例:一个简单的加法器
assign sum = a + b;
// 时序逻辑示例:一个D触发器
always @(posedge clk) begin
q <= d;
end
3.2 阻塞赋值与非阻塞赋值:面试必问,实战必坑
这个话题,我每次讲都要多花点时间。因为太重要了,也太容易搞混了。
阻塞赋值(=):它是“立即生效”的。在同一个always块里,前面的赋值会立刻影响后面的赋值。这就像串行执行,一条一条来。
非阻塞赋值(<=):它是“并行生效”的。在同一个always块里,所有赋值语句是同时计算的,但赋值动作要等到块结束时才统一更新。这就像大家同时举手,然后同时放下。
为什么会这样?这是硬件描述语言模拟硬件并行性的需要。
我曾经踩过的坑:
刚入行时,我在一个时序逻辑的always块里用了阻塞赋值,结果仿真波形怎么看怎么不对。折腾了一整天,最后发现是赋值方式用错了。从那以后,我给自己定了个规矩:写时序逻辑,只用非阻塞赋值;写组合逻辑,只用阻塞赋值。这个习惯一直保持到现在。
来看一个对比示例:
// 错误示范:时序逻辑中使用阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里c得到的是a更新后的值,不是硬件想要的并行行为
end
// 正确示范:时序逻辑中使用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里c得到的是a更新前的值,符合硬件并行行为
end
| 赋值类型 | 适用场景 | 执行特点 |
|---|---|---|
| 阻塞赋值(=) | 组合逻辑 | 立即执行,顺序执行 |
| 非阻塞赋值(<=) | 时序逻辑 | 并行计算,统一更新 |
3.3 状态机设计基础:让逻辑“有章可循”
状态机是FPGA设计的灵魂。无论是通信协议、数据通路控制,还是简单的按键消抖,背后都离不开状态机。
我常用的状态机写法是三段式。为什么是三段?因为清晰、好维护、不容易出错。
- 第一段:时序逻辑,描述状态跳转(当前状态 → 次态)
- 第二段:组合逻辑,描述状态跳转条件(次态如何产生)
- 第三段:组合逻辑或时序逻辑,描述输出
嗯,这里要注意:第三段输出,我建议用组合逻辑,这样输出响应快。但如果对毛刺敏感,也可以用寄存器输出。
// 三段式状态机示例(简化版)
// 第一段:状态跳转
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 第二段:次态逻辑
always @(*) begin
case (current_state)
IDLE: if (start) next_state = WORK;
else next_state = IDLE;
WORK: if (done) next_state = DONE;
else next_state = WORK;
DONE: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// 第三段:输出逻辑
assign busy = (current_state == WORK);
我的小技巧:状态编码尽量用独热码(One-Hot)或格雷码(Gray Code)。独热码译码简单,适合状态数不多的场景;格雷码相邻状态只变一位,适合跨时钟域传输。
3.4 同步复位与异步复位:复位方式的选择
复位,是每个数字系统都绕不开的话题。复位方式选不对,轻则浪费资源,重则系统不稳定。
异步复位:复位信号不依赖时钟,只要复位有效,立即复位。优点是响应快,缺点是容易受毛刺影响,且复位释放时可能违反时序约束。
同步复位:复位信号只在时钟沿有效时起作用。优点是抗干扰能力强,时序分析简单,缺点是复位信号宽度必须大于一个时钟周期。
我个人更倾向于异步复位、同步释放的方式。既保留了异步复位响应快的优点,又解决了复位释放时的时序问题。
// 异步复位、同步释放电路
reg rst_sync1, rst_sync2;
always @(posedge clk or posedge async_rst) begin
if (async_rst) begin
rst_sync1 <= 1'b1;
rst_sync2 <= 1'b1;
end else begin
rst_sync1 <= 1'b0;
rst_sync2 <= rst_sync1;
end
end
// 使用 rst_sync2 作为系统的复位信号
避坑指南:
我曾经在一个项目里,为了省事,直接用了外部按键作为异步复位。结果按键抖动导致系统频繁误复位。后来加了一个简单的消抖电路,问题才解决。所以,外部输入的复位信号,一定要做同步处理或消抖。
本章知识体系
下面这张图,把本章的核心知识点串了起来。你可以把它当作一个快速索引。
好了,这一章的内容就到这里。这些基础概念,你可以在后续的实战项目中反复验证。记住,基础不牢,地动山摇。把这一章吃透了,后面的路会顺很多。