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

返回本页常规视图.

进度概述

本项目旨在通过开源众包的方式对香山处理器的昆明湖架构进行单元(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. toffee。将在后面自动安装。也可按照快速开始中的提示手动安装最新版本。
  5. 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 - Frontend

前端模块验证文档

12.1.1 - IFU

IFU简介

IFU(Instruction Fetch Unit),取指令单元,负责从内存或ICache取出指令,经过预译码、扩展RVC和预检之后,将指令交给后续译码器进行进一步的译码。

IFU的子模块包括PreDecode,F3PreDecoder,RVCExpander,PredChecker和FrontendTrigger。

以下是IFU的架构简图:

framework

IFU功能介绍

XS_IFU流水级划分

香山的IFU一共分为5个阶段。

F0阶段:接收FTQ请求,同时告诉FTQ自己已经ready了

F1阶段:从FTQ请求中先计算出每个指令的pc,half_pc、cut_ptr(这是后续将icache返回的指令码进行切分的依据)

F2阶段:从icache获取响应数据(缓存行)并校验,提取出异常信息(包括页错误、访问错误、mmio信息);生成预测到的指令范围(但这并不是一个数字,而是一个用多位表示的bool数组,该位为1表示这一指令在预测块范围内);从缓存行中,利用上一阶段求出的cut_ptr切分出17×2的初步指令码,最后进行预译码和指令扩展。

F3阶段:这一阶段主要是对译码阶段的结果进行预检查,以及MMIO状态下的处理逻辑。

WB(写回)阶段:将预检查的结果写回FTQ,并向IBuffer写指令码和前端信息。

接收FTQ取指令请求(F0流水级)

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

指令切分产生初始指令码(F1、F2流水级)

F0流水级时,FTQ同时会向ICache发送取缓存行的指令。这是ICache在其S2流水级需要返回的,所以IFU在F2流水级才会得到ICache返回的缓存行。在此之前,IFU会在F1流水线先进行PC的计算,以及计算切分缓存行的指针。

进入F2流水级,IFU将会针对每个指令,取出对应的异常信息、物理地址、客户物理地址等。同时,根据FTQ的taken信息,IFU将会计算该预测块在无跳转和跳转发生情况下的有效指令范围。无跳转情况下的指令有效范围ftr_range即当前预测块从起始地址到下一个预测块的起始地址的差值。有跳转情况下的指令有效范围jump_range即当前预测块的起始地址到预测块中第一个跳转指令地址的差值。

最后,IFU需要从缓存行和上一流水级计算的指针,完成对17x2字节初始指令码的拼接。这里的拼接代码可能存在一些迷惑性

  val f2_cache_response_data = fromICache.map(_.bits.data)
  val f2_data_2_cacheline = Cat(f2_cache_response_data(0), f2_cache_response_data(0))

在调用cut之前,我们先是从ICache获取了缓存行(ICache返回的缓存行种类已经在ICache中进行了分类讨论,IFU中直接使用即可),然后将第0个缓存数据进行了拼接, 这一操作的原因来自于ICache中对数据的细粒度拆分:

fetch block可能跨缓存行,但是由于fetch block最大只有34B,如果将两个缓存行(2x64B)都传送给IFU则显得浪费,因此,fetch block的选择由ICache完成。

ICache返回给IFU的并不是直接的预测块,而是带有跨缓存行信息的64字节。

我们将每个缓存行分为8份,如下所示:

cacheline 0: |0-7|0-6|0-5|0-4|0-3|0-2|0-1|0-0|
cacheline 1: |1-7|1-6|1-5|1-4|1-3|1-2|1-1|1-0|

如果fetch block的起始位置为0-1,则必定不跨缓存行。

如果fetch block的起始位置为0-6,那么fetch block的位置为0-6~1-2,此时传送的缓存行结构如下:

cacheline 0: |0-7|0-6|xx|xx|xx|1-2|1-1|1-0|

由此,只要将该缓存行复制一遍,即可获得拼接后的fetch block。

综上所述,对这两种情况,我们都只需要把返回的cacheline复制一份拼接在一起,从中间截取就可以拿到数据。

预译码(F2流水级,主要由PreDecode模块完成)

在F2流水级,我们需要将上一步完成切分的指令码交给PreDecode子模块,他的作用主要有二:

其一是生成预译码信息,包括该指令是否是有效指令的开始、是否是RVC指令、是否是CFI指令、CFI指令类型(branch/jal/jalr/call/ret)、CFI指令的目标地址计算偏移等。输出的预译码信息中brType域的编码如下:

CFI指令类型 brType类型编码
非CFI 00
branch指令 01
jal指令 10
jalr指令 11

brType类型一览

其二是将初始指令码两两组合之后,得到16x4字节的指令码(从起始地址开始,2字节做地址递增,地址开始的4字节作为一条32位初始指令码)。

此外,预译码阶段还需要分类讨论,得出两种指令有效向量(起始指令是不是RVI指令的后半部分),并交给IFU进行判断,可以参阅后面的跨预测块32位指令处理部分

其他功能和详细内容参见PreDecode子模块的描述。

指令扩展(F3流水级)

这一部分将从PreDecode返回的16条指令码分别送交指令扩展器进行32位指令扩展(RVI保持不变, RVC指令根据手册的规定进行扩充)。

但是,如果指令非法,需要向IBuffer写入原始指令码。

预测错误预检查(F3流水级,主要由PreChecker子模块完成)

这一功能是为了将一些不依赖于执行结果的预测错误在早期就发现出来。这一阶段检查五类错误:

jal类型错误:预测块的范围内有jal指令,但是预测器没有对这条指令预测跳转;

ret类型错误:预测块的范围内有ret指令,但是预测器没有对这条指令预测跳转;

无效指令预测错误:预测器对一条无效的指令(不在预测块范围/是一条32位指令中间)进行了预测;

非CFI指令预测错误:预测器对一条有效但是不是CFI的指令进行了预测;

转移目标地址错误:预测器给出的转移目标地址不正确。

在预检查的最后将会修正之前预测的各个指令的跳转情况。同时,如果存在jal或者ret类型预测错误,还将修正fixedRange——这是指令有效范围向量,可以看作一个bool数组,其中某一位为1也就是对应的指令在这一范围内。

前端重定向(WB阶段)

如果在预测错误预检查的部分发现了上述的五类错误,那么需要在写回阶段产生一个前端重定向将F3以外的流水级进行冲刷, 从而让BPU能够从正确路径重新开始预测。

还有一种情况下需要冲刷流水线。在下一节中,如果误判了当前预测块的最后2B为RVI指令的上半部分,则也需要冲刷当前预测块F3之前的流水级。

跨预测块32位指令处理

因为预测块的长度有限制,因此存在一条RVI指令前后两字节分别在两个预测块的情况。IFU首先在第一个预测块里检查最后2字节是不是一条RVI指令的开始,如果是并且该预测块没有跳转,那么就设置一个标识寄存器f3_lastHalf_valid,告诉接下来的预测块含有后半条指令。在F2预译码时,会产生两种不同的指令有效向量:

预测块起始地址开始即为一条指令的开始,以这种方式根据后续指令是RVC还是RVI产生指令有效向量

预测块起始地址是一条RVI指令的中间,以起始地址 + 2位一条指令的开始产生有效向量

在F3,根据是否有跨预测块RVI标识来决定选用哪种作为最终的指令有效向量,如果f3_lastHalf_valid为高则选择后一种(即这个预测块第一个2字节不是指令的开始)。IFU所做的处理只是把这条指令算在第一个预测块里,而把第二个预测块的起始地址位置的2字节通过改变指令有效向量来无效掉。

将指令码和前端信息送入IBuffer(F3流水级)

F3流水级最终得到经过扩展的32位指令码(或者对于非法指令直接传递原始指令码),以及16条指令中每条指令的例外信息、 预译码信息、FTQ队列中的指针位置、其他后端需要的信息(比如经过折叠的PC)等。IFU除了常规的valid-ready控制信号外, 还会给IBuffer两个特殊的信号:一个是16位的io_toIbuffer_bits_valid(因为我们最后组合出来的指令也是16条, 所以这里每一位刚好也对应一个指令的状态,为1说明是一条指令的开始,为0则是说明是一条指令的中间),标识预测块里有效的指令。 另一个是16位的io_toIbuffer_bits_enqEnable,这个在io_toIbuffer_bits_valid的基础上与上了被修正过的预测块的指令范围fixedRange。 enqEnable为1表示这个2字节指令码是一条指令的开始且在预测块表示的指令范围内。

除此之外,异常信息也需要写给IBuffer。

注意一个特例:当且仅当发生guest page fault时,后端需要gpaddr信息,为了节省面积,gpaddr不走正常通路进入ibuffer, 而是随ftqPtr被发送到gpaMem,后端需要时从gpaMem读出。IFU需要保证gpf发生时通向gpaMem的valid拉高、gpaddr正确。

分支预测overriding冲刷流水线

当FTQ内未缓存足够预测块时,IFU可能直接使用简单分支预测器提供的预测地址进行取指,这种情况下,当精确预测器发现简单预测器错误时,需要通知IFU取消正在进行的取指请求。具体而言,当BPU的S2流水级发现错误时,需要冲刷IFU的F0流水级;当BPU的S3流水级发现错误时,需要冲刷IFU的F0/F1流水级(BPU的简单预测器在S1给出结果,最晚在S3进行overriding,因此IFU的F2/F3流水级一定是最好的预测,不需要冲刷;类似地,不存在BPU S2到IFU F1的冲刷)。

IFU在收到BPU发送的冲刷请求时,会将F0/F1流水级上取指请求的指针与BPU发送的冲刷请求的指针进行比较,若冲刷的指针在取指的指针之前,说明当前取指请求在错误的执行路径上,需要进行流水线冲刷;反之,IFU可以忽略BPU发送的这一冲刷请求。此外,比较的时候还需要注意flag的情况,flag是一个指示队列循环的指针,flag不同即在不同的“圈”上,此时反而是idx的值更小,ftqIdx才会更大。

指令信息和误预测信息写回FTQ(WB阶段)

在F3的下一级WB级,IFU将指令PC、预译码信息、错误预测指令的位置、正确的跳转地址以及预测块的正确指令范围等信息写回FTQ,同时传递该预测块的FTQ指针用以区分不同请求。

同时,正如前面提到的,IFU检测到预测错误时会进行前端冲刷,同样地,FTQ也需要据此进行冲刷,因此,这也是IFU写回错误信息的意义——可以辅助FTQ判断是否冲刷流水线。

MMIO处理逻辑

在处理器上电复位时,内存还没有准备好,此时需要从Flash中取指令执行。 这种情况下需要IFU向MMIO总线发送宽度为64位的请求从flash地址空间取指令执行。同时IFU禁止对MMIO总线的推测执行,即IFU需要等到每一条指令执行完成得到准确的下一条指令地址之后才继续向总线发送请求。

这之后,根据FTQ中的指令地址,决定是否MMIO取指令。

mmio_states

以上是MMIO状态机的简图。在一开始,处于m_idle状态,如果处于mmio请求的场景,则转换到 m_waitLastCmt,之后只要之前所有的指令都已完成提交——或者这一指令就是第一条指令, 则进入m_sendReq状态将请求发送到InstrUncache模块,发送完成后进入m_waitResp状态。 接收请求后,由于MMIO总线的带宽限制为64位, 因此存在一条指令一次MMIO请求不能取得完整指令码的情况(这是由于MMIO并不支持非对齐访问,具体地说,如果RVI指令的起始地址的[2, 1]两位为b11,则64位总线无法一次传递所有指令),所以需要增加地址进行重发,进入m_sendTLB状态, 再次查询ITLB,如果tlb的pbmt状态和上一条的存在差别,则为访问异常,综合TLB自身的异常结果和根据pbmt判定的访问异常,如果存在异常,则直接把指令和异常信息发到Ibuffer,进入等待,否则进入m_sendPMP状态,向PMP发送请求, 这里需要查看pmp_recheck的结果,如果该请求的mmio状态和上一条的mmio状态不一致,那么说明可能存在访问错误,置为访问异常,否则根据PMP的回复结果决定是否存在PMP异常。 如果存在异常,则将报错信息发送给Ibuffer,直接进入等待。 一切正常的话,进入m_resendReq状态,重新发送请求到InstrUncache模块。 无论是否重发,最后获得完整数据之后,根据地址从64位数据中截取指令码。并以每个预测块一条指令的形式(相当于只有起始地址的指令)发送到IBuffer。 接下来进入m_waitCommit状态等待,直到ROB返回指令已提交的信号即进入m_commited状态,CFI指令由后端发送给FTQ进行冲刷,而顺序指令则由IFU复用前端重定向通路刷新流水线, 同时复用FTQ写回机制,把它当作一条错误预测的指令进行冲刷,重定向到该指令地址 + 2或者+4(根据这条指令是RVI还是RVC选择)

对于跨缓存行预测块,他们的mmio和pbmt状态应当等同。不匹配的错误应当只在后一个缓存行报告。

此外,如果当前pbmt为nc,则会跳过waitLastCmt和waitCommit状态。因为这些内存空间是幂等的,所以可以进行推测性取指。

Trigger实现对于PC的硬件断点功能

该工作主要由FrontEndTrigger子模块完成。

IFU接口说明

为方便测试开展,需要对IFU的接口进行进一步的说明,以明确各个接口的含义。

FTQ交互接口

编译后可用的接口包括:

req FTQ取指请求

req是FTQ向IFU的取指令请求,编译后包含以下成员:

接口名 解释
ftqIdx 指示当前预测块在FTQ中的位置。
ftqOffset 指示预测块的大小
startAddr 当前预测块的起始地址。
nextlineStart 起始地址所在cacheline的下一个cacheline的开始地址。
nextStartAddr 下一个预测块的起始地址

redirect FTQ重定向请求

FTQ会向IFU发送重定向请求,这通过fromFtq.redirect完成,从而指示IFU应该冲刷的内容。

编译后,redirect包含以下接口成员:

接口名 解释
ftqIdx 需要冲刷的ftq预测块序号,包含flag和value两个量。
level 重定向等级
ftq_offset ftq预测块中跳转指令的位置

此外,还有valid变量指示是否需要重定向。

fromBPUFlush

来自IFU的冲刷请求,这是预测错误引起的,包括s3和s2两个同构成员,指示是否在BPU的s3和s2流水级发现了问题,s3的详细结构如下

接口名 解释
valid 是否存在s3流水级冲刷要求
ftqIdx s3流水级请求冲刷的预测块的指针

toFtq_pdWb 写回

接口名 解释
cfioffset 经由PredChecker修复的跳转指令的预测位置。但经过编译后,cfioffset的数值已经被优化了,只剩下了cfioffset_valid表示是否存在编译优化。
ftqIdx 表明预测块在FTQ中的位置,这条信息主要是对FTQ有用,要和FTQ传入的请求保持一致。
instrRange 可以看作是一个bool数组,表示该条指令是不是在这个预测块的有效指令范围内(第一条有效跳转指令之后的指令)。
jalTarget 表明该预测块跳转指令的跳转目标。
misOffset 表明错误预测的指令在预测块中的位置。
pc 预测块中所有指令的PC指针。
pd 每条指令的预测信息,包括CFI指令的类型、isCall、isRet和isRVC。
target 该预测块最后一条指令的下一条指令的pc。

ICache交互接口

控制信号

接口名 解释
icache_ready ICache通知IFU自己已经准备好了,可以发送缓存行了。
icache_stop IFU在F3流水级之前出现了问题,通知ICache停下。

ICacheInter.resp ICache传送给IFU的信息

接口名 解释
data ICache传送的缓存行。
doubleLine 指示ICache传来的预测块是否跨缓存行。
exception ICache向IFU报告每个缓存行上的异常情况,方便ICache生成每个指令的异常向量。
backendException ICache向IFU报告后端是否存在异常
gpaddr 客户页地址
isForVSnonLeafPTE 是否为非叶的PTE,这个数据最终会流向写回gpaddrMem的信号
itlb_pbmt ITLB基于客户页的内存类型,对MMIO状态有用
paddr 指令块的起始物理地址
vaddr 指令块起始虚拟地址、指令块起始物理地址
pmp_mmio 指示当前指令块是否在MMIO空间

性能相关接口

ICachePerf和perf,可以先不关注。

ITLBInter

该接口仅在MMIO状态下,IFU重发请求时活跃。

req IFU向ITLB发送的请求

这是IFU向ITLB发送的查询请求,只有一个量:bits_vaddr,传递需要让ITLB查询的虚拟地址。

resp ITLB返回给IFU的查询结果

这是ITLB返回给IFU的查询结果,包含如下接口:

接口名 解释
excp 指令的异常信息,包含三个量:访问异常指令af_instr、客户页错误指令gpf_instr、页错误指令pf_instr
gpaddr 客户页地址
isForVSnonLeafPTE 指示传入的是否是非叶PTE
paddr 指令物理地址
pbmt 指令的基于页的内存类型

UncacheInter

该接口在MMIO状态下活跃,负责接收IFU并返回指令码。

toUncache

这是IFU向Uncache发送的请求,除了ready和valid以外,还传送了一个48位数据,即需要获取的指令的物理地址。

fromUncache

这是Uncache给IFU的回复,除了valid以外,还传送一个32位数据,即指令码(可为RVC或RVI指令)

toIbuffer

IFU通过这个接口向Ibuffer写入取指结果。包含以下成员:

接口名 解释
backendException 是否存在后端异常
crossPageIPFFix 表示跨页异常向量
valid 和一般意义上的valid相区别,表示每条指令是否是合法指令的开始(RVI指令的上半条或者RVC指令)
enqable 对每条指令,其为valid并且在预测块的范围内
exceptionType 每个指令的异常类型
foldpc 压缩过后的pc
ftqOffset 指令是否在预测块范围中
ftqPtr ftq预测块在FTQ的位置
illegalInstr 这条指令是否为非法指令
instrs 拼接后的指令码
isLastInFtqEntry 判断该指令是否为这个预测块中最后一条有效指令的开始
pd 指令控制信息,包括CFI指令的类型和RVC指令的判定
triggered 指令是否触发前端的trigger

toBackend_gpaddrMem

这组接口在gpfault发生时使用,由IFU向gpaddrMem传递预测块指针和页错误地址。

接口名 解释
waddr 传递ftq指针
wdata.gpaddr 传递出错的客户页地址
wdata.isForVSnonLeafPTE 指示是否为非叶PTE
wen 类似valid,指示gpaddrMem存在gpfault需要处理

io_csr_fsIsOff

指示是否使能了fs.CSR,对非法指令的判断很关键。

rob_commits 来自ROB的提交信息

共分为8个相同结构的rob_commit,包含以下成员

接口名 解释
ftqIdx 预测块指针
ftqOffset 预测块的大小

pmp

和物理内存保护相关,在mmio状态下重发请求时使用。

req

IFU向pmp发起的请求,传递前一步从ITLB查询得到的物理地址。

resp

PMP给IFU的回复结果,包含以下成员

接口名 解释
mmio 在MMIO空间
instr 对指令的判断结果,当指令不可执行时,该值为true

mmio_commits

mmioFtqPtr

IFU传递给FTQ的idx,用于查询上一个预测块的MMIO状态

mmioLastCommit

上一个请求是MMIO请求

frontendTrigger

用于设置前端断点

包含以下成员:

debugMode

debug的模式

triggerCanRaiseBpExp

trigger是否可以引起断点异常

tEnableVec

信号数组,表示是否使能对应的trigger

tupdate

表示更新的断点信息,其中包含tdata和addr,addr是请求设置的断点idx。

tdata包括下列成员:

接口名 解释
matchType 断点匹配类型,等于、大于、小于
action 触发执行的动作
tdata2 触发端点的基准数值
select 是否选择
chain 是否传导

IFU功能点和测试点

功能点1 接收FTQ预测块请求

功能点1.1 F0流水级接收请求

向FTQ报告自己已ready。

所以,我们只需要在发送请求后检查和ftq相关的的ready情况即可。

序号 名称 描述
1 ready置位 IFU接收FTQ请求后,设置ready

功能点2 指令切分产生初始指令码

功能点2.1 F1流水级计算信息和切分指针

F1流水级也会计算PC。

同时还需要生成17位的切分指针(也就是从拼接后的缓存行切出初始指令码的idx数组,在昆明湖架构中,计算方式为拼接00和startAddr[5:1], 然后分别与0~16相加) 用于后续从缓存行提取初始指令码。

所以,首先我们需要检查F1流水级生成的PC的正确与否。如果可能,也需要检查一下切分指针的生成。

所以,可以总结出以下的测试点:

序号 名称 描述
2.1.1 PC生成 IFU接收FTQ请求后,在F1流水级生成PC
2.1.2 切取指针生成 IFU接收FTQ请求后,在F1流水级生成后续切取缓存行的指针

功能点2.2 F2流水级获取指令信息

包括获取异常信息、物理地址、客户物理地址、是否在MMIO空间等。

获取异常信息之后,还需要计算异常向量。ICache会为每个缓存行返回一个异常类型,只需要计算每个指令pc属于哪个缓存行, 然后将对应缓存行的异常类型赋给该位置即可。

所以,只需要分别检查几种指令信息即可。

序号 名称 描述
2.2.1 异常向量生成 IFU接收ICache内容后,会根据ICache的结果生成属于每个指令的异常向量
2.2.2 物理地址提取 IFU接收ICache内容后,会根据ICache的结果生成属于每个端口的物理地址。
2.2.3 客户物理地址提取 IFU接收ICache内容后,会根据ICache的结果生成0号端口的客户物理地址。
2.2.4 MMIO空间信息提取 IFU接收ICache内容后,会根据ICache的结果判断当前取指请求是否属于MMIO空间。

功能点2.3 F2流水级计算预测块有效指令范围

指令有效范围包括两种,无跳转和有跳转的

无跳转的指令有效范围为当前预测块从起始地址到下一个预测块的起始地址的所有指令。

有跳转的指令有效范围jump_range为当前预测块的起始地址到预测块中第一个跳转指令地址(包含第一个跳转指令地址)之间的所有指令。

最终的指令有效范围是两者相与的结果。

序号 名称 描述
2.3.1 无跳转指令有效范围生成 IFU根据FTQ请求,计算无跳转指令有效范围
2.3.2 有跳转指令有效范围生成 IFU根据FTQ请求,计算跳转指令有效范围

功能点2.4 提取初始指令码

IFU需要将ICache返回的缓存行复制一份并拼接。然后利用上一流水级计算的idx数组,从缓存行提取17x2字节的初始指令码。

序号 名称 描述
2.4 切取初始指令码 IFU根据上一流水级的切取指针,从缓存行提取初始指令码。

功能点3 预译码

多数的功能都由preDecoder子模块完成,因此这里只罗列由IFU本身需要完成的功能。

功能点3.1 F3流水级选取指令有效向量

由于存在跨缓存行的32位指令,IFU需要做的是,根据上一个预测块最后两字节是否为一条RVI指令的开始,从两种指令有效开始向量中,选择一种。

序号 名称 描述
3.1.1 上一预测块结尾为RVC或RVI下半部分 上一预测块的最后2字节恰为RVC指令或RVI指令的后半部分,选择第一位为True的有效开始向量
3.1.2 上一预测块结尾为RVI上半部分 上一预测块的最后2字节为RVI,选择第一位为False的有效开始向量

功能点4 指令扩展

将PreDecode返回的16条指令码分别送交指令扩展其进行32位指令扩展,RVI保持不变。RVC指令根据手册规定进行扩展。 如果指令非法,则需要将原始指令填写到CSR(控制状态寄存器)中。IFU自身仅仅控制最后填写的是哪种指令。

所以,我们对IFU模块,只关注最后写的指令是何种指令。注意,这里可以修改fsIsOff的入参,测试c.fp指令是否返回原始指令码

序号 名称 描述
4.1 合法RVC指令写扩展指令码 对合法RVC指令,写扩展后的指令码
4.2 非法RVC指令写原始指令码 对非法RVC指令,写原始指令码
4.3 RVI指令不扩展 RVI指令直接写入原始指令即可

功能点5 预测错误预检

主要由PredChecker子模块完成。测试点和PredChecker子模块的测试点类似,IFU没有额外的测试点。预检可以和重定向一起测试。

功能点6 前端重定向和流水线冲刷

功能点6.1 预测错误重定向

如果发现了预检阶段检出的错误,则需要产生前端重定向,将F3以外的流水级冲刷

只需要构造有预测错误的预测请求,检查冲刷情况即可。

序号 名称 描述
6.1.1 JAL预测错误冲刷 预测请求中存在JAL预测错误,需要冲刷流水线
6.1.2 RET预测错误冲刷 预测请求中存在RET预测错误,需要冲刷流水线
6.1.3 非CFI预测错误冲刷 预测请求中存在非CFI预测错误,需要冲刷流水线
6.1.4 无效指令预测错误冲刷 预测请求中存在无效指令预测错误,需要冲刷流水线
6.1.5 跳转目标错误冲刷 预测请求中存在跳转目标错误,需要冲刷流水线

功能点6.2 跨预测块32位指令处理

如果发现当前预测块的最后两个字节是一条RVI指令的开始,则设置一个标识f3_lastHalf_valid,告诉接下来的预测块含有后半条指令。

我们没有办法直接观察到这个标识,但是可以通过下一预测块来判定:

序号 名称 描述
6.2.1 跨预测块32位指令处理 连续传入两个预测块,其中有一条32位指令跨两个预测块,后一个预测块的指令开始向量的首位应该为False

但是,如果这一判断出现问题(比如当前预测块存在跳转),则需要进行流水线冲刷。

这一功能需要PredChecker子模块“配合”(仅仅通过外部IO的修改很难触发这个防御机制),实现起来比较麻烦,但是还是列举一个测试点:

序号 名称 描述
6.2.2 跨预测块指令误判 当IFU根据PredChecker修复的指令有效范围错判了跨预测块指令时,需要将F3以外的流水级全部冲刷

功能点7 将指令码和前端信息输出给IBuffer

功能点7.1 传送指令码和前端信息

传送给IBuffer的信息包括:经过扩展的32位指令码、16条指令中每条指令的例外信息、预译码信息、FTQ队列中的指针位置、其他后端需要的信息(经过折叠的PC)、 io_toIbuffer_bits_valid(表示指令是否是一条指令的开始)、io_toIbuffer_bits_enqEnable(前者与上被修正过的预测块指令范围, 从而还能表示指令是否在预测块表示的指令范围内)。

这里要做的只是确认这些信息是否正确传递

序号 名称 描述
7.1.1 指令码传送 IFU向IBuffer传送扩展后的指令码
7.1.2 异常信息传送 IFU向IBuffer传送每个指令的异常信息
7.1.3 预译码信息传送 IFU向IBuffer传递每个指令的预译码信息
7.1.4 FTQ指针传送 IFU向IBuffer传送FTQ预测块的指针
7.1.5 折叠PC传送 IFU向IBuffer传送折叠的PC
7.1.6 有效开始向量 IFU向IBuffer传送表示指令有效和指令是否为指令开始的向量

功能点7.2 客户页错误传送gpaddr信息

当且仅当发生guest page fault时,后端需要gpaddr信息,为了节省面积,gpaddr不走正常通路进入ibuffer, 而是随ftqPtr被发送到gpaMem,后端需要时从gpaMem读出。IFU需要保证gpf发生时通向gpaMem的valid拉高、gpaddr正确,同时还要传递预测块的ftqIdx(通过waddr传入)。

这里我们只需要确保在客户页错误发生时通向gpaMem的valid为高,且gpaddr正确填入。

序号 名称 描述
7.2 客户页错误 客户页错误发生时,IFU应将gpaMem的valid拉高且填入gpaddr

功能点8 分支预测冲刷流水线

当精确预测器发现简单预测器错误时,通知IFU取消正在进行的取指请求。

功能点8.1 核验指针

IFU收到BPU冲刷请求后,会将F0/F1流水级上取指令请求的指针比较,冲刷的指针在取指之前,即当前取指令请求在错误的执行路径上,才需要 冲刷IFU。

我们仍然需要从两个方向校验这个功能,即当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。 然而,当冲刷指令在取指令的指针之后时,则不能对流水线进行冲刷。

序号 名称 描述
8.1.1 错误执行路径 当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。
8.1.2 执行路径无误 当冲刷指令在取指令的指针之后时,IFU不能对流水线进行冲刷。

功能点8.2 BPU S2流水级发现错误

BPU的S2流水级发现错误时,需冲刷IFU的F0流水级

序号 名称 描述
测试点8.2 BPU S2流水级发现错误 当BPU的S2流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷

功能点8.3 BPU S3流水级发现错误

BPU的S3流水级发现错误时,需要冲刷IFU的F0和F1流水级

序号 名称 描述
8.3 BPU S3流水级发现错误 当BPU的S3流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0和F1流水级进行冲刷

功能点9 指令信息和误预测信息写回FTQ

功能点9.1 写回指令信息和误预测信息

将指令PC、预译码信息、错误预测指令的位置、正确的跳转地址以及预测块的正确指令范围等信息写回FTQ,并传递该预测块的FTQ指针。

序号 名称 描述
9.1.1 写回指令PC IFU的WB流水级,需要向FTQ写回指令PC
9.1.2 写回预译码信息 IFU的WB流水级,需要向FTQ写回每个指令的预译码信息
9.1.3 写回误预测指令位置 IFU的WB流水级,需要向FTQ写回BPU错误预测的指令位置
9.1.4 写回正确跳转地址 IFU的WB流水级,需要向FTQ写回该预测块的正确跳转地址
9.1.5 写回正确指令范围 IFU的WB流水级,需要向FTQ写回预测块的正确指令范围
9.1.6 传递预测块FTQ指针 IFU的WB流水级,需要向FTQ传递预测块的FTQ指针

功能点10 MMIO处理

功能点10.1 上电复位处理

处理器上电复位时,IFU需向MMIO总线发送宽度为64位的请求从flash地址空间取指令,并禁止对MMIO总线的推测执行。

上电的情况和正常情况其实没有任何区别,但是,上电时的MMIO请求没有任何差别,只是,第一条请求一定是MMIO,并且不需要等待。

序号 名称 描述
10.1 第一条MMIO指令 IFU收到的第一条MMIO请求可以直接查询Instr Uncache

功能点10.2 向InstrUncache发送请求

在正常的处理逻辑下,如果请求地址处于MMIO地址空间,则IFU会向FTQ查询指令提交状态,IFU需要等待当前请求之前的所有请求(包括MMIO和非MMIO)提交完成, 才能向InstrUncache模块发送请求。

这里需要和FTQ交互,可以让FTQ模拟请求提交情况,从而测试等待情况。 如果MMIO请求之前的请求都已经提交,则也不需要等待。反之,则需要一直等待直到查询结果表明前面的指令均已提交。故设计测试点如下:

序号 名称 描述
10.2.1 阻塞等待提交 IFU收到MMIO请求后,查询FTQ,如果前面还有尚未提交的指令,持续等待
10.2.2 无阻塞发送请求 如果查到FTQ不再有未提交的指令,则IFU将指令发送给Instr Uncache

功能点10.3 跨总线请求处理

由于MMIO不支持非对齐访问,因此当检测到的RVI指令地址[2,1]两位为b11时,64位总线无法一次传递所有指令,所以需要增加地址进行重发,再次查询ITLB。

序号 名称 描述
10.3.1 重发查询ITLB 遇到一次无法查询完毕的RVI指令时,需要向ITLB查询获得新增指令的物理地址

如果存在异常,则直接将指令和异常信息发送到IBuffer并等待,否则向PMP发送请求。

序号 名称 描述
10.3.2.1 ITLB异常 IFU查询ITLB出现异常时,应当将异常信息发送到IBuffer,然后等待ROB提交完成
10.3.2.2 ITLB返回物理地址 IFU查询ITLB正常返回物理地址时,IFU继续向PMP请求检查

根据pmp_recheck的结果,如果和上一次请求状态不一致,则说明存在访问错误, 为访问异常,不然则根据PMP的回复结果决定是否存在异常。如存在异常(访问异常和其他异常),则将报错信息发送给IBuffer并等待。如无异常,重新向InstrUncache模块 发送请求。

序号 名称 描述
10.3.3.1 请求状态不一致 IFU检查PMP之后如果发现重发请求状态和上一条请求状态不一致,是访问异常,需要将异常直接发送到IBuffer
10.3.3.2 PMP检查异常 PMP检查出现异常的情况下,也需要将异常直接发送到IBuffer并等待ROB提交。
10.3.3.3 Instr Cache请求重发 PMP检查若无异常,则向Instr Uncache发送请求获取指令码的后半部分。

功能点10.4 向IBuffer发送指令

IFU获得完整数据之后,根据地址从64位数据中截取指令码,并以每个预测块一条指令的形式发送到Ibuffer。等待ROB返回指令已提交的信号。

序号 名称 描述
10.4 向IBuffer发送指令 IFU在获得完整数据后,截取获得指令码,以每个预测块一条指令的形式发送给IBuffer

功能点10.5 指令冲刷

CFI指令的冲刷由后端发送给FTQ完成。所以不需要在这里设置测试点。

顺序指令由IFU复用前端重定向通路刷新流水线,并复用FTQ写回机制,将该指令当作误预测指令冲刷,重定向到+2或+4的位置。

+2和+4是由RVC和RVI指令决定的,所以设置测试点如下:

序号 名称 描述
10.5.1 RVI指令重定向 如果是RVI指令,传递给FTQ的冲刷请求应该重定向到PC+4
10.5.2 RVC指令重定向 如果是RVC指令,传递给FTQ的冲刷请求应该重定向到PC+2

功能点11 硬件断点

该功能主要由FrontEndTrigger子模块完成。不需要为这一功能额外设置测试点(参照FrontendTrigger的测试点即可)

测试点汇总

序号 功能 名称 描述
1 接收FTQ预测块请求 ready置位 IFU接收FTQ请求后,设置ready
2.1.1 F1流水级计算信息和切分指针 PC生成 IFU接收FTQ请求后,在F1流水级生成PC
2.1.2 F1流水级计算信息和切分指针 切取指针生成 IFU接收FTQ请求后,在F1流水级生成后续切取缓存行的指针
2.2.1 F2流水级获取指令信息 异常向量生成 IFU接收ICache内容后,会根据ICache的结果生成属于每个指令的异常向量
2.2.2 F2流水级获取指令信息 物理地址提取 IFU接收ICache内容后,会根据ICache的结果生成属于每个端口的物理地址。
2.2.3 F2流水级获取指令信息 客户物理地址提取 IFU接收ICache内容后,会根据ICache的结果生成0号端口的客户物理地址。
2.2.4 F2流水级获取指令信息 MMIO空间信息提取 IFU接收ICache内容后,会根据ICache的结果判断当前取指请求是否属于MMIO空间。
2.3.1 F2流水级计算预测块有效指令范围 无跳转指令有效范围生成 IFU根据FTQ请求,计算无跳转指令有效范围
2.3.2 F2流水级计算预测块有效指令范围 有跳转指令有效范围生成 IFU根据FTQ请求,计算跳转指令有效范围
2.4 提取初始指令码 切取初始指令码 IFU根据上一流水级的切取指针,从缓存行提取初始指令码。
3.1.1 F3流水级选取指令有效向量 上一预测块结尾为RVC或RVI下半部分 上一预测块的最后2字节恰为RVC指令或RVI指令的后半部分,选择第一位为True的有效开始向量
3.1.2 F3流水级选取指令有效向量 上一预测块结尾为RVI上半部分 上一预测块的最后2字节为RVI,选择第一位为False的有效开始向量
4.1 指令扩展 合法RVC指令写扩展指令码 对合法RVC指令,写扩展后的指令码
4.2 指令扩展 非法RVC指令写原始指令码 对非法RVC指令,写原始指令码
4.3 指令扩展 RVI指令不扩展 RVI指令直接写入原始指令即可
6.1.1 预测错误重定向 JAL预测错误冲刷 预测请求中存在JAL预测错误,需要冲刷流水线
6.1.2 预测错误重定向 RET预测错误冲刷 预测请求中存在RET预测错误,需要冲刷流水线
6.1.3 预测错误重定向 非CFI预测错误冲刷 预测请求中存在非CFI预测错误,需要冲刷流水线
6.1.4 预测错误重定向 无效指令预测错误冲刷 预测请求中存在无效指令预测错误,需要冲刷流水线
6.1.5 预测错误重定向 跳转目标错误冲刷 预测请求中存在跳转目标错误,需要冲刷流水线
6.2.1 跨预测块32位指令处理 跨预测块32位指令处理 连续传入两个预测块,其中有一条32位指令跨两个预测块,后一个预测块的指令开始向量的首位应该为False
6.2.2 跨预测块32位指令处理 跨预测块指令误判 当IFU根据PredChecker修复的指令有效范围错判了跨预测块指令时,需要将F3以外的流水级全部冲刷
7.1.1 传送指令码和前端信息 指令码传送 IFU向IBuffer传送扩展后的指令码
7.1.2 传送指令码和前端信息 异常信息传送 IFU向IBuffer传送每个指令的异常信息
7.1.3 传送指令码和前端信息 预译码信息传送 IFU向IBuffer传递每个指令的预译码信息
7.1.4 传送指令码和前端信息 FTQ指针传送 IFU向IBuffer传送FTQ预测块的指针
7.1.5 传送指令码和前端信息 折叠PC传送 IFU向IBuffer传送折叠的PC
7.1.6 传送指令码和前端信息 有效开始向量 IFU向IBuffer传送表示指令有效和指令是否为指令开始的向量
7.2 客户页错误传送gpaddr信息 客户页错误 客户页错误发生时,IFU应将gpaMem的valid拉高且填入gpaddr
8.1.1 核验指针 错误执行路径 当冲刷指针在取指令的指针之前时,IFU能够对流水线进行冲刷。
8.1.2 核验指针 执行路径无误 当冲刷指令在取指令的指针之后时,IFU不能对流水线进行冲刷。
8.2 BPU S2流水级发现错误 BPU S2流水级发现错误 当BPU的S2流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0流水级进行冲刷
8.3 BPU S2流水级发现错误 BPU S3流水级发现错误 当BPU的S3流水级出现错误,并且当前取指指针在错误执行路径上时,需要对IFU的F0和F1流水级进行冲刷
9.1.1 写回指令信息和误预测信息 写回指令PC IFU的WB流水级,需要向FTQ写回指令PC
9.1.2 写回指令信息和误预测信息 写回预译码信息 IFU的WB流水级,需要向FTQ写回每个指令的预译码信息
9.1.3 写回指令信息和误预测信息 写回误预测指令位置 IFU的WB流水级,需要向FTQ写回BPU错误预测的指令位置
9.1.4 写回指令信息和误预测信息 写回正确跳转地址 IFU的WB流水级,需要向FTQ写回该预测块的正确跳转地址
9.1.5 写回指令信息和误预测信息 写回指令范围 IFU的WB流水级,需要向FTQ写回预测块的正确指令范围
9.1.6 写回指令信息和误预测信息 传递预测块FTQ指针 IFU的WB流水级,需要向FTQ传递预测块的FTQ指针
10.1 上电复位处理 第一条MMIO指令 IFU收到的第一条MMIO请求可以直接查询Instr Uncache
10.2.1 向InstrUncache发送请求 阻塞等待提交 IFU收到MMIO请求后,查询FTQ,如果前面还有尚未提交的指令,持续等待
10.2.2 向InstrUncache发送请求 无阻塞发送请求 如果查到FTQ不再有未提交的指令,则IFU将指令发送给Instr Uncache
10.3.1 跨总线请求处理 重发查询ITLB 遇到一次无法查询完毕的RVI指令时,需要向ITLB查询获得新增指令的物理地址
10.3.2.1 跨总线请求处理 ITLB异常 IFU查询ITLB出现异常时,应当将异常信息发送到IBuffer,然后等待ROB提交完成
10.3.2.2 跨总线请求处理 ITLB返回物理地址 IFU查询ITLB正常返回物理地址时,IFU继续向PMP请求检查
10.3.3.1 跨总线请求处理 请求状态不一致 IFU检查PMP之后如果发现重发请求状态和上一条请求状态不一致,是访问异常,需要将异常直接发送到IBuffer
10.3.3.2 跨总线请求处理 PMP检查异常 PMP检查出现异常的情况下,也需要将异常直接发送到IBuffer并等待ROB提交。
10.3.3.3 跨总线请求处理 Instr Cache请求重发 PMP检查若无异常,则向Instr Uncache发送请求获取指令码的后半部分。
10.4 向IBuffer发送指令 向IBuffer发送指令 IFU在获得完整数据后,截取获得指令码,以每个预测块一条指令的形式发送给IBuffer
10.5.1 指令冲刷 RVI指令重定向 如果是RVI指令,传递给FTQ的冲刷请求应该重定向到PC+4
10.5.2 指令冲刷 RVC指令重定向 如果是RVC指令,传递给FTQ的冲刷请求应该重定向到PC+2
11.1.1 断点设置和检查 select1判定 给定tdata1的select位为1,随机构造其它输入,检查断点是否没有触发
11.1.2.1 断点设置和检查 select0关系匹配判定 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位匹配的输入,检查断点是否触发
11.1.2.2 断点设置和检查 select0关系不匹配判定 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位不匹配的输入,检查断点是否触发
11.2.1 链式断点 chain位测试 对每个trigger,在满足PC断点触发条件的情况下,设置chain位,检查断点是否一定不触发
11.2.2 链式断点 timing测试 对两个trigger,仅设置前一个trigger的chain位,且两trigger的timing位不同,随机设置PC等,测试后一个trigger是否一定不触发
11.2.3.1 链式断点 未命中测试 对两个trigger,仅设置前一个trigger的chain位,且两trigger的timing位相同,设置后一个trigger命中而前一个未命中,检查后一个trigger是否一定不触发
11.2.3.2 链式断点 命中测试 对两个trigger,仅设置前一个trigger的chain位,且两trigger的timing位相同且均命中,检查后一个trigger是否触发

12.1.1.1 - F3PreDecoder

子模块:F3PreDecoder模块简介

这个模块是从PreDecoder中时序优化出来的,负责判定CFI指令的类型

F3PreDecoder功能介绍

CFI指令类型判定

要想确定CFI指令类型,只需要分别尝试匹配JAL、JALR、BR和他们的RVC版本即可,注意,RVC的EBREAK 不应该被视为CFI指令。在匹配的过程中,自然CFI指令的类型就被甄别出来了。在这一步中,我们将所有指令分到如下四类brType中:

CFI指令类型 brType类型编码
非CFI 00
branch指令 01
jal指令 10
jalr指令 11

ret、call判定

然后,我们需要判断是否为call或者ret,这可以通过rd和rs的取值来考察,具体来说,RISCV的RVI指令中,提供了对rd和rs取值的约定, 当二者取到link寄存器的序号(x1为标准的返回地址寄存器,x5为备用的link寄存器),分别对应着压栈和弹栈。详细的对应情况如下:

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.1.1.2 - FrontendTrigger

FrontendTrigger子模块

该子模块的主要作用是在前端设置硬件断点和检查。

该模块的输入pc有一个隐含条件,那就是这个pc是通过ftq传递的startAddr计算出来的。

FrontendTrigger功能介绍

断点设置和断点检查

在IFU的FrontendTrigger模块里共4个Trigger,编号为0,1,6,8,每个Trigger的配置信息(断点类型、匹配地址等)保存在tdata寄存器中。

当软件向CSR寄存器tselect、tdata1/2写入特定的值时,CSR会向IFU发送tUpdate请求,更新FrontendTrigger内的tdata寄存器中的配置信息。 目前前端的Trigger仅可以配置成PC断点(mcontrol(tdata1)寄存器的select位为0;当select=1时,该Trigger将永远不会命中,且不会产生异常)。

在取指时,IFU的F3流水级会向FrontendTrigger模块发起查询并在同一周期得到结果。后者会对取指块内每一条指令在每一个Trigger上做检查, 当指令的PC和tdata2寄存器内容的关系满足mcontrol的match位所指示的关系(香山支持match位为0、2、3,对应等于、大于、小于)时, 该指令会被标记为Trigger命中,随着执行在后端产生断点异常,进入M-Mode或调试模式。

链式断点

根据RISCV的debug spec,香山实现的是mcontrol6。

前端的0、6、8号Trigger支持Chain功能。 当它们对应的Chain位被置时,只有当该Trigger和编号在它后面一位的Trigger同时命中,且timing配置相同时(在最新的手册中,这一要求已被删除),处理器才会产生异常。其中可以和6,8号trigger实现chain功能的7,9号trigger在后端访存部件中。

FrontendTrigger 接口说明

设计上并没有提供一个或一组对外的接口来查询某个断点的状态,因此,要在测试中检查断点状态,要么需要检查内部信号的情况(仓库中提供的构建脚本已经暴露了所有内部信号),要么通过具体执行过程中,断点的触发情况来判定。

输入接口

主要分为控制接口和执行信息(目前执行信息只有pc)

控制接口 io_frontendTrigger

本接口存储了frontendTrigger的控制信息,包含以下信号/信号组:

debugMode

当前是否处于debug模式下

tEnableVec

对FrontendTrigger的每个断点,指示其是否有效。

tUpdate

更新断点的控制信息,包含以下信号/信号组:

valid:此次更新是否有效/是否更新。

bits_addr:此次更新的是哪个断点(0~3)

bits_tdata_action:断点触发条件达成后的行为

bits_tdata_chain:断点是否链式传导

bits_tdata_matchType:断点匹配类型(等于、大于、小于三种)

bits_tdata_select:目前为止,select为0时为pc断点

bits_tdata_tdata2:用于和PC比较的基准值

triggerCanRaiseBpExp

trigger是否可以引起异常

pc

pc有一个隐含条件,就是16条指令的pc必定是连续的

输出接口

triggered:16条指令的断点触发情况。

FrontEndTrigger 测试点和功能点

功能点1 设置断点和断点检查

FrontEndTrigger目前仅支持设置PC断点,这通过设置断点的tdata1寄存器的select位为0实现。 同时,tdata2寄存器的mcontrol位负责设置指令PC和tdata2寄存器的地址需要满足的关系, 关系满足时,该指令会被标记为trigger命中。

所以,基于以上功能描述,我们需要测试:

select位为1时,断点是否永远不会触发。

select位为0时,当PC和tdata2的数据的关系满足tdata2的match位时,是否会设置断点。

select位为0时,当PC和tdata2的数据的关系不满足tdata2的match位时,断点是否一定不会触发。

综上所述,我们在这一功能点设计的测试点如下:

序号 名称 描述
1.1 select1判定 给定tdata1的select位为1,随机构造其它输入,检查断点是否没有触发
1.2.1 select0关系匹配判定 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位匹配的输入,检查断点是否触发
1.2.2 select0关系不匹配判定 给定tdata1的select位为0,构造PC与tdata2数据的关系同tdata2的match位不匹配的输入,检查断点是否触发

功能点2 链式断点

当某一个trigger的chain位被置后,当其后的trigger的chain位未设置,且两个trigger均命中并且两个trigger的timing相同时,后一个trigger才会触发。

对0号trigger,不需要考虑链式的情况

由此,我们可以设置几种测试点:

序号 名称 描述
2.1 chain位测试 对每个trigger,在满足PC断点触发条件的情况下,设置chain位,检查断点是否一定不触发。
2.2.1 未命中测试 对两个trigger,仅设置前一个trigger的chain位且两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.1.1.3 - PredChecker

子模块:PredChecker简介

分支预测检查器PredChecker接收来自IFU的预测块信息(包括预测跳转指令在预测块的位置、预测的跳转目标、预译码得到的指令信息、指令PC以及预译码得到的跳转目标偏移等),在模块内部检查五种类型的分支预测错误。模块内部分为两个流水线stage,分别输出信息,第一个stage输出给f3阶段,用于修正预测块的指令范围和预测结果。第二个stage输出给wb阶段,用于在发现分支预测错误时产生前端重定向以及写回给FTQ正确的预测信息。

PredChecker功能介绍

JAL预测错误检查

jal指令预测错误的条件是,预测块中有一条jal指令(由预译码信息给出),但是要么这个预测块没有预测跳转,要么此预测块预测跳转的指令在这条jal指令之后(即这条jal指令没有被预测跳转)。

RET预测错误检查

ret指令预测错误的条件是,预测块中有一条ret指令(由预译码信息给出),但是要么这个预测块没有预测跳转,要么此预测块预测跳转的指令在这条ret指令之后(即这条ret指令没有被预测跳转)。

更新指令有效范围向量

PredChecker在检查出Jal/Ret指令预测错误时,需要重新生成指令有效范围向量,有效范围截取到Jal/Ret指令的位置,之后的bit全部置为0。 需要注意的是,jal和ret指令的错误检查都会导致指令有效范围的缩短, 所以需要重新生成指令有效范伟fixedRange,同时修复预测结果。需要注意的是,这个修复只会针对RET预测错误和JAL预测错误导致的范围错误,对于后续要介绍的非CFI预测错误和无效指令预测错误,尽管他们会造成预测块的范围偏小,但是不会进行修复,而是直接在这里进行重定向。这样,重定向后重新取的指令会从这个出错的指令开始。

非CFI预测错误检查

非CFI预测错误的条件是被预测跳转的指令根据预译码信息显示不是一条CFI指令。

无效指令预测错误检查

无效指令预测错误的条件是被预测的指令的位置根据预译码信息中的指令有效向量显示不是一条有效指令的开始。

目标地址预测错误检查

目标地址预测错误的条件是,被预测的是一条有效的jal或者branch指令,同时预测的跳转目标地址和由指令码计算得到的跳转目标不一致。

分级输出检查结果

以上PredChecker检查结果会分为两级分别输出,前面已经提到,Jal/Ret指令由于需要重新生成指令有效范围向量和重新指定预测位置, 所以需要在错误产生的当拍(F3)直接输出结果到Ibuffer用于及时更正进入后端的指令 。而由于时序的考虑,其他错误信息(比如五种错误的错误位置、正确的跳转地址等)则是等到下一拍(WB)阶段才返回给IFU做前端重定向。

PredChecker接口说明

输入接口

fire_in:这个信号可以简单认为是模块有效性的控制信号。

ftqOffset:表示该预测块的跳转指令是否存在(valid),以及跳转指令的序号(bits)。

instrRange:对每条指令,表示该指令是否在预测块的有效指令范围内。

instrValid:表示的是对于每条32位的拼接指令,其是否为一条有效的指令(即低16位为一条RVC指令,或者整个32位为一条RVI指令)。

jumpOffset:如果某一指令为跳转指令,jumpOffset表示这个指令的跳转目标。

pc:指令的pc。

pds:预译码信息,包含指令的brType、是否为Ret(isRet)、是否为RVC指令(isRVC)。

target:下个预测块的开始地址。

输出接口

第一阶段输出

fixedRange:修复的指令有效范围向量,对每条指令i,fixedRange_i为真表示这条指令是否在当前预测块的有效指令范围内

fixedTaken:修复过后的CFI指令选取情况,对每条指令,fixedTaken_i为真表示这条指令是否是这个预测块的第一条CFI指令

第二阶段输出

fixedMissPred:对每条指令,PredChecker检查出的存在预测错误的情况,fixedMissPred_i为真表示这条指令存在预测错误

fixedTarget:对每条指令,给出修复过的下一条指令的位置(可以是常规的pc+2或+4,或者如果是跳转指令,给出跳转目标)。

jalTarget:对每条指令,给出跳转目标。

PredChecker测试点和功能点

功能点1 BPU预测信息的JAL预测错误检查

PredChecker会对传入的预测块进行JAL预测错误预检查并修正指令有效范围向量和预测的跳转指令。

对这一模块的测试,我们分为两部分:正确的输入是否会误检和确有JAL检测错误的预测块输入能否检出。

对于误检,我们设计如下的测试点:

序号 名称 描述
1.1.1 误检测试1 预测块中没有JAL指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JAL预测错误。
1.1.2 误检测试2 预测块中有JAL指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JAL预测错误。

对于JAL预测错误的正确检验,我们设计如下的测试点:

序号 名称 描述
1.2.1 存在JAL未预测 预测块中存在JAL指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出JAL预测错误。
1.2.2 预测的JAL并非第一条 预测块中存在JAL指令,但是BPU预测信息取的跳转指令在第一条JAL指令之后,检查PredChecker是否能检测出JAL预测错误。

功能点2 BPU预测信息的RET预测错误检查

PredChecker会对传入的预测块进行RET预测错误预检查并修正指令有效范围向量和新的预测结果。

和JAL预测错误类似,我们也按照误检和正检来构造。

对于误检,我们设计如下的测试点:

序号 名称 描述
2.1.1 误检测试1 预测块中没有RET指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报RET预测错误。
2.1.2 误检测试2 预测块中有RET指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报RET预测错误。

对于RET预测错误的正确检出,我们设计如下的测试点:

序号 名称 描述
2.2.1 存在RET未预测 预测块中存在RET指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
2.2.2 预测的跳转并非第一条 预测块中存在RET指令,但是BPU预测信息取的跳转指令在第一条RET指令之后,检查PredChecker是否能检测出RET预测错误。

功能点3 更新指令有效范围向量和预测跳转的指令

PredChecker在检查出Jal/Ret指令预测错误时,需要重新生成指令有效范围向量, 有效范围截取到Jal/Ret指令的位置,之后的bit全部置为0。 同时,还需要根据每条指令的预译码信息和BPU的预测信息修复预测跳转的结果。

所以,根据功能要求,我们可以划分出三类情况,分别是预测的有效范围和取用的跳转指令正确的情况, 由于RET和JAL预测错误引起的有效范围偏大和错判非跳转指令和无效指令引起的有效范围偏小。

序号 名称 描述
3.1 有效范围无误 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。
3.2 RET和JAL预测错误引起的范围偏大 如果检测到了JAL或RET类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。
3.3 非CFI和无效指令引起的预测范围偏小 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令,因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令

功能点4 非CFI预测错误检查

非CFI预测错误的条件是被预测跳转的指令根据预译码信息显示不是一条CFI指令。

要检验这一功能,我们仍然按误检和正确检验来设计测试点:

序号 名称 描述
4.1.1 误检测试1 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
4.1.2 误检测试2 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
4.2 正确检测测试 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误

功能点5 无效指令预测错误检查

无效指令预测错误的条件是被预测的指令的位置根据预译码信息中的指令有效向量显示不是一条有效指令的开始。

要检验这一功能,我们按照误检和正确检测来设计测试点:

序号 名称 描述
5.1.1 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.1.2 误检测试2 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.1.3 误检测试3 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.2 正确检测测试 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误

功能点6 目标地址预测错误检查

目标地址预测错误的条件是,被预测的是一条有效的jal或者branch指令, 同时预测的跳转目标地址和由指令码计算得到的跳转目标不一致。

和先前的思路一样,我们仍然按误检和检出两类组织测试点:

序号 名称 描述
6.1.1 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
6.1.2 误检测试2 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
6.2 正确检测测试 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误

功能点7 生成跳转和顺序目标

PredChecker还需要负责生成跳转和顺序目标。

我们通过随机生成译码信息进行测试

序号 名称 描述
7.1 随机测试 随机提供译码信息,检测生成的跳转目标和顺序目标。

测试点汇总

综上所述,所有的测试点如下:

序号 功能 名称 描述
1.1.1 BPU预测信息的JAL预测错误检查 误检测试1 预测块中没有JAL指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报JAL预测错误。
1.1.2 BPU预测信息的JAL预测错误检查 误检测试2 预测块中有JAL指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报JAL预测错误。
1.2.1 BPU预测信息的JAL预测错误检查 存在JAL未预测 预测块中存在JAL指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出JAL预测错误。
1.2.2 BPU预测信息的JAL预测错误检查 预测的JAL并非第一条 预测块中存在JAL指令,但是BPU预测信息取的跳转指令在第一条JAL指令之后,检查PredChecker是否能检测出JAL预测错误。
2.1.1 BPU预测信息的RET预测错误检查 误检测试1 预测块中没有RET指令且BPU预测信息也没有取用任何跳转指令的输入,检查PredChecker是否会误报RET预测错误。
2.1.2 BPU预测信息的RET预测错误检查 误检测试2 预测块中有RET指令且BPU预测信息取用的正是本条跳转指令的输入,检查PredChecker是否会误报RET预测错误。
2.2.1 BPU预测信息的RET预测错误检查 存在RET未预测 预测块中存在RET指令,但是BPU预测信息未预测跳转,检查PredChecker是否能检测出RET预测错误。
2.2.2 BPU预测信息的RET预测错误检查 预测的跳转并非第一条 预测块中存在RET指令,但是BPU预测信息取的跳转指令在第一条RET指令之后,检查PredChecker是否能检测出RET预测错误。
3.1 更新指令有效范围向量和预测跳转的指令 有效范围无误 不存在任何错误的情况下,PredChecker应当保留之前的预测结果。
3.2 更新指令有效范围向量和预测跳转的指令 RET和JAL预测错误引起的范围偏大 如果检测到了JAL或RET类的预测错误,PredChecker应该将有效指令的范围修正为预测块开始至第一条跳转指令。同时,应该将预测跳转的指令位置修正为预测块中的第一条跳转指令。
3.3 更新指令有效范围向量和预测跳转的指令 如果出现了非控制流指令和无效指令的误预测,不应该将预测跳转的指令重新修正到预测块中第一条跳转指令,因为后续会直接冲刷并重新从重定向的位置取指令,如果这里修正的话,会导致下一预测块传入重复的指令。
4.1.1 非CFI预测错误检查 误检测试1 构造不存在CFI指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
4.1.2 非CFI预测错误检查 误检测试2 构造存在CFI指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检非CFI预测错误
4.2 非CFI预测错误检查 正确检测测试 构造不存在CFI指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出非CFI预测错误
5.1.1 无效指令预测错误检查 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.1.2 无效指令预测错误检查 误检测试2 构造存在无效跳转指令并且未预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.1.3 无效指令预测错误检查 误检测试3 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检无效指令预测错误
5.2 无效指令预测错误检查 正确检测测试 构造无效指令但是预测了跳转的预测信息作为输入,测试PredChecker是否能检查出无效指令预测错误
6.1.1 目标地址预测错误检查 误检测试1 构造不存在跳转指令并且未预测跳转的预测信息作输入,测试PredChecker是否会错检目标地址预测错误
6.1.2 目标地址预测错误检查 误检测试2 构造存在有效跳转指令并且正确预测跳转的预测信息作为输入,测试PredChecker是否会错检目标地址预测错误
6.2 目标地址预测错误检查 正确检测测试 构造存在有效跳转指令的预测块和预测跳转但跳转目标计算错误的预测信息作为输入,测试PredChecker能否检出目标地址预测错误
7.1 生成跳转和顺序目标 随机测试 随机提供译码信息,检测生成的跳转目标和顺序目标。

12.1.1.4 - PreDecode

子模块:PreDecoder简介

预译码器PreDeocoder接受初始指令码并进行指令码拼接,拼接之后对每个指令码查询预译码表产生预译码信息,预译码信息包括该位置是否是有效指令开始、CFI指令类型、是否是RVC指令、是否是Call指令以及是否是Ret指令。预译码器会产生两种有效指令开始的向量,一种是默认第1个二字节必为有效指令开始,另一种是默认第2个二字节必为有效指令的开始,最终的选择在IFU端做。

所以,预译码器接收的输入是: 17 x 2B的初始指令码,这个2字节的初始指令码要么是一条RVC指令,要么是一条RVI指令的前半或后半部分。

预译码器的输出是:16x4B的拼接指令码;对每个4B指令码,该条指令是否为RVI或RVC指令(RVC指令只考虑该4B的低2B);对每个4B指令码,该条指令的跳转偏移;两个16位的有效指令开始向量,其中第一种向量假定当前预测块的起始2字节为一条有效指令的开始,而第二种向量假定当前预测块的起始2字节为一条有效RVI指令的结束(但是由于第二种向量的前两位必然为0和1,所以编译优化后,第二种向量实际只有14个信号,表示2-15位;同理,第1种向量的第0位因为恒为1,所以也被优化)

功能介绍

指令码生成

预译码器接受来自IFU完成指令切分的17 × 2字节的初始指令码,并以4字节为窗口,2字节为步进长度, 从第1个2字节开始,直到第16个2字节,选出总共16个4字节的指令码。

预译码信息生成

预译码器根据指令码产生预译码信息,主要包括:是否是RVC指令、是否是CFI指令、 CFI指令类型(branch/jal/jalr/call/ret)、CFI指令的目标地址计算偏移。

首先是判断是否是RVC指令,RVC指令的具体格式参阅RISCV手册的描述:

RVC

其中,决定指令是否为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:每条指令预译码信息,在时序优化之后,PreDecode模块的控制信息只剩下了valid和isRVC,后者表示这条指令是否为RVC指令

hasHalfValid:这个信号需要和pd的valid结合起来看,PreDecode的一个功能是求出指令开始向量,也就是对每个4B的拼接指令,判断其低2B是否为一条有效指令的开始(即一条RVI指令的前半部分,或者一条RVC指令),但是需要分类讨论该预测块的第一个2B是否为一条有效指令的开始。hasHalfValid表示的是当前预测块的第一个2B指令为一条RVI指令的后半部分时,给出的指令开始向量。类似地,pd中的valid指的是当前预测块的第一个2B指令为一条指令的开始时,给出的指令开始向量。

PreDecoder测试点和功能点

功能点1 生成指令码

子模块:PreDecoder简介

预译码器PreDeocoder接受初始指令码并进行指令码拼接,拼接之后对每个指令码查询预译码表产生预译码信息,预译码信息包括该位置是否是有效指令开始、CFI指令类型、是否是RVC指令、是否是Call指令以及是否是Ret指令。预译码器会产生两种有效指令开始的向量,一种是默认第1个二字节必为有效指令开始,另一种是默认第2个二字节必为有效指令的开始,最终的选择在IFU端做。

所以,预译码器接收的输入是: 17 x 2B的初始指令码,这个2字节的初始指令码要么是一条RVC指令,要么是一条RVI指令的前半或后半部分。

预译码器的输出是:16x4B的拼接指令码;对每个4B指令码,该条指令是否为RVI或RVC指令(RVC指令只考虑该4B的低2B);对每个4B指令码,该条指令的跳转偏移;两个16位的有效指令开始向量,其中第一种向量假定当前预测块的起始2字节为一条有效指令的开始,而第二种向量假定当前预测块的起始2字节为一条有效RVI指令的结束(但是由于第二种向量的前两位必然为0和1,所以编译优化后,第二种向量实际只有14个信号,表示2-15位;同理,第1种向量的第0位因为恒为1,所以也被优化) 功能介绍 指令码生成

预译码器接受来自IFU完成指令切分的17 × 2字节的初始指令码,并以4字节为窗口,2字节为步进长度, 从第1个2字节开始,直到第16个2字节,选出总共16个4字节的指令码。 预译码信息生成

预译码器根据指令码产生预译码信息,主要包括:是否是RVC指令、是否是CFI指令、 CFI指令类型(branch/jal/jalr/call/ret)、CFI指令的目标地址计算偏移。

预译码器从IFU接收完成指令切分的17 x 2 字节的初始指令码,以4字节为窗口,2字节为步进长度,选出16 x 4字节的指令码

我们需要随机生成初始指令码,并测试拼接的结果。

序号 名称 描述
1 拼接测试 随机生成17 x 2字节的初始指令码,检验PreDecoder拼接结果

功能点2 生成预译码信息

预译码器会根据指令码产生预译码信息,包括RVC指令的判定和CFI指令的目标地址计算偏移。

CFI类型的判定则时序优化到了F3PreDecoder中。

据此,我们可以设计下述测试点。

首先是判定RVC指令,我们随机生成输入初始指令码,对返回的16位RVC判定结果进行检验。 具体来说,对每32位指令,考虑RVC和RVI两种情况。

序号 名称 描述
2.1.1 RVC判定 传入RVC指令,应该判断为RVC
2.1.2 RVI判定 传入RVI指令,不应判断为RVC

然后,需要分别根据手册构造RVC和RVI扩展下的J指令和BR指令们,所以有如下的测试点:

序号 名称 描述
2.2.1 RVC.J计算 对传入RVC扩展的J指令,检查计算的偏移
2.2.2 RVI.J计算 对传入RVI扩展的J指令,检查计算的偏移
2.2.3 RVC.BR计算 对传入RVC扩展的BR指令,检查计算的偏移
2.2.4 RVI.BR计算 对传入RVI扩展的BR指令,检查计算的偏移

功能点3 生成指令开始向量

最后,预译码还需要生成两种指令开始向量:

序号 名称 描述
2.3.1 有效指令开始向量计算1 对预测块,假定第一条指令为一条有效指令的开始,对每条指令计算其是否为有效指令开始
2.3.2 有效指令开始向量计算2 对预测块,假定第一条指令为一条有效指令的结束,对每条指令计算其是否为有效指令开始

测试点汇总

综上所述,对PredDecoder,所有的测试点为:

序号 功能 名称 描述
1 拼接指令码 拼接测试 随机生成17 x 2字节的初始指令码,检验PreDecoder拼接结果
2.1.1 RVC判定 RVC判定 传入RVC指令,应该判断为RVC
2.1.2 RVC判定 RVI判定 传入RVI指令,不应判断为RVC
2.2.1 跳转目标计算 RVC.J计算 对传入RVC扩展的J指令,检查计算的偏移
2.2.2 跳转目标计算 RVI.J计算 对传入RVI扩展的J指令,检查计算的偏移
2.2.3 跳转目标计算 RVC.BR计算 对传入RVC扩展的BR指令,检查计算的偏移
2.2.4 跳转目标计算 RVI.BR计算 对传入RVI扩展的BR指令,检查计算的偏移
2.3.1 有效指令开始向量计算1 对预测块,假定第一条指令为一条有效指令的开始,对每条指令计算其是否为有效指令开始
2.3.2 有效指令开始向量计算2 对预测块,假定第一条指令为一条有效指令的结束,对每条指令计算其是否为有效指令开始

12.1.1.5 - RVCExpander

子模块:RVCExpander简介

RVCExpander是IFU的子模块,负责对传入的指令进行指令扩展,并解码计算非法信息。

该模块接收的输入量是两个:一条RVC指令或者RVI指令;CSR对fs.status的使能情况。

输出量也是两个:输入指令对应的RVI指令;RVC指令是否非法。

指令扩展

如果是RVI指令,则无需扩展。

否则对RVC指令,按照手册的约定进行扩展。

非法指令判断

RVI指令永远判断为合法。

对于RVC指令的判定,详细内容参阅20240411的RISCV手册的26.8节表格列出的指令条件。

RVCExpander接口说明

输入接口

fsIsOff:表示CSR是否使能fs.status

in:传入一个32位数据,其可以是一个完整的RVI指令,也可以是低16位RVC指令+高16位为RVI指令的一半(当然低16位也有可能是RVI指令的后半部分,但是RVCExpander不会区分,可以认为RVCExpander假定传入的32位数据的低16位一定为一条指令的开始)

输出接口

ill:表示这条指令是否为非法指令

out_bits:对RVI指令,直接返回,对RVC指令,返回扩展后的32位指令。

功能点和测试点

功能点1 指令扩展

RVCExpander负责接收预译码器拼接的指令码,并进行指令扩展,如果是16位RVC指令,需要按照RISCV手册的约定完成扩展

对此,我们需要随机生成RVI指令和RVC指令,送入预译码器:

序号 名称 描述
1.1 RVI指令保留 构造RVI指令传入,检查保留情况
1.2 RVC指令扩展 构造RVC指令传入,按手册检查扩展结果

功能点2 非法指令判断

RVCExpander在解析指令时,如发现指令违反了手册的约定,则需要判定该指令非法

对此,我们需要随机生成非法指令送入RVI中,并检测RVCExpander对合法位的校验;同时,我们还需要校验合法指令是否会被误判为非法指令:

此外,需要判定C.fp指令在CSR未使能fs.status的情况下,能否将这类指令判定为非法。

序号 名称 描述
2.1 常规非法指令测试 随机构造非法RVC指令传入,检查判断结果
2.2 合法指令测试 随机构造合法RVC指令传入,检查判断结果
2.3 C.fp指令测试 CSR未使能fs.status的情况下,C.fp指令应该为非法

测试点汇总

序号 功能 名称 描述
1.1 指令扩展 RVI指令保留 构造RVI指令传入,检查保留情况
1.2 指令扩展 RVC指令扩展 构造RVC指令传入,按手册检查扩展结果
2.1 非法指令判断 非法指令测试 随机构造非法RVC指令传入,检查判断结果
2.2 非法指令判断 合法指令测试 随机构造合法RVC指令传入,检查判断结果
2.3 C.fp指令测试 CSR未使能fs.status的情况下,C.fp指令应该为非法

RVC扩展辅助阅读材料

为方便参考模型的书写,在这里根据20240411版本的手册内容整理了部分指令扩展的思路。

对于RVC指令来说,op = instr(1, 0);funct = instr(15, 13)

op\funct 000 001 010 011 100 101 110 111
00 addi4spn fld lw ld lbu
lhu;lh
sb;sh
fsd sw sd
01 addi addiw li lui
addi16sp
zcmop
ARITHs
zcb
j beqz bnez
10 slli fldsp lwsp ldsp jr;mv
ebreak
jalr;add
fsdsp fwsp sdsp

在开始阅读各指令的扩展规则时,需要了解一些RVC扩展的前置知识,比如:

rd’, rs1’和rs2’寄存器:受限于16位指令的位宽限制,这几个寄存器只有3位来表示,他们对应到x8~x15寄存器。

op = b'00'

funct = b'000’: ADDI4SPN

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

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

这条指令的第12至11位是立即数的5至4位,第10至7位是立即数的9至6位,第6位是立即数的第2位,第7位是立即数的第3位。

这条指令最终扩展成为addi rd’, x2, nzuimm[9:2]

addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |

注意,该指令的立即数为0的时候,不合法。

funct = b'001’: fld

该指令从内存加载一个双精度浮点数到rd’寄存器。

offset的低三位是0,高位进行了0扩展。

这条指令最终扩展成为fld rd′,offset(rs1′)

fld的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000111 |

注意:在昆明湖环境下,该指令要求CSR使能fs.status,也即入参fsIsOff为假。

funct = b'010’: lw

该指令从内存加载一个32位的值到rd’寄存器。

offset的低两位是0,高位进行了0扩展。

这条指令最终扩展成为lw rd′,offset(rs1′)

lw的格式形如: | imm[11:0] | rs1 | 010 | rd | 0000011 |

funct = b'011’: ldsp

该指令从内存加载一个64位的值到rd’寄存器。

offset的低两位是0,高位进行了0扩展。

这条指令最终扩展成为ld rd′,offset(rs1′)

ld的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000011 |

funct = b'100’: zcb extensions 1

在RVC指令中,这部分对应的是zcb扩展中的5条指令:lbu,lhu,lh,sb,sh

在zcb扩展中,进一步地取instr[12:10]作为zcb扩展的指令码,我们记作funct_zcb

funct_zcb = b'000’: lbu

| 100 | 000 | rs1’ | uimm[0|1] | rd’ | 00 |

这个指令从rs1’+uimm的地址读取一字节,用0扩展并并加载到rd’中。

最终翻译为 lb rd’, uimm(rs1')

lb指令的格式形如:| imm[11:0] | rs1 | 000 | rd | 0000011 |

funct_zcb = b'001’, instr[6] =0 : lhu

| 100 | 001 | rs1’ | 0 | uimm[1] | rd’ | 00 |

这个指令从地址rs1’ + uimm读取半word,用0扩展加载到rd’中。

最终翻译为 lhu rd’, uimm(rs1')

lhu指令的格式形如:| imm[11:0] | rs1 | 101 | rd | 0000011 |

funct_zcb = b'001’, instr[6] =1 : lh

| 100 | 001 | rs1’ | 1 | uimm[1] | rd’ | 00 |

这个指令从地址rs1’ + uimm读取半word,符号扩展并加载到rd’中。

最终翻译为 lh rd’, uimm(rs1')

lh指令的格式形如:| imm[11:0] | rs1 | 001 | rd | 0000011 |

funct_zcb = b'010’: sb

| 100 | 010 | rs1’ | uimm[0 | 1] | rd’ | 00 |

这个指令把rs2’的低字节存储到地址rs1’ + uimm指示的内存地址中。

最终翻译为 sb rs2, uimm(rs1')

RVI中sb指令的格式形如:|imm[11:5] | rs2 | rs1 | 000 | imm[4:0] | 0100011 |

funct_zcb = b'011’: sh

| 100 | 011 | rs1’ | 0 | uimm[1] | rd’ | 00 |

这个指令把rs2’的低半字存储到地址rs1’ + uimmz指示的内存地址中。

最终翻译为 sh rd’, uimm(rs1')

sh指令的格式形如:|imm[11:5] | rs2 | rs1 | 001 | imm[4:0] | 0100011 |

funct = b'101’: fsd

fsd将rs2’中的双精度浮点数存储到rs1’ + imm指示的内存区域

该指令的立即数低3位为0,同时进行了0符号扩展。

最终这个指令将被扩展为fsd rs2′, offset(rs1′)

RVI的FSD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100011 |

注意:在昆明湖环境下,该指令要求CSR使能fs.status,也即入参fsIsOff为假。

funct = b'110’: sw

sw将rs2’中的一个字存储到rs1’ + imm指示的内存区域

该指令的立即数低2位为0,同时进行了0符号扩展。

最终这个指令将被扩展为sw rs2′, offset(rs1′)

RVI的SW格式形如:| imm[11:5]| rs2 | rs1 | 010 | imm[4:0] | 0100011 |

funct = b'111’: sd

fsd将rs2’中的双字存储到rs1’ + imm指示的内存区域

该指令的立即数低3位为0,同时进行了0符号扩展。

最终这个指令将被扩展为sd rs2′, offset(rs1′)

RVI的SD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100111 |

op = b'01'

funct = b'000’: addi

该指令将一个符号扩展的非0立即数加到rd存储的数字上,并将结果写入rd。

尽管手册规定立即数和rd不为0,但是立即数和rd为0的情况仍可视为合法。前者是HINT指令,而后者是NOP。

这条指令最终扩展成为addi rd, rd, imm

addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |

funct = b'001’: addiw

该指令的功能和addi类似,但是先计算得到32位数,然后再符号扩展至64位。

该指令的rd为0时非法。

当立即数不为0时,该指令最终扩展成为addiw, rd, rd, imm

addiw的指令格式为| imm[11:0] | rs1 | 000 | rd | 0011011 |

如果立即数为0,该指令将会扩展成为sext.w rd,不过和addiw的格式是一样的,因此可以将他们归为一类。

funct = b'010’: li

该指令将符号扩展的立即数加载到rd中。

当立即数为0时,该指令为hint,可以看作合法。

这条指令最终扩展成为addi rd, x0, imm

addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |

funct = b'011’: lui/addi16sp/zcm

当rd不为0且不为2时,为lui指令,可以扩展为lui rd, imm

lui指令的格式形如: | imm[31:12] | rd | 0110111 |

当rd为0时,为hint,也可当作cli进行译码。

当rd为2时,为addi16sp指令:

扩展为addi x2, x2, nzimm[9:4]

addi的格式形如:| imm[11:0] | rs1 | 000 | rd | 0010011 |

对addi16sp,立即数为0时非法。

此外,当第12至11位皆为0,第7位是1且第6至2位为0时,为zcmop,可以直接翻译为一个不起效的指令,比如与立即数0。

funct = b'100’: arith & zcb extension2

在RVC指令中,这部分对应的是数学运算指令和zcb扩展中的另一部分指令,数学计算指令的对应如下:

其中SRLI64和SRAI64在昆明湖环境下可以不考虑。

srli

当funct2为00时,为srli。

最终可翻译为srli rd′, rd′, 64

srli的格式形如:|0000000|shamt|rs1|101|rd|0010011|

srai

当funct2为01时,为srai。

最终可翻译为srai rd′, rd′, 64

SRAI的格式形如:|0100000|shamt|rs1|101|rd|0010011|

andi

该指令最终扩展为andi rd′, rd′, imm

andi的格式形如|imm[11:0]|rs1|111|rd|0010011|

sub

这条指令最终可以扩展为:sub rd′, rd′, rs2′

sub指令的格式形如:|0100000|rs2|rs1|000|rd|0110011|

xor

这条指令最终可以扩展为:xor rd′, rd′, rs2′

xor指令的格式形如:|0000000|rs2|rs1|100|rd|0110011|

or

这条指令最终可以扩展为:or rd′, rd′, rs2′

or指令的格式形如:|0000000|rs2|rs1|110|rd|0110011|

and

这条指令最终可以扩展为:and rd′, rd′, rs2′

and指令的格式形如:|0000000|rs2|rs1|111|rd|0110011|

subw

这条指令最终可以扩展为:subw rd′, rd′, rs2′

subw指令的格式形如:|0100000|rs2|rs1|000|rd|0111011|

addw

这条指令最终可以扩展为:addw rd′, rd′, rs2′

addw指令的格式形如:|0000000|rs2|rs1|000|rd|0111011|

mul

从mul开始的一部分指令属于zcb扩展。

zcb扩展中,当instr(12, 10) == “111”,且instr(6, 5)为"10"时,为mul指令。

zcb扩展中,当instr(12, 10) == “111”,且instr(6, 5)为"11"时,根据instr(4,2), 共有000的zext.b,001的sext.b,010的zext.h,011的sext.h,100的zext.w和101的not。

该指令可扩展为mul rd, rd, rs2

mul的格式为:|0000001|rs2|rs1|000|rd|0110011|

zext.b

这条指令可以翻译为:andi rd’/rs1’, rd’/rs1’, 0xff

andi的格式形如|imm[11:0]|rs1|111|rd|0010011|

sext.b

该指令翻译为sext.b rd, rd

sext.b指令在RVI下形如:

zext.h

该指令翻译为zext.h rd, rd

zext.h指令在RVI下形如:

sext.h

该指令翻译为sext.h rd, rd

sext.h指令在RVI下形如:

zext.w

该指令等价为add.uw rd’/rs1’, rd’/rs1’, zero

add.uw指令在RVI下形如:

not

该指令等价为xori rd’/rs1’, rd’/rs1’, -1

xori指令在RVI下形如: | imm[11:0] | rs1| 100 | rd | 0010011 |

funct = b'101’: j

最终这个指令将被扩展为jal x0, offset

jal的格式形如:| imm[20|10:1|11|19:12] | rd | 1101111 |

funct = b'110’: beqz

该指令可以扩展到beq rs1‘, x0, offset

beq指令形如: |imm[12|10:5]|rs2|rs1|000|imm[4:1|11]|1100011| imm[12|10:5]rs2rs1001imm[4:1|11]1100011BNE

funct = b'111’: bnez

最终这个指令将被扩展为bne rs1′, x0, offset

bne指令形如:|imm[12|10:5]| rs2 | rs1 | 001 | imm[4:1|11] | 1100011|

op = b'10'

funct = b'000’: slli

该指令将一个符号扩展的非0立即数加到rd存储的数字上,并将结果写入rd。

尽管手册规定立即数和rd不为0,但是立即数和rd为0的情况仍可视为合法。前者是HINT指令,而后者是NOP。

这条指令最终扩展成为slli rd, rd, shamt[5:0]

slli的格式形如:|000000|shamt|rs1|001|rd|0010011|

funct = b'001’: fldsp

该指令最终扩展成为fld rd, offset(x2)

fld的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000111 |

该指令要求CSR使能fs.status

funct = b'010’: lwsp

rd为0时非法。

这条指令最终扩展成为lw rd, offset(x2)

lw的格式形如: | imm[11:0] | rs1 | 010 | rd | 0000011 |

funct = b'011’: ldsp

rd为0时非法。

这条指令最终扩展成为ld rd, offset(x2)

lw的格式形如: | imm[11:0] | rs1 | 011 | rd | 0000011 |

funct = b'100’: jr/mv/ebreak/jalr/add

jr

当rd为0时,非法。

该指令最终可以扩展为jalr x0, 0(rs1)

jalr指令的格式为:|imm[11:0]|rs1|000|rd|1100111|

mv

rd为0时,是hint指令。

该指令最终可以扩展为add rd, x0, rs2

add指令形如:|0000000|rs2|rs1|000|rd|0110011|

ebreak

可以扩展为ebreak指令。

形如:|00000000000100000000000001110011|

jalr

该指令最终可以扩展为jalr x1, 0(rs1)

jalr指令的格式为:|imm[11:0]|rs1|000|rd|1100111|

add

该指令最终可以扩展为add rd, rd, rs2

add指令形如:|0000000|rs2|rs1|000|rd|0110011|

funct = b'101’: fsdsp

这条指令最终扩展成为fsd rs2, offset(x2)

RVI的FSD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100011 |

该指令要求CSR使能fs.status

funct = b'110’: swsp

这条指令最终扩展成为sw rs2, offset(x2)

RVI的SW格式形如:| imm[11:5]| rs2 | rs1 | 010 | imm[4:0] | 0100011 |

funct = b'111’: sdsp

该指令最终扩展成为sd rd, offset(x2)

RVI的SD格式形如:| imm[11:5]| rs2 | rs1 | 011 | imm[4:0] | 0100111 |

12.1.2 - ITLB

TLB 功能概述

现代操作系统通常采用虚拟内存管理机制(Virtual Memory Management),在处理器中对应需要内存管理单元(MMU,Memory Management Unit)来进行虚实地址的映射。MMU 负责处理 CPU 的内存访问请求,其功能包括虚实地址的映射、内存保护、CPU 高速缓存控制等。

虚实地址的映射是以页(Page)为单位的。在物理内存管理中,内核会将整个物理内存空间划分为一个一个的页帧(Page Frame),一般情况下页帧大小为 4KB,称为一个物理页帧,内核会将每一个物理页帧进行编号(PFN,Page Frame Number),每个页帧有唯一确定的 PFN。对于一个进程来说,如果它直接使用物理地址构建自己的地址空间,那么作为进程就需要关心每一个变量存放在哪一个物理地址,也就是说程序员需要清楚数据在内存中的具体布局,还需要每次都要考虑内存的分配问题;同时,对于多个进程同时进行的情况,哪些数据是共享的,如何避免地址冲突等等都会成为问题。

进程地址空间

MMU 为每个进程创建自己的虚拟地址空间,存储虚实地址的映射,在进程的视角看来它独享一段确定的(通常是连续的)地址,避免了其它进程的干扰;同时提供了虚实地址转换功能,这使得进程不必关心实际的物理地址在哪里,只需要对自己的地址空间进行操作。同时,对于一个进程来说,每次访问内存时并不是访问整个虚拟内存空间,因此进程实际需要占用的物理内存大小可以小于其虚拟地址空间的大小,由操作系统来决定要把哪一部分留在内存中,将剩余部分保存在磁盘中,在需要时再加载进入内存,极大的扩展了可用内存空间。

程序局部性原理,是计算机科学术语,指程序在执行时呈现出局部性规律,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。局部性原理又表现为:时间局部性空间局部性

  • 时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
  • 空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。

虚实页表映射

这样的由 MMU 创建的并负责维护的由虚拟地址指向物理地址的映射也将成为一项存储在一个物理页帧中,MMU 为了访问这样的物理页帧也需要一个根页表,根页表中存储着指向这些物理页帧的页表项(PTE),称为叶子 PTE。一个 PTE 的长度一般为 64 Bit(8 Bytes),而每一个一般物理页帧的大小为 4KB,这也就意味着一个物理页帧最多可以存储 4KB/8B = 2^9 个 PTE,因此根页表可以索引的范围即为 2^9 × 4KB = 2MB。2MB 的页表并不能满足内存日益增大的需要,在香山中实现的 SV48 即采用了四级页表的形式,通过四级的查询最终得到物理地址,每一级页表都能够索引 2^9 个下一级页表,最终找到需要的映射。四级页表下能够索引的地址范围达到了 2^9 × 2^9 × 2^9 × 2MB = 256TB。而页表本身也会比较大,如果存满的话大小会达到 4KB + 2^9 × 4KB + 2^9 × 2^9 × 4KB + 2^9 × 2^9 × 2^9 × 4KB = 537921540KB ≈ 513GB。当然,不是说每一级页表都要填满,页表的四级结构可以理解为一个多叉树形结构,只有需要用到的才会实际使用,很多的分支都不需要使用,因此页表的大小是可变的。

页表一般很大,需要存放在内存中,而处理器每一次访问内存的请求都需要先访问页表查找对应的物理页号然后再去读取所需数据,因此在不发生缺页的情况下,每次访存操作都需要两次访问内存才能得到物理地址,然后再次访问才能得到需要的数据。为了减少多次访存造成的开销,引入了地址转换后援缓存器(TLB,Translation Lookaside Buffer)。MMU 通常借助 TLB 来进行虚实地址的转换。TLB 一般是相连高速缓存(associative cache),相当于页表的 Cache,负责将最可能会用到的页表项对应的映射(虚拟地址与对应的物理地址)存储下来;在查找页表时首先查找 TLB 内存储的映射,如果没有命中再去查找内存中存储的完整页表。

同 Cache 一样,TLB 中页表项的组织方式一般有直接映射、全相联映射、组相连映射三种方式。直接映射一般通过模运算匹配,例如对昆明湖 48 行的 TLB 来说,其第 1 块只能对应内存的第 1/49/97/…/(n×48+1) 块,硬件结构简单、成本低、转换速度快,但是 TLB 表项利用率低,TLB miss 频繁,只适用于 TLB 大小与页表大小较接近的情况。全相联映射则不同,内存中的所有表项可以存放在 TLB 中的任意一项中,可以充分利用 TLB 的空间,冲突概率更低,但因此查找开销较高,适用于小容量 TLB。组相联映射是一种折中,可以二路组相联、四路组相联等。在香山的 TLB 模块中提供了丰富的参数配置,其中即包括采取哪一种相连方式,可以通过传入参数自行配置。本次验证的 ITLB 即采用 48 项全相联的结构。

二路组相联示意图 四路组相联示意图

香山的 MMU 模块由 TLB、Repeator、L2TLB、PMP&PMA 组成,其中 L2TLB 又包含了 Page Cache、Page Table Walker、Last Level Page Table Walker、Miss Queue 和 Prefetcher。在核内每次进行内存的操作(读写)时都需要通过 MMU 模块进行虚实地址的翻译,而 TLB 将被实例化为 ITLB(前端取指)和 DTLB(后端访存)。以 ITLB 为例,每当 ICache 或 IFU 需要进行取指操作,会先向 ITLB 发送一个地址转换请求,把需要转换的虚拟地址发给 ITLB;ITLB 接收到请求后就要查找自己存储的表项里有没有这个虚拟地址对应的映射,如果有的话就输出对应物理地址(paddr),之后由 PMP&PMA 模块检查对该物理地址的访问是否有效(包括地址是否有效、访问者是否有访问权限、页表属性等,其中对 ITLB 来说由于取出来的物理地址是待执行的指令,需要检查是否可以执行),检查通过后就可以把物理地址返回给前端。如果 ITLB 发现自己没有存储这样的表项,那么立即回应 miss,并同时发起 PTW 请求。前端接收到 miss 信号后会通过一些调度策略重新发起访问,在香山中体现为 miss 后不断重新给 TLB 发请求直到 hit。PTW 请求将交由 Page Table Walker 来执行,通过一些策略访问 L2TLB、Page Cache、内存中的完整页表,之后把访问到的 PTE(页表项)发回给 TLB(如果 PTW 都找不到那么会发生 Page Fault,同样返回给 TLB,TLB 收到 Page Fault 后会上报并由操作系统等从磁盘中加载页面)。TLB 接收到 PTE 的同时将 PTE 填充进自己的缓存中并向前端返回物理地址,前端才能通过该物理地址找到对应的指令。

TLB功能示意图

香山实现了二级 TLB,包括 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 模块进行权限检查。

12.1.2.1 - IO接口说明

香山实例化 TLB.sv 接口说明(ITLB)

基本控制信号

  • clock: 时钟信号,驱动 TLB 的时序逻辑。
  • reset: 复位信号,用于重置 TLB 的状态。

刷新(SFENCE)接口信号

  • io_sfence_valid: SFENCE 操作的有效性标志。
  • io_sfence_bits_rs1: SFENCE 操作是否使用寄存器 rs1 的值。
  • io_sfence_bits_rs2: SFENCE 操作是否使用寄存器 rs2 的值。
  • io_sfence_bits_addr: SFENCE 操作指定的地址,用于选择性刷新特定地址的 TLB 条目。
  • io_sfence_bits_id: 刷新操作指定的 asid/vmid,用于选择性刷新特定地址空间的 TLB 条目。
  • io_sfence_bits_flushPipe: 刷新整个管道。
  • io_sfence_bits_hv: 指示指令是否为 HFENCE.VVMA,即是否刷新虚拟化下由 vsatp 寄存器控制的条目。
  • io_sfence_bits_hg: 指示指令是否为 HFENCE.GVMA,即是否刷新由 hgatp 寄存器控制的条目。

控制与状态寄存器(CSR)接口信号

  • io_csr_satp_mode: SATP 寄存器的模式字段(如裸模式、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.1.2.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.1.2.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.1.2.4 - 环境配置

推荐使用WSL2+Ubuntu22.04+GTKWave

我们推荐Windows10/11用户通过WSL2进行开发,在此给出通过此方法进行环境配置的教程集锦,仅供参考。如环境安装过程中出现任何问题,欢迎在QQ群(群号:976081653)中提出,我们将尽力帮助解决。此页面将收集大家提出的所有环境配置相关问题并提供解决方案,欢迎随时向我们提问!

1、在Windows下安装WSL2(Ubuntu22.04)

参考资源:

— 微软官方教程:如何使用 WSL 在 Windows 上安装 Linux

— 其它资源:安装WSL2和Ubuntu22.04版本

2、打开WSL,换源

推荐使用清华源:清华大学开源软件镜像站-Ubuntu软件仓库

3、配置验证环境

请参照开放验证平台学习资源-快速开始-搭建验证环境配置picker环境。

4、使用 GTKWave

使用重庆大学硬件综合设计实验文档-Windows原生GTKWave给出的方法,可以通过在WSL中输入 gtkwave.exe wave.fst 打开在Windows下安装的GTKWave。请注意,gtkwave在使用中需要进入fst文件所在文件夹,否则会出现无法 initialize的情况。

cd out
gtkwave.exe {test_name}.fst
cd ..

5、使用VSCode插件Live Server查看验证报告

成功安装插件Live Server后,打开文件列表,定位到 /out/report/2025*-itlb-doc-*/index.html 右键并选择 Open With Live Server,之后在浏览器中打开提示的端口(默认为//localhost:5500)即可。

12.2 - Backend

后端模块验证文档

12.3 - Mem Block

访存模块验证文档

12.4 - Misc

其他模块验证文档

13 - 维护者

在提交 issue、pull request、discussion 时,如果指定对应模块的 maintainer 能更及时的得到响应。目前已有的维护人员如下(首字母排名):

验证工具:


主UT模块

子UT模块

*其他维护者陆续更新中

如果您对本项目感兴趣,欢迎申请成为本项目中的维护者。