第4章:Verilog基础语法:模块化设计、always块、assign语句、阻塞与非阻塞赋值
好,咱们进入正题。这一章讲的是Verilog最核心的几个语法点。说实话,很多新手一上来就被阻塞赋值和非阻塞赋值搞晕了。我当年也一样,第一次写计数器,波形仿真出来全是毛刺,查了半天才发现是赋值方式用错了。
咱们一个一个来。先聊聊模块化设计。
4.1 模块化设计——把大问题拆成小零件
什么叫模块化?说白了,就是搭积木。一个复杂的工业控制系统,你不可能把所有逻辑都塞到一个文件里。那样做,后期维护起来会让人崩溃的。
我在项目中遇到过这样一个案例:一个同事把整个电机控制逻辑写在一个模块里,足足两千行。后来要改一个PWM占空比的计算方式,他找了三天才找到对应的代码段。嗯,这就是没有模块化的后果。
模块化设计有几个好处:
- 可复用:写好的模块,换个项目还能用
- 可调试:哪个模块出问题,单独测它就行
- 可维护:改一个功能,不用动整个工程
一个典型的模块长这样:
module counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 8'd0;
else if (en)
count <= count + 1'b1;
end
endmodule
你看,端口定义清晰,功能一目了然。这就是模块化的精髓——高内聚、低耦合。
4.2 always块——时序逻辑的发动机
always块是Verilog里最常用的过程块。它有两种触发方式:
- 电平触发:用于组合逻辑
- 边沿触发:用于时序逻辑
在工业控制中,我们绝大多数情况用的是边沿触发。你想想看,一个电机转速信号,如果用电平触发,那信号抖动一下,计数器就乱跳了。用边沿触发,只在时钟上升沿采样,稳定得多。
这里有个常见的坑:
// 错误写法:敏感列表不全
always @(a or b) begin
c = a & b;
d = c | e; // e不在敏感列表里!
end
我曾经因为这个bug,整整排查了一个下午。仿真结果和实际硬件对不上,后来发现是敏感列表漏了一个信号。嗯,这里要注意:组合逻辑的敏感列表一定要写全。或者干脆用 always @(*),让工具自动推导。
4.3 assign语句——组合逻辑的快捷方式
assign语句,说白了就是连续赋值。它和always块的区别在于:
| 特性 | assign | always块 |
|---|---|---|
| 适用场景 | 组合逻辑 | 组合/时序逻辑 |
| 赋值方式 | 连续赋值 | 过程赋值 |
| 变量类型 | wire | reg |
| 代码简洁度 | 高 | 低 |
我个人习惯是:简单的组合逻辑用assign,复杂的用always块。比如:
// 简单的数据选择器
assign data_out = (sel) ? data_a : data_b;
// 复杂的译码逻辑
always @(*) begin
case (addr)
3'b000: cs = 8'b0000_0001;
3'b001: cs = 8'b0000_0010;
// ... 更多分支
default: cs = 8'b0000_0000;
endcase
end
4.4 阻塞与非阻塞赋值——最容易翻车的地方
好,重点来了。阻塞赋值(=)和非阻塞赋值(<=)的区别,是每个FPGA工程师必须搞清楚的。
简单说:
- 阻塞赋值(=):顺序执行,后面的语句等前面的执行完
- 非阻塞赋值(<=):并行执行,所有赋值同时发生
为什么会这样?因为硬件本身就是并行的。你想想看,一个D触发器,它的输出是在时钟沿来的时候同时更新的,不是先更新第一个再更新第二个。
看个例子:
// 阻塞赋值——容易出问题
always @(posedge clk) begin
a = b;
c = a; // c得到的是a更新后的值
end
// 非阻塞赋值——正确做法
always @(posedge clk) begin
a <= b;
c <= a; // c得到的是a更新前的值
end
在第一个例子中,c会得到b的值。因为a先被赋值,然后c再被赋值。但在硬件里,这两个操作应该是同时发生的。所以用阻塞赋值,你实际上是在描述一个和硬件行为不一致的逻辑。
- 时序逻辑用非阻塞赋值(<=)
- 组合逻辑用阻塞赋值(=)
- 同一个always块里,不要混用两种赋值方式
我曾经在一个项目里,把非阻塞赋值用在了组合逻辑里。结果仿真没问题,上板子就跑飞了。查了两天才发现,是因为非阻塞赋值在组合逻辑里会产生意想不到的延迟。嗯,从那以后,我再也不敢乱用了。
4.5 实战中的小技巧
最后分享几个我在工业控制项目里常用的技巧:
- 模块接口要规范:所有输入用wire,所有输出用reg。这样别人一看就知道哪个是驱动端,哪个是被驱动端。
- always块里尽量用非阻塞赋值:除非你明确知道自己在做组合逻辑。
- 敏感列表要完整:组合逻辑用
always @(*),时序逻辑用always @(posedge clk or negedge rst_n)。 - 一个always块只做一个功能:比如计数和状态机分开写,别混在一起。
好了,这一章的内容就这些。记住,Verilog语法不难,难的是理解硬件是怎么工作的。你写出来的代码,最终是要变成实实在在的电路。多想想「这个代码会综合成什么电路」,你的水平就会慢慢提升了。