亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial programming >> VHDL

如何为 VHDL 代码锁定模块创建 Tcl 驱动的测试平台

大多数 VHDL 仿真器使用工具命令语言 (Tcl) 作为其脚本语言。当您在模拟器的控制台中键入命令时,您正在使用 Tcl。此外,您可以使用 Tcl 创建在模拟器中运行并与您的 VHDL 代码交互的脚本。

在本文中,我们将创建一个使用 Tcl 而不是 VHDL 的自检测试平台来验证 VHDL 模块的行为是否正确。

另请参阅:
为什么需要学习 Tcl
使用 Tcl 的交互式测试台

您可以使用下面的表格下载本文和 ModelSim 项目中的代码。

DUT:VHDL 中的密码锁模块

在开始测试平台之前,我将介绍被测设备 (DUT)。它将是一个密码锁模块,当我们在密码键盘上输入正确的数字序列时,它将解锁保险库。

人们通常将密码锁称为密码锁 .但是,我认为这个术语是不准确的。输入正确的数字组合来解锁它是不够的。您还必须以正确的顺序输入它们。严格来说,密码锁实际上是一个排列锁 ,但我们称它为 密码锁 .

上图显示了酒店保险箱形式的密码锁。为简单起见,我们的示例将仅使用数字键,而不使用“CLEAR”和“LOCK”按钮。

密码锁模块的工作原理

我们的模块将从锁定位置开始,如果我们连续输入与密码匹配的四位数字,它将解锁保险箱。要重新锁定它,我们可以输入另一个错误的数字。因此,我们需要在 VHDL 中创建一个序列检测器。

上面的波形显示了密码锁模块是如何工作的。除了时钟和复位,还有两个输入信号:input_digitinput_enable .当使能在时钟上升沿为“1”时,模块将对输入数字进行采样。

这个模块只有一个输出:unlock 信号。想象一下,它控制着保险箱或某种保险库的锁定机制。 解锁 只有当用户输入了与正确 PIN 匹配的四个连续数字时,信号才应为“1”。在本文中,我们将使用 1234 作为密码。

实体

