信号与变量简要概述了 VHDL 的仿真语义

这个例子涉及 VHDL 语言最基本的方面之一:模拟语义。它适用于 VHDL 初学者,并提供了一个简化的视图,其中省略了许多细节(推迟的过程,VHDL 过程接口,共享变量……)对真正的完整语义感兴趣的读者应参考语言参考手册(LRM)。

信号和变量

大多数经典命令式编程语言都使用变量。它们是价值容器。赋值运算符用于在变量中存储值:

a = 15;

并且可以读取当前存储在变量中的值并在其他语句中使用:

if(a == 15) { print "Fifteen" }

VHDL 也使用变量,它们与大多数命令式语言具有完全相同的角色。但 VHDL 还提供了另一种价值容器:信号。信号还存储值,也可以分配和读取。可以存储在信号中的值的类型(几乎)与变量中的相同。

那么,为什么有两种价值容器呢?这个问题的答案是必不可少的,也是语言的核心。理解变量和信号之间的差异是在尝试用 VHDL 编程之前要做的第一件事。

让我们在一个具体的例子中说明这种差异:交换。

注意:以下所有代码段都是进程的一部分。我们稍后会看到什么是流程。

    tmp := a;
    a   := b;
    b   := tmp;

交换变量 ab。执行这 3 条指令后,a 的新内容是 b 的旧内容,反之亦然。与大多数编程语言一样,需要第三个临时变量(tmp)。如果我们想要交换信号而不是变量,我们会写:

    r <= s;
    s <= r;

要么:

    s <= r;
    r <= s;

结果相同,无需第三个临时信号!

注意:VHDL 信号分配运算符 <= 与变量赋值运算符:= 不同。

让我们看一下第二个例子,我们假设 print 子程序打印其参数的十进制表示。如果 a 是整数变量且其当前值为 15,则执行:

    a := 2 * a;
    a := a - 5;
    a := a / 5;
    print(a);

将打印:

5

如果我们在调试器中逐步执行此操作,我们可以看到 a 的值从最初的 15 变为 30,25,最后变为 5。

但是如果 s 是整数信号且其当前值为 15,则执行:

    s <= 2 * s;
    s <= s - 5;
    s <= s / 5;
    print(s);
    wait on s;
    print(s);

将打印:

15
3

如果我们在调试器中逐步执行此操作,则在 wait 指令之后才会看到 s 的任何值更改。而且,s 的最终值不会是 15,30,25 或 5 而是 3!

这种看似奇怪的行为是由于数字硬件的基本并行性质,我们将在以下部分中看到。

排比

VHDL 是硬件描述语言(HDL),它本质上是并行的。VHDL 程序是并行运行的顺序程序的集合。这些顺序程序称为进程:

P1: process
begin
  instruction1;
  instruction2;
  ...
  instructionN;
end process P1;

P2: process
begin
  ...
end process P2;

这些过程就像它们正在建模的硬件一样永远不会结束:它们是无限循环。执行完最后一条指令后,继续执行第一条指令。

与支持一种或另一种并行形式的任何编程语言一样,调度程序负责决定在 VHDL 模拟期间执行哪个进程(以及何时执行)。此外,该语言为进程间通信和同步提供了特定的结构。

调度

调度程序维护所有进程的列表,并为每个进程记录其当前状态,可以是 runningrun-ablesuspendedrunning 状态中最多只有一个进程:当前执行的进程。只要当前正在运行的进程不执行 wait 指令,它就会继续运行并阻止执行任何其他进程。VHDL 调度程序不是抢占式的:每个进程都有责任暂停自身并让其他进程运行。这是 VHDL 初学者经常遇到的问题之一:自由运行过程。

  P3: process
    variable a: integer;
  begin
    a := s;
    a := 2 * a;
    r <= a;
  end process P3;

注意:变量 a 在本地声明,而信号 sr 在其他地方声明,在更高的级别。VHDL 变量是声明它们的进程的本地变量,并且其他进程无法看到它们。另一个进程也可以声明一个名为 a 的变量,它不会与进程 P3 的变量相同。

一旦调度程序恢复 P3 进程,模拟就会卡住,模拟当前时间将不再进行,停止此操作的唯一方法是终止或中断模拟。原因是 P3 没有 wait 声明,因此将永远保持在 running 状态,循环其 3 条指令。没有其他过程将有机会运行,即使它是 run-able

即使包含 wait 语句的进程也可能导致同样的问题:

  P4: process
    variable a: integer;
  begin
    a := s;
    a := 2 * a;
    if a = 16 then
      wait on s;
    end if;
    r <= a;
  end process P4;

