3. Verilog基础语法(上):模块结构、wire与reg、always块、assign语句、阻塞与非阻塞赋值
各位同学好,今天我们正式开始啃Verilog语法这块硬骨头。说实话,很多初学者觉得语法枯燥,但我要告诉你——在CPLD/FPGA的世界里,语法就是你的螺丝刀和焊枪。搞懂了今天这五个核心概念,你就能看懂八成以上的电机控制代码。
3.1 模块结构——你的第一个“黑盒子”
Verilog里最基本的单元叫模块(module)。你可以把它想象成一个黑盒子:外面能看到的是引脚(端口),里面装的是逻辑电路。
一个标准的模块长这样:
module motor_controller (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire [7:0] speed,// 速度给定
output reg [7:0] pwm // PWM输出
);
// 这里写逻辑代码
endmodule
我个人习惯把端口声明和变量声明分开写,这样代码结构更清晰。注意看,input和output后面跟的是数据类型——这就是我们接下来要说的wire和reg。
pwm_generator而不是PWMGenerator。这在大型项目中能避免很多大小写敏感的问题。
3.2 wire与reg——连线 vs 寄存器
这两个数据类型是新手最容易搞混的。我简单总结一下:
- wire:相当于一根导线。它不存储值,只是把某处的信号“连”到另一处。
- reg:相当于一个存储单元。它会在时钟沿到来时“记住”当前值。
但这里有个坑——很多教材说“reg就是寄存器”,其实不完全对。在always块里赋值的变量必须声明为reg,哪怕它综合出来只是一根线。反过来,assign语句赋值的变量必须是wire。
我曾经在项目里犯过这个错:把一个计数器输出声明成wire,结果综合报错报了半天。后来养成习惯——always块里用reg,assign语句用wire,再也没出过问题。
| 数据类型 | 赋值方式 | 综合结果 | 典型用途 |
|---|---|---|---|
| wire | assign 连续赋值 | 连线或组合逻辑 | 模块间连接、组合逻辑输出 |
| reg | always 过程赋值 | 寄存器或组合逻辑 | 时序逻辑、状态机、计数器 |
3.3 always块——时序逻辑的“心脏”
always块是Verilog里最常用的过程块。它有两种典型写法:
组合逻辑写法:
always @(*) begin
// 敏感列表用 * 表示所有输入信号
// 这里写组合逻辑
end
时序逻辑写法:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
// 复位操作
end else begin
// 时钟上升沿触发的操作
end
end
嗯,这里要注意:敏感列表一定要写全。如果你写组合逻辑时漏了某个输入信号,综合出来的电路和你想的会完全不一样。我见过一个同事因为敏感列表少写了a,结果仿真和实测对不上,查了两天才找到原因。
@(posedge clk),但里面却用了组合逻辑赋值。结果综合工具给我生成了一个巨大的锁存器,直接把芯片资源吃光了。记住:时序逻辑用posedge/negedge,组合逻辑用@(*)。
3.4 assign语句——连续赋值的“直通车”
assign语句用来描述组合逻辑。它的特点是:只要右边表达式有任何变化,左边立即更新。
assign pwm_out = (counter < duty) ? 1'b1 : 1'b0;
这行代码的意思是:当计数器值小于占空比时,pwm_out输出高电平,否则输出低电平。你看,这就是一个典型的PWM生成逻辑,用assign一句话就搞定了。
我个人习惯把简单的组合逻辑用assign写,复杂的才放到always块里。这样代码读起来更清爽,也更容易定位问题。
3.5 阻塞与非阻塞赋值——时序的“分水岭”
这是Verilog里最容易被误解的概念,也是面试必考题。简单说:
- 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的。
- 非阻塞赋值(<=):并行执行,所有赋值同时生效。
为什么会有这种区别?因为硬件电路里,寄存器是在时钟沿同时更新的,不是一条一条顺序执行的。
看个例子你就明白了:
// 阻塞赋值——错误用法
always @(posedge clk) begin
a = b;
c = a; // 这里c得到的是a更新后的值
end
// 非阻塞赋值——正确用法
always @(posedge clk) begin
a <= b;
c <= a; // 这里c得到的是a更新前的值
end
我曾经在电机速度环里用阻塞赋值写了一个移位寄存器,结果仿真波形完全不对。查了半天才发现——阻塞赋值让数据在一个时钟周期内“穿透”了多个寄存器,相当于把流水线变成了直通线。从那以后,我给自己定了个规矩:时序逻辑一律用非阻塞赋值,组合逻辑才用阻塞赋值。
黄金法则:
- 写时序逻辑(always @(posedge clk))→ 用 <=(非阻塞)
- 写组合逻辑(always @(*) 或 assign)→ 用 =(阻塞)
- 同一个always块里不要混用两种赋值方式
3.6 知识体系总览
为了让你更直观地理解今天讲的内容,我画了一张图:
这张图把今天讲的五个知识点串起来了。你看,模块结构是外壳,wire/reg是数据类型,always和assign是两种赋值方式,阻塞/非阻塞是时序控制的核心。搞懂这些,你就掌握了Verilog的“骨架”。
好了,今天的内容就到这里。记住:理论要结合实践。我建议你打开开发环境,把今天讲的每个例子都敲一遍,看看仿真波形。只有亲手试过,才能真正理解这些概念。
课后练习:
- 写一个模块,用assign实现一个4位加法器
- 用always块写一个8位计数器,带异步复位
- 对比阻塞和非阻塞赋值在移位寄存器中的区别
公众号:蓝海资料掘金营,微信deep3321