天天看點

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

第2章:Verilog HDL和SystemVerilog

在本書中,Verilog HDL(IEEE 1364—2005)和SystemVerilog(IEEE 1800—2012)将被統一簡稱為Verilog。

本章主要介紹Verilog的常用文法,并将以SystemVerilog為主,包含SystemVerilog中很多新的、具備更優特性的文法,包括可被綜合的和用于仿真驗證的。但本章并不會太多地深入文法細節,依筆者淺見,文法本身隻是用來描述硬體和承載電路設計思想的工具,誠然,文法本身也很複雜,也飽含了規範制定者們對于數字電路及其描述方法的先進思想,也有太多需要學習和了解的地方,不過筆者更希望讀者能夠在後續章節的各種設計執行個體中學習和了解,而不要拘于文法本身。本章的結構和内容也更像是“簡明參考”,而非詳述文法的“教科書”,且遠未能涵蓋Verilog文法标準的所有内容,如果需要了解文法細則,可查閱IEEE 1800—2012原文。

本章會使用一些符号和特征來書寫文法規則,與IEEE标準中使用的有些相似,但因為隻用通配一些常用的規則,是以做了大幅簡化,這些符号和特征包括:

  • <>,尖括号,其内容為後面将進一步解釋的内容。
  • [],方括号,表示其中内容為可選。
  • [|],方括号和豎線,表示由豎線隔開的内容選其一或不選。
  • {},花括号,表示其中内容可選或可重複多次。
  • {|},花括号和豎線,表示由豎線隔開的内容選其一或不選或可重複選擇多次。
  • (|),圓括号和豎線,表示由豎線隔開的内容選其一。
  • 粗體字,表示語句中應有的關鍵字或符号。

上述符号本身不包含在語句之中。

對于初學者,學習至2.13節即可掌握常用的Verilog文法,可以進行後面章節的學習,2.14節及以後的内容主要是一些進階文法的示例,可以在以後遇到相關文法時再回來學習。

2.1 硬體描述語言簡介

在沒有電子設計自動化(EDA)軟體的年代,數字電路設計依賴于手繪電路原理圖,在EDA軟體出現的初期,也繼續沿用了繪制原理圖這一方式(或稱為原理圖輸入)。EDA原理圖輸入雖友善修改,具有一定的可重用性,但對于稍大規模的電路,布線工作仍然要花費大量時間,設計者仍要對器件和工藝非常熟悉。

在20世紀80年代,硬體描述語言開始大量出現,其使用貼近自然語言的文字來描述電路的連接配接、結構甚至直接描述電路功能。對于計算機,文字輸入更友善快捷;對于設計者,文字更抽象,與具體器件和實作工藝無關,更能集中精力在功能的描述上而不是繁瑣的連線中。當然,最後從文字到具體電路實作的轉換,依賴于成熟的、标準化的器件和電路單元,但這些是IC工藝設計者和EDA軟體設計者的任務,不需要數字邏輯設計者來操心了。

Verilog HDL(Verilog Hardware Description Language)誕生于1983年,同時期出現的數字電路硬體描述語言不下百種,發展至今最主流的有Verilog HDL和VHDL(Very high speed integrated circuit Hardware Description Language)兩種,VHDL在1987年成為IEEE标準,Verilog HDL在1995年成為IEEE标準(IEEE 1364)。

對于數字邏輯設計者來說,描述硬體的抽象層次自上而下主要可分為:

  • 系統級(System level),描述電路系統的整體結構、行為和性能。
  • 算法級(Algorithm level),描述單元電路的算法實作。
  • 寄存器傳輸級(Register transfer level,RTL),描述資料在寄存器間的流動、處理和互動。
  • 門級(Gate level),描述邏輯門及它們之間的電路連接配接。

比門級更低層次還有開關級(Switch level),它與器件工藝相關,對于普通數字邏輯設計者并不需要掌握。

描述硬體的方法又可分為行為描述和結構描述:

  • 行為描述(Behavioral modeling)描述電路的行為,由EDA軟體負責生成符合該行為的電路。
  • 結構描述(Structural modeling)描述電路的組成結構。

抽象層次和描述方法在Verilog标準中并沒有明确定義,它們之間也沒有嚴格對應關系,不過一般認為從系統級到RTL基本屬于行為描述,底層的RTL和門級基本屬于結構描述,而事實上系統級的大子產品組織結構也有結構描述的意思。RTL描述資料流動和處理時也稱為資料流描述。

稍大的數字系統幾乎不可能設計出來就正确無誤,對設計結果的測試驗證也是設計過程中非常重要的步驟。得益于EDA工具,許多測試驗證工作可以在計算機上模拟,而不必等到真實電路制造出來,這一過程稱為仿真。就像對一塊電路闆進行測試需要儀器儀表和模拟工作環境的測試電路一樣,對數字系統進行仿真驗證也需要設計測試系統,而且由于仿真測試系統最終不必變成實際電路,Verilog專為仿真驗證提供了更靈活的文法。  

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

圖2-1 抽象層次、描述方法、Verilog和VHDL的模組化能力

IEEE在2001年至2012年間多次對Verilog進行了修訂和擴充。在2005年擴充了大量支援系統級模組化和驗證的特性,并形成了獨立标準(IEEE 1800),被稱為SystemVerilog(注意并沒有空格);2009年,原1364标準被終止,并被合并進1800标準,原Verilog HDL成為SystemVerilog的子集(如圖2-1所示),SystemVerilog最新的正式标準是IEEE 1800—2012。因而我們現在所說的Verilog HDL嚴格來說都是SystemVerilog,本書後面統稱為Verilog。

新的SystemVerilog标準是一個“統一的硬體設計、規範和驗證語言”(Unified Hardware Design, Specification and Verification Language),承載了硬體設計和驗證兩大目标,但因SystemVerilog提出之初主要是擴充系統級模組化和驗證功能,因而人們常常有SystemVerilog隻适用于驗證,而不适用于實際電路的設計和描述的誤解。

2.2 設計方法和流程

複雜的數字邏輯電路一般采用階層化、子產品化的設計方法,即将複雜的整體功能拆分成許多互聯的功能子產品,而這些功能子產品又可以再細分為更小的子產品。合理的功能拆分、子產品功能定義和子產品間的互動規則将有效地降低整個系統的設計難度、提高設計的可行性和性能。圖2-2為功能和子產品拆分的示意圖。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

圖2-2 功能和子產品拆分的示意圖

這樣從整體功能設計出發逐漸拆分至底層的設計方法稱為自頂向下的設計方法。合理的功能拆分、子產品及子產品間互動的定義是需要對系統有全局掌握,并對重要細節足夠了解才能做到的,往往需要設計者有足夠的設計經驗,讀者應從簡單的系統開始,逐漸累積知識和經驗。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

圖2-3 數字邏輯系統設計流程

數字邏輯系統,特别是FPGA中的數字邏輯系統設計,按順序一般可分為以下幾個步驟(見圖2-3):

  • 規格制定:定義整個系統的功能、性能名額和重要參數,這一步主要是文檔編撰。
  • 上層設計:功能拆分、主要子產品定義和子產品間互動設計,這一步也主要是文檔編撰。
  • 細節設計:主要子產品内功能的實作,包括小型算法、狀态機、編解碼等,這一步一般包含文檔編撰和初步的編碼。
  • 編碼:使用可綜合的HDL編寫代碼。
  • 功能驗證:使用仿真工具驗證子產品或系統功能,需要編寫測試代碼。
  • 綜合:使用編譯器将HDL代碼轉換為門、觸發器級網絡表,往往又分為展述(Elaboration,展開所有層次結構并建立連接配接)和綜合(Synthesis,生成門、觸發器級網絡表)兩步。
  • 實作(Implementation),也常常稱為布局布線(Fit and Route),使用編譯器将門、觸發器級網絡表适配到特定的器件工藝(比如FPGA晶片)中,這一步常常還需要配合一些實體、時序上的限制輸入。
  • 時序驗證:帶有時序資訊(門、線路延遲等)對布局布線結果進行驗證,除驗證功能正确外,還驗證時序和性能合乎要求,常常也稱為“門級仿真”。

    對于FPGA設計,實作過程的最後往往還包含彙編(Assembler)這一步,用于生成配置FPGA的二進制檔案。

在單元子產品設計時,常常也會重複“細節設計-編碼-功能驗證”步驟,直到子產品合乎上層設計的定義為止,所有子產品設計驗證完成後,再連接配接好整個系統進行功能仿真。布局布線中也可能會出錯,比如FPGA晶片資源不夠,時序驗證也可能失敗,這些情況可能都需要修改細節設計甚至上層設計。

對于小規模的系統或獨立的子產品設計,從規格制定至編碼未必需要細分成四步,特别是對于有經驗的設計者,部分細節設計過程可以在編碼時完成。而對于初學者,最好是先理清系統或子產品的組織結構和各個部分的細節,做到“先有電路,再去描述”。一些底層的時序邏輯功能也可以“先有波形,再去描述”,以目标波形為參照去描述功能。

在Verilog中,并非所有的文法都可以被綜合成實際電路,具體哪些文法可以被綜合也取決于不同廠商的EDA工具,甚至工具的不同版本。除可綜合的文法外,還有大量文法是為了支援仿真驗證。本章将着重介紹主流FPGA開發工具可綜合的文法,以及部分在仿真驗證中常用的文法。

2.3 辨別符和關鍵字

辨別符是代碼中對象(比如變量、子產品等)的唯一名字,用于在代碼中引用該對象。辨別符隻可以由字母、下劃線、數字和美元符号“$”構成,并且隻能由字母或下劃線開頭,辨別符是區分大小寫的。不過考慮到EDA工具及其工作環境的相容性,非常不建議僅依賴大小寫來區分不同的辨別符。

關鍵字是Verilog預定義的,用于建構代碼結構的特殊辨別符,在Verilog中所有關鍵字都是全部小寫的。代碼中使用者定義的辨別符不能與關鍵字相同。IEEE 1800—2012的全部關鍵字見附錄A。

2.4 值、數和字面量

在Verilog中,線網和變量可取的基本值有四個:

  • 0,表示邏輯0、低電平、條件假。
  • 1,表示邏輯1、高電平、條件真。
  • x或X,表示未知的邏輯值。
  • z或Z,表示高阻态。

在Verilog中,有些資料類型可以取全部4個值(稱為四值),而有些資料類型僅能取0和1兩個值(稱為二值)。

Verilog的常數包括整型常數、浮點常數,字面量包括時間字面量、字元串字面量、結構字面量和數組字面量,為簡便起見,本書将數和字面量統稱為常數。

2.4.1 整型常數

整型常數除包含數值資訊外,還可包含位寬資訊,可以寫成二、八、十或十六進制,一般形式如下:[-]<位寬>'[s| S]<進制辨別><數值>其中:

  • “-”,僅用于有符号負數。
  • 位寬,十進制書寫的正整數,無論後面進制辨別指定什麼進制,均表示二進制位的數量。
  • “'”為英文撇點(ASCII碼0x27)。
  • “s”或“S”,用來指定該常數為有符号數,在Verilog中,負數被以補碼形式表達。
  • 進制辨別,字母b或B、o或O、d或D、h或H分别表示二進制、八進制、十進制和十六進制。
  • 數值,用進制辨別指定的進制書寫的無符号數值,即一串數字。

x或z可用于二、八、十六進制,每個x或z分别代表1位、3位和4位。十進制如果使用x或z,則隻能有一個x或z,表達所有位均為x或z,有時為了可讀性可用“?”代替z。

為了友善閱讀,任意兩位數字之間還可增加下劃線。

對于無符号數,如果指定的位寬大于數值部分寫出的寬度,高位一般将填充0,如果最高位為x或z,高位将填充x或z;如果指定的位寬小于數值部分寫出的寬度,高位将舍棄。對于有符号數,如果數值超過了指定寬度能表達的範圍,也将直接舍棄高位。

也有無位寬的形式:[-]['[s| S]<進制辨別>]<數值>它表達至少32位二進制的數值,而至多多少位在編譯時由它将賦給的對象的位數決定。如果沒有進制辨別,則該數必須是十進制有符号數。

還有無位寬的單一位形式:'[0| 1| x|X| z| Z]它表達無位寬的數值,具體多少位在編譯時由它将賦給的對象的位數決定,将使被指派對象的全部位賦為指定值。

代碼2-1是一些整型常數的例子。

代碼2-1 整型常數示例(注意左側行編号不是代碼内容)

1  //以下是指定位寬的

2  4'b1010             // 4位無符号數0b1010=10

3  6'd32 // 6位無符号數0b10000=32

4  8'b1010_0101 // 8位無符号數0b10100101=165

5  -8'sh55// 8位有符号數-0x55=-85,以“10101011”記錄

6  -5'sd4 // 5位有符号數-4,以“11100”記錄

7  5'sd-4 // 文法錯誤

8  8'd256 // 實際值為0

9  8'sd128// 實際值為-128

10  -8'sd129 // 實際值為127

11  1'sb1 // 實際值為-1!

12  3'b01z // 最低位為z(高阻态)

13  8'hx9 // 高4位為x(未知)

14  10'hz0 // 高6位均為z

15  16'd? // 全部16位為z

16  // 以下是未指定位寬的

17  'b1// 無符号數1

18  'h27EF // 無符号數0x27EF=10223

19  -'sb101// 有符号數-0b101=-5

20  127// 有符号數127

21  -1 // 有符号數-1

22  A5 // 文法錯誤

23  // 以下是無位寬的單一位

24  '1 // 全1,将使被指派對象全部位填充1

25  'z // 全z,将使被指派對象全部位填充z

2.4.2 浮點常數

Verilog中的浮點常數是遵循IEEE 754标準表達的雙精度浮點常數,可以使用小數形式(必須包含小數點)或科學計數法(e或E代表10的次幂),當中也可插入下劃線,如代碼2-2所示。

代碼2-2 浮點常數示例

1  0.0

2  3.1415926535898

3  2.99_792_458e8

4  4.2e-3

5  .80665              // 文法錯誤,小數點左右必須至少有一個數字(與C語言不同)

6  9. // 文法錯誤,同上

7  2333 // -_-#這是一個整型常數

FPGA編譯工具一般不支援浮點常數參與實時運算,但支援浮點常量在編譯期的運算。

2.4.3 時間常數和字元串常數

時間常數是以時間機關結尾的常數,時間機關包括s、ms、us(即μs)、ns、ps、fs。例如代碼2-3:

代碼2-3 時間常數示例

1  2.1ns

2  40ps

時間常數主要用于仿真驗證。

字元串常數是使用雙引号包裹的一串字元,在二進制中以ASCII碼形式表達(每個字元8位),可以被賦給整型量,指派時左側字元位于高位。如:

代碼2-4 字元串常數示例

1  "Hello world!\n"

字元串常數中可以用“\”引導一些特殊字元,比如 “\n”(換行)、“\t”(制表符)、“\\”(右斜杠)、“\"”(雙引号)。

2.5 線網

線網和變量在Verilog中用來保持邏輯值,也可統稱為值保持器。線網一般對應電路中的連線,不具備存儲能力,線網使用一個或多個持續指派或子產品端口來驅動,當有多個驅動時,線網的值取決于這多個驅動的強度。

線網有以下幾類:

  • wire,用于被門或持續指派驅動的線網,可以有多個驅動源。
  • tri,用于被多個驅動源驅動的線網。
  • uwire,用于被單個驅動源驅動的線網。
  • wand、wor、triand、trior,用于實作線邏輯(線與和線或)。
  • tri0、tri1,由電阻下拉或上拉的線網。
  • trireg,容性儲值的線網。
  • supply0、supply1,電源。
    帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

表2-1 wire和tri的多重驅動

其中wire是FPGA開發中最常用的線網類型,其他線網類型多數在FPGA中無法實作,一般用于編寫測試代碼。

多個不同強度驅動源驅動wire和tri時的情況如表2-1所示。

多個不同強度驅動源驅動wand和triand時的情況如表2-2所示,多個同強度驅動源驅動wor和trior時的情況如表2-3所示。多個同強度驅動源驅動tri0和tri1時的情況分别如表2-4和表2-5所示。這些多重驅動情況的真值表不應死記硬背,均是有邏輯規律的。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

多重驅動在FPGA設計中幾乎隻用于片外雙向IO口,在驅動為z時,接收外部輸入,在FPGA内部邏輯中并沒有三态門,也不支援多重驅動。

線網定義的常用形式:<線網類型> [[<資料類型>] [signed| unsigned] [<位寬>]] <辨別符>[=<指派>] {, <辨別符>[=<指派>]};其中:

  • 線網類型為上述wire、tri等。
  • 資料類型為下述變量的資料類型中除reg外的四值類型之一,如果為确定長度的類型,則後面不能有位寬說明,如果為整型才可以使用signed或unsigned指定有無符号。
  • 位寬,按以下形式定義的位寬:[,]這裡的方括号是實際的方括号,其中msb和lsb分别為最高位的索引和最低位的索引,值得注意的是,msb的值可以小于lsb,但仍然為最高位,因與通常的習慣不一緻,是以一般應避免使msb小于lsb。

    如果省略資料類型,預設為logic類型,如果還省略了位寬說明,預設為1bit。

代碼2-5是一些線網類型定義的示例:

代碼2-5 線網類型定義的示例

1  wire a;                   // 1位線網,等價于 wire logic a;

2  wire b = 1'b1;// 1位線網,并賦常數1

3  wire c = b; // 1位線網,并連接配接至線網b

4  wire [6:0] d;// 7位線網

5  wire [6:0] e0 = d, e1 = '1; // 7位線網,e0連接配接至d,e1賦常數127

6  wire signed [7:0] f, g = -8'sd16;// 8位線網,有符号類型,g賦常數-16

7  wire integer h; // 32位線網,有符号

8  wire integer unsigned i; // 32位線網,無符号

9  wire reg j; // 文法錯誤,reg不能作為線網資料類型

10  wire int k; // 文法錯誤,資料類型不能是二值類型

11  wire struct packed {

12  logic a;

13  logic signed [7:0] b;

14  } l; // struct類型線網,總計9位

Verilog的語句以分号結尾,單行注釋(到行尾)使用雙左斜杠“// ”,塊注釋(可以跨行)使用“/”和“/”包裹。代碼2-5中,前6行最為常用,初學者隻需掌握這6行的形式即可。

除上述線網類型之外,還有一種專用于端口連接配接的線網interconnect,不能被持續指派或過程指派。

2.6 變量

變量是抽象的值存儲單元,一次指派之後,變量将保持該值直到下一次指派。變量使用“過程”中的指派語句對其指派,變量的作用類似于觸發器,不過是否形成觸發器取決于代碼上下文。

變量有以下幾種常用資料類型:

(1)整型

  • bit,二值,預設無符号,常用于測試代碼中。
  • logic,四值,預設無符号,推薦在新設計中使用。
  • reg,四值,預設無符号,SystemVerilog出現之前最常用的變量類型。

    (2)定長整型

  • byte、shortint、int、longint,二值,分别為8位、16位、32位和64位,預設有符号。
  • integer,四值,32位,預設有符号。
  • time,四值,64位,預設無符号。

    (3)浮點型

  • shortreal、real,遵循IEEE 754标準表達的浮點小數,分别為32位和64位。
  • realtime,同real。

    (4)數組

(5)結構

(6)枚舉

前三者稱為簡單類型。而後面數組、結構和枚舉稱為複合類型。

變量一般由過程指派驅動,并且不能在多個過程塊中被驅動。

簡單類型變量定義的常用形式:[var] [<資料類型>] [signed| unsigned] [<位寬>] <辨別符>[=<初值>] {, <辨別符>[=<初值>]};其中:

  • var關鍵字和資料類型至少要存在一個,如果未指定資料類型,則預設為logic。
  • 資料類型,如果為确定長度的類型則後面不能有位寬說明,如果為整型才可以使用signed或unsigned指定有無符号。
  • 位寬,與線網定義中一樣。

注意,在使用logic關鍵字定義對象時,編譯器會自動根據代碼上下文決定該對象是logic變量還是logic類型線網,非常友善,這也是SystemVerilog推薦的定義值保持器的方法。

代碼2-6是變量定義的示例:

代碼2-6 變量定義的示例

1  var a;                      // 1位變量,等價于var logic a

2  logic b; // 1位變量,等價于var logic b

3  logic [11:0] c = 1234;// 12位變量,并賦初值1234

4  logic signed [19:0] d = c, e; // 20位變量,有符号,d賦初值1234

5  integer f;// 32位變量,有符号

6  integer [63:0] g; // 文法錯誤,定長類型不能有位寬說明

7  integer unsigned h; // 32位變量,無符号

8  reg [31:0] i; // 32位變量,無符号

9  reg signed [31:0] j; // 32位變量,有符号

10  bit [5:0] k; // 6位二值變量

11  bit signed [5:0] l; // 6位二值變量,有符号

12  byte m; // 8位二值變量,有符号

13  byte unsigned n; // 8位二值變量,無符号

14  byte [6:0] o; // 文法錯誤,定長類型不能有位寬說明

15  longint p;// 64位二值變量,有符号

16  longint unsigned q; // 64位二值變量,無符号

2.7 參數和常量

參數和常量在運作時值不變。在标準中,參數也是一種常量,不過參數常常用來指定子產品、接口的資料位寬等,因而得名。

參數和常量均可在定義時由常數或常量表達式指派。

參數包括以下類型:

  • parameter,可以被子產品外部修改的參數,在子產品執行個體化時修改或由defparam關鍵字修改。
  • localparam,不能被子產品外部修改的參數。
  • specparam,專用于指定時序、延遲資訊的參數。

    常量為:

  • const,類似于localparam,但可以在仿真期間更改值。

參數和常量定義的常用形式:<參數或常量類型> [<資料類型>] [signed| unsigned] [<位寬>] <辨別符> = <常數或常量表達式>] {, <辨別符> = <常數或常量表達式>};其中:

  • 資料類型如果為确定長度的類型則後面不能有位寬說明,如果為整型才可以使用signed或unsigned指定有無符号。

資料類型、符号指定和位寬也可以都省略,這時參數或常量的資料類型和位寬由定義時所賦予的初始值的類型和位寬确定,而在被賦予新值時,類型和位寬會自動變化。如果沒有指定位寬,預設LSB索引為0,MSB索引為位寬減一。

參數和常數的資料類型也可以是複合類型,将在後續小節介紹。

代碼2-7是參數和常量定義的例子。

代碼2-7 參數和常量定義的示例

1  parameter integer DW = 24;            // 32位有符号參數,值為24

2  parameter DataWidth = 24;// 同上

3  parameter WordSize = 64; // 32位有符号,值為64

4  localparam ByteSize = 8, WordBytes = WordSize / ByteSize;

5  // 兩個整型參數,後者由常量表達式指派

6  parameter Coef1r = 0.975;// 雙精度浮點參數

7  localparam wire signed [15 : 0] Coef1 = Coef1r * 32768;

8   // 16位有符号參數,自動四舍五入值為31949

9  parameter c0 = 4'hC; // 4位無符号參數,值為0xC=12

10  parameter [4 : 0] c1 = 27; // 5位無符号參數,值為27

11  localparam signed [19 : 0] c2 = 101325; // 20位有符号參數

12  localparam integer unsigned c3 = 299792458; // 32位無符号參數

const g = 9.08665; // 雙精度浮點常量

還可以用來定義參數化的資料類型,使用下面的格式:parameter type <辨別符> = <資料類型>;然後便可以用辨別符來定義線網或變量。例如代碼2-8。

代碼2-8 定義線網或變量

1  parameter DW = 24;

2  parameter type TData = logic signed [DW-1:0];

3  parameter type TDataL = logic signed [DW*2-1:0];

4  TData x1, x2;

5  TDataL px;

參數化的資料類型不能用defparam修改,可在子產品執行個體化時修改。

2.8 類型和位寬轉換

類型和位寬轉換分為隐式轉換和顯式轉換。指派時,如果左值(被指派者)和右值類型或位寬不同,編譯器會自行處理轉換;操作數類型如果與相關操作符對其類型要求不符,但可以轉換時,編譯器也會自行處理轉換,這樣一些轉換不需要我們在代碼中寫出轉換語句,稱為隐式轉換。

常見的幾種隐式轉換的規則:

  • 從整數轉換為浮點,保持值意不變。
  • 從浮點轉換為整數,會四舍五入處理。
  • 等長的有符号與無符号之間,直接位對位指派(是以最高位為1時,表達的值意會發生變化)。
  • 從長數轉換為短數,無論左右值有無符号,直接舍棄高位。
  • 從短數轉換為長數,如果短數為有符号數,高位填充符号位,否則填充0。

顯式轉換可以使用Verilog系統函數$cast()、$signed()和$unsigned(),或使用類型轉換表達式。系統函數是Verilog内置的函數,大部分不能綜合成實際電路,主要用于編寫仿真測試代碼。

$cast()函數用于轉換并指派,如$cast(a, b),将把b轉換為a的類型并指派給a。$cast()函數帶有傳回值,傳回0表示無法轉換,否則表示轉換成功。$cast()函數不能被綜合,僅可用于仿真測試代碼。

$signed(a)、$unsigned(a)函數将a轉換為有符号或無符号,可被綜合,有無符号的轉換并不對a進行任何操作,實際影響的是相關操作符或位寬轉換時高位填充什麼。

類型轉換表達式的常用形式是:(<目标類型>| <位寬>| signed| unsigned)'(<待轉換内容>)其中,目标類型應為簡單變量類型,位寬是常數或常量表達式。

代碼2-9是一些類型轉換的例子。

代碼2-9 類型轉換的示例

1  logic [3:0] a = 4'he;

2  logic [1:0] b = a;                      // b=2'b10

3  logic [5:0] c = a; // c=6'b001110

4  logic [5:0] d = 6'(a); // 同上

5  logic [5:0] e = 6'($signed(a));// e=6'b111110

6  logic signed [7:0] f = 4'sd5 * a; // f=70

7  logic signed [7:0] g = 4'sd5 * $signed(a); // g=-10

其中第2行隐式轉換位寬,長數賦給短數,舍棄高位。第3行隐式轉換位寬,短無符号數賦給長數,填充0;第4行顯式轉換,結果與第3行相同。

第5行将a轉換成有符号數(-4'sh2)之後再擴充位寬,将填充符号位1最後賦給無符号數,結果為6'h3e。

第6行有符号數5和無符号數14相乘,按8位無符号計算(見下節),結果為70;第7行先将a轉換為有符号數(-2),再與5相乘,結果為-10。

2.9 操作符和表達式

表達式由操作符和操作數構成,整個表達式代表了操作數經過運算後的結果。Verilog中可以用作操作數的包括。

  • 常數和字面量。
  • 參數及其中的一位或多位。
  • 線網、變量及其中的一位或多位。
  • 結構、聯合及其成員,對齊的結構中的一位或多位。
  • 數組及其中的元素,對齊的數組中的一位或多位。
  • 傳回上述内容的函數或方法。

Verilog中操作符的詳情見表2-6。

表2-6 Verilog操作符的功能、優先級、結合方向和操作數

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog
帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

關于表2-6和一些運算規則(如有沖突,靠前的規則優先)如下。

1)表中優先級數值越小優先級越高,同優先級操作符按結合方向區分先後,結合方向中的“左”意為自左向右、“右”意為自右向左。

2) 位選取、位拼接、流運算的結果為無符号數,無論操作數是否有符号。

3) 比較(含相等、不等)、非按位邏輯運算、縮減運算的結果均為1位無符号。

4) 比較(含相等、不等)運算:

  • 如果操作數中有浮點數,則另一個将被轉換為等值浮點數進行比較。
  • 僅當兩個整型操作數均為有符号數時,比較按有符号進行,否則按無符号進行。
  • 如果兩個整型操作數位寬不一緻,短操作數将被補充高位。

    5) 邏輯非、與、或、隐含和等價運算,操作數為0等價于1'b0(假),操作數非0等價于1'b1(真)。

6) 算術運算和按位邏輯運算符:

  • 如果操作數中有浮點數,則另一個将被轉換為等值浮點進行運算。
  • 表中整型結果位數均是指運算後不立即指派時的情況,如果立即指派,則以被指派對象的位數為準。
  • 如果操作數中有無符号數,則運算按無符号進行,結果為無符号數。
  • 如果操作數均為有符号數,則運算按有符号進行,結果為有符号數。
  • 如果有操作數位數不夠,則補充高位。

    7) 短操作數補充高位的規則:

  • 無符号數補充0。
  • 有符号常數補充符号位。
  • 有符号線網、變量和常量在操作按有符号進行時,補充符号位,否則補充0。

