第三章 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 作为系统的复位信号

避坑指南:

我曾经在一个项目里,为了省事,直接用了外部按键作为异步复位。结果按键抖动导致系统频繁误复位。后来加了一个简单的消抖电路,问题才解决。所以,外部输入的复位信号,一定要做同步处理或消抖

本章知识体系

下面这张图,把本章的核心知识点串了起来。你可以把它当作一个快速索引。

第三章:FPGA基础与Verilog语法回顾 组合逻辑 vs 时序逻辑 • 组合:无记忆,输入变输出立即变 • 时序:有记忆,时钟沿更新 • 组合用 assign / always @(*) • 时序用 always @(posedge clk) 阻塞赋值 vs 非阻塞赋值 • 阻塞(=):立即生效,顺序执行 • 非阻塞(<=):并行计算,统一更新 • 时序逻辑用 <=,组合逻辑用 = • 混用会导致仿真与综合不一致 状态机设计基础 • 三段式:状态跳转 + 次态 + 输出 • 编码:独热码 / 格雷码 • 注意:避免产生锁存器 • 推荐:组合逻辑输出,响应快 同步复位 vs 异步复位 • 异步:响应快,但易受毛刺影响 • 同步:抗干扰强,但需满足脉宽 • 推荐:异步复位,同步释放 • 外部复位必须做同步/消抖 图:本章知识体系结构图

好了,这一章的内容就到这里。这些基础概念,你可以在后续的实战项目中反复验证。记住,基础不牢,地动山摇。把这一章吃透了,后面的路会顺很多。


专注资料整理