你好!本文章为该博客的第⑨篇文章!

本文章包含北航2024计组实验课P7课下部分的个人理解,第一次课上题目的简要介绍和一种实现思路。

由于P7教程内容实现方法过于宽泛,所以这里我仅对自己的理解做一个分享,并不意味着这一环节的某一部分写法只有文章内一种,如果你觉得其他方法更好可以直接采纳。

本文章参考了 BUAA-CO-Lab P7 MIPS 微体系 | ROIFE BLOGP7 课下 & 课上总结 - 北航计算机组成原理 | Test Blog = FlyingLandlord’s Blog,在此非常感谢两位前辈做出的贡献!(没有这两篇文章的话,我可能还要在P7耗上超过一倍的时间)

本文章将在北航2024计组实验课结束之前持续更新,如果你在阅读本文的过程中遇到了不理解的地方/发现了有笔误的地方(哪怕只是非常小的笔误)/觉得某部分还有更好的实现,请务必在评论区里留下这些想法!(或者,直接联系我本人也可以!)

同样的,请务必仔细读完整篇文章,以及上述推荐的博客后再动手,以保证自己所理解的内容没有巨大的偏差。

需要注意的是,本文仅完全适用于北航2024计组课程,对于之前和之后的课程,教程部分内容可能会发生变动,发生冲突时请以实验教程为准!

以及,如果本文章涉及学术诚信问题,请第一时间和我联系,我将及时做出修改,谢谢支持!

这篇文章还有什么空白?

嗯……非常遗憾,直到动笔之前,我还没有构造出一个行之有效的调试P7的思路或者工具,因此我非常希望能有同学搞定这一部分分享一下,不过既然文章已经开始动笔了,那我就先把自己知道的写完再说吧。

我们要干啥?

(事先声明,本文提到的中断全是外部中断,异常全是内部异常,所以当我想要把这两个概念合起来说的时候,一定会表述成“异常中断”,如果我在这篇文章里违反了这个原则,请第一时间和我联系)

包括我在内,我认识的很多朋友看完P7的第一反应都是:它在说啥?

没错,P7教程写的确实会让人一下子不知道要干啥,你只能大概知道自己要新做的主要模块名:CP0,桥,然后附上两个Timer,最后通过某种方式把这些东西和你之前做的CPU连起来。

所以,在开始所有工作之前,我先对P7要做的事情做一个总结(此部分和FlyingLandlord的文章内容基本一致):

  1. 给原流水线CPU的各个模块进行修改,使之可以产生由该元件信息可以得到的异常,并将其通过流水寄存器一路流下去;

  2. 完成CP0,确定CP0的位置(我选择定到了M级),将以上异常信息流水至该级(而不是直接相连),并把CPU接收到的终端信号和CPU内产生的异常信息和CP0连接,使之可以完成两个事情:判断当前是否需要跳转至异常处理指令,保存处理异常中断需要的各个状态信号,在CPU响应完跳转,处理完异常中断后,跳回正常程序。

    是的,CP0其实就是个大号的处于M级的分支判断模块,实际上所有工作都是你原先设计的CPU在做,只是CP0这个东西可以帮你保存一些异常中断的状态信息,以及帮你判断当前时刻是否需要处理一个异常/中断而已。

  3. 将以上所有内容全部封装为一个 CPU.v(这里的大概意思就是,你在P6所做的所有内容,以及P7对CPU新增的内容,全部应该放入 CPU.v 里,而不是像P6之前那样直接放到 mips.v 里),然后在 mips.v 里把CPU和两个计时器装进去,最后设计一个 Bridge 模块把两个计时器,数据存储器和中断发生器(这玩意在课程里被抽象成了 mips.v 的几个输入输出端口,类似IM和DM)的数据读写端口连进 Bridge 做处理,再让CPU通过 Bridge 获得信息。