上述規則比較煩瑣,特别是在有無符号和長短不一的操作數混合在一起的時候。我們在編寫Verilog代碼的時候,盡量避免混合不同格式操作數的運算,在不可避免的時候再來考慮應用這些規則。讀者需了解有這樣一些規則,遇到問題時友善查閱,但在初學時不需要熟記它們。

代碼2-10是有關位寬的一些例子。

代碼2-10 運算位寬的相關示例

1  logic [3:0] a = 4'hF;

2  logic [5:0] b = 6'h3A;

3  logic [11:0] c = {a*b};   // c的值為38

4  logic [11:0] d = a*b;// d的值為870

5  logic signed [15:0] a0 = 16'sd30000, a1 = 16'sd20000;

6  logic signed [15:0] sum0 = (a0 + a1) >>> 1;      // sum0=-7768

7  logic signed [15:0] sum1 = (17'sd0 + a0 + a1) >>> 1; // sum1=25000

8  logic signed [15:0] sum2 = (17'(a0) + a1) >>> 1; // sum2=25000

其中,第3行因計算後沒有立即指派而是先位拼接,因而按6位計算,0xf×0x3a=15×58=870,870取低6位為38;而第4行運算後直接賦給12位變量d,因而按12位計算,結果為870。

第6行,本意是取a0和a1的平均值,但a0和a1相加時并未立即指派,而是先右移,因而加法按16位計算,其和因溢出得到-15536,再右移1位得到-7768,與本意不符;而第7行的加法自左向右結合,先計算17'sd0+a0,得到結果17'sd30000,再與a1相加,得到結果17'sd50000,避免了溢出,最後右移仍然得到17位結果25000,賦給16位sum1時舍棄最高位,結果符合意圖;第8行則使用類型轉換達到了意圖。

代碼2-11是混合不同符号和位寬的操作數的一些例子。

代碼2-11 混合不同符号和位寬的相關示例

1  logic [7:0] a = 8'd250;                   // 8'd250=8'hFA

2  logic signed [3:0] b = -4'sd6; // -4'sd6(4'hA)

3  logic c = a == b;// c=0

4  logic d = a == -4'sd6; // d=1

5  logic e = 8'sd0 > -4'sd6;// e=1

6  logic f = 8'd0 < b; // f=1

7  logic [7:0] prod0 = 4'd9 * -4'sd7; // prod0=193

8  logic signed [7:0] prod1 = 4'd9 * -4'sd7;// prod1=-63

9  logic [7:0] prod2 = 8'd5 * b;// prod2=50

10  logic [7:0] prod3 = 8'd5 * -4'sd6; // prod3=226

第3行,因a是無符号數,比較按無符号進行,b将被高位填充0到8位得到8'h0a後與a比較,顯然不相等,結果為0。第4行,按無符号進行,但有符号常數高位填充符号位,得到8'hfa後與a比較,兩者相等,結果為1。

第5行,比較按有符号進行,結果為1。第6行,比較按無符号進行,b被高位填充0,得到8'h0A與8'h0比較,結果為1。

第7行,乘法按無符号進行,4'd9被填充成8'd9,-4'sd7高位填充1,得到8'd249,與9相乘取8位,得到193。第8行與第7行一樣,結果為193,但最後賦給有符号數,得到數值-63,可以看到,這裡有符号數和無符号數相乘,按無符号相乘,結果與按有符号相乘是一緻的。

第9行,乘法按無符号進行,b被高位填充0,得到8'd10與8'd5相乘,結果為50。第10行,乘法按無符号進行,-4'sd6被高位填充符号位,得到8'hfa=250,與5相乘取8位,得到226。

表2-6中大部分操作符,特别是算術運算符的功能讀者應該都能直接了解,下面幾節将介紹一些初學者不易了解的操作符。

2.9.1 位選取操作符

位選取操作符用于選取多位資料中的1位或多位。無論原資料是否有符号,結果均為無符号。位選取操作符有兩種使用形式,第一種使用MSB和LSB:<操作數>[ : ]MSB表達式和LSB表達式必須為常量表達式,如果操作數本身在定義的時候MSB索引大于LSB索引,則MSB表達式的值應不小于LSB表達式的值,如果操作數本身MSB索引小于LSB索引,則MSB表達式的值應不大于LSB表達式的值。

第二種使用M/LSB和位寬:<操作數>[ (+:|-:) <位寬表達式>]使用“+:”時,M/LSB表達式為MSB和LSB中較小的一個,使用“-:”時,M/LSB表達式為MSB和LSB中較大的一個。

M/LSB表達式可以是變量表達式,而位寬表達式則必須是常量表達式。

代碼2-12是一些例子。

代碼2-12 位選取操作符的示例

1  logic [15:0] a = 16'h5e39;          // 16'b0101_1110_0011_1001

2  logic b = a[15], c = a['ha]; // b=1'b0, c=1'b1

3  logic [3:0] d = a[11:8], e = a[13:10]; // d=4'b1110, e=4'b0111

4  logic [7:0] f = a[7:0], g = a[2*4:1]; // f=8'h39, g=8'b0001_1100

5  logic [7:0] h = a[4+:8], i = a[15-:8]; // h=8'he3, i=8'h5e

6  logic [3:0] j;

7  logic [2:0] k = a[j+2:j]; // 文法錯誤,索引不能為變量表達式

8  logic [2:0] l = a[j+:3];// 假設j=3, l=3'b111

9  ...

10  a[7:4] = 4'h9; // a=16'h5e99

11  a[4] = 1'b0;// a=16'h5e89 

12  ...

注意第10、11行,指派語句實際需要放置在過程塊中,這裡隻是示意,表示位選取操作可以作為指派語句的左值。

2.9.2 位拼接和流運算符

位拼接運算符用于将多個資料拼接成更長的資料,常用形式如下:{<操作數1>{, <操作數2>}}花括号中可以有一個或多個操作數,多個操作數間以逗号隔開,無論操作數有無符号,位拼接的結果都是無符号數。左側的操作數會放置在結果的高位。

還有将一個或多個操作數重複多次的形式:{<重複次數>{<操作數1>{, <操作數2>}}}其中重複次數必須是非負的常數或常量表達式。

代碼2-13是位拼接操作符的一些例子。

代碼2-13 位拼接操作符的示例

1  logic [7:0] a = 8'hc2;            // a=1100_0010

2  logic signed [3:0] b = -4'sh6;// b=4'b1010=4'ha

3  logic [11:0] c = {a, b}; // c=12'hc2a

4  logic [15:0] d = {3'b101, b, a, 1'b0};// d=16'b101_1010_1100_0010_0

5  logic [63:0] e = {4*4{b}};// e=64'haaaa_aaaa_aaaa_aaaa

6  logic [17:0] f = {3{b, 2'b11}}; // f=18'b101011_101011_101011

7  logic [15:0] g = {a, {4{2'b01}}}; // g=16'hc255

8  ...

9  {a, b} = 12'h9bf; // a=8'h9b, b=4'hf=-4'sh1

10  ...

第9行的指派語句實際需要放置在過程塊中,這裡隻是示意,表示位拼接操作可以作為指派語句的左值。

流操作符用于将操作數按位、按幾位一組或按元素重新組織次序,常用的形式為:{(<<| >>)[<寬度>| <類型>]{<操作數1>{, <操作數2>}}}其中寬度為常數或常量表達式,類型可以是2.6節中定長整型中的一種。代碼2-14是流操作符的例子。

代碼2-14 流操作符的示例

1  logic [15:0] a = 16'h37bf;          // 16'b0011_0111_1011_1111

2  logic [15:0] b = {>>{a}}; // b=16'h37bf

3  logic [15:0] c = {<<{a}}; // c=16'hfdec=16'b1111_1101_1110_1100

4  logic [19:0] d = {<<{4'ha, a}}; // d=16'hfdec5

5  logic [15:0] e = {<< 4 {a}}; // e=16'hfb73

6  logic [15:0] f = {<< 8 {a}}; // f=16'hbf37

7  logic [15:0] g = {<< byte {a}}; // g=16'hbf37

8  logic [8:0] h = {<< 3 {{<< {9'b110011100}}}};    // h=9'b011_110_001

9  logic [3:0] i;

11  {<<{i}} = 4'b1011; // i=4'b1101

12  {<< 2 {i}} = 4'b1011;// i=4'b1110

13  ...

其中第2行将a從左至右,即從高位到低位逐位排列,得到結果與a本身一緻,第3行将a從右至左逐位排列,得到按位反向的結果。第4行将4'ha=4'b1010與a拼接,然後按位反向。

第5行,将a逐4位一組反向;第6行,将a逐8位一組反向,第7行效果與第6行一緻。

第8行,将9'b110_011_100逐位反向後得到9'b001_110_011,然後3位一組反向,得到9'b011_110_001,整體可以了解為3位一組,組内按位反向,組間位置不變。

第11、12行,指派語句實際應放在過程塊中,這裡隻是示意,表示流操作可以作為指派的左值。

2.9.3 按位邏輯運算符

按位邏輯運算符包括&(與)、|(或)、^(異或)、~&(與非)、~|(或非)、~^、^~(同或),是二進制操作符,形式上與一進制的縮減運算符是一樣的,具體是按位邏輯運算符還是縮減運算符取決于左側是否有操作數。

按位邏輯運算符将兩個操作數的各個位一一對應作邏輯運算,如果兩個操作數位寬不一緻,則較短的那個将被補充高位,運算後得到的結果與兩個操作數中較長者的位寬相同。按位邏輯運算符的作用容易了解,這裡不舉例,不過在有x和z參與時,情況稍稍複雜。

表2-7和表2-8是“與”和“或”邏輯運算符在有x和z參與運算時的情況。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

表2-9和表2-10是“異或”和“非”邏輯運算符在有x和z參與運算時的情況。

其他幾個運算符的情況可以由上述四個組合而來。

2.9.4 縮減運算符

縮減運算符包括&(與)、|(或)、^(異或)、~&(與非)、~|(或非)、~^、^~(同或),是一進制操作符。

縮減運算符将操作數中的所有位逐個進行邏輯運算(每次結果繼續跟下一位進行邏輯運算),得到1位輸出。表2-11是縮減運算符的例子。其中異或縮減和同或縮減的作用相當于檢測操作數中1的個數是奇數或偶數。

表2-11 縮減運算符的例子操作

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

如果操作數中有x、z,則規則與按位邏輯運算符一緻。

2.9.5 移位

移位運算符分為邏輯移位運算符(“<<”和“>>”)和算術移位運算符(“<<<”和“>>>”),它們将左側的操作數按位左或右移動右側操作數指定的位數。邏輯左移“<<”和算術左移“<<<”将左側操作數左移,高位舍棄,低位填充0,兩個功能一緻。邏輯右移“>>”和算術右移“>>>”将左側操作數右移,低位舍棄,邏輯右移移出的高位将填充0;而算術右移移出的高位在左側操作數為無符号數時填充0,為有符号數時填充符号位。代碼2-15是移位操作符的例子。

代碼2-15 移位操作符的示例

1  logic [7:0] a = 8'h9c;                 // 8'b10011100 = 156

2  logic signed [7:0] b = -8'sh64; // 8'b10011100 = -100

3  logic [7:0] c = a << 2; // c=8'b01110000

4  logic [7:0] d = b << 2; // d=8'b01110000

5  logic [7:0] e = b <<< 2; // e=8'b01110000

6  logic [7:0] f = b >> 2; // f=8'b00100111 = 39

7  logic [7:0] g = a >>> 2; // g=8'b00100111 = 39

8  logic [7:0] h = b >>> 2; // h=8'b11100111 = -25

9  logic [7:0] i = 9'sh9c; // i=9'b010011100

10  logic [7:0] j = i >>> 2; // j=8'b00100111

其中,第8、10行右移時,因左側操作數為有符号數,高位填充符号位,分别為“1”和“0”。

算術左移運算a<<

>>b,可了解為a/2b。

2.9.6 自增指派和自減指派

自增/自減運算符可寫成這樣幾種形式:a++、a--、++a、--a,如果它們自成一句,則表示a值加1或減1賦回給a,它們實際上也是指派語句的一種。如果它們是表達式中的一部分, 則運算符位于操作數左側表示先自增/減,再參與表達式計算;而運算符位于操作數右側則表示操作數先參與表達式計算,表達式完成之後再自增/減。代碼2-16是自增/自減的一些例子。

代碼2-16 自增/自減示例

1  logic [3:0] a = 4'h3;

2  logic [3:0] b;

3  ...

4  a++;                           // a=4

5  a--; // a=3

6  b = 4'd1 + a++; // b=4, a=4

7  b = 4'd1 + ++a; // b=6, a=5

第6行,a的原值3先與1相加賦給b,然後a自增;第7行,a先自增得到5,再與1相加賦給b。

注意,在Verilog标準裡并沒有規定類似:b = a++ + (a = a - 1);這樣有多個指派在一個語句中的情況,指派和運算的先後順序、b最後的值會是多少取決于不同編譯器的實作。我們應避免寫出這樣的語句。

2.9.7 條件判斷相關運算符

邏輯非(!)、與(&&)、或(||)、隐含(->)和等價(<->)都将操作數當作1位無符号來處理,0值當作1'b0,意為“假”,非0值當作1'b1,意為“真”。如操作數中包含x或z,則當作1'bx。1'bx與其他1位值的邏輯運算同按位邏輯運算規則一緻。

對于與和或運算,如果左側操作數已經能決定結果(在“與”中為0或在“或”中為1),則右側表達式将不會被求值(意為當中如有指派、函數都不會執行)。而對于隐含和等價運算,無論如何,兩側表達式均會被求值。

隐含運算(->):<操作數1> -> <操作數2>等價于:(!<操作數1> || <操作數2>)即操作數1為假或操作數2為真,如0->1、0->0、1->1、0->1'bx、1'bx->1的結果均為1'b1。

而等價運算(<->):<操作數1> <-> <操作數2>等價于:((<操作數1> -> <操作數2>) && (<操作數2> -> <操作數1>))相當于操作數1和操作數2同為真或同為假,如0<->0、1<->1的結果為1'b1。

算術比較運算符(<、<=、>=、>)比較簡單,根據字面意義了解即可。如果算術比較運算的操作數中含有x或z,則結果均為1'bx。

相等、不等和條件相等、條件不等運算符的差別在于對x和z的處理。在相等、不等運算符中,當x或z引起結果不确定時,結果為1'bx;而在條件相等、條件不等運算符中,x和z位也參與比對。

通配相等和通配不等運算a ==? b和a !=? b中,操作數b中的x或z将當作通配符,不參與比較。

代碼2-17是條件判斷中與x和z相關的例子。

代碼2-17 條件判斷中帶有x和z的例子

1  logic a = 4'b0010 || 2'b1z;                // a = 1'b1 | 1'bx = 1'b1

2  logic b = 4'b1001 < 4'b11xx; // b = 1'bx

3  logic c = 4'b1001 == 4'b100x;// c = 1'bx

4  logic d = 4'b1001 ! = 4'b000x;// d = 1'b1

5  logic e = 4'b1001 === 4'b100x; // e = 1'b0

6  logic f = 4'b100x === 4'b100x; // f = 1'b1

7  logic g = 4'b1001 ==? 4'b10xx; // g = 1'b1

8  logic h = 4'b1001 !=? 4'b11??; // h = 1'b1,?即為z

9  logic i = 4'b10x1 !=? 4'b100?; // i = 1'bx

第1行或運算,未知值與1“或”的結果為1。第2行,雖然無論4'b11xx的後兩位是什麼值都大于4'b1001,但根據标準,結果仍為1'bx。第3行,x 會影響結果,因而結果為1'bx。第4行,前幾位已經不相同了,因而結果為1'b1。

第7、8行,僅比較高兩位。第9行,比較前三位,左側x導緻結果不明确,因而結果為1'bx。

屬于集合(inside)運算也是條件判斷的一種,判斷inside左側操作數是否屬于右側集合。一般形式如下:<表達式> inside {<集合>}其中的集合可以是逗号分隔的元素清單(<元素1>, {<元素i>})、範圍([<下限>, <上限>])或數組,也可以是它們的任意組合。例如,1 inside {1, 2, 3, [5:9]} 為1'b1;4 inside {1, 2, 3, [5:9]} 為1'b0;6 inside {1, 2, 3, [5:9]} 為1'b1。

2.9.8 條件運算符

條件運算符的使用格式如下:<表達式1> ? <表達式2> : <表達式3>如果表達式1為真,則傳回表達式2的值,否則傳回表達式3的值,未被傳回值的表達式不會被求值。

代碼2-18 條件運算符示例

1  logic [2:0] grade = (score >= 90) ? 4 :

2  (score >= 80) ? 3 :

3  (score >= 70) ? 2 :

4  (score >= 60) ? 1 : 0;

注意條件運算符是自右向左結合的,是以可以有代碼2-18所示的例子。

這個例子并不是将“(s >= 90) ? 4 : (s >= 80)”作為一個整體當作第二個“?”的條件,而是自右向左結合,因而第2行及以後的内容均為第一個條件運算符的“表達式3”。以此類推。

2.9.9 let語句

let語句用來定義表達式模闆,帶有簡單的端口清單,可了解為帶參數的表達式,可在其他表達式裡使用它。一般形式是:let <表達式名>(<端口1>{, <端口i>}) = <引用端口的表達式>;代碼2-19是let語句的一些例子。

代碼2-19 let語句示例

1  let max(a, b) = a > b ? a : b;

2  let abs(a) = a > 0 ? a : -a;

3  logic signed [15:0] a, b, c;

4  ...

5  c = max(abs(a), abs(b));

6  ...

let語句看起來與後面将講到的帶有參數的宏定義編譯指令(

define)類似,但let語句有作用域,而帶參數的

define則是全局有效的。對于帶參數的表達式的定義,應盡量使用let語句。

2.10 結構和聯合

結構是多個相關量的集合,可以作為整體引用,也可引用其中的單個成員。

結構分為不緊湊的(unpacked)和緊湊的(packed),預設是不緊湊的。不緊湊的結構可以包含任意資料類型,成員之間可能會因為要對齊到整位元組而出現間隙。具體對齊方式标準中沒有定義,取決于編譯器,因而不能整體當作簡單類型參與相關運算。而對齊的結構的内部成員之間沒有位間隙,整體可以了解為一個多位資料,先定義的成員位于高位,可以整體參與算術或邏輯運算,可以指定有無符号。

代碼2-20是結構定義的例子。

代碼2-20 結構定義示例

1  struct {

2  logic signed [15:0] re;

3  logic signed [15:0] im;

4  } c0, c1;

5  struct {

6  time t; integer val;

7  } a;

代碼2-21 使用typedef關鍵字定義結構類型

1  typedef struct {

4  } Cplx;

5  Cplx c0, c1;

6  wire Cplx c2 = c0;

代碼2-20中定義了兩個匿名的結構,并同時定義了該結構類型的變量。

也可以使用“typedef”關鍵字為結構類型命名,而後使用該類型名定義變量或線網,如代碼2-21所示。

其中第1~4行定義了名為Cplx的類型,第5行定義兩個Cplx類型的變量c0和c1,第6行則定義了Cplx類型的線網,并與c0相連接配接。

結構既可以整體引用或指派,也可以使用成員運算符“.”選取内部成員引用或指派。對結構整體的指派可使用結構常數,一般形式如下:'{<成員1值>{, <成員i值>}}事實上,其中成員的值也可以是變量,了解為“常形式、變成員”。

代碼2-22是結構和成員通路的例子。

代碼2-22 結構和成員通路示例

1  logic signed [15:0] a = 16'sd3001;

2  logic signed [15:0] b = -16'sd8778;

3  Cplx c0, c1, c2;                 // c0=c1=c2='{x,x}

4  wire Cplx c3 = c1; // c3=c1='{x,x}

5  wire Cplx c4 = '{a, b};// c4={3001,-8778}

7  c0.re = 16'sd3001; // c0='{3001,x}

8  c0.im = b; // c0='{3001,-8778}

9  c1 = '{16'sd3001, -16'sd8778}; // c3=c1={3001,-8778}

10  c2 = '{a, -16'sd1};// c2={3001,-1}

11  c2 = '{c2.im, c2.re}; // c2={-1,3001}

12  a = 16'sd1;// c4={1,-8778}

注意,線網類型是随着驅動它的變量的變化而變化的。

對齊的結構使用“packed”關鍵字定義,還可以使用signed或unsigned關鍵字指定當作整體運算時是否有符号。代碼2-23是對齊的結構相關的例子。

代碼2-23 緊湊型結構示例

1  typedef struct packed signed {

5  Cplx c0 = {16'sd5, -16'sd5};

6  logic signed [15:0] a = c0.re;       // a=5

7  logic signed [15:0] b = c0[31:16];// b=5

8  logic [3:0] c = c0[17:14];// c=4'b0111

9  Cplx c1 = {<<16{c0}}; // c1='{-5,5}

聯合與結構類似,隻不過其中的成員共用值保持單元(線網或變量的位)。聯合分為緊湊和不緊湊兩類。不緊湊的聯合的對齊方式在标準中沒有定義,因而視編譯器不同,有可能各成員未必嚴格共用值保持單元。FPGA編譯工具一般不支援不緊湊的聯合。

代碼2-24是聯合的例子。

代碼2-24 聯合示例

1  typedef union packed {

2  logic [15:0] val;

3  struct packed {

4  logic [7:0] msbyte;

5  logic [7:0] lsbyte;

6  } bytes;

7  } Abc;

8  Abc a;

10  a.val = 16'h12a3;               // a.byte.msbyte=8'h12, lsbyte=8'a3

11  a.bytes.msbyte = 8'hcd; // a.val=16'hcda3

聯合Abc中,val和bytes結構占用相同單元,因而給a.val指派時,a.bytes中的内容同時變化,而給a.bytes.msbyte指派時,a.val的高位元組同時變化。

聯合還有帶标簽的類型(tagged),使用額外的位記錄标簽,訓示聯合中的目前有效成員。其可整體指派,指派的同時使用tagged關鍵字設定有效成員并對其指派。通路時,隻能通路目前有效成員。FPGA編譯器對帶标簽的聯合的支援有一定限制。

代碼2-25是帶标簽的聯合的例子。

代碼2-25 帶标簽的聯合示例

1  typedef union tagged {

2  logic [31:0] val;

4  byte b3, b2, b1, b0;

5  } bytes;

6  } Abct;

7  Abct ut;

8  logic [31:0] c;

9  byte d;

11  ut.val = 32'h7a3f5569;              // 無效語句

12  ut = tagged val 32'h1234abcd; // 被指派,并标記val為有效成員

13  d = ut.bytes.b0;// 無效語句

14  ut = tagged bytes '{'h11, 'h22, 'h33, 'h44};

15  // 被指派,并标記bytes為有效成員

16  d = ut.bytes.b0;// 有效通路,d=8'h44

17  ...

第11行,因ut還沒有被标記為有效成員,因而不能被指派;第13行,目前有效成員為val,因而不能通路bytes。

2.11 數組

數組是一系列變量或線網的順序組合。數組有以下幾種常用的定義形式:<簡單整型> [<位索引1範圍>]{[<位索引i範圍>]} <數組名>;

<簡單整型> {[<位索引範圍>]} <數組名>[<元素索引1範圍>]{[<元素索引i範圍>]};

<複合類型> <數組名>[<元素索引1範圍>]{[<元素索引i範圍>]};其中第一種定義的數組稱為緊湊(packed)數組。事實上,多位的logic、bit本身就是緊湊數組的一種,位選擇也就是數組元素選擇。多索引,即多元的緊湊數組本身相當于一個長整型資料,可以當作一個整型資料使用。

後兩種是非緊湊數組,複合類型可以是結構、聯合等。即使是整型非緊湊數組,一般也能當作整型整體使用。但任何數組或數組中的一段連續部分都可以用對等的類型整體指派。

位索引範圍的寫法與變量位寬定義的寫法一樣,元素索引範圍的寫法除了可以與變量位寬定義的寫法一樣,還可以隻寫明元素數量。引用數組元素與變量的位選取寫法一樣。

對于形如:<類型> [i - 1 : 0][j - 1 : 0] <數組名> [0 : m - 1][0 : n - 1];等價于<類型> [i - 1 : 0][j - 1 : 0] <數組名> [m][n];一般稱它有m×n個元素,每個元素為i×j位。通路時:<數組名> [m - 1][n - 1]是它的最後一個元素,有i×j位。<數組名> [m - 1][n - 1][i - 1]是它的最後一個元素的最高一個j位組。<數組名> [m - 1][n - 1][i - 1][j - 1]是它的最後一個元素的最高位。

數組的指派可使用數組常數,與結構常數類似。

代碼2-26是數組的一些例子。

代碼2-26 數組示例

1  logic [3:0][7:0] a[0:1][0:2] ='{

2  '{32'h00112233, 32'h112a3a44, 32'h22334455},

3  '{32'h33445566, 32'h4455aa77, 32'hf5667788}};

4  logic [31:0] b = a[0][2];          // 32'h22334455;

5  logic [15:0] c = a[0][1][2:1]; // 16'h2a3a;

6  logic [7:0] d = a[1][1][1]; // 8'haa;

7  logic [3:0] e = a[1][2][3][4+:4];// 4'hf;

9  a[0][0][3:2] = a[1][0][1:0]; // a[0][0]=32'h55662233

2.12 指派、過程和塊

指派用來驅動線網或變量,驅動線網可了解為建構線網的連接配接,驅動變量則是賦予變量新的值。Verilog中指派主要有兩種:

1) 持續指派,持續指派使得線網持續接收右值,相當于直接連接配接到組合邏輯的輸出。

2) 過程指派,隻是在特定事件發生時更新變量的值,而變量則會保持該值直到下一次更新。

此外還有過程持續指派,但其在FPGA設計中并不常用,這裡不作介紹。持續指派的左值隻能是線網,而過程指派的左值隻能是變量。

持續指派有兩種形式:線上網定義時指派和使用assign語句指派。線上網定義時指派在前面幾節的例子中已經出現很多了。assign語句指派的一般形式如下:assign <線網名1> = <表達式1>{, <線網名i> = <表達式i>};過程指派隻能位于過程中。過程是受一定條件觸發而執行的代碼結構,在Verilog中,有以下幾種過程。

1) initial過程,在啟動(仿真開始或實際電路啟動)時開始,執行一次。initial多用于仿真,其中的内容根據編譯器和FPGA的具體情況,部分可綜合。一般在編譯期處理,用于初始化寄存器的上電值或初始化存儲器的内容。

2) always過程,又分為以下幾種。

  • always過程,可指定或不指定觸發事件。不指定觸發事件時,從啟動時開始周而複始地執行。指定觸發事件時,在事件發生時執行。
  • always_comb過程,專用于描述組合邏輯,在過程内的語句所依賴的任何一個值發生變化時執行,輸出随着輸入的變化而立即變化,正是組合邏輯的行為。
  • always_latch過程,專用于描述鎖存器邏輯,由指定的線網或變量的值觸發内部語句執行,在FPGA中應避免出現鎖存器,因而,本書不會專門介紹always_latch過程。
  • always_ff過程,專用于描述觸發器邏輯(ff是觸發器flip-flop的縮寫),當指定的線網或變量(即時鐘)出現指定跳沿時執行。

    3) final過程,在仿真結束時執行一次。

4) task過程,即任務。

5) function過程,即函數。

任務和函數将在後續小節講到。

過程中的語句常常不止一句,Verilog使用塊組合多條語句,塊有兩種:

1) 順序塊,使用begin-end包裹,其中的多條語句是逐條執行的。

2) 并行塊,使用fork-join包裹,其中的多條語句是同時執行的。

順序塊是可綜合的,而并行塊一般是不可綜合的,多用于編寫測試代碼。

2.12.1 指派的延遲

實際電路是有傳輸延遲的。在Verilog中,可在指派語句中增加延遲,以模拟實際情況或産生需要的時序。延遲常用于編寫測試代碼,對實際電路來說是不可綜合的,在綜合過程中會忽略代碼中的延遲。

在指派語句中指定延遲有以下幾種常用形式:#<時間> <變量> = <表達式>;

<變量> = #<時間> <表達式>;

assign #<時間> <線網> = <表達式>;前兩種延遲指派在順序塊(begin-end)中,語句本身占用執行時間。第三種形式意為表達式的值變化之後,線網受到其影響将延遲。

在定義線網時,也可以指定線網的延遲,意為任何驅動該線網的變化都将延遲:<線網類型> [<資料類型符号位寬>] #<時間> <辨別符>...;代碼2-27是關于延遲的例子。

代碼2-27 begin-end塊中的延遲示例

1  logic [7:0] a = 8'd0, b = 8'd0;

2  wire [7:0] #5ns c = a;

3  wire [7:0] d;

4  assign #2ns d = c;

5  initial begin

6  #10ns a = 8'd10;

7  #20ns a = 8'd20;

8  b = #10ns 8'd30;

9  b = #20ns 8'd40;

10  #30ns a = 8'd30;

11  end

第2行定義c,并使得c随着a的變化而變化,但延遲5ns;第4行将d連接配接到c,随着c的變化而變化,但延遲2ns,相對于a則延遲7ns。

第5~11行是initial過程,其内容為一個begin-end塊。在仿真啟動時,塊内容開始執行。10ns時a值變為10,于是15ns時c變為10,17ns時d變為10;30ns時,a值變為20;40ns時,b變為30;60ns時,b變為40;90ns時,a變為30。其波形如圖2-4所示。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

圖2-4 代碼 2-27的波形

如果将其中的begin-end塊換為fork-join塊,如代碼2-28所示。

第6~10行語句将同時執行。a在10ns時變為10,在20ns時變為20,在30ns時變為30;b在10ns時變為30,在20ns時變為40。波形如圖2-5所示。

代碼2-28 fork-join中的延遲示例

1  ...

5  initial fork

11  join

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

圖2-5 代碼2-28的波形

在Verilog中,指派中除了可指定延遲外,還能指定觸發事件,後續章節将有提及,這裡不專門介紹。

2.12.2 指派的強度

在2.5節中提到了驅動強度。驅動強度在FPGA設計裡是不能綜合的,但對編寫測試代碼比較有用,初學者不必熟記本節知識。Verilog中驅動強度有以下幾種:

1) 1的強度,由強到弱:supply1、strong1、pull1、weak1、highz1。

2) 0的強度,由強到弱:supply0、strong0、pull0、weak0、highz0。