注意:VHDL 相等运算符是 =

如果在信号 s 的值为 3 时恢复进程 P4,它将永远运行,因为 a = 16 条件永远不会成立。

让我们假设我们的 VHDL 程序不包含这样的病态过程。当正在运行的进程执行 wait 指令时,它立即被挂起并且调度程序将其置于 suspended 状态。wait 指令也带有进程再次成为 run-able 的条件。例:

    wait on s;

意味着暂停我,直到信号 s 的值发生变化。调度程序记录此条件。然后调度程序在 run-able 中选择另一个进程,将其置于 running 状态并执行它。并且一直重复,直到所有 run-able 进程都被执行并暂停。

重要提示: 当几个进程是 run-able 时,VHDL 标准没有指定调度程序如何选择运行哪个进程。结果是,取决于模拟器,模拟器的版本,操作系统或其他任何东西,同一 VHDL 模型的两个模拟可以在一个点上做出不同的选择并选择要执行的不同过程。如果这个选择对模拟结果有影响,我们可以说 VHDL 是非确定性的。由于非确定性通常是不可取的,因此程序员有责任避免非确定性情况。幸运的是,VHDL 负责这一点,这就是信号进入图片的地方。

信号和进程间通信

VHDL 使用两个特定的特征来避免非确定性:

  1. 进程只能通过信号交换信息
  signal r, s: integer;  -- Common to all processes
...
  P5: process
    variable a: integer; -- Different from variable a of process P6
  begin
    a := s + 1;
    r <= a;
    a := r + 1;
    wait on s;
  end process P5;

  P6: process
    variable a: integer; -- Different from variable a of process P5
  begin
    a := r + 1;
    s <= a;
    wait on r;
  end process P6;

注意:VHDL 注释从 -- 扩展到行尾。

  1. 在执行过程期间,VHDL 信号的值不会改变

每次分配信号时,调度程序都会记录指定的值,但信号的当前值保持不变。这是与分配后立即获取新值的变量的另一个主要区别。

让我们看看上面的进程 P5 的执行情况,并假设 a=5s=1r=0 由调度程序恢复。执行指令 a := s + 1; 后,变量 a 的值发生变化,变为 2(1 + 1)。当执行下一条指令 r <= a; 时,它是分配给 ra(2)的新值。但是 r 是一个信号,r 的当前值仍为 0.因此,当执行 a := r + 1; 时,变量 a 取(立即)值 1(0 + 1),而不是 3(2 + 1),直觉会说。

什么时候会发出信号真的取其新价值?当调度程序执行所有可运行的进程时,它们都将被暂停。这也称为: 在一个三角形循环之后。只有这样,调度程序才会查看已分配给信号的所有值,并实际更新信号的值。VHDL 仿真是执行阶段和信号更新阶段的交替。在执行阶段,信号的值被冻结。象征性地,我们说在执行阶段和随后的信号更新阶段之间经过了时间的增量。这不是实时的。甲增量周期没有物理的持续时间。

由于这种延迟的信号更新机制,VHDL 是确定性的。进程只能与信号通信,并且在执行进程期间信号不会发生变化。因此,进程的执行顺序无关紧要:它们的外部环境(信号)在执行期间不会改变。让我们在前面的例子中用 P5P6 来说明这一点,其中初始状态是 P5.a=5P6.a=10s=17r=0,并且调度程序决定首先运行 P5 而接下来运行 P6。下表显示了两个变量的值,即执行每个过程的每条指令后的信号的当前值和下一个值:

过程/指令 P5.a P6.a s.current s.next r.current r.next
初始状态 10 17 0
P5 / a := s + 1 18 10 17 0
P5 / r <= a 18 10 17 0 18
P5 / a := r + 1 1 10 17 0 18
P5 / wait on s 1 10 17 0 18
P6 / a := r + 1 1 1 17 0 18
P6 / s <= a 1 1 17 1 0 18
P6 / wait on r 1 1 17 1 0 18
信号更新后 1 1 1 18

在初始条件相同的情况下,如果调度程序决定先运行 P6,然后运行 P5

过程/指令 P5.a P6.a s.current s.next r.current r.next
初始状态 10 17 0
P6 / a := r + 1 1 17 0
P6 / s <= a 1 17 1 0
P6 / wait on r 1 17 1 0
P5 / a := s + 1 18 1 17 1 0
P5 / r <= a 18 1 17 1 0 18
P5 / a := r + 1 1 1 17 1 0 18
P5 / wait on s 1 1 17 1 0 18
信号更新后 1 1 1 18

