3. Verilog基础语法(上):模块结构、端口定义、assign语句、always块、阻塞与非阻塞赋值
各位同学,今天我们来聊聊Verilog最基础的东西。说实话,我刚开始学FPGA的时候,觉得Verilog跟C语言差不多,不就是写代码嘛。结果第一次上板调试,LED死活不亮,查了半天才发现是赋值方式用错了。嗯,从那以后我再也不敢小看这些基础语法了。
这一章我们不讲复杂的东西,就讲五个核心概念:模块结构、端口定义、assign语句、always块、阻塞与非阻塞赋值。把这五个搞明白,你就能看懂大部分Verilog代码了。
核心要点:Verilog是硬件描述语言,不是编程语言。你写的每一行代码,最终都会变成实际的电路。这一点请时刻记住。
3.1 模块结构——Verilog的基本单元
一个Verilog程序,说白了就是一个模块(module)。模块就像是一个黑盒子,有输入、有输出,内部实现具体的功能。
我个人的习惯是,每写一个模块,先画个框图,把输入输出标清楚。这样写代码的时候思路特别清晰。
来看一个最简单的模块结构:
module led_controller(
input wire clk, // 时钟信号
input wire rst_n, // 复位信号,低电平有效
output reg led // LED控制信号
);
// 内部逻辑写在这里
endmodule
结构很简单:module 开头,endmodule 结尾。中间是端口定义和内部逻辑。注意,每个模块都要有一个名字,这里我叫它 led_controller。
小技巧:模块名最好跟文件名保持一致。比如这个模块放在 led_controller.v 文件里。这样找代码的时候不会乱。我在项目里见过有人把几十个模块全写在一个文件里,那叫一个酸爽...
3.2 端口定义——模块的"手脚"
端口就是模块跟外界通信的通道。有三种类型:
| 端口类型 | 关键字 | 说明 |
|---|---|---|
| 输入端口 | input | 数据从外部流入模块 |
| 输出端口 | output | 数据从模块流出到外部 |
| 双向端口 | inout | 既可以输入也可以输出(初学者先别碰) |
端口还需要指定数据类型。最常用的两种:
- wire:线网类型,可以理解为物理连线。用于assign语句赋值。
- reg:寄存器类型,可以存储数据。用于always块中赋值。
这里有个容易混淆的地方:reg 并不一定对应实际的寄存器。它只是一个变量类型。我在项目中见过有人以为用了 reg 就一定会生成寄存器,结果综合出来的电路跟他想的不一样。其实,reg 在组合逻辑中也可以使用,它只是表示一个变量,而不是硬件寄存器。
3.3 assign语句——连续赋值
assign 语句用来描述组合逻辑。它的特点是:只要输入变了,输出立刻跟着变。就像一根导线,左边输入什么,右边就输出什么。
来看个例子:
module and_gate(
input wire a,
input wire b,
output wire y
);
assign y = a & b; // 与门
endmodule
这个模块实现了一个与门。当 a 和 b 都是 1 时,y 输出 1;否则输出 0。注意,assign 左边的变量必须是 wire 类型。
重要:多个assign语句是并行执行的,不是顺序执行。这一点跟C语言完全不同。你写10个assign,它们同时工作,不分先后。
我曾经犯过一个错误:写了两个assign给同一个变量赋值,结果综合报错。因为一个变量只能被一个驱动源驱动,就像一根导线不能同时接两个电源一样。
3.4 always块——时序逻辑的核心
always 块是Verilog中最强大的结构之一。它既可以描述组合逻辑,也可以描述时序逻辑。关键看敏感列表怎么写。
基本语法:
always @(敏感列表) begin
// 逻辑代码
end
敏感列表告诉always块什么时候执行。常用的写法:
always @(a or b):当 a 或 b 变化时执行(组合逻辑)always @(posedge clk):当时钟上升沿时执行(时序逻辑)always @(posedge clk or negedge rst_n):时钟上升沿或复位下降沿时执行(带复位的时序逻辑)
来看一个带复位的计数器:
module counter(
input wire clk,
input wire rst_n,
output reg [3:0] cnt
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
cnt <= 4'd0; // 复位清零
else
cnt <= cnt + 1'b1; // 每个时钟加1
end
endmodule
这个计数器在每个时钟上升沿加1。复位时(rst_n为低电平)清零。注意,这里用的是 <= 而不是 =,这就是我们接下来要讲的重点。
警告:always块中,如果描述组合逻辑,敏感列表要包含所有输入信号。漏掉一个,综合出来的电路可能跟你想的不一样。我吃过这个亏,仿真没问题,上板就出bug。
3.5 阻塞赋值与非阻塞赋值——新手最容易踩的坑
这是Verilog中最容易混淆的概念,也是面试必考题。说白了,就两句话:
- 阻塞赋值(=):顺序执行,前面的赋值会阻塞后面的赋值。
- 非阻塞赋值(<=):并行执行,所有赋值同时发生。
为什么会有这种区别?因为硬件电路是并行工作的,而软件是顺序执行的。Verilog用这两种赋值方式来区分不同的硬件行为。
来看一个经典的例子:
// 阻塞赋值示例
always @(posedge clk) begin
a = b;
c = a;
end
// 非阻塞赋值示例
always @(posedge clk) begin
a <= b;
c <= a;
end
猜猜结果有什么不同?
阻塞赋值:先执行 a = b,a 变成了 b 的值。然后执行 c = a,此时 a 已经是新值了,所以 c 也等于 b 的值。最终 a 和 c 都等于 b。
非阻塞赋值:两个赋值同时发生。a <= b 把 b 的值赋给 a,c <= a 把 a 的旧值赋给 c。所以 c 等于 a 原来的值,而不是 b 的值。
| 赋值方式 | 执行顺序 | 适用场景 |
|---|---|---|
| 阻塞赋值 = | 顺序执行 | 组合逻辑(always @(*) 或 assign) |
| 非阻塞赋值 <= | 并行执行 | 时序逻辑(always @(posedge clk)) |
黄金法则:描述时序逻辑用非阻塞赋值(<=),描述组合逻辑用阻塞赋值(=)。这个规则请刻在脑子里。
我曾经在项目中把这两个搞混了,写了一个移位寄存器,结果仿真波形完全不对。查了两天才发现是赋值方式用错了。从那以后,我写代码前都会先问自己一句:这是组合逻辑还是时序逻辑?
3.6 本章小结
这一章我们讲了五个基础概念:
- 模块结构:module...endmodule,每个模块一个文件
- 端口定义:input、output、inout,配合wire和reg使用
- assign语句:连续赋值,用于组合逻辑
- always块:可描述组合逻辑和时序逻辑,看敏感列表
- 阻塞与非阻塞赋值:时序逻辑用<=,组合逻辑用=
这些是Verilog的基石。你想想看,如果连这些都没搞明白,后面写复杂模块的时候肯定会出问题。我建议你把每个例子都自己敲一遍,跑一下仿真,看看波形是什么样的。光看是学不会的,得动手。
我的建议:刚开始学的时候,可以写一些简单的模块,比如与门、或门、计数器、移位寄存器。每个模块都仿真一下,看看波形对不对。这样练上十来个模块,基础就扎实了。
公众号:蓝海资料掘金营,微信deep3321