# 從零開始的RISC-V SoC架構設計與Linux核心運行 - 硬體篇 :::success - **RISC-V SoC數位電路Verilog原始碼:[RISC-V_SoC](https://github.com/yutongshen/RISC-V_SoC)** ::: ## 前言 - 你曾經想過家裡電腦是怎麼跑起來的嗎?為什麼這些矽與金屬通電之後可以做出這麼多事情呢?為什麼電腦可以讓我們這麼方便的打遊戲、看影片、通訊以及文書處理呢?有沒有想過自己也能DIY做出一部電腦呢?過去的我是土木工程背景出身的一個小公僕,原本都是在下班時間才會寫程式的我,隨著資訊領域的知識學得越多,越覺得自己還有更多東西要學,在一次機緣下果斷辭去公務人員,回到了學校研讀資訊工程,逐漸地我慢慢瞭解了甚麼是邏輯設計、甚麼是計算機組織、甚麼是作業系統、甚麼是演算法、…,但上課教的東西總是死的,學校的教育只教這些零散的個體各自怎麼運作,但不曾教過怎麼組合成一隻完整的系統。所以我嘗試要把過去在資訊工程所我所學的每一個學科拼湊組合成一部能夠運作的機器。以下透過介紹我的設計,希望可以帶給有興趣的朋友們一些收穫。 - 我的硬體架構是以RISC-V CPU為核心的SoC,可運行Linux 4.20。硬體部分是由verilog硬體描述語言寫出來的,Bus採用AXI及APB為主(還有一些客製化的coherence訊號),周邊的控制暫存器規格、地址映射也大多是自己客製化設計的,所以原本的linux driver都無法支援,必須自己寫出對應的driver才能讓linux去控制我這些客製化的周邊(如UART、SPI等) <!-- - 另外如果你是完全新手或非相關領域的話,我整理了一些主題在底下連結,希望如果對計算機科學有興趣的朋友們可以零基礎的學習到數位電路設計、計算機架構、匯流排架構、周邊設備規格、作業系統、Linux驅動程式設計。裡面我會盡量用一般沒有相關背景也能看得懂的方式來呈現出來,如大家有甚麼建議也都可以回饋給我。(有些主題還沒整理好,之後會陸陸續續補充上去) - [電晶體工作原理與數位電路上的應用](https://hackmd.io/@w4K9apQGS8-NFtsnFXutfg/HJ6mJ-1Go) - [數位邏輯設計(Digital IC design)](https://hackmd.io/@w4K9apQGS8-NFtsnFXutfg/HyBty-1zo) - [計算機架構(Computer Architecture)](https://hackmd.io/@w4K9apQGS8-NFtsnFXutfg/r1EjJZyzj) - 工作原理 - 指令集架構(Instruction Set Architecture) - 暫存器(General-Purpose Register and Control Status Register) - 中斷及異常(Interrupt and Exception) - 匯流排架構(Bus Fabric) - 虛擬記憶體(Virtual Memory) - 快取記憶體(Cache) - 周邊設備(Peripheral) - UART (Universal Asynchronous Receiver/Transmitter) - SPI (Serial Peripheral Interface) - DAP (Debug Access Port) - 除錯工具開發(Debug tool) - Linux設備驅動程式開發(Linux Device Driver) --> - 在進入正題之前,先展示一下目前的成果: - 電路架構:藍框內的全部數位電路都只有自己一人團隊規劃設計實作,效能應該都有滿大的改善空間 ![](https://hackmd.io/_uploads/H1fCInMVh.png) <!---![](https://i.imgur.com/0lQsCVg.png)--> - 電路板接線:我的FPGA環境是用PYNQ-Z2。然後我把板子上的原有的ARM CPU完全捨棄掉,改用自己的RISC-V CPU來運行 ![](https://i.imgur.com/aYGA96b.jpg) - 完整的開機畫面 <iframe width="560" height="315" src="https://www.youtube.com/embed/LUEj9g0ChXA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> :::info - 畫面左邊為電路板透過UART傳送到Host電腦上打印出來的訊息,亦可以透過這個窗口由Host往電路板傳送訊息 - 畫面右邊為自己用Visual C#寫出來的Debugger UI,這個窗口是透過JTAG介面跟電路板進行通訊,可以透過這個窗口觀察或覆寫SoC上記憶體或暫存器數據,或是強制CPU進Halt來操控CPU單步執行Debugger送出之指令。還有SoC上0x0\~0x2000地址本應該是用ROM(只讀記憶體)來實做,但為了實驗開發方便,先以SRAM取代,至於0x0\~0x2000地址的ROM code可以透過Debugger download進去電路裡面。 ::: - 在RISC-V CPU上執行GCC編譯hello world程式 ![](https://i.imgur.com/puUd206.png) ## 硬體架構設計介紹 ### CPU架構 ![](https://i.imgur.com/t6aZQ7M.png) :::info - 上圖是我的CPU內核的架構設計,其設計概念對於有讀過計算機組織的朋友們應該不陌生,這就是五級管線化的架構,即為上圖綠色方塊的單元,由左(IFU)而右(WBU)共規劃五級管線化設計。 - **第一級**為指令索取(Instruction Fetch),負責根據PC (Program Counter)來去索取CPU該執行的二進制指令。 - **第二級**為指令解碼(Instruction Decode),負責處理指令分析、讀取通用或控制狀態暫存器(GPR & CSR)及產生控制訊號。 - **第三級**為執行(Execute),負責算術處理及中斷/異常處理,分支(Conditional Branch)處理也會在這個階段處理。 - **第四級**為記憶體存取(Memory Access),負責處理數據往外部記憶體的讀寫,也負責處理原子操作(Atomic Operation)。 - **第五級**為寫回(Write Back),如有需要會在這裡將通用暫存器(GPR)進行更新。 ::: #### Prefetch Unit 實現了預先讀取指令,讓指令解碼跟執行可以與指令讀取脫鉤,不受後面級數的管線化堵塞而影響效能,單元內配有FIFO架構,可以一次向Instruction Cache或記憶體索取一整批的指令,如果遇到Access Fault或是Page Fault會把Fault推進`flag_fifo`內,而指令則推進`data_fifo`。如IDU有空閒時就會把`pop`訊號舉起,從FIFO內取出指令。另外,遇到branch或是`fence.i`指令或是特權等級切換或是更新`satp`/`misa`控制狀態暫存器時會清空FIFO避免後面讀到不正確的指令。 ```verilog= module pfu ( input clk, input rstn, input flush, input [ `XLEN - 1:0] bootvec, input jump, input [`IM_ADDR_LEN-1:0] jump_addr, input pop, output logic jump_taken, output logic [`IM_ADDR_LEN-1:0] pc, output logic [`IM_ADDR_LEN-1:0] badaddr, output logic [`IM_DATA_LEN-1:0] inst, output logic [ 1:0] bad, output logic empty, // Inst Memory output logic imem_req, output logic [`IM_ADDR_LEN-1:0] imem_addr, input [ `XLEN-1:0] imem_rdata, input [ 1:0] imem_bad, input imem_busy ); ... always_ff @(posedge clk or negedge rstn) begin if (~rstn) begin data_fifo <= {16*`PFU_FIFO_DEPTH{1'b0}}; flag_fifo <= { 2*`PFU_FIFO_DEPTH{1'b0}}; end else if (fifo_wr) begin data_fifo[{wptr, 4'b0}+: `XLEN] <= imem_rdata; flag_fifo[{wptr, 1'b0}+:`XLEN/8] <= {`XLEN/16{imem_bad}}; end end ... endmodule ``` :::info - `fence.i`指令效果為軟體請求Instuction/Data同步,就是如果在IDU裡面即將要改寫隨後的指令時,那麼要能確保隨後的指令必須要是更新後的新指令。比方說原本在地址0x104存放的指令為**A**,而IDU此時正在執行PC=0x100的指令(假設指令長度為4 byte),其指令是要將存放在0x104地址的指令改為**B**,這時很有可能地址0x104早已進入管線裡面,且指令為**A**,因此為了確保0x104是**B**指令,管線裡面0x104以後的指令就必須清除,重新讀取。 - `satp`控制狀態暫存器,如下圖,這顆暫存器決定CPU在Supervisor mode及User mode時應該使用何種分頁模式(MODE),及存放的頁表實體地址(PPN)。倘若這個暫存器被更新,意味著原本的虛擬地址可能不再相同,必須做出適當的處理避免prefetch unit及BTB給出不預期的輸出 ![](https://i.imgur.com/8BRPQQc.png) - `misa`控制狀態暫存器,如下圖,這顆暫存器可以決定CPU為RV64或者RV32的組態(MXL)。另外,也可以決定CPU支援何種Extension,比方說bit [2]為1時,為開啟RVC(C Extension),這允許PC對齊2 (意思是PC可以為2/4/6/8..),但如果bit [2]為0時,PC就必須對齊4 (如4/8/12/16...),否則就會觸發Fetch misaligned的exception ![](https://i.imgur.com/ELzEVmV.png) ::: #### BTB (Branch Target Buffer) 輔助分支預測,其會記錄過去從經branch taken的target,當如果再次踩到這個分支指令時,會直接jump到上次的target的位置。這邊設計採用fully associativity,使用當前PC值作為`tag`來查詢buffer中是否存在曾經在這PC發生分支的紀錄。如果遇到`fence.i`指令或是特權等級切換或是更新`satp`/`misa`控制狀態暫存器時,將會清除buffer紀錄。 ```verilog= module btb ( input clk, input rstn, input flush, input [`IM_ADDR_LEN-1:0] pc_in, output logic [`IM_ADDR_LEN-1:0] pc_out, output logic taken, input wr, input [`IM_ADDR_LEN-1:0] addr_in, input [`IM_ADDR_LEN-1:0] target_in ); `define BTB_ENTRY 4 logic [ `BTB_ENTRY-1:0] valid; logic [ `IM_ADDR_LEN-1:0] tag [`BTB_ENTRY]; logic [ `IM_ADDR_LEN-1:0] target [`BTB_ENTRY]; logic [$clog2(`BTB_ENTRY)-1:0] wptr; always_ff @(posedge clk or negedge rstn) begin integer i; if (~rstn) begin for (i = 0; i < `BTB_ENTRY; i = i + 1) begin valid [i] <= 1'b0; tag [i] <= `IM_ADDR_LEN'b0; target[i] <= `IM_ADDR_LEN'b0; end wptr <= {$clog2(`BTB_ENTRY){1'b0}}; end else begin if (flush) begin for (i = 0; i < `BTB_ENTRY; i = i + 1) begin valid [i] <= 1'b0; end end else if (wr) begin valid [wptr] <= 1'b1; tag [wptr] <= addr_in; target[wptr] <= target_in; wptr <= wptr + {{$clog2(`BTB_ENTRY)-1{1'b0}}, 1'b1}; end end end always_comb begin integer i; pc_out = `IM_ADDR_LEN'b0; token = 1'b0; for (i = 0; i < `BTB_ENTRY; i = i + 1) begin pc_out = pc_out | ({`IM_ADDR_LEN{valid[i] && tag[i] == pc_in}} & target[i]); token = token | (valid[i] && tag[i] == pc_in); end end endmodule ``` #### IDU (Instruction Decode Unit) 負責解讀指令產生控制訊號、產生立即值(Immediate)、產生讀取及寫入通用暫存器及控制狀態暫存器之地址,在我的設計中有實現RV32/64IMAC指令集擴充,如下所列: ![](https://i.imgur.com/UVJW4hf.png) ![](https://i.imgur.com/5TR4rD6.png) ![](https://i.imgur.com/EI5AvpB.png) ![](https://i.imgur.com/TvzzALd.png) ![](https://i.imgur.com/DVPN3pb.png) ![](https://i.imgur.com/fUuU3N0.png) :::info - rd是以GPR的地址共5位元(0~31)來表達指令即將更新的是哪顆GPR - rs1、rs2是以GPR的地址共5位元(0~31)來表達指令是用哪顆GPR來當成運算元 - imm為立即值作為運算元 - 以`ADD x1, x2, x3`為例,rd欄位會是1,rs1欄位會是2,rs2欄位會是3,如下表示 ``` | 31 25 | 24 20 | 19 15 | 14 12 | 11 07 | 06 00 | +------------+---------+---------+------------+--------+------------+ | funct7 | rs2 | rs1 | funct3 | rd | opcode | +------------+---------+---------+------------+--------+------------+ 0000000 00011 00010 000 00001 0110011 add x1, x2, x3: 0x003100b3 ``` 指令的效果是讀取第2顆GPR及第3顆GPR的數據,進行相加(x2 + x3),把計算結果寫回第1顆GPR ::: #### GPR (General-Purpose Register) 以RISC-V Spec規範了以下這32顆通用暫存器,並與軟體層約定好function call時,呼叫者(caller)與被呼叫者(callee)應該要負責維護保存那些暫存器的數據。除了x0為永遠恆0以外,其他x1~x31都是可讀可寫的暫存器 | Register | ABI Name | Description | Saver | |:--------:|:--------:|:----------------------------------|:------:| | x0 | zero | Hard-wired zero | — | | x1 | ra | Return address | Caller | | x2 | sp | Stack pointer | Callee | | x3 | gp | Global pointer | — | | x4 | tp | Thread pointer | — | | x5 | t0 | Temporary/alternate link register | Caller | | x6–7 | t1–2 | Temporaries | Caller | | x8 | s0/fp | Saved register/frame pointer | Callee | | x9 | s1 | Saved register | Callee | | x10–11 | a0–1 | Function arguments/return values | Caller | | x12–17 | a2–7 | Function arguments | Caller | | x18–27 | s2–11 | Saved registers | Callee | | x28–31 | t3–6 | Temporaries | Caller | ```verilog= module rfu ( input clk, input rstn, input [ 4:0] rs1_addr, input [ 4:0] rs2_addr, output logic [`XLEN - 1:0] rs1_data, output logic [`XLEN - 1:0] rs2_data, input wen, input [ 4:0] rd_addr, input [`XLEN - 1:0] rd_data ); integer i; logic [`XLEN - 1:0] gpr [32]; always_ff @(posedge clk or negedge rstn) begin if (~rstn) begin for (i = 0; i < 32; i = i + 1) begin gpr[i] <= {`XLEN{1'b0}}; end end else begin if (wen & |rd_addr) begin gpr[rd_addr] <= rd_data; end end end assign rs1_data = gpr[rs1_addr]; assign rs2_data = gpr[rs2_addr]; endmodule ``` #### CSR (Control and Status Register) 我的設計將CSR歸納為以下四類,各別放在不同module裡: - 與Performance Management相關的歸納在PMU內 ```verilog= module pmu ( ... // CSR interface input csr_wr, input [ 11:0] csr_waddr, input [ 11:0] csr_raddr, input [`XLEN-1:0] csr_sdata, input [`XLEN-1:0] csr_cdata, output logic [`XLEN-1:0] csr_rdata ); ... always_comb begin csr_rdata = `XLEN'b0; case (csr_raddr) `CSR_CYCLE_ADDR: csr_rdata = mcycle [ 0+:`XLEN]; `CSR_CYCLEH_ADDR: csr_rdata = mcycle [32+: 32]; `CSR_TIME_ADDR: csr_rdata = mtime [ 0+:`XLEN]; `CSR_TIMEH_ADDR: csr_rdata = mtime [32+: 32]; `CSR_INSTRET_ADDR: csr_rdata = minstret [ 0+:`XLEN]; `CSR_INSTRETH_ADDR: csr_rdata = minstret [32+: 32]; `CSR_SCOUNTEREN_ADDR: csr_rdata = scounteren; `CSR_MCOUNTEREN_ADDR: csr_rdata = mcounteren; `CSR_MCYCLE_ADDR: csr_rdata = mcycle [ 0+:`XLEN]; `CSR_MCYCLEH_ADDR: csr_rdata = mcycle [32+: 32]; `CSR_MINSTRET_ADDR: csr_rdata = minstret [ 0+:`XLEN]; `CSR_MINSTRETH_ADDR: csr_rdata = minstret [32+: 32]; `CSR_MHARTID_ADDR: csr_rdata = mhartid; endcase end endmodule ``` - 與Memory Management相關的歸納在MMU內 ```verilog= module mmu_csr ( ... // CSR interface input csr_wr, input [ 11:0] csr_waddr, input [ 11:0] csr_raddr, input [ `XLEN-1:0] csr_sdata, input [ `XLEN-1:0] csr_cdata, output logic [ `XLEN-1:0] csr_rdata ); ... always_comb begin csr_rdata = `XLEN'b0; case (csr_raddr) `CSR_SATP_ADDR : csr_rdata = satp; endcase end endmodule ``` - 與Memory Protection相關的歸納在MPU內,其中PMA相關的CSR(`pmaaddr`、`pmacfg`),是我自己客製化的CSR,用途是在決定實體地址是否為Cacheable、是否支援Exclusive Access ```verilog= module mpu_csr ( ... // CSR interface input csr_wr, input [ 11:0] csr_waddr, input [ 11:0] csr_raddr, input [`XLEN-1:0] csr_sdata, input [`XLEN-1:0] csr_cdata, output logic [`XLEN-1:0] csr_rdata ); ... always_comb begin csr_rdata = `XLEN'b0; case (csr_raddr) `CSR_PMPCFG0_ADDR : csr_rdata = pmpcfg0[0+:`XLEN]; `CSR_PMPCFG1_ADDR : csr_rdata = pmpcfg0[63:32]; `CSR_PMPCFG2_ADDR : csr_rdata = pmpcfg2[0+:`XLEN]; `CSR_PMPCFG3_ADDR : csr_rdata = pmpcfg2[63:32]; `CSR_PMPADDR0_ADDR : csr_rdata = pmpaddr[0 ] & ({`XLEN{pmpcfg_a[0 ][1]}} | ~(pmpaddr[0 ] & ~(pmpaddr[0 ] + `XLEN'b1))); `CSR_PMPADDR1_ADDR : csr_rdata = pmpaddr[1 ] & ({`XLEN{pmpcfg_a[1 ][1]}} | ~(pmpaddr[1 ] & ~(pmpaddr[1 ] + `XLEN'b1))); `CSR_PMPADDR2_ADDR : csr_rdata = pmpaddr[2 ] & ({`XLEN{pmpcfg_a[2 ][1]}} | ~(pmpaddr[2 ] & ~(pmpaddr[2 ] + `XLEN'b1))); `CSR_PMPADDR3_ADDR : csr_rdata = pmpaddr[3 ] & ({`XLEN{pmpcfg_a[3 ][1]}} | ~(pmpaddr[3 ] & ~(pmpaddr[3 ] + `XLEN'b1))); `CSR_PMPADDR4_ADDR : csr_rdata = pmpaddr[4 ] & ({`XLEN{pmpcfg_a[4 ][1]}} | ~(pmpaddr[4 ] & ~(pmpaddr[4 ] + `XLEN'b1))); `CSR_PMPADDR5_ADDR : csr_rdata = pmpaddr[5 ] & ({`XLEN{pmpcfg_a[5 ][1]}} | ~(pmpaddr[5 ] & ~(pmpaddr[5 ] + `XLEN'b1))); `CSR_PMPADDR6_ADDR : csr_rdata = pmpaddr[6 ] & ({`XLEN{pmpcfg_a[6 ][1]}} | ~(pmpaddr[6 ] & ~(pmpaddr[6 ] + `XLEN'b1))); `CSR_PMPADDR7_ADDR : csr_rdata = pmpaddr[7 ] & ({`XLEN{pmpcfg_a[7 ][1]}} | ~(pmpaddr[7 ] & ~(pmpaddr[7 ] + `XLEN'b1))); `CSR_PMPADDR8_ADDR : csr_rdata = pmpaddr[8 ] & ({`XLEN{pmpcfg_a[8 ][1]}} | ~(pmpaddr[8 ] & ~(pmpaddr[8 ] + `XLEN'b1))); `CSR_PMPADDR9_ADDR : csr_rdata = pmpaddr[9 ] & ({`XLEN{pmpcfg_a[9 ][1]}} | ~(pmpaddr[9 ] & ~(pmpaddr[9 ] + `XLEN'b1))); `CSR_PMPADDR10_ADDR: csr_rdata = pmpaddr[10] & ({`XLEN{pmpcfg_a[10][1]}} | ~(pmpaddr[10] & ~(pmpaddr[10] + `XLEN'b1))); `CSR_PMPADDR11_ADDR: csr_rdata = pmpaddr[11] & ({`XLEN{pmpcfg_a[11][1]}} | ~(pmpaddr[11] & ~(pmpaddr[11] + `XLEN'b1))); `CSR_PMPADDR12_ADDR: csr_rdata = pmpaddr[12] & ({`XLEN{pmpcfg_a[12][1]}} | ~(pmpaddr[12] & ~(pmpaddr[12] + `XLEN'b1))); `CSR_PMPADDR13_ADDR: csr_rdata = pmpaddr[13] & ({`XLEN{pmpcfg_a[13][1]}} | ~(pmpaddr[13] & ~(pmpaddr[13] + `XLEN'b1))); `CSR_PMPADDR14_ADDR: csr_rdata = pmpaddr[14] & ({`XLEN{pmpcfg_a[14][1]}} | ~(pmpaddr[14] & ~(pmpaddr[14] + `XLEN'b1))); `CSR_PMPADDR15_ADDR: csr_rdata = pmpaddr[15] & ({`XLEN{pmpcfg_a[15][1]}} | ~(pmpaddr[15] & ~(pmpaddr[15] + `XLEN'b1))); `CSR_PMACFG0_ADDR : csr_rdata = pmacfg0[0+:`XLEN]; `CSR_PMACFG1_ADDR : csr_rdata = pmacfg0[63:32]; `CSR_PMACFG2_ADDR : csr_rdata = pmacfg2[0+:`XLEN]; `CSR_PMACFG3_ADDR : csr_rdata = pmacfg2[63:32]; `CSR_PMAADDR0_ADDR : csr_rdata = pmaaddr[0 ] & ({`XLEN{pmacfg_a[0 ][1]}} | ~(pmaaddr[0 ] & ~(pmaaddr[0 ] + `XLEN'b1))); `CSR_PMAADDR1_ADDR : csr_rdata = pmaaddr[1 ] & ({`XLEN{pmacfg_a[1 ][1]}} | ~(pmaaddr[1 ] & ~(pmaaddr[1 ] + `XLEN'b1))); `CSR_PMAADDR2_ADDR : csr_rdata = pmaaddr[2 ] & ({`XLEN{pmacfg_a[2 ][1]}} | ~(pmaaddr[2 ] & ~(pmaaddr[2 ] + `XLEN'b1))); `CSR_PMAADDR3_ADDR : csr_rdata = pmaaddr[3 ] & ({`XLEN{pmacfg_a[3 ][1]}} | ~(pmaaddr[3 ] & ~(pmaaddr[3 ] + `XLEN'b1))); `CSR_PMAADDR4_ADDR : csr_rdata = pmaaddr[4 ] & ({`XLEN{pmacfg_a[4 ][1]}} | ~(pmaaddr[4 ] & ~(pmaaddr[4 ] + `XLEN'b1))); `CSR_PMAADDR5_ADDR : csr_rdata = pmaaddr[5 ] & ({`XLEN{pmacfg_a[5 ][1]}} | ~(pmaaddr[5 ] & ~(pmaaddr[5 ] + `XLEN'b1))); `CSR_PMAADDR6_ADDR : csr_rdata = pmaaddr[6 ] & ({`XLEN{pmacfg_a[6 ][1]}} | ~(pmaaddr[6 ] & ~(pmaaddr[6 ] + `XLEN'b1))); `CSR_PMAADDR7_ADDR : csr_rdata = pmaaddr[7 ] & ({`XLEN{pmacfg_a[7 ][1]}} | ~(pmaaddr[7 ] & ~(pmaaddr[7 ] + `XLEN'b1))); `CSR_PMAADDR8_ADDR : csr_rdata = pmaaddr[8 ] & ({`XLEN{pmacfg_a[8 ][1]}} | ~(pmaaddr[8 ] & ~(pmaaddr[8 ] + `XLEN'b1))); `CSR_PMAADDR9_ADDR : csr_rdata = pmaaddr[9 ] & ({`XLEN{pmacfg_a[9 ][1]}} | ~(pmaaddr[9 ] & ~(pmaaddr[9 ] + `XLEN'b1))); `CSR_PMAADDR10_ADDR: csr_rdata = pmaaddr[10] & ({`XLEN{pmacfg_a[10][1]}} | ~(pmaaddr[10] & ~(pmaaddr[10] + `XLEN'b1))); `CSR_PMAADDR11_ADDR: csr_rdata = pmaaddr[11] & ({`XLEN{pmacfg_a[11][1]}} | ~(pmaaddr[11] & ~(pmaaddr[11] + `XLEN'b1))); `CSR_PMAADDR12_ADDR: csr_rdata = pmaaddr[12] & ({`XLEN{pmacfg_a[12][1]}} | ~(pmaaddr[12] & ~(pmaaddr[12] + `XLEN'b1))); `CSR_PMAADDR13_ADDR: csr_rdata = pmaaddr[13] & ({`XLEN{pmacfg_a[13][1]}} | ~(pmaaddr[13] & ~(pmaaddr[13] + `XLEN'b1))); `CSR_PMAADDR14_ADDR: csr_rdata = pmaaddr[14] & ({`XLEN{pmacfg_a[14][1]}} | ~(pmaaddr[14] & ~(pmaaddr[14] + `XLEN'b1))); `CSR_PMAADDR15_ADDR: csr_rdata = pmaaddr[15] & ({`XLEN{pmacfg_a[15][1]}} | ~(pmaaddr[15] & ~(pmaaddr[15] + `XLEN'b1))); endcase end endmodule ``` - 與系統權限、異常、中斷處理相關的歸納在SRU內 ```verilog= module sru ( ... // CSR interface input csr_wr, input [ 11:0] csr_waddr, input [ 11:0] csr_raddr, input [ `XLEN-1:0] csr_sdata, input [ `XLEN-1:0] csr_cdata, output logic [ `XLEN-1:0] csr_rdata ); ... always_comb begin csr_rdata = `XLEN'b0; case (csr_raddr) `CSR_SSTATUS_ADDR : csr_rdata = sstatus; `CSR_SEDELEG_ADDR : csr_rdata = `XLEN'b0; `CSR_SIDELEG_ADDR : csr_rdata = `XLEN'b0; `CSR_SIE_ADDR : csr_rdata = sie & mideleg; `CSR_STVEC_ADDR : csr_rdata = stvec; `CSR_SSCRATCH_ADDR : csr_rdata = sscratch; `CSR_SEPC_ADDR : csr_rdata = sepc; `CSR_SCAUSE_ADDR : csr_rdata = scause; `CSR_STVAL_ADDR : csr_rdata = stval; `CSR_SIP_ADDR : csr_rdata = sip & mideleg; `CSR_MSTATUS_ADDR : csr_rdata = mstatus; `CSR_MISA_ADDR : csr_rdata = misa; `CSR_MEDELEG_ADDR : csr_rdata = medeleg; `CSR_MIDELEG_ADDR : csr_rdata = mideleg; `CSR_MIE_ADDR : csr_rdata = mie; `CSR_MTVEC_ADDR : csr_rdata = mtvec; `CSR_MSCRATCH_ADDR : csr_rdata = mscratch; `CSR_MEPC_ADDR : csr_rdata = mepc; `CSR_MCAUSE_ADDR : csr_rdata = mcause; `CSR_MTVAL_ADDR : csr_rdata = mtval; `CSR_MIP_ADDR : csr_rdata = mip; endcase end endmodule ``` #### ALU (Arithmetic logic unit) 依照IDU產生的控制訊號進行運算,可提供AND、OR、XOR、ADD、SUB、SLT、SLTU、SLL、SRL、SRA運算 ```verilog= module alu ( input [`ALU_OP_LEN - 1:0] alu_op, input len_64, input [ `XLEN - 1:0] src1, input [ `XLEN - 1:0] src2, output logic [ `XLEN - 1:0] out, output logic zero_flag ); `include "alu_op.sv" logic [`XLEN - 1:0] out_pre; logic [`XLEN - 1:0] src1_zext; logic [`XLEN - 1:0] src1_post; logic [`XLEN - 1:0] src2_post; logic signed [`XLEN - 1:0] signed_src1; logic signed [`XLEN - 1:0] signed_src2; logic [ 5:0] shamt; assign signed_src1 = src1_post; assign signed_src2 = src2_post; assign zero_flag = ~|out; `ifdef RV32 assign out = out_pre; assign src1_zext = src1; assign src1_post = src1; assign src2_post = src2; assign shamt = {1'b0, src2[4:0]}; `else assign out = len_64 ? out_pre : {{32{out_pre[31]}}, out_pre[31:0]}; assign src1_zext = {{32{len_64}} & src1[`XLEN-1:32], src1[31:0]}; assign src1_post = len_64 ? src1 : {{32{src1 [31]}}, src1 [31:0]}; assign src2_post = len_64 ? src2 : {{32{src2 [31]}}, src2 [31:0]}; assign shamt = {src2[5] & len_64, src2[4:0]}; `endif always_comb begin out_pre = `XLEN'b0; case (alu_op) ALU_AND : out_pre = src1 & src2; ALU_OR : out_pre = src1 | src2; ALU_XOR : out_pre = src1 ^ src2; ALU_ADD : out_pre = src1 + src2; ALU_SUB : out_pre = src1 - src2; ALU_SLT : out_pre = (signed_src1 < signed_src2) ? `XLEN'b1 : `XLEN'b0; ALU_SLTU: out_pre = (src1_post < src2_post ) ? `XLEN'b1 : `XLEN'b0; ALU_SLL : out_pre = src1 << shamt; ALU_SRL : out_pre = src1_zext >> shamt; ALU_SRA : out_pre = signed_src1 >>> shamt; endcase end endmodule ``` #### MUL (Multiplier) 提供乘法運算,我直接讓synthesis tool選擇適合的乘法器 ```verilog= module mul ( input clk, input rstn, input trig, input flush, input signed1, input [ `XLEN-1:0] src1, input signed2, input [ `XLEN-1:0] src2, output logic [2*`XLEN-1:0] out, output logic okay ); logic [2*`XLEN-1:0] src1_ext; logic [2*`XLEN-1:0] src2_ext; logic [ 1:0] cur_state; logic [ 1:0] nxt_state; parameter[1:0] STATE_IDLE = 2'h0, STATE_EXEC = 2'h1, STATE_DONE = 2'h2; always_ff @(posedge clk or negedge rstn) begin: reg_src if (~rstn) begin src1_ext <= {2*`XLEN{1'b0}}; src2_ext <= {2*`XLEN{1'b0}}; end else if (trig) begin src1_ext <= {{`XLEN{signed1 & src1[`XLEN-1]}}, src1}; src2_ext <= {{`XLEN{signed2 & src2[`XLEN-1]}}, src2}; end end always_ff @(posedge clk or negedge rstn) begin: reg_out if (~rstn) begin out <= {2*`XLEN{1'b0}}; end else begin out <= src1_ext * src2_ext;; end end always_ff @(posedge clk or negedge rstn) begin: fsm if (~rstn) cur_state <= STATE_IDLE; else cur_state <= nxt_state; end always_comb begin: next_state nxt_state = cur_state; case (cur_state) STATE_IDLE: nxt_state = trig ? STATE_EXEC : STATE_IDLE; STATE_EXEC: nxt_state = STATE_DONE; STATE_DONE: nxt_state = STATE_IDLE; endcase end assign okay = cur_state == STATE_DONE; endmodule ``` #### DIV (Divider) 負責提供除法、餘數運算,使用Multi-cycle架構來設計 ```verilog= module div ( input clk, input rstn, input trig, input flush, input signed1, input [ `XLEN-1:0] src1, input signed2, input [ `XLEN-1:0] src2, output logic [2*`XLEN-1:0] out, output logic okay ); localparam STATE_IDLE = 2'b00; localparam STATE_EXEC = 2'b01; localparam STATE_OKAY = 2'b10; logic [ 1:0] cur_state; logic [ 1:0] nxt_state; logic [ `XLEN-1:0] src1_pos; logic [ `XLEN-1:0] src2_pos; logic [ `XLEN-1:0] src2_pos_latch; logic neg1; logic neg2; logic neg1_latch; logic neg2_latch; logic [ 6:0] src1_clz; logic [ 6:0] src2_clz; logic [ 6:0] sft_bit; logic [ 5:0] sft_bit_div2; logic [ 5:0] cnt; logic [ 5:0] nxt_cnt; logic skip; `define NBIT_DIV 2 logic [ `XLEN-1:0] dividend [`NBIT_DIV]; logic [ `XLEN-1:0] nxt_dividend [`NBIT_DIV]; logic [`NBIT_DIV-1:0] res; logic [ `XLEN-1:0] rem; logic [ `XLEN-1:0] tmp; assign neg1 = signed1 & src1[`XLEN-1]; assign neg2 = signed2 & src2[`XLEN-1]; assign src1_pos = neg1 ? -src1 : src1; assign src2_pos = neg2 ? -src2 : src2; clz_64 u_src1_clz ( .in ( src1_pos ), .out ( src1_clz ) ); clz_64 u_src2_clz ( .in ( src2_pos ), .out ( src2_clz ) ); assign sft_bit = 7'd`XLEN - 7'b1 - src2_clz + src1_clz; assign sft_bit_div2 = sft_bit[6:1]; assign nxt_cnt = 6'd31 - sft_bit_div2; always_ff @(posedge clk or negedge rstn) begin if (~rstn) cur_state <= STATE_IDLE; else cur_state <= nxt_state; end always_comb begin nxt_state = cur_state; case (cur_state) STATE_IDLE: nxt_state = flush ? STATE_IDLE : trig ? skip ? STATE_OKAY: STATE_EXEC: STATE_IDLE; STATE_EXEC: nxt_state = flush | ~|cnt ? STATE_OKAY : STATE_EXEC; STATE_OKAY: nxt_state = STATE_IDLE; endcase end always_comb begin okay = 1'b0; case (cur_state) STATE_IDLE: begin okay = 1'b0; end STATE_EXEC: begin okay = 1'b0; end STATE_OKAY: begin okay = 1'b1; end endcase end assign skip = (src2 == `XLEN'b1) || (src2 == -`XLEN'b1 && signed2) || (src2 == `XLEN'b0) || (src1_pos < src2_pos); always_ff @(posedge clk or negedge rstn) begin if (~rstn) begin out <= {2*`XLEN{1'b0}}; src2_pos_latch <= `XLEN'b0; neg1_latch <= 1'b0; neg2_latch <= 1'b0; end else begin if (skip) begin out[ 0+:`XLEN] <= ({`XLEN{src2 == `XLEN'b1 }} & src1) | ({`XLEN{src2 == -`XLEN'b1 && signed2}} & -src1) | ({`XLEN{src2 == `XLEN'b0 }} & -`XLEN'b1) | ({`XLEN{src1_pos < src2_pos }} & `XLEN'b0); out[`XLEN+:`XLEN] <= ({`XLEN{src2 == `XLEN'b1 }} & `XLEN'b0) | ({`XLEN{src2 == -`XLEN'b1 && signed2}} & -`XLEN'b0) | ({`XLEN{src2 == `XLEN'b0 }} & src1) | ({`XLEN{src1_pos < src2_pos }} & src1); end else if (cur_state == STATE_IDLE) begin out <= src1_pos << {sft_bit_div2, 1'b0}; src2_pos_latch <= src2_pos; neg1_latch <= neg1; neg2_latch <= neg2; end else begin if (~|cnt) begin out[ 0+:`XLEN] <= neg1_latch ^ neg2_latch ? -{out[`XLEN-1-`NBIT_DIV:0], res} : {out[`XLEN-1-`NBIT_DIV:0], res}; out[`XLEN+:`XLEN] <= neg1_latch ? -rem : rem; end else out <= {rem, out[`XLEN-1-`NBIT_DIV:0], res}; end end end always_ff @(posedge clk or negedge rstn) begin if (~rstn) cnt <= 6'b0; else begin if (cur_state == STATE_IDLE) cnt <= nxt_cnt; else cnt <= |cnt ? cnt - 6'b1 : cnt; end end assign tmp = out[`XLEN-`NBIT_DIV+:`XLEN]; genvar g; generate for (g = `NBIT_DIV-1; g >= 0; g = g - 1) begin: g_div_unit if (g == `NBIT_DIV-1) begin: g_div_unit_1st assign dividend[g] = {{`NBIT_DIV-1{1'b0}}, tmp[`XLEN-1:`NBIT_DIV-1]}; end else begin: g_div_unit_nth assign dividend[g] = {nxt_dividend[g+1][`XLEN-2:0], tmp[g]}; end assign res[g] = src2_pos_latch <= dividend[g]; assign nxt_dividend[g] = res[g] ? dividend[g] - src2_pos_latch : dividend[g]; end endgenerate assign rem = nxt_dividend[0]; endmodule ``` :::info - clz_64是一個組合電路,可以計算從最高位元開始往低位元算,要經過幾個位元才會遇到1。原始碼放在`src/cpu/util.sv`,概念上是用Divide & Conquer來設計 ::: #### BPU (Branch Processing Unit) 負責處理分支指令如BEQ、BNE、BLT、BGE、BLTU、BGEU,這邊我們只要實現EQ、LT、LTU這三種比較即可,另外三種則為前三種的反向,判斷出分支條件成立後,會有輸出訊號告訴IFU說「branch taken」,並會給予分支該跳到的目的地 ```verilog= module bpu ( input [`BPU_OP_LEN - 1:0] bpu_op, input jump, input branch, input cmp_flag, input [ `XLEN - 1:0] src1, input [ `XLEN - 1:0] src2, input [ `XLEN - 1:0] pc, input [ `XLEN - 1:0] imm, output logic valid, output logic [ `XLEN - 1:0] out ); `include "bpu_op.sv" logic flag; logic [`XLEN - 1:0] pc_imm; logic [`XLEN - 1:0] src1_imm; logic signed [`XLEN - 1:0] signed_src1; logic signed [`XLEN - 1:0] signed_src2; logic jump_en; logic branch_en; assign signed_src1 = src1; assign signed_src2 = src2; assign pc_imm = pc + imm; assign src1_imm = src1 + imm; assign jump_en = jump; assign branch_en = branch && (~cmp_flag ^ flag); assign valid = jump_en || branch_en; assign out = ({`XLEN{jump_en }} & src1_imm)| ({`XLEN{branch_en}} & pc_imm ); always_comb begin flag = 1'b0; case (bpu_op) BPU_EQ : flag = src1 == src2; BPU_LT : flag = signed_src1 < signed_src2; BPU_LTU: flag = src1 < src2 ; endcase end endmodule ``` #### TPU (Trap Processing Unit) 負責管理CPU管線內所發生的異常,並控管多異常併發時之優先級處理,異常的種類包括以下 | Exception Code | Description | |:--------------:|:-------------------------------| | 0 | Instruction address misaligned | | 1 | Instruction access fault | | 2 | Illegal instruction | | 3 | Breakpoint | | 4 | Load address misaligned | | 5 | Load access fault | | 6 | Store/AMO address misaligned | | 7 | Store/AMO access fault | | 8 | Environment call from U-mode | | 9 | Environment call from S-mode | | 10 | Reserved | | 11 | Environment call from M-mode | | 12 | Instruction page fault | | 13 | Load page fault | | 14 | Reserved | | 15 | Store/AMO page fault | #### DPU (Data Processing Unit) 負責管理Data Access及Atomic Operate,處理資料對齊、展開,如DPU忙碌時會通知Hazard Unit幫忙暫停後面的管線。 #### WBU (Write-Back Unit) 負責寫回需要更新的GPR。 #### Instruction L1 Cache Instruction的第一層的Cache,用來暫存IFU曾經讀取過的指令,這樣一來如果IFU又再次請求讀取相同地址的指令時,就能快速的從Cache中回覆給IFU,不用經過匯流排千里迢迢的跑到外部記憶體抓指令。 #### Data L1 Cache Data的第一層的Cache,用來暫存DPU曾經讀取過的數據,類似於上面描述,當DPU又再次請求讀取或寫入相同地址的數據時,就能快速的從Cache中進行讀或寫,不用經過匯流排到外部記憶體去操作。 :::info - 我採用的是Write-Through Policy & No Write Allocate,對於DPU的Write操作,如Cache hit會同時更新Cache及外部記憶體,如Cache miss則只更新外部記憶體 ::: #### IMMU (Instruction Memory Management Unit) 負責將IFU請求的虛擬地址映射到實體地址。IMMU包含了AXI master的介面,可以到外部記憶體訪問Page Table。內部也包含了TLB,可暫存過去曾經訪問過的Page Table Entry,減少匯流排的使用以提高效能。 #### DMMU (Data Memory Management Unit) 與IMMU類似,負責將DPU請求的虛擬地址映射到實體地址。DMMU包含了AXI master的介面,可以到外部記憶體訪問Page Table。內部也包含了TLB,可暫存過去曾經訪問過的Page Table Entry,減少匯流排的使用以提高效能。 #### Debug Register 提供APB slave介面,可讓外部來控制CPU的除錯操作,規格都是我客制化設計的,之後再詳細說明使用方法,主要的暫存器配置如下: ```verilog= module dbgapb ( input clk, input rstn, apb_intf.slave apb_intf, output logic [ 11: 0] addr_out, output logic [`XLEN - 1: 0] wdata_out, output logic gpr_rd, output logic gpr_wr, input [`XLEN - 1: 0] gpr_in, output logic csr_rd, output logic csr_wr, input [`XLEN - 1: 0] csr_in, input [`XLEN - 1: 0] pc, output logic [ 31: 0] inst_out, output logic exec, input halted, output logic attach ); ... always_comb begin prdata_t = 32'b0; case (apb_intf.paddr[11:0]) `DBGAPB_DBG_EN : prdata_t = {31'b0, dbg_en}; `DBGAPB_INST : prdata_t = dbg_inst; `DBGAPB_INST_WR : prdata_t = {31'b0, dbg_inst_wr}; `DBGAPB_WDATA_L : prdata_t = dbg_wdata[ 0+:32]; `ifndef RV32 `DBGAPB_WDATA_H : prdata_t = dbg_wdata[32+:32]; `endif `DBGAPB_WDATA_WR: prdata_t = {31'b0, dbg_wdata_wr}; `DBGAPB_RDATA_L : prdata_t = dbg_rdata[ 0+:32]; `ifndef RV32 `DBGAPB_RDATA_H : prdata_t = dbg_rdata[32+:32]; `endif endcase end ... endmodule ``` :::info - Debug Register可實現的操作包括強制CPU進Halt、塞指令給CPU做單步執行、讀取或寫入GPR及CSR、讀取PC。 ::: ### SoC架構 ![](https://hackmd.io/_uploads/H1fCInMVh.png) :::info - 左上角橙色是前一章節[CPU架構](###CPU架構)介紹的部分,以下會接著介紹SoC上的各個單元 ::: #### DAP (Debug Access Port) 參考ARM的規格來設計,其用途是將JTAG訊號轉換成APB及AXI master介面。APB會跟CPU的Debug APB對接,用來調適CPU內核;AXI會接到SoC上的Coherence Interconnect,用來讀寫SoC上的記憶體或暫存器 :::info - JTAG Slave包括以下訊號: | In/Out | Signal | Description | |:------:|:------:|:------------| | Input | TCK | JTAG Clock | | Input | TMS | Mode Select | | Input | TDI | Data In | | Output | TDO | Data Out | - DAP內有一個IR及多個DR暫存器,JTAG透過設定這兩暫存器可以做到控制APB及AXI的命令發送,其控制的狀態機如下圖。在Shift-IR或Shift-DR狀態可以使用TDI來寫入IR或DR,這狀態同時也會把IR或DR由低位元往高位元送出去給TDO ![](https://i.imgur.com/eUEFeFD.png) - IR暫存器是用來選擇下列不同的DR | IR value | DR select | |:--------:|:---------:| | 0x8 | ABORT | | 0xa | DPACC | | 0xb | APACC | | 0xe | IDCODE | | 0xf | BYPASS | - 操作`DPACC`可以選擇要控制APB或是AXI - 選擇好APB或AXI之後,操作`APACC`可以在APB或AXI上送出讀或寫命令 - 更多詳細的介紹可以翻閱ARM的規格書: [ARM® Debug Interface v5 Architecture Specification](https://documentation-service.arm.com/static/5f900a61f86e16515cdc0610?token=) ::: #### Coherence Interconnect 作為匯流排的中心樞紐,負責將Master的命令送給正確的Slave,且把Slave的回應正確的回傳給Master。另外也負責維護CPU Cache的一致性,當CPU以外的Master試圖改寫Cache內已有副本的地址時,Interconnect會通知Cache做適當的處置,以避免老舊的數據卡在Cache內使CPU無法取得更新後的數據 :::info - 目前的設計有3個Master來源:CPU、DAP、DMA;有5個Slave標的:ROM、System SRAM、DRAM Controller、Interrupt Controller、Peripheral、Default Slave - Default Slave是放在Interconnect內部,當讀或寫的地址映射不到任何一個Default Slave以外的Slave,這時候就會把讀寫命令送到這。Default Slave收到讀寫命令後,會無條件返回Decode error的回應 - Slave定址空間分布如下表: | Base Address | Size | Slave | |:------------:|:-----:|:---------------------:| | 0x0000_0000 | 8KB | Boot ROM | | 0x0002_0000 | 128KB | System SRAM | | 0x0400_0000 | 128MB | Interrupt Controller | | 0x1000_0000 | 8KB | Peripheral | | 0x8000_0000 | 512MB | DDR (DRAM Controller) | ::: #### Boot ROM 這是一個只讀記憶體,不能對它做寫入,裡面的數據都是被燒死固定住的。在CPU上電後,預設的Program Counter會指到0x0000_0000地址,也就是Boot ROM的區域,這裡會把啟動程序的程式碼編譯好存放在這(之後軟體篇會介紹這部分的程式碼),讓CPU能夠做一些基本的初始化,Boot ROM的程式碼執行完後就會跳到System SRAM或DDR的區域裡面進行下一階段的程序運行。 :::info - 其實我在FPGA上是把這塊換成SRAM,這樣我在開發撰寫啟動程序的時候就不用一直重新合成電路,直接透過JTAG就能把啟動程序載入到裡面了 ::: #### System SRAM 這是一塊可讀可寫的空間,在我實際的使用上,我會放上Bootloader(軟體篇再詳述Bootloader是甚麼用途,及如何載入System SRAM) #### DRAM Controller 負責協助進行匯流排與DDR之間的溝通,這區域的讀寫會反應到晶片外部的DRAM讀寫,而這個空間在我實際使用上是放置Linux Kernel(軟體篇會再詳述Linux Kernel) #### Interrupt Controller 包含兩個單元:**PLIC (Platform-Level Interrupt Controller)**、**CLINT (Core-Local INTerruptor)**,這兩個單元都是負責與中斷有關的工作。先認識一下RISC-V規範內的中斷類型有以下幾類: | Interrupt Code | Description | |:--------------:|:------------------------------| | 0 | Reserved | | 1 | Supervisor software interrupt | | 2 | Reserved | | 3 | Machine software interrupt | | 4 | Reserved | | 5 | Supervisor timer interrupt | | 6 | Reserved | | 7 | Machine timer interrupt | | 8 | Reserved | | 9 | Supervisor external interrupt | | 10 | Reserved | | 11 | Machine external interrupt | 其中**Machine external interrupt**及**Supervisor external interrupt**是由PLIC發送進CPU的;**Machine timer interrupt**、**Machine software interrupt**是由CLINT發送給CPU;剩下的interrupt都是靠CSR指令去更新`mip`來產生的。 :::info - `csrrsi x0, mip, 0x1`會產生**Supervisor software interrupt** - `addi x5, x0, 0x20; csrrs x0, mip, x5`會產生**Supervisor timer interrupt** - 想想看為什麼不使用`csrrsi x0, mip, 0x20`指令來送出**Supervisor timer interrupt**呢?下面指令的二進制型式或許能讓你想到答案 ``` | 31 20 | 19 15 | 14 12 | 11 07 | 06 00 | +----------------------+-----------+--------+--------+------------+ | csr | imm | 110 | rd | 1110011 | +----------------------+-----------+--------+--------+------------+ ``` ::: #### PLIC (Platform-Level Interrupt Controller) 負責接收各個裝置所送出來的中斷訊號,並仲裁中斷訊號中何者能接通到CPU內 ![](https://i.imgur.com/qwNoPbi.png) 架構如上圖,當裝置(像是UART、SPI)中斷發生時,來源中斷訊號會先打進PLIC Gateway中,再拿Priority來去跟別的有效的中斷訊號做比較,最後如果最高Priority的中斷有大於Threshold就會將EIP bit拉起通知Target(CPU)有中斷需要被處理,CPU也可以透過APB介面到Interrupt Claim/Complete暫存器中讀取代處理中斷的ID號。 :::info - 圖中的藍色方塊暫存器皆有接到APB interface,可由這介面進入讀寫。 - 圖中Target分別會接線到`mip.meip`及`mip.seip`(Machine external interrupt及supervisor external interrupt)。 ::: #### CLINT (Core-Local INTerruptor) 負責提供Software及Timer interrupt給Machine mode,會與CPU的控制狀態暫存器`mip.msip`及`mip.mtip`對接 - Software interrupt: 作為ipi使用,可透過軟體的方式來設定CLINT暫存器來發送軟體中斷訊號,讓ipi的目標CPU進入軟體中斷處理程序中,依照程序約定好的地址(Mailbox)讀取訊息,就能做到兩顆或多顆CPU通訊、同步的目的。 - Timer interrupt: 在系統中常常用來當作「時間」計算的用途,在規律週期的時鐘中斷以Linux系統為例,每次的中斷處理將會累加`jiffie`,這麼一來就能據以處理時間相關的事務,如排程 ```verilog= module clint ( input clk, input rstn, apb_intf.slave apb_intf, input [ 63: 0] systime, output logic [`CPU_NUM-1: 0] msip, output logic [`CPU_NUM-1: 0] mtip ); logic apb_wr; logic [31:0] prdata_msip; logic [31:0] prdata_timecmp; logic [31:0] prdata_time; logic [31:0] prdata_t; logic [63:0] mtimecmp [`CPU_NUM]; logic [63:0] mtime; always_comb begin: comb_apb_wr apb_wr = ~apb_intf.penable && apb_intf.psel && apb_intf.pwrite; end genvar g; generate for (g = 0; g < `CPU_NUM; g = g + 1) begin: g_apb_reg always_ff @(posedge clk or negedge rstn) begin: reg_msip if (~rstn) begin msip[g] <= 1'b0; end else if (apb_wr && apb_intf.paddr[15:0] == `CLINT_MSIP + 16'h4 * g[15:0]) begin msip[g] <= apb_intf.pwdata[0]; end end always_ff @(posedge clk or negedge rstn) begin: reg_mtimecmp if (~rstn) begin mtimecmp[g] <= 64'b0; end else if (apb_wr && apb_intf.paddr[15:0] == `CLINT_TIMECMP + 16'h8 * g[15:0]) begin mtimecmp[g][31:0] <= apb_intf.pwdata; end else if (apb_wr && apb_intf.paddr[15:0] == `CLINT_TIMECMP + 16'h8 * g[15:0] + 16'h4) begin mtimecmp[g][63:32] <= apb_intf.pwdata; end end always_ff @(posedge clk or negedge rstn) begin: reg_mtip if (~rstn) begin mtip[g] <= 1'b0; end else begin mtip[g] <= mtime >= mtimecmp[g]; end end end endgenerate always_comb begin: comb_mtime mtime = systime; end always_comb begin: comb_prdata_msip integer i; prdata_msip = 32'b0; for (i = 0; i < `CPU_NUM; i = i + 1) begin prdata_msip = prdata_msip | {31'b0, msip[i] & (apb_intf.paddr[7:2] == i[5:0])}; end prdata_msip = prdata_msip & {32{apb_intf.paddr[15:12] == 4'h0}}; end always_comb begin: comb_prdata_timecmp integer i; prdata_timecmp = 32'b0; for (i = 0; i < `CPU_NUM; i = i + 1) begin prdata_timecmp = prdata_timecmp | (mtimecmp[i][31: 0] & {32{apb_intf.paddr[8:3] == i[5:0] && !apb_intf.paddr[2]}})| (mtimecmp[i][63:32] & {32{apb_intf.paddr[8:3] == i[5:0] && apb_intf.paddr[2]}}); end prdata_timecmp = prdata_timecmp & {32{apb_intf.paddr[15:12] == 4'h4}}; end always_comb begin: comb_prdata_time prdata_time = (mtime[31: 0] & {32{apb_intf.paddr[15:0] == `CLINT_TIME}})| (mtime[63:32] & {32{apb_intf.paddr[15:0] == `CLINT_TIME + 16'h4}}); end assign prdata_t = prdata_msip | prdata_timecmp | prdata_time; always_ff @(posedge clk or negedge rstn) begin: reg_prdata if (~rstn) begin apb_intf.prdata <= 32'b0; end else begin apb_intf.prdata <= prdata_t; end end assign apb_intf.pslverr = 1'b0; assign apb_intf.pready = 1'b1; endmodule ``` :::info - 提供三類APB暫存器: - MSIP: 為Read/Write,直接影響軟體中斷的啟動與失效 - TIME: 為Read-Only,能夠讀取64位元的系統時間 - TIMECMP: 為Read/Write,當系統時間大過TIMECMP時,CLINT將會送出時鐘中斷訊號 ::: #### Peripheral 目前設計包含了**UART**及**SPI**,分別會再把Peripheral地址區間切割分給上述兩個設備 #### UART (Universal Asynchronous Receiver/Transmitter) 是一個非同步的通訊設備,以下圖形式對接兩個設備。在我的架構中,一端的UART在SoC晶片內,另外一端在SoC晶片外(連接到Host主機)。兩邊皆會有發送端(TX)及接收端(RX),然後需要共地(GND對接),否則訊號無法正常傳輸。 ![](https://i.imgur.com/QUtAyIu.png) 訊號的傳輸如同下圖,平時閒置TX會拉成高電位,當有數據要傳輸時,會降成低電位當作start bit,接著會由數據的低位元到高位元連續傳輸出去,傳輸結束後會再拉回高電位,此時至少要維持1~2個位元寬度的高電位後才能進行下一筆數據的傳輸。圖中每一個位元傳輸的頻率為波特率(baud rate),在我的設計中預設是115200 Hz,不過也可透過APB介面進去修改baud rate。 ![](https://i.imgur.com/kmilIPv.png) ```verilog= module uart ( input clk, input rstn, apb_intf.slave s_apb_intf, output logic irq_out, input uart_rx, output logic uart_tx ); ... assign tx_fifo_wr = apb_wr && s_apb_intf.paddr[11:0] == `UART_TXFIFO && ~tx_fifo_full && ~s_apb_intf.pwdata[31]; assign tx_fifo_wdata = s_apb_intf.pwdata[`UART_DATA_WIDTH-1:0]; ... always_comb begin prdata_t = 32'b0; case (s_apb_intf.paddr[11:0]) `UART_TXFIFO: prdata_t = {tx_fifo_full, 31'b0}; `UART_RXFIFO: prdata_t = {rx_fifo_empty, 23'b0, rx_fifo_rdata & {8{~rx_fifo_empty}}}; `UART_TXCTRL: prdata_t = {13'b0, txcnt, 14'b0, nstop, txen}; `UART_RXCTRL: prdata_t = {13'b0, rxcnt, 14'b0, 1'b0, rxen}; `UART_IE : prdata_t = {29'b0, perror_ie, rxwm_ie, txwm_ie}; `UART_IP : prdata_t = {29'b0, perror_ip, rxwm_ip, txwm_ip}; `UART_IC : prdata_t = {29'b0, perror_ip, rxwm_ip, txwm_ip}; `UART_DIV : prdata_t = {16'b0, div}; `UART_LCR : prdata_t = {26'b0, lcr, 3'b0}; endcase end ... endmodule ``` :::info - UART可以靠`UART_TXFIFO`來讓TX傳輸8位元的數據,並可以用`UART_RXFIFO`接收數據。 ::: #### SPI (Serial Peripheral Interface) 與剛剛提到的UART一樣都是要用在裝置間的通訊,不同的是SPI是以同步的形式來傳輸,也就是SPI會有Clock訊號來當作採樣數據時機的參考,接線如下 ![](https://i.imgur.com/o1aRVCM.png) :::info - SCLK為時鐘訊號,會由Master端發出。 - MOSI為Master端給Slave端的數據。 - MISO為Slave端給Master端的數據。 - <span style="text-decoration:overline">SS</span>為Slave的選擇訊號,當訊號為1時代表,不選擇Slave,意味MOSI、MISO上的訊號為無效。反之<span style="text-decoration:overline">SS</span>為0,代表MOSI、MISO上的訊號為有效。 ::: 在我的架構中,Master端是在SoC內,Slave端是接到SD卡插槽,因此我可以透過SPI介面發送SD命令操作SD卡(包括讀/寫),訊號的傳輸方式如下 ![](https://i.imgur.com/UF5oArO.png) :::info - SPI內會有兩個訊號CPOL、CPHA來決定SCLK極性及資料採樣的時機,實際上會依據要溝通的設備規格,可透過APB介面來設定這兩個訊號。 - CPOL為0時,代表SCLK在閒置時是維持0訊號 - CPOL為1時,代表SCLK在閒置時是維持1訊號 - CPHA為0時,代表設備約定在SCLK第一個edge(CPOL為0的話為正緣,CPOL為1的話為負緣)採樣數據,也意味著數據應該要在這edge發生前半個週期準備好 - CPHA為1時,代表設備約定在SCLK第二個edge(CPOL為0的話為負緣,CPOL為1的話為正緣)採樣數據,同樣意味著數據應該要在這edge發生前半個週期準備好 ::: # Authors [Yu-Tong Shen](https://github.com/yutongshen/) ###### tags: `RISC-V`、`Processor`、`Kernel`