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 本章知识体系
为了让你更直观地理解这一章的内容,我画了一张图,把阻塞/非阻塞赋值、组合/时序逻辑、状态机之间的关系串了起来。
这张图把这一章的核心逻辑串起来了。你从上往下看,先搞清楚赋值方式,再理解它们对应的逻辑类型,最后落到状态机这个综合应用上。记住这个脉络,写代码的时候就不会乱。
公众号:蓝海资料掘金营,微信deep3321