僅憑閱讀本文,您並不能學會如何用verilog實現單周期CPU,但是您的收獲可能有:知道怎么實現是麻煩的,知道麻煩的后果是什么,了解一種比較好的實現思路,了解課上測試的形式與內容。
PS:本人還沒死透,雖然在P3獻出了首掛,但仍可一搏,拖更的原因是,我第一遍寫代碼又寫復雜了,雖然能過,但是為了課上方便修改,所以又重寫了一遍(人不能沒有從頭再來的勇氣)
這里會粗略介紹搭建過程,重點介紹P4與P3在實現上的區別、我踩過的坑、第一次寫時代碼的缺陷以及第二次編寫時的優化點,希望能拿來警醒自己,也希望能給各位的設計提供一些可能的優化方案。
想學習單周期CPU理論的,請移步課本/課件,本人自知理論功底淺薄,且理論並非一篇文章就能講明白的,所以此處分享內容更偏重實現。
搭建中的注意點:
實現基本功能的關鍵在於兩表的填寫,實現的復雜度取決於對P3電路的翻譯方式。兩表,即數據通路表以及指令-信號真值表。在P3中,我們已經至少寫過一遍兩表了,這里不再多說表的填寫方式了。值得注意的是,與P3稍有不同,P3中我們可以嘗試先寫一條R型指令的所有信息,然后連接一下數據通路,然后再選I,J型的連接一下,造出來基本的框架,但P4推薦把表完全寫完之后再去寫代碼,理由如下:代碼描述電路不如直接連線那么直觀,如果想要在原有的數據通路上做修改的話,需要自己“耳聰目明”+“命名合理”,才能在修改的時候能夠知道該改哪條導線;如果列完表再去寫代碼的話,由於各個輸入輸出端口到底有哪些來源,需要怎樣的控制信號,都已經完全確定了,故節省了在修改上所花費的時間,並且從實際效果來看,可以節省在mips.v(頂層)定義的導線條數(不推薦再嘗試逐步修改實現所有指令的方法了,有時間不如看看流水線)
為了輔助寫代碼,看着P3的電路圖是一個方法,但是,前面說過了,代碼實現復雜度取決於翻譯方式!再次掏出P3電路說事兒:
我前段時間自認為連得還不算復雜,課上的時候改動也不大,於是我就使用了鐵憨憨式翻譯法。
鐵憨憨式翻譯法(錯誤!):兄弟們,看到這個電路里的導線了嗎?他們有五六十根,我們只要把所有模塊搭建好,把所有導線都取上名字,然后對應連起來,就好了,這導致的車禍現場是這樣的:
上圖為部分mips.v文件中的導線定義,所有導線定義有接近兩屏,可以自行想象。
雖然我使用了“前一元件 to 后一元件”的命名方法,但是,我第一次是邊造表邊連接的,所以很多導線在加指令的時候需要改名字,這樣改着改着,導線定義就弄了六十多行,連完之后出了bug懷疑人生,這可怎么改?事實上還是可以改的,合理的添加斷點,查看中間變量以及逐條測試指令可以解決這一問題,最終還是奇跡般的過了,然而我估計自己並不敢上課對這樣一坨“龐然大物”動手動腳。這個設計無疑是失敗的。首先,我這個電路圖就不好,比如這里:
NPC有多少種可能性呢?目前支持的指令中,無非是pc+4, pc+4+(offset<<2), {pc+4[31:28],25-0,00}, $寄存器。這四個東西我居然用了3個選擇器實現選擇(這樣的原因是從功利的角度來看,真正連接電路的時候特別好改,易於過課上),這會增加很多導線的定義,也是一種資源的浪費。很顯然的一種改進方式是用4選一多路選擇器,控制信號改為兩位的,這樣節省了導線,也減少了控制信號個數。
其次,我采用了邊列表邊寫代碼的方式,前面已經說了,這樣會有很多改動,對於不直觀且導線特別多的代碼來說,修改是災難性的,有時雖有注釋,但搞不清導線的真正意義,有時會重定義,對於多路選擇器,如果命名不好的話,也不知道該實例化哪一個。
找到失敗原因之后,第二次中我采用了如下的翻譯方法:
合並2選1多路選擇器,並將多路選擇器封裝到主干模塊內部的翻譯法(仍不夠優):先放一小段代碼以舉例子:

