4. Verilog基础回顾(二):阻塞与非阻塞赋值、时序逻辑与组合逻辑、状态机基础

好,咱们接着聊。上一章我们把Verilog的基本语法和数据类型过了一遍,这一章要啃的,是真正决定你代码“能不能用”的核心概念。说白了,就是阻塞赋值、非阻塞赋值、时序逻辑、组合逻辑,还有状态机。这些玩意儿,你要是搞混了,仿真跑得再欢,上板子也是一堆bug。我当年刚入行的时候,就在这上面栽过跟头。

4.1 阻塞赋值与非阻塞赋值:一个天一个地

先问个问题:你写Verilog的时候,有没有想过“=”和“<=”到底有啥区别?很多人一开始觉得,不就是赋值嘛,用哪个不一样?嗯,这里要注意,区别大了去了。

阻塞赋值(=),顾名思义,它是“阻塞”的。什么意思?就是它执行的时候,后面的语句都得等着。它是一条一条顺序执行的,跟C语言很像。我习惯把它用在组合逻辑的always块里。

非阻塞赋值(<=),它是“非阻塞”的。它不等着,它把要赋的值先记下来,等整个always块执行完了,再统一更新。这个特性,让它天生适合用来描述时序逻辑,也就是带时钟的寄存器。

核心口诀:

  • 组合逻辑用 =(阻塞赋值)
  • 时序逻辑用 <=(非阻塞赋值)

咱们看个例子,你就明白了。

// 错误示范:时序逻辑里用了阻塞赋值
always @(posedge clk) begin
    a = b;
    c = a;
end
// 仿真结果:a和c同时被更新为b的值,这不是你想要的寄存器链!
// 正确示范:时序逻辑里用非阻塞赋值
always @(posedge clk) begin
    a <= b;
    c <= a;
end
// 仿真结果:c得到的是a的旧值,a得到的是b的旧值,这才是真正的两级寄存器。

我在项目中遇到过,有个同事写了一个很大的状态机,里面全用的阻塞赋值。仿真的时候功能完全正确,结果综合出来,时序一塌糊涂,跑都跑不起来。后来我帮他排查,发现就是赋值方式用错了。你想想看,一个状态跳转,因为阻塞赋值导致信号传递顺序乱了,那不乱套了吗?

警告:千万不要在多个always块中对同一个变量赋值!这是Verilog的大忌,综合工具会报多驱动错误,或者干脆给你综合出个未知结果。

4.2 时序逻辑与组合逻辑:数字电路的两大基石

搞清楚了赋值方式,咱们再来看看这两种逻辑。说白了,数字电路就这两种东西。

组合逻辑:输出只取决于当前的输入。没有记忆功能,输入一变,输出立马跟着变。比如一个与门、一个加法器,都是组合逻辑。

时序逻辑:输出不仅取决于当前的输入,还取决于之前的状态。它有记忆功能,靠的是触发器(Flip-Flop)。比如计数器、寄存器,都是时序逻辑。

在Verilog里,我们怎么描述它们呢?

  • 组合逻辑:用 always @(*) 或者 assign 语句,赋值用 =
  • 时序逻辑:用 always @(posedge clk) 或者 always @(negedge clk),赋值用 <=

我个人的习惯是,能不用always块描述组合逻辑,就尽量不用。用 assign 语句,代码更清晰,也更容易避免一些奇怪的仿真问题。当然,复杂的组合逻辑,比如带优先级的译码器,用always块加case语句会更方便。

小技巧:写组合逻辑的时候,记得把所有敏感信号都列在敏感列表里,或者直接用 always @(*)。否则,仿真和综合结果可能不一致。我曾经就因为漏了一个敏感信号,仿真跑得好好的,综合出来功能全错,查了整整一天。

4.3 状态机基础:数字系统的“大脑”

状态机,说白了,就是让你的电路“有脑子”。它能记住自己当前在干什么,然后根据输入决定下一步要干什么。几乎所有复杂的数字系统,都离不开状态机。

状态机分两种:

  • Moore型:输出只取决于当前状态。
  • Mealy型:输出取决于当前状态和输入。

实际项目中,Moore型用得更多一些,因为它更稳定,不容易出现毛刺。但Mealy型在某些场景下更高效,比如需要根据输入快速改变输出的情况。

写状态机,我推荐三段式写法。为什么?因为它把状态跳转、状态寄存、输出逻辑分得清清楚楚,代码可读性强,也容易维护。

三段式状态机模板:

// 第一段:状态寄存器(时序逻辑)
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: begin
            if (start)
                next_state = WORK;
            else
                next_state = IDLE;
        end
        WORK: begin
            if (done)
                next_state = IDLE;
            else
                next_state = WORK;
        end
        default: next_state = IDLE;
    endcase
end

// 第三段:输出逻辑(组合逻辑或时序逻辑)
always @(*) begin
    case (current_state)
        IDLE:   out = 1'b0;
        WORK:   out = 1'b1;
        default: out = 1'b0;
    endcase
end

你看,这样写,状态机一目了然。第一段管状态跳转,第二段管下一个状态是什么,第三段管输出。出了问题,排查起来也快。

我曾经接手过一个项目,里面的状态机是用一段式写的,所有逻辑都揉在一个always块里。那代码,看着就头疼。后来我花了两天时间,把它重构成了三段式,不仅功能验证通过了,时序也好了很多。

避坑指南:

  • 状态编码尽量用独热码(One-hot)或者格雷码(Gray),别用二进制编码。独热码译码快,格雷码相邻状态跳转时只变化一位,能降低功耗和毛刺。
  • 一定要写default分支,防止状态机进入非法状态后“死机”。
  • 复位信号要可靠,确保状态机在上电后能进入一个已知的初始状态。

4.4 本章知识体系

为了让你更直观地理解这一章的内容,我画了一张图,把阻塞/非阻塞赋值、组合/时序逻辑、状态机之间的关系串了起来。

Verilog基础回顾(二):核心知识体系 赋值方式 阻塞赋值(=) 非阻塞赋值(<=) 组合逻辑 时序逻辑 状态机(三段式) 状态寄存器 (时序逻辑) 次态/输出逻辑 (组合逻辑) 核心原则:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值

这张图把这一章的核心逻辑串起来了。你从上往下看,先搞清楚赋值方式,再理解它们对应的逻辑类型,最后落到状态机这个综合应用上。记住这个脉络,写代码的时候就不会乱。


公众号:蓝海资料掘金营,微信deep3321