2. 硬件描述语言基础(Verilog):模块结构、数据类型、连续赋值与过程块
好,咱们正式开始啃 Verilog 这块硬骨头。很多同学觉得 Verilog 就是写代码,跟 C 语言差不多。嗯,这个想法很危险。我当年刚入行时也这么想,结果写出来的东西综合出来一堆莫名其妙的锁存器,被老工程师骂得狗血淋头。
Verilog 不是软件语言。它描述的是硬件——连线、触发器、组合逻辑。你写的每一行代码,最终都会变成芯片上实实在在的电路。想清楚这个,再往下学。
2.1 模块结构:Verilog 的基本单元
Verilog 里最基本的单位叫 模块(module)。一个模块就是一个硬件功能块,小到一个与门,大到整个 CPU,都是用模块搭起来的。
模块长什么样?看这个例子:
module and_gate (
input wire a, // 输入端口 a
input wire b, // 输入端口 b
output wire y // 输出端口 y
);
assign y = a & b; // 连续赋值:y 等于 a 与 b
endmodule
结构很简单:module 关键字 + 模块名 + 端口列表 + 内部逻辑 + endmodule。端口要声明方向——input、output、inout。这个别搞反了,我见过有人把输出写成输入,仿真没问题,一上板子直接短路。
模块结构要点:
- 每个
.v文件最好只放一个模块,模块名和文件名保持一致 - 端口列表写在括号里,每个端口单独声明方向
- 内部逻辑写在
module和endmodule之间
2.2 数据类型:wire 与 reg 的区别
Verilog 里最常用的两种数据类型是 wire 和 reg。很多新手在这上面栽跟头,包括我。
wire 代表连线。它本身不存储值,只是把两个点连起来。用 assign 赋值的就是 wire。
reg 代表寄存器。它能在时钟沿到来时保持值不变。在 always 块里赋值的变量必须是 reg 类型。
但注意——reg 不一定是真正的寄存器。在组合逻辑的 always 块里,reg 综合出来只是连线。这个坑我踩过,写了个组合逻辑用了 reg,以为会生成触发器,结果综合报告里一个 flip-flop 都没有。
| 特性 | wire | reg |
|---|---|---|
| 默认值 | 高阻态 z | 未知态 x |
| 赋值方式 | 连续赋值 assign | 过程赋值 always |
| 综合结果 | 连线 | 连线 或 触发器 |
| 能否在 always 里赋值 | 不能 | 能 |
我的习惯:组合逻辑用 wire + assign,时序逻辑用 reg + always。这样一眼就能看出代码意图,别人 review 也轻松。
2.3 连续赋值:assign 语句
assign 是 Verilog 里最直观的语句。它描述的是组合逻辑——输入一变,输出立刻跟着变。
assign sum = a + b;
assign full = (a & b) | (cin & (a ^ b));
连续赋值的特点:
- 等号左边必须是 wire 类型
- 等号右边可以是表达式、运算符、函数调用
- 赋值是并行的,没有先后顺序
为什么会强调并行?因为硬件里所有连线都是同时工作的。你写 assign y = a & b,a 和 b 一变,y 立刻更新,不需要等什么时钟。这和软件里一行一行执行完全不同。
我曾经犯过的错:在同一个模块里对同一个 wire 用了两个 assign。综合工具直接报错,说有多驱动源。记住——一根线只能被一个 assign 驱动,否则综合工具不知道听谁的。
2.4 过程块:always 语句
always 块是 Verilog 里最灵活、也最容易出问题的地方。它既能描述组合逻辑,也能描述时序逻辑。区别在于敏感列表怎么写。
2.4.1 组合逻辑的 always 块
always @(*) begin
if (sel)
y = a;
else
y = b;
end
@(*) 表示对所有输入信号敏感。只要 a、b、sel 任何一个变化,这个块就会重新执行。注意这里用的是阻塞赋值 =。
2.4.2 时序逻辑的 always 块
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
这里用了非阻塞赋值 <=。为什么?因为时序逻辑里所有赋值应该同时发生。非阻塞赋值能保证在同一个时钟沿,所有右边的值先被读取,然后再统一更新到左边。这样就不会出现赋值顺序导致的竞争。
黄金法则:
- 组合逻辑 always 块用阻塞赋值
= - 时序逻辑 always 块用非阻塞赋值
<= - 同一个 always 块里不要混用两种赋值
2.5 知识体系总览
下面这张图把本章的核心知识点串起来了。你对照着看,心里就有谱了。
2.6 避坑指南
最后分享几个我这些年踩过的坑,你记下来,能省不少 debug 时间。
- 敏感列表不完整:组合逻辑的 always 块里,如果敏感列表漏了某个输入,综合出来的电路和仿真结果不一致。用
@(*)最保险。 - 锁存器意外生成:在组合逻辑的 always 块里,如果某个分支没给所有输出赋值,综合工具会推断出锁存器。我当年写了一个 case 语句忘了 default,结果多出来 8 个 latch,被 mentor 骂了一周。
- 赋值混用:同一个 always 块里混用阻塞和非阻塞赋值,仿真结果会变得不可预测。记住——要么全用
=,要么全用<=。 - 端口方向搞反:顶层模块的端口方向一定要和测试文件里对应好。我见过有人把时钟写成 output,仿真波形全是高阻态,查了两天才发现。
嗯,这一章的内容就到这。Verilog 基础打牢了,后面写仿真脚本、做联调才能顺手。别急,慢慢来。