`timescale 1ns / 1ps module ALU( input [31:0] A, input [31:0] B_from_grf, input [31:0] B_from_ext, input [1:0] ALUsrc, //為了擴展方便,所以改成了兩位 input [3:0] ALUOp, output reg [31:0] result, output reg zero ); //0000:加法, 0001:減法, 0010:或運算, 0011:比較運算 wire [31:0] B; wire [31:0] maybe_b[3:0]; //為了簡化,把所有選擇的數據存到數組中,方便直接通過選擇信號直接選出來 assign maybe_b[0]=B_from_grf; assign maybe_b[1]=B_from_ext; assign B=maybe_b[ALUsrc]; always @ (*) begin case(ALUOp) 4'b0000:begin result<=A+B; zero<=0; end 4'b0001:begin result<=A-B; zero<=0; end 4'b0010:begin result<=A|B; zero<=0; end 4'b0011:begin if(A-B==0)begin zero<=1; end else begin zero<=0; end end endcase end endmodule
不像傳統的ALU,我把ALU不同的B輸入來源都作為單獨的輸入,並把ALUsrc信號也作為輸入。在ALU中,開辟了一塊可以存儲這幾種ALU的B端口的可能輸入情況的空間,用ALUsrc選擇,然后選出作為B的操作數,這樣相當於把多路選擇器封裝到ALU里面了,用模塊內的assign實現減少頂層導線的目的,也不用費盡心思去想怎么定義MUX才能知道這個MUX是干啥的。事實上,如果我們的MUX是以選擇哪些地方的東西為標准命名的(比如這里的MUX名字命名為ALUB_select),而不是以它的每個輸入是多少位,輸出多少位,多少輸入為標准命名的話(比如這里是32位輸入,32位輸出,命名為mux32_to_32,這樣),放在頂層更好(因為保證了ALU功能的單一性:計算,沒有雜糅選擇功能,加指令的時候也只需要改動mux,不需要改alu的輸入口個數)。
上述方法依然不夠優,因為它把ALU弄成了一個四不像的東西。事實上,在做完P5之后,會發現一個更好的實現方法是:將小的模塊與模塊再次封裝。比如ALU及其兩個輸入時用於選擇的n選1MUX,我們可以將這三個東西再打包一次,弄一個新的模塊(比如叫它為E模塊),這樣既避免了ALU內嵌mux而耦合嚴重,也因引入了E模塊而簡化了頂層mips.v的布線。類似地各位也可以將GRF以及它的輸入端的幾個MUX封裝成新模塊。
易錯點:
1.非阻塞賦值與display的內容不相符
評測機是通過看我們display的東西和它需要的一樣不一樣來評判我們是否正確的。如果采用非阻塞賦值,緊接着來一句display的話,由於非阻塞賦值是在過程塊結束時才統一賦值的,所以輸出的東西是未修改的。
2.位寬
對於0x00003000,它的位寬是32位,所以二進制寫法是32'b00000000000000000011000000000000,省事寫16進制的話,是32'h00003000,這里的32指的是位寬,而不是這個數在某種進制下有幾位!
3.jr跳轉
jr不只是可以跳31號寄存器存的地址,任何寄存器存儲的地址它都可以跳!這是室友P4課上遭遇的車禍現場,課下弱測並沒有測試出來!請務必檢查jr是否寫對了!
4.還是display
需要輸出32位寬的內存地址,不是【11:2】(10位)地址,請大家看好自己的輸出!和教程中提供的輸出比對一下,應該就會發現。
5.Reset常見誤區
首先是同步復位,這個老生常談了,可以參考我以前的博客。之后就是reset的地方夠不夠,對不對,我們需要reset的地方是程序計數器PC,寄存器堆GRF,數據存儲器DM,除此之外,如果發現自己在其他地方也使用了reset,就得好好斟酌一下,這里到底需不需要reset?比如,我發現自己在IM中加了一個reset,本地測試過了,提交沒輸出,為何?經過了一天的思考和從各方獲取經驗,我隱約意識到可能是復位出了問題,IM里面為什么需要reset?是防止PC值沒有變成0x00003000嗎?我最開始的確是這么想的,所以在里面加了reset,但是,后來我發現,我reset的並不是輸入進來的PC,而是,指令存儲空間!!也就是說,考慮到評測機最開始上來就reset一下,我讀入的code直接沒了,當然之后沒有輸出。為何本地沒有測出來問題?因為我沒有檢查reset,只是保持reset=0跑的程序。這個點卡了我一天多,也卡了評論區不少同學好長時間。還有類似的問題如輸出比正常的慢一周期的,也請看看reset。這樣,關於reset的坑差不多就介紹完了。
關於debug:
Verilog教程部分的視頻建議重新看一遍,學一學如何加斷點,如何加入中間變量作為信號,這些對於我們追溯bug的來源很有幫助,比如我就用這個方法逐步追溯一個跳轉bug,先在alu里面加斷點,填加中間變量看zero是多少,發現不符合預期,並發現ALU的參與計算的AB不是自己想要的,然后我又加斷點到GRF,成功發現自己把一條線連錯了導致傳入ALU的值不對。這里只是舉了一個簡單的例子,其他bug可以用這個方法類似解決。
另外,合理地設計測試程序也是很重要的,首先就是別好幾條指令一起測,一次我們就測一條指令,比如先充分測ori,然后有了ori之后,測加減法,有了加減法之后測跳轉以及其他的指令,最后可以再寫一個綜合測試程序把所有的都測一遍。不要一起測!!不瞞您說,我連加法都寫錯了,開始用綜合測試程序測的,測到的我的beq沒執行,然后我就一直看beq,好長時間之后才發現是我加法有問題,導致了值不符合分支條件。希望大家能引以為鑒。
更新:剛才看討論區的時候,看到了所謂的implement debug法,這個辦法我也曾經誤觸發過,挺有用的,可以查到很多因為筆誤寫出的bug。具體操作是:雙擊Synthesize-XST(就在語法檢測旁邊,所以我當時點錯誤觸發過這個技能),或者在最上方點擊綠色頭朝右的三角形,然后等着,看下面的warning和error,復制到IDE或者記事本上一條一條看,對着改基本上就能解決很多問題。
關於命名:
記住一件事:采用from_to(從某模塊的某接口到另一模塊的某接口)式命名看起來雖然很嚴謹很nb,但是CPU中並非某個接口只連接另外的一個接口,所以這樣命名是一場災難!就會像我一樣在P5中耗費巨大的精力重寫代碼規范命名!
本文就分享到這里吧,祝大家好運連連,AK P4