
1. 龙芯杯到底在比什么先搞清楚赛道再谈技术很多人第一次听到“龙芯杯”这三个字第一反应是“又一个电子设计竞赛”然后就去翻往届题目看到一堆LoongArch、Verilog、FPGA、SoC之类的词直接懵了。我当初也是这个状态。但真正把比赛规则和评分标准啃完之后才发现这个比赛的逻辑其实非常清晰它要求你在FPGA上从零搭建一个能跑LoongArch指令的处理器系统不是调库不是跑Demo而是从指令集手册开始一行一行写RTL最后让程序在你的CPU上跑起来。这件事的难度在于它同时横跨了三个通常被分开学习的领域计算机体系结构、数字逻辑设计、FPGA工程实现。你在课堂上学的流水线、Cache、总线协议在这里全部要变成可综合的Verilog代码你平时用Vivado点点鼠标生成的IP核在这里必须理解它背后的时序逻辑你以为写完了RTL就完事了结果综合报一堆DRC错误、时序不收敛、板级验证跑飞。所以这篇文章不是一篇“比赛介绍”而是一个完整的技术路径拆解——从你决定参赛那一刻起到最终在FPGA开发板上看到程序正确输出的全过程。适合谁看如果你是计算机或电子相关专业的学生学过数字电路和计算机组成原理但从来没有独立做过一个完整的处理器项目那这篇内容就是为你写的。如果你已经有一定FPGA开发经验但没接触过LoongArch指令集也可以直接跳到指令集和流水线设计的部分。我会尽量把每个关键决策背后的“为什么”讲清楚而不是只给一堆步骤让你照抄。2. LoongArch指令集你真正需要吃透的部分2.1 为什么不是MIPS不是RISC-V偏偏是LoongArch这是很多人第一个疑问。MIPS资料多、RISC-V生态好为什么比赛要用LoongArch原因其实不复杂龙芯杯的主办方希望推动国产指令集生态的建设而LoongArch是龙芯中科自主设计的指令集架构它继承了MIPS的很多设计思路但在指令编码、特权模式、地址翻译等方面做了大量简化和重新设计。从参赛者的角度看这其实是好事。LoongArch的指令格式比MIPS更规整手册写得也相对清晰。但问题在于网上可参考的开源实现远不如RISC-V那么多你很难找到一份“抄了就能跑”的代码。这意味着你必须真正理解每一条指令的语义而不是靠抄开源项目蒙混过关。我个人的建议是先把LoongArch的手册通读一遍重点看指令编码格式和异常处理机制。不需要一开始就记住所有指令但必须搞清楚几条核心指令的格式比如add.w、ld.w、st.w、beq、b、bl这些。因为你的译码模块就是根据这些编码来生成的如果连指令格式都搞错了后面全白搭。2.2 指令译码的实操细节从32位编码到控制信号LoongArch的指令长度固定为32位格式分为2R、3R、2RI8、2RI12、2RI16等几种。以3R型指令为例它的编码结构大致是操作码在高位三个寄存器索引分别占据固定的位段。你需要做的第一件事就是写一个译码模块把32位的指令拆解成操作码、源寄存器1、源寄存器2、目的寄存器、立即数如果有。这里有一个很容易踩的坑LoongArch的立即数在不同指令类型中的位段位置和符号扩展方式是不一样的。比如2RI12类型的立即数是12位需要符号扩展到32位而2RI16类型的偏移量是16位在计算跳转目标时需要左移两位再符号扩展。如果你在译码阶段没有正确处理这些细节后面执行阶段就会出现莫名其妙的地址错误。我的做法是在译码模块里用一个case语句根据操作码分类处理每种类型单独提取字段并做符号扩展。代码结构大概是这样的always (*) begin case (opcode) 7b0000000: begin // 3R类型 rs1 inst[9:5]; rs2 inst[14:10]; rd inst[4:0]; imm 32b0; end 7b0001000: begin // 2RI12类型 rs1 inst[9:5]; rd inst[4:0]; imm {{20{inst[21]}}, inst[21:10]}; end // ... 其他类型 endcase end这段代码看起来简单但实际写的时候你会发现LoongArch的指令编码并不是完全规整的有些指令的操作码分布在不同的位段需要仔细对照手册。我当时的做法是把手册中所有指令的编码整理成一张Excel表然后逐条核对译码逻辑确保没有遗漏。2.3 那些手册上不会告诉你的指令集坑手册上写的都是理想情况实际实现的时候有几个地方特别容易出问题。第一个是寄存器0的处理。LoongArch中寄存器0恒为零这意味着你不能往寄存器0写数据读寄存器0永远返回0。听起来很简单但在流水线实现中如果你在写回阶段没有对目的寄存器做判断就可能出现寄存器0被意外写入的情况。我的做法是在写回模块加一个判断if (rd ! 5b0) regfile[rd] result;。第二个是指令对齐问题。LoongArch的指令地址必须是4字节对齐的如果你的跳转指令计算出的目标地址不是4的倍数硬件应该触发地址错误异常。但在初版实现中很多人会忽略这个检查导致程序跑飞后完全不知道发生了什么。建议在取指阶段就加上对齐检查一旦发现异常就跳转到异常处理入口。第三个是延迟槽。LoongArch和MIPS一样有延迟槽的概念也就是说跳转指令后面的那条指令会先于跳转执行。这个机制在流水线中处理起来很麻烦因为你需要保证延迟槽中的指令在跳转生效之前完成执行。我的建议是在第一版实现中可以先不支持延迟槽把跳转指令当作普通指令处理等基本功能跑通之后再加入延迟槽支持。3. 从零搭建流水线五级结构的设计取舍3.1 为什么选五级流水线而不是三级或七级流水线的级数选择是一个经典的权衡问题。级数越多理论上的主频越高但冒险处理的复杂度也越大。三级流水线取指、译码执行、写回结构简单但每一级的工作量太大时序很难收敛。七级流水线虽然主频可以做得更高但数据冒险和控制冒险的处理会变得非常复杂对于第一次做处理器的人来说调试难度会急剧上升。五级流水线取指IF、译码ID、执行EX、访存MEM、写回WB是一个比较平衡的选择。每一级的工作量适中时序相对容易满足冒险处理也有成熟的方案可以参考。我在实际实现中选的就是五级结构最终在FPGA上跑到了50MHz左右的主频对于比赛来说已经够用了。当然如果你有足够的经验和时间也可以尝试更深的流水线。但我的建议是先把五级流水线跑通再考虑优化。很多队伍一上来就想做七级甚至更深的流水线结果冒险处理没做好连最基本的程序都跑不起来。3.2 数据冒险前递和阻塞的配合使用数据冒险是流水线中最常见的问题。比如下面这段代码add.w $r1, $r2, $r3 # r1 r2 r3 add.w $r4, $r1, $r5 # r4 r1 r5第二条指令需要用到第一条指令的结果但第一条指令的结果要到WB阶段才写回寄存器堆而第二条指令在EX阶段就需要这个值。如果不做处理第二条指令读到的就是旧的r1值。解决方案有两种前递Forwarding和阻塞Stalling。前递的思路是把EX阶段或MEM阶段的结果直接送到需要它的指令的输入端而不是等写回。阻塞的思路是让流水线暂停几个周期等结果写回后再继续。我的实现中同时用了这两种机制。对于EX阶段就能拿到结果的指令比如算术运算用前递解决对于需要等MEM阶段结果的指令比如load如果下一条指令紧接着就要用这个值就需要阻塞一个周期。具体的判断逻辑是// 前递判断 if (ex_mem_regwrite ex_mem_rd ! 0 ex_mem_rd id_rs1) forward_a 2b10; // 从EX/MEM前递 else if (mem_wb_regwrite mem_wb_rd ! 0 mem_wb_rd id_rs1) forward_a 2b01; // 从MEM/WB前递 else forward_a 2b00; // 不从流水线前递这里有一个细节load指令的结果在MEM阶段才可用如果下一条指令在EX阶段就需要这个值前递是来不及的必须阻塞一个周期。这个判断条件需要单独处理否则会出现数据错误。3.3 控制冒险分支预测的最简方案控制冒险是指跳转指令导致的流水线冲刷问题。当一条分支指令在EX阶段计算出跳转目标时后面已经取入流水线的指令可能都是错的需要冲刷掉。最简单的处理方式是总是预测不跳转也就是继续取顺序的下一条指令。如果分支实际跳转了就把后面取入的指令冲刷掉从新的目标地址重新取指。这种方式的代价是每次跳转都要浪费几个周期但实现简单不容易出错。我在第一版中用的就是这个方案。后来为了提升性能加入了一个简单的两位饱和计数器分支预测器对于循环类的代码效果比较明显。但说实话对于比赛来说最简单的方案往往是最稳妥的。先把功能跑通性能优化可以后面再说。3.4 流水线寄存器的命名和复位策略流水线寄存器是连接各级的桥梁命名清晰与否直接影响你调试时的效率。我的命名习惯是if_id_*表示取指到译码的寄存器id_ex_*表示译码到执行的寄存器以此类推。每个寄存器都包含数据和控制信号两部分。复位策略也很重要。建议只对PC和流水线寄存器做同步复位不要对寄存器堆做复位。因为寄存器堆的复位会导致综合工具生成大量的额外逻辑而且实际上程序启动时会自己初始化需要的寄存器。另外复位信号最好做同步处理避免亚稳态问题。4. FPGA实现从RTL到比特流的那些坑4.1 Vivado工程创建的几个关键设置Vivado是Xilinx FPGA的官方开发工具版本选择上建议用2020.2或2022.2这两个版本比较稳定网上教程也多。创建工程时有几个设置需要注意第一器件型号一定要选对。龙芯杯通常指定特定的开发板比如基于Artix-7或Kintex-7的板子。如果你选错了器件型号后面引脚约束和时序分析都会出问题。第二综合策略选择。Vivado默认的综合策略是Vivado Synthesis Defaults对于处理器这种组合逻辑较深的设