4. 硬件描述语言入门:Verilog/VHDL基础语法、模块化设计、Testbench编写
好,咱们进入正题。硬件描述语言,说白了就是给芯片画电路图的「代码版」。你想想看,总不能真拿笔在纸上画几万个晶体管吧?所以我们需要 Verilog 或 VHDL 这种语言,把电路行为描述出来,然后交给工具去综合成真实的门级网表。
我个人习惯用 Verilog,因为它语法更灵活,在工业界和学术界都占主流。当然 VHDL 也有它的优势,比如强类型检查更严格。但今天咱们以 Verilog 为主线,VHDL 我会在关键地方做个对比。
4.1 基础语法:先跑通一个最小系统
先看一个最简单的例子——一个与门。别小看它,这是所有复杂逻辑的基石。
// Verilog 与门
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodule
嗯,这里要注意:assign 是连续赋值语句,它描述的是组合逻辑。只要 a 或 b 变了,y 立刻跟着变——这就像一根导线直接连过去。
VHDL 版本长这样:
-- VHDL 与门
entity and_gate is
port (
a : in std_logic;
b : in std_logic;
y : out std_logic
);
end entity;
architecture rtl of and_gate is
begin
y <= a and b;
end architecture;
看到区别了吗?VHDL 把端口声明和功能实现分成了 entity 和 architecture 两部分。Verilog 则合在一个 module 里。我个人觉得 Verilog 写起来更顺手,但 VHDL 在大型项目中更容易维护——因为结构更清晰。
核心概念:硬件描述语言的核心是「并行」和「时序」。软件代码是顺序执行的,但硬件里所有 assign 和 always 块都是同时运行的。这一点刚转行的人最容易搞混。
4.2 模块化设计:像搭积木一样建芯片
我在项目中遇到过最头疼的事,就是有人把整个设计写在一个几千行的 module 里。调试起来简直噩梦。所以模块化设计不是可选项,是必选项。
模块化设计的原则很简单:
- 高内聚低耦合——每个模块只做一件事,接口尽量简单
- 层次化——顶层模块只负责连线,具体功能交给子模块
- 参数化——用 parameter 定义位宽、深度等常量,方便复用
举个例子,一个计数器模块:
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
然后顶层模块可以这样例化:
module top (
input wire clk,
input wire rst_n,
input wire en,
output wire [7:0] result
);
counter #(.WIDTH(8)) u_counter (
.clk (clk),
.rst_n(rst_n),
.en (en),
.count(result)
);
endmodule
你看,顶层只负责把信号连进去,具体怎么计数是 counter 的事。这样如果以后要改成 16 位计数器,改一个参数就行,不用动顶层。
我的经验:模块的端口数量最好控制在 10 个以内。超过这个数,连线的可读性会急剧下降。我曾经接手过一个模块,端口有 40 多个,光看接口定义就花了半天。后来我把它拆成了 5 个小模块,每个模块 8 个端口,世界清净了。
4.3 Testbench 编写:没有仿真,一切都是空谈
写完了代码,怎么知道它对不对?靠眼睛看?别闹。必须写 Testbench 做仿真。
Testbench 的核心思路是:
- 例化待测模块(DUT)
- 生成时钟和复位信号
- 施加激励(输入信号的变化)
- 检查输出是否符合预期
一个简单的 Testbench 示例:
`timescale 1ns / 1ps
module tb_counter;
reg clk;
reg rst_n;
reg en;
wire [7:0] count;
// 例化 DUT
counter #(.WIDTH(8)) u_dut (
.clk (clk),
.rst_n(rst_n),
.en (en),
.count(count)
);
// 生成时钟:周期 10ns
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 施加激励
initial begin
rst_n = 0;
en = 0;
#20;
rst_n = 1;
#10;
en = 1;
#100;
en = 0;
#50;
$finish;
end
// 监控输出
initial begin
$monitor("time=%0t, count=%d", $time, count);
end
endmodule
这里有几个关键点:
`timescale定义了时间单位和精度。1ns/1ps 表示单位是 1ns,精度到 1psinitial块是顺序执行的,但多个 initial 块之间是并行的forever用来生成周期信号,比如时钟$monitor会在信号变化时自动打印,调试利器
避坑指南:我曾经犯过一个低级错误——在 Testbench 里用 #5 延时,但忘了加 `timescale。结果仿真器默认单位是 1ps,实际延时只有 5ps,时钟跑得飞快,DUT 根本来不及响应。所以记住:写 Testbench 第一行必须是 `timescale。
4.4 知识体系总览
下面这张图概括了本章的核心内容。你可以把它当作一个思维导图,方便回顾。
4.5 实战建议:从哪开始练手?
光看不练是学不会的。我建议你按这个顺序来:
- 先写一个 8 位加法器——用 assign 实现,跑仿真验证
- 再写一个带使能的计数器——用 always 块,理解时序逻辑
- 然后写一个状态机——比如序列检测器,这是数字设计的核心
- 最后把这三个模块组合起来——练练模块化设计和 Testbench 的层次化
每个练习都写对应的 Testbench,确保覆盖率。我当年刚入行时,带我的老工程师说了一句话,我一直记着:「没有 Testbench 的代码,等于没写。」
总结一下:Verilog/VHDL 是硬件工程师的「普通话」。语法不难,难的是思维方式的转变——从软件的顺序思维切换到硬件的并行思维。多写、多仿真、多踩坑,自然就熟了。
公众号:蓝海资料掘金营,微信deep3321