Featured image of post 北航CO P7 MIPS微系统搭建

北航CO P7 MIPS微系统搭建

P7课下搭建任务


说在前面

还得是设计文档。
每次一旦盯着理论看,又想看全又想做细,就会陷入P5当时的茫然
总是想一针见血地写出最有远见卓识的代码,当然会踌躇,更何况还未必能写出来。
但是一步一步走出来,也就走出来了。

设计文档

在对P7的整体内容有了把握之后,我们可以知道,本次P7主要要完成的任务是:

  • 更改流水线各级使之可以产生异常
  • 添加 CP0 与 异常处理
  • 添加 Bridge 与两个外设(计时器)交互

ExcCode的产生与流水

根据教程的表格,我们可以得到:

异常与中断码 助记符与名称 指令与指令类型 描述
0 Int (外部中断) 所有指令 中断请求,来源于计时器与外部中断。
4 AdEL (取指异常) 所有指令 PC 地址未字对齐。
4 AdEL (取指异常) 所有指令 PC 地址超过 0x3000 ~ 0x6ffc。
4 AdEL (取数异常) lw 取数地址未与 4 字节对齐。
4 AdEL (取数异常) lh 取数地址未与 2 字节对齐。
4 AdEL (取数异常) lh, lb 取 Timer 寄存器的值。
4 AdEL (取数异常) load 型指令 计算地址时加法溢出。
4 AdEL (取数异常) load 型指令 取数地址超出 DM、Timer0、Timer1、中断发生器的范围。
5 AdES (存数异常) sw 存数地址未 4 字节对齐。
5 AdES (存数异常) sh 存数地址未 2 字节对齐。
5 AdES (存数异常) sh, sb 存 Timer 寄存器的值。
5 AdES (存数异常) store 型指令 计算地址加法溢出。
5 AdES (存数异常) store 型指令 向计时器的 Count 寄存器存值。
5 AdES (存数异常) store 型指令 存数地址超出 DM、Timer0、Timer1、中断发生器的范围。
8 Syscall (系统调用) syscall 系统调用。
10 RI(未知指令) 未知指令 未知的指令码。
12 Ov(溢出异常) add, addi, sub 算术溢出。

根据教程的要求,我们容易知道,只需分析好每一个部位可能产生的异常,然后随着流水线流水即可。
同一条指令在某一个阶段不会产生多种异常。(这由异常的划分方式确定)
但是一个阶段可能存在多个异常。其处理顺序为先来后到。也就是处理最老的指令,执行到最后面的指令的新错误。

有人说,你这样做就是只处理最新的错误啊,怎么保证处理了最新的错误还能处理旧的错误呢?

这是因为处理异常的方法是把句子变成nop,并重新从受害指令下一条开始执行。
这样,未被处理的指令的错误就会重新展现出来,再次被处理。

我们决定,传入流水线的ExcCode,是要经过一个有优先级的多路选择器的。
为了区分,我们命名为

ExcCode_X(本阶段产生的) 和ExcCode_X_true(实际传入流水线的) 以及ExcCode_X_last(流水线传进来的)

接下来就来判断异常吧。
我打算根据流水级来进行分类处理。

注意:
Int表示外部中断,更表示此处无异常。因此ExcCode可以大胆赋0,具体是否中断还要看外部信号。

附一个可能用到的表格:

条目 地址或地址范围 备注
数据存储器 0x0000_0000∼0x0000_2FFF
指令存储器 0x0000_3000∼0x0000_6FFF
PC 初始值 0x0000_3000
异常处理程序入口地址 0x0000_4180
计时器 0 寄存器地址 0x0000_7F00∼0x0000_7F0B 计时器 0 的 3 个寄存器
计时器 1 寄存器地址 0x0000_7F10∼0x0000_7F1B 计时器 1 的 3 个寄存器
中断发生器响应地址 0x0000_7F20∼0x0000_7F23

