cod25-grp29 卿晨 汪馨瞳 赵睿
-
基本功能:实现支持监控程序基本版本用到的 RV32I 指令集(共19条)和额外三条指令(CLZ、PACK、MIN),实现数据前传,实现内存(SRAM)访问功能和串口读写功能
-
进阶功能:实现分支预测;实现缓存;实现其他外设的操控;实现中断处理机制;实现虚拟内存管理
将 EXE,MEM,WB 段的寄存器的值前传给 ID 段
当 ID 段的某 rs 寄存器与后面流水段的 rd 寄存器相同时,出现冲突,直接使用前传的数据,否则默认使用 RegFile 读出的数据
当 ID 段和 EXE 段出现 load-use 冲突时,必定需要暂停一个周期
ID 段
以下判断顺序不可交换
- ID 段和 EXE 段是否是 load-use 冲突,如果是则暂停 ID 段并插入气泡
- ID 段和 EXE 段是否冲突,如果是则使用 EXE 段的数据
- ID 段和 MEM 段是否冲突,如果是则使用 MEM 段的数据
- ID 段和 WB 段是否冲突,如果是则使用 WB 段的数据
且 rs1 和 rs2 可能都冲突
EXE,MEM,WB 段
需要根据指令判断需要前传的数据
- LUI:立即数
- jump 指令:pc+4
- load 指令:内存数据
- 其它:ALU 计算结果
此外,在增加缓存和中断异常后,还需要针对性地对数据前传逻辑进行修改
80000000 li t0,0x0
80000004 addi t0,t0,0x80000100
80000008 sw a0,0(t0)
8000000c .loop:
8000000c li t1,1
80000010 add t1,t1,t1
80000014 lw a0,0(t0)
80000018 add a0,a0,t1
8000001c j .loop如上汇编代码的 80000008, 8000000c 之间存在寄存器的 RAW 冲突,而 80000010 和 80000014 之间存在 load-use 冲突
使用如上代码进行仿真,等循环数次后,缓存与 btb 均能命中(IF/MEM 均能 1 周期完成)时,波形如下
可以看到一般的寄存器间的 RAW 冲突都可以被数据前传解决,并不需要暂停流水线
但 load-use 冲突无法避免的还需要暂停一个周期(此时 EXE 是 load 而 ID 是 use)
由于数据前传难以进行消融,所以选择的两个版本都是实验的早期版本
实现2位动态预测 BHT,btb size为8,采用直接映射的方法
在 IF 段,用当前 pc 在 btb 中查找,若 btb_hit=1 则将下一条 pc 设置为 btb 表项中对应值,即预测跳转地址,然后将这个值继续向 ID 段流水
在 EXE 段,做出如下判断:
-
若为 branch/jump 指令且
btb_hit = 0则写btb表项 -
若
btb_hit = 1且为 branch 指令则比较 alu 计算出的跳转目标地址与流水到 EXE 段的预测跳转地址并进行状态转移,具体转移方式如下,同时若两者不同则需要 flush 流水线
-
若
btb_hit = 1且为 jump 指令则比较 alu 计算出的跳转目标地址与流水到 EXE 段的预测跳转地址,若不同则 flush 流水线并重写表项
波形图
- btb_hit=0 需要写表项:拉高btb_we,设置w_index和btb_wpc(btb_branch_state表示是否发生跳转,branch和jump的跳转都算;btb_jump_state表示是否为jump指令)
- btb_hit=1,更新状态:拉高btb_change_state,根据btb_branch_state进行状态转移(btb_branch_state为1表示本次指令需要跳转,为0说明不跳转)。下图发生了状态00->01的转移
Test 3CCT 具有大量跳转指令
有分支预测版为最终版
无分支预测版为最终版去掉 btb
原先最简单的实现,状态有IDLE READ_1 READ_2 WRITE_1 WRITE_2 WRITE_3 DONE,四周期读,五周期写,效率低
注意到,所有SRAM所需要的信号其实都可以通过组合逻辑直接在当周期生成,所以理论上可以把Controller做成透明的(当成一个信号转换器),根据SRAM极限读写周期要求发送ack即可:
- 根据
cycstbwesel和addr设置sram_addrceoe和be,设置三态门 IDLE状态时钟上升沿检测到cyc和stb时,说明这两个信号、地址、数据已经准备好一个周期了,如果是读操作,数据恰好读出,直接采样输出并发送ack,进入ACK状态;写操作则说明目标地址已经找到,下一个周期可以进行数据计算,拉低we并进入WRITE状态WRITE状态时钟上升沿到来,数据已经计算完毕,下一个周期就可以写到SRAM里了,直接拉高ack;ACK状态再把ack拉低,回到IDLE等待下一个请求
先考虑极限情况下的周期数:当出现请求时,提供的Wishbone Arbiter需要一个周期判断优先级;两次请求之间必须暂停一个周期,否则优先级高的请求会一直占用总线,优先级低的一直不会处理;剩下就是SRAM的两周期读三周期写
于是我们选择利用暂停的那一个周期即第一个状态PREPARE进行必要的判断:
- IF:更新PC寄存器(跳转地址的计算比较复杂,防止组合逻辑过长放在开始而不是更新流水线寄存器那个时钟上升沿),判断Cache和BTB是否命中以及是否有
bubble,是的话可以直接跳过(Cache和BTB直接使用下一个PC进行读取,组合逻辑保证在同一周期即可获得),否则直接用下一个PC进行取指 - MEM:更新数据(流水线寄存器上个周期刚刚就绪)判断是否有异常、
bubble或需要访存(可以直接跳过)、是否是CSR/CLINT读写、读操作Cache命中(可以组合逻辑单周期执行完毕)
然后我们进入正常的访存请求REQUEST状态:
- 在
stall信号下可以正常进行,bubble信号则需要在请求结束后抛弃结果,因为请求中间不可取消而且bubble信号可能只持续一个周期,需要寄存器进行记录 - Wishbone返回结果后,如果正常则直接更新流水线寄存器,
bubble则舍弃,err则抛出异常
此外设置FINISH状态,用于数据已经就绪但仍在stall的情况,前两个状态将数据暂存在寄存器中,等到stall结束,如果中间没有bubble信号,更新流水线寄存器,回到PREPARE
最后还有一些需要特别考虑的点:
- IF:伴随
bubble可能有跳转地址,在请求中间需要进行记录,且优先级为第一条异常大于第一条跳转,后续的不进行记录 - MEM:为了数据前传的即时性,通过组合逻辑多路选择器把Cache、
ack同周期的数据、stall时寄存器中的数据组合进行前传
实现页表功能之后的额外逻辑,我们选择在MMU阶段的状态机进行处理,不暴露给IF或MEM阶段
类似SRAM Controller,使用大量的组合逻辑信号保证CPU和SRAM之间互传的信号都是同时就绪而不是晚一个周期,做到了完全透明(TLB命中的情况)
具体状态机如下:
IDLE:判断是否使用页表、TLB是否命中,对应进入ACCESS或WALK_L1WALK_L1:访问一级页表,ack后立即进入WALK_L0WALK_L0:访问二级页表,ack后立即进入ACCESSACCESS:访问真实物理内存
值得讨论的优化点:
- 请求开始时保存特权模式在寄存器中,防止多级页表访问中间出现改变
- 根据状态、TLB是否命中,组合逻辑直接得出访存地址,保证跟
stb和cyc同时,不会延迟周期 - 组合逻辑判断页表和TLB项是否有效和有权限,有错误当即返回Page Fault,效率高
- 如果Wishbone返回
err,根据不同状态判断异常类型(WALK是Page Fault,ACCESS是Access Fault) - TLB未命中时,多级页表访问与物理内存访问之间不会拉低
cyc和stb,这样Wishbone Arbiter会认为请求一直在进行,从而不会被另一个阶段的请求打断,保证了逻辑和实现的正确合理
由于状态机优化难以进行消融,所以选择的两个版本都是实验的早期版本
采用直接映射的连接方式,每一个 set 储存 valid|tag|data,读写方式与 RegFile 类似,组合逻辑输出 cache_hit 信号
当且仅当输入的 tag 与对应 set 中的 tag 相同,且 valid=1 时,cache_hit=1
在 IF 段,如果 cache_hit=1,则直接使用缓存数据并开始下一条指令的取指,否则发起 Wishbone 请求
此外,在 fence.i 指令时需要清空 icache
采用直接映射的连接方式
与指令缓存相比,有如下额外特性:
-
cacheability 的判断:SRAM 地址可被缓存,MMIO 地址不可被缓存
-
write-through:我们实现了 write-through,即每次进行写内存操作时,直接既写入缓存又写入内存
-
存在字节操作(LB/SB):需要考虑非对齐情况下的缓存,对此我们实现的是每一个
set为tag|valid[3:0]|byte_data[3:0]- LB 时,判断对应字节的
valid位是否为 1,如果是则cache_hit=1 - LW 时,判断对应
set的valid是否为4'b1111,如果是则cache_hit=1 - SW 时,设置
valid=4'b1111 - SB 时,设置对应字节的
valid位为 1;此外,如果输入的tag与对应set中的tag不同,则将其它字节的valid位置零
此外,为了逻辑清晰,我们的设计是不论是字节操作还是整字操作,从缓存中读出的数据可以直接使用,因此对于字节操作,写 cache 时数据在流水线进行处理,读 cache 时数据在 cache 内部处理
- LB 时,判断对应字节的
使用与数据前传部分同样的代码进行仿真
可以看到在小范围的循环中,指令缓存能够一直命中,数据缓存在读内存指令时也能够命中
Test 4MDCT 具有大量访存指令
有缓存版为最终版
无缓存版为最终版去掉指令缓存和数据缓存
实现CSR寄存器堆,内部稀疏实现了以下CSR寄存器:
- mtvec:异常处理函数地址
- mscratch:栈记录
- mepc:异常指令地址
- mcause:异常原因,下图全部实现