下面的代码显示了密码锁模块的实体。因为这个模块的目的是为我们基于 TCL 的测试平台提供一个简单的 DUT 示例,所以我使用泛型对密码进行硬编码。这四个通用常量是二进制编码的十进制 (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;

就像密码一样,input_digit 信号也是 BCD 类型。其他输入和输出是 std_logics。

声明区域

该模块只有一个内部信号:一个移位寄存器,其中包含用户键入的最后四位数字。但我们没有使用 0 到 9 的 BCD 范围,而是让数字从 -1 到 9。这是 11 个可能的值。

type pins_type is array (0 to 3) of integer range -1 to 9;
signal pins : pins_type;

我们必须使用一个不是用户可以输入的数字的重置值,这就是 -1 的用途。如果我们为 pins 使用了 0 到 9 的范围 数组,将秘密密码设置为 0000 最初会打开保险库。使用这种方案,用户必须明确输入四个 0。

实现

在架构区域的顶部,我添加了一个并发语句,当 pins 信号匹配通用常量。下面的代码是组合的,但由于 pins 信号被计时,解锁 信号只会在时钟的上升沿发生变化。

unlock <= '1' when pins = (pin3, pin2, pin1, pin0) else '0';

下面的代码显示了读取用户输入的过程。它用 pin 制作了一个移位寄存器 input_enable 时通过移动所有值来发出信号 在时钟上升沿为“1”。结果是用户输入的最后四位数字存储在 pins 中 数组。

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 测试平台

首先,我们仍然需要一个基本的 VHDL 测试平台,即使我们使用 Tcl 进行验证。下面的代码显示了完整的 VHDL 文件。我已经实例化了 DUT 并创建了时钟信号,但仅此而已。除了生成时钟,这个测试平台什么都不做。

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 VHDL 仿真器。例如,如果您想在 Vivado 中使用它,您必须对其进行一些更改。那是因为它使用了一些特定于这个模拟器的命令。使用 Tcl 的一个缺点是您的代码会被锁定给特定的模拟器供应商。

作为参考,我推荐 Tcl Developer Xchange,它涵盖了一般的 Tcl 语言,以及 ModelSim 命令参考手册,它描述了所有 ModelSim 特定的命令。

如果您安装了 ModelSim,您可以使用下面的表格下载示例项目。

使用命名空间

我推荐的第一件事是创建一个 Tcl 命名空间。这是一个好主意,因为否则,您可能会无意中覆盖 Tcl 脚本中的全局变量。通过将所有代码包装在命名空间中,可以避免潜在的混乱。我们将从现在开始编写的所有 Tcl 代码放入 codelocktb 命名空间,如下图。

namespace eval ::codelocktb {

  # Put all the Tcl code in here

}

在命名空间内,我们必须从开始模拟开始,如下所示。我们使用 vsim 来做到这一点 命令,后跟 VHDL 测试平台的库和实体名称。这将加载模拟,但不会运行它。在我们使用 run 之前,没有模拟时间过去 命令稍后在脚本中。我还想包含一个 If 语句,它将加载波形(如果存在)。

# Load the simulation
vsim work.code_lock_tb

# Load the waveform
if {[file exists wave.do]} {
  do wave.do
}

声明命名空间变量

现在我们已经加载了仿真,我们可以开始与 VHDL 代码交互了。首先,我想阅读 clock_period 常量和密码通用到 Tcl 环境中。

在下面的代码中,我使用的是特定于 ModelSim 的 examine 命令读取 Tcl 中的 VHDL 信号和常数值。然后,我使用 Tcl 字符串和列表命令来提取时间值和时间单位。 密码 变量变成了我们从通用常量中读取的四位数字的列表。

# Read the clock period constant from the VHDL TB
variable clockPeriod [examine clock_period]

# Strip the braces: "{10 ns}" => "10 ns"
variable clockPeriod [string trim $clockPeriod "{}"]

# Split the number and the time unit
variable timeUnits [lindex $clockPeriod 1]
variable clockPeriod [lindex $clockPeriod 0]

# Read the correct PIN from the VHDL generics
variable pinCode [examine dut.pin0 dut.pin1 dut.pin2 dut.pin3]

请注意,我在 Tcl 脚本中使用的编码风格与在 VHDL 代码中不同。而不是下划线,我使用的是驼色套管。那是因为我遵循 Tcl 风格指南。当然,如果你喜欢的话,没有什么能阻止你在 Tcl 和 VHDL 文件中使用相同的样式。

此外,如果您使用过没有命名空间的 Tcl,您可能知道 set 关键字,这是在 Tcl 中定义变量的标准方法。在这里,我使用了较新的变量关键字。它就像一个全局变量,绑定到当前命名空间而不是全局范围。

最后,我们声明一个名为 errorCount 的变量 并将其初始化为0,如下所示。随着模拟通过测试用例进行,我们将在每次检测到错误时增加它。最后,我们可以用它来判断模块是通过还是失败。

variable errorCount 0

在 ModelSim 中打印文本

puts 命令是在 Tcl 中将文本打印到控制台的标准方式。但是这种方法在 ModelSim 中以一种不幸的方式起作用。 Windows 版本可以满足您的期望;它将字符串打印到控制台。另一方面,在 Linux 版本中,文本是在您启动 ModelSim 的 shell 中输出的,而不是在 GUI 中的控制台中。

下图显示了当我们输入 puts 时会发生什么 ModelSim 控制台中的命令。它出现在后面的终端窗口中。更糟糕的是,如果你使用桌面快捷方式启动 ModelSim,你将永远看不到输出,因为 shell 是隐藏的。

有一些变通方法可以改变 puts 的行为 命令。例如,您可以重新定义它(是的!您可以在 Tcl 中做到这一点)并使其在两个平台上都可以工作。但是在 Linux 和 Windows 中将文本打印到控制台的更直接的方法是使用 ModelSim 特定的 echo 命令。

我们将使用如下所示的自定义 Tcl 过程来打印文本。在这样做的同时,我们还在消息前面加上当前的模拟时间。在 ModelSim 中,您始终可以使用 $now 全局变量。

proc printMsg { msg } {
  global now
  variable timeUnits
  echo $now $timeUnits: $msg
}

模拟 N 个时钟周期

DUT 是一个时钟模块,这意味着在时钟上升沿之间不会发生任何事情。因此,我们希望根据时钟周期的持续时间逐步模拟。下面的 Tcl 过程使用 clockPeriod时间单位 我们之前从 VHDL 代码中提取的变量来实现这一点。

proc runClockCycles { count } {
  variable clockPeriod
  variable timeUnits

  set t [expr {$clockPeriod * $count}]
  run $t $timeUnits
}

该过程采用一个参数:count .我们将它与一个时钟周期的长度相乘,得到 N 个时钟周期的持续时间。最后,我们使用 ModelSim run 命令来模拟这么长时间。

检查来自 Tcl 的信号值

在 ModelSim 中,我们可以使用 examine 从 Tcl 读取 VHDL 信号 命令。下面的代码显示了我们用来读取信号值并检查它是否符合预期的 Tcl 过程。如果信号与 expectedVal 不匹配 参数,我们打印一条讨厌的消息并增加 errorCount 变量。

proc checkSignal { signalName expectedVal } {
  variable errorCount

  set val [examine $signalName]
  if {$val != $expectedVal} {
    printMsg "ERROR: $signalName=$val (expected=$expectedVal)"
    incr errorCount
  }
}

测试 PIN 序列

密码锁模块的输出不仅取决于当前输入,还取决于它们之前的值。因此,必须至少在将四位数字发送到 DUT 之后检查输出。只有在密码正确的情况下,解锁信号才会从“0”变为“1”。

下面的 Tcl 程序使用 ModelSim force 从 Tcl 更改 VHDL 信号的关键字。 -存款 切换到强制 关键字表示 ModelSim 将更改该值,但稍后让另一个 VHDL 驱动程序控制它,尽管在我们的测试台中没有其他实体控制 DUT 输入。

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
  }
}

