通過幾段程式碼理解Verilog裡面阻塞賦值和非阻塞賦值的區別,以及Verilog的for迴圈的使用

巨大八爪魚發表於2020-11-13

弄清楚阻塞賦值和非阻塞賦值的區別非常重要,否則我們就沒有辦法理解verilog裡面的for迴圈的執行結果。
簡單來說,阻塞賦值是給變數的現態賦值,非阻塞賦值是給變數的次態賦值。
所謂的現態,就是執行程式碼時變數的狀態,也就是當前狀態。次態,就是當前整個always程式碼塊執行完了之後,變數是什麼值,也就是下一個狀態。

注意:在同一分支下對同一變數不能同時使用非阻塞賦值和阻塞賦值,否則編譯不通過。
例如,下面的程式碼無法編譯通過:

reg [13:0] a = 0;
reg [3:0] state = 0;
always @(posedge refresh) begin // 這個always塊每秒執行一次
	if (state == 0) begin
		a = 14'd10; // 指定a的現態為10
		a <= 14'd20; // 指定a的次態為20
		number <= a; // 數碼管顯示a的現態
	end
end

錯誤提示:Error (10110): Verilog HDL error at main.v(22): variable "a" has mixed blocking and nonblocking Procedural Assignments -- must be all blocking or all nonblocking assignments
不能混合使用兩種賦值方式。

但是,下面的程式碼就可以編譯通過,因為對a的兩種賦值放在了不同的分支:

reg [13:0] a = 0;
reg [3:0] state = 0;
always @(posedge refresh) begin // 這個always塊每秒執行一次
	if (state == 0) begin
		a = 14'd10; // 指定a的現態為10
		number <= a; // 數碼管顯示a的現態
		state <= 1;
	end
	else if (state == 1) begin
		a <= 14'd20; // 指定a的次態為20
		number <= a; // 數碼管顯示a的現態
		state <= 0;
	end
end

程式執行結果是:程式執行一秒後,數碼管顯示number的值就一直是10。
這是因為,當state==0時,a的現態被修改為10,然後number賦值的是a的現態,所以第一秒末number被改為了10。
當state==1時,指定了a的次態為20,然而a的現態是10,所以number賦值的是10,而不是20。程式碼執行完畢後(也就是refresh的上升沿結束後),a的值才變為20,這個時候number已經賦值完畢了,肯定就顯示不出來20了。

【測試程式碼的框架】

網上很多程式碼都有復位功能,而下面這段程式碼偏偏就沒有定義復位輸入引腳。那麼,模組定不定義復位引腳,有什麼區別呢?
模組沒有做復位功能的話,編譯出來的電路就沒有辦法自己復位。必須把FPGA的重配置引腳nCONFIG拉低再拉高,重新花時間從外部Flash載入程式,才能重頭開始執行程式。具體花多少時間就很難說了。
如果模組做了復位功能的話,就能利用這個復位引腳直接復位,復位時間可以很短,因為FPGA不需要重新載入程式了。

`define SYSCLK 50000000 // 晶振大小為50MHz

module main(
    input clock, // 晶振提供的時鐘
    output [3:0] digits, // 數碼管位選
    output [7:0] segments // 數碼管段選
);
    
    reg [13:0] number = 0; // 數碼管顯示的數字
    SegmentDisplay segdisp(clock, digits, segments, number);
    
    // 每秒refresh產生一次上升沿
    integer counter = 0;
    wire refresh = (counter == `SYSCLK - 1);
    always @(posedge clock) begin
        if (refresh)
            counter <= 0;
        else
            counter <= counter + 1;
    end
    
    reg [13:0] a = 0;
    reg [3:0] state = 0;
    always @(posedge refresh) begin // 這個always塊每秒執行一次
        if (state == 0) begin
            a = 14'd10; // 指定a的現態為10
            number <= a; // 數碼管顯示a的現態
            state <= 1;
        end
        else if (state == 1) begin
            a <= 14'd20; // 指定a的次態為20
            number <= a; // 數碼管顯示a的現態
            state <= 0;
        end
    end
    
endmodule

【例1】

reg [13:0] a = 0, b = 0;
reg [3:0] state = 0;
always @(posedge refresh) begin // 這個always塊每秒執行一次
	case (state)
		0: begin
			a <= 14'd3;
			b <= 14'd7;
			number <= (a + b) * 5; // 0 or 150
		end
		1: begin
			a = 14'd14;
			b = 14'd6;
			number <= (a + b) * 3; // 60
		end
		2: begin
			a = 14'd20;
			b = 14'd10;
			number <= (a + b) * 4; // 120
		end
	endcase
	
	if (state == 2)
		state <= 0;
	else
		state <= state + 1'b1;
end

數碼管(number)的顯示順序:0 0 60 120 150 60 120 150 60 120 ……
最開始number為0,1秒過後refresh訊號產生上升沿,always塊得到執行,此時state的現態為0,於是執行case 0分支。程式碼中指定a和b的次態分別為3和7,然後給number賦值的時候,是將a和b的現態相加,再乘5,a和b的現態都是0,於是number=(0+0)*5=0
再過1秒,always塊再次執行,此時state的現態為1,執行case 1分支。程式碼指定a和b的現態分別為14和6,給number賦值的時候用的就是a和b的現態,於是number=(14+6)*3=60。
再過一秒,同理,a和b的現態分別變成了20和10,number賦完值是120。
再過一秒,回到了state=0分支,此時又是指定a和b的次態,而a和b的現態是20和10,所以這次算出來number的值是(20+10)*5=150。

