?
?
要想深入理解Verilog就必須正視Verilog語言同時具備硬件特性和軟件特性。在當下的教學過程中,教師和教材都過于強調Verilog語言的硬件特性和可綜合特性。將Verilog語言的行為級語法只作為語法設定來介紹,忽略了Verilog語言的軟件特性和仿真特性。使得初學者無法理解Verilog語言在行為級語法(過程塊、賦值和延遲)背后隱藏的設計思想。本文嘗試從仿真器的角度對Verilog語言的語法規則進行一番解讀。
“精分”的Verilog語言 ?在集成電路的設計流程中,Verilog源文件有兩個主要作用:綜合和仿真。在圖1中,數字①②③④標注的位置都可以使用Verilog作為設計的描述方法。- 綜合工具讀入源文件,通過綜合算法將設計轉化為網表,比如DC。能夠綜合的特性要求Verilog語言能夠描述信號的各種狀態(0,1,x,z)、信號和模塊的連接(例化)以及模塊的邏輯(賦值以及各種運算符)。
- 仿真器讀入源文件,生成一個可執行程序用于仿真硬件的行為,比如VCS。能夠仿真的特性要求Verilog語言又具有軟件特性,對每一條語句的執行語義和順序給出定義(延遲語句)。同時,軟件特性使得Verilog語言更加靈活,具備了豐富的行為級仿真能力(條件分支、循環等)。

?
仿真器基本架構?
Verilog語言確實不是一種可執行語言。圖2展示了利用Verilog源文件進行仿真的過程。絕大多數仿真器都遵循這一思路,比如VCS、iVerilog、ModelSim、Vivado和Quartus等。首先,準備Verilog源文件以及一些Verilog庫文件(標準單元等)。仿真器接收這些Verilog文件并將其轉化為可執行的仿真源文件(C/C++等)。在這一過程中,仿真器解析Verilog文件的語法結構,并且根據Verilog語法的規范,將語法結構轉化為仿真器中的事件響應函數或代碼段。這些函數和代碼段與仿真器框架源文件一起成為可執行仿真程序的源文件。接下類這些源文件經過編譯得到可執行的仿真程序。VCS和iVerilog可以看到生成的可執行文件。ModelSim、Vivado和Quartus使用GUI管理設計流程,從而將這個可執行文件屏蔽了,使其對于用戶可透明。用戶可以在工程中找到生成的可執行文件。最后,運行可執行的仿真程序,進行軟件仿真。?

