00 一些前言
數字邏輯是計算機組成與體系結構的前導課,但是在兩者的銜接之間並沒有那麼流暢,比如對面向硬體電路的設計思路缺乏。這篇總結是在數字邏輯和計組體系結構的銜接階段進行的。
雖然這篇文是兩門課的交接,也算是兩個系列的一種過渡,但不代表我的數字邏輯部分就已經完成,這個系列後續會繼續更新FPGA的小專案記錄,以及閱讀筆記等等。
本文主要是對於這個視訊的筆記,我覺得很有用,加之此前我的實驗報告由於課程關係不好直接往網上發,所以在這裡整理一部分程式碼留作黑箱子。後續還會根據課程老師的要求對本文進行補充和優化:
01 前提準備
- 工具:模擬、綜合工具的熟練Vivado和編輯器
- 面向硬體電路的設計思路
- 要學會造CPU,而不是隻會背課本。所以我打算加入試點班。
02 回顧數字邏輯
02-1 數值表示 | 原碼補碼 | 邏輯閘 | 布林代數
- 數值和數制:
- 重點是二進位制、十六進位制;
- 數值的原碼錶示和補碼錶示:
- 有符號數和無符號數
- 加減溢位問題;
- 基本邏輯閘:
- 與或非、NAND;
- 以及CMOS閘電路;
- 布林代數:
- 邏輯表示式;
- 真值表;
- 邏輯運算律;
- 卡諾圖;
02-2 譯碼器等器件
這部分基本上涵蓋了上學期數字邏輯的所有實驗,所以是數字邏輯裡比較重要的部分。
- n-2n譯碼器Decoder;
- 資料選擇器MUX;
- 一位全加器,序列進位多位全加器;
- 鎖存器和觸發器;
- 傳並行載入轉換器等;
- 觸發器的時序分析、延遲分析;
- 計數器;
- 移位暫存器;
- 狀態機設計;
02-3 原理部分
- 組合邏輯電路和時序邏輯電路的原理
- 只讀儲存器ROM基本原理
- 隨機儲存器RAM基本原理
- 動態儲存器DRAM基本原理
- 現場可程式設計門陣列FPGA基本原理
這部分我的數字邏輯課程是沒有講解的,但後續我會繼續瞭解,畢竟是課程相關的東西。
03 主要過渡內容
03-1 Verilog
這是一個硬體語言,我此前也寫過數字邏輯實踐4->面向硬體電路的設計思維--FPGA設計總述,就是在描述Verilog與高階語言軟體思路的不同。
03-1-1 怎麼學
- 採用RTL(Register Transfer Level,暫存器傳輸級)設計;
- 使用限制的可綜合子集
- 將自己實現的程式碼和參考程式碼對比思考
03-1-2 面向硬體電路的設計思路
- 謹記這不是在寫程式碼,而是在設計電路(並行);
- 先進行(設想)電路結構設計,再進行Verilog程式碼編寫;
- 遵循自頂向下、模組劃分、逐層細化的設計步驟;
- 不要寫一點、試一下、改一點、再試一下,這樣效率很低;
⭐03-1-3 可綜合程式碼Verilog(限制版)
在寫CPU時,主要使用以下語法,其他的儘量不要使用:
- 模組宣告module endmodule
- 埠宣告input output inout
- 線網資料型別 wire
- 變數資料型別 reg,integer
- 引數常量 (parameter constants)
- 整型數 (literal integer numbers)
- 模組例項化
- 連續賦值語句assgin
- always結構化語句
- begin...end塊
- 阻塞賦值=和非阻塞賦值<=
- 條件判斷語句if,if...else,case
- for迴圈
- 組合邏輯敏感列表的 @*
- 多維陣列(暫存器堆)
- generate表示式
03-1-4 程式碼風格建議(要求)
-
資料通路上的組合邏輯用assign寫,禁止用always寫;controller中可以用,datapath不可以。
-
用always寫組合邏輯的時候,只允許出現在生成狀態機的next state的時候,且該語句中只能出現阻塞賦值=;
-
寫時序邏輯的時候,always語句中,只允許出現非阻塞賦值(<=);
-
暫存器堆封裝成單獨的模組,以例項化的方式使用;
-
case語句在任何情況下都要有default;
- 這一點在上一篇文章也提到過;保證了安全性和沒實現指令的放置;
-
模組例項化時候的引數和埠只允許用名字相關的方式進行賦值和連線;
就是例項化的時候用名字對應的方式來確保例項化不會錯位;
即這種方式?
.clk(clk)
-
資料通路的組合邏輯中,1bit的邏輯運算用&、|、~、^這類位運算子;
控制訊號的組合邏輯中,1bit的邏輯運算用&& 、||、!這三個運算子。
-
運算優先順序!
03-2 程式碼例項
為了在實操裡有效注意到上面提到過的東西,針對我寫過的實驗型別,來看看更好的實現方式。
視訊中提到的程式碼在[這裡][step_into_mips/rtl_code at prepare · lvyufeng/step_into_mips (github.com)],也是下面規範程式碼的來源處,至於下面我的數字邏輯實驗程式碼,對比之下,那沒有價值。
03-2-1 模組呼叫和例項化
下面的程式碼沒什麼功能可言,但對於格式而言很標準,可以學習一下它的模組呼叫和例項化,我作了標註。
module top;
wire [15:0] btm_a;
wire [ 7:0] btm_b;
wire [ 3:0] btm_c;
wire [ 3:0] btm_y;
wire btm_z;
bottom #(
.A_WIDTH (16),
.B_WIDTH ( 7),
.Y_WIDTH ( 3)
)
//這裡的引數可以調整就相當於全域性變數,用一個變數儲存常數。
inst_btm(
.a (btm_a), //I
.b (btm_b), //I
.c (btm_c), //I
.y (btm_y), //O
.z (btm_z) //O
);
//這裡就是前面提到過的按名字來例項化變數
endmodule
module bottom #
(
parameter A_WIDTH = 8,
parameter B_WIDTH = 4,
parameter Y_WIDTH = 2
)
(
input wire [A_WIDTH-1:0] a,
input wire [B_WIDTH-1:0] b,
input wire [ 3:0] c,
output wire [Y_WIDTH-1:0] y,
output reg z
);
// internal logic
endmodule
03-2-2 基本邏輯閘
下面提到的是32位的與或非、與非、或非、異或、同或,這是ALU(運算器)實現的基礎。
module bit_logic(
input [31:0] a,
input [31:0] b,
output [31:0] y1,
output [31:0] y2,
output [31:0] y3,
output [31:0] y4,
output [31:0] y5,
output [31:0] y6,
output [31:0] y7
);
assign y1 = a & b; //與
assign y2 = a | b; //或
assign y3 = ~a; //非
assign y4 = ~(a & b); //與非
assign y5 = ~(a | b); //或非
assign y6 = a ^ b; //異或
assign y7 = a ~^ b; //同或
endmodule
03-2-3 譯碼器
當時我校實驗二的任務有一個是2-4譯碼器,我當時的程式碼(.v檔案)如下,使用always+case實現的:
module dec2to4(W,En,Y);
input[1:0]W;
input En;
output reg [0:3]Y;
always @(W,En)
case({En,W})
3'b100:Y = 4'b1000;
3'b101:Y = 4'b0100;
3'b110:Y = 4'b0010;
3'b111:Y = 4'b0001;
default: Y = 4'b0000;
endcase
endmodule
這時候回憶一下上面的內容,資料通路上的組合邏輯用assign寫,禁止用always,看如下規範程式碼:
module decoder_4_16(
input [ 3:0] in,
output [16:0] out
);
// one-hot,獨熱編碼
assign out[ 0] = (in == 3'd0 );
assign out[ 1] = (in == 3'd1 );
assign out[ 2] = (in == 3'd2 );
assign out[ 3] = (in == 3'd3 );
assign out[ 4] = (in == 3'd4 );
assign out[ 5] = (in == 3'd5 );
assign out[ 6] = (in == 3'd6 );
assign out[ 7] = (in == 3'd7 );
assign out[ 8] = (in == 3'd8 );
assign out[ 9] = (in == 3'd9 );
assign out[10] = (in == 3'd10);
assign out[11] = (in == 3'd11);
assign out[12] = (in == 3'd12);
assign out[13] = (in == 3'd13);
assign out[14] = (in == 3'd14);
assign out[15] = (in == 3'd15);
endmodule
但明顯可以發現,上面的程式碼缺點在於重複結構,看起來很笨重,所以可以用generate來代替,準確來說這不是for迴圈,只是通過這種方式在編譯器的層面自動生成了上面那麼多重複的語句:
//另一個5-32譯碼器
//用generate語句改善編碼效率
module decoder_5_32(
input [ 4:0] in,
output [31:0] out
);
genvar i;
generate for (i=0; i<32; i=i+1) begin : gen_for_dec_5_32
assign out[i] = (in == i);
end endgenerate
endmodule
//6-64譯碼器
module decoder_6_64(
input [ 5:0] in,
output [63:0] out
);
genvar i;
generate for (i=0; i<63; i=i+1) begin : gen_for_dec_6_64
assign out[i] = (in == i);
end endgenerate
endmodule
03-2-4 編碼器
編碼器當時學校任務是8-3編碼器,我的程式碼如下,也是個case:
module enc8to3(x,y);
input [7:0]x;
output [2:0]y;
reg[2:0]y;
always@(x)
begin
case(x)
8'b00000001:y=3'b000;
//x=8 ’b00000001,y 輸出為 3 ’b000
8'b00000010:y=3'b001;
//x=8 ’b00000010,y 輸出為 3 ’b001
8'b00000100:y=3'b010;
//x=8 ’b00000100,y 輸出為 3 ’b010
8'b00001000:y=3'b011;
//x=8 ’b00001000,y 輸出為 3 ’b011
8'b00010000:y=3'b100;
//x=8 ’b00010000,y 輸出為 3 ’b100
8'b00100000:y=3'b101;
//x=8 ’b00100000,y 輸出為 3 ’b101
8'b01000000:y=3'b110;
//x=8 ’b01000000,y 輸出為 3 ’b110
8'b10000000:y=3'b111;
//x=8·’b10000000,y 輸出為 3 ’b111
default: y=3'b000;
endcase
end
endmodule
來看看好一點的規範程式碼:?
module encoder_8_3(
input [7:0] in,
output [2:0] out
);
//獨熱
assign out = in[0] ? 3’d0 :
in[1] ? 3’d1 :
in[2] ? 3’d2 :
in[3] ? 3’d3 :
in[4] ? 3’d4 :
in[5] ? 3’d5 :
in[6] ? 3’d6 :
3’d7 ;
endmodule
//這其實是一個優先順序編碼器,是一層一層的else,所以最後的網格電路會比較慢
下面是一種更好的寫法:
//保證設計輸入in永遠至多隻有一個1
//即at-most-1-hot向量
module encoder_8_3(
input [7:0] in,
output [2:0] out
);
assign out = ({3{in[0]}} & 3’d0)
| ({3{in[1]}} & 3’d1)
| ({3{in[2]}} & 3’d2)
| ({3{in[3]}} & 3’d3)
| ({3{in[4]}} & 3’d4)
| ({3{in[5]}} & 3’d5)
| ({3{in[6]}} & 3’d6)
| ({3{in[7]}} & 3’d7);
endmodule
用邏輯運算子 | 使得所有的表示式並行,在電路上表示為同一級並行結構。下面可以看到這個語句與多路選擇器其實也很相似。
03-2-4 多路選擇器
當時我寫的是:
module mux2to1(w0,w1,s,f);
input w0,w1,s;
output reg f;
//assign f = s ? w1 : w0;
//這不是可綜合的程式碼,替換?
//2022-3-12
//上面說錯了,assign對於reg型別不可綜合,wire還行,甚至在資料通路上這個組合邏輯模組應當使用assign。
always @(w0,w1,s)
f = s ? w1 : w0;
endmodule
再來看規範程式碼,與上面的編碼器相同,給出了一個優先順序程式碼(速度不那麼快),和另一個並行程式碼(速度快):
module mux5_8b(
input [7:0] in0, in1, in2, in3, in4,
input [2:0] sel,
output [7:0] out
);
//優先順序程式碼
assign out = (sel==3’d0) ? in0 :
(sel==3’d1) ? in1 :
(sel==3’d2) ? in2 :
(sel==3’d3) ? in3 :
(sel==3’d4) ? in4 :
8’b0;
endmodule
module mux5_8b(
input [7:0] in0, in1, in2, in3, in4,
input [2:0] sel,
output [7:0] out
);
//並行程式碼
assign out = ({8{sel==3’d0}} & in0)
| ({8{sel==3’d1}} & in1)
| ({8{sel==3’d2}} & in2)
| ({8{sel==3’d3}} & in3)
| ({8{sel==3’d4}} & in4);
endmodule
//不要忘了寫{8{}}中的8,這是與in0等保持一致。
下面是一個特殊一點的多路選擇器,實現五選一,這個五中的每一個都是一個位寬為8的陣列。
module max5_1hot_8b(
input [7:0]in0, in1, in2, in3, in4,
input [4:0]sel_v,
output [7:0]out
);
assign out = ({8{sel_v[0]}} & in0)
|({8{sel_v[1]}} & in1)
|({8{sel_v[2]}} & in2)
|({8{sel_v[3]}} & in3)
|({8{sel_v[4]}} & in4);
endmodule
03-2-5 全加器 / 加法器
全加器事實上程式碼的操作空間不大,下面是我寫的:
module twoadder(x,y,carryin,Sum,carryout);
parameter n = 2;
input [n-1:0]x,y;
input carryin;
output reg [n-1:0]Sum;
output reg carryout;
always @(x,y,carryin)
begin
{carryout,Sum} = x + y + carryin;
end
endmodule
這是參考程式碼:
module adder(
input [31:0] a,
input [31:0] b,
input cin,
output [31:0] s,
output cout
);
assign {cout, s} = a + b + cin;
endmodule
//基本一致
03-2-6 暫存器 / D觸發器
這部分就是時序邏輯了,有好幾個形態的Dflipflop:
-
最普通的上跳沿觸發的D暫存器
module Dflipflop( input clk, input din, output reg q ); always @(posedge clk) begin q <= din; end endmodule
-
帶使能端的D暫存器,兩種推薦寫法:
//第一種:if module Dflipflop_en( input clk, input en, input din, output reg q ); always @(posedge clk) begin if (en) q <= din; end endmodule //第二種:三元運算 module Dflipflop_en( input clk, input en, input din, output reg q ); always @(posedge clk) begin q <= en ? din : q; end endmodule
-
帶復位的D暫存器,兩種推薦寫法:
。//寫法1:if-else if
module Dflipflop_r(
input clk,
input rst,
input din,
output reg q
);
always @(posedge clk) begin
if (rst) q <= 1’b0;
else if (en) q <= din;
end
endmodule
//寫法二:二重三元運算
module Dflipflop_r(
input clk,
input rst,
input din,
output reg q
);
always @(posedge clk) begin
q <= rst ? 1'b0 :
en ? din : q;
end
endmodule
-
以上兩個暫存器都推薦使用if的實現方式,因為軟體會對這種方式進行優化,對三元運算子的不會;
並且 if 的方式對於q優先順序顯示得較為清楚。
這裡強調一個問題:
就是rst要不要放在posedge裡,即:
always @(posedge clk, rst) begin
產生這個問題多半是因為課本上的程式碼,有些是上面例項程式碼的形式,有些是這個形式。這個問題也很簡單,即記住要把時鐘敏感的訊號放在@裡,非時鐘敏感的就不放進去。
03-2-7 暫存器堆
這個實驗我沒有做過,基本就是實現一個指令集對應的暫存器陣列。
module regfile(
input clk,
// READ PORT 1
input [ 4:0] raddr1,
output [31:0] rdata1,
// READ PORT 2
input [ 4:0] raddr2,
output [31:0] rdata2,
// WRITE PORT
input we,
//write enable, HIGH valid
input [ 4:0] waddr,
input [31:0] wdata
);
reg [31:0] rf[31:0];
//WRITE
always @(posedge clk) begin
if (we) rf[waddr]<= wdata;
end
//READ OUT 1
assign rdata1 = (raddr1==5'b0) ? 32'b0 : rf[raddr1];
//READ OUT 2
assign rdata2 = (raddr2==5'b0) ? 32'b0 : rf[raddr2];
//讀的時候是一個組合邏輯,所以是we,而不是en
endmodule
下面還有一個寫法,看起來很長,如果仔細看,下面讀1和讀2的操作都是並行的,具體實現的就是一個譯碼的操作,即將地址譯為一個one-hot。這種寫法是上面程式碼具體實現的樣子。
//另一個寫法
module regfile(
input clk,
// READ PORT 1
input [ 4:0] raddr1,
output [31:0] rdata1,
// READ PORT 2
input [ 4:0] raddr2,
output [31:0] rdata2,
// WRITE PORT
input we, //write enable, HIGH valid
input [ 4:0] waddr,
input [31:0] wdata
);
reg [31:0] rf[31:0];
wire [31:0] waddr_dec, raddr1_dec, raddr2_dec;
//WRITE
decoder_5_32 U0(.in(waddr ), .out(waddr_dec));
always @(posedge clk) begin
if (we & waddr_dec[ 0]) rf[ 0] <= wdata;
if (we & waddr_dec[ 1]) rf[ 1] <= wdata;
if (we & waddr_dec[ 2]) rf[ 2] <= wdata;
if (we & waddr_dec[ 3]) rf[ 3] <= wdata;
if (we & waddr_dec[ 4]) rf[ 4] <= wdata;
if (we & waddr_dec[ 5]) rf[ 5] <= wdata;
if (we & waddr_dec[ 6]) rf[ 6] <= wdata;
if (we & waddr_dec[ 7]) rf[ 7] <= wdata;
if (we & waddr_dec[ 8]) rf[ 8] <= wdata;
if (we & waddr_dec[ 9]) rf[ 9] <= wdata;
if (we & waddr_dec[10]) rf[10] <= wdata;
if (we & waddr_dec[11]) rf[11] <= wdata;
if (we & waddr_dec[12]) rf[12] <= wdata;
if (we & waddr_dec[13]) rf[13] <= wdata;
if (we & waddr_dec[14]) rf[14] <= wdata;
if (we & waddr_dec[15]) rf[15] <= wdata;
if (we & waddr_dec[16]) rf[16] <= wdata;
if (we & waddr_dec[17]) rf[17] <= wdata;
if (we & waddr_dec[18]) rf[18] <= wdata;
if (we & waddr_dec[19]) rf[19] <= wdata;
if (we & waddr_dec[20]) rf[20] <= wdata;
if (we & waddr_dec[21]) rf[21] <= wdata;
if (we & waddr_dec[22]) rf[22] <= wdata;
if (we & waddr_dec[23]) rf[23] <= wdata;
if (we & waddr_dec[24]) rf[24] <= wdata;
if (we & waddr_dec[25]) rf[25] <= wdata;
if (we & waddr_dec[26]) rf[26] <= wdata;
if (we & waddr_dec[27]) rf[27] <= wdata;
if (we & waddr_dec[28]) rf[28] <= wdata;
if (we & waddr_dec[29]) rf[29] <= wdata;
if (we & waddr_dec[30]) rf[30] <= wdata;
if (we & waddr_dec[31]) rf[31] <= wdata;
end
//READ OUT 1
decoder_5_32 U1(.in(raddr1), .out(raddr1_dec));
assign rdata1 = ({32{raddr1_dec[ 1]}} & rf[ 1]) //NOTE: we omit No. 0 entry because GR[0] always be zero.
| ({32{raddr1_dec[ 2]}} & rf[ 2])
| ({32{raddr1_dec[ 3]}} & rf[ 3])
| ({32{raddr1_dec[ 4]}} & rf[ 4])
| ({32{raddr1_dec[ 5]}} & rf[ 5])
| ({32{raddr1_dec[ 6]}} & rf[ 6])
| ({32{raddr1_dec[ 7]}} & rf[ 7])
| ({32{raddr1_dec[ 8]}} & rf[ 8])
| ({32{raddr1_dec[ 9]}} & rf[ 9])
| ({32{raddr1_dec[10]}} & rf[10])
| ({32{raddr1_dec[11]}} & rf[11])
| ({32{raddr1_dec[12]}} & rf[12])
| ({32{raddr1_dec[13]}} & rf[13])
| ({32{raddr1_dec[14]}} & rf[14])
| ({32{raddr1_dec[15]}} & rf[15])
| ({32{raddr1_dec[16]}} & rf[16])
| ({32{raddr1_dec[17]}} & rf[17])
| ({32{raddr1_dec[18]}} & rf[18])
| ({32{raddr1_dec[19]}} & rf[19])
| ({32{raddr1_dec[20]}} & rf[20])
| ({32{raddr1_dec[21]}} & rf[21])
| ({32{raddr1_dec[22]}} & rf[22])
| ({32{raddr1_dec[23]}} & rf[23])
| ({32{raddr1_dec[24]}} & rf[24])
| ({32{raddr1_dec[25]}} & rf[25])
| ({32{raddr1_dec[26]}} & rf[26])
| ({32{raddr1_dec[27]}} & rf[27])
| ({32{raddr1_dec[28]}} & rf[28])
| ({32{raddr1_dec[29]}} & rf[29])
| ({32{raddr1_dec[30]}} & rf[30])
| ({32{raddr1_dec[31]}} & rf[31]);
//READ OUT 2
decoder_5_32 U2(.in(raddr2), .out(raddr2_dec));
assign rdata2 = ({32{raddr2_dec[ 1]}} & rf[ 1])
| ({32{raddr2_dec[ 2]}} & rf[ 2])
| ({32{raddr2_dec[ 3]}} & rf[ 3])
| ({32{raddr2_dec[ 4]}} & rf[ 4])
| ({32{raddr2_dec[ 5]}} & rf[ 5])
| ({32{raddr2_dec[ 6]}} & rf[ 6])
| ({32{raddr2_dec[ 7]}} & rf[ 7])
| ({32{raddr2_dec[ 8]}} & rf[ 8])
| ({32{raddr2_dec[ 9]}} & rf[ 9])
| ({32{raddr2_dec[10]}} & rf[10])
| ({32{raddr2_dec[11]}} & rf[11])
| ({32{raddr2_dec[12]}} & rf[12])
| ({32{raddr2_dec[13]}} & rf[13])
| ({32{raddr2_dec[14]}} & rf[14])
| ({32{raddr2_dec[15]}} & rf[15])
| ({32{raddr2_dec[16]}} & rf[16])
| ({32{raddr2_dec[17]}} & rf[17])
| ({32{raddr2_dec[18]}} & rf[18])
| ({32{raddr2_dec[19]}} & rf[19])
| ({32{raddr2_dec[20]}} & rf[20])
| ({32{raddr2_dec[21]}} & rf[21])
| ({32{raddr2_dec[22]}} & rf[22])
| ({32{raddr2_dec[23]}} & rf[23])
| ({32{raddr2_dec[24]}} & rf[24])
| ({32{raddr2_dec[25]}} & rf[25])
| ({32{raddr2_dec[26]}} & rf[26])
| ({32{raddr2_dec[27]}} & rf[27])
| ({32{raddr2_dec[28]}} & rf[28])
| ({32{raddr2_dec[29]}} & rf[29])
| ({32{raddr2_dec[30]}} & rf[30])
| ({32{raddr2_dec[31]}} & rf[31]);
endmodule
04 設計基本流程 | 注意事項
基本如上圖所示,我們需要:
- CPU結構圖
- RTL程式碼編寫(即上面各種箱子)
- 功能模擬,vivado裡的Functional Simulation;必須先進行此步再綜合;模擬不正確不要向後做;
- 測試程式碼testbench,cpu檢驗時老師會給
- 綜合;
- Vivado佈局佈線;
- 驗證;
05 結語
05-1 課外書籍推薦
書不一定要是紙質書,看不一定是一頁一頁看,但這些書確實挺好:
-
《verilog數字系統設計教程》夏宇聞
查一些基本語法;
-
《自己動手寫CPU》雷思磊
快速做出來CPU;
-
《數字設計與計算機體系結構》戴維·莫尼·哈里斯
MIPS-FPGA的鼻祖;
讀者如果需要的話,可以聯絡我,我找到了一些資源。
05-2 安排
我想我會在下一篇會講解(記錄)vivado下載安裝使用以及第三方編輯器的調配。