延遲行為
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。
圖4 示例過程的事件隊列響應過程和波形圖
需要說明的是,雖然本文提供了一種思路能夠比較輕松地理解行為級描述的執行過程,但是仍然不建議大家在過程塊中混用阻塞賦值和非阻塞賦值。混用賦值語句是危險的。
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)。
圖5 可調試程序中添加的調試信息(利用objdump命令得到)
Verilog調試器的作用就是將可執行仿真程序和Verilog語言對應起來。一種思路是將編譯器插入的調試信息與Verilog語言對應起來。編譯器插入的調試信息是對應于可執行的仿真源文件,而這些源文件是由仿真器生成的。所以Verilog調試器可以獲得調試信息與Verilog語句的對應關系。另一種思路是通過編譯器直接給可執行仿真程序添加與Verilog語言對應的調試信息。這樣Verilog調試器從可執行程序就可以獲得必要的信息,而不需要額外的消息來源。
Verilog調試器只能對可以與Verilog語言對應的代碼部分添加斷點,也就是只能對事件響應函數添加斷點。Verilog調試器不能調試由仿真器提供的仿真框架源代碼。當單步調試遇到always塊結束或者assign語句之后,調試器不會進入仿真引擎,而是直接跳轉到下一個事件響應函數。此外,Verilog調試器還限制了斷點和單步調試的粒度只能以Verilog語句為單位,而不能進一步縮小粒度到可執行仿真程序的語句甚至匯編層面。
圖6 Verilog程序加斷點的過程
以圖6中的Verilog程序為例,經過Verilog仿真器得到右邊所示的仿真源文件,再經過編譯得到可執行程序。在左邊Verilog程序中的一行設置了一個斷點(圖5中左邊第7行),這個斷點實際上是設置在右邊的可執行程序的事件響應函數always1_4中的(圖5中右邊第2行)。每當仿真程序運行到always1_4時就觸發中斷,暫停程序執行。通過仿真器添加的調試提示信息,調試器能夠知道中斷的位置是Verilog語言的第7行,從而在圖形界面上顯示。
從斷點的位置開始單步調試。從可執行仿真程序的層面來說應該在第3行暫停,并且呈現第3行執行后狀態。但是,Verilog調試器會過濾仿真框架的程序,也就是過濾掉無法對應到Verilog程序的語句。所以,可執行程序不會在第3行之后暫停,而是繼續執行。第3行結束后,程序從事件響應函數返回,進入仿真框架的部分。仿真器框架的代碼也會被調試器忽略,直到仿真程序進入下一個事件響應函數。最終,程序進入always1_5。調試器會在第6行暫停,并且將中斷的位置對應到Verilog軟件的第8行。程序運行的標志會顯示在圖6中第8行的位置。
以上的過程對于用戶來說都是不可見的。從用戶的角度看來,只能看到程序指針從第7行跳到第8行,并且第7行語句的效果在波形圖上展現了出來。這就是Verilog語言調試背后隱藏的過程,其核心仍然是軟件調試。
結語
本文的初衷是提供通過仿真器理解Verilog語言的思路。文中關于Verilog仿真器的描述采用了最簡單、最直接的思路,當然也是效率最低的。實際的仿真器會通過各種軟件技巧進行優化,提高仿真效率。文中使用的一些概念借鑒自SystemC,比如仿真階段和“評估-更新”機制。電路仿真器的設計思路和概念都是類似的或者相通的,可以觸類旁通。
如果有讀者想進一步理解Verilog仿真器,不妨看一下開源Verilog仿真器iVerilog的源碼。此外,SystemC也是一套很好的硬件電路仿真框架,建議學習SystemC標準。IEEE的SystemC標準會闡述SystemC需要的仿真引擎以及編程規范。
作者才疏學淺,掛一漏萬,請大家多批評指正。
-
仿真器
+關注
關注
14文章
1018瀏覽量
83744 -
軟件
+關注
關注
69文章
4944瀏覽量
87492 -
Verilog
+關注
關注
28文章
1351瀏覽量
110100
發布評論請先 登錄
相關推薦
評論