?
可執行仿真源文件和仿真器框架源文件一般是不可見的。不過在開源軟件(例如iVerilog)中可以找到生成可執行仿真源文件的代碼。仿真程序通常采用基于事件的仿真架構。這種仿真架構的核心是事件隊列。事件隊列中按照事件的響應時間排列著一系列的事件。響應時間相同的事件之間不應該有決定性的事件依賴關系。如果需要確定這些事件之間的順序,可以引入Δ時間。響應時間為t+Δ的事件必然晚于響應時間為t的事件。但是從仿真時間上,仍然表現為在相同時刻響應。事件隊列按照時間先后順序逐個響應事件隊列中的事件。每一個事件,除了標注事件響應時間,還會標注事件類型以及其他需要的參數。通過事件類型,仿真引擎可以找到對應的響應函數。其他的參數則作為事件響應函數的輸入參數。事件響應函數會產生新的事件。這些新的事件還會插入到事件隊列中,并且按照其響應時間排序。
always @ (posedge clk) begin
q <= d;
end
經過仿真器的轉換就變成為如下的響應函數:
function always_block1 :
q = d;
這個響應函數會與clk信號的上升沿事件(positive)進行綁定。當響應clk信號的上升沿事件的時候,仿真器會調用always_block1這個函數。
?一個條件可以被綁定多個事件響應函數。比如時鐘信號的事件可以與所有的always塊的事件響應函數綁定。當時鐘信號的事件發生的時候,與其綁定的事件響應函數會逐個被調用。如果一個信號在多個always過程塊中都被賦值,那么一個變量會被多個事件響應函數修改。在硬件上,這些響應函數之間應該是并發的,沒有先后關系。但是,串行執行函數的軟件是做不到的這樣的并發的。在仿真器中,always過程塊之間也是有順序的。Verilog規定,always塊之間的執行順序是按照always塊在Verilog文件中的先后順序。這僅僅是為了適應軟件仿真器所引入的設定。
?如果敏感列表中有多個條件,表示always塊與這些信號都綁定。如果always塊沒有執行敏感列表或者是給出一個星號(*),表示always塊應該與過程塊中所有的右值變量綁定。在這種情況下,由每個事件都直接觸發事件響應函數可能會引起重復響應,即在某個時刻事件響應函數被多次觸發的情況。為了避免這樣的錯誤,仿真器中引入了仿真階段的概念。同一個仿真階段中響應的事件,響應時間必須,而且Δ時間也必須相同。在同一個仿真階段中,每個事件響應信號只能被觸發一次。每個仿真階段中,首先在事件隊列中找到需要響應事件,然后累計需要調用的事件響應函數。最后再依次調用這些事件響應函數。這樣就保證了同一個時間的信號變化只會觸發同一個always過程塊一次。
?除了always過程塊,在Verilog中還定義了其他的過程塊。與always過程塊不同,這些過程塊不由信號的事件觸發,而是要單獨在事件隊列上插入事件,并且與過程塊轉化成的響應函數綁定。initial過程塊只在仿真開始的時候執行一次。也就是說,如果定義initial過程塊,那么事件隊列上的第一個事件就是initial過程塊的事件。repeat過程塊和forever過程塊在事件響應函數結束時向電路中添加觸發下一次響應函數的事件。這個事件在下一個Δ時刻就會響應,由此往復。當重復了足夠多次數后,repeat過程塊會停止向事件隊列中添加事件,從而結束repeat語句。forever過程塊的循環不會結束。?
賦值語句?
Verilog語言提供了阻塞賦值和非阻塞賦值兩種賦值語句。 ?
a = b; // 阻塞賦值
a <= b; // 非阻塞賦值
按照語法定義,阻塞賦值會阻塞之后語句的執行;非阻塞賦值則不會阻塞之后語句的執行。阻塞語句達成的效果是下一條語句執行之前信號a已經變修改。非阻塞賦值達成的效果是,信號a的值只有到整個過程塊執行完,才會被修改。需要注意的是,非阻塞賦值雖然被延后,但是所賦的值仍是之前得到的值。這一段話著實令人感到疑惑。現在我們從軟件仿真器的角度重新來解析賦值語句。賦值語句其實包含兩個過程:評估和更新。評估過程確定了需要賦給信號的值,而更新過程才真正的修改了信號的值。評估過程和更新過程是相互獨立的。這兩個過程中的關聯只有需要賦的值。阻塞賦值的評估過程和更新過程是連續執行的,評估之后立即更新。所以,在執行下一條語句的時候,信號已經被修改了。在轉換成仿真器代碼時,阻塞賦值不需要特殊處理。例如
always @(a, b, c) begin : add_mux1
t = a + b;
d = t * c;
end
上述代碼轉化后的事件響應函數為
function add_mux1 :
t = a + b;
d = t * c;
非阻塞賦值的評估過程和更新過程是分開的。過程塊中執行到賦值語句的時候,只進行了評估過程,確定需要賦給信號的值,然后繼續向后執行。更新過程被延后到整個過程塊執行之后。例如
always @(a, b, c) begin : add_mux2
t <= a + b;
d <= t * c;
end
上述代碼轉化后的事件響應函數為
function add_mux2 :
t_update = a + b;
d_update = t * c;
t = t_update;
d = d_update;
當阻塞賦值和非阻塞賦值混合的時候,也遵循同樣的規則。例如
always @(a, b, c) begin : add_mux3
t <= a + b;
d = t * c;
end
上述代碼轉化后的事件響應函數為
function add_mux3 :
t_update = a + b;
d = t * c;
t = t_update;
對信號的賦值會產生一個事件,事件表示被賦值的信號發生了變化。如果有其他的過程塊依賴于被賦值的信號,那么這個事件會被添加到事件隊列中;反之,這個事件會被忽略。事件的響應時間為當前時間加Δ。賦值語句是仿真引擎能夠持續運行的關鍵。大部分always塊都是通過賦值語句向事件隊列添加新的事件的。?
延遲行為Verilog語言的延遲語句雖然不能綜合,但是在仿真過程中應用得很多。延遲語句可以用在testbench中構建時鐘信號和激勵,也可以用在Verilog模塊中模擬實際電路的延遲。延遲語句可以出現在兩條賦值語句之間,也可以出現一條賦值語句中間。
#3 a = b; //延遲語句在賦值語句之間
a = #3 b; //延遲語句在賦值語句內部
在賦值語句之間的延遲語句可以延遲語句的執行。對于仿真器來說,處于賦值語句之間的延遲語句有兩個作用。首先,延遲語句會暫停當前always塊的執行,結束當前的仿真階段,更新之前沒有完成的賦值,完成當前事件的響應并將控制前交還給事件隊列。然后,延遲語句會在事件隊列中添加一個新的事件。這個事件表示在延遲語句指定的時刻開始執行這函數剩下的部分。例如
always @(a, b, c) begin : add_mux4
t <= a + b;
#1 d = t * c;
end
上述代碼轉化后的事件響應函數為
function add_mux4_1 :
t_update = a + b;
t = t_update;
addEvent( curr_time + 1, add_mux4_2 );
function add_mux4_2 :
d = t * c;
Verilog文件中的1個過程塊被轉換為兩個函數。第一個函數add_mux4_1
對應于延遲語句之前的部分,第二個函數add_mux4_2
對應于延遲語句之后的部分。
add_event
是本文定義的一個原語,表示向事件隊列中添加一個事件。第一個參數表示事件響應的時間,第二個參數表示響應事件需要調用的事件響應函數。從第三個參數開始,之后的參數會作為事件響應函數的參數,傳遞給事件響應函數。
add_event( curr_time + 1,?
add_mux4_2?)
表示在當前時間(curr_time
)后1個時間單位的時候響應這個事件。事件需要調用add_mux4_2
函數。響應函數不需要額外的參數。在調用add_mux4_2
時,信號t已經完成更新。
在賦值語句中間的延遲語句將評估和更新階段分割到兩個時刻進行。評估過程仍然在語句執行的時候進行,但是更新過程延后到延遲語句指定的時刻進行。延遲語句是否阻塞過程塊的執行,取決于賦值語句本身。如果是阻塞賦值語句,賦值語句中間的延遲語句會阻塞過程塊的執行;如果是非阻塞賦值,延遲語句不會阻塞過程塊的執行。例如
always @(a, b, c) begin : add_mux5
t <= #1 a + b;
d = #2 t * c;
end
上述代碼轉化后的事件響應函數為
function add_mux5_1:
t_update = a + b; // 1
d_update = t * c; // 2
addEvent( curr_time + 1, update_t, t_update );
addEvent( curr_time + 2, add_mux5_2, d_update );
function update_t( t_update ) :
t = t_update; // 3
function add_mux5_2( d_update ) :
d = d_update; // 4
如果沒有延遲語句,事件響應函數的執行順序應該是 1->2->4->3。由于第一個語句中的延遲語句,語句3需要在當前時刻之后1個時間單位時執行,即update_t
。t_update
作為事件響應函數的參數,在update_t
中更新給信號t。由于第二個語句中的延遲語句,過程塊被打斷為兩個部分,第二個函數需要在當前時刻之后2個時間單位時執行,即add_mux5_2
。add_mux5_2
需要使用d_update
作為參數。理解到這一層,就可以處理更加復雜的波形了。例如下面這一段代碼。
module test;
reg x,y,z;
assign #25 a = 1;
always begin
#20;
x = #10 a;
#3 y = a;
#3 z = a;
#7;
end
endmodule
經過仿真器的轉換,上面的Verilog語句會形成如下的事件響應函數。
function assign1 :
a = 1;
function always1_1 :
addEvent( curr_time + 20, always1_2 );
function always1_2 :
x_update = a;
addEvent( curr_time + 10, always1_3, x_update );
function always1_3( x_update ) :
x = x_update;
addEvent( curr_time + 3, always1_4 );
function always1_4 :
y = a;
addEvent( curr_time + 3, always1_5 );
function always1_5 :
z = a;
addEvent( curr_time + 7, always1_6 );
function always1_6 :
addEvent( curr_time + delta, always1_1 );
在仿真開始時候,首先向事件隊列中添加兩個事件,分別是在0+25時刻調用assign1
,以及在0+0時刻調用always1_1
。事件響應過程如圖4所示。always過程塊被延遲語句分割成了6個響應函數。每個部分都向事件隊列添加能夠觸發下一個響應函數的事件。信號x
的第1次評估發生在20時刻,而第1次更新發生在30時刻,所以信號x的第一次賦值仍為X
。直到第2次評估時(63時刻)才能獲得有效的信號1
,并且在73時刻更新給信號x。
?

