RISC-V 指令集介绍(四)

程序的调试

提到了断点和 GDB,在 RISC-V 已经发布的官方标准中,除了用户指令集与特 权架构外,还包括了一个“外部调试器支持”标准(External Debugger Support)。

提示:不过与前两者不同的是,始终无法找到外部调试器支持标准在 1.0 以上的版本。在本书被撰写之际,该标准的最新官方版本是 0.13.2。鉴于这种 情况,本书对调试器仅做一般性的讨论,且仅限于嵌入式系统的程序调试。

对于嵌入式系统来说,调试器主要有两种实现方式。

1. 软件方式:ROM Monitor(只读存储器监视器)/GDB Stub(存根)

如图43 所示,在这种软件实现的调试器下,调试软件(例如 GDB、GNU Debugger)运行在主机电脑上,而目标系统中会首先运行一个叫 ROM Monitor 的 程序。对于 GDB 的情况,这个 ROM Monitor 也被称作 GDB Stub。GDB Stub 会通 过网络或者串行口接收用户通过 GDB 发来的调试指令,将被调试的应用程序从主 机载入到目标系统中,并设置软件断点等。当应用程序运行并触发了软件断点后, 处理器的控制又回到 GDB Stub 手中,然后由用户做进一步调试。GDB Stub 支持 的常用功能包括应用软件的载入、软件断点的设置、寄存器和内存的读取等。

图43 ROM Monitor / GDB Stub

对于这种用 ROM Monitor/GDB Stub 来实现调试器的方法,其优点是不需要额 外的硬件支持,其硬件开销比较小。但是其缺点是:

(1)其支持的功能比较有限。

(2)由于 GDB Stub 需要占用一定的内存,并与应用程序共存于系统中,其调试的方式有侵入性。这种 ROM Monitor / GDB Stub 的方式对裸金属系统 还能胜任,而对于比较复杂的嵌入式操作系统则会显得力不从心,甚至还会 发生资源冲突。

2. 硬件方式:JTAG(Joint Fest Action Group,联合测试工作组)

这也是 RISC-V 和其他的嵌入式处理器所采取的方式。这种方法除了需要处理 器本身的硬件支持外,还需要借助一个外部的调试控制器(见图44)或调试适 配器(见图45)。和前一种方式相比,这种调试方式很大程度上增加了硬件的 开销,而且其功能和稳定性也大为提高。

图44 硬件调试控制器

在这种调试方式下,处理器一般需要支持 JTAG 接口。同时,在主机上运行的 GDB 不再直接与处理器交换信息。取而代之的是,GDB 会同另外一个叫作 GDB 服务器的软件进行对话。GDB 服务器可以运行在主机之外的外部硬件上(见 图44),此时这个外部硬件被称作调试控制器。当然,GDB 服务器也可以和 GDB 运行在同一台主机上,而用一个相对简单的外部硬件来收发 JTAG 信号。此时这个外部硬件被称作调试适配器。

图45 硬件调试适配器

不论是调试控制器还是调试适配器,整个调试系统的本质都是通过 GDB 服务 器将 GDB 的命令转换为相应的 JTAG 操作,并通过独立于处理器的外部硬件来实 现这些 JTAG 操作。在实践中,许多 RISC-V 处理器都会采用 Open OCD(Open OnChip Debugger,开源片上调试器)作为调试软件,而 Open OCD 实际上起到了 GDB 服务器的作用。

设计基于 RISC-V 指令集的 Soft CPU

2018 RISC-V Soft CPU Contest 获奖作品:PulseRain Reindeer

说明:这是RISC-V RV32IM MCU Core的一个设计, 以展示如何在 FPGA 中实现 RISC-V 软核处理器。

