第三章:硬件描述语言基础(Verilog)——模块结构、端口定义、数据类型、assign与always块
各位同学,欢迎来到第三章。从这一章开始,我们正式进入Verilog的世界。
说实话,很多初学者一上来就被Verilog的语法吓住了。觉得它像C语言,又不完全是。我当年刚接触时也犯过迷糊——明明写的是“硬件描述”,怎么看起来像在写软件?
嗯,这里要给大家打个预防针:Verilog不是软件编程语言,它是用来描述硬件电路的。你写的每一行代码,最终都会映射成真实的逻辑门、触发器和连线。带着这个认知去学,事半功倍。
核心认知:Verilog代码 = 电路结构 + 行为描述。不是“执行”,而是“存在”。
3.1 模块结构——Verilog的基本单元
一个Verilog设计,说白了就是一堆模块(module)拼起来的。模块就像芯片里的“黑盒子”,有输入、有输出,内部干点啥事。
来看一个最简单的模块结构:
module led_controller(
input wire clk, // 时钟输入
input wire rst_n, // 复位输入(低有效)
output reg led_out // LED输出
);
// 内部逻辑写在这里
endmodule
我个人习惯把端口声明直接写在括号里,这样一眼就能看到模块的“长相”。当然你也可以把端口声明写在模块内部,两种方式都行,看团队规范。
模块的命名我建议用下划线分隔的小写字母,比如 uart_rx、spi_master。别用驼峰命名,也别用中文拼音——我在项目中见过有人用 dianji_kongzhi,虽然能看懂,但不太专业。
3.2 端口定义——模块的“手脚”
端口就是模块与外界通信的通道。Verilog里有三种端口方向:
| 方向 | 关键字 | 说明 |
|---|---|---|
| 输入 | input | 信号从外部流入模块内部 |
| 输出 | output | 信号从模块内部流出到外部 |
| 双向 | inout | 信号可进可出(如I2C数据线) |
这里有个坑:inout端口必须声明为wire类型,不能是reg。为什么?因为双向端口需要三态驱动,而reg类型在综合时会被实现为寄存器,没法直接挂到总线上。我曾经在一个I2C控制器项目里踩过这个坑,仿真怎么都对,一综合就报错,查了半天才发现是inout用了reg。
注意:inout端口在FPGA中通常用IOB(输入输出块)实现,需要配合三态门使用。初学者建议先避开inout,用input和output就够了。
3.3 数据类型——wire和reg
这是新手最容易搞混的地方。我简单总结一下:
- wire:相当于一根导线。它不存储值,只是把驱动端的值传递出去。用在assign语句、模块端口连接、连续赋值等场景。
- reg:相当于一个存储单元。它可以在always块中被赋值,并且会保持上次的值直到被重新赋值。
你想想看,如果电路里有一根线,它本身没有记忆能力,那就是wire。如果是一个D触发器,它能记住上一个时钟周期的值,那就是reg。
但注意:reg不一定会被综合成寄存器。如果你在always块里写组合逻辑,reg综合出来可能只是一堆门电路。我刚开始学的时候也以为reg就是寄存器,后来发现不是这么回事——reg只是语法上的变量类型,综合成什么取决于你怎么用它。
小技巧:在always @(*)中赋值的变量,用reg声明;在assign中赋值的变量,用wire声明。这样写代码,综合工具不会报错,别人也容易看懂。
3.4 assign语句——连续赋值
assign语句用来描述组合逻辑。它的意思是“只要右边变了,左边立刻跟着变”。
举个例子:
wire a, b, c;
assign c = a & b; // c = a 与 b
这段代码综合出来就是一个与门。a和b是输入,c是输出。只要a或b发生变化,c会立即更新——没有延迟,没有时钟控制。
assign的右边可以是任何表达式:与、或、非、异或、移位、拼接等等。但注意:assign左边必须是wire类型,不能是reg。
我在项目中常用assign来做简单的信号处理,比如:
// 按键消抖后的上升沿检测
assign key_rise = key_debounced & ~key_debounced_dly;
这种写法简洁明了,综合出来也就几个门电路,效率很高。
3.5 always块——时序与组合逻辑的“心脏”
always块是Verilog里最灵活、也最容易出错的语法结构。它有两种主要用法:
3.5.1 组合逻辑的always块
always @(*) begin
if (sel)
out = a;
else
out = b;
end
这里的 @(*) 表示“敏感列表包含所有输入信号”。只要a、b、sel中任何一个发生变化,这个块就会被触发执行。综合出来就是一个多路选择器。
写组合逻辑的always块时,有个铁律:所有输入信号都要出现在敏感列表里。如果你漏写了某个信号,综合工具会生成一个锁存器(latch),而不是你想要的组合逻辑。我见过不少新手因为这个bug调了一整天。
避坑指南:组合逻辑的always块中,每个分支都必须给所有输出赋值。否则综合工具会推断出锁存器。我曾经在一个状态机解码器里漏了一个分支,结果综合出来的电路多了8个锁存器,功耗和面积都超标了。
3.5.2 时序逻辑的always块
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter <= 8'd0;
else
counter <= counter + 1'b1;
end
这段代码描述了一个8位计数器,在时钟上升沿递增,复位时清零。注意这里用的是 非阻塞赋值 <=,而不是阻塞赋值 =。
为什么时序逻辑要用非阻塞赋值?因为非阻塞赋值能模拟寄存器的行为:所有赋值在时钟沿同时发生,不会互相影响。如果用阻塞赋值,仿真结果可能不对,综合出来的电路也可能有竞争冒险。
我个人的习惯是:组合逻辑用阻塞赋值,时序逻辑用非阻塞赋值。这个规则我用了十几年,从来没出过问题。
3.6 知识体系总览
下面这张图总结了本章的核心知识点,以及它们之间的关系:
3.7 本章小结
这一章我们讲了Verilog最基础、最核心的几个概念:
- 模块结构:module/endmodule是设计的骨架
- 端口定义:input/output/inout决定了信号的流向
- 数据类型:wire和reg各有各的用法,别搞混
- assign语句:描述组合逻辑的简洁方式
- always块:既能描述组合逻辑,也能描述时序逻辑
这些内容看起来简单,但它们是后续所有复杂设计的基础。我建议你打开开发环境,把每个例子都敲一遍,仿真看看波形。光看不练,永远学不会Verilog。
嗯,这一章就到这里。下一章我们会深入讨论运算符和表达式,到时候会用到今天讲的知识点。
公众号:蓝海资料掘金营,微信deep3321