前端模块验证文档
Frontend
- 1: IFU
- 1.1: F3PreDecoder
- 1.2: FrontendTrigger
- 1.3: PredChecker
- 1.4: PreDecode
- 1.5: RVCExpander
- 2: ITLB
1 - IFU
IFU简介
IFU(Instruction Fetch Unit),取指令单元,负责从内存或ICache取出指令,经过预译码、扩展RVC和预检之后,将指令交给后续译码器进行进一步的译码。
IFU的子模块包括PreDecode,F3PreDecoder,RVCExpander,PredChecker和FrontendTrigger。
以下是IFU的架构简图:
IFU功能介绍
XS_IFU流水级划分
香山的IFU一共分为5个阶段。
F0阶段:接收FTQ请求,同时告诉FTQ自己已经ready了
F1阶段:从FTQ请求中先计算出每个指令的pc,half_pc、cut_ptr(这是后续将icache返回的指令码进行切分的依据)
F2阶段:从icache获取响应数据(缓存行)并校验,提取出异常信息(包括页错误、访问错误、mmio信息);生成预测到的指令范围(但这并不是一个数字,而是一个用多位表示的bool数组,该位为1表示这一指令在预测块范围内);从缓存行中,利用上一阶段求出的cut_ptr切分出17×2的初步指令码,最后进行预译码和指令扩展。
F3阶段:这一阶段主要是对译码阶段的结果进行预检查,以及MMIO状态下的处理逻辑。
WB(写回)阶段:将预检查的结果写回FTQ,并向IBuffer写指令码和前端信息。
接收FTQ取指令请求(F0流水级)
在F0流水级,IFU接收来自FTQ以预测块为单位的取指令请求。请求内容包括预测块起始地址、起始地址所在cache line的下一个cache line开始地址、下一个预测块的起始地址、该预测块在FTQ里的队列指针、该预测块有无taken的CFI指令(控制流指令)和该taken的CFI指令在预测块里的位置以及请求控制信号(请求是否有效和IFU是否ready)。每个预测块最多包含32字节指令码,最多为16条指令。IFU需要置位ready驱动FTQ向ICache发送请求。
指令切分产生初始指令码(F1、F2流水级)
F0流水级时,FTQ同时会向ICache发送取缓存行的指令。这是ICache在其S2流水级需要返回的,所以IFU在F2流水级才会得到ICache返回的缓存行。在此之前,IFU会在F1流水线先进行PC的计算,以及计算切分缓存行的指针。
进入F2流水级,IFU将会针对每个指令,取出对应的异常信息、物理地址、客户物理地址等。同时,根据FTQ的taken信息,IFU将会计算该预测块在无跳转和跳转发生情况下的有效指令范围。无跳转情况下的指令有效范围ftr_range即当前预测块从起始地址到下一个预测块的起始地址的差值。有跳转情况下的指令有效范围jump_range即当前预测块的起始地址到预测块中第一个跳转指令地址的差值。
最后,IFU需要从缓存行和上一流水级计算的指针,完成对17x2字节初始指令码的拼接。这里的拼接代码可能存在一些迷惑性
val f2_cache_response_data = fromICache.map(_.bits.data)
val f2_data_2_cacheline = Cat(f2_cache_response_data(0), f2_cache_response_data(0))
在调用cut之前,我们先是从ICache获取了缓存行(ICache返回的缓存行种类已经在ICache中进行了分类讨论,IFU中直接使用即可),然后将第0个缓存数据进行了拼接, 这一操作的原因来自于ICache中对数据的细粒度拆分:
fetch block可能跨缓存行,但是由于fetch block最大只有34B,如果将两个缓存行(2x64B)都传送给IFU则显得浪费,因此,fetch block的选择由ICache完成。
ICache返回给IFU的并不是直接的预测块,而是带有跨缓存行信息的64字节。
我们将每个缓存行分为8份,如下所示:
cacheline 0: |0-7|0-6|0-5|0-4|0-3|0-2|0-1|0-0|
cacheline 1: |1-7|1-6|1-5|1-4|1-3|1-2|1-1|1-0|
如果fetch block的起始位置为0-1,则必定不跨缓存行。
如果fetch block的起始位置为0-6,那么fetch block的位置为0-6~1-2,此时传送的缓存行结构如下:
cacheline 0: |0-7|0-6|xx|xx|xx|1-2|1-1|1-0|
由此,只要将该缓存行复制一遍,即可获得拼接后的fetch block。
综上所述,对这两种情况,我们都只需要把返回的cacheline复制一份拼接在一起,从中间截取就可以拿到数据。
预译码(F2流水级,主要由PreDecode模块完成)
在F2流水级,我们需要将上一步完成切分的指令码交给PreDecode子模块,他的作用主要有二:
其一是生成预译码信息,包括该指令是否是有效指令的开始、是否是RVC指令、是否是CFI指令、CFI指令类型(branch/jal/jalr/call/ret)、CFI指令的目标地址计算偏移等。输出的预译码信息中brType域的编码如下:
CFI指令类型 | brType类型编码 |
---|---|
非CFI | 00 |
branch指令 | 01 |
jal指令 | 10 |
jalr指令 | 11 |
其二是将初始指令码两两组合之后,得到16x4字节的指令码(从起始地址开始,2字节做地址递增,地址开始的4字节作为一条32位初始指令码)。
此外,预译码阶段还需要分类讨论,得出两种指令有效向量(起始指令是不是RVI指令的后半部分),并交给IFU进行判断,可以参阅后面的跨预测块32位指令处理部分
其他功能和详细内容参见PreDecode子模块的描述。
指令扩展(F3流水级)
这一部分将从PreDecode返回的16条指令码分别送交指令扩展器进行32位指令扩展(RVI保持不变, RVC指令根据手册的规定进行扩充)。
但是,如果指令非法,需要向IBuffer写入原始指令码。
预测错误预检查(F3流水级,主要由PreChecker子模块完成)
这一功能是为了将一些不依赖于执行结果的预测错误在早期就发现出来。这一阶段检查五类错误:
jal类型错误:预测块的范围内有jal指令,但是预测器没有对这条指令预测跳转;
ret类型错误:预测块的范围内有ret指令,但是预测器没有对这条指令预测跳转;
无效指令预测错误:预测器对一条无效的指令(不在预测块范围/是一条32位指令中间)进行了预测;
非CFI指令预测错误:预测器对一条有效但是不是CFI的指令进行了预测;
转移目标地址错误:预测器给出的转移目标地址不正确。
在预检查的最后将会修正之前预测的各个指令的跳转情况。同时,如果存在jal或者ret类型预测错误,还将修正fixedRange——这是指令有效范围向量,可以看作一个bool数组,其中某一位为1也就是对应的指令在这一范围内。
前端重定向(WB阶段)
如果在预测错误预检查的部分发现了上述的五类错误,那么需要在写回阶段产生一个前端重定向将F3以外的流水级进行冲刷, 从而让BPU能够从正确路径重新开始预测。
还有一种情况下需要冲刷流水线。在下一节中,如果误判了当前预测块的最后2B为RVI指令的上半部分,则也需要冲刷当前预测块F3之前的流水级。
跨预测块32位指令处理
因为预测块的长度有限制,因此存在一条RVI指令前后两字节分别在两个预测块的情况。IFU首先在第一个预测块里检查最后2字节是不是一条RVI指令的开始,如果是并且该预测块没有跳转,那么就设置一个标识寄存器f3_lastHalf_valid,告诉接下来的预测块含有后半条指令。在F2预译码时,会产生两种不同的指令有效向量:
预测块起始地址开始即为一条指令的开始,以这种方式根据后续指令是RVC还是RVI产生指令有效向量
预测块起始地址是一条RVI指令的中间,以起始地址 + 2位一条指令的开始产生有效向量
在F3,根据是否有跨预测块RVI标识来决定选用哪种作为最终的指令有效向量,如果f3_lastHalf_valid为高则选择后一种(即这个预测块第一个2字节不是指令的开始)。IFU所做的处理只是把这条指令算在第一个预测块里,而把第二个预测块的起始地址位置的2字节通过改变指令有效向量来无效掉。
将指令码和前端信息送入IBuffer(F3流水级)
F3流水级最终得到经过扩展的32位指令码(或者对于非法指令直接传递原始指令码),以及16条指令中每条指令的例外信息、 预译码信息、FTQ队列中的指针位置、其他后端需要的信息(比如经过折叠的PC)等。IFU除了常规的valid-ready控制信号外, 还会给IBuffer两个特殊的信号:一个是16位的io_toIbuffer_bits_valid(因为我们最后组合出来的指令也是16条, 所以这里每一位刚好也对应一个指令的状态,为1说明是一条指令的开始,为0则是说明是一条指令的中间),标识预测块里有效的指令。 另一个是16位的io_toIbuffer_bits_enqEnable,这个在io_toIbuffer_bits_valid的基础上与上了被修正过的预测块的指令范围fixedRange。 enqEnable为1表示这个2字节指令码是一条指令的开始且在预测块表示的指令范围内。
除此之外,异常信息也需要写给IBuffer。
注意一个特例:当且仅当发生guest page fault时,后端需要gpaddr信息,为了节省面积,gpaddr不走正常通路进入ibuffer, 而是随ftqPtr被发送到gpaMem,后端需要时从gpaMem读出。IFU需要保证gpf发生时通向gpaMem的valid拉高、gpaddr正确。
分支预测overriding冲刷流水线
当FTQ内未缓存足够预测块时,IFU可能直接使用简单分支预测器提供的预测地址进行取指,这种情况下,当精确预测器发现简单预测器错误时,需要通知IFU取消正在进行的取指请求。具体而言,当BPU的S2流水级发现错误时,需要冲刷IFU的F0流水级;当BPU的S3流水级发现错误时,需要冲刷IFU的F0/F1流水级(BPU的简单预测器在S1给出结果,最晚在S3进行overriding,因此IFU的F2/F3流水级一定是最好的预测,不需要冲刷;类似地,不存在BPU S2到IFU F1的冲刷)。
IFU在收到BPU发送的冲刷请求时,会将F0/F1流水级上取指请求的指针与BPU发送的冲刷请求的指针进行比较,若冲刷的指针在取指的指针之前,说明当前取指请求在错误的执行路径上,需要进行流水线冲刷;反之,IFU可以忽略BPU发送的这一冲刷请求。此外,比较的时候还需要注意flag的情况,flag是一个指示队列循环的指针,flag不同即在不同的“圈”上,此时反而是idx的值更小,ftqIdx才会更大。
指令信息和误预测信息写回FTQ(WB阶段)
在F3的下一级WB级,IFU将指令PC、预译码信息、错误预测指令的位置、正确的跳转地址以及预测块的正确指令范围等信息写回FTQ,同时传递该预测块的FTQ指针用以区分不同请求。
同时,正如前面提到的,IFU检测到预测错误时会进行前端冲刷,同样地,FTQ也需要据此进行冲刷,因此,这也是IFU写回错误信息的意义——可以辅助FTQ判断是否冲刷流水线。
MMIO处理逻辑
在处理器上电复位时,内存还没有准备好,此时需要从Flash中取指令执行。 这种情况下需要IFU向MMIO总线发送宽度为64位的请求从flash地址空间取指令执行。同时IFU禁止对MMIO总线的推测执行,即IFU需要等到每一条指令执行完成得到准确的下一条指令地址之后才继续向总线发送请求。
这之后,根据FTQ中的指令地址,决定是否MMIO取指令。
以上是MMIO状态机的简图。在一开始,处于m_idle状态,如果处于mmio请求的场景,则转换到 m_waitLastCmt,之后只要之前所有的指令都已完成提交——或者这一指令就是第一条指令, 则进入m_sendReq状态将请求发送到InstrUncache模块,发送完成后进入m_waitResp状态。 接收请求后,由于MMIO总线的带宽限制为64位, 因此存在一条指令一次MMIO请求不能取得完整指令码的情况(这是由于MMIO并不支持非对齐访问,具体地说,如果RVI指令的起始地址的[2, 1]两位为b11,则64位总线无法一次传递所有指令),所以需要增加地址进行重发,进入m_sendTLB状态, 再次查询ITLB,如果tlb的pbmt状态和上一条的存在差别,则为访问异常,综合TLB自身的异常结果和根据pbmt判定的访问异常,如果存在异常,则直接把指令和异常信息发到Ibuffer,进入等待,否则进入m_sendPMP状态,向PMP发送请求, 这里需要查看pmp_recheck的结果,如果该请求的mmio状态和上一条的mmio状态不一致,那么说明可能存在访问错误,置为访问异常,否则根据PMP的回复结果决定是否存在PMP异常。 如果存在异常,则将报错信息发送给Ibuffer,直接进入等待。 一切正常的话,进入m_resendReq状态,重新发送请求到InstrUncache模块。 无论是否重发,最后获得完整数据之后,根据地址从64位数据中截取指令码。并以每个预测块一条指令的形式(相当于只有起始地址的指令)发送到IBuffer。 接下来进入m_waitCommit状态等待,直到ROB返回指令已提交的信号即进入m_commited状态,CFI指令由后端发送给FTQ进行冲刷,而顺序指令则由IFU复用前端重定向通路刷新流水线, 同时复用FTQ写回机制,把它当作一条错误预测的指令进行冲刷,重定向到该指令地址 + 2或者+4(根据这条指令是RVI还是RVC选择)
对于跨缓存行预测块,他们的mmio和pbmt状态应当等同。不匹配的错误应当只在后一个缓存行报告。
此外,如果当前pbmt为nc,则会跳过waitLastCmt和waitCommit状态。因为这些内存空间是幂等的,所以可以进行推测性取指。
Trigger实现对于PC的硬件断点功能
该工作主要由FrontEndTrigger子模块完成。
IFU接口说明
为方便测试开展,需要对IFU的接口进行进一步的说明,以明确各个接口的含义。
FTQ交互接口
编译后可用的接口包括:
req FTQ取指请求
req是FTQ向IFU的取指令请求,编译后包含以下成员:
接口名 | 解释 |
---|---|
ftqIdx | 指示当前预测块在FTQ中的位置。 |
ftqOffset | 指示预测块的大小 |
startAddr | 当前预测块的起始地址。 |
nextlineStart | 起始地址所在cacheline的下一个cacheline的开始地址。 |
nextStartAddr | 下一个预测块的起始地址 |
redirect FTQ重定向请求
FTQ会向IFU发送重定向请求,这通过fromFtq.redirect完成,从而指示IFU应该冲刷的内容。
编译后,redirect包含以下接口成员:
接口名 | 解释 |
---|---|
ftqIdx | 需要冲刷的ftq预测块序号,包含flag和value两个量。 |
level | 重定向等级 |
ftq_offset | ftq预测块中跳转指令的位置 |
此外,还有valid变量指示是否需要重定向。
fromBPUFlush
来自IFU的冲刷请求,这是预测错误引起的,包括s3和s2两个同构成员,指示是否在BPU的s3和s2流水级发现了问题,s3的详细结构如下
接口名 | 解释 |
---|---|
valid | 是否存在s3流水级冲刷要求 |
ftqIdx | s3流水级请求冲刷的预测块的指针 |
toFtq_pdWb 写回
接口名 | 解释 |
---|---|
cfioffset | 经由PredChecker修复的跳转指令的预测位置。但经过编译后,cfioffset的数值已经被优化了,只剩下了cfioffset_valid表示是否存在编译优化。 |
ftqIdx | 表明预测块在FTQ中的位置,这条信息主要是对FTQ有用,要和FTQ传入的请求保持一致。 |
instrRange | 可以看作是一个bool数组,表示该条指令是不是在这个预测块的有效指令范围内(第一条有效跳转指令之后的指令)。 |
jalTarget | 表明该预测块跳转指令的跳转目标。 |
misOffset | 表明错误预测的指令在预测块中的位置。 |
pc | 预测块中所有指令的PC指针。 |
pd | 每条指令的预测信息,包括CFI指令的类型、isCall、isRet和isRVC。 |
target | 该预测块最后一条指令的下一条指令的pc。 |
ICache交互接口
控制信号
接口名 | 解释 |
---|---|
icache_ready | ICache通知IFU自己已经准备好了,可以发送缓存行了。 |
icache_stop | IFU在F3流水级之前出现了问题,通知ICache停下。 |
ICacheInter.resp ICache传送给IFU的信息
接口名 | 解释 |
---|---|
data | ICache传送的缓存行。 |
doubleLine | 指示ICache传来的预测块是否跨缓存行。 |
exception | ICache向IFU报告每个缓存行上的异常情况,方便ICache生成每个指令的异常向量。 |
backendException | ICache向IFU报告后端是否存在异常 |
gpaddr | 客户页地址 |
isForVSnonLeafPTE | 是否为非叶的PTE,这个数据最终会流向写回gpaddrMem的信号 |
itlb_pbmt | ITLB基于客户页的内存类型,对MMIO状态有用 |
paddr | 指令块的起始物理地址 |
vaddr | 指令块起始虚拟地址、指令块起始物理地址 |
pmp_mmio | 指示当前指令块是否在MMIO空间 |
性能相关接口
ICachePerf和perf,可以先不关注。
ITLBInter
该接口仅在MMIO状态下,IFU重发请求时活跃。
req IFU向ITLB发送的请求
这是IFU向ITLB发送的查询请求,只有一个量:bits_vaddr,传递需要让ITLB查询的虚拟地址。
resp ITLB返回给IFU的查询结果
这是ITLB返回给IFU的查询结果,包含如下接口:
接口名 | 解释 |
---|---|
excp | 指令的异常信息,包含三个量:访问异常指令af_instr、客户页错误指令gpf_instr、页错误指令pf_instr |
gpaddr | 客户页地址 |
isForVSnonLeafPTE | 指示传入的是否是非叶PTE |
paddr | 指令物理地址 |
pbmt | 指令的基于页的内存类型 |
UncacheInter
该接口在MMIO状态下活跃,负责接收IFU并返回指令码。
toUncache
这是IFU向Uncache发送的请求,除了ready和valid以外,还传送了一个48位数据,即需要获取的指令的物理地址。
fromUncache
这是Uncache给IFU的回复,除了valid以外,还传送一个32位数据,即指令码(可为RVC或RVI指令)
toIbuffer
IFU通过这个接口向Ibuffer写入取指结果。包含以下成员:
接口名 | 解释 |
---|---|
backendException | 是否存在后端异常 |
crossPageIPFFix | 表示跨页异常向量 |
valid | 和一般意义上的valid相区别,表示每条指令是否是合法指令的开始(RVI指令的上半条或者RVC指令) |
enqable | 对每条指令,其为valid并且在预测块的范围内 |
exceptionType | 每个指令的异常类型 |
foldpc | 压缩过后的pc |
ftqOffset | 指令是否在预测块范围中 |
ftqPtr | ftq预测块在FTQ的位置 |
illegalInstr | 这条指令是否为非法指令 |
instrs | 拼接后的指令码 |
isLastInFtqEntry | 判断该指令是否为这个预测块中最后一条有效指令的开始 |
pd | 指令控制信息,包括CFI指令的类型和RVC指令的判定 |
triggered | 指令是否触发前端的trigger |
toBackend_gpaddrMem
这组接口在gpfault发生时使用,由IFU向gpaddrMem传递预测块指针和页错误地址。
接口名 | 解释 |
---|---|
waddr | 传递ftq指针 |
wdata.gpaddr | 传递出错的客户页地址 |
wdata.isForVSnonLeafPTE | 指示是否为非叶PTE |
wen | 类似valid,指示gpaddrMem存在gpfault需要处理 |
io_csr_fsIsOff
指示是否使能了fs.CSR,对非法指令的判断很关键。
rob_commits 来自ROB的提交信息
共分为8个相同结构的rob_commit,包含以下成员
接口名 | 解释 |
---|---|
ftqIdx | 预测块指针 |
ftqOffset | 预测块的大小 |
pmp
和物理内存保护相关,在mmio状态下重发请求时使用。
req
IFU向pmp发起的请求,传递前一步从ITLB查询得到的物理地址。
resp
PMP给IFU的回复结果,包含以下成员
接口名 | 解释 |
---|---|
mmio | 在MMIO空间 |
instr | 对指令的判断结果,当指令不可执行时,该值为true |
mmio_commits
mmioFtqPtr
IFU传递给FTQ的idx,用于查询上一个预测块的MMIO状态
mmioLastCommit
上一个请求是MMIO请求
frontendTrigger
用于设置前端断点
包含以下成员:
debugMode
debug的模式
triggerCanRaiseBpExp
trigger是否可以引起断点异常
tEnableVec
信号数组,表示是否使能对应的trigger
tupdate
表示更新的断点信息,其中包含tdata和addr,addr是请求设置的断点idx。
tdata包括下列成员:
接口名 | 解释 |
---|---|
matchType | 断点匹配类型,等于、大于、小于 |
action | 触发执行的动作 |
tdata2 | 触发端点的基准数值 |
select | 是否选择 |
chain | 是否传导 |
IFU功能点和测试点
功能点1 接收FTQ预测块请求
功能点1.1 F0流水级接收请求
向FTQ报告自己已ready。
所以,我们只需要在发送请求后检查和ftq相关的的ready情况即可。
序号 | 名称 | 描述 |
---|---|---|
1 | ready置位 | IFU接收FTQ请求后,设置ready |
功能点2 指令切分产生初始指令码
功能点2.1 F1流水级计算信息和切分指针
F1流水级也会计算PC。
同时还需要生成17位的切分指针(也就是从拼接后的缓存行切出初始指令码的idx数组,在昆明湖架构中,计算方式为拼接00和startAddr[5:1], 然后分别与0~16相加) 用于后续从缓存行提取初始指令码。
所以,首先我们需要检查F1流水级生成的PC的正确与否。如果可能,也需要检查一下切分指针的生成。
所以,可以总结出以下的测试点:
序号 | 名称 | 描述 |
---|---|---|
2.1.1 | PC生成 | IFU接收FTQ请求后,在F1流水级生成PC |
2.1.2 | 切取指针生成 | IFU接收FTQ请求后,在F1流水级生成后续切取缓存行的指针 |
功能点2.2 F2流水级获取指令信息
包括获取异常信息、物理地址、客户物理地址、是否在MMIO空间等。
获取异常信息之后,还需要计算异常向量。ICache会为每个缓存行返回一个异常类型,只需要计算每个指令pc属于哪个缓存行, 然后将对应缓存行的异常类型赋给该位置即可。
所以,只需要分别检查几种指令信息即可。
序号 | 名称 | 描述 |
---|---|---|
2.2.1 | 异常向量生成 | IFU接收ICache内容后,会根据ICache的结果生成属于每个指令的异常向量 |
2.2.2 | 物理地址提取 | IFU接收ICache内容后,会根据ICache的结果生成属于每个端口的物理地址。 |
2.2.3 | 客户物理地址提取 | IFU接收ICache内容后,会根据ICache的结果生成0号端口的客户物理地址。 |
2.2.4 | MMIO空间信息提取 | IFU接收ICache内容后,会根据ICache的结果判断当前取指请求是否属于MMIO空间。 |
功能点2.3 F2流水级计算预测块有效指令范围
指令有效范围包括两种,无跳转和有跳转的
无跳转的指令有效范围为当前预测块从起始地址到下一个预测块的起始地址的所有指令。
有跳转的指令有效范围jump_range为当前预测块的起始地址到预测块中第一个跳转指令地址(包含第一个跳转指令地址)之间的所有指令。
最终的指令有效范围是两者相与的结果。
序号 | 名称 | 描述 |
---|---|---|
2.3.1 | 无跳转指令有效范围生成 | IFU根据FTQ请求,计算无跳转指令有效范围 |
2.3.2 | 有跳转指令有效范围生成 | IFU根据FTQ请求,计算跳转指令有效范围 |
功能点2.4 提取初始指令码
IFU需要将ICache返回的缓存行复制一份并拼接。然后利用上一流水级计算的idx数组,从缓存行提取17x2字节的初始指令码。
序号 | 名称 | 描述 |
---|---|---|
2.4 | 切取初始指令码 | IFU根据上一流水级的切取指针,从缓存行提取初始指令码。 |
功能点3 预译码
多数的功能都由preDecoder子模块完成,因此这里只罗列由IFU本身需要完成的功能。
功能点3.1 F3流水级选取指令有效向量
由于存在跨缓存行的32位指令,IFU需要做的是,根据上一个预测块最后两字节是否为一条RVI指令的开始,从两种指令有效开始向量中,选择一种。
序号 | 名称 | 描述 |
---|---|---|
3.1.1 | 上一预测块结尾为RVC或RVI下半部分 | 上一预测块的最后2字节恰为RVC指令或RVI指令的后半部分,选择第一位为True的有效开始向量 |
3.1.2 | 上一预测块结尾为RVI上半部分 | 上一预测块的最后2字节为RVI,选择第一位为False的有效开始向量 |
功能点4 指令扩展
将PreDecode返回的16条指令码分别送交指令扩展其进行32位指令扩展,RVI保持不变。RVC指令根据手册规定进行扩展。 如果指令非法,则需要将原始指令填写到CSR(控制状态寄存器)中。IFU自身仅仅控制最后填写的是哪种指令。
所以,我们对IFU模块,只关注最后写的指令是何种指令。注意,这里可以修改fsIsOff的入参,测试c.fp指令是否返回原始指令码
序号 | 名称 | 描述 |
---|---|---|
4.1 | 合法RVC指令写扩展指令码 | 对合法RVC指令,写扩展后的指令码 |
4.2 | 非法RVC指令写原始指令码 | 对非法RVC指令,写原始指令码 |
4.3 | RVI指令不扩展 | RVI指令直接写入原始指令即可 |
功能点5 预测错误预检
主要由PredChecker子模块完成。测试点和PredChecker子模块的测试点类似,IFU没有额外的测试点。预检可以和重定向一起测试。
功能点6 前端重定向和流水线冲刷
功能点6.1 预测错误重定向
如果发现了预检阶段检出的错误,则需要产生前端重定向,将F3以外的流水级冲刷
只需要构造有预测错误的预测请求,检查冲刷情况即可。
序号 | 名称 | 描述 |
---|---|---|
6.1.1 | JAL预测错误冲刷 | 预测请求中存在JAL预测错误,需要冲刷流水线 |
6.1.2 | RET预测错误冲刷 | 预测请求中存在RET预测错误,需要冲刷流水线 |
6.1.3 | 非CFI预测错误冲刷 | 预测请求中存在非CFI预测错误,需要冲刷流水线 |
6.1.4 | 无效指令预测错误冲刷 | 预测请求中存在无效指令预测错误,需要冲刷流水线 |
6.1.5 | 跳转目标错误冲刷 | 预测请求中存在跳转目标错误,需要冲刷流水线 |
功能点6.2 跨预测块32位指令处理
如果发现当前预测块的最后两个字节是一条RVI指令的开始,则设置一个标识f3_lastHalf_valid,告诉接下来的预测块含有后半条指令。
我们没有办法直接观察到这个标识,但是可以通过下一预测块来判定:
序号 | 名称 | 描述 |
---|---|---|
6.2.1 | 跨预测块32位指令处理 | 连续传入两个预测块,其中有一条32位指令跨两个预测块,后一个预测块的指令开始向量的首位应该为False |
但是,如果这一判断出现问题(比如当前预测块存在跳转),则需要进行流水线冲刷。
这一功能需要PredChecker子模块“配合”(仅仅通过外部IO的修改很难触发这个防御机制),实现起来比较麻烦,但是还是列举一个测试点:
序号 | 名称 | 描述 |
---|---|---|
6.2.2 | 跨预测块指令误判 | 当IFU根据PredChecker修复的指令有效范围错判了跨预测块指令时,需要将F3以外的流水级全部冲刷 |
功能点7 将指令码和前端信息输出给IBuffer
功能点7.1 传送指令码和前端信息
传送给IBuffer的信息包括:经过扩展的32位指令码、16条指令中每条指令的例外信息、预译码信息、FTQ队列中的指针位置、其他后端需要的信息(经过折叠的PC)、 io_toIbuffer_bits_valid(表示指令是否是一条指令的开始)、io_toIbuffer_bits_enqEnable(前者与上被修正过的预测块指令范围, 从而还能表示指令是否在预测块表示的指令范围内)。
这里要做的只是确认这些信息是否正确传递
序号 | 名称 | 描述 |
---|---|---|
7.1.1 | 指令码传送 | IFU向IBuffer传送扩展后的指令码 |
7.1.2 | 异常信息传送 | IFU向IBuffer传送每个指令的异常信息 |
7.1.3 | 预译码信息传送 | IFU向IBuffer传递每个指令的预译码信息 |
7.1.4 | FTQ指针传送 | IFU向IBuffer传送FTQ预测块的指针 |
7.1.5 | 折叠PC传送 | IFU向IBuffer传送折叠的PC |
7.1.6 | 有效开始向量 | IFU向IBuffer传送表示指令有效和指令是否为指令开始的向量 |
功能点7.2 客户页错误传送gpaddr信息
当且仅当发生guest page fault时,后端需要gpaddr信息,为了节省面积,gpaddr不走正常通路进入ibuffer, 而是随ftqPtr被发送到gpaMem,后端需要时从gpaMem读出。IFU需要保证gpf发生时通向gpaMem的valid拉高、gpaddr正确,同时还要传递预测块的ftqIdx(通过waddr传入)。
这里我们只需要确保在客户页错误发生时通向gpaMem的valid为高,且gpaddr正确填入。
序号 | 名称 | 描述 |
---|---|---|
7.2 | 客户页错误 | 客户页错误发生时,IFU应将gpaMem的valid拉高且填入gpaddr |
功能点8 分支预测冲刷流水线
当精确预测器发现简单预测器错误时,通知IFU取消正在进行的取指请求。
功能点8.1 核验指针
IFU收到BPU冲刷请求后,会将F0/F1流水级上取指令请求的指针比较,冲刷的指针在取指之前,即当前取指令请求在错误的执行路径上,才需要 冲刷IFU。
我们仍然需要从两个方向校验这个功能,即当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。 然而,当冲刷指令在取指令的指针之后时,则不能对流水线进行冲刷。
序号 | 名称 | 描述 |
---|---|---|
8.1.1 | 错误执行路径 | 当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。 |
8.1.2 | 执行路径无误 | 当冲刷指令在取指令的指针之后时,IFU不能对流水线进行冲刷。 |
功能点8.2 BPU S2流水级发现错误
BPU的S2流水级发现错误时,需冲刷IFU的F0流水级
序号 | 名称 | 描述 |
---|---|---|
测试点8.2 | BPU S2流水级发现错误 | 当BPU的S2流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷 |
功能点8.3 BPU S3流水级发现错误
BPU的S3流水级发现错误时,需要冲刷IFU的F0和F1流水级
序号 | 名称 | 描述 |
---|---|---|
8.3 | BPU S3流水级发现错误 | 当BPU的S3流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0和F1流水级进行冲刷 |
功能点9 指令信息和误预测信息写回FTQ
功能点9.1 写回指令信息和误预测信息
将指令PC、预译码信息、错误预测指令的位置、正确的跳转地址以及预测块的正确指令范围等信息写回FTQ,并传递该预测块的FTQ指针。
序号 | 名称 | 描述 |
---|---|---|
9.1.1 | 写回指令PC | IFU的WB流水级,需要向FTQ写回指令PC |
9.1.2 | 写回预译码信息 | IFU的WB流水级,需要向FTQ写回每个指令的预译码信息 |
9.1.3 | 写回误预测指令位置 | IFU的WB流水级,需要向FTQ写回BPU错误预测的指令位置 |
9.1.4 | 写回正确跳转地址 | IFU的WB流水级,需要向FTQ写回该预测块的正确跳转地址 |
9.1.5 | 写回正确指令范围 | IFU的WB流水级,需要向FTQ写回预测块的正确指令范围 |
9.1.6 | 传递预测块FTQ指针 | IFU的WB流水级,需要向FTQ传递预测块的FTQ指针 |
功能点10 MMIO处理
功能点10.1 上电复位处理
处理器上电复位时,IFU需向MMIO总线发送宽度为64位的请求从flash地址空间取指令,并禁止对MMIO总线的推测执行。
上电的情况和正常情况其实没有任何区别,但是,上电时的MMIO请求没有任何差别,只是,第一条请求一定是MMIO,并且不需要等待。
序号 | 名称 | 描述 |
---|---|---|
10.1 | 第一条MMIO指令 | IFU收到的第一条MMIO请求可以直接查询Instr Uncache |
功能点10.2 向InstrUncache发送请求
在正常的处理逻辑下,如果请求地址处于MMIO地址空间,则IFU会向FTQ查询指令提交状态,IFU需要等待当前请求之前的所有请求(包括MMIO和非MMIO)提交完成, 才能向InstrUncache模块发送请求。
这里需要和FTQ交互,可以让FTQ模拟请求提交情况,从而测试等待情况。 如果MMIO请求之前的请求都已经提交,则也不需要等待。反之,则需要一直等待直到查询结果表明前面的指令均已提交。故设计测试点如下:
序号 | 名称 | 描述 |
---|---|---|
10.2.1 | 阻塞等待提交 | IFU收到MMIO请求后,查询FTQ,如果前面还有尚未提交的指令,持续等待 |
10.2.2 | 无阻塞发送请求 | 如果查到FTQ不再有未提交的指令,则IFU将指令发送给Instr Uncache |
功能点10.3 跨总线请求处理
由于MMIO不支持非对齐访问,因此当检测到的RVI指令地址[2,1]两位为b11时,64位总线无法一次传递所有指令,所以需要增加地址进行重发,再次查询ITLB。
序号 | 名称 | 描述 |
---|---|---|
10.3.1 | 重发查询ITLB | 遇到一次无法查询完毕的RVI指令时,需要向ITLB查询获得新增指令的物理地址 |
如果存在异常,则直接将指令和异常信息发送到IBuffer并等待,否则向PMP发送请求。
序号 | 名称 | 描述 |
---|---|---|
10.3.2.1 | ITLB异常 | IFU查询ITLB出现异常时,应当将异常信息发送到IBuffer,然后等待ROB提交完成 |
10.3.2.2 | ITLB返回物理地址 | IFU查询ITLB正常返回物理地址时,IFU继续向PMP请求检查 |
根据pmp_recheck的结果,如果和上一次请求状态不一致,则说明存在访问错误, 为访问异常,不然则根据PMP的回复结果决定是否存在异常。如存在异常(访问异常和其他异常),则将报错信息发送给IBuffer并等待。如无异常,重新向InstrUncache模块 发送请求。
序号 | 名称 | 描述 |
---|---|---|
10.3.3.1 | 请求状态不一致 | IFU检查PMP之后如果发现重发请求状态和上一条请求状态不一致,是访问异常,需要将异常直接发送到IBuffer |
10.3.3.2 | PMP检查异常 | PMP检查出现异常的情况下,也需要将异常直接发送到IBuffer并等待ROB提交。 |
10.3.3.3 | Instr Cache请求重发 | PMP检查若无异常,则向Instr Uncache发送请求获取指令码的后半部分。 |
功能点10.4 向IBuffer发送指令
IFU获得完整数据之后,根据地址从64位数据中截取指令码,并以每个预测块一条指令的形式发送到Ibuffer。等待ROB返回指令已提交的信号。
序号 | 名称 | 描述 |
---|---|---|
10.4 | 向IBuffer发送指令 | IFU在获得完整数据后,截取获得指令码,以每个预测块一条指令的形式发送给IBuffer |
功能点10.5 指令冲刷
CFI指令的冲刷由后端发送给FTQ完成。所以不需要在这里设置测试点。
顺序指令由IFU复用前端重定向通路刷新流水线,并复用FTQ写回机制,将该指令当作误预测指令冲刷,重定向到+2或+4的位置。
+2和+4是由RVC和RVI指令决定的,所以设置测试点如下:
序号 | 名称 | 描述 |
---|---|---|
10.5.1 | RVI指令重定向 | 如果是RVI指令,传递给FTQ的冲刷请求应该重定向到PC+4 |
10.5.2 | RVC指令重定向 | 如果是RVC指令,传递给FTQ的冲刷请求应该重定向到PC+2 |
功能点11 硬件断点
该功能主要由FrontEndTrigger子模块完成。不需要为这一功能额外设置测试点(参照FrontendTrigger的测试点即可)
测试点汇总
序号 | 功能 | 名称 | 描述 |
---|---|---|---|
1 | 接收FTQ预测块请求 | ready置位 | IFU接收FTQ请求后,设置ready |
2.1.1 | F1流水级计算信息和切分指针 | PC生成 | IFU接收FTQ请求后,在F1流水级生成PC |
2.1.2 | F1流水级计算信息和切分指针 | 切取指针生成 | IFU接收FTQ请求后,在F1流水级生成后续切取缓存行的指针 |
2.2.1 | F2流水级获取指令信息 | 异常向量生成 | IFU接收ICache内容后,会根据ICache的结果生成属于每个指令的异常向量 |
2.2.2 | F2流水级获取指令信息 | 物理地址提取 | IFU接收ICache内容后,会根据ICache的结果生成属于每个端口的物理地址。 |
2.2.3 | F2流水级获取指令信息 | 客户物理地址提取 | IFU接收ICache内容后,会根据ICache的结果生成0号端口的客户物理地址。 |
2.2.4 | F2流水级获取指令信息 | MMIO空间信息提取 | IFU接收ICache内容后,会根据ICache的结果判断当前取指请求是否属于MMIO空间。 |
2.3.1 | F2流水级计算预测块有效指令范围 | 无跳转指令有效范围生成 | IFU根据FTQ请求,计算无跳转指令有效范围 |
2.3.2 | F2流水级计算预测块有效指令范围 | 有跳转指令有效范围生成 | IFU根据FTQ请求,计算跳转指令有效范围 |
2.4 | 提取初始指令码 | 切取初始指令码 | IFU根据上一流水级的切取指针,从缓存行提取初始指令码。 |
3.1.1 | F3流水级选取指令有效向量 | 上一预测块结尾为RVC或RVI下半部分 | 上一预测块的最后2字节恰为RVC指令或RVI指令的后半部分,选择第一位为True的有效开始向量 |
3.1.2 | F3流水级选取指令有效向量 | 上一预测块结尾为RVI上半部分 | 上一预测块的最后2字节为RVI,选择第一位为False的有效开始向量 |
4.1 | 指令扩展 | 合法RVC指令写扩展指令码 | 对合法RVC指令,写扩展后的指令码 |
4.2 | 指令扩展 | 非法RVC指令写原始指令码 | 对非法RVC指令,写原始指令码 |
4.3 | 指令扩展 | RVI指令不扩展 | RVI指令直接写入原始指令即可 |
6.1.1 | 预测错误重定向 | JAL预测错误冲刷 | 预测请求中存在JAL预测错误,需要冲刷流水线 |
6.1.2 | 预测错误重定向 | RET预测错误冲刷 | 预测请求中存在RET预测错误,需要冲刷流水线 |
6.1.3 | 预测错误重定向 | 非CFI预测错误冲刷 | 预测请求中存在非CFI预测错误,需要冲刷流水线 |
6.1.4 | 预测错误重定向 | 无效指令预测错误冲刷 | 预测请求中存在无效指令预测错误,需要冲刷流水线 |
6.1.5 | 预测错误重定向 | 跳转目标错误冲刷 | 预测请求中存在跳转目标错误,需要冲刷流水线 |
6.2.1 | 跨预测块32位指令处理 | 跨预测块32位指令处理 | 连续传入两个预测块,其中有一条32位指令跨两个预测块,后一个预测块的指令开始向量的首位应该为False |
6.2.2 | 跨预测块32位指令处理 | 跨预测块指令误判 | 当IFU根据PredChecker修复的指令有效范围错判了跨预测块指令时,需要将F3以外的流水级全部冲刷 |
7.1.1 | 传送指令码和前端信息 | 指令码传送 | IFU向IBuffer传送扩展后的指令码 |
7.1.2 | 传送指令码和前端信息 | 异常信息传送 | IFU向IBuffer传送每个指令的异常信息 |
7.1.3 | 传送指令码和前端信息 | 预译码信息传送 | IFU向IBuffer传递每个指令的预译码信息 |
7.1.4 | 传送指令码和前端信息 | FTQ指针传送 | IFU向IBuffer传送FTQ预测块的指针 |
7.1.5 | 传送指令码和前端信息 | 折叠PC传送 | IFU向IBuffer传送折叠的PC |
7.1.6 | 传送指令码和前端信息 | 有效开始向量 | IFU向IBuffer传送表示指令有效和指令是否为指令开始的向量 |
7.2 | 客户页错误传送gpaddr信息 | 客户页错误 | 客户页错误发生时,IFU应将gpaMem的valid拉高且填入gpaddr |
8.1.1 | 核验指针 | 错误执行路径 | 当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。 |
8.1.2 | 核验指针 | 执行路径无误 | 当冲刷指令在取指令的指针之后时,IFU不能对流水线进行冲刷。 |
8.2 | BPU S2流水级发现错误 | BPU S2流水级发现错误 | 当BPU的S2流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷 |
8.3 | BPU S2流水级发现错误 | BPU S3流水级发现错误 | 当BPU的S3流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0和F1流水级进行冲刷 |
9.1.1 | 写回指令信息和误预测信息 | 写回指令PC | IFU的WB流水级,需要向FTQ写回指令PC |
9.1.2 | 写回指令信息和误预测信息 | 写回预译码信息 | IFU的WB流水级,需要向FTQ写回每个指令的预译码信息 |
9.1.3 | 写回指令信息和误预测信息 | 写回误预测指令位置 | IFU的WB流水级,需要向FTQ写回BPU错误预测的指令位置 |
9.1.4 | 写回指令信息和误预测信息 | 写回正确跳转地址 | IFU的WB流水级,需要向FTQ写回该预测块的正确跳转地址 |
9.1.5 | 写回指令信息和误预测信息 | 写回指令范围 | IFU的WB流水级,需要向FTQ写回预测块的正确指令范围 |
9.1.6 | 写回指令信息和误预测信息 | 传递预测块FTQ指针 | IFU的WB流水级,需要向FTQ传递预测块的FTQ指针 |
10.1 | 上电复位处理 | 第一条MMIO指令 | IFU收到的第一条MMIO请求可以直接查询Instr Uncache |
10.2.1 | 向InstrUncache发送请求 | 阻塞等待提交 | IFU收到MMIO请求后,查询FTQ,如果前面还有尚未提交的指令,持续等待 |
10.2.2 | 向InstrUncache发送请求 | 无阻塞发送请求 | 如果查到FTQ不再有未提交的指令,则IFU将指令发送给Instr Uncache |
10.3.1 | 跨总线请求处理 | 重发查询ITLB | 遇到一次无法查询完毕的RVI指令时,需要向ITLB查询获得新增指令的物理地址 |
10.3.2.1 | 跨总线请求处理 | ITLB异常 | IFU查询ITLB出现异常时,应当将异常信息发送到IBuffer,然后等待ROB提交完成 |
10.3.2.2 | 跨总线请求处理 | ITLB返回物理地址 | IFU查询ITLB正常返回物理地址时,IFU继续向PMP请求检查 |
10.3.3.1 | 跨总线请求处理 | 请求状态不一致 | IFU检查PMP之后如果发现重发请求状态和上一条请求状态不一致,是访问异常,需要将异常直接发送到IBuffer |
10.3.3.2 | 跨总线请求处理 | PMP检查异常 | PMP检查出现异常的情况下,也需要将异常直接发送到IBuffer并等待ROB提交。 |
10.3.3.3 | 跨总线请求处理 | Instr Cache请求重发 | PMP检查若无异常,则向Instr Uncache发送请求获取指令码的后半部分。 |
10.4 | 向IBuffer发送指令 | 向IBuffer发送指令 | IFU在获得完整数据后,截取获得指令码,以每个预测块一条指令的形式发送给IBuffer |
10.5.1 | 指令冲刷 | RVI指令重定向 | 如果是RVI指令,传递给FTQ的冲刷请求应该重定向到PC+4 |
10.5.2 | 指令冲刷 | RVC指令重定向 | 如果是RVC指令,传递给FTQ的冲刷请求应该重定向到PC+2 |
11.1.1 | 断点设置和检查 | select1判定 | 给定tdata1的select位为1,随机构造其它输入,检查断点是否没有触发 |
11.1.2.1 | 断点设置和检查 | select0关系匹配判定 | 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位匹配的输入,检查断点是否触发 |
11.1.2.2 | 断点设置和检查 | select0关系不匹配判定 | 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位不匹配的输入,检查断点是否触发 |
11.2.1 | 链式断点 | chain位测试 | 对每个trigger,在满足PC断点触发条件的情况下,设置chain位,检查断点是否一定不触发 |
11.2.2 | 链式断点 | timing测试 | 对两个trigger,仅设置前一个trigger的chain位,且两trigger的timing位不同,随机设置PC等,测试后一个trigger是否一定不触发 |
11.2.3.1 | 链式断点 | 未命中测试 | 对两个trigger,仅设置前一个trigger的chain位,且两trigger的timing位相同,设置后一个trigger命中而前一个未命中,检查后一个trigger是否一定不触发 |
11.2.3.2 | 链式断点 | 命中测试 | 对两个trigger,仅设置前一个trigger的chain位,且两trigger的timing位相同且均命中,检查后一个trigger是否触发 |
1.1 - F3PreDecoder
子模块:F3PreDecoder模块简介
这个模块是从PreDecoder中时序优化出来的,负责判定CFI指令的类型
F3PreDecoder功能介绍
CFI指令类型判定
要想确定CFI指令类型,只需要分别尝试匹配JAL、JALR、BR和他们的RVC版本即可,注意,RVC的EBREAK 不应该被视为CFI指令。在匹配的过程中,自然CFI指令的类型就被甄别出来了。在这一步中,我们将所有指令分到如下四类brType中:
CFI指令类型 | brType类型编码 |
---|---|
非CFI | 00 |
branch指令 | 01 |
jal指令 | 10 |
jalr指令 | 11 |
ret、call判定
然后,我们需要判断是否为call或者ret,这可以通过rd和rs的取值来考察,具体来说,RISCV的RVI指令中,提供了对rd和rs取值的约定, 当二者取到link寄存器的序号(x1为标准的返回地址寄存器,x5为备用的link寄存器),分别对应着压栈和弹栈。详细的对应情况如下:
F3Predecoder接口说明
in_instr: 传递 16 x 4B的拼接指令码
out_pd:每条指令的预译码信息,在F3Predecoder分析得到的是brType、isCall和isRet
F3PreDecoder子模块测试点和功能点
功能点1 CFI指令类型判定
要想确定CFI指令类型,只需要分别尝试匹配JAL、JALR、BR和他们的RVC版本即可,注意,RVC的EBREAK 不应该被视为CFI指令。
序号 | 名称 | 描述 |
---|---|---|
1.1 | 非CFI判定 | 对传入的非CFI指令(包括RVC.EBREAK),应该判定为类型0 |
1.2 | BR判定 | 对传入的BR指令,应该判定为类型1 |
1.3 | JAL判定 | 对传入的JAL指令,应该判定为类型2 |
1.4 | JALR判定 | 对传入的JALR指令,应该判定为类型3 |
功能点2 ret、call判定
然后,需要判断是否为call或者ret,这可以通过rd和rs的取值来考察。当然,首先必须得满足无条件跳转指令。
对于类型2,只有不为RVC指令且目的寄存器rd为link寄存器(x1或x5)时,才为Call。
对于类型3,在RVI指令下,当rd为link寄存器时,必为Call。当rs为link寄存器且rd不为时,必为Ret。 在RVC指令下,对C.JALR指令,为call,对C.JR指令,当rs1为link时,为Ret
序号 | 名称 | 描述 |
---|---|---|
2.1 | 非CFI和BR不判定 | 对传入的非CFI和BR指令,都不应判定为call或者ret |
2.2.1.1 | RVI.JAL判定call | 对传入的RVI.JAL指令,当rd设置为1或5,应当判定该指令为call |
2.2.1.2 | RVI.JAL例外 | 对传入的RVI.JAL指令,当rd设置为1和5之外的值,不应当判定该指令为call或ret |
2.2.2 | RVC.JAL不判定 | 对传入的RVC.JAL指令,无论什么情况都不能判定为call或ret |
2.3.1.1 | RVI.JALR和rd为link | 传入RVI.JALR指令,并且rd为1或5,无论其他取值,都应判定为call |
2.3.1.2 | RVI.JALR且仅rs为link | 传入RVI.JALR指令,rd不为1和5,rs为1或5,应判定为ret |
2.3.1.3 | RVI.JALR无link | 对传入的JALR指令,若rd和rs均不为link,则不应判定为ret和cal |
2.3.2.1 | RVC.JALR为Ret | 传入RVC.JALR指令,必定为call |
2.3.2.2.1 | RVC.JR且rs为link | 传入RVC.JR指令,rs为1或5,应判定为ret |
2.3.2.2.2 | RVC.JR且rs不为link | 传入RVC.JR指令,rs不为1或5,不应判定为ret |
测试点汇总
序号 | 功能 | 名称 | 描述 |
---|---|---|---|
1.1 | CFI指令类型判定 | 非CFI判定 | 对传入的非CFI指令(包括RVC.EBREAK),应该判定为类型0 |
1.2 | CFI指令类型判定 | BR判定 | 对传入的BR指令,应该判定为类型1 |
1.3 | CFI指令类型判定 | JAL判定 | 对传入的JAL指令,应该判定为类型2 |
1.4 | CFI指令类型判定 | JALR判定 | 对传入的JALR指令,应该判定为类型3 |
2.1 | ret、call判定 | 非CFI和BR不判定 | 对传入的非CFI和BR指令,都不应判定为call或者ret |
2.2.1.1 | ret、call判定 | RVC.JAL判定call | 对传入的RVC.JAL指令,当rd设置为1或5,应当判定该指令为call |
2.2.1.2 | ret、call判定 | RVC.JAL例外 | 对传入的RVC.JAL指令,当rd设置为1和5之外的值,不应当判定该指令为call或ret |
2.2.2 | ret、call判定 | RVI.JAL不判定 | 对传入的RVI.JAL指令,无论什么情况都不能判定为call或ret |
2.3.1.1 | ret、call判定 | RVI.JALR和rd为link | 传入RVI.JALR指令,并且rd为1或5,无论其他取值,都应判定为call |
2.3.1.2 | ret、call判定 | RVI.JALR且仅rs为link | 传入RVI.JALR指令,rd不为1和5,rs为1或5,应判定为ret |
2.3.1.3 | ret、call判定 | RVI.JALR无link | 对传入的JALR指令,若rd和rs均不为link,则不应判定为ret和cal |
2.3.2.1 | ret、call判定 | RVC.JALR为Ret | 传入RVC.JALR指令,必定为call |
2.3.2.2.1 | ret、call判定 | RVC.JR且rs为link | 传入RVC.JR指令,rs为1或5,应判定为ret |
2.3.2.2.2 | ret、call判定 | RVC.JR且rs不为link | 传入RVC.JR指令,rs不为1或5,不应判定为ret |
1.2 - FrontendTrigger
FrontendTrigger子模块
该子模块的主要作用是在前端设置硬件断点和检查。
该模块的输入pc有一个隐含条件,那就是这个pc是通过ftq传递的startAddr计算出来的。
FrontendTrigger功能介绍
断点设置和断点检查
在IFU的FrontendTrigger模块里共4个Trigger,编号为0,1,6,8,每个Trigger的配置信息(断点类型、匹配地址等)保存在tdata寄存器中。
当软件向CSR寄存器tselect、tdata1/2写入特定的值时,CSR会向IFU发送tUpdate请求,更新FrontendTrigger内的tdata寄存器中的配置信息。 目前前端的Trigger仅可以配置成PC断点(mcontrol(tdata1)寄存器的select位为0;当select=1时,该Trigger将永远不会命中,且不会产生异常)。
在取指时,IFU的F3流水级会向FrontendTrigger模块发起查询并在同一周期得到结果。后者会对取指块内每一条指令在每一个Trigger上做检查, 当指令的PC和tdata2寄存器内容的关系满足mcontrol的match位所指示的关系(香山支持match位为0、2、3,对应等于、大于、小于)时, 该指令会被标记为Trigger命中,随着执行在后端产生断点异常,进入M-Mode或调试模式。
链式断点
根据RISCV的debug spec,香山实现的是mcontrol6。
前端的0、6、8号Trigger支持Chain功能。
当它们对应的Chain位被置时,只有当该Trigger和编号在它后面一位的Trigger同时命中,且timing配置相同时(在最新的手册中,这一要求已被删除),处理器才会产生异常。其中可以和6,8号trigger实现chain功能的7,9号trigger在后端访存部件中。
FrontendTrigger 接口说明
设计上并没有提供一个或一组对外的接口来查询某个断点的状态,因此,要在测试中检查断点状态,要么需要检查内部信号的情况(仓库中提供的构建脚本已经暴露了所有内部信号),要么通过具体执行过程中,断点的触发情况来判定。
输入接口
主要分为控制接口和执行信息(目前执行信息只有pc)
控制接口 io_frontendTrigger
本接口存储了frontendTrigger的控制信息,包含以下信号/信号组:
debugMode
当前是否处于debug模式下
tEnableVec
对FrontendTrigger的每个断点,指示其是否有效。
tUpdate
更新断点的控制信息,包含以下信号/信号组:
valid:此次更新是否有效/是否更新。
bits_addr:此次更新的是哪个断点(0~3)
bits_tdata_action:断点触发条件达成后的行为
bits_tdata_chain:断点是否链式传导
bits_tdata_matchType:断点匹配类型(等于、大于、小于三种)
bits_tdata_select:目前为止,select为0时为pc断点
bits_tdata_tdata2:用于和PC比较的基准值
triggerCanRaiseBpExp
trigger是否可以引起异常
pc
pc有一个隐含条件,就是16条指令的pc必定是连续的
输出接口
triggered:16条指令的断点触发情况。
FrontEndTrigger 测试点和功能点
功能点1 设置断点和断点检查
FrontEndTrigger目前仅支持设置PC断点,这通过设置断点的tdata1寄存器的select位为0实现。 同时,tdata2寄存器的mcontrol位负责设置指令PC和tdata2寄存器的地址需要满足的关系, 关系满足时,该指令会被标记为trigger命中。
所以,基于以上功能描述,我们需要测试:
select位为1时,断点是否永远不会触发。
select位为0时,当PC和tdata2的数据的关系满足tdata2的match位时,是否会设置断点。
select位为0时,当PC和tdata2的数据的关系不满足tdata2的match位时,断点是否一定不会触发。
综上所述,我们在这一功能点设计的测试点如下:
序号 | 名称 | 描述 |
---|---|---|
1.1 | select1判定 | 给定tdata1的select位为1,随机构造其它输入,检查断点是否没有触发 |
1.2.1 | select0关系匹配判定 | 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位匹配的输入,检查断点是否触发 |
1.2.2 | select0关系不匹配判定 | 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位不匹配的输入,检查断点是否触发 |
功能点2 链式断点
当某一个trigger的chain位被置后,当其后的trigger的chain位未设置,且两个trigger均命中并且两个trigger的timing相同时,后一个trigger才会触发。
对0号trigger,不需要考虑链式的情况
由此,我们可以设置几种测试点:
序号 | 名称 | 描述 |
---|---|---|
2.1 | chain位测试 | 对每个trigger,在满足PC断点触发条件的情况下,设置chain位,检查断点是否一定不触发。 |
2.2.1 | 未命中测试 | 对两个trigger,仅设置前一个trigger的chain位 |
2.2.2 | 命中测试 | 对两个trigger,仅设置前一个trigger的chain位 |
测试点汇总
序号 | 功能 | 名称 | 描述 |
---|---|---|---|
1.1 | 断点设置和检查 | select1判定 | 给定tdata1的select位为1,随机构造其它输入,检查断点是否没有触发 |
1.2.1 | 断点设置和检查 | select0关系匹配判定 | 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位匹配的输入,检查断点是否触发 |
1.2.2 | 断点设置和检查 | select0关系不匹配判定 | 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位不匹配的输入,检查断点是否触发 |
2.1 | 链式断点 | chain位测试 | 对每个trigger,在满足PC断点触发条件的情况下,设置chain位,检查断点是否一定不触发 |
2.2.1 | 链式断点 | 未命中测试 | 对两个trigger,仅设置前一个trigger的chain位,设置后一个trigger命中而前一个未命中,检查后一个trigger是否一定不触发 |
2.2.2 | 链式断点 | 命中测试 | 对两个trigger,仅设置前一个trigger的chain位,检查后一个trigger是否触发 |
1.3 - PredChecker
子模块:PredChecker简介
分支预测检查器PredChecker接收来自IFU的预测块信息(包括预测跳转指令在预测块的位置、预测的跳转目标、预译码得到的指令信息、指令PC以及预译码得到的跳转目标偏移等),在模块内部检查五种类型的分支预测错误。模块内部分为两个流水线stage,分别输出信息,第一个stage输出给f3阶段,用于修正预测块的指令范围和预测结果。第二个stage输出给wb阶段,用于在发现分支预测错误时产生前端重定向以及写回给FTQ正确的预测信息。
PredChecker功能介绍
JAL预测错误检查
jal指令预测错误的条件是,预测块中有一条jal指令(由预译码信息给出),但是要么这个预测块没有预测跳转,要么此预测块预测跳转的指令在这条jal指令之后(即这条jal指令没有被预测跳转)。
RET预测错误检查
ret指令预测错误的条件是,预测块中有一条ret指令(由预译码信息给出),但是要么这个预测块没有预测跳转,要么此预测块预测跳转的指令在这条ret指令之后(即这条ret指令没有被预测跳转)。
更新指令有效范围向量
PredChecker在检查出Jal/Ret指令预测错误时,需要重新生成指令有效范围向量,有效范围截取到Jal/Ret指令的位置,之后的bit全部置为0。 需要注意的是,jal和ret指令的错误检查都会导致指令有效范围的缩短, 所以需要重新生成指令有效范伟fixedRange,同时修复预测结果。需要注意的是,这个修复只会针对RET预测错误和JAL预测错误导致的范围错误,对于后续要介绍的非CFI预测错误和无效指令预测错误,尽管他们会造成预测块的范围偏小,但是不会进行修复,而是直接在这里进行重定向。这样,重定向后重新取的指令会从这个出错的指令开始。
非CFI预测错误检查
非CFI预测错误的条件是被预测跳转的指令根据预译码信息显示不是一条CFI指令。
无效指令预测错误检查
无效指令预测错误的条件是被预测的指令的位置根据预译码信息中的指令有效向量显示不是一条有效指令的开始。
目标地址预测错误检查
目标地址预测错误的条件是,被预测的是一条有效的jal或者branch指令,同时预测的跳转目标地址和由指令码计算得到的跳转目标不一致。
分级输出检查结果
以上PredChecker检查结果会分为两级分别输出,前面已经提到,Jal/Ret指令由于需要重新生成指令有效范围向量和重新指定预测位置, 所以需要在错误产生的当拍(F3)直接输出结果到Ibuffer用于及时更正进入后端的指令 。而由于时序的考虑,其他错误信息(比如五种错误的错误位置、正确的跳转地址等)则是等到下一拍(WB)阶段才返回给IFU做前端重定向。
PredChecker接口说明
输入接口
fire_in:这个信号可以简单认为是模块有效性的控制信号。
ftqOffset:表示该预测块的跳转指令是否存在(valid),以及跳转指令的序号(bits)。
instrRange:对每条指令,表示该指令是否在预测块的有效指令范围内。
instrValid:表示的是对于每条32位的拼接指令,其是否为一条有效的指令(即低16位为一条RVC指令,或者整个32位为一条RVI指令)。
jumpOffset:如果某一指令为跳转指令,jumpOffset表示这个指令的跳转目标。
pc:指令的pc。
pds:预译码信息,包含指令的brType、是否为Ret(isRet)、是否为RVC指令(isRVC)。
target:下个预测块的开始地址。
输出接口
第一阶段输出
fixedRange:修复的指令有效范围向量,对每条指令i,fixedRange_i为真表示这条指令是否在当前预测块的有效指令范围内
fixedTaken:修复过后的CFI指令选取情况,对每条指令,fixedTaken_i为真表示这条指令是否是这个预测块的第一条CFI指令
第二阶段输出
fixedMissPred:对每条指令,PredChecker检查出的存在预测错误的情况,fixedMissPred_i为真表示这条指令存在预测错误
fixedTarget:对每条指令,给出修复过的下一条指令的位置(可以是常规的pc+2或+4,或者如果是跳转指令,给出跳转目标)。
jalTarget:对每条指令,给出跳转目标。
PredChecker测试点和功能点
功能点1 BPU预测信息的JAL预测错误检查
PredChecker会对传入的预测块进行JAL预测错误预检查并修正指令有效范围向量和预测的跳转指令。
对这一模块的测试,我们分为两部分:正确的输入是否会误检和确有JAL检测错误的预测块输入能否检出。
对于误检,我们设计如下的测试点:
序号 | 名称 | 描述 |
---|---|---|
1.1.1 | 误检测试1 | 预测块中没有JAL指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JAL预测错误。 |
1.1.2 | 误检测试2 | 预测块中有JAL指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JAL预测错误。 |
对于JAL预测错误的正确检验,我们设计如下的测试点:
序号 | 名称 | 描述 |
---|---|---|
1.2.1 | 存在JAL未预测 | 预测块中存在JAL指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出JAL预测错误。 |
1.2.2 | 预测的JAL并非第一条 | 预测块中存在JAL指令,但是BPU预测信息取的跳转指令在第一条JAL指令之后,检查PredChecker是否能检测出JAL预测错误。 |
功能点2 BPU预测信息的RET预测错误检查
PredChecker会对传入的预测块进行RET预测错误预检查并修正指令有效范围向量和新的预测结果。
和JAL预测错误类似,我们也按照误检和正检来构造。
对于误检,我们设计如下的测试点:
序号 | 名称 | 描述 |
---|---|---|
2.1.1 | 误检测试1 | 预测块中没有RET指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报RET预测错误。 |
2.1.2 | 误检测试2 | 预测块中有RET指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报RET预测错误。 |
对于RET预测错误的正确检出,我们设计如下的测试点:
序号 | 名称 | 描述 |
---|---|---|
2.2.1 | 存在RET未预测 | 预测块中存在RET指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。 |
2.2.2 | 预测的跳转并非第一条 | 预测块中存在RET指令,但是BPU预测信息取的跳转指令在第一条RET指令之后,检查PredChecker是否能检测出RET预测错误。 |
功能点3 更新指令有效范围向量和预测跳转的指令
PredChecker在检查出Jal/Ret指令预测错误时,需要重新生成指令有效范围向量, 有效范围截取到Jal/Ret指令的位置,之后的bit全部置为0。 同时,还需要根据每条指令的预译码信息和BPU的预测信息修复预测跳转的结果。
所以,根据功能要求,我们可以划分出三类情况,分别是预测的有效范围和取用的跳转指令正确的情况, 由于RET和JAL预测错误引起的有效范围偏大和错判非跳转指令和无效指令引起的有效范围偏小。
序号 | 名称 | 描述 |
---|---|---|
3.1 | 有效范围无误 | 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。 |
3.2 | RET和JAL预测错误引起的范围偏大 | 如果检测到了JAL或RET类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。 |
3.3 | 非CFI和无效指令引起的预测范围偏小 | 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令,因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令 |
功能点4 非CFI预测错误检查
非CFI预测错误的条件是被预测跳转的指令根据预译码信息显示不是一条CFI指令。
要检验这一功能,我们仍然按误检和正确检验来设计测试点:
序号 | 名称 | 描述 |
---|---|---|
4.1.1 | 误检测试1 | 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误 |
4.1.2 | 误检测试2 | 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误 |
4.2 | 正确检测测试 | 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误 |
功能点5 无效指令预测错误检查
无效指令预测错误的条件是被预测的指令的位置根据预译码信息中的指令有效向量显示不是一条有效指令的开始。
要检验这一功能,我们按照误检和正确检测来设计测试点:
序号 | 名称 | 描述 |
---|---|---|
5.1.1 | 误检测试1 | 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误 |
5.1.2 | 误检测试2 | 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误 |
5.1.3 | 误检测试3 | 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误 |
5.2 | 正确检测测试 | 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误 |
功能点6 目标地址预测错误检查
目标地址预测错误的条件是,被预测的是一条有效的jal或者branch指令, 同时预测的跳转目标地址和由指令码计算得到的跳转目标不一致。
和先前的思路一样,我们仍然按误检和检出两类组织测试点:
序号 | 名称 | 描述 |
---|---|---|
6.1.1 | 误检测试1 | 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误 |
6.1.2 | 误检测试2 | 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误 |
6.2 | 正确检测测试 | 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误 |
功能点7 生成跳转和顺序目标
PredChecker还需要负责生成跳转和顺序目标。
我们通过随机生成译码信息进行测试
序号 | 名称 | 描述 |
---|---|---|
7.1 | 随机测试 | 随机提供译码信息,检测生成的跳转目标和顺序目标。 |
测试点汇总
综上所述,所有的测试点如下:
序号 | 功能 | 名称 | 描述 |
---|---|---|---|
1.1.1 | BPU预测信息的JAL预测错误检查 | 误检测试1 | 预测块中没有JAL指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JAL预测错误。 |
1.1.2 | BPU预测信息的JAL预测错误检查 | 误检测试2 | 预测块中有JAL指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JAL预测错误。 |
1.2.1 | BPU预测信息的JAL预测错误检查 | 存在JAL未预测 | 预测块中存在JAL指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出JAL预测错误。 |
1.2.2 | BPU预测信息的JAL预测错误检查 | 预测的JAL并非第一条 | 预测块中存在JAL指令,但是BPU预测信息取的跳转指令在第一条JAL指令之后,检查PredChecker是否能检测出JAL预测错误。 |
2.1.1 | BPU预测信息的RET预测错误检查 | 误检测试1 | 预测块中没有RET指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报RET预测错误。 |
2.1.2 | BPU预测信息的RET预测错误检查 | 误检测试2 | 预测块中有RET指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报RET预测错误。 |
2.2.1 | BPU预测信息的RET预测错误检查 | 存在RET未预测 | 预测块中存在RET指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。 |
2.2.2 | BPU预测信息的RET预测错误检查 | 预测的跳转并非第一条 | 预测块中存在RET指令,但是BPU预测信息取的跳转指令在第一条RET指令之后,检查PredChecker是否能检测出RET预测错误。 |
3.1 | 更新指令有效范围向量和预测跳转的指令 | 有效范围无误 | 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。 |
3.2 | 更新指令有效范围向量和预测跳转的指令 | RET和JAL预测错误引起的范围偏大 | 如果检测到了JAL或RET类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。 |
3.3 | 更新指令有效范围向量和预测跳转的指令 | 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令,因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令。 | |
4.1.1 | 非CFI预测错误检查 | 误检测试1 | 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误 |
4.1.2 | 非CFI预测错误检查 | 误检测试2 | 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误 |
4.2 | 非CFI预测错误检查 | 正确检测测试 | 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误 |
5.1.1 | 无效指令预测错误检查 | 误检测试1 | 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误 |
5.1.2 | 无效指令预测错误检查 | 误检测试2 | 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误 |
5.1.3 | 无效指令预测错误检查 | 误检测试3 | 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误 |
5.2 | 无效指令预测错误检查 | 正确检测测试 | 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误 |
6.1.1 | 目标地址预测错误检查 | 误检测试1 | 构造不存在跳转指令并且未预测跳转的预测信息作输入,测试PredChecker是否会错检目标地址预测错误 |
6.1.2 | 目标地址预测错误检查 | 误检测试2 | 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误 |
6.2 | 目标地址预测错误检查 | 正确检测测试 | 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误 |
7.1 | 生成跳转和顺序目标 | 随机测试 | 随机提供译码信息,检测生成的跳转目标和顺序目标。 |
1.4 - PreDecode
子模块:PreDecoder简介
预译码器PreDeocoder接受初始指令码并进行指令码拼接,拼接之后对每个指令码查询预译码表产生预译码信息,预译码信息包括该位置是否是有效指令开始、CFI指令类型、是否是RVC指令、是否是Call指令以及是否是Ret指令。预译码器会产生两种有效指令开始的向量,一种是默认第1个二字节必为有效指令开始,另一种是默认第2个二字节必为有效指令的开始,最终的选择在IFU端做。
所以,预译码器接收的输入是: 17 x 2B的初始指令码,这个2字节的初始指令码要么是一条RVC指令,要么是一条RVI指令的前半或后半部分。
预译码器的输出是:16x4B的拼接指令码;对每个4B指令码,该条指令是否为RVI或RVC指令(RVC指令只考虑该4B的低2B);对每个4B指令码,该条指令的跳转偏移;两个16位的有效指令开始向量,其中第一种向量假定当前预测块的起始2字节为一条有效指令的开始,而第二种向量假定当前预测块的起始2字节为一条有效RVI指令的结束(但是由于第二种向量的前两位必然为0和1,所以编译优化后,第二种向量实际只有14个信号,表示2-15位;同理,第1种向量的第0位因为恒为1,所以也被优化)
功能介绍
指令码生成
预译码器接受来自IFU完成指令切分的17 × 2字节的初始指令码,并以4字节为窗口,2字节为步进长度, 从第1个2字节开始,直到第16个2字节,选出总共16个4字节的指令码。
预译码信息生成
预译码器根据指令码产生预译码信息,主要包括:是否是RVC指令、是否是CFI指令、 CFI指令类型(branch/jal/jalr/call/ret)、CFI指令的目标地址计算偏移。
首先是判断是否是RVC指令,RVC指令的具体格式参阅RISCV手册的描述:
其中,决定指令是否为RVC的部分在于指令的[1, 0]两位,不为3的情况下都是RVC指令。
其余的指令性质判定功能(CFI类型、是否为call和ret)被时序优化到了F3PreDecoder中,不过也可以认为是PreDecoder的一部分。
最后比较麻烦的是CFI指令的目标地址计算偏移,主要是对J和BR分支指令进行的计算,这需要综合RVI和RVC中jal和br指令的结构。 首先,是手册中对于C.J的描述
这里对imm立即数的注解是,立即数的每一位最后对应到的是偏移的哪一位。
所以,可以认为立即数是这么重组的:
instr(12) + instr(8) + instr(10, 9) + instr(6) + instr(7) + instr(2) + instr(11) +instr(5,3) + “0”
而RVI中,对于JAL指令,是这么定义的:
我们可以类似地计算立即数。
同样的,我们可以查询手册,参考BR类指令的立即数计算RVC和RVI指令对应的偏移。
PreDecode接口说明
输入接口
in_bits_data 17 x 2B的初始指令码,其中,每2个字节既可以代表一条RVC指令,也可以代表一个RVI指令的一半。
输出接口
instr:拼接后的 16 x 4B的初始指令码
jumpOffset:如果这条指令是跳转指令,则jumpOffset表示其跳转偏移
pd:每条指令预译码信息,在时序优化之后,PreDecode模块的控制信息只剩下了valid和isRVC,后者表示这条指令是否为RVC指令
hasHalfValid:这个信号需要和pd的valid结合起来看,PreDecode的一个功能是求出指令开始向量,也就是对每个4B的拼接指令,判断其低2B是否为一条有效指令的开始(即一条RVI指令的前半部分,或者一条RVC指令),但是需要分类讨论该预测块的第一个2B是否为一条有效指令的开始。hasHalfValid表示的是当前预测块的第一个2B指令为一条RVI指令的后半部分时,给出的指令开始向量。类似地,pd中的valid指的是当前预测块的第一个2B指令为一条指令的开始时,给出的指令开始向量。
PreDecoder测试点和功能点
功能点1 生成指令码
子模块:PreDecoder简介
预译码器PreDeocoder接受初始指令码并进行指令码拼接,拼接之后对每个指令码查询预译码表产生预译码信息,预译码信息包括该位置是否是有效指令开始、CFI指令类型、是否是RVC指令、是否是Call指令以及是否是Ret指令。预译码器会产生两种有效指令开始的向量,一种是默认第1个二字节必为有效指令开始,另一种是默认第2个二字节必为有效指令的开始,最终的选择在IFU端做。
所以,预译码器接收的输入是: 17 x 2B的初始指令码,这个2字节的初始指令码要么是一条RVC指令,要么是一条RVI指令的前半或后半部分。
预译码器的输出是:16x4B的拼接指令码;对每个4B指令码,该条指令是否为RVI或RVC指令(RVC指令只考虑该4B的低2B);对每个4B指令码,该条指令的跳转偏移;两个16位的有效指令开始向量,其中第一种向量假定当前预测块的起始2字节为一条有效指令的开始,而第二种向量假定当前预测块的起始2字节为一条有效RVI指令的结束(但是由于第二种向量的前两位必然为0和1,所以编译优化后,第二种向量实际只有14个信号,表示2-15位;同理,第1种向量的第0位因为恒为1,所以也被优化) 功能介绍 指令码生成
预译码器接受来自IFU完成指令切分的17 × 2字节的初始指令码,并以4字节为窗口,2字节为步进长度, 从第1个2字节开始,直到第16个2字节,选出总共16个4字节的指令码。 预译码信息生成
预译码器根据指令码产生预译码信息,主要包括:是否是RVC指令、是否是CFI指令、 CFI指令类型(branch/jal/jalr/call/ret)、CFI指令的目标地址计算偏移。
预译码器从IFU接收完成指令切分的17 x 2 字节的初始指令码,以4字节为窗口,2字节为步进长度,选出16 x 4字节的指令码
我们需要随机生成初始指令码,并测试拼接的结果。
序号 | 名称 | 描述 |
---|---|---|
1 | 拼接测试 | 随机生成17 x 2字节的初始指令码,检验PreDecoder拼接结果 |
功能点2 生成预译码信息
预译码器会根据指令码产生预译码信息,包括RVC指令的判定和CFI指令的目标地址计算偏移。
CFI类型的判定则时序优化到了F3PreDecoder中。
据此,我们可以设计下述测试点。
首先是判定RVC指令,我们随机生成输入初始指令码,对返回的16位RVC判定结果进行检验。 具体来说,对每32位指令,考虑RVC和RVI两种情况。
序号 | 名称 | 描述 |
---|---|---|
2.1.1 | RVC判定 | 传入RVC指令,应该判断为RVC |
2.1.2 | RVI判定 | 传入RVI指令,不应判断为RVC |
然后,需要分别根据手册构造RVC和RVI扩展下的J指令和BR指令们,所以有如下的测试点:
序号 | 名称 | 描述 |
---|---|---|
2.2.1 | RVC.J计算 | 对传入RVC扩展的J指令,检查计算的偏移 |
2.2.2 | RVI.J计算 | 对传入RVI扩展的J指令,检查计算的偏移 |
2.2.3 | RVC.BR计算 | 对传入RVC扩展的BR指令,检查计算的偏移 |
2.2.4 | RVI.BR计算 | 对传入RVI扩展的BR指令,检查计算的偏移 |
功能点3 生成指令开始向量
最后,预译码还需要生成两种指令开始向量:
序号 | 名称 | 描述 |
---|---|---|
2.3.1 | 有效指令开始向量计算1 | 对预测块,假定第一条指令为一条有效指令的开始,对每条指令计算其是否为有效指令开始 |
2.3.2 | 有效指令开始向量计算2 | 对预测块,假定第一条指令为一条有效指令的结束,对每条指令计算其是否为有效指令开始 |
测试点汇总
综上所述,对PredDecoder,所有的测试点为:
序号 | 功能 | 名称 | 描述 |
---|---|---|---|
1 | 拼接指令码 | 拼接测试 | 随机生成17 x 2字节的初始指令码,检验PreDecoder拼接结果 |
2.1.1 | RVC判定 | RVC判定 | 传入RVC指令,应该判断为RVC |
2.1.2 | RVC判定 | RVI判定 | 传入RVI指令,不应判断为RVC |
2.2.1 | 跳转目标计算 | RVC.J计算 | 对传入RVC扩展的J指令,检查计算的偏移 |
2.2.2 | 跳转目标计算 | RVI.J计算 | 对传入RVI扩展的J指令,检查计算的偏移 |
2.2.3 | 跳转目标计算 | RVC.BR计算 | 对传入RVC扩展的BR指令,检查计算的偏移 |
2.2.4 | 跳转目标计算 | RVI.BR计算 | 对传入RVI扩展的BR指令,检查计算的偏移 |
2.3.1 | 有效指令开始向量计算1 | 对预测块,假定第一条指令为一条有效指令的开始,对每条指令计算其是否为有效指令开始 | |
2.3.2 | 有效指令开始向量计算2 | 对预测块,假定第一条指令为一条有效指令的结束,对每条指令计算其是否为有效指令开始 |
1.5 - RVCExpander
子模块:RVCExpander简介
RVCExpander是IFU的子模块,负责对传入的指令进行指令扩展,并解码计算非法信息。
该模块接收的输入量是两个:一条RVC指令或者RVI指令;CSR对fs.status的使能情况。
输出量也是两个:输入指令对应的RVI指令;RVC指令是否非法。
指令扩展
如果是RVI指令,则无需扩展。
否则对RVC指令,按照手册的约定进行扩展。
非法指令判断
RVI指令永远判断为合法。
对于RVC指令的判定,详细内容参阅20240411的RISCV手册的26.8节表格列出的指令条件。
RVCExpander接口说明
输入接口
fsIsOff:表示CSR是否使能fs.status
in:传入一个32位数据,其可以是一个完整的RVI指令,也可以是低16位RVC指令+高16位为RVI指令的一半(当然低16位也有可能是RVI指令的后半部分,但是RVCExpander不会区分,可以认为RVCExpander假定传入的32位数据的低16位一定为一条指令的开始)
输出接口
ill:表示这条指令是否为非法指令
out_bits:对RVI指令,直接返回,对RVC指令,返回扩展后的32位指令。
功能点和测试点
功能点1 指令扩展
RVCExpander负责接收预译码器拼接的指令码,并进行指令扩展,如果是16位RVC指令,需要按照RISCV手册的约定完成扩展
对此,我们需要随机生成RVI指令和RVC指令,送入预译码器:
序号 | 名称 | 描述 |
---|---|---|
1.1 | RVI指令保留 | 构造RVI指令传入,检查保留情况 |
1.2 | RVC指令扩展 | 构造RVC指令传入,按手册检查扩展结果 |
功能点2 非法指令判断
RVCExpander在解析指令时,如发现指令违反了手册的约定,则需要判定该指令非法
对此,我们需要随机生成非法指令送入RVI中,并检测RVCExpander对合法位的校验;同时,我们还需要校验合法指令是否会被误判为非法指令:
此外,需要判定C.fp指令在CSR未使能fs.status的情况下,能否将这类指令判定为非法。
序号 | 名称 | 描述 |
---|---|---|
2.1 | 常规非法指令测试 | 随机构造非法RVC指令传入,检查判断结果 |
2.2 | 合法指令测试 | 随机构造合法RVC指令传入,检查判断结果 |
2.3 | C.fp指令测试 | CSR未使能fs.status的情况下,C.fp指令应该为非法 |
测试点汇总
序号 | 功能 | 名称 | 描述 |
---|---|---|---|
1.1 | 指令扩展 | RVI指令保留 | 构造RVI指令传入,检查保留情况 |
1.2 | 指令扩展 | RVC指令扩展 | 构造RVC指令传入,按手册检查扩展结果 |
2.1 | 非法指令判断 | 非法指令测试 | 随机构造非法RVC指令传入,检查判断结果 |
2.2 | 非法指令判断 | 合法指令测试 | 随机构造合法RVC指令传入,检查判断结果 |
2.3 | C.fp指令测试 | CSR未使能fs.status的情况下,C.fp指令应该为非法 |
RVC扩展辅助阅读材料
为方便参考模型的书写,在这里根据20240411版本的手册内容整理了部分指令扩展的思路。
对于RVC指令来说,op = instr(1, 0);funct = instr(15, 13)
op\funct | 000 | 001 | 010 | 011 | 100 | 101 | 110 | 111 |
---|---|---|---|---|---|---|---|---|
00 | addi4spn | fld | lw | ld | lbu lhu;lh sb;sh |
fsd | sw | sd |
01 | addi | addiw | li | lui addi16sp zcmop |
ARITHs zcb |
j | beqz | bnez |
10 | slli | fldsp | lwsp | ldsp | jr;mv ebreak jalr;add |
fsdsp | fwsp | sdsp |
在开始阅读各指令的扩展规则时,需要了解一些RVC扩展的前置知识,比如:
rd’, rs1’和rs2’寄存器:受限于16位指令的位宽限制,这几个寄存器只有3位来表示,他们对应到x8~x15寄存器。
op = b'00'
funct = b'000’: ADDI4SPN
该指令将一个0扩展的非0立即数加到栈指针寄存器x2上,并将结果写入rd'
其中,nzuimm[5:4|9:6|2|3]的含义是:
这条指令的第12至11位是立即数的5至4位,第10至7位是立即数的9至6位,第6位是立即数的第2位,第7位是立即数的第3位。
这条指令最终扩展成为addi rd’, x2, nzuimm[9:2]
addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |
注意,该指令的立即数为0的时候,不合法。
funct = b'001’: fld
该指令从内存加载一个双精度浮点数到rd’寄存器。
offset的低三位是0,高位进行了0扩展。
这条指令最终扩展成为fld rd′,offset(rs1′)
fld的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000111 |
注意:在昆明湖环境下,该指令要求CSR使能fs.status,也即入参fsIsOff为假。
funct = b'010’: lw
该指令从内存加载一个32位的值到rd’寄存器。
offset的低两位是0,高位进行了0扩展。
这条指令最终扩展成为lw rd′,offset(rs1′)
lw的格式形如: | imm[11:0] | rs1 | 010 | rd | 0000011 |
funct = b'011’: ldsp
该指令从内存加载一个64位的值到rd’寄存器。
offset的低两位是0,高位进行了0扩展。
这条指令最终扩展成为ld rd′,offset(rs1′)
ld的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000011 |
funct = b'100’: zcb extensions 1
在RVC指令中,这部分对应的是zcb扩展中的5条指令:lbu,lhu,lh,sb,sh
在zcb扩展中,进一步地取instr[12:10]作为zcb扩展的指令码,我们记作funct_zcb
funct_zcb = b'000’: lbu
| 100 | 000 | rs1’ | uimm[0|1] | rd’ | 00 |
这个指令从rs1’+uimm的地址读取一字节,用0扩展并并加载到rd’中。
最终翻译为 lb rd’, uimm(rs1')
lb指令的格式形如:| imm[11:0] | rs1 | 000 | rd | 0000011 |
funct_zcb = b'001’, instr[6] =0 : lhu
| 100 | 001 | rs1’ | 0 | uimm[1] | rd’ | 00 |
这个指令从地址rs1’ + uimm读取半word,用0扩展加载到rd’中。
最终翻译为 lhu rd’, uimm(rs1')
lhu指令的格式形如:| imm[11:0] | rs1 | 101 | rd | 0000011 |
funct_zcb = b'001’, instr[6] =1 : lh
| 100 | 001 | rs1’ | 1 | uimm[1] | rd’ | 00 |
这个指令从地址rs1’ + uimm读取半word,符号扩展并加载到rd’中。
最终翻译为 lh rd’, uimm(rs1')
lh指令的格式形如:| imm[11:0] | rs1 | 001 | rd | 0000011 |
funct_zcb = b'010’: sb
| 100 | 010 | rs1’ | uimm[0 | 1] | rd’ | 00 |
这个指令把rs2’的低字节存储到地址rs1’ + uimm指示的内存地址中。
最终翻译为 sb rs2, uimm(rs1')
RVI中sb指令的格式形如:|imm[11:5] | rs2 | rs1 | 000 | imm[4:0] | 0100011 |
funct_zcb = b'011’: sh
| 100 | 011 | rs1’ | 0 | uimm[1] | rd’ | 00 |
这个指令把rs2’的低半字存储到地址rs1’ + uimmz指示的内存地址中。
最终翻译为 sh rd’, uimm(rs1')
sh指令的格式形如:|imm[11:5] | rs2 | rs1 | 001 | imm[4:0] | 0100011 |
funct = b'101’: fsd
fsd将rs2’中的双精度浮点数存储到rs1’ + imm指示的内存区域
该指令的立即数低3位为0,同时进行了0符号扩展。
最终这个指令将被扩展为fsd rs2′, offset(rs1′)
RVI的FSD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100011 |
注意:在昆明湖环境下,该指令要求CSR使能fs.status,也即入参fsIsOff为假。
funct = b'110’: sw
sw将rs2’中的一个字存储到rs1’ + imm指示的内存区域
该指令的立即数低2位为0,同时进行了0符号扩展。
最终这个指令将被扩展为sw rs2′, offset(rs1′)
RVI的SW格式形如:| imm[11:5]| rs2 | rs1 | 010 | imm[4:0] | 0100011 |
funct = b'111’: sd
fsd将rs2’中的双字存储到rs1’ + imm指示的内存区域
该指令的立即数低3位为0,同时进行了0符号扩展。
最终这个指令将被扩展为sd rs2′, offset(rs1′)
RVI的SD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100111 |
op = b'01'
funct = b'000’: addi
该指令将一个符号扩展的非0立即数加到rd存储的数字上,并将结果写入rd。
尽管手册规定立即数和rd不为0,但是立即数和rd为0的情况仍可视为合法。前者是HINT指令,而后者是NOP。
这条指令最终扩展成为addi rd, rd, imm
addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |
funct = b'001’: addiw
该指令的功能和addi类似,但是先计算得到32位数,然后再符号扩展至64位。
该指令的rd为0时非法。
当立即数不为0时,该指令最终扩展成为addiw, rd, rd, imm
addiw的指令格式为| imm[11:0] | rs1 | 000 | rd | 0011011 |
如果立即数为0,该指令将会扩展成为sext.w rd,不过和addiw的格式是一样的,因此可以将他们归为一类。
funct = b'010’: li
该指令将符号扩展的立即数加载到rd中。
当立即数为0时,该指令为hint,可以看作合法。
这条指令最终扩展成为addi rd, x0, imm
addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |
funct = b'011’: lui/addi16sp/zcm
当rd不为0且不为2时,为lui指令,可以扩展为lui rd, imm
lui指令的格式形如: | imm[31:12] | rd | 0110111 |
当rd为0时,为hint,也可当作cli进行译码。
当rd为2时,为addi16sp指令:
扩展为addi x2, x2, nzimm[9:4]
addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |
对addi16sp,立即数为0时非法。
此外,当第12至11位皆为0,第7位是1且第6至2位为0时,为zcmop,可以直接翻译为一个不起效的指令,比如与立即数0。
funct = b'100’: arith & zcb extension2
在RVC指令中,这部分对应的是数学运算指令和zcb扩展中的另一部分指令,数学计算指令的对应如下:
其中SRLI64和SRAI64在昆明湖环境下可以不考虑。
srli
当funct2为00时,为srli。
最终可翻译为srli rd′, rd′, 64
srli的格式形如:|0000000|shamt|rs1|101|rd|0010011|
srai
当funct2为01时,为srai。
最终可翻译为srai rd′, rd′, 64
SRAI的格式形如:|0100000|shamt|rs1|101|rd|0010011|
andi
该指令最终扩展为andi rd′, rd′, imm
andi的格式形如|imm[11:0]|rs1|111|rd|0010011|
sub
这条指令最终可以扩展为:sub rd′, rd′, rs2′
sub指令的格式形如:|0100000|rs2|rs1|000|rd|0110011|
xor
这条指令最终可以扩展为:xor rd′, rd′, rs2′
xor指令的格式形如:|0000000|rs2|rs1|100|rd|0110011|
or
这条指令最终可以扩展为:or rd′, rd′, rs2′
or指令的格式形如:|0000000|rs2|rs1|110|rd|0110011|
and
这条指令最终可以扩展为:and rd′, rd′, rs2′
and指令的格式形如:|0000000|rs2|rs1|111|rd|0110011|
subw
这条指令最终可以扩展为:subw rd′, rd′, rs2′
subw指令的格式形如:|0100000|rs2|rs1|000|rd|0111011|
addw
这条指令最终可以扩展为:addw rd′, rd′, rs2′
addw指令的格式形如:|0000000|rs2|rs1|000|rd|0111011|
mul
从mul开始的一部分指令属于zcb扩展。
zcb扩展中,当instr(12, 10) == “111”,且instr(6, 5)为"10"时,为mul指令。
zcb扩展中,当instr(12, 10) == “111”,且instr(6, 5)为"11"时,根据instr(4,2), 共有000的zext.b,001的sext.b,010的zext.h,011的sext.h,100的zext.w和101的not。
该指令可扩展为mul rd, rd, rs2
mul的格式为:|0000001|rs2|rs1|000|rd|0110011|
zext.b
这条指令可以翻译为:andi rd’/rs1’, rd’/rs1’, 0xff
andi的格式形如|imm[11:0]|rs1|111|rd|0010011|
sext.b
该指令翻译为sext.b rd, rd
sext.b指令在RVI下形如:
zext.h
该指令翻译为zext.h rd, rd
zext.h指令在RVI下形如:
sext.h
该指令翻译为sext.h rd, rd
sext.h指令在RVI下形如:
zext.w
该指令等价为add.uw rd’/rs1’, rd’/rs1’, zero
add.uw指令在RVI下形如:
not
该指令等价为xori rd’/rs1’, rd’/rs1’, -1
xori指令在RVI下形如: | imm[11:0] | rs1| 100 | rd | 0010011 |
funct = b'101’: j
最终这个指令将被扩展为jal x0, offset
jal的格式形如:| imm[20|10:1|11|19:12] | rd | 1101111 |
funct = b'110’: beqz
该指令可以扩展到beq rs1‘, x0, offset
beq指令形如: |imm[12|10:5]|rs2|rs1|000|imm[4:1|11]|1100011| imm[12|10:5]rs2rs1001imm[4:1|11]1100011BNE
funct = b'111’: bnez
最终这个指令将被扩展为bne rs1′, x0, offset
bne指令形如:|imm[12|10:5]| rs2 | rs1 | 001 | imm[4:1|11] | 1100011|
op = b'10'
funct = b'000’: slli
该指令将一个符号扩展的非0立即数加到rd存储的数字上,并将结果写入rd。
尽管手册规定立即数和rd不为0,但是立即数和rd为0的情况仍可视为合法。前者是HINT指令,而后者是NOP。
这条指令最终扩展成为slli rd, rd, shamt[5:0]
slli的格式形如:|000000|shamt|rs1|001|rd|0010011|
funct = b'001’: fldsp
该指令最终扩展成为fld rd, offset(x2)
fld的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000111 |
该指令要求CSR使能fs.status
funct = b'010’: lwsp
rd为0时非法。
这条指令最终扩展成为lw rd, offset(x2)
lw的格式形如: | imm[11:0] | rs1 | 010 | rd | 0000011 |
funct = b'011’: ldsp
rd为0时非法。
这条指令最终扩展成为ld rd, offset(x2)
lw的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000011 |
funct = b'100’: jr/mv/ebreak/jalr/add
jr
当rd为0时,非法。
该指令最终可以扩展为jalr x0, 0(rs1)
jalr指令的格式为:|imm[11:0]|rs1|000|rd|1100111|
mv
rd为0时,是hint指令。
该指令最终可以扩展为add rd, x0, rs2
add指令形如:|0000000|rs2|rs1|000|rd|0110011|
ebreak
可以扩展为ebreak指令。
形如:|00000000000100000000000001110011|
jalr
该指令最终可以扩展为jalr x1, 0(rs1)
jalr指令的格式为:|imm[11:0]|rs1|000|rd|1100111|
add
该指令最终可以扩展为add rd, rd, rs2
add指令形如:|0000000|rs2|rs1|000|rd|0110011|
funct = b'101’: fsdsp
这条指令最终扩展成为fsd rs2, offset(x2)
RVI的FSD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100011 |
该指令要求CSR使能fs.status
funct = b'110’: swsp
这条指令最终扩展成为sw rs2, offset(x2)
RVI的SW格式形如:| imm[11:5]| rs2 | rs1 | 010 | imm[4:0] | 0100011 |
funct = b'111’: sdsp
该指令最终扩展成为sd rd, offset(x2)
RVI的SD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100111 |
2 - ITLB
TLB 功能概述
现代操作系统通常采用虚拟内存管理机制(Virtual Memory Management),在处理器中对应需要内存管理单元(MMU,Memory Management Unit)来进行虚实地址的映射。MMU 负责处理 CPU 的内存访问请求,其功能包括虚实地址的映射、内存保护、CPU 高速缓存控制等。
虚实地址的映射是以页(Page)为单位的。在物理内存管理中,内核会将整个物理内存空间划分为一个一个的页帧(Page Frame),一般情况下页帧大小为 4KB,称为一个物理页帧,内核会将每一个物理页帧进行编号(PFN,Page Frame Number),每个页帧有唯一确定的 PFN。对于一个进程来说,如果它直接使用物理地址构建自己的地址空间,那么作为进程就需要关心每一个变量存放在哪一个物理地址,也就是说程序员需要清楚数据在内存中的具体布局,还需要每次都要考虑内存的分配问题;同时,对于多个进程同时进行的情况,哪些数据是共享的,如何避免地址冲突等等都会成为问题。
MMU 为每个进程创建自己的虚拟地址空间,存储虚实地址的映射,在进程的视角看来它独享一段确定的(通常是连续的)地址,避免了其它进程的干扰;同时提供了虚实地址转换功能,这使得进程不必关心实际的物理地址在哪里,只需要对自己的地址空间进行操作。同时,对于一个进程来说,每次访问内存时并不是访问整个虚拟内存空间,因此进程实际需要占用的物理内存大小可以小于其虚拟地址空间的大小,由操作系统来决定要把哪一部分留在内存中,将剩余部分保存在磁盘中,在需要时再加载进入内存,极大的扩展了可用内存空间。
程序局部性原理,是计算机科学术语,指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。局部性原理又表现为:时间局部性和空间局部性。
- 时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
- 空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。
这样的由 MMU 创建的并负责维护的由虚拟地址指向物理地址的映射也将成为一项存储在一个物理页帧中,MMU 为了访问这样的物理页帧也需要一个根页表,根页表中存储着指向这些物理页帧的页表项(PTE),称为叶子 PTE。一个 PTE 的长度一般为 64 Bit(8 Bytes),而每一个一般物理页帧的大小为 4KB,这也就意味着一个物理页帧最多可以存储 4KB/8B = 2^9 个 PTE,因此根页表可以索引的范围即为 2^9 × 4KB = 2MB。2MB 的页表并不能满足内存日益增大的需要,在香山中实现的 SV48 即采用了四级页表的形式,通过四级的查询最终得到物理地址,每一级页表都能够索引 2^9 个下一级页表,最终找到需要的映射。四级页表下能够索引的地址范围达到了 2^9 × 2^9 × 2^9 × 2MB = 256TB。而页表本身也会比较大,如果存满的话大小会达到 4KB + 2^9 × 4KB + 2^9 × 2^9 × 4KB + 2^9 × 2^9 × 2^9 × 4KB = 537921540KB ≈ 513GB。当然,不是说每一级页表都要填满,页表的四级结构可以理解为一个多叉树形结构,只有需要用到的才会实际使用,很多的分支都不需要使用,因此页表的大小是可变的。
页表一般很大,需要存放在内存中,而处理器每一次访问内存的请求都需要先访问页表查找对应的物理页号然后再去读取所需数据,因此在不发生缺页的情况下,每次访存操作都需要两次访问内存才能得到物理地址,然后再次访问才能得到需要的数据。为了减少多次访存造成的开销,引入了地址转换后援缓存器(TLB,Translation Lookaside Buffer)。MMU 通常借助 TLB 来进行虚实地址的转换。TLB 一般是相连高速缓存(associative cache),相当于页表的 Cache,负责将最可能会用到的页表项对应的映射(虚拟地址与对应的物理地址)存储下来;在查找页表时首先查找 TLB 内存储的映射,如果没有命中再去查找内存中存储的完整页表。
同 Cache 一样,TLB 中页表项的组织方式一般有直接映射、全相联映射、组相连映射三种方式。直接映射一般通过模运算匹配,例如对昆明湖 48 行的 TLB 来说,其第 1 块只能对应内存的第 1/49/97/…/(n×48+1) 块,硬件结构简单、成本低、转换速度快,但是 TLB 表项利用率低,TLB miss 频繁,只适用于 TLB 大小与页表大小较接近的情况。全相联映射则不同,内存中的所有表项可以存放在 TLB 中的任意一项中,可以充分利用 TLB 的空间,冲突概率更低,但因此查找开销较高,适用于小容量 TLB。组相联映射是一种折中,可以二路组相联、四路组相联等。在香山的 TLB 模块中提供了丰富的参数配置,其中即包括采取哪一种相连方式,可以通过传入参数自行配置。本次验证的 ITLB 即采用 48 项全相联的结构。
香山的 MMU 模块由 TLB、Repeator、L2TLB、PMP&PMA 组成,其中 L2TLB 又包含了 Page Cache、Page Table Walker、Last Level Page Table Walker、Miss Queue 和 Prefetcher。在核内每次进行内存的操作(读写)时都需要通过 MMU 模块进行虚实地址的翻译,而 TLB 将被实例化为 ITLB(前端取指)和 DTLB(后端访存)。以 ITLB 为例,每当 ICache 或 IFU 需要进行取指操作,会先向 ITLB 发送一个地址转换请求,把需要转换的虚拟地址发给 ITLB;ITLB 接收到请求后就要查找自己存储的表项里有没有这个虚拟地址对应的映射,如果有的话就输出对应物理地址(paddr),之后由 PMP&PMA 模块检查对该物理地址的访问是否有效(包括地址是否有效、访问者是否有访问权限、页表属性等,其中对 ITLB 来说由于取出来的物理地址是待执行的指令,需要检查是否可以执行),检查通过后就可以把物理地址返回给前端。如果 ITLB 发现自己没有存储这样的表项,那么立即回应 miss,并同时发起 PTW 请求。前端接收到 miss 信号后会通过一些调度策略重新发起访问,在香山中体现为 miss 后不断重新给 TLB 发请求直到 hit。PTW 请求将交由 Page Table Walker 来执行,通过一些策略访问 L2TLB、Page Cache、内存中的完整页表,之后把访问到的 PTE(页表项)发回给 TLB(如果 PTW 都找不到那么会发生 Page Fault,同样返回给 TLB,TLB 收到 Page Fault 后会上报并由操作系统等从磁盘中加载页面)。TLB 接收到 PTE 的同时将 PTE 填充进自己的缓存中并向前端返回物理地址,前端才能通过该物理地址找到对应的指令。
香山实现了二级 TLB,包括 TLB 与 L2TLB。同样类似于 Cache 与 L2Cache,TLB(一级 TLB)通常是小容量、高速缓存,直接与处理器核心连接,用于加速最近访问过的虚拟地址到物理地址的转换;L2TLB(二级 TLB)容量较大,速度稍慢,但比直接访问内存要快。L2TLB 用来缓存更多的页表项,减少一级 TLB 未命中(TLB Miss)时对内存的频繁访问,香山目前有 1 个 ITLB 和 3 个 DTLB,都与同一个 L2TLB 连接。在这种二级结构下,TLB 未命中时将会首先查找 L2TLB,之后如果再次未命中才去访问内存,可以有效提高地址转换的命中率和性能。由于在 TLB 与 L2TLB 之间有着一定的物理距离,因此在 TLB 向 L2TLB 发出读取请求的时候需要进行加拍,这项工作交给了 MMU 中的 repeater 进行,是 TLB 与 L2TLB 之间的一个请求缓冲。同时,repeator 还需要负责对 TLB 向 L2TLB 发送的请求进行过滤(即 Filter 的功能),把重复的请求过滤掉,以减少 L2TLB 性能损失。
昆明湖架构支持 RISC-V 手册中定义的 Hypervisor 扩展,即 H 扩展。H 扩展为处理器提供了虚拟化的支持,即允许虚拟机运行在主机上,此时虚拟机将与主机共享 TLB,那么在 MMU 中也需要进行相应的调整与支持。TLB 需要能够同时容纳多个虚拟机的条目并做到隔离,同时需要引入 Hypervisor Page Table Walker(HPTW)用于遍历虚拟机的页表。
在 MMU 模块中还需要实现 PMP(Physical Memory Protection)与 PMA(Physical Memory Access)检查,不过这与 TLB 无关,在实现中无论请求是否有效或有足够权限,都会通过 TLB 先进行地址转换,之后再把转换的结果(物理地址)送到 PMP&PMA 模块进行权限检查。
2.1 - IO接口说明
香山实例化 TLB.sv 接口说明(ITLB)
基本控制信号
clock
: 时钟信号,驱动TLB
的时序逻辑。reset
: 复位信号,用于重置TLB
的状态。
刷新(SFENCE)接口信号
io_sfence_valid
:SFENCE
操作的有效性标志。io_sfence_bits_rs1
:SFENCE
操作是否使用寄存器rs1
的值。io_sfence_bits_rs2
:SFENCE
操作是否使用寄存器rs2
的值。io_sfence_bits_addr
:SFENCE
操作指定的地址,用于选择性刷新特定地址的TLB
条目。io_sfence_bits_id
: 刷新操作指定的asid/vmid
,用于选择性刷新特定地址空间的TLB
条目。io_sfence_bits_flushPipe
: 刷新整个管道。io_sfence_bits_hv
: 指示指令是否为HFENCE.VVMA
,即是否刷新虚拟化下由vsatp
寄存器控制的条目。io_sfence_bits_hg
: 指示指令是否为HFENCE.GVMA
,即是否刷新由hgatp
寄存器控制的条目。
控制与状态寄存器(CSR)接口信号
io_csr_satp_mode
:SATP
寄存器的模式字段(如裸模式、Sv32
、Sv39
等)。io_csr_satp_asid
: 当前SATP
寄存器的ASID
(地址空间标识符)。io_csr_satp_changed
: 指示SATP
寄存器的值是否已更改。io_csr_vsatp_mode
:VSATP
寄存器的模式字段。io_csr_vsatp_asid
:VSATP
寄存器的ASID
。io_csr_vsatp_changed
: 指示VSATP
寄存器的值是否已更改。io_csr_hgatp_mode
:HGATP
寄存器的模式字段。io_csr_hgatp_vmid
:HGATP
寄存器的VMID
(虚拟机标识符)。io_csr_hgatp_changed
: 指示HGATP
寄存器的值是否已更改。io_csr_priv_virt
: 指示是否在虚拟模式下运行。io_csr_priv_imode
: 指令模式的特权级(如用户态、内核态等)。
请求者(Requestor)接口信号
Requestor 0 信号
io_requestor_0_req_valid
:requestor0
的请求有效信号。io_requestor_0_req_bits_vaddr
:requestor0
的请求虚拟地址。io_requestor_0_resp_bits_paddr_0
:requestor0
的物理地址响应信号。io_requestor_0_resp_bits_gpaddr_0
:requestor0
的物理地址转换为GPA
(Guest Physical Address
)的响应信号。io_requestor_0_resp_bits_miss
:requestor0
请求的地址未命中的信号。io_requestor_0_resp_bits_excp_0_gpf_instr
:requestor0
出现General Protection Fault
(GPF
) 异常的信号。io_requestor_0_resp_bits_excp_0_pf_instr
:requestor0
出现Page Fault
(PF
) 异常的信号。io_requestor_0_resp_bits_excp_0_af_instr
:requestor0
出现Access Fault
(AF
) 异常的信号。
Requestor 1 信号
io_requestor_1_req_valid
:requestor1
的请求有效信号。io_requestor_1_req_bits_vaddr
:requestor1
的请求虚拟地址。io_requestor_1_resp_bits_paddr_0
:requestor1
的物理地址响应信号。io_requestor_1_resp_bits_gpaddr_0
:requestor1
的GPA
响应信号。io_requestor_1_resp_bits_miss
:requestor1
的未命中信号。io_requestor_1_resp_bits_excp_0_gpf_instr
:requestor1
出现GPF
异常的信号。io_requestor_1_resp_bits_excp_0_pf_instr
:requestor1
出现PF
异常的信号。io_requestor_1_resp_bits_excp_0_af_instr
:requestor1
出现AF
异常的信号。
Requestor 2 信号
io_requestor_2_req_ready
:requestor2
的请求就绪信号。io_requestor_2_req_valid
:requestor2
的请求有效信号。io_requestor_2_req_bits_vaddr
:requestor2
的请求虚拟地址。io_requestor_2_resp_ready
:requestor2
的响应就绪信号。io_requestor_2_resp_valid
:requestor2
的响应有效信号。io_requestor_2_resp_bits_paddr_0
:requestor2
的物理地址响应信号。io_requestor_2_resp_bits_gpaddr_0
:requestor2
的GPA
响应信号。io_requestor_2_resp_bits_excp_0_gpf_instr
:requestor2
出现GPF
异常的信号。io_requestor_2_resp_bits_excp_0_pf_instr
:requestor2
出现PF
异常的信号。io_requestor_2_resp_bits_excp_0_af_instr
:requestor2
出现AF
异常的信号。
刷新管道(Flush Pipe)信号
io_flushPipe_0
: 刷新管道0
的信号。io_flushPipe_1
: 刷新管道1
的信号。io_flushPipe_2
: 刷新管道2
的信号。
页表遍历(Page Table Walker, PTW)接口信号
PTW 请求信号
io_ptw_req_0_valid
:PTW req0
有效信号。io_ptw_req_0_bits_vpn
:PTW req0
的虚拟页号(VPN
)。io_ptw_req_0_bits_s2xlate
: 指示PTW req0
的转换模式。io_ptw_req_0_bits_getGpa
:PTW req0
的获取GPA
信号。io_ptw_req_1_valid
:PTW req1
有效信号。io_ptw_req_1_bits_vpn
:PTW req1
的虚拟页号。io_ptw_req_1_bits_s2xlate
: 指示PTW req1
的转换模式。io_ptw_req_1_bits_getGpa
:PTW req1
的获取GPA
信号。io_ptw_req_2_ready
:PTW req2
就绪信号。io_ptw_req_2_valid
:PTW req2
有效信号。io_ptw_req_2_bits_vpn
:PTW req2
的虚拟页号。io_ptw_req_2_bits_s2xlate
: 指示PTW req2
的转换模式。io_ptw_req_2_bits_getGpa
:PTW req2
的获取GPA
信号。
PTW 响应信号
io_ptw_resp_valid
:PTW resp
有效信号。io_ptw_resp_bits_s2xlate
: 指示PTW resp
的地址转换类型。io_ptw_resp_bits_s1_entry_tag
:PTW resp
的第一阶段页表条目标签。io_ptw_resp_bits_s1_entry_asid
:PTW resp
的第一阶段页表条目ASID
。io_ptw_resp_bits_s1_entry_vmid
:PTW resp
的第一阶段页表条目VMID
。io_ptw_resp_bits_s1_entry_perm_d
:PTW resp
的第一阶段页表条目可写位。io_ptw_resp_bits_s1_entry_perm_a
:PTW resp
的第一阶段页表条目已访问位。io_ptw_resp_bits_s1_entry_perm_g
:PTW resp
的第一阶段页表条目全局位。io_ptw_resp_bits_s1_entry_perm_u
:PTW resp
的第一阶段页表条目用户模式位。io_ptw_resp_bits_s1_entry_perm_x
:PTW resp
的第一阶段页表条目可执行位。io_ptw_resp_bits_s1_entry_perm_w
:PTW resp
的第一阶段页表条目可写位。io_ptw_resp_bits_s1_entry_perm_r
:PTW resp
的第一阶段页表条目可读位。io_ptw_resp_bits_s1_entry_level
:PTW resp
的第一阶段页表条目级别。io_ptw_resp_bits_s1_entry_ppn
:PTW resp
的第一阶段页表条目物理页号(PPN
)。io_ptw_resp_bits_s1_addr_low
:PTW resp
的第一阶段页表条目地址低位。io_ptw_resp_bits_s1_ppn_low_*
:PTW resp
的第一阶段页表条目PPN
低位。io_ptw_resp_bits_s1_valididx_*
:PTW resp
的第一阶段页表条目有效索引。io_ptw_resp_bits_s1_pteidx_*
:PTW resp
的第一阶段页表条目PTE
索引。io_ptw_resp_bits_s1_pf
:PTW resp
的第一阶段页表条目出现PF
。io_ptw_resp_bits_s1_af
:PTW resp
的第一阶段页表条目出现AF
。io_ptw_resp_bits_s2_entry_tag
:PTW resp
的第二阶段页表条目标签。io_ptw_resp_bits_s2_entry_vmid
:PTW resp
的第二阶段页表条目VMID
。io_ptw_resp_bits_s2_entry_ppn
:PTW resp
的第二阶段页表条目PPN
。io_ptw_resp_bits_s2_entry_perm_*
:PTW resp
的第二阶段页表条目的权限位。io_ptw_resp_bits_s2_entry_level
:PTW resp
的第二阶段页表条目级别。io_ptw_resp_bits_s2_gpf
:PTW resp
的第二阶段页表条目GPF
信号。io_ptw_resp_bits_s2_gaf
:PTW resp
的第二阶段页表条目GAF
信号。io_ptw_resp_bits_getGpa
:PTW resp
的获取GPA
信号。
2.2 - 功能详述
支持 SV48 分页机制
SV48
(Supervisor-mode Virtual Memory
)是一种基于 RISC-V
的页表虚拟内存寻址模式,指定了 48
位虚拟地址空间的结构,支持 256TB
的虚拟内存地址空间。使用四级页表结构:
在 SV48
的一个 PTE
中包含了如下字段:
-
N
:- 指示是否为
NAPOT PTE
。供Svnapot
扩展使用,如果未实现Svnapot
则该位必须由软件置0
,否则应当出现Page Fault
。目前香山昆明湖架构尚未支持此扩展。
- 指示是否为
-
PBMT
:Page-Based Memory Types
,即基于页面的内存类型,供Svpbmt
扩展使用,允许操作系统为每个页面指定不同的内存访问属性。0
:None
,没有特定的内存属性。1
:NC
,非缓存、幂等、弱序(RVWMO
),适用于主存。2
:IO
,非缓存、非幂等、强序(I/O
排序),适用于I/O
设备。3
:Reserved
,保留供将来标准使用。
同样的,如果未实现
Svpbmt
则这两位必须由软件置0
,否则应当出现Page Fault
。 -
Reserved
:- 保留位,供未来的标准使用。如果有任意一位不是
0
则会触发PF
异常。
- 保留位,供未来的标准使用。如果有任意一位不是
-
PPN
:- 表示物理页框号,指向实际的物理内存页。
PPN
与页面内偏移结合形成完整的物理地址,用于地址转换。
- 表示物理页框号,指向实际的物理内存页。
-
RSW
:- 保留供软件使用的位,通常用于特定的标志或操作,以便在软件实现中提供灵活性。
-
D
:- 脏位,指示该页面是否被写入。如果该位为
1
,表示该页的数据已被修改,需在换出时写回到存储设备。
- 脏位,指示该页面是否被写入。如果该位为
-
A
:- 访问位,指示该页是否被访问过。如果该位为
1
,表示该页已被读取或写入,用于页面替换算法。
- 访问位,指示该页是否被访问过。如果该位为
-
G
:- 全局页位,指示该页是否是全局页。如果该位为
1
,表示该页对所有进程可见,用于共享代码或数据。
- 全局页位,指示该页是否是全局页。如果该位为
-
U
:- 用户访问权限位,指示该页是否可被用户(
U
)模式访问。如果该位为1
,用户模式可以访问该页;若为0
,则仅限于特权模式。
- 用户访问权限位,指示该页是否可被用户(
-
X
:- 可执行位,指示该页是否可执行。如果该位为
1
,表示该页可以执行代码;若为0
,则不可执行。
- 可执行位,指示该页是否可执行。如果该位为
-
W
:- 可写位,指示该页是否可写。如果该位为
1
,表示该页可以写入数据;若为0
,则不可写。
- 可写位,指示该页是否可写。如果该位为
-
R
:- 可读位,指示该页是否可读。如果该位为
1
,表示该页可以读取数据;若为0
,则不可读。
- 可读位,指示该页是否可读。如果该位为
-
V
:- 有效位,指示该页表项是否有效。如果该位为
1
,表示该项有效,可以进行地址转换;若为0
,则表示该项无效。
- 有效位,指示该页表项是否有效。如果该位为
值得一提的是,如果该 PTE
并不是叶子 PTE
,即它所存储的 PPN
用来指向下一级页表,那么它的 X
、W
、R
位应全为零。在手册中的要求如下:
RISC-V H
扩展即 Hypervisor
扩展,增加了对虚拟化和 hypervisor
模式的支持,将会允许虚拟机监控程序和虚拟机的管理程序,允许操作系统运行在虚拟机上,并可以通过 hypervisor
调度虚拟机的运行。在 hypervisor
下使用 SV48x4
寻址模式,支持四倍页表扩展。
VPN[3]
进行了两位的扩展,也即大小从原来的 4KB
变为 16KB
,支持 $2^{11}$ 个 PTE
。值得注意的是,SV48x4
作用于虚拟机物理地址 VPA
,在虚拟机上创建进程地址空间时仍然采用的是 SV48
。也正是因此,虚拟机进行虚实地址转换的时候,首先将 48
位的虚拟机虚拟地址(GVA
)转换为 50
位的虚拟机物理地址(GPA
),之后再将 GPA
(相当于主机的 HVA
)转换为主机物理地址(HPA
)。在页表项中存储的是 44
位的 PPN
,这是由 56
位的物理地址去掉 12
位的页内偏移得到的,因此完全可以存的下扩展了两位(38
位)的 VPN
。
出于对面积等的优化考虑,在香山中采用 48
位的主机物理地址,而不是 Sv48
要求的 56
位物理地址,这是因为 48
位的物理地址已经可以索引 256TB
的物理地址空间,目前来说已经足够使用。但是由于 TLB
对虚拟机的支持,在虚拟机两阶段地址转换过程中(两阶段地址转换可见支持两阶段虚实地址翻译过程部分),虚拟机通过 VS
阶段转换的结果仍然是 56
位的虚拟机物理地址,只不过在进入 G
阶段地址转换时,G
阶段要求传入的 GPA
的高 6
位必须为 0
,这是因为在 Sv48x4
中客户机物理地址要求为 50
位,而 VS
阶段得到的物理地址是 56
位。为了保持 gpaddr
的完整性,PTW
传入 TLB
的 ppn
信号的位宽依然为 44
位,然而由于 TLB
不存储中间转换结果(中间物理地址 IPA
),也就不需要存储 44
位的 ppn
,在 TLB
表项中存储的只有主机的 ppn
,也即 36
位的 ppn
。
支持缓存映射条目
TLB
中存储的条目并不是页表项 PTE
,而是一个映射,一个从虚拟地址(来自于请求)到物理地址(来自于查找结果)的映射,当然还有一些访问所必须的信息。在目前的香山中 TLB
所存储的条目包含 tag[35]
、asid[16]
、vmid[14]
、level[2]
、ppn[33]
、8 × ppn_low[3]
、8 × valididx
、8 × pteidx
、s2xlate
、perm[6]
、g_perm[4]
。为供以后使用 svpbmt
扩展,还存储了 pbmt
与 g_pbmt
字段。
-
tag[34:0]
tag
,用于匹配条目。来源于VPN
的高35
位,在匹配的过程中,输入一个38
位的VPN
,通过将输入的VPN
的前35
位与tag
比较找到对应的条目,可以看到在一个条目中存储了PPN
的高位部分和8
个ppn_low
,之后将VPN
的后三位作为索引,可以索引这8
个ppn_low
,即可将ppn
与ppn_low[vpn_low]
拼接得到物理页框号。
-
asid[15:0]
- 地址空间标识符,用于区分不同的进程地址空间。
-
vmid[13:0]
- 虚拟机标识符,用于区分不同的虚拟机。
-
level[1:0]
- 指示页面的大小。
0
:4KB
,1
:2MB
,2
:1GB
,3
:512GB
。
- 指示页面的大小。
-
ppn[32:0]
- 物理页框号的高
33
位。在Sv48
要求下本该是41
位,出于面积考虑优化至33
位(见支持Sv48
分页机制部分)。
- 物理页框号的高
-
ppn_low[2:0]×8
- 物理页框号的低
3
位。用于TLB
压缩(见支持TLB
压缩部分)。
- 物理页框号的低
-
valididx×8
- 指示对应的
ppn_low
是否有效。用于TLB
压缩,为0
表示条目无效,即对应物理地址没有存储页表条目。
- 指示对应的
-
pteidx×8
- 指示原始请求对应压缩条目的哪一项。例如
vpn
低三位为010
,那么pteidx[3]
为1
,其它7
位为0
。
- 指示原始请求对应压缩条目的哪一项。例如
-
s2xlate[1:0]
- 指示是否启用两阶段地址转换。
0b00
:不启用,0b01
:仅使用第一阶段,0b10
:仅使用第二阶段,0b11
:启用两阶段地址转换。
- 指示是否启用两阶段地址转换。
-
perm[5:0]
- 指示主机的权限以及异常信息,包括
pf
、af
、a
、g
、u
、x
六位。其中pf
(page fault
)指示是否发生缺页异常;af
(access fault
)指示是否发生地址错误等访问错误异常;a
(access
)指示该表项是否最近被访问过,任何形式的访问(包括读、写、取指)均会将a
位置1
,用于页面替换算法;g
(global
)指示该条目指向的页面是否为全局页面;u
(user
)指示该条目指向的页面是否可以被用户模式访问,u
位为1
说明可以被UMode
访问,为0
说明可以被SMode
访问;x
(执行)指示该条目指向的页面是否可执行,itlb
用于取指的加速,所有取出的条目必须是可执行的。
- 指示主机的权限以及异常信息,包括
-
g_perm[3:0]
- 指示虚拟机的权限以及异常信息,包括
gpf
、gaf
、a
、x
四位,虚拟机的g
和u
两位不单独存储,与主机共用。一般情况下虚拟机对全局页、用户模式的处理与主机相同,而替换策略与访问权限控制可能不同,所以共用了g
、u
而不共用a
、x
。gpf
(guest page fault
)为虚拟机缺页异常,gaf
(guest access fault
)为虚拟机访问错误异常。
- 指示虚拟机的权限以及异常信息,包括
香山的 ITLB
采用 48
项全相联的结构,保存全部大小页,共能存储 48
条映射。
在支持 H
扩展的前提下,对于不同的 s2xlate
的状态 TLB
中存储的条目的值代表的意义也会有所区别:
类型 | s2xlate | tag | ppn | perm | g_perm | level |
---|---|---|---|---|---|---|
noS2xlate | b00 | 非虚拟化下的虚拟页号 | 非虚拟化下的物理页号 | 非虚拟化下的页表项 perm | 不使用 | 非虚拟化下的页表项 level |
allStage | b11 | 第一阶段页表的虚拟页号 | 第二阶段页表的物理页号 | 第一阶段页表的 perm | 第二阶段页表的 perm | 两阶段翻译中最大的 level |
onlyStage1 | b01 | 第一阶段页表的虚拟页号 | 第一阶段页表的物理页号 | 第一阶段页表的 perm | 不使用 | 第一阶段页表的 level |
onlyStage2 | b10 | 第二阶段页表的虚拟页号 | 第二阶段页表的物理页号 | 不使用 | 第二阶段页表的 perm | 第二阶段页表的 level |
支持 H
扩展后 TLB
中缓存的条目会有所变化(表中未提及的条目即没有变化):
支持 H 扩展 | vmid | s2xlate | g_perm |
---|---|---|---|
否 | 不保存 | 不保存 | 不保存 |
是 | 14位 | 2位 | 4位 |
支持保存全部大小页
在 RISC-V
架构中,大小页机制旨在优化虚拟内存的使用效率和性能。Sv48
支持多种页面大小,包括 4KB
、2MB
、1GB
页,在标准的设计中没有定义 512GB
的页,理论上可行,但目前并没有这样的需要,512GB
的页也无法加载进内存,因此标准不做要求。但是出于对完整性的考虑,香山中依然实现了对 512GB
大页的支持。
在一般的应用程序需求中,4KB
的页面足够满足日常的使用,可以存储较小的数据结构以及程序等,常用于大多数应用程序中。然而,有的程序可能会需要频繁访问大的数据结构或数据集,这时引入大页可以提升内存访问效率。每个大页覆盖的虚拟地址空间更大,可以显著减少页表条目的数量;在映射相同数量的内存时,所需的页表条目会大幅降低,这可以减少内存开销、减少页表查找频率,从而优化内存访问速度,尤其对频繁访问大块内存的应用,能够显著提升性能。大页通常包含连续的数据,可以提高命中率,更有效地利用缓存资源。
当然,由于大页覆盖的地址空间较大,可能导致内存碎片,而未被使用的大页空间无法被其他请求有效利用,也会浪费一定的内存资源。同时,管理不同大小的页面为内存管理带来了额外的复杂性。在混合使用小页和大页时,操作系统需要复杂的算法来优化内存分配和使用。现代操作系统通常采用混合使用大小页的模式以满足不同应用的不同需求。
在香山的 TLB
中,支持保存任意大小的页面,这是通过保存页面的 level
来实现的。根据不同的 level
,可以决定最终生成物理地址的方法(index
为页内偏移,来源于 vaddr
的低 12
位;ppn
、ppn_low
、tag
来源于 TLB
中存储的映射条目):
level | 页面大小 | paddr[47:0] |
---|---|---|
0 | 4KB | ppn[32:0] + ppn_low[2:0] + index[11:0] |
1 | 2MB | ppn[32:6] + tag[8:0] + index[11:0] |
2 | 1GB | ppn[32:15] + tag[17:0] + index[11:0] |
3 | 512GB | ppn[32:24] + tag[26:0] + index[11:0] |
支持 TLB 压缩
随着虚拟地址空间的不断扩展,传统 TLB
的大小和效率面临挑战,可能不足以覆盖应用程序的需求,导致频繁的缺失(TLB miss
),从而影响系统性能,导致性能瓶颈。为了应对这一问题,TLB
压缩技术应运而生,旨在提高 TLB
的有效性和性能。
在操作系统分配内存的时候,由于使用伙伴地址分配策略等原因,会倾向于将连续的物理页分配给连续的虚拟页。虽然随着程序的不断运行,页分配逐渐的从有序趋向于无序,但是这种页的相连性普遍存在,因此可以通过将多个连续的页表项在 TLB
硬件中合成为一个 TLB
项,以增大 TLB
容量。TLB
压缩通过优化页表结构,支持连续的映射,通过引入范围映射(range mapping
)机制,一个 TLB
条目可以映射一段连续的虚拟地址到一段连续的物理地址。
在实际中,以香山昆明湖架构为例,在 TLB
中存储 35
位的 vpn_high
(即 tag
),剩下的三位用于索引对应的 ppn_low
(一共有 8
个所以需要 3
位来索引)。每次匹配中,TLB
用传入的 vaddr[49:15]
(高 35
位)与 tag
进行匹配,找到对应的条目,这个条目中可以存储 8
个 PTE
,再根据 vaddr[14:12]
找到对应的 ppn_low
,之后检查对应的 valididx
是否有效,如果有效说明 hit
,将 ppn_low
与 ppn_high
拼接得到 PPN
,再与 vaddr[11:0]
拼接得到 paddr
。
在支持了 H
扩展后(见支持两阶段虚实地址翻译),TLB
压缩仅在 OnlyStage1
和 noS2xlate
下启用,在其他情况下不启用。
支持 TLB
压缩后 TLB
中缓存的条目会有所变化(表中未提及的条目即没有变化):
是否压缩 | tag | ppn | valididx | pteidx | ppn_low |
---|---|---|---|---|---|
否 | 38位 | 36位 | 不保存 | 不保存 | 不保存 |
是 | 35位 | 33位 | 8位 | 8位 | 8×3位 |
在支持了大小页的情况下,TLB
压缩在大页情况下(2MB/1GB/512GB
)不启用,仅在查询结果为小页(4KB
)情况下启用。大页在返回时会将 valididx
的 8
位全部设置为 1
,而由于大页的查询过程中只需要 PPN
的高位,大页下不使用 ppn_low
,ppn_low
的值在此时是未定义的。
支持 Hypervisor 扩展与两阶段虚实地址翻译
在 RISC-V
特权指令手册中定义了虚实地址的翻译过程:
-
设
a
为satp.ppn
×PAGESIZE
,并设i = LEVELS - 1
。(对于Sv48
,PAGESIZE = 2^{12}
,LEVELS = 4
)此时,satp
寄存器必须处于活动状态,即有效的特权模式必须是S
模式或U
模式。 -
设
pte
为地址a + va.vpn[i]
×PTESIZE
处的PTE
值。(对于Sv48
,PTESIZE = 8
)如果访问pte
违反了PMA
或PMP
检查,则引发与原始访问类型相应的访问错误异常。 -
如果
pte.v = 0
,或者pte.r = 0
且pte.w = 1
,或者pte
中设置了任何为未来标准使用保留的位或编码,则停止并引发与原始访问类型相应的页面错误异常。 -
否则,
PTE
是有效的。如果pte.r = 1
或pte.x = 1
,则转到步骤 5。否则,此PTE
是指向下一级页面表的指针。设i = i - 1
。如果i < 0
,则停止并引发与原始访问类型相应的页面错误异常。否则,设a = pte.ppn
×PAGESIZE
并转到步骤 2。 -
找到了叶子
PTE
。根据当前特权模式和mstatus
寄存器的SUM
和MXR
字段的值,确定请求的内存访问是否被pte.r
、pte.w
、pte.x
和pte.u
位允许。如果不允许,则停止并引发与原始访问类型相应的页面错误异常。 -
如果
i > 0
且pte.ppn[i-1 : 0] = 0
,则这是一个未对齐的大页;停止并引发与原始访问类型相应的页面错误异常。 -
如果
pte.a = 0
,或者如果原始内存访问是存储且pte.d = 0
,则引发与原始访问类型相应的页面错误异常,或者执行以下操作:- 如果对
pte
的存储将违反PMA
或PMP
检查,则引发与原始访问类型相应的访问错误异常。 - 以原子方式执行以下步骤:
- 比较
pte
与地址a + va.vpn[i]
×PTESIZE
处的PTE
值。 - 如果值匹配,将
pte.a
设为1
,并且如果原始内存访问是存储,还将pte.d
设为1
。 - 如果比较失败,返回步骤 2。
- 比较
- 如果对
-
翻译成功。翻译后的物理地址如下:
pa.pgoff = va.pgoff
。- 如果
i > 0
,则这是一个大页翻译,且pa.ppn[i - 1 : 0] = va.vpn[i - 1 : 0]
。 pa.ppn[LEVELS - 1 : i] = pte.ppn[LEVELS - 1 : i]
。
在一般的虚实地址翻译过程中,将按照如上所述的过程进行转换,由 satp 寄存器控制进行地址翻译。其中,前端取指通过 ITLB
进行地址翻译,后端访存通过 DTLB
进行地址翻译。ITLB
和 DTLB
如果 miss
,会通过 Repeater
向 L2TLB
发送请求。在目前设计中,前端取指和后端访存对 TLB
均采用非阻塞式访问,即一个请求 miss
后,会将请求 miss
的信息返回,请求来源调度重新发送 TLB
查询请求,直至命中。也就是说,TLB
本体是非阻塞的,可以向它连续发送请求,无论结果都可以在下一拍发送任意的请求,但是总体上由于前端的调度,体现为阻塞访问。
在支持了 H
扩展的前提下,香山的虚拟化地址翻译过程会经历两个阶段的地址转换,可以将它划分为 VS
阶段和 G
阶段。VS
阶段的地址转换由 vsatp
寄存器控制,其实与主机的 satp
寄存器非常相似。
页表项(PTE
)的长度为 64 bit
,也即每个 4KB
的页面可以存储 $2^9$ 个 PTE
。在 vsatp
寄存器中存储了第一级页表(即根页表)的物理地址 PPN
,通过这个 PPN
可以找到根页表,并根据 GVA
中的 VPN[3]
找到对应页表项 PTE
,在 PTE
中存储了指向下一级页表的 PPN
以及权限位等。以此方式通过逐级的查找最终达到叶子 PTE
并得到 PPN
,与 offset
合成后得到 GPA
。注意这里的 GPA
应当是 50
位的,最后一级的 PPN
应当是 38
位的,这是因为支持 SV48x4
的原因,虚拟机的物理地址被拓宽了两位。这样的拓宽并不难实现,只需要在主机分配虚拟机内存空间的时候分配一个 16KB
的大页作为根页表即可;通过多使用 12KB
(本来分配的根页表大小是 4KB
)的物理内存就可以实现虚拟机地址空间增大四倍。至于页表项能否放下多了两位的 PPN
,观察 PTE
中 PPN
的位数为 44
位,不需要担心这个问题。44
位的 PPN
放 38
位,前六位并没有清零要求,但是是被忽略的。
G
阶段的地址翻译则不同,由于支持了 SV48x4
,其根页表被扩展为 11
位 16KB
,因此需要特别注意 hgatp
寄存器中存储的 PPN
应当对齐 16KB
页,在标准情况下 PPN
的最后两 bit
应当被固定为零,意味着 hgatp
寄存器应当指向一个 16KB
页的起始地址,以避免根页表在不同的小页面内。
在实际的实现中,地址的翻译并不是这样理想化的先查虚拟机的页表得到 GPA
再使用这个 GPA
查主机的页表得到 HPA
。事实上的实现中,我们通过虚拟机的页表查到的下一级页表的物理地址是 GPA
,并不能通过它访问到下一级页表,每次访问虚拟机的下一级页表都需要进行一次 GPA
到 HPA
的转换。比如此时给定一个 GVA
,之后在虚拟机的一级页表(根页表)中根据 GVA[2]
(11 bit
)查找得到一个 PTE
,这个 PTE
存储的是二级页表的 GPA
,得到这个 GPA
并不能找到二级页表,因此需要将它转换为 HPA
,也就是 G
阶段的地址翻译。依次查找直到找到最终需要的那个 HPA
,共需要经历五次 G
阶段地址翻译,才能得到最终的 HPA
。
支持阻塞式与非阻塞式访问
阻塞式访问代表着 TLB
的端口同时仅支持一个请求,阻塞端口带 valid-ready
握手信号。在 TLB
准备好接收请求时,会将 ready
置 1
,由外部检测到 ready
后会发送请求。请求到达 TLB
时 valid
为 1
则 TLB
接收请求并将 ready
置 0
,不再接受新的请求。之后 TLB
会对请求进行匹配,查找结果,如果 miss
则发送 ptw
请求(同样为阻塞),等待直到 ptw
返回结果(物理地址或 pf
异常),然后 TLB
将结果保存并上报给请求方,再将 ready
置 1
。
对于非阻塞式请求,仅带 valid
信号,每当 valid
置 1
,TLB
即接受请求并在下一拍返回结果(hit/miss/异常
),无论是否命中都能在请求下一拍得到结果。如果 miss
的话,TLB
在返回 miss
结果同时会发起 PTW
请求(非阻塞),PTW
接收到请求则进行处理,在处理完成后回填进 TLB
中,然后如果请求方再次发起请求就可以命中。在香山 ITLB
的具体实现中,TLB
本体虽然是非阻塞的,不存储请求的信息,但当前端发起的取指请求 miss
后,将会由前端进行调度不断发起相同取指请求直到 hit
,才能将指令送到处理器进行处理,因此会体现出阻塞的效果。
请求来源 | iCache | IFU |
---|---|---|
请求数量 | 2 | 1 |
请求类型 | 非阻塞请求 | 阻塞请求 |
握手信号 | 仅带 valid 信号 | 带 valid 和 ready 信号 |
处理方式 | 可以继续处理其他指令 | 等待 iTLB 响应后继续处理指令 |
支持读取 PTW 返回条目
每次 TLB
发生 miss
之后,会向 L2TLB
发送 Page Table Walk
请求。由于 TLB
与 L2TLB
之间有比较长的物理距离,需要在中间加拍,这项工作由 repeator
完成。同时,repeator
还需要对 PTW
请求进行过滤,以避免 TLB
中出现重复项,因此也被称为 filter
。目前香山中 TLB
发出的 PTW
请求的内容包含 VPN
、s2xlate
、getGPA
三个信号以及必要的控制信号:
-
VPN:
- 虚拟页框号,
TLB
在miss
之后会将VPN
发送给PTW
用于索引对应的物理页,PTW
会将叶子页表的PPN
返回给TLB
,下次TLB
查询的时候就可以找到该页并可以通过页内偏移找到物理地址。
- 虚拟页框号,
-
s2xlate:
- 两阶段地址转换标志,指示当前的两阶段地址转换模式。
TLB
中该标志将通过vsatp
与hgatp
寄存器的mode
域进行判断:
s2xlate vsatp.mode hgatp.mode 0b00 0 0 0b01 1 0 0b10 0 1 0b11 1 1 - 两阶段地址转换标志,指示当前的两阶段地址转换模式。
-
getGPA:
- 指示当前
PTW
请求是否为请求客户机物理地址。用于客户机缺页等情况的处理(详见支持发生GPF
时重新发起请求部分)。
- 指示当前
在支持了 TLB
压缩后,PTW
返回的结果主要包括 resp_valid
、tag[33:0]
、asid[15:0]
、perm[6:0]
、level[1:0]
、ppn[35:0]
、addr_low[2:0]
、ppn_low[2:0]
× 8
、valididx
× 8
、pteidx
× 8
、pf
、af
(各个信号的含义可见支持缓存映射条目部分)。TLB
接收到有效的 PTW resp
后即将这些条目存进自己的缓存中。
在支持了 H
扩展后,TLB
压缩仅在 noS2xlate
和 onlyStage1
时启用,需要添加 s2xlate
信号指示两阶段地址转换的类型,并分开返回 s1
和 s2
。其中,s1
阶段可以与之前的主机地址转换合并,在主机地址转换中,s1
添加的部分信号以及位宽不适用,添加或扩充的信号如下所示:
支持 H 扩展 | s2xlate[1:0] | tag | vmid[13:0] | ppn | s2 | getGPA |
---|---|---|---|---|---|---|
否 | 无 | [32:0] | 无 | [32:0] | 无 | 无 |
是 | 有 | [34:0] | 有 | [40:0] | 有 | 有 |
其中,tag
扩充的两位是由于虚拟机采用 Sv48x4
,将 hypervisor
下的虚拟地址从 48
位扩充为 50
位,因此 tag
相应需要多两位。vmid
指示虚拟机号。ppn
多的 8
位是因为主机采用 48
位物理地址,而第一阶段转换出来的虚拟机物理地址为 56
位(在进入下一阶段时要求前 6
位是 0
,变为 50
位),getGPA
可见后面支持发生 GPF
时重新发起请求部分。
s2
部分用于第二阶段地址转换,即从虚拟机物理地址到主机物理地址的转换,此时 asid
无效,resp
的信号包括 tag[37:0]
、vmid[13:0]
、ppn[37:0]
、perm[6:0]
、level
、gpf
、gaf
。由于不考虑 TLB
压缩,tag
即为 38
位,来自 50
位虚拟地址的高 38
位。值得注意,在目前的香山昆明湖架构中,这里的 ppn
有效的位数仅有后 36
位,之所以 ppn
位宽为 38
位是出于优化的需要,香山 TLB
中通过 readResult
方法从 PTW
中读取信息,s1
、s2
阶段复用了 readResult
方法,由于在 s1
阶段的需要用到 50
位的物理地址,readResult.ppn
被定义为 38
位,以至于在 verilog
文件中传入 s2.ppn
时也需要额外多传 2
位,事实上这两位仅仅传进 TLB
中而不起作用,可以忽略。
支持 H 扩展 | s2_tag[37:0] | s2_vmid[14:0] | s2_ppn[37:0] | s2_perm[6:0] | s2_level[1:0] | gpf | gaf |
---|---|---|---|---|---|---|---|
否 | 无 | 无 | 无 | 无 | 无 | 无 | 无 |
是 | 有 | 有 | 有 | 有 | 有 | 有 | 有 |
添加了 H
拓展后的 MMU
,PTW
返回的结构分为三部分,第一部分 s1
是原先设计中的 PtwSec-torResp
,存储第一阶段翻译的页表,第二部分 s2
是 HptwResp
,存储第二阶段翻译的页表,第三部分是 s2xlate
,代表这次 resp
的类型,仍然分为 noS2xlate
、allStage
、onlyStage1
和 onlyStage2
。如下图。其中 PtwSectorEntry
是采用了 TLB
压缩技术的 PtwEntry
。
支持回填条目与两阶段条目融合
参照支持缓存映射条目与支持读取 PTW
返回条目,对于主机地址转换(nos2xlate
)的情况对应填入 entry
中的对应表项即可,此时访客有关信号无效。注意大页时,即 level
不为 0
时,ppn_low
无效。
TLB entry | 填入的来自 PTW 的信号 |
---|---|
s2xlate[1:0] | 0b00 (nos2xlate) |
tag[34:0] | s1.tag[34:0] |
asid[15:0] | s1.asid[15:0] |
vmid[13:0] | 无效 |
level[1:0] | s1.level[1:0] |
ppn[32:0] | s1.ppn[32:0] |
ppn_low[2:0]×8 | s1.ppn_low_* |
valididx×8 | s1.valididx_* |
pteidx×8 | s1.pteidx_* |
perm_pf | s1.pf |
perm_af | s1.af |
perm_a | s1.perm.a |
perm_g | s1.perm.g |
perm_u | s1.perm.u |
perm_x | s1.perm.x |
gperm_gpf | 无效 |
gperm_gaf | 无效 |
gperm_a | 无效 |
gperm_x | 无效 |
s2xlate=0b00 时填入 TLB entry 示意表 |
在 OnlyStage1
的情况下,主机的异常信号以及部分不可复用的权限位无效,其余均与主机地址转换一致。
TLB entry | 填入的来自 PTW 的信号 |
---|---|
s2xlate[1:0] | 0b01 (OnlyStage1) |
tag[34:0] | s1.tag[34:0] |
asid[15:0] | s1.asid[15:0] |
vmid[13:0] | s1.vmid[13:0] |
level[1:0] | s1.level[1:0] |
ppn[32:0] | s1.ppn[32:0] |
ppn_low[2:0]×8 | s1.ppn_low_* |
valididx×8 | s1.valididx_* |
pteidx×8 | s1.pteidx_* |
perm_pf | s1.pf |
perm_af | s1.af |
perm_a | s1.perm.a |
perm_g | s1.perm.g |
perm_u | s1.perm.u |
perm_x | s1.perm.x |
gperm_gpf | 无效 |
gperm_gaf | 无效 |
gperm_a | 无效 |
gperm_x | 无效 |
s2xlate=0b01 时填入 TLB entry 示意表 |
对于 OnlyStage2
的情况,asid
无效,vmid
使用 s1.vmid
(由于 PTW
模块无论什么情况都会填写这个字段,所以可以直接使用这个字段写入),pteidx
根据 s2
的 tag
的低 3
位来确定。如果 s2
是大页,那么 TLB
项的 valididx
均为有效,否则 TLB
项的 pteidx
对应 valididx
有效。ppn
的填写复用了 allStage
的逻辑,将在 allStage
的情况下介绍。
TLB entry | 填入的来自 PTW 的信号 |
---|---|
s2xlate[1:0] | 0b10 (OnlyStage2) |
tag[34:0] | s2.tag[37:3] |
asid[15:0] | 无效 |
vmid[13:0] | s1.vmid[13:0] |
level[1:0] | s2.level[1:0] |
ppn[32:0] | s2.ppn[35:3] |
ppn_low[2:0]×8 | { s2.ppn[2:0], 无效×7 } |
valididx×8 | { 1, 0×7 } |
pteidx×8 | s2.tag[2:0] |
perm_pf | 无效 |
perm_af | 无效 |
perm_a | 无效 |
perm_g | 无效 |
perm_u | 无效 |
perm_x | 无效 |
gperm_gpf | s2.gpf |
gperm_gaf | s2.gaf |
gperm_a | s2.perm.a |
gperm_x | s2.perm.x |
s2xlate=0b10 时填入 TLB entry 示意表 |
如果两阶段地址转换均启用,TLB
将两阶段的结果合并存储,并丢弃中间物理地址(s1
阶段的 ppn
),仅存储最终物理地址。level
需要取 s1.level
与 s2.level
中的较大值,此时需要注意,当 s1
阶段为大页,而 s2
阶段为小页的情况下,例如中间物理地址指向一个 2MB
页,而 s2
阶段转换的结果却是一个 4KB
页,在这种情况下,需要特殊处理,将 s1.tag
的高位(在此例子中为高 11+9+9=29
位)和 s2.tag
的低位(在此例子中为低 9
位)共 38
位合并存储到 tag
与 pteidx
中,如果不足 38
位则在后面补 0
(例如中间物理地址指向 1GB
页而 s2
阶段指向 2MB
页,此时 tag[34:0] = {s1.tag[34:15], s2.tag[17:9], 6'b0}
)。在这种情况(s1
大页 s2
小页)下 ppn
也需要处理后存储,根据 s2.level
将 s2.ppn
与 s2.tag
进行拼接后存储。
TLB entry | 填入的来自 PTW 的信号 |
---|---|
s2xlate[1:0] | 0b11 (allStage) |
tag[34:0] | 根据策略选择 s1.tag/s2.tag 的部分位 |
asid[15:0] | s1.asid |
vmid[13:0] | s1.vmid |
level[1:0] | s1.level 与 s2.level 的较大者 |
ppn[32:0] | s2.ppn 与 s2.tag 根据 s2.level 的拼接的高位 |
ppn_low[2:0]×8 | s2.ppn 与 s2.tag 根据 s2.level 的拼接的低位 |
valididx×8 | 根据 level 确定 |
pteidx×8 | tag 的低位 |
perm_pf | s1.pf |
perm_af | s1.af |
perm_a | s1.perm.a |
perm_g | s1.perm.g |
perm_u | s1.perm.u |
perm_x | s1.perm.x |
gperm_gpf | s2.gpf |
gperm_gaf | s2.gaf |
gperm_a | s2.perm.a |
gperm_x | s2.perm.x |
s2xlate=0b11 时填入 TLB entry 示意表 |
支持发生 GPF 时重新发起 PTW 请求
在香山的 TLB
中并不会保存中间物理地址。在两阶段地址转换过程中,如果第一阶段发生缺页异常,即 PTW
返回 gpf
,此时 TLB
将 PTW
返回的结果存入 TLB
项内,请求方再次请求的时候发现 gpf
,此时 TLB
会返回 miss
,即使已经存储了这个映射。同时,TLB
将发起带 getGPA
标志的 PTW
请求,请求这个虚拟地址,并维护一组寄存器暂存相关信号:
信号 | 作用 |
---|---|
need_gpa | 表示此时有一个请求正在获取 gpaddr |
need_gpa_robidx | 存储请求的 ROB(Reorder Buffer)索引,用于跟踪请求来源,目前未使用 |
need_gpa_vpn[37:0] | 存储请求的 vpn,即 50 位虚拟地址的高 38 位 |
need_gpa_gvpn[43:0] | 存储获取的 gpaddr 的 gvpn,虚拟机通过转换得到的 56 位虚拟机物理地址的高 44 位,前六位在第二阶段地址转换中被要求为全 0 |
need_gpa_refill | 表示该请求的 gpaddr 已经被填入 need_gpa_gvpn |
每当 TLB
发起带 getGPA
标志的请求时,就会将 need_gpa
置 1
,并将请求的 vpn
填入到 need_gpa_vpn
中,同时将 need_gpa_refill
置 0
。当 PTW
返回结果的时候,TLB
将 PTW resp
中的 vpn
提取出来与 need_gpa_vpn
进行比较,判断是否是对之前 getGPA
请求的回应。如果是,那么将 PTW resp
中的 s2 tag
填入到 need_gpa_gvpn
中并将 need_gpa_refill
置 1
,表示已经获取到需要的 gvpn
。下一次 TLB
接收到相同请求时就可以通过 need_gpa_gvpn
得到 gpaddr
,之后 TLB
会将 need_gpa
置 0
,但保留其它寄存器,因此下次其它的请求发生 gpf
时也可以再次使用相同的 need_gpa_vpn
找到 paddr
而无需再次发起 PTW
请求。
注意这里的 gvpn
是 44
位的,这是由于客户机采用 56
位物理地址,为了维护 gpaddr
的完整性,所以在这里需要存储 44
位的 gvpn
,但是事实上 gvpn
的前 6
位一定会是 0
,否则说明第一阶段产生了错误的物理地址,会触发 gpf
,在此时需要将错误信息保存在 mtval2/htval
寄存器中,因此需要完整的 gpaddr
,正常情况下并不需要。(当发生页面错误时,mtval2
将被填充为生成错误的物理地址,帮助异常处理程序;htval
将被填充为导致异常的虚拟地址,帮助 hypervisor
识别问题)
如果发生了 redirect
,即重定向(可能触发了跳转/分支指令等或发生异常),此时之前的指令可能不会再访问 TLB
,TLB
需要根据 robidx
跟踪请求来源,有选择性地刷新相关的寄存器(即上表中提到的)。目前香山昆明湖架构中尚未实现,而是通过在需要 redirect
的时候发送 flushPipe
指令来实现的,无论哪一个请求端口被刷新均会导致这些寄存器被刷新。
getGPA
标志并不用于判断指令是否是请求 gpaddr
,PTW
不需要关心请求是干什么的,只需要负责查找并返回结果;TLB
内会通过一系列寄存器的比较来判断。这个信号的作用在于防止 TLB
重填,每次 TLB
发送带 getGPA
标志的请求时,PTW
在返回时会将 getGPA
信号传递回 TLB
,从而使 TLB
不进行重填,不存储此项 gpaddr
。
支持 PLRU 替换算法
LRU
(Least Recently Used
)算法核心思想就是替换掉最近最少使用的页,也就是最长时间没有访问的页。LRU
算法将内存中的每个页组织成了一个链表的形式,如图所示:
链表有两端,一端是最近最少使用的页,可以称为 LRU
端,另一端是最近刚刚使用的页,即最近使用最频繁的页,称之为 MRU
(Most Recently Used
)端。每次访问的时候如果命中,那么就将命中的页移动到 MRU
端,如果 miss
则触发缺页,此时需要加载页面。如果这时候内存已满,那么就需要进行页面替换,选择 LRU
端的页进行替换,并把新访问的页放在 MRU
端。这就是 LRU
替换算法,是 cache
替换的经典算法。
但是由于 LRU
需要为 cache
行维护一个链表数据结构,在多路组相联的 cache
行中需要为每一路配置链表并跟踪每一行的使用时间,LRU
算法有着巨大的开销。因此虽然 LRU
在页面替换中表现出色,也依然不常使用。
在香山的昆明湖架构中,TLB
采用 PLRU
(pseudo-LRU
)替换算法,详细来说是 tree-based PLRU
算法。假设当前 Cache
是 n
路组相联(n
一般是 2
的整数幂)的结构,那么需要定义 n-1
位用来进行二叉树索引,假设为 0
表示左,为 1
表示右,如图所示:
对目前的香山昆明湖架构来说,采用每路 48 cache
行的二路组相联结构下,PLRU
需要维护一个 48
项的链表和一个一级的二叉树(1
位),而采用 LRU
将需要维护一个 48
项的链表和 48
个 2
项的链表,有一定的开销优势,随着路数的增加,优势会更加明显;同时,对二叉树的维护成本也比链表更低。
当然,PLRU
多级二叉树的选择策略下并不能做到与 LRU
一样精确控制,每次二分地排除掉一半不一定能找到绝对 LRU
的条目。
支持 SFENCE.VMA 指令
SFENCE.VMA
指令(Supervisor Memory-Management Fence Instruction
)是定义在 RISC-V
指令架构的指令:
在内存管理中,页表负责将虚拟地址映射到物理地址。当修改了页表后,这些修改不会自动在处理器的缓存中生效。为了确保后续的指令能使用更新后的页表,必须通过 SFENCE.VMA
指令来刷新这些缓存。此外,处理器在执行指令时,可能隐式地对内存管理数据结构进行读取和写入操作,但这些隐式操作和显式的内存操作通常是无序的。SFENCE.VMA
指令可以强制处理器将某些隐式操作在显式操作之前完成,从而确保操作的顺序性。
SFENCE.VMA
是 RISC-V
架构中的一条特权指令,用于刷新与地址翻译相关的本地硬件缓存,处理内存管理数据结构的同步,特别是当需要确保对这些数据结构的修改在不同的硬件组件之间保持一致时需要频繁使用该指令。SFENCE.VMA
只影响本地核心(hart
),如果需要在多个核心之间同步,则需要核间中断等额外机制。虽然 SFENCE.VMA
指令对于维护一致性至关重要,但频繁调用可能会影响系统性能,因此,应根据实际需要合理使用,以平衡一致性和性能之间的关系。
SFENCE.VMA
的行为依赖于 rs1
和 rs2
,在 RISC-V
特权指令集中如下所述:
条件 |
---|
- 如果 rs1=x0 且 rs2=x0 ,栅栏会对所有地址空间的页面表的所有读写进行排序,并将所有地址翻译缓存条目标记为 invalid。 |
- 如果 rs1=x0 且 rs2 不是 x0 ,栅栏会对指定的地址空间的页面表的所有读写进行排序,但不对全局映射进行排序。它还会失效与指定地址空间匹配的地址翻译缓存条目,但不包括全局映射的条目。 |
- 如果 rs1 不是 x0 且 rs2=x0 ,栅栏会对所有地址空间的与 rs1 对应的虚拟地址的叶子页面表条目的读写进行排序,并失效包含该虚拟地址的所有叶子页面表条目的地址翻译缓存条目。 |
- 如果 rs1 不是 x0 且 rs2 不是 x0 ,栅栏会对与 rs1 对应的虚拟地址在指定地址空间的叶子页面表条目的读写进行排序,并失效与 rs1 对应的虚拟地址并匹配指定地址空间的所有叶子页面表条目的地址翻译缓存条目,但不包括全局映射的条目。 |
- 如果 rs1 中的值不是有效的虚拟地址,则 SFENCE.VMA 指令没有效果,且不会引发异常。 |
- 当 rs2=x0 时,rs2 中的值的 SXLEN-1:ASIDMAX 位保留供将来标准使用。在标准扩展定义其用法之前,这些位应由软件置为零并被当前实现忽略。此外,如果 ASIDLEN < ASIDMAX ,则实现应忽略 rs2 中值的 ASIDMAX-1:ASIDLEN 位。 |
SFENCE.VMA
指令的作用是确保在执行该指令之前的所有写入操作已经被提交到内存。这意味着 Store Buffer
中的所有未完成写入都会被写入到 DCache
或最终的内存地址中;SFENCE.VMA
发出刷新信号,通知 MMU
(内存管理单元)更新 TLB
(转换后备缓冲区)等内部状态。这一刷新信号是瞬时的,并且没有返回确认信号。在验证时需要通过再次访问观察是否 miss
的形式来进行,也可以通过分析波形文件观察 TLB
内部寄存器行为。
Store Buffer(存储缓冲区) |
---|
Store Buffer 用于提高内存写入效率,允许 CPU 在发出写入操作后,立即继续执行后续指令,而不需要等待内存系统确认写入完成。这有助于减少 CPU 的闲置时间,提高指令执行的整体效率。写回时,写入数据首先被放入 Store Buffer,随后,数据会按某种策略写入主内存(如 DCache 或其他存储层级)。Store Buffer 维护写入操作的顺序,但不保证这些写入操作立即反映在内存中。在多核处理器中,Store Buffer 可以帮助降低缓存一致性协议的复杂性。 |
支持 HFENCE.VVMA 与 HFENCE.GVMA 指令
事实上,对 hv
(SFENCE Bundle
中的信号,用于刷新第一阶段地址转换的条目)和 hg
(SFENCE Bundle
中的信号,用于刷新第二阶段地址转换的条目)信号不为 0
的情况执行的指令并不是 SFENCE.VMA
,而是 HFENCE.VVMA
和 HFENCE.GVMA
:
这两个指令与 SFENCE.VMA
功能很相似,区别在于 HFENCE.VVMA
适用于由 vsatp
控制的 VS
级别内存管理数据结构;HFENCE.GVMA
适用于由 hgatp
控制的虚拟机监管程序 G
阶段内存管理数据结构。
HFENCE.VVMA
仅在 M
模式或 HS
模式生效,类似于暂时进入 VS
模式并执行 SFENCE.VMA
指令,可以保证当前 hart
之前的所有存储操作在后续的隐式读取 VS
级别内存管理数据结构之前都已经排序;注意这里所说的隐式读取指的仅有在 HFENCE.VVMA
之后执行的,并且 hgatp.VMID
与执行 HFENCE.VVMA
相同的时候,简单来说就是仅对当前这一个虚拟机生效。rs1
与 rs2
的功能与 SFENCE.VMA
相同。
对 HFENCE.GVMA
来说,rs1
指定的是客机的物理地址。由于主机采用 SV48
而虚拟机采用 SV48x4
,客机物理地址比主机物理地址多两位,因此此时需要将 rs1
对应的客机物理地址右移两位。如果某一个虚拟机的地址翻译模式更改了,也即 hgatp.MODE
对某个 VMID
更改了,则必须使用 HFENCE.GVMA
指令,将 rs1
设为 0
,rs2
设为 0
或 VMID
进行刷新。
在香山中,由于 TLB
本身不存储中间物理地址,也即 TLB
并不存储 VS
阶段转换出来的虚拟机物理地址,也无法单独提供 G
阶段地址转换请求。在 TLB
中存储的是两阶段地址翻译的最终结果,因此 HFENCE.VVMA
与 HFENCE.GVMA
在 TLB
中作用相同,均为刷新掉两阶段地址翻译的结果。无论 hv
与 hg
哪一个信号为 1
都将刷新两阶段的条目。
支持 SINVAL 扩展
在 RISC-V
特权指令集中定义了 Svinval
扩展(Supervisor Virtual Address Invalidation
),在香山昆明湖架构实现了该扩展。Svinval
扩展的意义在于将 SFENCE.VMA
指令更加细化为 SFENCE.W.INVAL
、SINVAL.VMA
、SFENCE.INVAL.IR
三条指令(HFENCE.VVMA
和 HFENCE.GVMA
同理)。
SINVAL.VMA
指令事实上与 SFENCE.VMA
指令的功能基本一致,只是添加了对 SFENCE.W.INVAL
与 SFENCE.INVAL.IR
两个指令的相互排序,可以理解为需要在两个指令中间进行。SFENCE.W.INVAL
指令用于确保当前 RISC-V hart
可见的任何先前存储在后续由同一个 hart
执行的 SINVAL.VMA
指令之前被重新排序。SFENCE.INVAL.IR
指令确保当前 hart
执行的任何先前 SINVAL.VMA
指令在后续隐式引用内存管理数据结构之前被排序。当由单个 hart
按顺序(不一定连续)执行 SFENCE.W.INVAL
、SINVAL.VMA
和 SFENCE.INVAL.IR
时,可以相当于执行了 SFENCE.VMA
指令。
支持软件更新 A/D 位
A
位(Access
)用于指示某一页面是否被访问过。如果处理器对该页面进行任何形式的访问(读/写/取指),则 A
位会被设置为 1
。每当 CPU
访问某个页面时,操作系统或硬件会自动将 A
位设置为 1
,这种更新通常是硬件支持的,由处理器在地址转换时自动进行。
D
位(Dirty
)指示页面是否被修改。如果页面在内存中被写入,则 D
位会被设置为 1
,表示该页面的内容已被更改。当处理器对页面进行写操作时,通常会自动将 D
位设置为 1
,这种更新通常也是由硬件支持的。在页面替换过程中,操作系统会检查 D
位,如果 D
位为 1
,操作系统会将页面写回到磁盘,并在写回后清除 D
位,以表示页面已经被保存且不再是“脏”的。
在香山昆明湖架构中,并不支持硬件更新 A/D
位,而是在需要更新的时候通过 Page Fault
通知软件进行页表更新。具体来说,每当处理器访问某一页时检查该页 A
位如果是 0
,那么会发生 PF
;同样的,每当处理器写入某一页时检查该页的 D
位如果是 0
,同样会发生 PF
。在软件处理异常后,操作系统会允许处理器再次访问页面,只有在页表得到更新且相关状态位(A
和 D
位)被正确设置后,处理器才能继续进行后续的内存访问。
2.3 - 关键信号说明
相关 CSR 寄存器
val csr = Input(new TlbCsrBundle)
csr
:包含 satp
、vsatp
、hgatp
三个寄存器的信息以及一些权限信息。
class TlbCsrBundle(implicit p: Parameters) extends XSBundle {
val satp = new TlbSatpBundle()
val vsatp = new TlbSatpBundle()
val hgatp = new TlbHgatpBundle()
val priv = new Bundle {
val mxr = Bool()
val sum = Bool()
val vmxr = Bool()
val vsum = Bool()
val virt = Bool()
val spvp = UInt(1.W)
val imode = UInt(2.W)
val dmode = UInt(2.W)
}
override def toPrintable: Printable = {
p"Satp mode:0x${Hexadecimal(satp.mode)} asid:0x${Hexadecimal(satp.asid)} ppn:0x${Hexadecimal(satp.ppn)} " +
p"Priv mxr:${priv.mxr} sum:${priv.sum} imode:${priv.imode} dmode:${priv.dmode}"
}
}
TlbCsrBundle
中包含了 satp
、vsatp
、hgatp
以及 priv
特权标志。其中 satp
与 vsatp
通过 TlbSatpBundle
实现,包括 mode
、asid
、ppn
、changed
以及一个 apply
方法:
class SatpStruct(implicit p: Parameters) extends XSBundle {
val mode = UInt(4.W)
val asid = UInt(16.W)
val ppn = UInt(44.W)
}
class TlbSatpBundle(implicit p: Parameters) extends SatpStruct {
val changed = Bool()
// Todo: remove it
def apply(satp_value: UInt): Unit = {
require(satp_value.getWidth == XLEN)
val sa = satp_value.asTypeOf(new SatpStruct)
mode := sa.mode
asid := sa.asid
ppn := sa.ppn
changed := DataChanged(sa.asid) // when ppn is changed, software need do the flush
}
}
hgatp
通过 TlbHgatpBundle
实现,区别在于将 asid
替换为 vmid
:
class HgatpStruct(implicit p: Parameters) extends XSBundle {
val mode = UInt(4.W)
val vmid = UInt(16.W)
val ppn = UInt(44.W)
}
class TlbHgatpBundle(implicit p: Parameters) extends HgatpStruct {
val changed = Bool()
// Todo: remove it
def apply(hgatp_value: UInt): Unit = {
require(hgatp_value.getWidth == XLEN)
val sa = hgatp_value.asTypeOf(new HgatpStruct)
mode := sa.mode
vmid := sa.vmid
ppn := sa.ppn
changed := DataChanged(sa.vmid) // when ppn is changed, software need do the flush
}
}
SATP
satp (Supervisor Address Translation and Protection)
用于内核态(Supervisor mode
)进行虚拟地址到物理地址的转换管理,通常在非虚拟化环境或作为虚拟机监控程序(VMM
)时使用。mode
:地址转换模式,控制虚拟地址的转换,位宽为4
。其允许的值包含0
、8
、9
,如果是其它值应当触发illegal instruction fault
。0
:Bare
模式,不进行地址转换。8
:SV39
模式,使用三级页表支持39
位虚拟地址空间。9
:SV48
模式,使用四级页表支持48
位虚拟地址空间。
asid
:地址空间标识符,用于区分不同进程,香山昆明湖架构使用的SV48
中最大长度为16
。ppn
:Page Table Pointer
,根页表的物理页框号,其位宽为44
位,由物理地址右移12
位得到。
VSATP
vsatp (Virtual Supervisor Address Translation and Protection)
是虚拟机中客体操作系统的地址转换寄存器,提供虚拟机的虚拟地址到中间物理地址(IPA
)的转换。mode
:页表模式,控制虚拟地址的转换,模式值与satp
中的类似。asid
:虚拟机内地址空间标识符。ppn
:虚拟机页表的物理基地址。
HGATP
hgatp (Hypervisor Guest Address Translation and Protection)
是虚拟机监控程序(Hypervisor
)的二级地址转换寄存器,用于将虚拟机的中间物理地址(IPA
)转换为主机物理地址(HPA
)。mode
:页表模式,如SV39x4
或SV48x4
,用于虚拟机的二级地址转换。0
:Bare
模式,不进行二级地址转换。8
:SV39x4
模式,即39
位虚拟地址空间,允许四倍页表扩展。9
:SV48x4
模式,即48
位虚拟地址空间,允许四倍页表扩展。
vmid
:虚拟机标识符,区分不同虚拟机。ppn
:二级页表的物理基地址。
satp
管理主机地址空间的虚拟地址到物理地址的转换,vsatp
用于虚拟机中的虚拟地址到中间物理地址(IPA
)的转换,而 hgatp
则负责虚拟机二级地址转换,将 IPA
转换为主机物理地址。
PRIV
-
mxr :
Bool()
机器可执行只读(MXR)位。控制在用户模式下是否允许执行某些在机器层面被标记为只读的页面。 -
sum :
Bool()
特权模式可访问用户(SUM)位。控制特权模式下对用户模式地址的访问权限。 -
vmxr :
Bool()
虚拟机器可执行只读(VMXR
)位。控制虚拟机内的用户是否可以执行只读页面。 -
vsum :
Bool()
虚拟特权模式可访问用户(VSUM
)位。控制虚拟化环境中特权模式对用户模式地址的访问权限。 -
virt :
Bool()
虚拟化状态位。指示当前系统是否处于虚拟化模式。 -
spvp :
UInt(1.W)
超级特权虚拟模式(SPVP
)。指示当前是否处于虚拟化环境中的超级特权模式。 -
imode :
UInt(2.W)
指示当前(ITLB
)指令的处理模式:0x3
: ModeM(机器模式)0x2
: ModeH(虚拟机监控程序模式,已删除)0x1
: ModeS(特权模式)0x0
: ModeU(用户模式)
-
dmode :
UInt(2.W)
指示当前(DTLB
)数据的处理模式。
changed
- 用于标志对应
CSR
中的信息是否更改,一旦Mode
或Asid
(Vmid
)更改则必须同步将changed
置1
,TLB
在检测到changed
为1
时将会执行刷新操作,刷新掉旧的Asid
(Vmid
)的映射。
base_connect()
def base_connect(sfence: SfenceBundle, csr: TlbCsrBundle): Unit = {
this.sfence <> sfence
this.csr <> csr
}
// overwrite satp. write satp will cause flushpipe but csr.priv won't
// satp will be delayed several cycles from writing, but csr.priv won't
// so inside mmu, these two signals should be divided
def base_connect(sfence: SfenceBundle, csr: TlbCsrBundle, satp: TlbSatpBundle) = {
this.sfence <> sfence
this.csr <> csr
this.csr.satp := satp
}
sfence
val sfence = Input(new SfenceBundle)
sfence
:用于传入 SfenceBundle
,执行 SFENCE
指令刷新 TLB
缓存。
class SfenceBundle(implicit p: Parameters) extends XSBundle {
val valid = Bool()
val bits = new Bundle {
val rs1 = Bool()
val rs2 = Bool()
val addr = UInt(VAddrBits.W)
val id = UInt((AsidLength).W) // asid or vmid
val flushPipe = Bool()
val hv = Bool()
val hg = Bool()
}
override def toPrintable: Printable = {
p"valid:0x${Hexadecimal(valid)} rs1:${bits.rs1} rs2:${bits.rs2} addr:${Hexadecimal(bits.addr)}, flushPipe:${bits.flushPipe}"
}
}
valid
- 有效标志信号,指示
SFENCE.VMA
操作的请求是否有效。如果该信号为高(1
),表示当前的SFENCE.VMA
操作需要执行;如果为低(0
),则没有操作需要执行。
rs1
- 表示需要使用
SFENCE.VMA
指令中的rs1
寄存器的值,这个值通过信号addr
传入,标记了需要刷新的虚拟地址。 - 当
rs1
为非零时,表示SFENCE.VMA
只针对该虚拟地址所对应的页表条目进行刷新操作;如果rs1
为零,则表示刷新所有虚拟地址的映射。
rs2
- 表示需要使用
SFENCE.VMA
指令中的rs2
寄存器的值,其中存储着需要刷新的ASID
,通过信号id
传入。 - 当
rs2
为非零时,表示SFENCE.VMA
只对指定的ASID
进行刷新操作;如果rs2
为零,则表示刷新所有地址空间的映射。这个信号主要用于区分不同进程的地址空间。
addr
- 表示
SFENCE.VMA
指令中rs1
对应的虚拟地址(可能是部分地址)。该信号提供了具体的虚拟地址信息,当rs1
为非零时,TLB
将使用该地址作为参考,刷新与该地址对应的页表条目。它用于精细控制哪些地址映射需要被刷新。 - 信号的位宽为
VAddrBits
,即虚拟地址的位宽,可见于 \ref{subsec:consts},大小被定义为50
,其中事实上使用的只有addr[47:12]
,也即四级页表的四级索引部分,用于找到对应虚拟地址的页表项。
id
- 表示
SFENCE.VMA
操作涉及的地址空间标识符(ASID
)。用于指定某个具体的ASID
。它允许在多地址空间的场景下(例如多个进程共享一个处理器),只刷新某个特定进程的地址映射。 - 信号位宽为
AsidLength
,可见于 \ref{subsec:consts},大小为16
,意味着同时支持 $2^{16}$ 个虚拟地址空间。
flushPipe
- 控制是否需要 清空流水线。
SFENCE.VMA
操作不仅可能涉及刷新TLB
,还可能需要清空流水线以确保所有未完成的指令(可能依赖旧的地址映射)不会继续使用过时的页表映射。这个信号为高时,表示需要清空流水线。
hv
- 表示当前指令是否为
HFENCE.VVMA
。
hg
- 表示当前指令是否为
HFENCE.GVMA
。
外部传入参数
参数说明
class TLB(Width: Int, nRespDups: Int = 1, Block: Seq[Boolean], q: TLBParameters)(implicit p: Parameters) extends TlbModule
with HasCSRConst
with HasPerfEvents
参数 | 说明 |
---|---|
Width: Int |
指示 requestor 的数量 |
nRespDups: Int = 1 |
需要复制 response 的数目,默认为 1 (不复制) |
Block: Seq[Boolean] |
指示每个 requestor 是否被阻塞 |
q: TLBParameters |
TLB 使用的参数 |
p: Parameter |
全局参数(香山架构参数) |
实例化 TLB
时以香山架构的 itlb
为例:
val itlb = Module(new TLB(coreParams.itlbPortNum, nRespDups = 1, Seq.fill(PortNumber)(false) ++ Seq(true), itlbParams))
-
Width
值为coreParams.itlbParams
(实际计算逻辑):itlbPortNum: Int = ICacheParameters().PortNumber + 1 // Parameters.scala: line 276 ICacheParameters.PortNumber: Int = 2 // ICache.scala: line 43
最终
Width = 3
-
Block
参数说明:Seq.fill(PortNumber)(false) ++ Seq(true) // 前 2 端口不阻塞,第 3 端口阻塞
对应
itlb
的三个requestor
:requestor0/1
不阻塞,requestor2
阻塞。
VAddrBits
def VAddrBits = {
if (HasHExtension) {
if (EnableSv48)
coreParams.GPAddrBitsSv48x4
else
coreParams.GPAddrBitsSv39x4
} else {
if (EnableSv48)
coreParams.VAddrBitsSv48
else
coreParams.VAddrBitsSv39
}
} // Parameters.scala: line 596~608
// 相关参数定义
def HasHExtension = coreParams.HasHExtension // Parameters.scala: line582
coreParams.HasHExtension: Boolean = true // Parameters.scala: line66
coreParams.EnableSv48: Boolean = true // Parameters.scala: line91
// 地址位宽定义
coreParams.VAddrBitsSv39: Int = 39
coreParams.GPAddrBitsSv39x4: Int = 41
coreParams.VAddrBitsSv48: Int = 48
coreParams.GPAddrBitsSv48x4: Int = 50 // Parameters.scala: line71~74
- 香山昆明湖架构下的值:
50
- 地址处理逻辑:
- 主机地址转换时仅使用后 48 位(前两位忽略)
- 支持虚拟机时,物理地址扩展为 50 位(符合
Sv48x4
规范)
AsidLength
def AsidLength = coreParams.AsidLength // Parameters.scala: line 619
AsidLength: Int = 16 // Parameters.scala: line 79
- ASID 位宽:16 位
- 作用:标识地址空间,防止进程/虚拟机虚拟地址冲突
- 支持规模:
- 最大
65536
个并发进程(16 位) - 虚拟机通过
vmid
标识(14 位,支持16384
个虚拟机,符合手册要求)
- 最大
2.4 - 环境配置
推荐使用WSL2+Ubuntu22.04+GTKWave
我们推荐Windows10/11用户通过WSL2进行开发,在此给出通过此方法进行环境配置的教程集锦,仅供参考。如环境安装过程中出现任何问题,欢迎在QQ群(群号:976081653)中提出,我们将尽力帮助解决。此页面将收集大家提出的所有环境配置相关问题并提供解决方案,欢迎随时向我们提问!
1、在Windows下安装WSL2(Ubuntu22.04)
参考资源:
— 微软官方教程:如何使用 WSL 在 Windows 上安装 Linux
— 其它资源:安装WSL2和Ubuntu22.04版本
2、打开WSL,换源
推荐使用清华源:清华大学开源软件镜像站-Ubuntu软件仓库
3、配置验证环境
请参照开放验证平台学习资源-快速开始-搭建验证环境配置picker环境。
4、使用 GTKWave
使用重庆大学硬件综合设计实验文档-Windows原生GTKWave给出的方法,可以通过在WSL中输入 gtkwave.exe wave.fst
打开在Windows下安装的GTKWave。请注意,gtkwave在使用中需要进入fst文件所在文件夹,否则会出现无法
initialize的情况。
cd out
gtkwave.exe {test_name}.fst
cd ..
5、使用VSCode插件Live Server查看验证报告
成功安装插件Live Server后,打开文件列表,定位到 /out/report/2025*-itlb-doc-*/index.html
右键并选择 Open With Live Server
,之后在浏览器中打开提示的端口(默认为//localhost:5500
)即可。