正如我们所看到的,在执行我们的两个进程之后,无论执行顺序如何,结果都是相同的。

这种反直觉的信号分配语义是 VHDL 初学者经常遇到的第二类问题的原因:由于延迟了一个 delta 周期,显然不起作用的分配。当在调试器中逐步运行进程 P5 时,在分配了 r 并且已经为 a 分配了 r + 1 之后,可以预期 a 的值为 19,但是调试器顽固地说 r=0a=1 ……

注意:在同一执行阶段可以多次分配相同的信号。在这种情况下,它是决定信号下一个值的最后一个赋值。其他任务完全没有效果,就像它们从未被执行过一样。

是时候检查我们的理解了:请回到我们的第一个交换示例并尝试理解原因:

  process
  begin
    ---
    s <= r;
    r <= s;
    ---
  end process;

实际上交换信号 rs 而不需要第三个临时信号,为什么:

  process
  begin
    ---
    r <= s;
    s <= r;
    ---
  end process;

将完全相同。试着理解为什么,如果 s 是一个整数信号并且它的当前值是 15,我们执行:

  process
  begin
    ---
    s <= 2 * s;
    s <= s - 5;
    s <= s / 5;
    print(s);
    wait on s;
    print(s);
    ---
  end process;

信号 s 的两个第一个分配没有效果,为什么 s 最终被分配 3 以及为什么两个打印值是 15 和 3。

物理时间

为了对硬件进行建模,能够对某些操作所花费的物理时间进行建模非常有用。以下是如何在 VHDL 中完成此操作的示例。该示例为同步计数器建模,它是一个完整的,自包含的 VHDL 代码,可以编译和模拟:

-- File counter.vhd
entity counter is
end entity counter;

architecture arc of counter is
  signal clk: bit; -- Type bit has two values: '0' and '1'
  signal c, nc: natural; -- Natural (non-negative) integers
begin
  P1: process
  begin
    clk <= '0';
    wait for 10 ns; -- Ten nano-seconds delay
    clk <= '1';
    wait for 10 ns; -- Ten nano-seconds delay
  end process P1;

  P2: process
  begin
    if clk = '1' and clk'event then
      c <= nc;
    end if;
    wait on clk;
  end process P2;

  P3: process
  begin
    nc <= c + 1 after 5 ns; -- Five nano-seconds delay
    wait on c;
  end process P3;
end architecture arc;

在进程 P1 中,wait 指令不会等到信号值发生变化,就像我们到目前为止看到的那样,而是等待给定的持续时间。该过程模拟时钟发生器。信号 clk 是我们系统的时钟,周期为 20 ns(50 MHz)并具有占空比。

处理 P2 模拟一个寄存器,如果刚出现 clk 的上升沿,则将其输入 nc 的值分配给其输出 c,然后等待 clk 的下一个值更改。

处理 P3 模拟增量器,将其输入 c 的值(递增 1)分配给其输出 nc …,物理延迟为 5 ns。然后等待,直到其输入 c 的值发生变化。这也是新的。到目前为止,我们总是分配信号:

  s <= value;

由于前面部分解释的原因,我们可以隐含地翻译成:

  s <= value; -- after delta

这个小型数字硬件系统可以用下图表示:

StackOverflow 文档

随着物理时间的引入,并且我们知道我们还有以三角洲测量的符号时间,我们现在有一个二维时间,我们将表示 T+D,其中 T 是以纳秒为单位测量的物理时间,而 D 是一些增量(没有实际持续时间)。

完整的图片

我们尚未讨论 VHDL 仿真的一个重要方面:在执行阶段之后,所有进程都处于 suspended 状态。我们非正式地说,调度程序然后更新已分配的信号的值。但是,在我们的同步计数器示例中,它是否应同时更新信号 clkcnc?物理延误怎么样?接下来 suspended 状态的所有进程和 run-able 状态都没有发生什么?