如果要對線網指定驅動強度,可以線上網定義時:<線網類型> (<1的強度>, <0的強度>) [<資料類型符号位寬>] <辨別符>...;也可在持續指派時:assign (<1的強度>, <0的強度>) <線網名1>...;其中1的強度和0的強度不能同為highz。線網被不同強度驅動時,以最強的為準;當有多個最強強度時,則根據2.5節多重驅動的規則而定。

代碼2-29是有關驅動強度的例子。

代碼2-29 驅動強度的示例

1  logic [1:0] data = '1;

2  logic pup = '0;

3  wire (pull1, highz0) sda = pup;

4  assign (highz1, strong0) sda = data[0];

5  assign (highz1, strong0) sda = data[1];

6  initial begin                  // pup + data[0] + data[1]

7  #10ns data[0] = '0; // sda: hz0 + st0 + hz1 = st0

8  #10ns data[1] = '0; // sda: hz0 + st0 + st0 = st0

9  #10ns data = '1;// sda: hz0 + hz1 + hz1 = hz

10  #10ns pup = '1; // sda: pu1 + hz1 + hz1 = pu1

11  #10ns data[0] = '0; // sda: pu1 + st0 + hz1 = st0

12  #10ns data[1] = '0; // sda: pu1 + st0 + st0 = st0

