跳轉至內容

可程式設計邏輯/Verilog 針對軟體程式設計師

來自華夏公益教科書,開放的世界,開放的書籍

Verilog 是一種硬體描述語言 (HDL)。雖然編碼風格看起來類似於 C++ 或 Java 等軟體語言,包括 if 語句、迴圈、變數和表示式,但您不能像編寫軟體一樣編寫 Verilog。優秀的硬體設計師頭腦中會清晰地呈現他們想要生成的硬體結構。Verilog 語言僅僅是一種方便的表達這些硬體結構的方式:多路複用器、暫存器、隨機邏輯、狀態機等等。

時鐘和復位

[編輯 | 編輯原始碼]

軟體程式通常從呼叫 main() 例程開始。執行過程一直持續到程式碼退出。在 Verilog 中沒有這樣的起點:數位電路會一直執行,不斷地處理其輸入,觀察其內部狀態並生成輸出。電路以由稱為時鐘的特殊訊號決定的規律時間間隔進行處理。您以前聽說過這種訊號:2GHz CPU 是由 2 GHz 訊號驅動的電路。這意味著該電路每秒將執行 20 億步。

數位電路的典型起點由另一個稱為復位的特殊訊號標記。當復位處於活動狀態時,電路不會嘗試執行任何操作。但是一旦復位被停用,人們就期望數位電路開始執行有用的操作。

您可以將您的數字設計視為一系列步驟。每一步都在一個時鐘週期內執行。如果您想執行一項複雜的任務,使用多個步驟,有時甚至是數千個步驟,是很正常的。

並行執行

[編輯 | 編輯原始碼]

軟體程式是一系列彙編指令。如果您更改程式碼並重新編譯它,您可以生成新的指令並在同一臺計算機上執行不同的程式:您不需要扔掉您的計算機併購買一臺新的計算機來執行新的程式。

不幸的是,硬體不能像那樣工作:您的設計所需的所有邏輯都必須定義,並且不能在執行時更改。如果您需要在一個步驟內新增三個數字,那麼您需要提前提供該特殊的加法器。

然而,硬體與軟體相比有一個很大的優勢,這就是為什麼它如此強大的原因:所有步驟都以並行方式執行,並且始終執行。對於複雜的設計,實際上會有數百萬個程式碼片段,所有程式碼都同時執行。

並行執行是剛接觸軟體語言時 Verilog 如此奇怪的原因。讓我們嘗試瞭解一下,如果您編寫的所有 C 程式碼都始終以並行方式執行,那麼它將如何編寫。考慮這段微小的 C 程式碼

main() {
  int a = 5; int b = 7;
  if(a < b) {
    b = b - a;
  } 
}

假設您有一個神奇的變數 __LINE__,您必須控制下一行程式碼執行的順序。但是所有行都永遠執行,並且同時執行。下面的 C 程式碼將與原始程式碼具有相同的行為

