第三讲:硬件描述语言基础(Verilog)——模块结构、端口定义、数据类型与过程块
各位同学,欢迎来到第三讲。从这一章开始,我们正式进入Verilog的世界。说实话,我刚开始学Verilog的时候,觉得它跟C语言长得挺像,心想这有什么难的?结果一上手就踩了不少坑。今天这一讲,我会把最基础、也最容易出错的地方掰开揉碎了讲清楚。
3.1 模块结构——Verilog的基本单元
Verilog里所有的设计,都是以模块(module)为单位的。你可以把模块想象成一个黑盒子,有输入、有输出,里面装着具体的逻辑。
一个完整的模块长这样:
module counter (
input wire clk,
input wire rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0000;
else
count <= count + 1'b1;
end
endmodule
嗯,这里要注意:module和endmodule必须成对出现,就像括号一样。我见过不少新手把endmodule拼成end,或者干脆忘了写——这种错误在综合时会报一堆莫名其妙的错误,排查起来很头疼。
3.2 端口定义——模块的“接线端子”
端口就是模块跟外界通信的接口。说白了,就是输入、输出、双向三种。
| 端口类型 | 关键字 | 说明 |
|---|---|---|
| 输入 | input |
只能从外部驱动,内部只能读 |
| 输出 | output |
只能从内部驱动,外部只能读 |
| 双向 | inout |
既可以输入也可以输出(三态门) |
我个人习惯把端口声明和数据类型写在一起,比如output reg [3:0] count。这样一眼就能看出端口的方向和类型,不用来回翻代码。你想想看,如果端口声明和数据类型分开写,代码一长就容易看花眼。
inout在FPGA里用起来比较麻烦,需要配合三态缓冲器。初学者尽量别用,除非你很清楚自己在做什么。我早期一个项目里用了inout,结果仿真怎么都不对,最后发现是三态控制逻辑写反了。
3.3 数据类型——wire和reg
这是新手最容易混淆的地方。我直接说结论:
- wire:线网类型,用于连续赋值(assign语句)或模块之间的连接。它不存储值,只是“导线”。
- reg:寄存器类型,用于过程赋值(always块内)。它存储值,可以保持状态。
但是!这里有个关键点:reg不一定会被综合成寄存器。比如下面这个例子:
reg [3:0] temp;
always @(*) begin
temp = a + b;
end
这个temp虽然是reg类型,但综合出来只是一堆组合逻辑,没有触发器。为什么会这样?因为always @(*)是组合逻辑描述,reg在这里只是语法上的要求,不代表硬件实现。
3.4 assign语句——连续赋值
assign用来描述组合逻辑。它的特点是:只要右边表达式里的信号发生变化,左边就会立刻更新。
assign sum = a + b;
assign data_out = (sel) ? data_a : data_b;
我个人习惯用assign处理简单的组合逻辑,比如数据选择、加法器。如果逻辑比较复杂,我会改用always @(*),因为可读性更好。
这里有个坑:assign的左边必须是wire类型,不能是reg。反过来,always块里赋值的变量必须是reg类型。这个规则是Verilog语法强制的,没得商量。
3.5 always块——过程赋值
always块是Verilog里最灵活、也最容易出问题的地方。它有两种常见写法:
3.5.1 组合逻辑:always @(*)
always @(*) begin
if (sel)
out = a;
else
out = b;
end
注意:组合逻辑里要用阻塞赋值(=)。而且必须把所有输入信号都列在敏感列表里,或者直接用@(*)自动推导。我见过有人手写敏感列表漏了信号,结果仿真和综合结果不一致——这种bug最难查。
3.5.2 时序逻辑:always @(posedge clk)
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
时序逻辑里要用非阻塞赋值(<=)。这个规则一定要刻在脑子里。为什么?因为非阻塞赋值能保证多个寄存器在同一个时钟沿同时更新,不会出现竞争。
=,时序逻辑用<=。混用的话,仿真结果会变得非常诡异。我曾经在一个项目里把阻塞和非阻塞混在一起,结果仿真波形看起来完全正确,但上板就是不对——查了整整两天才找到原因。
3.6 知识体系总览
下面这张图把本章的核心知识点串了起来,你可以对照着复习:
3.7 本章小结
这一讲我们覆盖了Verilog最核心的基础知识。说白了,写Verilog就是在描述硬件,不是写软件。你每写一行代码,都要想清楚它对应什么电路——是寄存器?是组合逻辑?还是连线?
我刚开始学的时候,总是用软件的思维写硬件,结果综合出来的电路跟预期完全不一样。后来我养成了一个习惯:写代码之前,先在纸上画个框图,标清楚哪些是寄存器、哪些是组合逻辑。这个习惯帮我避免了很多低级错误。
好了,这一讲就到这里。记住:基础不牢,地动山摇。把模块结构、端口、数据类型、assign和always块这几个概念彻底搞明白,后面的路就好走了。