需要說明的是,雖然本文提供了一種思路能夠比較輕松地理解行為級描述的執行過程,但是仍然不建議大家在過程塊中混用阻塞賦值和非阻塞賦值。混用賦值語句是危險的。
?
Assign賦值前面介紹的側重于過程塊。對于Assign賦值語句,原理其實是也一樣的。例如
assign a = #5 b & c;
這條assign語句同樣可以看做一個事件響應函數。這個函數綁定的事件是信號b
或信號c
發生變化。延遲語句的效果也是一樣的。延遲語句將評估和更新過程分開。當信號b
或信號c
發生變化時進行評估,并在事件隊列中添加一個新的更新事件。5個時間單位之后,響應更新事件,將評估的值更新給信號a
。
轉換后的事件響應函數如下。
function assign1:
a_update = b & c;
addEvent( curr_time + 5, update_a, a_update );
function update_a( a_update ) :
a = a_update;
?
?
調試?
Verilog仿真器普遍提供了Verilog代碼的調試能力,比如斷點和單步運行。在VCS、ModelSim、Vivado和Quartus中都能找到調試模式。斷點和單步運行是典型的軟件調試手段,是軟件工程師的看家本領。但是對于硬件來說,斷點和單步運行卻是不可理解的,因為硬件是并行的。如果將斷點理解為硬件電路在某一個時刻的狀態,那么此時應該有多條語句被同時中斷。硬件電路不會像軟件一樣在某個函數中中斷并且單步執行,而且其他過程塊或語句毫無影響。 ?前面已經介紹過,Verilog并不是可執行語言。真正的可執行仿真程序是由仿真器提供的仿真框架源代碼和由Verilog語言轉換而來的仿真程序源代碼構成的。實際上,Verilog語言調試的斷點并不是添加給硬件的或者Verilog源文件的,而是添加到可執行仿真程序中對應的事件響應函數的。單步調試的對象也是可執行仿真程序中的事件響應函數。所以,Verilog代碼可以引入斷點和單步調試。 ?在進一步解釋Verilog調試器的機制之前,必須先解釋一下軟件調試器是如何調試程序的。為了使得可執行文件可調試,編譯器會在可執行程序中添加調試信息。以C語言為例,編譯器會在可執行文件中添加調試信息(如圖5所示)。添加的位置是在對應于C語言語句的匯編代碼段起始的位置。在進行軟件調試的時候,軟件調試器會在有調試信息的地方暫停(比如0x4005a5)。進行單步調試的時候,每一步也都是停在C語言語句開始的地方(比如0x4005bf)。 ?

