日本黄色一级经典视频|伊人久久精品视频|亚洲黄色色周成人视频九九九|av免费网址黄色小短片|黄色Av无码亚洲成年人|亚洲1区2区3区无码|真人黄片免费观看|无码一级小说欧美日免费三级|日韩中文字幕91在线看|精品久久久无码中文字幕边打电话

當(dāng)前位置:首頁 > > ZYNQ


前言

第一次聽到RISC-V這個(gè)詞大概是兩年前,當(dāng)時(shí)覺得它也就是和MIPS這些CPU架構(gòu)沒什么區(qū)別,因此也就不以為然了。直到去年,RISC-V這個(gè)詞開始頻繁地出現(xiàn)在微信和其他網(wǎng)站上,此時(shí)我再也不能無動(dòng)于衷了,于是開始在網(wǎng)上搜索有關(guān)它的資料,開始知道有SiFive這個(gè)網(wǎng)站,知道SiFive出了好幾款RISC-V的開發(fā)板。可是最便宜的那一塊開發(fā)板都要700多RMB,最后還是忍痛出手了一塊。由于平時(shí)上班比較忙,所以玩這塊板子的時(shí)間并不多,也就是晚上下班后和周末玩玩,自己照著芯片手冊(cè)寫了幾個(gè)例程在板子上跑跑而已。

再后來發(fā)現(xiàn)網(wǎng)上已經(jīng)有如何設(shè)計(jì)RISC-V處理器的書籍賣了,并且這個(gè)處理器是開源的,于是果斷買了一本來閱讀并瀏覽了它的開源代碼,最后表示看不懂。從那之后一個(gè)“從零開始寫RISC-V處理器”的想法開始不斷地出現(xiàn)在我的腦海里。我心里是很想學(xué)習(xí)、深入研究RISC-V的,但是一直以來都沒有verilog和FPGA的基礎(chǔ),可以說是CPU設(shè)計(jì)領(lǐng)域里的門外漢,再加上很少業(yè)余時(shí)間,為此一度猶豫不決。但是直覺告訴我已近不能再等了,我決定開始自學(xué)verilog和FPGA,用簡(jiǎn)單易懂的方式寫一個(gè)RISC-V處理器并且把它開源出來,在提高自身的同時(shí)希望能幫助到那些想入門RISC-V的同學(xué),于是tinyriscv終于在2019年12月誕生了。

tinyriscv是一個(gè)采用三級(jí)流水線設(shè)計(jì),順序、單發(fā)射、單核的32位RISC-V處理器,全部代碼都是采用verilog HDL語言編寫,核心設(shè)計(jì)思想是簡(jiǎn)單、易懂。

緒論

RISC-V是什么

RISC,即精簡(jiǎn)指令集處理器,是相對(duì)于X86這種CISC(復(fù)雜指令集處理器)來說的。RISC-V中的V是羅馬數(shù)字,也即阿拉伯?dāng)?shù)字中的5,就是指第5代RISC。

RISC-V是一種指令集架構(gòu),和ARM、MIPS這些是屬于同一類東西。RISC-V誕生于2010年,最大的特點(diǎn)是開源,任何人都可以設(shè)計(jì)RISC-V架構(gòu)的處理器并且不會(huì)有任何版權(quán)問題。

既生ARM,何生RISC-V

ARM是一種很優(yōu)秀的處理器,這一點(diǎn)是無可否認(rèn)的,在RISC處理器中是處于絕對(duì)老大的地位。但是ARM是閉源的,要設(shè)計(jì)基于ARM的處理器是要交版權(quán)費(fèi)的,或者說要購買ARM的授權(quán),而且這授權(quán)費(fèi)用是昂貴的。

RISC-V的誕生并不是偶然的,而是必然的,為什么?且由我從以下兩大領(lǐng)域進(jìn)行說明。

先看開源軟件領(lǐng)域(或者說是操作系統(tǒng)領(lǐng)域),Windows是閉源的,Linux是開源的,Linux有多成功、對(duì)開源軟件有多重要的意義,這個(gè)不用多說了吧。再看手機(jī)操作系統(tǒng)領(lǐng)域,iOS是閉源的,Android是開源的,Android有多成功,這個(gè)也不用多說了吧。對(duì)于RISC處理器領(lǐng)域,由于有了ARM的閉源,必然就會(huì)有另外一種開源的RISC處理器。RISC-V之于CPU的意義,就好比Linux之于開源軟件的意義。

或者你會(huì)說現(xiàn)在也有好多開源的處理器架構(gòu)啊,比如MIPS等等,為什么偏偏是RISC-V?這個(gè)在這里我就不細(xì)說了,我只想說一句:大部分人能看到的機(jī)遇不會(huì)是一個(gè)好的機(jī)遇,你懂的。

可以說未來十年乃至更長(zhǎng)時(shí)間內(nèi)不會(huì)有比RISC-V更優(yōu)秀的開源處理器架構(gòu)出現(xiàn)。錯(cuò)過RISC-V,你注定要錯(cuò)過一個(gè)時(shí)代。

淺談Verilog

verilog,確切來說應(yīng)該是verilog HDL(Hardware Description Language ),從它的名字就可以知道這是一種硬件描述語言。首先它是一種語言,和C語言、C++語言一樣是一種編程語言,那么verilog描述的是什么硬件呢?描述電阻?描述電容?描述運(yùn)算放大器?都不是,它描述的是數(shù)字電路里的硬件,比如與、非門、觸發(fā)器、鎖存器等等。

既然是編程語言,那一定會(huì)有它的語法,學(xué)過C語言的同學(xué)再來看verilog得代碼,會(huì)發(fā)現(xiàn)有很多地方是相似的。

verilog的語法并不難,難的是什么時(shí)候該用wire類型,什么時(shí)候該用reg類型,什么時(shí)候該用assign來描述電路,什么時(shí)候該用always來描述電路。assign能描述組合邏輯電路,always也能描述組合邏輯電路,兩者有什么區(qū)別呢?

用always描述組合邏輯電路

我們知道數(shù)字電路里有兩大類型的電路,一種是組合邏輯電路,另外一種是時(shí)序邏輯電路。組合邏輯電路不需要時(shí)鐘作為觸發(fā)條件,因此輸入會(huì)立即(不考慮延時(shí))反映到輸出。時(shí)序邏輯電路以時(shí)鐘作為觸發(fā)條件,時(shí)鐘的上升沿到來時(shí)輸入才會(huì)反映到輸出。

在verilog中,assign能描述組合邏輯電路,always也能描述組合邏輯電路。對(duì)于簡(jiǎn)單的組合邏輯電路的話兩者描述起來都比較好懂、容易理解,但是一旦到了復(fù)雜的組合邏輯電路,如果用assign描述的話要么是一大串要么是要用好多個(gè)assign,不容易弄明白。但是用always描述起來卻是非常容易理解的。

既然這樣,那全部組合邏輯電路都用always來描述好了,呵呵,既然assign存在就有它的合理性。

用always描述組合邏輯電路時(shí)要注意避免產(chǎn)生鎖存器,if和case的分支情況要寫全。

在tinyriscv中用了大量的always來描述組合邏輯電路,特別是在譯碼和執(zhí)行階段。

數(shù)字電路設(shè)計(jì)中的時(shí)序問題

