BUAA CO CPU
前言
CPU设计文档
概述

P7总电路如下:
1 |
|
数据通路模块
IFU(取指令单元)
端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
clk | I | 1 | 时钟信号 |
reset | I | 1 | 同步复位信号 |
IFU_STALL | I | 1 | 使能信号,用于暂停情况 |
nextPC | I | 32 | 下一条要被执行的指令的地址 |
F_inStr_IM | I | 32 | 外部 IM 传入的指令 |
F_PC | O | 32 | 输出当前正在执行的指令的地址 |
F_inStr | O | 32 | 输出当前正在执行的指令 |
PC_IM | O | 32 | 传至外部 IM 的PC值 |
GRF(通用寄存器组)
端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
PC | I | 32 | 用于输出指定信息 |
clk | I | 1 | 时钟信号 |
reset | I | 1 | 同步复位信号 |
GRF_A1 | I | 5 | 将对应寄存器的数据读出到 GRF_RD1 |
GRF_A2 | I | 5 | 将对应寄存器的数据读出到 GRF_RD2 |
GRF_A3 | I | 5 | 写入目标寄存器的编号 |
GRF_WD | I | 32 | 数据输入信号 |
W_writeReg_EN | I | 1 | 写使能信号 |
GRF_RD1 | O | 32 | 输出 GRF_A1 指定的寄存器中的 32 位数据 |
GRF_RD2 | O | 32 | 输出 GRF_A2 指定的寄存器中的 32 位数据 |
NPC
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
F_PC | I | 32 | 当前指令地址 |
D_RD1 | I | 32 | 用于jr指令,传入地址 |
D_imm | I | 25 | 立即数来源 |
D_isBranch | I | 1 | 用于B类指令判断是否跳转 |
D_inStrType | I | 10 | 传递指令类型 |
F_nextPC | O | 32 | 输出下一指令地址 |
D_PC8 | O | 32 | 传递PC + 8,用于jal指令的转发 |
CMP
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
D_RD1 | I | 32 | 参与比较的第一个值(转发后) |
D_RD2 | I | 32 | 参与比较的第二个值(转发后) |
D_inStrType | I | 10 | D级指令类型 |
isBranch | O | 32 | 输出结果至NPC |
EXT
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
EXT_imm | I | 26 | 包含需要位扩展的立即数 |
EXT_extOp | I | 2 | 位扩展方式 |
D_extResult | O | 32 | 位扩展结果 |
ALU
端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
aluOp | I | 3 | ALU 功能选择信号 |
ALU_src1 | I | 32 | 参与 ALU 计算的第一个值 |
ALU_src2 | I | 32 | 参与 ALU 计算的第二个值 |
ALU_result | O | 32 | 输出 ALU 计算结果 |
MUDI(乘除模块)
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
clk | I | 1 | 时钟信号 |
reset | I | 1 | 同步复位信号 |
E_isStart | I | 1 | 乘除计算指令开始信号 |
MUDI_mudiOp | I | 3 | MUDI 功能选择信号 |
MUDI_src1 | I | 32 | 参与 MUDI 计算的第一个值 |
MUDI_src2 | I | 32 | 参与 MUDI 计算的第二个值 |
isBusy | O | 1 | MUDI 计算判断信号 |
MUDI_HI | O | 32 | HI 寄存器值 |
MUDI_LO | O | 32 | LO 寄存器值 |
DM(数据存储器)
端口定义
信号名 | 方向 | 位宽 | 描述 |
---|---|---|---|
DM_writeMem_EN | I | 1 | 写使能信号 |
M_inStrType | I | 10 | M 级指令类型 |
DM_ADDR | I | 32 | 传入存取数据的地址 |
DM_dataIN | I | 32 | 传入待存储的数据 |
M_dataOUT_DM | I | 32 | 从外部 DM 传入的数据 |
M_dataOUT | O | 32 | 输出 M_dataOUT_DM 的数据 |
DM_ADDR_DM | O | 32 | 向外部 DM 传入地址 |
DM_dataIN_DM | O | 32 | 向外部 DM 传入数据 |
DM_byteen_DM | O | 32 | 向外部 DM 传入写使能信号 |
Controller(控制模块)
控制信号
序号 | 信号名 | 位宽 | 描述 |
---|---|---|---|
1 | inStrType | 10 | 传递指令信息 |
2 | regDst | 2 | GRF 中 A3 接口输入数据选择 |
3 | aluSrc | 1 | ALU_src2 接口输入数据选择 |
4 | writeMem_EN | 1 | DM 写入使能信号 |
5 | memToReg | 1 | GRF 中 WD 接口输入数据选择 |
6 | writeReg_EN | 1 | GRF 写入使能信号 |
7 | aluOp | 3 | ALU 功能选择信号 |
8 | mudiOp | 3 | MUDI 功能选择信号 |
9 | extOp | 2 | 立即数符号扩展选择 |
10 | MUDI_sel | 1 | HI, LO 寄存器选择 |
11 | ALU_or_MUDI | 1 | ALU, MUDI 结果选择 |
12 | isStart | 1 | 乘除有关指令开始信号 |
13 | D_TuseRs | 2 | 判断指令对rs的使用时间 |
14 | D_TuseRt | 2 | 判断指令对rt的使用时间 |
15 | E_Tnew | 2 | 判断指令产生新值与E级距离 |
16 | M_Tnew | 2 | 判断指令产生新值与M级距离 |
STALL(暂停控制器)
1 | assign D_rs = D_inStr[25:21]; |
流水级寄存器
1 | module D_REG ( |
转发的实现
1 | assign D_RD1 = (M_inStrType == `jal && D_inStr[25:21] == 5'b11111) ? M_PC8 : (M_writeReg_NUM == D_inStr[25:21] && D_inStr[25:21] != 0 && M_writeReg_EN) ? M_aluResult : GRF_RD1; |
思考题
- P3
1:上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能。
答:状态存储:IFU、GRF、DM;状态转移:NPC、Controller、ALU、EXT。
2:现在我们的模块中 IM 使用 ROM, DM 使用 RAM, GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。
答:我认为是合理的。ROM为只读寄存器,与IM的特性相同;RAM为读写寄存器且带有异步复位功能,与DM特性相同;GRF寄存器堆刚好可以使用Register实现。
3:在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。
答:我还设计了NPC模块,将有关PC的计算部分独立出来作为一个模块。
4:事实上,实现 nop
空指令,我们并不需要将它加入控制信号真值表,为什么?
答:空指令到来后,控制信号均为假,已实现不进行任何操作的目的,所以不需要额外加入控制信号真值表。
5:阅读 Pre 的 “MIPS 指令集及汇编语言” 一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。
答:我认为测试样例覆盖率较好,但是数据范围较小,且未设计$0寄存器的测试。并且未测试sub指令。
- P4
1. 阅读下面给出的 DM 的输入示例中(示例 DM 容量为 4KB,即 32bit × 1024字),根据你的理解回答,这个 addr 信号又是从哪里来的?地址信号 addr 位数为什么是 [11:2] 而不是 [9:0] ?

答:Addr由ALU的计算结果得出,由于1个字占4个字节,所以Addr后两位恒为0,为了节省空间从而取[11:2]。
2. 思考上述两种控制器设计的译码方式,给出代码示例,并尝试对比各方式的优劣。
指令对应的信号如何取值,如:
1 | case(instruction_type) |
控制信号每种取值所对应的指令,如:
1 | assign memToReg[0] = lw | lb; |
前者的优势是在设计时思路较为清晰,后者的优势是代码便于理解,修改或调试时更加方便。
3:在相应的部件中,复位信号的设计都是同步复位,这与 P3 中的设计要求不同。请对比同步复位与异步复位这两种方式的 reset 信号与 clk 信号优先级的关系。
答:同步复位时clk信号优先级高于reset信号,只有clk上升沿到来时reseti信号有效才会复位;异步复位时reset信号优先级高于clk信号,无论clk信号为何值,只要reset信号有效就会复位。
4:C 语言是一种弱类型程序设计语言。C 语言中不对计算结果溢出进行处理,这意味着 C 语言要求程序员必须很清楚计算结果是否会导致溢出。因此,如果仅仅支持 C 语言,MIPS 指令的所有计算指令均可以忽略溢出。 请说明为什么在忽略溢出的前提下,addi 与 addiu 是等价的,add 与 addu 是等价的。提示:阅读《MIPS32® Architecture For Programmers Volume II: The MIPS32® Instruction Set》中相关指令的 Operation 部分。
答:在忽略溢出的前提下,add与addi均不需要考虑对于溢出的判断,且保存结果的后32位,从而与addu、addiu等价。
- P5
1:我们使用提前分支判断的方法尽早产生结果来减少因不确定而带来的开销,但实际上这种方法并非总能提高效率,请从流水线冒险的角度思考其原因并给出一个指令序列的例子。
答:提前的分支判断会使TuseRt、TuseRs的值提前发生变化,从而影响暂停控制器对于是否需要暂停的判断:比如
1 | add $t0, $t1, $t2 |
若判断提前至D级,则需要暂停使得beq中$t0为计算后的新值,而不提前则不需要暂停。
2:因为延迟槽的存在,对于 jal 等需要将指令地址写入寄存器的指令,要写回 PC + 8,请思考为什么这样设计?
答:若考虑延迟槽,则PC+4指令在当前jal指令进入D级时已经取出并进入流水线,在返回时要从PC+8开始执行,否则PC+4会执行两次,导致程序错误。
3:我们要求大家所有转发数据都来源于流水寄存器而不能是功能部件(如 DM 、 ALU ),请思考为什么?
答:如果从功能部件进行转发,则会增长关键路径长度,其会使时钟频率变长,降低效率。
4:我们为什么要使用 GPR 内部转发?该如何实现?
答:GRF的内部转发等价于W级数据转发至D级,若不实现内部转发,则位于D级的指令要使用W级写入的数据时,输出的是旧数据而非新数据。(读写在上升沿同时发生,所以读出的是新写入前的数据)。实现方法如下,只需在GRF对输出进行修改:
1 | assign GRF_RD1 = (GRF_A3 && W_writeReg_EN && GRF_A3 == GRF_A1) ? GRF_WD : register[GRF_A1]; |
5:我们转发时数据的需求者和供给者可能来源于哪些位置?共有哪些转发数据通路?
答:如图所示,黄线、红线、绿线起点为供给者,终点为需求者。

6:在课上测试时,我们需要你现场实现新的指令,对于这些新的指令,你可能需要在原有的数据通路上做哪些扩展或修改?提示:你可以对指令进行分类,思考每一类指令可能修改或扩展哪些位置。
答:可能需要增加控制信号,增加流水级寄存器的传递值以及增加ALU、NPC的计算方法。
7:确定你的译码方式,简要描述你的译码器架构,并思考该架构的优势以及不足。
答:译码器结构分为两部分,判断指令类型以及输出控制信号:
1 | wire rType, nop, add, sub, jr, ori, lw, sw, beq, lui, jal; |
1 | // inStrType |
优点在于代码整体直观简洁,且便于修改。
- P6
1:为什么需要有单独的乘除法部件而不是整合进 ALU?为何需要有独立的 HI、LO 寄存器?
答:乘除相关的指令涉及到时序逻辑,而ALU的功能目前仅涉及到组合逻辑,所以乘除相关指令更适合放到单独的模块中。如果不使用独立的HI、LO寄存器,即在GRF中添加HI、LO寄存器,那么mfhi、mflo、mthi、mtlo等指令关于HI、LO寄存器的操作会变得混乱,会与通用寄存器的写入、读取操作混淆。
2:真实的流水线 CPU 是如何使用实现乘除法的?请查阅相关资料进行简单说明。
答:CPU执行乘法指令是以加法为基础进行的,乘法的本质是若干个相同的数相加。CPU执行除法指令是以减法为基础的,减法也是以加法为基础的,除法的本质是看被除数能够减去除数几次,这个“几次”便是得到的商,减去若干次后剩下的不够再减一次的部分便是余数。
3:请结合自己的实现分析,你是如何处理 Busy 信号带来的周期阻塞的?
答:在暂停模块中加入对MUDI模块有关的Start信号和Busy信号的考虑,当二者有一个有效时,则触发暂停。
4:请问采用字节使能信号的方式处理写指令有什么好处?(提示:从清晰性、统一性等角度考虑)
答:字节使能信号一方面保证了数据传输之间的一致性,即所有存取指令向DM传输的、从DM传来的地址和数据都是32位,另一方面也使存取的具体操作变得更加清晰。
5:请思考,我们在按字节读和按字节写时,实际从 DM 获得的数据和向 DM 写入的数据是否是一字节?在什么情况下我们按字节读和按字节写的效率会高于按字读和按字写呢?
答:不是,写入和读出的数据为32位。当按字节对数据进行处理的指令较多时按字节读写会有更高的效率。
6:为对抗复杂性你采取了哪些抽象和规范手段?这些手段在译码和处理数据冲突的时候有什么样的特点与帮助?
答:将乘除模块的计算结果与ALU的计算结果在E级通过多路选择器并到一起,这样p6的转发操作与p5完全相同不用修改。
7:在本实验中你遇到了哪些不同指令类型组合产生的冲突?你又是如何解决的?相应的测试样例是什么样的?
答:新的冒险冲突主要是针对乘除运算器和寄存器的使用争夺问题。1.乘除寄存器在运算时末能产生相应的HI和LO时需要执行mfio/mfhi/mtlo/mthi指令,这时直接阻塞即可。2.mtlo/mthi指令需要利用的Rs寄存器的最新的值可能还未产生,此时利用P5就构建好的转发路径即可。3.mflo/mfhi指令改变了寄存器的值.若有之后的指令需要使用相同寄存器时,同理进行转发即可。
8:如果你是手动构造的样例,请说明构造策略,说明你的测试程序如何保证覆盖了所有需要测试的情况;如果你是完全随机生成的测试样例,请思考完全随机的测试程序有何不足之处;如果你在生成测试样例时采用了特殊的策略,比如构造连续数据冒险序列,请你描述一下你使用的策略如何结合了随机性达到强测的效果。
答:首先给每个寄存器赋上不同的随机初值,再对其进行生成不同的操作。完全随机代码的缺点就是可能难以覆盖所有的矛盾冲突,因此我特意手动构造了一些关于冲突的指令,对于这些冲突的构造也是主要针对第7个思考题中提及的矛盾。
- P7
1.请查阅相关资料,说明鼠标和键盘的输入信号是如何被 CPU 知晓的?
鼠标和键盘相当于中断发生器,当鼠标和键盘产生输入信号时,其值会被写入相关寄存器,中断发生器将会给cpu一个中断信号,cpu收到中断信号后进入到异常处理程序中,在异常处理程序中获得键鼠的信息。
2.请思考为什么我们的 CPU 处理中断异常必须是已经指定好的地址?如果你的 CPU 支持用户自定义入口地址,即处理中断异常的程序由用户提供,其还能提供我们所希望的功能吗?如果可以,请说明这样可能会出现什么问题?否则举例说明。(假设用户提供的中断处理程序合法)
一种思路是,假如我们的cpu让用户定义入口地址,则可能需要我们新开辟输入端口令cpu输入地址,当用户键入地址时就需要中断来处理,但是此时还用户输入的地址还并未被知晓因此便无法得知从何处进入中断处理程序。而且我们指定入口地址的话,将无需软件工程师再费心从何处进入异常,而只需要编写自己所需要的异常处理程序即可,因此,这会大大降低软件工程师的压力。
3.为何与外设通信需要 Bridge?
因为如果不用桥而直接让cpu与外界设备进行通信,则cpu的端口就会变得很多(原本需要在桥上开的端口都将开到cpu上)这样会大大增加cpu的复杂度,而使用bridge就会让端口变得十分简洁。
4.请阅读官方提供的定时器源代码,阐述两种中断模式的异同,并针对每一种模式绘制状态移图。
模式0和模式1的相同点在于他们都需要“手动”开启计数使能而开始计数而不能自动开始计数,而他们的区别主要体现在:当计时器计时结束时,模式1下的计数器会自动将初值加载到计数器重新开始倒计数;在模式0下的计数器会停止计数并将计数器的计数使能关闭而停止倒计数,当计数使能被设置为1后再次开始计数。
5. 倘若中断信号流入的时候,在检测宏观 PC 的一级如果是一条空泡(你的 CPU 该级所有信息均为空)指令,此时会发生什么问题?在此例基础上请思考:在 P7 中,清空流水线产生的空泡指令应该保留原指令的哪些信息?
这时如果这个空泡是一个延迟槽指令的话,应该向cp0的epc中写入该pc-4,但是由于空泡指令不包含任何信息,这导致会写入到epc一个错误的值,同时不包含isBD这一信息的话,也无法判断该空泡处是否本应是一个延迟槽指令。因此,清空流水线所产生的空泡指令应该保留pc和isBD两个信息。
6. 为什么 jalr
指令为什么不能写成 jalr $31, $31
?
当jalr或jalr的延迟槽异常时需要进入异常处理程序,处理完后需要跳回jalr重新执行,此时$31的值已被改变,使得程序运行出错。