完整(但简化)的模拟算法如下:

  1. 初始化

    • 将当前时间 Tc 设置为 0 + 0(0 ns,0 delta-cycle)
    • 初始化所有信号。
    • 执行每个进程,直到它挂起 wait 语句。
      • 记录信号分配的值和延迟。
      • 记录恢复过程的条件(延迟或信号变化)。
    • 计算下一次 Tn 作为最早的时间:
      • wait for <delay> 暂停的进程的恢复时间。
      • 下一次信号值应改变的时间。
  2. 模拟周期

    • Tc=Tn
    • 更新需要的信号。
    • run-able 中说明正在等待已更新的其中一个信号的值更改的所有进程。
    • run-able 中说明由 wait for <delay> 语句暂停的所有进程,其恢复时间为 Tc
    • 执行所有可运行的进程,直到它们挂起。
      • 记录信号分配的值和延迟。
      • 记录恢复过程的条件(延迟或信号变化)。
    • 计算下一次 Tn 作为最早的:
      • wait for <delay> 暂停的进程的恢复时间。
      • 下一次信号值应改变的时间。
    • 如果 Tn 为无穷大,请停止模拟。否则,开始一个新的模拟循环。

手动模拟

最后,让我们现在在上面介绍的同步计数器上手动执行简化的仿真算法。我们任意决定,当几个进程可运行时,顺序将是 P3> P2> P1。下表表示初始化和第一个模拟周期期间系统状态的演变。每个信号都有自己的列,其中指示了当前值。当执行信号分配时,如果当前值是 a,则将调度值附加到当前值,例如 a/b@T+D,并且在时间 T+D(物理时间加上 delta 周期),下一个值将是 b。最后 3 列表示恢复暂停进程的条件(必须更改的信号名称或进程恢复的时间)。

初始化阶段:

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 0 + 0
初始化所有信号 0 + 0 ‘0’ 0 0
P3/nc<=c+1 after 5 ns 0 + 0 ‘0’ 0 0/1 @ 5 + 0
P3/wait on c 0 + 0 ‘0’ 0 0/1 @ 5 + 0 c
P2/if clk='1'... 0 + 0 ‘0’ 0 0/1 @ 5 + 0 c
P2/end if 0 + 0 ‘0’ 0 0/1 @ 5 + 0 c
P2/wait on clk 0 + 0 ‘0’ 0 0/1 @ 5 + 0 clk c
P1/clk<='0' 0 + 0 ‘0’/ ‘0’ @ 0 + 1 0 0/1 @ 5 + 0 clk c
P1/wait for 10 ns 0 + 0 ‘0’/ ‘0’ @ 0 + 1 0 0/1 @ 5 + 0 10 + 0 clk c
下次计算 0 + 0 0 + 1 ‘0’/ ‘0’ @ 0 + 1 0 0/1 @ 5 + 0 10 + 0 clk c

模拟周期#1

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 0 + 1 ‘0’/ ‘0’ @ 0 + 1 0 0/1 @ 5 + 0 10 + 0 clk c
更新信号 0 + 1 ‘0’ 0 0/1 @ 5 + 0 10 + 0 clk c
下次计算 0 + 1 5 + 0 ‘0’ 0 0/1 @ 5 + 0 10 + 0 clk c

注意:在第一个模拟周期中没有执行阶段,因为我们的 3 个进程都没有满足其恢复条件。P2 正在等待 clk 的值更改并且 clk 上有一个事务,但由于旧值和新值相同,这不是值更改

模拟周期#2

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 5 + 0 ‘0’ 0 0/1 @ 5 + 0 10 + 0 clk c
更新信号 5 + 0 ‘0’ 0 1 10 + 0 clk c
下次计算 5 + 0 10 + 0 ‘0’ 0 1 10 + 0 clk c

注意:同样,没有执行阶段。nc 发生了变化,但没有一个过程正在等待 nc

模拟周期#3

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 10 + 0 ‘0’ 0 1 10 + 0 clk c
更新信号 10 + 0 ‘0’ 0 1 10 + 0 clk c
P1/clk<='1' 10 + 0 ‘0’/ ‘1’ @ 10 + 1 0 1 clk c
P1/wait for 10 ns 10 + 0 ‘0’/ ‘1’ @ 10 + 1 0 1 20 + 0 clk c
下次计算 10 + 0 10 + 1 ‘0’/ ‘1’ @ 10 + 1 0 1 20 + 0 clk c

模拟周期#4

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 10 + 1 ‘0’/ ‘1’ @ 10 + 1 0 1 20 + 0 clk c
更新信号 10 + 1 ‘1’ 0 1 20 + 0 clk c
P2/if clk='1'... 10 + 1 ‘1’ 0 1 20 + 0 c
P2/c<=nc 10 + 1 ‘1’ 0/1 @ 10 + 2 1 20 + 0 c
P2/end if 10 + 1 ‘1’ 0/1 @ 10 + 2 1 20 + 0 c
P2/wait on clk 10 + 1 ‘1’ 0/1 @ 10 + 2 1 20 + 0 clk c
下次计算 10 + 1 10 + 2 ‘1’ 0/1 @ 10 + 2 1 20 + 0 clk c

