第三讲:硬件描述语言基础(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在这里只是语法上的要求,不代表硬件实现。

记住这个原则:wire用于assign和模块连接,reg用于always块内赋值。至于综合出来是组合逻辑还是时序逻辑,取决于你怎么写always块。

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

时序逻辑里要用非阻塞赋值(<=)。这个规则一定要刻在脑子里。为什么?因为非阻塞赋值能保证多个寄存器在同一个时钟沿同时更新,不会出现竞争。

我的经验:写always块时,先想清楚你要描述的是组合逻辑还是时序逻辑。组合逻辑用=,时序逻辑用<=。混用的话,仿真结果会变得非常诡异。我曾经在一个项目里把阻塞和非阻塞混在一起,结果仿真波形看起来完全正确,但上板就是不对——查了整整两天才找到原因。

3.6 知识体系总览

下面这张图把本章的核心知识点串了起来,你可以对照着复习:

Verilog基础 模块结构 module ... endmodule 端口定义 input / output / inout 数据类型 wire / reg assign语句 连续赋值 左边必须是wire always块 组合逻辑:@(*) 用 = 时序逻辑:@(posedge clk) 用 <= 常见陷阱 阻塞与非阻塞混用 敏感列表遗漏 reg不一定综合成寄存器 核心:想清楚硬件结构,再写代码

3.7 本章小结

这一讲我们覆盖了Verilog最核心的基础知识。说白了,写Verilog就是在描述硬件,不是写软件。你每写一行代码,都要想清楚它对应什么电路——是寄存器?是组合逻辑?还是连线?

我刚开始学的时候,总是用软件的思维写硬件,结果综合出来的电路跟预期完全不一样。后来我养成了一个习惯:写代码之前,先在纸上画个框图,标清楚哪些是寄存器、哪些是组合逻辑。这个习惯帮我避免了很多低级错误。

好了,这一讲就到这里。记住:基础不牢,地动山摇。把模块结构、端口、数据类型、assign和always块这几个概念彻底搞明白,后面的路就好走了。

可可精品资料库