第六届“龙芯杯”大赛参赛总结

第六届“龙芯杯”大赛已经落下帷幕,我所在的队伍是我们学校唯一的团队赛参赛队,最终获得了三等奖的成绩。

比赛简介

“龙芯杯”大赛是全国大学生计算机系统能力大赛(英文简称 NSCSCC)的赛道之一,NSCSCC 总共有 3 个赛道,分别为:

  • 操作系统:主要是做系统软件方面的工作
  • 编译系统:构建编译系统并进行编译优化
  • CPU:设计并实现一个计算机系统,并能够运行系统软件

“龙芯杯”则对应 CPU 赛道。“龙芯杯”下又分 3 项赛事,分别为:

  • 个人赛:该赛事我校无人参加,具体内容不太清楚;
  • 团队赛:基于 MIPS32I 实现计算机系统,要求通过大赛提供的功能、性能和系统测试。团队赛分为初赛和决赛,能否进入决赛主要看前面 3 项测试得分;决赛排名主要看性能得分、系统完备性(稳定启动操作系统、支持外设)和答辩表现;
  • LoongArch 挑战赛:第六届“龙芯杯”新增赛事,据说只有成功启动操作系统才能进入决赛。

比赛筹备

须知少日拏云志,曾许人间第一流。

开始契机

我们是从寒假里开始准备比赛的(寒假在二月份,比赛最终提交是当年的八月份),因此实际上有半年左右的准备时间。具体开始的契机是上届参加比赛的学长在寒假里组织了参赛培训。培训结束后,同班的徐同学问我是否有组队意愿,我正好也准备参加比赛,于是我们一拍即合,创建了队伍。

第一个流水线

真正开始设计是开学后,当时我们聚在一起商量了一下,决定花一到两周的时间重写上学习体系结构的五级流水线,最后看谁的时序较好。这时候我正好在自学 chisel,于是我先用 chisel 写了一个简单的 LoongArch32 CPU 核练手。写完后,我陷入了抉择:是使用 chisel 进行开发,还是继续使用熟悉的 Verilog?思量过后我决定还是使用 Verilog 更为稳妥,毕竟是团队赛事,翻车了可是 4 个人的心血。于是,我开始使用 Verilog 重写流水线。

我们第一次商量的结果是将五级改为四级,去掉看上去没什么用的写回级,以减轻流水线刷新的惩罚。在我重构的代码中,主要的改动有下:

  • 将流水线控制逻辑由“互锁式”改为“无互锁式”(参考了《计算机组成与设计--硬件/软件接口》)
  • 去掉写回级,将流水线缩短为四级
  • 将指令集替换为 MIPS
  • 加入了自行编写的华莱士树乘法器和位移除法器

完成了基本功能的调试后,一跑综合,时钟频率竟然能达到惊人的 120 MHz。两周过后,大家似乎都比较忙,只有我将代码完全从头写了一遍,因此也基本确定了由我来进行内核设计和开发的分工。

丰富流水线的功能

完成了基本的流水线后,我按照上学期的思路开始增加例外处理和类-SRAM 总线,其他同学则按照分工开始做 Cache 和 TLB。例外处理的实现相对而言比较简单,最让人头疼的是类-SRAM 总线上事务的取消。

在流水线中,经常会遇到重定向请求冲刷流水线,这时取指或访存的事务就需要被取消。取消这些进行中的事务在我看来是一个比较繁琐的过程,原因有三:

  1. 重定向随时都可能发生,此时事务处理可能处在任何一个阶段;
  2. 总线协议无法对一个已经请求握手的协议进行直接取消,只能由流水线取回再丢弃;
  3. 类-SRAM 总线需要主方保证返回的数据一定有地方缓存。