?
結語?
本文的初衷是提供通過仿真器理解Verilog語言的思路。文中關于Verilog仿真器的描述采用了最簡單、最直接的思路,當然也是效率最低的。實際的仿真器會通過各種軟件技巧進行優化,提高仿真效率。文中使用的一些概念借鑒自SystemC,比如仿真階段和“評估-更新”機制。電路仿真器的設計思路和概念都是類似的或者相通的,可以觸類旁通。 ?如果有讀者想進一步理解Verilog仿真器,不妨看一下開源Verilog仿真器iVerilog的源碼。此外,SystemC也是一套很好的硬件電路仿真框架,建議學習SystemC標準。IEEE的SystemC標準會闡述SystemC需要的仿真引擎以及編程規范。 ?作者才疏學淺,掛一漏萬,請大家多批評指正。 ? ?作者簡介
?
作者:王君實?
電子科技大學博士。主要研究方向為片上互聯結構、計算機體系結構,已在IEEE Transactions on Computers (CCF A 類期刊)等高水平期刊和CODES+ISSS、ISCAS等頂級會議上發表高水平論文10 余篇,申請專利4項。長期從事片上系統建模和模型開發工作。博士期間主導開發了多核片上系統高層次建模與設計工具ESYSim。該系列的工具獲得中國研究生電子設計大賽集成電路專項賽(也就是現在的中國研究生“創芯”大賽的前身)特等獎。現從事CPU性能建模和優化工作。?
審核編輯 :李倩
?
評論