鉴于这里对 CPU.v 的描述仍然有点模糊,我这里做一些说明:

  1. 从内容上说,CPU.v 与P6时候的 mips.v 基本相同。CPU.v 里包含的是我们基于P6完成的 mips.v 内容的扩展,在P7中我们需要把扩展后的 mips.v 里的所有内容迁移过来。
  2. 从结构上说,P6的 mips.v 完成的也是一块CPU,但顶层端口限制使得这个CPU只能是提交要求里的端口,而P7的 CPU.v 不是顶层模块,不受提交要求限制,因此可以在P6基础上自由修改/扩展。具体来说,我选择保留P6时的所有端口,并加入一个 HWInt 端口,向CPU输入外部中断信号。
  3. 以及,这里有一个设计思想:CPU实际上并不需要知道有哪些外设,也不需要特别清楚这些外设具体是如何被写入/读取的,它只需要知道数据存储器的某一部分地址被划给了某些外设,因此CPU可以通过对这些地址进行读写操作来对外设进行访问,仅此而已。

Update(2024/11/27):这里需要强调的是,因为桥的存在,CPU不应该对每个外设都引入端口进行特殊处理,而是把所有对外设的操作集中到一组端口,所以CPU向外传出的端口信号实际上只需要对IM的读写,对DM(在CPU眼里看是DM,在外界看是各种外设的集合)的读写,以及其他testbench需要的端口。

总之,你要干的三件事情是: 产生异常,处理异常中断,用桥连接外设

如果你是按照我的思路做完的P7,那么以上我提到的模块关系应该如下:

  • mips.v
    • CPU.v
      • CP0.v
    • Bridge.v
    • Timer.v

同时,为了辅助理解以上目标,我们指出几条比较重要的数据通路,作为判断你是否理解正确的部分参考:

  • 异常:从CPU的各级元件产生,经由流水寄存器进入M级,在CP0进行判断,产生是否响应异常中断的控制信号,其信号回传至CPU各级决定是否进行处理异常中断的跳转操作。
  • 中断:从中断发生器和计时器产生,打包后直接进入CPU的CP0,在CP0进行判断,产生是否响应异常中断的控制信号,其信号回传至CPU各级决定是否进行处理异常中断的跳转操作(后面这部分是异常中断都要做的事情,具体咋做后面再说)
  • 包含外设的数据读取:从CPU产生读取地址,经过Bridge的分析,从DM和计时器中选定对应的外设进行读取,结果传回CPU中。
  • 包含外设的数据写入:从CPU产生写入地址和值,经过Bridge的分析,从DM,计时器和异常发生器中选定对应的外设进行写入。

经过以上分析,我们大致提炼出了P7的总目标。

具体怎么实现?

由于我们的总目标分了三点,那我们叙述这一部分的时候也分三段吧。

当然,你会发现前两部分有时候会有重叠,毕竟前两个目标干的是一件事——处理异常和中断,这里只是对这件事预先进行了一个拆分而已。

而中断的产生是由外设决定的,这玩意无需做任何说明,所以我们首先要额外说明一下如何产生异常。

产生异常

异常就是指令的参数或行为并不符合我们的期望,因此我们需要知道哪些行为不符合我们的期望。

首先,我们给出一个教程中的表格,以说明哪些情况会产生哪一类异常:

异常与中断码 助记符与名称 指令与指令类型 描述
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 算术溢出。

这里我们进行一些说明:

  • EXCcode (异常与中断码)为 Int(外部中断)时,在课程内其含义实际上是此时没有异常发生,仅考虑中断是否发生。也正因为如此,在没有异常产生时,我们可以考虑给CP0传入 Int,让其根据实际中断信号判断是否发生中断,而无需考虑异常。
  • 尽管以上表格列出了海量的情况,但最后产生异常信息的时候,我们只需要处理出其 EXCcode 即可,无需附加任何额外信息
  • 除以0不是任何异常。实际上,除以0是未定义行为,按照标准我们无需进行任何处理,评测数据也不应该有除以0的指令。

接下来,我们要想办法探测这些异常。

我们会发现一个事情:从F级到M级,每一级都恰好有一个模块可以分辨一些异常。比如,对于未知指令,我们在控制单元进行解码的时候就能发现问题。因此,我们可以给F级的PC,D级的控制单元,E级的ALU和M级的数据存储器(这个东西是外设,所以你可以写个模块专门处理异常相关信息)各加一个输出端口,用于输出该时刻用该模块是否探查出异常。

以及,某个元件在同一指令执行到同一级时,可能会因为多种原因产生异常(如,使用 lw 时,既有不对齐的问题,也有地址超出范围的问题),但是在实现正确的情况下,其产生的 EXCcode 必然唯一。