13  end

第7行,10ns時,驅動sda的三個源分别是:pup為highz0、data[0]為strong0、data[1]為highz1,是以結果為strong0;第9行,30ns時,全部三個源為highz,是以結果為highz;第10行,一個pull1、兩個high1,是以結果為pull1;第11行,data[0]為strong0,是以結果為strong0。

2.12.3 流程控制語句

在過程中,除了指派,更多的邏輯功能使用流程控制語句實作。常用的流程控制語句如下。

1) if-else語句。

2) case語句,包括:

  • case語句
  • casez語句
  • casex語句

    3) 循環語句,包括:

  • forever語句
  • repeat語句
  • while語句
  • do-while語句
  • for語句
  • foreach語句

if-else語句的形式一般是:[unique| unique0| priority] if(<條件表達式1>) <單一語句或塊1>

{else if(<條件表達式i>) <單一語句或塊i>}

[else <單一語句或塊j>]其中的語句和塊也可以是僅包含一個分号的空語句。上述形式的意義是:如果條件表達式1為真,則執行語句或塊1;否則如果條件表達式i為真,則執行語句或塊i,有多個else if的以此類推;最後,如果前面的條件表達式均為假,則執行語句或塊j。一旦任何一個語句或塊被執行,整個if-else語句結束,因而,if-else語句中的條件表達式将逐一求值,直到遇到值為真的表達式,則後續表達式将不被求值。

case語句的一般形式是:[unique| unique0| priority](case| casez| casex)(<條件表達式>) [inside| match]

<條件項1>{, <條件項2>}: <單一語句或塊1>

{<條件項i>{, <條件項i+1>}: <單一語句或塊i>}

[default: <單一語句或塊j>]

endcase其中條件表達式和條件項可以但隻能有一個是常數或常量表達式。意為如果條件表達式與條件項1比對,則執行語句或塊1;如果條件表達式與條件項2比對,則執行語句或塊2;以此類推,如果沒有比對,則指定語句或塊j。熟悉C語言的讀者需注意,Verilog的case本身不能穿越條件,因而沒有也不需要break語句。

case語句的比對需要條件表達式和條件項中的z和x均一緻。

casez則将條件表達式和條件項中的z(?)視為無關,不參與比對。

casex則将條件表達式和條件項中的x和z(?)均視為無關,不參與比對。

如果使用inside關鍵字,則條件項可以是集合(見2.9.7節inside操作符部分);如果使用match關鍵字,則可比對帶标簽的聯合的活動元素和元素内容。

if-else語句和case語句還可以使用unique、unique0或priority關鍵字修飾(置于if或case關鍵字之前)。它們的意義分别如下。

1) unique:

  • 要求所有條件互斥,即不能有任何情況使得多個條件表達式為真或比對多個條件項。
  • 要求分支完備,即不能有任何情況使得所有條件表達式為假或不比對任何條件項(有else或default除外)。

2) unique0,要求所有條件互斥,但不要求分支完備。

3) priority,當條件重疊,即有情況使得多個條件表達式為真或比對多個條件項時,靠前者優先。

forever語句的形式如下:forever <單一語句或塊>意為語句或塊将被一直重複執行。

repeat語句的形式如下:repeat(<次數表達式>) <單一語句或塊>意為語句或塊将重複執行由次數表達式指定的次數;如果次數表達式的值包含z或x,則按0處理。

while語句的一般形式如下:while(<條件表達式>) <單一語句或塊>意為如果條件表達式為真,則重複執行語句或塊,直到條件表達式為假,一般條件表達式的值應依賴于語句或塊,否則将形成無限循環。

do-while語句的一般形式如下:do <單一語句或塊> while(<條件表達式>)意為先執行一次語句或塊,然後求條件表達式。如為真,則重複執行,否則結束。一般條件表達式的值應依賴于語句或塊,否則将形成無限循環。

for語句的一般形式如下:for([<初始語句>]; [<條件表達式>]; [<步進語句>]) <單一語句或塊>括号中初始語句和步進語句也可以是由逗号分隔的多句:for([<初始語句1>{, <初始語句i>}]; [<條件表達式>]; [<步進語句1>{, <步進語句j>}]) <單一語句或塊>意為先執行初始語句,然後求條件表達式,如為真則執行語句或塊,然後執行步進語句,再次求條件表達式,如為真,依此循環,直到條件表達式為假,整個語句結束。步進語句一般是對條件表達式所依賴的變量進行更新,如果語句或塊包含對條件表達式所依賴的變量更新,步進語句也可以與條件表達式無關。

foreach語句用于窮舉數組中的元素,一般形式如下:foreach(<數組名>[<辨別符1>{, <辨別符i>}]) <單一語句或塊>其中數組名為待窮舉的數組名,辨別符為新命名的辨別符,用來按次序比對元素的索引,可在語句或塊中作為整數使用,例如:foreach(arr[i, j]) arr[i][j] = i + j;将使arr中的每個元素指派為其兩個索引之和。

值得注意的是,硬體描述語言描述的是各電路單元同時運作的電路,上述所有循環語句并不意味着其中的語句按時間先後在一步一步地執行,而是EDA工具綜合出一個“龐大的”電路來完成整個循環過程描述的行為。這個電路一般是組合邏輯電路,結果将随着輸入的變化而變化,僅有必要的電路延遲,電路規模取決于循環的次數和内容。是以,在使用循環語句編寫需要綜合成實際電路的代碼時要十分小心,以避免過多的資源消耗。

2.12.4 always過程

always過程是Verilog中,特别是FPGA設計中最重要的文法元素。重要的邏輯結構基本都是由always過程實作的。

通用always過程的一般形式有:always <單一語句或塊>

always@(<敏感值清單>) <單一語句或塊>

always@(*) <單一語句或塊>

always@(<事件清單>) <單一語句或塊>第一種形式,沒有指定過程執行的觸發條件,過程将不斷重複執行。

第二種形式,敏感值的形式一般是:<變量或線網1> {(,| or) <變量或線網i>}當敏感值清單中的任何一個變量或線網發生值改變時,過程執行。如果塊内語句依賴的所有變量和線網都在敏感值清單中列出,則always過程塊将形成組合邏輯;如果塊内語句依賴的變量和線網隻有部分在敏感值清單中列出或者内部語句存在分支不完整,則always過程塊将形成鎖存器邏輯。

第三種形式,使用“*”代替敏感值清單,編譯器将自動找出塊内語句依賴的所有變量填充,主要用于實作組合邏輯。

第四種形式,事件的最常用形式是:(posedge| negedge| edge) <變量或線網1> {(,| or) (posedge| negedge| edge) <變量或線網i>}意為事件清單中的任何一個變量或線網出現上升沿(posedge)、下降沿(negedge)或任意沿(edge)時,觸發過程執行,主要用于實作觸發器(時序邏輯)。在FPGA設計中,限于FPGA的内部結構,一般隻能使用posedge。如果使用negedge将占用更多單元,而edge則一般不能使用。

用作沿觸發的變量和線網如果有多位,将隻有最低位有效。當電平由0變為1、x或z時,或電平由x或z變為1時,均認為是上升沿;當電平由1變為0、x或z時,或電平由x或z變為0時,均認為是下降沿。

代碼2-30是always過程使用的一些例子。

代碼2-30 通用always過程示例

1  logic ck = '1;

2  wire #2ns clk = ck;            // 将ck延遲2ns得到clk

3  logic [7:0] a = '0, b = '0;

4  logic [7:0] c, d, e, f;

5  always begin

6  #10ns ck = ~ck; // 産生周期20ns的時鐘ck

7  end

8  always begin

9  #5ns a = a + 8'b1; // 産生10ns周期遞增的a

10  #5ns b = b + 8'b1; // 産生10ns周期遞增的b

12  always@(a, b) begin // 組合邏輯加法器

13  c = a + b;

14  end

15  always@(*) begin // 與上一個always過程等價

16  d = a + b;

17  end

18  always@(clk, a) begin// clk控制的鎖存器

19  if(clk) e = a;

20  end

21  always@(posedge clk) begin // clk上升沿觸發的觸發器

22  f = a;

23  end

第5行的always過程沒有敏感值或事件清單,它将周而複始地執行,每10ns将ck反相,産生20ns周期的時鐘,第2行将ck延遲2ns得到clk。第8行的always過程也沒有敏感值或事件清單,産生10ns周期遞增的a和b,a相對于b超前5ns變化。

第12行的always過程帶有敏感值清單,當a或b的值發生變化時,c指派為a和b的和,形成組合邏輯,注意其中的逗号也可替換為or關鍵字。第15行的always塊使用了“*”,與第12行的always過程等價。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

圖2-6 代碼2-30的波形

第18行的always過程帶有敏感值清單,當clk或a的值發生變化,且clk為高時e指派為a,因而clk為高電平時,e随着a的變化而變化,clk為低的情況沒有寫出,e保持原值,形成鎖存器。

第18行的always過程帶有事件清單,在clk出現上升沿時,f指派為a,形成觸發器。

仿真波形如圖2-6所示。

值得注意的是,雖然c和d由組合邏輯驅動,但直到a或b的值在5ns時第一次變化,它們才出現正确的值,這與真實的組合邏輯電路稍有不符。

Verilog還提供了專用于組合邏輯、鎖存器和觸發器的always_comb、always_latch和always_ff過程,這裡介紹always_comb和always_ff過程。建議在描述組合邏輯和觸發器邏輯時使用這兩個專用的過程,而不是使用通用always過程。

always_comb過程的形式為:always_comb<單一語句或塊>非常簡單,與用always@(*)描述的組合邏輯相比有以下優點:

  • 啟動時會執行一次,與真實組合邏輯電路的行為相符。
  • 不允許被驅動的變量在任何其他過程中驅動。
  • 如果内部條件分支不完整,會形成鎖存器,編譯器會給出警告。

always_ff過程的常用形式為:always_ff@(posedge<時鐘> iff <時鐘使能條件> or posedge <異步複位>)當然,其中的posedge也可以是negedge或edge,後面還可以增加更多的異步控制。不過對于FPGA來說,限于其内部結構,一般應使用posedge,并且至多一個異步複位。

代碼2-31是always_comb和always_ff的例子。

代碼2-31 always_comb和always_ff過程示例

1  always_comb begin                  // 組合邏輯加法器

2  c = a + b;

3  end

4  always_comb begin // 錯誤,因分支不完備,實際為鎖存器

5  if(clk) d = a + b;

6  end

7  always_comb begin // 組合邏輯,包含加法器和減法器,

8  if(clk) e = a + b; // 并由clk控制資料選擇器選擇

9  else e = a - b;

10  end

11  always_ff@(posedge clk) begin // clk上升沿觸發的觸發器

12  f = a;

14  always_ff@(posedge clk iff en) // clk上升沿觸發的觸發器,并帶有使能

15  begin // 輸入en,當en為高時,時鐘有效

16  g = a;

17  end

18  always_ff@(posedge clk iff en| rst) // clk上升沿觸發

19  begin // 帶有使能和同步複位