最终,纠结了几天后,我打算采用一种队列的结构处理访存请求。这主要是因为类-SRAM 总线上返回数据的顺序与发起请求顺序一致,于是便可以借助队列结构简化访存逻辑。而对于事务取消,在队列里对应的访存请求上添加一个 cancel 标记即可,加入标记的请求仍然继续访存,只不过流水线直接将其丢弃。这种“队列化”的访存控制器一直沿用到最终设计的取指部分,访存由于不存在“取消正在执行的事务”这种需求而采用了“流水化”的控制器。

另一方面,由于初赛不需要 TLB,因此另一项重要的工作是将 cache 接入 CPU 核。在之前的课程中,由于时间关系,我们最终并没有将 cache 接入 CPU 的经验,因此调试起来花费了一番功夫。由于核内和访存子系统分别由我和徐同学开发,因此调试时常常需要约个事件一块看波形,谁也没法保证自己负责的部分是完全正确的。

一个学期过的很快,在开始的一个月过后,由于学业压力,我们的开发过程也被迫暂停了,之后的很长一段时间里进展都十分缓慢。

前后端分离

这一个学期里,流水线最大变化是进行了前后端的分离。所谓前端,指的是流水线中负责取回指令的部分;而后端则是负责执行指令、改变处理器状态的部分。分离前后段的核心是一个指令队列,它的功能如下:

  • 暂存前端取回但还无法向后端发射的指令(取指宽度小于发射宽度)
  • 切断从写回级一直反串到取指流水级的控制信号

经过前后端分离后,我又进一步修改前端和 i-cache,将取指宽度拓宽为了 16B(4 条指令)。做前后端分离本是为了之后改双发射做准备,但遗憾的是,最终未能尝试双发射结构,决赛提交的是一个单发射流水线。

弄巧成拙的延迟槽

在之前的课程中,老师常说 MIPS 的延迟槽是一个历史包袱,之前说实话我并不理解这句话的含义,直到开始做分支预测器。分支预测器是由另外两位罗同学和肖同学负责的,罗同学先写了一个简单的基于局部历史和两位饱和计数器的预测器,而肖同学则依据调研的论文进行设计。拿到罗同学的预测器,我按照核内的需求修改了接口,但是很快遇到了一个问题。

MIPS 最著名的特征之一便是转移延迟槽,它的设计初衷是在发生分支跳转时能够少刷掉一条指令,从而提升 IPC。但是,对于非同步 RAM 的访存接口和分支预测来说,它却成了一个“包袱”。遇到的问题是:

  • 如何保证分支发生时,延迟槽指令一定被取回了?由哪个阶段来保证?
  • 分支预测时,发生分支的指令到底是分支指令?还是延迟槽指令?

如何解决这个问题?我陷入了思考。解决这个问题的关键在与如何理解“分支”:从处理器外部看来,分支的发生将原本顺序的取指序地址转移到了另一个位置。在想明白“分支”后,对于 MIPS 而言,发生取指地址转移的位置是延迟槽指令,而非分支指令。于是,自然想到将所有分支在流水线中的发生都延迟到延迟槽指令进行,让延迟槽真正成为延迟槽。这样便解决了上面的两个问题:

  • 分支发生由延迟槽指令发起,流水线只需要刷掉后面所有指令即可,无需保证;
  • 分支预测的对象应为延迟槽指令。

具体实现方法是:分支指令流过执行级时不会立刻触发分支,而是拨动一个“开关”,告诉下一条延迟槽指令是否需要触发分支;待延迟槽指令到达执行级时再触发分支。

尝试启动 ucore

实际上在启动 ucore 前,就已经调试过一个系统软件,也就是大赛初赛要求的系统测试。该系统测试全部使用汇编语言编写,但与功能、性能测试不同的是,需要将 CPU 核集成到一个小型的 SOC 中,操作 UART 控制器这样的外设。由于系统中存在外设,也就不能再通过仿真进行调试,好在 Vivado 工具链中包含一种基于内部逻辑分析单元(ILA)的在线调试工具。它的工作原理是:通过在 SOC 中加入 ILA 单元(综合后占用一些硬件资源)连接到想要抓取的信号上,然后在上板时设定一些触发条件,抓取满足触发条件的时刻附近的波形。上板调试的难度在于真正的硬件具有一定的随机性,不会像仿真那样每次跑出来的结果都一样。而解决的办法也只能是在随机中找规律,按照代码一个函数一个函数地检查,最终定位 CPU 卡住/出错的地方。最后我们发现,大多数错误都与 uncache 访存有关,访存顺序和访存大小都是需要特别注意的。

