在前天將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種。(之後我們都認爲+-*之間是沒有優先級的,先算誰都是靠括號來的。)
- ((a+b)+c)+d
- (a+(b+c))+d
- a+((b+c)+d)
- a+(b+(c+d))
- (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工程移動,是指當在自己機器上編寫,到實驗室機器上刷到板子上,所遇到的一系列可能的問題。
- ISE中的一些部件竟然是記住文件的絕對位置的,當你移到另一臺機器上時,就有可能出現文件找不到的情況。解決方案:重新生成那個文件。
- 當你從ISE10.1移動到ISE11.1時,如果,使用了single port memory 那麼,你應該需要將single port memory 變成 block memory中的single port。因爲,前者已經被他放棄了。神奇的是如果,我不regenerate那個memory,ISE是不會報錯的,而且整一個綜合到生成bit文件都是沒有問題的,但是,一燒到板子上,就出現了詭異的事情。竟然,一部分代碼可以跑,另一部分代碼不能跑。
ISE仿真
multisim應該是個比ISE自帶的仿真器更好的選擇。
- 如果,ISE無法仿真,好像要打開一個webclient的服務就可以了。
- ISE仿真的時候,某次我發現仿真結果都是高阻態。但是,從邏輯上來說不應該。結果,在仿真輸出的地方看到了1條warning,意思是說有一個memory模塊找不到,所以沒有將其進行仿真,直接都輸出高阻態。一個模塊丟了,都可以仿真,真是神奇的邏輯。另外,其實,我那個模塊還在那個文件夾裏,只不過由於某種神奇的原因,它找不到了。我又新建了幾個memory模塊,有一部分找到了,一部分沒找到。前兩天我去仿真的時候,發現原來找不到的都可以找到了。神奇!
ISE綜合
- ISE會將沒有命名的符號當做wire,只有1位的wire。這個時候,很有可能因爲筆誤,大小寫寫錯了。但是,它會給出warning,所以,waring也是一個需要看的東西。
- 前不久遇到更加詭異的事情,仿真都已經正確的情況下。下載到板子上運行,就出現了一些詭異的現象,似乎只能從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,又算什麼呢),也十分好玩的。