20  if(rst) h = '0;

21  else h = a;

22  end

23  always_ff@(posedge clk iff en or posedge arst) // clk上升沿觸發

24  begin // 帶有使能和異步複位

25  if(arst) i = '0; // arst上升沿或為高時i複位

26  else i = a;

27  end

“iff”關鍵字描述的使能采用的是門控時鐘的方式,但限于FPGA結構,門控時鐘并不利于FPGA綜合,最好采用Q-D回報的形式。上述例子中最後三個always_ff過程可修改為代碼2-32的寫法,推薦在FPGA設計中使用。

代碼2-32 避免門控時鐘的觸發器使能和複位

1  always_ff@(posedge clk)          // clk上升沿觸發的觸發器,并帶有使能

2  begin

3  if(en) g = a; // 當en為高時,才更新g

4  end

5  always_ff@(posedge clk) // clk上升沿觸發

6  begin // 帶有使能和同步複位

7  if(rst) h = '0;

8  else if(en) h = a;

9  end

10  always_ff@(posedge clk or posedge arst) // clk上升沿觸發

11  begin // 帶有使能異步複位

12  if(arst) i = '0;// arst上升沿或為高時i複位

13  else if(en) i = a;

2.12.5 阻塞和非阻塞指派

除非阻塞指派(<=)以外的所有指派均為阻塞指派。

在順序塊中,阻塞指派語句将“阻塞”後面語句的求值和指派,即阻塞指派語句是按書寫次序逐條求值和指派的;而非阻塞指派語句不會“阻塞”後面語句的求值和指派,所有非阻塞語句将與最後一條阻塞指派語句同時執行。例如代碼2-33所示。

代碼2-33 initial過程中混合阻塞指派和非阻塞指派示例

1  logic a = '0, b = '0, c;

2  initial begin                 // 執行次序結果

3  a = '1; //  1a = 1

4  b = a; //  2b = 1, 使用次序1之後的a

5  a <= '0;//  4a = 0

6  b <= a; //  4b = 1, 使用次序3之後的a

7  c = '0; //  3c = 0

8  c = b; //  4c = 1, 使用次序3之後的b

最終的結果将是a=0,b=1,c=1。

在always_ff過程中混合阻塞和非阻塞指派語句的示例見代碼2-34。

代碼2-34 always_ff過程中混合阻塞指派和非阻塞指派示例

1  logic clk = '1;

2  always #10 clk = ~clk;

3  logic [1:0] a[4] = '{'0, '0, '0, '0};

4  always_ff@(posedge clk) begin :eg0    // 一個時鐘過後a[0]=2'b11

5  a[0][0] = '1;

6  a[0][1] = a[0][0];

8  always_ff@(posedge clk) begin :eg1// 一個時鐘過後a[1]=2'b11

9  a[1][0] = '1;

10  a[1][1] <= a[1][0];

12  always_ff@(posedge clk) begin :eg2// 一個時鐘過後a[2]=2'b01

13  a[2][0] <= '1;

14  a[2][1] = a[2][0];

15  end

16  always_ff@(posedge clk) begin :eg3// 一個時鐘過後a[3]=2'b01

17  a[3][0] <= '1;

18  a[3][1] <= a[3][0];

19  end

在上面的例子中,前兩個塊的兩條語句是分兩個次序指派的,一個時鐘過後,a[0]和a[1]的兩位均為1;後兩個塊的兩條語句是同一個次序指派的,一個時鐘過後,a[2]和a[3]均為2'b01。是以,前兩個always_ff過程實際上隻綜合出一個觸發器,a[?][0]和a[?][1]均連接配接到這個D觸發器的輸出,如圖2-7所示;後兩個always_ff過程實際上綜合出兩個D觸發器,一個是a[?][0],另一個是a[?][1],它們級聯,形成一個移位寄存器,如圖2-8所示。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

在always_comb過程中混合阻塞和非阻塞指派語句的示例見代碼2-35。

代碼2-35 always_comb過程中混合阻塞指派和非阻塞指派示例

1  logic [3:0] a = 4'd0;

2  always #10 a = a + 4'd1;

3  logic [3:0] b[4], c[4];

4  always_comb begin

5  b[0] = a + 4'd1;             // b[0]=a+1

6  c[0] = b[0] + 4'd1;// c[0]=a+2

8  always_comb begin

9  b[1] = a + 4'd1; // b[1]=a+1

10  c[1] <= b[1] + 4'd1; // c[1]=a+2

12  always_comb begin

13  b[2] <= a + 4'd1; // b[2]=a+1

14  c[2] = b[2] + 4'd1;// c[2]将形成觸發器鎖定a變化前的b+1

16  always_comb begin

17  b[3] <= a + 4'd1; // b[3]=a+1

18  c[3] <= b[3] + 4'd1; // c[3]将形成觸發器鎖定a變化前的b+1

其中,前兩個過程塊将正常構成組合邏輯,如圖2-9所示。但後兩個過程塊将形成一個由a的值改變觸發的觸發器,如圖2-10所示。這與預期不符。

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

從上面兩段示例代碼可以看出,混有阻塞指派語句和非阻塞指派語句的過程塊是比較難于了解和确定代碼行為的。是以,強烈建議:

  • 描述觸發器邏輯的時候一律使用非阻塞指派,這樣每個被指派的變量都形成觸發器。
  • 描述組合邏輯的時候一律使用阻塞指派,這樣可以確定不會出現觸發器。
  • 在測試代碼中使用initial過程或不帶敏感值/事件清單的always過程時使用阻塞指派,以形成便于了解的時序。

2.13 子產品

子產品是Verilog中最基礎的結構,邏輯設計的階層化就是由子產品實作的。子產品用來封裝數字邏輯的資料、功能以及時序。子產品可大可小,或小到實作一個邏輯門,或大到實作一個CPU。

子產品本身隻是一張“圖紙”,在需要用到子產品功能的時候,需要将它“執行個體化”,一個子產品可以在不同地方被多次執行個體化,實作了代碼的可重用性。一個設計最頂層的子產品是由仿真器或編譯器來執行個體化的。

使用關鍵字module和endmodule來定義一個子產品,例如代碼2-36。

代碼2-36 hello_world子產品

1  module hello_world;

2  initial begin

3  $display("Hello World!");

4  end

5  endmodule

該代碼描述了一個名為“hello_world”的子產品,功能是在仿真環境中顯示“Hello World!”字元串,這個功能隻能用于仿真驗證,是不能被綜合為實際電路的。注意在子產品辨別符後面有分号,而在endmodule關鍵字後沒有分号。

再如代碼2-37。

代碼2-37 非門子產品

1  module not_gate (

2  input wire loigc a,          // 輸入端口,logic型線網

3  output var logic y/ 輸出端口,logic型變量 /

4  );

5  assign y = ~a;

6  endmodule

該代碼描述了一個名為not_gate的非門,功能就是一個非門,可以被綜合為實際電路。實際的電路總是有輸入輸出的,因而它比代碼2-36增加了子產品的端口,對于非門,有一個輸入a和一個輸出y。第2和第3行的input和output關鍵字指明端口的方向,這兩行後面還包含注釋,分别是由“// ”引導的單行注釋和由“/”和“/”包裹的塊注釋。第5行的assign指派語句将a取反賦給y。

子產品定義的常見形式是:module<子產品名> [(

(input| output| inout) (<線網定義1>| <變量定義1>){,

(input| output| inout) (<線網定義i>| <變量定義i>)}

)];

{<子產品内容>}

endmodule[:<子產品名>]其中,圓括号中的内容稱為端口清單,每一行可定義一個端口或多個同向同類型的端口。

input、output、inout用于指定端口方向為輸入、輸出或雙向,其後的線網或變量定義與2.5節和2.6節的形式一緻,資料類型可以是結構、數組等複合類型。在FPGA設計中,隻有output可以搭配變量定義。除頂層子產品以外,一般也不應使用inout端口。

