Verilog数字系统设计综合教程
前言:为什么学习Verilog?
Verilog是一种用于描述电子系统硬件功能的硬件描述语言,与C、Java等软件编程语言不同,Verilog描述的是电路的结构、行为和数据流,它是现代数字电路设计的基石,是成为一名合格的数字IC设计工程师或FPGA工程师的必备技能。

本教程将分为以下几个部分:
- 第一部分:基础入门 - 了解Verilog是什么,如何搭建环境,编写最简单的模块。
- 第二部分:核心语法 - 深入学习Verilog的数据类型、运算符和结构。
- 第三部分:行为建模 - 掌握
always块,这是Verilog描述时序逻辑的关键。 - 第四部分:结构化建模 - 学习如何实例化模块,构建复杂的系统。
- 第五部分:高级主题 - 探讨测试平台、有限状态机、同步设计原则等。
- 第六部分:实践项目 - 通过一个完整的项目巩固所学知识。
- 第七部分:学习资源与工具推荐
第一部分:基础入门
1 什么是Verilog?
Verilog是一种用于对电子系统进行抽象描述的语言,你可以把它想象成“电路的C语言”,它允许你从不同层次描述电路:
- 行为级: 描述电路的功能和算法,不关心具体实现。
- RTL级 (Register-Transfer Level): 描述数据在寄存器之间的流动和变换,这是最常用、最核心的设计层次。
- 门级: 描述由基本逻辑门(与、或、非等)构成的电路。
- 开关级: 描述晶体管级别的连接。
2 开发环境
要进行Verilog设计,你需要两个基本工具:
- 文本编辑器:
- 专业IDE: Verilog-Mode (Emacs/Vim插件), Visual Studio Code (配合Verilog插件), Synopsys VCS (商业, 集成度高)。
- 轻量级: Sublime Text, Notepad++。
- 仿真器:
- 开源: Icarus Verilog (
iverilog) + GTKWave (波形查看器),这是初学者首选的免费组合。 - 商业: ModelSim/Questa Simulator ( Mentor/Siemens EDA ), Xcelium ( Synopsys ), VCS ( Synopsys ),工业界标准,功能强大。
- 开源: Icarus Verilog (
3 你的第一个Verilog程序:半加器
一个半加器有两个输入 A 和 B,两个输出 Sum (和) 和 Cout (进位)。

Sum = A ^ B(异或)Cout = A & B(与)
代码示例: half_adder.v
// 这是一个半加器的Verilog模块定义
// module <module_name> (<port_list>);
module half_adder(
input A, // 输入端口A
input B, // 输入端口B
output Sum, // 输出端口Sum
output Cout // 输出端口Cout
);
// 描述逻辑功能
// assign语句用于组合逻辑,将右侧表达式的值赋给左侧的线网
assign Sum = A ^ B;
assign Cout = A & B;
// 模块定义结束
endmodule
4 模块的基本结构
一个Verilog模块由以下几部分组成:
module module_name (
// 1. 端口声明
input wire in1,
output reg out1,
inout tri inout1
);
// 2. 内部信号/变量/寄存器声明
wire internal_wire;
reg [7:0] data_reg; // 8位寄存器
// 3. 功能描述
// 可以是组合逻辑 (assign)
// 也可以是时序逻辑 (always块)
assign out1 = in1 & internal_wire;
always @(posedge clk) begin
if (reset)
data_reg <= 8'b0;
else
data_reg <= in1;
end
endmodule
第二部分:核心语法
1 数据类型
Verilog中最核心的数据类型是线网和寄存器。
-
wire: 用于表示物理连接,如导线,它不能存储值,必须通过assign语句或模块实例化来驱动。
(图片来源网络,侵删)wire w1, w2; assign w1 = a & b;
-
reg: 用于在always块中赋值的变量,它并不一定代表硬件上的寄存器,只是表示其值在always块中被重新赋值,在综合时,只有被时钟边沿触发的reg才会被综合成寄存器。reg r1; always @(posedge clk) begin r1 <= a & b; // 这个r1会被综合成寄存器 end -
integer,real: 用于仿真,一般不用于综合。 -
parameter: 用于定义常量,提高代码可读性和可维护性。parameter DATA_WIDTH = 8; reg [DATA_WIDTH-1:0] data_bus;
2 运算符
Verilog的运算符与C语言非常相似。
| 类别 | 运算符 | 描述 | 示例 |
|---|---|---|---|
| 算术 | , , , , | 加、减、乘、除、取模 | assign c = a + b; |
| 逻辑 | &&, \\|\\|, |
逻辑与、或、非 | assign y = enable && (a > b); |
| 按位 | &, \\|, ^, |
按位与、或、异或、取反 | assign c = a & b; |
| 缩减 | &, \\|, ^ |
将向量缩减为一位(与、或、异或) | assign flag = &data_vector; // 所有位都为1时为1 |
| 关系 | >, <, >=, <= |
大于、小于、大于等于、小于等于 | assign y = (a > b); |
| 相等 | , , , | 逻辑相等/不等,全等/不全等 | 注意: 和 会比较X和Z,推荐在测试平台使用 |
| 移位 | <<, >> |
左移、右移 | assign shifted = data << 2; |
| 条件 | 三元运算符 | assign y = (sel) ? a : b; |
3 向量
多位数据用向量表示。
// 定义一个8位向量,MSB是7,LSB是0 wire [7:0] my_byte; // 定义一个16位向量,MSB是15,LSB是0 reg [15:0] my_word; // 可以使用位选和部分选择 assign my_byte[7:4] = my_word[15:12]; // 高4位赋值 assign my_byte[3:0] = my_word[3:0]; // 低4位赋值 // 也可以用 `:` 简化写法 assign my_byte = my_word[15:8]; // 等同于 my_byte[7:0] = my_word[15:8]
第三部分:行为建模 (always块)
always块是Verilog描述时序逻辑和复杂组合逻辑的核心。
1 always块的语法
always @(event_expression) begin
// 顺序执行的语句
// ...
end
event_expression (事件表达式) 决定了always块何时被触发。
2 组合逻辑的always块
如果用于组合逻辑,event_expression通常是输入信号的列表,敏感列表。
重要: 必须在always块内为所有输出的reg类型变量赋值,否则会生成锁存器,这通常是设计错误。
示例:一个2选1多路选择器
module mux2to1(
input [7:0] a, b,
input sel,
output reg [7:0] y
);
// 敏感列表包含所有输入
// @(*) 是一种简写,表示所有在块内使用的信号都是敏感信号
always @(*) begin
if (sel == 1'b1)
y = a;
else
y = b;
end
endmodule
3 时序逻辑的always块
如果用于时序逻辑,event_expression通常是时钟边沿。
posedge clk: 上升沿触发negedge clk: 下降沿触发
示例:一个带同步复位端的D触发器
module d_ff (
input clk,
input reset, // 同步复位,高电平有效
input d,
output reg q
);
// 在时钟的上升沿执行
always @(posedge clk) begin
if (reset) // 如果复位信号有效
q <= 1'b0; // 复位
else
q <= d; // 否则,将d的值赋给q
end
endmodule
注意: 在时序逻辑中,我们使用非阻塞赋值 <=,它表示在时钟边沿到来时,将右侧的值“计划”给左侧,所有右侧的计算在同一时间点完成,这能避免仿真时的竞争条件,符合硬件行为。
第四部分:结构化建模
结构化建模就是通过实例化已存在的模块来构建更复杂的系统,这就像搭积木。
1 模块实例化
语法格式:<实例名> <模块名> (.<端口连接>);
示例:用两个半加器和一个或门构建一个全加器
首先定义或门 or_gate.v
module or_gate(
input a, b,
output y
);
assign y = a | b;
endmodule
然后实例化模块来构建全加器 full_adder.v
module full_adder(
input a, b, cin,
output sum, cout
);
// 内部线网,用于连接半加器和或门
wire w1, w2, w3;
// 实例化第一个半加器
// 实例名: ha1
// 模块名: half_adder
// 端口连接: (a->a, b->b, sum->w1, cout->w2)
half_adder ha1 (
.a(a),
.b(b),
.sum(w1),
.cout(w2)
);
// 实例化第二个半加器
half_adder ha2 (
.a(w1),
.b(cin),
.sum(sum),
.cout(w3)
);
// 实例化或门
or_gate og (
.a(w2),
.b(w3),
.y(cout)
);
endmodule
第五部分:高级主题
1 测试平台
测试平台是用于验证设计模块是否正确的另一个Verilog模块,它不综合成硬件,只在仿真时使用。
测试平台的关键点:
- 实例化被测模块:
full_adder my_dut ( .a(a_in), .b(b_in), .cin(cin_in), .sum(sum_out), .cout(cout_out) );
- 生成激励信号: 使用
initial块或always块来产生输入信号。 - 监视输出信号: 使用
$display或$monitor在控制台打印结果,或使用$dumpfile和$dumpvars生成波形文件,用GTKWave查看。
示例:全加器的测试平台 tb_full_adder.v
`timescale 1ns / 1ps // 定义时间尺度
module tb_full_adder();
// 定义内部变量作为激励和观测点
reg a_in, b_in, cin_in;
wire sum_out, cout_out;
// 实例化被测模块
full_adder my_dut (
.a(a_in),
.b(b_in),
.cin(cin_in),
.sum(sum_out),
.cout(cout_out)
);
// 生成激励的initial块
initial begin
// 打开波形文件
$dumpfile("full_adder.vcd");
$dumpvars(0, tb_full_adder);
// 测试用例1
a_in = 0; b_in = 0; cin_in = 0;
#10; // 等待10个时间单位
// 测试用例2
a_in = 0; b_in = 0; cin_in = 1;
#10;
// 测试用例3
a_in = 0; b_in = 1; cin_in = 0;
#10;
// ... 更多测试用例 ...
$display("Simulation finished.");
$finish; // 结束仿真
end
// 监视输出
initial begin
$monitor("Time = %0t, a=%b, b=%b, cin=%b, sum=%b, cout=%b",
$time, a_in, b_in, cin_in, sum_out, cout_out);
end
endmodule
2 有限状态机
FSM是数字系统设计的核心,用于控制复杂的操作序列,它由状态、状态寄存器和组合逻辑(次态逻辑和输出逻辑)组成。
FSM有两种类型:
- Moore型: 输出只取决于当前状态。
- Mealy型: 输出取决于当前状态和输入。
设计步骤:
- 分析问题: 确定需要哪些状态。
- 画状态转移图: 清晰地表示状态转移条件和输出。
- 状态编码: 为每个状态分配一个唯一的二进制码。
- 写出Verilog代码:
- 定义
reg来存储当前状态和次态。 - 用一个
always块(在时钟边沿)来更新状态。 - 用另一个
always块(组合逻辑)或assign来计算次态和输出。
- 定义
3 同步设计原则
为了设计稳定、可靠的数字系统,必须遵循同步设计原则:
- 全局时钟: 整个系统由一个主时钟驱动,所有时序逻辑都在该时钟的边沿触发。
- 单时钟沿: 尽量只使用时钟的单一沿(通常是上升沿)来触发所有寄存器。
- 避免异步信号: 外部输入的信号通常是异步的,必须同步到系统时钟域,最简单的方法是使用两级触发器同步器。
reg [1:0] sync_reg; always @(posedge clk) begin sync_reg[0] <= async_in; sync_reg[1] <= sync_reg[0]; end // 使用 sync_reg[1] 作为同步后的信号 - 注意建立和保持时间: 这是时序分析的基础,确保数据在时钟有效沿到来前稳定足够长时间(建立时间),并在沿到来后保持稳定足够长时间(保持时间)。
第六部分:实践项目
项目:设计一个简单的数字时钟
功能:
- 显示时、分、秒。
- 有一个复位键,可以将时钟复位到00:00:00。
- 有一个使能键,可以暂停/继续计时。
设计思路:
-
顶层模块 (
digital_clock):- 输入:
clk(1Hz时钟),reset,enable。 - 输出:
hour[5:0],minute[6:0],second[6:0](7位二进制可以表示0-99)。 - 内部实例化三个计数器模块。
- 输入:
-
计数器模块 (
counter):- 参数化,可以设置最大计数值。
- 输入:
clk,reset,enable。 - 输出:
count。 - 功能: 当
enable为高时,在clk的上升沿进行计数,当计数值达到最大值时清零并产生进位信号。
-
连接逻辑:
- 秒计数器的进位连接到分计数器的
enable。 - 分计数器的进位连接到时计数器的
enable。
- 秒计数器的进位连接到分计数器的
这个项目能很好地练习模块化设计、参数化模块和时序逻辑。
第七部分:学习资源与工具推荐
书籍
- 入门经典:
- 《Verilog HDL入门》 - J. Bhasker:非常适合初学者,语言通俗易懂,例子丰富。
- 进阶圣经:
- 《Verilog HDL高级数字设计》 - Clive "Max" Maxfield:非常有趣,从工程实践角度出发,讲解了很多设计技巧和陷阱。
- 《Digital Design and Computer Architecture》 - Harris & Harris:结合了数字设计和计算机体系结构,讲解MIPS处理器设计,非常经典。
在线资源
- Nandland: https://www.nandland.com/ - 提供非常棒的Verilog和FPGA入门教程,互动性强。
- ASIC World: http://www.asic-world.com/verilog/ - 一个非常全面的Verilog语法参考手册。
- Coursera / edX: 搜索 "Digital Design" 或 "Computer Architecture",有很多名校的优质课程。
- FPGA厂商官方文档: Xilinx (AMD) 和 Intel (Altera) 提供了海量的官方教程、App Notes和示例代码,是解决实际问题的最佳资源。
工具
- 仿真与波形查看:
- Icarus Verilog + GTKWave: 免费,跨平台,命令行操作,适合学习底层原理。
- Verilator: 开源,将Verilog转成C++模型,速度快,适合大型项目验证。
- ModelSim/QuestaSim: 工业界标准,图形界面友好,调试功能强大(学生版免费但有功能限制)。
- FPGA开发套件:
- Xilinx Vivado / Vitis: 用于Xilinx FPGA开发。
- Intel Quartus Prime: 用于Intel FPGA开发。
- Lattice Diamond: 用于Lattice FPGA开发。
学习建议
- 动手实践: 不要只看不练,跟着教程敲代码,然后自己修改、扩展功能。
- 从简单开始: 先实现一个门,再做一个多路选择器,然后是一个计数器,最后再挑战复杂的FSM或CPU。
- 仿真先行: 每写一个模块,都为其编写一个测试平台,仿真验证是数字设计的核心环节。
- 理解综合: 思考你写的Verilog代码最终会生成什么样的硬件电路,使用综合工具(如Vivado)查看RTL视图和门级视图,这会让你对硬件有更直观的认识。
- 阅读优秀代码: 阅读开源项目(如RISC-V CPU核心)的代码,学习别人的设计风格和技巧。
祝你学习顺利,早日成为一名优秀的数字设计工程师!
