
本系列文章将和讀者一起巡禮數字邏輯線上學習網站 HDLBits 的教程與習題,并附上解答和一些作者個人的了解,相信無論是想 7 分鐘精通 Verilog,還是對 Verilog 和數電知識查漏補缺的同學,都能從中有所收獲。
首先附上傳送門:
https://hdlbits.01xz.net/wiki/Vector3hdlbits.01xz.net
Vector3 - HDLBits
Vector3 - HDLBitshdlbits.01xz.net
hdlbits.01xz.nethdlbits.01xz.net
片選操作符用于選擇向量的一部分比特。而連接配接操作符 { a,b,c },将較小的向量連接配接在一起,用于建立更大的向量。連接配接操作符是一個很常用的運算符。下面舉些例子:
{3'b111, 3'b000} => 6'b111000
{1'b1, 1'b0, 3'b101} => 5'b10101
{4'ha, 4'd10} => 8'b10101010 // 4'ha and 4'd10 are both 4'b1010 in binary
連接配接操作符的基本文法使用
{ }将較小的向量括起來,每個 { } 内的向量使用
逗号作為間隔。
連接配接運算符中的向量
務必需要标注位寬,不然綜合器怎麼能知道你的結果需要多寬的位寬。是以 { 1,2,3 } 這樣的操作是非法的,并會産生一個 Error:unsized constants are not allowed in concatenations.
習慣上,我們會把位連接配接符用在指派語句的右側,表示将較小的向量連接配接成較大的向量,賦予左值。但實際上位連接配接符同樣可以用在指派語句左側,比如:
assign {cout,sum} = a + b + cin;
在表示全加器時,可以使用一句 assign 語句實作結果和進位的指派。再看一些 HDLBits 上的栗子:
input [15:0] in;
output [23:0] out;
assign {out[7:0], out[15:8]} = in; // 連接配接符用于指派語句左側,交換了位元組的順序
assign out[15:0] = {in[7:0], in[15:8]}; // 連接配接符用于指派語句右側,交換了位元組的順序
assign out = {in[7:0], in[15:8]}; // 此語句作用上與上兩句相同交換了位元組順序,但不同的是指派語句右側為16位
//賦予左值後,右值擴充為24位,高8位賦零,前兩句中,高8位為未指派狀态
牛刀小試 上圖上方的方格表示子產品 32 位輸入向量,按照上下對應關系,輸出為下方的 4 個 8 比特向量。
解答與分析module top_module (
input [4:0] a, b, c, d, e, f,
output [7:0] w, x, y, z );//
// assign { ... } = { ... };
assign w = {a,b[4:2]};
assign x = {b[1:0],c,d[4]};
assign y = {d[3:0],e[4:1]};
assign z = {e[0],f,2'b11};
endmodule
本題中我們練習了在指派語句右側使用位連接配接符的文法,在左側使用位連接配接符的場景也很常見,比如在全加器中使用一句 assign 語句實作結果和進位的指派。
Problem 16 : Vector reversal 1這裡直接上題:給定一個 8bit 輸入向量,将其反向輸出。
解答與分析module top_module (
input [7:0] in,
output [7:0] out
);
assign {out[0],out[1],out[2],out[3],out[4],out[5],out[6],out[7]} = in;
endmodule
這裡使用左側位連接配接符,比較笨的方法完成了題目。好,假設輸入為 1024bit 向量,那咋辦?
接下來我們将讨論兩種使用
循環實作的方法,可能會涉及一些沒有讨論過的知識點,但請不用擔心,我們将在後續的文章或者題目中詳細讨論。
使用 for 循環integer i;
always @(*) begin
for (i=0; i<8; i++) //Use integer for pure Verilog.
out[i] = in[8-i-1];
end
我們可以在建立一個組合邏輯 always 塊(後續文章中會詳細解釋什麼是組合邏輯 always 塊),在塊中的組合邏輯将會按照一定的順序運作。for 循環描述了電路的行為,而不是電路的結構,是以,for 循環必須置于比如 always 塊這樣的過程塊中。(描述電路行為)
但需要強調的是,for 循環中的“循環”指的是代碼層面的循環,而如你所知,電路是不存在循環這種的東西的,無論是信号而是門電路,都不存在循環一說。實際上,for 循環表示的代碼将被綜合器解析,for 循環将被分别解析為硬體電路。(不過在仿真中,确實按照循環處理)。
是以 for 循環可以了解為
代碼循環的文法,減少編碼量,但真正的
硬體電路不存在循環,還是該怎麼樣怎麼樣。
另請注意循環變量 i,HDLBits 上的 solution 中,i 定義于 for 循環的括号中,這在 Verilog 的文法中是不被允許的,是 SystemVerilog 的文法。筆者在 ISE 中實測了一下,綜合會将其作為警告,但在預設情況下,仿真将會視其為錯誤。Verilog 的文法需要
提前定義 integer 變量,即整形。
使用 generate 生成塊generate
genvar i;
for (i=0; i<8; i = i+1) begin: my_block_name
assign out[i] = in[8-i-1];
end
endgenerate
generate 生成塊很有意思的一點是,雖然在 generate ,endgenerate 之間使用的仍然是 for 循環,但生成塊的概念和上面的 for 循環完全
不同。
for 循環和 Verilog 中其他的幾種循環語句 while ,forever,repeat 本質上都用于控制語句的執行次數。但
生成塊主要用于動态生成語句,例化 something(不隻是例化子產品),生成塊與上述的過程塊循環語句不同,并不是描述電路的一種行為。
生成塊可以例化
assign 語句,
子產品,
信号和變量的聲明以及 always initial 這樣的
過程塊。循環生成塊是生成塊中的一種類型,在綜合過程中同樣被綜合器進行編譯,這個過程可以看做綜合過程中動态生成更多 Verilog 代碼的預處理過程。在上面的例子中,generate 塊在綜合的過程中,綜合了 8 句 assign 指派語句。
總的來說,for 循環強調了對電路的行為描述,在綜合的過程中
循環展開,而生成塊則用于綜合過程中,
動态生成代碼,兩者有本質上的不同。
說了這麼多,讀者可能還會對生成塊一知半解 :( ,不過沒有關系,筆者覺得要了解這個概念,還是要多寫寫代碼,綜合仿真一下。另外後續的文章或者題目中還會詳細讨論這些概念。
說一點筆者在實踐而不是從書本(或者知乎文章: )上得來的發現:在生成塊中的 for 循環中不能像前例一樣使用 integer 作為循環變量,而是必須使用 genvar 變量。
Problem 17 : Replication operator連接配接操作符允許我們将短小的向量連接配接在一起構成更寬的向量。很友善,但有的時候需要将多個重複的向量連接配接在一起,諸如 assign a = {b,b,b,b,b,b}; 這樣的語句寫多了是非常讓人憂愁的。而重複操作符文法就可以在這種情況下幫到你,允許你将一個向量重複多次,并将它們連接配接在一起,文法是這樣:{ 重複次數 { 向量 } }。
重複次數必須是一個常量,而且請特别注意重複操作符
有兩對 { }.外層的 {} 不能少。
來自 HDLBits 的例子
{5{1'b1}} // 5'b11111 (or 5'd31 or 5'h1f)
{2{a,b,c}} // The same as {a,b,c,a,b,c}
{3'd5, {2{3'd6}}} // 9'b101_110_110. It's a concatenation of 101 with
// the second vector, which is two copies of 3'b110.
如果寫成 {3'd5, 2{3'd6}} ,少了一對 {} 是錯誤的.。
牛刀小試重複操作符的應用場景之一是在有符号數的擴充。有符号數的擴充是将符号位填充待擴充的比特。比如要将 4bit 的 4'b0101 有符号數擴充為 8bit ,0 是符号位,那麼擴充之後為 8'b0000 0101.
現在要求你建構一個電路,将一個 8bit 有符号數擴充為 32bit 數。
解答與分析module top_module (
input [7:0] in,
output [31:0] out );//
// assign out = { replicate-sign-bit , the-input };
assign out = {{24{in[7]}},in};
endmodule
将符号位 in[7] 擴充 24 位,後接原本的 8bit 數。
Problem 18 : More Replication 牛刀小試将 5 個 1bit 信号分别組成下圖中兩個 25 bit 信号,輸出向量為這兩個 25bit 向量的逐位操作的結果。如果兩個待比較信号某比特相同,則結果向量對應的該比特位 1 。
現在要求你建構一個電路,将一個 8bit 有符号數擴充為 32bit 數。
out[24] = ~a ^ a; // a == a, so out[24] is always 1.
out[23] = ~a ^ b;
out[22] = ~a ^ c;
...
out[ 1] = ~e ^ d;
out[ 0] = ~e ^ e;
根據上圖,這題使用位連接配接符和重複操作符是再舒服不過了。
解答與分析module top_module (
input a, b, c, d, e,
output [24:0] out );//
// The output is XNOR of two vectors created by
// concatenating and replicating the five inputs.
// assign out = ~{ ... } ^ { ... };
assign out = ~{{5{a}},{5{b}},{5{c}},{5{d}},{5{e}}} ^ {{5{a,b,c,d,e}}};
endmodule
本題的重點在于如何快速地使用連接配接操作符和重複操作符建構圖中的兩個待比較向量。
Problem 19 : Modules本題開始,我們将讨論一早我們就見識但還沒有深入了解的概念:子產品。
截止目前,我們已經對 Verilog 中子產品這一概念建立了初步的印象:子產品是一個電路,通過輸入輸出端口和外部的電路聯系。無論多大,多複雜的數字電路都是由一個個子產品以及其他組成部分(比如 assign 指派語句以及 always 過程塊)互相連接配接所構成的。在一個子產品中可以例化下一級的子產品,這就形成了層級的概念(hierarchy)。
下圖展示了一個簡單的,擁有下級子產品的子產品。在本題的練習中,建立一個下級子產品 mod_a,将其的三個端口 in1 ,in2 ,out 按照圖中的連接配接方式,分别連接配接到頂層子產品的 a ,b,out 端口上。mod_a 已經在測試代碼中提供給你,你不需要自己寫一個子產品,隻需要在你的頂層子產品中,例化 mod_a 子產品即可。
子產品例化的基本文法 :子產品名 執行個體名(定義連接配接 port 的信号);
比如
mod_a instance1 ( wa, wb, wc );
例化了一個 mod_a 子產品,将例化的執行個體命名為 instance1 。括号中是子產品端口的連接配接。port 的連接配接有兩種方式,這裡根據端口的位置定義了信号的連接配接,wire wa,wb,wc 按照順序連接配接到子產品端口上,兩種信号連接配接方式将在後文中具體讨論。
當你從頂層子產品的角度看去,下層子產品 mod_a 對你來說就是一個黑盒子,盒子裡的内容并不重要。重要的是子產品的輸入輸出端口。
并不在乎 module body 中有什麼
子產品的層級是通過在子產品中例化下一級子產品産生的。雖然不同的子產品寫在不同的 .v 檔案中,(一般推薦一個 .v 檔案中隻寫一個子產品),但隻要這些子產品在 ISE/Vivado/Quartus 這些開發軟體中處于一個 Project。綜合器就能在你例化子產品時,找到對應的子產品和 .v 檔案。
子產品中可以例化其他子產品,但在子產品中不允許再定義其他子產品。這項文法規則類似于在 C 語言函數中可以調用其他函數,但不能定義其他函數。
子產品信号連接配接的兩種方式在執行個體化子產品時,有兩種常用的方式來進行子產品端口的信号連接配接:按端口順序以及按端口名稱連接配接端口。
按端口順序,
mod_a instance1 ( wa, wb, wc );
wa, wb, wc 分别連接配接到子產品的 第一個端口(in1),第二個端口(in2)以及第三個端口(out)。這裡所謂的端口順序指的是子產品端口的定義順序。這種方式的弊端在于,一旦端口清單發生改變,所有子產品執行個體化中的端口連接配接都需要改變。
按端口名稱,
mod_a instance2 ( .out(wc), .in1(wa), .in2(wb) );
在這種方式中根據端口名稱指定外部信号的連接配接。這樣一來就和端口聲明的順序完全沒有關系。一旦子產品出現改動,隻要修改相應的部分即可。實際上,一般都使用這種方式來進行子產品執行個體化。
解答與分析module top_module ( input a, input b, output out );
mod_a U_mod_a(
.in1(a)
, .in2(b)
, .out(out));
//mod_a U_mod_a(a, .b, out); //使用按照端口順序的方式 聲明信号連接配接
endmodule
本題中同時使用了兩種方式定義了端口的信号連接配接,實際上按照端口名稱連接配接的方式用得更多,因為更加容易處理子產品端口清單的變動。
值得注意的是,在執行個體化子產品時,一般一個端口用一行表示,這樣更直覺一些。至于逗号放在前面還是放在後面,那倒無所謂。但我看過 NVIDIA 的開源代碼将逗号放在前面之後,覺得這樣挺好的,故也就這麼寫了。