端口定義常常也可以簡略為:(input| output| inout) <資料類型> [signed| unsigned] [<位寬>] <端口名>[=<初值>] {, ...}或:(input| output| inout) [signed| unsigned] [<位寬>] <端口名>[=<預設值>] {, ...}這兩種簡略寫法都沒有指明端口是線網還是變量,後者還沒有指定資料類型。那麼對于input或inout端口,兩者都将預設為線網,後者的資料類型将預設為logic;而對于output端口,前者将預設為變量,後者将預設為logic類型的線網。線網類型都預設為wire,如果需要預設為其他類型線網,可使用`default編譯指令修改。在FPGA設計中,input和inout端口隻支援線網類型,而output端口可以是線網也可以是變量。

對于輸入線網或輸出的變量,可以指定預設值或初始值,必須是常數或常量表達式。如果輸入線網在子產品執行個體化時沒有連接配接,則連接配接預設值;輸出變量的初始值是仿真開始或電路啟動時的初始值。目前,FPGA開發工具對端口預設值并不能很好地支援,應避免在FPGA設計中為端口指定預設值。

因而,在FPGA設計中,整型端口最常見的簡略寫法為:(input| inout) [wire](signed| unsigned) <位寬> <端口名1> {,<端口名i>}

output logic (signed| unsigned) <位寬> <端口名1> {,<端口名i>}後者指定資料類型logic,而logic類型又可根據子產品中使用它的上下文自動适應為變量或線網,因而可算是“萬能”的整型輸出端口定義方式了。

子產品内容主要可以有:

  • 資料定義(線網、變量)
  • 資料類型定義(結構、聯合等)
  • 參數、常量定義
  • 任務、函數定義
  • 子產品、接口執行個體化
  • 持續指派
  • 過程
  • 生成結構

parameter型的參數定義也可以定義在子產品定義的頭部,一般形式是:module<子產品名> #(

{,

}

)[(

endmodule[:<子產品名>]其中parameter型參數定義與2.7節中的形式一緻。

例如代碼2-38中的子產品描述了一個帶有使能和同步複位的時序邏輯加法器。

代碼2-38 參數化的時序邏輯加法器

1  module my_adder #(

2  parameter DW = 8             // integer類型參數,預設值8

3  )(

4  input clk, rst, en,

5  input [DW - 1 :0] a, b,// 使用參數定義位寬

6  output logic [DW : 0] sum // 使用參數定義位寬

7  );

8  always_ff@(posedge clk) begin

9  if(rst) sum <= '0;

10  else if(en) sum <= a + b;

12  endmodule

上面例子先定義了參數DW(意為資料寬度),然後用DW來定義輸入和輸出端口的位寬。像這樣用參數來定義子產品的某些特征的好處是,當設計中多處需要功能相似但特征(比如位寬、系數等)不同的子產品時,我們不必重複設計子產品,而隻需要在執行個體化時修改參數即可。參數化的子產品設計顯著提高了子產品的可重用性。

子產品執行個體化語句的一般形式為:<子產品名> [#(.<參數名1>(<參數指派1>){, .<參數名i>(參數指派i)})]

<執行個體名1> [(

.<端口名1>(<執行個體1端口連接配接1>){,

.<端口名i>(<執行個體1端口連接配接i>)}

)] {,

<執行個體名k> [(

.<端口名1>(<執行個體k端口連接配接1>){,

.<端口名i>(<執行個體k端口連接配接i>)}

)]};其中參數名和端口名都是子產品定義時指定的名字,參數值必須是常數或常量表達式,端口連接配接則是執行個體化該子產品的上層子產品中的線網或變量,也可以是常數或常量表達式。

對于沒有參數的子產品,不應有參數指派部分,即“#(...)”部分;對于有參數的子產品,如果不需要修改子產品定義時賦予參數的預設值,也可省略參數指派部分。

相同參數的多個執行個體化可以寫在一個子產品執行個體化語句中,以逗号分隔多個執行個體名及其端口連接配接,如代碼2-39所示。

代碼2-39 子產品執行個體化示例

1  module test_sum_ff;

2  logic clk = '0;

3  always #5 clk = ~clk;

4  logic [7:0] a = '0, b = '0, sum_ab;

5  logic co_ab;

6  logic [11:0] c = '0, d = '0;

7  logic [12:0] sum_cd;

9  #10 a++; b++; c++; d++;

11  my_adder #(.DW(8)) the_adder_8b(

12  .clk(clk), .rst(1'b0), .en(1'b1),

13  .a(a), .b(b), .sum({co_ab, sum_ab})

14  );

15  my_adder #(.DW(12)) the_adder_12b(

16  .clk(clk), .rst(1'b0), .en(1'b1),

17  .a(c), .b(d), .sum(sum_cd)

18  );

19  endmodule

第11~14行執行個體化了8位加法器the_adder_8b。注意sum輸出端口連接配接使用了位拼接運算符,将sum的低8位連接配接到了sum_ab,而第9位連接配接到了co_ab。因為代碼2-38中子產品定義時,DW參數的預設值就是8,是以第11行中“#(.DW(8))”可以省略。

第15~18行執行個體化了12位加法器the_adder_12b,其sum端口直接連接配接到了13位的sum_cd。兩個加法器的rst和en輸入使用常數連接配接。

在上面的一般形式中,使用的參數和端口連接配接寫法都是形如“.<名字>(<值/連接配接>)”,稱為按名字的參數指派和端口連接配接。此外還有按順序的參數指派和端口連接配接。例如代碼2-39中的第15~18行還可以寫成這樣:

15  my_adder #(12) the_adder_12b(

16  clk, 1'b0, 1'b1,

17  c, d, sum_cd

此時,必須保證端口連接配接的書寫順序和端口定義時的順序完全一緻。

如果端口連接配接的線網或變量與端口名一緻,還可以省略括号及其中内容,如:

16  .clk, .rst(1'b0), .en(1'b1),

其中的.clk等價于.clk(clk)。

有多個輸出端口的子產品在執行個體化時,可能有些端口并不需要引出,可以空缺括号中的内容,如 “.outx()”。

注意,子產品執行個體化中端口的連接配接并不是指派,隻是連線。端口和連接配接到端口的線網或變量中位寬較寬的,高位将獲得值z,對于FPGA一般将獲得值0。

2.14 接口

初學者可跳過此節,當覺得在很多子產品中寫一模一樣的長長的端口定義很煩瑣時,可再來學習本節。

接口用來将多個相關的端口組織在一起,為了說明接口的作用,先考慮下面代碼:

代碼2-40 存儲器及其測試

1  module mem #(

2  parameter LEN = 256, DW = 8

4  input wire clk, rst,

5  input wire [$clog2(LEN) - 1 : 0] addr,

6  input wire [DW - 1 : 0] d,

7  input wire wr,

8  output logic [DW - 1 : 0] q

9  );

10  logic [DW - 1 : 0] m[LEN] = '{LEN{'0}};

11  always_ff@(posedge clk) begin

12  if(rst) m <= '{LEN{'0}};

13  else if(wr) m[addr] <= d;

15  always_ff@(posedge clk) begin

16  if(rst) q <= '0;

17  else q <= m[addr];

18  end

20  

21  module mem_tester #(

22  parameter LEN = 256, DW = 8

23  )(

24  input wire clk, rst,

25  output logic [$clog2(LEN) - 1 : 0] addr,

26  output logic [DW - 1 : 0] d,

27  output logic wr,

28  input wire [DW - 1 : 0] q

29  );

30  initial addr = '0;

31  always@(posedge clk) begin

32  if(rst) addr <= '0;

33  else addr <= addr + 1'b1;

34  end

35  assign wr = 1'b1;

36  assign d = DW'(addr);

37  endmodule

38

39  module testmem;

40  logic clk = '0, rst = '0;

41  always #5 clk = ~clk;

42  initial begin

43  #10 rst = '1;

44  #20 rst = '0;

45  end

46  logic [5:0] addr;

47  logic [7:0] d, q;

48  logic wr;

49  mem_tester #(64,8) the_tester(clk, rst, addr, d, wr, q);

50  mem #(64,8) the_mem(clk, rst, addr, d, wr, q);

51  endmodule

第一個子產品mem描述了一個帶有同步複位的存儲器,存儲單元是用數組描述的,輸出是先讀模式,子產品内的代碼應容易了解,此處不贅述。第二個子產品mem_tester用測試存儲器,它在clk的驅動下産生了遞增的addr和資料,并一直将wr置1。第三個子產品是頂層,執行個體化前兩者,并産生前兩者共同需要的clk和rst信号。

從這個例子中可以看出,存儲器需要用到的幾個端口addr、d、wr、q、clk和rst在子產品端口定義中出現了兩次,在頂層子產品中定義了它們,并在子產品執行個體化時重複書寫了兩遍。對于更複雜的設計,可能存在更多的子產品需要用到同樣的端口,全部分散書寫出來,降低了代碼效率、可讀性和可維護性。

接口可以将衆多端口組織在一起,形成一個模闆,在需要時執行個體化接口,便可使用一個辨別符引用。

接口定義的常用形式如下:interface <接口名> [#(

)][(

(input| output| inout) (<外部共享端口定義1>){,

(input| output| inout) (<外部共享端口定義i>)}

{<線網或變量定義>;}

{<其他内容>}

{modport <角色名> (

(input| output| inout) <端口1>{,

(input| output| inout) <端口i>}

);}

endinterface[:<接口名>]其中,<其他内容>可以是參數/常量定義、任務或函數。接口内部定義的線網或變量是應用該接口的子產品内需要用到的端口,而定義在接口頭部的外部共享端口還可以在執行個體化接口時與接口外部的線網或變量連接配接。modport關鍵字引導角色定義,用于指定不同端口在接口的不同角色(比如主、從、監聽)下的方向。

接口的執行個體化與子產品的執行個體化形式幾乎一樣,此處不贅述。在子產品的端口定義清單中引用接口時,可使用“<接口名>.[<角色名>]”的形式,在子產品中引用接口内的端口時,使用“<接口名>.[<端口名>]”的形式。

使用接口,可将代碼2-40修改為代碼2-41。

代碼2-41 存儲器及其測試

1  interface membus #(                 // 定義名為membus的接口

4  input wire clk, input wire rst// 外部共享端口clk和rst

5  );

6  logic [$clog2(LEN) - 1 : 0] addr;

7  logic [DW - 1 : 0] d, q;

8  logic wr;

9  modport master(output addr, d, wr, // 定義角色master

10   input clk, rst, q);

11  modport slave(input clk, rst, addr, d, wr, // 定義角色slave

12   output q);

13  endinterface

14  

15  module mem #(parameter LEN = 256, DW = 8)

16  (membus.slave bus); // 引用接口,并命名為bus

17  logic [DW - 1 : 0] m[LEN] = '{LEN{'0}};

18  always_ff@(posedge bus.clk) begin // 引用bus中的clk

19  if(bus.rst) m <= '{LEN{'0}};

20  else if(bus.wr) m[bus.addr] <= bus.d;

21  end

22  always_ff@(posedge bus.clk) begin

23  if(bus.rst) bus.q <= '0;

24  else bus.q <= m[bus.addr];

25  end

26  endmodule

27  

28  module mem_tester #(parameter LEN = 256, DW = 8)

29  (membus.master bus);

30  initial bus.addr = '0;

31  always@(posedge bus.clk) begin

32  if(bus.rst) bus.addr <= '0;

33  else bus.addr <= bus.addr + 1'b1;

35  assign bus.wr = 1'b1;

36  assign bus.d = bus.addr;

38  

39  module testintfmem;

46  membus #(64,8) the_bus(clk, rst); // 執行個體化端口

47  mem_tester #(64,8) the_tester(the_bus); // 在執行個體化子產品時使用端口

48  mem #(64,8) the_mem(the_bus); // 在執行個體化子產品時使用端口

49  endmodule

2.15 生成塊

初學者可跳過此節,當覺得在子產品中重複寫類似的有規律的内容比較煩瑣時,再來學習本節。

代碼2-42 8位格雷碼到二進制碼轉換

1  module gray2bin (

2  input wire [7:0] gray,

3  output logic [7:0] bin

5  assign bin[7] = ^gray[7:7];

6  assign bin[6] = ^gray[7:6];

7  assign bin[5] = ^gray[7:5];

8  assign bin[4] = ^gray[7:4];

9  assign bin[3] = ^gray[7:3];

10  assign bin[2] = ^gray[7:2];

11  assign bin[1] = ^gray[7:1];

12  assign bin[0] = ^gray[7:0];

13  endmodule

生成塊可根據一定的規律,使用條件生成語句、循環生成語句等,重複構造生成塊的内容,等效于按照規律重複書寫了生成塊中的内容。考慮代碼2-42。

該代碼描述了一個将8位格雷碼轉換到二進制碼的組合邏輯,可以看到書寫了8行很有規律的持續指派。試想,如果需要像這樣描述64位格雷碼到二進制碼的轉換呢?如果需要參數化位數呢?生成塊可完成類似的需求。

生成常用形式如下:generate

{| | }

endgenerate其中,for生成語句的形式為:{for(genvar<生成變量>=<初始值>; <條件表達式>; <步進語句>) begin [:<塊辨別>]

{<生成内容>}

end這與過程中的for語句形式相似,不過,循環條件所用的變量必須使用genvar關鍵字定義,循環步進和條件必須隻由生成變量決定。

if生成語句和case生成語句的形式與過程中的if語句和case語句相似,不過,所有的條件表達式必須是常量表達式。

生成語句中的生成内容與子產品中能包含的内容基本一緻,生成内容中還可以再嵌套其他生成語句。

如果使用生成塊,代碼2-42可參數化,改寫為代碼2-43。

代碼2-43 參數化位數的格雷碼到二進制碼轉換

1  module gray2bin #(

2  parameter DW = 8

4  input wire [DW - 1 : 0] gray,

5  output logic [DW - 1 : 0] bin

6  );

7  generate

8  for(genvar i = 0; i < DW; i++) begin :binbits

9  assign bin[i] = ^gray[DW - 1 : i];

12  endgenerate

2.16 任務和函數

任務和函數将一些語句實作的一定功能封裝在一起,以便重複使用。任務和函數都隻能在過程塊中調用。

任務定義的一般形式:task [static| automatic] <任務名> (

(input| output| inout| [const] ref) <變量定義1>{,

(input| output| inout| [const] ref) <變量定義i>}

);

{<變量或常量定義>| <語句>}

endtask函數定義的一般形式:function [static| automatic] <資料類型符号位寬> <函數名> (

{<變量或常量定義>| <語句>| <函數名>=<表達式>| return <表達式>}

endfunction其中的static和automatic關鍵字用于指定任務和函數的生命周期,使用automatic關鍵字的任務和函數中的變量均為局部變量,在每次任務或函數調用時均會重新初始化,可被多個同時進行的過程調用,或被遞歸調用,類似于程式設計語言的可重入。FPGA開發工具一般隻支援automatic類型的任務和函數。

任務和函數本身類似于順序塊,因而在順序塊中能使用的語句(過程指派、流程控制等)都能在任務和函數中使用。

任務中可以有時序控制(延時、事件),而函數中不能有。

代碼2-44是任務和函數的例子。

代碼2-44 任務和函數示例

1  module test_task_func;

2  localparam DW = 8;

3  

4  task automatic gen_reset(

5  ref reset, input time start, input time stop

7  #start reset = 1'b1;

8  #(stop - start) reset = 1'b0;

9  endtask

10  logic rst = 1'b0;

12  initial gen_reset(rst, 10ns, 25ns);

13  

14  function automatic [$clog2(DW) - 1 : 0] log2(

15  input [DW - 1 : 0] x

16   );

17  log2 = 0;

18  while(x > 1) begin

19  log2++;

20  x >= 1;

22  endfunction

23  logic [DW - 1 : 0] a = 8'b0;

24  logic [$clog2(DW) - 1 : 0] b;

25  always #10 a++;

26  assign b = log2(a);

27  endmodule

第4行的任務gen_reset用于在reset上産生複位信号,第14行的函數用于求輸入x的底2對數。

2.17 包

包(package)用來封裝一些常用的常量變量定義、資料類型定義、任務和函數定義等,在需要使用時,可使用import關鍵字導入。

包定義的形式是:package <包名>;

<包内容,資料定義、資料類型定義、任務定義、函數定義等>

endpackage引用包時,使用:import <包名>::(<内容名>|);使用内容名(資料、資料類型、任務、函數等)時,隻導入相應内容;使用“”時,将導入包内全部内容。

import語句可以用在子產品内,也可以用在子產品頭部。

代碼2-45是包的例子。

代碼2-45 包的示例

1  package Q15Types;

2  typedef logic signed [15:0] Q15;

3  typedef struct packed { Q15 re, im; } CplxQ15;

4  function CplxQ15 add(CplxQ15 a, CplxQ15 b);

5  add.re = a.re + b.re;

6  add.im = a.im + b.im;

7  endfunction

8  function CplxQ15 mulCplxQ15(CplxQ15 a, CplxQ15 b);

9  mulCplxQ15.re = (32'(a.re)b.re - 32'(a.im)b.im) >>> 15;

10  mulCplxQ15.im = (32'(a.re)b.im + 32'(a.im)b.re) >>> 15;

12  endfunction

13  endpackage

15  module testpackage;

16  import Q15Types::*;

17  CplxQ15 a = '{'0, '0}, b = '{'0, '0};

18  always begin

19  #10 a.re += 16'sd50;

20  a.im += 16'sd100;

21  b.re += 16'sd200;

22  b.im += 16'sd400;

24  CplxQ15 c;

25  always_comb c = mulCplxQ15(a, b);

26  real ar, ai, br, bi, cr, ci, dr, di;

27  always@(c) begin

28  ar = real'(a.re) / 32768;

30  ai = real'(a.im) / 32768;

31  br = real'(b.re) / 32768;

32  bi = real'(b.im) / 32768;

33  cr = real'(c.re) / 32768;

34  ci = real'(c.im) / 32768;

35  dr = ar br - ai bi;

36  di = ar bi + ai br;

37  if(dr < 1.0 && dr > -1.0 && di < 1.0 && di > -1.0) begin

38  if(cr - dr > 1.0/32768.0 || cr - dr < -1.0/32768.0)

39  $display("err:\t", cr, "\t", dr);

40  if(ci - di > 1.0/32768.0 || ci - di < -1.0/32768.0)

41  $display("err:\t", ci, "\t", di);

42  end

43  end

44  endmodule

這個例子在Q15Types包中定義了Q15(Q1.15格式)和CplxQ15資料類型,并定義了CplxQ15的加法和乘法運算。第15行之後的testpackage子產品中,對CplxQ15類型的乘法進行測試。

Verilog規範中還定義了一些标準包,包括信号量、郵箱、随機和程序,可用于複雜測試代碼的編寫,讀者可适當了解。

2.18 系統任務和函數

系統任務和函數是标準中定義的用于在仿真和編譯過程中執行一些特殊功能的任務和函數,全部以“$”符号開頭,有的可帶參數,有的無參數,無參數或可不帶參數的系統任務和函數在調用時可以不帶括号。

大多數系統任務和函數都應該在過程中被調用。系統任務和函數本身都是不能綜合成實際電路的,主要用于仿真測試。但部分任務和函數會幹預綜合過程,影響綜合結果,是可綜合的代碼中有用的内容,比如類型轉換和存儲器相關函數和任務。

标準中定義的系統任務和函數有近兩百個,這裡介紹一些常用的。

1) 顯示相關:$display、$write、$strobe、$monitor。

2) 檔案相關:$fopen、$fclose、$fdisplay、$fwrite、$fstrobe、$fmonitor、$fscanf。

3) 存儲器相關:$readmemh、$readmemb、$writememh、$writememb。

4) 仿真相關:$stop、$finish。

5) 錯誤和資訊:$fatal、$error、$warning、$info。

6) 類型轉換:$itor、$rtoi、$bitstoreal、$realtobits、$bitstoshortreal、$shortrealtobits。

7) 數學:$clog2、$ceil、$floor、$ln、$log10、$exp、$pow、$sqrt、$sin、$cos、$tan、$asin、$acos、$atan、$atan2、$hypot、$sinh、$cosh、$tanh、$asinh、$acosh、$atanh。

其他部分系統任務和函數在後續章節中也會有些許提及,未提及的系統任務和函數讀者可查閱标準适當了解。

2.18.1 顯示相關

顯示相關任務的一般使用形式是:($display| $write| $strobe| $monitor)(<參數1>{, <參數i>});這些任務将把待顯示的内容輸出到仿真環境的終端或仿真工具的控制台視窗。參數可以是字元串(以雙引号包裹)、線網、變量或帶有傳回值的表達式。如果是字元串,還可在字元串中加入格式說明,每個格式說明将按次序對應後面一個參數,并按照一定的格式被後面的對應參數替換。沒有對應格式說明的參數,如果是緊湊類型将按預設格式顯示;如果是非緊湊類型,則隻有位元組數組會按照字元串顯示,其他會被認為不合法。

$display用于即時顯示,并會在最後添加換行。

$write用于即時顯示,但不會添加換行。

$strobe會在一個仿真時間步的最後顯示,即當同時并行執行的所有語句執行完之後,它才會輸出顯示,常用于檢測每個時間步線網或變量的變化結果。

$monitor一經運作,便會在引用到的任何一個變量發生變化時,輸出一次顯示,可用于持續監測線網或變量的變化。

字元串中的格式說明均以“%”開始,常用的格式說明如表2-12所示。

表2-12 格式說明符說明符說明示例

帶你讀《FPGA應用開發和仿真》之二:Verilog HDL和SystemVerilog

對于整數,顯示寬度與該整數位寬下所能表達的最大數值的寬度一緻,如對于16位無符号數,十六進制寬4位、十進制寬5位、八進制寬6位、二進制寬16位。如果數值達不到顯示位寬,則十進制高位填充空格,其他進制填充0。也可指定最小顯示位寬,形式為“%<最小位寬>(h|d|o|b)”,其中的最小位寬為非負整數,如果實際數值窄于這個值則填充空格或0,如果實際數值寬于這個數值則擴充顯示寬度。

二進制顯示時,為z或x的位顯示“z”或“x”。

八進制或十六進制顯示時,如果對應的3位或4位:

  • 全是z或x則顯示“z”或“x”。
  • 不全是但含有x則顯示“X”。
  • 不全是但含有z且不含x則顯示“Z”。

    十進制顯示時,如果所有位:

字元串也可用同樣的形式指定顯示的寬度,窄于指定寬度的左側填充空格,寬于指定寬度的擴充寬度顯示。

對于浮點數,%e、%f、%g可指定左右對齊、顯示寬度和小數位數,形式為“%(+|-)<寬度>.<小數位>(e|f|g)”,其功能與C語言格式輸出的格式說明完全相容,規則較為複雜,在FPGA設計中也很少用到,讀者可參考C語言相關資料,這裡不贅述。

2.18.2 檔案相關

檔案相關任務和函數用于讀寫EDA工具運作的計算機上的檔案,可用于讀取激勵檔案、寫仿真結果到檔案等。

$fopen用于打開檔案,并傳回一個用于後續通路打開的檔案的描述符,形式如下:<多通道描述符> = $fopen(<檔案名>);或:<檔案描述符> = $fopen(<檔案名>, <類型>);其中多通道描述符是32位二進制整數,每一位代表一個通道,第0位預設代表标準輸出(同顯示任務),采用多通道描述符,可以将輸出資訊同時寫入多個檔案(含标準輸出)中,隻需要将多個通道的描述符按位或用作描述符即可。檔案描述符也是32位二進制整數,不過每個數值代表一個檔案,0、1、2預設為标準輸入、标準輸出和标準錯誤。

檔案名和類型都是字元串,檔案名可以帶有相對路徑或絕對路徑,類型則如表2-13所示。

表2-13 打開檔案的類型說明符說明"r"、"rb"隻讀"w"、"wb"隻寫,檔案存在則覆寫,不存在則建立"a"、"ab"追加,檔案存在則從結尾追加内容,檔案不存在則建立"r+"、"r+b"、"rb+"可讀可寫"w+"、"w+b"、"wb+"可讀可寫,檔案存在則覆寫,不存在則建立"a+"、"a+b"、"ab+"可讀可寫,從檔案結尾開始,檔案不存在則建立$close用于關閉檔案,形式如下:$fclose(<多通道描述符>| <檔案描述符>)檔案一旦關閉,便不能再讀寫,已使用$fmonitor和$fstrobe任務發起的讀寫操作将被隐式地取消。

$fdisplay、$fwrite、$fstrobe和$fmonitor幾個任務的使用方式如下:($fdisplay| $fwrite| $fstrobe| $fmonitor)(<多通道或檔案描述符>, <參數1>{, <參數i>});除增加了多通道或檔案描述符作為第一個參數,其他與對應的顯示任務幾乎一樣,當然内容從顯示到終端變成了寫入檔案。

$fscanf用于以一定的格式從檔案中讀入資料,形式如下: = $fscanf(<檔案描述符>, <格式說明>, <變量1>{, <變量i>});其中的格式說明與顯示任務的格式說明類似,可以使用一連串多個格式說明符比對多個變量,從檔案中讀取文本内容并按指定格式轉換後指派給變量。對于整數和字元串,每個說明符比對到空白(空格、制表符、換行等)前的一段内容;對于字元,每個說明符比對一個字元;對于時間,還将四舍五入到系統的時間精度;對于層次路徑,直接傳回目前層次路徑,并不讀檔案。

Integer類型的傳回值表示成功比對并指派的變量個數,如果為-1,表示檔案已結束。

除了上述這些檔案操作任務和函數外,還有二進制讀函數$fread、擷取操作位置的函數$ftell、設定操作位置的函數$fseek和重置操作位置的函數$rewind,這裡不贅述,讀者可參考标準文檔。

2.18.3 存儲器相關

存儲器相關的任務用于從檔案中讀取内容來初始化存儲器或将存儲器的内容儲存到檔案中。所謂存儲器就是整型數組。

$readmemh和$readmemb分别從檔案中讀取十六進制和二進制文本到存儲器中。形式是:($readmemh| $readmemb)(<檔案名>, <數組名>[, <起始位址> [, <終止位址>]]);其中檔案名對應的檔案的内容必須符合以下規則:

  • 隻能包含空白、注釋(行注釋或塊注釋)、“@”字元和常數,常數由z、Z、x、X和對應進制的數字組成。
  • 被空白或注釋分隔的數表達資料,每個資料内容對應數組中的一個元素。
  • “@”字元後緊跟的數表達位址,必須是十六進制,指定下一個資料的位址。
  • 未被指定位址的資料的位址為前一資料位址加1,檔案開頭先出現資料時,該資料位址為0。

典型的可用于$readmemh任務的檔案内容(注意左側為行号,并非檔案内容)如下:

1    @0000  00

2    @0001  5A

3    @0002  7F

4    @0003  5A

5    @0004  00

6    @0005  A6

7    @0006  81

8    @0007  A6

$writememh和$writememb分别将存儲器中的内容以十六進制形式或二進制形式寫入檔案。形式是:($writememh| $writememb)(<檔案名>, <數組名>[, <起始位址> [, <終止位址>]]);寫到檔案裡的内容符合上面的規則,并且一般帶有“@”開頭的位址,除非數組元素為非緊湊類型。

2.18.4 仿真相關

$stop用于暫停仿真,$finish用于結束仿真,它們的使用形式如下:($stop| $finish)[([0| 1| 2])];可帶圓括号或不帶圓括号,可帶參數或不帶參數。帶參數0表示不顯示資訊到終端,帶參數1表示顯示仿真時間和位置到終端,帶參數2表示顯示仿真時間、位置以及仿真占用計算機CPU和存儲器的統計資訊。不帶參數等價于帶參數1。

2.18.5 錯誤和資訊

$fatal、$error、$warning、$info用于在編譯期(準确地說是展述時)或在仿真運作時給出嚴重錯誤、錯誤、警告和資訊。嚴重錯誤将終止展述或仿真;錯誤不會終止展述或仿真,但展述時如出現錯誤将不會啟動仿真;警告和資訊隻給出資訊。它們經常用來做編譯時的常量和參數合法性的報告或運作時變量合法性的報告,也常常用來做測試時功能校驗的報告。

它們都類似顯示任務,可帶有字元串或常量、變量表達式作為參數,并支援字元串中的格式說明符。$fatal則多一個結束号參數。它們的使用形式如下:$fatal[(<結束号>{, <參數>})];

($error| $warning| $info)[([<參數1>{, <參數i>}])];2.18.6 類型轉換和數學函數

1.類型轉換函數

$itor(x),将整型資料x轉換為real型(雙精度浮點)。

$rtoi(x),将real型資料x轉換為integer型。

$bitstoreal(x),将符合IEEE 754規範的64位編碼x轉換為real型。

$realtobits(x),将real型資料x轉換為符合IEEE 754規範的64位編碼。

$bitstoshortreal(x),将符合IEEE 754規範的32位編碼x轉換為shortreal型。

$shortrealtobits(x),将shortreal型資料x轉換為符合IEEE 754規範的32位編碼。

例如:$itor(123)→ 123.0

$rtoi(456.7)→ 457

$bitstoreal(64'h3fd8_0000_0000_0000) → 0.375

$realtobits(0.375) → 64'h3fd8_0000_0000_000

$bitstoshortreal(32'h3ec0_0000) → 0.375

$realtoshortbit(0.375)→ 32'h3ec0_0000$cast、$signed和$unsigned三個函數在2.8節已有介紹,此處不贅述。

2.數學函數

$clog2(x),傳回不小于x的以2為底的對數的最小整數。

$ceil(x),傳回不小于x的最小整數。

$floor(x),傳回不大于x的最大整數。

以上三個傳回值為整數,以下傳回值均為real型。

$ln(x),傳回x的自然對數。

$log10(x),傳回x的常用對數。

$exp(x),傳回e(自然對數的底)的x次幂。

$pow(x, y),傳回x的y次幂,等價于x**y。

$sqrt(x),傳回x的平方根。

$sin(x)、 $cos(x) 、$tan(x),傳回x(弧度)的正弦、餘弦和正切。

$asin(x)、$acos(x)、$atan(x),傳回x的反正弦、反餘弦和反正切(均為弧度)。

$atan2(y, x),傳回複數x+yi的輻角,值域為(-π,π)。

$hypot(x, y),傳回複數x+yi的模。

$sinh(x)、$cosh(x)、$tanh(x),傳回x的雙曲正弦、雙曲餘弦和雙曲正切。

$asinh(x)、$acosh(x)、$atanh(x),傳回x的反雙曲正弦、反雙曲餘弦和反雙曲正切。

2.19 編譯指令

編譯指令用來設定編譯過程的一些屬性、控制編譯流程等,Verilog所有的編譯指令均以沉音符号“`”(ASCII碼0x60)開頭。注意不要将沉音符号與撇點“'”混淆。編譯指令均獨占一行,并不以分号結尾,可帶有注釋。這裡簡單介紹幾個常用的編譯指令。

  • `default_nettype,設定預設線網類型
  • define、

    undef和`undefineall,宏定義。
  • `include,包含檔案。
  • ifdef、

    ifndef、

    elsif、

    else和`endif,條件編譯。
  • `timescale,時間機關和精度設定。
  • `resetall,重置所有編譯指令。