最后就是传递异常了。按道理来说,这其实只是一个流水的过程,没啥可讲的,但实际上我们会面对这样几个问题:

  • 如果一条指令会连续触发多个异常,我们应该如何处理?
  • 如果多个指令都会产生异常,我们应该如何处理?

此时我们规定一个原则:我们优先处理最早可以探查到的异常

比如说,如果一条指令在E级,M级都会产生异常,那么我们认为这个指令首先触发的是E级的异常,因此传入CP0的异常必须是E级产生的异常。

比如说,如果有两条指令在E级和M级都产生异常,那么我们认为M级的指令首先触发了M级的异常,因此传入CP0的异常必须是M级产生的异常。

至此,我们已经可以把所有异常检测出来,并且传递到M级了。

Update(2024/11/26):但是,我们仍然有一些工作要做,那就是在检测出异常/遇到中断时,我们不应该让M级与其之前的指令对其他模块产生任何影响

具体来说,有以下几种情况(我们事先说明,当发现遇到异常或中断时,CP0寄存器会产生一个req为1的信号,以表示CPU将进入异常处理状态,以下提到的req为1都是这个意思):

  1. 当PC探查出异常时,我们必须把读取出的指令覆盖为NOP
  2. 当req为1时,若此时存在一个可能会修改HI寄存器/LO寄存器的指令在E级,则乘除模块不应该对其进行任何响应(即不写入,不计算)
  3. 当req为1时,DM的字节写使能信号必须全部为0,读取出的数据必须用0覆盖

下面我们思考如何处理这些异常,以及从外部可能输入的中断。

处理异常中断

异常中断处理思路

接下来我们要展开一个问题:我们是如何处理异常中断的?

具体来说,在一个时钟周期内,

  1. CP0会先检查现在是否正在处理异常中断(不在异常中断处理里处理异常中断),再检查CP0所在流水级的指令是否存在异常,以及外部是否有未被禁用的中断(如果同时出现,我们认为此时发生了一个中断而非异常)。

  2. 如果现在没有在处理异常中断,并且CP0所在流水级存在异常或外部产生了未被禁用的中断,那么CP0会向CPU发送一个“跳转至异常中断处理”的信号。

  3. CPU在接收到该信号之后,对CP0所在流水级及其之前的流水级进行“清空”(之后的流水级继续正常执行),并让PC直接跳转至异常中断处理地址(0x4180),此时CP0记录下处理完异常后需要返回的地址,以及出现异常的指令是否为延迟槽内指令。

在CPU完成异常中断处理之后,会通过 eret 回到“处理完异常后需要返回的地址”里。

所以我们再次重复一下下面这段话:

其实CP0就是一个大号的处于M级的分支判断模块,实际上所有工作都是你原先设计的CPU在做,只是CP0这个东西可以帮你保存一些异常中断的状态信息(包含处理完之后回到哪一条指令),以及帮你判断当前时刻是否需要处理一个异常/中断而已。

以及,既然CP0会使用一些寄存器来保存异常中断的状态信息,那么我们在某些时刻是希望对这几个寄存器进行读写的,所以CP0也应该支持对其内部的寄存器进行读写。

所以,我们根据这个思路,以及教程给出的一种实现的模块,来构建这个CP0。

CP0的实现

端口、寄存器、寄存器的有效位

下面我们贴出教程给出的端口表格(稍有修改):

(这一段本来应该放在表格后给出,但是后来发现放到前面似乎更有助于理解整个表格)

其实教程在给出这些表格的时候,就已经给出了CP0的两个主要功能:读写内部寄存器(mfc0,mtc0),判断是否进行异常跳转。前者和普通的GRF差别不大,只需要在后续做一点点特殊修改即可,而后者是一个比较新的东西,我们会具体展开说明如何实现后者。

端口 方向 位数 解释
clk IN 1 时钟信号。
reset IN 1 复位信号。
en IN 1 写使能信号。
CP0Add IN 5 读取/写入目标寄存器地址。
CP0In IN 32 写入寄存器数据。
CP0Out OUT 32 读出寄存器数据。
VPC IN 32 当前异常中断发生时PC的值。
BDIn IN 1 当前异常中断发生指令是否是延迟槽内指令。
ExcCodeIn IN 5 当前发生异常类型。
HWInt IN 6 外部产生的中断信号。
EXLClr IN 1 是否结束异常中断处理(复位 EXL)。
EPCOut OUT 32 EPC 的值。
Req OUT 1 进入处理程序请求。