main() {
  __LINE__ = 1;
  while(1) {
  if(__LINE__ == 1) { int a = 5; int b = 7; __LINE__ = 2}
  if(__LINE__ == 2) {if(a < b) { __LINE__ = 3; else __LINE__ = 4;}
  if(__LINE__ == 3) {    b = b - a; }
  }
}

很可怕,不是嗎?硬體就是這樣工作的。硬體設計是一系列步驟。在每一步的開始,所有程式碼都會從頭開始執行,因此您必須在某處儲存您在上一步中的狀態,以便您可以在您離開的地方繼續執行。在上面的示例中,__LINE__ 是您的狀態。

Hello World

[編輯 | 編輯原始碼]

您如何在 Verilog 中實現經典的“Hello World”程式?在硬體中無法使用 printf。但我們可以編寫類似的程式碼,以突出顯示硬體與軟體的不同之處。讓我們嘗試設計一個電路,該電路有一個輸入:一個 8 位字元,和一個輸出:HelloWorld 輸出。該電路將一步一步地讀取傳入的字元,當它檢測到“Hi”序列(字母 H 後跟字母 i”時,它會將輸出提升到 1,以表示 HelloWorld,直到電路復位。如果您有一個開發板,您可以將該訊號連線到 LED,當您輸入正確的序列時,您將看到燈光亮起。

電路的宣告如下

module HelloWorld(input [7:0] char, output HelloWorld, input clk, input reset);

電路的核心是一個狀態機,它將首先查詢字母“H”,然後查詢字母“i”。

always @(posedge clk) begin
  switch(state) begin
    case GOT_NOTHING: begin
    if(char=='H')  state <= GOT_H;
    else state <= GOT_NOTHING;
  end
    case GOT_H: begin
    if(char=='i')  state <= GOT_HI;
    else state <= GOT_NOTHING;
  end
end

然後,我們只需要在處於 GOT_HI 狀態時,將 HelloWorld 輸出訊號驅動為真即可

assign HelloWorld = (state == GOT_HI);

而且我們不應該忘記宣告我們用於編碼狀態的狀態型別和常量

parameter GOT_NOTHING = 0;
parameter GOT_H = 1;
parameter GOT_HI = 2;
reg [1:0] state;

現在您已經瞭解了 Verilog 的每個程式碼塊都在每一步都從頭開始執行,我們就可以討論更高階的語言結構,而不會將它們與它們的軟體等效項混淆。是的,Verilog 支援迴圈和 if 語句。在合理的範圍內。

首先,迴圈必須在同一步驟內完成。您不能在一步中開始迴圈並在以後完成。如果您想這樣做,您必須在某處儲存一個狀態,告訴您您在迴圈中的位置,並在每一步都適當地恢復。

其次,迴圈不能具有取決於其他變數的可變迭代次數。您只能迴圈遍歷一個常量。例如,您可以迴圈遍歷向量的所有 16 位。但您不能迴圈直到兩個變數相等。

指標和記憶體

[編輯 | 編輯原始碼]

正如我們之前所見,您不能在執行時建立新的硬體。這意味著 Verilog 中沒有 malloc() 語句的等效項。您不能分配記憶體。記憶體要麼存在,要麼不存在。如果您考慮一下,這與您的個人電腦上的情況是一樣的:您有 8 GB 的 RAM,此後您將耗盡記憶體。只是您的程式往往會使用少於可用總記憶體的記憶體(通常)。

由於我們無法分配記憶體,因此指標在 Verilog 中沒有意義。但是有記憶體。它們是允許您讀取和寫入資料的較小塊。它們的主要限制是,您只能在每一步執行一定數量的讀寫操作。實際上,大多數記憶體最多隻允許您在每一步執行一個讀或寫操作。在 Verilog 中,記憶體看起來像一個數組。陣列的維度指定了記憶體的深度和寬度。例如,這將是一個 256 個字的 32 位記憶體

reg [31:0] my_memory[255:0];

您可以在 always 塊內訪問記憶體的內容,但請注意:每次您引用記憶體訊號時,都會算作一個讀或寫埠。除非您知道您的記憶體至少具有與迴圈大小相同的埠數量,否則您絕對不想在迴圈內訪問記憶體訊號。像從一個地址讀取到另一個地址這樣的看似簡單的事情需要正確實現。下面的程式碼不好,因為它將消耗兩個讀埠,這太浪費了

always @(posedge clk) begin
  if(select_addr1)
    data <= my_memory[addr1];
  else
    data <= my_memory[addr2];
end

您想用這種方式編寫記憶體讀取程式碼,以確保只訪問一次記憶體陣列

always @(posedge clk) begin
  if(select_addr1)
    addr = addr1;
  else
    addr = addr2;

  data <= my_memory[addr];
end

交換值

[編輯 | 編輯原始碼]

一個經典的軟體面試問題是如何在不使用臨時變數的情況下交換兩個變數的內容。如果你還沒聽說過這個技巧,那麼對於整數來說,解決方案是使用異或運算子。

function swap(int a, b) {
  a = a xor b;
  b = a xor b;
  a = a xor b;
}

Verilog 更加強大:你可以在不使用臨時變數和任何操作的情況下交換兩個變數。這是怎麼實現的呢?請記住,當你給一個變數賦值時,賦值會在下一步生效。因此,要交換兩個變數,你真正需要說的是:“b 的下一個值是 a 的當前值,a 的下一個值是 b 的當前值。” 這在 Verilog 中可以使用非阻塞賦值 ("<=") 來表達。

always @(posedge clk) begin
  a <= b;
  b <= a;
end

Verilog 中的另一種賦值方式被稱為阻塞賦值(你習慣看到的 "=")。這種賦值意味著對值的更改會立即生效。它在 always 塊中計算中間值很有用,但是當你想分配一個從一步到下一步都存在的訊號時,非阻塞賦值是首選。因此,下面的程式碼是錯誤的:在 always 塊結束時,a 和 b 都有相同的值,而 a 的原始值丟失了。

always @(posedge clk) begin
  a = b; // we just lost a's value
  b = a;
end

步驟在 Verilog 中是一個非常重要的概念,幾乎不可能在沒有輕鬆訪問所有變數的歷史記錄的情況下除錯數位電路的行為。由於每個塊在每一步都從頭開始執行,你需要知道上一步生成的狀態,才能理解為什麼當前步沒有按你的預期執行。這就是硬體設計師使用波形的原因,波形是一種特殊的數值跟蹤,顯示了數值隨時間的變化情況。由於時間是一系列離散的步驟,因此很容易在圖表中排列起來。硬體設計師幾乎從不單步執行 Verilog 程式碼,因為並行執行使得不可能有一個正在執行的“當前行”的概念。

組合語言

[編輯 | 編輯原始碼]

你編寫的 C 程式並不是真正執行在你的 PC 上的程式。它會被編譯成彙編指令,然後由你 PC 內部的處理器執行。Verilog 的組合語言等效是什麼?

像下面這樣簡單的 C 程式碼會被翻譯成這些彙編指令

main() {
  int a = 2;
  int b = a + 1;
}
0000000000400448 <main>:
  400448:	55                   	push   %rbp
  400449:	48 89 e5             	mov    %rsp,%rbp
  40044c:	c7 45 f8 02 00 00 00 	movl   $0x2,0xfffffffffffffff8(%rbp)
  400453:	8b 45 f8             	mov    0xfffffffffffffff8(%rbp),%eax
  400456:	83 c0 01             	add    $0x1,%eax
  400459:	89 45 fc             	mov    %eax,0xfffffffffffffffc(%rbp)
  40045c:	c9                   	leaveq 
  40045d:	c3                   	retq


C 還是 Verilog 更強大?

[編輯 | 編輯原始碼]

閱讀完 Verilog 的所有限制後,你可能會想知道為什麼要費這個勁。軟體不是更強大嗎?並不總是這樣。如果你需要執行非常複雜的函式,這些函式本質上始終相同(例如影片壓縮、3D 圖形等),那麼你可以將它們硬編碼到 Verilog 中。在軟體中,執行大型計算需要許多彙編指令:處理器在一個週期內可以處理的基本上只是一些對 64 位值的簡單操作。

想象一下,你需要同時執行 100 個 32 位數字的加法。在軟體中,你需要 100 條指令。在硬體中,只要你負擔得起空間,你就可以用你想要的任意步數來編碼它,包括只有一步。

reg [31:0] sum [99:0];

always @(posedge clk) begin
  for(i=0;i<100;i++) begin
    sum[i] <= a[i] + b[i];
  end
end

該程式碼的軟體版本看起來會一模一樣,只是執行需要 100 個週期,而不是 1 個。硬體執行速度比軟體快 100 倍。

華夏公益教科書