3. Verilog基础语法(上):模块结构、数据类型、运算符、always块与assign语句
各位同学,欢迎来到实战课的第一章。说实话,每次讲Verilog基础语法,我都想起自己刚入行时踩过的坑。那时候总觉得语法简单,结果写出来的代码要么综合不过,要么仿真对不上。嗯,今天咱们就把这些基础打扎实了。
3.1 模块结构——Verilog的骨架
Verilog里最基本的单元就是模块(module)。你可以把它想象成一个黑盒子:有输入、有输出,里面装着逻辑。我习惯把模块比作一个芯片的“功能块”,每个模块完成一件事。
一个标准的模块长这样:
module counter (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
output reg [3:0] count // 4位计数器输出
);
// 内部逻辑写在这里
endmodule
注意看,module和endmodule是成对出现的。端口列表里,我习惯把输入写在前面,输出写在后面。这不是强制要求,但团队协作时,统一的风格能省不少事。
3.2 数据类型——wire和reg的区别
这是新手最容易搞混的地方。说白了,wire就是一根导线,reg就是一个寄存器。但注意,这里的“寄存器”不是硬件里的寄存器,而是Verilog里的一个变量类型。
| 类型 | 特点 | 典型用法 |
|---|---|---|
| wire | 组合逻辑,连续赋值 | assign语句、模块端口连接 |
| reg | 时序逻辑,过程赋值 | always块内赋值 |
我曾经犯过一个低级错误:在always块里给wire赋值,结果综合报错。后来才明白,wire只能用assign驱动,reg才能在always里用。
input wire或output reg。我建议你养成这个习惯,代码更清晰。
3.3 运算符——别小看这些符号
Verilog的运算符和C语言很像,但有几个坑要注意。我列一下常用的:
- 算术运算符:+、-、*、/。注意除法在FPGA里很耗资源,能用移位就别用除法。
- 位运算符:&、|、^、~。这些是逐位操作的。
- 逻辑运算符:&&、||、!。结果是布尔值(0或1)。
- 关系运算符:>、<、>=、<=。注意
<=也是非阻塞赋值,别搞混了。 - 移位运算符:<<、>>。左移一位相当于乘2,右移一位相当于除2。
举个例子,你想想看:
wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b1100;
// 位运算
wire [3:0] c = a & b; // 结果是 4'b1000
// 逻辑运算
wire d = a && b; // 结果是 1(因为a和b都不为0)
为什么会这样?因为位运算是逐位比较,逻辑运算是把整个数当成布尔值。我在项目中见过有人把&和&&混用,仿真没问题,综合出来逻辑全错了。
3.4 always块——时序逻辑的核心
always块是Verilog里最强大的结构之一。它有两种主要用法:
组合逻辑:
always @(*) begin
// 敏感列表用 *,表示所有输入信号变化都触发
if (sel)
out = a;
else
out = b;
end
时序逻辑:
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else
count <= count + 1'b1;
end
注意看,时序逻辑里我用的是<=(非阻塞赋值),组合逻辑里用的是=(阻塞赋值)。这个区别很重要。我曾经在项目里把两者混用,结果仿真波形怎么看都不对,折腾了两天才发现是赋值方式的问题。
3.5 assign语句——连续赋值的艺术
assign语句用来描述组合逻辑。它和always块的区别在于:assign是连续赋值,只要右边变了,左边立刻更新;always块是过程赋值,需要敏感列表触发。
举个例子:
// 用assign实现一个2选1多路选择器
wire [3:0] mux_out;
assign mux_out = sel ? a : b;
这比用always块写简洁多了。我个人的习惯是:简单的组合逻辑用assign,复杂的用always @(*)。这样代码可读性更好。
3.6 知识体系总览
下面这张图是我自己总结的Verilog基础语法结构,你可以把它当作学习地图:
这张图把本章的核心内容串起来了。模块是骨架,数据类型是血肉,运算符是工具,always和assign是表达逻辑的方式。你想想看,写Verilog其实就是用这些元素搭积木。
好了,这一章的内容就到这里。记住,基础语法虽然简单,但它是你后续写复杂逻辑的根基。我建议你每学一个知识点,都打开仿真工具跑一遍,亲眼看到波形变化,印象才深刻。