以及CP0内需要保存的各个32位寄存器(稍有修改):

寄存器 编号 功能
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 任何异常中断发生时置位,这会强制进入核心态(也就是进入异常中断处理程序)并禁止再次触发异常中断。
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 EPC 31:0 记录异常中断处理结束后需要返回的 PC。

接下来,我们具体描述如何实现CP0的各个功能。

读写状态寄存器

这一个功能主要是用来实现 mfc0,mtc0 的。

对于CP0的读写功能,我们主要用 enCP0AddCP0InCP0Out 端口来完成任务即可。这个东西其实和普通的GRF差不多,读寄存器就把 CP0Add 对应的寄存器的值输出到 CP0Out,该写寄存器就写到 CP0Add 对应的寄存器即可。

但是有一个需要注意的地方,CP0内的三个状态寄存器仅有部分位是有用的,对于无用位我们会选择将其始终维持在0,即初始化时置为0,写入时不向这些位进行写入。

进入异常中断处理程序

这一个功能主要是用来实现异常中断处理的。

VPC 开始,到 HWInt 结束(EXLClr 不在内,这个端口后面会具体说明),这些是我们判断是否进入异常中断处理程序的所有输入端口。我们来根据这些端口,以及以上给出的功能域信号的表格,填一下“异常中断处理思路”里给出的思路。

  1. CP0会先检查现在 EXL是否为1(是否正在处理异常中断),再检查 ExcCodeIn是否不为0(当前是否存在异常)和 HWInt&IM是否不为0且IE不为0(存在未被禁用的中断)。
  2. 如果现在 EXL 为0,并且 ExcCodeIn 不为0或“HWInt&IM 不为0且IE不为0”,那么CP0会将 Req 置为1,同时记录一下 EPCBDEXLExcCode 的值,否则置为0。
  3. CPU在接收到 Req 为1之后,对CP0所在流水级及其之前的流水级进行“清空”(之后的流水级继续正常执行),并让PC直接跳转至异常中断处理地址(0x4180)。

额外的,独立于以上所有内容的是,只要没有 reset 信号,每个时钟周期都要记录一次 IP

上面没有说明的是,这里的 VPC 应该接入什么呢?是PC寄存器当前的值吗?并不是。

我们在这里需要插入一个“宏观PC”的概念。实际上,不论是CP0在处理异常,还是外部给CPU一个中断,在CP0和外部眼里,你的CPU表现的应该像是“一个有延迟槽的单周期CPU”,每条指令只有两个状态:执行完了和没执行(而不会细化到执行到了哪一级流水线)。

(打个比方,这个概念有点类似于你在用Mars跑自己写的代码对拍P5P6的时候,实际上你并不关心这块CPU内部是长啥样的,有几个流水级,观察到的就是一个“有延迟槽的单周期CPU”。而当你观察Mars的PC时,看到的也不是5个流水线各自的PC是多少,而是一个“整体的PC值”。我们所引入的宏观PC就是这里的PC。)

由于我们的CP0设置在了M级,异常处理就只能在M级来做,所以这个宏观PC的值就等同于M级PC的值,在CPU处理异常中断时,我们就认为M级PC的值是当前时刻的PC,也就是我们传入的 VPC 的来源。

于是,CP0的判断功能被勾勒完了。

接下来我们细说CP0记录的都是什么值:

  • 记录 ExcCode 时,如果当前同时有异常和中断,那么我们不应该直接记录 ExcCodeIn,而是应该记录成一个 Int
  • BDIn 为1时,表明发生异常中断的指令为延迟槽内的指令,此时我们应该让 EPC 记录 VPC-4,即延迟槽前一条指令(某个分支跳转指令),否则正常记录 VPC
  • EXL 置为1。
  • BD 置为 BDIn

为什么 BDIn 为1时, EPC 应该记录 VPC-4 呢?

对绝大多数情况来说,EPC 就是 VPC ,这个没错。但问题在于,如果对于分支跳转指令和其延迟槽内的指令呢?