这个 RISC-V 软核处理器名叫 PulseRain Reindeer,其源自于美国 PulseRain Technology 公司内部产品线的一个简化版本。在 2018 年由 RISC-V 官方组织的 RISC-V Soft CPU 竞赛中,该软核处理器位列季军(https://riscv.org/2018/10/risc-vcontest)。在本书创作之际,有幸能与国内的小脚丫团队合作,将软核处理器 做了进一步的改进与提升,并顺利移植到了小脚丫综合实验平台上。读者可以从 PulseRain Technology 在 GitHub 的官方账号上找到其完整的源代码,其文件名是 Reindeer_Step-1.1.2.zip。

PulseRain Reindeer 的处理器核心采用 Verilog 2001 编写,其余的外设等部分 采用 System Verilog 编写,并部分引用了 PulseRain Technology 的 PulseRain RTL 库。这个处理器在开发过程中遵循了本书提出的 FARM 开发模式,在设计之初就 对软件的配套做了考量,并在软硬件设计上做了安排。由此,当在 FPGA 中加入 PulseRain Reindeer 软核后,与本书配套的小脚丫综合实验平台就可以作为第三 方开发板,直接整合到 Arduino IDE 集成开发环境中。与此对应的 Arduino 支 持包(板级支持包),也已经在 GitHub 上公开发布。在本书的代码资源中,其 对应的文件名是 Arduino_RISCV_IDE-master.zip。

PulseRain Reindeer 软核还有非常好的通用性和可移植性。除了在 Intel 等主流 的 FPGA 架构使用,该软核也在一些高性价比的 FPGA 新架构(例如 EFINIX 公 司的 Quantum 架构 FPGA)上得到了成功的移植和验证。在 EFINIX Trion T20/C4 FPGA 上,该软核可以达到超过 110 MHz 的时钟主频。对此感兴趣的读者,可以在 PulseRain Technology 的官方 GitHub 上找到与此相关的移植代码。在 Arduino 支 持包里,也已经有了对 EFINIX Trion T20 开发板的支持。将会结合 FPGA 的器件特点,对 PulseRain Reindeer 的内核设计进行详细讨论。

适合于FPGA的设计目标

正如在讨论FPGA时提到的AT2 定律一样,数字设计在很多时候是在“鱼”与“熊 掌”之间寻找一个合适的平衡点。在设计之初,有必要明确定义设计所追求的目标, 特别是要在多个互相冲突的设计指标之间作出取舍。

就基于 FPGA 的 RISC-V 软核处理器来说,认为其设计重点应该集中在以 下几方面:

(1)软核处理器应该以 MCU(Microcontroller)设计为主。

在讨论“数字逻辑与处理器各自适用的领域”(2.15 节)时曾经提到, 所有的任务都可以被归为两类:控制密集型和处理(计算)密集型。FPGA 含有大量的数字逻辑资源,比处理器更适合于处理(计算)密集型的任务。

但是对控制密集型的工作,普通的数字逻辑却往往力不从心,而在 FPGA 中 嵌入软核 MCU,则可以很好地弥补这一缺点(见图 2-33)。

(2)软核处理器应该能达到比较高的主时钟频率。

由于控制密集型的任务和处理(计算)密集型的任务不可避免地会有交互, 如果嵌入 FPGA 中的软核 MCU 能和其他的电路工作在同一频率下,则可以避 免时钟域跨越,并简化数据交换的方式。

对处理器设计来说,除了时钟主频以外,还有一个重要的指标就是 CPI (Clocks Per Instruction,指令的平均周期数)。追求高主频,在某种程度上 会对 CPI 造成负面影响。这是因为高主频往往意味着更长的流水线设计。当跳转预测失败时,往往需要清空流水线(Flush the Pipeline),并重新取指, 长流水线在此时会需要更多的时钟周期来重新装载。

在 FPGA 中嵌入软核处理器的主要目的是为了做控制,而不是处理,即 该软核处理器并不需要追求非常强大的计算能力和计算效率。因此,和时钟主频相比,CPI 在这里可以看作一个次要的设计指标。

(3)软核处理器应该尽量降低对 FPGA 资源的消耗。

根据 AT2 定律,这个设计指标在某种程度上是与“追求高主频”相冲突的。 随着 FPGA 器件容量的不断提升,笔者倾向于把该指标的优先级放在“追求 高主频”之后。

有一点要指出的是,FPGA 的资源除了逻辑资源以外,还包括片上内存。 而由于软核处理器需要存储程序和数据,会消耗比较多的内存,而片上内存 往往是 FPGA 的紧缺资源。以与本书配套的小脚丫综合实验平台为例,其采 用的 FPGA(Intel Cyclone 10 LP 10CL016YU256C8G)包含多达 15 000 个逻 辑单元,其片上内存却只有 56 KB。如果需要在软核处理器运行嵌入式操作 系统,则这些内存会显得捉襟见肘。

为了节约宝贵的 FPGA 片上内存,可以有两种解决方法:

① 在软核处理器中支持 C Extension,以提高代码密度。

根据 Andrew Waterman 的博士论文,C Extension 可以将代码密度提高约 40%。但其代价是处理器的设计变得复杂,并占用更多的逻辑资源。考虑到 FPGA 的其余部分也需要消耗片上内存,这种方法所能带来的改变非常有限。

② 在软核处理器中支持片外内存的访问。

由于 DRAM 的存储密度要比 SRAM(Synchronous Dynamic Random Access Memory,同步动态随机存取存储器)高出许多,如果 FPGA 中的软核 处理器能访问片外的 DRAM,则可以将大部分的片上内存用于除了软核处理 器之外的其他功能。这个方法的代价就是需要额外的逻辑资源来实现 DRAM 控制器。

考虑到大部分的软核 MCU 的时钟主频都低于 200 MHz,一个比较实用 的方案就是在 FPGA 之外放置一个 SDRAM(常用的规格有 PC-100、PC133 等)。和 DDR 相比,SDRAM 的控制器相对比较简单,其逻辑开销也较 小,而且很多 FPGA 厂商都会提供现成的 IP。与本书配套的小脚丫综合实验 平台便采用了这一方案,在 FPGA 之外配置了 8 MB 的 SDRAM,用来作为 PulseRain Reindeer 的代码和数据内存。

(4)软核处理器应采用(冯·诺依曼架构)。

从处理器内存架构的角度来说,目前主要有两种选择:冯·诺依曼架构 (von Neumann Architecture)与哈佛架构(Harvard Architecture)。

如图1 所示,冯·诺依曼架构的核心思想是“存储程序”。在冯·诺 依曼架构下,程序代码和数据被不加区分地存放在同一个物理内存中。其优 点是由于只有一条内存总线,控制相对简单,内存控制器的开销比较小;其 缺点是这条唯一的内存总线会成为提升系统性能的瓶颈。

图1 冯·诺依曼架构

而哈佛架构则将程序代码和数据在物理内存中分开存储。如图2 所 示,在哈佛架构中有两条内存总线,分别用来访问代码内存与数据内存。 这种架构的优点是指令取指和数据读写有各自的专用总线,在物理内存中不 会发生冲突,有利于系统性能特别是 CPI(Clock Per Instruction,执行某个程序的指令平均时钟周期数)的提升,其缺点是内存控制器的开销较大。特别 是由于代码内存和数据内存在物理内存中分为两块,缺乏总体调度的灵活性, 给内存使用效率和软件开发带来负面影响。

在FPGA 中嵌入软核处理器的主要目的是为了完成控制任务,而不是要 追求非常强大的计算能力和计算效率,因此哈佛结构所带来的 CPI 性能提升 只是一个次要的设计指标。而同时,如果软核处理器采用 FPGA 片上内存来 存储程序代码和数据,则哈佛架构这种双内存设计会让有限的片上内存变得 更加左支右绌;如果采用片外内存,则只支持单总线结构。

由此,笔者建议该软核处理器应采用冯·诺依曼架构,而不是哈佛架构。

(5)软核处理器应该要方便软件的开发设计。

与 FARM 开发模式相呼应,软核处理器中还带有一些额外的功能模块, 以方便软件的开发设计,特别是对于 Arduino IDE 集成开发环境的支持。

PulseRain Reindeer的设计策略

基于以上对设计目标的讨论,PulseRain Reindeer 处理器采用了如下的设计策略。

1. 采用了 2×2 的流水线设计,内存布局采用冯·诺依曼架构

为了追求较高的时钟主频,PulseRain Reindeer 处理器中包含有 4 级流水线。

● 取指(Instruction Fetch)。

● 指令译码(Instruction Decode)。

● 指令执行(Execution)。

● 数据访问(Data Access)

包括寄存器的更新与内存的读写。 与普通的 4 级流水不同的是,PulseRain Reindeer 对这 4 个流水线阶段采用了 2×2 的布局,如图3 所示。在这种布局下,在双数时钟周期下,只有“取指”和“指 令执行”这两个阶段是活跃的。而在单数时钟周期,只有“指令译码”和“数据访 问”这两个阶段是活跃的。

图3 2×2 流水线设计

采取这种布局主要是出于以下考虑:

(1)FPGA 的内部结构不同于普通的数字芯片。在 FPGA 中,寄存器 并不是最稀缺的资源,而减少大块的组合逻辑则往往是降低走线资源消耗、 提高时钟频率的关键。PulseRain Reindeer 采用多级流水线结构便是出于此目的。

(2)而采用 2×2 的流水线布局,则以牺牲 CPI 为代价,减小对逻辑 资源的消耗。同时,在这种布局中,指令取指和内存数据访问被安排在不 同的时钟周期,从而避免了冯·诺依曼架构的单内存总线带来的内存访问 难题。

(3)作为比较,PulseRain Reindeer 在初始设计阶段也曾经考虑过两级流 水,以简化控制,并提高 CPI。通过将设计原型在 Intel Cyclone 10 C8 级别上 的布线测试后发现,这种两级流水设计的时钟主频在 70 MHz 左右就发生了时 序收敛的困难,而 4 级流水则可以在同样的 FPGA 器件上运行超过 100 MHz 的时钟频率。

2. 支持 FPGA 片上内存与片外内存的混合使用

与访问 FPGA 片上内存不同的是,片外内存往往都有比较大的访问延迟。当 片上内存与片外内存混合使用时,情况就变得比较复杂。在内存控制单元和流水线 的设计上,PulseRain Reindeer 为此做了调校,以支持片上内存与片外内存的混合 使用。

说明:在与本书配套的小脚丫综合实验平台上,PulseRain Reindeer 可以被 灵活配置,以同时支持片上内存与片外的 SDRAM 访问。由于大容量片外内 存的存在,使得 PulseRain Reindeer 无须再支持 C Extension,从而减少了对 FPGA 逻辑资源的消耗。

3. 支持基于硬件的引导加载程序(加载器)

前面提到了 FARM 开发模式,其中谈到了对 Arduino IDE 集成开发环 境的支持。这里只想提及一下其中的程序 Image 下载部分,这个问题实际上并非 Arduino 独有,而是一般性的问题。

传统的下载办法(实际上也是 Arduino 采用的办法),便是在处理器上预先运 行一个称为 Bootloader 的软件,通过这个软件同主机上的上传工具通信,来下载程序 Image,如图4 所示(实际上图4 可以看作是图43 的简化版)。

图4 传统程序Image下载方式

图4 这种方法的缺点是需要在上电以后将 Bootloader 载入到处理器的内存 中。一般的做法是将 Bootloader 放入 ROM,并映射到处理器的地址空间中。对于 FPGA 的软核处理器,则可以将 Bootloader 代码直接作为比特流的一部分,用来初 始化片上内存。但是这种做法除了要占用相当可观的片上内存外,还存在可移植性 的问题,并非所有厂商的 FPGA 都会支持将内存初始化数据存放于比特流中。

为了更好地支持 FARM 开发模式,PulseRain Reindeer 除了软核处理器本身, 还为之配套设计了一个基于硬件的引导加载程序(Hardware Based Bootloader)。 如图5 所示,这个基于硬件的 Bootloader 会与处理器核共享同一个串口,并且它 还会与处理器核中的内存控制器协调工作,以将程序 Image 载入 FPGA 片上内存 或片外内存中。和传统的下载方法相比,这种基于硬件的 Bootloader 不需要任何 ROM 来存储代码,并且它本身可以被用来复位和启动处理器核,以及提供复位后 的初始地址,从而比传统方法更稳定与灵活,在不同 FPGA 器件之间的可移植性也比较好。

与之相对应的是(见图6),Arduino IDE 在主控端会运行一个 Python 脚本 (reindeer_config.py)作为上传工具。这个 Python 脚本还可以独立于 Arduino IDE 单独运行。对 elf 文件,这个 Python 脚本会调用工具链,将 elf 文件中的相关部分 截取出来,并通过基于硬件的引导加载程序载入到内存中。

图5 基于硬件的引导加载程序

图6 用Python Script载入软件

人工智能芯片与自动驾驶