第三讲:Verilog基础与组合逻辑——模块化设计、assign语句、always块、阻塞与非阻塞赋值
各位同学,欢迎来到实战课第三讲。今天咱们聊点硬核的——Verilog里最常用的几个基础结构。说实话,很多新手觉得Verilog就是写代码,跟C语言差不多。嗯,我当年也这么想,结果第一次写出来的模块,仿真怎么跑都不对。后来才明白,Verilog不是“程序”,它是“电路描述”。你写的是硬件,不是软件。
这一讲,我会带着大家把模块化设计、assign语句、always块、阻塞与非阻塞赋值这几个核心概念彻底讲透。每个知识点我都会结合自己踩过的坑来讲,希望能帮你少走弯路。
一、模块化设计——把大电路拆成小积木
模块化设计,说白了就是“分而治之”。一个复杂的交易系统,你不可能在一个文件里写完所有逻辑。我习惯把功能拆成独立的模块,每个模块只干一件事。
举个例子,一个简单的加法器模块:
module adder (
input [7:0] a,
input [7:0] b,
output [8:0] sum
);
assign sum = a + b;
endmodule
你看,这个模块只有三行核心代码。它只做加法,别的什么都不管。这就是模块化的精髓——高内聚、低耦合。
模块化设计的好处很明显:
- 可复用——写好的模块可以在多个项目里直接用
- 易调试——出问题了,定位到具体模块就行
- 好维护——改一个模块不影响其他部分
我在做量化交易系统的行情解析模块时,就把整个模块拆成了:数据接收、协议解析、校验、缓存四个子模块。每个子模块独立测试,最后再拼起来。这样出了问题,一眼就能看出是哪个环节的锅。
二、assign语句——组合逻辑的“直通车”
assign语句,是Verilog里描述组合逻辑最直接的方式。它的意思就是“持续赋值”——只要右边信号变了,左边立刻跟着变。
来看个例子:
module mux2to1 (
input a,
input b,
input sel,
output y
);
assign y = sel ? a : b;
endmodule
这个二选一多路选择器,用assign一行就搞定了。sel为1时输出a,为0时输出b。注意,这里没有时钟,没有时序,纯组合逻辑。
assign语句可以描述各种组合逻辑:
- 算术运算:加法、减法、乘法
- 逻辑运算:与、或、非、异或
- 位运算:按位与、按位或、移位
- 条件运算:三目运算符 ? :
我个人建议,能用assign描述的逻辑,尽量用assign。它简单、直观、不容易出错。只有遇到复杂的组合逻辑,才考虑用always块。
三、always块——组合逻辑的另一种写法
always块是Verilog里最灵活的语句块。它既可以描述组合逻辑,也可以描述时序逻辑。今天我们先讲组合逻辑的用法。
组合逻辑的always块,写法是这样的:
module and_gate (
input a,
input b,
output reg y
);
always @(*) begin
y = a & b;
end
endmodule
注意几个关键点:
- 敏感列表用
@(*),表示“所有输入信号变化都触发” - 输出变量要声明为
reg类型 - 块内使用阻塞赋值
=
你可能会问:为什么组合逻辑要用reg类型?这不是时序逻辑才用的吗?嗯,这个问题我当年也困惑过。其实,Verilog里的reg只是表示“变量”,不一定对应寄存器。在组合逻辑的always块里,reg只是用来保存中间结果的变量,综合后还是组合电路。
我曾经在一个项目里,写了一个组合逻辑的always块,忘了给某个分支赋值。结果综合出来一堆锁存器,功耗和面积都超标。后来花了整整两天才排查出来。从那以后,我每次写完always块,都会检查一遍:每个分支是不是都赋值了?
四、阻塞赋值与非阻塞赋值——一字之差,天壤之别
这是Verilog里最坑的地方,没有之一。阻塞赋值用 =,非阻塞赋值用 <=。看起来就差一个符号,但行为完全不同。
| 特性 | 阻塞赋值 (=) | 非阻塞赋值 (<=) |
|---|---|---|
| 执行顺序 | 立即执行,阻塞后续语句 | 并行执行,不阻塞后续语句 |
| 使用场景 | 组合逻辑 | 时序逻辑 |
| 综合结果 | 组合电路 | 寄存器/触发器 |
| 典型错误 | 在时序逻辑中用阻塞赋值 | 在组合逻辑中用非阻塞赋值 |
来看个对比的例子:
// 错误写法:时序逻辑里用阻塞赋值
always @(posedge clk) begin
a = b;
c = a; // 这里c拿到的是a的新值,不是旧值
end
// 正确写法:时序逻辑里用非阻塞赋值
always @(posedge clk) begin
a <= b;
c <= a; // 这里c拿到的是a的旧值,符合寄存器行为
end
为什么会有这种区别?因为硬件里,寄存器是边沿触发的。在同一个时钟沿,所有寄存器同时采样输入,同时更新输出。非阻塞赋值正好模拟了这个行为——先计算所有右边的值,再统一赋值给左边。
我记得有一次,帮同事调试一个交易系统的订单处理模块。他写了一个状态机,时序逻辑里用了阻塞赋值。仿真跑得好好的,但上板子就不对。我一看代码,立马发现问题。改成非阻塞赋值后,一切正常。他当时感叹:原来坑在这里。
五、知识体系总览
下面这张图,是我自己总结的本章知识体系。你可以把它当作一个思维导图来看:
这张图把今天讲的内容串起来了。模块化设计是架构层面的思想,assign和always是实现手段,阻塞与非阻塞赋值是具体细节。三者层层递进,缺一不可。
六、避坑指南
最后,我把自己这些年踩过的坑总结一下,希望能帮你避开:
- 组合逻辑里用非阻塞赋值——仿真可能对,但综合出来的电路行为不对。我曾经因为这个,浪费了整整一周。
- always块里漏掉分支——会生成锁存器。我的习惯是,写完后用lint工具检查一遍。
- 模块端口太多——超过10个端口,就该考虑拆分了。我见过一个模块有30多个端口,那代码简直没法看。
- assign和always混用——同一个信号,不要在assign里赋值,又在always里赋值。这样会报多驱动错误。
好了,这一讲的内容就到这里。记住我今天讲的这些原则,写代码的时候多想想:这是组合逻辑还是时序逻辑?该用阻塞还是非阻塞?模块是不是该拆分了?养成好习惯,后面会少很多麻烦。