模拟周期#5

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 10 + 2 ‘1’ 0/1 @ 10 + 2 1 20 + 0 clk c
更新信号 10 + 2 ‘1’ 1 1 20 + 0 clk c
P3/nc<=c+1 after 5 ns 10 + 2 ‘1’ 1 二分之一 @ 15 + 0 20 + 0 clk
P3/wait on c 10 + 2 ‘1’ 1 二分之一 @ 15 + 0 20 + 0 clk c
下次计算 10 + 2 15 + 0 ‘1’ 1 二分之一 @ 15 + 0 20 + 0 clk c

注意:人们可以认为 nc 更新将安排在 15+2,而我们安排在 15+0。当将非零物理延迟(此处为 5 ns)添加到当前时间(10+2)时,增量循环消失。实际上,delta 周期仅用于区分不同的模拟时间 T+0T+1 ……具有相同的物理时间 T。一旦物理时间改变,就可以重置增量周期。

模拟周期#6

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 15 + 0 ‘1’ 1 二分之一 @ 15 + 0 20 + 0 clk c
更新信号 15 + 0 ‘1’ 1 2 20 + 0 clk c
下次计算 15 + 0 20 + 0 ‘1’ 1 2 20 + 0 clk c

注意:同样,没有执行阶段。nc 发生了变化,但没有一个过程正在等待 nc

模拟周期#7

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 20 + 0 ‘1’ 1 2 20 + 0 clk c
更新信号 20 + 0 ‘1’ 1 2 20 + 0 clk c
P1/clk<='0' 20 + 0 ‘1’/ ‘0’ @ 20 + 1 1 2 clk c
P1/wait for 10 ns 20 + 0 ‘1’/ ‘0’ @ 20 + 1 1 2 30 + 0 clk c
下次计算 20 + 0 20 + 1 ‘1’/ ‘0’ @ 20 + 1 1 2 30 + 0 clk c

模拟周期#8

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 20 + 1 ‘1’/ ‘0’ @ 20 + 1 1 2 30 + 0 clk c
更新信号 20 + 1 ‘0’ 1 2 30 + 0 clk c
P2/if clk='1'... 20 + 1 ‘0’ 1 2 30 + 0 c
P2/end if 20 + 1 ‘0’ 1 2 30 + 0 c
P2/wait on clk 20 + 1 ‘0’ 1 2 30 + 0 clk c
下次计算 20 + 1 30 + 0 ‘0’ 1 2 30 + 0 clk c

模拟周期#9

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 30 + 0 ‘0’ 1 2 30 + 0 clk c
更新信号 30 + 0 ‘0’ 1 2 30 + 0 clk c
P1/clk<='1' 30 + 0 ‘0’/ ‘1’ @ 30 + 1 1 2 clk c
P1/wait for 10 ns 30 + 0 ‘0’/ ‘1’ @ 30 + 1 1 2 40 + 0 clk c
下次计算 30 + 0 30 + 1 ‘0’/ ‘1’ @ 30 + 1 1 2 40 + 0 clk c

模拟周期#10

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 30 + 1 ‘0’/ ‘1’ @ 30 + 1 1 2 40 + 0 clk c
更新信号 30 + 1 ‘1’ 1 2 40 + 0 clk c
P2/if clk='1'... 30 + 1 ‘1’ 1 2 40 + 0 c
P2/c<=nc 30 + 1 ‘1’ 二分之一 @ 30 + 2 2 40 + 0 c
P2/end if 30 + 1 ‘1’ 二分之一 @ 30 + 2 2 40 + 0 c
P2/wait on clk 30 + 1 ‘1’ 二分之一 @ 30 + 2 2 40 + 0 clk c
下次计算 30 + 1 30 + 2 ‘1’ 二分之一 @ 30 + 2 2 40 + 0 clk c

模拟周期#11

操作 Tc Tn clk c nc P1 P2 P3
设置当前时间 30 + 2 ‘1’ 二分之一 @ 30 + 2 2 40 + 0 clk c
更新信号 30 + 2 ‘1’ 2 2 40 + 0 clk c
P3/nc<=c+1 after 5 ns 30 + 2 ‘1’ 2 三分之二 @ 35 + 0 40 + 0 clk
P3/wait on c 30 + 2 ‘1’ 2 三分之二 @ 35 + 0 40 + 0 clk c
下次计算 30 + 2 35 + 0 ‘1’ 2 三分之二 @ 35 + 0 40 + 0 clk c