北航计组P7

前言

  • 咳咳,更的越来越少了(),最近在致力于把测评机搭得花里胡哨,所以一直没有写博客,现在刚好有点闲所以写一下P7的博客,再不写课上的指令什么的都忘光了

课下搭建注意

  • 欸为什么不写课下的搭建博客。第一,我觉得之前的学长已经写得很好了(反正看懂博客P7就成功一半了,看懂源码P7就成了hhh);第二,我好懒;第三,之前似乎没看到有人写测试的博客,所以我来填一点点坑,希望对以后的同学有用(话说2024以后计组好像要改革了,希望不要删掉P7
  • 搭建博客
  • 一些补充的搭建要点
    • 说实话我没看明白为什么要加入eret,似乎返回的EPC并不会触发这个异常()
      assign pc_error = (|PC[1: 0]) | (PC < 32'h0000_3000) | (PC > 32'h0000_6fff) | !D_eret;
    • Req和stall的优先级反了,这样会出bug下面同样给出测出bug的代码,或者直接用我的测评机吧()
      E_PC <= stall ? D_PC : (Req ? 32'h0000_4180 : 32'b0);
      ori $1, $0, 0x4180
      lw $31, 0($1) <- exception
      beq $31, $0, jlabel <- interrupt
      nop
      jlabel:
      nop
      • 如果按照学长先判断阻塞再判断异常的话,处理lw的异常时会意外触发外部中断,导致CP0模块的cause寄存器记录interrupt的被意外置位
      • 话说课程组也说过reset位应该是最优先的,但是实际我们实现时都是将reset放在优先级最低的位置,不过还是过了,大概是因为使用reset没办法和mars对拍吗(烧烤ing)
    • 尽量不要用课程组的Mars的计时器吧,毕竟Mars终究还是一个单周期CPU,和我们的时钟不一样,而且就算是和好兄弟们对拍也需要保证你们阻塞的思路是一致的,烦死了(悟空脸)

课下测试

CP0

  • 毕竟是P7新增的模块,自然和异常中断是强相关的。其中和异常处理程序只会使用CauseEPC两个寄存器
    • Cause(ExcCode)
      • 用于判断异常中断类型,一般在异常处理程序开头就读出,再通过掩码处理跳转不同异常的处理部分
    • Cause(BD)
      • 这个位主要是为了中断设计的,假如在一条会发生跳转的跳转指令的延迟槽触发了中断异常,这个时候我们最后应该返回跳转指令而非其延迟槽
      • 中断并不改变原有程序执行路径,只是暂时停止去做一些其他的事情,我们跳回延迟槽就默认这条跳转指令不会发生跳转,显然矛盾了,所以只能设置一个延迟槽位跳回跳转指令让跳转指令重新执行一遍
      • 因为BD不是给异常用的,所以我们处理异常的时候反而需要特判BD位,防止跳错位置
    • Cause(IP)
      • 这6个位记录了中断产生的原因,通过它们我们可以处理不同的中断,不论是计时器的中断还是外界给的中断

handler

  • P7测试与以往最不同的地方大概就是多了一段异常处理程序。为了保证我们的程序可以正常运行下去,有一个适合自己生成器的异常处理程序是很重要的呀,我编写异常处理程序时主要关注以下几个点
    • 对于PC的异常(单独从AdEL错误中分出来)
    • 对于其余AdEL、AdES、RI、Ov处理
    • 对于Interrupt处理
    • 记得存储上下文和恢复上下文
    • 生成器遵从一般汇编程序编写思路,不使用$a0, $a1, $k0, $k1,因为异常处理程序刚开始需要使用这些寄存器
  • 对于PC错误异常
    • 超出正常PC错误,从内存0x2180位置读出新的EPC(这个不是什么特殊位置,或者你可以专门用一个寄存器作为存储应该返回的地址,主要就是为了一个可控,保证测试代码可以正常执行下去)
    • PC未对齐,强行对齐,并将PC指向下一位置(0x3003 -> 0x3000 -> 0x3004)
  • 对于其他异常
    • 直接跳过这条异常指令(记得判断延迟槽,如果是延迟槽需要跳两位,因为此时EPC指向了异常指令前面的跳转指令)
  • 对于Interrupt
    • 对于tb文件中给出的中断,我们直接响应0x7f20即可,tb文件接受到响应会取消中断
    • 对于计时器触发的异常,我们需要写计时器的CTRL寄存器,赋0给使能位就是CTRL[0]或者干脆直接全给0就好了
  • 其他注意点
    • 调用函数慎用jal,改用beqj,因为有的时候我们在主程序中会使用31号寄存器做些什么,异常程序按理不应该改变除了$a0, $a1, $k0, $k1的寄存器,或者请在保存上下文之后再使用jal
handler源码
# 主要处理PC异常
_quick_handle:
mfc0 $k0, $13
andi $k0, $k0, 0x00fc

# 没有srl指令,这一步骤判断异常是否为0x0004
ori $k1, $0, 0x0010
beq $k0, $k1, adel_handler_quick
nop

beq $0, $0, _entry
nop

# 入口程序
_entry:
mfc0 $1, $13
ori $k0, $0, 0x1000
sw $sp, -4($k0)

addi $k0, $k0, -256
add $sp, $0, $k0

beq $0, $0, _save_context
nop

# PC错误
adel_handler_quick:
mfc0 $k0, $14
addi $k0, $k0, -0x3000
lui $k1, 0xffff
ori $k1,$k1,0xe000
and $k0,$k0,$k1
bne $k0,$0,adel_type_2
nop
mfc0 $k0, $14
andi $k0,$k0,3
bne $k0,$0,adel_type_1
nop
jal _entry
nop

# PC未对齐
adel_type_1:
mfc0 $k0, $14
andi $k0, $k0, 0xfffc
addi $k0, $k0, 4
mtc0 $k0, $14
eret
ori $1, $0, 0x1234

# PC超出范围
adel_type_2:
ori $k0, $0, 0x2180
lw $k0, 0($k0)
mtc0 $k0,$14
nop
eret
ori $1, $0, 0x1234

# 判断异常中断类型
_main_handler:
mfc0 $k0, $13
andi $k0, $k0, 0x00fc

ori $k1, $0, 0x0000
beq $k0, $k1, int_handler
nop
ori $k1, $0, 0x0010
beq $k0, $k1, adel_handler
nop
ori $k1, $0, 0x0014
beq $k0, $k1, ades_handler
nop
ori $k1, $0, 0x0028
beq $k0, $k1, ri_handler
nop
ori $k1, $0, 0x0030
beq $k0, $k1, ov_handler
nop
ori $k1, $0, 0x0020
beq $k0, $k1, syscall_handler
nop

# 判断中断类型
int_handler:
sw $ra, 0($sp)
addi $sp, $sp, -16
mfc0 $v0, $12
sw $v0, 0($sp)
mfc0 $v0, $13
sw $v0, 4($sp)

# check INT[3]
lw $v0, 0($sp)
lw $v1, 4($sp)
and $v0, $v1, $v0
andi $v0, $v0, 0x800
bne $v0, $0, timer1_handler
nop

# check INT[2]
lw $v0, 0($sp)
lw $v1, 4($sp)
and $v0, $v1, $v0
andi $v0, $v0, 0x400
bne $v0, $0, timer0_handler
nop
jal interrupt_handler
nop

# 外部中断
interrupt_handler:
lui $k0, 0xffff
ori $k0, $k0, 0xffff
addi $k1, $0, 0x2180
lw $k1, 0($k1)
addi $k0, $0, 0x7f20
sb $0, 0($k0)
jal _restore_context
nop

# Timer0中断
timer0_handler:
lui $k0, 0xffff
addi $k1, $0, 0x2180
lw $k1, 0($k1)
addi $k0, $0, 0x7f00
sw $0, 0($k0)
jal _restore_context
nop

# Timer1中断
timer1_handler:
lui $k0, 0xffff
ori $k0, $k0, 0x1
addi $k1, $0, 0x2180
lw $k1, 0($k1)
addi $k0, $0, 0x7f10
sw $0, 0($k0)
jal _restore_context
nop

# 其他AdEL异常直接跳过
adel_handler:
mfc0 $t0, $14
mfc0 $k0, $13
lui $t2, 0x8000
and $t3, $k0, $t2
addi $t0, $t0, 4
bne $t3, $t2, adel_nxt
nop
addi $t0, $t0, 4
adel_nxt:
mtc0 $t0, $14
jal _restore_context
nop

# AdES异常直接跳过
ades_handler:
mfc0 $t0, $14
mfc0 $k0, $13
lui $t2, 0x8000
and $t3, $k0, $t2
addi $t0, $t0, 4
bne $t3, $t2, ades_nxt
nop
addi $t0, $t0, 4
ades_nxt:
mtc0 $t0, $14
jal _restore_context
nop

# 未知指令直接跳过
ri_handler:
mfc0 $t0, $14
mfc0 $k0, $13
lui $t2, 0x8000
and $t3, $k0, $t2
addi $t0, $t0, 4
bne $t3, $t2, ri_nxt
nop
addi $t0, $t0, 4
ri_nxt:
mtc0 $t0, $14
jal _restore_context
nop

# 算术溢出直接跳过
ov_handler:
mfc0 $t0, $14
mfc0 $k0, $13
lui $t2, 0x8000
and $t3, $k0, $t2
addi $t0, $t0, 4
bne $t3, $t2, ov_nxt
nop
addi $t0, $t0, 4
ov_nxt:
mtc0 $t0, $14
jal _restore_context
nop

# 处理一下syscall直接跳过
syscall_handler:
mfc0 $t0, $14
mfc0 $k0, $13
lui $t2, 0x8000
and $t3, $k0, $t2
addi $t0, $t0, 4
bne $t3, $t2, syscall_nxt
nop
addi $t0, $t0, 4
syscall_nxt:
mtc0 $t0, $14
jal _restore_context
nop

# 返回
_restore:
eret
ori $1, $0, 0x1234

# 保存上下文
_save_context:
sw $2, 8($sp)
sw $3, 12($sp)
sw $4, 16($sp)
sw $5, 20($sp)
sw $6, 24($sp)
sw $7, 28($sp)
sw $8, 32($sp)
sw $9, 36($sp)
sw $10, 40($sp)
sw $11, 44($sp)
sw $12, 48($sp)
sw $13, 52($sp)
sw $14, 56($sp)
sw $15, 60($sp)
sw $16, 64($sp)
sw $17, 68($sp)
sw $18, 72($sp)
sw $19, 76($sp)
sw $20, 80($sp)
sw $21, 84($sp)
sw $22, 88($sp)
sw $23, 92($sp)
sw $24, 96($sp)
sw $25, 100($sp)
sw $28, 112($sp)
sw $29, 116($sp)
sw $30, 120($sp)
sw $31, 124($sp)
mfhi $k0
sw $k0, 128($sp)
mflo $k0
sw $k0, 132($sp)
jal _main_handler
nop

# 恢复上下文
_restore_context:
addi $sp, $0, 0x1000
addi $sp, $sp, -256
lw $2, 8($sp)
lw $3, 12($sp)
lw $4, 16($sp)
lw $5, 20($sp)
lw $6, 24($sp)
lw $7, 28($sp)
lw $8, 32($sp)
lw $9, 36($sp)
lw $10, 40($sp)
lw $11, 44($sp)
lw $12, 48($sp)
lw $13, 52($sp)
lw $14, 56($sp)
lw $15, 60($sp)
lw $16, 64($sp)
lw $17, 68($sp)
lw $18, 72($sp)
lw $19, 76($sp)
lw $20, 80($sp)
lw $21, 84($sp)
lw $22, 88($sp)
lw $23, 92($sp)
lw $24, 96($sp)
lw $25, 100($sp)
lw $28, 112($sp)
lw $30, 120($sp)
lw $31, 124($sp)
lw $k0, 128($sp)
mthi $k0
lw $k0, 132($sp)
mtlo $k0
lw $29, 116($sp)
ori $1,$0,1
beq $0, $0, _restore
nop

异常测试

  • 忘了叠甲了,我说的异常是指内部指令出现了问题导致的异常,我们主要解决下面几个问题
    • 是否能正确响应各种异常
      • 把各种异常试一下就好了,我是按照我在handler的分类尝试的
      • 注意不要一直异常,最好穿插一些正确的指令,否则万一这个CPU啥都判为异常呢()
    • 是否能正确处理延迟槽(BD)
      • 比较easy,放在jal指令后面就好了
    • 是否能正确处理乘除槽
      • 这个我采取的是在异常指令前后随机插入multu或者mtlo什么的,异常处理程序中有专门读HI LO寄存器的,我们就不需要额外验证了
    • 是否能正确处理异常与阻塞
      • 这个主要和存取类指令有关,主要就存取类指令比较方便设计阻塞,例如后面放一条不会跳的beq指令,一劳永逸;前面就放一条load指令好了,尽可能地多阻塞一会,方便查出流水寄存器可能的错误

中断测试

  • 嘿嘿,我的中断测试单独分开了,但是中断测试实际上也会涉及异常的处理
  • 由于对于一个指令位置的中断有两种方案
    • 将需要中断指令的PC加入tb文件中。优点:优雅、方便;缺点:对测评机不友好(),无法测试中断和阻塞之间的耦合关系
    • 使用计时器中断,几条指令过后自动触发中断。优点:测试更加灵活;缺点:不方便对拍(虽然我已经尽力保证中间不出现阻塞了,难保有人实现从P5就与我不一致)
    • 注意力各位,就算使用计时器中断也不要和Mars对拍,中断要和自己的好兄弟们对拍哦,或者上github找一个学长的改一改(感谢FlyingLandlord学长orz)
  • 接下来测试数据主要解决下面几个问题
    • 是否能正确响应各种中断
      • 这个和上面异常一样,各种中断都试一下
    • 是否能正确处理跳转(尤其是延迟槽)时中断
      • 分别测试跳转指令时、跳转指令延迟槽内、刚从eret中返回时中断
    • 是否能正确处理阻塞和中断
      • 这里我利用了一下乘除槽,由于div mflo指令相隔很多周期仍然会阻塞,更加方便我们在中间发挥,尤其注意这个阻塞主要测试那条产生的nop,所以请务必保证传递正确与否会导致结果不同
      • 例如在被阻塞的指令之前放一条会跳转的指令,这样BD位没实现好就会发生错误
    • 是否能正常处理异常和中断
      • 这个其实可以可以融合在其他环节中,当然随便拉一条异常指令给它设置断点即可
    • 是否能正常处理延迟槽
      • 为了防止有人异常和中断分开了,按理来说异常时候没问题这里也没问题,不过为了保险
    • 是否能正常处理乘除槽
      • 和上面一条同理,不过中断的延迟槽更方便了,生成一条乘除指令随机在这条指令前、这条指令、这条指令后生成中断点

关于数据生成器

  • 提示,没有加入P7新增指令的转发,不过异常处理程序已经很多变了吧(),各位还是自己测试一下转发什么的吧
  • 因为很懒,所以我从P3~P7的数据生成器是一整个大型文件,因此很多函数需要传递各种参数限制生成的数据(),提供给大家,不过大概率很难调用吧,里面有几个外置的函数可供P7使用!
    dataMaker = DataMaker()
    dataMaker.exc_test(500, True, "./test")
    # exc_test(times, forbidden=True, _dir=".")
    # times生成指令最大长度,不建议超过1000条因为这里有中断程序
    # forbidden是否禁止RI测试,方便和Mars对拍
    # _dir设置好文件夹,生成的数据会在这个文件夹下

    dataMaker.int_test(500, _dir="./test", tb_dir="./tbs")
    # int_test(times, noTimer=False, noInterrupt=False, _dir=".", tb_dir=".")
    # times同上
    # noTimer不使用计时器产生中断
    # noInterrupt不使用tb产生中断
    # _dir同上
    # tb_dir如果需要tb的中断,生成的tb文件会在这个文件夹下
    DataMaker源码

课上测试

  • 这个课上是真的测试哦
  • 首先大家手要快,拿到了之后立马提交前四个,否则会等得很煎熬,不过我19:00提交的,19:01前四道题就过了,过题最快的一集,看它转的时候有那么一点点小紧张,不过我相信我的测评机(如果没过我大概率也de不出来,已经想好用拉肚子作为逃跑的理由了)
  • 最后一道题是一道抽象指令题,归为啥类都不合适哈
  • withdraw
    • withdraw
    • 将最近一条sw的行为撤销
    • 首先在CP0中添加四个额外的寄存器DomainLoDomainHiChangeAddrChangeData
      • ChangeAddr: 最近一次sw修改值的地址
      • ChangeData: 最近一次sw修改之前的值,用于恢复这个地址的值
      • DomainLo: 允许撤销sw指令地址下界,低于这个值报AdES异常
      • DomainHi: 允许撤销sw指令地址上界,超过这个值报AdES异常
    • 注意实现这些值,寄存器编号忘了;mfc0可以获取它们的值,mtc0只可以更改地址上下界两个寄存器,而且保证是字对齐
    • 如果withdraw指令前面没有出现过sw,则忽略这条指令
    • 如果withdraw发挥了作用,其效果相当于一条sw指令
  • 一道相当繁琐的指令题
    • 首先将四个寄存器加入CP0
    • CP0中设置一个符号位flag,标识是否已经有sw指令了
    • 在CP0中多加一层ExcCode的判断
      //withdraw时判断是否有ExcCodePlus
      assign newExcCode = (ExcCode != 5'b0) ? ExcCode :
      (ExcCodePlus != 5'b0) ? ExcCodePlus : 5'b0;
    • 建议传入ByteEn,通过是否位4'b1111来判断是否为sw
    • 遇到withdraw指令判断逻辑较为复杂
      //withdraw是外界传入的信号,标识withdraw指令 
      if (withdraw & flag) begin
      if(Addr < DomainLo || Addr >= DomainHi) begin
      ExcCodePlus = `EXC_AdES;
      end

      if(!Req) begin
      outByteEn = 4'b1111;
      outAddr = ChangeAddr;
      outData = ChangeData;
      end
      end
    • 然后在M级把这些输出值接到DM即可
      assign m_data_byteen = (withdraw) ? outByteEn :....
      assign m_data_addr ...
      assign m_data_wdata ...
    • 最后这条指令不需要转发和暂停,课程组还算优点人性,要我当助教我就这样(doge)
      • 变异load指令withdraw $rt, offset($base)
        addr <- GPR[base] + offset
        vaddr <- addr[31: 2]
        temp1 <- Mem[vaddr]
        temp2 <- Mme[CP0.ChangeAddr]
        temp3 <- temp2[4:0]
        if temp1 > CP0.ChangeData:
        withdraw <- 这个withdraw是原题的withdraw
        elif temp1 < CP0.ChangeData:
        GPR[rt] <- temp1
        else:
        GPR[temp3 ^ rt] <- temp2
        • 写完我自己都笑了,希望明年不会有这么阴间的()

彩蛋

  1. 我的测评机有一个彩蛋噢
  2. 嘻嘻
    alt text
    alt text