如果异常中断发生在了延迟槽内的指令,那么我们就要面对一个问题:我们无法单纯通过这条指令的运行结果判断分支跳转指令会把PC改成什么样。因此,我们必须让前一条分支跳转指令“重新跑一遍”,所以需要返回的地址也就变成了 VPC-4 。其余情况下,需要返回的地址就是 VPC

退出异常中断处理程序

这一个功能主要是用来实现 eret 的。

我们从D级的控制单元引出一个新的信号(记为 ID_EXLClr ),将其流水至M级后与 EXLClr 进行对接,当此时控制器所在级运行的指令为 eret 时,我们把这个信号置1,否则置0。

EXLClr 置为1时,我们需要修改 EXL 为0。

然后?没了。这个功能其实就是解除由 EXL 记录的异常中断处理状态而已。

(当然,这个 EXLClr 后面还有一个作用,我们在改CPU的时候再说)

接下来我们要展开的就是,CPU内除了产生异常信号,还应该对异常中断信号和指令做出什么回应。

CP0内信号的优先级问题

实际上,只有 req 和其他信号是可能同时出现的,其他信号里两两不会同时出现,所以我们只需要明确 req 的优先级高于其他所有信号即可。

CPU的修改

在原来的CPU部分,我们主要要多做两件事:

  1. 根据 req 信号清空M级与其之前的流水级,并跳入异常中断程序;
  2. 程序运行到 eret 的时候跳回正常程序。

关于 mfc0mtc0syscall,我们会发现在根据以上内容搭完框架之后,这几条指令都变成了普通的新指令,所以我们不做展开叙述。(如果说有什么新东西的话,那就是这会带来一个读写CP0的新通路,但是这个东西做起来比加乘除模块还好搞,想必大家都已经非常熟练了吧)

首先需要强调的是,resetreqID_EXLClrstall 是四种信号,各个模块在接收到这些信号的表现是有不同的,有些时候不能进行粗暴的合并,这一点我们马上就会感觉到。

(这里我相对于教程加了一个 ID_EXLClr 信号,主要也是用来应对 eret 这个指令的特殊性的,毕竟这条指令没延迟槽,运行到 eret 的时候需要清空一下延迟槽,并且跳入的PC来源也会发生改变,所以需要引入一个信号)

前者其实看起来是比较好搞的,貌似就是给所有流水寄存器(M级到W级也要清空)发一个清空信号即可……吗?

这就不得不提到一种特殊情况了。当我们产生阻塞信号时,D级到E级的流水寄存器会被清空一次,导致下一周期的E级会产生一个空泡,再下一个周期这个空泡就到了M级。而如果有一个中断恰好在空泡进入M级的时候产生,那么CP0就会选择处理这个中断,而此时CP0记录下的返回地址会是什么呢?

没错,如果你产生空泡的方式是完全的清空所有寄存器,包括传递PC的寄存器,那么此时你会把返回地址记录成 0x00000000(或者 0x00003000,或者什么别的你实现的东西),这显然不符合我们的期望。

同理,判断一条指令是否为延迟槽内指令的的信号也不能在阻塞的时候被暴力清空。

而这只是针对 stall 信号的特殊情况,对于 reset 后刚开始的几个周期,也有可能产生类似的错误。因此我们需要对这四种信号产生的时候流水寄存器的行为做一个规定。