要分析數(shù)字電路中的時(shí)序問題,就一定要提到以下這個(gè)模型。

其中對(duì)時(shí)序影響最大的是上圖中的組合邏輯電路。所以要避免時(shí)序問題,最簡(jiǎn)單的方法減小組合邏輯電路的延時(shí)。組合邏輯電路里的串聯(lián)級(jí)數(shù)越多延時(shí)就越大,實(shí)在沒辦法減小串聯(lián)級(jí)數(shù)時(shí),可以采用流水線的方式將這些級(jí)數(shù)用觸發(fā)器隔開。

流水線設(shè)計(jì)

要設(shè)計(jì)處理器的話,流水線是繞不開的。當(dāng)然你也可以抬杠說:”用狀態(tài)機(jī)也可以實(shí)現(xiàn)處理器啊,不一定要用流水線。”

采用流水線設(shè)計(jì)方式,不但可以提高處理器的工作頻率,還可以提高處理器的效率。但是流水線并不是越長(zhǎng)越好,流水線越長(zhǎng)要使用的資源就越多、面積就越大。

在設(shè)計(jì)一款處理器之前,首先要確定好所設(shè)計(jì)的處理器要達(dá)到什么樣的性能(或者說主頻最高是多少),所使用的資源的上限是多少,功耗范圍是多少。如果一味地追求性能而不考慮資源和功耗的話,那么所設(shè)計(jì)出來的處理器估計(jì)就只能用來玩玩,或者做做學(xué)術(shù)研究。

tinyriscv采用的是三級(jí)流水線,即取指、譯碼和執(zhí)行,設(shè)計(jì)的目標(biāo)就是要對(duì)標(biāo)ARM的Cortex-M3系列處理器。

代碼風(fēng)格

代碼風(fēng)格其實(shí)并沒有一種標(biāo)準(zhǔn),但是并不代表代碼風(fēng)格不重要。好的代碼風(fēng)格可以讓別人看你的代碼時(shí)有一種賞心悅目的感覺。哪怕代碼只是寫給自己看,也一定要養(yǎng)成好的代碼風(fēng)格的習(xí)慣。tinyriscv的代碼風(fēng)格在很大程度上沿用了寫C語言代碼所采用的風(fēng)格。

下面介紹tinyriscv的一些主要的代碼風(fēng)格。

縮進(jìn)

統(tǒng)一使用4個(gè)空格。

if語句

不管if語句下面有多少行語句,if下面的語句都由begin…end包起來,并且begin在if的最后,如下所示:

1if (a == 1'b1) begin 2 c <= b; 3end else begin 4 c <= d; 5end 

case語句

對(duì)于每一個(gè)分支情況,不管有多少行語句,都由begin…end包起來,如下所示:

1case (a) 2 c: begin 3 e = g; 4 end 5 default: begin 6 b = t; 7 end 8endcase

always語句

always語句后跟begin,如下所示:

1always @ (posedge clk) begin 2 a <= b; 3end 

其他

=、==、<=、>=、+、-、*、/、@等符號(hào)左右各有一個(gè)空格。

,和:符號(hào)后面有一個(gè)空格。

對(duì)于模塊的輸入信號(hào),不省略wire關(guān)鍵字。

每個(gè)文件的最后留一行空行。

if、case、always后面都有一個(gè)空格。

硬件篇

硬件篇主要介紹tinyriscv的verilog代碼設(shè)計(jì)。

tinyriscv整體框架如圖2_1所示。

圖2_1 tinyriscv整體框架

可見目前tinyriscv已經(jīng)不僅僅是一個(gè)內(nèi)核了,而是一個(gè)小型的SOC,包含一些簡(jiǎn)單的外設(shè),如timer、uart_tx等。

tinyriscv SOC輸入輸出信號(hào)有兩部分,一部分是系統(tǒng)時(shí)鐘clk和復(fù)位信號(hào)rst,另一部分是JTAG調(diào)試信號(hào),TCK、TMS、TDI和TDO。

上圖中的小方框表示一個(gè)個(gè)模塊,方框里面的文字表示模塊的名字,箭頭則表示模塊與模塊之間的的輸入輸出關(guān)系。

下面簡(jiǎn)單介紹每個(gè)模塊的主要作用。

jtag_top:調(diào)試模塊的頂層模塊,主要有三大類型的信號(hào),第一種是讀寫內(nèi)存的信號(hào),第二種是讀寫寄存器的信號(hào),第三種是控制信號(hào),比如復(fù)位MCU,暫停MCU等。

pc_reg:PC寄存器模塊,用于產(chǎn)生PC寄存器的值,該值會(huì)被用作指令存儲(chǔ)器的地址信號(hào)。

if_id:取指到譯碼之間的模塊,用于將指令存儲(chǔ)器輸出的指令打一拍后送到譯碼模塊。

id:譯碼模塊,純組合邏輯電路,根據(jù)if_id模塊送進(jìn)來的指令進(jìn)行譯碼。當(dāng)譯碼出具體的指令(比如add指令)后,產(chǎn)生是否寫寄存器信號(hào),讀寄存器信號(hào)等。由于寄存器采用的是異步讀方式,因此只要送出讀寄存器信號(hào)后,會(huì)馬上得到對(duì)應(yīng)的寄存器數(shù)據(jù),這個(gè)數(shù)據(jù)會(huì)和寫寄存器信號(hào)一起送到id_ex模塊。

id_ex:譯碼到執(zhí)行之間的模塊,用于將是否寫寄存器的信號(hào)和寄存器數(shù)據(jù)打一拍后送到執(zhí)行模塊。

ex:執(zhí)行模塊,純組合邏輯電路,根據(jù)具體的指令進(jìn)行相應(yīng)的操作,比如add指令就執(zhí)行加法操作等。此外,如果是lw等訪存指令的話,則會(huì)進(jìn)行讀內(nèi)存操作,讀內(nèi)存也是采用異步讀方式。最后將是否需要寫寄存器、寫寄存器地址,寫寄存器數(shù)據(jù)信號(hào)送給regs模塊,將是否需要寫內(nèi)存、寫內(nèi)存地址、寫內(nèi)存數(shù)據(jù)信號(hào)送給rib總線,由總線來分配訪問的模塊。

div:除法模塊,采用試商法實(shí)現(xiàn),因此至少需要32個(gè)時(shí)鐘才能完成一次除法操作。

ctrl:控制模塊,產(chǎn)生暫停流水線、跳轉(zhuǎn)等控制信號(hào)。

clint:核心本地中斷模塊,對(duì)輸入的中斷請(qǐng)求信號(hào)進(jìn)行總裁,產(chǎn)生最終的中斷信號(hào)。

rom:程序存儲(chǔ)器模塊,用于存儲(chǔ)程序(bin)文件。

ram:數(shù)據(jù)存儲(chǔ)器模塊,用于存儲(chǔ)程序中的數(shù)據(jù)。

timer:定時(shí)器模塊,用于計(jì)時(shí)和產(chǎn)生定時(shí)中斷信號(hào)。目前支持RTOS時(shí)需要用到該定時(shí)器。

uart_tx:串口發(fā)送模塊,主要用于調(diào)試打印。

gpio:簡(jiǎn)單的IO口模塊,主要用于點(diǎn)燈調(diào)試。

spi:目前只有master角色,用于訪問spi從機(jī),比如spi norflash。

PC寄存器

PC寄存器模塊所在的源文件:rtl/core/pc_reg.v

PC寄存器模塊的輸入輸出信號(hào)如下表所示:

PC寄存器模塊代碼比較簡(jiǎn)單,直接貼出來:

 1always @ (posedge clk) begin  2 // 復(fù)位  3 if (rst == `RstEnable || jtag_reset_flag_i == 1'b1) begin  4 pc_o <= `CpuResetAddr;  5 // 跳轉(zhuǎn)  6 end else if (jump_flag_i == `JumpEnable) begin  7 pc_o <= jump_addr_i;  8 // 暫停  9 end else if (hold_flag_i >= `Hold_Pc) begin 10 pc_o <= pc_o; 11 // 地址加4 12 end else begin 13 pc_o <= pc_o + 4'h4; 14 end 15end 16 

第3行,PC寄存器的值恢復(fù)到原始值(復(fù)位后的值)有兩種方式,第一種不用說了,就是復(fù)位信號(hào)有效。第二種是收到j(luò)tag模塊發(fā)過來的復(fù)位信號(hào)。PC寄存器復(fù)位后的值為CpuResetAddr,即32’h0,可以通過改變CpuResetAddr的值來改變PC寄存器的復(fù)位值。

第6行,判斷跳轉(zhuǎn)標(biāo)志是否有效,如果有效則直接將PC寄存器的值設(shè)置為jump_addr_i的值。因此可以知道,所謂的跳轉(zhuǎn)就是改變PC寄存器的值,從而使CPU從該跳轉(zhuǎn)地址開始取指。

第9行,判斷暫停標(biāo)志是否大于等于Hold_Pc,該值為3’b001。如果是,則保持PC寄存器的值不變。這里可能會(huì)有疑問,為什么Hold_Pc的值不是一個(gè)1bit的信號(hào)。因?yàn)檫@個(gè)暫停標(biāo)志還會(huì)被if_id和id_ex模塊使用,如果僅僅需要暫停PC寄存器的話,那么if_id模塊和id_ex模塊是不需要暫停的。當(dāng)需要暫停if_id模塊時(shí),PC寄存器也會(huì)同時(shí)被暫停。當(dāng)需要暫停id_ex模塊時(shí),那么整條流水線都會(huì)被暫停。

第13行,將PC寄存器的值加4。在這里可以知道,tinyriscv的取指地址是4字節(jié)對(duì)齊的,每條指令都是32位的。

通用寄存器

通用寄存器模塊所在的源文件:rtl/core/regs.v

一共有32個(gè)通用寄存器x0~x31,其中寄存器x0是只讀寄存器并且其值固定為0。

通用寄存器的輸入輸出信號(hào)如下表所示:

注意,這里的寄存器1不是指x1寄存器,寄存器2也不是指x2寄存器。而是指一條指令里涉及到的兩個(gè)寄存器(源寄存器1和源寄存器2)。一條指令可能會(huì)同時(shí)讀取兩個(gè)寄存器的值,所以有兩個(gè)讀端口。又因?yàn)閖tag模塊也會(huì)進(jìn)行寄存器的讀操作,所以一共有三個(gè)讀端口。

讀寄存器操作來自譯碼模塊,并且讀出來的寄存器數(shù)據(jù)也會(huì)返回給譯碼模塊。寫寄存器操作來自執(zhí)行模塊。

先看讀操作的代碼,如下:

 1// 讀寄存器1  2always @ (*) begin  3 if (rst == `RstEnable) begin  4 rdata1_o = `ZeroWord;  5 end else if (raddr1_i == `RegNumLog2'h0) begin  6 rdata1_o = `ZeroWord;  7 // 如果讀地址等于寫地址,并且正在寫操作,則直接返回寫數(shù)據(jù)  8 end else if (raddr1_i == waddr_i && we_i == `WriteEnable) begin  9 rdata1_o = wdata_i; 10 end else begin 11 rdata1_o = regs[raddr1_i]; 12 end 13end 14 15// 讀寄存器2 16always @ (*) begin 17 if (rst == `RstEnable) begin 18 rdata2_o = `ZeroWord; 19 end else if (raddr2_i == `RegNumLog2'h0) begin 20 rdata2_o = `ZeroWord; 21 // 如果讀地址等于寫地址,并且正在寫操作,則直接返回寫數(shù)據(jù) 22 end else if (raddr2_i == waddr_i && we_i == `WriteEnable) begin 23 rdata2_o = wdata_i; 24 end else begin 25 rdata2_o = regs[raddr2_i]; 26 end 27end 28 

可以看到兩個(gè)寄存器的讀操作幾乎是一樣的。因此在這里只解析讀寄存器1那部分代碼。

第5行,如果是讀寄存器0(x0),那么直接返回0就可以了。

第8行,這涉及到數(shù)據(jù)相關(guān)問題。由于流水線的原因,當(dāng)前指令處于執(zhí)行階段的時(shí)候,下一條指令則處于譯碼階段。由于執(zhí)行階段不會(huì)寫寄存器,而是在下一個(gè)時(shí)鐘到來時(shí)才會(huì)進(jìn)行寄存器寫操作,如果譯碼階段的指令需要上一條指令的結(jié)果,那么此時(shí)讀到的寄存器的值是錯(cuò)誤的。比如下面這兩條指令:

1add x1, x2, x3 2add x4, x1, x5

第二條指令依賴于第一條指令的結(jié)果。為了解決這個(gè)數(shù)據(jù)相關(guān)的問題就有了第8~9行的操作,即如果讀寄存器等于寫寄存器,則直接將要寫的值返回給讀操作。

第11行,如果沒有數(shù)據(jù)相關(guān),則返回要讀的寄存器的值。

下面看寫寄存器操作,代碼如下:

 1// 寫寄存器  2always @ (posedge clk) begin  3 if (rst == `RstDisable) begin  4 // 優(yōu)先ex模塊寫操作  5 if ((we_i == `WriteEnable) && (waddr_i != `RegNumLog2'h0)) begin  6 regs[waddr_i] <= wdata_i;  7 end else if ((jtag_we_i == `WriteEnable) && (jtag_addr_i != `RegNumLog2'h0)) begin  8 regs[jtag_addr_i] <= jtag_data_i;  9 end 10 end 11end 

第5~6行,如果執(zhí)行模塊寫使能并且要寫的寄存器不是x0寄存器,則將要寫的值寫到對(duì)應(yīng)的寄存器。

第7~8行,jtag模塊的寫操作。

CSR寄存器模塊(csr_reg.v)和通用寄存器模塊的讀、寫操作是類似的,這里就不重復(fù)了。

取指

目前tinyriscv所有外設(shè)(包括rom和ram)、寄存器的讀取都是與時(shí)鐘無關(guān)的,或者說所有外設(shè)、寄存器的讀取采用的是組合邏輯的方式。這一點(diǎn)非常重要!

tinyriscv并沒有具體的取指模塊和代碼。PC寄存器模塊的輸出pc_o會(huì)連接到外設(shè)rom模塊的地址輸入,又由于rom的讀取是組合邏輯,因此每一個(gè)時(shí)鐘上升沿到來之前(時(shí)序是滿足要求的),從rom輸出的指令已經(jīng)穩(wěn)定在if_id模塊的輸入,當(dāng)時(shí)鐘上升沿到來時(shí)指令就會(huì)輸出到id模塊。

取到的指令和指令地址會(huì)輸入到if_id模塊(if_id.v),if_id模塊是一個(gè)時(shí)序電路,作用是將輸入的信號(hào)打一拍后再輸出到譯碼(id.v)模塊。

譯碼

譯碼模塊所在的源文件:rtl/core/id.v

譯碼(id)模塊是一個(gè)純組合邏輯電路,主要作用有以下幾點(diǎn):

1.根據(jù)指令內(nèi)容,解析出當(dāng)前具體是哪一條指令(比如add指令)。

2.根據(jù)具體的指令,確定當(dāng)前指令涉及的寄存器。比如讀寄存器是一個(gè)還是兩個(gè),是否需要寫寄存器以及寫哪一個(gè)寄存器。

3.訪問通用寄存器,得到要讀的寄存器的值。

譯碼模塊的輸入輸出信號(hào)如下表所示:

以add指令為例來說明如何譯碼。下圖是add指令的編碼格式:

可知,add指令被編碼成6部分內(nèi)容。通過第1、4、6這三部分可以唯一確定當(dāng)前指令是否是add指令。知道是add指令之后,就可以知道add指令需要讀兩個(gè)通用寄存器(rs1和rs2)和寫一個(gè)通用寄存器(rd)。下面看具體的代碼:

 1case (opcode)  2...  3 `INST_TYPE_R_M: begin  4 if ((funct7 == 7'b0000000) || (funct7 == 7'b0100000)) begin  5 case (funct3)  6 `INST_ADD_SUB, `INST_SLL, `INST_SLT, `INST_SLTU, `INST_XOR, `INST_SR, `INST_OR, `INST_AND: begin  7 reg_we_o = `WriteEnable;  8 reg_waddr_o = rd;  9 reg1_raddr_o = rs1; 10 reg2_raddr_o = rs2; 11 end 12... 13 

第1行,opcode就是指令編碼中的第6部分內(nèi)容。

第3行,`INST_TYPE_R_M的值為7’b0110011。

第4行,funct7是指指令編碼中的第1部分內(nèi)容。

第5行,funct3是指指令編碼中的第4部分內(nèi)容。

第6行,到了這里,第1、4、6這三部分已經(jīng)譯碼完畢,已經(jīng)可以確定當(dāng)前指令是add指令了。

第7行,設(shè)置寫寄存器標(biāo)志為1,表示執(zhí)行模塊結(jié)束后的下一個(gè)時(shí)鐘需要寫寄存器。

第8行,設(shè)置寫寄存器地址為rd,rd的值為指令編碼里的第5部分內(nèi)容。

第9行,設(shè)置讀寄存器1的地址為rs1,rs1的值為指令編碼里的第3部分內(nèi)容。

第10行,設(shè)置讀寄存器2的地址為rs2,rs2的值為指令編碼里的第2部分內(nèi)容。

其他指令的譯碼過程是類似的,這里就不重復(fù)了。譯碼模塊看起來代碼很多,但是大部分代碼都是類似的。

譯碼模塊還有個(gè)作用是當(dāng)指令為加載內(nèi)存指令(比如lw等)時(shí),向總線發(fā)出請(qǐng)求訪問內(nèi)存的信號(hào)。這部分內(nèi)容將在總線一節(jié)再分析。

譯碼模塊的輸出會(huì)送到id_ex模塊(id_ex.v)的輸入,id_ex模塊是一個(gè)時(shí)序電路,作用是將輸入的信號(hào)打一拍后再輸出到執(zhí)行模塊(ex.v)。

執(zhí)行

執(zhí)行模塊所在的源文件:rtl/core/ex.v

執(zhí)行(ex)模塊是一個(gè)純組合邏輯電路,主要作用有以下幾點(diǎn):

1.根據(jù)當(dāng)前是什么指令執(zhí)行對(duì)應(yīng)的操作,比如add指令,則將寄存器1的值和寄存器2的值相加。

2.如果是內(nèi)存加載指令,則讀取對(duì)應(yīng)地址的內(nèi)存數(shù)據(jù)。

3.如果是跳轉(zhuǎn)指令,則發(fā)出跳轉(zhuǎn)信號(hào)。

執(zhí)行模塊的輸入輸出信號(hào)如下表所示:

下面以add指令為例說明,add指令的作用就是將寄存器1的值和寄存器2的值相加,最后將結(jié)果寫入目的寄存器。代碼如下:

 1...  2`INST_TYPE_R_M: begin  3 if ((funct7 == 7'b0000000) || (funct7 == 7'b0100000)) begin  4 case (funct3)  5 `INST_ADD_SUB: begin  6 jump_flag = `JumpDisable;  7 hold_flag = `HoldDisable;  8 jump_addr = `ZeroWord;  9 mem_wdata_o = `ZeroWord; 10 mem_raddr_o = `ZeroWord; 11 mem_waddr_o = `ZeroWord; 12 mem_we = `WriteDisable; 13 if (inst_i[30] == 1'b0) begin 14 reg_wdata = reg1_rdata_i + reg2_rdata_i; 15 end else begin 16 reg_wdata = reg1_rdata_i - reg2_rdata_i; 17 end 18 ... 19 end 20...

第2~4行,譯碼操作。

第5行,對(duì)add或sub指令進(jìn)行處理。

第6~12行,當(dāng)前指令不涉及到的操作(比如跳轉(zhuǎn)、寫內(nèi)存等)需要將其置回默認(rèn)值。

第13行,指令編碼中的第30位區(qū)分是add指令還是sub指令。0表示add指令,1表示sub指令。

第14行,執(zhí)行加法操作。

第16行,執(zhí)行減法操作。

其他指令的執(zhí)行是類似的,需要注意的是沒有涉及的信號(hào)要將其置為默認(rèn)值,if和case情況要寫全,避免產(chǎn)生鎖存器。

下面以beq指令說明跳轉(zhuǎn)指令的執(zhí)行。beq指令的編碼如下:

beq指令的作用就是當(dāng)寄存器1的值和寄存器2的值相等時(shí)發(fā)生跳轉(zhuǎn),跳轉(zhuǎn)的目的地址為當(dāng)前指令的地址加上符號(hào)擴(kuò)展的imm的值。具體代碼如下:

 1...  2`INST_TYPE_B: begin  3 case (funct3)  4 `INST_BEQ: begin  5 hold_flag = `HoldDisable;  6 mem_wdata_o = `ZeroWord;  7 mem_raddr_o = `ZeroWord;  8 mem_waddr_o = `ZeroWord;  9 mem_we = `WriteDisable; 10 reg_wdata = `ZeroWord; 11 if (reg1_rdata_i == reg2_rdata_i) begin 12 jump_flag = `JumpEnable; 13 jump_addr = inst_addr_i + {{20{inst_i[31]}}, inst_i[7], inst_i[30:25], inst_i[11:8], 1'b0}; 14 end else begin 15 jump_flag = `JumpDisable; 16 jump_addr = `ZeroWord; 17 end 18 ... 19end 20...

第2~4行,譯碼出beq指令。

第5~10行,沒有涉及的信號(hào)置為默認(rèn)值。

第11行,判斷寄存器1的值是否等于寄存器2的值。

第12行,跳轉(zhuǎn)使能,即發(fā)生跳轉(zhuǎn)。

第13行,計(jì)算出跳轉(zhuǎn)的目的地址。

第15、16行,不發(fā)生跳轉(zhuǎn)。

其他跳轉(zhuǎn)指令的執(zhí)行是類似的,這里就不再重復(fù)了。

訪存

由于tinyriscv只有三級(jí)流水線,因此沒有訪存這個(gè)階段,訪存的操作放在了執(zhí)行模塊中。具體是這樣的,在譯碼階段如果識(shí)別出是內(nèi)存訪問指令(lb、lh、lw、lbu、lhu、sb、sh、sw),則向總線發(fā)出內(nèi)存訪問請(qǐng)求,具體代碼(位于id.v)如下:

 1...  2`INST_TYPE_L: begin  3 case (funct3)  4 `INST_LB, `INST_LH, `INST_LW, `INST_LBU, `INST_LHU: begin  5 reg1_raddr_o = rs1;  6 reg2_raddr_o = `ZeroReg;  7 reg_we_o = `WriteEnable;  8 reg_waddr_o = rd;  9 mem_req = `RIB_REQ; 10 end 11 default: begin 12 reg1_raddr_o = `ZeroReg; 13 reg2_raddr_o = `ZeroReg; 14 reg_we_o = `WriteDisable; 15 reg_waddr_o = `ZeroReg; 16 end 17 endcase 18end 19`INST_TYPE_S: begin 20 case (funct3) 21 `INST_SB, `INST_SW, `INST_SH: begin 22 reg1_raddr_o = rs1; 23 reg2_raddr_o = rs2; 24 reg_we_o = `WriteDisable; 25 reg_waddr_o = `ZeroReg; 26 mem_req = `RIB_REQ; 27 end 28...

第2~4行,譯碼出內(nèi)存加載指令,lb、lh、lw、lbu、lhu。

第5行,需要讀寄存器1。

第6行,不需要讀寄存器2。

第7行,寫目的寄存器使能。

第8行,寫目的寄存器的地址,即寫哪一個(gè)通用寄存器。

第9行,發(fā)出訪問內(nèi)存請(qǐng)求。

第19~21行,譯碼出內(nèi)存存儲(chǔ)指令,sb、sw、sh。

第22行,需要讀寄存器1。

第23行,需要讀寄存器2。

第24行,不需要寫目的寄存器。

第26行,發(fā)出訪問內(nèi)存請(qǐng)求。

問題來了,為什么在取指階段發(fā)出內(nèi)存訪問請(qǐng)求?這跟總線的設(shè)計(jì)是相關(guān)的,這里先不具體介紹總線的設(shè)計(jì),只需要知道如果需要訪問內(nèi)存,則需要提前一個(gè)時(shí)鐘向總線發(fā)出請(qǐng)求。

在譯碼階段向總線發(fā)出內(nèi)存訪問請(qǐng)求后,在執(zhí)行階段就會(huì)得到對(duì)應(yīng)的內(nèi)存數(shù)據(jù)。

下面看執(zhí)行階段的內(nèi)存加載操作,以lb指令為例,lb指令的作用是訪問內(nèi)存中的某一個(gè)字節(jié),代碼(位于ex.v)如下:

 1...  2`INST_TYPE_L: begin  3 case (funct3)  4 `INST_LB: begin  5 jump_flag = `JumpDisable;  6 hold_flag = `HoldDisable;  7 jump_addr = `ZeroWord;  8 mem_wdata_o = `ZeroWord;  9 mem_waddr_o = `ZeroWord; 10 mem_we = `WriteDisable; 11 mem_raddr_o = reg1_rdata_i + {{20{inst_i[31]}}, inst_i[31:20]}; 12 case (mem_raddr_index) 13 2'b00: begin 14 reg_wdata = {{24{mem_rdata_i[7]}}, mem_rdata_i[7:0]}; 15 end 16 2'b01: begin 17 reg_wdata = {{24{mem_rdata_i[15]}}, mem_rdata_i[15:8]}; 18 end 19 2'b10: begin 20 reg_wdata = {{24{mem_rdata_i[23]}}, mem_rdata_i[23:16]}; 21 end 22 default: begin 23 reg_wdata = {{24{mem_rdata_i[31]}}, mem_rdata_i[31:24]}; 24 end 25 endcase 26 end 27... 28 

第2~4行,譯碼出lb指令。

第5~10行,將沒有涉及的信號(hào)置為默認(rèn)值。

第11行,得到訪存的地址。

第12行,由于訪問內(nèi)存的地址必須是4字節(jié)對(duì)齊的,因此這里的mem_raddr_index的含義就是32位內(nèi)存數(shù)據(jù)(4個(gè)字節(jié))中的哪一個(gè)字節(jié),2’b00表示第0個(gè)字節(jié),即最低字節(jié),2’b01表示第1個(gè)字節(jié),2’b10表示第2個(gè)字節(jié),2’b11表示第3個(gè)字節(jié),即最高字節(jié)。

第14、17、20、23行,寫寄存器數(shù)據(jù)。

回寫

由于tinyriscv只有三級(jí)流水線,因此也沒有回寫(write back,或者說寫回)這個(gè)階段,在執(zhí)行階段結(jié)束后的下一個(gè)時(shí)鐘上升沿就會(huì)把數(shù)據(jù)寫回寄存器或者內(nèi)存。

需要注意的是,在執(zhí)行階段,判斷如果是內(nèi)存存儲(chǔ)指令(sb、sh、sw),則向總線發(fā)出訪問內(nèi)存請(qǐng)求。而對(duì)于內(nèi)存加載(lb、lh、lw、lbu、lhu)指令是不需要的。因?yàn)閮?nèi)存存儲(chǔ)指令既需要加載內(nèi)存數(shù)據(jù)又需要往內(nèi)存存儲(chǔ)數(shù)據(jù)。

以sb指令為例,代碼(位于ex.v)如下:

 1...  2`INST_TYPE_S: begin  3 case (funct3)  4 `INST_SB: begin  5 jump_flag = `JumpDisable;  6 hold_flag = `HoldDisable;  7 jump_addr = `ZeroWord;  8 reg_wdata = `ZeroWord;  9 mem_we = `WriteEnable; 10 mem_req = `RIB_REQ; 11 mem_waddr_o = reg1_rdata_i + {{20{inst_i[31]}}, inst_i[31:25], inst_i[11:7]}; 12 mem_raddr_o = reg1_rdata_i + {{20{inst_i[31]}}, inst_i[31:25], inst_i[11:7]}; 13 case (mem_waddr_index) 14 2'b00: begin 15 mem_wdata_o = {mem_rdata_i[31:8], reg2_rdata_i[7:0]}; 16 end 17 2'b01: begin 18 mem_wdata_o = {mem_rdata_i[31:16], reg2_rdata_i[7:0], mem_rdata_i[7:0]}; 19 end 20 2'b10: begin 21 mem_wdata_o = {mem_rdata_i[31:24], reg2_rdata_i[7:0], mem_rdata_i[15:0]}; 22 end 23 default: begin 24 mem_wdata_o = {reg2_rdata_i[7:0], mem_rdata_i[23:0]}; 25 end 26 endcase 27 end 28... 29 

第2~4行,譯碼出sb指令。

第5~8行,將沒有涉及的信號(hào)置為默認(rèn)值。

第9行,寫內(nèi)存使能。

第10行,發(fā)出訪問內(nèi)存請(qǐng)求。

第11行,內(nèi)存寫地址。

第12行,內(nèi)存讀地址,讀地址和寫地址是一樣的。

第13行,mem_waddr_index的含義就是寫32位內(nèi)存數(shù)據(jù)中的哪一個(gè)字節(jié)。

第15、18、21、24行,寫內(nèi)存數(shù)據(jù)。

sb指令只改變讀出來的32位內(nèi)存數(shù)據(jù)中對(duì)應(yīng)的字節(jié),其他3個(gè)字節(jié)的數(shù)據(jù)保持不變,然后寫回到內(nèi)存中。

跳轉(zhuǎn)和流水線暫停

跳轉(zhuǎn)就是改變PC寄存器的值。又因?yàn)樘D(zhuǎn)與否需要在執(zhí)行階段才知道,所以當(dāng)需要跳轉(zhuǎn)時(shí),則需要暫停流水線(正確來說是沖刷流水線。流水線是不可以暫停的,除非時(shí)鐘不跑了)。那怎么暫停流水線呢?或者說怎么實(shí)現(xiàn)流水線沖刷呢?tinyriscv的流水線結(jié)構(gòu)如下圖所示。


其中長(zhǎng)方形表示的是時(shí)序邏輯電路,云狀型表示的是組合邏輯電路。在執(zhí)行階段,當(dāng)判斷需要發(fā)生跳轉(zhuǎn)時(shí),發(fā)出跳轉(zhuǎn)信號(hào)和跳轉(zhuǎn)地址給ctrl(ctrl.v)模塊。ctrl模塊判斷跳轉(zhuǎn)信號(hào)有效后會(huì)給pc_reg、if_id和id_ex模塊發(fā)出流水線暫停信號(hào),并且還會(huì)給pc_reg模塊發(fā)出跳轉(zhuǎn)地址。在時(shí)鐘上升沿到來時(shí),if_id和id_ex模塊如果檢測(cè)到流水線暫停信號(hào)有效則送出NOP指令,從而使得整條流水線(譯碼階段、執(zhí)行階段)流淌的都是NOP指令,已經(jīng)取出的指令就會(huì)無效,這就是流水線沖刷機(jī)制。


下面看ctrl.v模塊是怎么設(shè)計(jì)的。ctrl.v的輸入輸出信號(hào)如下表所示:

可知,暫停信號(hào)來自多個(gè)模塊。對(duì)于跳轉(zhuǎn)(跳轉(zhuǎn)包含暫停流水線操作),是要沖刷整條流水線的,因?yàn)樘D(zhuǎn)后流水線上其他階段的其他操作是無效的。對(duì)于其他模塊的暫停信號(hào),一種最簡(jiǎn)單的設(shè)計(jì)就是也沖刷整條流水線,但是這樣的話MCU的效率就會(huì)低一些。另一種設(shè)計(jì)就是根據(jù)不同的暫停信號(hào),暫停不同的流水線階段。比如對(duì)于總線請(qǐng)求的暫停只需要暫停PC寄存器這一階段就可以了,讓流水線上的其他階段繼續(xù)工作。看ctrl.v的代碼:

 1...  2 always @ (*) begin  3 if (rst == `RstEnable) begin  4 hold_flag_o = `Hold_None;  5 jump_flag_o = `JumpDisable;  6 jump_addr_o = `ZeroWord;  7 end else begin  8 jump_addr_o = jump_addr_i;  9 jump_flag_o = jump_flag_i; 10 // 默認(rèn)不暫停 11 hold_flag_o = `Hold_None; 12 // 按優(yōu)先級(jí)處理不同模塊的請(qǐng)求 13 if (jump_flag_i == `JumpEnable || hold_flag_ex_i == `HoldEnable || hold_flag_clint_i == `HoldEnable) begin 14 // 暫停整條流水線 15 hold_flag_o = `Hold_Id; 16 end else if (hold_flag_rib_i == `HoldEnable) begin 17 // 暫停PC,即取指地址不變 18 hold_flag_o = `Hold_Pc; 19 end else if (jtag_halt_flag_i == `HoldEnable) begin 20 // 暫停整條流水線 21 hold_flag_o = `Hold_Id; 22 end else begin 23 hold_flag_o = `Hold_None; 24 end 25 end 26 end 27...

第3~6行,復(fù)位時(shí)賦默認(rèn)值。

第8行,輸出跳轉(zhuǎn)地址直接等于輸入跳轉(zhuǎn)地址。

第9行,輸出跳轉(zhuǎn)標(biāo)志直接等于輸入跳轉(zhuǎn)標(biāo)志。

第11行,默認(rèn)不暫停流水線。

第13、14行,對(duì)于跳轉(zhuǎn)操作、來自執(zhí)行階段的暫停、來自中斷模塊的暫停則暫停整條流水線。

第16~18行,對(duì)于總線暫停,只需要暫停PC寄存器,讓譯碼和執(zhí)行階段繼續(xù)運(yùn)行。

第19~21行,對(duì)于jtag模塊暫停,則暫停整條流水線。

跳轉(zhuǎn)時(shí)只需要暫停流水線一個(gè)時(shí)鐘周期,但是如果是多周期指令(比如除法指令),則需要暫停流水線多個(gè)時(shí)鐘周期。

總線

設(shè)想一下一個(gè)沒有總線的SOC,處理器核與外設(shè)之間的連接是怎樣的??赡軙?huì)如下圖所示:

可見,處理器核core直接與每個(gè)外設(shè)進(jìn)行交互。假設(shè)一個(gè)外設(shè)有一條地址總線和一條數(shù)據(jù)總線,總共有N個(gè)外設(shè),那么處理器核就有N條地址總線和N條數(shù)據(jù)總線,而且每增加一個(gè)外設(shè)就要修改(改動(dòng)還不小)core的代碼。有了總線之后(見本章開頭的圖2_1),處理器核只需要一條地址總線和一條數(shù)據(jù)總線,大大簡(jiǎn)化了處理器核與外設(shè)之間的連接。

目前已經(jīng)有不少成熟、標(biāo)準(zhǔn)的總線,比如AMBA、wishbone、AXI等。設(shè)計(jì)CPU時(shí)大可以直接使用其中某一種,以節(jié)省開發(fā)時(shí)間。但是為了追求簡(jiǎn)單,tinyriscv并沒有使用這些總線,而是自主設(shè)計(jì)了一種名為RIB(RISC-V Internal Bus)的總線。RIB總線支持多主多從連接,但是同一時(shí)刻只支持一主一從通信。RIB總線上的各個(gè)主設(shè)備之間采用固定優(yōu)先級(jí)仲裁機(jī)制。

RIB總線模塊所在的源文件:rtl/core/rib.v

RIB總線模塊的輸入輸出信號(hào)如下表所示(由于各個(gè)主、從之間的信號(hào)是類似的,所以這里只列出其中一個(gè)主和一個(gè)從的信號(hào)):

RIB總線本質(zhì)上是一個(gè)多路選擇器,從多個(gè)主設(shè)備中選擇其中一個(gè)來訪問對(duì)應(yīng)的從設(shè)備。

RIB總線地址的最高4位決定要訪問的是哪一個(gè)從設(shè)備,因此最多支持16個(gè)從設(shè)備。

仲裁方式采用的類似狀態(tài)機(jī)的方式來實(shí)現(xiàn),代碼如下所示:

 1...  2 // 主設(shè)備請(qǐng)求信號(hào)  3 assign req = {m2_req_i, m1_req_i, m0_req_i};  4  5  6 // 授權(quán)主設(shè)備切換  7 always @ (posedge clk) begin  8 if (rst == `RstEnable) begin  9 grant <= grant1; 10 end else begin 11 grant <= next_grant; 12 end 13 end 14 15 // 仲裁邏輯 16 // 固定優(yōu)先級(jí)仲裁機(jī)制 17 // 優(yōu)先級(jí)由高到低:主設(shè)備0,主設(shè)備2,主設(shè)備1 18 always @ (*) begin 19 if (rst == `RstEnable) begin 20 next_grant = grant1; 21 hold_flag_o = `HoldDisable; 22 end else begin 23 case (grant) 24 grant0: begin 25 if (req[0]) begin 26 next_grant = grant0; 27 hold_flag_o = `HoldEnable; 28 end else if (req[2]) begin 29 next_grant = grant2; 30 hold_flag_o = `HoldEnable; 31 end else begin 32 next_grant = grant1; 33 hold_flag_o = `HoldDisable; 34 end 35 end 36 grant1: begin 37 if (req[0]) begin 38 next_grant = grant0; 39 hold_flag_o = `HoldEnable; 40 end else if (req[2]) begin 41 next_grant = grant2; 42 hold_flag_o = `HoldEnable; 43 end else begin 44 next_grant = grant1; 45 hold_flag_o = `HoldDisable; 46 end 47 end 48 grant2: begin 49 if (req[0]) begin 50 next_grant = grant0; 51 hold_flag_o = `HoldEnable; 52 end else if (req[2]) begin 53 next_grant = grant2; 54 hold_flag_o = `HoldEnable; 55 end else begin 56 next_grant = grant1; 57 hold_flag_o = `HoldDisable; 58 end 59 end 60 default: begin 61 next_grant = grant1; 62 hold_flag_o = `HoldDisable; 63 end 64 endcase 65 end 66 end 67...

第3行,主設(shè)備請(qǐng)求信號(hào)的組合。

第7~13行,切換主設(shè)備操作,默認(rèn)是授權(quán)給主設(shè)備1的,即取指模塊。從這里可以知道,從發(fā)出總線訪問請(qǐng)求后,需要一個(gè)時(shí)鐘周期才能完成切換。

第18~66行,通過組合邏輯電路來實(shí)現(xiàn)優(yōu)先級(jí)仲裁。

第20行,默認(rèn)授權(quán)給主設(shè)備1。

第24~35行,這是已經(jīng)授權(quán)給主設(shè)備0的情況。第25、28、31行,分別對(duì)應(yīng)主設(shè)備0、主設(shè)備2和主設(shè)備1的請(qǐng)求,通過if、else語句來實(shí)現(xiàn)優(yōu)先級(jí)。第27、30行,主設(shè)備0和主設(shè)備2的請(qǐng)求需要暫停流水線,這里只需要暫停PC階段,讓譯碼和執(zhí)行階段繼續(xù)執(zhí)行。

第3647行,這是已經(jīng)授權(quán)給主設(shè)備1的情況,和第2435行的操作是類似的。

第4859行,這是已經(jīng)授權(quán)給主設(shè)備2的情況,和第2435行的操作是類似的。

注意:RIB總線上不同的主設(shè)備切換是需要一個(gè)時(shí)鐘周期的,因此如果想要在執(zhí)行階段讀取到外設(shè)的數(shù)據(jù),則需要在譯碼階段就發(fā)出總線訪問請(qǐng)求。

中斷

中斷(中斷返回)本質(zhì)上也是一種跳轉(zhuǎn),只不過還需要附加一些讀寫CSR寄存器的操作。

RISC-V中斷分為兩種類型,一種是同步中斷,即ECALL、EBREAK等指令所產(chǎn)生的中斷,另一種是異步中斷,即GPIO、UART等外設(shè)產(chǎn)生的中斷。

對(duì)于中斷模塊設(shè)計(jì),一種簡(jiǎn)單的方法就是當(dāng)檢測(cè)到中斷(中斷返回)信號(hào)時(shí),先暫停整條流水線,設(shè)置跳轉(zhuǎn)地址為中斷入口地址,然后讀、寫必要的CSR寄存器(mstatus、mepc、mcause等),等讀寫完這些CSR寄存器后取消流水線暫停,這樣處理器就可以從中斷入口地址開始取指,進(jìn)入中斷服務(wù)程序。

下面看tinyriscv的中斷是如何設(shè)計(jì)的。中斷模塊所在文件:rtl/core/clint.v

輸入輸出信號(hào)列表如下:

先看中斷模塊是怎樣判斷有中斷信號(hào)產(chǎn)生的,如下代碼:

 1...  2 always @ (*) begin  3 if (rst == `RstEnable) begin  4 int_state = S_INT_IDLE;  5 end else begin  6 if (inst_i == `INST_ECALL || inst_i == `INST_EBREAK) begin  7 int_state = S_INT_SYNC_ASSERT;  8 end else if (int_flag_i != `INT_NONE && global_int_en_i == `True) begin  9 int_state = S_INT_ASYNC_ASSERT; 10 end else if (inst_i == `INST_MRET) begin 11 int_state = S_INT_MRET; 12 end else begin 13 int_state = S_INT_IDLE; 14 end 15 end 16 end 17...

第3~4行,復(fù)位后的狀態(tài),默認(rèn)沒有中斷要處理。

第6~7行,判斷當(dāng)前指令是否是ECALL或者EBREAK指令,如果是則設(shè)置中斷狀態(tài)為S_INT_SYNC_ASSERT,表示有同步中斷要處理。

第8~9行,判斷是否有外設(shè)中斷信號(hào)產(chǎn)生,如果是則設(shè)置中斷狀態(tài)為S_INT_ASYNC_ASSERT,表示有異步中斷要處理。

第10~11行,判斷當(dāng)前指令是否是MRET指令,MRET指令是中斷返回指令。如果是,則設(shè)置中斷狀態(tài)為S_INT_MRET。

下面就根據(jù)當(dāng)前的中斷狀態(tài)做不同處理(讀寫不同的CSR寄存器),代碼如下:

 1...  2 always @ (posedge clk) begin  3 if (rst == `RstEnable) begin  4 csr_state <= S_CSR_IDLE;  5 cause <= `ZeroWord;  6 inst_addr <= `ZeroWord;  7 end else begin  8 case (csr_state)  9 S_CSR_IDLE: begin 10 if (int_state == S_INT_SYNC_ASSERT) begin 11 csr_state <= S_CSR_MEPC; 12 inst_addr <= inst_addr_i; 13 case (inst_i) 14 `INST_ECALL: begin 15 cause <= 32'd11; 16 end 17 `INST_EBREAK: begin 18 cause <= 32'd3; 19 end 20 default: begin 21 cause <= 32'd10; 22 end 23 endcase 24 end else if (int_state == S_INT_ASYNC_ASSERT) begin 25 // 定時(shí)器中斷 26 cause <= 32'h80000004; 27 csr_state <= S_CSR_MEPC; 28 inst_addr <= inst_addr_i; 29 // 中斷返回 30 end else if (int_state == S_INT_MRET) begin 31 csr_state <= S_CSR_MSTATUS_MRET; 32 end 33 end 34 S_CSR_MEPC: begin 35 csr_state <= S_CSR_MCAUSE; 36 end 37 S_CSR_MCAUSE: begin 38 csr_state <= S_CSR_MSTATUS; 39 end 40 S_CSR_MSTATUS: begin 41 csr_state <= S_CSR_IDLE; 42 end 43 S_CSR_MSTATUS_MRET: begin 44 csr_state <= S_CSR_IDLE; 45 end 46 default: begin 47 csr_state <= S_CSR_IDLE; 48 end 49 endcase 50 end 51 end 52...

第3~6行,CSR狀態(tài)默認(rèn)處于S_CSR_IDLE。

第1023行,當(dāng)CSR處于S_CSR_IDLE時(shí),如果中斷狀態(tài)為S_INT_SYNC_ASSERT,則在第11行將CSR狀態(tài)設(shè)置為S_CSR_MEPC,在第12行將當(dāng)前指令地址保存下來。在第1323行,根據(jù)不同的指令類型,設(shè)置不同的中斷碼(Exception Code),這樣在中斷服務(wù)程序里就可以知道當(dāng)前中斷發(fā)生的原因了。

第24~28行,目前tinyriscv只支持定時(shí)器這個(gè)外設(shè)中斷。

第30~31行,如果是中斷返回指令,則設(shè)置CSR狀態(tài)為S_CSR_MSTATUS_MRET。

第34~48行,一個(gè)時(shí)鐘切換一下CSR狀態(tài)。

接下來就是寫CSR寄存器操作,需要根據(jù)上面的CSR狀態(tài)來寫。

 1...  2// 發(fā)出中斷信號(hào)前,先寫幾個(gè)CSR寄存器  3 always @ (posedge clk) begin  4 if (rst == `RstEnable) begin  5 we_o <= `WriteDisable;  6 waddr_o <= `ZeroWord;  7 data_o <= `ZeroWord;  8 end else begin  9 case (csr_state) 10 // 將mepc寄存器的值設(shè)為當(dāng)前指令地址 11 S_CSR_MEPC: begin 12 we_o <= `WriteEnable; 13 waddr_o <= {20'h0, `CSR_MEPC}; 14 data_o <= inst_addr; 15 end 16 // 寫中斷產(chǎn)生的原因 17 S_CSR_MCAUSE: begin 18 we_o <= `WriteEnable; 19 waddr_o <= {20'h0, `CSR_MCAUSE}; 20 data_o <= cause; 21 end 22 // 關(guān)閉全局中斷 23 S_CSR_MSTATUS: begin 24 we_o <= `WriteEnable; 25 waddr_o <= {20'h0, `CSR_MSTATUS}; 26 data_o <= {csr_mstatus[31:4], 1'b0, csr_mstatus[2:0]}; 27 end 28 // 中斷返回 29 S_CSR_MSTATUS_MRET: begin 30 we_o <= `WriteEnable; 31 waddr_o <= {20'h0, `CSR_MSTATUS}; 32 data_o <= {csr_mstatus[31:4], csr_mstatus[7], csr_mstatus[2:0]}; 33 end 34 default: begin 35 we_o <= `WriteDisable; 36 waddr_o <= `ZeroWord; 37 data_o <= `ZeroWord; 38 end 39 endcase 40 end 41 end 42...

第11~15行,寫mepc寄存器。

第17~21行,寫mcause寄存器。

第23~27行,關(guān)閉全局異步中斷。

第29~33行,寫mstatus寄存器。

最后就是發(fā)出中斷信號(hào),中斷信號(hào)會(huì)進(jìn)入到執(zhí)行階段。

 1...  2 // 發(fā)出中斷信號(hào)給ex模塊  3 always @ (posedge clk) begin  4 if (rst == `RstEnable) begin  5 int_assert_o <= `INT_DEASSERT;  6 int_addr_o <= `ZeroWord;  7 end else begin  8 // 發(fā)出中斷進(jìn)入信號(hào).寫完mstatus寄存器才能發(fā)  9 if (csr_state == S_CSR_MSTATUS) begin 10 int_assert_o <= `INT_ASSERT; 11 int_addr_o <= csr_mtvec; 12 // 發(fā)出中斷返回信號(hào) 13 end else if (csr_state == S_CSR_MSTATUS_MRET) begin 14 int_assert_o <= `INT_ASSERT; 15 int_addr_o <= csr_mepc; 16 end else begin 17 int_assert_o <= `INT_DEASSERT; 18 int_addr_o <= `ZeroWord; 19 end 20 end 21 end 22...

有兩種情況需要發(fā)出中斷信號(hào),一種是進(jìn)入中斷,另一種是退出中斷。

第9~12行,寫完mstatus寄存器后發(fā)出中斷進(jìn)入信號(hào),中斷入口地址就是mtvec寄存器的值。

第13~15行,發(fā)出中斷退出信號(hào),中斷退出地址就是mepc寄存器的值。

JTAG

JTAG作為一種調(diào)試接口,在處理器設(shè)計(jì)里算是比較大而且復(fù)雜、卻不起眼的一個(gè)模塊,絕大部分開源處理器核都沒有JTAG(調(diào)試)模塊。但是為了完整性,tinyriscv還是加入了JTAG模塊,還單獨(dú)為JTAG寫了一篇文章《深入淺出RISC-V調(diào)試》,感興趣的同學(xué)可以去看一下,這里不再單獨(dú)介紹了。要明白JTAG模塊的設(shè)計(jì)原理,必須先看懂RISC-V的debug spec。

RTL仿真驗(yàn)證

寫完處理器代碼后,怎么證明所寫的處理器是能正確執(zhí)行指令的呢?這時(shí)就需要寫testbench來測(cè)試了。其實(shí)在寫代碼的時(shí)候就應(yīng)該在頭腦里進(jìn)行仿真。這里并沒有使用ModelSim這些軟件進(jìn)行仿真,而是使用了一個(gè)輕量級(jí)的iverilog和vvp工具。

在寫testbench文件時(shí),有兩點(diǎn)需要注意的,第一點(diǎn)就是在testbench文件里加上讀指令文件的操作:

1initial begin 2 $readmemh ("inst.data", tinyriscv_soc_top_0.u_rom._rom); 3end 

第2行代碼的作用就是將inst.data文件讀入到rom模塊里,inst.data里面的內(nèi)容就是一條條指令,這樣處理器開始執(zhí)行時(shí)就可以從rom里取到指令。

第二點(diǎn)就是,在仿真期間將仿真波形dump出到某一個(gè)文件里:

1initial begin 2 $dumpfile("tinyriscv_soc_tb.vcd"); 3 $dumpvars(0, tinyriscv_soc_tb); 4end 

這樣仿真波形就會(huì)被dump出到tinyriscv_soc_tb.vcd文件,使用gtkwave工具就可以查看波形了。

到這里,硬件篇的內(nèi)容就結(jié)束了。

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
關(guān)閉