第三讲:硬件描述语言Verilog入门
各位同学好,我是你们的CPU架构设计讲师。今天我们来聊聊Verilog——这门在数字电路设计领域里,几乎人手必备的硬件描述语言。
说实话,我刚开始接触Verilog的时候,总觉得它跟C语言差不多。后来被现实狠狠教育了一顿——硬件描述语言和软件编程语言,完全是两码事。你想想看,C语言是顺序执行的,而Verilog描述的是并行工作的硬件电路。这个思维转变,是入门的第一道坎。
Verilog语法基础:从“线”和“寄存器”开始
Verilog的核心,说白了就是描述“线”和“寄存器”怎么连、怎么工作。我们先看几个最基础的东西。
1. 模块与端口
每个Verilog设计,都是从module开始的。模块就像是一个黑盒子,有输入、输出,内部封装了逻辑。
module adder (
input [3:0] a, // 4位输入
input [3:0] b, // 4位输入
output [4:0] sum // 5位输出(考虑进位)
);
assign sum = a + b;
endmodule
嗯,这里要注意:端口的方向一定要写清楚。input、output、inout,少写一个都不行。我见过太多新手因为漏了端口声明,仿真跑出来全是X态,急得满头大汗。
2. 数据类型:wire 与 reg
这是最容易混淆的地方。我简单总结一下:
- wire:表示连线,由组合逻辑驱动。比如assign语句的输出,必须是wire类型。
- reg:表示寄存器,在always块中被赋值。注意,reg不一定是真正的寄存器——在组合逻辑的always块中,它只是表示一个变量。
核心记忆法:在always块里赋值的变量,必须声明为reg;在assign语句里赋值的变量,必须声明为wire。
3. 运算符:跟C语言很像,但别大意
Verilog的运算符大部分跟C语言一样:+、-、*、&、|、^等等。但有几个特殊的:
===和!==:全等比较,会考虑X态和Z态。我在调试总线协议时,经常用这个来检查信号是否处于高阻态。{}:拼接运算符。比如{a, b}就是把a和b拼在一起。<<和>>:移位运算符。注意,算术右移>>>会保留符号位。
模块化设计:把大电路拆成小积木
做CPU设计,不可能把所有逻辑写在一个模块里。模块化设计,说白了就是“分而治之”。
我个人习惯把设计分成三层:
- 顶层模块:负责连接各个子模块,就像主板上插芯片。
- 功能模块:比如ALU、寄存器堆、控制单元,每个模块完成一个独立功能。
- 基础单元:比如加法器、多路选择器、触发器,这些是最小的积木块。
举个例子,一个简单的ALU模块:
module alu (
input [3:0] a, b,
input [1:0] op, // 00:加, 01:减, 10:与, 11:或
output reg [4:0] result
);
always @(*) begin
case (op)
2'b00: result = a + b;
2'b01: result = a - b;
2'b10: result = a & b;
2'b11: result = a | b;
default: result = 0;
endcase
end
endmodule
我的经验:写模块时,接口越简单越好。每个模块只做一件事,做好一件事。我曾经接手过一个同事写的模块,一个模块里塞了五个功能,调试了整整三天才理清楚。从那以后,我坚持“一个模块一个功能”的原则。
仿真与测试:不仿真的设计都是耍流氓
写完了代码,怎么知道对不对?仿真。说白了,就是给设计喂输入,看输出是不是你想要的。
1. Testbench 怎么写
Testbench也是一个模块,但它没有输入输出。它的任务就是:生成激励、检查结果。
module tb_adder;
reg [3:0] a, b;
wire [4:0] sum;
// 实例化被测试模块
adder u_adder (
.a(a),
.b(b),
.sum(sum)
);
// 生成测试激励
initial begin
a = 4'b0000; b = 4'b0000;
#10;
a = 4'b0011; b = 4'b0101;
#10;
a = 4'b1111; b = 4'b0001;
#10;
$finish;
end
// 监视输出
initial begin
$monitor("Time=%0t, a=%d, b=%d, sum=%d", $time, a, b, sum);
end
endmodule
这里有几个关键点:
initial块只执行一次,适合用来初始化。#10表示延迟10个时间单位。硬件世界里,时间就是一切。$monitor是系统任务,每次信号变化时自动打印。
2. 仿真工具的选择
常用的仿真工具有:
| 工具 | 特点 | 适用场景 |
|---|---|---|
| Vivado Simulator | Xilinx家的,跟Vivado集成 | Xilinx FPGA设计 |
| ModelSim/Questa | 业界标准,功能强大 | 通用数字设计 |
| Icarus Verilog | 开源免费,轻量级 | 学习和小型项目 |
| Verilator | 高性能,支持SystemVerilog | 大型项目仿真加速 |
避坑指南:我曾经用Icarus Verilog仿真一个带X态传播的设计,结果发现它把X态当成0处理了,导致我误以为设计没问题。后来换用ModelSim才抓到真正的bug。所以,仿真工具的选择很重要,尤其是处理X态和Z态时。
3. 仿真波形怎么看
仿真跑完了,会生成波形文件(.vcd或.wlf)。打开波形,你要关注的是:
- 时钟和复位:是不是按预期工作?
- 数据路径:输入到输出,延迟了几个周期?
- 控制信号:使能、选择、读写信号,时序对不对?
我个人的习惯是:先看整体波形,确认基本功能;再放大看细节,检查时序边界。尤其是建立时间和保持时间,这是最容易出问题的地方。
知识体系总览
下面这张图,是我自己总结的Verilog入门知识体系。你可以把它当作学习路线图:
写在最后
Verilog入门其实不难,难的是从软件思维切换到硬件思维。我见过很多有经验的软件工程师,写出来的Verilog代码跟C语言一样——到处都是for循环和function调用,综合出来一堆莫名其妙的电路。
记住一句话:你写的每一行Verilog,最终都会变成实实在在的电路。门、触发器、连线,它们都在那里。所以,写代码的时候,脑子里要想着电路图。
好了,这一讲就到这里。去写个加法器、写个计数器,跑一下仿真,看看波形。动手做一遍,比看十遍都管用。