CO_P7

CO_P7

Charles Lv7

P7总结

(一).CP0响应机制

CP0是检测异常和中断的重要部件,异常和中断通过以下接口传入——

  • 中断信息通过 HWInt[7:2] 接口进入,其中HWInt[2]连接Timer0的终端请求,HWInt[3]连接Timer1的终端请求,HWInt[4]连接外部的中断请求。
  • 异常信息通过ExcCode_inBD_in两个接口传入,ExcCode_in传入的是异常代码(ADEL,ADES,RI,OV),BD_in传入的是延迟槽指令标志(有效则表示当前指令为延迟槽指令)。

检测到中断或者异常时,CP0会进行判断并响应,决定是否将req(CP0向CPU发出的中断请求)置1。判断逻辑如下——

1
2
3
wire inter_req  = (|(HWInt & IM)) & IE & (!EXL);//中断有效判断
wire exc_req = (ExcCode_in != 5'd0) & (!EXL);//异常有效判断
assign req = inter_req | exc_req;//

当req有效,CP0还需要完成以下任务——

  • EXL置1

  • 将M级PC存入EPC(延迟槽指令中存入EPC-4)

  • 如果当前响应中断,ExcCode寄存器写入0;若响应异常,ExcCode寄存器写入外部传入的异常代码ExcCode_in

  • BD寄存器写入外部传入的BD_in信号

  • 此外,无论是否发出中断请求,在每一周期均需要将HWInt[6:2]写入Cause寄存器中的的IP域

    系统桥设计

    系统桥其实是充当一个“交换机”的角色,将CPU传来的地址写入相应的外设(DM、Timer0、Timer1),只需要组合逻辑便可实现。

    异常识别

    P7中我们考虑的异常情况有以下几种——

    • F级异常:
      • PC地址没有字对齐(AdEL)
      • PC地址超过0x3000 ~ 0x6ffc(AdEL)
    • D级异常:
      • 未知的指令码(RI)
    • E级异常:
      • addi、add、sub计算溢出(Ov)
      • load类指令计算地址时加法溢出(AdEL)
      • store类指令计算地址时加法溢出(AdES)
    • M级异常:
      • lw取数地址未 4 字节对齐(AdEL)
      • lh、lhu取数地址未与 2 字节对齐(AdEL)
      • lh、lhu、lb、lbu取 Timer 寄存器的值(AdEL)
      • load型指令取数地址超出 DM、Timer0、Timer1 的范围(AdEL)
      • sw存数地址未 4 字节对齐(AdES)
      • sh存数地址未 2 字节对齐(AdES)
      • sh、sb存 Timer 寄存器的值(AdES)
      • sw向计时器的 Count 寄存器存值(AdES)
      • store型指令存数地址超出 DM、Timer0、Timer1 的范围(AdES)

    针对以上列出的异常情况,我们在每一个流水级对异常进行检测。由于教程提出了以下要求——

    • 发生取指异常后视为 nop 直至提交到 CP0。
    • 发生 RI 异常后视为 nop 直至提交到 CP0。
    • loadstore 类算址溢出按照 AdELAdES 处理。

    因此不会出现一个指令在多级出现异常的情况。如果某个流水级出现了新的异常,我们将这个异常流水到下一级即可,而不是流水上一级传来的异常;如果这个流水级没有出现新的异常,则将上一级传来的异常继续流水给下一级即可。

(二).一些细节点

一.异常清空需要屏蔽写能信号

如果某一时刻,中断(或指令异常)到来,我们必须清空宏观 PC(或受害指令)及其之后的指令。不仅如此,在这一时刻,在宏观 PC(或受害指令)及其之后的指令所在的流水级,不应该对部件的状态进行改变。

考虑中断,假设宏观 PC 表示 M 级的 PC,CP0 在 M 级,那么 MDU (在 E 级), DM (在 M 级), CP0 (在 M 级)的写能信号应该屏蔽(GRF 不需要因为它在 W 级)。如果不是中断而是指令异常,假设指令异常统一在 M 级处理,也是这三个部件的写能信号应该屏蔽。

在这种情况下,在 M 级的 DM 和 CP0 的状态改变被完全撤回,但是在 E 级的 MDU 的状态改变有可能不能被撤回(我们的设计被允许在这种情况下不撤回,如果非要撤回,需要额外的实现)。所以如果使用宏观 PC ,那么宏观 PC 最好不要在 W 级,因为这样就必须实现 DM 和 CP0 的撤回,成本很高。如果指令异常在某一级流水线统一处理,那么这个流水级也最好不要是 W 级,一样的原因。

二.多异常的处理

如果有多个指令将触发多个异常,我们需要确保指令序列前面的指令先触发异常,后面的指令再触发异常。

注意:有多个指令将触发多个异常并不等价于有多个指令将同时触发多个异常(保证在后者的条件下前面的指令先触发异常,是不足够的),因此一旦探测到异常就处理是不对的,还需要考虑前面的指令有没有潜在的异常。只有明确了该指令前面的所有指令以后都不会发生异常,才能进入中断处理程序。为了简便地实现这一目的,我们可以使用流水线,将异常码 (Exception code) 流水,到某一个流水级统一处理。由于最晚触发的指令异常在 E 级(根据不同实现也可能是 M 级),所以在 E 级、 M 级处理指令异常都是可以的(前面所说的 W 级撤回成本高)。

Q: 我们讨论了宏观 PC 的位置(F/D/E/M)和指令异常处理级的位置(E/M),那么 CP0 的位置呢?

A: 首先排除 F 级,那样就无法实现 mtc0 。不建议在 D 级和 W 级,那么会使得流水级延迟高,限制时钟周期。所以 E 级和 M 级是最佳选择。此外,假如你的宏观 PC 和指令异常处理的流水级二者有任意一个选择的是 M 级,你的 CP0 就不能放在 E 级(无法撤回)。

如果有一个指令将触发多个异常,我们需要确保其触发流水级低的异常。

例如:pc 所对应的地址并不在分配给 IM 的地址空间之中,这时候我们将触发 AdEL 取指异常,但这时取出来的指令恰好是存储指令并且恰好将触发 AdES 存数异常,这时发送给 CP0 的应该是 AdEL 而非 AdES

课下似乎不会涉及这个问题,但这周的课上就涉及到了。

中断优先级高于异常优先级。

三.中断时宏观 PC 为冒泡

中断有可能在宏观 PC 为冒泡 (Bubble) 的时刻到来。为了实现精准异常,我们可以保持流水线中指导 EPC 的信息(指 pcReadDatabranchDelay )不被清空 (Clear) ,从而使得冒泡的 pcReadDatabranchDelay 与后一个指令相同(指的是后一个非冒泡指令,下同)。通过这种方法,如果后一个指令是延迟槽(即前一个指令是跳转指令),那么 EPC 就是跳转指令(前一个指令);如果后一个指令是非延迟槽,那么 EPC 就是后一个指令,从而实现精准异常。

Q: 冒泡的 pcReadDatabranchDelay 与前一个指令相同可以吗?

A: 不可以。如果前一个指令是跳转指令,这没什么问题。但是如果前一个指令不是跳转指令,这样做会使得前一条指令被执行两次(在 MIPS 指令集中,跳转指令执行一次与多次的效果是一样的,但非跳转指令则不能保证)。

注意:如果前一个指令是跳转指令(无论冒泡的 pcReadDatabranchDelay 与后一个指令相同还是与前一个指令相同),跳转指令总是会执行两次,所以 jalrrsrd 不能相同(否则总是不跳转),因为在这种情况下,jaljalr 总是会写入两次 $rard

Q: 相应地,指令异常也有可能在受害指令为冒泡的时刻到来吗?

A: 我们无需担心这一点,因为在我们的设计中,通常能够保证冒泡不会产生异常(即受害指令不会是冒泡)。

四.EPC 的数据冒险 (Data hazard)

EPC 可能在 CP0 所在流水级之前使用( eret ),因此需要解决数据冒险(利用阻塞,甚至可以转发)。

为了简便,只阻塞就可以了。

某一时刻,你的流水线中可能既有用户指令(指 .text 段内的指令),又有内核指令(指 .ktext 段内的指令)

这种情况下应该禁止中断,也就是说 EXL 应该置 1(我一直置的是 0,导致需要一些额外的实现)。

(三).设计思考——两个思想

P7做的比较艰难,主要是刚开始对教程不太理解,后来在对于微系统的实现以及debug过程中与同学、助教进行了许多交流,才觉得对于P7微系统的稍微理解。回顾来看,我认为P7困扰我的其实是两个思想——软硬件协同的视角以及模块封装的思想,遂作此文,以分享笔者粗浅的思考。

一.软硬件协同的视角

此处引用一位助教的原话“我们的硬件只需要忠诚地翻译指令”,所以我们只是实现硬件而不用去看软件要干什么,我们只要知道软件提出了什么需求,也就是我们的硬件要去实现怎样的接口(这里助教给我讲了一个形象的例子,后端-API-前端,后端不用去思考前端去干什么,实现好API接口即可)。 这也就解答了我在最初设计时两个很大的困惑:

  • 如何处理中断?是在中断时把m_data_addr变成0x7f20吗?
    答案是不用的, 是否访问0x7f20是软件程序去决定的事情,而我们的硬件只用支持软件去访问0x7f20即可。这也就实现了教程所言“对中断发生器的响应是通过系统桥来实现的,通过 store 类指令访问地址 0x7F20,就可以达到响应中断的目的”。
  • 异常处理是怎样处理的?
    这个问题真的困扰了我很久,但其实这是我们不用考虑的,异常处理是异常处理程序的事情,而我们的硬件只需要实现能够支持异常处理程序执行,也就是可以忠诚地翻译相应指令并且实现异常处理程序所需要的接口(也可以理解为保存好现场)即可。

二.模块封装

我感觉刚做P7会有一种手忙脚乱的感觉,现在回看,模块封装的思想会帮助我们理清到底要干什么。其实我们要做的事情就是:

  • 在原来的流水线CPU中加入CP0模块,将其封装成新的CPU模块(这其中当然包括各种异常信号的识别与流水)
  • 实现Bridge模块
  • 实例化两个Timer
  • 将CPU、Bridge 、Timer0、Timer1封装成我们的MIPS微系统
    然后按照一个模块一个模块来实现,再把线都接上就好

(四).Bug分享

  • Req越沿采样错误,应该是Req置高的下一个周期F级pc才变为0x4180(笔者的Req是wire型,组合逻辑运算输出)
    此处再次引用助教的讲解,信号在一个时钟周期中应该是左开右闭的。左开是为了在时钟上升沿保持稳定,右闭是为了保证下一时钟上升沿在稳定状态采样,所以要遵循这个理念,不能够越沿采样。
  • 中断的优先级高于异常,即如果CP0中中断信号有效那么ExcCode就是5'd0,否则才是输入的ExcCode
  • sw指令可以访存Timer的前两个寄存器,而不能访问count寄存器,这里可以根据Timer设计文档确定count的Addr
  • W级流水寄存器在Req信号置高时也是要清空的,所有流水寄存器都要在Req信号置高时清空
  • 各种地址范围敲错
  • Title: CO_P7
  • Author: Charles
  • Created at : 2022-12-26 20:54:03
  • Updated at : 2023-11-05 21:36:02
  • Link: https://charles2530.github.io/2022/12/26/co-p7/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments