这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

进度概述

本项目旨在通过开源众包的方式对香山处理器的昆明湖架构进行单元(Unit Test, UT)验证。下图是香山昆明湖架构各个模块验证情况。

查看测试报告

总统计数据如下:

总测试用例数(Total Cases): - 测试用例通过数(Passed Cases): - 测试用例通过率(Passed Rate): -
测试用例未过数(Failed Cases): - 测试用例跳过数(Skipped Cases): - 测试用例跳过率(Skip Rate): -
总功能覆盖点数(Function Coverage): - 覆盖点已覆盖数(Covered Functions): - 覆盖点已覆盖率(Covered Rate): -
总代码行覆盖率(Total Lines): - 总代码行覆盖数(Covered Lines): - 总代码行覆盖率(Covered Rate): -

*总代码行会随着DUT的增加而不断增加,因此:总代码行覆盖率不是最终覆盖率

其他内容快捷连接:


香山昆明湖DUT验证进展



注:本文档中的统计信息根据测试结果自动生成
数据自动更新日期:1970-01-01 00:00:00

1 - 目标验证单元


查看测试报告

上图共有-个模块,默认情况下模块为灰色,当模块中的测试用例数大于-时,该模块被完全点亮。目前已经完全点亮的模块为-个,待点亮的模块有-个。

通用处理器模块简介

高性能处理器是现代计算设备的核心,它们通常由三个主要部分组成:前端、后端和访存系统。这些部分协同工作,以确保处理器能够高效地执行复杂的计算任务。

  • 前端:前端部分,也被称为指令获取和解码阶段,负责从内存中获取指令并将其解码成处理器可以理解的格式。这一阶段是处理器性能的关键,因为它直接影响到处理器可以多快地开始执行指令。前端通常包括指令缓存、分支预测单元和指令解码器。指令缓存用于存储最近访问过的指令,以减少对主内存的访问次数,从而提高处理速度。分支预测单元则尝试预测程序中的条件分支,以便提前获取和解码后续指令,这样可以减少等待分支结果的时间。

  • 后端:后端部分,也称为执行阶段,是处理器中负责实际执行指令的地方。这一阶段包括了算术逻辑单元(ALU)、浮点单元(FPU)和各种执行单元。这些单元负责进行算术运算、逻辑运算、数据传输和其他处理器操作。后端的设计通常非常复杂,因为它需要支持多种指令集架构(ISA)并优化性能。为了提高效率,现代处理器通常采用超标量架构,这意味着它们可以同时执行多条指令。

  • 访存:访存系统是处理器与内存之间交互的桥梁。它包括了数据缓存、内存控制器和高速缓存一致性协议。数据缓存用于存储处理器频繁访问的数据,以减少对主内存的访问次数。内存控制器负责管理处理器与内存之间的数据传输。高速缓存一致性协议确保在多处理器系统中,所有处理器看到的内存状态是一致的。

高性能处理器的设计需要在这三个部分之间找到平衡,以实现最佳的性能。这通常涉及到复杂的微架构设计,以及对处理器流水线的优化。

2 - 准备验证环境

基础环境需求

本项目基于Python编程语言进行UT验证,采用的工具和测试框架为pickertoffee环境需求如下:

  1. Linux操作系统。建议WSL2下安装Ubuntu22.04。
  2. Python。建议Python3.11。
  3. picker。按照快速开始中的提示安装最新版本。
  4. lcov。用于后续test阶段报告生成。使用包管理器即可下载:sudo apt install lcov

环境配置完成后,clone仓库:

git clone https://github.com/XS-MLVP/UnityChipForXiangShan.git
cd UnityChipForXiangShan
pip3 install -r requirements.txt # 安装python依赖(例如 toffee)

下载RTL代码:

默认从仓库https://github.com/XS-MLVP/UnityChipXiangShanRTLs中下载。用户也可以自行按照XiangShan文档编译生成RTL。

make rtl    # 该命下载最新的rtl代码,并解压至rtl目录,并创建软连接

可以用以下命令指定下载的rtl版本:

make rtl args="rtl.version=\'openxiangshan-kmh-fad7803d-24120901\'"

所有RTL下载包请在UnityChipXiangShanRTLs中查看。

RTL压缩包的命名规范为:名称-微架构-Git标记-日期编号.tar.gz,例如openxiangshan-kmh-97e37a2237-24092701.tar.gz。在使用时,仓库代码会过滤掉git标记和后缀,例如通过 cfg.rtl.version 访问到的版本号为:openxiangshan-kmh-24092701。压缩包内的目录结构为:

openxiangshan-kmh-97e37a2237-24092701.tar.gz
└── rtl           # 目录
    |-- *.sv      # 所有sv文件
    `-- *.v       # 所有v文件

编译DUT

该过程的目的是将RTL通过picker工具打包为Python模块。可以通过make命令指定被打包DUT,也可以一次性打包所有DUT。

如果想要自行打包某个dut,需要创建编写scripts目录中的build_ut_<name>.py脚本。这一脚本必须实现一个build方法,在打包时会被自动调用。此外还有一个line_coverage_files方法,用于指定行覆盖率参考的文件。

picker的打包支持内部信号的加入,详见picker的--internal参数,传递给其一个自定义的yaml即可。

# 调用scripts目录中的build_ut_<name>.py中的build方法,创建待验证的Python版DUT
make dut DUTS=<name>  # DUTS的值如果有多个,需要用逗号隔开,支持通配符。DUTS默认值为 "*",编译所有DUT
# 例如:
make dut DUTS=backend_ctrl_block_decode

make dut DUTS=backend_ctrl_block_decode为例,命令执行完成后,会在dut目录下生成对应的Python包:

dut/
├── __init__.py
├── DecodeStage
├── Predecode
└── RVCExpander

完成转换后,在测试用例代码中可以import对应的DUT,例如:

from dut.PreDecode import DUTPreDecode
dut = DUTPreDecode()

编辑配置

运行rtl、dut、test等命令时,默认使用configs/_default.yaml中的配置项。

当然,也可以使用自定义配置,方法如下:

# 指定自定义CFG文件
make CFG=path/to/your_cfg.yaml

类似地,可以在命令行直接指定键值对传入。目前仅有test相关阶段支持命令行配置键值对:

# 指定KV,传递命令行参数,键值对之间用空格隔开
make test KV="log.term-level=\'debug\' test.skip-tags=[\'RARELY_USED\']"

3 - 运行测试

本项目基于PyTest测试框架进行验证。运行测试时,PyTest框架自动搜索所有test_*.py文件,并自动执行其中所有以test_开头的测试用例(Test Case)。

# 执行所有ut_*目录中的test case
make test_all
# 执行指定目录下的test case
make test target=<dir>
# 例如执行ut_backend/ctrl_block/decode目录中所有的test case
make test target=ut_backend/ctrl_block/decode

可通过args参数传递Pytest的运行参数,例如启动x-dist插件的多核功能:

make test args="-n 4"     # 启用 4 个进程
make test args="-n auto"  # 让框架自动选择启用多少个进程

*注:x-dist可以在多节点上并发运行测试,可参考其文档

运行完成后,默认在out/report目录会生成html版本的测试报告,其 html 文件可通过浏览器直接打开查看(VS Code IDE建议安装Open In Default Browser插件)。

运行测试主要完成以下三部分内容:

  1. 按要求运行Test Case,可通过cfg.tests中的选项进行配置
  2. 统计测试结果,输出测试报告。有toffee-report自动生成 (总测试报告,所有Test的结果合并在一起)
  3. 根据需要(cfg.doc_result.disable = True)在测试报告上进行进一步数据统计

4 - 添加测试

添加一个全新的 DUT 测试用例,需要完成以下三部分内容(本节以前端的ifu下的rvc_expander为例):

  1. 添加编译脚本: 在scripts目录下使用python编写对应rtl的编译文件(例如build_ut_frontend_ifu_rvc_expander.py)。
  2. 构建测试环境: 在目录中创建目标测试 UT 目录(例如ut_frontend/ifu/rvc_expander)。如果有需要的话,可以在tools、comm等模块中添加该 DUT 测试需要的基础工具。
  3. 添加测试用例: 在测试 UT 目录,按PyTest 规范添加测试用例。

如果是在已有的 DUT 测试中增加内容,按原有目录结构添加即可。

如何通过 picker 和 toffee 库进行 Python 芯片验证,请参考:https://open-verify.cc/mlvp/docs

在测试时还需要关心以下内容:

  1. UT 模块说明: 在添加的模块顶层文件夹中,添加README.md说明,具体格式和要求请参考模板
  2. 代码覆盖率:代码覆盖率是芯片验证的重要指标,一般需需要覆盖目标 DUT 的所有代码。
  3. 功能覆盖率:功能覆盖率即目标功能验证完成了多少,一般需要达到 100%。

在后续的文档中,我们将继续以rvc_expander模块为例,详细说明上述过程。

*注:目录或文件名称需要合理,以便于能通过命名知晓其具体含义。

4.1 - 添加编译脚本

脚本目标

scripts目录下使用python编写对应rtl的编译文件(例如build_ut_frontend_ifu_rvc_expander.py)。
该脚本的目标是提供 RTL 到 Python DUT 的编译、目标覆盖文件,以及自定义功能等内容。

创建过程

确定文件名称

香山昆明湖 DUT 验证进展中选择需要验证的 UT,如果没有或者进一步细化,可通过编辑configs/dutree/xiangshan-kmh.yaml自行添加。
比如,我们要验证的是前端部分的ifu模块下的rvc_expander模块,那么需要在configs/dutree/xiangshan-kmh.yaml中添加对应的部分(目前yaml中已经有该模块了,此处为举例):

name: "kmh_dut"
desc: "所有昆明湖DUT"
children:
  - name: "frontend"
    desc: "前端模块"
    children:
      - name: "ifu"
        desc: "指令单元 (Instruction Fetch Unit)"
        children:
          - name: "rvc_expander"
            desc: "RVC指令扩充器"

脚本文件的命名格式如下:

scripts/build_<顶层模块>_<下层模块名>_..._<目标模块名>.py

目前本项目内置了 4 个顶层模块:

  1. ut_frontend 前端
  2. ut_backend 后端
  3. ut_mem_block 访存
  4. ut_misc 其他

其中的子模块没有ut_前缀(顶层目录有该前缀是为了和其他目录区分开)。

例如验证目标 DUT 为rvc_expander模块:
该模块是属于前端的,所以顶级模块为ut_frontend,它的下层模块为ifu,目标模块为rvc_expander
通过刚才我们打开的yaml文件也可以知道,frontend的children 为ifuifu的children 为rvc_expander
所以,需要创建的脚本名称为build_ut_frontend_ifu_rvc_expander.py

编写 build(cfg) -> bool 函数

build 函数定义如下:

def build(cfg) -> bool:
    """编译DUT
    Args:
        cfg: 运行时配置,可通过它访问配置项,例如 cfg.rtl.version
    Return:
        返回 True 或者 False,表明该函数是否完成预期目标
    """

build 在 make dut 时会被调用,其主要是将目标 RTL 转换为 Python 模块。在该过程中也可以加入其他必要过程,例如编译依赖项等。以build_ut_frontend_ifu_rvc_expander.py为例,主要完成了 RTL 检查、DUT 检查、RTL 编译、disasm 依赖编译等工作:

import os
from comm import warning, info


def build(cfg):
    # import 相关依赖
    from toffee_test.markers import match_version
    from comm import is_all_file_exist, get_rtl_dir, exe_cmd, get_root_dir
    # 检查RTL版本(version参数为空,表示所有版本都支持)
    if not match_version(cfg.rtl.version, "openxiangshan-kmh-*"):
        warning("ifu frontend rvc expander: %s" % f"Unsupported RTL version {cfg.rtl.version}")
        return False
    # 检查在当前RTL中,目标文件是否存在
    f = is_all_file_exist(["rtl/RVCExpander.sv"], get_rtl_dir(cfg=cfg))
    assert f is True, f"File {f} not found"
    # 如果dut中不存在RVCExpander,则调用picker进行Python打包
    if not os.path.exists(get_root_dir("dut/RVCExpander")):
        info("Exporting RVCExpander.sv")
        s, out, err = exe_cmd(f'picker export --cp_lib false {get_rtl_dir("rtl/RVCExpander.sv", cfg=cfg)} --lang python --tdir {get_root_dir("dut")}/ -w rvc.fst -c')
        assert s, "Failed to export RVCExpander.sv: %s\n%s" % (out, err)
    # 如果tools中不存在disasm/build,则需要编译disasm
    if not os.path.exists(get_root_dir("tools/disasm/build")):
        info("Building disasm")
        s, _, _ = exe_cmd("make -C %s" % get_root_dir("tools/disasm"))
        assert s, "Failed to build disasm"
    # 编译成功
    return True

def line_coverage_files(cfg):
    return ["RVCExpander.v"]

picker 的使用方式请参考其文档使用

scripts目录中可以创建子目录保存 UT 验证需要的文件,例如 rvc_expander 模块创建了scripts/frontend_ifu_rvc_expander目录,其中的rtl_file.f用来指定输入的 RTL 文件,line_coverage.ignore用来保存需要忽略的代码行统计。自定义目录的命名需要合理,且能通过名字判断其所属模块和文件。

编写 line_coverage_files(cfg) -> list[str] 函数

line_coverage_files 函数的定义如下:

def line_coverage_files(cfg)-> list[str]:
    """指定需要覆盖的文件
    Args:
        cfg: 运行时配置,可通过它访问配置项,例如 cfg.rtl.version
    Return:
        返回统计代码行覆盖率的目标RTL文件名
    """

build_ut_frontend_ifu_rvc_expander.py文件中,line_coverage_files函数的定义如下:

def line_coverage_files(cfg):
    return ["RVCExpander.v"]

标识该模块关注的是对RVCExpander.v文件的覆盖。如果要开启测试结果处理,还需要在configs/_default.yaml中的doc-resultdisable=False(默认参数是False,也就是开启状态);如果不开启测试结果处理则(disable = True)。注意,如果不开启测试结果处理,那么上述函数就不会被调用。

4.2 - 构建测试环境

确定目录结构

UT(Unit Test, 单元测试)所在的目录位置的层级结构应该与名称一致,例如frontend.ifu.rvc_expander应当位于ut_frontend/ifu/rvc_expander目录,且每层目录都需要有__init__.py,便于通过 python 进行import

本章节的文件为your_module_wrapper.py(如果你的模块是rvc_expander,那么文件就是rvc_expander_wrapper.py)。

wrapper 是包装的意思,也就是我们测试中需要用到的方法封装成和dut解耦合的API提供给测试用例使用。

*注:解耦合是为了测试用例和 DUT 解耦,使得测试用例可以独立于 DUT 进行编写和调试,也就是在测试用例中,不需要知道 DUT 的具体实现细节,只需要知道如何使用 API 即可。可以参照将验证代码与DUT进行解耦

该文件应该放于ut_frontend_or_backend/top_module/your_module/env(这里依然以rvc_expander举例:rvc_expander属于前端,其顶层目录则应该是ut_frontendrvc_expander的顶层模块是ifu,那么次级目录就是ifu;之后的就是rvc_expander自己了;最后,由于我们是在构建测试环境,再建一级env目录。将它们连起来就是:ut_frontend_or_backend/top_module/your_module/env)目录下。

ut_frontend/ifu/rvc_expander
├── classical_version
│   ├── env
│   │   ├── __init__.py
│   │   └── rvc_expander_wrapper.py
│   ├── __init__.py
│   └── test_rvc_expander.py
├── __init__.py
├── README.md
└── toffee_version
    ├── agent
    │   └── __init__.py
    ├── bundle
    │   └── __init__.py
    ├── env
    │   ├── __init__.py
    │   └── ref_rvc_expand.py
    ├── __init__.py
    └── test
        ├── __init__.py
        ├── rvc_expander_fixture.py
        └── test_rvc.py

这里rvc_expander目录下有classical_version传统版本和toffee_version使用toffee的版本。 传统版本就是使用pytest框架来进行测试,toffee只使用了其Bundle;而在toffee版本中,我们会使用更多toffee的特性。
一般来说,使用传统版本就已经可以覆盖绝大多数情况了,只有在传统版本不能满足需求时,才需要使用toffee版本。
编写测试环境的时候,两个版本选择一个就行
模块(例如rvc_expander)中的代码目录结构由贡献者自行决定(我们写的时候并不需要再建一级classical_versiontoffee_version目录),但需要满足 python 规范,且逻辑和命名合理。

Env 编写要求

  • 需要进行 RTL 版本检查
  • Env 提供的 API 需要和引脚、时序无关
  • Env 提供的 API 需要稳定,不能随意进行接口/返回值修改
  • 需要定义必要的 fixture
  • 需要初始化功能检查点(功能检查点可以独立成一个模块)
  • 需要进行覆盖率统计
  • 需要有说明文档

编写测试环境:传统版本

在 UT 验证模块的测试环境中,目标是完成以下工作:

  1. 对 DUT 进行功能封装,为测试提供稳定 API
  2. 定义功能覆盖率
  3. 定义必要 fixture 提供给测试用例
  4. 在合理时刻统计覆盖率

以 IFU 环境中的 RVCExpander 为例(ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py):

1. DUT 封装

以下内容位于ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py

class RVCExpander(toffee.Bundle):
    def __init__(self, cover_group, **kwargs):
        super().__init__()
        self.cover_group = cover_group
        self.dut = DUTRVCExpander(**kwargs) # 创建DUT
        self.io = toffee.Bundle.from_prefix("io_", self.dut) # 通过 Bundle 使用前缀关联引脚
        self.bind(self.dut)                 # 把 Bundle 与 DUT 进行绑定

    def expand(self, instr, fsIsOff):
        self.io["in"].value = instr         # 给DUT引脚赋值
        self.io["fsIsOff"].value = fsIsOff  # 给DUT引脚赋值
        self.dut.RefreshComb()              # 推动组合电路
        self.cover_group.sample()           # 调用sample对功能覆盖率进行统计
        return self.io["out_bits"].value, self.io["ill"].value  # 返回结果 和 是否是非法指令

    def stat(self):                         # 获取当前状态
        return {
            "instr": self.io["in"].value,         # 输入指令
            "decode": self.io["out_bits"].value,  # 返回展开结果
            "ilegal": self.io["ill"].value != 0,  # 输入是否非法
        }

在上述例子中,class RVCExpanderDUTRVCExpander进行了封装,对外提供了两个 API:

  • expand(instr: int, fsIsOff: bool) -> (int, int) :该函数用于接受输入指令 instr 进行解码,返回(结果,非法指令标记)。如果非法指令标记不为 0,者说明输入指令非法。
  • stat() -> dict(instr, decode, ilegal):该函数用于返回当前的状态,其中包含当前的输入指令,解码结果以及非法指令标记。

上述 API 屏蔽了 DUT 的引脚,对外程序通用功能。

2. 定义功能覆盖率

尽可能的在 Env 中定义好功能覆盖率,如果有必要也可以在测试用例中定义覆盖率。toffee 功能覆盖率的定义请参考什么是功能覆盖率。为了完善功能检查点和测试用例之间的对应关系,功能覆盖率定义完成后,需要在适合的位置进行检查点和测试用例的对应(测试点反标)。

以下内容位于ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py

import toffee.funcov as fc
# 创建功能覆盖率组
g = fc.CovGroup(UT_FCOV("../../../CLASSIC"))

def init_rvc_expander_funcov(expander, g: fc.CovGroup):
    """Add watch points to the RVCExpander module to collect function coverage information"""

    # 1. Add point RVC_EXPAND_RET to check expander return value:
    #    - bin ERROR. The instruction is not illegal
    #    - bin SUCCE. The instruction is not expanded
    g.add_watch_point(expander, {
                                "ERROR": lambda x: x.stat()["ilegal"] == False,
                                "SUCCE": lambda x: x.stat()["ilegal"] != False,
                          }, name = "RVC_EXPAND_RET")
    ...
    # 5. Reverse mark function coverage to the check point
    def _M(name):
        # get the module name
        return module_name_with(name, "../../test_rv_decode")

    #  - mark RVC_EXPAND_RET
    g.mark_function("RVC_EXPAND_RET",     _M(["test_rvc_expand_16bit_full",
                                              "test_rvc_expand_32bit_full",
                                              "test_rvc_expand_32bit_randomN"]), bin_name=["ERROR", "SUCCE"])
    ...

在上述代码中添加了名为RVC_EXPAND_RET的功能检查点来检查RVCExpander模块是否具有返回非法指令的能力。需要满足ERRORSUCCE两个条件,即stat()中的ileage需要有True也需要有False值。在定义完检查点后,通过mark_function方法,对会覆盖到该检查的测试用例进行了标记。

3. 定义必要fixture

以下内容位于ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py

version_check = get_version_checker("openxiangshan-kmh-*")             # 指定满足要的RTL版本
@pytest.fixture()
def rvc_expander(request):
    version_check()                                                    # 进行版本检查
    fname = request.node.name                                          # 获取调用该fixture的测试用例
    wave_file = get_out_dir("decoder/rvc_expander_%s.fst" % fname)     # 设置波形文件路径
    coverage_file = get_out_dir("decoder/rvc_expander_%s.dat" % fname) # 设置代码覆盖率文件路径
    coverage_dir = os.path.dirname(coverage_file)
    os.makedirs(coverage_dir, exist_ok=True)                           # 目标目录不存在则创建目录
    expander = RVCExpander(g, coverage_filename=coverage_file, waveform_filename=wave_file)
                                                                       # 创建RVCExpander
    expander.dut.io_in.AsImmWrite()                                    # 设置io_in引脚的写入时机为立即写入             
    expander.dut.io_fsIsOff.AsImmWrite()                               # 设置io_fsIsOff引脚的写入时机为立即写入  
    init_rvc_expander_funcov(expander, g)                              # 初始化功能检查点
    yield expander                                                     # 返回创建好的 RVCExpander 给 Test Case
    expander.dut.Finish()                                              # Tests Case运行完成后,结束DUT
    set_line_coverage(request, coverage_file)                          # 把生成的代码覆盖率文件告诉 toffee-report
    set_func_coverage(request, g)                                      # 把生成的功能覆盖率数据告诉 toffee-report
    g.clear()                                                          # 清空功能覆盖统计

上述 fixture 完成了以下功能:

  1. 进行 RTL 版本检查,如果不满足"openxiangshan-kmh-*"要求,则跳过调用改 fixture 的测试用例
  2. 创建 DUT,并指定了波形,代码行覆盖率文件路径(路径中含有调用该 fixure 的用例名称:fname)
  3. 调用init_rvc_expander_funcov添加功能覆盖点
  4. 结束 DUT,处理代码行覆盖率和功能覆盖率(发往 toffee-report 进行处理)
  5. 清空功能覆盖率

*注:在 PyTest 中,执行测试用例test_A(rvc_expander, ....)前(rvc_expander是我们在使用fixure装饰器时定义的方法名),会自动调用并执行rvc_expander(request)yield关键字前的部分(相当于初始化),然后通过yield返回rvc_expander调用test_A用例(yield返回的对象,在测试用例里就是我们fixture下定义的方法名),用例执行完成后,再继续执行fixtureyield关键字之后的部分。比如:参照下面统计覆盖率的代码,倒数第四行的 rvc_expand(rvc_expander, generate_rvc_instructions(start, end)),其中的rvc_expander就是我们在fixture中定义的方法名,也就是yield返回的对象。

4. 统计覆盖率

以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py

N = 10
T = 1<<16
@pytest.mark.toffee_tags(TAG_LONG_TIME_RUN)
@pytest.mark.parametrize("start,end",
                         [(r*(T//N), (r+1)*(T//N) if r < N-1 else T) for r in range(N)])
def test_rvc_expand_16bit_full(rvc_expander, start, end):
    """Test the RVC expand function with a full compressed instruction set

    Description:
        Perform an expand check on 16-bit compressed instructions within the range from 'start' to 'end'.
    """
    # Add check point: RVC_EXPAND_RANGE to check expander input range.
    #   When run to here, the range[start, end] is covered
    covered = -1
    g.add_watch_point(rvc_expander, {
                                "RANGE[%d-%d]"%(start, end): lambda _: covered == end
                          }, name = "RVC_EXPAND_ALL_16B", dynamic_bin=True)
    # Reverse mark function to the check point
    g.mark_function("RVC_EXPAND_ALL_16B", test_rvc_expand_16bit_full, bin_name="RANGE[%d-%d]"%(start, end))
    # Drive the expander and check the result
    rvc_expand(rvc_expander, generate_rvc_instructions(start, end))
    # When go to here, the range[start, end] is covered
    covered = end
    g.sample()                                                              # 覆盖率采样

在定义了覆盖率之后,还需要在测试用例中进行覆盖率统计。上述代码中,在测试用例中使用add_watch_point添加了一个功能检查点rvc_expander,并在后面进行了标记和采样,而且在最后一样对覆盖率进行了采样。 覆盖率采样,实际上是通过回调函数触发了一次add_watch_point中bins的判断,当其中bins的判断结果为True时,就会统计一次Pass。

编写测试环境:toffee版本

使用python语言进行的测试可以通过引入我们的开源测试框架toffee来得到更好的支持。

toffee的官方教程可以参考这里

bundle:快捷DUT封装

toffee通过Bundle实现了对DUT的绑定。toffee提供了多种建立Bundle与DUT绑定的方法。相关代码

手动绑定

toffee框架下,用于支持绑定引脚的最底层类是Signal,其通过命名匹配的方式和DUT中的各个引脚进行绑定。相关代码参照ut_frontend/ifu/rvc_expander/toffee_version

以最简单的RVCExpander为例,其io引脚形如:

module RVCExpander(
  input  [31:0] io_in,
  input         io_fsIsOff,
  output [31:0] io_out_bits,
  output        io_ill
);

一共四个信号,io_in, io_fsIsOff, io_out_bits, io_ill。我们可以抽取共同的前缀,比如"io_"(不过由于in在python中有其他含义,其不能直接作为变量名,虽然可以使用setattr 和getattr方法来规避这个问题,但是出于代码简洁的考虑,我们只选取"io"作为前缀),将后续部分作为引脚名定义在对应的Bundle类中:


class RVCExpanderIOBundle(Bundle):
	_in, _fsIsOff ,_out_bits,_ill = Signals(4)

然后在更高一级的Env或者Bundle中,采取from_prefix的方式完成前缀的绑定:

self.agent = RVCExpanderAgent(RVCExpanderIOBundle.from_prefix("io").bind(dut))

自动定义Bundle

实际上,Bundle类的定义也不一定需要写明,可以仅仅通过前缀绑定:


self.io = toffee.Bundle.from_prefix("io_", self.dut) # 通过 Bundle 使用前缀关联引脚
self.bind(self.dut)   

如果Bundle的from_prefix方法传入dut,其将根据前缀和DUT的引脚名自动生成引脚的定义,而在访问的时候,使用dict访问的思路即可:


self.io["in"].value = instr
self.io["fsIsOff"].value = False

Bundle代码生成

toffee框架的scripts提供了两个脚本。

bundle_code_gen.py脚本主要提供了三个方法:

def gen_bundle_code_from_dict(bundle_name: str, dut, dict: dict, max_width: int = 120)
def gen_bundle_code_from_prefix(bundle_name: str, dut, prefix: str = "", max_width: int = 120):
def gen_bundle_code_from_regex(bundle_name: str, dut, regex: str, max_width: int = 120):

通过传入dut和生成规则(包括dict、prefix、regex三种),自动生成对应的bundle代码。

而bundle_code_intel_gen.py则解析picker生成的signals.json文件,自动生成层次化的bundle代码。可以直接在命令行调用:

python bundle_code_intel_gen.py [signal] [target]

如发现自动生成脚本存在bug,欢迎提issue以便我们修正。

Agent:驱动方法

如果说Bundle是将DUT的数据职责进行抽象的话,那么Agent则是将DUT的行为职责封装为一个个接口。简单地说,Agent通过封装多个对外开放的方法,将多组IO操作抽象为一个具体的行为:


class RVCExpanderAgent(Agent):
    def __init__(self, bundle:RVCExpanderIOBundle):
        super().__init__(bundle)
        self.bundle = bundle
    
    @driver_method()
    async def expand(self, instr, fsIsOff):             # 传入参数:RVC指令和fs.status使能情况
        self.bundle._in.value = instr                   # 引脚赋值
        self.bundle._fsIsOff.value = fsIsOff            # 引脚赋值
        
        await self.bundle.step()                        # 推动时钟
        return self.bundle._out_bits.value,             # 返回值:扩展后指令
                self.bundle._ill.value                  # 返回值:指令合法校验

譬如,RVCExpander的指令扩展功能接收输入的指令(可能为RVI指令,也可能为RVC指令)和CSR对fs.status的使能情况。我们将这个功能抽象为expand方法,提供除self以外的两个参数。同时,指令扩展最终将会返回传入指令对应的RVI指令和该指令是否合法的判断,对应地,该方法也返回这两个值。

Env:测试环境

class RVCExpanderEnv(Env):
    def __init__(self, dut:DUTRVCExpander):
        super().__init__()
        dut.io_in.xdata.AsImmWrite()        
        dut.io_fsIsOff.xdata.AsImmWrite()   # 设置引脚写入时机
        self.agent = RVCExpanderAgent(RVCExpanderIOBundle.from_prefix("io").bind(dut)) # 补全前缀,绑定DUT

覆盖率定义

定义覆盖率组的方式和前述方式类似,这里就不再赘述了。

测试套件定义

测试套件的定义略有不同:

@toffee_test.fixture
async def rvc_expander(toffee_request: toffee_test.ToffeeRequest):
    import asyncio
    version_check()
    dut = toffee_request.create_dut(DUTRVCExpander)
    start_clock(dut)
    init_rvc_expander_funcov(dut, gr)
    
    toffee_request.add_cov_groups([gr])
    expander = RVCExpanderEnv(dut)
    yield expander

    cur_loop = asyncio.get_event_loop()
    for task in asyncio.all_tasks(cur_loop):
        if task.get_name() == "__clock_loop":
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                break

由于toffee提供了更强大的测试覆盖率管理功能,因此不需要手动设置行覆盖率。同时,由于toffee的时钟机制,建议在套件代码最后额外检查任务是否全部结束。

4.3 - 添加测试用例

命名要求

所有测试用例文件请以test_*.py的方式进行命名,*用测试目标替换(例如test_rvc_expander.py)。所有测试用例也需要以test_前缀开头。用例名称需要具有明确意义。

命名举例如下:

def test_a(): # 不合理,无法通过a判断测试目标
    pass

def test_rvc_expand_16bit_full(): # 合理,可以通过用例名称大体知道测试内容
    pass

使用 Assert

在每个测试用例中,都需要通过assert来判断本测试是否通过。 pytest统计的是assert语句的结果,因此assert语句需要保证能够通过。

以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py中:

def rvc_expand(rvc_expander, ref_insts, is_32bit=False, fsIsOff=False):
    """compare the RVC expand result with the reference

    Args:
        rvc_expander (warpper): the fixture of the RVC expander
        ref_insts (list[int]]): the reference instruction list
    """
    find_error = 0
    for insn in ref_insts:
        insn_disasm = disasmbly(insn)
        value, instr_ex = rvc_expander.expand(insn, fsIsOff)
        if is_32bit:
            assert value == insn, "RVC expand error, 32bit instruction need to be the same"
        if (insn_disasm == "unknown") and  (instr_ex == 0):
            debug(f"find bad inst:{insn}, ref: 1, dut: 0")
            find_error +=1
        elif (insn_disasm != "unknown") and  (instr_ex == 1):
            if (instr_filter(insn_disasm) != 1): 
                debug(f"find bad inst:{insn},disasm:{insn_disasm}, ref: 0, dut: 1")
                find_error +=1
    assert 0 == find_error, "RVC expand error (%d errros)" % find_error

编写注释

每个测试用例都需要添加必要的说明和注释,需要满足Python 注释规范

测试用例说明参考格式:

def test_<name>(a: type_a, b: type_b):
    """Test abstract

    Args:
        a (type_a): description of arg a.
        b (type_b): description of arg b.

    Detailed test description here (if need).
    """
    ...

用例管理

为了方便测试用例管理,可通过 toffee-test 提供的@pytest.mark.toffee_tags标签功能,请参考 本网站的其他部分和toffee-test

参考用例

如果很多测试用例(Test)具有相同的操作,该公共操作部分可以提炼成一个通用函数。以 RVCExpander 验证为例,可以把压缩指令的展开与参考模型(disasm)的对比封装成以下函数:

以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py中:

def rvc_expand(rvc_expander, ref_insts, is_32bit=False, fsIsOff=False):
    """compare the RVC expand result with the reference

    Args:
        rvc_expander (warpper): the fixture of the RVC expander
        ref_insts (list[int]]): the reference instruction list
    """
    find_error = 0
    for insn in ref_insts:
        insn_disasm = disasmbly(insn)
        value, instr_ex = rvc_expander.expand(insn, fsIsOff)
        if is_32bit:
            assert value == insn, "RVC expand error, 32bit instruction need to be the same"
        if (insn_disasm == "unknown") and  (instr_ex == 0):
            debug(f"find bad inst:{insn}, ref: 1, dut: 0")
            find_error +=1
        elif (insn_disasm != "unknown") and  (instr_ex == 1):
            if (instr_filter(insn_disasm) != 1): 
                debug(f"find bad inst:{insn},disasm:{insn_disasm}, ref: 0, dut: 1")
                find_error +=1
    assert 0 == find_error, "RVC expand error (%d errros)" % find_error

在上述公共部分中有 assert,因此调用该函数的 Test 也能提过该 assert 判断运行结果是否提过。

在测试用例的开发过程中,通常存在大量的调试工作,为了让验证环境快速就位,需要编写一些“冒烟测试”进行调试。RVCExpander 展开 16 位压缩指令的冒烟测试如下:

@pytest.mark.toffee_tags(TAG_SMOKE)
def test_rvc_expand_16bit_smoke(rvc_expander):
    """Test the RVC expand function with 1 compressed instruction"""
    rvc_expand(rvc_expander, generate_rvc_instructions(start=100, end=101))

为了方便进行管理,上述测试用例通过toffee_tags标记上了 SMOKE 标签。它的输入参数为rvc_expander,则在在运行时,会自动调用对应同名的fixture进行该参数的填充。

RVCExpander 展开 16 位压缩指令的测试目标是对 2^16 所有压缩指令进行遍历,检测所有情况是否都与参考模型 disasm 一致。在实现上,如果仅仅用一个 Test 进行遍历,则需要耗费大量时间,为此我们可以利用 PyTest 提供的parametrize对 test 进行参数化配置,然后通过pytest-xdist插件并行执行:

以下内容位于ut_frontend/ifu/rvc_expander/classical_version/test_rvc_expander.py中:

N = 10
T = 1<<16
@pytest.mark.toffee_tags(TAG_LONG_TIME_RUN)
@pytest.mark.parametrize("start,end",
                         [(r*(T//N), (r+1)*(T//N) if r < N-1 else T) for r in range(N)])
def test_rvc_expand_16bit_full(rvc_expander, start, end):
    """Test the RVC expand function with a full compressed instruction set

    Description:
        Perform an expand check on 16-bit compressed instructions within the range from 'start' to 'end'.
    """
    # Add check point: RVC_EXPAND_RANGE to check expander input range.
    #   When run to here, the range[start, end] is covered
    g.add_watch_point(rvc_expander, {
                                "RANGE[%d-%d]"%(start, end): lambda _: True
                          }, name = "RVC_EXPAND_ALL_16B").sample()

    # Reverse mark function to the check point
    g.mark_function("RVC_EXPAND_ALL_16B", test_rvc_expand_16bit_full, bin_name="RANGE[%d-%d]"%(start, end))

    # Drive the expander and check the result
    rvc_expand(rvc_expander, generate_rvc_instructions(start, end))

在上述用例中定义了参数化参数start, end,用来指定压缩指令的开始值和结束值,然后通过装饰器@pytest.mark.parametrize对他们进行分组赋值。变量 N 可以指定将目标数据进行分组的组数,默认设置为 10 组。在运行时用例test_rvc_expand_16bit_full会展开为test_rvc_expand_16bit_full[0-6553]test_rvc_expand_16bit_full[58977-65536]10 个测试用例运行。

4.4 - 代码覆盖率

代码覆盖率是一项评价指标,它衡量了被测代码中哪些部分被执行了,哪些部分没有被执行。通过统计代码覆盖率,可以评估测试的有效性和覆盖程度。

代码覆盖率包括:

  • 行覆盖率(line coverage): 被测代码中被执行的行数,最简单的指标,一般期望达到 100%。
  • 条件覆盖率(branch coverage): 每一个控制结构的每个分支是否均被执行。例如,给定一个 if 语句,其 true 和 false 分支是否均被执行?
  • 有限状态机覆盖率(fsm coverage): 状态机所有状态是否都达到过。
  • 翻转覆盖率(toggle coverage): 统计被测代码中被执行的翻转语句,检查电路的每个节点是否都有 0 -> 1 和 1 -> 0 的跳变。
  • 路径覆盖率(path coverage): 检查路径的覆盖情况。在 always 语句块和 initial 语句块中,有时会使用 if … else 和 case 语句,在电路结构上便会产生一系列的数据路径。。

*我们主要使用的模拟器是 Verilator,优先考虑行覆盖率。Verilator 支持覆盖率统计,因此我们在构建 DUT 时,如果要开启覆盖率统计,需要在编译选项中添加-c参数。

本项目中相关涉及位置

开启覆盖率需要在编译时(使用 picker 命令时)加上“-c”参数(参考 picker 的参数解释),同时在文件中设置启用行覆盖率,这样在使用 toffee 测试时,才能够生成覆盖率统计文件。

结合上面的描述,在本项目中也就是编译,编写和启用行覆盖率函数和测试的时候会涉及到代码覆盖率:

添加编译脚本部分

编写编译脚本

# 省略前面
    if not os.path.exists(get_root_dir("dut/RVCExpander")):
        info("Exporting RVCExpander.sv")
        s, out, err = exe_cmd(f'picker export --cp_lib false {get_rtl_dir("rtl/RVCExpander.sv", cfg=cfg)
                                                              } --lang python --tdir {get_root_dir("dut")}/ -w rvc.fst -c')
        assert s, "Failed to export RVCExpander.sv: %s\n%s" % (out, err)
# 省略后面

s, out, err=...这一行,我们使用 picker 命令,并且开启代码了覆盖率(命令最后的"-c"参数)。

设置目标覆盖文件(line_coverage_files 函数)

按照需求编写line_coverage_files(cfg) -> list[str]函数,并且开启测试结果处理(doc_result.disable = False)让其被调用。

构建测试环境部分

定义必要 fixture

set_line_coverage(request, coverage_file)                          # 把生成的代码覆盖率文件告诉 toffee-report

通过函数toffee-test.set_line_coverage把覆盖率文件传递给 toffe-test,这样其才能够收集数据,以便于后面生成的报告带有行覆盖率。

忽略指定统计

有时候,我们可能需要手动指定某些内容不参与覆盖率统计。例如有些是不需要被统计的,有些统计不到是正常的。这时候我们就可以忽略这些内容,这对优化覆盖率报告或调试非常有帮助。 目前我们的框架可以使用两种方式来实现忽略统计的功能:

1.通过 verilator 指定忽略统计的内容

使用 verilator_coverage_off/on 指令

Verilator 支持通过注释指令来忽略特定代码段的覆盖率统计。例如,使用如下的指令:

// *verilator coverage_off*
// 忽略统计的代码段
...
// *verilator coverage_on*

举个例子

module example;
    always @(posedge clk) begin
        // *verilator coverage_off*
        if (debug_signal) begin
            $display("This is for debugging only");
        end
        // *verilator coverage_on*
        if (enable) begin
            do_something();
        end
    end
endmodule

在上述示例中,debug_signal 部分的代码将不会计入覆盖率统计,而 enable 部分仍然会被统计。

更多 verilator 的忽略统计方式请参照verilator 官方文档

2.通过 toffee 指定需要过滤掉的内存

def set_line_coverage(request, datfile, ignore=[]):
    """Pass

    Args:
        request (pytest.Request): Pytest的默认fixture,
        datfile (string): DUT生成的
        ignore (list[str]): 覆盖率过滤文件/或者文件夹
    """

ignore 参数可以指定在覆盖率文件中需要过滤掉的内容,例如:

...
set_line_coverage(request, coverage_file,
                  get_root_dir("scripts/frontend_ifu_rvc_expander"))

在统计覆盖率时,会在"scripts/frontend_ifu_rvc_expander"目录中搜索到line_coverage.ignore文件,然后按其中每行的通配符进行过滤。

# Line covarge ignore file
# ignore Top file
*/RVCExpander_top*%

上述文件表示,在统计覆盖率时,会忽略掉包含"RVCExpander_top"关键字的文件(实际上是收集了对应的数据,但是最后统计的时候忽略了)。

查看统计结果

在经过前面所有步骤之后,包括准备测试环境中的下载 RTL 代码编译 DUT编辑配置 ;添加测试中的添加编译脚本,构建测试环境添加测试用例

现在运行测试,之后就默认在out/report目录会生成 html 版本的测试报告。

也可以在进度概述图形下方的“当前版本”选择对应的测试报告(按照测试时间命名),然后点击右侧链接即可查看统计结果。

4.5 - 功能覆盖率

功能覆盖率(Functional Coverage)是一种用户定义的度量标准,用于度量验证中已执行的设计规范的比例。功能覆盖率关注的是设计的功能和特性是否被测试用例覆盖到了。

反标是指将功能点与测试用例对应起来。这样,在统计时,就能看到每个功能点对应了哪些测试用例,从而方便查看哪些功能点用的测试用例多,哪些功能点用的测试用例少,有利于后期的测试用例优化。

本项目中相关涉及位置

功能覆盖率需要我们先定义了才能统计,主要是在构建测试环境的时候涉及。

构建测试环境中:

其他:

  • 在 Test case 中使用,可以在每个测试用例里也编写一个功能点。

功能覆盖率使用流程

指定 Group 名称

测试报告通过 Group 名字和 DUT 名字进行匹配,利用 comm.UT_FCOV 获取 DUT 前缀,例如在 Python 模块ut_frontend/ifu/rvc_expander/classical_version/env/rvc_expander_wrapper.py中进行如下调用:

from comm import UT_FCOV
# 本模块名为:ut_frontend.ifu.rvc_expander.classical_version.env.rvc_expander_wrapper
# 通过../../../去掉了classical_version和上级模块env,rvc_expander_wrapper
# UT_FCOV会默认去掉前缀 ut_
g = fc.CovGroup(UT_FCOV("../../../CLASSIC"))
# name = UT_FCOV("../../../CLASSIC")

name 的值为frontend.ifu.rvc_expander.CLASSIC,在最后统计结果时,会按照最长前缀匹配到目标 UT(即匹配到:frontend.ifu.rvc_expander 模块)

创建覆盖率组

使用toffeefuncov可以创建覆盖率组。

import toffee.funcov as fc
# 使用上面指定的GROUP名字
g = fc.CovGroup(name)

这两步也可以合成一句g = fc.CovGroup(UT_FCOV("../../../CLASSIC"))。 创建的g对象就表示了一个功能覆盖率组,可以使用其来提供观察点和反标。

添加观察点和反标

在每个测试用例内部,可以使用add_watch_pointadd_cover_point是其别名,二者完全一致)来添加观察点和mark_function来添加反标。 观察点是,当对应的信号触发了我们在观察点内部定义的要求后,这个观察点的名字(也就是功能点)就会被统计到功能覆盖率中。 反标是,将功能点和测试用例进行关联,这样在统计时,就能看到每个功能点对应了哪些测试用例。

对于观察点的位置,需要根据实际情况来定,一般来说,在测试用例外直接添加观察点是没有问题的。 不过有时候我们可以更加的灵活。

  1. 在测试用例之外(decode_wrapper.py中)
def init_rvc_expander_funcov(expander, g: fc.CovGroup):
    """Add watch points to the RVCExpander module to collect function coverage information"""
    # 1. Add point RVC_EXPAND_RET to check expander return value:
    #    - bin ERROR. The instruction is not illegal
    #    - bin SUCCE. The instruction is not expanded
    g.add_watch_point(expander, {
                                "ERROR": lambda x: x.stat()["ilegal"] == False,
                                "SUCCE": lambda x: x.stat()["ilegal"] != False,
                          }, name = "RVC_EXPAND_RET")
    # 5. Reverse mark function coverage to the check point
    def _M(name):
        # get the module name
        return module_name_with(name, "../../test_rv_decode")

    #  - mark RVC_EXPAND_RET
    g.mark_function("RVC_EXPAND_RET",_M(["test_rvc_expand_16bit_full",
                                              "test_rvc_expand_32bit_full",
                                              "test_rvc_expand_32bit_randomN"]), bin_name=["ERROR", "SUCCE"])

    # The End                                                                              
    return None 

这个例子的第一个g.add_watch_point是放在测试用例之外的,因为它和现有的测试用例没有直接关系,放在测试用例之外反而更加方便。添加观察点之后,只要add_watch_point方法中的bins条件触发了,我们的toffee-test框架就能够收集到对应的功能点。

  1. 在测试用例之中(test_rvc_expander.py中)
N=10
T=1<<32
@pytest.mark.toffee_tags([TAG_LONG_TIME_RUN, TAG_RARELY_USED])
@pytest.mark.parametrize("start,end",
                         [(r*(T//N), (r+1)*(T//N) if r < N-1 else T) for r in range(N)])
def test_rvc_expand_32bit_full(rvc_expander, start, end):
    """Test the RVC expand function with a full 32 bit instruction set

    Description:
        Randomly generate N 32-bit instructions for each check, and repeat the process K times.
    """
    # Add check point: RVC_EXPAND_ALL_32B to check instr bits.
    covered = -1
    g.add_watch_point(rvc_expander, {"RANGE[%d-%d]"%(start, end): lambda _: covered == end},
                      name = "RVC_EXPAND_ALL_32B", dynamic_bin=True)
    # Reverse mark function to the check point
    g.mark_function("RVC_EXPAND_ALL_32B", test_rvc_expand_32bit_full)
    # Drive the expander and check the result
    rvc_expand(rvc_expander, list([_ for _ in range(start, end)]))
    # When go to here, the range[start, end] is covered
    covered = end
    g.sample()

这个例子的观察点在测试用例里面,因为这里的startend是由pytest.mark.parametrize来决定的,数值不是固定的,所以我们需要在测试用例里面添加观察点。

采样

在上一个例子的最后,我们调用了g.sample(),这个函数的作用是告诉toffee-testadd_watch_point里的bins已经执行过了,判断一下是不是True,是的话就为这个观察点记录一次Pass。

有手动就有自动。我们可以在构建测试环境时,在定义fixture中加入StepRis(lambda x: g.sample()),这样就会在每个时钟周期的上升沿自动采样。

以下内容来自ut_backend/ctrl_block/decode/env/decode_wrapper.py

@pytest.fixture()
def decoder(request):
    # before test
    init_rv_decoder_funcov(g)
    func_name = request.node.name
    # If the output directory does not exist, create it
    output_dir_path = get_out_dir("decoder/log")
    os.makedirs(output_dir_path, exist_ok=True)
    decoder = Decode(DUTDecodeStage(
        waveform_filename=get_out_dir("decoder/decode_%s.fst"%func_name),
        coverage_filename=get_out_dir("decoder/decode_%s.dat"%func_name),
    ))
    decoder.dut.InitClock("clock")
    decoder.dut.StepRis(lambda x: g.sample())
    yield decoder
    # after test
    decoder.dut.Finish()
    coverage_file = get_out_dir("decoder/decode_%s.dat"%func_name)
    if not os.path.exists(coverage_file):
        raise FileNotFoundError(f"File not found: {coverage_file}")
    set_line_coverage(request, coverage_file, get_root_dir("scripts/backend_ctrlblock_decode"))
    set_func_coverage(request, g)
    g.clear()

如上面所示,我们在yield之前调用了g.sample(),这样就会在每个时钟周期的上升沿自动采样。

StepRis函数的作用是在每个时钟周期的上升沿执行传入的函数,详情可参照picker使用介绍

5 - 如何参与本项目

如何提交Bug

按 ISSUE 模板进行提交,标记上对应的标签(bug,bug等级等)

对应模块的维护者进行检查,并修改他给出的标记和香山分支

如何提交文档

本仓库文档以PR的形式在本仓库提交,DUT文档在仓库UnityChipForXiangShan/documents/content/zh-cn/docs/98_UT中进行提交。

本项目欢迎任何人以ISSUEDISCUSSForkPR的方式参与。

万众一芯QQ交流群:

6 - 模板-PR

# Description

Please include a summary of the changes and the related issue. 
Please also include relevant motivation and context. 
List any dependencies that are required for this change.

Fixes # (issue)

## Type of change

Please delete options that are not relevant.

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

# How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. 
Please also list any relevant details for your test configuration

- [ ] Test A
- [x] Test B

**Test Configuration**:
* Firmware version:
* Hardware:
* Toolchain:
* SDK:

# Checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream modules

展示效果如下:

Description

Please include a summary of the changes and the related issue. Please also include relevant motivation and context. List any dependencies that are required for this change.

Fixes # (issue)

Type of change

Please delete options that are not relevant.

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration

  • Test A
  • Test B

Test Configuration:

  • Firmware version:
  • Hardware:
  • Toolchain:
  • SDK:

Checklist:

  • My code follows the style guidelines of this project
  • I have added the appropriate labels
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

7 - 模板-ISSUE

## Description

A brief description of the issue.

## Steps to Reproduce

1. Describe the first step
2. Describe the second step
3. Describe the third step
4. ...

## Expected Result

Describe what you expected to happen.

## Actual Result

Describe what actually happened.

## Screenshots

If applicable, add screenshots to help explain your problem.

## Environment

- OS: [e.g. Windows 10, macOS 10.15, Ubuntu 20.04]
- Browser: [e.g. Chrome 86, Firefox 82, Safari 14]
- Version: [e.g. 1.0.0]

## Additional Information

Add any other context about the problem here.

展示效果如下:

Description

A brief description of the issue.

Steps to Reproduce

  1. Describe the first step
  2. Describe the second step
  3. Describe the third step

Expected Result

Describe what you expected to happen.

Actual Result

Describe what actually happened.

Screenshots

If applicable, add screenshots to help explain your problem.

Environment

  • OS: [e.g. Windows 10, macOS 10.15, Ubuntu 20.04]
  • Browser: [e.g. Chrome 86, Firefox 82, Safari 14]
  • Version: [e.g. 1.0.0]

Additional Information

Add any other context about the problem here.

Checklist

  • I have searched the existing issues
  • I have added the appropriate labels
  • I have reproduced the issue with the latest version
  • I have provided a detailed description of the bug
  • I have provided steps to reproduce the issue
  • I have included screenshots (if applicable)
  • I have provided the environment details (OS, version, etc.)

8 - 模板-UT-README

# 模块名称

## 测试目标

<测试目标测试方法描述>


## 测试环境

<测试环境描述依赖描述>

## 功能检测

<给出目标待测功能与对应的检测方法>

|序号|所属模块|功能描述|检查点描述|检查标识|检查项|
|-|-|-|-|-|-|
|-|-|-|-|-|-|


## 验证接口

<接口的描述>


## 用例说明

#### 测试用例1

|步骤|操作内容|预期结果|覆盖功能点|
|-|-|-|-|
|-|-|-|-|

#### 测试用例2

|步骤|操作内容|预期结果|覆盖功能点|
|-|-|-|-|
|-|-|-|-|


## 目录结构

<对本模块的目录结构进行描述>


## 检测列表


- [ ] 本文档符合指定[模板]()要求
- [ ] Env提供的API不包含任何DUT引脚和时序信息
- [ ] Env的API保持稳定(共有[ X ]个)
- [ ] Env中对所支持的RTL版本(支持版本[ X ])进行了检查
- [ ] 功能点(共有[ X ]个)与[设计文档]()一致
- [ ] 检查点(共有[ X ]个)覆盖所有功能点
- [ ] 检查点的输入不依赖任何DUT引脚,仅依赖Env的标准API
- [ ] 所有测试用例(共有[ X ]个)都对功能检查点进行了反标
- [ ] 所有测试用例都是通过 assert 进行的结果判断
- [ ] 所有DUT或对应wrapper都是通过fixture创建
- [ ] 在上述fixture中对RTL版本进行了检查
- [ ] 创建DUT或对应wrapper的fixture进行了功能和代码行覆盖率统计
- [ ] 设置代码行覆盖率时对过滤需求进行了检查

展示效果如下:

模块名称

测试目标

<测试目标、测试方法描述>

测试环境

<测试环境描述,依赖描述>

功能检测

<给出目标待测功能与对应的检测方法>

序号 所属模块 功能描述 检查点描述 检查标识 检查项
- - - - - -

验证接口

<接口的描述>

用例说明

测试用例1

步骤 操作内容 预期结果 覆盖功能点
- - - -

测试用例2

步骤 操作内容 预期结果 覆盖功能点
- - - -

目录结构

<对本模块的目录结构进行描述>

检测列表

  • 本文档符合指定模板要求
  • Env提供的API不包含任何DUT引脚和时序信息
  • Env的API保持稳定(共有[ X ]个)
  • Env中对所支持的RTL版本(支持版本[ X ])进行了检查
  • 功能点(共有[ X ]个)与设计文档一致
  • 检查点(共有[ X ]个)覆盖所有功能点
  • 检查点的输入不依赖任何DUT引脚,仅依赖Env的标准API
  • 所有测试用例(共有[ X ]个)都对功能检查点进行了反标
  • 所有测试用例都是通过 assert 进行的结果判断
  • 所有DUT或对应wrapper都是通过fixture创建
  • 在上述fixture中对RTL版本进行了检查
  • 创建DUT或对应wrapper的fixture进行了功能和代码行覆盖率统计
  • 设置代码行覆盖率时对过滤需求进行了检查

9 - 常用API

comm 模块

在comm中提供了部分可公用的API,可通过以下方式进行调用:

# import all
from comm import *
# or direct import functions you need
from com import function_you_need
# or access from module
import comm
comm.function_you_need()

cfg 子模块

get_config(cfg=None)

获取当前的Config配置

  • 输入:如果cfg不为空,则返回cfg。否则则自动通过toffee获取全局Config。
  • 返回:Config对象
import comm
cfg = comm.get_config()
print(cfg.rtl.version)

cfg_as_str(cfg: CfgObject):

把config对象转换为字符类型

  • 输入:Config对象
  • 返回:编码后的Config对象
import comm
cfg_str = comm.cfg_as_str(comm.get_config())

cfg_from_str(cfg_str)

把字符类型的Config对象还原

  • 输入:编码后的Config对象
  • 返回:Config对象
import comm
cfg = comm.cfg_from_str(cfg_str)

dump_cfg(cfg: CfgObject = None, cfg_file=None)

把config对象保持到文件

  • 输入:
    • cfg 需要保存的config
    • cfg_file 目标文件
import comm
cfg = comm.get_config()
comm.dump_cfg(cfg, "config.yaml")

functions 子模块

get_log_dir(subdir="", cfg=None)

获取日志目录

  • 输入:
    • subdir: 子目录
    • cfg:配置文件
  • 输出:日志目录
import comm
my_log = comm.get_log_dir("my_log")
print(my_log) # /workspace/UnityChipForXiangShan/out/log/my_log

get_out_dir(subdir="", cfg=None)

获取输出目录

  • 输入:
    • subdir: 子目录
    • cfg:配置文件
  • 输出:输出目录

get_rtl_dir(subdir="", cfg=None)

获取RTL目录

  • 输入:
    • subdir: 子目录
    • cfg:配置文件
  • 输出:RTL目录

get_root_dir(subdir="")

获取根目录:

  • 输入:根目录下的子目录
  • 输出:当前仓库的根目录

is_all_file_exist(files_to_check, dir)

判断文件是否在指定目录中都存在

  • 输入:
    • files_to_check: 需要检查的文件列表
    • dir:目标目录
  • 输出:是否都存在,只要有一个文件不存在都返回False

time_format(seconds=None, fmt="%Y%m%d-%H%M%S")

格式化时间

  • 输入:
    • seconds:需要格式化的时间,为None表示当前时间
    • fmt:时间格式
  • 返回:格式化之后的时间字符串
import comm
import time
print(time_format(time.time())) # 20241202-083726

base64_encode(input_str)

base64编码:

  • 输入:需要编码的字符串
  • 输出:编码之后的字符串
import comm
print(comm.base64_encode("test")) # dGVzdA==

base64_decode(base64_str)

base64解码:

  • 输入:bas64编码
  • 输出:解码之后的原始字符串
import comm
print(comm.base64_decode("dGVzdA==")) # test

exe_cmd(cmd, no_log=False)

执行操作系统命令:

  • 输入:
    • cmd:需要执行的os命令
    • 是否需要返回命令行输出
  • 输出:success,stdout、sterr
    • sucess:命令是否执行成功
    • 命令标准输出字符串(no_log=True时,强制为空)
    • 命令标准错误字符串(no_log=True时,强制为空)
import comm
su, st, er = exe_cmd("pwd")
print(st)

get_git_commit()

获取当前仓库git commit号

get_git_branch()

获取当前仓库git 分支名称

UT_FCOV(group, ignore_prefix=“ut_”)

获取功能覆盖率分组

  • 输入:
    • group 分组名称
    • ignore_prefix需要去掉的前缀
  • 输出:带模块前缀的覆盖率分组名

例如,在ut_backend/ctrl_block/decode/env/decode_wrapper.py中调用:

print(UT_FCOV("../../INT"))
# out
backend.ctrl_block.decode.INT

get_version_checker(target_version)

获取版本检测函数

  • 输入:目标版本字符串
  • 输出:检测函数

返回的检测函数,一般在fixture中进行版本判断。

import comm
import pytest

checker = comm.get_version_checker("openxiangshan-kmh-24092701+")

@pytest.fixture
def fixture():
  checker()
  ...

module_name_with(names, prefix=None)

给names统一加上模块前缀

  • 输入:
    • nanmes 需要添加前缀的字符列表
    • prefix 模块前缀
  • 返回:添加完成后的字符串列表

例如在a/b/c/d/e.py文件中调用该方法:

import comm
print(comm.module_name_with(["X", "Y"], ,"../../x"))
# out
["a.b.c.x.X", "a.b.c.x.Y"]

get_all_rtl_files(top_module, cfg)

获取名称为 top_module 的模块所依赖的所有 RTL 文件(.v.sv)的列表,并确保列表的第一个元素是 top_module 所在文件的绝对路径。所有 RTL 文件均位于 UnityChipForXiangShan/rtl/rtl 目录下。

  • 输入:

    • top_module:模块名称,类型为 str
    • cfg:配置信息,类型为 CfgObject
  • 输出:

    • 返回一个包含字符串的列表,列表中的每个字符串为模块依赖的 RTL 文件的绝对路径。列表的第一个元素为 top_module 所在文件的路径。

假设 top_module"ALU",且其依赖的 RTL 文件包括 ALU.svadder.vmultiplier.v

paths = get_all_rtl_files("ALU", cfg)

"""
paths可能的内容:
[
    "/path/to/UnityChipForXiangShan/rtl/rtl/ALU.sv",
    "/path/to/UnityChipForXiangShan/rtl/rtl/adder.v",
    "/path/to/UnityChipForXiangShan/rtl/rtl/multiplier.v"
]
"""

10 - 其他

测试用例管理

如果测试用例和目标RTL版本紧密相关,RTL发生变化,之前的测试用例不一定适用。此外,不同场景下有不同需求,例如验证测试环境时,不运行耗时太长的用例等。因此需要对用例进行管理,让用户能在在特定场景下跳过某些用例。为了实现该目标,我们需要通过pytest.mark.toffee_tags对于每个用例进行tag和version标记。然后在配置文件中设置需要跳过哪些tag或者只运行哪些tag的测试。

@pytest.mark.toffee_tags("my_tag", "version1 < version13")
def test_case_1():
    ...

例如上述test_case_1被标记上了标签my_tag,支持版本设置为version1version13。因此可以在配置文件中指定test.skip-tags=["my_tag"],来表示运行过程中跳过该用例。

pytest.mark.toffee_tags的参数说明如下:

@pytest.mark.toffee_tags(
    tag: Optional[list, str]     = []    # 用例标签
    version: Optional[list, str] = [],   # 用例rtl版本需求
    skip: callable               = None, # 自定义是否调过该用例,skip(tag, version, item): (skip, reason)
)

toffee_tags函数的参数tag支持strlist[str]类型。version参数也可以是strlist[str]类型,当为list类型时,进行精确匹配,如果为str则匹配规则如下:

  1. name-number1 < namer-number2: 表示版本需要在number1number2之间(包含边界,number表示数字,也可以为小数,eg 1.11
  2. name-number1+:表示number1版本以及以后的版本
  3. name-number1-:表示number1版本以及以前的版本

如果不存在上述情况,且有*或者?表示通配符类型。其他情况为精确匹配。

预定义标签,可以在comm/constants.py中查看,例如:

# Predefined tags for test cases
TAG_LONG_TIME_RUN = "LONG_TIME_RUN"  # 运行时间长
TAG_SMOKE         = "SMOKE"          # 冒烟测试
TAG_RARELY_USED   = "RARELY_USED"    # 非常少用
TAG_REGRESSION    = "REGRESSION"     # 回归测试
TAG_PERFORMANCE   = "PERFORMANCE"    # 性能测试
TAG_STABILITY     = "STABILITY"      # 稳定测试
TAG_SECURITY      = "SECURITY"       # 安全测试
TAG_COMPATIBILITY = "COMPATIBILITY"  # 兼容测试
TAG_OTHER         = "OTHER"          # 其他
TAG_CI            = "CI"             # 集成测试
TAG_DEBUG         = "DEBUG"          # 测试
TAG_DEMO          = "DEMO"           # demo

在默认配置中(config/_default.yaml),会过滤掉:LONG_TIME_RUNREGRESSIONRARELY_USEDCI 标记的测试。

可以通过@pytest.mark.toffee_tags可以为每个用例添加标签,也可以在模块中定义如下变量,实现对整个模块的所有测试用例添加标签。

toffee_tags_default_tag     = []   # 对应 tag 参数
toffee_tags_default_version = []   # 对应 version 参数
toffee_tags_default_skip    = None # 对应 skip 参数

*注:本环境中的版本号会自动过滤掉git标记,例如下载的RTL名称为openxiangshan-kmh-97e37a2237-24092701.tar.gz,则其版本号在本项目中为:openxiangshan-kmh-24092701, 可通过cfg.rtl.version或者comm.get_config().rtl.version获得。

版本检查

除了可以用标签toffee_tags自动检查版本外,还可以通过get_version_checker主动进行检查。一个单元测试通常由测试环境(Test Env)和测试用例组成(Test Case),Env对RTL引脚和功能进行封装,然后向Case提供稳定API,因此在Env中需要进行RTL版本判断,判断是否需要跳过使用本环境的所有测试用例。例如在Env中:

...
from comm import get_version_checker

version_check = get_version_checker("openxiangshan-kmh-*") # 获取RTL版本检查器,同toffee_tags中的veriosn参数

@pytest.fixture()
def my_fixture(request):
    version_check()                                        # 在 fixture 中主动检查
    ....
    yield dut
    ...

在上述例子中,Env在名称为my_fixturefixture中主动进行了版本检查。因此,在测试用例每次调用它时都会进行版本检查,如果检查不满足要求,则会跳过该用例的执行。

仓库目录说明

UnityChipForXiangShan
├── LICENSE            # 开源协议
├── Makefile           # Makefile主文件
├── README.en.md       # 英文readme
├── README.zh.md       # 中文readme
├── __init__.py        # Python模块文件,可以把整个UnityChipForXiangShan当成一个模块进行import
├── pytest.ini         # PyTest 配置文件
├── comm               # 公用组件:日志,函数,配置等
├── configs            # 配置文件目录
├── documents          # 文档
├── dut                # dut生成目录
├── out                # log,report等生成目录
├── requirements.txt   # python依赖
├── rtl                # rtl缓存
├── run.py             # 主python入口文件
├── scripts            # dut编译脚本
├── tools              # 公共工具模块
├── ut_backend         # 后端测试用例
├── ut_frontend        # 前端测试用例
├── ut_mem_block       # 访存测试用例
└── ut_misc            # 其他测试用例

配置文件说明

默认配置与说明如下:

# 默认配置文件
# 配置加载顺序: _default.yaml -> 用户指定的 *.yaml -> 命令行参数 eg: log.term-level='debug'
# RTL 配置
rtl:
  # RLT下载地址,从该地址获取所有*.gz.tar文件当成目标RTL
  base-url: https://<your_rtl_download_address>
  # 需要下载的RTL版本 eg: openxiangshan-kmh-97e37a2237-24092701
  version: latest
  # 需要存储RTL的目录,相对于当前配置文件的路径
  cache-dir: "../rtl"
# 测试用例配置(tag和case支持通配符)
test:
  # 跳过标签,所有带有该标签的测试用例都会被跳过
  skip-tags: ["LONG_TIME_RUN", "RARELY_USED", "REGRESSION", "CI"]
  # 目标标签,只有带有该标签的测试用例才会被执行(skip-tags会覆盖run-tags)
  run-tags: []
  # 跳过的测试用例,所有带有该名字(或者模块名)的测试用例都会被跳过。
  skip-cases: []
  # 目标测试用例,只有带有该名字(或者模块名)的测试用例才会被执行(skip-cases会覆盖run-cases)。
  run-cases: []
  # 跳过异常,所有抛出该异常的测试用例都会被跳过
  skip-exceptions: []
# 输出配置
output:
  # 输出目录,相对于当前配置文件的路径
  out-dir: "../out"
# 测试报告配置
report:
  # 报告生成目录,相对于output.out-dir
  report-dir: "report"
  # 报告名称,支持变量替换:%{host} 主机名,%{pid} 进程ID,%{time} 当前时间
  report-name: "%{host}-%{pid}-%{time}/index.html"
  # 报告内容
  information:
    # 报告标题
    title: "XiangShan KMH Test Report"
    # 报告用户信息
    user:
      name: "User"
      email: "User@example.email.com"
    # 目标行覆盖率 eg: 90 表示 90%
    line_grate: 99
    # 其他需要展示的信息,key为标题,value为内容
    meta:
      Version: "1.0"
# 日志配置
log:
  # 根输出级别
  root-level: "debug"
  # 终端输出级别
  term-level: "info"
  # 文件日志输出级别
  file-dir: "log"
  # 文件日志名称,支持变量替换:%{host} 主机名,%{pid} 进程ID,%{time} 当前时间
  file-name: "%{host}-%{pid}-%{time}.log"
  # 文件日志输出级别
  file-level: "info"
# 测试结果配置(该数据用于填充documents中的统计图等,原始数据来源于toffee-test生成的report)
#  运行完测试后,可通过 `make doc` 查看结果
doc-result:
  # 是否开测试结果后处理
  disable: False
  # 目标DUT的组织结构配置
  dutree: "%{root}/configs/dutree/xiangshan-kmh.yaml"
  # 结果名称,将会保存到输出的report目录
  result-name: "ut_data_progress.json"
  # 创建的测试报告的软连接到 hugo
  report-link: "%{root}/documents/static/data/reports"

可在上述配置文件中添加自定义参数,通过cfg = comm.get_config()获取全局配置信息,然后通过cfg.your_key进行访问。cfg信息为只读信息,默认情况下不能进行修改。

11 - 必要规范

为了方便将所有人的贡献集合在一起,需要在编码、环境、文档编写等方面采用相同的“规范”。

环境要求

  • python: 在python编码过程中,尽可能的采用标准库,采用兼容Python3大部分版本的通用语法(尽可能的在Python3.6 - Python3.12中通用),不要使用过旧或者过新的语法。
  • 操作系统: 建议Ubuntu 22.04,windows下,建议使用WSL2环境。
  • hugo 建议版本 0.124.1(版本过旧不支持软连接)
  • 少依赖 尽可能少的使用第三方C++/C库
  • picker 建议使用wheel安装picker工具和xspcomm库

测试用例

  • 代码风格 建议采用 PEP 8 规范
  • build脚本 需要按DUT的命名结构进行规范命名,不然无法正确收集验证结果。例如backend.ctrl_block.decodeUT在scripts目录中对应的build文件名称应该为build_ut_backend_ctrl_block_decode.py(以固定前缀build_ut_开始,点.用下划线_进行替换)。在脚本中实现 build(cfg) -> boolline_coverage_files(cfg) -> list[str] 方法。build用于编译DUT为python模块,line_coverage_files方法用于返回需要统计的代码行覆盖率文件。
  • 用例标签 如果用例无法做到版本通用,需要用pytest.mark.toffee_tags标记支持的版本。
  • 用例抽象 编写的测试用例输入不能出现DUT的具体引脚等强耦合内容,只能调用基于DUT之上的函数封装。例如对于加法器 adder,需要把dut的目标功能封装为 dut_wrapper.add(a: int, b: int) -> int, bool,在test_case中仅仅调用 sum, c = add(a, b)进行测试。
  • 覆盖抽象 在编写功能覆盖率时,其检查点函数的输入也不能有DUT引脚。
  • 环境抽象 对于一个验证,通常分为2部分:Test Case 和 Env (用例以外的都统一称为Env,它包含DUT、驱动、监控等),其中Env需要提供对外的功能抽象接口,不能对外呈现出太多细节。
  • 测试说明 在每个DUT的验证环境中,需要通过README.md对该环境进行说明,例如需要对Env提供给Case的接口进行说明,目录结构说明等。

PR编写

  • 标题 简洁明了,能概括PR的主要内容。
  • 详细描述 详细说明PR的目的,修改的内容以及相关背景信息。入解决已有的问题需要给出链接(例如Issue)。
  • 关联问题 在描述中关联相关问题,例如 Fixes #123,以便在合并PR时关闭关联问题。
  • 测试 需要进行测试,并对测试结果进行描述
  • 文档 PR涉及到的文档需要同步修改
  • 分解 当PR涉及到的修改很多时,需要判断是否拆分成多个PR
  • 检查清单 检查编译是否通过、代码风格是否合理、是否测试通过、是否有必要的注释等
  • 模板 以及提供的PR模块请参考链接

ISSUE编写

要求同上

12 - 验证文档

12.1 - 验证文档规范

本规范规定了“万众一芯”验证文档的必要形式和结构(不是验证报告的模板),已发布和将来将要发布的文档都需要遵循这一规范。

万众一芯验证文档格式规范

验证文档标题请用一号标题格式(一个#),以加法器验证文档为例,其标题可以为:进位加法器设计与验证。

文档概述

文档概述标题请用二号标题格式(两个#)。

【必填项】 在该部分对整个文档进行简约描述,例如内容概述,待验证模块的基本功能、特殊需求、特定规格、目标读者、知识前置等。目的是通过对该部分,读者便了解是否具有其感兴趣的内容。例如本文档是对验证文档的编写要求进行描述,便于多文档协作,规范验证的数据输入,特定数据标签等。

术语说明

术语说明标题请用二号标题格式(两个#)。

【必填项】 该部分需要列出术语和关键概念解释,方便读者参考。

  1. 优先解释模块专有缩写(如TLB, FIFO等),且用缩写(全名)的格式填写在“名称”一栏中。
  2. 对容易混淆的概念请务必明确(如虚拟地址和物理地址等)
  3. 示例格式如下:
名称 定义
TLB(Translation Lookaside Buffer) 地址转换的缓存单元,用于加速虚拟地址到物理的转换
FIFO(First In First Out) 先进先出队列
写回 发生在Cache替换时,如果被替换块为脏块,需要将缓存行写回对应内存位置

如果有其他补充情况请在此说明,例如:上述命名描述仅针对香山处理器,不代表RISC-V标准或者其他处理器。

前置知识

前置知识标题请用二号标题格式(两个#)。

【可选项】 在阅读文档或进行验证之前,建议掌握一些关键前置知识,以便更深入理解相关内容。例如,在撰写LoadStoreQueue(LSQ)文档时,讲述RAW(Read After Write)违例有助于理解操作之间的依赖关系。在撰写Icache或L2Cache文档时,介绍缓存层级、替换策略和一致性模型等基本概念也有助于读者理解。如果涉及复杂算法,也应对其进行简要描述。

基本要求:

  1. 该部分内容应简洁,易于理解。如篇幅较长,可将内容移至附录。
  2. 针对较为复杂的内容,可以通过图像、伪代码和案例进行解释,以降低理解难度。

下面是一个举例:

st-ld违例

在现代处理器中,Load 和 Store 指令通常采用乱序执行的方式进行处理。这种执行策略旨在提高处理器的并行性和整体性能。然而,由于 Load 和 Store 指令在流水线中的乱序执行,常常会出现 Load 指令越过更早的相同地址的 Store 指令的情况。这意味着,Load 指令本应通过前递(forwarding)机制从 Store 指令获取数据,但由于 Store 指令的地址或数据尚未准备好,导致 Load 指令未能成功前递到 Store 的数据,而 Store 指令已被提交。由此,后续依赖于该 Load 指令结果的指令可能会出现错误,这就是 st-ld 违例。

考虑以下伪代码示例:

ST R1, 0(R2)  ; 将 R1 的值存储到 R2 指向的内存地址
LD R3, 0(R2)  ; 从 R2 指向的内存地址加载值到 R3
ADD R4, R3, R5 ; 使用 R3 的值进行计算

假设在这个过程中,Store 指令由于某种原因(如缓存未命中)未能及时完成,而 Load 指令已经执行并读取了旧的数据(例如,从内存中读取到的值为 0)。此时,Load 指令并未获得 Store 指令更新后的值,导致后续计算的数据错误。

通过上述例子,可以清楚地看到 Store-to-Load 违例如何在乱序执行的环境中导致数据一致性问题。这种问题强调了在指令调度和执行过程中,确保正确的数据流动的重要性。现代处理器通过多种机制来检测和解决这种违例,以维护程序的正确性和稳定性。

整体框图

整体框图标题请用二号标题格式(两个#)。

【可选项】 该部分为可选章节,若模块含多个子模块或复杂数据流,需提供框图辅助说明,便于读者理解。

基本要求:

  1. 图必须清晰,最好为矢量图,可使用Visio/Draw.io等工具绘制,导出为PNG/SVG格式;
  2. 图中需标注关键信号流向;
  3. 框图中子模块命名需与“子模块列表”章节严格一致;
  4. 图像和图表标题的位置需要居中;
  5. 如果有多个图表,图表题目需要添加相应标号,如图1、图2等;

示例:

示例图像

示例图1:IFU 整体框图

流水级示意图

流水级示意图标题请用二号标题格式(两个#)。

【可选项】 若为流水线型模块,需说明各级流水功能与时序关系。

编写要求

  1. 图必须清晰,最好为矢量图,可使用Visio/Draw.io等工具绘制,导出为PNG/SVG格式;
  2. 涉及到的模块名称需要与上下文保持一致;
  3. 重要信号除了列出信号名称以外,还需要标明位宽等信息;
  4. 图像和图表标题的位置需要居中;
  5. 如果有多个图表,图表题目需要添加相应标号,如图1、图2等。

示例:

示例图像

示例图2:LSU-LoadUnit 流水线架构图

子模块列表

子模块列表标题请用二号标题格式(两个#)。

【可选项】 如果一个模块由多个子模块组成,则需要在此处列出所有相关的子模块,并进行简要说明。这有助于清晰地展示模块的结构和功能,便于读者理解各个子模块之间的关系及其在整体系统中的作用。

以下是IFU top文档中的一个示例:

子模块 描述
PreDecoder 预译码模块,用于生成有效指令标识和类型信息
F3Predecoder F3阶段预译码模块,从PreDecoder中时序优化出来的模块,负责判定CFI指令的类型
RVCExpander RVC指令扩展模块,负责对传入的指令进行指令扩展,并解码计算非法信息
PredChecker 预检查模块,校验并修正预测信息
FrontendTrigger 前端断点模块,用于在前端设置硬件断点和检查

模块功能说明

模块功能说明标题请用二号标题格式(两个#)。

【必填项】 需采用功能树形式逐级分解DUT的各项功能,并对所有功能进行描述,确保每个功能点都对应相应的测试点。这种结构化的方法不仅有助于全面覆盖所有功能,还便于后续文档的维护和更新。

编写规则:

  1. 请使用 <mrs-functions>``</mrs-functions> 标签包裹整个“模块功能说明”部分;
  2. 采用 X.Y.Z 多级编号(如 1.2.3 表示主功能 1 → 子功能 2 → 测试点 3,且可进一步细分);
  3. 多级编号的标题格式按照级别增加,例如:“1. 读FIFO操作”应为三号标题格式 “1.1. 常规读取”应为四号标题格式;
  4. 功能描述应清晰列出输入条件、处理过程和输出结果。
  5. 针对每个功能进行测试点分解,应详细列出每个测试点,明确其目的和预期结果。
  6. 如果测试点较多可以先列一个小表格。

具体来说,可以按照如下的格式写作(示例内容仅供参考,并不代表实际逻辑或内容):

示例:FIFO模块功能说明

示例1. 读FIFO操作

示例1.1. 常规读取

功能描述:当rd_en=1且empty=0时,在时钟上升沿输出rdata

建议观测点

  • 读指针递增逻辑
  • rdata与预期数据匹配

示例2. 写FIFO操作

示例2.1. 常规写入

功能描述:当wr_en=1且full=0时,在时钟上升沿存储wdata

观测点

  • 写指针递增逻辑
  • 存储阵列数据更新

示例3. 接收FTQ取指令请求(F0流水级)

​在F0流水级,IFU接收来自FTQ以预测块为单位的取指令请求。请求内容包括预测块起始地址、起始地址所在cache line的下一个cache line开始地址、下一个预测块的起始地址、该预测块在FTQ里的队列指针、该预测块有无taken的CFI指令(控制流指令)和该taken的CFI指令在预测块里的位置以及请求控制信号(请求是否有效和IFU是否ready)。每个预测块最多包含32字节指令码,最多为16条指令。IFU需要置位ready驱动FTQ向ICache发送请求。

示例3.1. F0流水级接收请求

IFU应当能向FTQ报告自己已ready。

所以,对于这一测试点我们只需要在发送请求后检查和ftq相关的的ready情况即可。

序号 功能名称 测试点名称 描述
1.1 IFU_RCV_REQ READY IFU接收FTQ请求后,设置ready

常量说明

常量说明标题请用二号标题格式(两个#)。

【可选项】 需要列出模块中所有可配置参数及其物理意义,以便于用户理解各参数的作用和影响。

示例:

常量名 常量值 解释
ADDR_WIDTH 64 地址总线位宽
FIFO_DEPTH 8 深度配置

接口说明

接口说明标题请用二号标题格式(两个#)。

【必填项】 详细解释各种接口的含义和来源,包括接口的功能、用途。这有助于用户理解各接口的工作原理和应用场景,从而更有效地使用这些接口。

编写规则

  1. 信号按功能(如时钟复位、数据输入、控制信号等)或来源(其他模块)分组;
  2. 可以将一些同质的信号一起解释;
  3. 特殊协议信号需注明时序要求(如AXI的VALID/READY握手)。

接口时序

接口时序标题请用二号标题格式(两个#)。

【可选项】 针对复杂接口,可以提供波形图案例,以直观展示信号变化和时间关系。

以下是节选自IFU top文档的一个例子:

接口时序

FTQ 请求接口时序示例

port1

上图示意了三个 FTQ 请求的示例,req1 只请求缓存行 line0,紧接着 req2 请求 line1 和 line2,当到 req3 时,由于指令缓存 SRAM 写优先,此时指令缓存的读请求 ready 被指低,req3 请求的 valid 和地址保持直到请求被接收。

ICache 返回接口以及到 Ibuffer 和写回 FTQ 接口时序示例

port2

上图展示了指令缓存返回数据到 IFU 发现误预测直到 FTQ 发送正确地址的时序,group0 对应的请求在 f2 阶段了两个缓存行 line0 和 line1,下一拍 IFU 做误预测检查并同时把指令给 Ibuffer,但此时后端流水线阻塞导致 Ibuffer 满,Ibuffer 接收端的 ready 置低,goup0 相关信号保持直到请求被 Ibuffer 接收。但是 IFU 到 FTQ 的写回在 tio_toIbuffer_valid 有效的下一拍就拉高,因为此时请求已经无阻塞地进入 wb 阶段,这个阶段锁存的了 PredChecker 的检查结果,报告 group0 第 4(从 0 开始)个 2 字节位置对应的指令发生了错误预测,应该重定向到 vaddrA,之后经过 4 拍(冲刷和重新走预测器流水线),FTQ 重新发送给 IFU 以 vaddrA 为起始地址的预测块。

测试点总表

测试点总表标题请用二号标题格式(两个#)。

【必填项】 对模块功能说明中细分的测试点进行综合整理,采用表格形式列出,便于用户快速查阅和理解。

表格规范

  1. 请用<mrs-testpoints></mrs-testpoints>标签包裹测试点总表,方便我们后续使用脚本提取测试点
  2. 表格共列四项,序号,功能名称,测试点名称和解释
    1. 序号:测试点的序号格式和功能点类似。即,即测试点1.2.3.4 可能是功能点1.2.3的第4个测试点
    2. 功能名称:用英文大写命名,可用下划线分割单词,可以使用markdown语法为功能名称添加到具体功能点解释的链接,即形如[文本](跳转目标)
    3. 测试点名称:用英文大写命名,可用下划线分割单词
    4. 解释:简单描述测试点所需的输入和输出需求,明确判断条件

表格示例

以下是节选自IFU top文档的一个例子:

序号 功能名称 测试点名称 描述
1.1 IFU_RCV_REQ READY IFU接收FTQ请求后,设置ready
2.1.1 IFU_F1_INFOS PC IFU接收FTQ请求后,在F1流水级生成PC
2.1.2 IFU_F1_INFOS CUT_PTR IFU接收FTQ请求后,在F1流水级生成后续切取缓存行的指针
2.2.1 IFU_F2_INFOS EXCP_VEC IFU接收ICache内容后,会根据ICache的结果生成属于每个指令的异常向量

附录

附录标题请用二号标题格式(两个#)。

【可选项】 此部分用于存放正文的补充内容,以便进行扩展和详细说明,旨在使文档格式更加清晰,排版更加合理。

以下是节选自IFU RVCExpander文档的一个例子:

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

addi4spn

该指令将一个0扩展的非0立即数加到栈指针寄存器x2上,并将结果写入rd'

其中,nzuimm[5:4|9:6|2|3]的含义是: ···

下方展示了模板和两个验证案例:

12.1.1 - 文档模板

以下是一份验证文档的完整模板(请一定同提交的验证报告区分开来)


# 验证文档各部分说明

## 文档概述【必填项】 

在该部分对整个文档进行简约描述,例如内容概述,待验证模块的基本功能、特殊需求、特定规格、目标读者、知识前置等。目的是通过对该部分,读者便了解是否具有其感兴趣的内容。例如本文档是对验证文档的编写要求进行描述,便于多文档协作,规范验证的数据输入,特定数据标签等。

## 术语说明 【必填项】 列出术语和关键概念解释,方便读者参考

优先解释模块专有缩写(如TLB, FIFO等),如果有缩写,请用`缩写(全称)的方式填在表格的“名称”栏目中`

对容易混淆的概念请务必明确(如虚拟地址和物理地址等)

| 名称 | 定义 |
| ------- | ---|
| 缩写1(FULL_NAME_1)	| 描述1 |
| 缩写2(FULL_NAME_2)	| 描述2 |
| 概念名1	| 描述3 |

## 前置知识【可选项】

在阅读文档或进行验证之前,建议掌握一些关键前置知识,以便更深入理解相关内容。例如,在撰写LoadStoreQueue(LSQ)文档时,讲述RAW(Read After Write)违例有助于理解操作之间的依赖关系。在撰写Icache或L2Cache文档时,介绍缓存层级、替换策略和一致性模型等基本概念也有助于读者理解。如果涉及复杂算法,也应对其进行简要描述。

基本要求:
1. 该部分内容应简洁,易于理解。如篇幅较长,可将内容移至附录。
2. 针对较为复杂的内容,可以通过图像、伪代码和案例进行解释,以降低理解难度。


## 整体框图 【可选项】 若模块含多个子模块或复杂数据流,需提供框图辅助说明

可使用Visio/Draw.io等工具绘制,导出为PNG/SVG格式;
需标注关键信号流向;
框图中子模块命名需与“子模块列表”章节严格一致。

## 流水级示意图 【可选项】 若为复杂流水线型模块,需说明各级流水功能与时序关系

可使用Visio/Draw.io等工具绘制,导出为PNG/SVG格式;
涉及到的模块名称需要保持一致性
重要数据除了列出名称以外,还需要标明位宽等信息

## 子模块列表 【可选项】 若模块由多个子模块组成,需在此列出

以下是IFU top文档中的一个示例:

| 子模块                 | 描述                |
| ---------------------- | ------------------- |
| [子模块1](子模块1文档位置) | 子模块1描述       |
| [子模块2](子模块2文档位置) | 子模块2描述       |
| [子模块3](子模块3文档位置) | 子模块3描述       |


<mrs-functions>

## 模块功能说明 【必填项】 需按功能树形式逐级分解,每个功能点需对应后续测试点。

请用<mrs-functions></functions>包裹整个“模块功能说明”部分。

采用X.Y.Z多级编号(如1.2.3表示主功能1→子功能2→测试点3,也可以继续细分)

功能描述需明确输入条件、处理过程、输出结果

### 1. 功能A说明
针对功能A分解测试点

如果测试点较多可以先列一个小表格

### 2. 功能B说明

针对功能B分解测试点

如果测试点较多可以先列一个小表格

### 3. 功能C说明

针对功能C分解测试点

如果测试点较多可以先列一个小表格;针对每个测试点,给出设置cov_group的建议

</mrs-functions>


## 常量说明 【可选项】 需列出模块中所有可配置参数及其物理意义


| 常量名 | 常量值 | 解释 |
| ---- | ---- | ---- |
| 常量1 | 64 | 常量1解释 |
| 常量2 | 8 | 常量2解释 |
| 常量3 | 16 | 常量3解释 |


## 接口说明 【必填项】 详细解释各种接口的含义、来源

信号按功能(如时钟复位、数据输入、控制信号等)或来源(其他模块)分组;

可以将一些同质的信号一起解释;

特殊协议信号需注明时序要求(如AXI的VALID/READY握手)。

使用时,请将下面的接口组名称和说明替换为符合您模块实际意义的内容

### 接口组1说明

请在这里填充接口组1的说明

#### 接口组1_1说明

请在这里填充接口组1_1的说明

如果不能细分,请进一步说明该组中所有接口

### 接口组2说明

请在这里填充接口组2的说明

如果不能细分,请进一步说明该组中所有接口

...

## 接口时序 【可选项】 对复杂接口,提供波形图的案例

### 案例1

请在这里填充时序案例1

### 案例2

请在这里填充时序案例2

## 测试点总表 (【必填项】针对细分的测试点,列出表格)

实际使用下面的表格时,请用有意义的英文大写的功能名称和测试点名称替换下面表格中的名称

<mrs-testpoints>

| 序号 |  功能名称 | 测试点名称      | 描述                  |
| ----- |-----------------|---------------------|------------------------------------|
| 1\.1\.1 | FUNCTION_1_1 | TESTPOINT_A | 功能1\.1的测试点A,使用时请替换为您的测试点的输入输出和判断方法 | 
| 1\.1\.2 | FUNCTION_1_1 | TESTPOINT_B | 功能1\.1的测试点B,使用时请替换为您的测试点的输入输出和判断方法 | 
| 1\.1\.3 | FUNCTION_1_1 | TESTPOINT_C | 功能1\.1的测试点C,使用时请替换为您的测试点的输入输出和判断方法 | 
| 1\.2\.1 | FUNCTION_1_2 | TESTPOINT_X | 功能1\.2的测试点X,使用时请替换为您的测试点的输入输出和判断方法 | 
| 1\.2\.2 | FUNCTION_1_2 | TESTPOINT_Y | 功能1\.2的测试点Y,使用时请替换为您的测试点的输入输出和判断方法 | 
| 2\.1 | FUNCTION_2 | TESTPOINT_2A | 功能2的测试点2A,使用时请替换为您的测试点的输入输出和判断方法 | 
| 2\.2 | FUNCTION_2 | TESTPOINT_2B | 功能2的测试点2B,使用时请替换为您的测试点的输入输出和判断方法 | 

</mrs-testpoints>

## 附录【可选项】 

此部分用于存放正文的补充内容,以便进行扩展和详细说明,旨在使文档格式更加清晰,排版更加合理。

12.1.2 - FIFO文档案例

以下以FIFO为例,展示了一个简单的文档案例

`timescale 1ns / 1ps
module FIFO ( //data_width = 8  data depth =8
  input clk,
  input rst_n,
  input wr_en,          //写使能
  input rd_en,          //读使能
  input [7:0]wdata,     //写入数据输入
  output [7:0]rdata,    //读取数据输出
  output empty,         //读空标志信号
  output full           //写满标志信号
);
  reg [7:0] rdata_reg = 8'd0;
  assign rdata = rdata_reg;

  reg  [7:0] data [7:0];     //数据存储单元(8bit数据8个)
  reg  [3:0] wr_ptr = 4'd0;  //写指针
  reg  [3:0] rd_ptr = 4'd0;  //读指针
  wire [2:0] wr_addr;        //写地址(写指针的低3位)
  wire [2:0] rd_addr;        //读地址(读指针的低3位)

assign wr_addr = wr_ptr[2:0];
assign rd_addr = rd_ptr[2:0];

always@(posedge clk or negedge rst_n)begin //写数据
  if(!rst_n) 
    wr_ptr <= 4'd0;
  else if(wr_en && !full)begin
    data[wr_addr]  <= wdata;
    wr_ptr <= wr_ptr + 4'd1;
  end
end

always@(posedge clk or negedge rst_n)begin //读数据
  if(!rst_n)
    rd_ptr <= 'd0;
  else if(rd_en && !empty)begin
    rdata_reg  <= data[rd_addr];
    rd_ptr <= rd_ptr + 4'd1;
  end
end

assign empty = (wr_ptr == rd_ptr); //读空
assign full  = (wr_ptr == {~rd_ptr[3],rd_ptr[2:0]}); //写满

endmodule

FIFO 模块验证文档

文档概述

本文档描述FIFO的功能,并根据功能给出测试点参考,方便测试的参与者理解测试需求,编写相关测试用例。

术语说明

缩写 全称 定义
FIFO First In First Out 先进先出的数据缓冲队列

功能说明

本次需要验证的是FIFO,一种常见的硬件缓冲模块,在硬件电路中临时存储数据,并按照数据到达的顺序进行处理。

本次需要验证的FIFO每次可写可读8位数据,容量为8。

1. 读FIFO操作

1.1. 常规读取

功能描述:当rd_en=1且empty=0时,在时钟上升沿输出rdata

建议观测点

  • 读指针递增逻辑
  • rdata与预期数据匹配

1.2. 读空栈

功能描述:当empty=1且rd_en=1时,rdata保持无效值

建议观测点

  • empty信号持续为高
  • 读指针无变化

1.3. 无读使能不读

功能描述:当rd_en=0时,无论FIFO状态如何均不更新rdata

建议观测点

  • 连续写入后关闭读使能,验证读指针冻结

2. 写FIFO操作

2.1. 常规写入

功能描述:当wr_en=1且full=0时,在时钟上升沿存储wdata

观测点

  • 写指针递增逻辑
  • 存储阵列数据更新

2.2. FIFO已满无法写入

功能描述:当full=1且wr_en=1时,wdata被丢弃

观测点

  • full信号持续为高
  • 存储阵列内容不变

2.3. 无写使能不写

功能描述:当wr_en=0时,无论FIFO状态如何均不写入数据

观测点

  • 写指针冻结
  • 存储阵列内容保持不变

3. 复位操作

3.1. 复位控制

功能描述:当rst_n=0时,清空FIFO并重置指针

观测点

  • 复位后empty=1且full=0
  • 读写指针归零

常量说明

常量名 常量值 解释
FIFO_DEPTH 8 FIFO存储单元数量
DATA_WIDTH 8 数据总线位宽

接口说明

输入接口

信号名 方向 位宽 描述
clk Input 1 主时钟
rst_n Input 1 异步复位
wr_en Input 1 写使能
wdata Input 8 写入数据
rd_en Input 1 读使能

输出接口

信号名 方向 位宽 描述
rdata Output 8 读出数据
empty Output 1 FIFO空标志(高有效)
full Output 1 FIFO满标志(高有效)

测试点总表

建议各个测试点的覆盖组使用下表描述的功能和测试点名称进行命名。

比如FIFO_READ的测试点NORMAL,其覆盖点建议命名为FIFO_READ_NORMAL

序号 功能名称 测试点名称 描述
1.1 FIFO_READ NORMAL fifo有数据时,设置读使能,可以读出数据
1.2 FIFO_READ EMPTY fifo为空时,设置读使能,无法读出数据
1.3 FIFO_READ NO_EN fifo有数据时,不设置读使能,无法读出数据
2.1 FIFO_WRITE NORMAL fifo未满时,设置写使能,可以写入数据
2.2 FIFO_WRITE FULL fifo已满时,设置写使能,可以写入数据
2.3 FIFO_WRITE NO_EN fifo未满时,不设置写使能,无法写入数据
3.1 FIFO_RESET RESET 重置后,栈为空

12.1.3 - 果壳Cache文档案例

本文档将以果壳L1Cache作为案例,展示一个具有相当复杂度的模块的验证说明文档例子(请一定同提交的验证报告区分开来)。

果壳L1Cache验证文档

文档概述

本文档针对NutShell L1Cache的验证需求撰写,通过对其功能进行描述并依据功能给出参考测试点,从而帮助验证人员编制测试用例。

果壳(NutShell)是一款由5位中国科学院大学本科生设计的基于RISC-V RV64开放指令集的顺序单发射处理器(NutShell·Github), 隶属于国科大与计算所“一生一芯”项目。而果壳Cache(NutShell Cache)是其缓存模块,采用可定制化设计(L1 Cache和L2 Cache采用相同的模板生成,只需要调整参数),具体来说,L1 Cache(指令Cache和数据Cache)大小为32KB,L2 Cache大小为128KB, 在整体结构上,果壳Cache采用三级流水的结构。

本次验证的目标是L1 Cache,即一级缓存。

术语说明

名称 定义
MMIO(Memory-Mapped Input/Output) 内存映射IO
写回 Cache需要进行替换时,会将脏替换块写回内存
关键字优先方案 缺失发生时,系统会优先获取CPU所需要的当前指令或数据所对应的字

前置知识

Cache的层次结构

Cache有三种主要的组织方式:直接映射(Direct-Mapped)Cache、组相连(Set-Associative)Cache和全相连(Fully-Associative)Cache。对于物理内存中的一个数据,如果在Cache中只有一个位置可以存放它,这就是直接映射Cache;如果有多个位置可以存放这个数据,这就是组相连Cache;如果Cache中的任何位置都可以存放这个数据,这就是全相连Cache。

直接映射Cache和全相连Cache实际上是组相连Cache的两种特殊情况。现代处理器中的Cache通常属于这三种方式中的一种。例如,翻译后备缓冲区(TLB)和Victim Cache多采用全相连结构,而普通的指令缓存(I-Cache)和数据缓存(D-Cache)则采用组相连结构。当处理器需要执行一个指令时,它会首先查找该指令是否在I-Cache中。如果在,则直接从I-Cache中读取指令并执行;如果不在,则需要从内存中读取指令到I-Cache中,再执行。与I-Cache类似,当处理器需要读取或写入数据时,会首先查找D-Cache。如果数据在D-Cache中,则直接读取或写入;如果不在,则需要从内存中加载数据到D-Cache中。与I-Cache不同的是,D-Cache需要考虑数据的一致性和写回策略。为了保证数据的一致性,当数据在D-Cache中被修改后,需要同步更新到内存中。

composition

Cache的写入

在执行写数据时,如果只是向D-Cache中写入数据而不改变其下级存储器中的数据,就会导致D-Cache和下级存储器对于同一地址的数据不一致(non-consistent)。为了保持一致性,一般Cache在写命中状态下采用两种写入方式: (1)写通(Write Through):数据写入D-Cache的同时也写入其下级存储器。然而,由于下级存储器的访问时间较长,而存储指令的频率较高,频繁地向这种较慢的存储器中写入数据会降低处理器的执行效率。 (2)写回(Write Back):数据写入D-Cache后,只是在Cache line上做一个标记,并不立即将数据写入更下级的存储器。只有当Cache中这个被标记的line要被替换时,才将其写入下级存储器。这种方式能够减少向较慢存储器写入数据的频率,从而获得更好的性能。然而,这种方式会导致D-Cache和下级存储器中许多地址的数据不一致,给存储器的一致性管理带来一定的负担。

D-Cache处理写缺失一般有两种策略:

(1)非写分配(Non-Write Allocate):直接将数据写入下级存储器,而不将其写入D-Cache。这意味着当发生写缺失时,数据会直接写入到下级存储器,而不会经过D-Cache。

(2)写分配(Write Allocate):在发生写缺失时,会先将相应地址的整个数据块从下级存储器中读取到D-Cache中,然后再将要写入的数据合并到这个数据块中,最终将整个数据块写回到D-Cache中。这样做的好处是可以在D-Cache中进行更多的操作,但同时也增加了对内存的访问次数和延迟。 写通(Write Through)和非写分配(Non-Write Allocate)将数据直接写入下级存储器,而写回(Write Back)和写分配(Write Allocate)则会将数据写入到D-Cache中。通常情况下,D-Cache的写策略搭配为写通+非写分配或写回+写分配。

写通示意图

write-through

写通示意图

write-back

写回示意图

替换策略

读写D-Cache发生缺失时,需要从对应的Cache Set中找到一个cache行,来存放从下级存储器中读出的数据,如果此时这个Cache Set内的所有Cache行都已经被占用了,那么就需要替换掉其中一个,如何从这些有效的Cache行找到一个并替换它,这就是替换策略,本节介绍几种最常用的替换策略。

近期最少使用法会选择最近被使用次数最少的Cache行,因此这个算法需要追踪每个Cache行的使用情况,这需要为每个Cache行都设置一个年龄(age)部分,每当一个Cache行被访问时,它对应的年龄部分就会增加,或者减少其他Cache行的年龄值,这样当进行替换时,年龄值最小的那个Cache行就是被使用次数最少的了,会选择它进行替换。

随机替换算法硬件实现简单,这种方法发生缺失的频率会更高一些,但是随着Cache容量的增大,这个差距是越来越小的。在实际的设计中,很难实现严格的随机,一般采用一种称为时钟算法(clock algorithm)的方法实现近似的随机,它的工作原理本质上是一个时钟计数器,计数器的宽度由Cache的路的个数决定,当要替换时,就根据这个计数器选择相应的行进行替换。这种方法硬件复杂度较低,也不会损失较多的性能,因此是一种折中的方法。

整体框图和流水级

以下是L1Cache的整体框图和流水级示意:

Cache

子模块列表

以下是NutShell L1Cache的一些子模块:

子模块 描述
s1 缓存阶段1
s2 缓存阶段2
s3 缓存阶段3
metaArray 以数组形式存储元数据
dataArray 以数组形式存储缓存数据
arb 总线仲裁器

上下游通信总线采用SimpleBus总线,包含了req和resp两个通路,其中req通路的cmd信号表明请求的操作类型,可以通过检查该信号获得访问类型。SimpleBus总线共有七种操作类型,由于NutShell文档未涉及probe和prefetch操作,在验证中只出现五种操作:read、write、readBurst、writeBurst、writeLast,前两种为字读写,后三种为Burst读写,即一次可以操作多个字。

模块功能说明

Cache的功能是降低访存的时间开销,其功能本质上和内存是一致的。也就是说,不论是向Cache存数还是取数,其都应该和直接向内存存取的数是一样的。 因此,Cache的基础读写功能将成为我们的第一个功能点。

进一步,访问Cache的地址空间分为MMIO和内存。其中,访问MMIO的地址空间时,Cache一定会Miss,然后将请求转发到MMIO端口上。而访问内存的地址空间时,Cache则会根据该地址所在的Cache Line是否在Cache中而触发Hit或者Miss。Hit则直接返回响应,Miss则会将请求转发到内存端口。如果被替换的受害者行之前被写过,是dirty的,则要先将受害者行写回(write-back)内存,否则直接从内存加载缺失的Cache Line,重填(refill)回Cache。

1. 内存备份

Cache的功能本质上和内存是一致的,所以,不管向Cache存或取数据,本质上都应该和从内存存取的数一样。

据此,我们为这一功能点安排了一个测试点:即Cache应当为内存的备份。在实际测试过程中,必须同时考虑读写两方面的一致性。

2. MMIO

Cache会根据地址所在的区间,判断是否发生MMIO请求。

2.1. MMIO读写

如果发生MMIO请求则会将请求转发到MMIO的端口上,而不会发生Cache行的读写。此外,MMIO请求不是Burst请求,每次只会写入或读出一个地址的数据,而不是一个Cache行的数据。因此,在MMIO端口上不应当观测到Burst的请求类型。

据此,我们可以设计下述两个测试点:

序号 功能名称 测试点名称 描述
2.1.1 CACHE_MMIO_RW FORWARD Cache接收到MMIO空间的请求时,不应发生读写,而是直接转发给MMIO端口
2.1.2 CACHE_MMIO_RW NO_BURST Cache接收到MMIO空间的请求时,MMIO端口接收到的Cache请求不应为BURST类型

2.2. MMIO阻塞

NutShell手册指出,在检测出MMIO请求后会阻塞流水线。

因此,我们将设计这一测试点:当MMIO请求发出后,应当检查流水线是否阻塞。

3. Cache命中

NutShell的Cache采用写回策略,因此,在写命中时,需要标记脏块,后续发生缓存行替换时再将对应的缓存行写回内存。

同时,因为采用写回方式,所以,即使写命中也不需要同内存进行交互,因此收到回复的周期数更少。

3.1. 写命中

由于果壳Cache采用写回策略,因此,在发生写命中时,需要标记脏位,后续还要写回内存中。据此,可以设置一个测试点。

3.2. 命中时序

命中发生时,即使是写命中,也无需写回或者重填,因此,回复的时间会更短一些。

以下是本功能点的所有测试点:

序号 功能名称 测试点名称 描述
3.2.1 CACHE_HIT WRITE Cache写命中时,应设置脏位
3.2.2 CACHE_HIT SHORTER Cache写命中时,回复的周期应该更少

4. Cache缺失

为了创造Cache Miss的测试环境,首先需要通过一系列的Load操作先将Cache填满。后续需要触发Cache Miss时,只需要访问上述Load覆盖范围之外的地址即可。

4.1. 缺失通用行为

发生Cache Miss时,会阻塞流水线,同时,NutShell Cache重填时采用关键字优先方案,即缺失发生时,系统会优先获取CPU所需要的当前指令或数据所对应的字。因此,Cache向内存请求数据时,发出的首个地址应当是向Cache发出请求时的地址。例如,假设向Cache发出0x1000地址的读请求,此时发生Cache Miss,Cache会向内存发出读请求,这个请求的首地址应当是0x1000。显然Cache缺失时,回复的时间会更长。

从而,我们可以划分如下的测试点:

序号 功能名称 测试点名称 描述
4.1.1 CACHE_MISS_COMMON BLOCK 发生缺失时,也会阻塞流水线
4.1.2 CACHE_MISS_COMMON CRITICAL_WORD Cache缺失时,Cache发出请求的首个地址应当是向Cache请求的地址
4.1.3 CACHE_MISS_COMMON LONGER Cache缺失时,回复的时间会更长

4.2. 脏块写回

当需要替换的Cache块是脏块时,首先会进行写回的操作。

在进行测试时,我们首先需要创建脏块的环境,由于NutShell Cache采用随机替换的策略,因此我们考虑将整个Cache都设置成脏块。操作也是简单的,在上述的Load的基础上,只需要在每个CacheLine的起始地址进行一次Store操作即可。

4.3. 干净块不写回

当需要替换的Cache块是干净的时,不会写回这个Cache块。

常量说明

常量名 常量值 解释
缓存行大小 64 以字节为单位的缓存行大小
L1Cache大小 32 L1Cache的总容量,单位为千字节

接口说明

信号 说明
clock
reset
时钟
复位信号
io_flush
io_empty
io_in_*


请求总线信号(req & resp)
io_out_mem_*
io_mmio_*
io_out_coh_*
victim_way_mask
cache向内存请求的总线信号
cache向MMIO请求的总线信号
一致性相关的信号
受害者相关信号,即被替换的cache块相关信息

测试点总表

实际使用下面的表格时,请用有意义的英文大写的功能名称和测试点名称替换下面表格中的名称

序号 功能名称 测试点名称 描述
1 CACHE_BACKUP BACKUP 对Cache的存取应该同对内存的存取一致
2.1.1 CACHE_MMIO_RW FORWARD Cache接收到MMIO空间的请求时,不应发生读写,而是直接转发给MMIO端口
2.1.2 CACHE_MMIO_RW NO_BURST
2.2 CACHE_MMIO BLOCK MMIO请求发生时,应当阻塞流水线
3.1 CACHE_HIT WRITE Cache写命中时,应设置脏位
3.2 CACHE_HIT SHORTER Cache写命中时,回复的周期应该更少
4.1.1 CACHE_MISS_COMMON BLOCK 发生缺失时,也会阻塞流水线
4.1.2 CACHE_MISS_COMMON CRITICAL_WORD Cache缺失时,Cache发出请求的首个地址应当是向Cache请求的地址
4.1.3 CACHE_MISS_COMMON LONGER Cache缺失时,回复的时间会更长
4.2 CACHE_MISS DIRTY Cache缺失时,Cache发出请求的首个地址应当是向Cache请求的地址
4.3 CACHE_MISS CLEAN Cache缺失时,回复的时间会更长

12.2 - Frontend

前端模块验证文档

12.2.1 - FTQ概述

下文(包括所有的FTQ文档)中会提到一些关于BPU和IFU的相关知识,详情需要去查看对应的文档:

hint:建议先从BPU基础设计中着重理解以下概念:

  1. 什么是分支预测?
  2. 什么是分支预测块?一个有帮助的链接:预测块
  3. (可选)什么是重定向,什么是预测结果重定向?
  4. (可选)分支预测的流水级

简介

FTQ 是分支预测和取指单元之间的缓冲队列,它的主要职能是暂存 BPU 预测的取指目标,并根据这些取指目标给 IFU 发送取指请求。它的另一重要职能是暂存 BPU 各个预测器的预测信息,在指令提交后把这些信息送回 BPU 用作预测器的训练,因此它需要维护指令从预测到提交的完整的生命周期。另外,后端将存储来自FTQ的取指目标PC,便于自身读取。

![[Pasted image 20250222103931.png]]

模块之间的中转站

从上图,FTQ很大程度上相当于一个中转站,中间人的角色,一方面,它承担着BPU和IFU之间的交互,这通常是因为BPU预测的速度快于IFU取值执行,所以使用FTQ作为缓冲。另一方面,它承担着后端与前端的交互,比如把前端将要执行的pc交给后端去执行。

显然,FTQ的 中转远不止这么多,下面更具体地讨论一下FTQ怎么中转各个前端或后端模块的信息的。

BPU和FTQ

BPUtoFTQ:BPU会将分支预测结果和meta数据发给FTQ。

  • 从分支预测结果中,我们可以提取出分支预测块对应的取值目标,比如,一个不跨缓存行且所有指令均为RVC指令的分支预测块对应的取值目标,是从分支预测块起始地址开始的以2B为间隔的连续16条指令。
  • meta信息则存储了各个预测器相关的预测信息,由于BPU预测有三个流水级,每个流水级都有相应的预测器,所以只有到s3阶段才有可能收集到所有预测器的预测信息,直到此时FTQ才接受到完整的meta,这些信息会在该分支预测块的全部指令被后端提交时交给BPU进行训练
  • FTBEntry:严格来说,它其实也是meta的一部分,但是因为更新的时候ftb_entry需要在原来的基础上继续修改,为了不重新读一遍ftb,另外给它存储一个副本。

FTQtoBPU:FTQ会将带元数据的训练信息和重定向信息发回给BPU

FTQ和IFU

FTQtoIFU:FTQ会将存储的取值目标发往IFU进行取值译码和把后端的重定向信息也移交给IFU

  • 取值目标同时也发给:
    • toICache:同样的取值目标会被发给指令缓存单元,看对应的指令是否在缓存单元内存在,如果有会被直接发送给IFU加速取值效率
    • toPrefetch: prefetch是ICache的一个组件,负责预取功能
  • 转发后端重定向:
    • 后端重定向不仅需要转发给BPU帮助其回到正确状态,也同时需要转发给IFU帮助其回到正确状态

IFUtoFTQ:IFU将预译码信息和重定向信息写回FTQ

  • 预译码信息:包含分支预测块对应的预测宽度内所有指令的预译码信息 预测宽度:一个指令块预测块覆盖的指令范围,香山中是16条rvc指令
  • 重定向信息其实也是根据预译码信息得到的:当预译码信息中指出预测块内某一条指令预测出错时,写回IFU重定向信息

后端和FTQ

FTQ到后端:FTQ会将存储的取值目标发往后端,后端存储 PC后,在本地即可进行读取取指目标。

  • 除了IFU,预测块的取值目标也会发给后端,但这里有一点区别:IFU空闲时才能从FTQ中获取取值目标,但是后端会一直取得最新的预测块的取指目标

后端到FTQ:后端重定向和指令commit

  • 后端重定向与更新:后端是实际执行指令的单元,通过后端的执行结果,才能确认一条指令是否执行错误,产生重定向,同时,在发生重定向时,根据后端实际执行结果生成更新信息。
  • 指令commit:当一个分支预测块内的所有指令都被执行,在后端提交,这标志着FTQ队列中这个分支预测块对应的FTQ项已经结束了它的生命周期,可以从队列中移除了,这时候,我们就可以把它的更新信息发给FTQ了。

FTQ指针

FTQ的全名叫取值目标队列,队列中的一个项叫做FTQ项,BPU写入预测结果时是写入队列中哪个位置,IFU又是从哪个队列取FTQ项?这时候,我们需要一个FTQ指针去索引FTQ项,而由于和不同模块的交互需要索引不同的FTQ项,因此,有以下类型的FTQ指针,下面,由指令生命周期为例,大致介绍这些指针:

指令在 FTQ 中的生存周期

指令以预测块为单位,从 BPU 预测后便送进 FTQ,直到指令所在的预测块中的所有指令全部在后端提交完成,FTQ 才会在存储结构中完全释放该预测块所对应的项。这个过程中发生的事如下:

  1. 预测块从 BPU 发出,进入 FTQ,bpuPtr 指针加一,初始化对应 FTQ 项的各种状态,把各种预测信息写入存储结构;如果预测块来自 BPU 覆盖预测逻辑,则恢复 bpuPtr 和 ifuPtr
  2. FTQ 向 IFU 发出取指请求,ifuPtr 指针加一,等待预译码信息写回
  3. IFU 写回预译码信息,ifuWbPtr 指针加一,如果预译码检测出了预测错误,则给 BPU 发送相应的重定向请求,恢复 bpuPtr 和 ifuPtr
  4. 指令进入后端执行,如果后端检测出了误预测,则通知 FTQ,给 IFU 和 BPU 发送重定向请求,恢复 bpuPtrifuPtr 和 ifuWbPtr
  5. 指令在后端提交,通知 FTQ,等 FTQ 项中所有的有效指令都已提交,commPtr 指针加一,从存储结构中读出相应的信息,送给 BPU 进行训练

预测块 n 内指令的生存周期会涉及到 FTQ 中的 bpuPtrifuPtrifuWbPtr 和 commPtr 四个指针,当 bpuPtr 开始指向 n+1 时,预测块内的指令进入生存周期,当 commPtr 指向 n+1 后,预测块内的指令完成生存周期。

循环队列

FTQ队列实际上是一个循环队列,所有类型的FTQ指针都是同一类型,ftqPtr的value字段用来表示索引,flag字段则用来表示循环轮数,flag只有一位,进入新的循环时flag位翻转。 这样,我们就可以在一个有限的队列空间内不断更新新的项,以及正确进行比较,判断哪个项在队列中更靠前或者更靠后。

12.2.1.1 - FTQ顶层

简述

在FTQ概述中,我们已经知道了,FTQ的作用就是多个模块交互的中转站,大致了解了它接受其他模块的哪些信息,它如何接受并存储这些信息在FTQ中,并如何把这些存储信息传递给需要的模块。 下面我们来具体了解一下FTQ与其他模块的交互接口,我们会对这种交互有一个更具体的认识。

IO一览

模块间IO

  • fromBpu:接受BPU预测结果的接口(BpuToFtqIO)
  • fromIfu:接受IFU预译码写回的接口(IfuToFtqIO)
  • fromBackend:接受后端执行结果和commit信号的接口(CtrlToFtqIO)
  • toBpu:向BPU发送训练信息和重定向信息的接口(FtqToBpuIO)
  • toIfu:向IFU发送取值目标和重定向信息的接口(FtqToIfuIO)
  • toICache:向ICache发送取值目标的接口(FtqToICacheIO)
  • toBackend:向后端发送取值目标的接口(FtqToCtrlIO)
  • toPrefetch:向Prefetch发送取值目标的接口(FtqToPrefetchIO)
  • mmio

其他

上述是主要的IO接口,此外,还有一些用于性能统计的IO接口,比如对BPU预测正确和错误结果次数进行统计,并进行转发的IO, 还有转发BPU各预测器预测信息的IO。

BpuToFtqIO

IfuToFtqIO

我们知道从IFU,我们会得到预译码信息和重定向信息,而后者其实也是从预译码信息中生成。所以从IFU到FTQ的接口主要就是用来传递预译码信息的

  • pdWb:IFU向FTQ写回某个FTQ项的预译码信息
    • 接口类型:PredecodeWritebackBundle
    • 信号列表:
      • pc:一个分支预测块覆盖的预测范围内的所有pc
        • 接口类型:Vec(PredictWidth, UInt(VAddrBits.W))
      • pd:预测范围内所有指令的预译码信息
        • 接口类型:Vec(PredictWidth, new PreDecodeInfo)
        • PreDecodeInfo:每条指令的预译码信息
          • 接口类型:PreDecodeInfo
          • 信号列表:
            • valid:预译码有效信号
              • 接口类型:Bool
            • isRVC:是RVC指令
              • 接口类型:Bool
            • brType:跳转指令类型
              • 接口类型:UInt(2.W)
              • 说明:根据brType的值判断跳转指令类型
                • b01:对应分支指令
                • b10:对应jal
                • b11:对应jalr
                • b00:对应非控制流指令
            • isCall:是Call指令
              • 接口类型:Bool
            • isRet:是Ret指令
              • 接口类型:Bool
      • ftqIdx:FTQ项的索引,标记写回到哪个FTQ项
        • 接口类型:FtqPtr
      • ftqOffset:由BPU预测结果得到的,在该指令块中指令控制流指令的位置(指令控制流指令就是实际发生跳转的指令)
        • 接口类型:UInt(log2Ceil(PredictWidth).W)
      • misOffset:预译码发现发生预测错误的指令在指令块中的位置
        • 接口类型:ValidUndirectioned(UInt(log2Ceil(PredictWidth).W))
        • 说明:它的valid信号拉高表示该信号有效,也就说明存在预测错误,会引发重定向
      • cfiOffset:由预译码结果得到的,在该指令块中指令控制流指令的位置(指令控制流指令就是实际发生跳转的指令)
        • 接口类型:ValidUndirectioned(UInt(log2Ceil(PredictWidth).W))
      • target:该指令块的目标地址
        • 接口类型:UInt(VAddrBits.W)
        • 说明:所谓目标地址,即在指令块中有控制流指令时,控制流指令的地址,在没有控制流指令时,指令块顺序执行,该指令块最后一条指令的下一条指令
      • jalTarget:jal指令的跳转地址
        • 接口类型:UInt(VAddrBits.W)
      • instrRange:有效指令范围
        • 接口类型:Vec(PredictWidth, Bool())
        • 说明:表示该条指令是不是在这个预测块的有效指令范围内(第一条有效跳转指令之前的指令)

CtrlToFtqIO

后端控制块向FTQ发送指令提交信息,后端执行结果的接口。

  • rob_commits:一个提交宽度内的RobCommitInfo信息。
    • 接口类型:Vec(CommitWidth, Valid(new RobCommitInfo))
    • 详情链接:RobCommitInfo
  • redirect:后端提供重定向信息的接口。
    • 接口类型:Valid(new Redirect)
    • 详情链接:Redirect
  • ftqIdxAhead:提前重定向的FTQ指针,将要重定向的FTQ项的指针提前发送
    • 接口类型: Vec(BackendRedirectNum, Valid(new FtqPtr))
    • 说明:虽然有三个接口,但实际上只用到了第一个接口,后面两个弃用了
  • ftqIdxSelOH:独热码,本来是依靠该信号从提前重定向ftqIdxAhead中选择一个,但现在只有一个接口了,独热码也只有一位了。
    • 接口类型:Valid(UInt((BackendRedirectNum).W))
    • 说明:为了实现提前一拍读出在ftq中存储的重定向数据,减少redirect损失,后端会向ftq提前一拍(相对正式的后端redirect信号)传送ftqIdxAhead信号和ftqIdxSelOH信号。

FtqToBpuIO

FtqToICacheIO

FTQ向IFU发送取值目标,ICache是指令缓存,如果取值目标在ICache中命中,由ICache将指令发给IFU

  • req:FTQ向ICache发送取值目标的请求
    • 接口类型:Decoupled(new FtqToICacheRequestBundle)
    • 信号列表:
      • pcMemRead:FTQ针对ICache发送的取值目标,ICache通过5个端口同时读取取指目标
        • 接口类型:Vec(5, new FtqICacheInfo)
        • FtqICacheInfo: FTQ针对ICache发送的取值目标
          • 信号列表:
            • ftqIdx:指令块在FTQ中的位置索引
              • 接口类型:FtqPtr
            • startAddr:预测块起始地址
              • 接口类型:UInt(VAddrBits.W)
            • nextlineStart:起始地址所在cacheline的下一个cacheline的开始地址
              • 接口类型:UInt(VAddrBits.W)
            • 说明:通过startAddr(blockOffBits - 1)这一位(也就是块内偏移地址的最高位)可以判断该预读取pc地址是位于cacheline的前半块还是后半块,若是前半块,由于取值块大小为cacheline大小的一半,不会发生跨cacheline行
      • readValid: 对应5个pcMemRead是否有效
      • backendException:是否有后端异常

FtqToCtrlIO

FTQ向后端控制模块转发PC,后端将这些pc存储在本地,之后直接在本地读取这些pc 写入后端pc mem

  • pc_mem_wen:FTQ向后端pc存储单元pc_mem写使能信号
    • 接口类型:Output(Bool())
  • pc_mem_waddr:写入地址
    • 接口类型:Output(UInt(log2Ceil(FtqSize).W))
  • pc_mem_wdata:写入数据,是一个指令块的取值目标
    • 接口类型:Output(new Ftq_RF_Components),详见FTQ子队列相关介绍 写入最新目标
  • newest_entry_en:是否启用
    • 接口类型:Output(Bool())
  • newest_entry_target:最新指令块的跳转目标
    • 接口类型:Output(UInt(VAddrBits.W))
  • newest_entry_ptr:最新指令块的索引值
    • 接口类型: Output(new FtqPtr)

FtqToPrefetchIO

  • req:FTQ向Prefetch发送取值目标的请求
    • 接口类型:FtqICacheInfo
  • flushFromBPU: 来自BPU的冲刷信息
    • 接口类型:BpuFlushInfo
    • 信号列表:
      • s2 :BPU预测结果重定向(注意这种重定向是BPU自己产生的,与其他类型要做区分)发生在s2阶段时,此阶段的分支预测块的索引
        • 接口类型:Valid(new FtqPtr)
        • 说明:valid信号有效时,说明此时s2流水级分支预测结果与其s1阶段预测结果不一致,产生s2阶段重定向
      • s3:BPU预测结果重定向(注意这种重定向是BPU自己产生的,与其他类型要做区分)发生在s3阶段时,此阶段的分支预测块的索引
        • 接口类型:Valid(new FtqPtr)
        • 说明:与s2类似
      • 说明:发生预测结果重定向的时候,预取单元和IFU都可能会被冲刷,比如,如果发生s2阶段重定向,FTQ会比较发给IFU req接口中的ftqIdx和s2阶段预测结果的ftqIdx,如果s2阶段的ftqIdx不在req的ftqIdx之后,这意味着,s2阶段产生的预测结果重定向之前的错误预测结果s1阶段预测结果被发给IFU进行取指了,为了消除这种错误,需要向IFU发送s2阶段flush信号。
  • backendException:后端执行发生的异常
    • 接口类型:UInt(ExceptionType.width.W)
    • 说明:表示后端执行时发生异常的类型,有这样几种类型的异常:
def none:  UInt = "b00".U(width.W)
def pf:    UInt = "b01".U(width.W) // instruction page fault
def gpf:   UInt = "b10".U(width.W) // instruction guest page fault
def af:    UInt = "b11".U(width.W) // instruction access fault

12.2.1.2 - FTQ子队列

文档概述

请注意:从本篇开始,就涉及待验证的功能点和测试点了

在之前的介绍中,我们采用FTQ项这个术语描述描述FTQ队列中的每一个元素,实际上,这只是一种便于抽象的说法。

实际上的FTQ队列,是由好多个子队列共同构成的,一些子队列维护一类信息,另一些子队列维护另一类信息,相同ftqIdx索引的子队列信息共同构成一个完整的FTQ项。

为什么要把它们分开成多个子队列呢?因为某些模块只需要FTQ项中的某一些信息,比如IFU想要取值目标,它只需要专门存储取值目标的子队列提供的信息就行了。另外,在我们更改FTQ项的内容时,也只需要写入需要更新的子队列,比如IFU预译码写回时,只需要写回专门存储预译码信息的队列了。

下面来介绍一些FTQ的主要子队列,以及它们内部存储的数据结构。此外,FTQ还有一些存储中间状态的更小的队列

术语说明

名称 定义
FTB项 分支预测结果的基本组成项,包含对预测块中分支指令和跳转指令的预测
取指目标 一个预测块内包含的所有指令PC,当然,它不是直接发送所有PC,而是发送部分信号,接收方可由该信号推出所有PC

子模块列表

子模块 描述
ftq_redirect_mem
重定向存储子队列,存储来自分支预测结果的重定向信息
ftq_pd_mem 预译码存储子队列,存储来自IFU的对指令块的预译码信息
ftb_entry_mem FTB项存储子队列,存储自分支预测结果中的ftb项
ftq_pc_mem 取指目标子队列,存储来自分支预测结果的取指目标

模块功能说明

1. ftq_redirect_mem存储重定向信息

ftq_redirect_mem是香山ftq的一个子队列。它记录了重定向需要的一些信息,帮助重定向回正确状态,这些信息来自于BPU分支预测中的RAS预测器,以及顶层的分支历史指针,如果想要了解,可以参考BPU的RAS子文档了解如何通过这些信息回溯到之前的状态。

它是一个寄存器堆,由64(FtqSize)个表项(Ftq_Redirect_SRAMEntry)构成。支持同步读写操作。有3个读端口和1个写端口,每个读端口负责与不同的模块交互。

1.1 ftq_redirect_mem读操作

  • 读操作:
    • 输入:
      • 需要使能ren,这是一个向量,可指定任意读端口可读
        • 对应接口:ren
      • 从任意读端口中输入要读取的元素在ftq_redirect_mem中的地址,这是一个从0到ftqsize-1的索引
        • 对应接口:raddr
    • 输出:
      • 从发起输入的读端口对应的读出端口中读出Ftq_Redirect_SRAMEntry。
        • 对应接口:rdata

1.2 ftq_redirect_mem写操作

  • 写操作
    • 输入:
      • 需要使能wen,可指定写端口可写
        • 对应接口:wen
      • 向写端口中输入要写入的元素在ftq_redirect_mem中的地址,这是一个从0到ftqsize-1的索引
        • 对应接口:waddr
      • 向wdata中写入Ftq_Redirect_SRAMEntry
        • 对应接口:wdata
  • 多端口读:可以从多个读端口读取结果

每个子队列的读写基本都是类似的,后面不再赘述

Ftq_Redirect_SRAMEntry

ftq_redirect_mem存储的表项。继承自SpeculativeInfo,存储RAS预测器相关重定向信息,根据这些信息回溯到之前的状态

  • sc_disagree:统计分支指令在sc预测器中预测是否发生错误
    • 接口类型:Some(Vec(numBr, Bool()))
    • 说明:Option 类型,表明这个值可能不存在,在非FPGA平台才有,否则为none
    • 信号列表:
      • SpeculativeInfo:推测信息,帮助BPU在发生重定向的时候回归正常的状态
        • 接口列表:
          • histPtr:重定向请求需要恢复的全局历史指针,可参见BPU顶层文档了解详情
            • 接口类型:CGHPtr
        • 说明:以下都属于RAS重定向信息,可参见BPU文档了解如何利用这些信息进行重定向
          • ssp:重定向请求指令对应的 RAS 推测栈栈顶在提交栈位置的指针
            • 接口类型:UInt(log2Up(RasSize).W)
          • sctr:重定向请求指令对应的 RAS 推测栈栈顶递归计数 Counter
            • 接口类型:RasCtrSize.W
          • TOSW:重定向请求指令对应的 RAS 推测栈(队列)写指针
            • 接口类型:RASPtr
          • TOSR:重定向请求指令对应的 RAS 推测栈(队列)读指针
            • 接口类型:RASPtr
          • NOS:重定向请求指令对应的 RAS 推测栈(队列)读指针
            • 接口类型:RASPtr
          • topAddr:
            • 接口类型:UInt(VAddrBits.W)
序号 功能名称 测试点名称 描述
1.1 FTQ_REDIRECT_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
1.2 FTQ_REDIRECT_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取

2. ftq_pd_mem存储预译码信息

由64(FtqSize)个表项(Ftq_pd_Entry)构成。支持同步读写操作。有2个读端口和1个写端口。具有读写使能信号。

存储来自IFU预译码的写回信息,它是一个寄存器堆,由64(FtqSize)个表项(Ftq_pd_Entry)构成。有2个读端口和1个写端口。

ftq_pd_mem直接接收来自IfuToFtqIO的信号,从中获取Ftq_pd_Entry,表示一个指令块对应的预译码信息表项。读取时获取预测块内某条指令的预测信息

Ftq_pd_Entry

  • brMask:一个指令预测宽度内(16条rvc指令)的指令块中,哪些指令是分支指令
    • 接口类型:Vec(PredictWidth, Bool())
  • jmpInfo:jump信息,其值对应不同的jmp指令类型,表示指令块内jmp指令类型
    • 接口类型:ValidUndirectioned(Vec(3, Bool()))
    • 说明:  jumpinfo有效的时候,第0位是0,表示jal指令,第0位是1,表示jalr指令,第1位是1,表示call指令,第二位是1,表示ret指令。
  • jmpOffset:jmp指令在指令预测块中的偏移地址
    • 接口类型: UInt(log2Ceil(PredictWidth).W)
  • rvcMask:一个预测块内的指令(16条rvc指令)哪些是rvc指令
    • 接口类型:Vec(PredictWidth, Bool())

2.1 ftq_pd_mem写操作

PredecodeWritebackBundle(IfuToFtqIO)如何写入ftq_pd_mem的一条Ftq_pd_Entry

Ftq_pd_Entry项的写入是通过PredecodeWritebackBundle这个接口进行写入的(其实也就是IfuToFtqIO) 从fromPdWb接口中接收信号生成表项

  • brmask:PredecodeWritebackBundle有一个预测块内的所有指令的预译码信息,当一条指令的预译码信息有效(valid)且是分支指令(is_br)时, bool序列对应位置的指令被判定为分支指令
  • jumpInfo:
    • valid:预测块内存在一条指令,其预译码信息有效(valid),且是jmp指令(isJal或者isJalr)时,jumpInfo有效
    • bits:预测块内的第一条有效跳转指令的info,它是一个三位序列,从低到高(拉高)对应该指令被预译码为是isJalr,isCall,isRet
  • jmpOffset:预测块内第一条有效jmp跳转指令的偏移
  • rvcMask:原封不动接受同名信号
  • jalTarget:原封不动接收同名信号

2.2 ftq_pd_mem写操作

ftq_pd_mem的一条Ftq_pd_Entry如何以PreDecodeInfo(to pd)的形式输出

PreDecodeInfo是一个Ftq_pd_Entry中的一条指令的预译码,需要输入offset,指定该预译码指令在预测块内的偏移

  • valid:直接set为1

  • isRVC:设置为rvcMask bool序列中对应偏移的值

  • isBr:设置为brMask bool序列中对应偏移的值

  • isJalr:输入的偏移量等于jumpOffset,且jumpInfo有效并指明该指令type是isJalr(jmpInfo.valid && jmpInfo.bits(0))

序号 功能名称 测试点名称 描述
2.1 FTQ_PD_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
2.2 FTQ_PD_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取

3. ftb_entry_mem存储FTB项

有两个读端口,一个写端口,FtqSize个表项,存储的数据项为FTBEntry_FtqMem,FTBEntry_FtqMem与FTBEntry基本上是一致的。

FTBEntry_FtqMem

  • brSlots:分支指令槽
    • 接口类型:Vec(numBrSlot, new FtbSlot_FtqMem)
    • FtbSlot_FtqMem:
      • 信号列表:
        • offset:给分支指令在相对于指令块起始地址的偏移
          • 接口类型:UInt(log2Ceil(PredictWidth).W)
        • sharing:对于tailSlot来说,启用sharing表示把这个slot让给分支指令来被预测
          • 接口类型:Bool
        • valid:预测槽有效
          • 接口类型:Bool
          • 说明:当slot有效时,我们才能说这条指令是br指令还是jmp指令
  • tailSlot:跳转指令槽
    • 接口类型:FtbSlot_FtqMem
  • FTBEntry_part:FTBEntry_FtqMem的父类,存储部分FTB信息,记录跳转指令的类型
    • 信号列表:
      • isCall:接口类型:Bool
      • isRet:接口类型:Bool
      • isJalr:接口类型:Bool

3.1 ftb_entry_mem读操作

除了读出FTB项之外,顶层还可以从FTBEntry_FtqMem获取以下有效信息,在这里我们不需要验证以下内容,但是在验证顶层的时候我们会用到以下内容,在此处提一下,此外,以下内容并不会生成具体的信号接口,而是产生相应的判断逻辑:

  • jmpValid:预测块中jmp指令有效
    • 说明:当tailslot有效且不分享给分支指令时,jmp有效
  • getBrRecordedVec:三维向量,对于三个slot
    • 说明:接收一个offset偏移,如果命中有效分支slot(或者sharing拉高的tailslot),对应slot的向量元素拉高。
  • brIsSaved:给定offset的指令是否是分支指令
    • 说明:采用slot预测结果来说明是不是分支指令,前提需要信号有效
  • getBrMaskByOffset:
    • 说明:在给定offset范围内的三个slot中的指令是否是有效分支指令,用一个三位maks表示
  • newBrCanNotInsert:能否插入新的brSlot
    • 说明:给定offset超过有效tailSlot对应的offset时,不能插入新的brSlot
序号 功能名称 测试点名称 描述
3.1 FTQ_ENTRY_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
3.2 FTQ_ENTRY_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取

4. ftq_pc_mem存储取指目标

pc存储子队列。存储项为Ftq_RF_Components,用来读取取指信息,取值信息交给IFU进行取指。

Ftq_RF_Components

信号含义

  • startAddr: 预测块的起始地址
  • nexLineAddr: 预测块下一个缓存行的起始地址
    • startAddr加上64个字节,一个缓存行的大小是64字节
  • isNextMask: 一个预测宽度内的16条指令各自是否属于下一个预测块(在最新版本rtl中已被编译优化掉)
    • 通过计算某条指令相对于预测块起始地址的偏移量(每条指令两个字节)得到偏移地址,该偏移地址的第4位(从0开始)为1,表示该指令属于下一个预测块。
    • 进一步说,其实也就可以根据它判断该指令是否在预测块跨缓存行的时候判断该指令是否属于下一个cacheline了
  • fallThruError :预测出的下一个顺序取指地址是否存在错误
4.1 ftq_pc_mem写操作

信息获取:上述信息都可以从一个单流水级分支预测结果 (BranchPredictionBundle)中获取。 获取方式:startAddr直接获取BranchPredictonBundle中的pc,fallThruError直接获取BranchPredictionBundle中的fallThruError。

4.2 ftq_pc_mem读操作

多端口读:ftq_pc_mem的每个读端口的读地址被直接连到各个FTQ指针的写入信号,这样做的目的,是可以及时的读取,从pc存储子队列读出的项一定是此时FTQ指针指向的项

读写时机

写入时机:BPU流水级的S1阶段,创建新的预测entry时写入 读出时机: 读数据每个时钟周期都会存进Reg。如果IFU不需要从bypass中读取数据,Reg数据直连给Icache和IFU,如果IFU不需要从bypass中读取数据,Reg数据直连给Icache和IFU

序号 功能名称 测试点名称 描述
4.1 FTQ_PC_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
4.2 FTQ_PC_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取

5. ftq_meta_1r_sram存储meta信息

存储的数据为Ftq_1R_SRAMEntry,同样有FtqSize项 Ftq_1R_SRAMEntry接口列表

  • meta:分支预测的meta数据
  • ftb_entry:分支预测的FTB项 写入时机:在 BPU的s3阶段接收信息,因为对于一个指令预测块,只有在其s3阶段才能获取完整的mata信息,同样被接收的还有最后阶段ftqentry信息
序号 功能名称 测试点名称 描述
5.1 FTQ_META_1R_SRAM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
5.2 FTQ_META_1R_SRAM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取

接口说明

Ftq_Redirect_SRAMEntry

ftq_redirect_mem存储的表项。继承自SpeculativeInfo,存储RAS预测器相关重定向信息,根据这些信息回溯到之前的状态

  • sc_disagree:统计分支指令在sc预测器中预测是否发生错误
    • 接口类型:Some(Vec(numBr, Bool()))
    • 说明:Option 类型,表明这个值可能不存在,在非FPGA平台才有,否则为none
    • 信号列表:
      • SpeculativeInfo:推测信息,帮助BPU在发生重定向的时候回归正常的状态
        • 接口列表:
          • histPtr:重定向请求需要恢复的全局历史指针,可参见BPU顶层文档了解详情
            • 接口类型:CGHPtr
        • 说明:以下都属于RAS重定向信息,可参见BPU文档了解如何利用这些信息进行重定向
          • ssp:重定向请求指令对应的 RAS 推测栈栈顶在提交栈位置的指针
            • 接口类型:UInt(log2Up(RasSize).W)
          • sctr:重定向请求指令对应的 RAS 推测栈栈顶递归计数 Counter
            • 接口类型:RasCtrSize.W
          • TOSW:重定向请求指令对应的 RAS 推测栈(队列)写指针
            • 接口类型:RASPtr
          • TOSR:重定向请求指令对应的 RAS 推测栈(队列)读指针
            • 接口类型:RASPtr
          • NOS:重定向请求指令对应的 RAS 推测栈(队列)读指针
            • 接口类型:RASPtr
          • topAddr:
            • 接口类型:UInt(VAddrBits.W)

Ftq_pd_Entry

  • brMask:一个指令预测宽度内(16条rvc指令)的指令块中,哪些指令是分支指令
    • 接口类型:Vec(PredictWidth, Bool())
  • jmpInfo:jump信息,其值对应不同的jmp指令类型,表示指令块内jmp指令类型
    • 接口类型:ValidUndirectioned(Vec(3, Bool()))
    • 说明:  jumpinfo有效的时候,第0位是0,表示jal指令,第0位是1,表示jalr指令,第1位是1,表示call指令,第二位是1,表示ret指令。
  • jmpOffset:jmp指令在指令预测块中的偏移地址
    • 接口类型: UInt(log2Ceil(PredictWidth).W)
  • rvcMask:一个预测块内的指令(16条rvc指令)哪些是rvc指令
    • 接口类型:Vec(PredictWidth, Bool())

测试点总表

序号 功能名称 测试点名称 描述
1.1 FTQ_REDIRECT_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
1.2 FTQ_REDIRECT_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取
2.1 FTQ_PD_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
2.2 FTQ_PD_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取
3.1 FTQ_ENTRY_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
3.2 FTQ_ENTRY_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取
4.1 FTQ_PC_MEM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
4.2 FTQ_PC_MEM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取
5.1 FTQ_META_1R_SRAM WRITE 向单端口输入wen,waddr决定是否写入以及写入地址,写入wdata
5.2 FTQ_META_1R_SRAM READ 向多端口中输入ren,raddr决定是否读以及读取地址,从rdata读取

附录

虽然列在附录,但实际上这段内容依然十分重要,当你需要的时候请一定要查看。

其余状态子队列

上述存储结构是FTQ中比较核心的存储结构,实际上,还有一些子队列用来存储一些状态信息,也同样都是存储ftqsize个(64)元素。主要有以下:

update_target:记录每个FTQ项的跳转目标,跳转目标有两种,一种是当该FTQ项对应的分支预测结果中指明的该分支预测块中执行跳转的分支指令将要跳转到的地址,另一种则是分支预测块中不发生跳转,跳转目标为分支预测块中指令顺序执行的下一条指令地址。

  • 此外,与之配套的还有newest_entry_target,newest_entry_ptr用来指示新写入的跳转目标地址,和它对应的指令预测块或者说FTQ项的在FTQ中的位置,同时,有辅助信号newest_entry_target_modified和newest_entry_ptr_modified用来标识该地址的FTQ项跳转地址是否被修改。

写入时机:上一个周期的bpu_in_fire有效的时候,或者说相对于bpu_in_fire有效时延迟一个周期写入。

newest_entry_ptr,newest_entry_target:这几个内部信号,表明我们当前最新的有效FTQ项。BPU新的写入,重定向等等都会对最新FTQ项进行新的安排,在相应的文档中,对其生成方式做具体的描述。

cfiIndex_vec:记录每个FTQ项的发生跳转的指令cfi(control flow instruction)指令在其分支预测块中的位置 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。

mispredict_vec:记录每个FTQ项的分支预测结果是否有误,初始化为false

pred_stage:记录每个FTQ项的分支预测结果是来自于哪个阶段 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。

pred_s1_cycle:记录每个FTQ项的分支预测结果对应的s1阶段的分支预测结果生成的时间(cycle数) 写入时机:相对于bpu_in_fire有效时延迟两个周期写入。

commitStateQueueReg:记录每个FTQ项中对应的分支预测块中每条指令(一般是16条rvc指令,对应一个预测宽度)的提交状态,提交状态有c_empty ,c_toCommit ,c_committed ,c_flushed,依次用从0开始的从小到大的枚举量表示,初始化为c_empty状态 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。

entry_fetch_status:记录每个FTQ项的分支预测结果是否被送到ifu中,该状态由两个枚举量f_to_send , f_sent来表示, 初始化为f_sent状态。 写入时机:上一个周期的bpu_in_fire有效的时候,相对于bpu_in_fire有效时延迟一个周期写入。 写入数据:写入f_to_send

entry_hit_status:记录每个FTQ项拿到的分支预测结果是否是ftb entry hit的,即生成该分支预测结果的时候是否是从FTB ( 预测结果生成:hit)(非必须了解)中,读取到了对应的记录表项。初始化为not_hit状态。 写入时机:当来自BPU的全局分支预测信息中s2阶段的分支预测结果有效时,写入s2阶段分支预测结果中指名的hit状态,因为FTB预测器是分支预测s2阶段开始生效的,在此时判断预测项是否在FTB缓存中命中

newest_entry_ptr,newest_entry_target这几个内部信号,它们不是队列,但是它们很重要,表明我们当前应该关注的最新的FTQ项及对应的跳转目标。BPU新的写入,重定向等等都会对最新FTQ项进行新的安排,在涉及到修改该信号的相应的文档中,对其生成方式做具体的描述。

12.2.1.3 - FTQ接收BPU分支预测结果

文档概述

BPU会将分支预测结果和meta数据发给FTQ。

  • 从分支预测结果中,我们可以提取出分支预测块对应的取值目标,比如,一个不跨缓存行且所有指令均为RVC指令的分支预测块对应的取值目标,是从分支预测块起始地址开始的以2B为间隔的连续16条指令。
  • meta信息则存储了各个预测器相关的预测信息,由于BPU预测有三个流水级,每个流水级都有相应的预测器,所以只有到s3阶段才有可能收集到所有预测器的预测信息,直到此时FTQ才接受到完整的meta,这些信息会在该分支预测块的全部指令被后端提交时交给BPU进行训练
  • FTBEntry:严格来说,它其实也是meta的一部分,但是因为更新的时候ftb_entry需要在原来的基础上继续修改,为了不重新读一遍ftb,另外给它存储一个副本。

术语说明

名称 定义
BPU (Branch Prediction Unit) 分支预测单元
FTQ (Fetch Target Queue) 采集目标队列
IFU (Instruction Fetch Unit) 指令采集单元
RAS (Return Address Stack) 返回地址堆
FTQ Entry FTQ队列中的单个表项

模块功能说明

1. 新的预测块进队条件

1.1 成功接收数据

1.1.1 FTQ准备好接收信号
  • FTQ准备好接收信号:      当FTQ队列中元素小于FtqSize或者可以提交指令块(canCommit拉高,说明可以提交指令块,在后面的文档: FTQ向BPU发送更新信息中介绍怎么判断是否可以提交指令块)的时候,来自BPU的新的指令预测块可以进入FTQ队列,队列准备好接收新的预测块,fromBpu的resp接口ready信号拉高。
1.1.2 BPU准备好要发送的信号
  • BPU准备好要发送的信号:      当BPU发往FTQ的接口vaid信号拉高,表示发送信号准备好

满足以上两个条件时,fromBpu的resp接口fire,表示接口数据被成功发送到FTQ中。

1.2 允许BPU入队allowBpuIn

  • 重定向发生时,会回滚到之前的状态,新发送的BPU预测信息自然就不需要了。允许BPU入队时不能发生重定向
1.2.1 后端重定向发生
  1. 后端重定向发生:
    • 标志:接收后端写回信息的接口fromBackend的重定向接口redirect有效,则该周期不允许入队,如果没有发生真实提前重定向realAhdValid(参见FTQ接收后端重定向一文),则下一个周期也不允许入队。
1.2.2 IFU重定向发生
  1. IFU重定向发生:
    • 标志:IFU重定向信息生成的两个周期,均不许入队(参见FTQ接收IFU重定向一文了解IFU重定向信息的生成)

只要避免上述两种重定向出现的情况,就可以允许BPU入队,即可以把发送到FTQ的数据,写入FTQ项

1.3 以BPU预测结果重定向的方式入队

上述的BPU入队方式是一个全新的预测块进队,即BPU分支预测的s1阶段结果入队,此时未发生预测结果重定向。

当BPU发生预测结果重定向时,只要允许BPU入队allowBpuIn,也可以看作预测结果入队,不过这种入队是覆写队列中已有的FTQ项,没有写入新的指令块。

  • BPU预测结果发生重定向的具体标志:fromBpu的resp接口的s2(s2阶段的预测信息)有效,且s2的hasRedirect拉高,表示在s2阶段发生了重定向,s3阶段重定向是一样的。

综合两种形式的BPU入队,这里称之为广义BPU入队方便区分,记为bpu_in_fire,该信号拉高,表明发生广义BPU入队。

2. 写入FTQ项

之前已经说明过了,FTQ项只是一个抽象的概念,FTQ有很多个子队列组成,它们的项共同构成一个FTQ项,所以,向FTQ中写入FTQ项,实际上就是就是把BPU的预测信息写到对应的FTQ子队列中。

FTQ主要获取以下信息作为bpu_in_resp

  • bpu_in_resp:BPU交给FTQ的resp详见BPU文档,resp中含有s1,s2,s3三个阶段的指令预测信息,bpu_in_resp将获取其中某一阶段预测信息selectedResp作为其值。未发生重定向时,使用s1作为预测结果,s2或者s3发生重定向信息时,优先s3的预测信息作为selectedResp。某阶段发生重定向的标志与上文讲述的一样一样。 从selectedResp(bpu_in_resp)中,我们还可以获取以下目标信息帮助我们写入子队列:ftq_idx,帮助我们索引写入子队列的地址

2.1 写入FTQ子队列:

2.1.1 写入ftq_pc_mem
  • ftq_pc_mem: 来自BPU的selectedResp预测信息被写入ftq_pc_mem, 该存储结构有ftqsize个表项,对应队列中的所有ftq表项,每个存储元素可以推出对应的ftq表项中每条指令的pc地址 接收信号列表:
    • wen:接收bpu_in_fire作为写使能信号
    • waddr:接收selectedResp的ftq_idx
    • wdata:selectedResp的相应信号
2.1.2 写入ftq_redirect_mem
  • ftq_redirect_mem: 在BPU的s3(也就是最终阶段)接收信息,因为重定向信息只有在s3阶段才能得到。里面存储了RAS重定向相关的信息帮助BPU进行重定向。 接收信号列表:
    • wen:从BPU(fromBpu)回应(resp)的lastStage有效信号
    • waddr:从BPU回应的lastStage的ftq_idx.value
    • wdata:从BPU回应的last_stage_spec_info
2.1.3 写入ftq_meta_1r_sram
  • ftq_meta_1r_sram:在 BPU的s3阶段接收信息,同样是因为对于一个指令预测块,只有在其s3阶段才能获取完整的mata信息,同样被接收的还有最后阶段ftqentry信息 接收信号列表:
    • wen:从BPU(fromBpu)回应(resp)的lastStage有效信号
    • waddr:从BPU回应的lastStage的ftq_idx的value
    • wdata:
      • meta:从BPU回应的last_stage_meta
      • ftb_entry:从BPU回应的last_stage_ftb_entry
2.1.4 写入ftb_entry_mem
  • ftb_entry_mem:虽然ftq_meta_1r_sram中存储有最后阶段ftbentry,但此处出于更高效率读取专门把它存在ftb_entry_mem中。 接收信号列表:
    • wen:从BPU(fromBpu)回应(resp)的lastStage有效信号
    • waddr:从BPU回应的lastStage的ftq_idx的value字段
    • wdata:从BPU回应的last_stage_ftb_entry 从中可以看到,FTQ虽然名字上听起来是一个队列,实际上内部却是由数个队列组成,他们共同构成了FTQ这个大队列

2.2 写入状态队列

上述存储结构是FTQ中比较核心的存储结构,实际上,还有一些子队列用来存储一些状态信息,也同样都是存储ftqsize个(64)元素,需要被写入,写入时机是在发生bpu_in_fire的下一个周期,或者再下一个周期 。主要有以下:

2.2.1 写入update_target

update_target:记录每个FTQ项的跳转目标,跳转目标有两种,一种是当该FTQ项对应的分支预测结果中指明的该分支预测块中执行跳转的分支指令将要跳转到的地址,另一种则是分支预测块中不发生跳转,跳转目标为分支预测块中指令顺序执行的下一条指令地址。

  • 此外,与之配套的还有newest_entry_target,newest_entry_ptr用来指示bpu_in_resp推出的跳转目标地址,表示下一次预测时开始的目标地址,和它对应的bpu_in_resp指令预测块在FTQ中的位置。
    • 同时,有辅助信号newest_entry_target_modified和newest_entry_ptr_modified用来标识该这两个字段是否被修改。
  • 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
  • 写入数据:bpu_in_resp.getTarget
2.2.2 写入cfiIndex_vec

cfiIndex_vec:记录每个FTQ项的发生跳转的指令cfi(control flow instruction)指令在其分支预测块中的位置

  • 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
  • 写入数据:bpu_in_resp推断出的跳转目标
2.2.3 写入mispredict_vec

mispredict_vec:记录每个FTQ项的所有指令的预测结果是否有误,初始化为false

  • 写入时机:相对于bpu_in_fire有效时延迟两个周期写入。
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
  • 写入数据:将该指令块的所有预测结果对应的值设置为false
2.2.4 写入pred_stage

pred_stage:记录每个FTQ项的分支预测结果是来自于哪个阶段

  • 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
写入pred_s1_cycle(不需要测试)

pred_s1_cycle:记录每个FTQ项的分支预测结果对应的s1阶段的分支预测结果生成的时间(cycle数)

  • 写入时机:相对于bpu_in_fire有效时延迟两个周期写入。
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
2.2.5 写入commitStateQueueReg

commitStateQueueReg:记录每个FTQ项中对应的分支预测块中每条指令(一般是16条rvc指令,对应一个预测宽度)的提交状态,提交状态有c_empty ,c_toCommit ,c_committed ,c_flushed,依次用从小到大的枚举量表示,初始化为c_empty状态

  • 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。
  • 写入数据:写入c_empty
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
2.2.6 写入entry_fetch_status

entry_fetch_status:记录每个FTQ项的分支预测结果是否被送到ifu中,该状态由两个枚举量f_to_send , f_sent来表示, 初始化为f_sent状态。

  • 写入时机:相对于bpu_in_fire有效时延迟一个周期写入。
  • 写入数据:写入f_to_send
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
2.2.7 写入entry_hit_status

entry_hit_status:记录每个FTQ项拿到的分支预测结果是否是ftb entry hit的,即生成该分支预测结果的时候是否是从ftb中,读取到了对应的记录表项。初始化为not_hit状态。

  • 写入时机:当来自BPU的全局分支预测信息中s2阶段的分支预测结果有效时,写入s2阶段分支预测结果中指名的hit状态
  • 写入地址:bpu_in_resp记录的要写入FTQ的地址
  • 写入数据:f_to_send

注:之所以延迟时钟周期写入,是为了缩短关键路径,以及帮助减少扇出

3 转发分支预测重定向

3.1 转发给IFU

  • s2以及s3阶段的预测重定向信息通过FTQ与Ifu的接口toIfu的flushFromBpu发送给IFU,当完整分支预测结果中的s2阶段分支预测结果发生预测结果重定向时,flushFromBpu.s2.valid拉高,flushFromBpu.s2.bits接收s2阶段分支预测结果中指明的该分支预测结果在FTQ中的位置ftq_idx。

3.2 转发给预取

  • 该重定向信号同样会通过toPrefetch.flushFromBpu接口以相同的方式传递给Prefetch s3阶段向IFU以及Prefetch的重定向传递与s2阶段的重定向信号传递一样。该阶段的重定向信号传递会覆盖可能的s2阶段重定向信号传递结果

4 修正FTQ指针

此外,分支预测结果重定向也会影响ifuPtr与pfPtr两个指针信号的写入信号。

4.1 正常修改

  • 正常情况下,allowToIfu(条件和allowToBpu一样),同时BPU向Ifu发送FTQ项的io接口toIfu.req发生fire的时候,ifuPtr寄存器中写入ifuPtr+1。同样发生修改的还有pfPtr,当allowToIfu,同时BPU向Prefetch发送FTQ项的io接口totoPrefetch.req发生fire的时候。

4.2 发生重定向时修改

  • 而如果是发生重定向的时候,比如s2阶段预测结果发生重定向,此时,若ifuPtr不在s2阶段预测结果中指明的ftq_idx之前,ifuPtr写入该ftq_idx,pfPtr_write同样如此

bpuptr: 由FTQ交给BPU用于指示新的指令预测块应该放到FTQ队列中的位置,上述存储结构,ftq_pc_mem,ftq_redirect_mem,ftq_meta_1r_sram,ftb_entry_mem基本上也是通过与该指针相关的信号得知信息应该存储的addr(bpuptr交给BPU,BPU基于此获知每个阶段预测结果的ftq_idx)。

bpuptr寄存器的输出值直接连到FTQ发往BPU的接口toBpu的enq_ptr字段中,当然,再次之前,bpuptr的值会根据实际情况修改。

在enq from bpu的过程中,正常情况下,发生enq的时候,也就是新的预测块进队时,bpuptr+1,BPU将要向FTQ中写入的位置前进一位

但是,如果发生重定向的时候,比如,如果s2阶段预测结果发生重定向,bpuptr被更新为s2阶段分支预测结果的ftq_idx+1,表示BPU将要向FTQ中写入的位置为s2阶段预测结果在FTQ中位置的后一位,因为此时新的全局预测结果会基于s2的预测结果展开下一轮预测(即以s2分支预测块的下一块展开预测,自然会被写入),该结果会覆盖enq_fire发生时的结果,此外s3阶段的分支预测重定向时,会覆盖可能的s2阶段重定向修改的bpuptr

其他的ftq指针也是类似的,用于指示写入FTQ的地址

接口说明

FTQ接收BPU分支预测结果工程中涉及到的IO接口如下,在FTQ顶层IO一文中有详细说明

接口 作用
fromBackend 根据是否有重定向确认是否允许BPU预测结果入队
fromBPU 接收BPU预测结果
toIfu 发送更新的IFU指针,转发BPU预测结果重定向
toPrefetch 发送更新的Prefetch指针,转发BPU预测结果重定向
toBpu 发送更新的BPU指针

测试点总表

序号 功能名称 测试点名称 描述
1.1.1 BPU_IN_RECEIVE FTQ_READY 当FTQ队列中元素小于FtqSize或者可以提交指令块的时候,队列准备好接收新的预测块
1.1.2 BPU_IN_RECEIVE BPU_VALID BPU准备好要发送的信号
1.2.1 BPU_IN_ALLOW BACKEND 接收后端写回信息的接口fromBackend的重定向接口redirect有效,则该周期不允许入队,如果没有发生真实提前重定向,则下一个周期也不允许入队
1.2.2 BPU_IN_ALLOW IFU IFU重定向信息生成的两个周期,均不许入队
1.3.1 BPU_IN_BY_REDIRECT REDIRECT 当BPU发生预测结果重定向时,只要允许BPU入队allowBpuIn,也可以看作预测结果入队
2.1.1 WRITE_FTQ_SUBQUEUE FTQ_PC 根据BPU预测结果写入ftq_pc_mem
2.1.2 WRITE_FTQ_SUBQUEUE FTQ_REDIRECT 根据BPU预测结果写入ftq_redirect_mem
2.1.3 WRITE_FTQ_SUBQUEUE FTQ_MATA 根据BPU预测结果写入ftq_meta_1r_sram
2.1.4 WRITE_FTQ_SUBQUEUE FTQ_ENTRY 根据BPU预测结果写入ftb_entry_mem
2.2.1 WRITE_FTQ_STATEQUEUE UPDATED_TARGET 根据BPU预测结果写入update_target
2.2.2 WRITE_FTQ_STATEQUEUE CFIINDEX 根据BPU预测结果写入cfiIndex_vec
2.2.3 WRITE_FTQ_STATEQUEUE MISPREDICT 根据BPU预测结果写入mispredict_vec
2.2.4 WRITE_FTQ_STATEQUEUE PRED_STAGE 根据BPU预测结果写入pred_stage
2.2.5 WRITE_FTQ_STATEQUEUE COMMITSTATE 根据BPU预测结果写入commitStateQueueReg
2.2.6 WRITE_FTQ_STATEQUEUE ENTRY_FETCH_STATU 根据BPU预测结果写入entry_fetch_status
2.2.7 WRITE_FTQ_STATEQUEUE ENTRY_HIT_STATU 根据BPU预测结果写入entry_hit_status
3.1 TRANSFER_BPU_REDIRECT IFU 转发分支预测重定向给IFU
3.2 TRANSFER_BPU_REDIRECT PREFETCH 转发分支预测重定向给PREFETCH
4.1 UPDATE_FTQ_PTR NORMAL 正常情况下修改FTQ指针
4.2 UPDATE_FTQ_PTR REDIRECT 发生重定向时修改FTQ指针

12.2.1.4 - FTQ向IFU发送取指目标

文档概述

IFU需要取FTQ中的项进行取指令操作,同时也会简单地对指令进行解析,并写回错误的指令 FTQ发送给IFU的信号同时也需发送给ICache一份,ICache是指令缓存,帮助快速读取指令。

术语说明

  • ifuPtr:该寄存器信号指示了当前FTQ中需要读取的项的指针。直接发送给io.toIfu.req接口的ftqIdx。
  • entry_is_to_send:entry_fetch_status存储每个FTQ项的发送状态,初始化并默认为当前ifuptr指向的项对应的发送状态,后续可能因为旁路逻辑等改变
  • entry_ftq_offset: 从cfiIndex_vec中初始化并默认为当前ifuptr指向项的跳转指令在预测块中的偏移,后续可能因为旁路逻辑等改变
  • entry_next:本次取指结束后下一次取值的开始地址
  • pc_mem_ifu_ptr_rdata:获取ifuptr指向FTQ项的取指信息(从ftq_pc_mem的读取接口ifuPtr_rdata中获取)
  • pc_mem_ifu_plus1_rdata:获取ifuptr+1指向FTQ项的pc相关信息(从ftq_pc_mem的读取接口ifuPtrPlus1_rdata中)
  • copied_ifu_plus1_to_send:多个相同的复制信号,entry_fetch_status中指向ifuPtrPlus1的项是f_to_send状态或者上一周期bpu_in_fire,同时旁路bpu指针bpu_in_bypass_ptr等于ifuptr+1时,信号copied_ifu_plus1_to_send在一周期后拉高
  • copied_ifu_ptr_to_send:同理,只是把ifuptr+1改成了ifuptr

模块功能说明

1. 获取取指目标信息

获取取指目标有两个来源,一个是BPU写入信息时,直接将取指目标旁路出来,一种则是从存储取指目标的队列ftq_pc_mem中读取。使用前一种方式的前提,是刚好ifuPtr指向的读取项刚好就是旁路指针信号bpu_in_resp_ptr(BPU入队时写入项的ftqIdx)

  • 旁路逻辑:pc信号在被写入存储子队列时就被旁路一份,写入信号ftq_pc_mem.io.wdata在bpu_in_fire信号拉高时被旁路到旁路信号寄存器bpu_in_bypass_buf中。同时被旁路的还有指针信号bpu_in_resp_ptr,在同样的条件下被旁路到寄存器bpu_in_bypass_ptr中
  • 读取ftq_pc_mem: 存储pc相关的取指目标,该存储队列有多个读接口,对所有ftqptr的写入信号(比如ifuPtr_write, ifuPtrPlus1_write等)被直接连接到存储队列的读取接口,这样,在ftqPtr寄存器正式被更新时,就可以同时直接从对应的读取接口中返回对应指针的读取结果,比如ftq_pc_mem.io.ifuPtr_rdata

1.1 准备发往ICache的取指目标

有以下三种情况,分别对应测试点1.1.1,1.1.2,1.1.3

  1. 旁路生效,即旁路bpu指针等于ifuptr,且上一周期bpu输入有效结果(last_cycle_bpu_in表示上一周期bpu_in_fire)有效(也就相当于该旁路指针是有效的),此时,直接向toICache接口输入旁路pc信息bpu_in_bypass_buf
  2. 不满足情况1,但是上一周期发生ifu_fire(即FTQ发往IFU的接口发生fire),成功传输信号,此toICache中被写入pc存储子队列ftq_pc_mem中ifuptr+1对应项的结果,这是因为此时发生了ifu_fire,新的ifuptr还未来得及更新(即加1),所以直接从后一项中获取新的发送数据
  3. 前两种情况都不满足,此时toICache接口中被写入pc存储队列中ifuptr对应项的结果

1.2 提前一周期准备发往Prefetch的取指目标

有以下三种情况,分别对应测试点1.2.1,1.2.2,1.2.3 同样有三种情况:

  1. bpu有信号写入(bpu_in_fire),同时bpu_in_resp_ptr等于pfptr的写入信号pfptr_write, (此时pfptr_write还没有正式被写入pfptr中),读取bpu向pc存储队列的写入信号wdata,下一周期写入ToPrefetch      xxxptr_write:是相应FTQptr寄存器的write信号,连接到寄存器的写端口,寄存器在时钟上升沿成功写入write信号
  2. 不满足情况1,且由bpu到prefetch的接口发生fire,即bpu向预取单元成功发送信号,pc存储单元的pfPtrPlus1_rdata下一周期写入ToPrefetch接口,选择指针加1对应项的原因与toICache类似。
  3. 不满足以上两种情况:pc存储单元的pfPtr_rdata在下一周期被写入ToPrefetch接口

1.3 设置下一个发送的指令块的起始地址

有以下三种情况,分别对应测试点1.3.1,1.3.2,1.3.3

target(entry_next_addr)旁路逻辑: 有三种情况:

  1. 上一周期bpu写入信号,且旁路指针等于ifuptr:
    • toIfu:写入旁路pc信息bpu_in_bypass_buf
    • entry_is_to_send :拉高
    • entry_next_addr :bpu预测结果中跳转地址last_cycle_bpu_target
    • entry_ftq_offset :bpu预测结果中跳转指令在预测块中的偏移last_cycle_cfiIndex
  2. 不满足情况1,bpu到ifu的接口发生fire,信号成功写入
    • toIfu:写入pc存储队列的读出信号ifuPtrPlus1_rdata,这同样是因为ifuptr还没来得及更改,所以直接使用ifuptr+1对应项的rdata
    • entry_is_to_send :发送状态队列中ifuPtrPlus1对应项为f_to_send或者在上一周期bpu有写入时旁路bpu指针等于ifuptr加1,entry_is_to_send拉高。
    • entry_next_addr :
      • 如果上一周期bpu有写入且bpu旁路指针等于ifuptr+1,写入bpu旁路pc信号的startAddr字段,而这个项的pc信息还没有写入,正在pc旁路信号中,这是因为ifuptr+1对应下一个指令预测块,它的起始地址实际上就是ifuptr对应指令的预测块的跳转目标。
      • 如果不满足该条件,
        1. ifuptr等于newest_entry_ptr: 使用newest_entry_target作为entry_next_addr,newest_entry_ptr,newest_entry_target这几个内部信号,表明我们当前队列中最新的有效的FTQ项。如之前所说,BPU新的写入,重定向等等都会对最新FTQ项进行新的安排,在相应的文档中,对其生成方式做具体的描述。
        2. 不满足条件1:使用pc存储队列的ifuPtrPlus2_rdata.startAddr
  3. 不满足情况1,2:
  • toIfu:写入pc存储队列的读出信号ifuPtr_rdata
  • entry_is_to_send :发送状态队列中ifuPtr对应项为f_to_send或者在上一周期bpu有写入时旁路bpu指针等于ifuptr
  • entry_next_addr :
  • 如果上一周期bpu有写入且bpu旁路指针等于ifuptr+1,写入bpu旁路pc信号的startAddr字段。
  • 如果不满足该条件,          1. ifuptr等于newest_entry_ptr: 使用newest_entry_target作为entry_next_addr。          2. 不满足上面的条件1:使用pc存储队列的ifuPtrPlus1_rdata.startAddr,为什么条件2和条件3,一个使用ifuPtrPlus2_rdata.startAddr作为entry_next_addr ,一个使用ifuPtrPlus1_rdata.startAddr作为,这也是出于时序的考虑: 因为要获得实际上的ifuptr+1对应项的start值作为结果,而因为第一处那里因为ifuptr还没来得及更新(加1)同步到当前实际的ifuptr,所以要加2来达到实际上的ifuptr+1对应的值,而第二处的ifuptr已经更新了,所以只用加1就行了。

2. 发送取指信息

2.1 发送取指目标

2.1.1 发送给IFU

toIfu接口的req接口: FTQ通过该接口向IFU发送取指信号:

  • valid:要发送的FTQ项处于将发送状态entry_is_to_send且ifuptr不等于bpuptr
  • nextStartAddr:递交最终的entry_next_addr
  • ftqOffset:递交最终的entry_ftq_offset
  • toIfu:递交pc信息
2.1.2 发送给ICache

toICache的req接口: FTQ通过该接口向ICache发送取指信号:

  • valid:FTQ项处于将发送状态entry_is_to_send且ifuptr不等于bpuptr
  • readValid:ICache的有多个read接口,readVlid是一个向量,表示这几个read接口是否有效,readVlid中的每个元素的写入值与valid一样
  • pcMemRead:同样是一个向量,对应readVlid向量的ICache的多个pc信号read接口,从toIfu接口中将pc信息结果写入向量中各接口,接口的ftqIdx字段被写入ifuPtr
  • backendException:后端出现异常,同时后端pc错误指针等于ifuPtr

2.1.3 发送给Prefetch

toPrefetch的req接口:

  • valid:传给预取模块的项的状体toPrefetchEntryToSend为1,(toPrefetchEntryToSend会玩一个周期存储nextCycleToPrefetchEntryToSend的值),且pfptr不等于bpuptr,
  • toPrefetch:递交pc
  • ftqIdx字段被设置为pfptr寄存器的值
  • backendException:在后端pc错误指针等于pfptr的时候,传入后端异常信号,否则传入无异常信号

2.2 错误命中

错误命中falsehit: 当发往Ifu的pc接口toIfu中发生fallThruError(预测块的fall through地址小于预测的起始地址时),且hit状态队列entry_hit_status中ifuPtr对应项显示命中的话,进行如下判断:

当发往ifu的接口toIfu的req接口发生fire,且bpu的预测结果不发生满足以下条件的重定向: s2或者s3的重定向的预测块对应的FTQ项索引号ftq_idx等于ifuptr, 此时,hit状态队列中ifuptr对应项被设置为false_hit。

2.3 BPU冲刷

bpu向ifu的req请求的flush: 发往ifu的flushfrombpu(来自bpu的冲刷)接口中,记录有s2,s3阶段的指针,如果其中一条指针不大于发往ifu的req接口的ftqIdx的时候,表示应该被冲刷掉req信号,即冲刷掉新的发送给FTQ的预测信息。

2.4 更新发送状态

成功发送: 发往ifu的req接口发生fire,且req不被来自bpu的flush给冲刷掉时: entry_fetch_status状态队列中ifuptr对应项的发送状态置为f_sent。表示该ftq项被成功发送 了

接口说明

顶层IO 子接口 作用
toIFU req 发送取指目标
toIFU flushfrombpu 冲刷掉发送给IFU的取指目标
toICache req 发送取指目标
toPrefetch req 发送取指目标

测试点总表

序号 功能名称 测试点名称 描述
1.1.1 GET_PC_FOR_ICACHE COND1 旁路生效,即旁路bpu指针等于ifuptr,且上一周期bpu输入有效结果有效,直接向toICache接口输入旁路pc信息bpu_in_bypass_buf
1.1.2 GET_PC_FOR_ICACHE COND2 不满足情况1,但是上一周期发生ifu_fire,成功传输信号,此时toICache中被写入pc存储子队列ftq_pc_mem中ifuptr+1对应项的结果
1.1.3 GET_PC_FOR_ICACHE COND3 前两种情况都不满足,此时toICache中被写入pc存储队列中ifuptr对应项的结果
1.2.1 GET_PC_FOR_PREFETCH COND1 bpu有信号写入,同时bpu_in_resp_ptr等于pfptr的写入信号pfptr_write, 读取bpu向pc存储队列的写入信号wdata,下一周期写入ToPrefetch
1.2.2 GET_PC_FOR_PREFETCH COND2 不满足情况1,且由bpu到prefetch的接口发生fire,即bpu向预取单元成功发送信号,pc存储单元的pfPtrPlus1_rdata下一周期写入ToPrefetch接口
1.2.3 GET_PC_FOR_PREFETCH COND3 不满足以上两种情况:pc存储单元的pfPtr_rdata在下一周期被写入ToPrefetch接口
1.3.1 SET_NEXT_ADDR COND1 上一周期bpu写入信号,且旁路指针等于ifuptr时设置下一个发送的指令块的起始地址
1.3.2 SET_NEXT_ADDR COND2 不满足情况1,bpu到ifu的接口发生fire时设置下一个发送的指令块的起始地址
1.3.3 SET_NEXT_ADDR COND3 不满足情况1,2时设置下一个发送的指令块的起始地址
2.1.1 SEND_PC IFU 向IFU发送取指目标
2.1.2 SEND_PC ICACHE 向ICache发送取指目标
2.1.3 SEND_PC PREFETCH 向Prefetch发送取指目标
2.2 FALSE_HIT FALSE_HIT 当发往Ifu的pc接口toIfu中发生fallThruError,且FTB项命中时判断是否是错误命中
2.3 FLUSH_FROM_BPU FLUSH_FROM_BPU 发往ifu的flushfrombpu(来自bpu的冲刷)接口中的s2,s3阶段的指针其中一条指针不大于发往ifu的req接口的ftqIdx的时候,应该冲刷掉新的发送给FTQ的预测信息
2.4 UPDATE_SEND_STATU UPDATE_SEND_STATU 发往ifu的req接口发生fire,且req不被来自bpu的flush给冲刷掉时:
entry_fetch_status状态队列中ifuptr对应项的发送状态置为f_sent

12.2.1.5 - IFU向FTQ写回预译码信息

文档概述

IFU获取来自BPU的预测信息之后,会执行预译码,并将FTQ项写回FTQ中去。我们会比对FTQ中原BPU预测项和预译码的结果,判断是否有预测错误

基本流程

预译码写回ftq_pd_mem:

  • FTQ从pdWb接口中获取IFU的写回信息,FTQ首先将预译码写回信息写回到ftq_pd_mem,

更新提交状态队列commitStateQueue:

  • 然后根据写回信息中指令的有效情况更新提交状态队列commitStateQueue。

比对错误:

  • 同时,从ftb_entry_mem读出ifu_Wb_idx所指的FTB项,将该FTB项的预测结果与预译码写回结果进行对比,看两者对分支的预测结果是否有所不同。

综合错误:

  • 之后就综合根据预译码信息可能得到的错误:有前面说的比对BPU的预测结果和预译码结果得到的错误,也有直接根据预译码得到的错误预测信息。根据错误预测结果更新命中状态队列。

更新写回指针

  • 最后,如果IFU成功写回,ifu_Wb_idx更新加1。

术语说明

名称 定义
预译码 IFU会对取指目标进预译码,之后写回FTQ
ifuWbPtr IFU写回指针,知识IFU预译码要写入FTQ的位置

模块功能说明

1. 预译码写回ftq_pd_mem

写回有效:预译码信息pdWb有效时,写有效 写回地址:pdWb的ftqIdx的value 写回值:解析整个pdWb的结果

2. 更新提交状态队列

当预译码信息pdWb有效时,相当于写回有效,此时,根据预译码信息中每条指令的有效情况和该指令是否在有效范围内,判断指令的提交状态是否可以修改,若可以修改,则将提交状态队列,写回项中的指令状态修改

详细信号表示

pdWb有效时,ifu_wb_valid拉高。 此时,对于预译码信息中每一条指令的预译码结果pd做判断: 如果预译码结果valid,且指令在有效范围内(根据insrtRange的bool数组指示),则提交状态队列commitStateQueue中,写回项中的指令状态修改为c_toCommit,表示可以提交,这是因为只有在FTQ项被预译码写回后,才能根据后端提交信息提交该FTQ项,之后会把预译码信息一并发往更新通道。

3. 比对预测结果与预译码结果

从ftb存储队列ftb_entry_mem中的读取ifu写回指针ifuwbptr的对应项:

  • pdWb有效的时候,读有效,读取地址为预译码信息中指示的ftqIdx。 当命中状态队列指示待比对项ftb命中,且回写有效时,读取出FTB存储队列中对应的项,与预译码信息进行比对,当BPU预测的FTB项指示指令是有效分支指令,而预译码信息中则指示不是有效分支指令时,发生分支预测错误,当BPU预测的FTB项指示指令是有效jmp指令,而预译码信息中则指示不是有效jmp指令时,发生跳转预测错误

详细信号表示:

ifu_wb_valid回写有效时,ftb_entry_mem回写指针对应读使能端口ren有效,读取地址为ifu_wb_idx预测译码信息中指示的ftqIdx的value值。 回写项命中且回写有效,hit_pd_valid信号有效,此时,读取ftb存储队列中的FTB项,读出brSlots与tailSlot,并进行比对:

3.1 判断是否有分支预测错误br_false_hit

测试点3.1.1和3.1.2对应以下两种条件导致的br_false_hit
  • 判断是否有分支预测错误br_false_hit:
    1. brSlots的任意一项有效,同时在预译码信息中不满足这一项对应的pd有效且isBr字段拉高表明是分支指令,
    2. taiSlot有效且sharing字段拉高表明该slot为分支slot,同时在预译码信息中不满足这一项对应的pd有效且isBr字段拉高表明是分支指令 满足任意条件可判断发生分支预测错误br_false_hit,该信号拉高

3.2 判断是否发生jmp预测错误jal_false_hit

  • 判断是否发生jmp预测错误jal_false_hit:
    • 预测结果中必须指明指令预测有效,且其中isJal拉高表面是jal指令或者指明是isjalr指令

4. 预译码错误

直接从预测结果中获取错误预测相关信息,如果回写项ftb命中且missoffset字段有效表明有错误预测的指令,hit_pd_mispred信号拉高,表示预译码结果中直接指明有预测错误的指令。

5. 综合错误

综合比对预测结果与预译码结果得到的错误信息,与预译码错误直接获得的预测错误,任意一种发生时has_false_hit拉高表示有预测错误,此时,命中状态队列entry_hit_status中写回项的状态置为h_false_hit

6. 更新写回指针

ifu_wb_valid拉高,表示写回有效,将ifuWbPtr更新为原值加1。

接口说明

顶层IO 子接口
fromIfu pdWb

测试点总表

序号 功能名称 测试点名称 描述
1 WB_PD WB_PD 向ftq_pd_mem中写回预译码信息
2 UPDATE_COMMITSTATE UPDATE_COMMITSTATE 当预译码信息pdWb有效时,根据预译码信息中每条指令的有效情况和该指令是否在有效范围内,判断指令的提交状态是否可以修改,若可以修改,则将提交状态队列,写回项中的指令状态修改
3.1.1 BR_FALSE_HIT COND1 brSlots的任意一项有效,同时在预译码信息中不满足这一项对应的pd有效且isBr字段拉高
3.1.2 BR_FALSE_HIT COND2 taiSlot有效且sharing字段拉高表明该slot为分支slot,同时在预译码信息中不满足这一项对应的pd有效且isBr字段拉高
3.2 JAL_FALSE_HIT JAL_FALSE_HIT 指令预测有效,且其中isJal拉高或者指明是isjalr指令
4 PD_MISS PD_MISS 如果回写项ftb命中且missoffset字段有效表明有错误预测的指令,hit_pd_mispred信号拉高
5 FALSE_HIT FALSE_HIT 综合比对预测结果与预译码结果得到的错误信息,与预译码错误直接获得的预测错误,任意一种发生时has_false_hit拉高表示有预测错误,此时,命中状态队列entry_hit_status中写回项的状态置为h_false_hit
6 UPDATE_IFU_WB_PTR UPDATE_IFU_WB_PTR ifu_wb_valid拉高,将ifuWbPtr更新为原值加1

12.2.1.6 - FTQ接收后端重定向

文档概述

FTQ重定向信息有两个来源,分别是IFU 和 后端。两者的 重定向接口大致相似,但重定向的过程有一定区别。

对于重定向,后端有提前重定向机制,为了实现提前一拍读出在ftq中存储的重定向数据,减少redirect损失,后端会向ftq提前一拍(相对正式的后端redirect信号)传送ftqIdxAhead信号和ftqIdxSelOH信号。ftqIdxSelOH信号出现的原因,是早期版本要读多个ftqIdxAhead信号,以独热码的形式选其中一路作为最终确认的提前索引值,但现在只需要从一个端口获取ftqIdx信号了,ftqIdxAhead只能确认这一个端口了。

术语说明

名称 定义
sc_disagree 统计SC预测错误用的性能计数器中需要用到的值,SC预测器是BPU子预测器TAGE-SC预测器的一个部分

模块功能说明

1. 接收后端重定向信号

时序

1.1 提前重定向

第一个周期:

  • 后端重定向写回时,首先会从后端到FTQ的IO接口(CtrltoFtqIO)中,看ftqIdx是不是有效信号,且此时后端正式重定向信号redirect无效(因为提前重定向会比正式重定向提前一拍,所以此时正式重定向无效),这时,提前重定向信号aheadValid有效, 将使用提前获取的重定向ftqIdx,

1.2 真实提前重定向

第二个周期:

  • 如果此时后端正式重定向信号有效了,且ftqIdxSelOH拉高,说明在正式重定向阶段成功对ftqIdxAhead信号进行选中,同时上一周期重定向信号aheadValid是有效的,则真实提前重定向信号realAhdValid拉高,在此时读取

1.3 存储后端重定向信号

第三个周期:

  • 该周期会把来自后端的重定向信息的存储一份在寄存器backendRedirectReg中,具体的来说,当上一个周期后端重定向有效时,将后端重定向bits字段(存储实际内容)被写入寄存器的bits字段。
  • 而实际决定信号是否有效的valid字段(决定该信号是否有效)则在上一周期真实提前重定向信号有效(表示确实使用了提前重定向的ftqIdx进行重定向)的情况下,被写入false,因为提前重定向发生时,我们直接使用当前的后端重定向信号交给FTQ就可以了。而不需要多保存一个周期。
  • 真实提前重定向信号无效时,则由上一周期后端正式重定向的有效值决定,只有信号有效时,我们才需要把它存下来,之后交给FTQ。

2. 选择重定向信号

信号抉择: 是提前获取后端重定向信息还是延迟一个周期从寄存器内读取? 真实重定向有效时,直接将后端重定向信息传递给FTQ,否则,取重定向寄存器内的信号作为重定向信息传递给FTQ,相当于晚一个周期发送重定向信息。最后被选择的重定向信息作为后端重定向结果fromBackendRedirect发送给FTQ

接下来讲讲后端重定向在这三个周期到底通过ftqIdx到底读了哪些FTQ子队列中的信息,以及怎么使用它们。

3. 整合子队列信号

3.1 读取子队列

接下来讲讲后端重定向在这三个周期到底通过ftqIdx到底读了哪些FTQ子队列中的信息,以及怎么使用它们。

后端重定向读取的子队列:

  • ftq_redirect_mem:FTQ会根据后端重定向提供的ftqIdx读出ftq_Redirect_SRAMEntry,借助它提供的信息重定向到之前的状态。
  • ftq_entry_mem:读出重定向指令块对应的FTB项
  • ftq_pd_mem:读出重定向指令块的预译码信息

3.1.1 发生提前重定向时,读取子队列需要两个周期

3.1.2 未发生提前重定向时,读取子队列需要三个周期

读子队列时序: 第一个周期:

  • 提前重定向信号有效时,将子队列的读端口,读有效信号拉高,输入ftqIdxAhead的value字段作为读地址,发起读取请求。

第二个周期:

  • case1. 如果第一周期的提前重定向无效,而现在正式重定向有效,则在此时才拉高读有效信号,使用正式重定向接口的ftqIdx作为读取地址,发起读取请求。
  • case2. 真实提前重定向有效了,此时因为前一个周期已经发起读取请求,此时可以直接从子队列的读端口读出了

第三个周期

  • 真实提前重定向无效,但至少前一个周期正式重定向发起的读取请求能保证在当前周期从子队列中读出。

3.2 将子队列信息整合到后端重定向信号

处理读取信息 FTQ会将从子队列中读出的信息整合到fromBackendRedirect中。 具体来说:

  • 重定向redirect接口的CfiUpdateInfo接口直接接收ftq_Redirect_SRAMEntry中的同名信号。
  • 利用fromBackendRedirect中指示的ftqOffset读取指令块预译码信息中实际跳转指令的预译码信息,该ftqOffset为后端执行过后确定的控制流指令在指令块内的偏移。
    • 得到的预译码信息被直接连接到CfiUpdateInfo接口的pd接口中
  • 对于读出的指令块对应的FTB项,我们可以从中得知实际执行时得到的跳转指令,是否在FTB项被预测为跳转指令,或者是被预测为jmp指令,如果是,则cfiUpdateInfo的br_hit接口或者jr_hit接口被拉高,表示对应的分支预测结果正确了。
    • 具体来说:通过发送ftqOffset,ftb项以brIsSaved的方式判断是否br_hit,判断是否jr_hit的方式也是类似的(r_ftb_entry.isJalr && r_ftb_entry.tailSlot.offset === r_ftqOffset)。
    • 在CfiUpdateInfo接口设置为br_hit的时候,还会根据这条发生跳转的分支指令是哪个槽从ftq_Redirect_SRAMEntry重定向接口的sc_disagree统计SC预测错误用的性能计数器中,获取对应值,最后整合到后端重定向接口中(如果没有br_hit,对应计数器的两个值都为0)。

接口说明

顶层IO 功能
fromBackend 接收后端重定向信息

测试点总表

序号 功能名称 测试点名称 描述
1.1 RECERIVE_BACKEND_REDIRECT REDIRECT_AHEAD 后端重定向写回时,首先会从后端到FTQ的IO接口(CtrltoFtqIO)中,看ftqIdx是不是有效信号,且此时后端正式重定向信号redirect无效,这时,提前重定向信号aheadValid有效
1.2 RECERIVE_BACKEND_REDIRECT REAL_REDIRECT_AHEAD 如果此时后端正式重定向信号有效了,且ftqIdxSelOH拉高,同时上一周期重定向信号aheadValid是有效的,则真实提前重定向信号realAhdValid拉高
1.3 RECERIVE_BACKEND_REDIRECT STORE_REDIRECT 后端真实重定向无效时写入寄存器
2 CHOOSE_AHEAD CHOOSE_AHEAD 真实重定向有效时,直接将后端重定向信息传递给FTQ,否则,取重定向寄存器内的信号作为重定向信息传递给FTQ
3.1.1 READ_FTQ_SUBQUEUE READ_AHEAD 发生提前重定向时,读取子队列需要两个周期
3.1.2 READ_FTQ_SUBQUEUE READ_NO_AHEAD 未发生提前重定向时,读取子队列需要三个周期
3.2 ADD_SUBQUEUE_INFO ADD_SUBQUEUE_INFO 将子队列信息整合到后端重定向信号

12.2.1.7 - FTQ接收IFU重定向

文档概述

除了后端,IFU也会发送重定向相关消息,和后端不同,IFU的重定向信息来自于预译码写回信息。相同的是,它们都是通过BranchPredictionRedirect的接口传递重定向信息。

术语说明

名称 定义
RedirectLevel 重定向等级,重定向请求是否包括本位置,低表示在本位置后重定向,高表示在本位置重定向。它在之后决定了由重定向导致的冲刷信号是否会影响到发生重定向的指令

模块功能说明

1. IFU重定向信号生成

流程

IFU重定向是通过这个BranchPredictionRedirect接口传递的,下面来讲述IFU重定向怎么生成IFU的BranchPredictionRedirect内相应信号的,这个过程需要两个周期 信号列表: 第一个周期

1.1 IFU 重定向触发条件

  • valid:当预译码写回pdWb有效,且pdWb的missOffset字段有效表明存在预测错误的指令,同时后端冲刷信号backendFlush无效时,valid信号有效。

1.2 IFU生成重定向信号

  • ftqIdx:接收pdWb指定的ftqIdx
  • ftqOffset:接收pdWb的missOffset的bits字段
  • level:RedirectLevel.flushAfter,将重定向等级设置为flushAfter
  • BTBMissBubble:true
  • debugIsMemVio:false
  • debugIsCtrl:false
  • cfiUpdate: 信号列表:
    • pc:pdWb中记录的指令块中所有指令pc中,missOffset对应的pc
    • pd:pdWb中记录的指令块中所有指令的pd中,missOffset对应的pd
    • predTaken:从cfiIndex_vec子队列中读取pdWb中ftqIdx索引的项是否valid,有效说明指令块内被预测为有控制流指令。
    • target:pdWb中的target
    • taken:pdWb中cfiOffset的valid字段,有效时表明预译码认为指令块中存在指令控制流指令
    • isMisPred:pdWb中missOffset的valid字段,有效时表明预译码认为指令块中存在预测错误的指令

第二个周期: 该周期进行的信号生成是在第一周期valid字段有效的情况下才继续的

  • cifUpdate: 信号列表:
    • 重定向RAS相关信号:通过ftqIdx索引从 ftq_redirect_mem读出ftq_Redirect_SRAMEntry,把其中的所有信号直接传递给cfiUpdate的同名信号中。
    • target:已在第一周期写入cfiUpdate的pd有效,且isRet字段拉高,指明发生预测错误的指令本是一条Ret指令,此时,将target设置为cfiUpdate的topAddr,帮助回到发生错误之前的状态。

2. 重定向结果生效

两个周期生成完整的重定向信息后,IFU重定向信息才有效,有可能被FTQ采取,完整的IFU重定向结果记为ifuRedirectToBpu

3. IFU 冲刷信号 (ifuFlush)

指令流控制信号: ifuFlush:来自IFU的冲刷信号,主要是由IFU重定向造成的,生成IFU重定向信息的两个周期内,该信号都拉高

  • 标志:IFU重定向信息产生接口BranchPredictionRedirect中valid有效,表示开始生成重定向信息,该周期以及下一个周期,ifuFlush拉高

接口说明

顶层IO 作用
fromIFU 接收来自IFU的预译码信息

接口时序

测试点总表

序号 功能名称 测试点名称 描述
1.1 IFU_REDIRECT IFU_REDIRECT_GRN_VALID 当预译码写回pdWb有效,且pdWb的missOffset字段有效表明存在预测错误的指令,同时后端冲刷信号backendFlush无效时,valid信号有效
1.2 IFU_REDIRECT IFU_REDIRECT_GEN 允许生成IFU重定向时,在两周期内生成具体信号
2 IFU_REDIRECT_TO_BPU IFU_REDIRECT_TO_BPU IFU重定向生成后,IFU重定向结果生效
3 IFU_FLUSH IFU_FLUSH 生成IFU重定向信息的两个周期内,ifuFlush信号都拉高

12.2.1.8 - FTQ向后端发送取指目标

文档概述

pc取值目标会发给后端pc mem让他自己进行存储,之后从自己的pc mem取指,此外,最新的FTQ项和对应的跳转目标也会发给后端。

怎样算是一个最新的FTQ项,BPU最新发送的预测块可以是最新的FTQ项,其次,重定向发生时,需要回滚到发生错误预测之前的状态,从指定的FTQ项开始重新开始预测,预译码等等,这也可以是被更新的最新的FTQ项。

术语说明

名称 定义
暂无 暂无

模块功能说明

流程

1.发送取值目标到pc mem

  • 发送时机:bpu_in_fire,即BPU向前端发送有效预测信息,或者重定向信息的时候。以此为基础之后的第二个周期,进行发送,通过将toBackend接口的pc_mem_wen设置为true的方式指明开始发送
  • 接口信号列表:
    • pc_mem_wen:设置为true
    • pc_mem_waddr:接收bpu_in_fire那个周期BPU发送的ftqIdx
    • pc_mem_wdata:接收bpu_in_fire那个周期,FTQ读取的ftq_pc_mem中的取指目标

2.更新最新的FTQ项

  • 发送时机:
    • 最新的FTQ项可能是由BPU写入最新预测信息造成的,发送取值目标到pc mem也是因为BPU写入最新预测信息才写入的,如果是这种情况造成的,更新FTQ项和写入pc mem的时机是一致的。
    • 此外发生重定向时,也会进行状态回滚更新FTQ项,标志是后端接口fromBackend的重定向redirect信号有效,或者写入BPU的接口toBPU的redirctFromIFU拉高说明当前有来自IFU的重定向
      • (注释(可忽略)IFU重定向信号生成有两个周期,可以认为第一个周期预译码信息中missoffset有效说明IFU重定向发生,也可以认为第二个周期redirctFromIFU拉高说明重定向发生,此处取后者)。
    • 同样是向toBackend中写入
  • 接口信号列表:
    • newest_entry_en:前面说的发送时机到来时,再延迟一个周期达到真正的写入时机,这时才拉高信号
    • newest_entry_ptr:发送时机到来时的newest_entry_ptr,在真正的写入时机写入
    • newest_entry_target:发送时机到来时的newest_entry_target newest_entry_ptr,newest_entry_target这几个都是同名的内部信号,如之前所说,BPU新的写入,重定向等等都会对最新FTQ项进行新的安排,在相应的文档中,对其生成方式做具体的描述。

接口说明

顶层IO 作用
toBackend 发送取指令目标,让后端进行储存

测试点总表

序号 功能名称 测试点名称 描述
1 SEND_PC_TO_BACKEND SEND_PC 发送取值目标到pc mem
2 SEND_PC_TO_BACKEND UPDATE_NEWEST 更新最新的FTQ项

12.2.1.9 - 执行单元修改FTQ状态队列

文档概述

后端的写回信息,包括重定向信息和更新信息,实际上都是执行之后,由实际执行单元根据结果发回的

术语说明

名称 定义
cfiIndex_vec 控制流指令索引队列,记录每个指令块中控制流指令的索引
update_target 更新目标队列,记录每个指令块的跳转目标
FTQ最新项 BPU新的写入,重定向等等都会对最新FTQ项进行新的安排,表明我们当前关注的最新FTQ项。

模块功能说明

1. 由后端的写回信号修改FTQ状态

1.1 修改FTQ状态队列

从后端写回FTQ接口fromBackend中的redirect接口中,我们可以读出valid,ftqPtr,ftqOffset(后端实际执行时确认的控制流指令的偏移),taken,mispred字段,依靠它们来判断,如何修改FTQ的状态队列和相关的变量

后端执行单元写回时被修改的队列

1.1.1 修改cfiIndex_vec

  • cfiIndex_vec: 修改方式:执行写回修改队列中ftqPtr那一项
    • valid:fromBackend中的redirect接口中,valid有效,taken有效,且ftqOffset小于或者等于cfiIndex_vec中ftqPtr那一项指定的偏移:这说明重定向发生,实际执行结果判断ftqPtr索引的指令块确实会发生跳转,且实际执行跳转的指令在被预测为发生跳转的指令之前或等于它。所以这时指令块是会发生跳转的,控制流索引队列的ftqPtr项valid
    • bits:fromBackend中的redirect接口中,valid有效,taken有效,且ftqOffset小于cfiIndex_vec中ftqPtr那一项指定的偏移,偏移量被更新为更小值ftqOffset。

1.1.2 修改update_target

  • update_target:
    • ftqPtr索引项的跳转目标修改为fromBackend的redirect接口中的cifUpdate中指定的target

1.1.3 修改mispredict_vec

  • mispredict_vec:
    • 如果该重定向指令是来自后端的重定向指令, ftqPtr索引项的ftqOffset偏移指令被设置为fromBackend的redirect接口中的cifUpdate中指定的isMisPred

1.2 修改FTQ最新项

  • newest_entry_target:
    • 被修改为重定向接口中cfiUpdate指定的target
    • 辅助信号newest_entry_target_modified被指定为true
  • newest_entry_ptr:
    • 修改为重定向接口指定的ftqIdx
    • 辅助信号newest_entry_ptr_modified被指定为true

2. 由IFU的写回信号修改FTQ状态

IFU既然也能和后端一样生成重定向信息,那么他也能在产生重定向信息的时候修改这些状态队列和FTQ最新项,区别:

  • 但是,由于IFU没有真的执行,所以它的预译码结果并不能作为决定指令块是不是真的被错误预测了,所以它不能修改mispredict_vec的状态
  • 其次,后端重定向优先级永远高于IFU重定向,两者同时发生时只采用后端重定向。

所以这个部分也有以下测试点:

2.1.1 修改cfiIndex_vec

2.1.2 修改update_target

2.2 修改FTQ最新项

常量说明

常量名 常量值 解释
常量1 64 常量1解释
常量2 8 常量2解释
常量3 16 常量3解释

接口说明

顶层IO 子接口
fromBackend redirect

测试点总表

实际使用下面的表格时,请用有意义的英文大写的功能名称和测试点名称替换下面表格中的名称

序号 功能名称 测试点名称 描述
1.1.1 BACKEDN_REDIRECT_UPDATE_STATE UPDATE_CFIINDEXVEC 后端重定向修改cfiinedex状态队列
1.1.2 BACKEDN_REDIRECT_UPDATE_STATE UPDATE_UPDATE_TARGET 后端重定向修改update_target状态队列
1.1.3 BACKEDN_REDIRECT_UPDATE_STATE UPDATE_MISPREDICTVEC 后端重定向修改mispredict状态队列
1.2 BACKEDN_REDIRECT_UPDATE_NEWEST BACKEDN_REDIRECT_UPDATE_NEWEST 后端重定向修改FTQ最新项
2.1.1 IFU_REDIRECT_UPDATE_STATE UPDATE_CFIINDEXVEC IFU重定向修改cfiinedex状态队列
2.1.2 IFU_REDIRECT_UPDATE_STATE UPDATE_UPDATE_TARGET IFU重定向修改update_target状态队列
2.2 IFU_REDIRECT_UPDATE_NEWEST IFU_REDIRECT_UPDATE_NEWEST IFU重定向修改FTQ最新项

12.2.1.10 - 冲刷指针和状态队列

文档概述

之前讲了,后端和IFU重定向写回会修改一些状态队列。此外,FtqPtr也是一种比较重要的维护信息。由后端或者IFU引起的重定向,需要恢复各种类型用来索引FTQ项的FtqPtr。而当重定向是由后端发起的时候,还要修改提交状态队列,说明指令已经被执行。

术语说明

名称 定义
FTQ指针 用来索引FTQ项,有不同类型的FTQ指针,比如bpuPtr,ifuPtr
flush 冲刷,发生时需要重置FTQ指针,以及重置其他状态
融合指令 一条指令可以和其他指令融合,形成融合指令

模块功能说明

1. 冲刷FTQ指针及提交状态队列

流程

后端和IFU的重定向信号都会冲刷指针,更具体的来说:

1.1 冲刷条件

  • 后端写回接口fromBackend有效,或者IFU重定向有效:(当预译码写回pdWb有效,且pdWb的missOffset字段有效表明存在预测错误的指令,同时后端冲刷信号backendFlush无效)。(参考:从IFU重定向的第一个周期,重定向valid值有效条件)

1.2 冲刷指针

第一个周期:

  • 冲刷指针:确认后端和IFU的重定向信号可能冲刷指针时,从两个重定向来源的redirect接口读出重定向信息,包括ftqIdx,ftqOffset,重定向等级RedirectLevel。有两个来源时,优先后端的重定向信息。 冲刷指针列表:
    • bpuPtr:ftqIdx+1
    • ifuPtr:ftqIdx+1
    • ifuWbPtr:ftqIdx+1
    • pfPtr:ftqIdx+1 注:只是在当前周期向指针寄存器写入更新信息,实际生效是在下一个周期。 这样一来,所有类型指针当前指向的都是发生重定向的指令块的下一项了,我们从这一项开始重新进行分支预测,预译码,等等。

1.3 冲刷提交状态队列

第二个周期: 如果上一个周期的重定向来源是后端,FTQ会进一步更改提交状态队列

  • 提交状态队列中,对于重定向的指令块(通过ftqIdx索引),位于ftqOffset后面的指令的状态被设置为c_empty
  • 对于正好处于ftqOffset的指令,判断RedirectLevel,低表示在本位置后flush,高表示在本位置flush,所以level为高时,对于的指令提交状态被设置为flush。

2 转发到顶层IO

实际上,在发生重定向的时候,还涉及一些将重定向信息通过FTQ顶层IO接口转发给其他模块的操作,比如ICache需要flush信号取进行冲刷,IFU也需要后端的重定向信号对它进行重定向,具体来说: 在流程的第一个周期:

2.1 flush转发到icacheFlush

  • flush信号顶层IO转发(icacheFlush):
    • 确认后端和IFU的重定向信号可能冲刷指针时,拉高FTQ顶层IO接口中的icacheFlush信号,把重定向产生的flush信号转发给ICache

2.2 重定向信号转发到IFU

  • 重定向信号顶层IO转发(toIFU):
    • redirect:
      • bits:接收来自后端的重定向信号
      • valid:后端的重定向信号有效时有效,保持有效,直到下个周期依然有效

3 重排序缓冲区提交

其实,除了后端重定向会更新提交状态队列,最直接的更新提交状态队列的方式是通过FTQ顶层IO中frombackend里提供的提交信息,rob_commits告知我们哪些指令需要被提交。

rob_commits的valid字段有效,可以根据其中信息对指令进行提交,修改状态队列。对于被执行的指令,是如何提交的,如何对应地修改提交状态队列,有两种情况:

3.1 提交普通指令

  • 对于普通指令,根据rob_commits的ftqIdx和ftqOffset索引提交状态队列中的某条指令,将对应的提交状态设置为c_commited

3.2 提交融合指令

  • 对于融合指令,根据提交类型commitType对被索引的指令和另一与之融合的指令进行提交,将对应的提交状态设置为c_commited
    1. commitType = 4:同时把被索引指令的下一条指令设为c_commited
    2. commitType = 5:同时把被索引指令的之后的第二条指令设为c_commited
    3. commitType = 6:同时把被指令块的下一个指令块的第0条指令设为c_commited
    4. commitType = 7:同时把被指令块的下一个指令块的第1条指令设为c_commited

接口说明

顶层IO 作用
fromBackend 接收后端重定向和指令提交
fromIfu 接收IFU重定向
icacheFlush 将flush信号转发到icache
toIFU 将后端重定向转发到IFU

测试点总表

序号 功能名称 测试点名称 描述
1.1 FLUSH_FTQPTR_AND_COMMITSTATE FLUSH_COND 后端写回接口fromBackend有效,或者IFU重定向有效时,进行冲刷
1.2 FLUSH_FTQPTR_AND_COMMITSTATE FLUSH_FTQ_PTR 优先采用后端重定向信息冲刷FTQ指针
1.3 FLUSH_FTQPTR_AND_COMMITSTATE FLUSH_COMMIT_STATE 发生后端重定向时,进一步修改提交状态队列
2.1 TRANSFER_TO_TOP FLUSH 后端和IFU的重定向信号可能冲刷指针,拉高FTQ顶层IO接口中的icacheFlush信号
2.2 TRANSFER_TO_TOP IFU 将重定向信号转发到IFU
3.1 COMMIT_BY_ROB NORMAL 对于普通指令,根据rob_commits的ftqIdx和ftqOffset索引提交状态队列中的某条指令,将对应的提交状态设置为c_commited
3.2 COMMIT_BY_ROB FUSION 对于融合指令,根据提交类型commitType对被索引的指令和另一与之融合的指令进行提交,将对应的提交状态设置为c_commited

12.2.1.11 - FTQ向BPU发送更新与重定向信息

文档概述

FTQ将已提交指令的更新信息发往BPU进行训练,同时转发重定向信息。

术语说明

名称 定义
暂无 暂无

模块功能说明

1. 转发重定向

向toBPU接口进行转发:

1.1 IFU重定向结果有效

  • redirctFromIFU:IFU重定向结果有效时,拉高该信号(注意:IFU重定向有效的时机有两种说法,因为IFU重定向结果生成需要两个周期,此处取后者,即,IFU重定向生成过程的第二个周期有效,也是IFU生成完整重定向结果的周期)

1.2 选择后端重定向或者IFU重定向

  • redirect:如果后端重定向结果fromBackendRedirect有效,选用fromBackendRedirect,否则选用IFU重定向结果ifuRedirectToBpu

2 BPU更新暂停

BPU的更新需要两个周期,故需要三种状态去表明我们当前的更新状态:更新的第一个周期,第二个周期,更新完成。 当发生更新的时候,会暂停FTQ对指令块的提交以及发送更新信息。

3 提交指令块

FTQ需要对当前comPtr指向的当前提交指令块,进行判断是否能够提交。 这个过程比较复杂。 由于 香山V2版本 的后端会在 ROB 中重新压缩 FTQ entry,因此并不能保证提交一个 entry 中的每条指令,甚至不能保证每一个 entry 都有指令提交。

判断一个 entry 是否被提交有如下几种可能

  • robCommPtr 在 commPtr 之后(ptr更大)。也就是说,后端已经开始提交之后 entry 的指令,在 robCommPtr 指向的 entry 之前的 entry 一定都已经提交完成
  • commitStateQueue 中的某个指令块内最后一条有效范围内指令被提交。FTQ项中该指令被提交意味着这FTQ项内的指令已经全部被提交

在此以外,还必须要考虑到,后端存在 flush itself 的 redirect 请求,这意味着这条指令自身也需要重新执行,这包括异常、load replay 等情况。在这种情况下,这一FTQ项不应当被提交以更新 BPU,否则会导致 BPU 准确率显著下降。

3.1 canCommit

具体来看,判断commPtr指向的指令块能否提交,如果可以提交记为canCommit。

canCommit的设置条件如下:

3.1.1 COND1

  • 当commPtr不等于ifuWbPtr,且没有因为BPU更新而暂停,同时robCommPtr在commPtr之后。之所以要求commPtr不等于ifuWbPtr是因为,前面说过了必须先预译码写回FTQ项才能提交

3.1.2 COND2

  • commitStateQueue 中commPtr对应指令块有指令处于c_toCommit 或c_committed状态。且指令块中最后一条处于c_toCommit 或c_committed状态的指令是c_committed的。

这两种情况下,canCommit拉高,说明可以提交该指令块

3.2 canMoveCommPtr

3.2.1 提交指令块更新提交指针

在commPtr指向的指令块如果能提交,那么我们自然可以移动CommPtr指向下一个FTQ项了。

3.2.2 指令冲刷更新提交指针

但除此之外,commitStateQueue 中commPtr对应指令块的第一条指令被后端重定向冲刷掉了时,这表明该指令需要重新执行,这一FTQ项不应被提交,但是却可以更新CommPtr指针,因为该指令块内已经没有可以提交的指令了。

  • CanMoveCommPtr时,commPtr指针更新加1(一周期后成功写入)。

3.3 robCommPtr更新

有几种情况

3.3.1 COND1

  • 当来自后端接口fromBackend的rob_commits信息中,有信息有效时,取最后一条有效交割信息的ftqIdx作为robCommPtr

3.3.2 COND2

  • 不满足情况1,选取commPtr, robCommPtr中较大的那个

3.4 mmio提交

发往mmioCommitRead接口

  • mmioLastCommit:

3.4.1 COND1

  • 当commPtr比来自mmioCommitRead接口的mmioFtqPtr大时,

3.4.2 COND2

  • 或者两者正好相等,且commPtr指向的指令块中有c_toCommit 或c_committed状态的指令,最后一条处于c_toCommit 或c_committed状态的指令是c_committed的

在这两种情况下,mmioLastCommit信号在下一个周期被拉高

4 发送BPU更新信息

FTQ需要从FTQ子队列中,读取提交项的预测信息,重定向信息,meta信息,用这些信息来对BPU发送更新信息。

当canCommit时,可以提交commPtr指向的指令块时,从ftq_pd_mem,ftq_redirect_mem,ftq_meta_1r_sram_mem这些子队列,以及一些小的状态队列中读出对应指令块的相应信息,这些信息需要专门花一个周期才能读取到。具体来说:

  • 从预译码信息子队列ftq_pd_mem中读取提交提交指令块(commptr所指)的预译码信息
  • 从取指目标子队列ftq_pc_mem中读取取指信息
  • 从分支预测重定向信息子队列ftq_redirect_mem中读取提交指令块的重定向信息。
  • 从预测阶段状态队列中读取提交块来自BPU的哪个预测阶段
  • 从meta信息子队列ftq_meta_1r_sram中读取提交指令块的meta,和相应的ftb_entry。
  • 从提交状态队列commitStateQueueReg中读取提交状态,并确认指令块中哪些指令为c_committed,用bool数组表示
  • 从控制流索引状态队列cfiIndex_vec中读取指令控制流指令在块中索引
  • 结合错误预测状态队列mispredict_vec,和提交状态队列信息确认指令块中的提交错误指令。(即提交状态指示为c_commited 同时错误预测指示为预测错误)
  • 从表项命中状态队列entry_hit_status中读取提交指令块是否命中

根据相关信息进行判断:

  • 获取提交块的目标,如果commPtr等于newest_entry_ptr,则取newest_entry_target_modified拉高时记录下的newest_entry_target,否则取ftq_pc_mem.io.commPtrPlus1_rdata.startAddr,获取到的提交块目标将会被用来辅助新FTB项的生成

4.1 将子队列读取信息发向更新通道

整合完上述信息后,FTQ会向toBpu的update接口发送更新请求,具体如下:

  • valid:canCommit 且 指令块满足命中或者存在cfi指令,valid接口有效,表明可以发送更新请求
  • bits:
    • false_hit:提交块命中状态指示为h_false_hit时,该信号拉高
    • pc:提交块的取指信息中的startAddr
    • meta:提交块的meta
    • cfi_idx:提交块中cfi指令的index
    • full_target:提交块的目标
    • from_stage:提交块来自哪个预测阶段
    • spec_info:提交块的meta
    • pred_hit:提交块的命中状态为hit或者false_hit

另外,被更新的FTB表项也会同时被转发到更新接口,但是新的FTB表项生成方式相对复杂,下一节专门展开叙述

4.2 修正FTB项

更新结果会基于旧的FTB项进行更新,然后直接转发给更新接口。你可能需要先阅读FTB项相关文档了解FTB项的结构和相关信号生成方式

commit表项的相关信息会被发送给一个名为FTBEntryGen的接口,经过一系列组合电路处理,输出更新后的FTB表项信息。

为了更新FTB项,提交项如下信息会被读取:

  • 取值目标中的起始地址 startAddr
  • meta中旧FTB项 old_entry
  • 包含FTQ项内32Byte内所有分支指令的预译码信息 pd
  • 此FTQ项内有效指令的真实跳转结果 cfiIndex,包括是否跳转,以及跳转指令相对startAddr的偏移
  • 此FTQ项内分支指令(如跳转)的跳转地址(执行结果)
  • 预测时FTB是否真正命中(旧FTB项是否有效)
  • 对应FTQ项内所有可能指令的误预测 mask

接下来介绍如何通过这些信息更新FTB。 FTB项生成逻辑:

4.2.1 情况1:FTB未命中,则创建一个新的FTB项

我们会根据预译码信息进行判断,预译码会告诉我们,指令块中cfi指令是否是br指令,jmp指令信息(以及是哪种类型的jmp指令)

  1. 无条件跳转指令处理:
    • 不论是否被执行,都一定会被写入新FTB项的tailSlot
    • 如果最终FTQ项内跳转的指令是条件分支指令,写入新FTB项的第一个brSlot(目前也只有这一个),对应的strongbias被设置为1作为初始化
  2. pftAddr设置:
    • 存在无条件跳转指令时:以无条件跳转指令的结束地址设置
    • 无无条件跳转指令时:以startAddr+取指宽度(32B)设置
    • 特殊情况:当4Byte宽度的无条件跳转指令起始地址位于startAddr+30时,虽然结束地址超出取指宽度范围,仍按startAddr+32设置
  3. carry位根据pftAddr的条件同时设置
  4. 设置分支类型标志:
    • isJalr、isCall、isRet按照无条件跳转指令的类型设置
    • 特殊标志:当且仅当4Byte宽度的无条件跳转指令起始地址位于startAddr+30时,置last_may_be_rvi_call位

详细信号说明

  • cfiIndex有效(说明指令块存在跳转指令),且pd的brmask指明该指令是br指令。则判断控制流指令是br指令

  • pd的jmpinfo有效,且cifIndx有效。则进一步根据jmpinfo判断是那种类型的jmp指令

    1. 第零位为0:jal
    2. 第零位为1:jalr
    3. 第一位为1:call
    4. 第二位为1:ret
  • 判断最后一条指令是否是rvi(4byte)的jmp指令:jmpinfo有效,pd中jmpOffset等于15,且pd的rvcMask指明最后一条指令不是rvc指令

  • 判断cfi指令是否是jal指令:cfiindx = jmpOffset,且根据之前的判断确认jmp指令是jal指令

  • 判断cfi指令是jalr指令也是同理的。

  • FTB生成:valid被初始化为true

    • brslot:在判断控制流指令是br指令时,进行填充
      • valid:初始化为true
      • offset:cfiindx
      • lower和stat:根据startaddr和提交块指定的target计算
      • 对应的strongbias:被初始化为true
    • tailslot:pd的jmpinfo有效时,进行填充
      • valid:根据之前的判断确认jmp指令是jal指令或者是jalr指令时,valid有效
      • offset:pd的jmpoffset
      • lower和stat:根据startaddr和target计算,如果cfi指令是jalr指令,使用提交块指定的target,否则用pd预测的jalTarget
      • 对应的strongbias:根据之前的判断确认jmp指令是jalr指令时,拉高。strongbias是针对于BPU的ittage预测器的,该预测器基于一些统计信息工作,strongbias用来指向指令跳转偏好的强弱,其中jal指令不需要记录strongbias。
    • pftAddr:上方介绍已经够详细了
    • carry:上方介绍已经足够
    • isJalr/isCall/isRet
    • last_may_be_rvi_call

4.2.2 情况2:FTB命中,修改旧的FTB项

4.2.2.1 插入brslot的FTB项

在原来的基础上改动即可,比如插入新的slot,注意,只针对新的brslot

  1. 修改条件:首先根据oldftbentry判断在旧entry中,cfi指令是否被记录为br指令,如果不是,则说明这是一个新的br指令
  2. 接着从旧FTB中判断哪些slot可以被插入slot:
    • brslot:如果旧FTB的brslot无效,表示该slot空闲,此时可以在此位置插入新的brslot,此外,如果新slot在旧slot之前(新的br指令在旧slotbr指令之前执行,或者说在指令块之前的位置),即使不空也能插入
    • tailslot:当不能在brslot插入时,才考虑tailslot,同样,在该slot空闲或者新slot在旧slot之前,可以插入此位置
  3. 插入slot:
    1. brslot:能插入时则在这里插入,不能的时候,把对应的strongbias拉低,因为这说明新slot一定在旧slot之后(如果不想要详细了解ittage的原理可以不用理解原因)。
    2. tailslot:能插入时则在这里插入,不能的时候,如果新slot在旧slot之后,把对应的strongbias拉低,如果不在之后,当原brslot有效(即不空闲),则用插入前的brslot代替该tailslot。对应的strongbias维持不变。

注:tailslot不能插入且新slot在其之前,其实就已经说明brslot一定是可以插入的,所以才有后面的替代

pftaddr 出现新的br指令,同时旧的FTB项内没有空闲的slot,这说明确实发生了在FTB项内确实发生了FTB项的替换,pftaddr也需要做相应的调整。

  • 如果没有能插入的位置,使用新的br指令的偏移作为pftaddr对应的偏移,因为此时,新br指令一定在两个slot之后。否则,使用旧FTB项的最后一个slot的offset。将ptfoffset结合startAddr得到最后的pftAddr,carry也进行相应的设置。
  • last_may_be_rvi_call,isCall,isRet ,isJalr全部置false。
4.2.2.2 修改jmp target的FTB项

修改条件当cfi指令是一个jalr指令,且旧的tailslot对应的是一个jump的指令,但tailslot指示的target与提交项指示的target不同时,说明需要对跳转目标进行修改。

  • 根据正确的跳转目标对lower和stat进行修改
  • 两位strongbias设置成0
4.2.2.3 修改bias的FTB项

当cfi指令就是原FTB项的条件跳转指令,只需要根据跳转情况设置跳转的强弱

  • brslot:旧的brslot有发生跳转时,bias在原bias拉高,发生跳转的cfiindex等于该slot的offset,brslot有效时,保持拉高,其余情况拉低。
  • tailslot:旧的brslot没有跳转,而tailslot有分支指令且发生跳转,把brslot的bias置为false,tailslot保持bias的方式与上面的brslot一致。

修改条件:当旧的bias拉高且对应的旧的FTB项中的slot中有分支指令,同时修改后的bias拉低。任何一个slot出现这种情况都需要进行修改。

最后,需要抉择出一个修改的FTB项

  • 如果cfi是一个新的分支指令,我们采用插入新的slot的FTB项。
  • 如果是cfi是一个jalr指令,且跳转目标发生修改,我们采用修改jmp跳转目标的FTB项
  • 如cfi指令就是原FTB项的条件跳转指令,采用修改bias的FTB项

4.3 发送新FTB项及相关信号

此时,根据是否hit,我们已经得到更新后的FTB项了,在这个基础上我们继续更新一些相关信号以发送到FTQ更新接口。

  • new_br_insert_pos:使用之前我们判断的FTB项中可插入位置的bool数组
  • taken_mask:根据cfi指令在更新后FTB项的位置判断,只有分支指令才做此计算,若是jmp指令置为0。
  • jump_taken: cfi指令在更新后FTB项的taislot,且jmpValid。
  • mispred_mask的最后一项:更新后的FTB项jumpValid,且预译码推断的jmp指令在提交项的错误预测信息中指示错误。
    • mispred_mask 预测块内预测错误的掩码。第一、二位分别代表两个条件分支指令是否预测错误,第三位指示无条件跳转指令是否预测错误。
      • 接口类型:Vec(numBr+1, Bool())
  • old_entry:如果hit,且FTB项不做任何修改,即不满足上述三种修改FTB项的条件,拉高该信号,说明更新后的FTB项是旧的FTB项。
发送处理后的更新信息

此时,我们就可以向BPU发送处理好的更新信息了,下面是update的接口接收的信号

  • ftb_entry:更新后的FTB项
  • new_br_insert_pos:上一小节已述
  • mispred_mask:上一小节已述
  • old_entry:上一小节已述
  • br_taken_mask: 上一小节已述
  • br_committed:根据提交项的提交状态信息判断新FTB项中的有效分支指令是否已经提交
  • jmp_taken:上一小节已述

接口说明

顶层IO 作用
toBpu 向BPU发送重定向信息与更新信息
fromBackend 获取指令交割信息,判断指令块是否被提交
mmioCommiRead 发送mmio指令的提交信息

测试点总表 (【必填项】针对细分的测试点,列出表格)

实际使用下面的表格时,请用有意义的英文大写的功能名称和测试点名称替换下面表格中的名称

序号 功能名称 测试点名称 描述
1.1 TRANSFER_REDIRECT REDIRECT_FROM_FLUSH IFU重定向结果有效时,拉高该信号
1.2 TRANSFER_REDIRECT CHOOSE_REDIRECT 如果后端重定向结果fromBackendRedirect有效,选用fromBackendRedirect,否则选用IFU重定向结果ifuRedirectToBpu
2 UPDATE_STALL UPDATE_STALL 当发生BPU的更新时候,会暂停FTQ对指令块的提交以及发送更新信息
3.1.1 CAN_COMMIT_ENTRY COND1 当commPtr不等于ifuWbPtr,且没有因为BPU更新而暂停,同时robCommPtr在commPtr之后,canCommit拉高
3.1.2 CAN_COMMIT_ENTRY COND2 commitStateQueue 中commPtr对应指令块有指令处于c_toCommit 或c_committed状态。且指令块中最后一条处于c_toCommit 或c_committed状态的指令是c_committed的,canCommit拉高
3.2.1 MOVECOMMPTR BY_ROB_COMMIT 在commPtr指向的指令块如果能提交,可以移动CommPtr
3.2.2 MOVECOMMPTR BY_FLUSH commitStateQueue 中commPtr对应指令块的第一条指令被后端重定向冲刷掉,可以移动CommPtr
3.3.1 UPDATE_ROB_COMM_PTR COND1 当来自后端接口fromBackend的rob_commits信息中,有信息有效时,取最后一条有效交割信息的ftqIdx作为robCommPtr
3.3.2 UPDATE_ROB_COMM_PTR COND2 不满足情况1,选取commPtr, robCommPtr中较大的那个
3.4.1 MMIO_LAST_COMMIT COND1 当commPtr比来自mmioCommitRead接口的mmioFtqPtr大时,mmioLastCommit信号在下一个周期被拉高
3.4.2 MMIO_LAST_COMMIT COND2 两者正好相等,且commPtr指向的指令块中有c_toCommit 或c_committed状态的指令,最后一条处于c_toCommit 或c_committed状态的指令是c_committed的,mmioLastCommit信号在下一个周期被拉高
4.1 SEND_UPDATE_TO_BPU SEND_SUBQUEUE_INFO_TO_UPDATE 将提交项的子队列读取信息发向更新通道
4.2.1 UPDATE_FTB_ENTRY CREATE_NEW FTB未命中,创建一个新的FTB项
4.2.2.1 CREATE_NEW_FTB_ENTRY INSERT FTB未命中,创建一个新的FTB项,在原来的基础上改动即可,插入新的slot
4.2.2.2 CREATE_NEW_FTB_ENTRY jmp target FTB未命中,创建一个新的FTB项,在原来的基础上改动即可,当cfi指令是一个jalr指令,且旧的tailslot对应的是一个jump的指令,但tailslot指示的target与提交项指示的target不同时,说明需要对跳转目标进行修改
4.2.2.3 CREATE_NEW_FTB_ENTRY bias FTB未命中,创建一个新的FTB项,在原来的基础上改动即可,当cfi指令就是原FTB项的条件跳转指令,只需要根据跳转情况设置跳转的强弱
4.3 SEND_UPDATE_TO_BPU SEND_NEW_FTB_RELATED 根据是否hit,我们已经得到更新后的FTB项了,在这个基础上我们继续更新一些相关信号以发送到FTQ更新接口。

12.2.2 - IFU

本文档参考香山IFU设计文档写成

本文档撰写的内容截至[c670557]

请注意,本文档撰写的测试点仅供参考,如能补充更多测试点,最终获得的奖励可能更高!

IFU说明文档

文档概述

本文档描述IFU的功能,并根据功能给出测试点参考,方便测试的参与者理解测试需求,编写相关测试用例。

为方便验证参与者,本文档中还额外给出了整体框图和流水级的示意图,以及各个rtl接口的详细说明。此外,本文档还给出了两个时序示例。

术语说明

名称 描述
RVC(RISC-V Compressed Instructions) RISC-V 手册"C"扩展规定的 16 位长度压缩指令
RVI(RISC-V Integer Instructions) RISC-V 手册规定的 32 位基本整型指令
IFU(Instruction Fetch Unit) 取指令单元
FTQ(Fetch Target Queue) 取指目标队列
ICache(L1 Instruction Cache) 一级指令缓存
IBuffer(Instruction Buffer) 指令缓冲
CFI(Control Flow Instruction) 控制流指令
ITLB(Instruction Translation Lookaside Buffer) 指令地址转译后备缓冲器
InstrUncache(Instruction Ucache Module) 指令 MMIO 取指处理单元

整体框图

以下是IFU的架构简图:

framework

流水级示意图

香山的IFU一共分为5个stage。

F0 stage:接收FTQ请求,同时告诉FTQ自己已经ready了,同时,FTQ也会通知ICache准备数据。此外,这一阶段还会接受重定向信息。

F1 stage:从FTQ请求中先计算出每个指令的pc,half_pc,并计算cut_ptr(这是后续将icache返回的指令码进行切分的依据)

F2 stage:从icache获取响应数据(缓存行)并校验,提取出异常信息(包括页错误、访问错误、mmio信息);生成预测到的指令范围(但这并不是一个数字,而是一个用多位表示的bool数组,该位为1表示这一指令在预测块范围内);从缓存行中,利用上一阶段求出的cut_ptr切分出17×2的初步指令码,最后进行预译码和指令扩展。

F3 stage:这一阶段主要是对译码阶段的结果进行预检查,对RVC指令进行扩展,以及MMIO状态下的处理逻辑,并向IBuffer写指令码和前端信息

WB(写回)stage:将预检查的结果写回FTQ。根据预检查结果判断是否进行内部冲刷。

以下是一张示意图:

stages

子模块列表

子模块 描述
PreDecoder 预译码模块
F3Predecoder F3阶段预译码模块
RVCExpander RVC指令扩展模块
PredChecker 预检查模块
FrontendTrigger 前端断点模块

IFU模块功能说明

FTQ 将预测块请求分别发送到 ICache 和 IFU 模块,IFU 等到来自 ICache 返回至多两个缓存行的指令码后,进行切分产生取指令请求范围限定的初始指令码,并送到预译码器进行预译码下一拍根据预译码信息修正有效指令范围,同时进行指令码扩展并将指令码及其他信息发送给 IBuffer 模块。当 ICache 查询地址属性发现是 MMIO 地址空间时,IFU 需要将地址发送给 MMIO 处理单元取指令,这个时候处理器进入多周期顺序执行模式,IFU 阻塞流水线直到收到来自 ROB 的提交信号时,IFU 才允许下一个取指令请求的进行,同时 IFU 需要对跨页的 MMIO 地址空间 32 位指令做特殊处理(重发机制)。

1. 接收FTQ取指令请求(F0流水级)

​ 在F0流水级,IFU接收来自FTQ以预测块为单位的取指令请求。请求内容包括预测块起始地址、起始地址所在cache line的下一个cache line开始地址、下一个预测块的起始地址、该预测块在FTQ里的队列指针、该预测块有无taken的CFI指令(控制流指令)和该taken的CFI指令在预测块里的位置以及请求控制信号(请求是否有效和IFU是否ready)。每个预测块最多包含32字节指令码,最多为16条指令。IFU需要置位ready驱动FTQ向ICache发送请求。

1.1. F0流水级接收请求

IFU应当能向FTQ报告自己已ready。

所以,对于这一测试点我们只需要在发送请求后检查和ftq相关的的ready情况即可。

序号 功能名称 测试点名称 描述
1.1 IFU_RCV_REQ READY IFU接收FTQ请求后,设置ready

2. 指令切分产生初始指令码(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复制一份拼接在一起,从中间截取就可以拿到数据。

详细的信息可以参考ICache文档

2.1. F1流水级计算信息和切分指针

F1流水级也会计算PC。

同时还需要生成17位的切分指针(也就是从拼接后的缓存行切出初始指令码的idx数组,在昆明湖架构中,计算方式为拼接00和startAddr[5:1], 然后分别与0~16相加) 用于后续从缓存行提取初始指令码。

所以,首先我们需要检查F1流水级生成的PC的正确与否。如果可能,也需要检查一下切分指针的生成。

可以总结出以下的细分测试点:

序号 功能名称 测试点名称 描述
2.1.1 IFU_F1_INFOS PC IFU接收FTQ请求后,在F1流水级生成PC
2.1.2 IFU_F1_INFOS CUT_PTR IFU接收FTQ请求后,在F1流水级生成后续切取缓存行的指针

2.2. F2流水级获取指令信息

包括获取异常信息、物理地址、客户物理地址、是否在MMIO空间等。

获取异常信息之后,还需要计算异常向量。ICache会为每个缓存行返回一个异常类型,只需要计算每个指令pc属于哪个缓存行, 然后将对应缓存行的异常类型赋给该位置即可。

所以,只需要分别检查几种指令信息即可。

序号 功能名称 测试点名称 描述
2.2.1 IFU_F2_INFOS EXCP_VEC IFU接收ICache内容后,会根据ICache的结果生成属于每个指令的异常向量
2.2.2 IFU_F2_INFOS PADDR IFU接收ICache内容后,会根据ICache的结果生成属于每个端口的物理地址。
2.2.3 IFU_F2_INFOS GPADDR IFU接收ICache内容后,会根据ICache的结果生成0号端口的客户物理地址。
2.2.4 IFU_F2_INFOS MMIO IFU接收ICache内容后,会根据ICache的结果判断当前取指请求是否属于MMIO空间。

2.3. F2流水级计算预测块有效指令范围

指令有效范围包括两种,无跳转和有跳转的

无跳转的指令有效范围为当前预测块从起始地址到下一个预测块的起始地址的所有指令。

有跳转的指令有效范围jump_range为当前预测块的起始地址到预测块中第一个跳转指令地址(包含第一个跳转指令地址)之间的所有指令。

最终的指令有效范围是两者相与的结果。

我们需要分别对两种有效范围进行检查,再检查最终结果。

序号 功能名称 测试点名称 描述
2.3.1 IFU_INSTR_VALID_RANGE NORMAL IFU根据FTQ请求,计算无跳转指令有效范围
2.3.2 IFU_INSTR_VALID_RANGE JUMP IFU根据FTQ请求,计算跳转指令有效范围
2.3.3 IFU_INSTR_VALID_RANGE FINAL IFU综合两类指令有效范围,生成最终指令有效范围

2.4. 提取初始指令码

IFU需要将ICache返回的缓存行复制一份并拼接。然后利用上一流水级计算的idx数组,从缓存行提取17x2字节的初始指令码。

序号 功能名称 测试点名称 描述
2.4 IFU_INSTR_CUT CUT IFU根据上一流水级的切取指针,从缓存行提取初始指令码。

3. 预译码(F2流水级,主要由PreDecode模块完成)

在F2流水级,我们需要将上一步完成切分的指令码交给PreDecode子模块,他的作用主要有二:

其一是生成预译码信息,包括该指令是否是有效指令的开始、是否是RVC指令、是否是CFI指令、CFI指令类型(branch/jal/jalr/call/ret)、CFI指令的目标地址计算偏移等。输出的预译码信息中brType域的编码如下:

CFI指令类型 brType类型编码
非CFI 00
branch指令 01
jal指令 10
jalr指令 11

brType类型一览

其二是将初始指令码两两组合之后,得到16x4字节的指令码(从起始地址开始,2字节做地址递增,地址开始的4字节作为一条32位初始指令码)。

此外,预译码阶段还需要分类讨论,得出两种指令有效向量(起始指令是不是RVI指令的后半部分),并交给IFU进行判断选择,可以参阅后面的跨预测块32位指令处理部分

其他功能和详细内容(比如怎么判断RET和CALL指令等)参见PreDecodeF3Predecoder子模块的描述。

3.1. 指令码拼接

对于上一功能中生成的指令序列,应当拼接成为16x4的指令码序列。

序号 功能名称 测试点名称 描述
3.1.1 IFU_PREDECODE CONCAT 将生成的指令序列拼接成为16x4的指令码序列

3.2. 判定RVC指令

PreDecode功能需要判断一条指令是否为RVC指令

序号 功能名称 测试点名称 描述
3.2.1 IFU_PREDECODE_RVC RVC 传入RVC指令,应该判断为RVC
3.2.2 IFU_PREDECODE_RVC RVI 传入RVI指令,不应判断为RVC

3.3. 计算跳转偏移

预译码阶段需要对BR和J类型的跳转指令偏移进行计算。

序号 功能名称 测试点名称 描述
3.3.1 IFU_PREDECODE_JMP_TGT RVC_J 对传入RVC扩展的J指令,检查计算的偏移
3.3.2 IFU_PREDECODE_JMP_TGT RVI_J 对传入RVI扩展的J指令,检查计算的偏移
3.3.3 IFU_PREDECODE_JMP_TGT RVC_BR 对传入RVC扩展的BR指令,检查计算的偏移
3.3.4 IFU_PREDECODE_JMP_TGT RVI_BR 对传入RVI扩展的BR指令,检查计算的偏移

3.4. 判定CFI指令类型

预译码阶段需要对CFI指令的类型进行判断,一共有四种判断结果:非CFI指令、BR指令、JAL指令、JALR指令

序号 功能名称 测试点名称 描述
3.4.1 IFU_PREDECODE_CFI_TYPE NON_CFI 对传入的非CFI指令(包括RVC.EBREAK),应该判定为类型0
3.4.2 IFU_PREDECODE_CFI_TYPE BR 对传入的BR指令,应该判定为类型1
3.4.3 IFU_PREDECODE_CFI_TYPE JAL 对传入的JAL指令,应该判定为类型2
3.4.4 IFU_PREDECODE_CFI_TYPE JALR 对传入的JALR指令,应该判定为类型3

3.5. 判定RET和CALL

预译码阶段需要判断一条指令是否为ret或者call指令,具体请参阅F3Predecoder文档

序号 功能名称 测试点名称 描述
3.5.1 IFU_PREDECODE_RET_CALL NON_CFI_BR 对传入的非CFI和BR指令,都不应判定为call或者ret
3.5.2.1.1 IFU_PREDECODE_RET_CALL RVI_JAL_CALL 对传入的RVI.JAL指令,当rd设置为1或5,应当判定该指令为call
3.5.2.1.2 IFU_PREDECODE_RET_CALL RVI_JAL_NOP 对传入的RVI.JAL指令,当rd设置为1和5之外的值,不应当判定该指令为call或ret
3.5.2.2 IFU_PREDECODE_RET_CALL RVC_JAL_NOP 对传入的RVC.JAL指令,无论什么情况都不能判定为call或ret
3.5.3.1.1 IFU_PREDECODE_RET_CALL RVI_JALR_CALL 传入RVI.JALR指令,并且rd为1或5,无论其他取值,都应判定为call
3.5.3.1.2 IFU_PREDECODE_RET_CALL RVI_JALR_RET 传入RVI.JALR指令,rd不为1和5,rs为1或5,应判定为ret
3.5.3.1.3 IFU_PREDECODE_RET_CALL RVI_JALR_NOP 对传入的JALR指令,若rd和rs均不为link,则不应判定为ret和call
3.5.3.2.1 IFU_PREDECODE_RET_CALL RVC_JALR_CALL 传入RVC.JALR指令,必定为call
3.5.3.2.2.1 IFU_PREDECODE_RET_CALL RVC_JR_RET 传入RVC.JR指令,rs为1或5,应判定为ret
3.5.3.2.2.2 IFU_PREDECODE_RET_CALL RVC_JR_NOP 传入RVC.JR指令,rs不为1或5,不应判定为ret

3.6. 计算指令有效开始向量

预译码阶段需要根据两种情况计算有效指令开始向量,IFU top需要对有效指令开始向量进行选择。

序号 功能名称 测试点名称 描述
3.6.1 IFU_PREDECODE_VALID_STARTS LAST_IS_END 上一预测块的最后2字节恰为RVC指令或RVI指令的后半部分,按第一位为True推导有效开始向量
3.6.2 IFU_PREDECODE_VALID_STARTS LAST_NOT_END 上一预测块的最后2字节上一预测块的最后2字节为RVI指令的前半部分,按第一位为False推导有效开始向量

4. 指令扩展(F3流水级)

这一部分将从PreDecode返回的16条指令码分别送交指令扩展器(RVCExpander)进行32位指令扩展(RVI保持不变, RVC指令根据手册的规定进行扩充)。

但是,如果RVC指令非法,需要向IBuffer写入原始指令码。

4.1. 指令扩展和检错

指令扩展阶段需要分RVC和RVI指令进行考虑,其中RVC指令需要判断合法与否。

序号 功能名称 测试点名称 描述
4.1.1 IFU_RVC_EXPAND VALID_RVC 对合法RVC指令,写扩展后的指令码,判断结果为合法指令
4.1.2 IFU_RVC_EXPAND INVALID_RVC 对非法RVC指令,写原始指令码,判断结果为非法指令
4.1.3 IFU_RVC_EXPAND RVI RVI指令直接写入原始指令即可,判断结果为合法指令

5. 预测错误预检查(F3流水级,主要由PreChecker子模块完成)

这一功能是为了将一些不依赖于执行结果的预测错误在早期就发现出来。这一阶段检查五类错误:

jal类型错误:预测块的范围内有jal指令,但是预测器没有对这条指令预测跳转;

ret类型错误:预测块的范围内有ret指令,但是预测器没有对这条指令预测跳转;

jalr类型错误:预测块的范围内有jalr指令,但是预测器没有对这条指令预测跳转;

无效指令预测错误:预测器对一条无效的指令(不在预测块范围/是一条32位指令中间)进行了预测;

非CFI指令预测错误:预测器对一条有效但是不是CFI的指令进行了预测;

转移目标地址错误:预测器给出的转移目标地址不正确。

在预检查的最后将会修正之前预测的各个指令的跳转情况。同时,如果存在jal或者ret类型预测错误,还将修正fixedRange——这是指令有效范围向量,可以看作一个bool数组,其中某一位为1也就是对应的指令在这一范围内。

这一部分的功能点和PredChecker子模块的功能点相同。

5.1. BPU预测信息的JAL预测错误检查

PredChecker会对传入的预测块进行JAL预测错误预检查并修正指令有效范围向量和预测的跳转指令。

对这一模块的测试,我们分为两部分:正确的输入是否会误检和确有JAL检测错误的预测块输入能否检出。

对此,我们设计如下的测试点:

序号 功能名称 测试点名称 描述
5.1.1.1 IFU_PRECHECK_JAL_MISS NOP 预测块中没有JAL指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JAL预测错误。
5.1.2.1 IFU_PRECHECK_JAL_MISS CORRECT 预测块中有JAL指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JAL预测错误。
5.1.2.1 IFU_PRECHECK_JAL_CHECK NO_SEL 预测块中存在JAL指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出JAL预测错误。
5.1.2.2 IFU_PRECHECK_JAL_CHECK SEL_LATE 预测块中存在JAL指令,但是BPU预测信息取的跳转指令在第一条JAL指令之后,检查PredChecker是否能检测出JAL预测错误。

5.2. BPU预测信息的RET预测错误检查

PredChecker会对传入的预测块进行RET预测错误预检查并修正指令有效范围向量和新的预测结果。

和JAL预测错误类似,我们也按照误检和正检来构造。

为此,我们设计如下的测试点:

序号 功能名称 测试点名称 描述
5.2.1.1 IFU_PRECHECK_RET_MISS NOP 预测块中没有RET指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报RET预测错误。
5.2.2.1 IFU_PRECHECK_RET_MISS CORRECT 预测块中有RET指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报RET预测错误。
5.2.2.1 IFU_PRECHECK_RET_CHECK NO_SEL 预测块中存在RET指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
5.2.2.2 IFU_PRECHECK_RET_CHECK SEL_LATE 预测块中存在RET指令,但是BPU预测信息取的跳转指令在第一条RET指令之后,检查PredChecker是否能检测出RET预测错误。

5.3. BPU预测信息的JALR预测错误检查

PredChecker会对传入的预测块进行JALR预测错误预检查并修正指令有效范围向量和新的预测结果。

和JAL/RET预测错误类似,我们也按照误检和正检来构造。

为此,我们设计如下的测试点:

序号 功能名称 测试点名称 描述
5.3.2.2 IFU_PRECHECK_JALR_MISS NOP 预测块中没有JALR指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JALR预测错误。
5.3.2.2 IFU_PRECHECK_JALR_MISS CORRECT 预测块中有JALR指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JALR预测错误。
5.3.2.2 IFU_PRECHECK_JALR_CHECK NO_SEL 预测块中存在JALR指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
5.3.2.2 IFU_PRECHECK_JALR_CHECK SEL_LATE 预测块中存在JALR指令,但是BPU预测信息取的跳转指令在第一条JALR指令之后,检查PredChecker是否能检测出JALR预测错误。

5.4. 更新指令有效范围向量和预测跳转的指令

PredChecker在检查出Jal/Ret/Jalr指令预测错误时,需要重新生成指令有效范围向量, 有效范围截取到Jal/Ret/Jalr指令的位置,之后的bit全部置为0。 同时,还需要根据每条指令的预译码信息和BPU的预测信息修复预测跳转的结果。

所以,根据功能要求,我们可以划分出三类情况,分别是预测的有效范围和取用的跳转指令正确的情况, 由于RET和JAL预测错误引起的有效范围偏大和错判非跳转指令和无效指令引起的有效范围偏小。

序号 功能名称 测试点名称 描述
5.4.1 IFU_PREDCHECK_FIX NOP 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。
5.4.2 IFU_PREDCHECK_FIX BIGGER_FIX 如果检测到了JAL、RET、JALR类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。
5.4.3 IFU_PREDCHECK_FIX SMALLER_NOP 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令(也即不能扩大范围),因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令

5.5. 非CFI预测错误检查

非CFI预测错误的条件是被预测跳转的指令根据预译码信息显示不是一条CFI指令。

要检验这一功能,我们仍然按误检和正确检验来设计测试点:

序号 功能名称 测试点名称 描述
5.5.1.1 IFU_PREDCHECK_NON_CFI_MISS NOP 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.5.1.2 IFU_PREDCHECK_NON_CFI_MISS CORRECT 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.5.2 IFU_PREDCHECK_NON_CFI_CHECK ERROR 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误

5.6. 无效指令预测错误检查

目标地址预测错误的条件是,被预测的是一条有效的jal或者branch指令, 同时预测的跳转目标地址和由指令码计算得到的跳转目标不一致。

和先前的思路一样,我们仍然按误检和检出两类组织测试点:

序号 功能名称 测试点名称 描述
5.6.1.1 IFU_PREDCHECK_INVALID_MISS NOP 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.6.1.2 IFU_PREDCHECK_INVALID_MISS INVALID_JMP 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.6.1.3 IFU_PREDCHECK_INVALID_MISS CORRECT 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.6.2 IFU_PREDCHECK_INVALID_MISS ERROR 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误

5.7. 目标地址预测错误检查

无效指令预测错误的条件是被预测的指令的位置根据预译码信息中的指令有效向量显示不是一条有效指令的开始。

要检验这一功能,我们按照误检和正确检测来设计测试点:

序号 功能名称 测试点名称 描述
5.7.1.1 IFU_PREDCHECK_TARGET_MISS NOP 构造不存在跳转指令并且未预测跳转的预测信息作输入,测试PredChecker是否会错检目标地址预测错误
5.7.1.2 IFU_PREDCHECK_TARGET_MISS CORRECT 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
5.7.2 IFU_PREDCHECK_TARGET_CHECK ERROR 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误

5.8. 生成跳转和顺序目标

PredChecker还需要负责生成跳转和顺序目标。

我们通过随机生成译码信息进行测试

序号 功能名称 测试点名称 描述
5.8 IFU_PREDCHECK_TARGETS TARGETS 随机提供译码信息,检测生成的跳转目标和顺序目标。

6. 前端重定向(WB阶段)

如果在预测错误预检查的部分发现了上述的6类错误,那么需要在写回阶段产生一个前端重定向将F3以外的流水级进行冲刷, 从而让BPU能够从正确路径重新开始预测。

还有一种情况下需要冲刷流水线。在下一节中,如果误判了当前预测块的最后2B为RVI指令的上半部分,则也需要冲刷当前预测块F3之前的流水级。

6.1. 预测错误重定向

如果发现了预检阶段检出的错误,则需要产生前端重定向,将F3以外的流水级冲刷

只需要构造有预测错误的预测请求,检查冲刷情况即可。

序号 功能名称 测试点名称 描述
6.1.1 IFU_REDIRECT JAL 预测请求中存在JAL预测错误,需要冲刷流水线
6.1.2 IFU_REDIRECT RET 预测请求中存在RET预测错误,需要冲刷流水线
6.1.3 IFU_REDIRECT JALR 预测请求中存在JALR预测错误,需要冲刷流水线
6.1.4 IFU_REDIRECT NON_CFI 预测请求中存在非CFI预测错误,需要冲刷流水线
6.1.5 IFU_REDIRECT INVALID 预测请求中存在无效指令预测错误,需要冲刷流水线
6.1.6 IFU_REDIRECT TARGET_FAULT 预测请求中存在跳转目标错误,需要冲刷流水线

7. 跨预测块32位指令处理

因为预测块的长度有限制,因此存在一条RVI指令前后两字节分别在两个预测块的情况。IFU首先在第一个预测块里检查最后2字节是不是一条RVI指令的开始,如果是并且该预测块没有跳转,那么就设置一个标识寄存器f3_lastHalf_valid,告诉接下来的预测块含有后半条指令。在F2预译码时,会产生两种不同的指令有效向量:

  • 预测块起始地址开始即为一条指令的开始,以这种方式根据后续指令是RVC还是RVI产生指令有效向量

  • 预测块起始地址是一条RVI指令的中间,以起始地址 + 2位一条指令的开始产生有效向量

在F3,根据是否有跨预测块RVI标识来决定选用哪种作为最终的指令有效向量,如果f3_lastHalf_valid为高则选择后一种(即这个预测块第一个2字节不是指令的开始)。IFU所做的处理只是把这条指令算在第一个预测块里,而把第二个预测块的起始地址位置的2字节通过改变指令有效向量来无效掉。

7.1. 跨预测块32位指令处理

如果发现当前预测块的最后两个字节是一条RVI指令的开始,则设置一个标识f3_lastHalf_valid,告诉接下来的预测块含有后半条指令。

我们没有办法直接观察到这个标识,但是可以通过下一预测块的开始向量的首位来判断。

7.2. 跨预测块指令误判

但是,如果这一判断出现问题(比如当前预测块存在跳转),则需要进行流水线冲刷。

这一功能需要PredChecker子模块“配合”(仅仅通过外部IO的修改很难触发这个防御机制),实现起来比较麻烦,但是还是列举一个测试点(见后文总表)

8. 将指令码和前端信息送入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正确。

8.1. 传送指令码和前端信息

传送给IBuffer的信息包括:经过扩展的32位指令码、16条指令中每条指令的例外信息、预译码信息、FTQ队列中的指针位置、其他后端需要的信息(经过折叠的PC)、 io_toIbuffer_bits_valid(表示指令是否是一条指令的开始)、io_toIbuffer_bits_enqEnable(前者与上被修正过的预测块指令范围, 从而还能表示指令是否在预测块表示的指令范围内)。

这里要做的只是确认这些信息是否正确传递

序号 功能名称 测试点名称 描述
8.1.1 IFU_TO_IBUFFER INSTRS IFU向IBuffer传送扩展后的指令码
8.1.2 IFU_TO_IBUFFER EXCP IFU向IBuffer传送每个指令的异常信息
8.1.3 IFU_TO_IBUFFER PD_INFO IFU向IBuffer传递每个指令的预译码信息
8.1.4 IFU_TO_IBUFFER FTQ_PTR IFU向IBuffer传送FTQ预测块的指针
8.1.5 IFU_TO_IBUFFER FOLD_PC IFU向IBuffer传送折叠的PC
8.1.6 IFU_TO_IBUFFER VALID_STARTS IFU向IBuffer传送表示指令有效和指令是否为指令开始的向量

功能点8.2. 客户页错误传送gpaddr信息

当且仅当发生guest page fault时,后端需要gpaddr信息,为了节省面积,gpaddr不走正常通路进入ibuffer, 而是随ftqPtr被发送到gpaMem,后端需要时从gpaMem读出。IFU需要保证gpf发生时通向gpaMem的valid拉高、gpaddr正确,同时还要传递预测块的ftqIdx(通过waddr传入)。

这里我们只需要确保在客户页错误发生时通向gpaMem的valid为高,且gpaddr正确填入。

序号 功能名称 测试点名称 描述
8.2.1 IFU_TO_GPAMEM GPADDR 客户页错误发生时,IFU应将gpaMem的valid拉高且填入gpaddr

9. 分支预测overriding冲刷流水线

当FTQ内未缓存足够预测块时,IFU可能直接使用简单分支预测器提供的预测地址进行取指,这种情况下,当精确预测器发现简单预测器错误时,需要通知IFU取消正在进行的取指请求。具体而言,当BPU的S2流水级和S3流水级发现错误时,需要冲刷且仅冲刷IFU的F0流水级(参见香山的提交#6f9d483)。

IFU在收到BPU发送的冲刷请求时,会将F0流水级上取指请求的指针与BPU发送的冲刷请求的指针进行比较,若冲刷的指针在取指的指针之前,说明当前取指请求在错误的执行路径上,需要进行流水线冲刷;反之,IFU可以忽略BPU发送的这一冲刷请求。此外,比较的时候还需要注意flag的情况,flag是一个指示队列循环的指针,flag不同即在不同的“圈”上,此时反而是idx的值更小,ftqIdx才会更大。

9.1 核验指针

IFU收到BPU冲刷请求后,会将F0/F1流水级上取指令请求的指针比较,冲刷的指针在取指之前,即当前取指令请求在错误的执行路径上,才需要 冲刷IFU。

我们仍然需要从两个方向校验这个功能,即当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。 然而,当冲刷指令在取指令的指针之后时,则不能对流水线进行冲刷。

序号 功能名称 测试点名称 描述
9.1.1 IFU_OVERRIDE_FLUSH_CHECK BEFORE 当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。
9.1.2 IFU_OVERRIDE_FLUSH_CHECK NOT_BEFORE 当冲刷指令在取指令的指针相同或之后时,IFU不能对流水线进行冲刷。

9.2 BPU S2/S3流水级发现错误

BPU的S2和S3流水级发现错误时,需冲刷IFU的F0流水级。故设计下列检查点

序号 功能名称 测试点名称 描述
9.2.1 IFU_OVERRIDE_FLUSH S2 当BPU的S2流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷
9.2.2 IFU_OVERRIDE_FLUSH S3 当BPU的S3流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷

10. 指令信息和误预测信息写回FTQ(WB阶段)

在F3的下一级WB级,IFU将指令PC、预译码信息、错误预测指令的位置、正确的跳转地址以及预测块的正确指令范围等信息写回FTQ,同时传递该预测块的FTQ指针用以区分不同请求。

同时,正如前面提到的,IFU检测到预测错误时会进行前端冲刷,同样地,FTQ也需要据此进行冲刷,因此,这也是IFU写回错误信息的意义——可以辅助FTQ判断是否冲刷流水线。

10.1 写回指令信息和误预测信息

将指令PC、预译码信息、错误预测指令的位置、正确的跳转地址以及预测块的正确指令范围等信息写回FTQ,并传递该预测块的FTQ指针。

序号 功能名称 测试点名称 描述
10.1.1 IFU_WB_FTQ PCS IFU的WB流水级,需要向FTQ写回指令PC
10.1.2 IFU_WB_FTQ PD_INFO IFU的WB流水级,需要向FTQ写回每个指令的预译码信息
10.1.3 IFU_WB_FTQ ERR_POS IFU的WB流水级,需要向FTQ写回BPU错误预测的指令位置
10.1.4 IFU_WB_FTQ TARGET IFU的WB流水级,需要向FTQ写回该预测块的正确跳转地址
10.1.5 IFU_WB_FTQ RANGE IFU的WB流水级,需要向FTQ写回预测块的正确指令范围
10.1.6 IFU_WB_FTQ FTQ_PTR IFU的WB流水级,需要向FTQ传递预测块的FTQ指针

11. MMIO处理逻辑

在处理器上电复位时,内存还没有准备好,此时需要从Flash中取指令执行。 这种情况下需要IFU向MMIO总线发送宽度为64位的请求从flash地址空间取指令执行。同时IFU禁止对MMIO总线的推测执行,即IFU需要等到每一条指令执行完成得到准确的下一条指令地址之后才继续向总线发送请求。

这之后,根据FTQ中的指令地址,决定是否MMIO取指令。

mmio_states

  1. 状态机默认在 m_idle 状态,若 F3 流水级是 MMIO 取指令请求,且此前没有发生异常,状态机进入 m_waitLastCmt 状态。
  2. m_waitLastCmt)IFU 通过 mmioCommitRead 端口到 FTQ 查询,IF3 预测块之前的指令是否都已提交,如果没有提交则阻塞等待前面的指令都提交完1
  3. m_sendReq)将请求发送到 InstrUncache 模块,向 MMIO 总线发送请求。
  4. m_waitResp)InstrUncache 模块返回后根据 pc 从 64 位数据中截取指令码。
  5. 若 pc 低位为3'b110,由于 MMIO 总线的带宽限制为 8B 且只能访问对齐的区域,本次请求的高 2B 将不是有效的数据。若返回的指令数据表明指令不是 RVC 指令,则这种情况需要对 pc+2 的位置(即对齐到下一个 8B 的位置)进行重发才能取回完整的 4B 指令码。
    1. 重发前,需要重新对 pc+2 进行 ITLB 地址翻译和 PMP 检查(因为可能跨页)(m_sendTLBm_TLBRespm_sendPMP),若 ITLB 或 PMP 出现异常(access fault、page fault、guest page fault)、或检查发现 pc+2 的位置不在 MMIO 地址空间,则直接将异常信息发送到后端,不进行取指。
    2. 若无异常,(m_resendReqm_waitResendResp)类似 2/3 两步向 InstrUncache 发出请求并收到指令码。
  6. 当 IFU 寄存了完整的指令码,或出错(重发时的ITLB/PMP出错,或 Uncache 模块 tilelink 总线返回 corrupt2)时,(m_waitCommit)即可将指令数据和异常信息发送到 IBuffer。需要注意,MMIO 取指令每次只能非推测性地向总线发起一条指令的取指请求,因此也只能向 IBuffer 发送一条指令数据。并等待指令提交。
    1. 若这条指令是 CFI 指令,由后端发送向 FTQ 发起冲刷。
    2. 若是顺序指令,则由 IFU 复用前端重定向通路刷新流水线,同时复用 FTQ 写回机制,把它当作一条错误预测的指令进行冲刷,重定向到该指令地址 +2 或者 +4(根据这条指令是 RVI 还是 RVC 选择)。这一机制保证了 MMIO 每次只取入一条指令。
  7. 提交后,(m_commited)状态机复位到 m_idle 并清空各类寄存器。

除了上电时,debug 扩展、Svpbmt 扩展可能也会使处理器在运行的任意时刻跳到一块 MMIO 地址空间取指令,请参考 RISC-V 手册。对这些情况中 MMIO 取指的处理是相同的。

11.1. 上电复位处理

处理器上电复位时,IFU需向MMIO总线发送宽度为64位的请求从flash地址空间取指令,并禁止对MMIO总线的推测执行。

上电的情况和正常情况其实没有任何区别,但是,上电时的MMIO请求没有任何差别,只是,第一条请求一定是MMIO,并且不需要等待。

序号 功能名称 测试点名称 描述
11.1.1 IFU_MMIO_RESET FIRST_MMIO IFU收到的第一条MMIO请求可以直接查询Instr Uncache

11.2. 向InstrUncache发送请求

在正常的处理逻辑下,如果请求地址处于MMIO地址空间,则IFU会向FTQ查询指令提交状态,IFU需要等待当前请求之前的所有请求(包括MMIO和非MMIO)提交完成, 才能向InstrUncache模块发送请求。

这里需要和FTQ交互,可以让FTQ模拟请求提交情况,从而测试等待情况。 如果MMIO请求之前的请求都已经提交,则也不需要等待。反之,则需要一直等待直到查询结果表明前面的指令均已提交。

此外,对于属性为NC的内存区域,可以进行推测执行,无需等待前面的指令提交。

故设计测试点如下:

序号 功能名称 测试点名称 描述
11.2.1 IFU_MMIO_SEND_UNCACHE BLOCK IFU收到MMIO请求后,查询FTQ,如果前面还有尚未提交的指令,持续等待
11.2.2 IFU_MMIO_SEND_UNCACHE FREE 如果查到FTQ不再有未提交的指令,则IFU将指令发送给Instr Uncache
11.2.3 IFU_MMIO_SEND_UNCACHE NC 对于属性为NC的内存区域,无需等待前一条指令完成提交

11.3. 跨总线请求处理

由于MMIO不支持非对齐访问,因此当检测到的RVI指令地址[2,1]两位为b11时,64位总线无法一次传递所有指令,所以需要增加地址进行重发,再次查询ITLB。

序号 功能名称 测试点名称 描述
11.3.1 IFU_MMIO_RESEND_ITLB RESEND 遇到一次无法查询完毕的RVI指令时,需要向ITLB查询获得新增指令的物理地址

如果存在异常,则直接将指令和异常信息发送到IBuffer并等待,否则向PMP发送请求。

序号 功能名称 测试点名称 描述
11.3.2.1 IFU_MMIO_RESEND_ITLB EXCP IFU查询ITLB出现异常时,应当将异常信息发送到IBuffer,然后等待ROB提交完成
11.3.2.2 IFU_MMIO_RESEND_ITLB PADDR IFU查询ITLB正常返回物理地址时,IFU继续向PMP请求检查

根据pmp_recheck的结果,如果和上一次请求状态不一致,则说明存在访问错误, 为访问异常,不然则根据PMP的回复结果决定是否存在异常。如存在异常(访问异常和其他异常),则将报错信息发送给IBuffer并等待。如无异常,重新向InstrUncache模块 发送请求。

序号 功能名称 测试点名称 描述
11.3.3.1 IFU_MMIO_PMP_RECHECK STATUS_DIFF IFU检查PMP之后如果发现重发请求状态和上一条请求状态不一致,是访问异常,需要将异常直接发送到IBuffer
11.3.3.2 IFU_MMIO_PMP_RECHECK EXCP PMP检查出现异常的情况下,也需要将异常直接发送到IBuffer并等待ROB提交。
11.3.3.3 IFU_MMIO_PMP_RECHECK RESEND_UNCACHE PMP检查若无异常,则向Instr Uncache发送请求获取指令码的后半部分。

11.4. 向IBuffer发送指令

IFU获得完整数据之后,根据地址从64位数据中截取指令码,并以每个预测块一条指令的形式发送到Ibuffer。等待ROB返回指令已提交的信号。

序号 功能名称 测试点名称 描述
11.4 IFU_MMIO_TO_IBUFFER INSTR IFU在获得完整数据后,截取获得指令码,以每个预测块一条指令的形式发送给IBuffer

11.5. 指令冲刷

CFI指令的冲刷由后端发送给FTQ完成。所以只需要指令类型正确传达即可。

顺序指令由IFU复用前端重定向通路刷新流水线,并复用FTQ写回机制,将该指令当作误预测指令冲刷,重定向到+2或+4的位置。

+2和+4是由RVC和RVI指令决定的,所以设置测试点如下:

序号 功能名称 测试点名称 描述
11.5.1 IFU_MMIO_FLUSH_NON_CFI RVI 如果是RVI指令,传递给FTQ的冲刷请求应该重定向到PC+4
11.5.2 IFU_MMIO_FLUSH_NON_CFI RVC 如果是RVC指令,传递给FTQ的冲刷请求应该重定向到PC+2

12. Trigger实现对于PC的硬件断点功能

该工作主要由FrontEndTrigger子模块完成。本处先进行简单说明。

在 IFU 的 FrontendTrigger 模块里共 4 个 Trigger,编号为 0-3,每个 Trigger 的配置信息(断点类型、匹配地址等)保存在 tdata 寄存器中。

当软件向 CSR 寄存器 tselecttdata1/2 写入特定的值时,CSR 会向 IFU 发送 tUpdate 请求,更新 FrontendTrigger 内的 tdata 寄存器中的配置信息。目前前端的 Trigger 仅可以配置成 PC 断点(mcontrol.select 寄存器为 0;当 mcontrol.select=1 时,该 Trigger 将永远不会命中,且不会产生异常)。

在取指时,IFU 的 F3 流水级会向 FrontendTrigger 模块发起查询并在同一周期得到结果。后者会对取指块内每一条指令在每一个 Trigger 上做检查,当不处于 debug 模式时,指令的 PC 和 tdata2 寄存器内容的关系满足 mcontrol.match 位所指示的关系(香山支持 mcontrol.match 位为 0、2、3,对应等于、大于、小于)时,该指令会被标记为 Trigger 命中,随着执行在后端产生断点异常,进入 M-Mode 或调试模式。前端的 Trigger 支持 Chain 功能。当它们对应的 mcontrol.chain 位被置时,只有当该 Trigger 和编号在它后面一位的 Trigger 同时命中时,处理器才会产生异常3

FrontendTrigger的测试点可参照子模块文档,这里转录如下:

12.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位时,断点是否一定不会触发。

综上所述,我们在这一功能点设计的测试点如下:

序号 功能名称 测试点名称 描述
12.1.1 IFU_FRONTEND_TRIGGER SELECT1 给定tdata1的select位为1,随机构造其它输入,检查断点是否没有触发
12.1.2.1 IFU_FRONTEND_TRIGGER_SELECT0 MATCH 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位匹配的输入,检查断点是否触发
12.1.2.2 IFU_FRONTEND_TRIGGER_SELECT0 NOT_MATCH 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位不匹配的输入,检查断点是否触发

12.2. 链式断点

当某一个trigger的chain位被置后,当其后的trigger的chain位未设置,且两个trigger均命中时,后一个trigger才会触发。

对0号trigger,不需要考虑链式的情况

由此,我们可以设置几种测试点:

序号 功能名称 测试点名称 描述
12.2.1 IFU_FRONTEND_TRIGGER_CHAIN SELF 对每个trigger,在满足PC断点触发条件的情况下,设置chain位,检查断点是否一定不触发。
12.2.2.1 IFU_FRONTEND_TRIGGER_CHAIN NOT_HIT 对两个trigger,仅设置前一个trigger的chain位,设置后一个trigger命中而前一个未命中,检查后一个trigger是否一定不触发。
12.2.2.2 IFU_FRONTEND_TRIGGER_CHAIN HIT 对两个trigger,仅设置前一个trigger的chain位且均命中,检查后一个trigger是否触发。

IFU接口说明

为方便测试开展,需要对IFU的接口进行进一步的说明,以明确各个接口的含义。

FTQ交互接口

编译后可用的接口包括:

req FTQ取指请求

在f0流水级传入

req是FTQ向IFU的取指令请求,编译后包含以下成员:

接口名 解释
ftqIdx 指示当前预测块在FTQ中的位置。
ftqOffset 指示预测块的大小
startAddr 当前预测块的起始地址。
nextlineStart 起始地址所在cacheline的下一个cacheline的开始地址。
nextStartAddr 下一个预测块的起始地址

redirect FTQ重定向请求

在f0流水级传入

FTQ会向IFU发送重定向请求,这通过fromFtq.redirect完成,从而指示IFU应该冲刷的内容。

编译后,redirect包含以下接口成员:

接口名 解释
ftqIdx 需要冲刷的ftq预测块序号,包含flag和value两个量。
level 重定向等级
ftq_offset ftq预测块中跳转指令的位置

此外,还有valid变量指示是否需要重定向。

fromBPUFlush

在f0流水级传入

来自BPU的冲刷请求,这是预测错误引起的,包括s3和s2两个同构成员,指示是否在BPU的s3和s2流水级发现了问题,s3的详细结构如下

接口名 解释
valid 是否存在s3流水级冲刷要求
ftqIdx s3流水级请求冲刷的预测块的指针

toFtq_pdWb 写回

在WB阶段传出

接口名 解释
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自己已经准备好了,可以发送缓存行了。f0流水级就要设置。
icache_stop IFU在F3流水级之前出现了问题,通知ICache停下。

ICacheInter.resp ICache传送给IFU的信息

在f2流水级使用

接口名 解释
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重发请求时活跃(f3流水级用到)。

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 是否传导

接口时序

FTQ 请求接口时序示例

FTQ请求接口时序示例

上图示意了三个 FTQ 请求的示例,req1 只请求缓存行 line0,紧接着 req2 请求 line1 和 line2,当到 req3 时,由于指令缓存 SRAM 写优先,此时指令缓存的读请求 ready 被指低,req3 请求的 valid 和地址保持直到请求被接收。

ICache 返回接口以及到 Ibuffer 和写回 FTQ 接口时序示例

ICache返回接口以及到Ibuffer和写回FTQ接口时序示例

上图展示了指令缓存返回数据到 IFU 发现误预测直到 FTQ 发送正确地址的时序,group0 对应的请求在 f2 阶段了两个缓存行 line0 和 line1,下一拍 IFU 做误预测检查并同时把指令给 Ibuffer,但此时后端流水线阻塞导致 Ibuffer 满,Ibuffer 接收端的 ready 置低,goup0 相关信号保持直到请求被 Ibuffer 接收。但是 IFU 到 FTQ 的写回在 tio_toIbuffer_valid 有效的下一拍就拉高,因为此时请求已经无阻塞地进入 wb 阶段,这个阶段锁存的了 PredChecker 的检查结果,报告 group0 第 4(从 0 开始)个 2 字节位置对应的指令发生了错误预测,应该重定向到 vaddrA,之后经过 4 拍(冲刷和重新走预测器流水线),FTQ 重新发送给 IFU 以 vaddrA 为起始地址的预测块。

MMIO 请求接口时序示例

MMIO请求接口时序示例

上图展示了一个 MMIO 请求 req1 的取指令时序,首先 ICache 返回的 tlbExcp 信息报告了这是一条 MMIO 空间的指令(其他例外信号必须为低),过两拍 IFU 向 InstrUncache 发送请求,一段时间后收到响应和 32 位指令码,同拍 IFU 将这条指令作为一个预测块发送到 Ibuffer,同时发送对 FTQ 的写回,复用误预测信号端口,重定向地址为紧接着下一条指令的地址。此时 IFU 进入等待指令执行完成。一段时间后 rob_commits 端口报告此条指令执行完成,并且没有后端重定向。则 IFU 重新发起下一条 MMIO 指令的取指令请求。

测试点汇总

再次声明,本测试点仅供参考,如果有其他测试点需要补充可以告知我们。

建议覆盖点采用功能名称_测试点名称命名。

序号 功能名称 测试点名称 描述
1 IFU_RCV_REQ READY IFU接收FTQ请求后,设置ready
2.1.1 IFU_F1_INFOS PC IFU接收FTQ请求后,在F1流水级生成PC
2.1.2 IFU_F1_INFOS CUT_PTR IFU接收FTQ请求后,在F1流水级生成后续切取缓存行的指针
2.2.1 IFU_F2_INFOS EXCP_VEC IFU接收ICache内容后,会根据ICache的结果生成属于每个指令的异常向量
2.2.2 IFU_F2_INFOS PADDR IFU接收ICache内容后,会根据ICache的结果生成属于每个端口的物理地址。
2.2.3 IFU_F2_INFOS GPADDR IFU接收ICache内容后,会根据ICache的结果生成0号端口的客户物理地址。
2.2.4 IFU_F2_INFOS MMIO IFU接收ICache内容后,会根据ICache的结果判断当前取指请求是否属于MMIO空间。
2.3.1 IFU_INSTR_VALID_RANGE NORMAL IFU根据FTQ请求,计算无跳转指令有效范围
2.3.2 IFU_INSTR_VALID_RANGE JUMP IFU根据FTQ请求,计算跳转指令有效范围
2.3.3 IFU_INSTR_VALID_RANGE FINAL IFU综合两类指令有效范围,生成最终指令有效范围
2.4 IFU_INSTR_CUT CUT IFU根据上一流水级的切取指针,从缓存行提取初始指令码。
3.1.1 IFU_PREDECODE CONCAT 将生成的指令序列拼接成为16x4的指令码序列
3.2.1 IFU_PREDECODE_RVC RVC 传入RVC指令,应该判断为RVC
3.2.2 IFU_PREDECODE_RVC RVI 传入RVI指令,不应判断为RVC
3.3.1 IFU_PREDECODE_JMP_TGT RVC_J 对传入RVC扩展的J指令,检查计算的偏移
3.3.2 IFU_PREDECODE_JMP_TGT RVI_J 对传入RVI扩展的J指令,检查计算的偏移
3.3.3 IFU_PREDECODE_JMP_TGT RVC_BR 对传入RVC扩展的BR指令,检查计算的偏移
3.3.4 IFU_PREDECODE_JMP_TGT RVI_BR 对传入RVI扩展的BR指令,检查计算的偏移
3.4.1 IFU_PREDECODE_CFI_TYPE NON_CFI 对传入的非CFI指令(包括RVC.EBREAK),应该判定为类型0
3.4.2 IFU_PREDECODE_CFI_TYPE BR 对传入的BR指令,应该判定为类型1
3.4.3 IFU_PREDECODE_CFI_TYPE JAL 对传入的JAL指令,应该判定为类型2
3.4.4 IFU_PREDECODE_CFI_TYPE JALR 对传入的JALR指令,应该判定为类型3
3.5.1 IFU_PREDECODE_RET_CALL NON_CFI_BR 对传入的非CFI和BR指令,都不应判定为call或者ret
3.5.2.1.1 IFU_PREDECODE_RET_CALL RVI_JAL_CALL 对传入的RVI.JAL指令,当rd设置为1或5,应当判定该指令为call
3.5.2.1.2 IFU_PREDECODE_RET_CALL RVI_JAL_NOP 对传入的RVI.JAL指令,当rd设置为1和5之外的值,不应当判定该指令为call或ret
3.5.2.2 IFU_PREDECODE_RET_CALL RVC_JAL_NOP 对传入的RVC.JAL指令,无论什么情况都不能判定为call或ret
3.5.3.1.1 IFU_PREDECODE_RET_CALL RVI_JALR_CALL 传入RVI.JALR指令,并且rd为1或5,无论其他取值,都应判定为call
3.5.3.1.2 IFU_PREDECODE_RET_CALL RVI_JALR_RET 传入RVI.JALR指令,rd不为1和5,rs为1或5,应判定为ret
3.5.3.1.3 IFU_PREDECODE_RET_CALL RVI_JALR_NOP 对传入的JALR指令,若rd和rs均不为link,则不应判定为ret和call
3.5.3.2.1 IFU_PREDECODE_RET_CALL RVC_JALR_CALL 传入RVC.JALR指令,必定为call
3.5.3.2.2.1 IFU_PREDECODE_RET_CALL RVC_JR_RET 传入RVC.JR指令,rs为1或5,应判定为ret
3.5.3.2.2.2 IFU_PREDECODE_RET_CALL RVC_JR_NOP 传入RVC.JR指令,rs不为1或5,不应判定为ret
3.6.1 IFU_PREDECODE_VALID_STARTS LAST_IS_END 上一预测块的最后2字节恰为RVC指令或RVI指令的后半部分,按第一位为True推导有效开始向量
3.6.2 IFU_PREDECODE_VALID_STARTS LAST_NOT_END 上一预测块的最后2字节上一预测块的最后2字节为RVI指令的前半部分,按第一位为False推导有效开始向量
4.1.1 IFU_RVC_EXPAND VALID_RVC 对合法RVC指令,写扩展后的指令码,判断结果为合法指令
4.1.2 IFU_RVC_EXPAND INVALID_RVC 对非法RVC指令,写原始指令码,判断结果为非法指令
4.1.3 IFU_RVC_EXPAND RVI RVI指令直接写入原始指令即可,判断结果为合法指令
5.1.1.1 IFU_PRECHECK_JAL_MISS NOP 预测块中没有JAL指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JAL预测错误。
5.1.2.1 IFU_PRECHECK_JAL_MISS CORRECT 预测块中有JAL指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JAL预测错误。
5.1.2.1 IFU_PRECHECK_JAL_CHECK NO_SEL 预测块中存在JAL指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出JAL预测错误。
5.1.2.2 IFU_PRECHECK_JAL_CHECK SEL_LATE 预测块中存在JAL指令,但是BPU预测信息取的跳转指令在第一条JAL指令之后,检查PredChecker是否能检测出JAL预测错误。
5.2.1.1 IFU_PRECHECK_RET_MISS NOP 预测块中没有RET指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报RET预测错误。
5.2.2.1 IFU_PRECHECK_RET_MISS CORRECT 预测块中有RET指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报RET预测错误。
5.2.2.1 IFU_PRECHECK_RET_CHECK NO_SEL 预测块中存在RET指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
5.2.2.2 IFU_PRECHECK_RET_CHECK SEL_LATE 预测块中存在RET指令,但是BPU预测信息取的跳转指令在第一条RET指令之后,检查PredChecker是否能检测出RET预测错误。
5.3.2.2 IFU_PRECHECK_JALR_MISS NOP 预测块中没有JALR指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JALR预测错误。
5.3.2.2 IFU_PRECHECK_JALR_MISS CORRECT 预测块中有JALR指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JALR预测错误。
5.3.2.2 IFU_PRECHECK_JALR_CHECK NO_SEL 预测块中存在JALR指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
5.3.2.2 IFU_PRECHECK_JALR_CHECK SEL_LATE 预测块中存在JALR指令,但是BPU预测信息取的跳转指令在第一条JALR指令之后,检查PredChecker是否能检测出JALR预测错误。
5.4.1 IFU_PREDCHECK_FIX NOP 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。
5.4.2 IFU_PREDCHECK_FIX BIGGER_FIX 如果检测到了JAL、RET、JALR类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。
5.4.3 IFU_PREDCHECK_FIX SMALLER_NOP 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令(也即不能扩大范围),因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令
5.5.1.1 IFU_PREDCHECK_NON_CFI_MISS NOP 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.5.1.2 IFU_PREDCHECK_NON_CFI_MISS CORRECT 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.5.2 IFU_PREDCHECK_NON_CFI_CHECK ERROR 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误
5.6.1.1 IFU_PREDCHECK_INVALID_MISS NOP 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.6.1.2 IFU_PREDCHECK_INVALID_MISS INVALID_JMP 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.6.1.3 IFU_PREDCHECK_INVALID_MISS CORRECT 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.6.2 IFU_PREDCHECK_INVALID_MISS ERROR 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误
5.7.1.1 IFU_PREDCHECK_TARGET_MISS NOP 构造不存在跳转指令并且未预测跳转的预测信息作输入,测试PredChecker是否会错检目标地址预测错误
5.7.1.2 IFU_PREDCHECK_TARGET_MISS CORRECT 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
5.7.2 IFU_PREDCHECK_TARGET_CHECK ERROR 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误
5.8 IFU_PREDCHECK_TARGETS TARGETS 随机提供译码信息,检测生成的跳转目标和顺序目标。
6.1.1 IFU_REDIRECT JAL 预测请求中存在JAL预测错误,需要冲刷流水线
6.1.2 IFU_REDIRECT RET 预测请求中存在RET预测错误,需要冲刷流水线
6.1.3 IFU_REDIRECT JALR 预测请求中存在JALR预测错误,需要冲刷流水线
6.1.4 IFU_REDIRECT NON_CFI 预测请求中存在非CFI预测错误,需要冲刷流水线
6.1.5 IFU_REDIRECT INVALID 预测请求中存在无效指令预测错误,需要冲刷流水线
6.1.6 IFU_REDIRECT TARGET_FAULT 预测请求中存在跳转目标错误,需要冲刷流水线
7.1 IFU_CROSS_BLOCK NORMAL 连续传入两个预测块,其中有一条32位指令跨两个预测块,后一个预测块的指令开始向量的首位应该为False
7.2 IFU_CROSS_BLOCK ERROR 当IFU根据PredChecker修复的指令有效范围错判了跨预测块指令时,需要将F3以外的流水级全部冲刷
8.1.1 IFU_TO_IBUFFER INSTRS IFU向IBuffer传送扩展后的指令码
8.1.2 IFU_TO_IBUFFER EXCP IFU向IBuffer传送每个指令的异常信息
8.1.3 IFU_TO_IBUFFER PD_INFO IFU向IBuffer传递每个指令的预译码信息
8.1.4 IFU_TO_IBUFFER FTQ_PTR IFU向IBuffer传送FTQ预测块的指针
8.1.5 IFU_TO_IBUFFER FOLD_PC IFU向IBuffer传送折叠的PC
8.1.6 IFU_TO_IBUFFER VALID_STARTS IFU向IBuffer传送表示指令有效和指令是否为指令开始的向量
8.2.1 IFU_TO_GPAMEM GPADDR 客户页错误发生时,IFU应将gpaMem的valid拉高且填入gpaddr
9.1.1 IFU_OVERRIDE_FLUSH_CHECK BEFORE 当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。
9.1.2 IFU_OVERRIDE_FLUSH_CHECK NOT_BEFORE 当冲刷指令在取指令的指针相同或之后时,IFU不能对流水线进行冲刷。
9.2.1 IFU_OVERRIDE_FLUSH S2 当BPU的S2流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷
9.2.2 IFU_OVERRIDE_FLUSH S3 当BPU的S3流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷
10.1.1 IFU_WB_FTQ PCS IFU的WB流水级,需要向FTQ写回指令PC
10.1.2 IFU_WB_FTQ PD_INFO IFU的WB流水级,需要向FTQ写回每个指令的预译码信息
10.1.3 IFU_WB_FTQ ERR_POS IFU的WB流水级,需要向FTQ写回BPU错误预测的指令位置
10.1.4 IFU_WB_FTQ TARGET IFU的WB流水级,需要向FTQ写回该预测块的正确跳转地址
10.1.5 IFU_WB_FTQ RANGE IFU的WB流水级,需要向FTQ写回预测块的正确指令范围
10.1.6 IFU_WB_FTQ FTQ_PTR IFU的WB流水级,需要向FTQ传递预测块的FTQ指针
11.1.1 IFU_MMIO_RESET FIRST_MMIO IFU收到的第一条MMIO请求可以直接查询Instr Uncache
11.2.1 IFU_MMIO_SEND_UNCACHE BLOCK IFU收到MMIO请求后,查询FTQ,如果前面还有尚未提交的指令,持续等待
11.2.2 IFU_MMIO_SEND_UNCACHE FREE 如果查到FTQ不再有未提交的指令,则IFU将指令发送给Instr Uncache
11.2.3 IFU_MMIO_SEND_UNCACHE NC 对于属性为NC的内存区域,无需等待前一条指令完成提交
11.3.1 IFU_MMIO_RESEND_ITLB RESEND 遇到一次无法查询完毕的RVI指令时,需要向ITLB查询获得新增指令的物理地址
11.3.2.1 IFU_MMIO_RESEND_ITLB EXCP IFU查询ITLB出现异常时,应当将异常信息发送到IBuffer,然后等待ROB提交完成
11.3.2.2 IFU_MMIO_RESEND_ITLB PADDR IFU查询ITLB正常返回物理地址时,IFU继续向PMP请求检查
11.3.3.1 IFU_MMIO_PMP_RECHECK STATUS_DIFF IFU检查PMP之后如果发现重发请求状态和上一条请求状态不一致,是访问异常,需要将异常直接发送到IBuffer
11.3.3.2 IFU_MMIO_PMP_RECHECK EXCP PMP检查出现异常的情况下,也需要将异常直接发送到IBuffer并等待ROB提交。
11.3.3.3 IFU_MMIO_PMP_RECHECK RESEND_UNCACHE PMP检查若无异常,则向Instr Uncache发送请求获取指令码的后半部分。
11.4 IFU_MMIO_TO_IBUFFER INSTR IFU在获得完整数据后,截取获得指令码,以每个预测块一条指令的形式发送给IBuffer
11.5.1 IFU_MMIO_FLUSH_NON_CFI RVI 如果是RVI指令,传递给FTQ的冲刷请求应该重定向到PC+4
11.5.2 IFU_MMIO_FLUSH_NON_CFI RVC 如果是RVC指令,传递给FTQ的冲刷请求应该重定向到PC+2
12.1.1 IFU_FRONTEND_TRIGGER SELECT1 给定tdata1的select位为1,随机构造其它输入,检查断点是否没有触发
12.1.2.1 IFU_FRONTEND_TRIGGER_SELECT0 MATCH 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位匹配的输入,检查断点是否触发
12.1.2.2 IFU_FRONTEND_TRIGGER_SELECT0 NOT_MATCH 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位不匹配的输入,检查断点是否触发
12.2.1 IFU_FRONTEND_TRIGGER_CHAIN SELF 对每个trigger,在满足PC断点触发条件的情况下,设置chain位,检查断点是否一定不触发。
12.2.2.1 IFU_FRONTEND_TRIGGER_CHAIN NOT_HIT 对两个trigger,仅设置前一个trigger的chain位,设置后一个trigger命中而前一个未命中,检查后一个trigger是否一定不触发。
12.2.2.2 IFU_FRONTEND_TRIGGER_CHAIN HIT 对两个trigger,仅设置前一个trigger的chain位且均命中,检查后一个trigger是否触发。

  1. 需要特别指出的是,Svpbmt 扩展增加了一个 NC 属性,其代表该内存区域是不可缓存的、但是幂等的,这意味着我们可以对 NC 的区域进行推测执行,也就是不需要“等待前面的指令提交”就可以向总线发送取指请求,表现为状态机跳过等待状态。实现见 #3944。 ↩︎

  2. 截至本文档撰写的版本,这个功能尚未实现(不过在比较新的提交里已经实现了),后续新的rtl加入后会去掉该下划线,或者读者可以自行编译香山源码生成rtl以支持这一特性。 ↩︎

  3. 在过去(riscv-debug-spec-draft,对应 XiangShan 2024.10.05 合入的 PR#3693 前)的版本中,Chain 还需要满足两个 Trigger 的 mcontrol.timing 是相同的。而在新版(riscv-debug-spec-v1.0.0)中,mcontrol.timing 被移除。目前 XiangShan 的 scala 实现仍保留了这一位,但其值永远为 0 且不可写入,编译生成的 verilog 代码中没有这一位。参考:https://github.com/riscv/riscv-debug-spec/pull/807。 ↩︎

12.2.2.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寄存器),分别对应着压栈和弹栈。详细的对应情况如下:

links

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

12.2.2.2 - FrontendTrigger

FrontendTrigger子模块

该子模块的主要作用是在前端设置硬件断点和检查。

该模块的输入pc有一个隐含条件,那就是这个pc是通过ftq传递的startAddr计算出来的。

FrontendTrigger功能介绍

断点设置和断点检查

在IFU的FrontendTrigger模块里共4个Trigger,编号为0,1,2,3,每个Trigger的配置信息(断点类型、匹配地址等)保存在tdata寄存器中。

当软件向CSR寄存器tselecttdata1/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。

当它们对应的Chain位被置时,只有当该Trigger和编号在它后面一位的Trigger同时命中,且timing配置相同时(在最新的手册中,这一要求已被删除),处理器才会产生异常。

在过去(riscv-debug-spec-draft,对应 XiangShan 2024.10.05 合入的 PR#3693 前)的版本中,Chain 还需要满足两个 Trigger 的 mcontrol.timing 是相同的。而在新版(riscv-debug-spec-v1.0.0)中,mcontrol.timing 被移除。目前 XiangShan 的 scala 实现仍保留了这一位,但其值永远为 0 且不可写入,编译生成的 verilog 代码中没有这一位。

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位且两trigger的timing位相同,设置后一个trigger命中而前一个未命中,检查后一个trigger是否一定不触发。
2.2.2 命中测试 对两个trigger,仅设置前一个trigger的chain位且两trigger的timing位相同且均命中,检查后一个trigger是否触发。

测试点汇总

序号 功能 名称 描述
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是否触发

12.2.2.3 - PredChecker

子模块:PredChecker简介

分支预测检查器PredChecker接收来自IFU的预测块信息(包括预测跳转指令在预测块的位置、预测的跳转目标、预译码得到的指令信息、指令PC以及预译码得到的跳转目标偏移等),在模块内部检查五种类型的分支预测错误。模块内部分为两个流水线stage,分别输出信息,第一个stage输出给IFU的f3阶段,用于修正预测块的指令范围和预测结果。第二个stage输出给wb阶段,用于在发现分支预测错误时产生前端重定向以及写回给FTQ(Fetch Target Queue)正确的预测信息。

PredChecker功能介绍

JAL预测错误检查

jal指令预测错误的条件是,预测块中有一条有效jal指令(由预译码信息给出),但是要么这个预测块没有预测跳转,要么此预测块预测跳转的指令在这条jal指令之后(即这条jal指令没有被预测跳转)。

JALR预测错误检查

jalr指令预测错误的条件是,预测块中有一条有效jalr指令(由预译码信息给出),而且这个指令不是ret指令,但是要么这个预测块没有预测跳转,要么此预测块预测跳转的指令在这条jalr指令之后(即这条jalr指令没有被预测跳转)。

RET预测错误检查

ret指令预测错误的条件是,预测块中有一条有效ret指令(由预译码信息给出),但是要么这个预测块没有预测跳转,要么此预测块预测跳转的指令在这条ret指令之后(即这条ret指令没有被预测跳转)。

更新指令有效范围向量

PredChecker在检查出Jal/Ret/JALR指令预测错误时,需要重新生成指令有效范围向量,有效范围截取到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:来自BPU(分支预测单元)的预测信息,表示该预测块的跳转指令是否存在(valid),以及跳转指令的序号(bits)。

instrRange:来自PreDecode的预译码信息,对每条指令,表示该指令是否在预测块的有效指令范围内。

instrValid:来自PreDecode的预译码信息,表示的是对于每条32位的拼接指令,其是否为一条有效的指令(即低16位为一条RVC指令,或者整个32位为一条RVI指令)。

jumpOffset:来自PreDecode的预译码信息,如果某一指令为跳转指令,jumpOffset表示这个指令的跳转目标。

pc:指令的pc。

pds:来自PreDecode模块的预译码信息,包含指令的brType、是否为Ret(isRet)、是否为RVC指令(isRVC)。

target:来自BPU,下个预测块的开始地址。

输出接口

第一阶段输出

fixedRange:修复的指令有效范围向量,对每条指令i,fixedRange_i为真表示这条指令是否在当前预测块的有效指令范围内

fixedTaken:修复过后的CFI指令选取情况,对每条指令,fixedTaken_i为真表示这条指令是否是这个预测块的第一条CFI指令

第二阶段输出

fixedMissPred:对每条指令,PredChecker检查出的存在预测错误的情况,fixedMissPred_i为真表示这条指令存在预测错误

fixedTarget:对每条指令,给出修复过的下一条指令的位置(可以是常规的pc+2或+4,或者如果是跳转指令,给出跳转目标)。

jalTarget:对每条指令,给出跳转目标。

faultType:每条指令的错误类型,取指范围包含noFault,jalFault,retFault,targetFault,notCFIFault,invalidTaken,jalrFault,分别对应数字0~6

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 BPU预测信息的JALR预测错误检查

PredChecker会对传入的预测块进行JALR预测错误预检查并修正指令有效范围向量和新的预测结果。

和JAL/RET预测错误类似,我们也按照误检和正检来构造。

对于误检,我们设计如下的测试点:

序号 名称 描述
3.1.1 误检测试1 预测块中没有JALR指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JALR预测错误。
3.1.2 误检测试2 预测块中有JALR指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JALR预测错误。

对于JALR预测错误的正确检出,我们设计如下的测试点:

序号 名称 描述
3.2.1 存在JALR未预测 预测块中存在JALR指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
3.2.2 预测的跳转并非第一条 预测块中存在JALR指令,但是BPU预测信息取的跳转指令在第一条JALR指令之后,检查PredChecker是否能检测出JALR预测错误。

功能点4 更新指令有效范围向量和预测跳转的指令

PredChecker在检查出Jal/Ret/Jalr指令预测错误时,需要重新生成指令有效范围向量, 有效范围截取到Jal/Ret/Jalr指令的位置,之后的bit全部置为0。 同时,还需要根据每条指令的预译码信息和BPU的预测信息修复预测跳转的结果。

所以,根据功能要求,我们可以划分出三类情况,分别是预测的有效范围和取用的跳转指令正确的情况, 由于RET和JAL预测错误引起的有效范围偏大和错判非跳转指令和无效指令引起的有效范围偏小。

序号 名称 描述
4.1 有效范围无误 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。
4.2 RET、JAL、JALR预测错误引起的范围偏大 如果检测到了JAL、RET、JALR类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。
4.3 非CFI和无效指令引起的预测范围偏小 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令,因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令

功能点5 非CFI预测错误检查

非CFI预测错误的条件是被预测跳转的指令根据预译码信息显示不是一条CFI指令。

要检验这一功能,我们仍然按误检和正确检验来设计测试点:

序号 名称 描述
5.1.1 误检测试1 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.1.2 误检测试2 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.2 正确检测测试 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误

功能点6 无效指令预测错误检查

无效指令预测错误的条件是被预测的指令的位置根据预译码信息中的指令有效向量显示不是一条有效指令的开始。

要检验这一功能,我们按照误检和正确检测来设计测试点:

序号 名称 描述
6.1.1 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
6.1.2 误检测试2 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
6.1.3 误检测试3 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
6.2 正确检测测试 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误

功能点7 目标地址预测错误检查

目标地址预测错误的条件是,被预测的是一条有效的jal或者branch指令, 同时预测的跳转目标地址和由指令码计算得到的跳转目标不一致。

和先前的思路一样,我们仍然按误检和检出两类组织测试点:

序号 名称 描述
7.1.1 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
7.1.2 误检测试2 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
7.2 正确检测测试 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误

功能点8 生成跳转和顺序目标

PredChecker还需要负责生成跳转和顺序目标。

我们通过随机生成译码信息进行测试

序号 名称 描述
8.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.1 BPU预测信息的JALR预测错误检查 误检测试1 预测块中没有JALR指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JALR预测错误。
3.1.2 BPU预测信息的JALR预测错误检查 误检测试2 预测块中有JALR指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JALR预测错误。
3.2.1 BPU预测信息的JALR预测错误检查 存在JALR未预测 预测块中存在JALR指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
3.2.2 BPU预测信息的JALR预测错误检查 预测的跳转并非第一条 预测块中存在JALR指令,但是BPU预测信息取的跳转指令在第一条JALR指令之后,检查PredChecker是否能检测出JALR预测错误。
4.1 更新指令有效范围向量和预测跳转的指令 有效范围无误 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。
4.2 更新指令有效范围向量和预测跳转的指令 RET和JAL预测错误引起的范围偏大 如果检测到了JAL或RET类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。
4.3 更新指令有效范围向量和预测跳转的指令 范围偏小不修正 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令,因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令。
5.1.1 非CFI预测错误检查 误检测试1 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.1.2 非CFI预测错误检查 误检测试2 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
5.2 非CFI预测错误检查 正确检测测试 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误
6.1.1 无效指令预测错误检查 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
6.1.2 无效指令预测错误检查 误检测试2 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
6.1.3 无效指令预测错误检查 误检测试3 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
6.2 无效指令预测错误检查 正确检测测试 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误
7.1.1 目标地址预测错误检查 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作输入,测试PredChecker是否会错检目标地址预测错误
7.1.2 目标地址预测错误检查 误检测试2 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
7.2 目标地址预测错误检查 正确检测测试 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误
8.1 生成跳转和顺序目标 随机测试 随机提供译码信息,检测生成的跳转目标和顺序目标。

12.2.2.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

其中,决定指令是否为RVC的部分在于指令的[1, 0]两位,不为3的情况下都是RVC指令。

其余的指令性质判定功能(CFI类型、是否为call和ret)被时序优化到了F3PreDecoder中,不过也可以认为是PreDecoder的一部分,可以设置测试点进行测试

最后比较麻烦的是CFI指令的目标地址计算偏移,主要是对J和BR分支指令进行的计算,这需要综合RVI和RVC中jal和br指令的结构。 首先,是手册中对于C.J的描述

JOP

这里对imm立即数的注解是,立即数的每一位最后对应到的是偏移的哪一位。

所以,可以认为立即数是这么重组的:

instr(12) + instr(8) + instr(10, 9) + instr(6) + instr(7) + instr(2) + instr(11) +instr(5,3) + “0”

而RVI中,对于JAL指令,是这么定义的:

RVIJ

我们可以类似地计算立即数。

同样的,我们可以查询手册,参考BR类指令的立即数计算RVC和RVI指令对应的偏移。

RVIBR

RVCBR

PreDecode接口说明

输入接口

in_bits_data 17 x 2B的初始指令码,其中,每2个字节既可以代表一条RVC指令,也可以代表一个RVI指令的一半。

输出接口

instr:拼接后的 16 x 4B的初始指令码

jumpOffset:如果这条指令是跳转指令,则jumpOffset表示其跳转偏移

pd:每条指令预译码信息,包括valid、isRVC、brType、isRet、isCall。其中第0条指令的valid已经被优化了

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中。 可以设计测试点测试PreDecode对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指令,检查计算的偏移

参照F3PreDecoder的测试点,设计如下测试点:

序号 名称 描述
2.3.1 非CFI判定 对传入的非CFI指令(包括RVC.EBREAK),应该判定为类型0
2.3.2 BR判定 对传入的BR指令,应该判定为类型1
2.3.3 JAL判定 对传入的JAL指令,应该判定为类型2
2.3.4 JALR判定 对传入的JALR指令,应该判定为类型3
2.4.1 非CFI和BR不判定 对传入的非CFI和BR指令,都不应判定为call或者ret
2.4.2.1.1 RVI.JAL判定call 对传入的RVI.JAL指令,当rd设置为1或5,应当判定该指令为call
2.4.2.1.2 RVI.JAL例外 对传入的RVI.JAL指令,当rd设置为1和5之外的值,不应当判定该指令为call或ret
2.4.2.2 RVC.JAL不判定 对传入的RVC.JAL指令,无论什么情况都不能判定为call或ret
2.4.3.1.1 RVI.JALR和rd为link 传入RVI.JALR指令,并且rd为1或5,无论其他取值,都应判定为call
2.4.3.1.2 RVI.JALR且仅rs为link 传入RVI.JALR指令,rd不为1和5,rs为1或5,应判定为ret
2.4.3.1.3 RVI.JALR无link 对传入的JALR指令,若rd和rs均不为link,则不应判定为ret和cal
2.4.3.2.1 RVC.JALR为Ret 传入RVC.JALR指令,必定为call
2.4.3.2.2.1 RVC.JR且rs为link 传入RVC.JR指令,rs为1或5,应判定为ret
2.4.3.2.2.2 RVC.JR且rs不为link 传入RVC.JR指令,rs不为1或5,不应判定为ret

功能点3 生成指令开始向量

最后,预译码还需要生成两种指令开始向量:

序号 名称 描述
3.1 有效指令开始向量计算1 对预测块,假定第一条指令为一条有效指令的开始,对每条指令计算其是否为有效指令开始
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 CFI指令类型判定 非CFI判定 对传入的非CFI指令(包括RVC.EBREAK),应该判定为类型0
2.3.2 CFI指令类型判定 BR判定 对传入的BR指令,应该判定为类型1
2.3.3 CFI指令类型判定 JAL判定 对传入的JAL指令,应该判定为类型2
2.3.4 CFI指令类型判定 JALR判定 对传入的JALR指令,应该判定为类型3
2.4.1 ret、call判定 非CFI和BR不判定 对传入的非CFI和BR指令,都不应判定为call或者ret
2.4.2.1.1 ret、call判定 RVI.JAL判定call 对传入的RVI.JAL指令,当rd设置为1或5,应当判定该指令为call
2.4.2.1.2 ret、call判定 RVI.JAL例外 对传入的RVI.JAL指令,当rd设置为1和5之外的值,不应当判定该指令为call或ret
2.4.2.2 ret、call判定 RVC.JAL不判定 对传入的RVC.JAL指令,无论什么情况都不能判定为call或ret
2.4.3.1.1 ret、call判定 RVI.JALR和rd为link 传入RVI.JALR指令,并且rd为1或5,无论其他取值,都应判定为call
2.4.3.1.2 ret、call判定 RVI.JALR且仅rs为link 传入RVI.JALR指令,rd不为1和5,rs为1或5,应判定为ret
2.4.3.1.3 ret、call判定 RVI.JALR无link 对传入的JALR指令,若rd和rs均不为link,则不应判定为ret和cal
2.4.3.2.1 ret、call判定 RVC.JALR为Ret 传入RVC.JALR指令,必定为call
2.4.3.2.2.1 ret、call判定 RVC.JR且rs为link 传入RVC.JR指令,rs为1或5,应判定为ret
2.4.3.2.2.2 ret、call判定 RVC.JR且rs不为link 传入RVC.JR指令,rs不为1或5,不应判定为ret
3.1 计算有效指令开始向量 有效指令开始向量计算1 对预测块,假定第一条指令为一条有效指令的开始,对每条指令计算其是否为有效指令开始
3.2 计算有效指令开始向量 有效指令开始向量计算2 对预测块,假定第一条指令为一条有效指令的结束,对每条指令计算其是否为有效指令开始

12.2.2.5 - RVCExpander

子模块:RVCExpander简介

RVCExpander是IFU的子模块,负责对传入的指令进行指令扩展,并解码计算非法信息。

该模块接收的输入量是两个:一条RVC指令或者RVI指令;CSR对fs.status的使能情况。

输出量也是两个:输入指令对应的RVI指令;RVC指令是否非法。

指令扩展

如果是RVI指令,则无需扩展。

否则对RVC指令,按照手册的约定进行扩展。

非法指令判断

RVI指令永远判断为合法。

对于RVC指令的判定,详细内容参阅20240411的RISCV手册的26.8节表格列出的指令条件。

常量说明

常量名 常量值 解释
XLEN 64 通用寄存器位宽,决定指令扩展时使用rv32还是rv64还是rv128
fLen 64 香山支持d扩展,故为64

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 |

当立即数为0时,这一字段reserved

当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 |

12.2.3 - 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,包括 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 模块进行权限检查。

验证功能点列表及说明

此处将给出划分功能点及测试点的示例。如果您划分了新的功能点,请及时与我们沟通,我们会根据沟通结果修正功能点列表。测试点原则上可根据验证实际情况自行划分,此处仅给出示例。

功能点1:TLB接收请求

功能说明

TLB 应当正常接收来自 IFU 与 ICache 的取指令请求,查找自身页表并作出适当的反应:miss 情况下返回 miss 并同时向 PTW 发送遍历请求,hit 情况返回正确结果。验证时此处应关注 TLB 做出的反应,无需关注请求本身的多种情况。

测试点示例

No. 名称 说明
1.1 接收来自 ICache 请求(requestor0、1) ITLB 根据请求查找自身缓存 TLBuffer,返回 hit/miss 结果
1.2 接收来自 IFU 请求(requestor2) 注意此处为阻塞式访问,每次访问后若 miss 应当 reset 后再次访问
1.3 接收条件判断(requestor0、1) valid 信号
1.4 接受条件判断(requestor2) valid-ready 信号

功能点2:TLB miss 处理

功能说明

miss 情况下发送页表遍历请求,额外需要注意边界情况下的请求处理,保证 TLB 发送的 PTW 请求正确。

测试点示例

No. 名称 说明
2.1 返回 miss 结果 比对后发现 miss 并返回
2.2 发起 PTW req(同时检验 PTW req valid 0/1) 从端口 0/1 发起页表遍历请求
2.3 发起 PTW req(同时检验 PTW req valid-ready 2) 从端口 2 发起页表遍历请求
2.4 不同情况下发起 PTW req 改变 CSR(vsatp、hgatp),依然能够正常发送请求
2.5 PTW resp valid 信号有效 检验该信号是否正常
2.6 重填 nonStage 条目,之后能正确访问
2.7 重填 OnlyStage1 条目,之后能正确访问
2.8 重填 OnleStage2 条目,之后能正确访问
2.9 重填 allStage 条目,之后能正确访问

功能点3:TLB hit 处理

功能说明

hit 情况下返回查询到的物理地址。requestor2 应当结束阻塞。

测试点示例

No. 名称 说明
3.1 主机查询得到物理地址 paddr
3.2 虚拟机查询得到物理地址 gpaddr
3.3 虚拟机查询得到中间物理地址 IPA

功能点4:替换策略

功能说明

根据文档说明,香山的 ITLB 使用 PLRU 替换策略,具体实现时使用的是外部提供的库。验证时可自学 PLRU 算法,并设计合理策略。

测试点示例

No. 名称 说明
4.1 填满后持续重填随机次数 建议建立参考模型进行对比
4.2 随机 hit/miss 一段时间 建议建立参考模型进行对比

功能点5:TLB 缓存大小

功能说明

检验 TLB 是否能够支持理论最大(48*8)项页表条目的存储。注意 PLRU 替换策略将导致一定情况下不能填满,验证中无需考虑因为该策略导致的未填充满。

测试点示例

No. 名称 说明
5.1 顺序填充至满 检验最终能够存储的最大条目数,这将直接影响 TLB 加速取指的效率
5.2 乱序随机 模拟应用场景,记录并检验条目数

功能点6:TLB 压缩

功能说明

支持 TLB 压缩,具体可见文档。注意保证随机性。

测试点示例

No. 名称 说明
6.1 压缩 8 项条目 一个压缩条目内的 8 项页表项都可以正常 hit
6.2 全满压力测试 全部填满时可连续命中
6.3 idx 随机测试 检测对应 idx 信号是否有效

功能点7:刷新

功能说明

TLB 模块需要在进程切换等场景下频繁刷新,也需要接收定向刷新指令刷新指定条目。验证中要建立填入-刷新-检验miss情况的流程,建议自定义函数完成。注意页表属性 Global 的影响,自行制定合适的策略。

测试点示例

No. 名称 说明
7.1 SFENCE rs1=0 rs2=0 刷新全部条目
7.2 SFENCE rs1=0 rs2=1 刷新指定条目
7.3 SFENCE rs1=1 rs2=0 刷新指定地址空间
7.4 SFENCE rs1=1 rs2=1 刷新指定地址空间的指定条目
7.5 带 flushpipe 的 Sfence 清空流水线
7.6 SFENCE hv=1/hg=1 刷新虚拟机的条目
7.7 flushPipe0 清空流水线0
7.8 flushPipe1 清空流水线1
7.9 flushPipe2 清空流水线2
7.10 satp.changed 按一定策略刷新
7.11 vsatp.changed 按一定策略刷新
7.12 hgatp.changed 按一定策略刷新

功能点8:Reset

功能说明

保证正常复位。TLB 工作流程涉及多个周期,需保证在各个阶段中执行 reset 均能正常复位。

测试点示例

No. 名称 说明
8.1 Reset 复位 检查所有信号按预期复位
8.2 请求同时复位 检查所有信号按预期复位
8.3 resp 同时复位 检查所有信号按预期复位

功能点9:权限检查

功能说明

TLB 并不涉及复杂的页属性检查,仅涉及用户态/内核态的权限。由于 ITLB 存储的全部为指令页,页属性必须全部可执行。

测试点示例

No. 名称 说明
9.1 主机状态下(U/S)访问权限检查 U 只能访问 U=1,S 只能访问 U=0
9.2 虚拟机状态下(VU/VS)访问权限检查 VU 只能访问 U=1,VS 只能访问 U=0
9.3 权限切换时的行为 IT级别验证时报告权限切换时会出现一个信号异常,可重点关注
9.4 X=0 页面不可执行时的行为

功能点10:异常处理

功能说明

ITLB 在异常方面承担的主要职责就是上报。当下层模块报告 GPF 时,由于 ITLB 不存储中间物理地址,此时重填需要首先发送一个带 GetGPA 标志的重填,标志当前重填请求是请求的虚拟机物理地址,PTW 会把这个请求标志发送回来,带该标志的 PTW resp 不会被存入 TLBuffer。

测试点示例

No. 名称 说明
10.1 s1-pf 主机缺页异常
10.2 s1-af 主机访问权限异常
10.3 s2-gpf 虚拟机缺页异常
10.4 s2-gaf 虚拟机访问权限异常
10.5 getGPA 信号相关 验证中自行拆分

功能点11:隔离

功能说明

地址空间隔离,通过 asid、vmid 实现进程、虚拟机之间的隔离。

测试点示例

No. 名称 说明
11.1 进程间隔离
11.2 虚拟机间隔离
11.3 虚拟机的进程间隔离

功能点12:并行访问

功能说明

模块包含 3 个端口,其访问是可以同时接收的,但是TLB的查询必须按序。TLB会将接收的请求暂存,以队列形式处理,当然 requestor2 作为阻塞式访问不参与这个过程。在实际场景下,对与 TLB miss 的情况,ICache 会自行组织重新持续发送请求。

测试点示例

No. 名称 说明
12.1 同时 hit 三个预期会 hit 的请求同一拍进入 TLB
12.2 同时 miss 三个预期会 miss 的请求同一拍进入 TLB
12.3 随机顺序回填 模拟实际场景持续发送请求,并以随机顺序回填
12.4 发送请求同时回填请求的地址 在同一拍对同一个地址发送请求&回填

功能点13:大小页支持

功能说明

TLB 支持保存全部大小页,不同 level 的页面都应该可以存入 TLB 中。

测试点示例

No. 名称 说明
13.1 level=0
13.2 level=1
13.3 level=2
13.4 level=3

功能点14:时序

功能点说明

检验 TLB 时序,保证每拍的信号级别行为正确。

测试点示例

No. 名称 说明
14.1 请求命中时序(requestor0、1)
14.2 请求命中时序(requestor2)
14.3 请求miss时序(requestor0、1)
14.4 请求miss时序(requestor2)

12.2.3.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 寄存器的模式字段(如裸模式、Sv32Sv39 等)。
  • 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 的物理地址转换为 GPAGuest 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: requestor1GPA 响应信号。
  • 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: requestor2GPA 响应信号。
  • 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 信号。

12.2.3.2 - 功能详述

支持 SV48 分页机制

SV48Supervisor-mode Virtual Memory)是一种基于 RISC-V 的页表虚拟内存寻址模式,指定了 48 位虚拟地址空间的结构,支持 256TB 的虚拟内存地址空间。使用四级页表结构:

Sv48_vaddr

Sv48_paddr

Sv48_pagetable

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 用来指向下一级页表,那么它的 XWR 位应全为零。在手册中的要求如下:

XWR 示意图

RISC-V H 扩展即 Hypervisor 扩展,增加了对虚拟化和 hypervisor 模式的支持,将会允许虚拟机监控程序和虚拟机的管理程序,允许操作系统运行在虚拟机上,并可以通过 hypervisor 调度虚拟机的运行。在 hypervisor 下使用 SV48x4 寻址模式,支持四倍页表扩展。

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 传入 TLBppn 信号的位宽依然为 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 × valididx8 × pteidxs2xlateperm[6]g_perm[4]。为供以后使用 svpbmt 扩展,还存储了 pbmtg_pbmt 字段。

TLB 缓存条目

  • tag[34:0]

    • tag,用于匹配条目。来源于 VPN 的高 35 位,在匹配的过程中,输入一个 38 位的 VPN,通过将输入的 VPN 的前 35 位与 tag 比较找到对应的条目,可以看到在一个条目中存储了 PPN 的高位部分和 8ppn_low,之后将 VPN 的后三位作为索引,可以索引这 8ppn_low,即可将 ppnppn_low[vpn_low] 拼接得到物理页框号。
  • asid[15:0]

    • 地址空间标识符,用于区分不同的进程地址空间。
  • vmid[13:0]

    • 虚拟机标识符,用于区分不同的虚拟机。
  • level[1:0]

    • 指示页面的大小。04KB12MB21GB3512GB
  • 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]

    • 指示主机的权限以及异常信息,包括 pfafagux 六位。其中 pfpage fault)指示是否发生缺页异常;afaccess fault)指示是否发生地址错误等访问错误异常;aaccess)指示该表项是否最近被访问过,任何形式的访问(包括读、写、取指)均会将 a 位置 1,用于页面替换算法;gglobal)指示该条目指向的页面是否为全局页面;uuser)指示该条目指向的页面是否可以被用户模式访问,u 位为 1 说明可以被 UMode 访问,为 0 说明可以被 SMode 访问;x(执行)指示该条目指向的页面是否可执行,itlb 用于取指的加速,所有取出的条目必须是可执行的。
  • g_perm[3:0]

    • 指示虚拟机的权限以及异常信息,包括 gpfgafax 四位,虚拟机的 gu 两位不单独存储,与主机共用。一般情况下虚拟机对全局页、用户模式的处理与主机相同,而替换策略与访问权限控制可能不同,所以共用了 gu 而不共用 axgpfguest page fault)为虚拟机缺页异常,gafguest 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 支持多种页面大小,包括 4KB2MB1GB 页,在标准的设计中没有定义 512GB 的页,理论上可行,但目前并没有这样的需要,512GB 的页也无法加载进内存,因此标准不做要求。但是出于对完整性的考虑,香山中依然实现了对 512GB 大页的支持。

Sv48 大小页示意图

在一般的应用程序需求中,4KB 的页面足够满足日常的使用,可以存储较小的数据结构以及程序等,常用于大多数应用程序中。然而,有的程序可能会需要频繁访问大的数据结构或数据集,这时引入大页可以提升内存访问效率。每个大页覆盖的虚拟地址空间更大,可以显著减少页表条目的数量;在映射相同数量的内存时,所需的页表条目会大幅降低,这可以减少内存开销、减少页表查找频率,从而优化内存访问速度,尤其对频繁访问大块内存的应用,能够显著提升性能。大页通常包含连续的数据,可以提高命中率,更有效地利用缓存资源。

当然,由于大页覆盖的地址空间较大,可能导致内存碎片,而未被使用的大页空间无法被其他请求有效利用,也会浪费一定的内存资源。同时,管理不同大小的页面为内存管理带来了额外的复杂性。在混合使用小页和大页时,操作系统需要复杂的算法来优化内存分配和使用。现代操作系统通常采用混合使用大小页的模式以满足不同应用的不同需求。

在香山的 TLB 中,支持保存任意大小的页面,这是通过保存页面的 level 来实现的。根据不同的 level,可以决定最终生成物理地址的方法(index 为页内偏移,来源于 vaddr 的低 12 位;ppnppn_lowtag 来源于 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 进行匹配,找到对应的条目,这个条目中可以存储 8PTE,再根据 vaddr[14:12] 找到对应的 ppn_low,之后检查对应的 valididx 是否有效,如果有效说明 hit,将 ppn_lowppn_high 拼接得到 PPN,再与 vaddr[11:0] 拼接得到 paddr

香山 TLB 压缩示意图

在支持了 H 扩展后(见支持两阶段虚实地址翻译),TLB 压缩仅在 OnlyStage1noS2xlate 下启用,在其他情况下不启用。

支持 TLB 压缩后 TLB 中缓存的条目会有所变化(表中未提及的条目即没有变化):

是否压缩 tag ppn valididx pteidx ppn_low
38位 36位 不保存 不保存 不保存
35位 33位 8位 8位 8×3位

在支持了大小页的情况下,TLB 压缩在大页情况下(2MB/1GB/512GB)不启用,仅在查询结果为小页(4KB)情况下启用。大页在返回时会将 valididx8 位全部设置为 1,而由于大页的查询过程中只需要 PPN 的高位,大页下不使用 ppn_lowppn_low 的值在此时是未定义的。

支持 Hypervisor 扩展与两阶段虚实地址翻译

RISC-V 特权指令手册中定义了虚实地址的翻译过程:

  1. asatp.ppn × PAGESIZE,并设 i = LEVELS - 1。(对于 Sv48PAGESIZE = 2^{12}LEVELS = 4)此时,satp 寄存器必须处于活动状态,即有效的特权模式必须是 S 模式或 U 模式。

  2. pte 为地址 a + va.vpn[i] × PTESIZE 处的 PTE 值。(对于 Sv48PTESIZE = 8)如果访问 pte 违反了 PMAPMP 检查,则引发与原始访问类型相应的访问错误异常。

  3. 如果 pte.v = 0,或者 pte.r = 0pte.w = 1,或者 pte 中设置了任何为未来标准使用保留的位或编码,则停止并引发与原始访问类型相应的页面错误异常。

  4. 否则,PTE 是有效的。如果 pte.r = 1pte.x = 1,则转到步骤 5。否则,此 PTE 是指向下一级页面表的指针。设 i = i - 1。如果 i < 0,则停止并引发与原始访问类型相应的页面错误异常。否则,设 a = pte.ppn × PAGESIZE 并转到步骤 2。

  5. 找到了叶子 PTE。根据当前特权模式和 mstatus 寄存器的 SUMMXR 字段的值,确定请求的内存访问是否被 pte.rpte.wpte.xpte.u 位允许。如果不允许,则停止并引发与原始访问类型相应的页面错误异常。

  6. 如果 i > 0pte.ppn[i-1 : 0] = 0,则这是一个未对齐的大页;停止并引发与原始访问类型相应的页面错误异常。

  7. 如果 pte.a = 0,或者如果原始内存访问是存储且 pte.d = 0,则引发与原始访问类型相应的页面错误异常,或者执行以下操作:

    • 如果对 pte 的存储将违反 PMAPMP 检查,则引发与原始访问类型相应的访问错误异常。
    • 以原子方式执行以下步骤:
      • 比较 pte 与地址 a + va.vpn[i] × PTESIZE 处的 PTE 值。
      • 如果值匹配,将 pte.a 设为 1,并且如果原始内存访问是存储,还将 pte.d 设为 1
      • 如果比较失败,返回步骤 2。
  8. 翻译成功。翻译后的物理地址如下:

    • 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 进行地址翻译。ITLBDTLB 如果 miss,会通过 RepeaterL2TLB 发送请求。在目前设计中,前端取指和后端访存对 TLB 均采用非阻塞式访问,即一个请求 miss 后,会将请求 miss 的信息返回,请求来源调度重新发送 TLB 查询请求,直至命中。也就是说,TLB 本体是非阻塞的,可以向它连续发送请求,无论结果都可以在下一拍发送任意的请求,但是总体上由于前端的调度,体现为阻塞访问。

在支持了 H 扩展的前提下,香山的虚拟化地址翻译过程会经历两个阶段的地址转换,可以将它划分为 VS 阶段和 G 阶段。VS 阶段的地址转换由 vsatp 寄存器控制,其实与主机的 satp 寄存器非常相似。

VS 阶段:GVA 至 GPA

页表项(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,观察 PTEPPN 的位数为 44 位,不需要担心这个问题。44 位的 PPN38 位,前六位并没有清零要求,但是是被忽略的。

G 阶段:GPA 至 HPA

G 阶段的地址翻译则不同,由于支持了 SV48x4,其根页表被扩展为 1116KB,因此需要特别注意 hgatp 寄存器中存储的 PPN 应当对齐 16KB 页,在标准情况下 PPN 的最后两 bit 应当被固定为零,意味着 hgatp 寄存器应当指向一个 16KB 页的起始地址,以避免根页表在不同的小页面内。

在实际的实现中,地址的翻译并不是这样理想化的先查虚拟机的页表得到 GPA 再使用这个 GPA 查主机的页表得到 HPA。事实上的实现中,我们通过虚拟机的页表查到的下一级页表的物理地址是 GPA,并不能通过它访问到下一级页表,每次访问虚拟机的下一级页表都需要进行一次 GPAHPA 的转换。比如此时给定一个 GVA,之后在虚拟机的一级页表(根页表)中根据 GVA[2]11 bit)查找得到一个 PTE,这个 PTE 存储的是二级页表的 GPA,得到这个 GPA 并不能找到二级页表,因此需要将它转换为 HPA,也就是 G 阶段的地址翻译。依次查找直到找到最终需要的那个 HPA,共需要经历五次 G 阶段地址翻译,才能得到最终的 HPA

香山昆明湖架构 TLB 两阶段地址翻译过程

支持阻塞式与非阻塞式访问

阻塞式访问代表着 TLB 的端口同时仅支持一个请求,阻塞端口带 valid-ready 握手信号。在 TLB 准备好接收请求时,会将 ready1,由外部检测到 ready 后会发送请求。请求到达 TLBvalid1TLB 接收请求并将 ready0,不再接受新的请求。之后 TLB 会对请求进行匹配,查找结果,如果 miss 则发送 ptw 请求(同样为阻塞),等待直到 ptw 返回结果(物理地址或 pf 异常),然后 TLB 将结果保存并上报给请求方,再将 ready1

对于非阻塞式请求,仅带 valid 信号,每当 valid1TLB 即接受请求并在下一拍返回结果(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 请求。由于 TLBL2TLB 之间有比较长的物理距离,需要在中间加拍,这项工作由 repeator 完成。同时,repeator 还需要对 PTW 请求进行过滤,以避免 TLB 中出现重复项,因此也被称为 filter。目前香山中 TLB 发出的 PTW 请求的内容包含 VPNs2xlategetGPA 三个信号以及必要的控制信号:

  • VPN

    • 虚拟页框号,TLBmiss 之后会将 VPN 发送给 PTW 用于索引对应的物理页,PTW 会将叶子页表的 PPN 返回给 TLB,下次 TLB 查询的时候就可以找到该页并可以通过页内偏移找到物理地址。
  • s2xlate

    • 两阶段地址转换标志,指示当前的两阶段地址转换模式。TLB 中该标志将通过 vsatphgatp 寄存器的 mode 域进行判断:
    s2xlate vsatp.mode hgatp.mode
    0b00 0 0
    0b01 1 0
    0b10 0 1
    0b11 1 1
  • getGPA

    • 指示当前 PTW 请求是否为请求客户机物理地址。用于客户机缺页等情况的处理(详见支持发生 GPF 时重新发起请求部分)。

在支持了 TLB 压缩后,PTW 返回的结果主要包括 resp_validtag[33:0]asid[15:0]perm[6:0]level[1:0]ppn[35:0]addr_low[2:0]ppn_low[2:0] × 8valididx × 8pteidx × 8pfaf(各个信号的含义可见支持缓存映射条目部分)。TLB 接收到有效的 PTW resp 后即将这些条目存进自己的缓存中。

在支持了 H 扩展后,TLB 压缩仅在 noS2xlateonlyStage1 时启用,需要添加 s2xlate 信号指示两阶段地址转换的类型,并分开返回 s1s2。其中,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]levelgpfgaf。由于不考虑 TLB 压缩,tag 即为 38 位,来自 50 位虚拟地址的高 38 位。值得注意,在目前的香山昆明湖架构中,这里的 ppn 有效的位数仅有后 36 位,之所以 ppn 位宽为 38 位是出于优化的需要,香山 TLB 中通过 readResult 方法从 PTW 中读取信息,s1s2 阶段复用了 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 拓展后的 MMUPTW 返回的结构分为三部分,第一部分 s1 是原先设计中的 PtwSec-torResp,存储第一阶段翻译的页表,第二部分 s2HptwResp,存储第二阶段翻译的页表,第三部分是 s2xlate,代表这次 resp 的类型,仍然分为 noS2xlateallStageonlyStage1onlyStage2。如下图。其中 PtwSectorEntry 是采用了 TLB 压缩技术的 PtwEntry

PTW resp 结构示意图

支持回填条目与两阶段条目融合

参照支持缓存映射条目与支持读取 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 根据 s2tag 的低 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.levels2.level 中的较大值,此时需要注意,当 s1 阶段为大页,而 s2 阶段为小页的情况下,例如中间物理地址指向一个 2MB 页,而 s2 阶段转换的结果却是一个 4KB 页,在这种情况下,需要特殊处理,将 s1.tag 的高位(在此例子中为高 11+9+9=29 位)和 s2.tag 的低位(在此例子中为低 9 位)共 38 位合并存储到 tagpteidx 中,如果不足 38 位则在后面补 0(例如中间物理地址指向 1GB 页而 s2 阶段指向 2MB 页,此时 tag[34:0] = {s1.tag[34:15], s2.tag[17:9], 6'b0})。在这种情况(s1 大页 s2 小页)下 ppn 也需要处理后存储,根据 s2.levels2.ppns2.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,此时 TLBPTW 返回的结果存入 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_gpa1,并将请求的 vpn 填入到 need_gpa_vpn 中,同时将 need_gpa_refill0。当 PTW 返回结果的时候,TLBPTW resp 中的 vpn 提取出来与 need_gpa_vpn 进行比较,判断是否是对之前 getGPA 请求的回应。如果是,那么将 PTW resp 中的 s2 tag 填入到 need_gpa_gvpn 中并将 need_gpa_refill1,表示已经获取到需要的 gvpn。下一次 TLB 接收到相同请求时就可以通过 need_gpa_gvpn 得到 gpaddr,之后 TLB 会将 need_gpa0,但保留其它寄存器,因此下次其它的请求发生 gpf 时也可以再次使用相同的 need_gpa_vpn 找到 paddr 而无需再次发起 PTW 请求。

注意这里的 gvpn44 位的,这是由于客户机采用 56 位物理地址,为了维护 gpaddr 的完整性,所以在这里需要存储 44 位的 gvpn,但是事实上 gvpn 的前 6 位一定会是 0,否则说明第一阶段产生了错误的物理地址,会触发 gpf,在此时需要将错误信息保存在 mtval2/htval 寄存器中,因此需要完整的 gpaddr,正常情况下并不需要。(当发生页面错误时,mtval2 将被填充为生成错误的物理地址,帮助异常处理程序;htval 将被填充为导致异常的虚拟地址,帮助 hypervisor 识别问题)

如果发生了 redirect,即重定向(可能触发了跳转/分支指令等或发生异常),此时之前的指令可能不会再访问 TLBTLB 需要根据 robidx 跟踪请求来源,有选择性地刷新相关的寄存器(即上表中提到的)。目前香山昆明湖架构中尚未实现,而是通过在需要 redirect 的时候发送 flushPipe 指令来实现的,无论哪一个请求端口被刷新均会导致这些寄存器被刷新。

getGPA 标志并不用于判断指令是否是请求 gpaddrPTW 不需要关心请求是干什么的,只需要负责查找并返回结果;TLB 内会通过一系列寄存器的比较来判断。这个信号的作用在于防止 TLB 重填,每次 TLB 发送带 getGPA 标志的请求时,PTW 在返回时会将 getGPA 信号传递回 TLB,从而使 TLB 不进行重填,不存储此项 gpaddr

支持 PLRU 替换算法

LRULeast Recently Used)算法核心思想就是替换掉最近最少使用的页,也就是最长时间没有访问的页。LRU 算法将内存中的每个页组织成了一个链表的形式,如图所示:

LRU 算法示意图

链表有两端,一端是最近最少使用的页,可以称为 LRU 端,另一端是最近刚刚使用的页,即最近使用最频繁的页,称之为 MRUMost Recently Used)端。每次访问的时候如果命中,那么就将命中的页移动到 MRU 端,如果 miss 则触发缺页,此时需要加载页面。如果这时候内存已满,那么就需要进行页面替换,选择 LRU 端的页进行替换,并把新访问的页放在 MRU 端。这就是 LRU 替换算法,是 cache 替换的经典算法。

但是由于 LRU 需要为 cache 行维护一个链表数据结构,在多路组相联的 cache 行中需要为每一路配置链表并跟踪每一行的使用时间,LRU 算法有着巨大的开销。因此虽然 LRU 在页面替换中表现出色,也依然不常使用。

在香山的昆明湖架构中,TLB 采用 PLRUpseudo-LRU)替换算法,详细来说是 tree-based PLRU 算法。假设当前 Cachen 路组相联(n 一般是 2 的整数幂)的结构,那么需要定义 n-1 位用来进行二叉树索引,假设为 0 表示左,为 1 表示右,如图所示:

PLRU 二叉索引示意图

对目前的香山昆明湖架构来说,采用每路 48 cache 行的二路组相联结构下,PLRU 需要维护一个 48 项的链表和一个一级的二叉树(1 位),而采用 LRU 将需要维护一个 48 项的链表和 482 项的链表,有一定的开销优势,随着路数的增加,优势会更加明显;同时,对二叉树的维护成本也比链表更低。

当然,PLRU 多级二叉树的选择策略下并不能做到与 LRU 一样精确控制,每次二分地排除掉一半不一定能找到绝对 LRU 的条目。

支持 SFENCE.VMA 指令

SFENCE.VMA 指令(Supervisor Memory-Management Fence Instruction)是定义在 RISC-V 指令架构的指令:

SFECE.VMA 指令

在内存管理中,页表负责将虚拟地址映射到物理地址。当修改了页表后,这些修改不会自动在处理器的缓存中生效。为了确保后续的指令能使用更新后的页表,必须通过 SFENCE.VMA 指令来刷新这些缓存。此外,处理器在执行指令时,可能隐式地对内存管理数据结构进行读取和写入操作,但这些隐式操作和显式的内存操作通常是无序的。SFENCE.VMA 指令可以强制处理器将某些隐式操作在显式操作之前完成,从而确保操作的顺序性。

SFENCE.VMARISC-V 架构中的一条特权指令,用于刷新与地址翻译相关的本地硬件缓存,处理内存管理数据结构的同步,特别是当需要确保对这些数据结构的修改在不同的硬件组件之间保持一致时需要频繁使用该指令。SFENCE.VMA 只影响本地核心(hart),如果需要在多个核心之间同步,则需要核间中断等额外机制。虽然 SFENCE.VMA 指令对于维护一致性至关重要,但频繁调用可能会影响系统性能,因此,应根据实际需要合理使用,以平衡一致性和性能之间的关系。

SFENCE.VMA 的行为依赖于 rs1rs2,在 RISC-V 特权指令集中如下所述:

条件
- 如果 rs1=x0rs2=x0,栅栏会对所有地址空间的页面表的所有读写进行排序,并将所有地址翻译缓存条目标记为 invalid。
- 如果 rs1=x0rs2 不是 x0,栅栏会对指定的地址空间的页面表的所有读写进行排序,但不对全局映射进行排序。它还会失效与指定地址空间匹配的地址翻译缓存条目,但不包括全局映射的条目。
- 如果 rs1 不是 x0rs2=x0,栅栏会对所有地址空间的与 rs1 对应的虚拟地址的叶子页面表条目的读写进行排序,并失效包含该虚拟地址的所有叶子页面表条目的地址翻译缓存条目。
- 如果 rs1 不是 x0rs2 不是 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 指令

事实上,对 hvSFENCE Bundle 中的信号,用于刷新第一阶段地址转换的条目)和 hgSFENCE Bundle 中的信号,用于刷新第二阶段地址转换的条目)信号不为 0 的情况执行的指令并不是 SFENCE.VMA,而是 HFENCE.VVMAHFENCE.GVMA

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 相同的时候,简单来说就是仅对当前这一个虚拟机生效。rs1rs2 的功能与 SFENCE.VMA 相同。

HFENCE.GVMA 来说,rs1 指定的是客机的物理地址。由于主机采用 SV48 而虚拟机采用 SV48x4,客机物理地址比主机物理地址多两位,因此此时需要将 rs1 对应的客机物理地址右移两位。如果某一个虚拟机的地址翻译模式更改了,也即 hgatp.MODE 对某个 VMID 更改了,则必须使用 HFENCE.GVMA 指令,将 rs1 设为 0rs2 设为 0VMID 进行刷新。

在香山中,由于 TLB 本身不存储中间物理地址,也即 TLB 并不存储 VS 阶段转换出来的虚拟机物理地址,也无法单独提供 G 阶段地址转换请求。在 TLB 中存储的是两阶段地址翻译的最终结果,因此 HFENCE.VVMAHFENCE.GVMATLB 中作用相同,均为刷新掉两阶段地址翻译的结果。无论 hvhg 哪一个信号为 1 都将刷新两阶段的条目。

支持 SINVAL 扩展

RISC-V 特权指令集中定义了 Svinval 扩展(Supervisor Virtual Address Invalidation),在香山昆明湖架构实现了该扩展。Svinval 扩展的意义在于将 SFENCE.VMA 指令更加细化为 SFENCE.W.INVALSINVAL.VMASFENCE.INVAL.IR 三条指令(HFENCE.VVMAHFENCE.GVMA 同理)。

SINVAL.VMA 指令事实上与 SFENCE.VMA 指令的功能基本一致,只是添加了对 SFENCE.W.INVALSFENCE.INVAL.IR 两个指令的相互排序,可以理解为需要在两个指令中间进行。SFENCE.W.INVAL 指令用于确保当前 RISC-V hart 可见的任何先前存储在后续由同一个 hart 执行的 SINVAL.VMA 指令之前被重新排序。SFENCE.INVAL.IR 指令确保当前 hart 执行的任何先前 SINVAL.VMA 指令在后续隐式引用内存管理数据结构之前被排序。当由单个 hart 按顺序(不一定连续)执行 SFENCE.W.INVALSINVAL.VMASFENCE.INVAL.IR 时,可以相当于执行了 SFENCE.VMA 指令。

SINVAL.VMA

SFENCE.W.INVAL 和 SFENCE.INVAL.IR

支持软件更新 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。在软件处理异常后,操作系统会允许处理器再次访问页面,只有在页表得到更新且相关状态位(AD 位)被正确设置后,处理器才能继续进行后续的内存访问。

12.2.3.3 - 关键信号说明

相关 CSR 寄存器

val csr = Input(new TlbCsrBundle)

csr:包含 satpvsatphgatp 三个寄存器的信息以及一些权限信息。

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 中包含了 satpvsatphgatp 以及 priv 特权标志。其中 satpvsatp 通过 TlbSatpBundle 实现,包括 modeasidppnchanged 以及一个 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。其允许的值包含 089,如果是其它值应当触发 illegal instruction fault
    • 0: Bare 模式,不进行地址转换。
    • 8: SV39 模式,使用三级页表支持 39 位虚拟地址空间。
    • 9: SV48 模式,使用四级页表支持 48 位虚拟地址空间。
  • asid:地址空间标识符,用于区分不同进程,香山昆明湖架构使用的 SV48 中最大长度为 16
  • ppnPage Table Pointer,根页表的物理页框号,其位宽为 44 位,由物理地址右移 12 位得到。

SATP

VSATP

  • vsatp (Virtual Supervisor Address Translation and Protection) 是虚拟机中客体操作系统的地址转换寄存器,提供虚拟机的虚拟地址到中间物理地址(IPA)的转换。
  • mode:页表模式,控制虚拟地址的转换,模式值与 satp 中的类似。
  • asid:虚拟机内地址空间标识符。
  • ppn:虚拟机页表的物理基地址。

VSATP

HGATP

  • hgatp (Hypervisor Guest Address Translation and Protection) 是虚拟机监控程序(Hypervisor)的二级地址转换寄存器,用于将虚拟机的中间物理地址(IPA)转换为主机物理地址(HPA)。
  • mode:页表模式,如 SV39x4SV48x4,用于虚拟机的二级地址转换。
    • 0: Bare 模式,不进行二级地址转换。
    • 8: SV39x4 模式,即 39 位虚拟地址空间,允许四倍页表扩展。
    • 9: SV48x4 模式,即 48 位虚拟地址空间,允许四倍页表扩展。
  • vmid:虚拟机标识符,区分不同虚拟机。
  • ppn:二级页表的物理基地址。

HGATP

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 中的信息是否更改,一旦 ModeAsidVmid)更改则必须同步将 changed1TLB 在检测到 changed1 时将会执行刷新操作,刷新掉旧的 AsidVmid)的映射。

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 的三个 requestorrequestor0/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 个虚拟机,符合手册要求)

12.2.3.4 - 环境配置

WSL2+Ubuntu22.04+GTKWave(Windows用户推荐使用)

我们推荐 Windows10/11 用户通过 WSL2 进行开发,在此给出通过此方法进行环境配置的教程集锦,仅供参考。如环境安装过程中出现任何问题,欢迎在QQ群(群号:976081653)中提出,我们将尽力帮助解决。此页面将收集大家提出的所有环境配置相关问题并提供解决方案,欢迎随时向我们提问!

1、在 Windows 下安装 WSL2(Ubuntu22.04)

参考资源:

— 微软官方教程:如何使用 WSL 在 Windows 上安装 Linux

— 其它资源:安装WSL2和Ubuntu22.04版本

2、打开 WSL,换源

推荐使用清华源:清华大学开源软件镜像站-Ubuntu软件仓库

3、配置验证环境

请参照开放验证平台学习资源-快速开始-搭建验证环境配置环境。

以下是示例方法:

# 基本工具包
cd ~ && sudo apt-get update
sudo apt-get install -y build-essential cmake git wget curl lcov autoconf flex bison libgoogle-perftools-dev gcc python3.11 python3.11-dev python3.11-distutils python3-pip python-is-python3
rm -rf /var/lib/apt/lists/*
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1
curl -sS https://bootstrap.pypa.io/get-pip.py | python3.11
# verilator
git clone https://github.com/verilator/verilator.git
cd verilator
git checkout v4.218 # 4.218为最低需求版本,可自行查看并选择新版本
autoconf && ./configure && make -j$(nproc) && make install
cd .. && rm -rf verilator
# verible
curl -sS https://github.com/chipsalliance/verible/releases/download/v0.0-3946-g851d3ff4/verible-v0.0-3946-g851d3ff4-linux-static-x86_64.tar.gz -o /tmp/
tar -zxvf /tmp/verible-v0.0-3946-g851d3ff4-linux-static-x86_64.tar.gz -C /tmp/
copy /tmp/verible-v0.0-3946-g851d3ff4/bin/verible-* /usr/local/bin/
sudo chmod +x /usr/local/bin/verible-*
rm /tmp/verible-*
# pcre2
curl -sS https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.45/pcre2-10.45.tar.gz -o /tmp/
tar -zxvf /tmp/pcre2-10.45.tar.gz -C /tmp/
cd /tmp/pcre2-10.45
./configure --prefix=/usr/local && make -j$(nproc) && make install
rm -rf /tmp/pcre2* && cd ~
# swig 
# 注意不要使用 apt install swig,将会下载不符合最低要求的版本 4.0.2
curl -sS http://prdownloads.sourceforge.net/swig/swig-4.3.0.tar.gz -o /tmp/
tar -zxvf /tmp/swig-4.3.0.tar.gz -C /tmp/
cd /tmp/swig-4.3.0
./configure --prefix=/usr/local && make -j$(nproc) && make install
rm -rf /tmp/swig* && cd ~
# 更新本地包
apt-get update && apt-get -y upgrade
# picker
git clone https://github.com/XS-MLVP/picker.git --depth=1
cd picker
make init && make && make install
cd .. && rm -rf picker
# UnityChipForXiangShan
git clone https://github.com/XS-MLVP/UnityChipForXiangShan.git
cd UnityChipForXiangShan
pip3 install --no-cache-dir -r requirements.txt

4、使用 GTKWave 查看波形文件

使用重庆大学硬件综合设计实验文档-Windows原生GTKWave给出的方法,可以通过在WSL中输入 gtkwave.exe wave.fst 打开在 Windows 下安装的 GTKWave。请注意,gtkwave在使用中需要进入 fst 文件所在文件夹,否则会出现无法 initialize 的情况。

gtkwave.exe /out/{test_name}.fst

5、使用 VSCode 插件 Live Server 查看验证报告

成功安装插件Live Server后,打开文件列表,定位到 /out/report/2025*-itlb-doc-*/index.html 右键并选择 Open With Live Server,之后在浏览器中打开提示的端口(默认为//localhost:5500)即可。

docker一键部署方案(MAC用户可用)

我们提供了 MAC 可用的 docker 环境,已在 Docker Hub 发布,名称为 unitychip-env。安装 Docker Desktop 后在命令行使用以下命令即可获取并打开开发环境。需下载约 500MB 的镜像,展开后约占用 1GB 空间。

docker search unitychip-env
docker pull dingjunbi/unitychip-env && docker run unitychip-env
cd UnityChipForXiangShan && git pull

Docker Hub使用文档

Docker:docker 拉取镜像及查看pull下来的image在哪里

12.3 - Backend

后端模块验证文档

12.4 - Mem Block

访存模块验证文档

12.5 - Misc

其他模块验证文档

13 - 维护者

在提交 issue、pull request、discussion 时,如果指定对应模块的 maintainer 能更及时的得到响应。目前已有的维护人员如下(首字母排名):

验证工具:


主UT模块

子UT模块

*其他维护者陆续更新中

如果您对本项目感兴趣,欢迎申请成为本项目中的维护者。