1. 程式人生 > >阻塞賦值與非阻塞賦值的再分析

阻塞賦值與非阻塞賦值的再分析

在Verilog HDL設計中,經常會遇到阻塞賦值與非阻塞賦值,這是學習邏輯設計時最基礎的知識點。設計者經常會在書中看到一些建議:什麼時候該用阻塞賦值,什麼情況下使用非阻塞賦值。可是,如果僅僅按照這樣的設計推薦來進行設計的話,經常會碰到一頭霧水的情況。本文就對阻塞賦值和非阻塞賦值進行詳細的討論,深入分析這兩種賦值語句的區別。

“阻塞”與“非阻塞”疑問的由來,主要體現在always或initial語句中。

在任何書上都會碰到的分析為:阻塞賦值“=”與非阻塞賦值“<=”的區別在於:非阻塞賦值語句右端表示式計算完後並不立即賦值給左端,而是馬上啟動下一條語句繼續執行;而阻塞賦值語句在每個右端表示式計算完後立即賦值給左端變數。

1、語義分析

首先來看阻塞賦值與非阻塞賦值在語義上的含義。

always @ (posedge clk)
begin
	q1  =  d;
	q2  =  q1;
	q3  =  q2;
end
always語句塊對clk的上升沿敏感,只有在clk上升沿到來後,才執行always裡面的語句。根據HDL語法的基礎知識可以知道:begin...end語句塊中的所有語句都是順序執行的。根據上文中“右端表示式計算完後立即賦值給左端變數”。即在該語句塊中,“右式計算”和“左式更新”完全完成後,才開始執行下一條語句。也就是q1 = d執行完成後,當q1的值更新為d的值後,才開始執行q2 = q1,同理,q3最後的值為d.

在所有的語句執行完以後,該always語句等待clk的上升沿到來,從而再次觸發begin...end語句塊。

其次來分析非阻塞賦值。

非阻塞賦值,就是指當前語句的執行互惠阻塞下一語句的執行。下面為非阻塞賦值的語句結構:

sum  <=  A+B

上式中左邊的A+B稱之為右式(RHS)計算事件,SUM稱之為左式(LHS)更新事件。無論左式還是右式,都有可能即是變數,又是表示式。

非阻塞賦值的RHS計算屬於活躍事件,需要優先執行,因此首先執行A+B,然後產生一個更新事件,把計算結果更新到SUM中,將“更新事件”放入事件佇列中,但不會馬上執行,即使這個更新事件屬於當前時間的事件。因為非阻塞賦值的更新事件優先順序較低,需要執行完begin...end中其他當前時間的活躍事件和非活躍事件之後,才會執行當前時間的非阻塞賦值更新事件。

如果在非賦值阻塞的右式中有延時引數,例如:

sum = #5 A+B

那麼A+B完成以後,會將sum的更新事件放入事件佇列中,並在當前模擬時間內5ns後才會執行該事件。

下面來分析非阻塞賦值程式碼的執行過程:

always @ (posedge clk)
begin
	q1  <=  d;
	q2  <=  q1;
	q3  <=  q2;
end
首先會執行第一句中q1 <= d的RHS計算,計算完成後,將更新q1事件放入事件列表,q1的值並不會立即變化,而是維持在原來的值;然後執行第二句中q2 <= q1的RHS計算,同樣計算完成後,將更新q2事件放入事件列表,q2的值也並不變化,q1,q2的值都保持原來各自的值;再執行q3 <= q2,產生一個更新事件,將q2的當前值賦值給q3,這個更新q3事件也放入事件列表中。

這是always語句塊執行完成,並且等待下一個clk上升沿到來。

通過上面的分析可知,現在事件佇列中有三個更新事件需要完成,那麼什麼時候執行這三個等待事件呢?只有噹噹前時間內的所有活躍事件和非活躍事件執行完成之後,才開始執行這些非阻塞賦值的更新事件。這樣就相當於將d,q1,q2的值同時賦值給了q1,q2,q3.

2、綜合後的電路

對於下面的HDL描述:

always @ (posedge clk or negedge rstn)
begin
	if(!rstn)
		A <= 0;
	else
		A <= B;
end

這是一個標準的DFF描述,綜合完成後是一個標準的D觸發器。那麼同理對於上文所描述的非阻塞賦值,有如下的電路圖:



對於阻塞賦值,實現的電路結果如下圖


其中的q1和q2,在邏輯程式碼綜合的時候,就被優化掉了。

那麼,為什麼好多書上會不根據實際的需求,來推薦設計者按照一些固定的建議來設計呢?這主要是為了設計者減少程式碼的錯誤,為初學者在開發時省去很多麻煩。但這並不是很好的設計。因為按照實際電路的要求,在always語句中,有時也需要採用阻塞賦值語句。最常見的是兩段式或者三段式狀態機。還有其他的一些情況,可以通過下面的案例來說明阻塞賦值與非阻塞賦值的區別。

3、案例分析

本例選自《設計與驗證 verilog HDL》

這裡有一個數組:Data[0]、Data[1]、Data[2]和Data[3]。它們都是4bit的資料。我們需要在它們當中找到一個最小的資料,同時將該資料的索引輸出到LidMin中,這個演算法有點類似於“氣泡排序”的過程。而且需要在一個時鐘週期內完成。例如,如果這4個數據中Data[2]最小,那麼LidMin的值為2.

reg  [1:0]           LidMin;
reg  [3:0]           Data [0:3];

always @ (posedge clk or negedge rstn)
begin
	if(!rstn)
		LidMin  <=  0;
	else
	begin
		if(Data[0] <= Data[LidMin])
			LidMin  <= 0;
		if(Data[1] <= Data[LidMin])
			LidMin  <= 1;
		if(Data[2] <= Data[LidMin])
			LidMin  <= 2;
		if(Data[3] <= Data[LidMin])
			LidMin  <= 3;
	end
end

原意是首先將LidMin設定為一個初始值,然後將Data[0] ~ Data[3]與Data[LidMin]進行比較,每比較一個數,就將較小的索引暫存在LidMin中,然後再進行下一次比較。當4組資料比較完成之後,最小的資料所以就會保留在LidMin中。

在上面的程式中使用了非阻塞賦值,結果發現,模擬波形不是所需要的功能,如下圖示,圖中Data[0] ~ Data[3]分別為11、3、10、12,LidMin的初始值為0。LidMin的計算結果應該為1,但模擬波形卻為2.

那麼為什麼會得到這樣的結果呢?

在時鐘上升沿到來後,且rstn訊號無效開始執行以下4個語句,假設只是LidMin是0,Data[0] ~ Data[3]分別為11、3、10、12:

		if(Data[0] <= Data[LidMin])
			LidMin  <= 0;
		if(Data[1] <= Data[LidMin])
			LidMin  <= 1;
		if(Data[2] <= Data[LidMin])
			LidMin  <= 2;
		if(Data[3] <= Data[LidMin])
			LidMin  <= 3;

第一句的if為真,因此執行LidMin <= 0,而這時候,LidMin並沒有立刻被賦值,而是排程到時間佇列中等待執行,這是非阻塞賦值的特點。

第二句的if為真,因此執行LidMin <= 1,這時LidMin也沒有立刻被賦值為1,而是排程到事件佇列中等待執行,當前的LidMin還是0,沒有發生變化。

第三句的if為真,因此執行LidMin <= 2,將更新事件排程到時間佇列中等待執行,當前LidMin還是0.

第四句的if為假,因此跳過LidMin <= 3不執行,這是跳出always語句,等待下一個時鐘上升沿。

在以上的always語句執行完成後,在當前時間下,事件佇列中3個被排程的非阻塞更新事件開始執行,他們分別將LidMin更新為0、1和2.

按照verilog語言的規範,這3個更新事件屬於同一時間內的事件,它們之間的執行順序隨機,這就產生了不確定性。一般在實現的時候是根據它們被排程的先後順序執行的。事件佇列就像一個存放事件的FIFO,它是分層事件佇列的一部分,如圖所示:


這三個更新事件在同一時刻被一一執行,而真正起作用的是最後一個更新事件,因此在防身的時候得到的最終結果是LidMin為2.

然而想要的結果是,在每個if語句判斷並執行完成以後,LidMin先暫存這個中間值,再進行下一次比較,也就是說在進行下一次比較值錢,這個LidMin必須被更新,而這一點也正是阻塞賦值的特點,因此將程式碼做如下更改:

always @ (posedge clk or negedge rstn)
begin
	if(!rstn)
		LidMin  <=  0;
	else
	begin
		if(Data[0] <= Data[LidMin])
			LidMin  = 0;
		if(Data[1] <= Data[LidMin])
			LidMin  = 1;
		if(Data[2] <= Data[LidMin])
			LidMin  = 2;
		if(Data[3] <= Data[LidMin])
			LidMin  = 3;
	end
end

模擬結果如下圖


在程式碼執行過程中,第二句的if為真,執行LidMin = 1,根據阻塞賦值的特點LidMin被立刻賦值為1.在執行第三句if的時候,if(Data[2] <= Data[LidMin])為假,直接跳過LidMin = 2不執行,同樣也跳過LidMin = 3不執行。LidMin最終賦值為1。這正是想要的結果。

另外,為了使程式碼看起來更加簡潔,使用for語句修改:

always @( posedge clk or negedge rstn)
begin
	integer i;
	if (!rst n )
		LidMin <= 0
	else 
	begin
		for (i = 0;i <= 3; i = i + 1 ) 
		begin
			if ( Data[i] <= Data[LidMin] )
				Li dMin = i;
		end
	end
end