YatCPU
Introduction
- 单周期 CPU:本实验的目的是从零开始编写一个可以运行 RV32I 指令集程序的单周期 CPU,这个 CPU 将会是后续所有实验的基础。完成本实验之后,可以通过仿真的方式,加载运行 RV32I 程序,验证正确性。
- 中断机制:本实验在单周期 CPU 的基础上,添加了中断控制器以及中断控制流的内容,使单周期 CPU 可以处理来自外部设备的中断,响应 IO。完成本实验之后,CPU 将具备响应 UART 数据中断、时钟中断的功能,你将可以使用 UART 端口来操作 CPU 运行中的程序。
- 流水线 CPU:本实验在以上两个实验基础上,对 CPU 进行性能优化,从单周期的 CPU 修改为多周期以及流水线的 CPU。本实验将通过几个性能评测,来验证 CPU 性能优化的效果。
- 总线处理:本实验在流水线 CPU 的基础上,给 CPU 添加总线功能,实现外设与 CPU 的解耦。在完成总线实验后,添加外设无需修改 CPU 本身,只需要实现总线协议并与总线对接即可。本实验目的是进一步完善 CPU 实验的总体框架,提供更贴近真实计算机系统的结构实践。
Lab0—get ready for experiment
chisel 3基本学习
chisel3基本语法和功能
- 区别举例:想要在chisel3中使用一个常量,要写when(value===12.U),而不是if(value==12)
基本类型
- 无符号整数**
UInt
与有符号整数SInt
** ,使用.W来指定整数位宽(Uint(8.W)
),.U来将scala中的整数转换为chisel3的硬件整数(见我们最开始举的例子) - 布尔值Bool;使用.B来转换成硬件布尔值(如**
true.B
**) - 模块:声明一个模块需要继承
Module
类,并通过io
成员声明输入输出端口。
1 |
|
- 组合逻辑
val wire = Wire(UInt(8.W)) val wireinit = WireInit(0.U(8.W))
- 时序电路
- 寄存器
val reg = Reg(UInt(8.W)) val reginit = RegInit(0.U(8.W))
Chisel3 项目结构
1 |
|
- 反汇编:llvm-objdump
- 目标文件反汇编:
objdump -s -d main.o > main.o.txt
//将main.o 反汇编并将结果输出到txt文件中。 - 可执行文件反汇编:
objdump -s -d main> main.txt
- 目标文件反汇编:
objdump反汇编常用参数
objdump -d <file(s)>
: 将代码段反汇编;objdump -S <file(s)>
: 将代码段反汇编的同时,将反汇编代码与源代码交替显示,编译时需要使用g
参数,即需要调试信息;objdump -C <file(s)>
: 将C++符号名逆向解析objdump -l <file(s)>
: 反汇编代码中插入文件名和行号objdump -j section <file(s)>
: 仅反汇编指定的section
Lab1:单周期CPU
本实验的目的:理解 CPU 的基本结构以及 CPU 是如何执行指令的。
- 基本概念,然后会按照指令执行的步骤逐步构造数据通路和控制单元(期间会留有填写代码的任务,请记得完成),最终构造成一个简单的单周期 RISC-V 处理器。
基本概念:
- 数据通路:显示数据从一个组件流向另一个组件的所有方式。
- 控制信号:让各个数据通路部件知道自己要干什么。CPU 原理图中的 Decoder、ALUControl、JumpJudge 三个元件都可以看作控制单元。他们接收指令并输出控制信号。
- 组合单元与状态单元:组合逻辑电路构成的单元叫组合单元,时序逻辑电路构成的单元叫做状态单元。
- 本实验中只有寄存器属于状态单元(内存不属于 CPU 内核的范畴),其余的均为组合单元。
- 组合单元:输出只取决于当前的输入,并且不需要时钟作为触发条件,输入会立即(不考虑延时)反映到输出
- 状态单元:存储了状态,并且以时钟作为触发条件,时钟的上升沿到来时输入才会反映到输出
实现方式:
我们设计的 RISC-V CPU 能执行 RISC-V 指令的一个核心子集(RV32I):
- 算术逻辑指令:
add
、sub
、slt
等 - 存储器访问指令:
lb
、lw
、sb
等 - 分支指令:
beq
、jar
等
我们将执行指令分为五个不同的阶段:
- 取指:从内存中获取指令数据
- 译码:弄清楚这条指令的意义,并读取寄存器数据
- 执行:用 ALU 计算结果
- 访存(
load
/store
指令):读写内存 - 回写(除了
store
指令外所有指令):将结果写回寄存器
下面我们先按照上述步骤逐步构建数据通路部件,然后在 CPU 顶层模块将这些数据通路部件实例化并且连接起来。(下面涉及的代码都位于 lab1/src/main/scala/riscv
目录下)
Chisel教程——02.Chisel环境配置和第一个Chisel模块的实现与测试-CSDN博客
取指:
重点:
- 理解各个变量都是什么。,我们需要实现的是when指令有效时,先取出pc的当前指令,然后再判断是否需要jump—也就是判断jump_flag_id是否有效,如果有效就要把pc的值换成需要jump到的地址,否则就是顺序执行pc+4;
- 主要pc+4这个点,chisel语言写的比较不同4.U表示无符号的整数。
进行测试:sbt "testOnly riscv.singlecycle.InstructionFetchTest"
译码
一些知识:
- 多路选择器:Mux
Mux
类似于传统的三元运算符,参数依次为 (条件, 为真时的值, 为假时的值)
,建议用 true.B
和 false.B
来创建Chisel中的布尔值。
重点:
对于ALUop2,我有个小疑惑,提供的图里是0的时候连接reg,而1连接immediate,但是实际写的时候反过来了。不太理解。已经理解了,看前面提供的代码发现,所提供的Aluop2source结构体里面,reg对应的值是0,immediate对应的是1,故要对应上。
注意不要漏,所有i指令都要添加上,他甚至有一个单独的lui没有被添加,要额外加上。主要就是看那个指令有没有i标识。
对于使能指令比较简单,一开始傻傻的还去使用Mux,其实完全没有必要,因为值仅仅是真或假,直接判断是否符合就行了。Mux一般用于给条件后,读取不同值(非0,1)
- 只有load指令可以访问内存
- 只有store指令可以写入到内存
- 故自然而然得到结果
稍微困难一点点的是写回输入来源,由于有四个来源,不能使用两个通路的Mux了,要使用MuxLookup。使用指南:
- 三个参数:
1
2
3
4
5
6
7
8
9
10
11
12
13import chisel3._
import chisel3.util._
val key = Wire(UInt(3.W))
val default = 0.U
val lookupTable = Seq(
0.U -> "b000".U,
1.U -> "b001".U,
2.U -> "b010".U,
3.U -> "b011".U
)
val result = MuxLookup(key, default, lookupTable)- 两个参数:
1
2
3
4
5
6
7
8
9
10
11
12
13import chisel3._
import chisel3.util._
val key = Wire(UInt(3.W))
val default = 0.U
val lookupTable = Seq(
0.U -> "b000".U,
1.U -> "b001".U,
2.U -> "b010".U,
3.U -> "b011".U
)
val result = MuxLookup(key, default)(lookupTable)seq表示有序集合/序列。然后箭头左右表示的是一种映射关系。
执行
- 目的:给ALU的输入端口赋值。
- 我们要看到ALU.scala实现文件:
可以看到我们需要传入的是alu执行什么功能,op1和op2,然后他会计算出结果result。至于func的来源就是ALU_control,里面有一个output是会根据指令内容来输出alu_funct。
值得注意的是,我们在得到op1和op2时,op1_source和op2_source均是指示变量(类似于bool)表示输入来源,而没有运用我们上一个模块写的 如下:(或许是还没有到组合成一个完整单周期CPU的时候,尚且在一个周期一个周期实现,且各种数据来源比较割裂)
访存
- 只有load和store才有访存阶段。数据从内存读到寄存器或者反过来。
- 判断读还是写看 memory_read_enable使能,为1则read,反之write。
写回
多路选择器,决定从哪里得到写回的数据。
组成CPU
- CPUBundle 是 CPU 和内存等外设进行数据交换的通道。
烧板
- 生成verilator文件(运行Top.scala)
- 生成 Vivado 项目
- 生成比特流文件
- 烧录:由于是在wsl中进行的,会导致没有“驱动”的问题,然后发现了win上之前安装的vivado是可以连接到debian里面的,所以就直接在里面打开,根据program_device.tcl里面的指令(结合vivado中的烧录键),实际上只需要自己多输入最后那步
close_hw_target
,这样子能够保证vitis正常工作。
烧录结果:(后打开clock之后,会一闪一闪亮红灯)
vitis显示如下:
实验报告:
合规性测试部分:
1 |
|
lab2:
1 |
|
lab3:
1 |
|
lab1的测试结果:
Lab2中断
实验目的:
- 学习CSR寄存器及其操作命令
- 中断控制器的原理和设计
- 编写一个简单的定时中断发生器
CSR寄存器的操作命令:
回顾:中断和异常
CSR:用来控制和保存CPU的其他功能的状态。例如终端使能状态,特权等级等。
- mstatus寄存器:记录机器模式下的状态(status),如中断是否启用等。
- mepc寄存器:保存了终端返回后需要执行的指令地址,当 CPU 执行中断时,
mepc
寄存器被自动设置为当前指令的地址,如果EX
阶段正在执行跳转,则设置为跳转的目标地址。 - mcause寄存器:保存了中断的原因
- mtvec寄存器:保存了中断处理程序的地址。发生执行中断时会传给pc寄存器
- 中断发生时,CPU需要清空并阻塞流水线,并在CSR寄存器写入中断相关的信息。由于CSR寄存器堆实现只有一个读写端口,故需要多个周期才能写入CSR寄存器。写完后,发出控制信号,开始处理。
中断处理程序:实现更加复杂的功能。
- 我们可以将CSR指令分解为:CSR寄存器组,ID译码单元,EX执行单元,WB写回单元。
- CSR寄存器组需要根据ID模块译码后给出的控制信号和CSR寄存器地址来对内部寄存器进行寻址,获取其内容并修改。
- ID译码单元需要识别CSR指令。(看手册)
- EX执行单元:CSR 指令都是原子读写的,即一条指令的执行结果中,既要把目标 CSR 寄存器原来的内容写入到目标通用寄存器中,还要按指令语义把从目标 CSR 寄存器读出来的内容修改之后再写回给该CSR 寄存器。此时 EX 里面的 ALU 单元是空闲的,要得到写入 CSR 寄存器的值,可以复用 ALU,也可以不复用。
- WB写回单元:支持 CSR 相关操作指令后,写回到目标通用寄存器的数据来源就多了一个从目标 CSR 寄存器读出来的修改前的值。
中断控制器CLINT
检测外部中断,并在中断到来并且中断使能时,中断CPU目前执行流,设置好相关的CSR信息后跳转到中断处理程序。
- 保存到CSR寄存器的信息:CPU执行完当前指令的下一个状态。
- 让当前指令执行完后再跳转到中断处理程序
响应(硬件)中断
https://blog.csdn.net/zyhse/article/details/136390088
- 获取CPU下一个状态信息,一个周期内写入到相应的寄存器。
写入的内容包括:mepc,mcause,mstatus。
mepc
:保存的是中断或者异常处理完成后,CPU返回并开始执行的地址。所以对于异常和中断,mepc
的保存内容需要注意。mcause
:保存的是导致中断或者异常的原因,具体内容请查阅特权级手册里的相关内容。mstatus
:在响应中断时,需要将mstatus
寄存器中的MPIE
标志位设置为0
,禁用中断。- 注意注意注意:这里不是自己手动地将MPIE位设置为0,是将MIE位设置为0,然后硬件会自动的把MPIE位修改然后从
mtvec
获取中断处理程序的地址,跳转到该地址执行进一步的中断处理
- 注意注意注意:这里不是自己手动地将MPIE位设置为0,是将MIE位设置为0,然后硬件会自动的把MPIE位修改然后从
该部分的代码截图:
解释:disable_interrupt是用来修改MIE位的,并且让后续禁用中断(因为我们要保证不会发生中断嵌套)
然后看到数据中的信号连接:
- 着重提醒mcause的原因,我们在同一个文件的最上方看到了Status的结构体定义,了解到就这么几种,并没有把所有的原因都列举出来,所以暂时只需要这样子写。(更多的mcause原因可以查看后面的截图或者直接看特权手册第二卷)
mstatus的指令结构:
(硬件)中断返回
需要写入的寄存器:mstatus
从mepc中获取跳转目标地址(原本正常执行的下一条地址)
把 MIE 位置为 MPIE 位,那么 MPIE 为 1 的话 mret
就会恢复中断,如果 MPIE 为 0 的话,mret
则不改变 mstatus
的值,这也导致了我们不支持中断嵌套。
CLINT的实现:for简单—采用纯组合逻辑实现。
CLINT 需要一个周期就把多个寄存器的内容修改的功能,而正常的 CSR 指令只能对一个寄存器读-修改-写(Read-Modify-Write, RMW)。所以 CLINT 和 CSR 之间有独立的优先级更高的通路,用来快速更新 CSR 寄存器的值。
一个好的解释:
简单的定时中断发生器
MMIO的定时中断发生器—Timer
MMIO 简单来说就是:该外设用来和 CPU 交互的寄存器是与内存一起编址的,所以 CPU 可以通过访存指令(load/store)来修改这些寄存器的值,从而达到 CPU 和外设交互的目的。
CPU发出的逻辑地址要发送到哪个设备,就由逻辑地址的高位作为外围设备的位选信号即可,低位则用于设备内部的寻址。
实验任务:
- EX 执行单元在处理 CSR 指令时能够正确地得到写入 CSR 寄存器的数据。(done
- CSR 寄存器组可以正确支持CLINT和来自CSR指令的读写操作。(done
- 定时中断发生器可以正确产生中断信号,并且实现 Timer 寄存器的 MMIO。
- CLINT 能够正确的响应中断并且在中断结束后回到原来的执行流。(done
如果能够正确完成本次实验,那么你的 CPU 就可以运行更加复杂的程序了,可以运行一下俄罗斯方块程序试试,如果想要上手玩的话,也许需要一个串口转接板,这样就可以通过电脑的键盘通过 UART 串口给程序输入字符了。
任务1:EX 执行单元在处理 CSR 指令时能够正确地得到写入 CSR 寄存器的数据
这个地方要注意,csr寄存器的立即数和之前译码阶段的立即数是不太一样的。译码阶段取立即数主要是针对立即数长度or位置不同于一般指令的进行获取。(如图)
从上方的指令划分的图中可以看到,crs寄存器的立即数和R指令寄存器指令取寄存器数是一样的(15-19)→ 在上图也是可以看到目标寄存器的值和立即数来源都是rs1,故后面只用在此处取值就行了。唯一的区别是第二个寄存器加上原本的func7组合在一起合成了crs寄存器的func7指示。(附上译码阶段的取指令的图:其中rs1对应的数据存储到reg1_data,rs2对应的数据存储到reg2_data—但此处是应该被忽略的,因为rs2部分的数据被合并到了func7中) mips的rs,rt的位置是确定的。
要注意的是,uimm是15-19位的原码,而source是rs1里面的值
任务一结果:
任务2:CSR 寄存器组可以正确支持CLINT和来自CSR指令的读写操作。
CSRRegister.CycleL
和CSRRegister.CycleH
这两个参数通常是用于访问或设置 CSR(Control and Status Registers)寄存器中的低位和高位的值。
首先我们看到CSR中需要我们实现的是可以正确读取CSR寄存器组的信息,并且可以正确与CLINT交互—将信息传到CLINT)
然后我们看到提示写了:如果数据线与CLINT冲突了,我们需要优先进行数据更新,这样子保证了CLINT读到的数是最新的。
我们可以借鉴已经写好的代码中判断条件的方法:
可以看到如果需要进行数据更新,要判断reg_write_enable_id是否为1,然后判断对应的地址是哪一个—>对应了需要更改的那一个寄存器的值。使用Mux来进行数据选择,如果条件满足,那么输入到CLINT中的值是reg_write_data_ex,若不满足则输入旧的值(也就是前面读取的寄存器组原本的值)
功能3:定时中断发生器可以正确产生中断信号,并且实现 Timer 寄存器的 MMIO
实现一个MMIO的定时中断发生器—timer
MMIO:该外设用来和CPU交互的寄存器一起编址,这样子CPU就可以通过访存指令来修改这些寄存器的值,从而实现CPU与外设交互。即内存映射。
没有总线时可以使用多路选择器(即现在阶段用多路选择器实现)
内部逻辑:两个控制寄存器 enable
寄存器和 limit
寄存器。
enable
寄存器:控制定时中断发生器的使能,为false则不产生中断,映射到地址空间的逻辑地址为0x80000008.limit
寄存器:用来控制定时器的中断发生间隔。映射到地址空间逻辑地址:0x80000004。内部有个加一计数器,达到limit为标准的界限时,定时器会发生一次中断信号(enable使能)。注:产生中断信号的时长没有太大关系,但是至少应该大于一个 CPU 时钟周期,确保 CPU 能够正确捕捉到该信号即可。
任务4:CLINT 能够正确的响应中断并且在中断结束后回到原来的执行流(更多内容在前面中断概念处)
CLINT的一些理解概念:
CLINT
具有固定的优先级方案,但不支持给定特权级别内的嵌套中断(抢占)。 然而,较高的特权级别可能会抢占较低的特权级别。CLINT
提供两种操作模式,直接模式和向量模式。- 在直接模式下,所有中断和异常都会捕获到
mtvec.BASE
。 - 在向量模式下,异常trap到
mtvec.BASE
,但中断将直接跳转到它们的向量表索引。
- 在直接模式下,所有中断和异常都会捕获到
一些代码解释:
InterruptStatus对象:
- 定义了中断状态的常量值
- None表示没有中断
- Timer0表示计时器0中断
- Ret表示返回状态
InterruptEntry对象:
- 定义了中断入口地址的常量值
- Timer0表示计时器0中断入口地址
InterruptState对象:
- 定义了中断状态机的不同状态
- Idle:空闲状态
- SyncAssert:同步断言状态
- AsyncAssert:异步断言状态
- MRET:表示从中断返回的状态
CSRState对象:
- 定义了CSR状态机的不同状态。
Idle
: 空闲状态,值为0x0
。Traping
: 陷入状态,值为0x1
。Mret
: 从中断返回状态,值为0x2
。
- mstatus寄存器和mcause寄存器
mstatus指令结构:(我们使用到的是32位的
mcause指令相关:
可能出现的机器级异常代码
mtvec
Lab3流水线CPU
内容简介:
- 竞争冒险的处理是流水线 CPU 设计的难点和关键所在。
- 设计一个简单的三级流水线 CPU(IF、ID 和 EX 三级),它只涉及分支和跳转指令带来的控制冒险,然后,我们再将三级流水线 CPU 的 EX 级继续切分为 EX、MEM 和 WB,形成经典的五级流水线,这样做带来的数据冒险需要使用阻塞和转发技术进行处理;最后,我们将分支和跳转提前到 ID 阶段,进一步缩短分支延迟。
- 参考资料:计组黑书4.5-4.8节
- 学习:
- 使用流水线设计缩短关键路径
- 正确处理流水线阻塞与清空
- 使用转发逻辑减少流水线阻塞
流水线寄存器:
缓存作用,切分组合逻辑,缩短关键路径。(存储该阶段产生的各种信息和数据。
基本功能:
- 在每一个时钟周期,根据复位(流水线清空)或阻塞(流水线暂停)的状态,将寄存器内容情况、保持或设置为新的值。输出则为其中保存的值。
- 为了方便复用,我们可以定义一个带参数的
PipelineRegister
模块,用来实现不同数据位宽的流水线寄存器。
task0:补充完成PipelineRegister.scala
stall
和flush
分别为流水线寄存器的阻塞和清空信号,in
和out
分别为要写入寄存器的值和寄存器的当前值。
- 解释:我们要存储结果,并且下一个阶段有可能还要用到前面那个阶段的状态信息,所以要用寄存器来传递、存储。
对比总结
特性 | Scala 变量 (var ) |
Chisel 寄存器 (Reg ) |
---|---|---|
功能 | 软件变量,临时存储值 | 硬件寄存器,存储状态 |
硬件生成 | 不生成硬件 | 生成硬件寄存器 |
值的更新 | 随程序执行更新 | 时钟边沿更新 |
硬件复位 | 不支持复位行为 | 可设置复位值 (RegInit ) |
用途 | 软件逻辑辅助计算 | 描述硬件逻辑和状态存储 |
三级流水线
- 两组流水线寄存器:
IF2ID
和ID2EX
划分出三个阶段。(已写好)- 取指(Instruction Fetch,IF):根据 PC 中的指令地址从内存中取出指令码;
- 译码(Instruction Decode,ID):将指令码解码为控制信号并从寄存器组中读取操作数;
- 执行(Execute,EX):包括 ALU 运算、访问内存和结果写回。
处理竞争冒险
解决控制冒险—清空
- 由于所有数据处理都在EX阶段,不会出现数据冲突,无需考虑。我们只需处理程序跳转(beq指令等)带来的控制冒险。
- EX执行到跳转指令
- EX端执行到分支指令且分支成立
- 发生中断,接收到CLINT发来的中断信号
InterruptAssert
。相当于在EX指令至上叠加了跳转指令,要丢弃之前的—IF和ID
- 无论哪种情况,都是由 EX 段向 IF 段发送跳转信号
jump_flag
和跳转的目标地址jump_address
,但在jump_address
写入 PC 并从该处取出指令前,流水线的 IF 和 ID 段已经各有两条不需要执行的指令,好在这两条指令的结果还没有写回,我们只需要清空对应的流水线寄存器,把它们变成两条空指令即可。 - 我们用一个控制单元来检测控制冒险并清空流水线,模块定义在
src/main/scala/riscv/core/threestage/Control.scala
,为了避免此题过于简单(呵呵,我们没有提供模块接口,请根据以上分析确定模块的输入输出,在// Lab3(Flush)
处将代码补充完整,并在src/main/scala/riscv/core/threestage/CPU.scala
的// Lab3(Flush)
处补充相关连线,使其能够通过ThreeStageCPUTest
测试。
control:
如果传入了要跳转的信号,那我们控制器需要输出信号,确保能把IF和ID两个阶段清空
CPU:与control连线。
首先要保证输入信息传入;注意的是Interrupt_flag 信号的来源
五级流水线
注意,上面的 CPU 结构图是我们完成所有实验之后的结果,在完成“缩短分支延迟”实验之前,我们 CPU 的结构将与上图稍有不同。例如,我们紧接着讨论的五级流水线 CPU 在 EX 阶段判断程序是否发生跳转,而不是 ID 阶段。
- 使用阻塞的方式解决数据冒险(由于数据的处理不止在执行阶段了,所以会出现数据冒险),使用**旁路和将分支跳转提前到ID阶段(这两步选做)**进一步提升效率。
解决数据冒险:阻塞
- 当处于 ID 阶段的指令要读取的寄存器依赖于 EX 或 MEM 阶段的指令时,发生数据冒险。可以保持这IF和ID阶段状态不变,直到相关数据被放出来。
- 位于 ID 阶段的指令和位于 WB 阶段的指令之间不会发生数据冒险,这是因为我们的寄存器组模拟实现了 Double Pumping 功能,即 WB 阶段在前半个时钟周期向寄存器组写入数据,ID 阶段在后半个时钟周期从寄存器组读出数据(读写分离)。
- 值得注意的是,我们在阻塞 PC 和 IF2ID 寄存器以保持 IF 和 ID 阶段不变的同时,需要清空 ID2EX 寄存器以在 EX 阶段插入空指令(“气泡”),否则 ID 阶段的指令还是会进入 EX 阶段,这样就不是“阻塞”,而变成“重复”了。
jalr
是跳转指令,虽然它后面两条指令依赖于它写入的寄存器,但是它们本就不应该紧接着被执行,而是应该被清空,所以在第 10 个时钟周期应该清空 IF2ID 和 ID2EX 寄存器,而不是阻塞。
情况 | PC阻塞 | IF2ID阻塞 | ID2EX清空 | 说明 |
---|---|---|---|---|
数据冒险(ID依赖EX或MEM) | 是 | 是 | 是 | 保持当前指令状态,防止错误执行 |
数据冒险(ID依赖WB) | 否 | 否 | 否 | WB 阶段支持双抽泵,不需要阻塞 |
控制冒险(跳转指令如jalr) | 否 | 是 | 是 | 跳转指令需要清空后续流水段,移除错误指令 |
依赖寄存器x0 | 否 | 否 | 否 | x0结果总是0,无需阻塞 |
核心逻辑:
- 数据冒险检测(依赖判断):
- EX 阶段的寄存器写入:如果 ID 阶段的指令需要读取的寄存器(
rs1_id
或rs2_id
)依赖 EX 阶段的目标寄存器(rd_ex
),需要阻塞。 - MEM 阶段的寄存器写入:如果 ID 阶段的指令依赖 MEM 阶段的目标寄存器(
rd_mem
),需要阻塞。
- EX 阶段的寄存器写入:如果 ID 阶段的指令需要读取的寄存器(
- 清空信号:
- 控制冒险(跳转指令):遇到跳转信号(
jump_flag
),需要清空 IF 和 ID 阶段的指令。
- 控制冒险(跳转指令):遇到跳转信号(
- 阻塞信号:
- PC 阻塞与 IF 阶段阻塞:在数据冒险发生时,需要阻塞 PC 和 IF 阶段,保持当前指令不变。
拓展:使用旁路(转发)减少阻塞
- 用一个控制单元来处理流水线的阻塞和清空
- 用一个旁路单元来检测数据冒险并发出旁路控制信号
- 在执行单元中根据旁路单元的控制信号使用对应的旁路数据
Control:
Forwarding:检测数据冒险(看是否存在冲突),发出转发信号。看是与那个寄存器冲突。
Execute:看CPU流程图,可以得知数据的来源有两类,直接得到(已经写回了的数据,直接从寄存器中得到),发生数据冒险的数据(看写到了哪个寄存器中,如果是ex2mem,则从mem转发,如果是mem2wb,就从wb转发
拓展:缩短分支延迟
- 往cpu中加入少量硬件,将分支/跳转指令的执行从EX段提前到ID段,进而把程序跳转的损失减少到一个时钟周期
Steps:
- 把跳转的判断从EX段移到ID段
- 跳转的目标地址原本是在Ex段的ALU进行计算的,故我们要在ID段添加一个加法器来计算目标地址
- 添加额外的旁路逻辑,将前面指令的执行结果旁路到ID段给分支or跳转指令使用,如果所依赖的结果还没有产生,还需要进行阻塞
1. 在 ID 段实现跳转判断
核心目标:将跳转判断逻辑(如条件跳转的比较)从 EX 段移到 ID 段。
具体实现:
- 在
InstructionDecode.scala
中:- 定义额外的逻辑单元,用于在 ID 段进行分支条件的比较判断。
- 根据分支指令的操作码(opcode)和功能码(funct3),确定需要执行的比较操作(如
==
,<
,>=
等)。 - 添加对寄存器值(
rs1
和rs2
)的读取和比较逻辑。
- 目标地址计算:
- 添加一个加法器,计算跳转目标地址。跳转目标地址为:
PC + offset
,其中offset
是指令中解析出的立即数(imm
)。 - 在 ID 段,将 PC 和立即数相加生成跳转目标地址。
- 添加一个加法器,计算跳转目标地址。跳转目标地址为:
- 在
示例代码(在
InstructionDecode.scala
中):1
2
3
4
5
6scala
复制代码
when (isBranch) {
branchTarget := currentPC + imm
branchTaken := (rs1Value === rs2Value) // Example: BEQ condition
}
如果信号的值需要通过逻辑运算生成或在后续逻辑中被赋值,则需要使用 Wire
。
2. 添加旁路逻辑
核心目标:解决跳转判断所依赖的数据可能尚未生成的问题。
具体实现:
- 在
Forwarding.scala
中,扩展旁路逻辑,使得 ALU 或存储单元的输出能够在 ID 段被立即使用。 - 判断数据依赖的来源:
- 如果依赖的数据在 EX 段,需要旁路 EX 段的 ALU 输出。
- 如果依赖的数据在 MEM 段,需要旁路 MEM 段的存储输出。
- 阻塞处理:
- 如果数据依赖尚未解决(例如还在加载数据),则插入气泡(阻塞流水线)。
- 在
示例代码(在
Forwarding.scala
中):1
2
3
4
5
6
7
8scala
复制代码
when (rs1Valid && rs1Source === EX_STAGE) {
rs1Value := exAluOutput
}.elsewhen (rs1Valid && rs1Source === MEM_STAGE) {
rs1Value := memDataOutput
}
3. 修改控制逻辑
核心目标:更新
Control.scala
中的控制信号生成逻辑,以支持分支跳转在 ID 段的实现。具体实现:
- 根据指令类型(分支/跳转),生成额外的控制信号:
branchTaken
:判断是否跳转。branchTarget
:跳转的目标地址。
- 在 ID 段判断后,如果确定分支成功,则向流水线发出控制信号清空后续指令(如插入气泡)。
- 根据指令类型(分支/跳转),生成额外的控制信号:
示例代码(在
Control.scala
中):1
2
3
4
5
6
7scala
复制代码
when (branchTaken) {
pcSrc := BRANCH_TARGET
pipelineFlush := true
}
4. 测试竞争冒险
- 核心目标:模拟和解决所有可能的竞争冒险情况。
- 指导原则:
- 测试分支跳转依赖前面指令的结果。例如:
BEQ R1, R2, offset
ADD R1, R3, R4
- 验证是否正确解决数据依赖问题。
- 测试分支跳转依赖前面指令的结果。例如:
- 测试数据冒险的关键:
- 如果冒险可通过旁路解决,则确保旁路逻辑正常工作。
- 如果旁路无法解决,则检查阻塞是否正常。
lab4:总线
- CPU除了通过内存控制器访问内存以外,还可以通过总线访问外部设备。使用总线可以减少电路布线数量以及电路设计复杂度。避免CPU与外部直接连接。(具体的硬件操作则进一步抽象为读写硬件设备上的寄存器)
在本实验中,你将学习到:
- AXI4-Lite 总线协议原理
- 使用状态机实现总线协议
总线前置知识:
AXI4-Lite总线通信协议
写地址通道:
写数据通道:
写响应通道
读地址通道:
读数据通道:
状态机:
- 总线仲裁:所有设备在通信之前都要检测总线是都占用。**每一个设备则需要增加总线请求线以及总线授权线,连接到总线仲裁器。**在设备需要通过总线传输数据前,需要先通过总线请求线请求总线的访问权限。总线仲裁器则通过总线授权线来授予访问权限,从而避免设备之间的冲突。
- 总线交换机:类似于网络交换机的方式连接。不同对的设备之间可以通过交换机同时进行通信。
读操作:
- IF发出取指信号,包括:读请求(valid)和读地址(pc),如果对应的主机处于空闲状态,则对本次读取做出响应(空闲→读请求状态),产生并发送读请求(ARVALID),读地址(ARADDR)
- 当 从机(slave)接收到读请求且处于空闲状态,则返回主机(master)读准备(ARREADY)—表示可以读取。(完成一次读地址的握手
- 从机(slave)开始准备需返回的数据(RDATA),读返回请求(RVALID),主机跳变到下一个读数据状态。
- 当主机的RVALID和RREADY完成握手,主机得到目标数据,则产生读完成的信号(RREADY),把数据返还给取值模块,就完成了一次读操作,此时主机跳转回空闲状态。
写操作:
相比读多了一个接收写回复(如图)
- 有总线后的MMIO(已给)
- 把总线加到流水线上
实验任务:
- 完成主从设备内部的状态机(按照上面给的状态图)
主从设备的状态机切换图在预备知识里面给出了,不需要自己去总结,实现所需的寄存器和模块输入输出接口已经给出,你只需要实现状态切换以及相应的握手信号。
资料:
https://blog.csdn.net/weixin_45937291/article/details/129771811
实验指导:
实现 AXI4Lite 从设备模块
- 实现状态机:
- 在每个状态中,根据相应的输入信号更新状态和输出信号。
- 例如,在
ReadAddr
状态中,等待ARVALID
信号变高,并将ARREADY
信号置高。 - 一旦地址被接受,将状态切换到
ReadData
并准备输出数据。
- 实现读写逻辑:
- 在
ReadData
状态中,等待RREADY
信号,输出RDATA
和RRESP
。 - 在
WriteAddr
状态中,等待AWVALID
信号,并将AWREADY
信号置高。 - 在
WriteData
状态中,等待WVALID
信号,并将写数据和写掩码存储到寄存器中。
- 在
- 实现响应逻辑:
- 在
WriteResp
状态中,输出写响应信号BVALID
和BRESP
。
- 在
实现 AXI4Lite 主设备模块
- 实现状态机:
- 在每个状态中,根据相应的输出信号更新状态和输入信号。
- 例如,在
ReadAddr
状态中,输出ARVALID
信号,并等待ARREADY
信号变高。 - 一旦地址被接受,将状态切换到
ReadData
并准备接收数据。
- 实现读写逻辑:
- 在
ReadData
状态中,等待RVALID
信号,接收RDATA
和RRESP
。 - 在
WriteAddr
状态中,输出AWVALID
信号,并等待AWREADY
信号变高。 - 在
WriteData
状态中,输出写数据和写掩码信号WSTRB
。
- 在
- 实现响应逻辑:
- 在
WriteResp
状态中,等待BVALID
信号,接收写响应信号BRESP
。
- 在
ps:一些chisel的知识:
- Bundle:用于将不同类型的信号划分为一组
- Vec用于表示一个可索引的、相同类型的信号的集合
- <> 运算符:用于连接两个信号,类似于 Verilog 中的
assign
语句。
实验报告¶
- 简要概括不同测试用例的功能,描述它们分别从什么层面测试 CPU,以及使用了什么方法加载测试程序指令,以及测试用例的执行结果。
- 对于填空涉及到的信号,使用测试框架输出波形图,描述在执行不同指令时候对应的部件的关键信号的变化情况。
- 使用实验板上的 LED 或者数码管等外设,体现你的 CPU 可以响应定时器中断或者其他外部中断。
- 在完成实验的过程中,遇到的关于实验指导不明确或者其他问题,或者改进的建议。