Verilog回憶

在前天將41 instructions pipelined full forwarding cpu驗收完成,寫verilog的日子就算是告一段落了,很可能以後再也不會去動它了。在此,一來記錄一下過去,二來,也給要寫類似東西的人以啓示。

初見Verilog

最初見到Verilog應該是在邏輯與計算機設計基礎的實驗課上見到,同時見到的還有FPGA板子和Xilinx ISE。最開始,我以爲這課基本上是從理論到理論,接下來的計算機組成,那更是如此。因爲,在此之前,我買過國內的一本計算機組成的書,它上面也有講加法器,乘法器,除法器等等ALU部件,還有其他CPU部分,寄存器堆,內部總線,ISA等等。但是,它似乎沒有讓你去實現它的意思。理論很多,但是具體怎麼實現,不知道。

最開始的時候,我們也只是用verilog寫寫,多路選擇器,譯碼器,加法器,以及到後面稍微複雜時序電路。

當時,老師說,我們最終會實現一個cpu的時候,我開始對此有了小小的期待。(我不知道國內的其它學校他們是怎麼上這課,還是從理論到理論嗎?)

邏輯課的大程

在邏輯課快結束的時候,老師需要我們寫一個大程。他對大程的要求是,要用VGA顯示,要用到ram or rom,並且得有時序電路。他還給了一個例子,打乒乓球,應該是前些屆學長做的。當時,不知怎麼的,我想到了去做硬件解24點問題。此處應該算是縮減版的24點,首先,運算只能有+-*,4張牌,每一張都是0-15之內的一個值。主要原因是除法運算不是一個時鐘週期可以完成的,做多週期的要複雜很多,每張只能是0-15也是爲了簡單起見,剛好4個bit。

軟件24點

用軟件和硬件實現24點都是十分類似。最根本的方法都是窮舉法。我網上搜到的都是窮舉法,應該還是有其他更快的方法的,但是即便是窮舉也沒有多少種。

括號的嵌套方式(其實也就是運算的順序)總共有5種。(之後我們都認爲+-*之間是沒有優先級的,先算誰都是靠括號來的。)

  1. ((a+b)+c)+d
  2. (a+(b+c))+d
  3. a+((b+c)+d)
  4. a+(b+(c+d))
  5. (a+b)+(c+d)

對於給定的任一括號嵌套方式,我們都有3*3*3=27種運算符組合。

對於任一種括號嵌套和運算符組合,我們都有4!=24種安排4個數的順序。

總的來說也就是,組合共有5*27*24=3240種。每種也就進行3次運算,所以總的運算量十分少,窮舉也不是什麼很糟糕的算法。

硬件24點

軟件是串行的,硬件可是並行的。那不是什麼多核或者是GPGPU的並行,而是比他們低層得多的,bit單位的並行。我當時的做法是,類似於外面套兩層循環,第一層循環去枚舉4個數字的排列情況,第二層循環去枚舉運算符的組合,最裏面並行的去計算5種括號嵌套方式。然後,找出其中一組最終值爲24。

計組的多時鐘CPU

計組的單時鐘CPU怎麼做的,我已經記得不太清楚了。但是,多時鐘我還是記得比較清楚的。當時的那一次的4個人合作幾乎是我大學裏面最爲成功的一次合作。不管是從分工,效率,過程,還是結果都非常的好。我是負責將另一個組員畫好的圖,用verilog拼起來。我用了4個小時,將它拼起來,並且又用了4個小時,仿真調試通過。總共實現了38條指令。下面兩個圖應該大部分都是對的,我隱約記着是有幾個小bug的。應該是出在,下下張圖上,也就是處理字,半字,字節load和store上。

邏輯圖


狀態機


計算機體系pipeline CPU with stall

這個實驗花費了我20+h才完成,完成的十分痛苦,光是下面這個圖我就花了4-5h,還有花了差不多一半時間在和xilinx戰鬥。(此處實現了38條指令。關於stall的邏輯此處並沒有畫出。)

邏輯圖

計算機體系pipeline CPU with full forwarding

邏輯圖

Hazard Solver

