基于 Tcl 驱动的 VHDL 密码锁模块测试平台
大多数 VHDL 仿真器将工具命令语言 (Tcl) 作为主要脚本接口。在仿真器控制台输入命令即是在使用 Tcl,也可编写脚本驱动和观测 VHDL 设计。
本文指导构建自检测试平台,用 Tcl 而非 VHDL 验证简单密码锁模块行为。结果是一个清晰、可维护的测试框架,无需重新编译 VHDL 即可运行、修改和复用。
延伸阅读:
下方下载代码和 ModelSim 工程文件。
DUT:VHDL 密码锁模块
深入测试平台前,先回顾被测设备 (DUT)。该模块模拟简单锁,在数字小键盘输入正确数字序列时打开保险箱。
虽常称「组合锁」,但正确术语是「排列锁」——数字顺序至关重要。为清晰起见,全文称其为 代码锁。

上图为典型酒店保险箱。为简化,设计仅使用数字键,省略 CLEAR 和 LOCK 键。
代码锁模块工作原理
锁初始为锁定状态。用户连续输入四位匹配秘密 PIN 的数字时解锁。输入错误序列则重新上锁。此行为以简单序列检测器用 VHDL 实现。
波形展示模块时序。除时钟和复位外,模块接受两个输入:input_digit (0–9 整数) 和 input_enable (std_logic)。DUT 在使能为高的上升沿采样数字。
唯一输出为 unlock (std_logic),仅当连续四位数字匹配正确 PIN 时置高。本例密码为 1234。
实体
以下实体定义通过泛型硬编码秘密 PIN。每个泛型为 0–9 范围整数,代表二进制编码十进制 (BCD) 数字。
entity code_lock is generic (pin0, pin1, pin2, pin3 : integer range 0 to 9); port ( clk : in std_logic; rst : in std_logic; input_digit : in integer range 0 to 9; input_enable : in std_logic; unlock : out std_logic ); end code_lock;
声明区
模块使用单个内部信号——4 位移位寄存器存储最近输入的数字。允许寄存器保持 -1 到 9 的值。特殊值 -1 用于复位状态,确保 PIN 0000 不会默认解锁。
type pins_type is array (0 to 3) of integer range -1 to 9; signal pins : pins_type;
实现
架构包含组合赋值,当移位寄存器匹配泛型 PIN 值时驱动 unlock 为高。因 pins 是时序的,unlock 仅在上升沿变化。
unlock <= '1' when pins = (pin3, pin2, pin1, pin0) else '0';
下面进程实现移位寄存器。复位时所有项设为 -1。上升沿 input_enable 为高时,当前数字存入位置 0,前值右移。
PINS_PROC : process(clk) begin if rising_edge(clk) then if rst = '1' then pins <= (others => -1); else if input_enable = '1' then pins(0) <= input_digit; pins(1 to 3) <= pins(0 to 2); end if; end if; end if; end process;
VHDL 测试平台
虽用 Tcl 做验证逻辑,仍需基础 VHDL 测试平台生成时钟和复位。下面是完整 VHDL 测试平台文件。
library ieee; use ieee.std_logic_1164.all; entity code_lock_tb is end code_lock_tb; architecture sim of code_lock_tb is constant clk_hz : integer := 100e6; constant clock_period : time := 1 sec / clk_hz; signal clk : std_logic := '1'; signal rst : std_logic := '1'; signal input_digit : integer range 0 to 9; signal input_enable : std_logic := '0'; signal unlock : std_logic; begin clk <= not clk after clock_period; DUT : entity work.code_lock(rtl) generic map (1,2,3,4) port map ( clk => clk, rst => rst, input_digit => input_digit, input_enable => input_enable, unlock => unlock ); end architecture;
Tcl 测试平台
下面 Tcl 脚本在 ModelSim 中运行。若用 Vivado 或其他仿真器,需微调,因部分命令为仿真器专用。详见 ModelSim 命令参考手册。
下方表单下载示例工程。
使用命名空间
将脚本封装在命名空间防止全局变量冲突。所有 Tcl 代码位于 codelocktb 命名空间。
namespace eval ::codelocktb { # All Tcl code here }
初始化仿真
用 vsim 启动仿真,若存在波形文件则加载。
# Load the simulation vsim work.code_lock_tb # Load the waveform if {[file exists wave.do]} { do wave.do }
将 VHDL 值导入 Tcl
用 ModelSim 的 examine 命令读取 clock_period 常量和 PIN 泛型。下面片段提取数值和时间单位。
variable clockPeriod [examine clock_period] variable clockPeriod [string trim $clockPeriod "{}"] variable timeUnits [lindex $clockPeriod 1] variable clockPeriod [lindex $clockPeriod 0] variable pinCode [examine dut.pin0 dut.pin1 dut.pin2 dut.pin3]
初始化 errorCount 变量跟踪失败数。
variable errorCount 0
带时间戳打印
echo 命令在所有平台的 ModelSim 控制台显示消息。下面过程给每条消息加当前仿真时间前缀。
proc printMsg { msg } { global now variable timeUnits echo $now $timeUnits: $msg }
运行指定时钟周期数
因 DUT 对时钟敏感,按时钟周期倍数推进时间。
proc runClockCycles { count } { variable clockPeriod variable timeUnits set t [expr {$clockPeriod * $count}] run $t $timeUnits }
检查信号值
checkSignal 过程将 VHDL 信号与期望值比对,不匹配时报错。
proc checkSignal { signalName expectedVal } { variable errorCount set val [examine $signalName] if {$val != $expectedVal} { printMsg "ERROR: $signalName=$val (expected=$expectedVal)" incr errorCount } }
模拟 PIN 输入
此过程驱动 DUT 输入模拟用户输入 4 位 PIN。四位输入完毕后检查 unlock 输出。
proc tryPin { digits } { variable pinCode set pinStatus "incorrect" if { $digits == $pinCode } { set pinStatus "correct" } printMsg "Entering $pinStatus PIN code: $digits" foreach i $digits { force input_digit $i -deposit force input_enable 1 -deposit runClockCycles 1 force input_enable 0 -deposit runClockCycles 1 } if { $pinStatus == "correct" } { checkSignal unlock 1 } else { checkSignal unlock 0 } }
测试序列与结果报告
短暂初始化后释放复位,运行多个测试用例:两个边界情况 (0000 和 9999)、正确 PIN 和反序 PIN。最后报告整体结果。
runClockCycles 10 force rst '0' -deposit runClockCycles 1 printMsg "Checking reset value" checkSignal unlock 0 tryPin {0 0 0 0} tryPin {9 9 9 9} tryPin $pinCode tryPin [lreverse $pinCode] if { $errorCount == 0 } { printMsg "Test: OK" } else { printMsg "Test: Failure ($errorCount errors)" }
运行测试平台
在 ModelSim 中执行:
source code_lock/code_lock_tb.tcl
成功运行输出类似下面,每条消息前有时间戳。
VSIM> source code_lock/code_lock_tb.tcl # vsim # 110 ns: Checking reset value # 110 ns: Entering incorrect PIN code: 0 0 0 0 # 190 ns: Entering incorrect PIN code: 9 9 9 9 # 270 ns: Entering correct PIN code: 1 2 3 4 # 350 ns: Entering incorrect PIN code: 4 3 2 1 # 430 ns: Test: OK
若 DUT 有缺陷(如 unlock 赋值笔误),脚本报告失败和错误计数。
VSIM> source code_lock/code_lock_tb.tcl # vsim # 110 ns: Checking reset value # 110 ns: Entering incorrect PIN code: 0 0 0 0 # 190 ns: Entering incorrect PIN code: 9 9 9 9 # 270 ns: Entering correct PIN code: 1 2 3 4 # 350 ns: ERROR: unlock=0 (expected=1) # 350 ns: Entering incorrect PIN code: 4 3 2 1 # 430 ns: Test: Failure (1 errors)
结语
职业生涯中我编写过许多基于 Tcl 的测试平台。它们提供独特优势:交互式测试、无需重编译即可修改测试框架、额外验证层防范设计和测试平台双方的 Bug。
但也有代价。Tcl 脚本比纯 VHDL 测试平台明显慢,语言脆弱——错误难早期捕获,调试不如 Python 等高级语言直观。
尽管有缺点,Tcl 仍是 FPGA 工具链和仿真的事实标准胶水语言。若使用 Xilinx Vivado、Intel Quartus 或 Mentor Graphics Questa,精通 Tcl 将受益匪浅。欢迎在评论区分享经验。
VHDL