├── .gitbook.yaml ├── .github └── workflows │ └── main.yml ├── .gitignore ├── README.md ├── SUMMARY.md ├── 介绍 ├── NutShell-arch.png ├── NutShell-tutorial.md └── introduction.md ├── 其他 ├── debug.md ├── diff-test.png ├── env.md ├── setting.md └── setting.svg ├── 功能部件 ├── address-division.svg ├── bpu.md ├── cache-module.svg ├── cache.md ├── csr.md ├── lsu.md ├── pic │ ├── BPU-NutShell.jpg │ ├── BTB-NutShell.jpg │ ├── MEMFSM-NutShell.png │ └── PHT-NutShell.jpg ├── tlb.md └── tlb.svg ├── 流水线 ├── exu.md ├── idu.md ├── ifu.md ├── isu.md ├── wbu.md └── wbu.svg ├── 系统 ├── bus.md ├── mem-model.svg ├── mem.md ├── mmio.svg ├── peripheral-real.svg └── peripheral.md └── 设计文档 └── 乱序核 ├── Argo技术文档.md └── pic ├── argo-frontend.png ├── argo-frontend.svg ├── argo-microarchitecture.png └── argo-microarchitecture.svg /.gitbook.yaml: -------------------------------------------------------------------------------- 1 | root: ./ 2 | 3 | structure: 4 | readme: README.md 5 | summary: SUMMARY.md 6 | 7 | redirects: 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Build gitbook 2 | name: CI 3 | 4 | # Controls when the action will run. Triggers the workflow on push or pull request 5 | # events but only for the master branch 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | # This workflow contains a single job called "build" 15 | build: 16 | # The type of runner that the job will run on 17 | runs-on: ubuntu-latest 18 | 19 | # Steps represent a sequence of tasks that will be executed as part of the job 20 | steps: 21 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 22 | - uses: actions/checkout@v2 23 | 24 | - name: Publish Gitbook 25 | uses: tuliren/publish-gitbook@v1.0.0 26 | with: 27 | # Github token for the repo 28 | personal_token: ${{ secrets.PERSONAL_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac dir 2 | .DS_Store 3 | 4 | # VS Code 5 | .vscode/ 6 | 7 | # VS Code 8 | _book/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NutShell 文档 2 | 3 | 该 repo 和 github.io 绑定, 域名为 https://oscpu.github.io/NutShell-doc/, 欢迎访问! 4 | 5 | 备用部署域名:https://oscpu.gitbook.io/nutshell 6 | 7 | Gitbook 构建工作目前已移交给 Github-CI 完成; 文档改写完成后, 本地可运行 `gitbook serve ./ ./building` 查看的生成网页, 但请不要把 ./building/ push 到主线上. 8 | 9 | 10 | 11 | ## 文档编写规范 12 | 13 | 1. 文档格式是 Markdown, 使用中文编写, 可夹杂英文;在使用英文的缩写前, 保证本页面或相近页面出现过对应的全称解释, 比如: 14 | 15 | ``` 16 | 我们的分支预测器使用了分支目标缓冲器(Branch Target Buffer, BTB)这一结构, ...... 17 | ...... 18 | 目前 BTB 中包含了 ...... 19 | ``` 20 | 21 | 2. 说明结构的时候尽量配图, 如果嫌麻烦的话可以手画一张草图传上去然后 后续使用软件画出来做润色 22 | 23 | 3. 写的时候要记住文档给面向一般读者, 所以多写一些总览框架, 不必拘泥于类似信号的含义这样的细节 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # NutShell 2 | 3 | * [README](README.md) 4 | 5 | ### 介绍 6 | 7 | * [NutShell (果壳) 处理器核](介绍/introduction.md) 8 | * [快速上手教程](介绍/NutShell-tutorial.md) 9 | 10 | ---- 11 | 12 | ### 流水线设计细节 13 | 14 | * [取指级](流水线/ifu.md) 15 | * [译码级](流水线/idu.md) 16 | * [发射级](流水线/isu.md) 17 | * [执行级](流水线/exu.md) 18 | * [写回级](流水线/wbu.md) 19 | 20 | ---- 21 | 22 | ### 功能部件设计细节 23 | 24 | * [访存单元](功能部件/lsu.md) 25 | * [CSR单元](功能部件/csr.md) 26 | * [分支预测单元](功能部件/bpu.md) 27 | * [Cache](功能部件/cache.md) 28 | * [TLB](功能部件/tlb.md) 29 | 30 | ---- 31 | 32 | ### 系统设计 33 | 34 | * [访存系统](系统/mem.md) 35 | * [外设系统](系统/peripheral.md) 36 | * [总线](系统/bus.md) 37 | 38 | ---- 39 | 40 | ### 其他 41 | 42 | * [配套生态](其他/env.md) 43 | * [参数化配置](其他/setting.md) 44 | * [调试指南](其他/debug.md) 45 | 46 | -------------------------------------------------------------------------------- /介绍/NutShell-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/介绍/NutShell-arch.png -------------------------------------------------------------------------------- /介绍/NutShell-tutorial.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # NutShell处理器核快速上手教程 4 | 5 | 6 | 7 | ## 源代码结构 8 | 9 | 项目 Repository 建立在 Scala 框架下, 其中主要的的目录和文件如下 10 | 11 | ``` 12 | . 13 | ├── debug/ # 处理器核测试脚本 14 | ├── fpga/ # 用于FPGA平台调试运行的相关文件 15 | ├── project/ # 构建SBT项目的相关配置文件 16 | ├── src/ # 处理器核源代码 17 | ├── script/ # 其他脚本文件 18 | ├── tool/ # 其他工具 19 | ├── Makefile 20 | └── README.md # 项目介绍 21 | ``` 22 | 23 | ./src 下的文件是项目最核心的处理器核源代码, 简要说明如下 24 | 25 | ``` 26 | ./src 27 | ├── main/scala 28 | │ ├── bus # 总线相关 29 | │ ├── device # 输入输出设备相关 30 | │ ├── nutcore # 核心相关 31 | │ ├── sim # 仿真相关 32 | │ ├── system # 外围系统 33 | │ ├── top # 项目顶层文件及配置文件 34 | │ └── utils # 工具相关 35 | └── test 36 | ├── csrc # C++测试文件, 主要用于Verilator仿真项目构建 37 | ├── vsrc # Verilog测试文件 38 | └── scala # Scala测试文件 39 | ``` 40 | 41 | 42 | 43 | ## 准备工作 44 | 45 | 推荐在 Ubuntu 18.04 或者 Debian 10 以上的 Linux 发行版环境中运行本项目. 46 | 47 | ### 安装 Mill 48 | 49 | * 请参考[该指南的Manual部分](https://www.lihaoyi.com/mill/#manual) 50 | 51 | ### 安装 GNU RISCV 工具链 52 | 53 | Ubuntu 18.04 或者 Debian 10 以上: 54 | 55 | ```bash 56 | sudo apt-get install g++-riscv64-linux-gnu 57 | ``` 58 | 59 | 其他平台 60 | 61 | * 选项一: 使用我们预编译的[工具链](https://github.com/LvNA-system/labeled-RISC-V/releases/download/v0.1.0/riscv-toolchain-2018.05.24.tar.gz) (未测试) 62 | 63 | * 选项二: 从源文件编译安装[工具链](https://github.com/riscv/riscv-tools) 64 | 65 | ### 安装 Verilator 66 | 67 | Ubuntu 18.04 或者 Debian 10 以上: 68 | 69 | ```bash 70 | sudo apt-get install verilator 71 | ``` 72 | 73 | 其他平台 74 | 75 | * 参考[官方教程](https://www.veripool.org/projects/verilator/wiki/Installing)从源文件编译安装 76 | 77 | 78 | 79 | ## 关联项目 80 | 81 | > 运行和测试 NutShell 核心还需要一些关联项目的辅助, 来提供核上运行时, 操作系统, 以及行为对比验证等 82 | 83 | ### NEMU 84 | 85 | NEMU (NJU Emulator) 是一个简单但完整的全系统模拟器, 目前支持 x86, mips32, riscv32, riscv64 指令集. 在一生一芯项目中, NEMU 作为一个重要的基础设施, 被用来与处理器核作对比仿真验证. 86 | 87 | NEMU 的源代码参见[这里](https://github.com/OpenXiangShan/NEMU). 88 | 89 | ### AM 90 | 91 | AM (Abstract Machine) 是一个向程序提供运行时环境的包装库, 它提供了一个面向裸金属的运行时环境, 把程序与体系结构进行了解耦. 我们只要在 AM 的框架下编写好程序, 就能方便地运行在 NEMU 和 Nutshel 之上. AM 在一生一芯项目中被用来包装一系列测试程序从而验证核心的正确性. 92 | 93 | * AM 项目的完整源码由于课程设计需求暂不公开, 我们也提供了一份 NutShell 特供版本在[此处](https://github.com/OSCPU/nexus-am). 它的安装运行过程和相关概念请参考南京大学计算机系统基础课程的[PA2部分](https://nju-projectn.github.io/ics-pa-gitbook/ics2019/2.3.html). 94 | 95 | ### RISCV-PK & RISCV-LINUX 96 | 97 | RISCV-PK (The RISC-V Proxy Kernel) 是一个轻量的 RISCV 运行时环境, 它能够将搭载程序的 I/O 系统请求代理到主机来完成. 我们项目中没有这样的需求, 而是主要使用了其中的 BBL (Berkeley Boot Loader) 部分, 它为 Linux 内核的运行提供预先准备, 包括注册 M mode 中断处理请求, 读取并解析设备树, 设置相应 CSR 寄存器值等等. 当 BBL 运行结束, 它就会将控制器正式交给内核. 98 | 99 | OSCPU 项目组中的 riscv-linux 工程内置了 BBL, 也包含了一个最精简的 Linux 内核, ELF文件仅有1.4MB 大小, 初始 init 为一个输出 Hello World 的小程序. 100 | 101 | 102 | 103 | ## 仿真运行流程 104 | 105 | 请参考[此处](https://github.com/OSCPU/NutShell#run-programs-by-simulation)的说明来使用我们预编译好的映像文件进行处理器核的仿真运行. (推荐) 106 | 107 | 如果要手动编译运行映像, 我们以 Microbench 作为例子进行说明. 108 | 109 | 110 | 111 | ### Microbench 112 | 113 | Microbench 是一个建立在 AM 之上的基准测试程序, 位置在 nexus-am/apps/microbench/. 114 | 115 | * 准备好 NutShell, NEMU, AM 这三个项目, 做好 NutShell 的 Setting 工作 116 | * 设置三个环境变量 117 | * `NEMU_HOME` = NEMU 项目的**绝对路径** 118 | * `NUTSHELL_HOME` = NutShell 项目的**绝对路径** 119 | * `AM_HOME` = AM 项目的**绝对路径** 120 | 121 | * 进入 nexus-am/apps/microbench/, 执行 122 | 123 | ```bash 124 | make ARCH=riscv64-nemu mainargs=test run 125 | ``` 126 | 127 | 该命令首先会使用 AM 运行时框架编译 Microbench 源代码, 形成一个内存映像二进制文件 (.bin), 然后让该文件载入进 NEMU 模拟器的内存中运行, 最后可以在终端中看到相应的输出以及最后的 Pass 字样. 128 | 129 | 其中, ARCH 指定 AM 的编译条件, `-` 之前部分指示指令集, `-` 之后部分指示运行平台; mainargs 指定目标程序的传入参数, 在 Microbench 中可以传入 "test", "train", "ref" 来设定测试规模. 130 | 131 | * 进入 nexus-am/apps/microbench/, 执行 132 | 133 | ```bash 134 | make ARCH=riscv64-nutshell mainargs=test run 135 | ``` 136 | 137 | 该命令与上面唯一的不同是让 Microbench 运行到了 NutShell 的仿真平台之上, 如果顺利的话可以看到终端输出了和 NEMU 上一致的内容(除了运行时间有差异). 138 | 139 | 值得注意的是, 第一次运行该命令会从头开始 build NutShell 项目, 通常第一次 build 的时间会非常漫长, 这是因为 Mill 工具会下载依赖的 Scala/Java 包, 这些包在国内的网络环境中下载速度很慢. 一旦第一次构建成功后, 后续就不会重复下载了, build 速度会快很多. 140 | 141 | 183 | -------------------------------------------------------------------------------- /介绍/introduction.md: -------------------------------------------------------------------------------- 1 | # NutShell 处理器介绍 2 | 3 | NutShell 是使用 Chisel 语言模块化设计的, 基于 RISC-V RV64 开放指令集的顺序单发射处理器实现, 隶属于国科大与计算所“一生一芯”项目. 这款处理器的很多设计受到了 NOOP (南京大学教学用五级流水线处理器) 的影响. 4 | 5 | NutShell 基于 9 级流水线顺序设计. 存储系统方面, NutShell 包含一级指令缓存和数据缓存及可选的二级缓存. 处理器通过 AXI4 总线与外界相连. NutShell 支持 M、S、U 三个特权级, 支持 I、M、A、C、Zicsr 与 Zifencei 指令扩展, 支持虚实地址转换, 包含页表缓冲 (TLB) 以加速地址转换过程, 支持 Sv39 分页方案. 6 | 7 | 整体的微结构设计如下图所示: 8 | 9 | ![](NutShell-arch.png) 10 | 11 | NutShell 的设计由三部分组成: 负责分支预测和取指的前端 (Front-end), 负责执行指令的执行引擎 (Execution Engine, 也称后端 Back-end), 以及用于访存操作的访存单元 (LSU). 前端完成取指与译码操作后会将指令放置在译码缓冲区中. 后端从译码缓冲区中读出指令并顺序执行. 访存单元作为一个功能单元 (FU) 包含在后端流水线中. 控制逻辑分布在流水线的各个部分. -------------------------------------------------------------------------------- /其他/debug.md: -------------------------------------------------------------------------------- 1 | # 调试指南 2 | 3 | 依托 Chisel/Scala 的高级语言特性和 Verilator 模拟器的强大功能, NutShell 在调试方面十分方便, 基本可以摆脱传统 Verilog 开发中根据信号波形进行调试的方式. 下面将介绍一些我们在开发过程中的调试技巧. 4 | 5 | ## 差分测试框架 6 | 7 | 差分测试框架是 NutShell 开发的一大亮点, 也是我们实现敏捷开发的关键点之一. 使用该方法, 我们创造了 2 天修复 6 个启动 Debian 过程中复杂 Bug 的奇迹, 且均为一次定位, 无需通过波形回溯. 整个差分测试框架的结构图如下所示: 8 | 9 | ![](diff-test.png) 10 | 11 | 其中, NEMU (NJU Emulator) 是一个南京大学的教学模拟器, 它的正确性由 QEMU 作为保证. 我们通过对比每一条指令执行后处理器的状态与 NEMU 的执行状态, 来判断处理器是否正确地执行了当前指令. 具体的实现细节参考 `src/test/csrc/difftest.c` 和 `src/test/csrc/emu.h` 等文件. 当仿真时出现对比失败, 则会自动停止仿真, 同时打印出出错位置的寄存器值和最近执行的指令与 PC. 12 | 13 | 差分测试是默认开启的, 如果想要关闭, 可以手动设置 `src/main/scala/sim/DiffTest.scala` 里 `enable` 的初始值为 false.B, 或者在测试程序中加入对 `AXI4DiffTestCtrl` 这一外设的读写. 14 | 15 | 16 | 17 | ## 日志调试 18 | 19 | 在日常开发代码功能的过程中, 我们最常使用的是日志调试的方式. 在代码中调用 printf() 函数即可在仿真时打印出日志, 常见的使用方式有如下: 20 | 21 | * C语言格式:printf("Log: %x, %d\n", signal1, signal2) 22 | * Scala语言格式:printf(p"Log: \${signal1}, \${signal2}\n") 23 | 24 | 以上两种语句均会在每一拍输出一条对应的 Log, 我们也提供了一个 GTimer() 函数返回当前的运行周期数, 方便在调试的时候作为参考. 另外, 日志的输出可以放在 Chisel 的条件语句中, 当且仅当该周期的条件为真时才会打印出该条日志, 示例如下: 25 | 26 | ``` 27 | when (io.flush || io.dtlb.req.fire()) { 28 | printf(p"Time: ${GTimer()}, Fire: ${io.dtlb.req.fire()}\n") 29 | } 30 | ``` 31 | 32 | ### Debug 调试框架 33 | 34 | 我们在日志输出的基础上建立了一个 Debug 框架, 实现参考 src/main/scala/utils/Debug.scala, 开发者只需要在代码的相应位置调用 Debug() 函数包络日志输出语句, 即可以将日志输出的控制权交由一个可配置的 ”EnableDebug“ 开关, 避免了在测试完成后需要手动删除所有的日志输出语句. 示例如下: 35 | 36 | ``` 37 | Debug(){ 38 | printf("xxxxxx") // 无条件输出 39 | when(xxx){ // 条件输出 40 | printf("xxxxxx") 41 | } 42 | } 43 | ``` 44 | 45 | 46 | 47 | ## 波形调试 48 | 49 | 在一些必要的情况下, 可能开发者还是需要使用波形来进行调试, 我们在设计中也保留了这一功能. 在 `Makefile` 中 `VERILATOR_FLAGS` 的赋值里添加额外添加两个参数 `--debug --trace` 即可在仿真时生成 VCD 波形文件. -------------------------------------------------------------------------------- /其他/diff-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/其他/diff-test.png -------------------------------------------------------------------------------- /其他/env.md: -------------------------------------------------------------------------------- 1 | # 配套生态 2 | 3 | 在 OSCPU 项目组中, 我们还提供了一系列测试程序与操作系统, 包括: 4 | 5 | * nexus-am 6 | * FreeRTOS 7 | * RT-Thread 8 | * xv6 9 | * Linux Kernel 10 | 11 | 下面将简要介绍一下如何在 NutShell 上测试与运行这些配套的生态软件, 部分内容在 `快速上手教程` 中已经提过, 这里不再赘述. 12 | 13 | ### nexus-am 14 | 15 | AM的相关内容在 `快速上手教程` 里有所介绍. 我们在 AM 中提供了一个新的 riscv64-nutshell 抽象机器用于对 NutShell 处理器进行测试. 配置好 AM 的运行环境后, 在需要运行的 APP 或 Test 中执行 `make ARCH=riscv64-nutshell run` 即可. 16 | 17 | ### FreeRTOS 18 | 19 | 在 Demo/riscv64/ 目录下执行 `make nutshell` 20 | 21 | ### RT-Thread 22 | 23 | 参考工程 README 24 | 25 | ### xv6 26 | 27 | 在根目录下执行 `make nutshell` 28 | 29 | ### Linux Kernel 30 | 31 | 克隆 riscv-rootfs 项目, 根据应用的需求编译好 rootfsimg. 32 | 33 | 克隆 riscv-linux 项目, 配置使用 riscv 架构下的 `emu_defconfig`, 手动额外指定一下 rootfs 的位置为之前编译好的 initramfs, 然后进行编译 34 | 35 | 克隆 riscv-pk 项目, 根据 `快速上手教程` 里的步骤操作, 最后执行 `make nutshell` 36 | 37 | -------------------------------------------------------------------------------- /其他/setting.md: -------------------------------------------------------------------------------- 1 | # 参数化配置 2 | 3 | NutShell 支持参数化配置, 目前经过验证的核心配置集有小核、顺序核、乱序核, 运行环境配置集有仿真, FPGA (支持 pynq 和 axu3cg), 相关配置集已经预设在代码中. 4 | 5 | 6 | 7 | 8 | 9 | 其中, 顺序核是本文档主要针对介绍的, 小核以顺序核为基础进行裁剪;而乱序核部分则是王华强同学的毕业设计内容, 现已融入 NutShell 配置框架中. 10 | 11 | 在 `src/main/scala/top/Settings.scala` 中给出了目前支持可配置的各个参数, 截取默认的配置如下所示: 12 | 13 | ``` 14 | object DefaultSettings { 15 | def apply() = Map( 16 | "MemMapBase" -> 0x0000000000000000L, 17 | "MemMapRegionBits" -> 0, 18 | "MMIOBase" -> 0x0000000040000000L, 19 | "MMIOSize" -> 0x0000000040000000L, 20 | "ResetVector" -> 0x80000000L, 21 | "NrExtIntr" -> 1, 22 | 23 | "HasL2cache" -> true, 24 | "HasPrefetch" -> true, 25 | "EnableMultiIssue" -> false, 26 | "EnableSuperScalarExec" -> false, 27 | "EnableOutOfOrderExec" -> false, 28 | "HasDTLB" -> true, 29 | "HasITLB" -> true, 30 | "HasDcache" -> true, 31 | "HasIcache" -> true, 32 | "MmodeOnly" -> false, 33 | "IsRV32" -> false, 34 | 35 | "EnableILA" -> true, 36 | "EnableDebug" -> false, 37 | "EnableRVC" -> true, 38 | "EnableCoh" -> true, 39 | "SDIMem" -> false 40 | ) 41 | } 42 | ``` 43 | 44 | 在工程的 Makefile 中, 你可以设定环境变量 `CORE` 和 `BOARD` 指定生成的处理器使用哪一种预设配置集, 默认为顺序核+仿真环境. 45 | 46 | ### 配置集测试情况 47 | 48 | 顺序核:可运行 AM 应用, Linux Kernel, Debian 等 49 | 50 | 乱序核:可运行 AM 应用, Linux Kernel 等 51 | 52 | 小核(32位):可运行 AM 应用等 -------------------------------------------------------------------------------- /其他/setting.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
小核
[Not supported by viewer]
顺序核
[Not supported by viewer]
乱序核
[Not supported by viewer]
SIM
SIM
pynq%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22Sim%22%20style%3D%22rounded%3D1%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22250%22%20y%3D%22340%22%20width%3D%2260%22%20height%3D%2230%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
[Not supported by viewer]
axu3cg
axu3cg
-------------------------------------------------------------------------------- /功能部件/address-division.svg: -------------------------------------------------------------------------------- 1 | 2 |
31
[Not supported by viewer]
tag
[Not supported by viewer]
index
[Not supported by viewer]
word index
[Not supported by viewer]
byte offset
[Not supported by viewer]
0
[Not supported by viewer]
3
[Not supported by viewer]
6
[Not supported by viewer]
13
[Not supported by viewer]
Division of 32-bit physical address in cache
<font><font style="font-size: 19px">Division of 32-bit physical address in cache</font><br></font>
-------------------------------------------------------------------------------- /功能部件/bpu.md: -------------------------------------------------------------------------------- 1 | # 分支预测单元 2 | 3 | 在冯诺依曼体系结构中, 指令供给的效率极大地影响了处理器的性能. NutShell 处理器核在取指单元中加入了硬件分支预测器, 用于在转移指令的取指阶段预测出指令跳转的方向和目标地址, 并从预测的目标地址处继续取指令执行, 在一定程度上减轻指令流水线中转移指令引起的阻塞, 从而提高处理器的性能. 4 | 5 | ## Next-Line分支预测器 6 | 7 | NutShell 核的前端每周期会在取指前一拍访问分支预测器, 并在下个周期返回结果. 如果预测正确, 指令流水线就不会断流. 我们采用了 Next-Line 分支预测器 (Next-Line Predictor, NLP), 通过综合考虑分支目标缓冲器 (Branch Target Buffer, BTB)、模式历史表 (Pattern History Table, PHT) 和返回地址栈 (Return Address Stack, RAS) 对不同类型的转移指令做出快速的预测. 整体的微结构如下图所示: 8 | 9 | 10 | 11 | 12 | 13 | ### 预测机制 14 | 15 | BTB 中缓存了跳转指令的地址高位信息、跳转目标、指令类型等信息. 在做分支预测时会用取指 PC 索引 BTB 表项, 如果 PC 高位与读到的 BTB 表项的标签匹配则认为 BTB 命中, 再根据 BTB 中记录的指令类型判断跳转方向和跳转目标. 如果类型为条件分支指令, 则需要访问模式历史表 (PHT) 来判断是否跳转; 如果类型为返回指令, 则选择返回地址栈 (RAS) 的栈顶内容作为跳转目标; 如果类型为直接或间接跳转指令, 则选择 BTB 中记录的跳转目标. 16 | 17 | 18 | 19 | 当分支预测器判断一条指令为条件分支跳转时, 需要访问 PHT 进一步对跳转方向做出预测. PHT 的每一项都是一个两位饱和计数器, 计数器的内容指示了强跳转、弱跳转、弱不跳转、强不跳转四种分支指令可能所处的状态, 该两位计数器的高位则指示了分支预测的方向. 20 | 21 | 在分支预测器中, RAS 和 PHT 用同步写异步读的寄存器来实现, BTB 由于面积较大而通过快速 SRAM 来实现, 因此在访问前者时需要将取指 PC 缓存一拍. 结合三者预测跳转方向和跳转目标可以在两拍之内完成, 因此如果预测正确, 将不会有取指空泡产生. 22 | 23 | ### 更新机制 24 | 25 | 在初始化阶段, BTB 所有表项都是无效的, 当跳转指令第一次被取出时都会按不跳转来处理. 后端的执行单元发现指令跳转目标错误或者方向错误时会刷新流水线, 同时更新BTB相应的表项. 如果取指 PC 对应的表项无效或已被占用, 则重新分配该表项. 26 | 27 | 与此同时, 后端每执行一条条件分支指令, 都会将正确跳转方向和分支目标等信息返回给分支预测单元, 分支预测器会更新 PHT, 按照如下图所示的状态机更新两位饱和计数器: 28 | 29 | 30 | 31 | 后端每执行一条函数调用指令 (call) 或函数返回指令 (ret), 也会将相关信息传给分支预测器. 如果该指令是 call 指令, 则根据指令的宽度 (4 字节或 2 字节) 计算返回地址并写入 RAS 栈顶; 如果该指令是 ret 指令, 则将RAS的栈顶弹出. 32 | -------------------------------------------------------------------------------- /功能部件/cache-module.svg: -------------------------------------------------------------------------------- 1 | 2 |
Cache array
read
[Not supported by viewer]
hit check
victim choose
[Not supported by viewer]
commit for hit
refill for miss
[Not supported by viewer]
Cache array
read
[Not supported by viewer]
Memory
[Not supported by viewer]
MMIO
[Not supported by viewer]
coherence
[Not supported by viewer]
refill read
[Not supported by viewer]
request
[Not supported by viewer]
request read
[Not supported by viewer]
hit write
[Not supported by viewer]
refill write
[Not supported by viewer]
write forward
[Not supported by viewer]
to CPU
[Not supported by viewer]
Read Mux
[Not supported by viewer]
Write Mux
[Not supported by viewer]
Stage 1
[Not supported by viewer]
Stage 2
[Not supported by viewer]
Stage 3
[Not supported by viewer]
-------------------------------------------------------------------------------- /功能部件/cache.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | 3 | NutShell 的数据 Cache 和指令 Cache 以及 L2 Cache 均采用一个可定制的 Cache 模块, 这个 Cache 模块的结构图如下所示. 4 | ![](cache-module.svg) 5 | 6 | ### Cache模块的结构 7 | Cache 模块可以分为存储部分 (Cache Array) 和控制逻辑部分 (control logic). 其中存储部分又可以分为元数据 (Meta Array) 和数据 (Data Array). 控制逻辑部分被划分分为三级流水线, 对外会提供对 Memory Mapped Input/Output (MMIO) 空间、Memory 空间的访问接口. 8 | 9 | 10 | 11 | ### 存储部分 12 | 13 | 指令 Cache 和数据 Cache 的存储的数据大小均为为 32KB. L2 Cache 则为 128KB. 映射方式采用的是四路组相联映射, Cache的替换策略为随机替换, 并采取写回的策略. 每个 Cache 行的大小为 64B. 14 | 顺序核 Cache 目前采用实地址作为标签 (tag), 实地址作为索引 (index). 因此访问 Cache 的地址为经过 TLB (Translation Lookaside Buffer) 进行虚实地址转化后的 32 位实地址. 地址划分如下图所示: 15 | ![](address-division.svg) 16 | 元数据项包含了每个 Cache 组 (Cache Set) 的标签 (tag)、有效位 (valid)、脏位 (dirty) 以及路掩码 (waymask). 17 | 18 | 19 | 20 | 21 | ### 控制逻辑 22 | 23 | Cache 的控制逻辑是一个三级流水线的结构. 阶段 1 (Stage 1) 接收来自 NutCore 或者其它 Cache 的访存请求, 这个访存请求的信息通过 SimpleBus 总线传递. 同时进行地址划分, 截取访存地址的索引并向 Cache Array 发送读请求, 读取这个索引对应的 Cache 组. 在下一拍返回数据. 阶段2(Stage 2) 得到上一级流水线传递过来的访存请求信息, 以及 Cache Array 返回的数据. 在这一阶段进行命中检查: 截取访存地址的标签, 并与该 Cache 组四路的 tag 进行比较. 命中则会生成 Cache hit 信号传递给阶段 3. 否则发送 Cache miss 信号, 并随机指定该组中的受害者 Cache 行 (Victim CacheLine) 以做 Cache 替换用. 阶段 3 (Stage 3) 根据阶段 2 的结果进行不同的处理: 24 | 25 | * Cache hit: 读指令 (load) 直接返回数据, 写指令 (store) 发送对 Cache Array 的写请求 (hit write). 写的内容包括写入的数据 (通过写掩码选择的写数据和命中的原始数据进行拼接), 以及将该 Cache 行的脏位置 1 表示该Cache 行的数据被改写过. 26 | 27 | * Cache miss: 当缓存中不存在访存请求所需要的数据时, 就会触发 Cache miss. 此时 Cache 需要通过 Memory 访问接口向下一级存储设备发 Burst 访存请求 (SimpleBus 总线支持 Burst 请求), 读取该 miss 地址的一个 Cacheline 的数据填入受害者 Cache 行. 而如果受害者 Cache 行的数据被修改过, 则需要在读取数据之前将该 Cache 行的数据写入下一级存储. 重填时我们采用了关键字优先 (critical word first) 技术, 即首先发送的 Burst 请求地址是 miss 的访存请求需要操作的字地址. 这样做的好处是当第一个字从下一级存储返回的时候就可以根据访存请求对其进行读写操作, 然后提交给处理器, 而不需要等待整个 Cache 行都返回才进行操作和提交. 在重填阶段, 阶段3会向 Cache Array 发送写请求 (refill write), 将返回的 Cache 行以每次写一个字 (8 Byte) 的形式分 8 次写入受害者 Cache 行. 由于我们顺序核采用的是 blocking Cache, 因此当发生 Cache miss 的时候会阻塞整个控制逻辑流水线, 停止处理新的访存请求. 28 | 29 | * MMIO 请求: MMIO 请求来自于处理器 NutCore 对 MMIO 地址空间的访问, 主要用于访问 MMIO 外设的寄存器和 RAM. 这部分地址空间由于不是主存中的数据, 因此是不会在 Cache 中命中的. 阶段 3 收到 MMIO 请求后,会类似 Cache miss 一样阻塞流水线, 但是通过 MMIO 访问接口对这部分地址进行读写, 在得到 response 后提交该 MMIO 请求. 不同于使用 Memory 访问接口, MMIO 的访问不是 Burst 请求, 意味着它一次请求只会对应一个字. 30 | 31 | 32 | 33 | ### L2 Cache预取器 34 | 35 | NutCore 处理器在 L2 Cache 中加入了 Next-line 预取器. 当触发 L1 Cache miss 时, L1 Cache 会向 L2 Cache 发送 Burst 请求, L2 Cache 预取器每收到一个 Burst 请求都会在下一拍向 L2 Cache 发送 Prefetch 请求. Prefetch 请求和读请求在 Cache 中的控制逻辑基本相同,只是最后不会响应或返回数据. -------------------------------------------------------------------------------- /功能部件/csr.md: -------------------------------------------------------------------------------- 1 | # CSR 单元 2 | 3 | CSR 单元主要负责特权级指令的执行以及例外/中断的处理. 在 NutShell 中, CSR 单元是流水线中的一个功能部件. 4 | 5 | > 在 NutShell 中, "CSR 寄存器" 和 "CSR 单元" 是不同的概念. "CSR 寄存器" 是指 RISC-V 手册中规定的控制与状态寄存器. "CSR 单元" 则是指执行级流水线中负责处理特权指令、中断、例外的功能单元. 6 | 7 | RISC-V 特权级是比较复杂的, 涉及到很多寄存器的读写与控制, 为了简化设计难度、提高代码的可读性, 我们构造了一个特殊的 MaskedRegMap 结构来优化特权级寄存器的读写, 并围绕这一设计给出相应的特权级控制逻辑. 针对每一个 CSR 寄存器构造 MaskedRegMap, 包括了以下这几个经过凝练的通用读写控制域: 8 | 9 | * CSR 寄存器索引 10 | * CSR 寄存器存储位置 11 | * 读、写掩码 12 | * 写副作用 (side effect) 函数 13 | 14 | 每当接受到读写请求时均会按照设定做出相应的修饰再最终读写物理寄存器. 具体实现细节请参考源码中的 RegMap.scala 15 | 16 | 在 CSR 单元中, 我们大量地使用了 BoringUtils 这一 Chisel 内置类来进行飞线, 这是因为很多其他的功能部件需要从 CSR 中获取到当前系统状态来实现相应功能 (比如 LSU, TLB 等). 17 | 18 | NutShell 默认支持以下的 CSR 寄存器. 要调整可使用的 CSR 以及设置 CSR 的初始值, 参见源码中的 CSR.scala: 19 | 20 | ``` 21 | # U 模式 22 | 1. Ustatus 2. Uie 3. Utvec 4. Uscratch 23 | 5. Uepc 6. Ucause 7. Utval 8. Uip 24 | # S 模式 25 | 1. Sstatus 2. Sedeleg 3. Sideleg 4. Sie 26 | 5. Stvec 6. Scounteren 7. Sscratch 8. Sepc 27 | 9. Scause 10. Stval 11. Sip 12. Satp 28 | # M 模式 29 | 1. Mvendorid 2. Marchid 3. Mimpid 4. Mhartid 30 | 5. Mstatus 6. Mstatus 7. Misa 8. Medeleg 31 | 9. Mideleg 10. Mie 11. Mtvec 12. Mcounteren 32 | 13. Mscratch 14. Mepc 15. Mcause 16. Mtval 33 | 29. Mip 34 | ``` 35 | 36 | 除访存例外之外的所有例外/中断均会在译码阶段被附加在指令上, 这样的指令随后会被发送给 CSR 单元执行. 37 | 38 | 访存例外的处理相对特殊. 在一条访存指令实际执行前, 我们无法判断这条访存指令是否会导致例外. 因此, 在访存指令发生例外时, 访存单元会将产生例外的指令以及例外信息转发到 CSR 单元, 由 CSR 单元控制 CSR 寄存器的变化以及指令的写回. -------------------------------------------------------------------------------- /功能部件/lsu.md: -------------------------------------------------------------------------------- 1 | # 访存单元 2 | 3 | 访存单元负责处理全部的访存指令, 若访存指令没有产生例外则由访存单元进行写回, 否则由 CSR 单元进行写回(参见[CSR单元](./csr.md)部分). 4 | 5 | 由于开发时间的限制, 访存部分并未做流水化处理. 访存单元通过一个状态机控制访存单元到 DCache 的握手. 目前, 访存单元是 NutShell 顺序核的最大性能瓶颈. 6 | 7 | 目前的访存状态机如下图所示: 8 | 9 | 10 | 11 | 与一致性相关的访存细节, 参见[访存系统](../系统/mem.md)部分. 12 | -------------------------------------------------------------------------------- /功能部件/pic/BPU-NutShell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/功能部件/pic/BPU-NutShell.jpg -------------------------------------------------------------------------------- /功能部件/pic/BTB-NutShell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/功能部件/pic/BTB-NutShell.jpg -------------------------------------------------------------------------------- /功能部件/pic/MEMFSM-NutShell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/功能部件/pic/MEMFSM-NutShell.png -------------------------------------------------------------------------------- /功能部件/pic/PHT-NutShell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/功能部件/pic/PHT-NutShell.jpg -------------------------------------------------------------------------------- /功能部件/tlb.md: -------------------------------------------------------------------------------- 1 | # TLB 2 | 3 | NutShell 支持 Sv39 分页方案和虚实地址转换, 因此页表缓冲 (TLB, Translation Lookaside Buffer) 是处理器的一个关键部件, 它实现得好坏直接决定了地址转换的性能. 4 | 5 | NutShell 包含一个 4 项四路组相连的指令页表缓冲 (ITLB) 和一个 64 项 4 路组相连的数据页表缓冲 (DTLB), 它们的结构图如下所示: 6 | 7 | 8 | 9 | ![](tlb.svg) 10 | 11 | 12 | 13 | TLB 采用四路组相连结构, 分为 ITLB 和 DTLB, 分别接收来自 IFU 和 LSU 的访存请求, 并将虚地址转换为实地址. 代码实现上, TLB 分为主模块和执行模块, 主模块负责处理 TLB 模块的 I/O 接口交互, 并将地址转换功能分给执行模块. 14 | 15 | 16 | 17 | ### 实现细节 18 | 19 | 当 TLB 命中时, 可一拍得到实地址结果. 如果不命中, 则将通过状态机方式实现的 HWPTW (HardWare Page Table Walker) 访存页表, 并进行对页表的填充, 同时会阻塞后续指令进入TLB. 20 | 21 | 为了保持 TLB 的一致性, ITLB 和 DTLB 都经过数据 Cache 进行访存, 当 sfence.vma 指令修改 satp 寄存器内容时, 需要将 TLB 内容刷为无效 (也可通过 asid 域避免 TLB 的刷新). Sv39 分页方案采用三级页表, 因此当 TLB 未命中时需要访问三次内存, 具有一定的性能损耗. 22 | 23 | 无论命中与否, 都需要根据读写行为和页表项状态位等判断权限是否合法, 以及是否需要回写页表项状态位. 如果 ITLB 权限不合法, 为了保证指令执行的正确顺序, 需要等待指令 Cache 清空后将例外信息传给流水线相应部件; 如果 DTLB 不合法, 由于访存已经处于执行阶段, 会直接将例外传给 LSU (Load Store Unit). 另外, 出于时序优化的考量, 实现中会将例外信息缓存一拍发出. 24 | 25 | -------------------------------------------------------------------------------- /功能部件/tlb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
TLB
Array
[Not supported by viewer]
Hit Check Logic
[Not supported by viewer]
PTW
[Not supported by viewer]
Req
Req
Read
Read
Write
Write
Miss
[Not supported by viewer]
Resp
Resp
Mem
Mem
-------------------------------------------------------------------------------- /流水线/exu.md: -------------------------------------------------------------------------------- 1 | # 执行级 2 | 3 | 执行 (Execute) 阶段接收发射级给出的源操作数和译码信息, 分派给相应的功能单元进行计算, 最后把计算结果提交到写回级. NutShell 包含以下标准功能单元: 4 | 5 | * ALU/BRU: 算数逻辑单元 ALU 负责处理整数运算指令. 跳转处理单元BRU 负责处理跳转指令. BRU 可以与 ALU 合并, 复用 ALU 的逻辑来计算跳转指令的条件和跳转地址, 也可以作为一个单独的功能单元接入到流水线中. 6 | 7 | * MDU: 乘除法单元 MDU 负责处理所有的乘法与除法指令. 目前的乘除法单元尚未流水化, 只支持一条指令在其中执行. 8 | 9 | * CSR/MOU: 控制与状态寄存器单元 CSR 与访存定序指令单元 MOU 用于处理各种特权指令. 与常规功能单元的区别为进入这个功能单元的指令会阻塞流水线. 详见功能部件部分的 [CSR单元](../功能部件/csr.md). 10 | * LSU: 访存单元 LSU 负责处理所有的访存指令. 详见功能部件部分的 [访存单元](../功能部件/lsu.md). 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /流水线/idu.md: -------------------------------------------------------------------------------- 1 | # 译码级 2 | 3 | 译码 (Decode) 阶段接收指令对齐缓冲给出的指令进行译码. 译码后的结果暂存到译码结果缓冲区中, 等待进入处理器后端执行部分. 4 | 5 | 译码部分主要使用了模式匹配方法, 支持 I、M、A、C、Zicsr、Zifencei 等扩展, 最终获得规则化的译码信息如下: 6 | 7 | ``` 8 | class CtrlSignalIO extends NutCoreBundle { 9 | val src1Type = Output(SrcType()) 10 | val src2Type = Output(SrcType()) 11 | val fuType = Output(FuType()) 12 | val fuOpType = Output(FuOpType()) 13 | val rfSrc1 = Output(UInt(5.W)) 14 | val rfSrc2 = Output(UInt(5.W)) 15 | val rfWen = Output(Bool()) 16 | val rfDest = Output(UInt(5.W)) 17 | val isNutCoreTrap = Output(Bool()) 18 | val isSrc1Forward = Output(Bool()) 19 | val isSrc2Forward = Output(Bool()) 20 | val noSpecExec = Output(Bool()) // This inst can not be speculated 21 | val isBlocked = Output(Bool()) // This inst requires pipeline to be blocked 22 | } 23 | 24 | class DataSrcIO extends NutCoreBundle { 25 | val src1 = Output(UInt(XLEN.W)) 26 | val src2 = Output(UInt(XLEN.W)) 27 | val imm = Output(UInt(XLEN.W)) 28 | } 29 | ``` 30 | 31 | 目前译码部分没有使用压缩指令展开逻辑将压缩指令展开成普通指令. 压缩指令与普通指令一样被直接译码. -------------------------------------------------------------------------------- /流水线/ifu.md: -------------------------------------------------------------------------------- 1 | # 取指级 2 | 3 | 取指 (Fetch) 阶段进行分支预测, 根据分支预测的结果更新程序计数器 PC, 同时向指令缓存 (ICache) 发出请求, 每一拍最多从 ICache 中读取一个 Cache Line (64bit) 的数据, 可换算为 2 条普通指令或者 4 条压缩指令. 指令缓存返回的结果会被压入到指令对齐缓冲 (IBF) 中, 由指令对齐缓冲识别指令边界并交给译码逻辑. 4 | 5 | 6 | 7 | ## 分支预测 8 | 9 | NutShell 使用 Next-Line 分支预测器 (NLP). 它负责给出下一周期前端指令缓存请求的地址. 在 Next-Line 分支预测器预测跳转不会发生的情况下, 前端会发出当前程序计数器 (PC)+8 的取指请求. 默认的前端分支预测器使用两位饱和计数进行预测, 提供 512 项的跳转目标缓存 (BTB) 和 16 项的返回地址栈 (RAS). Next-Line 分支预测器在更新时会记录跳转指令的种类, 随后会根据跳转指令的种类来进行不同的预测. Next-Line 分支预测器在后端跳转指令写回时更新. 针对 64bit 的取指宽度, NLP 会返回 4bit 的跳转预测向量. 跳转预测向量标识在哪条指令处可能发生跳转, 指令对齐缓冲将使用跳转预测向量来选择有效的指令. 关于分支预测器更多的细节请参考 “分支预测单元” 这一节. 10 | 11 | 12 | 13 | ## 指令对齐缓冲 14 | 15 | 为了处理引入 RISC-V 压缩指令 RVC 后的指令对齐问题, 前端设置了单独的指令对齐缓冲, 它会判断指令的长度并将不同的指令分离. 如果有指令横跨两次 ICache 结果, 指令对齐缓冲会负责将其拼接成一条指令, 将拼接好的指令交给指令译码单元 (IDU) 进行译码. -------------------------------------------------------------------------------- /流水线/isu.md: -------------------------------------------------------------------------------- 1 | # 发射级 2 | 3 | 处理器在流水线的这一级上读取寄存器堆, 并从流水线的后半部分获取前递的数据. 离开这一级的指令会获得其所需的全部源操作数. 4 | 5 | 数据前递的来源包括: 6 | 7 | 1. 执行级功能单元产生的结果 8 | 1. 写回级待写回的数据 -------------------------------------------------------------------------------- /流水线/wbu.md: -------------------------------------------------------------------------------- 1 | # 写回级 2 | 3 | 在写回 (Writeback) 阶段, 执行级各个功能部件完成其运算后将指令的结果传递过来. 如果指令能够顺利地传输到写回级, 则说明该指令被成功执行, 此后进入提交 (Commit) 状态, 对那些需要写回的指令, 把写寄存器堆请求发送给 ISU 执行. 4 | 5 | 另外, 在写回级还有一项重要的任务是进行指令的重定向 (Redirect) . 重定向主要会在一下几个场景中发生: 6 | 7 | * ALU 单元计算发现分支指令预测错误, 重定向程序计数器 PC 为正确的跳转地址 8 | * CSR 单元发现当前指令触发了异常或者产生了中断, 重定向程序计数器 PC 为相应的异常处理地址 9 | * MOU 单元发现当前指令为 fencei 或者 sfence_vma, 重定向程序计数器 PC 为 PC + 4, 即下一条指令 10 | 11 | 重定向一旦发生, 会导致写回级之前所有流水级中的指令被冲刷掉. 这保证了我们的处理器核是实现精确异常的, 也解释了为什么 MOU 也会触发重定向 (目的并不是改变执行流, 而是冲刷流水线). 12 | 13 | 功能单元在计算过程中产生重定向请求后, 不会立即触发, 而是会保留重定向信息, 顺着流水线传递到写回级, 统一由写回级传递给前端处理, 优先级在执行级(EXU)给出. 14 | 15 | 总结一下, 写回级与其他流水级的交互示意图如下: 16 | 17 | ![](wbu.svg) 18 | 19 | -------------------------------------------------------------------------------- /流水线/wbu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
EXU
[Not supported by viewer]
WBU
[Not supported by viewer]
ISU
[Not supported by viewer]
[Not supported by viewer]
IFU
[Not supported by viewer]
Register Write Request
<font style="font-size: 14px">Register Write Request</font>
Redirect Info
[Not supported by viewer]
Inst Info
[Not supported by viewer]
Redirect Request
[Not supported by viewer]
RF
[Not supported by viewer]
-------------------------------------------------------------------------------- /系统/bus.md: -------------------------------------------------------------------------------- 1 | # 总线 2 | 3 | 我们使用 SimpleBus 作为 NutCore 的访存总线, 它的设计借鉴了 AMBA, TileLink 等总线的思想, 根据我们的需求加入了一定的功能, 是专门为本项目设计的一套最小满足功能需求的总线. 4 | 5 | 目前, SimpleBus 有两个实现层级, 分别是 SimpleBusUC 和 SimpleBusC. 6 | 7 | ## SimpleBusUC 8 | 9 | SimpleBusUC 是 SimpleBus 的最基本实现, 用于非 Cache 的访存通路中, 它包含了 req 和 resp 两个通路, 使用 Decoupled 方式握手, 信号细节如下: 10 | 11 | | req 信号名称 | 位宽 | 注释 | 12 | | :----------- | ---------- | ----------------------------------------------------- | 13 | | req.addr | AddrBits | 访存地址(位宽与体系结构实现相关) | 14 | | req.size | 3 | 访存大小(访存Byte = 2^(req.size)) | 15 | | req.cmd | 4 | 访存指令, 详见 SimpleBus.scala 中 SimpleBusCmd 的实现 | 16 | | req.wdata | DataBits | 内存写数据(位宽与体系结构实现相关) | 17 | | req.wmask | DataBits/8 | 内存写掩码 | 18 | | req.user | UserBits | 用户自定义数据, 在访存过程中不被修改 | 19 | | req.id | IdBits | 标识访存请求的来源, 在访存过程中不被修改 | 20 | 21 | 22 | 23 | | resp 信号名称 | 位宽 | 注释 | 24 | | ------------- | -------- | ------------------------------------ | 25 | | resp.cmd | 4 | 访存状态回复 | 26 | | resp.rdata | DataBits | 访存读数据 | 27 | | resp.user | UserBits | 用户自定义数据, 在访存过程中不被修改 | 28 | | resp.id | IdBits | 标识访存请求的来源, 在访存过程中不被修改 | 29 | 30 | SimpleBus的id通道在NutShell顺序核的设计中并没有被使用. 31 | 32 | 我们也内置了基于 SimpleBusUC 的各类 CrossBar, 经过了一定的验证, 方便开发者进行复用. 33 | 34 | 35 | 36 | ## SimpleBusC 37 | 38 | SimpleBusC 在 SimpleBusUC 的基础上增加了与一致性相关的功能, 用于 Cache 的访存通路中, 本质上是由两个 SimpleBusUC 组合而成的: 39 | 40 | ``` 41 | class SimpleBusC(val userBits: Int = 0) extends SimpleBusBundle { 42 | val mem = new SimpleBusUC(userBits) 43 | val coh = Flipped(new SimpleBusUC(userBits)) 44 | } 45 | ``` 46 | 47 | 其中 mem 是访存通道, coh 是一致性维护通道. 48 | 49 | 50 | 51 | ## 与 AXI4 的转换 52 | 53 | 考虑到在 FPGA 验证和实际流片过程中, 相关 IP 接口通常是标准化的总线协议(比如 AMBA 系列), 我们无法直接让 SimpleBus 总线接入外设和内存, 因此我们加入了 SimpleBus 到 AXI4 的转换部件, 最终以 AXI4 协议的形式暴露给 SoC, 详见[访存系统](./mem.md)和[外设系统](./peripheral.md)章节. -------------------------------------------------------------------------------- /系统/mem-model.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Frontend
Frontend
Backend
Backend
ICache
ICache
DCache
DCache
TLB
TLB
DMA
DMA
Coh Manager
Coh Manager
ToAXI4
ToAXI4
AXI4-RAM
AXI4-RAM
NutCore
NutCore
-------------------------------------------------------------------------------- /系统/mem.md: -------------------------------------------------------------------------------- 1 | # 访存系统 2 | 3 | NutShell 的访存模型示意图如下所示: 4 | 5 | 6 | 7 | ![](mem-model.svg) 8 | 9 | 10 | 11 | 其中, NutCore 核心的访存请求从 ICache 和 DCache 里发出, 由 Coherence Manager 维护一致性, 最后将其转换为 AXI4 总线单读写口的访存请求接入到 RAM 中. 12 | 13 | ### 内存一致性 14 | 15 | NutShell 满足缓存一致性. 从维护内存一致性的角度出发, 我们将 TLB 与 DMA 的访存请求接入到 DCache 中, 而不是接入内存之前的 CrossBar 里. 16 | 17 | ### 访存总线 18 | 19 | 除了与 AXI4-RAM 的交互使用 AXI4 总线, 其余所有的访存请求均采用 SimpleBus 总线. 与总线相关的内容, 请参考 [总线](bus.md) 章节. 20 | 21 | -------------------------------------------------------------------------------- /系统/mmio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Frontend
Frontend
Backend
Backend
ToAXI4
ToAXI4
UART
UART
Flash
Flash
Keyboard
Keyboard
Timer
Timer
Keyboard
Keyboard
mmio
[Not supported by viewer]
mmio
[Not supported by viewer]
-------------------------------------------------------------------------------- /系统/peripheral-real.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
SDRAM
SDRAM
SDRAM controller
SDRAM controller
NutCore
NutCore
AXI-to-APB
AXI-to-APB
UART
UART
GPIO
GPIO
SPI_Flash
SPI_Flash
ETHMAC
ETHMAC
SDC
SDC
APB
[Not supported by viewer]
APB
[Not supported by viewer]
APB-to-AXI
APB-to-AXI
AXI-Frontend
AXI-Frontend
AXI-MEM%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20value%3D%22AXI-Frontend%22%20style%3D%22text%3Bhtml%3D1%3BstrokeColor%3Dnone%3BfillColor%3Dnone%3Balign%3Dcenter%3BverticalAlign%3Dmiddle%3BwhiteSpace%3Dwrap%3Brounded%3D0%3BfontSize%3D16%3B%22%20vertex%3D%221%22%20parent%3D%221%22%3E%3CmxGeometry%20x%3D%22120%22%20y%3D%22270%22%20width%3D%22140%22%20height%3D%2220%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E
[Not supported by viewer]
AXI-MMIO
AXI-MMIO
AXI-Frontend
AXI-Frontend
-------------------------------------------------------------------------------- /系统/peripheral.md: -------------------------------------------------------------------------------- 1 | # 外设系统 2 | 3 | 本节主要介绍 NutShell 所使用的外设系统. 由于实现的原因, 我们在仿真和流片版本使用了不同的外设系统, 其中仿真外设示意图如下: 4 | 5 | ![](mmio.svg) 6 | 7 | 8 | 9 | 流片外设示意图如下: 10 | 11 | ![](peripheral-real.svg) 12 | 13 | 14 | 15 | ### NutShell SoC 16 | 17 | NutShell SoC 主要由 NutCore 处理器核, 各个外设 IP 核以及 SDRAM 控制器组成. NutCore 提供了对外的 AXI-MEM 接口 (AXI4 协议), 与 SDRAM 控制器连接. 而 MMIO 地址空间访问则是通过 AXI-MMIO (AXI-Lite协议) 接口, 首先信号会被转换为 APB 协议总线信号. APB 信号选择器会根据访问的地址空间将 NutShell 的访问信号 (master) 传递给不同的外设控制器 (slave), 达到对 MMIO 空间进行访问的目的. 18 | 19 | 各个外设 IP 会将 APB 信号转换为内部控制器的信号, 来进行对外设 IP 内部寄存器的读写. 而 ETHMAC 以及 SDC 两种外设还需要实现对主存进行读写, 因此这两个外设的信号也同时能作为 APB master, 转换为 AXI-frontend (AXI4协议), 接入到 NutCore. 这个信号在 NutCore 内部会转换为一个对 Data Cache 的 SimpleBus 访存请求. 20 | 21 | 22 | 23 | ### 外设 24 | 25 | 我们的外设 IP 主要来源于以 OpenCore 为代表的开源网站. 以下是对各个外设的详细介绍: 26 | 27 | * UART (Universal Asynchronous Receiver/Transmitter), 用于NutCore对串口的输入输出. 28 | * GPIO (General-Purpose Input/Output), 我们使用 4 个 GPIO 输入端口作为我们的中断输入端口, 连接到各个外设 IP 的中断信号. 当外设产生中断时, 会使能 GPIO 中断并向 NutCore 处理器发送中断. 29 | * SPI_Flash, 我们使用 SPI (Serial Peripheral Interface) 信号端口的 Flash 来作为我们的 ROM, NutCore 会从 SPI_Flash 中读取第一条指令. 30 | * ETHMAC, 以太网控制器, SoC 通过 Tx 端发送以太网帧. 而当 Rx 接收到数据时, 会作为 APB master 向 NutCore 发送 AXI-frontend 写请求写入 DMA 接收地址. 31 | * SDC (Secure Digital Card), 已经流片的板子上我们实现了 SDHC (Secure Digital High Capacity) 以及 SPI 两种接口的 SDC controller 32 | -------------------------------------------------------------------------------- /设计文档/乱序核/Argo技术文档.md: -------------------------------------------------------------------------------- 1 | Argo Processor Technical Reference Manual 2 | ===================== 3 | 4 | * Copyright (c) 2019-2020 Huaqiang Wang. All rights reserved. 5 | * wanghuaqiang16@mails.ucas.ac.cn 6 | 7 | NOTE: 这里面的内容纯属画饼, 如果实现, 纯属巧合. 8 | 9 | 在没有特殊说明的情况下, 每个部分中列出的列出的技术要点按从上至下的路线实现. 10 | 11 | --- 12 | 13 | # 1. 文档历史 14 | 15 | Date|Change 16 | -|- 17 | 2019.12.23|确定技术路线 18 | 2020.3.3|超标量微架构框架 19 | 2020.4.5|前后端基本说明 20 | 2020.4.14|完善跳转取消部分的说明 21 | 2020.4.22|完善访存子系统说明 22 | 2020.7.12|将原始的文档迁移到NutShell文档中 23 | 24 | --- 25 | 26 | # 2. 目录 27 | 28 | [TOC] 29 | 30 | --- 31 | 32 | # 3. 总述 33 | 34 | Argo是使用Chisel语言设计的, 基于 RISC-V RV64IMAC 开放指令集的乱序多发射处理器实现. Argo采用基于隐式重命名(Rename-In-ROB)的乱序设计, 采用分布式保留站, 分派时读取寄存器的设计模式. 存储系统方面, Argo包含一级指令缓存和数据缓存及可选的二级缓存. 处理器通过AXI4总线与外界相连. Argo支持M, S两个特权级, 支持Zicsr与Zifence指令集扩展. 支持虚实地址转换, 包含页表缓冲(TLB)以加速地址转换过程. 35 | 36 | 37 | 38 | Argo的开发受到了`BOOM`的很大影响. Argo包括前端取指部分, 后端执行部分和访存处理部分这三大主要部分. 39 | 40 | ![](pic/argo-microarchitecture.png) 41 | 42 | `Argo`的技术路线是, 先实现长流水的前端与简单顺序后端以进行功能验证. 在此过程中完善测试框架并验证其可靠性. 实现RISC-V指令集上的各个指令并验证实现的正确性, 以确保能正确理解RISC-V指令集. 这一版本被称为顺序单发射版本. 在顺序单发射版本上尝试启动Linux等复杂操作系统, 验证特权指令与虚拟存储相关机制的实现. 在验证通过后尝试流片. 此后进行顺序双发射版本的开发. 在顺序单发射版本的基础上扩展前端的宽度, 同时在后端添加简单的顺序双发射逻辑. 通过这一版本验证超标量前端逻辑的正确性, 并设计超标量处理器核的正确性比对逻辑. 在最终阶段, 重构后端实现为乱序实现, 在这一基础上针对之前版本中简化的逻辑进行扩展, 以尽可能地提高乱序核的性能表现. 可见, 乱序处理器的模块是在这三个阶段中逐步完成的. 通过逐步迭代设计可以避免同时引入大量新模块, 导致错误难以定位的问题. 事实证明, 逐步迭代设计是正确的开发思路. 下面将简要介绍前两个版本的设计. 43 | 44 | 处理器版本|前端实现|后端实现|访存实现 45 | -|-|-|- 46 | 顺序单发射 NOOP-RV64|标量取指单元|标量执行单元|多周期访存单元 47 | 顺序双发射 NOOP-RV64D|Jason超标量取指单元|超标量执行单元(主副流水线)|多周期访存单元 48 | 乱序双发射 Argo|Jason超标量取指单元|Hercules乱序执行单元|Achilles流水化访存单元(支持乱序) 49 | 50 | 表: 不同版本的处理器实现所采用的的模块 51 | 52 | ## 3.1. 顺序单发射 53 | 54 | TODO: PIC 55 | 56 | 图1: 顺序单发射处理器的流水线结构. 57 | 58 | 图1展示了顺序单发射处理器的流水线结构. 除去取指部分包括多个流水级外大体上可以看成取指/译码/执行/写回四级流水. 顺序单发射处理器设计中简化的部分包括: 59 | 60 | 1) 访存单元采用多周期的设计, 同一时刻只能有一条访存指令由状态机控制执行. 61 | 2) 执行级只有一级, 且同时只能容纳一条指令. 62 | 63 | 由于访存单元的多周期设计, 一条访存指令至少需要三个周期才能完成执行. 这是此顺序单发射实现的最大瓶颈. 64 | 65 | ## 3.2. 顺序双发射 66 | 67 | TODO: PIC 68 | 图2: 顺序双发射处理器的流水线结构 69 | 70 | 图2展示了顺序双发射处理器的流水线结构. 顺序双发射处理器可以看作是对上面顺序单发射处理器的简单扩展. 由于顺序双发射处理器并非最终的设计目标, 因此在设计的过程中同样进行了一定程度上的简化. 简化的部分包括: 71 | 72 | * LSU采用多周期的设计, 同一时刻只能有一条访存指令由状态机控制执行. 73 | * 执行级只有一级, 且同时只能容纳一条指令. 74 | * 主副流水线设计, 只有ALU指令可以经由副流水线执行. 75 | 76 | 除取指部份外顺序双发射核心也可以看成是取指/译码/执行/写回四级流水. 主副流水线的设计使得设计和硬件复杂度得以简化, 但是放弃了很多挖掘指令并行性的机会. 执行流水线也没有进行深入的优化, 多周期访存仍然是其最大的瓶颈. 77 | 78 | ## 3.3. 乱序双发射 79 | 80 | 乱序双发射是主要的设计目标. 顺序处理器中简化的设计在乱序设计中不再简化. 访存单元被流水化, 不同的功能单元组成了非对称长度的执行流水线. 微架构细节将在下文详述. 81 | 82 | --- 83 | 84 | # 4. 微架构说明 85 | 86 | ## 4.1. 乱序超标量架构整体设计: Argo 87 | 88 | ![Argo-Draft.png](Argo-Draft.png) 89 | 90 | Argo的设计由三部分组成, 负责分支预测和取指的前端(Frontend), 负责执行指令的后端(Backend), 以及用于访存操作的访存单元(LS). 前端完成取指与译码操作后会将指令放置在译码缓冲区中. 后端从译码缓冲区中读出指令并乱序执行. 访存单元作为一个功能单元(FU)包含在后端流水线中. 91 | 92 | 一条指令在Argo中执行的流程如下. 单周期分支预测器(NLP)预测出下一周期要发给ICache的取指地址. ICache一次返回64bit的结果, 将结果保存到指令对齐缓冲(IAB)中. 指令对齐缓冲判断指令的长度并将不同的指令分离. 如果有指令横跨两次ICache结果, 指令对齐缓冲会负责将其拼接成一条指令. IAB每周期将至多两条拼接好的指令交给指令译码单元(IDU)进行译码. 指令译码单元每周期至多翻译两条指令, 并将结果写入到译码缓冲区中, 此时指令在前端的处理宣告结束. 后端顺序读取译码缓冲区中的指令. 当发现后端目前所持有的空闲资源(空重排序缓冲项, 空保留站项等)足以满足指令的要求, 且没有其他特殊原因(如特权指令的串行化限制)阻止指令执行时, 将指令分派到后端对应的部件中. 指令会被分配一个保留站项, 并根据其类型分配到对应的保留站中. 在分派阶段, 指令读寄存器获取所需的源操作数并将其写入到保留站中. 指令在保留站监听未就绪的源操作数, 等待到所有源操作数就绪, 且功能单元(FU)允许指令进入后将指令发射到功能单元. 功能单元在完成指令指示的操作后将结果写回到公共数据总线(CDB)上. 公共数据总线上的结果会被广播到各保留站及重排序缓冲(ROB). 此后, 指令在ROB中等待前导指令全部提交完毕后进入提交流程. 提交操作使这条指令根据其结果对处理器状态产生不可逆的影响. 提交操作后, 流水线不再跟踪这条指令, 指令的执行彻底结束. 93 | 94 | ## 4.2. 前端: Jason 95 | 96 | ### 4.2.1. 整体设计 97 | 98 | `Argo`所采用的前端`Jason`默认宽度为2, 包含一个单周期分支预测器及可选的多周期分支预测器. 单周期预测器结构简单, 负责给出下一周期指令缓存(ICache)请求的地址. 多周期分支预测器结构复杂, 负责给出精确的跳转预测. 前端每周期可以从ICache中取得最多64bit的指令. 我们称ICache每次返回的64bit结果为一个指令行(Instruction Line). ICache返回的结果会进入指令对齐缓冲, 由指令对齐缓冲来处理非对齐及非定长指令. 指令对齐缓冲最多可以同时装载4条指令, 输出2条指令. 指令对齐缓冲将输出结果提供到宽度为2的译码单元进行译码. 前端尾部设置了译码结果缓冲区. 译码结果会被暂存到译码结果缓冲区中, 以实现尽可能解耦前端与后端的目的. 99 | 100 | ### 4.2.2. 单周期分支预测器 NLP 101 | 102 | 单周期分支预测器负责给出下一周期前端指令缓存请求的地址. 在单周期分支预测器预测跳转不会发生的情况下, 前端会发出当前程序计数器(PC)+8的取指请求. 默认的前端分支预测器使用两位饱和计数进行预测, 提供512项的跳转目标缓存(BTB)和16项的返回地址栈(RAS). 103 | 104 | 针对64bit的取指宽度, NLP会返回4bit的跳转预测向量. 跳转预测向量标识在哪条指令处可能发生跳转. 在一个指令行中存在多个潜在的跳转时, NLP只预测最早的一条分支的结果. 105 | 106 | #### 4.2.2.1. 非对齐指令跳转预测 107 | 108 | RISC-V的压缩指令会将32位的指令压缩为16位的指令. 只有部分常用的指令能够进行压缩, 压缩后的指令可以在译码阶段之前展开为32位的正常指令. RISC-V默认的指令长度为32位, 默认指令对齐也是32位. 在取值地址不对齐时会产生取指地址对齐异常. 在启用压缩指令的情况下, 指令对齐变成16位. 无论是32位长的指令或是16位长的指令均可以按16位对齐, 此时取值地址对齐异常不会产生. 这样的设计是基于RISC-V"不对特定微体系结构做优化"的原则, 但是这样的对齐方式却为取指带来了困扰. 109 | 110 | 在启用压缩指令集的情况下, RISC-V指令可以是2字节对齐的. 如下图所示. 111 | 112 | ``` 113 | // 非对齐跳转指令横跨两个指令行(两次Cache访问)的情况 114 | 0: ADD(Compressed) a0, a0, a1 115 | 2: MUL a2, a2, a3 116 | 6: BNE a1, a2, 24 117 | 10: MUL a2, a2, a3 118 | 12: ... 119 | ``` 120 | 121 | 第一次ICache请求返回的结果 122 | ADD1|MUL1|MUL1|BNE1 123 | -|-|-|- 124 | 0|2|4|6 125 | 126 | 第二次ICache请求返回的结果 127 | BNE1|MUL2|MUL2|... 128 | -|-|-|- 129 | 8|10|12|14 130 | 131 | 图: 非对齐跳转指令横跨两次Cache访问的指令示例 132 | 133 | 当一条32位的跳转指令横跨两次取指时, 如果在第一次取指时就预测其跳转, 会导致指令的下半部分没有办法取得. 这种错误可以在取得指令之后通过检查发现. 但是这样会导致从发射预测的跳转结果到重新发送指令后半段的取指请求期间的取指结果全部撤销. 由此产生前端空泡, 影响前端取指部分性能. 为此, `Argo`还提供了非对齐指令跳转预测的额外功能. `Argo`采用的策略是在单周期分支预测器NLP中预测这种情况的出现, 在这种情况出现时将下一周期的Cache请求的地址设置为顺序地址. 取出指令的后半部分的ICache请求发出之后, 再根据跳转预测的结果发送Cache请求. 134 | 135 | ``` 136 | 0 137 | 24 //跳转目标地址 138 | 28 139 | ... 140 | 8 //发现跳转指令的后半部分没有取得, 发出跳转指令的后半部分的取指请求 141 | 24 //跳转目标地址 142 | ``` 143 | 图: 不使用非对齐指令跳转预测时ICache收到的取指请求 144 | 145 | ``` 146 | 0 147 | 8 //预测到跳转指令需要两次Cache访问, 发出跳转指令的后半部分的取指请求 148 | 24 //跳转目标地址 149 | ``` 150 | 图: 使用非对齐指令跳转预测时ICache收到的取指请求 151 | 152 | ### 4.2.3. 多周期分支预测器 MCP 153 | 154 | 出于时序考虑, 一些复杂的分支预测器需要多个多个周期来完成分支预测. 除此之外, 我们还可以根据预译码的结果来对分支跳转进行修正. 多周期分支预测器(MCP)与单周期分支预测器(NLP)同时开始预测, 但是会在预测结束后将预测结果添加到分支预测队列中. 在取得跳转译码结果后, 前端从队列中取出多周期分支预测的结果. 根据预译码的结果进行修正后, 若多周期分支预测的结果与之前的预测不同, 则发出前端重定向请求. 在不影响后端执行的前提下加载来自预测结果分支的指令. 155 | 156 | ### 4.2.4. 指令页表缓冲 157 | 158 | `Argo`包含一个4项的小型指令页表缓冲. RISC-V的TLB重填是由硬件负责的. 在页表缓冲没有命中时, 指令页表缓冲(ITLB)会向数据缓存(DCache)发出请求, 根据`satp`CSR寄存器的值来读取对应的页表. 159 | 160 | ### 4.2.5. 指令缓存 161 | 162 | `Argo`使用三级流水的缓存作为一级缓存. 默认配置下以及缓存是四路组相联的. 数据缓存与指令缓存的结构相似. 在第一个流水级传入地址, 第二个流水级进行tag的比对, 第三个流水级返回结果或向外发出访存请求. 由于时间的限制, `Argo`使用的缓存使用实地址进行索引, 在开始Cache访问前必须先取得TLB查询的结果. 163 | 164 | ### 4.2.6. 指令对齐缓冲 IAB (IBF) 165 | 166 | 为了处理引入RISC-V压缩指令RVC后的指令对齐问题, 前端设置了单独的指令对齐缓冲(IAB). 167 | 168 | 由于启用压缩指令的情况下指令可以是2字节对齐的, 因此一些指令会横跨两次ICache结果. 指令对齐缓冲根据指令地位的指令长度标识, 将ICache返回的结果组装成独立的指令. 在这一阶段, 也会使用标准的压缩指令(TODO: REF)展开逻辑来展开压缩指令为普通的指令, 从而降低译码(Decode)级的压力. 指令对齐缓冲每周期最多接受64bit的有效输入, 最多输出2条完成对齐的有效指令. 169 | 170 | 171 | 172 | ### 4.2.7. 跳转指令预译码 173 | 174 | 175 | 176 | Argo可以在ICache读取指令/返回结果时进行指令的部分译码, 判断其是否为跳转指令. 这样在ICache返回结果时就可以使用部分译码结果与多周期分支预测器的结果来一同更正指令流流向. 如果一条非跳转指令被预测为跳转, 则刷新IFU并从这条指令的顺序后续指令开始取指. 177 | 178 | 对于需要两次取指才能取回的指令, 即一条32位的指令分布在两次Cache请求结果的末尾和开头的情况, 预译码仍能正确判断其是否为跳转指令. 这得益于RISC-V指令集的编码设计. 如果一条指令是16位的压缩指令, 那一定可以根据指令前16位的内容判断其是否为跳转指令. 而如果一条指令是32位的常规指令, 标志着其为跳转指令的信息也能在低16位指令中取得. 因此, 对于需要两次取指才能取回的指令也可以在其前半部分返回时判断出其是否为跳转指令. 179 | 180 | ## 4.3. 后端: Hercules 181 | 182 | ### 4.3.1. 整体设计 183 | 184 | `Argo`所采用的乱序后端`Hercules`是基于隐式寄存器重命名(Rename-in-ROB)的乱序设计. 在一条指令的结果被确认有效前, 其将被暂存在ROB中对应的结果域内. 直到一条指令被确认有效之后, ROB中的结果才会写回到物理寄存器堆中. 乱序后端采用分布式保留站, 支持精确例外回溯. 乱序后端最多同时发射两条指令, 提交两条指令. 特权指令, 例外与中断会导致后端退化成顺序单发射. 访存单元连接 PIPT Cache. 185 | 186 | 指令在乱序后端中执行时的关键动作如下: 187 | 188 | 分派(Dispatch)指检查待发射指令的相关性, 将指令从后端指令输入端口被分别发送到不同保留站并在ROB中分配对应项的过程. 指令的分派是一个顺序的过程, 在前面的指令没有完成分派的情况下, 后面的指令不能越过前面的指令被分派. 由于所有的保留站均只有一个输入端口, 因此同时分派的指令的数目也受到保留站数目的限制. 在此阶段中, 正在被分派到保留站中的指令也会尝试获取所需源操作数的值, 根据寄存器映射表的信息从ROB的结果项或体系结构寄存器中读出所需的值. 如果所需的源操作数未就绪, 则在写入保留站的过程中将此数据项标识为未就绪, 由保留站在数据就绪时获取数据. 189 | 190 | 发射(Issue)指在保留站中的指令在其所有源操作数就绪, 且目标功能器件就绪的情况下, 被发送到功能器件开始执行的过程. 源操作数未就绪的指令会保留在保留站中, 等待所需的源操作数. 191 | 192 | 写回(Writeback)指功能部件完成其操作后, 将指令的结果广播到公共数据总线上的过程. 在这一过程中, 同时请求公共数据总线的功能部件可能要高于公共数据总线的最大宽度. 为此有两种可能的解决方案. 一是直接争用, 各个功能器件按照优先级争夺公共数据总线的使用权, 没有获得使用权的指令不能离开功能器件. 二是添加公共数据总线广播队列, 将没有取得公共数据总线使用权的指令及其结果添加到一个队列中, 待公共数据总线空闲之后再行提交. 各保留站会监听公共数据总线上广播的结果, 提取保留站内源操作数未就绪指令所需的源操作数. 同时, ROB也会监听公共数据总线上的结果, 将结果写回ROB内对应的位置上, 并标识该指令为"已写回"的状态. 193 | 194 | 提交(Commit)指确认一组指令(取决于ROB宽度)确定会执行, 使得这条指令的各种效应生效的过程. 能够合法提交的指令之前的所有指令都已经被确定会执行(提交), 且之前没有重定向操作阻止这条指令被执行. 在这种情况下, ROB会分析同时提交的指令之间的相关关系, 选取有效的指令并完成相应的提交操作. 提交操作包括写回指令的结果到体系结构寄存器堆中, 使其对应的重定向请求生效, 使`store`指令对应的访存操作开始执行. 提交成功代表着这条指令不会被撤销, 此后重定向缓冲(ROB)不再需要追踪这条指令. 195 | 196 | 特殊地, 对于某些特权指令, 其效应生效的过程要早于提交的时刻. 比如缓存一致性指令, CSR寄存器操作等. 由于这些指令出现的次数要远少于正常指令, 这些指令并不支持乱序执行. 后端在检测到这些指令时会阻塞流水线, 直到这些指令之前的所有指令均已提交才会允许这些指令开始执行. 即, 这些指令并不会被推测执行, 因此其效应在提交前生效不会产生影响. 197 | 198 | 进入后端执行的指令, 按照其在流水线中的位置, 可以分为以下几个状态: 199 | 200 | 1. 译码(Decode)完成, 等待分派(Dispatch) 201 | 1. 分派(Dispatch)完成, 等待发射(Issue) 202 | 1. 指令发射(Issue)进入功能部件, 等待执行完成后写回(Writeback)到公共数据总线 203 | 1. 指令结果已由公共数据总线写回ROB, 正在等待这条指令之前的指令按序提交(Commit) 204 | 1. 之前指令的结果已全部提交, 这条指令正在提交(Commit) 205 | 206 | 访存类指令根据其执行的不同阶段, 相比其他指令有更多的状态. 访存指令的额外状态将在访存系统部分详细介绍. 207 | 208 | ### 4.3.2. 分派总线 209 | 210 | 指令进入到后端后通过分派总线分派到保留站和重排序缓冲. 在指令分派时会检查指令间的相关情况并设置相应标识位, 检查是否有阻塞流水线的指令并采取相应行动. 211 | 212 | ### 4.3.3. 重排序缓冲 213 | 214 | 重排序缓冲是乱序执行的核心器件之一. 为了实现精确例外, 指令必须顺序结束以保证引发例外指令之后的指令不会对处理器的状态产生不可逆的影响. 指令在开始乱序执行时依照顺序进入到重排序缓冲中. 当重排序缓冲头部的指令全部写回结果之后, 重排序缓冲启动退出机制(提交), 允许提交的指令对处理器的状态产生不可逆的影响. 215 | 216 | `Argo`的重排序缓冲采用两路组相连的形式(路数可以根据发生宽度变化). 每次指令成功分派时推进重排序缓冲头部指针, 217 | TODO 218 | 219 | `Argo`中的重排序缓冲在指令提交时所触发的不可逆操作包括: 写回指令的结果到物理寄存器, 为错误的分支预测跳转和例外处理启动PC重定向机制, 通知LSU`store`操作可以开始执行, 检测后端执行过程中触发的例外并通知CSR. 220 | 221 | ### 4.3.4. 保留站 222 | 223 | 指令在进入后端后会被分派到各个保留站, 等待自身所需的源操作数以及功能器件就绪之后再被执行. `Argo`采用了分布式保留站的设计. 在默认情况下各个保留站根据入站的先后顺序设置优先级掩码, 跟据优先级掩码来决定指令出站的优先级. LSU对应的保留站属于例外情况, 指令的出站受到访存指令的顺序限制. 由于LSU将顺序执行访存单元保留站发出的指令, store指令在出站时必须保持与其他store指令的顺序关系. 否则, 最终写入DCache的数据将出现问题. 出于简化设计考虑, LSU的保留站和访存单元中的访存重定序队列彼此分离. 只有在LSU保留站中准备好操作数, 计算出访存目标虚拟地址的指令才可以进入到访存重定序队列中. 所有的保留站均会监听公共数据总线, 在公共数据总线上出现保留站所需的数据时更新保留站中指令的源操作数及状态. 在允许跳转指令提前取消的情况下, 重定向总线也会提供相应的跳转ID, 使得保留站可以根据跳转ID取消不应被执行的指令. 224 | 225 | ### 4.3.5. 公共数据总线 226 | 227 | 公共数据总线接受功能部件的结果, 将其提交到重排序缓冲并广播给各个保留站. `Argo`当前的公共数据总线宽度为2. 完成执行的指令在提交到公共数据总线的过程中需要经过仲裁. 在多条指令同时提交的情况下, `Argo`有两种可选的方式来选择能够广播到公共数据总线上的指令. 228 | 229 | 1) 功能部件分为两组: 第一组包括ALU/BRU, MDU, CSR, MOU, 第二组包括ALU, LSU. 两组功能部件各对应一个提交端口. 提交时, CSR, MOU等特殊指令在执行前会等待流水线清空, 不会出现争用. MDU, LSU在可以写回时立即写回, ALU/BRU在出现争用时进行退避, 暂缓写回. 230 | 231 | 2) 功能部件不分组, 各功能部件同时争夺写回端口的使用权. LSU, MDU等需要较长时间完成的功能器件拥有较高的优先级, 以保证依赖这些指令结果的指令能尽早开始执行. 没有获得提交端口使用权的指令暂缓写回. 232 | 233 | 234 | 235 | ### 4.3.6. 物理寄存器堆 236 | 237 | `Argo`采用隐式寄存器重命名的设计, 会在指令分派`dispatch`时读取重命名寄存器和物理寄存器, 在指令写回`writeback`阶段写入重命名寄存器, 在提交`commit`阶段读取重命名寄存器并写入物理寄存器. 目前重命名寄存器堆默认为为6读2写, 体系结构寄存器堆为4读2写. 在时序允许的情况下, 将公共数据总线宽度提高到3, 重命名寄存器堆扩展到6读3写可以更好地减轻写回端口争用对性能的影响. 238 | 239 | ### 4.3.7. 功能单元 240 | 241 | `Argo`的功能单元从保留站获取源操作数和译码信息进行计算, 计算完成后将结果提交到公共数据总线上. `Argo`包含以下标准功能单元: 242 | 243 | #### 4.3.7.1. ALU/BRU 244 | 245 | 算数逻辑单元`ALU`负责处理整数运算指令. 其加法器同时也可以被用来计算跳转指令所需的地址. 246 | 247 | #### 4.3.7.2. MDU 248 | 249 | 乘除法单元`MDU`负责处理所有的乘法与除法指令. 目前的乘除法单元尚未流水化, 只支持一条指令在其中执行. 250 | 251 | #### 4.3.7.3. CSR/MOU 252 | 253 | 控制与状态寄存器单元`CSR`与访存定序指令单元`MOU`用于处理各种特权指令. 与常规功能单元的区别为进入这两个功能单元的指令会阻塞流水线. 从ROB到CSR有一条额外的数据通路以处理处理器后端触发的例外. 254 | 255 | #### 4.3.7.4. LSU 256 | 257 | 访存单元`LSU`负责处理所有的访存指令. 详见访存系统部分. 258 | 259 | #### 4.3.7.5. 功能单元接口 260 | 261 | `Argo`支持额外添加功能单元以支持自定义指令. 自定义的功能单元接口与算数逻辑单元的接口基本相同. 下以添加额外的算数逻辑单元为例, 说明自定义功能单元的添加方式: 262 | 263 | ```scala 264 | val aluExtra = Module(new ALU(hasBru = true)) 265 | val alueOut = aluExtra.access( 266 | valid = aluers.io.out.valid, 267 | src1 = aluers.io.out.bits.decode.data.src1, 268 | src2 = aluers.io.out.bits.decode.data.src2, 269 | func = aluers.io.out.bits.decode.ctrl.fuOpType 270 | ) 271 | alu1rs.io.out.ready := aluExtra.io.in.ready 272 | ``` 273 | 274 | 功能单元可以接受来自保留站的源操作数及指令字段. 保留站与功能单元之间使用`DecoupledIO`相连. 275 | 276 | ```scala 277 | val aluecommit = Wire(new OOCommitIO) // 声明乱序写回总线 278 | alue.io.out.ready := alu1CDBready // 根据公共数据总线的仲裁结果, 判断是否可以进行写回 279 | aluecommit.decode := alu1rs.io.out.bits.decode // 指令译码结果, 用于调试 280 | aluecommit.commits := aluOut // 功能单元运算结果 281 | aluecommit.isMMIO := false.B // Trace比对时是否需要跳过此指令结果, 用于调试 282 | aluecommit.prfidx := alu1rs.io.out.bits.prfDest // 指令在ROB中的位置(指令占用的重命名寄存器) 283 | aluecommit.decode.cf.redirect := alu1.io.redirect // 指令的重定向请求 284 | aluecommit.exception := false.B // 指令是否触发后端例外 285 | aluecommit.intrNO := 0.U // 后端例外向量 286 | ``` 287 | 288 | 提交时, 至少需要向公共数据总线提交以上的数据. 这些连线并不都会被综合为真实的电路实现, 用于调试的连线将在综合过程中自动被优化. 289 | 290 | 自定义的需要多周期完成的功能单元(流水化的或非流水化的)需要时刻监听公共数据总线上的跳转信息部分. 在监听到BRU发出的跳转取消请求时根据内部指令的跳转掩码取消在错误跳转分支上的指令. 当前正准备写回的指令不受跳转取消请求影响. 291 | 292 | ### 4.3.8. 例外处理 293 | 294 | 目前, 此乱序核心可以处理的例外可以按来源分为两类: 一般例外和后端例外. 295 | 296 | 在此实现中, 例外处理单元作为一个功能部件(FU)被接入到流水线中. 采用这一设计可以使特权指令复用常规指令的数据通路, 从而降低设计复杂度. 同时, 在写回阶段之前使得特权操作对CSR寄存器产生影响也可以使得CSR的差分测试成为可能. 得益于RISC-V指令集的后发优势, RISC-V的中断及异常机制较为工整. 除访存相关的异常之外, 其余异常以及中断均可以在译码级被检测出并直接发送到访存单元执行. 部分访存相关的异常需要到访存单元才能被检测出. 为了处理这样的异常, 重定向缓冲中设置了`intrNO`域和`exception`状态位来追踪后端访存处理单元(LSU)产生的异常. (除此之外, `intrNO`域的作用还包括在仿真时保存例外的相关信息, 以便于使用差分测试进行验证) 当一条指令准备提交时, 如果其在重排序缓冲(ROB)中的`exception`状态位被置高, 例外处理单元将会接收到此指令并进行处理, 并在产生结果之后重新写回到公共结果总线(CDB). 297 | 298 | 在访存操作中, 访存相关的后端例外会在地址生成之后(地址对齐异常)和TLB操作结束之后(缺页异常)产生. 这些例外在产生之后仅会设置标志位, 不会影响访存流程的正常执行. 这些标志位会在对应指令写回到公共数据总线时被读取并提交例外到公共数据总线上. 由于提交了例外, 涉及到`store`的操作会在提交前被取消, 不会触发内存写入操作. 299 | 300 | TODO: 图 301 | 302 | 部分特权指令会导致流水线的强制刷新, 比如对`satp`寄存器的写入意味着虚拟存储相关的配置发生了变化, 需要强制刷新流水线来确保后续执行的指令使用的是新的配置. 303 | 304 | ### 4.3.9. 推测执行与跳转取消 305 | 306 | 在使用集中阻塞式访存系统的前提下, 推测执行部件的功能如下: 307 | 308 | `CSR`与`FENCEI`指令在译码阶段被标记成不可推测执行的指令. (TODO: 指令介绍) 对于标记为不可推测执行的指令, 乱序发射部件会将其阻塞, 直至ROB队列完全清空. 在此情况下, 此指令可以被确定一定会被成功执行或被后端异常中断. 309 | 310 | 311 | 312 | 通过提前取消跳转指令, 新的PC可以在跳转错误被发现之后立即提交到前端, 而不需等待这条指令之前的指令全部完成. 在分支预测错误的情况下可以有效减少因重定向导致的性能损失. 313 | 314 | 针对提前取消有多种实现方式, 主要的区别在于跳转现场的恢复方式. 一种做法保存跳转现场, 在发现分支错误时直接恢复. 其优点在于现场恢复的速度快, 只需要一个周期. 缺点是每条跳转指令需要保存完整的跳转现场, 显著增大了硬件复杂度. 大量现代处理器使用了这种设计(TODO). 另一种做法在发现跳转错误后逐步恢复跳转现场, 优势是不需消耗大量硬件资源保留现场, 劣势是逐拍恢复需要额外的周期. 315 | 316 | `Argo`提供了"不进行提前取消", "提前取消, 等待跳转指令前的指令全部完成后再分派新的指令", "提前取消并通过检查点立即恢复处理器状态"三种实现. 后两种实现都需要为处于乱序执行状态的所有指令添加分支标识向量. 分支标识向量指示对应的指令受到那些分支指令的影响. 当分支指令进入后端开始执行后会获得一个分支编号. 此后所有指令分支标志向量上的这个位置均被置高, 代表指令受到分支的影响. 在分支指令预测正确正常结束之后, 回收分支编号并更新所有分支标识向量(将分支标志向量的对应位置置为低). 如果分支预测错误, 跳转处理单元会发出重定向请求, 并将这条跳转指令的分支编号进行广播. 保留站与重排序缓冲接收到广播的分支编号, 根据分支编号与分支标识向量比对的结果选取取消的指令. 317 | 318 | 319 | 320 | ## 4.4. 访存系统 321 | 322 | 323 | 324 | 目前, Argo的Cache采用PIPT的索引模式. 因此在虚拟内存(Virtual Memory)开启的情况下, 在进行Cache访问之前需要进行TLB访问来将虚拟地址转化为物理地址. LSU会在`load`指令向DCache发出请求的同时的时候进行store queue写入和数据前递, 前递的内容为数据和位掩码. 在DCache返回结果时, LSU会根据根据位掩码将前递的数据与从Cache取回的数据拼接成新的结果. 325 | 326 | 在这一实现中, 访存指令在进入到访存处理单元LSU之后可以处于以下几种状态: 327 | 328 | `load`指令 329 | 330 | 1. 等待请求DTLB 331 | 1. 等待DTLB结果 332 | 1. 等待请求DCache 333 | 1. 等待DCache结果 334 | 1. 等待写回到结果总线 335 | 336 | `store`指令 337 | 338 | 1. 等待请求DTLB 339 | 1. 等待DTLB结果 340 | 1. 等待写入store queue 341 | 1. 等待写回到结果总线 342 | 343 | 对于`store`指令, 写回到结果总线并非指令的结束, 一条`store`指令在取得其物理地址之后会被加入到store queue中, 在这条指令确定被提交之后才会执行实际的访存操作. store queue中的指令可以分为两种状态: 344 | 345 | 1. 已取得物理地址, 等待指令提交 346 | 1. 指令已经提交, 等待请求DCache 347 | 348 | LSU可以被划分成4个流水级: 地址生成与TLB请求(ADDR/TLB), DCache请求(Cache1), 等待DCache结果(Cache2), 获取DCache结果并写回(Cache3). 值得注意的是, 在刷新流水线时, 某些访存指令的状态不应当发生变动. 对于已经向DCache发出访存请求的指令(load/store), 由于此实现中使用的DCache不支持直接刷新, 因此需要维持原有的状态, 等待DCache回复访存结果. 已经提交但尚未完成访存操作的`store`指令不会受到刷新流水线的影响, 将会继续执行. 349 | 350 | 351 | ### 4.4.1. 访存重定序缓冲 MemReorderBuffer 352 | 353 | 为了支持非阻塞式缓存, `Argo`使用存储器定序缓冲来支持访存操作的乱序返回. 访存指令在取得所需的全部源操作数后会进入到访存重定序队列中. 访存单元在获得数据缓存使用权时, 访存重定序队列会顺序发送正在排队的操作到数据缓存. 这些操作在发送的同时带有标识其在访存重定序队列中的标签. 数据缓存返回的操作结果根据带有的标签信息被写回到访存重定序队列中, 并被标记为`finished`状态. 访存重定序队列监视队列头部的指令, 在其被标识为`finished`之后执行写回操作. 在当前配置下, 访存重定序队列会根据入队顺序顺序地发出访存请求. 当数据缓存返回load指令的结果时, 结果会被立即处理并写回到公共数据总线. 而store指令则会. 354 | 355 | 在使用PIPT数据缓存且启用虚拟存储的情况下, 在访存操作被发送到数据缓存之前需要先进行虚实地址转换操作. 此时进入到访存重定序队列的指令需要先完成地址转换, 在获得实地址之后才可以进行随后的访存操作. 356 | 357 | 358 | 359 | ### 4.4.2. 写指令队列 360 | 361 | `Argo`通过读写操作分离来实现精确例外. 写指令队列(StoreQueue)负责跟踪已经取得了物理地址的写指令的状态, 并在这条指令提交之后执行实际的访存操作. 一条涉及到`store`操作的指令在被提交到写指令队列之后就需要参与到访存前递中, 直到其离开写指令队列为止. 离开写指令队列的条件包括刷新流水线或访存操作彻底完成. 362 | 363 | ### 4.4.3. 访存前递逻辑 364 | 365 | 由于读写操作的分离, 会出现在`store`将数据写入数据缓存前, `load`就试图读取此数据的情况. 为此需要前递这些`store`的数据. 在`Argo`中, 无论是`load`还是`store`都要经历一个共同的"同步时间节点". 在任意时刻只有一条指令可以处于"同步时间节点". 处于同步时间节点的`load`指令会在此时发出数据缓存访问请求, 而`store`指令则会被加入到写指令队列中(PIPT)或在此时发出数据缓存访问请求(VIPT). VIPT情况下`store`会在地址转换缓冲-数据缓存联合体返回物理地址之后再写入写指令队列. 366 | 367 | 设置同步时间节点的目的是, 保证访存操作在执行前递之前的顺序性. 在PIPT的情况下前递操作会在同步时间节点执行. 此时虚实地址转换操作已经结束, 实地址将被用来进行前递比对. 前递的数据和前递的写掩码将被保存下来, 在数据缓存返回结果之后与结果合并. 在VIPT的情况下前递操作会在数据缓存-地址转换缓存联合体返回结果后进行, 前递的结果与缓存返回的结果直接合并. 值得注意的是, VIPT这里的设计不支持非阻塞式缓存, 在访存结果乱序返回的情况下无法维持正确的前递顺序. 究其核心, 以上设计的目的是保证在进行前递比对的时刻, 可以很容易地取得访存操作的顺序来获得正确的前递结果. 368 | 369 | 在龙芯464e处理器核(TODO: 引用)中, 使用了回滚而非前递来处理上述的冒险问题. 其理由是使用回滚的操作尽管会提高前端设计的复杂度, 但是可以有效地减轻前递逻辑对物理设计造成的压力. 基于简化前端设计的考虑, `Argo`仍使用前递而非回滚来处理访存冒险. (FIXIT) 370 | 371 | ### 4.4.4. 乱序访存与回滚 372 | 373 | 乱序访存对于乱序处理器的性能有着关键性的影响. 在一条`Load`指令开始乱序执行时其越过的`Store`指令会获得一个`storeMask`. 在`Store`指令取得物理地址后会根据`storeMask`检查所有相关的`Load`, 将越过这条`Store`的`Load`指令的地址与自身的访存地址比较. 如果依赖于这条`Store`的Load指令被错误地提前执行, 则为`Store`指令设置回滚标记. 当一条有回滚标记的指令提交时, 触发回滚操作. 后端刷新整个流水线, 从触发回滚操作的这条指令后开始重新执行. 374 | 375 | 早期Argo允许`Load`指令之间的乱序执行, 不允许涉及到`Store`指令的乱序执行. 376 | 377 | ### 4.4.5. 原子指令 378 | 379 | RV64A包含三种类别的原子指令: AMO, LR, SC. LR/SC指令与其他指令集(如MIPS)中的LR/SC语义基本相同. AMO指令由四部分操作组成: 读取指定地址的数据, 使用寄存器中读取出的数据与读取结果进行运算, 将从内存中读出的数据写回寄存器, 将运算结果写回内存. 由于`LR`指令在执行时会对处理器状态产生不可逆的印象, `Argo`将这些原子指令视为需要阻塞的指令, 不会将其推测执行或乱序执行. 380 | 381 | 382 | 383 | ### 4.4.6. 内存映射IO指令 MMIO 384 | 385 | 内存映射IO指令(Memory Mapped IO, MMIO)与MIPS中的不可缓存的访存指令相似. 这些指令会绕过缓存, 直接访问内存地址映射的某些寄存器或是外设. 不同于一般指令, 非缓存的访存指令在执行时需要进行特殊的限制. 具体来说, MMIO可能被用作写外设控制寄存器, 对一个控制寄存器的写入会引起其他寄存器的值的变化. 若提前读取了其他寄存器的值, 由于CPU无法识别上述的连带效应, CPU内部的前递机制并不能保证这个值的正确性. 386 | 387 | 在启用"严格限制非缓存指令顺序"的情况下, 一条MMIO`store`指令会阻塞后面的MMIO指令直到这条指令被正常提交. 在PIPT的情况下, 在`load`访存操作开始之前就可以判断一条指令是否是MMIO指令. 因此可以很容易地保证后方的MMIO不越过前方的MMIO`store`执行. 而对于VIPT的情况, 物理地址要到缓存`load`操作/地址转换缓存查询完成时才能取得. 此时后面的指令可能已经进入缓存开始执行, 为此需要使用回滚机制. 388 | 389 | ### 4.4.7. 数据缓存争用 390 | 391 | 目前数据缓存不支持同时写入和读取, 因此分离的数据缓存读写请求面临着缓存争用的问题. `Argo`的解决办法是使发送到Cache的请求和Cache返回的结果都带有ID, 根据ID来区分不同来源的请求. 392 | 393 | 目前对数据缓存的请求按优先级高低可以分为以下四类: 394 | 395 | 1. DTLB重填 396 | 1. ITLB重填 397 | 1. CPU访存指令 398 | 1. 其他请求 399 | 400 | 在多个请求同时发生时, 仲裁逻辑会根据优先级选择优先级最高的请求提交到数据缓存. 401 | 402 | --- 403 | 404 | # 5. 扩展与优化 405 | 406 | ## 5.1. 向量扩展 407 | 408 | TODO 409 | 410 | --- 411 | 412 | # 6. 编程模型 413 | 414 | Argo处理器实现了RV64架构, 其中包括: 415 | 416 | * RV64 IMAC指令集 417 | * M, S 特权态 418 | * 外部中断, 时钟中断与软件中断 419 | 420 | --- 421 | 422 | # 7. 系统控制 423 | 424 | ## 7.1. CSR寄存器列表 425 | 426 | `Argo`默认支持以下的CSR寄存器. 要调整可使用的CSR以及设置CSR的初始值, 参见`CSR.scala`. 427 | 428 | ``` 429 | Sstatus 430 | Sedeleg 431 | Sideleg 432 | Sie 433 | Stvec 434 | Scounteren 435 | Sscratch 436 | Sepc 437 | Scause 438 | Stval 439 | Sip 440 | Satp 441 | Mvendorid 442 | Marchid 443 | Mimpid 444 | Mhartid 445 | Mstatus 446 | Mstatus 447 | Misa 448 | Medeleg 449 | Mideleg 450 | Mie 451 | Mtvec 452 | Mcounteren 453 | Mscratch 454 | Mepc 455 | Mcause 456 | Mtval 457 | Mip 458 | Pmpcfg0 459 | Pmpcfg1 460 | Pmpcfg2 461 | Pmpcfg3 462 | PmpaddrBase 463 | PmpaddrBase 464 | PmpaddrBase 465 | PmpaddrBase 466 | ``` 467 | 468 | --- 469 | 470 | # 8. 存储模型 471 | 472 | `Argo`满足RISC-V弱访存顺序模型(RISC-V Weak Memory Ordering, RVWMO) 473 | 474 | --- 475 | 476 | # 10. Debug & Trace 477 | 478 | `Argo`使用南京大学体系结构开源仓库`ProjectN`所提供的工作流来进行验证. 479 | 480 | 要使用基于`NEMU`的对比测试, 需要按步骤完成以下准备: 481 | 482 | 1. 安装RISC-V工具链. RISC-V开源仓库详细介绍了安装的流程, 在此不再赘述. 483 | 2. 克隆`Argo`处理器仓库和支持RV64仿真的`NEMU`仓库到本地. 484 | 3. 配置环境变量. 将`NEMU_HOME`和`NOOP_HOME`环境变量分别设置为NEMU和处理器仓库所在的位置. 485 | 4. 设置`NOOP.scala`中`NOOPConfig`的参数`debug`为`true`. 486 | 487 | 此时`Argo`和`NEMU`均处于准备好进行仿真验证的状态. 在此已完成适配的任意负载的根目录下执行`make noop`即可开始仿真. 488 | 489 | 为了更容易地进行测试, 处理器仓库目录下`debug`目录包含了测试所需的Makefile文件. 在此目录下运行`make cpu`可以编译C++仿真器并运行一段简短的指令来测试cpu的正确性. 490 | 491 | 详细的使用说明, 请参阅代码仓库根目录下`readme.md`. 492 | 493 | --- 494 | 495 | # 11. 性能计数器 496 | 497 | `Argo`的性能计数器分为基础计数器与结构相关计数器两组. 基础计数器在各种情况下都可用, 结构相关计数器只有在使用对应的微结构时才被激活. 参见`CSR.scala`中性能计数器的部分. 498 | 499 | --- 500 | 501 | # 12. 开发与性能测试环境 502 | 503 | Argo的验证使用了RISC-V开源仓库及南京大学体系结构项目开源仓库中的资源. 处理器的设计采用南京大学体系结构项目开源仓库中的NOOP-RV32(NJU Out of Order Processor: RV32)及其测试框架作为开发基础. NOOP-RV32是一个基于RV32IM指令集的简单顺序单发射处理器. 本文将其扩展为支持RV64IMAC, Zicsr, Zifence的顺序处理器NOOP-RV64作为性能测试的基准. 在开发过程中再次将NOOP-RV64进行扩展为顺序双发射架构NOOP-RV64D. 基于NOOP-RV64及NOOP-RV64D的设计, 完成乱序处理器Argo的设计. 处理器的验证部分扩展了南京大学体系结构项目开源仓库中的NEMU(NJU Emulator)作为处理器正确实现的参考. Argo的验证框架将NEMU原本支持RV32IM指令集的模拟扩展到了支持RV64IMAC, Zicsr, Zifence的模拟, 并针对RISC-V的虚拟存储与异常处理机制添加了相应的逻辑. 处理器验证和测试使用的指令负载包括移植到Argo测试框架中的RISC-V开源仓库测试集RISCV-test, 移植到Argo测试框架中的RISC-V随机指令生成器RISCV-torture, 南京大学体系结构项目开源仓库中的抽象计算机项目nexus-am包含的处理器测试程序, 以及常用的Coremark, Microbench, Drhystone等测试程序. 504 | 505 | Argo的验证没有使用RISC-V开源仓库中的Spike进行trace对比, 也没有使用Rocket和riscv-sodor配套的测试环境. 取而代之的是使用南京大学体系结构项目开源仓库ProjectN提供的处理器开发工作流. 使用南京大学体系结构项目开源仓库中NEMU, AM, NOOP及其处理器开发工作流的目的是自行控制处理器验证的细节, 并提高处理器验证的效率. 在开发新功能时, 首先在模拟器NEMU中实现对应的功能, 并将NEMU执行的结果与QEMU相比较. 在NEMU验证无误后, 进行硬件开发并使用NEMU来进行逐拍的结果核状态比对. 自行补充的模拟器实现也更加容易根据开发的需要而进行更改. 例如当数据缓存出现问题时, 可以在NEMU模拟器的访存处理部分添加调试代码, 获取正确的访存trace与处理器比较. 506 | 507 | --- 508 | 509 | # 13. 名词对照表 510 | 511 | * Arithmetic and Logic Unit ALU 算术逻辑单元 512 | * Architectural Register File ARF 体系结构寄存器 513 | * Back-end 流水线后端 514 | * Branch Target Buffer 跳转目标缓存 515 | * Branch Unit BRU 跳转处理单元 516 | * Checkpoint 处理器状态检查点 (Rename Snapshots 重命名快照) 517 | * Control and Status Register CSR 控制与状态寄存器 518 | * Common Data Bus CDB 公共结果总线 519 | * Front-end 流水线前端 520 | * Instruction Align Buffer IAB 指令对齐缓冲 521 | * Instruction Line Inst Line 指令行 522 | * Load and Store Unit LSU 访存处理单元 523 | * Multiplication and Division Unit MDU 乘法与除法单元 524 | * Memory Ordering Buffer 存储器定序缓冲 525 | * Multi Cycle Predictor MCP 多周期分支预测器 526 | * Next-Line Predictor NLP 指令行预测器 (单周期) 527 | * Physical Register File PRF 物理寄存器 528 | * Reorder Buffer ROB 重排序缓冲 529 | * Return Address Stack RAS 返回地址栈 530 | * Reservation Station RS 保留站 531 | * Store Queue SQ 写回队列 532 | * Translation Lookaside Buffer TLB 页表缓存 533 | 534 | 535 | --- 536 | 537 | # 14. TODO 538 | 539 | * 跑一波torture DONE 540 | * 跟一下mb-train的bug microbench 的 421c 回回都错 DONE 541 | * 分离Bru DONE 542 | * commit queue, commit 优先级 DONE // 是否需要分成两组 543 | * 性能计数器 DONE 544 | * 处理Dcache争用问题 DONE 545 | * 启用TLB: DONE 546 | * VIPT cache, 亲妈啊 delayed 547 | * Atom: AMO DONE 548 | * LR/SC DONE 549 | * ROB提交直接做成双端口写 -> ROB提交扩展到多端口写 delayed 550 | * 用mask实现RS优先级机制 计划提前: 找出分离Bru之后性能变差的原因 DONE 551 | * 探究分离式保留站和一体式保留站的优劣 delayed 552 | * 初始化时出现意外访存请求, 需要优化 DONE 553 | * Backend重构: RS选取所需的指令 delayed 554 | * cache bug DONE 555 | * 性能优化: 统计cache换入换出 556 | * AMO, TLB DONE 557 | * 推测执行 DONE 558 | * 原子指令设置为block inst DONE 559 | * NOTE: memesize 回调 560 | * TLB: do not return if flush DONE 561 | * 潜在的性能问题: simplebus-axi Crossbar!!!!!!!!!!!!!! 562 | * NOOP SoC 中 SimpleBusCrossbarNto1 SimpleBusID 和 AXI ID 的兼容性 563 | * 自动 extend id 的SBCrossbar 564 | * 在SoC中追踪访存情况, 检查为何会取出奇怪的值 怀疑是L2cache? 128? M状态下推测执行会跳转到虚地址, 在没有TLB的情况下读出错误的值 DONE 565 | * LSU重构到4拍 DONE 566 | * Important: mmio回滚 in-progress 567 | * Addr 生成是否需要锁存一周期 DONE 568 | * TLB查询在addr生成的同一拍开始 DONE 569 | * load结束后直接提交, 不等待AMO DONE 570 | * AMO: 只有在AMO指令finish的情况下触发计算 DONE 571 | * LSU: 不设置DTLB ptr, 直接使用dmem ptr 来减少拍数 572 | * TLB性能问题: 将TLB与正常访存操作解耦 573 | * LSU: d$3直接写回公共数据总线 DONE 574 | * DMA的DCache使用优先级调整到最低 575 | * simplebus-axi Crossbar 576 | * 提供ERROR的功能, 添加标准的MemAccessFault 577 | * 替换SoC中的SimpleBusCrossbarNto1 578 | * NOOP SoC 中 SimpleBusCrossbarNto1 SimpleBusID 和 AXI ID 的兼容性 579 | * 提交部分调整: 多条指令(>2)一同提交 580 | * 分支预测器 BPD in-progress 581 | * EnableBrResolutionRegister Branch的影响dealy一拍再执行 delayed 582 | * 参数调整, 性能测试 583 | 584 | * 片内总线协议? AXI4? 读写并行? 585 | 586 | # 15. 未来计划 587 | 588 | * 高性能的 OoO L/S 589 | * EnableBrResolutionRegister 590 | * 地址相关性预测: 乱序load/store 591 | 592 | --- 593 | 594 | Copyright (C) 2019-2020 Huaqiang Wang 595 | 596 | -------------------------------------------------------------------------------- /设计文档/乱序核/pic/argo-frontend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/设计文档/乱序核/pic/argo-frontend.png -------------------------------------------------------------------------------- /设计文档/乱序核/pic/argo-frontend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
PC
PC
IF0
IF0
IF2
IF2
IF4
IF4
IF3
IF3
BTB
BTB
RAS
RAS
ICACHE
S1/S2
PIPEREG
ICACHE...
ICACHE
S2/S3 
PIPEREG
ICACHE...
INST
ALIGN
BUFFER
INST...
uOP
BUFFER
uOP...
ITLB
ITLB
Decoder
Decoder
Decoder
Decoder
IF1
IF1
Decode
Decode
NPC
NPC
MCP (Framework Only)
MCP (Framework Only)
NLP
NLP
From Backend
Redirect
From Backend...
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /设计文档/乱序核/pic/argo-microarchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OSCPU/NutShell-doc/28644a08f34a077cab5f6de202491bd3a2488459/设计文档/乱序核/pic/argo-microarchitecture.png --------------------------------------------------------------------------------