NEORV32:用FPGA实现一颗可裁剪的RISC-V软核MCU

NEORV32:用FPGA实现一颗可裁剪的RISC-V软核MCU
最近有个朋友跟我聊到一个问题MCU 到底能不能做到 FPGA 的效果我一开始觉得这问题问反了一个是顺序执行处理器一个是并行逻辑阵列底层思路差得挺远。直到我认真玩了一个开源项目才明白很多人想问的其实是能不能既留住 MCU 的软件生态和快速开发又用上 FPGA 的可重构、并行和自定义硬件能力。这个项目叫 NEORV32它把一个完整的 32 位 RISC-V MCU 用可综合的 RTL 实现塞进了 FPGA 里。简单说你在 FPGA 上烧入 bitstream 之后这个软核 MCU 就能像 STM32 一样跑 C 代码但外设、总线甚至自定义指令都可以自己改。这套思路对刚入 FPGA 的人、做软硬件协同开发的工程师、以及被 MCU 外设限制逼疯的嵌入式老手都特别有参考价值。我把这个项目前前后后折腾了一遍从顶层设计拆到引脚约束从编译第一个点灯程序到拿真实 MCU 和它通信。这篇文章我不打算只贴 README而是把项目为什么敢这么设计、上手时会踩哪些坑、以及我对“MCU 和 FPGA 到底怎么融合”的理解都写出来希望能帮你省点时间。1. 项目整体设计与思路拆解1.1 它到底解决了一个什么“拧巴”的问题MCU 的优势在于价格低、功耗低、上手快NVIC 中断、定时器、DMA、各种通信接口都是现成的写软件比写逻辑快得多。FPGA 的优势则是真正的并行执行、引脚级时序可控、逻辑可以随时改但开发门槛高CPU 内核、总线、外设控制器这些基础设施几乎都要自己搭。NEORV32 的思路很讨巧既然 FPGA 里什么都能搭那我干脆把一颗“标准 MCU”用硬件描述语言完整复刻出来然后再把你可能需要的 MCU 外设也一一实现最后做成一个高度可裁剪的片上系统。你拿到手的不再是一个需要从零开始拼 CPU 和外设的裸 FPGA而是一个带 bootloader、带驱动程序、带示例工程的软核 MCU。这样“MCU 的易用性”和“FPGA 的灵活性”就第一次真正揉在了一起。我最早接触软核 CPU 时总觉得这东西是玩具性能肯定不行。但实际跑下来发现NEORV32 在 50MHz 到 100MHz 的常见 FPGA 频率下做裸机控制、协议解析、采集预处理完全够用。它不是拿来和 i.MX 或者树莓派比性能的它的定位就是“一颗你随时能改外设、改总线映射甚至改指令集的 MCU”。1.2 软核 MCU 和“MCU FPGA 双芯片方案”有什么区别很多人第一反应是我直接用一颗 STM32 加一片 FPGA把通信接口拉好不就行了这确实是工业上最常见的做法我也做过类似项目用 STM32H743 的 FMC 总线去读写外部 FPGA 的寄存器把 FPGA 当成一个大内存外设来操作。这个方案优点是成熟、灵活、算力分开缺点是板级复杂度高、通信带宽受接口速度限制、还要处理跨时钟域和信号完整性调试起来从示波器到逻辑分析仪都得备齐。NEORV32 这种片内软核方案最大的优势是 CPU 和总线、外设之间不再有物理引脚和 PCB 走线访问延迟是“纳秒级”带宽是“总线宽度 × 时钟频率”。你可以在同一片 FPGA 里既跑软件流程又用硬件逻辑做并行加速两边通过片上总线直接交互省掉了外部通信协议的开销。当然软核 MCU 也替代不了独立 MCU 和 FPGA 搭配的所有场景特别是断电后需要低功耗待机、需要大量隔离模拟前端的场景独立芯片还是更靠谱。所以我更愿意把 NEORV32 理解成一种“软硬协同设计的一体化原型平台”而不是非此即彼的替代品。1.3 架构全景一颗可以被“裁剪”的 MCUNEORV32 的架构层次非常清晰从底往上大致是RISC-V CPU 核心、片上总线、存储器和外设组。CPU 默认支持 RV32I/E 基础指令集可选 M 扩展乘除法、C 压缩指令、Zmmul、Zfinx 等总线采用经典 Wishbone 架构把指令访问和数据访问统一到同一套地址空间通过不同地址窗口映射到 IMEM指令内存、DMEM数据内存和各种外设。让我觉得最惊喜的是它的外设清单。GPIO、UART、SPI、I2C、定时器、看门狗、中断控制器这些常规项都有还带了 PWM、CRC、真随机数发生器、片上调试模块、onewire 接口等等。重点是所有这些外设都能通过顶层参数决定要不要综合出来用不到的外设直接关闭资源占用和时序压力立刻降下来。这种“按需定制”的能力正是 MCU 芯片无法给你的也正好补上了“为什么非要用 FPGA 做一颗 MCU”这个问题的一块拼图。我把自己常用的几个模块整理成了下面的表方便对照理解它到底做了什么模块类型配置选项典型用途CPU 内核RV32I/E M/C/Zmmul 可选跑主逻辑、算法、协议栈总线Wishbone 主从接口统一访问 IMEM/DMEM/外设存储器IMEM/DMEM 大小可配程序加载和运行数据空间通信接口UART/SPI/I2C/onewire和外部 MCU、传感器通信定时控制定时器、WDT、PWM延时、看门狗复位、电机控制调试接口JTAG/片上调试模块在线断点、变量查看杂项CRC、TRNG、Sysinfo校验、随机数、芯片信息读取这套结构其实非常贴近真实 MCU 的“内核 总线矩阵 外设”模型。你如果接触过 STM32 的 RCC 时钟树、ARM 的 AHB/APB 总线层次再看 NEORV32 会毫无压力。甚至因为它开源你可以直接打开 RTL 源码查看某个外设的寄存器是怎么映射的、总线仲裁是怎么处理的这种透明度是商业 MCU 永远给不了你的。2. 核心细节解析与实操要点2.1 “上电第一件事”软核 MCU 的复位和启动流程所有 MCU 上电后都要面对同一件事从哪个地址取第一条指令。NEORV32 把 RISC-V 的复位向量设计在地址 0x00000000但在 FPGA 里你不能像真芯片那样在内部 Mask ROM 中烧死一段出厂固件所以项目采用了一个很聪明的做法把 bootloader 在综合时直接初始化到片内 ROM/IMEM 里。FPGA 配置完成后软核 CPU 自然复位然后从地址 0 开始执行 bootloader。bootloader 完成的事和普通 MCU 的 system bootloader 类似初始化堆栈、配置串口或调试口、等待用户通过 UART/SPI/JTAG 下载应用程序到 IMEM/DMEM然后跳转到用户代码入口。所以整个启动过程会有一个“FPGA bitstream 配置 - 软核复位 - bootloader 运行 - 用户程序加载- 执行”的链条。如果你曾经调过 MCU 的 boot 引脚对这个流程会非常亲切。实操中我特别提醒一点复位信号释放时机很关键。很多 FPGA 板子上的 FPGA 逻辑复位由外部按键或电源芯片产生如果你在时钟还没有稳定时就把复位释放了CPU 可能从一个中间态开始跑表现就是 bootloader 无输出或者行为随机。稳妥做法是把外部异步复位经过同步器和延迟释放逻辑处理后再送到软核复位端或者直接用片内 PLL 的 locked 信号作为复位释放条件。2.2 关键配置项参数怎么选才不会把项目带沟里NEORV32 的可配置参数非常多刚接触的人容易眼花缭乱。我建议第一次上手不要追求“全都要”而是按照“最小可跑”原则来。CPU 扩展先只开基础项外设只留 UART 和 GPIO等通了以后再逐步加乘法指令、SPI、I2C、定时器。因为每开一个功能都会增加资源占用某些选项组合还会把关键路径变长导致主频上不去。几个典型配置点我列一下我的实践经验CPU 扩展如果要跑浮点算法或者频繁乘除法就开 M 扩展如果资源紧张只做裸机控制M 扩展可以先不开。硬件乘除法开 M 扩展后综合工具一般会用 DSP 资源不开就退化成软件乘法速度差很多但对时序更友好。片上内存大小IMEM 大小决定你能下载多大的程序DMEM 大小决定运行时的堆栈和全局变量空间。两个都建议设成 8KB 起步调试时少很多“内存不足”的苦恼。UART 分频如果你板载晶振是 50MHz又想让串口输出 115200 波特率必须正确设置时钟分频系数否则乱码问题会困扰你很久。调试模块需要在线调试再开它本身会占用一部分逻辑资源并且引入额外的时钟域处理逻辑不开能省一块资源。这里还要多说一句关于“配置怎么传进去”的问题。NEORV32 顶层是一个带大量参数的模块你在例化时通过 parameter 传递数值。如果你在例化时漏掉了某个新版本引入的参数综合工具会报错或者用默认值因此升级项目版本之后一定要重新对着rtl/neorv32_top.v的参数表核对一遍不要盲信旧代码。2.3 从 MCU 思维理解 FPGA 的资源和时序约束很多只写过 MCU 代码的人第一次在 FPGA 里综合出软核 CPU 后会惊讶于资源占用怎么这么“夸张”。其实一颗 RISC-V 软核的基本资源开销并不高NEORV32 精简配置下可能只占几千个 LUT全功能配置可以有十足的存在感LUT、FF、BRAM、DSP 全线出动。打个比方FPGA 可编程资源相当于一间餐厅软核 MCU 是租下前厅开店外设就是店里雇的服务员和摆的桌子桌子越多越气派但能腾出来给“现场乐队”自定义加速逻辑的地方就少了。所以在真正开始加自己的并行处理逻辑之前一定要先把软核的资源占用摸清楚。Vivado 综合后看 utilization 报告如果是 Gowin 或者安路这类国产工具看资源报告的位置也大同小异。我惯用的方法是先单独综合 NEORV32 得到一个“基础资源红线”然后留出至少 30% 到 50% 的 LUT 余量给自定义硬件逻辑否则后面想加并行加速器时会发现没地方放了。时序上更要留神。软核 CPU 是一个复杂的同步逻辑系统如果时钟约束不写或者写到 200MHz 但实际最多只能跑 120MHz综合实现后必然出现时序违规。你想让软核跑得稳第一件事就是给顶层时钟约束一个合理目标频率比如 50MHz 或 80MHz然后看 WNS最差时序裕量是否为正。WNS 是负数时优先考虑降频、关掉部分外设优化布线再考虑改代码优化关键路径。3. 实操过程与核心环节实现3.1 拉取项目并搭建最小可跑工程我先说获取和目录结构。将 NEORV32 仓库 clone 到本地后重点看两个目录rtl/是全部硬件源码sw/是软件示例、驱动和链接脚本。项目还提供了 setup 脚本和多种开发板示例但我更推荐新手自己新建一个空白工程然后手动把 RTL 源码加进去这样你能知道每一份代码是干什么的而不是盲依赖脚本。以 Vivado 为例操作流程大概是新建工程 - 芯片型号选你的 FPGA - 把rtl/neorv32/下所有.v文件加入工程 - 新建一个顶层 Verilog 模块 - 例化neorv32_top- 编写 XDC 管脚约束 - 综合实现生成 bitstream。如果你想用高云或安路平台原理一模一样唯一区别是工程建立方式和约束文件格式。顶层例化的核心内容大致长这样我简化后给你参考module top ( input wire clk_50m, input wire rst_n, output wire uart_txd, input wire uart_rxd, output wire [1:0] led ); wire clk; wire pll_locked; // 假设你使用一个 PLL 把 50MHz 转成 100MHz 给软核 my_pll pll_i ( .clk_in1 (clk_50m), .clk_out1(clk), .locked (pll_locked) ); reg [3:0] rst_shreg; always (posedge clk) begin rst_shreg {rst_shreg[2:0], ~rst_n pll_locked}; end wire rst ~rst_shreg[3]; neorv32_top #( .CLOCK_FREQUENCY (100_000_000), .INT_BOOTLOADER_EN (1), .CPU_EXTENSION_RISCV_M (1), .UART0_EN (1), .GPIO_EN (1) ) neorv32_i ( .clk_i (clk), .rstn_i (~rst), .uart0_txd_o(uart_txd), .uart0_rxd_i(uart_rxd), .gpio_o (led) ); endmodule注意这个顶层不是完整可复制代码具体信号名和参数要按你实际使用的 NEORV32 版本来。我的意图是让你看到例化一个软核 MCU 并不比例化一个普通 IP 复杂多少。关键是复位和时钟的处理一定不要把时钟直接接到未经 PLL 处理的粗糙时钟上也不要让复位信号在自己的逻辑里乱跑。3.2 综合、实现和烧录以及“型号不存在”这类坑写完顶层后接下来就是综合。如果你在 Vivado 里发现“这个 FPGA 型号在库里没有”不用慌这通常不是工程问题而是器件库没装全。你要么在安装 Vivado 时勾选对应系列器件支持要么到官方网站下载对应器件库在线安装也可以直接设置工程在用“board part”时选择已安装的器件。如果是国产 FPGA 工具一般只支持自家厂家的器件换型的时候更要提前确认支持列表。约束文件 XDC 里至少要写清楚三件事时钟频率、UART 引脚、复位引脚。我遇到过不少人把所有引脚约束写完后却忘记在综合属性里设置“dont touch”导致复位信号在综合时被优化掉上电后软核永远停在不复位状态。一般对异步复位信号加keep TRUE或者“dont_touch”属性比较安全。下载 bitstream 之后如果你用板载 USB-UART 连接电脑打开串口终端设好波特率按下复位键理论上能看到 NEORV32 bootloader 的输出。看到类似“NEORV32 bootloader”的字符就说明软核已经活了。什么输出都没有时先查时钟约束再用 ILA 或 SignalTap 抓一下复位和时钟信号千万别一上来就怀疑 CPU 逻辑坏了。3.3 编译并下载第一个用户程序软核跑起来只是第一步真正让它像 MCU 一样干活需要编译用户程序然后通过 bootloader 加载。项目软件侧提供了大量示例比如常见的 blink、uart 回环等。你需要在电脑上准备一套 RISC-V GNU 工具链发行版带 riscv64-unknown-elf-gcc 或 riscv32-unknown-elf-gcc 都行建议按照 NEORV32 文档推荐的版本安装。进入sw/example/blink目录执行 make生成的目标文件里有一个.hex或.bin这就是要下载到软核里的用户程序。如果你不想自己编译项目也常常提供预编译的neorv32_exe.bin可以直接用串口工具发送。bootloader 的使用方式很简单在串口终端里根据菜单提示选择把 hex 文件“上传”到 RAM然后输入运行命令启动。这里我最容易踩的坑是串口工具的“发送文本模式”和“发送 HEX/BIN 文件模式”搞混。bootloader 接收的是 Intel HEX 文本不是裸二进制文件所以工具必须选择“发送文本文件”或“上传 HEX”如果你直接发送 .bin大概率会出现校验错误或者程序跑飞。程序下载完成并运行后看到 LED 按你的预期闪烁那一刻你会觉得一切都是值得的。这一步实际上已经完整复现了“MCU 的软件下载-运行流程”而且你完全可以再烧回去改写外设逻辑再重新下载这种“同一个芯片不同时间拥有不同外设”的体验就是软核加 FPGA 独有的乐趣。3.4 让真实 MCU 和软核 MCU 配合一种非常典型的用法你可能会说既然软核 MCU 这么好是不是就不需要独立 MCU 了也不全是。我最近做实验时特意把一颗 STM32H743 通过 FMC 总线接到了一个包含 NEORV32 的 FPGA 上让 FPGA 作为一个从设备挂在 STM32 的地址空间里。FPGA 内部的软核负责处理对时间要求较高的协议解析和信号采集STM32 则做应用逻辑、人机交互和网络协议。两边的数据交换对 STM32 来讲就是读写几个地址内部软核则通过 FPGA 片内的接口寄存器快速响应。这种模式在实际项目里非常常见。你从 MCU 发出的数据要跨越芯片边界进入 FPGA就必须处理信号完整性和跨时钟同步问题。一个简单可靠的做法是所有跨芯片信号都做成寄存器输出读操作采用握手或者 FIFO避免让 MCU 直接异步采样 FPGA 内部高速信号。如果你用 SPI 或者 I2C 连接也要把时钟极性和相位约定清楚否则数据偶尔错位很难排查。另一个例子是热词里提到的 HUSB238 这类 PD 芯片与 MCU 的 IIC 通信。实际工程中你可以把 HUSB238 接在 FPGA 引脚上让软核 MCU 用 I2C 外设去读取 PD 协商结果也可以把 I2C 接口做到自己的自定义逻辑里完全绕过 CPU。这不只是技术验证更像是一套“MCU 时代的外设接入方式 FPGA 时代的自定义硬件加速”的组合拳。4. 常见问题与排查技巧实录4.1 上电后串口乱码或没有任何输出这是新手最容易碰到的问题。乱码大概率是 UART 波特率分频算错了CLOCK_FREQUENCY 参数是 100MHz但实际 PLL 输出是 50MHzbootloader 和串口终端波特率对不上出来就是一堆字符。此时先检查顶层约束的时钟频率和 NEORV32 参数是否一致再用示波器或逻辑分析仪看 TX 引脚有没有电平翻转。完全没输出时常见原因是复位信号释放错误或引脚约束没生效建议用内嵌逻辑分析仪抓复位和时钟。4.2 程序编译成功但下载后跑飞程序在 PC 上编译是好的下载到软核就飞了这一般不是 C 代码 bug而是启动流程问题。先确认你下载的是 bootloader 能识别的 HEX 格式再确认链接脚本里 ROM 起始地址和 bootloader 跳转入口匹配。NEORV32 的示例工程默认链接脚本已经配好但如果你自己改过内存大小就要同步调整链接脚本里的地址定义。另外下载过程被串口工具中断也会留下半截镜像建议下载完成后重新复位再看行为。4.3 综合后时序跑不到目标频率软核能不能跑高主频取决于你选了哪些扩展、外设和综合策略。最直接的办法是降低目标频率很多板载逻辑用 50MHz 或 80MHz 已经完全够用。如果必须上高频可以关掉硬件乘除法、关掉调试模块、减少不必要的外设和总线主端口然后在综合工具里开启寄存器复制或性能优化模式再配合合理的管脚分配通常能把关键路径压下来。4.4 OpenOCD 或 JTAG 调试连不上NEORV32 支持片上调试但前提是你编译 bitstream 时把调试模块打开并且外部 JTAG 引脚连接正确。我遇到过不少情况是调试器连不上其实原因很简单FPGA 的 JTAG 引脚和调试器引脚没有共地或者板载 JTAG 链路里还有其他芯片抢占了 TDI/TDO 通路。检查供电、共地、断开其他 JTAG 设备再用 OpenOCD 的扫描命令确认链路基本能解决绝大多数问题。我把上面这些常见问题整理成一张速查表方便你对着排查现象优先排查点常见解决办法串口无输出时钟/复位/引脚约束检查 PLL 锁定加同步复位核对约束串口乱码波特率分频参数核对 CLOCK_FREQUENCY重算 UART 分频程序跑飞镜像格式/链接地址使用 Intel HEX调整链接脚本入口综合时序不过关键路径/扩展项降频、关扩展、优化布局布线JTAG 连不上电平/接地/链路共地断开其他器件扫描链路外部通信不稳定跨时钟域/电平接口加同步器、FIFO检查协议参数4.5 几个和“MCU 转 FPGA”特别相关的习惯问题我观察到一个现象很多 MCU 经验丰富的人第一次写 FPGA 时会把所有逻辑都写在always块里习惯性想着“我要在程序里做状态判断”。FPGA 的思维方式应该是“我要生成一个电路结构”。比如你想让 LED 闪烁不要想成 C 语言里的delay循环而要想成一个计数器在时钟边沿不断累加、比较器在计数到某个值时翻转输出。软核 MCU 的学习曲线虽然比纯逻辑设计平滑但解决了“怎么把程序跑起来”之后一定要回头补习数字逻辑的基本功否则永远只能把 FPGA 当成一个“能跑 C 的外设盒子”来用可惜了它真正的价值。5. 经验沉淀这个项目给我的启发与可扩展方向5.1 对 MCU 开发者FPGA 不再是遥不可及的天花板我接触过很多只写单片机的工程师他们对 FPGA 的第一反应是“那是硬件工程师的活”。但 NEORV32 这类开源软核项目其实把门槛大幅降低了你可以继续用 GCC、写 C 代码、调串口FPGA 只是承载你软件的平台之一。当你把第一个点灯程序下载到软核里跑通后再去学习 Verilog、理解并行电路心态完全不一样因为你知道自己代码最终会变成什么样的电路而不是在一个黑盒子里运行。5.2 对 FPGA 开发者软核是验证和自动化提升的利器反过来FPGA 工程师也不要觉得软核只是“软件工程师的玩具”。在实际项目中用软核 MCU 跑寄存器初始化、跑测试向量、做主控状态机能极大减少使用 Verilog 写复杂控制逻辑的痛苦。你可以把一个数据采集链路里的控制逻辑交给软核把高速并行数据通路用纯逻辑实现两边通过片上总线协作。这种软硬协同的架构在中大型 FPGA 项目里越来越常见。5.3 后面可以往哪些方向发展我最近正在尝试的方向是在 NEORV32 里挂一个自定义协处理器模块用自定义指令来实现一个简单的 FIR 滤波或卡尔曼滤波加速。这其实就是很多商用 RISC-V 芯片里的“自定义指令扩展”思路只不过在这里指令集、总线、外设都是开源的你可以一点点看着自己的设计从软件模拟变成真实硬件。另一个方向是把它和外部高速接口结合起来比如 LVDS 图像输入采集、PCIe 链路初始化甚至 MIPI 摄像头数据接收先用软核把寄存器配置和链路训练搞定再用 FPGA 逻辑去处理高速数据流。我个人在实际操作中的体会是如果你想通过一个项目把 MCU 和 FPGA 这两个领域串起来理解NEORV32 是很好的起点。它不会像商业软核 IP 那样给你一堆黑盒接口每个模块的 Verilog 代码都摆在面前你可以一边跑 C 代码一边打开 RTL 看硬件是怎么执行的这种“软硬件对照学习”的体验非常值钱。踩过几次坑之后你会发现很多问题并不是项目本身有 bug而是你对软件和硬件之间的边界理解还不够清晰。慢慢把这条边界摸透了你既能写出好的嵌入式软件也能写出可综合的高质量逻辑才算是真正把这两个领域打通了。

最新新闻

日新闻

周新闻

月新闻