- mstatus:状态
- mie:中断使能
- mie:中断等待
- mtval:异常信息
- satp:页表地址
其中只有satp有单独对外暴露的接口,其他都只通过MEM阶段读写对外暴露,异常处理逻辑都在内部进行:
- 根据mstatus、特权模式和mip mie对应位判断是否有中断,输出到WB阶段
- 根据WB阶段提供的信息,设置相应的寄存器(mret指令则是恢复),输出异常跳转地址到WB阶段
CSR寄存器读写在MEM阶段,ID阶段会根据特权模式和寄存器读写权限判断CSR读写指令是否合法,MEM阶段的写入会根据WARL、WPRI进行mask处理
CPU各个阶段处理异常中断的逻辑:
- 各个阶段检测自己阶段出现的异常,异常信号跟流水线一起流动,遇到异常信号则跳过当前阶段并发送
flush - MEM阶段进行CSR读写指令
- WB阶段进行异常处理:如果有异常或中断信号,根据异常大于中断的优先级进行处理,把PC、指令、原因等信息提供给CSR寄存器堆,接收异常跳转地址并提供给IF同时
flush - 对于CSR读写和Store指令,延迟处理中断直到当前指令退休,避免可能出现的无限中断和重复写入等问题
同时,特权模式的维护也在CSR寄存器堆内进行,暴露给MMU和ID阶段进行相关判断
MEM阶段外接一个CLINT设备,内部实现两个MMIO寄存器mtime和mtimecmp:
- 把0x02000000左右的地址,映射到CLINT设备,MEM阶段遇到后不通过Wishbone访存,直接组合逻辑读取,单周期写入,效率更高
- 由于两个都是64位寄存器,CLINT内部增加地址和字节使能判断,以保证写入和读取的内容正确
- 不写入mtime时,每个时钟周期mtime + 1(可根据不同中断时间要求自行设置)
- 通过组合逻辑硬件连线,mtime >= mtimecmp时设置mip对应位置为1
根据 satp 这个 csr 寄存器以及当前的 privilege_mode,判断是否使用页表
如果使用页表,MMU 实现为了 Page Table Walker,完成以下功能:
- 从 Wishbone 总线上读取虚拟地址访存请求
- 按照 Privileged Spec,访问页表,检查权限,翻译地址
- 使用翻译而来的物理地址发起真正的 Wishbone 请求
- 将内存数据返回请求方
这样实现能够对流水线透明,流水线仍然只需要发送 Wishbone 信号,只是中间加了一层 MMU 进行地址转换
与指令缓存实现类似,与之相比增加了权限位,以及 sfence.vma 需要清空 TLB
我们实现了 VIVT,保持了对流水线透明,且在当前监控程序下并不存在别名和歧义的问题
仿真运行监控版本 3 并通过串口让其运行 UTEST_1PTB
可以看到在切换到用户态的瞬间 paging_en 变为 1,即开启页表,此后能够正确翻译地址
一条指令后 tlb 能够 hit
Test 4MDCT 具有大量访存指令
运行监控版本 3,前者为最终版去掉 TLB,后者为最终版
实现了VGA,具体实现方法如下:
-
新增IP核BRAM,设定为Simple Dual Port RAM,开启字节使能,WIDTH设置为8(r3g3b2的rgb编码方式),DEPTH设置为30000(对应储存200*150的像素图像),mode为No Change,对应BRAM波形如下
-
实现BRAM controller,实现Wishbone MUX (Masters) -> BRAM(bus slaves) port A的逻辑
-
VGA从 BRAM port B 读数据,VGA模块输出的横纵坐标hdata和vdata到读取地址的映射为读取地址
fb_b_addr = vdata / SCALE * FB_WIDTH + hdata / SCALE其中SCALE=4,FB_WIDTH=200 -
thinpad_top文件中实现接线:设定外设的起始地址为 0x2000_0000 ,运行
wb_mux.py生成wb_mux_4.v并把 BRAM 通过 BRAM controller 接入 wishbone -
相关文件实现:
- 实现了
video2bin.py文件,将视频转换为200*150分辨率、r3g3b2的原始二进制数据流,按照帧的播放顺序写入二进制文件 - 实现了汇编代码
video_loop.s,可以从 0x80400000(对应EXTRAM)开始的地址按帧写入 0x20000000 开始的一片地址(对应BRAM地址) - 实验时可以先把
video2bin.py输出的二进制文件写入 EXTRAM,把汇编代码编译出的二进制文件写入 BaseRAM 播放视频
- 实现了
可以实现视频播放
合作愉快!累但很有收获
- 在实现了数据前传与分支预测后发现会
ERROR: timeout during RunD,通过仿真观察波形图发现是数据前传遇到 jump 指令时前传了 alu 的计算结果(jump的目标地址)而非应该写入 rd 的 pc+4 ,导致读串口结束时的返回值错误,从而进入死循环。通过修改 jump 指令数据前传的值解决了该问题。 - 在实现分支预测、中断异常和状态机优化后 merge 代码出现问题
ERROR: timeout during WaitBoot,通过仿真观察波形图发现是trap_pc、跳转回传pc和btb_hit出的pc优先级设置有误,修改后解决了该问题。
- 流水线 CPU 设计与多周期 CPU 设计的异同?插入等待周期(气泡)和数据旁路在处理数据冲突的性能上有什么差异。
相同点:
- 基本组件都一样:ALU,MMU,寄存器堆,Cache等
- 单个指令执行顺序一样(分成五个阶段)
- 指令都按程序顺序执行
不同点:
- 多周期CPU同一时间只执行一条指令,流水线CPU在同一时间至多有五条指令在执行
- 流水线CPU不同阶段间需要流水线寄存器,多周期则是一个大型状态机
- 多周期CPU的控制信号在IF阶段取出指令后立即生成,随状态机生效;流水线CPU则是在ID阶段生成控制信号并随流水线流动
- 多周期CPU可以进行部件复用(比如MMU),而流水线不行(必须在IF和MEM阶段各有一个)
- 流水线CPU会产生数据冲突、结构冲突和控制冲突(用数据前传、Arbiter和
stallflush控制信号解决),多周期则没有 - 效率上来看,多周期CPU的时钟周期可能较慢(组合逻辑路径长),流水线可能相对较快,但流水线的整体性能受最慢的阶段的影响,中间可能有很多
bubble - 流水线在遇到跳转或者异常时,可能会出现加载错误指令、中断延迟处理等控制问题,多周期不会执行错误指令,可以即时响应中断
性能差异:
- 绝大多数数据冲突可以直接用数据旁路解决,不需要插入气泡并且让流水线暂停在ID阶段
- 对于Load-Use数据冲突,数据旁路也需要等待访存结束才能获得数据,相对性能差距没那么大,但还是可以节省两个周期
- 使用数据旁路时流水线可以时刻接近满负荷状态,硬件利用率更高,插入气泡则会使一些流水线部件处于空闲状态
- 中断响应不能在气泡里进行,多余的气泡会增加中断响应的延迟
- 数据旁路会增加CPU的组合逻辑路径长度,使得延迟增大,时钟频率略低
- 总体来说,数据旁路在处理数据冲突的性能上显著优于插入气泡
- 如何使用 Flash 作为外存,如果要求 CPU 在启动时,能够将存放在 Flash 上固定位置的监控程序读入内存,CPU 应当做什么样的改动?
使用 Flash 作为外存:
- 实现 Flash controller,达成 Wishbone MUX (Masters) -> Flash(bus slaves) 的功能
- thinpad_top文件中实现接线:设定 Flash 的基址,并使其通过 controller 接入 wishbone
CPU 的改动:
- 实现一个小容量的额外显存,比如BRAM
- 实现这个额外显存的控制器,达成 Wishbone MUX (Masters) -> 显存(bus slaves) 的功能
- thinpad_top文件中实现接线:设定这个额外显存的基址,并使其通过 controller 接入 wishbone
- 在这个额外显存中预先写入一段二进制文件,其对应的汇编代码为从 Flash 起始位置读入监控程序然后写入内存中,最后跳转到内存监控程序起始地址
- 设置初始 pc 为这个额外显存的起始地址
- 如何将 DVI 作为系统的输出设备,从而在屏幕上显示文字?
- 实现额外的显存,例如 BRAM
- 实现这个额外显存的控制器,达成 Wishbone MUX (Masters) -> 显存(bus slaves) 的功能
- 实现 DVI 读显存的功能
- thinpad_top文件中实现接线:设定外设的基址,并使其通过 controller 接入 wishbone
- (分支预测)对于性能测试中的 3CCT 测例,计算一下你设计的分支预测在理论上的准确率和性能提升效果,和实际测试结果对比一下是否相符。
UTEST_3CCT:
lui t0, %hi(TESTLOOP64)
.LC2_0:
bne t0, zero, .LC2_1
jr ra
.LC2_1:
j .LC2_2
.LC2_2:
addi t0, t0, -1
j .LC2_0
addi t0, t0, -1在有数据前传、有cache、有流水线优化、50M时钟的情况下进行分析。记 .LC2_0 -> .LC2_1 -> .LC2_2 为一次循环。分支预测的实现方式为2位动态预测,预测初始状态为第一次跳转状态(若第一次不跳转则初始状态00,跳转则初始状态11)。
没有分支预测时:
bne指令、j指令执行到 EXE 段后都需要 flush 冲刷流水线,每个周期有四条指令,其中有三条跳转指令,需要 flush 三次流水线。在我们的流水线优化方法中,flush 时 EXE 阶段回传跳转目标地址,IF 段会使用这个地址在 icache 中查找,若 icache_hit 可以减少一个 bubble 直接用 ichache hit 出的值。又在有数据前传的情况下不会出现数据冲突,有cache时每个指令在每个流水段只存在一个时钟周期,因此每次循环需要7个时钟周期(4个指令+3个bubble),没有分支预测时的总时间约为
有分支预测时:
在循环中遇到bne指令和j指令时,仅在第一次循环 btb 表中没有时和最后一次循环跳转方式改变时需冲刷流水线,其他时候均会 btb_hit ,因此理论上准确率约为100%。一个周期执行4条指令,在有数据前传的情况下不会出现数据冲突,又因为有cache,每个指令在每个流水段只存在一个时钟周期,因此有分支预测时的时间约为
实验结果:
- 无 btb:9.4s
- 有 btb:5.4s
- (缓存)对于性能测试中的 4MDCT 测例,计算一下你设计的缓存在理论上的命中率和性能提升效果,和实际测试结果对比一下是否相符。
理论分析
以下仅考虑循环主体的 6 条指令
.loop:
sw t0,0(sp)
lw t1,0(sp)
addi t1,t1,-1
sw t1,0(sp)
lw t0,0(sp)
bnez t0,.loop
命中率:
- 指令缓存:除了第一次循环以外都能命中,即命中率接近 100%
- 数据缓存:每次 lw 都能命中,即命中率 100%
性能提升:
-
有缓存:
- 先假设每条指令都只需要 1 个周期
- 读内存(取指、
lw)缓存命中,不需要额外周期;写内存(sw)需要 5 个周期(1 周期拉高 stb&cyc,1 周期 arbiter 仲裁,3 周期写 sram),即额外的 4 个周期;但是第一个sw写入的是上次循环读出来的值,并没有更改,因此并不会访存,不需要额外周期 - 数据冲突:
addi与lw、bnez与lw有 load-use 冲突,因此各需要暂停 1 个周期;其余均可通过数据前传解决 - 控制冲突:分支预测可看作都成功
总之,一次循环需要 12 个周期,共 32M 次循环,在 50M 的时钟下大约需要
$\frac{12*32M}{50M}=7.68s$ -
无缓存:
- 读内存(取指、
lw)需要 4 个周期,写内存(sw)需要 5 个周期;但是当 IF 和 MEM 段同时存在请求时,会覆盖掉一个准备的周期 - 访存会带来很多气泡,因此不存在数据冲突
总之,一次循环需要 38 个周期,约
$\frac{38*32M}{50M}=24.32s$ - 读内存(取指、
实验结果
有缓存:$8.1s$
无缓存:$25.5s$
- (虚拟内存)考虑支持虚拟内存的监控程序。如果要初始化完成后用 G 命令运行起始物理地址 0x80100000 处的用户程序,可以输入哪些地址?分别描述一下输入这些地址时的地址翻译流程。
地址:0x0 或 0x80100000
0x0:
-
根据
satp中的一级页表基地址以及虚拟地址0x0的VPN[1]算出一个地址,此地址处存放的数据为零级页表PAGE_TABLE_USER_CODE的基地址 -
根据虚拟地址
0x0的VPN[0]找到零级页表PAGE_TABLE_USER_CODE中对应的页表项 -
将页表项中的
PPN与虚拟地址0x0的VPO组成对应的物理地址0x80100000
0x80100000:
-
根据
satp中的一级页表基地址以及虚拟地址0x80100000的VPN[1]算出一个地址,此地址处存放的数据为零级页表PAGE_TABLE_KERNEL_CODE的基地址 -
根据虚拟地址
0x80100000的VPN[0]找到零级页表PAGE_TABLE_KERNEL_CODE中对应的页表项 -
将页表项中的
PPN与虚拟地址0x80100000的VPO组成对应的物理地址0x80100000
- (异常与中断)假设第 a 个周期在 ID 阶段发生了 Illegal Instruction 异常,你的 CPU 会在周期 b 从中断处理函数的入口开始取指令执行,在你的设计中,b - a 的值为?
- 发生异常后,ID阶段会用组合逻辑立即抛出
flush信号,此时IF阶段刚取出这一条指令,这个周期还在PREPARE阶段,收到bubble不会发送Wishbone请求而是直接跳过 - 此后异常信号会随着流水线向前流动,每个阶段看到异常信号都会发送
flush,这会导致IF一直在PREPARE循环 - 到WB阶段进行异常处理,组合逻辑读取CSR寄存器,取出
mtvec的值作为跳转地址,随bubble一并传给IF - 下个周期IF直接在异常处理函数入口地址进入正常执行流程
- 因为异常信号不能前传(前面的指令需要正常执行并退休),所以MEM如果有需要访存的操作,读写会对应增加三、四周期的
stall - 综上所述,正常情况下 b - a = 4,特殊情况下可能为7或8
卿晨:负责数据前传、缓存icache dcache、虚存mmu和tlb的设计与实现,实验报告撰写
汪馨瞳:负责所有指令的编写、数据前传、分支预测、外设的设计与实现,实验报告撰写
赵睿:负责lab5的初始流水线编写、状态机优化、中断异常的设计与实现,实验报告撰写
debug由所有人共同完成


