真正开始尝试启动 ucore 已经是决赛提交前夕。经过我与徐同学的讨论,我们决定跳过 PMON 直接启动 ucore。一方面是因为启动 PMON 需要额外添加若干指令,而我们又没有测试这些指令的代码和环境,从头搭建又要花许多时间,但 ucore 需要添加的指令不多,可以等遇到了再调试;另一方面是因为 ucore 本身体积不大,可以直接烧录到板载的 flash 中,再用一个简易的 bootloader 将其加载到内存中,而无需先启动 PMON 这样稍微复杂一些的 BIOS 兼 bootloader。

由于我们没有搭建 SOC 的经验,因此一开始启动 ucore 时使用的是大赛提供的 soc_up,该 SOC 中支持不少外设,但 ucore 中只有 UART 驱动,因此在接入 CPU 核时我删掉了其他用不到的外设。启动 ucore 主要遇到了两大问题:一是 UART 在读取数据时总是和输入差了 15 个字符;二是进入内核后常陷入例外,多为保留指令和 TLB 例外。

对于第二个问题,经过上板排查后发现是 TLB 例外在 MIPS 规范中的一个空洞。我们的 TLB 延续了体系结构实验中的“并行查找”风格,各 TLB 项之间没有任何优先级;而 ucore 在处理充填的过程中也没有保证同一个虚拟双页对应的 TLB 项唯一,结果运行时一旦遇到同时命中 2 项的虚地址,转换出来的物理地址将会是错误的,于是取指和访存都会受到影响。于是修改了 TLB 充填的函数,便解决了这个问题。

而对于第一个问题,由于当时没有其他的思路,于是我决定从头搭一个 SOC,如果还有问题,只能是软件或 CPU 核的问题。当然从头搭 SOC 不是那么容易的,首先就是在实验课中完全没有过相关的经历和理解,而我则是参看了《计算机系统设计(下册)》后才知道 Vivado 中有一个 Block Design 的功能能够基于 IP 进行 SOC 搭建。在练习使用了几天 Block Design 后,我开始着手进行 SOC 的搭建。在整个 SOC 中,除了 CPU 核之外,必需的外设有:UART、SPI、GPIO(confreg)以及 DDR3。

其中,最为复杂的是 DDR3 控制器,它通过调用 Xilinx IP 核 memory interface generator 来实现。首先是参数配置的复杂,其次是时钟输入的复杂,最后则是复位信号的复杂。不过还好,我参考了清华大学往届的 nontrivial-mips 项目,从中获取了许多启发和设计思路,最终完成了 DDR3 控制器的集成。

结果,使用了新的 SOC 后,UART 问题仍然存在。我觉得更有可能是软件的问题,因为新 SOC 中的 UART 控制器调用的是 Xilinx IP 核 AXI UART 16550,在查阅手册之后我发现寄存器空间与之前不兼容,也许在功能上也有一些细微的差别是我没有注意到的。查手册之余,我在 github 上找到了它的官方驱动程序仓库,于是便照着修改。还是不行。那只能一点一点试了,所有问题集中在 UART 初始化的函数中,我不断改变初始化参数,最后发现只要将 UART 的队列功能关闭,输入即可正常使用(奇怪的是 ucore 最开始的时候默认就是将其关闭的,但是在原来的 SOC 中仍然有这个问题)。

解决了 UART 的问题后,ucore 可以在新的 SOC 上运行了,尽管不是特别稳定。

决赛前的冲刺