看了這段程式碼,我們就很容易理解阻塞和非阻塞賦值到底是什麼區別了。

【例2】

reg [13:0] a = 0;
reg [3:0] state = 0;
always @(posedge refresh) begin // 這個always塊每秒執行一次
	if (state == 0) begin
		a = 14'd10;
		number <= a;
		a = 14'd20;
		
		state <= state + 1'b1; // 不能寫成state<=1, 否則整個這段程式碼都會被優化掉
	end
end

程式執行結果為number=10。首先a的現態賦值為10,然後把a的現態賦值給number,number=10,然後a的現態改為20,這並沒有影響number的值。
最後一句話本來該寫state<=1的,但是筆者發現整個if語句都會被Quartus II優化掉,無論if裡面寫什麼都得不到執行(哪怕只是簡單的一句話)。這可能是因為state變數被定義為了reg型,編譯器覺得沒用就把整個程式碼塊刪掉了。寫成state<=state+1'b1就沒問題了。

【例3】

程式中多次指定一個變數的次態,那麼只有最後一次的值有效。

reg [13:0] a = 0;
reg [3:0] state = 0;
always @(posedge refresh) begin // 這個always塊每秒執行一次
	if (state == 0) begin
		a <= 14'd100;
		a <= 14'd200;
	end
	else if (state == 1) begin
		a <= a + 14'd10;
		number <= a;
		a <= a + 14'd20;
	end
	else if (state == 2)
		number <= a;
	
	if (state <= 2)
		state <= state + 1'b1;
end

程式執行結果:兩秒過後數碼管顯示200,再過一秒顯示220。
分析:第一秒末,a指定了兩次次態,但只有第二次的200有效,於是a的次態為200。
第二秒末,a的現態為200,於是number被賦值為200,數碼管顯示200。a的次態被賦值了兩次,一次是a的現態加上10等於200+10=210,另一次是a的現態加上20等於200+20=220,只有第二次賦值有效,所以a的次態是220。
第三秒末,number賦值為a的現態220。

【例4】

reg [13:0] a = 0;
reg [3:0] state = 0;
always @(posedge refresh) begin // 這個always塊每秒執行一次
	if (state == 0) begin
		a = 14'd10;
	end
	else if (state == 1) begin
		a <= 14'd20;
		number = a;
		a <= 14'd30;
	end
	
	if (state <= 1)
		state <= state + 1'b1;
end

程式執行結果:第二秒末,數碼管顯示10。

分析:第二秒末,a的現態為10,所以number被賦值為10。兩次對a賦值賦的是a的次態,且只有第二次有效,所以a的次態是30,但沒有賦給number,沒有顯示出來。

【例5】

終於可以講一下for迴圈了。在Verilog裡面,for迴圈的主要作用是在單個時鐘週期內立即得出運算結果。
比如,筆算乘法需要算出多個部分積,再把這幾個積相加得到最終結果。計算機的乘法器執行乘法運算也是同樣的方法。
always語句塊雖然也有迴圈的功能,但是完成多次迴圈需要多個時鐘週期,每個時鐘週期執行一次迴圈。多週期乘法器就是每個時鐘週期計算一下部分積,最後再相加,效率不是很高。
for迴圈可以在一個時鐘週期內執行完整個迴圈,代價是把相應的電路複製幾遍,比較浪費FPGA內部的資源。單週期乘法器就是一個時鐘週期一下子算出來所有的部分積,直接相加,一個週期直接得出乘法運算的結果。

reg [13:0] a = 0, i;
reg [3:0] state = 0;
always @(posedge refresh) begin // 這個always塊每秒執行一次
	case (state)
		0: begin
			a = 100;
			number <= a; // 100
		end
		1: begin
			for (i = 0; i < 4; i = i + 1'b1)
				a <= a + i * 10;
			number <= a; // 100
		end
		2: begin
			number <= a; // 130
		end
	endcase
	
	if (state == 2)
		state <= 0;
	else
		state <= state + 1'b1;
end

for迴圈括號裡面的i不能用非阻塞賦值語句賦值,否則編譯不通過。道理很簡單,因為那樣的話全是在指定i的次態,i的現態一直不變,那不就成了死迴圈了。
在上面的程式碼中,迴圈體內部用的是非阻塞賦值,指定a的次態。
迴圈一共執行4遍,第一遍迴圈i=0,a的現態是100,賦值a的次態100+0*10=100。第二遍迴圈i=1,a的現態還是100,賦值a的次態100+1*10=110。
第三遍迴圈i=2,賦值a的次態100+2*10=120。第四遍迴圈i=3,a的現態還是沒變仍然是100,賦值a的次態100+3*10=130。
a的次態賦了四次,只有最後一次有效,是130。
接著把a的現態賦值給number,數碼管顯示100。
到了第三秒,執行state==2那個分支的時候,a的現態是130,所以賦給number後數碼管顯示130。

【例6】

把例5迴圈體裡面的a改成阻塞賦值:a = a + i * 10,情況就不一樣了。
第一秒末數碼管顯示100。第二秒末,執行state==1分支,第一遍迴圈i=0,a的現態是100,賦值a的現態100+0*10=100。第二遍迴圈i=1,a的現態還是100,賦值a的現態100+1*10=110。
第三遍迴圈i=2,這回a的現態是110,賦值a的現態110+2*10=130。第四遍迴圈i=3,a的現態是130,賦值a的現態130+3*10=160。然後把a的現態賦給number顯示出來,數碼管顯示的是160。
當執行state==2分支時,a的現態還是160,賦給number後數碼管的顯示保持160不變。

相關文章