3. Verilog基础语法:模块结构、数据类型、运算符、always块、assign语句
好,咱们直接进入正题。
Verilog 这东西,说白了就是硬件描述语言。它不是软件,是描述电路的语言。你写的每一行代码,最终都会变成实实在在的门电路、触发器和连线。这一点,我希望你从一开始就刻在脑子里。
3.1 模块结构——Verilog的基本单元
一个 Verilog 设计,本质上就是一堆模块的集合。模块就像芯片里的一个功能块,有输入、有输出,内部干点啥。
模块的基本骨架长这样:
module 模块名 (
input 端口1,
output 端口2,
inout 端口3
);
// 内部逻辑写在这里
endmodule
我个人习惯,模块名用大写字母开头,比如 TopModule、DataPath。这样在工程里一眼就能认出是模块。
举个例子,一个最简单的与门模块:
module AndGate (
input a,
input b,
output y
);
assign y = a & b;
endmodule
你看,assign 语句把输出 y 和输入 a、b 的与运算结果连起来了。这就是组合逻辑。
3.2 数据类型——线网与寄存器
Verilog 里最常用的两种数据类型:wire 和 reg。
- wire:线网类型,代表物理连线。它只能被
assign语句驱动,或者被模块端口连接。 - reg:寄存器类型,代表存储单元。它只能在
always块里被赋值。
嗯,这里要注意:reg 不一定真的综合成寄存器。如果你在组合逻辑的 always 块里用 reg,它最终可能只是一根线。这跟软件里的变量完全是两码事。
举个例子:
wire w1; // 一根线
reg r1; // 一个寄存器(也可能是线)
assign w1 = a & b; // 正确,wire 用 assign 驱动
always @(*) begin
r1 = a & b; // 正确,reg 在 always 块里赋值
end
我在项目中遇到过一个问题:有人把组合逻辑里的 reg 当成真正的寄存器,结果综合出来的电路跟他想的不一样。说白了,reg 只是语法上的要求,不代表硬件一定是寄存器。
3.3 运算符——跟C语言很像,但有坑
Verilog 的运算符,大部分跟 C 语言一样。但有几个地方特别容易踩坑。
| 运算符类型 | 符号 | 说明 |
|---|---|---|
| 算术 | + - * / % | 加减乘除取模 |
| 逻辑 | && || ! | 逻辑与、或、非 |
| 按位 | & | ~ ^ | 按位与、或、非、异或 |
| 归约 | & | ^ ~& ~| ~^ | 对向量所有位操作 |
| 移位 | << >> | 左移、右移 |
| 拼接 | { } | 把多个信号拼在一起 |
| 条件 | ? : | 三目运算符 |
你想想看,&& 和 & 有什么区别?前者是逻辑与,结果只有 0 或 1;后者是按位与,结果是对应位相与。我刚开始学的时候,经常把这两个搞混,仿真结果对不上,查了半天才发现是这里写错了。
还有一个常用的技巧:用拼接运算符 {} 来组合信号。比如:
wire [3:0] a = 4'b1010;
wire [3:0] b = 4'b0101;
wire [7:0] c = {a, b}; // c = 8'b10100101
这在做数据打包的时候特别方便。
3.4 assign语句——组合逻辑的直通车
assign 语句,说白了就是给 wire 赋值。它描述的是组合逻辑,没有时钟,没有边沿,输入一变,输出立刻跟着变。
基本用法:
assign 线网名 = 表达式;
比如:
wire [7:0] sum;
assign sum = a + b;
这里 sum 就是 a 和 b 相加的结果。注意,assign 的左边必须是 wire 类型,不能是 reg。
另外,assign 语句是并行执行的。你写 10 条 assign,它们同时生效,没有先后顺序。这一点跟软件完全不一样。
3.5 always块——时序逻辑的核心
always 块是 Verilog 里最强大的结构之一。它既可以描述组合逻辑,也可以描述时序逻辑。
基本语法:
always @(敏感列表) begin
// 逻辑代码
end
敏感列表决定了 always 块什么时候执行。常用的写法:
- 组合逻辑:
always @(*)或always @(a or b or c) - 时序逻辑:
always @(posedge clk)或always @(negedge clk)
举个例子,一个带异步复位的 D 触发器:
reg q;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里用了 <= 非阻塞赋值。为什么用非阻塞?因为在时序逻辑里,非阻塞赋值能保证所有赋值在时钟边沿同时生效,不会出现竞争。我建议你养成习惯:时序逻辑用 <=,组合逻辑用 =。
嗯,这里有个坑:如果你在同一个 always 块里混用阻塞和非阻塞赋值,仿真结果会非常诡异。我曾经因为这个,仿真波形怎么看怎么不对,最后发现是赋值方式用错了。
3.6 知识体系总览
下面这张图,把本章的核心知识点串起来了。你一看就明白。
这张图把模块结构、数据类型、运算符、assign 和 always 块的关系理清楚了。你写代码的时候,脑子里要有这张图。
3.7 避坑指南
最后,分享几个我踩过的坑:
- 多驱动问题:同一个 wire 被多个 assign 驱动,或者被 assign 和 always 同时驱动。综合会报错,仿真结果也不对。
- 敏感列表不完整:组合逻辑的 always 块,如果敏感列表漏了某个输入,仿真结果跟综合结果可能不一致。用
always @(*)可以避免这个问题。 - 阻塞与非阻塞混用:同一个 always 块里,不要同时用
=和<=。仿真结果会让人崩溃。 - 位宽不匹配:赋值时,左右两边位宽不一致,工具会自动截断或扩展。这可能导致意想不到的结果。我建议你显式指定位宽,比如
assign y = {2'b00, a};
好了,这一章的内容就这些。记住,Verilog 是描述硬件的,不是写软件的。多动手写代码,多仿真,多看看综合后的电路图,慢慢就有感觉了。