大赛要求启动系统后能够运行一些软件来展示系统的完备性,我觉得可以把功能测试里的“记忆游戏”移植到 ucore 中运行。一是它的内容比较简单,移植起来难度不是很大;二是它需要操作 4 种板载外设,也可以证明系统的完备性。在与徐同学交流了我的想法后,他决定试试,最后,他花了不到一天的时间就将记忆游戏成功移植到了 ucore 上运行。

决赛的前一天,我进行性能测试计数时发现了一个诡异的异常:第三个测试点在某些情况下显示结果异常。经过一上午紧张的调试,我们发现又是访存的问题!修改了 d-cache 中不到一行的代码,这个问题得到了解决,修改后的版本也成了我们最终决赛的提交版本。

决赛现场

团队赛的决赛除了可以由大赛组委进行核查的功能、性能、系统测试外,还增加了自定义指令添加和答辩的环节。

自定义指令添加

指令添加最多只允许两名队员参加,于是这个任务便落到了参与核内开发最多的我和徐同学身上。受疫情的影响,决赛全部改为线上进行,我和徐同学提前到教学楼 6 楼找了一个偏僻的教室,这时还没开课,应该也不会有什么人来教学楼。

下午两点,添加指令环节开始,赛题在微信群里公布,还附带了进行仿真的 coe 文件。赛题并不难,我们大致经过不到 30 分钟就通过了仿真。而后上板时,我们发现单色 LED 的亮灭情况似乎和正常的功能测试有所不同,后来阅读了测试代码后才发现是大赛组委为了区分而有意为之,也算是虚惊一场。

答题后的队伍进入了一个指定会议室进行等待,这个环节也不需要提交什么文件,而是由会议中的老师在线进行验收,过程就和实验课差不多。

答辩

答辩是整个比赛的最后一个环节,也就是结合事先准备好的 PPT 进行报告,报告后回答一些专家的问题。说起来答辩有一些遗憾,一个是我在回答第一专家的问题是一直没说到点上,多亏了另一位老师比较熟悉我们的代码风格才帮我们解了围;另一个是在画流水线结构图时由于疏忽没画 TLB,被专家发现并做了提醒。

参赛总结

悟已往之不谏,知来者之可追。

收获

参加“龙芯杯”的收获还是很多的,主要是两点:

  • 系统能力:经历了从 SOC 到系统软件的全栈式开发、移植和调试,对金算计体系结构的深度和广度都有了更加深入的理解和运用;

  • 工程能力:从建立文件夹开始管理整个项目,并且使用、熟悉了整个 IC 前端的设计流程,学会运用一些之前从未使用过的工具进行 SOC 设计。

不足

“龙芯杯”每年赛题固定的比赛性质势必导致竞争越来越激烈,相比于其他赛道,它更像是一个已知题目的“命题作文”。命题作文想要写好看重的是积累,对于清华大学这样的强校,几乎连年包揽特等奖和一等奖,很大程度上得益于较为完整的基础设施。这里的基础设施包括但不限于:

  • 设计流程、设计语言和设计方法
  • 调试工具链
  • 完整的系统软件移植

有了这些基础,他们的起步便已经比我们高很多了。反观我校,我们几乎是从零开始(此处并没有苛责学长的意思,我们十分感激对我们提供过帮助的竞赛小组中的各位学长)。说到底,一方面还是人太少,就更加难成体系;一方面是课程安排上接触专业内容太晚,难以形成积淀。其他学校的实力都是每年上升,而我们还在原地踏步,恐怕之后拿奖都难。

受到设计方法直接影响的是分工问题,核内模块之间紧耦合,难以拆散分配到每个人身上,这就导致了人员分工极不平衡,没有发挥出 4 人队伍的全部实力。

反思

今年过去,我们也成为学弟口中的学长,我们能为他们留下什么?我们的经验是好的经验吗?“龙芯杯”竞赛小组将被引向何方?当这些担子真实地落到肩上,才深感责任之重大。可以肯定的是,走老路是不行的。