default_nettype用來設定預設的線網類型,形式為:

default_nettype(<線網類型>| none)2.13節提到了子產品端口的預設線網類型為wire,便可以使用這個編譯指令來更改。Verilog有一個比較危險的特性是可以隐式定義線網,即編譯器将把未定義過的辨別符認定為預設類型的線網,因而任何地方一個筆誤,都将形成一個預設類型的線網,這多半是不可預期的,是以建議初學者将線網類型的預設值設定為none:`default_nettype none這樣便杜絕了編譯器将筆誤認定為新線網,當然也使得我們在簡寫子產品的端口定義時,不能省略wire關鍵字。

define、

undef和

undefineall用來定義宏和解除宏定義,宏可以在代碼中使用或用于條件編譯指令中,編譯器直接将宏按定義時的文本展開。它們的使用形式如下:

define<宏名>[(<參數1>{, <參數i>})] <宏内容>

`undef<宏名>

undefineall其中

define還可以帶有參數,宏内容中參數部分會以使用時的參數内容替代,

undefineall用于解除所有已定義的宏。例如:

define PI 3.14159265358979324

`define MAX(a, b) ((a) > (b) ? (a) : (b))

`undefine PI注意其中MAX宏的内容大量使用了括号,這是為了防止處于複雜表達式中的宏展開時優先級錯亂。

使用宏的格式是:`<宏名>。注意宏名前面帶有沉音符。

include用于包含檔案,等同于直接将被包含的檔案的全部内容替換在目前位置,當需要執行個體化位于其他檔案中的子產品、導入位于其他檔案中的包時,往往需要使用該編譯指令。一般形式為:

include (<<檔案路徑和檔案名>>| "<檔案路徑和檔案名>")其中檔案路徑可以是絕對路徑或相對路徑。使用雙引号時,相對路徑以編譯器目前工作目錄(常常是檔案所在目錄)為起點;使用尖括号時,以編譯器和規範設定的目錄為起點。

大多數EDA工具,特别是帶有圖形界面的工具,都以工程的形式管理多個源檔案,在同一個工程中的任何源檔案中均可直接執行個體化定義在其他源檔案中的子產品,并不需要使用`include編譯指令。

ifdef、

elsif、

else和

endif為條件編譯指令,常用形式為:(

ifdef <宏名1>| `ifndef <宏名1>)

<代碼段1>

{`elsif

<代碼段i>}

[`else

<代碼段k>]

endif使用

ifdef時,如果宏名1被定義,則代碼段1将被編譯,否則如果宏名i被定義,代碼段i将被編譯; 如果宏名1至宏名i均未定義,則代碼段k将被編譯。使用`ifndef時,如果宏名1未被定義,則代碼段1将被編譯,否則,如果宏名i被定義,代碼段i将被編譯;如果宏名1被定義而宏名i均未被定義,則代碼段k将被編譯。

timescale用于設定時間機關和精度,在2.12.1節中介紹延遲時,所有的時間都帶有機關,比如“ns”,而如果使用

timescale編譯指令設定了機關和精度,則可省略機關。一般形式是:`timescale(1| 10| 100)[m| u| n| p| f]s / (1| 10| 100)[m| u| n| p| f]s其中m、u(μ)、n、p、f為國際機關制詞頭,“/”左側為時間機關,右側為時間精度,時間精度必須不大于時間機關。定義了時間機關和精度之後,所有不帶機關的時間均會乘以時間機關,所有時間均會被四舍五入到時間精度。例如:

代碼2-46 begin-end塊中延遲的示例

1  `timescale 10ns/1ns

3  #1.55 a = 8'd10;// 實際延遲量為16ns

resetall用于重置除了宏定義以外所有被編譯指令設定的項目到預設狀态,形式為:

resetall。

繼續閱讀