第一阶段:Verilog 基础入门讲义
对应课程目录第 1~3 章。建议学习时间:3~5 天。
本阶段学习目标
完成本阶段后,你应该能够:
- 解释 Verilog、FPGA 和数字电路之间的关系
- 区分组合逻辑与时序逻辑
- 看懂并编写基本的
module - 正确使用
input、output、wire和reg - 完成模块例化和端口连接
- 正确书写不同进制、不同位宽的常量
- 使用常见运算符描述简单组合逻辑
- 编写基础 Testbench 并观察仿真结果
第 1 章:数字电路与 Verilog 概述
1.1 Verilog 是什么
Verilog 是一种硬件描述语言,英文为 Hardware Description Language,简称 HDL。它不是用来描述“处理器按顺序执行哪些指令”,而是用来描述:
- 电路中有哪些输入和输出
- 信号之间如何连接
- 多个电路单元如何同时工作
- 信号在时钟到来时如何变化
例如:
|
|
这行代码不是要求设备先读取 a,再读取 b,最后计算 y。它表示硬件中始终存在一个与门,a 或 b 变化后,y 会随电路传播而变化。
1.2 FPGA、ASIC 与 Verilog
FPGA
FPGA 是可重复配置的数字芯片。Verilog 代码经过综合、布局布线并生成配置文件后,可以下载到 FPGA 中形成实际电路。
ASIC
ASIC 是针对特定功能制造的芯片。Verilog 也可以用于 ASIC 设计,但芯片制造完成后,其硬件结构通常不能再次修改。
三者关系
|
|
必须记住:综合工具只会把“可综合的 Verilog”转换成硬件。用于测试的延时、打印等语句通常只能用于仿真。
1.3 数字信号
理想数字电路主要使用两个逻辑值:
| 逻辑值 | 常见含义 |
|---|---|
1'b0 |
逻辑 0、低电平、条件不成立 |
1'b1 |
逻辑 1、高电平、条件成立 |
Verilog 仿真实际采用四值逻辑:
| 值 | 含义 |
|---|---|
0 |
逻辑 0 |
1 |
逻辑 1 |
x |
未知,常见于信号没有初始化或存在冲突 |
z |
高阻态,常见于三态总线或未驱动线路 |
看到仿真波形中的 x 时,不要把它当作 0。它通常表示设计或 Testbench 中有信号没有被正确赋值。
1.4 组合逻辑与时序逻辑
组合逻辑
组合逻辑的输出只取决于当前输入。
|
|
常见组合电路包括:
- 与门、或门、非门
- 加法器
- 比较器
- 编码器和译码器
- 多路选择器
时序逻辑
时序逻辑的输出不仅取决于当前输入,还与之前保存的状态有关,通常由时钟控制。
常见时序电路包括:
- 触发器
- 寄存器
- 计数器
- 状态机
本阶段主要学习组合逻辑。时钟和时序逻辑将在第 7 章开始系统学习。
1.5 第一个模块:与门
新建 and_gate.v:
|
|
逐行理解:
| 代码 | 作用 |
|---|---|
module and_gate ( |
开始定义名为 and_gate 的模块 |
input wire a |
定义输入端口 a |
output wire y |
定义输出端口 y |
assign y = a & b |
用与运算持续驱动 y |
endmodule |
结束模块定义 |
模块可以理解为一个有明确输入和输出的电路元件。
1.6 与门真值表
a |
b |
y = a & b |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 0 | 0 |
| 1 | 1 | 1 |
1.7 第一个 Testbench
Testbench 是用于验证设计的仿真代码。它本身一般不会被综合到 FPGA 中。
新建 and_gate_tb.v:
|
|
关键内容:
`timescale 1ns/1ps:时间单位是 1 ns,仿真精度是 1 psdut:Design Under Test,被测试的设计initial:仿真开始时执行一次#10:等待 10 个时间单位,只用于仿真$monitor:信号变化时打印信息,只用于仿真$finish:结束仿真
预期输出类似:
|
|
1.8 本章易错点
- Verilog 描述的是电路,不要默认代码像普通软件一样逐行运行
- 每条普通语句后通常需要分号
- 模块必须以
endmodule结束 - Verilog 区分大小写,
data和Data是两个不同名称 #10、$monitor和$finish用于仿真,不应写进需要综合的功能模块
1.9 本章练习
- 将与门改为或门,并写出真值表。
- 实现非门
y = ~a。 - 实现异或门,并验证只有输入不同时输出才为 1。
- 思考:两个输入同时变化时,与门是否需要“执行”一次才会更新?
1.10 本章检查点
在进入下一章前,确认你能回答:
- Verilog 与 C、Python 的核心区别是什么?
x和z分别表示什么?- 组合逻辑和时序逻辑的判断依据是什么?
- Testbench 为什么通常不会下载到 FPGA?
第 2 章:模块与端口
2.1 模块是设计的基本单位
一个 Verilog 工程通常由多个模块组成。小模块实现单一功能,上层模块通过例化连接这些小模块。
模块的一般结构如下:
|
|
推荐遵守以下命名方式:
- 模块名和信号名使用小写字母与下划线,例如
and_gate - 时钟常命名为
clk - 复位常命名为
rst、rst_n或reset - 低电平有效信号常使用
_n后缀,例如rst_n - Testbench 模块常使用
_tb后缀
2.2 输入、输出与双向端口
input
输入信号由模块外部提供,模块内部只能读取它。
|
|
output
输出信号由模块内部产生,提供给模块外部。
|
|
inout
双向端口既可以输入也可以输出,常用于芯片引脚级的共享总线。
|
|
初学阶段不要随意使用 inout。FPGA 内部逻辑通常使用方向明确的独立输入和输出信号。
2.3 单比特与多比特信号
单比特信号:
|
|
8 位信号:
|
|
[7:0] 表示信号由 8 个比特组成:
|
|
data[7]是最高有效位,简称 MSBdata[0]是最低有效位,简称 LSBdata[7:4]表示选择高 4 位
2.4 wire 与 reg
wire
wire 表示网络或连线。它通常由以下来源驱动:
assign连续赋值- 另一个模块的输出端口
- 模块输入端口
|
|
reg
在 Verilog 中,过程块内被赋值的信号需要声明为 reg。
|
|
需要特别注意:reg 是语言中的变量类型,不代表它一定会综合成物理寄存器。上面的代码仍然是组合逻辑。是否产生寄存器,取决于赋值所在的过程和逻辑是否需要保存状态。
本阶段先遵守两个简单规则:
- 被
assign驱动的信号使用wire - 在
always或initial中被赋值的信号使用reg
2.5 模块例化
假设已经有一个与门模块:
|
|
可以在上层模块中例化它:
|
|
这段例化代码的结构是:
|
|
and_gate 类似电路元件的型号,u_and_gate 是这个具体元件实例的名称。同一个模块可以被例化多次,每次都会对应一份独立硬件。
2.6 两种端口连接方式
按名称连接
|
|
这种方式清晰、不依赖端口排列顺序,工程中优先使用。
按位置连接
|
|
这种方式代码较短,但端口顺序一旦写错就会连接错误,不推荐用于较复杂模块。
2.7 实践:多个逻辑门组成一个模块
新建 logic_gates.v:
|
|
新建 logic_gates_tb.v:
|
|
这是一个简单的自检式 Testbench。它不只产生波形,还会自动比较实际输出与预期输出。
表达式 {and_y, or_y, xor_y, not_a} 使用了拼接运算,将 4 个单比特信号组合为一个 4 位数。拼接运算会在第 3 章详细介绍。
2.8 本章易错点
- 例化时不要把模块名和实例名混淆
- 按名称连接时,点号后的名称必须是被例化模块的端口名
- 信号位宽应与端口位宽一致
- 同一根普通
wire不应由多个逻辑源同时驱动 reg不等于物理寄存器- 顶层模块的端口最终需要通过约束文件连接到 FPGA 引脚
2.9 本章练习
- 编写一个
nand_gate模块,实现y = ~(a & b)。 - 编写顶层模块,同时例化两个
and_gate。 - 定义一个 8 位输入
data,分别输出它的高 4 位和低 4 位。 - 将
logic_gates的 Testbench 故意改错一个预期值,观察自检结果。
2.10 本章检查点
wire和reg的基本使用规则是什么?- 例化模块为什么会产生实际硬件?
- 按名称连接和按位置连接,哪种方式更不容易出错?
[7:0]一共有多少位?
第 3 章:数值与运算符
3.1 Verilog 数值的写法
Verilog 常量的完整形式是:
|
|
常用进制标识:
| 标识 | 进制 | 示例 | 含义 |
|---|---|---|---|
b |
2 | 4'b1010 |
4 位二进制数 1010 |
o |
8 | 6'o52 |
6 位八进制数 52 |
d |
10 | 8'd25 |
8 位十进制数 25 |
h |
16 | 8'hA5 |
8 位十六进制数 A5 |
以下数值相等:
|
|
下划线可以提高可读性,不影响数值:
|
|
建议初学阶段总是明确写出位宽,避免工具对无位宽常量采用默认规则造成意外结果。
3.2 位宽截断与扩展
截断
把更宽的值赋给较窄的信号时,高位会被丢弃:
|
|
最终 value 为 4'b1100。
无符号扩展
把较窄的无符号值赋给更宽信号时,通常在高位补 0:
|
|
最终 value 为 8'b0000_1010。
加法位宽
两个 4 位数相加最多需要 5 位保存结果:
|
|
显式补 0 后再相加,可以清楚地保留最高位进位。
3.3 无符号数与有符号数
普通向量默认是无符号数:
|
|
使用 signed 声明有符号数:
|
|
8 位二进制补码的范围是:
|
|
例如:
|
|
有符号数和无符号数混合运算容易出现符号扩展问题。初学阶段应尽量让参与同一运算的信号具有一致的位宽和符号属性。
3.4 算术运算符
| 运算符 | 含义 | 示例 |
|---|---|---|
+ |
加法 | a + b |
- |
减法 | a - b |
* |
乘法 | a * b |
/ |
除法 | a / b |
% |
取余 | a % b |
乘法器、除法器可能占用较多 FPGA 资源。常数除以 2 的幂通常可以用移位实现,但具体优化应交给综合工具并结合时序、资源报告判断。
3.5 位运算符与逻辑运算符
这两类运算符非常容易混淆。
位运算符
位运算会对向量的每一位分别计算:
| 运算符 | 含义 |
|---|---|
& |
按位与 |
| ` | ` |
^ |
按位异或 |
~ |
按位取反 |
|
|
逻辑运算符
逻辑运算把整个操作数判断为真或假,结果通常是 1 位:
| 运算符 | 含义 |
|---|---|
&& |
逻辑与 |
| ` | |
! |
逻辑非 |
|
|
判断条件时常使用逻辑运算;对总线逐位操作时使用位运算。
3.6 归约运算符
归约运算只在一个操作数前使用,将多位信号归约为 1 位:
|
|
&data:所有位都为 1 时结果为 1|data:任意一位为 1 时结果为 1^data:所有位进行异或,可用于简单奇偶校验
3.7 比较运算符
| 运算符 | 含义 |
|---|---|
== |
相等 |
!= |
不相等 |
> |
大于 |
< |
小于 |
>= |
大于或等于 |
<= |
小于或等于 |
=== |
全等,连 x、z 也参与精确比较 |
!== |
非全等 |
设计模块通常使用 == 和 !=。Testbench 检查结果时常使用 === 和 !==,这样未知值 x 不会被悄悄忽略。
注意:<= 在不同语境中可能表示“小于或等于”,也可能表示时序逻辑中的非阻塞赋值。需要根据代码位置判断。
3.8 移位运算符
| 运算符 | 含义 |
|---|---|
<< |
逻辑左移,低位补 0 |
>> |
逻辑右移,高位补 0 |
<<< |
算术左移 |
>>> |
算术右移,有符号数高位补符号位 |
示例:
|
|
移位后的结果位宽通常与左操作数一致,移出的位会丢失。
3.9 拼接与复制运算符
拼接
使用大括号连接多个信号:
|
|
复制
|
|
当 a = 1'b1 时,repeated = 4'b1111。
3.10 条件运算符
条件运算符常用于描述简单二选一逻辑:
|
|
含义是:
select为 1 时,y = aselect为 0 时,y = b
综合后通常对应一个二选一多路选择器。
3.11 运算优先级
Verilog 运算符有优先级,但不要依赖记忆编写复杂表达式。使用括号明确意图:
|
|
清晰的括号既方便阅读,也可以减少优先级理解错误。
3.12 实践:4 位简单 ALU
ALU 是算术逻辑单元。下面的 ALU 根据 op 选择不同运算:
op |
功能 | 输出 |
|---|---|---|
2'b00 |
加法 | a + b |
2'b01 |
减法 | a - b |
2'b10 |
按位与 | a & b |
2'b11 |
按位或 | `a |
新建 simple_alu.v:
|
|
这个示例只保留 4 位结果,因此加法的最高位进位和减法的借位没有单独输出。后续学习完整加减法器时会处理这些标志。
新建 simple_alu_tb.v:
|
|
3.13 本章易错点
- 忽略位宽可能造成结果截断
&与&&的含义不同|data是归约或,a | b是按位或- 无符号数与有符号数混合运算可能改变比较和扩展结果
- 逻辑右移与算术右移对负数的处理不同
- 不要在除法运算中让除数为 0
- 复杂表达式应使用括号明确计算顺序
3.14 本章练习
- 将
8'hA6转换成二进制和十进制。 - 计算
4'b1101 & 4'b1011、|和^的结果。 - 使用拼接运算把两个 4 位输入组成一个 8 位输出。
- 使用复制运算把 1 位输入复制成 8 位输出。
- 给
simple_alu增加按位异或功能。需要相应扩大或重新编码op。 - 给 ALU 增加一个
carry输出,保留加法的最高位进位。 - 测试
4'd15 + 4'd1,解释为什么当前 ALU 的y等于 0。
3.15 本章检查点
8'hA5中的8、h、A5分别表示什么?- 两个 4 位数相加为什么可能需要 5 位结果?
&data和a & b有什么不同?==和===各适合在什么场景使用?- 如何使用条件运算符描述二选一多路选择器?
第一阶段综合练习
练习 A:三输入多数表决器
输入为 a、b、c。当至少两个输入为 1 时,输出 y 为 1。
要求:
- 先写出 8 种输入组合的真值表
- 使用位运算和
assign实现 - 编写自检式 Testbench,遍历全部输入组合
提示:三个“两两相与”的结果再进行或运算。
练习 B:8 位数据拆分与重组
输入一个 8 位信号 data_in,完成以下输出:
high输出高 4 位low输出低 4 位swapped将高低 4 位互换
要求使用位选择、范围选择和拼接运算。
练习 C:扩展 ALU
将本章的 4 位 ALU 扩展为 8 位,并支持以下功能:
op |
功能 |
|---|---|
3'b000 |
加法 |
3'b001 |
减法 |
3'b010 |
按位与 |
3'b011 |
按位或 |
3'b100 |
按位异或 |
3'b101 |
左移 1 位 |
3'b110 |
右移 1 位 |
3'b111 |
输出 b |
同时输出 zero 标志。为每一种操作至少编写一个测试用例。
参考答案
第 1 章练习要点
|
|
组合电路始终存在,输入变化会通过电路传播到输出,不需要像软件函数一样被主动调用。
第 2 章练习要点
|
|
8 位数据拆分:
|
|
第 3 章练习要点
|
|
拼接与复制:
|
|
保留加法进位:
|
|
4'd15 + 4'd1 的完整结果是 5'b1_0000。当前 ALU 只保留低 4 位,所以 y = 4'b0000。
综合练习 A 参考逻辑
|
|
综合练习 B 参考逻辑
|
|
第一阶段完成标准
满足以下条件后,再进入第二阶段:
- 能独立编写包含输入、输出和
assign的模块 - 能独立编写基础 Testbench 并覆盖所有简单输入组合
- 能使用按名称连接的方式例化模块
- 能正确判断信号位宽以及运算结果是否会被截断
- 能区分位运算、逻辑运算和归约运算
- 能完成三输入多数表决器和扩展 ALU 中至少一个综合练习