RISC-V 指令集介绍(五)
RISC-V 指令集介绍(五)
PulseRain Reindeer的RTL设计
从软核 MCU 移植性的角度来说,可以将整个 FPGA 划分为两部分:①与 FPGA 平台相关部分;②独立于 FPGA 平台部分。
对于具有 PulseRain Reindeer 软核 MCU 的 FPGA 来说,整个 FPGA 的顶层架 构如图7 所示。将 PulseRain Reindeer 软核 MCU 移植到不同的 FPGA 平台上时, 需要对应的平台提供以下模块。
图7 FPGA顶层架构
1. PLL(Phase Locked Loop,锁相环)
PLL 一般由 FPGA 片外的石英振荡器提供时钟参考。在有些 FPGA 平台上, 时钟参考也可以由片上的 RC 振荡器来提供。
2. SDRAM 厂商 IP
如果 PulseRain Reindeer 被设置成需要使用 FPGA 片外 SDRAM,则需要使用 FPGA 厂商提供的 SDRAM IP(对于 SDRAM,网上也可以找到一些开源的 IP)。
3. FPGA 片上内存
对于不同的 FPGA 平台,其片上内存的配置方式会略有不同。例如在 Intel 公 司提供的 FPGA 中,就有 M9K(每块内存 9 Kb),M10K(每块内存 10 Kb), M20K(每块内存 20 Kb)等多种不同的片上内存种类。在目前 的 PulseRain Reindeer 的 RTL 代码中,对片上内存采用了由综合软件根据代码推断(Infer)的 方式。如果这种 Infer 的方式不能被厂商的 FPGA 综合软件完全认可,则需要根 据 FPGA 厂商的指引加以重新配置。
4.4.2 独立于 FPGA 平台部分
在图7 中左下角的 Hardware Based Bootloader 已经在图5 中有详细描述, 此处不再赘述。而图7 中的 PulseRain Reindeer 软核 MCU 则在很大程度上与具体 的 FPGA 平台无关,其内部细节如图8 所示。
图8 PulseRain Reindeer 软核MCU
从图8 可以看出,PulseRain Reindeer 软核 MCU 主要由三部分组成。
1. 外围设备
在 PulseRain Reindeer 软核 MCU 中,外围设备通过 Wishbone 总线和处理器核相连。根据具体应用的不同,这些外围设备可以被灵活地定制化,这也是 FPGA 相 对于普通数字芯片的优势之一。
一般常用的外围设备有:
● 串行口 UART(Universal Asynchronous Receiver/Transmitter)。
● I 2 C 总线接口。
● SPI 接口。
● GPIO(General Purpose Input Output)。
● PWM(Pulse Width Modulation)。
● PS2。
● MicroSD。
● 旋转编码器(Rotary Encoder)。
2. 内存接口
PulseRain Reindeer 采用了冯·诺依曼架构,将程序代码和数据不加区分地 存放于内存中。而对 FPGA 来说,内存又分为片上内存(Block RAM)和片外内 存。片外内存控制器则需要通过一个在 MCU 之外的中间模块(例如图7 中的 SDRAM 控制器)和具体的内存 IP 做数据交换。在与配套的小脚丫综合实验 平台上采用了 SDRAM 作为片外内存,具体的做法将在后续章节讨论实验平台时 做详细描述。
3. RISC-V 处理器核
处理器核部分包括通用寄存器、CSR 寄存器、内存地址分配、流水线的数据 通路和控制等。
4.4.3 通用寄存器的设计 在 RISC-V 用户指令集标准(User-Level ISA)中提到,RV32 定义了 32 个 32位的通用寄存器(其中 x0 恒为零值)。在 FPGA 中,如果直接用触发器来实现这 些通用寄存器,则需要 32×32=1 024 个触发器。对于小脚丫平台上的 Intel Cyclone 10 LP(10CL016YU256C8G)FPGA,则根据图 2-1 中的逻辑单元结构,至少需要 消耗相同数量的逻辑单元才能实现所有的通用寄存器(大约占该 FPGA 总逻辑容 量的 7%)。
同时,通过观察 RISC-V 指令格式,可以发现许多 RISC-V 指令都包含两个源 寄存器(标记为 rs1 和 rs2),即在同一指令中,需要读取两个通用寄存器。如果 用触发器来实现通用寄存器,则同时还需要两个 32 :1 的多路复用器,每个多路复 用器的数据宽度都是 32 位。
综合以上考虑,PulseRain Reindeer 中采用了两块简单双口 Block RAM 来实现 通用寄存器,如图9 所示。
图9 用Block RAM来实现通用寄存器
在图9 中,当寄存器被写入时,同样的数据会被同时写入这两块 Block RAM 中。而在寄存器读取时,这两块 Block RAM 分别对应源寄存器 1 与 2。
在图8 所示的 4 个流水线阶段中,寄存器的读地址在“取指”阶段就可以 确定。而寄存器的写地址和写数据会在“数据访问”阶段被确定。因为 PulseRain Reindeer 采用的 2×2 流水线设计,“取指”和“数据访问”发生在不同的时钟周期, 所以不会产生由于对内存同时读写而造成的数据模糊(但是由于数据相关性而引起的流水线阶段之间的转发问题依然会发生)。
Block RAM 的输出驱动能力一般都弱于触发器,如果让这些 Block RAM 的输 出直接参与很多组合逻辑,则会对时序收敛产生负面影响。同时,Block RAM 会有一个时钟周期的读延迟。如果在“取指”阶段给出寄存器读地址,则数据 会在“指令译码”阶段变得有效,而这些寄存器读数据会在“指令执行”阶段被用 到。采用 2×2 的流水线布局后,可以在“指令执行”阶段将 Block RAM 的输出寄 存后再操作,无须再做太多的数据相关性处理。Block RAM 的输出驱动能力弱, 将 Block RAM 的输出寄存后再操作则有利于提高时钟主频。
4.4.4 CSR 寄存器的实现
表 3-14 所列出的 CSR 寄存器在 PulseRain Reindeer 中都得到了实现。与 通用寄存器的数据读操作不同的是,对 CSR 这类控制寄存器的额外读操作可 能会产生不必要的副作用,因此 CSR 寄存器的读地址和读使能要在“指令译码” 阶段才能确定。CSR 寄存器都是用触发器实现的,不存在 Block RAM 这样的读 延迟,所以读数据依然可以及时在“指令执行”阶段得到使用。
当中断或异常发生时,流水线会被暂停,而某些 CSR 寄存器,如 mtvec、 mepc、mtval、mcause 等会在此时被读取或更新。
4.4.5 时钟定时器的实现
由图8 可以看出,PulseRain Reindeer 的地址空间主要被分为两部分:代码 / 数据内存和内存映射寄存器。 内存映射寄存器主要被用来作为外围设备寄存器的 地址空间映射。理论上时钟定时器也是一种外围设备,然而,考虑到 RISC-V 标准 对时钟定时器已经做了明确的定义,所以在 PulseRain Reindeer 中直接将其包含在 了处理器核当中。
在介绍时钟定时器的 mtime 寄存器时曾经提到,时钟定时器应该运行在固 定的计数频率,并建议处理器主频能被该计数频率所整除(3.6.3 节)。所以在 PulseRain Reindeer 中,将该计数频率设置为了 1 MHz,即时钟定时器的分辨率 为 1μs。
4.4.6 流水线的设计
1. 取指器
PulseRain Reindeer 是一个 RV32IM 处理器。通过对外部大容量 DRAM 内存的 支持,PulseRain Reindeer 避免了压缩指令集(C Extension)的实现。由于只需要 支持 32 位的读取,取指器也不用考虑太多指令地址边界对齐的问题。
FPGA 片上内存一般只有一个时钟周期的读延迟,而外部内存的读延迟则往往 要大得多。通过对指令读地址的判断,内存控制器可以很快地确定是否需要读延迟, 从而设立相应的握手信号来反馈给取指器,以实现 FPGA 片上内存和片外内存的 混合使用。
由图 3-5 可以看出,如果指令需要读取通用寄存器,则源寄存器 1 的地址总是 在位 [19:15],而源寄存器 2 的地址总是在位 [24:20]。考虑到对通用寄存器的读取 不会有其他作用,不论指令的类型是什么,PulseRain Reindeer 都会以这两个位置 上的数值为地址,对通用寄存器进行读取。
2. 指令译码器
从图 3-5 还可以看出,指令位 [6 :0] 是操作码。而根据图 3-2,在 RV32IM 下, 位 [1 :0] 总是 3,所以在 PulseRain Reindeer 中只需要对位 [6:2] 译码便可确定指令 操作类型,并产生相应的控制信号。这些控制信号会在接下来的指令执行器中被 用到。
3. 指令执行器
指令执行器需要执行以下的几类指令:
● ALU(Arithmetic Logic Unit,算术逻辑单元)。
如图10 所示,算术逻辑指令包括“加”“减”“移位”“与”“或”“异或” 等。在参与算术逻辑的两个操作数中(图10 中的寄存器 X 与 Y),操作数 X 总 是来自于通用寄存器,而操作数 Y 则可以来自通用寄存器或者指令自带的立即数。 对 ALU 的操作选择和数据源选择都来自于指令译码器产生的控制信号。
图10 算术逻辑单元
● 乘除法(M Extension)。
PulseRain Reindeer 支持 RV32IM 指令集。
其中 M Extension(硬件乘除法)可 以被选择性地配置。
● 无条件跳转指令(JAL / JALR)。
对于无条件跳转,其后一条指令的地址需要被存入目标寄存器中。
● LUI / AUIPC(Load Upper Immediate / Add Upper Immediate to PC)。
这两条立即地址构建指令(见图 3-10)的结果也会被写入目标寄存器。
以上这些指令都会更新目标寄存器,其具体的写入值如表1 所示。
表1 目标寄存器的写入值
除了以上这些指令外,执行器还需要对下面的这些指令做出处理:
● CSR 操作指令。
指令执行器与 CSR 寄存器有专用总线相连,以做数据更新。
● BRANCH 指令,ECALL / EBREAK,LOAD / STORE。
这些指令无须更新目标寄存器,会产生相应的内部控制标记,供流水线控制器 做参考。
4. 数据访问
在数据访问阶段,通用寄存器会被更新,由 LOAD / STORE 指令产生的内存访问也会在这个阶段产生。
由于 2×2 的流水线布局,“内存访问”阶段和“取指” 阶段被安排在了不同的时钟周期,以尽量降低内存访问冲突发生的可能性。
5. 流水线控制
流水线控制的主要目的就是对跳转指令和异常 / 中断的处理,如图11 所示。
因为流水线控制比较烦琐和复杂,所以图11 只列出了其中的主要部分。
图11 流水线控制的主要状态
对于 2×2 的流水线布局,取指与指令执行状态和指令译码与数据访问状态分 别对应于图3 提到的双数时钟周期(“取指”与“指令执行”)和单数时钟周期(“指令译码”与“数据访问”)。其中跳转指令则会将流水线控制转入初始化状 态,以重新加载流水线。
而异常 / 中断的处理则需要一个额外的异常处理状态,以根据异常 / 中断的具 体类别,设置异常编码(见表3-18与表3-19),并确定异常/中断处理的返回地址(即 mepc 寄存器)。
处理器验证的方式
黑盒(Black Box)测试与白盒(White Box)测试 作为软件运行的最终平台,处理器的准确性是至关重要的。对此,RISC-V 官 方在 GitHub 上公布了一套 RISC-V 指令集的标准测试程序,以作为处理器兼容性 和正确性认证的标准。
RISC-V 的合规测试在 RV32I 下共有 55 个标准测试程序, 而在 M Extension 下则有 8 个测试程序。
对于 RISC-V 合规测试中包含的这些测试程序,其做法都是采用 Signature 检 测验证法,即测试程序在运行过程中会向内存中写入某些标记。在程序运行结束后, 再将内存中的这些数据读取出来,并与标准结果做比对。这个方法不对处理器运行 的中间状态做监测,可以看作是一种黑盒验证法。
然而,在商用开发中,这种黑盒验证法还不充分。
实际上,当黑盒测试失败时, 确定出问题的具体位置是一件非常困难的事情,在许多商用处理器的开发中,往往 采用白盒验证法。其具体做法如下:
(1)在处理器硬件开发之前,先用软件开发一个处理器的模拟器,用来 确定处理器的行为模式。
(2)将测试程序作为这个模拟器的输入,在其上运行,并产生测试向量。 测试向量包含处理器在每个时钟周期下应有的内部状态,如程序计数器(PC)的值、取指器提供的指令、所有通用寄存器的值、内存读写的地址与数据等。
(3)将同样的程序作为处理器 RTL 仿真的输入,将仿真所得到的处理 器内部状态与测试向量做比较。
(4)修改 RTL 设计,直至仿真通过所有的测试向量为止。 PulseRain Reindeer 对这两种验证方法都有采用。在后边会介绍用 Verilator 进行黑盒法验证,以及用 Modelsim 进行白盒法验证。
图12 Verilator仿真
在介绍小脚丫综合实验平台时,还会对 Verilator 的具体操作做进一步介绍。
4.5.3 用 Modelsim 做处理器的白盒验证
前边提到了如何用 Verilator 来对 RISC-V 处理器内核做验证检验。在 Verilator 进行的仿真中并不包含外部内存控制器,所有的代码都是通过测试平台写 入到片上的 Block RAM 里面。这个做法虽然可以很快地对 RISC-V 处理器内核进 行合规测试验证,但是存在以下问题:
(1)Verilator 不能很方便地检测模块内部的信号。所以验证的主要方法 就是仿真运行合规测试的各个程序(elf 文件)。在仿真结束后,通过读取内 存中的数据,并与标准的 Signature 做比较来进行判断。在 4.5.1 节中介绍白 盒验证法时提到,更稳妥和精确的验证方法应该在每个时钟周期都对处理器 的状态(PC 程序计数器、IR 指令寄存器,以及通用寄存器的值等)进行侦测, 并与测试向量比较,以便及时发现并定位问题。
(2)上文提到的 Verilator 仿真只包含了 RISC-V 处理器内核部分,但 是在实际的系统中,FPGA 除了包含处理器内核之外,还包含了由 FPGA 厂 商提供的各类 IP。例如在小脚丫综合实验平台的 FPGA 里就包含了 PLL 和 SDRAM 控制器等。为了保证系统的正确运行,更好的做法应该是将所有这 些 IP 也包含在内,从 FPGA 上电复位开始仿真。
因此,PulseRain Reindeer 在处理器级别(包括处理器内核、外设与 DRAM 控 制器等)的验证采取了以下的白盒验证法。实际上,许多商用系统也采用了类似的 验证方式:
(1)用 Modelsim 代替 Verilator,以方便对模块内部信号的检测,并且 对所有的 IP 都建立仿真库。作为一款优秀的商用仿真软件,Modelsim 一直是 主流 FPGA 厂商青睐的仿真软件。在 Intel Prime Quartus Lite Edition 中带有一 个 Modelsim 初学者版本,可以被用来仿真所涉及的所有样例。
(2)为外部内存芯片也建立相对应的仿真模型。例如,小脚丫平台上 采用的外部内存芯片是 SDRAM(IS45S16400J),在用 Intel Prime Quartus 产生 SDRAM 控制器时,软件也会提供一个相对应的仿真模型。用户还 可以为这个仿真模型提供一个 dat 文件,作为 SDRAM 的内存初始值。在 验证检验仿真时,这个 dat 文件正好可以被用来存放验证检验的程序代码 (见图13)。
(3)将合规测试的各个程序在 RISC-V 的模拟器上运行,产生测试向量。 在所采用的测试向量中,测试向量的每一行包括了 PC 值(程序计数器)、 IR 值(指令)、各通用寄存器的值等。为简化起见,提供的测试平台将 只比较测试向量中的前两列(PC 值与 IR 值)。
使用的测试向量,来自于 PulseRain Technology 公司内部开发的 RISC-V 模拟器。类似的 RISC-V 模拟器在 GitHub 上有很多,读者也可以将这些模拟器稍做修改后产生自己所需要的测试向量。
(4)使用测试平台将 UUT(Unit Under Test,在这里即为 PulseRain Reindeer Step RISC-V 微控制器)、SDRAM Simulation Model 整合在一起,如 图13 所示。
图13 Modelsim 仿真 (5)运行 Modelsim 仿真,并提取 UUT 内部信号与测试向量做比较。