ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FPGA上跑Linux:基于RISC-V软核的轻量级系统与在线升级实践

FPGA上跑Linux:基于RISC-V软核的轻量级系统与在线升级实践 FPGA上跑Linux当年我第一次看到这个说法时确实挺好奇的。等自己动手折腾了快一个月才明白CPU核只是冰山一角真正花时间的地方全在总线、DDR、启动链路和系统裁剪上。这篇文章记录一下我完成的一套方案一个核心代码量仅5000行左右的RISC-V软核在FPGA上跑起轻量级Linux并在此基础上做了现场可用的在线升级机制。如果你是FPGA工程师想往RISC-V和嵌入式Linux方向扩展或者你本身是做嵌入式Linux的想搞明白FPGA侧的门道这篇应该都能帮上忙。我不会只贴“成功日志”更多是讲清楚每一步为什么这么做、有哪些坑可以提前避开。1. 为什么是RISC-V软核选型与设计思路1.1 方案选型能跑Linux的软核不是“随便写个CPU”FPGA上做软核CPU通常想到的是MicroBlaze或者NiosII。这两个确实是成熟方案问题是商业授权、封闭生态、定制成本都比较高。换成RISC-V之后开源、指令集精简而且相关工具链已经非常完整很适合自己做软核。但“能在FPGA上跑Linux”有一个硬性门槛处理器必须带MMU也就是内存管理单元。没有MMU的处理器Linux内核根本不愿意配合因为它的进程管理、虚拟内存、fork、exec、用户态与内核态切换全都建立在页表映射和地址保护之上。我之前用过PicoRV32这类只有一两千行代码的小核跑裸机或者RTOS很轻松但要跑Linux就不行了——它压根没有S模式也没有MMU硬上Linux只能跑uClinux很多应用和驱动都会出问题。所以选型时我把重点放在“有MMU、有S特权级、有足够稳定的总线接口”这几个条件上。最终采用的方案是围绕VexRiscv框架扩展出来的一个RV32软核支持Sv32页表、支持M/S/U三级特权模式核心RTL代码量大概5000行。这个规模不算大但足够把Linux内核启动起来。有一点要提前说清楚5000行指的是CPU核心也就是流水线、寄存器堆、MMU、中断和总线接口这些关键部分整个SoC还包括DDR控制器、Flash控制器、UART等外设这些往往通过LiteX这类框架自动生成。不要指望一个文件就撑起整个系统。1.2 架构拆解处理器内部是怎么设计的这个软核采用的是单发射、顺序执行的5级流水线结构。对比那些支持乱序执行、多发射的高性能核它的优势是时序容易收敛面积小逻辑简单出问题好排查。在FPGA上流水线深度和分支预测策略都会直接影响最高时钟频率顺序发射的代价是IPC低一点但换来的是更简单的实现和更稳定的时序这个取舍对FPGA上的软核来说非常合理。为了跑Linux软核必须包含三个关键机制特权级切换M模式运行OpenSBI或BootROMS模式运行Linux内核U模式运行用户进程。这样内核和用户程序可以被硬件隔离。中断和异常使用RISC-V标准的PLIC和CLINT中断控制器提供外部中断和定时器中断。Linux的进程调度依赖定时器中断没有这部分系统启动到一半就会卡住。MMU这个核实现了Sv32页表格式能处理32位虚拟地址到物理地址的转换。没有它Linux就只能退化成uClinux方案。我实际跑起来的时钟频率在90MHz左右性能当然比不上硬核CPU但跑轻量级Linux、跑基础网络服务、跑业务逻辑都够用。很多应用场景其实根本不缺那点性能缺的是能自由改代码的CPU。1.3 为什么代码量能控制在5000行左右“5000行Verilog跑Linux”听起来像魔术其实是设计上做了大量取舍。RISC-V指令集本身就是典型RISC设计指令数量少编码规则整齐这是先天优势。其次这个核没有做浮点单元浮点运算交给软件模拟硬要算浮点会慢但对轻量级Linux来说完全可以接受。也没有做复杂的乱序执行和深度缓存cache用简单直连映射大小也就2KB到8KB够用就行。最关键的一点是大量“处理器外围”工作被外包给了Bus Matrix和现成IP。处理器只需要实现取指、译码、执行、访存、写回这几个核心环节然后通过AXI接口挂到总线上剩下的DDR控制器、SPI Flash控制器、UART控制器都可以用现成方案。这样核心代码自然就压到5000行左右不会膨胀成一个几万行的怪物。2. 硬件平台搭建从RTL到可运行SoC2.1 FPGA板卡选型与存储资源规划跑Linux对资源的要求比裸机高不少首要瓶颈不是逻辑单元而是存储。我用的是Artix-7级别的板卡带有128MB DDR3和16MB SPI NOR Flash。35T这个量级的逻辑资源已经足够放软核加各种外设LUT利用率一般也就60%左右布线压力不大。如果板卡没有DDR只在FPGA片上BRAM里跑Linux那会很痛苦。Linux内核解压后要占用内存initramfs要解压到内存进程栈、堆、页表全都要内存效应期的片上BRAM只有1.8Mbit左右远远不够。所以选板卡时必须确认有DDR/SDRAM最好是DDR3容量至少64MB才比较好跑一个稍微完整的文件系统。Flash容量也要提前规划。一个FPGA bitstream文件大约几MBOpenSBI大约几十到一百多KBLinux内核压缩镜像几MBrootfs取决于你放多少工具BusyBox initramfs可以压到几MB如果放完整工具链就会超过十几MB。16MB Flash能装但比较紧凑有条件建议直接上32MB或64MB给后续在线升级留出空间。2.2 外设与总线搭建CPU之外的另一半工作CPU核心本身不产生价值它必须能访问DDR、能从Flash启动、能和外界通信这就要靠总线把它们串起来。这个SoC采用AXI4总线作为主干CPU作为主设备DDR、Flash、UART、Timer、中断控制器都挂在总线上作为从设备。总线矩阵选择了LiteX自动生成省去了大量手写状态机的工作。地址映射是整个系统最需要提前想清楚的事。我用的映射大约是外设地址范围说明DDR30x40000000 - 0x47FFFFFF主内存Linux运行于此SPI Flash0x20000000 - 0x20FFFFFF存放bitstream、固件、文件系统UART0xF0000000串口控制台与升级通道CLINT/PLIC0xF0010000附近定时器与中断控制器DDR控制器是系统里最大的坑。自己用Verilog写一个DDR3控制器从PHY层到Bank调度工作量不是一两个月能稳定收尾的。实际做法是使用现成方案比如Xilinx MIG IP或者LiteDRAM生成的可综合控制器。这些方案经过大量验证时序和训练逻辑相对可靠你只需要在约束文件里填对时钟频率、引脚位置和DDR型号即可。UART模块在方案里相对简单但也不能掉以轻心。串口控制台是联调阶段最核心的观测手段如果UART都有问题后面全盲调很难受。2.3 启动方式与Flash布局FPGA上电后首先加载的是bitstream。bitstream存在SPI Flash里FPGA厂商的配置引擎会自动从Flash地址0读取配置。配置完成后软核CPU开始复位取指。这时候就会出现一个关键问题CPU从哪里开始执行第一条指令我是把OpenSBI放在Flash里的固定偏移CPU复位后通过BootROM把OpenSBI加载到内存然后OpenSBI初始化S模式环境再跳转到Linux内核入口。Flash布局大致是这样区域偏移内容FPGA配置区0x00000000bitstreamOpenSBI区0x00500000左右引导固件内核A区0x00600000左右Linux内核镜像DTB A区0x00A00000左右设备树内核B区0x00B00000备用内核镜像DTB B区0x00F00000备用设备树rootfs区0x01000000BusyBox文件系统这个分区表在项目一开始就要定好因为后面在线升级全靠它。我最初没重视后面改分区偏移差点把整个系统又折腾了一遍。3. 轻量级Linux系统的构建与启动3.1 软件栈OpenSBI Linux内核 initramfs跑Linux需要在FPGA里跑一套完整的软件栈这里用的是OpenSBI、Linux内核、BusyBox initramfs三件套。OpenSBI运行在M模式下负责最底层的硬件初始化、中断转发和SBI调用服务相当于RISC-V世界里的“固件”。OpenSBI跑完后会跳到S模式启动Linux内核。选择OpenSBI而不是传统的BBL原因是它维护更活跃、功能更完整社区主流基本都转向它了。Linux内核直接使用主线内核源码配置成RISC-V架构。我这里因为是RV32所以工具链前缀是riscv32-unknown-linux-gnu-如果是RV64则用riscv64-linux-gnu-。工具链可以直接下载预编译版本也可以让Buildroot自己编后者更省心。initramfs是最省事的根文件系统方案。它把BusyBox和必要的脚本打包进一个cpio归档再和内核一起编译成镜像。启动时内核会把它解压到内存当作根文件系统。好处是不用管SD卡、不用写Flash根文件系统驱动非常适合软核起步阶段。用Buildroot构建的过程很简单配置目标架构为RISC-V选择BusyBox指定内核源码路径和内核defconfig然后build。一次构建会同时生成工具链、内核镜像和rootfs归档省去很多手工步骤。3.2 内核裁剪的关键配置Linux内核默认配置打开的功能特别多覆盖了各种硬件平台在FPGA这种资源受限环境下必须裁剪。以下几个配置我认为非常关键CONFIG_MMUy CONFIG_SERIAL_8250y CONFIG_SERIAL_8250_CONSOLEy CONFIG_RD_GZIPy CONFIG_INITRAMFS_SOURCErootfs.cpio.gz CONFIG_CMDLINEconsolettyS0,115200 root/dev/ram0CONFIG_MMU必须开启没有MMU就别想跑标准Linux。CONFIG_SERIAL_8250提供串口驱动console输出全靠它。CONFIG_INITRAMFS_SOURCE指向打包好的rootfs内核会把它内嵌进镜像。CONFIG_CMDLINE用于指定启动参数。如果设备树里也有chosen/bootargs两者可能会冲突调试时要留意必要时用CONFIG_CMDLINE_FORCE强制覆盖设备树里的参数。编译前尽量关闭不用的驱动大量的网络协议、声卡驱动、USB主机驱动、GPU驱动等统统不要。裁剪不是靠猜而是先编一版查看镜像大小再启动看内核输出的设备注册信息把用不到的模块关掉。这样一轮轮下来内核镜像可以从几MB压到2-3MB甚至更小。3.3 启动流程与日志解析正常启动时串口应该能看到这样的输出顺序OpenSBI v1.2 ____ _____ ____ _____ / __ \ / ____| _ \_ _| ... [ 0.000000] Linux version 6.1.0-riscv [ 0.000000] Kernel command line: consolettyS0,115200 root/dev/ram0 ... [ 1.200000] Run /init as init process看到“Run /init as init process”意味着内核已经成功挂载rootfs并启动了用户态第一个进程。如果到这里接着出现BusyBox的启动日志和命令行提示符整条链路就通了。我踩过一次很典型的坑日志卡在内核解压完、刚跳转到内核入口就没输出。这种问题通常不是内核本身而是MMU初始化或内存映射参数有问题。定位方法是先把earlycon打开让它尽早输出再就是检查DDR地址是否和链接地址匹配。4. FPGA上的在线升级实现4.1 在线升级到底在更新什么既然系统能在现场跑起来那么“在线升级”这个概念就需要拆开看。在FPGALinux这种组合里升级的对象至少分三层FPGA逻辑升级更新bitstream即CPU、总线、外设的硬件逻辑。固件升级更新OpenSBI、Linux内核镜像、设备树DTB。应用升级更新rootfs里的业务程序、配置脚本等。传统做法是接JTAG线把电脑拿到设备旁边烧Flash这在研发调试阶段没问题但真正的工业现场不可能这么干。在线升级的意义在于系统运行状态下通过网络或串口把新版本传到设备由系统自身完成Flash更新然后重启生效。需要注意在线升级不等于“热切换”。Linux内核本身不能在家里运行、原地把自己替换成新内核操作系统启动过程必须是完整重来一次。所以在线升级的本质是“更新持久化镜像 重启加载新镜像”。4.2 Flash分区设计与A/B备份策略在线升级里最怕的不是升级失败而是升级失败后没有退路。为此我采用了A/B双分区机制。所谓A/B就是把内核和rootfs同时准备两份分别放在不同Flash区域。系统正常从A区启动升级时只写B区。写完B区后校验文件头和数据校验和再修改启动标记告诉引导程序下一次从B区启动。如果B区启动失败引导程序自动回退到A区。MTD分区用起来大概是这样的概念mtdpartsspi0.0:1M(openSBI),4M(kernel_a),4M(kernel_b),1M(dtb_a),1M(dtb_b),8M(rootfs_a),8M(rootfs_b)Linux起来后用cat /proc/mtd查看实际分区# cat /proc/mtd dev: size erasesize name mtd0: 00100000 00010000 openSBI mtd1: 00400000 00010000 kernel_a mtd2: 00400000 00010000 kernel_b mtd3: 00100000 00010000 dtb_a mtd4: 00100000 00010000 dtb_b mtd5: 00800000 00010000 rootfs_a mtd6: 00800000 00010000 rootfs_bA/B分区会多占用一倍Flash容量这是最直接的代价。如果Flash实在紧张至少要对内核分区做双备份因为这是升级中最常改、最容易写坏的部分。OpenSBI和bitstream可以只保留一个黄金版本正常使用中很少更新。4.3 升级流程实战串口、网络与脚本当前系统正常启动时在线升级最直接的做法就是在Linux里写Flash分区。假设备份内核已经通过scp或者串口传到了/tmp/Image执行flashcp -v /tmp/Image /dev/mtd2flashcp会先擦除mtd2分区再把数据写进去比直接dd到/dev/mtd2更安全因为它不会让你误擦超范围。写完内核后设备树也要一起更新flashcp -v /tmp/system.dtb /dev/mtd4然后修改启动标记让引导程序下次从B区启动。如果是U-Boot可以用fw_setenvfw_setenv bootpart b sync reboot如果系统已经完全起不来只能进引导程序恢复那么串口就是最后一条命。在U-Boot或自研bootloader里用Ymodem协议接收文件再写入Flashloady 0x40000000 erase 0x0B000000 0x400000 cp.b 0x40000000 0x0B000000 0x400000这里的地址要格外小心一定是引导程序视角的Flash地址而不是Linux视角的地址。混淆地址是我在调试中犯过的错误一次写错就可能导致系统无法启动。如果没有把握先把旧的Flash内容备份到内存或者用md命令读几个字节确认分区首部特征再动手。整个升级过程最好脚本化脚本里强制做校验md5sum /tmp/Image md5sum /tmp/Image.remote # 不一致则退出没有校验的升级脚本就是在拿现场设备做盲测。4.4 掉电保护与回滚机制在线升级最大的敌人是写着写着断电。如果正在擦写Flash的过程中断电该分区内容既不是旧版本也不是新版本系统可能直接变砖。所以升级策略的底线是永远不要先擦当前启动分区。先把新镜像写到B区校验成功后再切换启动标记。这样就算升级过程断电A区还是好的下次上电还是从A区启动。再搭配一个bootcount机制每次引导程序启动一个分区时在Flash里记录启动次数加1。用户程序正常运行后通过命令把计数清零。如果新分区启动时反复重启计数会持续累积达到阈值后引导程序自动切换回旧分区。这是嵌入式领域通用的回滚方案。FPGA侧还有个特殊的坑bitstream在Flash里如果被写坏FPGA会配置失败整块板子直接起不来。我的办法是做一个极简的“golden”bitstream放在Flash最前面现场需要更新逻辑时只更新后面主bitstream区域如果主bitstream校验失败FPGA硬件引擎会回退到golden配置。golden版本只做基础功能保证系统能启动、能继续做恢复操作这相当于给硬件上了一道保险。5. 踩坑实录常见问题与排查技巧5.1 串口完全没有任何输出遇到“上电后串口死寂”的情况第一反应不要怪内核先确认硬件和RTL。按这个顺序排查会更高效确认时钟和复位。用逻辑分析仪或ILA抓一下CPU时钟是否在跑复位释放时间是否正常。确认UART波特率。串口终端必须和RTL里的波特率分频一致。RTL里如果写成9600终端却设成115200自然没输出。确认UART引脚绑定。很多板卡USB转串口芯片的TX/RX方向容易搞反或者共地没接好。确认内核命令行真的传到了。很多时候不是没输出而是consolettyS0,115200没生效内核输出没走串口。其中“硬件方向接反”这种低级错误最容易让人白折腾半天。遇到串口没输出我会先用万用表或者示波器看USB转串口芯片的TX引脚确认有没有波形再考虑软件问题。5.2 内核启动到一半卡住或重启如果串口日志能打出来但卡在“Uncompressing Linux... done, booting the kernel”之后通常是MMU和内存的问题。这里有个典型的连锁反应MMU初始化时页表设置不正确内核第一条访问虚拟内存的指令就触发异常异常处理又没准备好于是挂死。我遇到过几次这样的卡顿最后定位都是DDR训练参数不匹配。解决思路是确认DDR控制器的时序参数和真实内存颗粒匹配特别是tRCD、tRP、tCL这些值。在OpenSBI里加一个简单的内存读写测试启动时就对DDR地址范围做一遍踏步读写提前暴露问题。确认内核链接地址和DDR物理地址对应。如果CPU从0x40000000取指内核镜像在编译时也必须指定运行地址在该区域两者不一致时表现就是各种莫名卡死。5.3 Flash写入失败或者升级后无法启动升级脚本执行完文件也显示写入成功重启后系统却起不来。这种情况大概率是分区偏移错了。Flash厂商的擦除块大小不统一有的256KB有的64KB。如果分区定义和Flash实际擦除块没对齐写入时可能把别的分区擦掉。查看/proc/mtd里的erasesize再对一下分区偏移就能发现很多问题。还有一次是升级后忘记sync。Linux有缓存机制flashcp写完后数据还在缓存立刻断电可能丢失。升级脚本里必须加sync最好再延时等缓存落盘。这是在线升级最容易被忽略的细节。为了救砖我强烈建议在Flash最前面留一块完全不被业务升级触碰的“急救区”放一个能通过串口下载文件并写Flash的最小引导程序。配置好这个后再折腾在线升级心里会踏实很多——最坏情况下顶多回退或者串口重刷不需要开盖拆Flash用编程器。5.4 地址映射导致的“灵异现象”调试过程中我遇到过一个很隐蔽的现象内核跑起来了命令行也能执行但偶尔访问某个外设时系统重启。后来发现是总线地址映射有重叠区CPU发出去的外设访问地址同时命中了Flash和UART两个从设备。总线仲裁器看到多个设备同时应答直接挂死。这类问题排查起来非常烦因为现象随机。建议在SoC搭建阶段就要一份清晰的总线地址分配表检查是否有重叠区域。另外在RTL里给总线加一个默认slave一旦地址没匹配到任何设备就返回一个错误响应而不是让总线挂在未知状态。这个默认slave设计虽小却能帮我省下大量的摸黑调试时间。最后再分享一个小技巧在线升级这套机制不要等整个系统全部调通了才做最好在第一次Linux能启动时就顺手把Flash分区和升级脚本写好。早期分区改动成本低等业务代码和上层驱动都依赖固定地址后再改分区才是真的伤筋动骨。先把升级通道打通后面不管调内核还是调应用都能省很多事。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表