数字逻辑基础回顾与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

代码写完了,怎么上板验证?

  1. 综合与实现:在Vivado或Quartus里跑一遍,确保没有语法错误和时序违例。
  2. 分配引脚:把clk接到板子上的时钟引脚,rst_n接到按键,led接到板子上的LED。
  3. 生成比特流:下载到板子上。
  4. 观察结果:按下复位键,LED应该开始闪烁。

我第一次上板时,LED死活不亮。检查了半天,发现是复位信号极性搞反了——板子上的复位按键是低电平有效,我代码里写的是高电平有效。嗯,这种低级错误,谁还没犯过几次呢?

调试小技巧:如果LED不闪烁,先检查时钟是否正常。用示波器或逻辑分析仪看时钟引脚,或者写一个更简单的程序——让LED常亮,确认硬件连接没问题。

5. 本章知识体系

下面这张图,是我自己总结的本章知识结构。你把它存下来,以后写代码时对照着看,能少走很多弯路。

数字逻辑基础与Verilog快速上手 组合逻辑 vs 时序逻辑 组合:即时响应,无记忆 时序:时钟沿触发,有记忆 毛刺:组合高,时序低 阻塞 vs 非阻塞赋值 阻塞(=):顺序执行 非阻塞(<=):并行执行 组合用=,时序用<= 同步 vs 异步复位 异步:不依赖时钟 同步:只在时钟沿有效 推荐:异步复位同步释放 实践:LED闪烁程序 计数器分频 → 寄存器输出 → 上板验证

好了,这一讲的内容就到这里。记住:组合逻辑、阻塞赋值、同步复位,这三组概念是FPGA设计的基石。你先把LED闪烁程序跑通,后面咱们再聊更复杂的信号处理。

课后作业:修改LED闪烁程序,让LED以0.5秒为周期闪烁。提示:只需要改计数器的比较值。

专注资料整理