現(xiàn)簡化CPU:取指-執(zhí)行全流程與存儲器設(shè)計(jì))
簡介這份課程設(shè)計(jì)文檔面向電子信息類專業(yè)學(xué)生及計(jì)算機(jī)組成原理學(xué)習(xí)者圍繞「簡化計(jì)算機(jī)系統(tǒng)設(shè)計(jì)」這一經(jīng)典實(shí)驗(yàn)課題提供從指令系統(tǒng)到處理器與存儲器聯(lián)調(diào)的完整實(shí)現(xiàn)思路。內(nèi)容涵蓋簡單指令集idle、load、move、addp、subp、mulp、logp、comp、sh、swap、jmp、jz、read、write、stop 等的編碼與執(zhí)行流程用 Verilog HDL 編寫處理器模塊調(diào)用 64×8 存儲器模塊并在頂層以原理圖方式連接成簡化計(jì)算機(jī)核心部件最后將指令序列存入存儲器分析取指、譯碼、執(zhí)行與存儲過程。資源包共 1 個 doc 文件約 384KB包含實(shí)驗(yàn)?zāi)康?、?shí)驗(yàn)內(nèi)容、預(yù)習(xí)要求與實(shí)驗(yàn)報(bào)告結(jié)構(gòu)并附有頂層 BLOCK 圖說明及 CPU 模塊的 Verilog 代碼節(jié)選可直接對照復(fù)現(xiàn)狀態(tài)機(jī)與寄存器設(shè)計(jì)。已有 382 人學(xué)習(xí)適合作為課程設(shè)計(jì)參考與 Verilog 實(shí)踐排錯思路的補(bǔ)充材料。1. 簡化計(jì)算機(jī)系統(tǒng)設(shè)計(jì)一份能跑通取指-執(zhí)行全流程的 Verilog 課設(shè)資源如果你正在做計(jì)算機(jī)組成原理的課程設(shè)計(jì)大概率會遇到一個尷尬局面課本上講清楚了馮·諾依曼結(jié)構(gòu)實(shí)驗(yàn)指導(dǎo)書也給了指令格式但真到動手寫處理器模塊時(shí)取指、譯碼、執(zhí)行這幾個狀態(tài)怎么串起來、存儲器什么時(shí)候讀、PC 什么時(shí)候加一全得自己摸索。這份資源就是沖著這個痛點(diǎn)來的——它用 Verilog HDL 實(shí)現(xiàn)了一個簡化處理器模塊配套 64×8 的存儲器模塊頂層把兩者連成完整的計(jì)算機(jī)核心部件系統(tǒng)指令序列寫進(jìn)存儲器后能完整跑出取指、譯碼、執(zhí)行、寫回的流程。適合電子信息類專業(yè)做《計(jì)算機(jī)組成實(shí)驗(yàn)》課程設(shè)計(jì)的學(xué)生也適合想從零理解 CPU 數(shù)據(jù)通路的手工派。下面按“指令系統(tǒng)怎么定、處理器怎么寫、存儲器怎么配、頂層怎么連、坑在哪”的順序拆開講。2. 指令系統(tǒng)與狀態(tài)機(jī)16 條指令怎么編碼、狀態(tài)怎么跳2.1 指令格式與操作碼分配這份設(shè)計(jì)的指令字長 16 位高 4 位IR[15:12]是操作碼低 12 位按指令類型拆成寄存器編號、移位模式或地址偏移。操作碼用parameter定義一共 16 個狀態(tài)值其中idle到stop覆蓋了數(shù)據(jù)傳送、算術(shù)運(yùn)算、邏輯運(yùn)算、移位、跳轉(zhuǎn)和訪存幾大類。先把這張表吃透后面寫狀態(tài)機(jī)才不會亂操作碼助記符功能關(guān)鍵字段0000idle空操作/復(fù)位態(tài)無0001load立即數(shù)裝入 R0IR[11:8] 為立即數(shù)0010move寄存器間傳送IR[11:8] 指定源/目的0011addp加法IR[11:10] 目的IR[9:8] 源0100subp減法同上0101mulp乘法結(jié)果跨兩個寄存器同上0110logp與/或/異或/取反IR[11:10] 運(yùn)算類型0111comp比較IR[11:10] 比較模式1000sh移位循環(huán)/算術(shù)/邏輯IR[11:10] 類型IR[9:8] 寄存器1001meth1自增/自減/加二/減二IR[11:10] 模式1010swap交換走訪存路徑1011jmp無條件跳轉(zhuǎn)地址在低 12 位1100jz零跳轉(zhuǎn)依賴狀態(tài)標(biāo)志1101read讀存儲器走訪存路徑1110write寫存儲器走訪存路徑1111stop停機(jī)狀態(tài)機(jī)鎖死這里有個容易忽略的點(diǎn)load的立即數(shù)只有 4 位所以能裝的常數(shù)范圍是 0 到 15。如果指令序列里需要更大的數(shù)得靠meth1反復(fù)自增或者用addp拼出來。我見過有人直接往load里塞 8 位數(shù)仿真出來高 4 位被截掉排查半天以為是寄存器位寬寫錯了。2.2 狀態(tài)機(jī)的兩段式寫法處理器內(nèi)部用status寄存器驅(qū)動整個執(zhí)行流程status_change塊在時(shí)鐘上升沿推進(jìn)狀態(tài)seq塊在時(shí)鐘下降沿做數(shù)據(jù)操作。這種“上升沿改狀態(tài)、下降沿動數(shù)據(jù)”的寫法在課設(shè)里很常見好處是狀態(tài)切換和數(shù)據(jù)更新錯開半個周期避免同一時(shí)刻讀寫沖突。狀態(tài)跳轉(zhuǎn)的核心邏輯是這樣的always (negedge reset or posedge clock) begin: status_change if (reset 1b0) status 0; else case (status) 0: status 1; // 取指 1: if (IR[15:12] stop) // 譯碼遇 stop 鎖死 status 1; else status 2; 2: case (IR[15:12]) // 判斷是否訪存類指令 swap, jmp, jz, read, write: status 3; default: status 0; endcase 3: if (IR[15:12] swap) // swap 單獨(dú)處理 status 0; else status 4; 4: status 5; 5: case (IR[15:12]) read: status 6; // read 多一個等待周期 default: status 0; endcase default: status 0; endcase end這段代碼的邏輯說明狀態(tài) 0 負(fù)責(zé)把存儲器數(shù)據(jù)打進(jìn)IR并讓 PC 加一狀態(tài) 1 做譯碼同時(shí)把PC送到MAR準(zhǔn)備取操作數(shù)狀態(tài) 2 到 5 是訪存類指令的公共路徑read因?yàn)橐却鎯ζ鲾?shù)據(jù)回來多占一個狀態(tài) 6。參數(shù)上要注意reset是低電平有效仿真時(shí)如果復(fù)位信號只給一個時(shí)鐘周期的低電平status能回到 0但seq塊里的寄存器不一定全清干凈——因?yàn)閟eq塊用的是negedge clock復(fù)位釋放和時(shí)鐘沿的先后關(guān)系會影響初始化結(jié)果。穩(wěn)妥做法是復(fù)位信號至少保持兩個完整時(shí)鐘周期。2.3 取指與 PC 更新的時(shí)序配合取指階段在status 0時(shí)執(zhí)行IR {M_data_in, 8b00000000}把存儲器輸出的 8 位數(shù)據(jù)放到指令寄存器高 8 位低 8 位補(bǔ)零同時(shí)PC PC 1b1。這里PC用的是阻塞賦值而IR用的是非阻塞賦值混用是有意為之PC要在當(dāng)前狀態(tài)內(nèi)立刻更新供后續(xù)MAR PC使用IR則等到時(shí)鐘沿統(tǒng)一生效。如果兩個都用非阻塞狀態(tài) 1 里讀到的PC還是舊值取操作數(shù)地址就會錯位。這個細(xì)節(jié)在課本上不會專門講但仿真波形一拉就能看出來——MAR比預(yù)期晚一個周期變化基本就是這里出的問題。3. 處理器模塊實(shí)現(xiàn)寄存器組、ALU 與訪存控制怎么落地3.1 寄存器組與數(shù)據(jù)通路的聲明方式seq塊內(nèi)部聲明了PC、R0到R3、累加器A、以及兩個臨時(shí)變量temp9 位和temp216 位。temp多一位是為了捕獲加法進(jìn)位temp2用于乘法結(jié)果的高低字節(jié)拆分。這些寄存器全部在always塊內(nèi)部用reg聲明屬于塊內(nèi)局部變量綜合工具會把它映射成觸發(fā)器。寫法上有個講究R0到R3用阻塞賦值而IR、MAR、MDR用非阻塞。前者是組合邏輯風(fēng)格的即時(shí)更新后者是時(shí)序邏輯的沿觸發(fā)更新兩套語義混在一個塊里初看別扭但在這個狀態(tài)機(jī)框架下能跑通——因?yàn)闋顟B(tài)推進(jìn)和數(shù)據(jù)操作分屬不同時(shí)鐘沿不會產(chǎn)生競爭。數(shù)據(jù)通路的寬度要盯緊M_address是 12 位對應(yīng) 64×8 存儲器的地址空間實(shí)際只用了低 6 位64 個單元高 6 位在頂層連接時(shí)要么接地要么懸空建議顯式接 0否則綜合工具可能報(bào)位寬不匹配的警告。M_data_in和M_data_out都是 8 位和寄存器組寬度一致算術(shù)運(yùn)算的溢出判斷靠temp[8] ^ temp[7]實(shí)現(xiàn)——最高進(jìn)位和次高進(jìn)位異或結(jié)果為 1 表示有符號溢出。這個判斷式在加法和減法里都用了減法走的是補(bǔ)碼加法路徑(~A) 1邏輯是對的但要注意A取反后位寬仍是 8 位加 1 產(chǎn)生的進(jìn)位由temp的第 9 位接住。3.2 算術(shù)與邏輯指令的執(zhí)行分支以加法addp為例執(zhí)行階段根據(jù)IR[11:10]選擇目的寄存器把寄存器值和累加器A做帶符號擴(kuò)展的加法addp: case (IR[11:10]) 2b00: begin temp ({R0[7], R0[7:0]}) ({A[7], A[7:0]}); R0 temp[7:0]; overflow temp[8] ^ temp[7]; end 2b01: begin temp ({R1[7], R1[7:0]}) ({A[7], A[7:0]}); R1 temp[7:0]; overflow temp[8] ^ temp[7]; end // R2、R3 分支結(jié)構(gòu)相同略 endcase{R0[7], R0[7:0]}是符號擴(kuò)展把 8 位有符號數(shù)擴(kuò)成 9 位再相加這樣temp[8]就是真正的符號位進(jìn)位。如果直接寫R0 AVerilog 會按 8 位運(yùn)算進(jìn)位丟失溢出標(biāo)志永遠(yuǎn)為 0。乘法mulp的結(jié)果是 16 位低 8 位存目的寄存器高 8 位存相鄰寄存器比如R0 * A的低字節(jié)進(jìn)R0、高字節(jié)進(jìn)R1這個約定要在指令序列設(shè)計(jì)時(shí)提前規(guī)劃好不然乘完發(fā)現(xiàn)高字節(jié)把別的數(shù)據(jù)覆蓋了。邏輯指令logp里有個小陷阱IR[9:8]為2b11的分支寫的是R1 R1 R1實(shí)際效果是保持R1不變看起來像占位符如果指令序列里誤用了這個編碼結(jié)果不會報(bào)錯但邏輯不對調(diào)試時(shí)容易漏掉。3.3 訪存指令的握手時(shí)序read和write走的是狀態(tài) 3 到 6 的路徑。write在狀態(tài) 3 把M_data_out賦給MDR狀態(tài) 4 拉高Write_Read信號狀態(tài) 5 撤銷read則在狀態(tài) 5 之后多等一個狀態(tài) 6讓存儲器數(shù)據(jù)穩(wěn)定后再打進(jìn)MDR。Write_Read是輸出端口高電平寫、低電平讀頂層連到存儲器的讀寫控制端。這里的關(guān)鍵參數(shù)是存儲器模型的讀延遲——如果用的 IP 核或行為模型有一個時(shí)鐘周期的讀延遲狀態(tài) 6 就是必須的如果存儲器是組合邏輯輸出地址一變數(shù)據(jù)立刻有效狀態(tài) 6 可以省掉但省掉后read和write的狀態(tài)路徑不對稱狀態(tài)機(jī)得改。我一般建議保留狀態(tài) 6多一個周期換時(shí)序確定性課設(shè)場景下不值得為省一個周期去冒險(xiǎn)。4. 存儲器模塊與頂層連接64×8 存儲器和原理圖頂層怎么配4.1 64×8 存儲器的初始化文件格式存儲器模塊用 64 個 8 位單元地址線 6 位頂層接M_address[5:0]數(shù)據(jù)線 8 位。初始化靠.mif文件格式是地址和數(shù)據(jù)的鍵值對每行一個單元DEPTH 64; WIDTH 8; ADDRESS_RADIX HEX; DATA_RADIX HEX; CONTENT BEGIN 00 : 10; 01 : 1F; 02 : 20; 03 : 30; 04 : 40; 05 : F0; END;DEPTH和WIDTH必須和存儲器實(shí)例化參數(shù)一致ADDRESS_RADIX用 HEX 時(shí)地址寫兩位十六進(jìn)制。指令序列按 16 位一條拆成兩個 8 位單元存放高字節(jié)在前、低字節(jié)在后取指時(shí)IR {M_data_in, 8b00000000}先拿高字節(jié)下一個周期再拿低字節(jié)拼成完整指令——等等這里要核對一下原設(shè)計(jì)的取指只在一個狀態(tài)里讀了一次 8 位數(shù)據(jù)說明指令是 8 位對齊存放的每條指令占一個存儲單元的高 8 位低 8 位留空或存操作數(shù)。如果指令序列里連續(xù)放多條指令地址要按 2 遞增00、02、04…奇數(shù)地址空著。這個約定在寫.mif時(shí)最容易翻車地址連續(xù)寫 00、01、02 會導(dǎo)致第二條指令的高字節(jié)被當(dāng)成低字節(jié)讀進(jìn)來仿真波形上IR的值和預(yù)期對不上但狀態(tài)機(jī)照跑不誤屬于典型的“能跑但結(jié)果錯”。4.2 頂層原理圖連接的端口映射頂層用原理圖方式連接處理器模塊和存儲器模塊的端口對應(yīng)關(guān)系如下處理器端口方向存儲器端口說明M_address[11:0]輸出address[5:0]高 6 位接 0低 6 位接地址M_data_out[7:0]輸出data_in[7:0]寫數(shù)據(jù)M_data_in[7:0]輸入data_out[7:0]讀數(shù)據(jù)Write_Read輸出wren高電平寫、低電平讀clock輸入clock全局時(shí)鐘reset輸入—只接處理器原理圖里要額外加一個輸入引腳接clock一個接resetoverflow接輸出引腳方便觀察。地址線高位接地的做法在原理圖上就是拉一個接地符號連到M_address[11:6]別懸空——懸空在高阻態(tài)仿真下可能被解析成 XMAR的值帶 X 后整個地址譯碼失效波形上一片紅。如果原理圖工具不支持總線分線就在處理器模塊內(nèi)部把M_address聲明成 12 位、實(shí)際只驅(qū)動低 6 位高 6 位賦 0頂層直接整條總線連過去省去分線的麻煩。4.3 指令序列設(shè)計(jì)與執(zhí)行流程驗(yàn)證指令序列要覆蓋取指、譯碼、執(zhí)行、寫回四個階段建議按“裝入立即數(shù) → 算術(shù)運(yùn)算 → 存結(jié)果 → 跳轉(zhuǎn) → 停機(jī)”的順序編排。一個能驗(yàn)證加法的最小序列00 : 10; // load R0, 立即數(shù) 0 02 : 1F; // load R0, 立即數(shù) 15覆蓋前值 04 : 30; // addpR0 R0 A 06 : E0; // write把結(jié)果寫回存儲器 08 : F0; // stop仿真時(shí)重點(diǎn)看三個信號status是否按 0→1→2→0 循環(huán)推進(jìn)PC是否在每條指令取指后加一M_address在訪存指令時(shí)是否指向預(yù)期單元。如果status卡在 1 不動檢查IR[15:12]是不是stop的編碼1111被誤讀如果PC跳變不規(guī)則看jmp/jz分支里PC的賦值是不是被后面的取指覆蓋了。驗(yàn)證通過的標(biāo)準(zhǔn)是給定指令序列R0到R3的終值和手工推算一致overflow在溢出場景下拉高stop執(zhí)行后status鎖死不再變化。5. 避坑與排查五個讓仿真結(jié)果對不上的典型問題5.1 現(xiàn)象IR 值始終為 0狀態(tài)機(jī)空轉(zhuǎn)原因通常是.mif文件沒加載成功或者存儲器實(shí)例化時(shí)初始化文件路徑寫錯。綜合工具對路徑大小寫敏感Windows 下寫的mem.mif到 Linux 環(huán)境可能找不到。解決方法是仿真前先單獨(dú)例化存儲器模塊給一個固定地址讀一次看data_out是否有值沒有就檢查文件路徑和$readmemh語句如果用的是行為模型。另外.mif里CONTENT段的關(guān)鍵字拼寫錯誤也會導(dǎo)致靜默失敗工具不報(bào)錯但存儲器全零。5.2 現(xiàn)象加法結(jié)果正確但 overflow 永遠(yuǎn)為 0前面提過根因是加法沒做符號擴(kuò)展temp只聲明了 8 位或者賦值時(shí)截?cái)嗔说?9 位。檢查temp的位寬聲明是不是reg [8:0]以及加法表達(dá)式里兩個操作數(shù)是否都做了{(lán)sign, value}擴(kuò)展。還有一種隱蔽情況overflow在seq塊開頭被清零overflow 1b0如果加法分支里的賦值被后面的分支覆蓋比如case沒寫default導(dǎo)致落空標(biāo)志就丟了。給每個算術(shù)分支都顯式賦值別依賴默認(rèn)值。5.3 現(xiàn)象read 指令讀回的數(shù)據(jù)比寫入晚一個周期這是存儲器讀延遲和狀態(tài)機(jī)不匹配的典型表現(xiàn)。如果存儲器模型是同步讀時(shí)鐘沿觸發(fā)輸出read的狀態(tài) 6 必須保留如果誤刪了狀態(tài) 6MDR在數(shù)據(jù)有效前就被采樣讀回的是上一個地址的內(nèi)容。排查方法是在read分支里加一個臨時(shí)輸出把status和M_data_in同時(shí)打到波形上看數(shù)據(jù)有效的時(shí)刻和MDR更新時(shí)刻差幾個周期。差一個周期就補(bǔ)一個狀態(tài)差半個周期說明存儲器是組合輸出反而要去掉狀態(tài) 6。5.4 現(xiàn)象頂層原理圖編譯報(bào)位寬不匹配M_address是 12 位存儲器地址是 6 位直接連線工具會警告。解決方式有兩種在處理器內(nèi)部把M_address改成 6 位但原設(shè)計(jì)的端口聲明是 12 位改端口要同步改所有引用或者在頂層加一個總線分線器取低 6 位、高位接地。我一般選后者不動底層模塊頂層多一個分線符號而已。注意分線后M_address[11:6]接地的那個網(wǎng)絡(luò)要命名不然原理圖網(wǎng)表里出現(xiàn)匿名網(wǎng)絡(luò)后續(xù)查信號對不上號。5.5 現(xiàn)象jz 跳轉(zhuǎn)條件永遠(yuǎn)不成立jz依賴狀態(tài)標(biāo)志判斷但原設(shè)計(jì)里status是 3 位、只用于狀態(tài)機(jī)推進(jìn)沒有獨(dú)立的零標(biāo)志寄存器。如果指令序列里用jz做循環(huán)控制得先確認(rèn)比較指令comp是否把結(jié)果寫進(jìn)了某個寄存器比如R0 1表示相等然后jz判斷的是這個寄存器而不是隱式標(biāo)志。常見錯誤是照搬課本上“零標(biāo)志位”的概念以為jz會自動讀 ALU 的輸出實(shí)際這份設(shè)計(jì)里沒有這條通路得靠comp顯式產(chǎn)生可判斷的值。改法是在comp分支里把比較結(jié)果寫進(jìn)固定寄存器jz分支讀同一個寄存器做條件。6. 進(jìn)階驗(yàn)證用波形對照表把每條指令跑穿課設(shè)驗(yàn)收時(shí)最怕的是“仿真能跑但說不清為什么對”。我的習(xí)慣是給每條指令建一張波形對照表把預(yù)期值和實(shí)測值并排放跑完一輪指令序列后逐條打勾。具體做法是在 testbench 里用$monitor打印status、PC、IR、R0到R3、overflow采樣時(shí)刻選在時(shí)鐘下降沿之后避開狀態(tài)切換的競爭窗口輸出重定向到日志文件。然后手工推算每條指令執(zhí)行后的寄存器值和日志逐行比對。這張表長這樣指令執(zhí)行前 R0預(yù)期 R0實(shí)測 R0overflow 預(yù)期實(shí)測load 150151500addp R0A15303000subp R0-A30151500mulp R0*A15低字節(jié)低字節(jié)——乘法那行故意留空因?yàn)榻Y(jié)果跨兩個寄存器得同時(shí)看R0和R1單列放不下。對照表跑完如果某條指令的實(shí)測值和預(yù)期差一個固定偏移大概率是取指時(shí)地址步進(jìn)錯了該加 2 加了 1如果差得毫無規(guī)律回去查.mif里指令字節(jié)的高低位順序。這套流程走一遍驗(yàn)收時(shí)被問到“這條指令為什么這么跳”能直接翻日志指給對方看比現(xiàn)場口算靠譜得多。從那以后我每次做狀態(tài)機(jī)類的課設(shè)都強(qiáng)制先把指令序列的預(yù)期波形手畫一遍再寫代碼畫不出來的地方就是設(shè)計(jì)沒想清楚的地方。希望這份拆解幫到你少在仿真波形里泡兩個晚上。本文還有配套的精品資源點(diǎn)擊獲取