我们使用以下宏定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
`define     Exc_AdEL       5'd4
`define     Exc_AdES       5'd5
`define     Exc_Syscall    5'd8
`define     Exc_RI         5'd10
`define     Exc_Ov         5'd12

`define  IM_start  32'h3000
`define  IM_end    32'h6fff
`define  DM_start  32'h0000
`define  DM_end    32'h2fff
`define  T0_start  32'h7f00
`define  T0_end    32'h7f0b
`define  T1_start  32'h7f10
`define  T1_end    32'h7f1b
`define  T0_count_start  32'h7f08
`define  T0_count_end    32'h7f0b
`define  T1_count_start  32'h7f18
`define  T1_count_end    32'h7f1b
`define  Int_generator_start 32'h7f20
`define  Int_generator_end   32'h7f23

ExcCode_F

我们发现只能发生: AdEL: PC未字对齐PC超界

1
2
3
assign ExcCode_F = ((pc_F[1:0] != 2'b0) || pc_F < `IM_start || pc_F > `IM_end) ? `Exc_AdEL : 0;

assign ExcCode_F_true = ExcCode_F;

ExcCode_D

D级的核心工作是译码。

RI:未知指令
Syscall: 系统调用

RI与Syscall信号也由Ctrl译码时顺带产生。

1
2
3
4
5
6
assign RI = !(nop || add || sub || And || Or || slt || sltu || lui || addi || andi || ori ||
              lb || lh || lw || sb || sh || sw ||
              mult || multu || div || divu || mfhi || mflo || mthi || mtlo ||
              beq || bne || jal || jr || mfc0 || mtc0 || eret || syscall);

assign Syscall = syscall;
1
2
3
4
5
assign ExcCode_D = (Syscall) ? `Exc_Syscall :
                   (RI) ? `Exc_RI : 0;
//此处虽然有优先级,但是两者是不会同时发生的
assign ExcCode_D_true = (ExcCode_D_last) ? ExcCode_D_last : ExcCode_D;
//优先级,同一指令按最老的错误来

ExcCode_E

AdEL: 计算load地址加法溢出
AdES:计算store地址加法溢出
Ov:add,addi,sub算数溢出

这些信号利用ALU模块得出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
wire overflow_add;
wire overflow_sub;

wire [32:0] ext_num_1;
wire [32:0] ext_num_2;

wire [32:0] ext_add;
wire [32:0] ext_sub;

assign ext_num_1 = {num_1[31], num_1};
assign ext_num_2 = {num_2[31], num_2};

assign ext_add = ext_num_1 + ext_num_2;
assign ext_sub = ext_num_1 - ext_num_2;

assign overflow_add = ext_add[32] ^ ext_add[31];
assign overflow_sub = ext_sub[32] ^ ext_sub[31];

assign Ov = ((add || addi) && overflow_add) 
            || (sub && overflow_sub);

assign AdEL_E = (DMRd_E) && overflow_add;
assign AdES_E = (DMWr_E) && overflow_add;

大错特错!大错特错!!!

