訊號與變數簡要概述了 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