数字逻辑基础回顾与Verilog快速上手
各位同学,欢迎来到第二讲。今天我们要聊的,是FPGA设计里最基础、也最绕不开的几个概念。说实话,很多新手一上来就急着写代码,结果综合出来一堆莫名其妙的bug。我当年也吃过这个亏,所以这一讲,咱们把地基打牢。
核心要点:组合逻辑 vs 时序逻辑、阻塞 vs 非阻塞赋值、同步 vs 异步复位。这三个对比,是FPGA设计的“三岔口”,走错一步,调试三天。
1. 组合逻辑与时序逻辑:一个“即时响应”,一个“听时钟的”
组合逻辑,说白了就是输入一变,输出立刻跟着变。没有记忆,没有状态。比如一个与门,A和B都是1,输出就是1,不需要等任何时钟沿。
时序逻辑就不一样了。它得等时钟沿来了,才把输入“拍”到输出上。我习惯把时序逻辑想象成一个“守门员”——时钟沿就是哨声,哨声不响,球不能进。
为什么会这样?因为时序逻辑里有寄存器(Flip-Flop)。寄存器只在时钟上升沿(或下降沿)采样输入,然后保持住这个值,直到下一个时钟沿。
嗯,这里要注意:组合逻辑容易产生毛刺,时序逻辑天然抗毛刺。我在项目中遇到过,一个组合逻辑的输出直接连到另一个模块的时钟端,结果毛刺导致整个系统误触发。后来我加了一级寄存器做同步,问题就解决了。
| 特性 | 组合逻辑 | 时序逻辑 |
|---|---|---|
| 输出更新 | 立即响应输入变化 | 等待时钟沿 |
| 存储能力 | 无 | 有(寄存器) |
| 典型电路 | 加法器、多路选择器 | 计数器、状态机 |
| 毛刺风险 | 高 | 低 |
2. 阻塞与非阻塞赋值:Verilog里最容易踩的坑
这个知识点,我敢说90%的初学者都搞混过。我自己第一次写代码时,把非阻塞赋值用在了组合逻辑里,结果仿真波形怎么看怎么不对。
先看代码:
// 阻塞赋值(=):顺序执行,像C语言
always @(*) begin
a = b & c;
d = a | e; // 这里用的是更新后的a
end
// 非阻塞赋值(<=):并行执行,所有赋值同时发生
always @(posedge clk) begin
a <= b & c;
d <= a | e; // 这里用的是更新前的a
end
简单记:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。这是铁律,别问为什么,先记住。
我曾经在项目里见过一个同事,在时序逻辑里用了阻塞赋值,结果综合出来的电路比预期多了一级寄存器,时序怎么都跑不满。查了两天才找到原因。
我的个人习惯:写always块之前,先问自己一句“这个块是组合逻辑还是时序逻辑?”然后立刻决定用哪种赋值方式。养成这个习惯,能省掉80%的调试时间。
3. 同步复位与异步复位:复位信号怎么处理?
复位,就是把寄存器清0或置1。但复位信号怎么接,有讲究。
异步复位:复位信号不依赖时钟,只要复位有效,输出立刻变。优点是响应快,缺点是容易受毛刺影响。
同步复位:复位信号只在时钟沿有效。优点是抗干扰,缺点是需要复位信号至少保持一个时钟周期。
看代码对比:
// 异步复位
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
// 同步复位
always @(posedge clk) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
嗯,这里要注意:Xilinx和Altera(Intel)的器件对复位策略的推荐不同。我建议你查一下目标器件的用户指南。我个人习惯用异步复位、同步释放的方式,既保证了快速响应,又避免了毛刺问题。
避坑指南:我曾经在一个项目里,把异步复位信号直接连到了全局复位网络,结果复位信号上有个毛刺,导致部分寄存器被意外复位。后来我加了一个同步器,把异步复位信号同步到时钟域后再使用,问题就解决了。
4. 编写第一个LED闪烁程序并上板验证
理论讲完了,咱们来点实际的。写一个LED闪烁程序,让板子上的LED以1秒为周期闪烁。
思路很简单:用计数器对时钟分频。假设板子上的时钟是50MHz(周期20ns),要产生1秒的周期,需要计数到25,000,000。
module led_blink (
input wire clk,
input wire rst_n,
output reg led
);
reg [24:0] cnt;
// 计数器
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 25'd0;
else if (cnt == 25'd24_999_999)
cnt <= 25'd0;
else
cnt <= cnt + 1'b1;
end
// LED输出
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
led <= 1'b0;
else if (cnt == 25'd24_999_999)
led <= ~led;
else
led <= led;
end
endmodule
代码写完了,怎么上板验证?
- 综合与实现:在Vivado或Quartus里跑一遍,确保没有语法错误和时序违例。
- 分配引脚:把clk接到板子上的时钟引脚,rst_n接到按键,led接到板子上的LED。
- 生成比特流:下载到板子上。
- 观察结果:按下复位键,LED应该开始闪烁。
我第一次上板时,LED死活不亮。检查了半天,发现是复位信号极性搞反了——板子上的复位按键是低电平有效,我代码里写的是高电平有效。嗯,这种低级错误,谁还没犯过几次呢?
调试小技巧:如果LED不闪烁,先检查时钟是否正常。用示波器或逻辑分析仪看时钟引脚,或者写一个更简单的程序——让LED常亮,确认硬件连接没问题。
5. 本章知识体系
下面这张图,是我自己总结的本章知识结构。你把它存下来,以后写代码时对照着看,能少走很多弯路。
好了,这一讲的内容就到这里。记住:组合逻辑、阻塞赋值、同步复位,这三组概念是FPGA设计的基石。你先把LED闪烁程序跑通,后面咱们再聊更复杂的信号处理。
课后作业:修改LED闪烁程序,让LED以0.5秒为周期闪烁。提示:只需要改计数器的比较值。