本节摘要:测试台是给被测设计"出考卷、监考、收卷"的顶层仿真环境。本节搭一个结构化的标准测试台——时钟发生器、复位序列、激励进程、实例化四件套,讲清测试台代码与设计代码的风格差异,并建立可持续扩展的工程布局。
第 1 章已经跑过一个最简测试台,现在是把它"工程化"的时候。标准结构四件套:信号声明区(所有激励与观测信号)、实例化(把被测设计挂上来)、时钟与复位发生器(所有测试共用的基础设施)、激励进程(测试用例本体)。以第 4 章的 ALU 为例搭一个完整版:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity alu8_tb is end entity alu8_tb; -- 测试台无端口,它就是顶层 architecture sim of alu8_tb is -- 四件套之一:信号区 signal a, b, y : std_logic_vector(7 downto 0) := (others => '0'); signal op : std_logic_vector(2 downto 0) := (others => '0'); signal zero, ovf : std_logic := '0'; signal clk : std_logic := '0'; signal rst : std_logic := '1'; signal done : boolean := false; -- 停机旗标 begin -- 四件套之二:实例化被测设计 dut : entity work.alu8 port map (a => a, b => b, op => op, y => y, zero => zero, ovf => ovf); -- 四件套之三:时钟发生器,done 拉真即停 clk_gen : process begin while not done loop clk <= '0'; wait for 5 ns; clk <= '1'; wait for 5 ns; end loop; wait; end process; -- 四件套之四:主激励进程 main : process procedure check_one(xa, xb : in natural; xop : in natural; expect_y : in natural; tag : in string) is variable got : natural; begin a <= std_logic_vector(to_unsigned(xa, 8)); b <= std_logic_vector(to_unsigned(xb, 8)); op <= std_logic_vector(to_unsigned(xop, 3)); wait for 20 ns; -- 组合电路,留稳时间 got := to_integer(unsigned(y)); assert got = expect_y report "FAIL " & tag & ": 期望 " & integer'image(expect_y) & " 实得 " & integer'image(got) severity error; end procedure; begin wait for 15 ns; rst <= '0'; check_one(10, 20, 0, 30, "加法"); -- 10+20=30 check_one(200, 100, 1, 100, "减法"); -- 200-100=100 check_one(12, 10, 2, 8, "与"); -- 1100 and 1010 = 1000 check_one(127, 1, 0, 128, "加法边界"); -- 127+1(无符号视角) done <= true; wait; end process; end architecture sim;
信息量不小,逐块拆。时钟发生器用 while not done 循环代替裸循环,激励进程把 done 拉真后时钟自然停摆,比第 1 章"时钟永远跑"的写法可控。激励进程里定义了过程(procedure) check_one——测试台代码的高级特性用起来不客气:过程把"发激励、等稳定、判分、报错"固化成一次调用,新增用例只要加一行。这正体现了测试台与设计代码的风格分野:设计代码为综合器而写、处处克制;测试台为人的效率而写、尽管用高级特性。

测试台一多,文件组织要有章法。推荐"一设计一测试、同名配对":alu8 与 alu8_tb 文件名成对出现,放同一目录,仿真顶层永远是 _tb 结尾的实体。公共激励(比如标准的复位序列、总线功能模型)抽成包(package)放在工程公共库,多个测试台 use 同一个包——测试代码的复用纪律与设计代码完全一致。
另一个值得养成的习惯是用常量参数化测试参数:时钟周期、复位时长、用例数量都写成测试台顶部的常量,调周期只改一处。测试台虽然不综合,但它是会跟着项目活很久的代码,随手写的债迟早要还。
跑起来的命令与第 1 章相同(分析、精化测试台实体、运行导出波形)。命令行里除了波形文件,还应看到激励进程打印的每一句 FAIL——没有 FAIL 输出且仿真正常结束,就是全绿的信号。
⚠️ 常见坑:激励在时钟沿"恰好同时"变化,制造仿真竞赛——设计在真实电路里采到的是稳定值,仿真里却可能采到变化前一瞬的值。稳妥做法是激励避开时钟沿(比如时钟沿后偏移一两纳秒再变),让采样窗口干净。
💡 关键直觉:测试台的四个部分对应监考的四件事——出卷(激励)、发卷(实例化接线)、控制时间(时钟复位)、阅卷(断言)。缺哪件,考试就不可信。
VHDL testbench 没有"标准库"——每个项目都要从零写一个。推荐维护一个通用 testbench 模板:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity tb_dut is generic( CLK_PERIOD : time := 10 ns ); end entity; architecture sim of tb_dut is signal clock : std_logic := '0'; signal reset : std_logic := '1'; begin -- 时钟生成 clock <= not clock after CLK_PERIOD / 2; -- 复位序列 process begin reset <= '1'; wait for CLK_PERIOD * 5; reset <= '0'; wait; end process; -- DUT 实例化 dut: entity work.dut port map( clock => clock, reset => reset, ... ); -- 激励生成 stim: process begin wait until reset = '0'; wait for CLK_PERIOD * 2; -- 测试场景 ... report "ALL TESTS PASSED" severity note; std.env.finish; end process; end architecture;
不要用 if-then-report 手写 100 个检查点。改用 PSL 或 VHDL-2008 的 assertion:
assert counter < 16 report "Counter overflow" severity failure;
配合 simulator 的 -assert 选项,失败立即停止。这是验证大规模设计的标准方法。
"把所有测试场景写在一个进程里"——50 个场景叠在一起,定位失败时不知道是哪一步崩的。应该把每个场景独立成 process,用 wait until reset = '0' and rising_edge(clock) 同步。