http://kellen.wang/zh/blog/2015/03/03/what-is-good-verilog-coding-style/
1. 前言
前段時間在公司負責制定代碼規範,費了九牛二虎之力,終於整理出來一份文檔。由於保密規定的緣故,無法與大家直接分享這份文檔,但是文檔中的大部分規範都是我自己長期總結出來的,在這裏也與大家分享一下。
2. 代碼示範
爲求直觀,首先貼上一份示範代碼,然後我再進行逐條詳細解釋。
以下代碼是我之前做的一個同步FIFO模塊,代碼如下:
013 | parameter DEPTH = 32 , |
014 | parameter DATA_W = 32 |
019 | input wire [DATA_W- 1 : 0 ] wdata , |
020 | output wire full_flg , |
022 | output wire [DATA_W- 1 : 0 ] rdata , |
023 | output wire empty_flg |
025 | `ifdef DUMMY_SYNC_FIFO |
026 | assign full_flg = 1'd0 ; |
028 | assign empty_flg = 1'd0 ; |
030 | `include "get_width.inc" |
034 | localparam DLY = 1'd1 ; |
035 | localparam FULL = 1'd1 ; |
036 | localparam NOT_FULL = 1'd0 ; |
037 | localparam EMPTY = 1'd1 ; |
038 | localparam NOT_EMPTY = 1'd0 ; |
039 | localparam ADDR_W = get_width(DEPTH- 1 ); |
043 | reg [ADDR_W- 1 : 0 ] waddr; |
044 | reg [ADDR_W- 1 : 0 ] raddr; |
045 | wire [ADDR_W- 1 : 0 ] waddr_nxt; |
046 | wire [ADDR_W- 1 : 0 ] raddr_nxt; |
050 | assign waddr_nxt = waddr + 1 ; |
051 | assign raddr_nxt = raddr + 1 ; |
052 | assign full_flg = (waddr_nxt == raddr)? FULL : NOT_FULL; |
053 | assign empty_flg = (waddr == raddr)? EMPTY : NOT_EMPTY; |
054 | assign iwreq = wreq & ~full_flg; |
057 | always @( posedge clk or negedge rst_n) begin |
061 | else if (wreq & (full_flg == NOT_FULL)) begin |
062 | waddr <= #DLY waddr_nxt; |
066 | always @( posedge clk or negedge rst_n) begin |
070 | else if (rreq & (empty_flg == NOT_EMPTY)) begin |
071 | raddr <= #DLY raddr_nxt; |
077 | iError_fifo_write_overflow: |
078 | assert property (@( posedge wclk) disable iff (!rst_n) (iwreq & !full_flg)); |
079 | iError_fifo_read_overflow: |
080 | assert property (@( posedge rclk) disable iff (!rst_n) (irreq & !empty_flg)); |
101 | `endif // `ifdef DUMMY_SYNC_FIFO |
由於博客剛剛開通,代碼高亮似乎還調得不是很好,大家先將就着看好了。下面詳細講解一下我在進行這個模塊設計的時候遵循了哪些希望向大家推薦的代碼風格。
3. 代碼風格
3.1 規則總覽
在設計這個模塊的時候,我主要遵從了以下幾條規則:
- Verilog2001標準的端口定義
- DUMMY模塊
- 邏輯型信號用參數賦值
- 內嵌斷言
- memory shell
3.2 規則解釋
接下來我們逐一解釋以下爲什麼要這麼做。
3.2.1 Verilog2001標準的端口定義
08 | input wire [DATA_W- 1 : 0 ] wdata , |
09 | output wire full_flg , |
11 | output wire [DATA_W- 1 : 0 ] rdata , |
相對於verilog1995的端口定義,這種定義方式將端口方向,reg或wire類型,端口位寬等信息都整合到了一起,減少了不必要的重複打字和出錯機率,也使得代碼長度大大縮短,非常緊湊。另外,用於控制模塊編譯的例化參數都被放置於端口定義之前,有利於在模塊例化時進行配置,也是IP化模塊最好的編寫方式。例如在這個同步fifo設計中,我希望這個模塊的深度和數據位寬是可以配置的,那麼我就把這2個參數放在端口聲明的前面。另外要說明的一點是,一旦在模塊中出現了可以配置的例化參數,最好在文件頭的描述部分增加有關這些參數有效值範圍的說明。
3.2.2 DUMMY模塊
在做項目的時候,一個大的系統會被分割成很多細小的部分,由不同的人負責,設計完成後上傳到具有版本管理功能的服務器上。有時候有的人忘記在上傳代碼之前進行嚴格測試,或者根本傳錯了版本,就會造成其他人仿真報錯。有時候我們希望用FPGA進行原型驗證,但是有的模塊設計根本還沒有完成,而反覆修改FPGA頂層文件又會顯著提高版本出錯的機率,最好的辦法就是將這些有問題的模塊臨時替換成dummy模塊。dummy模塊不僅可以隔離問題模塊,還可以顯著加速仿真過程,可謂一舉兩得。傳統上大家在完成設計之後會另外建立一個只有接口代碼的空文件,例如dummy_sync_fifo.v,當需要將sync_fifo變成dummy的時候,就將文件清單中的文件名改掉,但這樣的方式會增加文件,容易造成管理的混亂,反覆修改文件清單顯然也不是一個好的做法。我推薦的dummy方式如下所示:
4 | assign empty_flg = 1'd0 ; |
7 | `endif // `ifdef DUMMY_SYNC_FIFO |
這裏推薦的方式是在模塊的頂層文件中寫一個宏控制的綜合控制邏輯,當DUMMY_SYNC_FIFO宏被定義的時候,綜合工具就只會將整個模塊綜合成沒有任何邏輯的dummy模塊了。
3.2.3 邏輯型信號用參數賦值
很多人做RTL設計的時候爲了省事,在代碼中對數值型信號和邏輯型信號完全不做區分,用同樣的方式賦值。如果這種時候稍微做一點點改變,就能讓你的代碼可讀性大大提高,例如:
1 | assign full_flg = (waddr_nxt == raddr); |
和
2 | localparam NOT_FULL = 1'd0 ; |
3 | assign full_flg = (waddr_nxt == raddr) ? FULL : NOT_FULL; |
你覺得哪一個閱讀起來更直觀?而將所有邏輯型信號的數值參數化的另外一個好處,就是在如veridi這樣業界良心的仿真軟件中,你可以在仿真波形中直接看到FULL或NOT_FULL這樣的文字參數,大大提高了波形的友好程度,比起你在那痛苦地目測這根線到底是高電平還是低電平輕鬆多了。
3.2.4 內嵌斷言
有的IC設計工程師覺得斷言是驗證工程師才需要學習的東西,其實不然,好的模塊內嵌斷言可以及時發現模塊內部的錯誤狀態,防止模塊的不當使用,極大地提高模塊的驗證效率。但是,斷言屬於不可綜合的語句(在ZEBU這種變態系統中使用除外),直接放在模塊設計代碼中需要進行必要的特殊處理,如下所示:
3 | iError_fifo_write_overflow: |
4 | assert property (@( posedge wclk) disable iff (!rst_n) (iwreq & !full_flg)); |
5 | iError_fifo_read_overflow: |
6 | assert property (@( posedge rclk) disable iff (!rst_n) (irreq & !empty_flg)); |
首先使用了綜合指令的註釋synopsys translate_off以防綜合工具對這段語句進行綜合,然後再加上一個DEBUG_ON的宏進行二次保護。上例中的斷言可以保證這個sync_fifo在使用過程中一旦發生“過讀”或者“過寫”就會立刻打印報錯信息。
3.2.5 memory shell
在IC設計中經常需要用到memory,memory通常不是用verilog描述實現的(這種方式實現不是不可以,而是性價比太低了),而是需要調用FPGA裏的存儲資源,或是由後端生成。但是在進行仿真的時候,我們不妨用verilog寫一個行爲模型來替代實現。這種原型驗證和仿真驗證的不一致,導致了跟dummy模塊設計一樣的麻煩,那就是需要對代碼進行反覆修改。另外,在不同項目中有可能根據不同的情況採用不同的後端物理層來生成memory,或者由於不同的工藝生成不同的memory,這種memory的接口協議可能多少會有一些不一樣,同樣會導致需要在不同工藝和項目中修改IP代碼,造成出錯的風險。比較好的做法就是像以下例子中那樣使用一個memory shell來隔離這種修改。
這個memory shell定義了一組標準的接口,用於在IP模塊中進行例化。而在這個memory shell模塊內部,可使用宏控制的綜合分支控制語句根據不同情況綜合不同的memory或仿真模型。當同一個size的memory被多個模塊調用的時候,這種設計的好處更加明顯,因爲當接口協議變化時,你只需要改動memory shell文件內部的連接邏輯就可以了,這個shell在不同模塊中的例化語句都是不需要改動的。
4. 總結
良好的代碼風格可以提高代碼的可讀性,減少犯錯機會,也可以提高代碼調試的效率,但積累良好的代碼風格不是一朝一夕的事,需要一步一個腳印,一點點積累。本文長期更新,如果你有好的想法和建議,歡迎在本文底部留言。另外也歡迎其他verilog語言學習者與我共同交流,有任何疑問可以到本博“答疑專區”提出,我必知無不言,言無不盡。