tryPin 程序使用我们的 printMsg 程序来告知它正在做什么,它正在输入哪个 PIN 码,以及它是否是正确的密码。它还使用 runClockCycles 程序运行一个时钟周期,同时操纵 DUT 输入来模拟用户输入 PIN。

最后,它使用 checkSignal 验证 DUT 是否按预期运行的程序。正如我已经解释过的,checkSignal 过程将打印一条错误消息并增加 errorCount 解锁时的变量 信号与预期值不符。

测试用例和完成状态

在上面的 Tcl 代码中,我们已经开始了模拟,我们定义了一堆变量和过程,但我们根本没有模拟任何时间。仿真仍处于 0 ns。没有模拟时间过去。

在我们的自定义命名空间快结束时,我们开始调用 Tcl 过程。如下面的代码所示,我们首先运行十个时钟周期。之后,我们释放重置并检查 unlock 输出的期望值为“0”。

runClockCycles 10

# Release reset
force rst '0' -deposit
runClockCycles 1

# Check reset value
printMsg "Checking reset value"
checkSignal unlock 0

# Try a few corner cases
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)"
}

我们可以尝试所有 10000 个不同的 PIN 码,但这需要相当长的时间。 Tcl 驱动的仿真比纯 VHDL 测试平台慢得多。模拟器必须经常启动和停止,这需要很多时间。因此,我决定只检查极端情况。

我们称 tryPin 四次,使用 PIN 码:0000、9999、正确的 PIN,以及正确 PIN 中的数字以相反的顺序排列。我想在创建密码锁时这是一个容易犯的错误,只看组合,而不看数字的顺序。

最后,在 Tcl 代码的最后,但仍在命名空间内,我们检查 errorCount 变量并打印“测试:OK”或“测试失败”。

运行测试台

现在是有趣的部分:运行测试台。我更喜欢使用 Tcl source 命令,如下图,但你也可以使用 ModelSim 特有的 do 命令。事实上,ModelSim DO 文件实际上只是具有不同后缀的 Tcl 文件。

source code_lock/code_lock_tb.tcl

在我的代码的最终版本中,没有错误。下面的清单显示了成功模拟的输出。 Tcl 脚本通知我们它在做什么,我们可以看到所有消息行都有一个时间戳。那是我们的 printMsg 工作中的程序。最后,测试台停止并打印“Test:OK”。

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 未通过测试时的样子。为此,我在密码锁模块中创建了一个错误。我已经替换了 pin1 的检查 与 pin2 以便 DUT 忽略 pin1 价值。很容易打错,如下代码所示。

unlock <= '1' when pins = (pin3, pin2, pin2, pin0) else '0';

当我们现在运行测试平台时,您可以从下面的清单中看到检测到故障。最后,测试台会打印“Test:Failure”以及错误数量。

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 的测试平台,但我对它们的看法有些分歧。

一方面,您可以做一些单独使用 VHDL 无法完成的很酷的事情。例如,交互式测试平台。无需重新编译即可更改测试平台也很好。最后,使用完全不同的语言进行验证可能是有利的。你必须在两种不同的技术中犯同样的错误才能让它通过而不被发现,这是不太可能的。

另一方面,也有一些缺点。基于 Tcl 的测试平台比 VHDL 对应的测试平台慢很多。另一个重要问题是供应商锁定。创建完全可移植的 Tcl 测试台是不可能的,而 VHDL 测试台可以在任何有能力的模拟器上运行。

Tcl 测试平台可能不值得的最后一个原因是语言本身。它没有很好的防止编程错误的功能,而且调试 Tcl 问题也很困难。它不像 Python 或 Java 那样直观也不宽容。

然而,它的作用是作为 VHDL 和软件世界之间的粘合语言。而且因为大多数 FPGA 工具,不仅是模拟器,都支持 Tcl,我强烈推荐学习它。

这些想法只是我的意见。在评论部分告诉我你的想法!


VHDL

  1. 如何在 VHDL 中创建字符串列表
  2. 如何在 VHDL 测试平台中停止仿真
  3. 如何在 VHDL 中创建 PWM 控制器
  4. 如何在 VHDL 中创建环形缓冲区 FIFO
  5. 使用 Tcl 的交互式测试平台
  6. 如何创建自检测试平台
  7. 如何在 VHDL 中创建链接列表
  8. 如何在 VHDL 中使用不纯函数
  9. 如何在 VHDL 中使用函数
  10. 如何在 VHDL 中创建有限状态机
  11. 如何在 VHDL 中使用过程
  12. 如何免费安装 VHDL 模拟器和编辑器