信号 PC流水寄存器行为 BD流水寄存器行为 其他流水寄存器行为
reset 复位至 0x00003000 复位至 0 清空
req 设置为 0x00004180 设置为 0 清空
ID_EXLClr (仅对F级到D级流水寄存器起作用,且此时stall信号为0 设置为当前周期的 EPC 的值 不发生变化 清空
flush(仅指对D级到E级流水寄存器起作用的阻塞信号) 不发生变化 不发生变化 清空

以及,当这些信号中的某几个同时出现时,我们按照从上至下的优先级依次处理。

Update(2024/11/26):除此之外,我们还需要考虑一个问题:eret 在D级时就会发挥作用,而此时可能会出现一条 mtc0 将要写入 EPC 但未写入的情况。此时,我们应该增加一种阻塞情况,即当D级指令为 eret ,且E级或M级存在向 CP0 写入 EPC 的指令时,我们选择进行阻塞。

总之,如此连接之后,对流水寄存器的信号操作应该是完全正确了。

最后,我们在产生 req 信号的时候向 PC 写入 0x00004180,否则在产生 EXLClr 信号的时候向 PC 写入 EPC,对异常中断的处理就基本结束了。

用桥连接外设

这一步相对于前面的东西来说,实际上没啥好说的,首先导入教程里给的 Timer,然后写一个Bridge,最后在 mips.v 里把线连一下,没了。

(平心而论,这段教程给的信息其实足够了)

我只在这里描述一下Bridge里有啥端口吧,当你知道有啥端口之后,实际实现真的很简单。

我们需要的端口有:

  • CPU对外设的读写地址,字节写入使能,读取数据端口;
  • DM的读写地址,字节写入使能,读取数据端口;
  • Timer0和Timer1的读写地址,写入使能,读取数据端口;
  • 中断发生器的写入地址,字节写入使能端口。

没了(

课上考试题目简述与思路

题目简述

第一次P7的课上测试包含5道题,其中前4道题为对课下部分的强测(功能强测,异常强测,中断强测,冒险强测),最后一题为新增指令题,新增指令为 withdraw

关于 withdraw,其只有一种编码:0x04000000opcode0b000001),作用大致为撤销最后一次 sw 操作。

具体来说,题目要求在CP0中添加 18,19,20,21 号寄存器,其中 $18$19 分别存储当前 withdraw 指令可以撤销的写入地址的上界和下界(左闭右开),$20 存储上一次成功执行的 sw 指令的写入地址,$21 存储上一次成功执行的 sw 指令写入地址的原数值。这四个寄存器都允许通过 mfc0 进行读取,但仅 $18$19 允许通过 mtc0 进行写入, $20$21 仅能在 sw 指令被正确运行时被进行修改。

withdraw 指令到达M级时,若 CP0[$19] <= CP0[$20] < CP0[$18],则 DM[CP0[$20]] <= CP0[$21],否则报出 AdES 异常。

withdraw 指令运行之前不存在任何一条 sw 被运行,则此次指令运行被忽略,即不进行任何操作。

保证测试数据中不会出现使用 mtc0 写入CP0中 $20$21 的指令。

保证 CP0[$18] < CP0[$19],且 CP0[$18],CP0[$19] 均已对齐(可被4整除)且均在DM存储范围内。

实现思路

内容很多,但是可以一点点来。

首先,作为集成在CP0的寄存器,我们可以先把最基本的常规读写功能写好。

然后我们注意到,整个题目实际上就是 swwithdraw 进行各种交互,所以我们可以先添加 sw 指令对CP0的更多影响,再尝试加入 withdraw 本体。

对于加入 sw 对CP0的影响,我们需要这样几个信息:当前指令是否是 sw,当前指令写入地址是多少,当前指令写入地址原数值是多少。如果当前 req 为0 且指令为 sw,则对 $20$21 进行更新即可。

而对于 withdraw 本体来说,则需要对CP0内和CPU两段都进行修改(对CP0内修改是因为异常判断最好是在CP0内搞定,或者就需要把 $18$19 引出CP0做判断再把结果引回来)。对CP0来说,我们需要一个新的1位寄存器信号(记为 exist_sw)记录一下在运行该指令前是否已经出现了正常运行的 sw 指令,将该信号和 $20$21 暴露为三个输出,用于后续操作。此外,我们需要新增一种 req 为1的情况,即当前指令为 withdraw,且之前已经有过对 sw 的修改,且不满足 CP0[$19] <= CP0[$20] < CP0[$18] ,且 EXL 不为1的情况。

而在整个CPU进行修改的话,我们主要是需要加入 withdraw 对内存的写入,即在M级当前运行指令为 withdraw 时,若 exist_sw 为1,则向DM写入,否则不做任何操作。(如果你之前实现“req 为1时不进行对DM的任何写入”这一步做的比较可扩展,那么你不需要再做任何操作,否则还需要考虑一下这个)

然后?

你做完了。

恭喜AC!并且,恭喜AK!

在CO之前……

就像是我本想纪念,但是已经消失的回忆一样,这部分内容也莫名其妙的消失了。

我想,你应该能用下面这一段,在我的博客设法找出来点什么。

1
now, it's time to Leave CO Behind.