module pipeline_HazardSolve_Forwording(
	input wire[4:0] regA,input wire[4:0] regB,input wire RegAUsed,input wire RegBUsed,
	input wire ID_jump,input wire[5:0] ID_instID,
	input wire[5:0] EXE_instID,input wire EXE_RegWrite,input wire[4:0] EXE_wd,
	input wire[5:0] MEM_instID,input wire MEM_RegWrite,input wire[4:0] MEM_wd,
	output reg pcStall,output reg IF_ID_stall,output reg IF_ID_pushBubble,
	output reg ID_EXE_pushBubble,
	output reg[1:0]forwardingMUXA,output reg[1:0]forwardingMUXB,
	output reg ID_forwardingMUXMem
);
parameter  ID_add=1,ID_addu=2,ID_sub=3,ID_subu=4,ID_slt=5,ID_sltu=6,ID_sll=7,ID_srl=8,
		ID_sra=9,ID_and=10,ID_or=11,ID_nor=12,ID_xor=13,ID_clo=14,ID_clz=15,ID_beq=16,
		ID_bne=17,ID_bgtz=18,ID_bgez=19,ID_bltz=20,ID_blez=21,ID_addi=22,ID_addiu=23,
		ID_xori=24,ID_slti=25,ID_sltiu=26,ID_andi=27,ID_ori=28,ID_lui=29,ID_lw=30,ID_sw=31,
		ID_lwl=32,ID_lwr=33,ID_lbu=34,ID_lb=35,ID_sb=36,ID_lhu=37,ID_lh=38,ID_sh=39,ID_j=40,
		ID_jal=41,ID_jr=42,ID_sllv=43,ID_srlv=44,ID_srav=45;
		
	//Control Hazard.
	always @(*)
		begin
			if(ID_jump==1'b1)
				begin
					IF_ID_pushBubble<=1'b1;
				end
			else
				begin
					IF_ID_pushBubble<=1'b0;
				end
		end
	
	//Data Hazard.
	wire ID_issw,EXE_islw,MEM_islw;
	
	assign ID_issw = ID_instID==ID_sw || ID_instID==ID_sh || ID_instID==ID_sb;
	
	assign EXE_islw = EXE_instID==ID_lw || EXE_instID==ID_lbu || EXE_instID==ID_lb ||
		EXE_instID==ID_lhu || EXE_instID==ID_lh;
	//assign EXE_issw = EXE_instID==ID_sw || EXE_instID==ID_sh || EXE_instID==ID_sb;
	assign MEM_islw = MEM_instID==ID_lw || MEM_instID==ID_lbu || MEM_instID==ID_lb ||
		MEM_instID==ID_lhu || MEM_instID==ID_lh;
	//assign MEM_issw = MEM_instID==ID_sw || MEM_instID==ID_sh || MEM_instID==ID_sb;
	
	wire EXE_dataHazard,MEM_dataHazard;
	wire EXE_dataHazardA,EXE_dataHazardB,MEM_dataHazardA,MEM_dataHazardB;
	
	assign EXE_dataHazard = (EXE_RegWrite==1'b1)&&((EXE_wd==regA && RegAUsed==1'b1 &&
		regA!=5'b00000)||(EXE_wd==regB && RegBUsed==1'b1 && regB!=5'b00000));
	assign EXE_dataHazardA = (EXE_RegWrite==1'b1)&&(EXE_wd==regA && RegAUsed==1'b1 
		&& regA!=5'b00000);
	assign EXE_dataHazardB = (EXE_RegWrite==1'b1)&&(EXE_wd==regB && RegBUsed==1'b1 
		&& regB!=5'b00000);
	
	assign MEM_dataHazard = (MEM_RegWrite==1'b1)&&((MEM_wd==regA && RegAUsed==1'b1 
		&& regA!=5'b00000)||(MEM_wd==regB && RegBUsed==1'b1 && regB!=5'b00000));
	assign MEM_dataHazardA = (MEM_RegWrite==1'b1)&&(MEM_wd==regA && RegAUsed==1'b1
		&& regA!=5'b00000);
	assign MEM_dataHazardB = (MEM_RegWrite==1'b1)&&(MEM_wd==regB && RegBUsed==1'b1
		&& regB!=5'b00000);
	
	always @(*)
		begin
			if(EXE_dataHazard==1'b1 && EXE_islw==1'b1 && (ID_issw==1'b0 ||
				EXE_dataHazardA==1'b1))
				begin
					pcStall<=1'b1;
					IF_ID_stall<=1'b1;
					ID_EXE_pushBubble<=1'b1;
				
				end
			else
				begin
					pcStall<=1'b0;
					IF_ID_stall<=1'b0;
					ID_EXE_pushBubble<=1'b0;
					//------------------------------------
					if(EXE_dataHazardA==1'b1)
						begin
							forwardingMUXA<=2'b01;
						end
					else if(MEM_dataHazardA==1'b1)
						begin
							if(MEM_islw==1'b1)
								forwardingMUXA<=2'b11;
							else
								forwardingMUXA<=2'b10;
						end
					else
						forwardingMUXA<=2'b00;
					//------------------------------------
					if(EXE_dataHazardB==1'b1 && ID_issw==1'b1 && EXE_islw==1'b1)
						ID_forwardingMUXMem<=1'b1;
					else
						ID_forwardingMUXMem<=1'b0;
					//------------------------------------
					if(EXE_dataHazardB==1'b1)
						begin
							forwardingMUXB<=2'b01;
						end
					else if(MEM_dataHazardB==1'b1)
						begin
							if(MEM_islw==1'b1)
								forwardingMUXB<=2'b11;
							else
								forwardingMUXB<=2'b10;
						end	
					else
						forwardingMUXB<=2'b00;
					//------------------------------------
				end
		
			
		
		end

endmodule

嚴格的測試代碼

/////////////////////////////////////////////////
Memory[0] = 4;
Memory[4] = 0;
////////////////////////////////////////////////
start: lw $t0,0($zero)
add $t1,$t0,$t0
sub $t1,$t1,$t0
beq $t1,$t0,BEQ1
addi $t2,$t1,1
BEQ1: lw $t2,0($zero)
sw $t2,4($zero)
lw $t2,0($zero)
sw $t1,4($t2)
lw $t2,0($zero)
bne $t2,$t1,BNE1
addi $t2,$t1,2
BNE1:
j start

Xilinx ISE的詭異

用過這個軟件的人,我想都忘不了在它上面寫代碼和調試的痛苦。它會出各種詭異的事情。現在就列一下,我那些不是辦法的辦法。我主要使用的是ISE10.1版本,可能其他版本並無此問題,那就非常好了。

ISE 工程移動

所謂ISE工程移動,是指當在自己機器上編寫,到實驗室機器上刷到板子上,所遇到的一系列可能的問題。

  1. ISE中的一些部件竟然是記住文件的絕對位置的,當你移到另一臺機器上時,就有可能出現文件找不到的情況。解決方案:重新生成那個文件。
  2. 當你從ISE10.1移動到ISE11.1時,如果,使用了single port memory 那麼,你應該需要將single port memory 變成 block memory中的single port。因爲,前者已經被他放棄了。神奇的是如果,我不regenerate那個memory,ISE是不會報錯的,而且整一個綜合到生成bit文件都是沒有問題的,但是,一燒到板子上,就出現了詭異的事情。竟然,一部分代碼可以跑,另一部分代碼不能跑。

ISE仿真

multisim應該是個比ISE自帶的仿真器更好的選擇。

  1. 如果,ISE無法仿真,好像要打開一個webclient的服務就可以了。
  2. ISE仿真的時候,某次我發現仿真結果都是高阻態。但是,從邏輯上來說不應該。結果,在仿真輸出的地方看到了1條warning,意思是說有一個memory模塊找不到,所以沒有將其進行仿真,直接都輸出高阻態。一個模塊丟了,都可以仿真,真是神奇的邏輯。另外,其實,我那個模塊還在那個文件夾裏,只不過由於某種神奇的原因,它找不到了。我又新建了幾個memory模塊,有一部分找到了,一部分沒找到。前兩天我去仿真的時候,發現原來找不到的都可以找到了。神奇!

ISE綜合

  1. ISE會將沒有命名的符號當做wire,只有1位的wire。這個時候,很有可能因爲筆誤,大小寫寫錯了。但是,它會給出warning,所以,waring也是一個需要看的東西。
  2. 前不久遇到更加詭異的事情,仿真都已經正確的情況下。下載到板子上運行,就出現了一些詭異的現象,似乎只能從rs,rt寄存器中讀出0值。進行不斷嘗試後,發現綜合的warning裏面有,XXX is assigned,but doesn't connect to any ,so it will be trimmed之類的。我從代碼裏面看,那些線明明有連着其他線,但是,不管怎麼弄,它都不變。後來是不斷亂撞,終於衝出了迷宮。我是將cpu的部分都封裝在一個叫pipeline 的module,從這個module中拉出一些線來,用於外部LED的顯示,比如pc值。但同時,我還拉了另外一些線,regA,regB之類的,這個在top模塊中是用不上的,但是我在調試pipeline時,是會用上這些。本來這應該沒有什麼大問題。但是,似乎恰好和它的bug重疊了,它在trim wire的時候,不單單將top模塊中的那些多餘的線trim了,而且還將pipeline裏面的同名的線也trim掉了。所以,解決方案是,不要讓多餘的線通到top模塊中。

硬件仿真是怎麼做的

我不知道各位在用仿真的時候,有沒有想過這個問題——仿真是怎麼做到的。

我也是最近纔想到這個問題的。也想到了一個比較優雅的算法,類似design pattern中的chain of responsibility.

也是一個挺有趣的問題。

後記

雖然,我們絕大多數人在工作後是不會真的去寫CPU的,可能連用verilog的情況都不一定有,但是,這樣一個訓練既有意義(如果能理解這個,那麼DLP,TLP,又算什麼呢),也十分好玩的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章