1
2
assign Ov = (((ALUOp == `ADD) || (ALUOp == `ADDI)) && !DMRd_E && !DMWr_E && overflow_add) 
            || ((ALUOp == `SUB) && overflow_sub);

de出这个bug真费了不少功夫。
第一次de出来是意识到ALUOp是ADD,未必指令就是ADD,
还有可能是store与load指令!!

结果还没过。。

这次很随便一个数据居然de出来了
因为一条ori指令出现了Ov!!!

这让我感到极为震惊。我就看了看Ov的所有相关信号————
当我看到add,并发现它是一个32位数时……

太抽象了!!!!! add原来是数据信号啊不是控制信号!!!
控制信号是ALUOp!!!!!

Update 12.1:
这也太抽象了!!!
感谢COKiller!!!
没想到能在这里de三次。。

第一次是因为sw犯了Ov
第二次是因为ori犯了Ov
第三次是因为div犯了Ov!!!!!!!

天打五雷轰。

multu,divu乃至于mfc0,mfhi都在ALUOp对应ADD!!!

所以我老实了。
我直接把Instr传到E级,由Instr指导is_ADD。

1
2
3
4
assign is_ADD = (Instr_E[31:26] == 6'b000000) && (Instr_E[5:0] == 6'b100000);

assign Ov = ((is_ADD || (ALUOp == `ADDI)) && overflow_add) 
            || ((ALUOp == `SUB) && overflow_sub);

这样应该就没有问题了罢。。

1
2
3
4
5
assign ExcCode_E = (AdEL_E) ? `Exc_AdEL :
                   (AdES_E) ? `Exc_AdES :
                   (Ov) ? `Exc_Ov : 0;

assign ExcCode_E_true = (ExcCode_E_last) ? ExcCode_E_last : ExcCode_E;

ExcCode_M

AdEL:

  • lw,lh字对齐
  • lh,lb取Timer
  • 超范围

AdES:

  • sw,sh字对齐
  • sh,sb写Timer
  • 所有store写Count寄存器
  • 超范围
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
assign AdEL_M = (DMRd_M == `LH && MemAddr_M[0])
             || (DMRd_M == `LW && MemAddr_M[1:0])
             || (DMRd_M == `LH && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
             || (DMRd_M == `LH && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
             || (DMRd_M == `LB && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
             || (DMRd_M == `LB && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
             || (DMRd_M && (MemAddr_M < `DM_start || MemAddr_M > `DM_end))
             || (DMRd_M && (MemAddr_M < `T0_start || MemAddr_M > `T0_end))
             || (DMRd_M && (MemAddr_M < `T1_start || MemAddr_M > `T1_end))
             || (DMRd_M && (MemAddr_M < `T0_start || MemAddr_M > `T0_end))
             || (DMRd_M && (MemAddr_M < `Int_generator_start || MemAddr_M > `Int_generator_end));

assign AdES_M = (DMWr_M == `SH && MemAddr_M[0])
             || (DMWr_M == `SW && MemAddr_M[1:0])
             || (DMWr_M == `SH && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
             || (DMWr_M == `SH && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
             || (DMWr_M == `SB && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
             || (DMWr_M == `SB && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
             || (DMWr_M && MemAddr_M >= `T0_count_start && MemAddr_M <= `T0_count_end)
             || (DMWr_M && MemAddr_M >= `T1_count_start && MemAddr_M <= `T1_count_end)
             || (DMWr_M && (MemAddr_M < `DM_start || MemAddr_M > `DM_end))
             || (DMWr_M && (MemAddr_M < `T0_start || MemAddr_M > `T0_end))
             || (DMWr_M && (MemAddr_M < `T1_start || MemAddr_M > `T1_end))
             || (DMWr_M && (MemAddr_M < `Int_generator_start || MemAddr_M > `Int_generator_end));

assign ExcCode_M = (AdEL_M) ? `Exc_AdEL : 
                   (AdES_M) ? `Exc_AdES : 0;

assign ExcCode_M_true = (ExcCode_M_last) ? ExcCode_M_last : ExcCode_M;

好嘞笑死了,已经被自己蠢死了

应当改为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
 assign AdEL_M = (DMRd_M == `LH && MemAddr_M[0])
            || (DMRd_M == `LW && MemAddr_M[1:0])
            || (DMRd_M == `LH && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
            || (DMRd_M == `LH && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
            || (DMRd_M == `LB && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
            || (DMRd_M == `LB && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
            || 
           (DMRd_M &&
           (MemAddr_M < `DM_start || MemAddr_M > `DM_end) &&
           (MemAddr_M < `T0_start || MemAddr_M > `T0_end) &&
           (MemAddr_M < `T1_start || MemAddr_M > `T1_end) &&
           (MemAddr_M < `Int_generator_start || MemAddr_M > `Int_generator_end));

assign AdES_M = (DMWr_M == `SH && MemAddr_M[0])
            || (DMWr_M == `SW && MemAddr_M[1:0])
            || (DMWr_M == `SH && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
            || (DMWr_M == `SH && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
            || (DMWr_M == `SB && MemAddr_M >= `T0_start && MemAddr_M <= `T0_end)
            || (DMWr_M == `SB && MemAddr_M >= `T1_start && MemAddr_M <= `T1_end)
            || (DMWr_M && MemAddr_M >= `T0_count_start && MemAddr_M <= `T0_count_end)
            || (DMWr_M && MemAddr_M >= `T1_count_start && MemAddr_M <= `T1_count_end)
            || 
            (DMWr_M && 
            (MemAddr_M < `DM_start || MemAddr_M > `DM_end) &&
            (MemAddr_M < `T0_start || MemAddr_M > `T0_end) &&
            (MemAddr_M < `T1_start || MemAddr_M > `T1_end) &&
            (MemAddr_M < `Int_generator_start || MemAddr_M > `Int_generator_end));

不同的异常类型确实应该或起来,但是地址越界是种异常!!
必须同时满足在所有区域之外!内部表述应使用

W级不产生异常。
至此,我们貌似写完了所有异常。

CP0及异常处理

我们一般将CP0放进M级
这意味着指令异常在M级进行处理。

这形成了单周期CPU的封装: 一般认为M级指令为正在执行的指令
一般认为PC_M即为宏观PC
一般认为W级处的指令是已完成的指令
一般认为F,D,E级的指令都是未开始执行的指令

一旦有这样单周期的认知,那么
出现异常就是指M级指令出现异常;
受害PC即为M级PC。
或者说,任何指令在某一流水级产生的异常,
都要等待该指令进入M级才可被处理。

CP0基础信息

课程组给出的接口表格:

端口 方向 位数 解释
clk I 1 时钟信号
reset I 1 复位信号
en I 1 写使能信号
CP0Addr I 5 寄存器地址
CP0In I 32 CP0
CP0Out O 32 CP0
VPC I 32 受害PC
BDIn I 1 是否是延迟槽指令
ExcCodeIn I 5 记录异常类型
HWInt I 6 输入中断信号
EXLClr I 1 用来复位 EXL
EPCOut O 32 EPC 的值
Req O 1 进入处理程序请求

课程组给出的关键寄存器信息:

寄存器 编号 功能
SR 12 配置异常的功能
Cause 13 记录异常发生的原因和情况
EPC 14 记录异常处理结束后需要返回的PC

课程组给出的关键功能域:

寄存器 功能域 位域 解释
SR(State Register) IM(Interrupt Mask) 15:10 分别对应六个外部中断,相应位置1表示允许中断,置0表示禁止中断。这是一个被动的功能,只能通过mtc0这个指令修改,通过修改这个功能域,我们可以屏蔽一些中断。
SR(State Register) EXL(Exception Level) 1 任何异常发生时置1,这会强制进入核心态(也就是进入异常处理程序)并禁止中断。
SR(State Register) IE(Interrupt Enable) 0 全局中断使能,该位置1表示允许中断,置0表示禁止中断。
Cause BD(Branch Delay) 31 当该位置1时,EPC指向当前指令的前一条指令(一定为跳转),否则指向当前指令。
Cause IP(Interrupt Pending) 15:10 6位待决的中断位,分别对应6个外部中断,相应位置1表示有中断,置0表示无中断。每个周期将会被修改一次,修改的内容来自计时器和外部中断。
Cause ExcCode 6:2 异常编码,记录当前发生的是什么异常。
EPC - - 记录异常处理结束后需要返回的PC。

对于关键域,我们使用宏定义以方便书写:

1
2
3
4
5
6
`define       IM         SR[15:10]
`define       EXL        SR[1] 
`define       IE         SR[0]
`define       BD         Cause[31]
`define       IP         Cause[15:10]
`define       ExcCode    Cause[6:2]

对于CP0,它的主要功能即为
存储异常中断的相关信息,表征异常中断的开始与结束,
引导转向异常中断模块处理与退回原指令。

判断异常中断的发生

1
2
3
4
5
6
7
wire Exc_req;
wire Int_req;

assign Exc_req = (ExcCodeIn != 5'b0) && (`EXL == 1'b0); //我们不太确定EXL为1时能否触发异常。但是其实我是把EXL==1当成全局异常判断使能看待的
assign Int_req = (|(HWInt & `IM)) && `IE && (`EXL == 1'b0);

assign req = Exc_req || Int_req;

这里比较巧妙的是 (|(HWInt & `IM)) 这一语句

巧妙使用了按位与,先得到各个信号是否中断中断是否被允许
然后再把这6位或起来(不或也可以)

当然最后不能忘了全局使能与EXL限制

存储异常中断的相关信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
always @(posedge clk) begin
    if (reset) begin
        SR <= 0;
        Cause <= 0;
        EPC <= 0;
    end
    else begin
        `IP <= HWInt;  
        if (req) begin
            `EXL <= 1'b1;
            EPC <= (BDIn) ? (VPC - 32'd4) : VPC; 
            `ExcCode <= (Int_req) ? 5'b0 : ExcCodeIn;
            `BD <= BDIn;
        end
        if (EXLCLr) begin
            `EXL <= 1'b0;
        end
    end
end

`ExcCode <= (Int_req) ? 5’b0 : ExcCodeIn;
这句话是表明优先级的,Int与Exc的区别,在CP0中就是靠Cause寄存器来展现。
如果同时发生Int与Exc,如何保证Int优先?
只需一个三目运算符,先判断中断。

EPC <= (BDIn) ? (VPC - 32’d4) : VPC;
这句话体现了BDIn的功能。
如果这句话是延迟槽语句,那么你应当保证跳转指令正常进行。如果你只重新执行延迟槽,跳转指令就不能实现。
那么此时我们就不采用直接重新执行受害指令的方法,而是采用执行受害指令前一句的分支跳转。

但是为什么可以选择执行受害指令上一句?
这样的做法无疑是执行了两遍这一指令。

但是正因为它是跳转指令,它并没有累加效应。
就连唯一有写功能的jal也只会写他对应的那一个值。

那就有人说,你这不是钻空子吗?万一添加个新跳转指令,让你给$ra写当前$ra的值加4,这不就完了吗?

新指令在W级,延迟槽在M级判出问题(或中断)。这样的话你完全来得及让第一次的写入无效,只需把W级写使能修改一下,把BDIn和req引出去,并说明这个时候不能写入。

诶诶,那又有人问了,你这么写不就说明你的写使能是可能最后突然改变的,那你之前要是执行过转发怎么办?

这就不得不说咱们的单周期思想了。在异常中断面前,M级以前的指令都相当于没有执行。转发给你啥都无所谓。

那就又有人问了,M级本身要是被转发了怎么办?

害,M级如果发生异常中断了,那就也需要重新执行,也相当于未执行指令。

好,一段小思考结束了。

写入关键寄存器

这一操作也应在CP0中完成
其实也只是添加了如下内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*always @(posedge clk) begin
    if (reset) begin
        SR <= 0;
        Cause <= 0;
        EPC <= 0;
    end
    else begin
        `IP <= HWInt;  
        if (req) begin
            `EXL <= 1'b1;
            EPC <= (BDIn) ? (VPC - 32'd4) : VPC; 
            `ExcCode <= (Int_req) ? 5'b0 : ExcCodeIn;
            `BD <= BDIn;
        end
        if (EXLCLr) begin
            `EXL <= 1'b0;
        end*/
        if (en && !req) begin //执行mtc0时发生中断,则不予写入
            case (CP0Addr)
                5'd12 : begin
                    SR <= CP0In; 
                end
                5'd14 : begin
                    EPC <= CP0In; 
                end
                default: begin
                    
                end
            endcase
        end/*
    end
end*/

但是具体en,CP0Addr,CP0In怎么得到,那就是CP0外部的事了。

在这里我还是打算直接写好。因为这确实是一块很小的,而且与下一部分关系不大的内容。

核心就是添加mtc0,mfc0指令,放进流水线等等。

这个和mfhi,mthi的逻辑几乎完全相同。

mtc0

先分析mtc0吧,只需要在M级给CP0的en接口接上Mtc0_M
写的地址是rd_M 写的内容是MemData_M

注意控制信号Tuse_rt = 2

mfc0

mfc0要注意RFWr置1,Tnew = 2 写的地址是rt 写的内容是RegData

1
2
assign RegData = Mfc0_W ? CP0Out_W :
                WdSel_W ? MemReadData_W : ALUData_W;

可以发现CP0Addr始终为rd,所以直接让该端口接rd_M。

整体来说是这个样子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
CP0 Cp0(
    .clk(clk),
    .reset(reset),
    .en(Mtc0_M),
    .CP0Addr(rd_M),
    .CP0In(MemData_M),
    .VPC(pc_M),
    .BDIn(PCSel_W), // 如果上一句是跳转,那么这一句是延迟槽
    .ExcCodeIn(ExcCode_M_true),
    .HWInt(HWInt),
    .EXLCLr(EXLClr),
    .CP0Out(CP0Out_M),
    .EPCOut(EPC),
    .req(req)
);

CPU进行异常处理

  • 语句跳转 遇到异常时进入Exception Handler 0x00004180
    在异常处理程序结束时会执行指令eret回到EPC

  • 语句清空 利用CP0生成的req信号对所有流水线寄存器进行清空。 (因为CP0在M级,那么此时W级的操作在本时钟周期已完成,由于同步复位,清空寄存器只会影响下一周期的内容,这样下一周期所有指令都已完成,不受干扰。)

    (注意精确异常,认真阅读教程中关于乘除槽精确异常的讲述。 )

这里有很重要的一点在于eret没有延迟槽。因此执行完eret后,下一条指令应当是EPC。

eret在D级,此时eret的下一条指令(物理层面)在F级

我们需要F级的指令是EPC而非eret的下一条指令


以下内容看乐子就行

我们采用一种奇妙的做法:

当eret在F级时,我们直接进行检测该指令是否是eret。

如果是,那么NPC为EPC。

1
2
3
4
5
6
wire Eret_F;

assign Eret_F = (Instr_F == 32'd01000010000000000000000000011000);

assign _npc =  Eret_F ? EPC :
              PCSel_D ? npc : (pc_F + 32'd4);

解释一下之前的定义:
_npc为真正的NPC
PCSel_D是判断D级是否为跳转指令
npc是跳转指令算出来的npc

这里要注意EPC它并不是一个定值,他可能被mtc0改变!!
因此有一套转发与阻塞逻辑:
我现在需要一个正确的pc

1
2
3
4
assign _EPC = (Mtc0_D && rd_D == 5'd14) ? V2_D :
              (Mtc0_E && rd_E == 5'd14) ? V2_E_Data :
              (Mtc0_M && rd_M == 5'd14) ? CP0In :
              EPC;

这样的转发足够吗?
不够!必须阻塞!!

mtc0不被阻塞的前提是mtc0认为自己能在写CP0之前拿到对的值
所以他在没有到达CP0的时候的值都有可能不对

EPC的值是在mtc0到达W级的时候被传送到F级供eret使用

在mtc0在M级的时候,它的CP0In肯定也是对的,可以转发。

在mtc0在D,E级的时候,都可以考虑阻塞。

1
2
3
4
5
6
7
/*assign stall = start_busy || (busy && MDALUOp_D) ||*/
        ((Eret_F && Mtc0_D && rd_D == 5'd14)) ||
        ((Eret_F && Mtc0_E && rd_E == 5'd14)) ||/*
        ((A1 == RegAddr_E) && (RFWr_E) && (A1 != 5'b0) && (Tuse_rs < Tnew_E)) ||
        ((A1 == RegAddr_M) && (RFWr_M) && (A1 != 5'b0) && (Tuse_rs < Tnew_M)) ||
        ((A2 == RegAddr_E) && (RFWr_E) && (A2 != 5'b0) && (Tuse_rt < Tnew_E)) ||
        ((A2 == RegAddr_M) && (RFWr_M) && (A2 != 5'b0) && (Tuse_rt < Tnew_M));*/

我们看似用的是D级的阻塞,但这样也能起到阻塞F级eret的作用


这一段的写法过于创新。我们不得不有把这种设计毁掉的打算。

Update 12.1:
我很抱歉。在没有de出真正的bug之前,我选择把这一种方法全部删去。采用了大家普遍使用的D级执行。

当然,在修改之后当时并没能解决任何问题。

这里要注意一点:
eret严格意义上在M级才能视为被执行。
因此EXLClr需要在Eret信号传到M级时才置1。

至于eret的清空延迟槽,就不再多说了,需要注意它的BD应为0。
在阻塞上我也是正常全力阻塞,转发也不想写了。。

逻辑就是eret在D级,mtc0在E,M级且要写的内容是EPC时直接大胆阻塞。

对于异常跳转,监测标志为req
req置1时,pc需变为0x00004180

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/*always @(posedge clk) begin
    if (reset) begin
        PCreg <= 32'h00003000;
    end*/ 
    else if (req) begin
        PCreg <= 32'h00004180;
    end/*
    else if (En_pc) begin
        PCreg <= npc;
    end
end*/

req优先级需注意,低于reset,位于第二高的地位。

紧接着,我们解决req对寄存器的清空。

本来很简单的一件事情,由于阻塞而变得复杂。
阻塞,产生了空泡,但这个空泡不应是全空的。

这个空泡应当是上一条指令生命的延续。

尤其是PC信息与Bd信息。
对于阻塞型清空,应保持不变。

如D_E流水线寄存器中:

1
2
3
         pc_E <= reset ? 32'h00003000 :
                   req ? 32'h0000_4180 : pc_D;
        PCSel_E <= (reset || req) ? 0 : PCSel_D;

真的是这样吗??

Update 12.2 上机debug

空泡要存的信息究竟是谁?

被阻塞指令的pc和跳转指令的PCSel

因此

1
2
3
4
        pc_E <= reset ? 32'h00003000 :
               req ? 32'h00004180 : pc_D;
       ExcCode_E_last <= 0;
       PCSel_E <= (reset || req) ? 0 : PCSel_E;

两种继承方式! PCSel直接继承,pc流水继承。

其次我已做了改动,eret不再是PCSel指令了。

为什么要这么做呢?

eret他要起到修正PCSel的作用这确实,但是他还有很多自己的特别作用!

你不能对它进行额外的流水处理!

你总不能让一句nop拥有eret性质吧,我们EXLClr可是跟eret很有关系的。

外设及其交互

其实整体来说,外设的作用就只有信息存储与中断产生。

中断信号的导入

中断信号相对简单,只需要传递给HWInt接口。

Timer0 输出的中断信号接入 HWInt[0] (最低中断位),Timer1 输出的中断信号接入 HWInt[1],来自中断发生器的中断信号接入 HWInt[2]。

1
2
wire [5:0] HWInt;
assign HWInt = {3'b0, interrupt, IRQ_1, IRQ_0};

系统桥与信息存储

我们在此构建一个新模块Bridge

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
module BRIDGE(
    input wire [31:0] addr,
    input wire [3 :0] byteen,
    input wire [31:0] Rd_T0,
    input wire [31:0] Rd_T1,
    input wire [31:0] Rd_DM,
    output wire [31:0] Rd,
    output wire [3:0] m_data_byteen,
    output wire [3:0] m_int_byteen,
    output wire hit_T0,
    output wire hit_T1
    );

wire hit_DM;
wire hit_Int;

assign hit_T0 = (byteen == 4'b1111) && (addr <= `T0_end) && (addr >= `T0_start);
assign hit_T1 = (byteen == 4'b1111) && (addr <= `T1_end) && (addr >= `T1_start);

assign hit_DM = (addr <= `DM_end) && (addr >= `DM_start);
assign hit_Int = (addr <= `Int_generator_end) && (addr >= `Int_generator_start);

assign m_data_byteen = (hit_DM) ? byteen : 0;
assign m_int_byteen = (hit_Int) ? byteen : 0;

assign Rd = (hit_T0) ? Rd_T0 :
            (hit_T1) ? Rd_T1 :
            (hit_DM) ? Rd_DM : 0;

endmodule

我们可以看出,它无非是

  • 根据addr和byteen得出新的字节使能信号
  • 对各方面的读取进行选择,得出真正的读入内容

这样,我们就可以顺利搭建顶层模块:

无非是两个Timer,还有一些信息处理与传输嘛,最后还是给CPU使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
wire hit_T0;
wire hit_T1;

wire [31:0] Rd;

BRIDGE bridge(
    .addr(addr),
    .byteen(byteen),
    .Rd_T0(Rd_T0),
    .Rd_T1(Rd_T1),
    .Rd_DM(m_data_rdata),
    .Rd(Rd),
    .m_data_byteen(m_data_byteen),
    .m_int_byteen(m_int_byteen),
    .hit_T0(hit_T0),
    .hit_T1(hit_T1)
);

wire IRQ_0;
wire [31:0] Rd_T0;
TC Timer_0(
    .clk(clk),
    .reset(reset),
    .Addr(addr[31:2]),
    .WE(hit_T0),
    .Din(m_data_wdata),
    .Dout(Rd_T0),
    .IRQ(IRQ_0)
);

wire IRQ_1;
wire [31:0] Rd_T1;
TC Timer_1(
    .clk(clk),
    .reset(reset),
    .Addr(addr[31:2]),
    .WE(hit_T1),
    .Din(m_data_wdata),
    .Dout(Rd_T1),
    .IRQ(IRQ_1)
);
endmodule

注意一下TC传的地址是[31:2]

连完顶层模块,P7好像就,结束了?

通过ISE把语法错误搞完,然后…

陷入无尽的debug之中了。

测试文档

P6既然过了,P7的无异常情况基本是不需要考虑的。

基本处理模式

测试首先从简单的异常开始,比如简单的Ov

认真观察波形图,分析一下进入异常处理程序后各寄存器的情况,以及是怎么跳转回去的,跳转回去各寄存器的情况又如何。

此时的异常处理程序还比较简单,比如可以只有4条语句:

取EPC,EPC+4,存EPC,eret

(这个还能顺便测测阻塞)

1
2
3
4
5
6
.ktext 0x4180
    mfc0	$k0, $14
    addu	$k0, $k0, 4
    mtc0	$k0, $14
    eret
    99999999 (一条无效指令,测一测延迟槽清空)

防止漏判

构造出所有引发异常的组合。

你只需要对着你那个表格,一种种地敲,一种种检验。

关键是看好ExcCode,这个最简单,但也往往是大量错误出现的地方。

防止误判

这个是比较难发现的,在使用COKiller之前,我碰运气确实找到了一些样例误判,但是用了COKiller我才发现div触发Ov没有被我发现。

我的建议就是多用评测机,多测点数据,认真看代码。

其他细节谬误

这些我觉得可以对着各种学长博客所强调的点先检查是否实现,再写测试程序来看写的是否正确。

例如空泡的信息继承,乘除槽的执行与否等等。

思考题

请查阅相关资料,说明鼠标和键盘的输入信号是如何被 CPU 知晓的?

鼠标和键盘等都属于外设,外设与CPU的时钟频率可谓是天差地别。中间必须要有一个接口,实现两者之间的信息交流。

这个接口往往有硬件的控制信号,更有软件的处理程序,能够使这些信号变为CPU接受的形式。

请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)

因为我们的异常处理程序是统一放在这里的,你要执行异常处理程序就是要到达这个pc。

不过用户自己实现处理异常中断应该也不会有大的问题,毕竟我们自己搭CPU测试的时候不就是自己写处理中断的程序嘛,写得很烂就是了。。

但是0x4180本身位置的控制一定是经过慎重考虑的,有其空间分配的优越性。

为何与外设通信需要 Bridge?

高内聚,低耦合。

外设可以添加,但是CPU不应为之改变。
这时候,系统桥就能起到分析处理双方信息的作用。

当我们添加新外设,只需改动Bridge来控制要读谁,要写谁的问题。

请阅读官方提供的定时器源代码,阐述两种中断模式的异同,并针对每一种模式绘制状态移图。

计时模式

整个流程两种模式十分类似,差别在于是否具有周期性。

对于IDLE,ctrl[0]为0转为状态LOAD,并把IRQ置0,接下来,无条件进入计时阶段,如果中途ctrl[0]为1就终止,进入IDLE,否则就计数完成时进入INT,发送中断。

到了INT阶段,
if(`ctrl[2:1] == 2’b00),那么就一直中断,进入IDLE继续循环。
否则,IRQ置0,即只中断这一个周期,然后进入IDLE重新循环。

不过共同之处在于,ctrl[0]对中断的产生都有明显的控制作用。

倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?

会发生EPC为0的问题。这个问题在设计文档已经提到过,还是那句话:

空泡应当是上一条指令生命的延续。

我们处理的核心问题是EPC,所以说空泡要着重继承与之有关的PC值与Bd值。

空泡要存的信息究竟是谁?

被阻塞指令的pc和跳转指令的PCSel

为什么 jalr 指令为什么不能写成 jalr $31,$31

这也是我的设计文档中所提到的一个问题。

Bd为1而是用重复执行跳转指令的方法确实有潜在问题,正如jalr $31, $31的情况:

这是一个有累加效应的写指令。两次执行jalr的写效果是不同的。

解决方案文中也有讨论到:
异常中断在延迟槽发生,那么就屏蔽req时的写GRF,让此时W级的写指令无效。

下一次回到这句话的时候才是真正执行。

倘若延迟槽又出现问题,那就再让此时W级写使能无效即可。

comments powered by Disqus
Easy Life and Easy Learning
使用 Hugo 构建
主题 StackJimmy 设计