使用Verilog搭建一個單周期CPU


使用Verilog搭建一個單周期CPU

搭建篇

總體結構

其實跟使用logisim搭建CPU基本一致,甚至更簡單,因為完全可以照着logisim的電路圖來寫,各個模塊和模塊間的連接在logisim中非常清楚。唯一改變了的只有GRF和DM要多一個input PC端口,用來display的時候輸出PC值;IFU同理多了一個output PC,用來把PC的值傳給GRF和DM。其他的模塊我都是直接對着logisim原封不動地用Verilog重新實現了一遍。目前支持指令集{addu、subu、ori、lw、sw、beq、jal、jr、nop、lui、sb、lb、sh、lh、jalr、addi}。

IFU

端口如下圖所示,僅多了一個output PC,實現應該是非常簡單的。

但是要注意一點,如果進行了初始化(如下),那么一定不能用非阻塞賦值,否則你會發現你的IM根本讀不進code.txt里的內容(非阻塞賦值會在initial后進行賦值0,讀的code.txt又被清成0了,所以啥也讀不到)

	initial begin
		pc = 32'h00003000;
		for(i=0;i<1024;i = i + 1) begin
            im[i] = 32'h00000000;//正確寫法
            //im[i] <= 32'h00000000;//錯誤寫法
		end
		$readmemh("code.txt",im);
	end

GRF

端口多了一個input PC,用來display的時候可以獲取到PC值以輸出。

這個需要注意的是0號寄存器,他不能被寫入,特判一下就可以。也可以和我一樣reg [31:0] rf[31:1];,根本沒有0號寄存器自然也寫不了他,然后輸出寄存器值的時候assign RD1 = (A1 == 0) ? 32'b0 : rf[A1];,判斷是否為輸出0號寄存器的值。

EXT

用位拼接寫,非常簡單,符號擴展就將最高位復制就可以了。

	assign ext_imm16 = (EXTOp == 2'b00) ? {{16{1'b0}},imm16[15:0]} :
						(EXTOp == 2'b01) ? {{16{imm16[15]}},imm16[15:0]} :
						(EXTOp == 2'b10) ? {imm16[15:0],{16{1'b0}}} :
											{{16{1'b0}},imm16[15:0]};

但是注意不要在位拼接里出現沒位數的數,Verilog會默認成32位,而不是你想象中的1位。錯誤示范如下:

	assign ext_imm16 = (EXTOp == 2'b00) ? {{16{0}},imm16[15:0]} :
						(EXTOp == 2'b01) ? {{16{imm16[15]}},imm16[15:0]} :
						(EXTOp == 2'b10) ? {imm16[15:0],{16{0}}} :
											{{16{0}},imm16[15:0]};

總之寫Verilog的時候養成好習慣吧,數字都加上位數和進制,防止在奇怪的地方出錯還找不到bug。

ALU

這個就更簡單了。沒啥寫的就說說上次那個奇偶校驗跳轉的指令吧。

首先Verilog里是有異或運算的:^,這個就是異或,不要課上用|~手寫一個異或出來。然后我們知道縮減運算符^A就等於A[31]^A[30]^A[29]^...^A[1]^A[0]。那么A中有奇數個1就相當於^A == 1 ,A中有偶數個1就相當於^A == 0,然后就跟beq一樣跳轉就可以了(beq的Zero是A == B,這個指令的Zero是^A

DM

這個也比較簡單,我依然加了lb、sb、lh、sh指令,在Verilog里寫只需要用位拼接來寫就可以了,比logisim方便不少。

對寫入數據進行處理:(SSel為2'b00即原數據,SSel為2'b01即sb時,SSel為2'b10即sh時)

	always @(*) begin
		if(SSel == 2'b00) begin
			WData = WD;
		end
		else if(SSel == 2'b01) begin
			WData = (A[1:0] == 2'b00) ? {dm[A[11:2]][31:8],WD[7:0]} :
						(A[1:0] == 2'b01) ? {dm[A[11:2]][31:16],WD[7:0],dm[A[11:2]][7:0]} :
						(A[1:0] == 2'b10) ? {dm[A[11:2]][31:24],WD[7:0],dm[A[11:2]][15:0]} :
														{WD[7:0],dm[A[11:2]][23:0]};
		end
		else if(SSel == 2'b10) begin
			WData = (A[1] == 1'b0) ? {dm[A[11:2]][31:16],WD[15:0]} : {WD[15:0],dm[A[11:2]][31:16]};
		end
	end

對讀出數據進行處理:(LSel為2'b00即原數據,為2'b01即lb時,2'b10即lh時)

	always @(*) begin
		case(LSel)
			2'b00:begin
				RD = dm[A[11:2]];
			end
			2'b01:begin
				RD = (A[1:0] == 2'b00) ? {{24{dm[A[11:2]][7]}},dm[A[11:2]][7:0]} :
				     (A[1:0] == 2'b01) ? {{24{dm[A[11:2]][15]}},dm[A[11:2]][15:8]} :
					 (A[1:0] == 2'b10) ? {{24{dm[A[11:2]][23]}},dm[A[11:2]][23:16]} :
										 {{24{dm[A[11:2]][31]}},dm[A[11:2]][31:24]};
			end
			2'b10:begin
				RD = (A[1] == 1'b0) ? {{16{dm[A[11:2]][15]}},dm[A[11:2]][15:0]} : {{16{dm[A[11:2]][31]}},dm[A[11:2]][31:16]};
			end
			default:RD = 32'h00000000;
		endcase
	end

話說回來我的測評點生成機好像忘記了測lb、sb、lh、sh(逃

MUX

這個一定不要按高老板ppt里的那個寫。我一開始按他的寫然后de了半天才找到原來是MUX的錯誤。

高老板寫法:

后來改成了三目運算符就AC了。。。現在也沒看懂他的是什么原理(也可能是對的?

	assign Out = (S0 == 0 && S1 == 0) ? D0 :
	             (S0 == 1 && S1 == 0) ? D1 : 
				 (S0 == 0 && S1 == 1) ? D2 :
				                        D3 ;

Controller

先寫宏定義

`define ADDU  6'b100001
`define SUBU  6'b100011
`define ORI   6'b001101
`define LW    6'b100011
`define SW    6'b101011
`define BEQ   6'b000100
`define JAL   6'b000011
`define JR    6'b001000
`define LUI   6'b001111
`define LB    6'b100000
`define SB    6'b101000
`define LH    6'b100001
`define SH    6'b101001
`define RTYPE 6'b000000
`define ADDI  6'b001000
`define JALR  6'b001001
`define J     6'b000010

之后每一個指令都用一個wire表示,注意用宏定義加`,以及R型指令是Rtype與funct的與。

	wire addu,subu,ori,lw,sw,beq,jal,jr,lui,lb,sb,lh,sh,addi,jalr,j;
	assign RType = (opcode == `RTYPE);
	assign addu  =  RType&(funct == `ADDU);
	assign subu  =  RType&(funct == `SUBU);
	assign ori   = (opcode == `ORI );
	assign lw    = (opcode == `LW);
	assign sw    = (opcode == `SW);
	assign beq   = (opcode == `BEQ);
	assign jal   = (opcode == `JAL);
	assign jr    =  RType&(funct == `JR);
	assign lui   = (opcode == `LUI);
	assign lb    = (opcode == `LB);
	assign sb    = (opcode == `SB);
	assign sh    = (opcode == `SH);
	assign lh    = (opcode == `LH);
	assign addi  = (opcode == `ADDI);
	assign jalr  = RType&(funct == `JALR);
	assign j     = (opcode == `J);

之后對每個控制信號根據真值表加指令,兩位的和一位的各舉了一個例子。

	assign NPCOp[0] = beq | jr | jalr;
	assign NPCOp[1] = jal | jr | jalr | j;
	
	assign RFWr = addu | subu | ori | lw | jal | lui | lb | lh | addi | jalr ;

datapath

這個和Controller作為mips的子模塊,datapath用來把所有除了controller的模塊連接起來,然后在mips里與controller連接。

結構圖如下

image-20201204114817321

加指令篇

跟P3一樣分析即可。eg:加addi(不考慮溢出)

分析數據通路

判斷是否需要增加新的通路以實現該指令,如ALU是否要增加計算功能之類的。addi不需要因此直接改控制信號即可。

確定控制信號

對於NPCOp,這不是一個跳轉指令,因此NPCOp取00

對於RFWr,要回寫到R[rt],因此RFWr為1

對於EXTOp,要進行符號擴展,所以取01

對於ALUOp,加法,所以取00

對於DMWr,不用寫入DM,所以取0

對於WRSel,由於寫入的是R[rt],所以取01

對於WDSel,由於寫入的數據來自ALU的計算結果,所以取00

對於BSel,由於參與ALU計算的第二個數來自EXT,所以取1

對於SSel和LSel,由於不涉及半字或字節,都取00

添加指令信號

先定義ADDI

`define ADDI  6'b001000

再添加wire addi

	assign addi  = (opcode == `ADDI);

修改控制信號

在addi控制信號為1的地方加上addi。

如RFWr為1,則在 assign RFWr = addu | subu | ori | lw | jal | lui | lb | lh ;最后或addi。

即變成 assign RFWr = addu | subu | ori | lw | jal | lui | lb | lh | addi;

其他控制信號依次添加即可,加完所有控制信號后,addi的添加完成。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM