ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用Efinity IDE在FPGA上开发RISC-V软核:从环境搭建到调试实战

用Efinity IDE在FPGA上开发RISC-V软核:从环境搭建到调试实战 如果你和我一样这几年一直在关注 RISC-V 的进展大概率会陷入一种感觉资料很多但真正能落到硬件上的完整工具链不多。尤其是想在一个 FPGA 平台上完整跑一个可定制的 RISC-V 软核时Xilinx 和 Intel 的老路径大家都很熟Efinity 反而成了资料最稀缺、但性价比极高的选择之一。Efinity IDE 是 Efinix 面向自家 FPGA 提供的集成开发环境它把硬件工程、软核配置、嵌入式软件编译和深度调试全部装进了一套工具不需要你在多个软件之间来回切换。这篇文章我会从环境搭建开始一直讲到调试器视图里的实际操作内容覆盖我实际跑过的工程流程和踩过的坑适合准备用 Efinix 平台做 RISC-V 软核开发的 FPGA 工程师也适合想从嵌入式视角反向了解 FPGA 工具链的朋友。1. 为什么要用 Efinity 这套 IDE 跑 RISC-V先看三条开发路线的差异1.1 老牌软核平台、开源软核、Efinity 的方案对比在 FPGA 里跑软核 CPU很多人第一反应是 Intel 的 Nios II 或者 AMD 的 MicroBlaze。这两个方案生态确实成熟文档多、例程多、问题一搜就有答案但前提是你得在那个硬件平台上。如果你手头的项目因为成本、功耗、封装尺寸或者供货原因选了 Efinix 的芯片那前面那套经验就不能直接平移过来工具链、IP 授权方式、调试流程全都得重新适应。另一条路线是使用开源软核比如 VexRiscv、PicoRV32 这类。这类软核用 Verilog 写成可以手动集成到任意 FPGA 工程里灵活性最高。但问题也很现实你需要自己搭交叉编译工具链自己写链接脚本自己处理启动代码甚至调试器都要额外配置。对于只想把 RISC-V 当做一个可编程控制器来用、而不是研究处理器微架构的团队来说这套流程的学习成本太高了很容易在还没跑起来第一个程序之前就劝退。Efinity 的路线正好卡在中间。Efinix 自家的 SoftRISC-V 软核可以直接在 Efinity IDE 里通过图形界面配置IDE 同时负责 FPGA 综合布线、软核 RTL 生成、嵌入式软件编译和 JTAG 调试。也就是说你不需要单独安装一套 Eclipse 或者 VS Code 交叉编译环境也不用在命令行里敲 make 和 riscv-none-embed-gcc。对大多数做端侧控制、数据采集、电机驱动这类应用的人来说这套闭环体验比开源方案友好太多。1.2 这套 IDE 到底解决的是什么问题从工程协作角度看FPGA 工程师和嵌入式工程师的工作习惯差别很大。FPGA 工程师关心 RTL 时序和引脚约束嵌入式工程师关心内存布局和中断响应。Efinity IDE 把两套流程放进同一个工程树里硬件源文件和 C 代码可以同时管理编译软件时不需要离开 IDE检查硬件问题时也不用切窗口。从我个人体会来说它的最大价值在于把“认知负担”降下来了。你只需要维护一个工程文件Core Designer 生成的 RTL 会自动变成工程里的源码软件工程会自动关联到软核配置。如果发现时序不满足直接在 IDE 里改约束重新跑如果发现软件逻辑有问题直接在调试视图里打断点看变量。当然它也不是万能的。如果你要做的处理器是一个非常规的多核架构或者需要深度移植 Linux 系统那 Efinity 这套软核就不太合适它更适合裸机或者轻量级 RTOS 场景。选型时想清楚自己的边界才不会到后期发现工具撑不住需求。2. 环境搭建三板斧版本、许可证、驱动2.1 版本选择与许可证申请我第一次装 Efinity IDE 时犯过一个错误随手从官网下载了当时的最新版安装完才发现手头的开发板板载 JTAG 驱动和这个版本存在兼容问题最后又退回上一个版本重新装。我的建议是先确认你的开发板型号再去官网找对应的 Getting Started 文档里面通常会写明推荐的 IDE 版本范围。许可证是另一个容易被忽略的环节。Efinity IDE 本身安装包不大但综合、布局布线、生成位流这些功能都需要有效的 License。初次安装后,如果你直接打开工程跑综合大概率会在日志里看到类似License not found的错误。正确做法是到 Efinix 官网注册账号在授权页面申请对应器件系列的 License。以我用的 Titanium 系列为例官方对部分器件提供可申请的免费 License申请时会让你填网卡 MAC 地址生成一个绑定机器的授权文件。拿到 License 文件后在 IDE 的 Help 菜单里找到 License 管理界面指定文件路径就行。这里有个容易踩的细节License 跟网卡绑定如果你在虚拟机上运行 IDE或者电脑开了无线和有线双网卡IDE 可能会选错网卡导致 License 校验失败。保险做法是暂时禁用不用的网卡或者确认 IDE 读取的 HostID 和申请时填的 MAC 地址一致。2.2 Windows 下的驱动与 USB 识别Efinity 开发板通常通过板载 FTDI USB 转 JTAG 芯片连接电脑也有一部分板卡会用一个独立的调试器小盒子。不管是哪种形式装完 IDE 之后都要单独确认 USB 驱动是否就位。我在 Windows 下遇到过这样的情况:开发板插上之后,系统能识别到 USB 设备但设备管理器里显示的是一个带黄色感叹号的未知设备IDE 里自然找不到调试器。排查思路非常简单拔掉板卡,装好驱动,再插上。Efinity IDE 安装目录里一般会带驱动文件,或者在官网下载页面有单独驱动包。装完驱动后,设备管理器里应该出现一个串行设备或者 JTAG 设备节点。如果还是没有,检查 USB 线是不是只供电不传数据的充电线——这种线在 FPGA 调试场景里我至少遇见过两次浪费了半个小时。另外杀毒软件和 Windows 防火墙也可能拦截驱动安装。我习惯于在安装 IDE 和驱动期间把实时防护临时关掉装完再打开这样能减少很多莫名其妙的安装失败。2.3 工程目录规范与中文路径的坑IDE 类工具对工程路径非常敏感。Efinity IDE 在某些版本中对中文目录、目录中的空格处理有问题会导致综合阶段找不到文件或者脚本报错。我现在的习惯是在某个固定盘符下建一个纯英文目录比如 D:\EfinityWorkspace每个工程再在下面建独立子目录工程名也不带空格和特殊符号。还有一个很多人不注意的点Efinity IDE 工程里会自动生成大量中间文件如果放在系统盘的用户目录下时间长了会占用好几个 GB。我建议工作空间和工作目录都放到大容量数据盘同时在工程设置里把仿真和综合中间文件目录指向工程内的 cache 文件夹方便定期清理。3. Core Designer 里配置 SoC总线、内存和外设的关系3.1 拖一个 SoftRISC-V 内核之前要想清楚的事打开 Core Designer 之后左侧会列出很多 IPSoftRISC-V 是其中最核心的一个。把它添加到画布上之后会弹出 CPU 参数配置界面。第一件要决定的事是基础 ISA 配置。常见的 SoftRISC-V 内核基于 RV32IMCM 表示支持硬件整数乘除法指令C 表示支持压缩指令。对嵌入式控制类负载来说这个组合非常实用硬件乘法能显著提升运算效率压缩指令能减小代码体积。如果你有大量浮点运算需求可以考虑带 F 扩展的配置但代价是占用更多逻辑资源和功耗。我的经验是裸机控制任务先用 RV32IMC真遇到浮点性能瓶颈再加 FPU不要一上来就追求全功能。第二件要决定的事是是否使能 Debug Module。默认情况下调试模块是开启的这能让后面接 IDE 调试器实现断点单步但如果你的设计最终要量产且对资源占用极其敏感可以在发布版本里关掉 Debug Module 以节省逻辑单元。调试模块会占用一小部分 JTAG 引脚和内部逻辑平时开发阶段建议一直开着。还有时钟配置。Core Designer 里会让你选择 CPU 时钟频率和复位信号来源。常见做法是外部晶振时钟经过 PLL 倍频后作为系统时钟复位信号使用外部按键复位和上电复位逻辑共同产生。需要注意的是后续 SDC 约束里的时钟周期必须和这里配置的时钟频率一致否则时序分析结果没有意义。3.2 内存选择Block RAM 到底够不够用软核 CPU 运行程序需要内存Efinity 平台上最常见的方式是用 FPGA 内部 Block RAM 作为 CPU 的指令和数据存储。Core Designer 里创建 Block RAM 控制器或者直接用内存 IP设好容量和位宽再把它的地址空间接到 CPU 总线上。这里有个新手常见的认知误区64KB 是不是很大对于纯裸机程序64KB 确实能装下不少代码但一旦你用了标准 C 库的 printf、sprintf 这类函数代码体积会迅速膨胀。我做过一个带简单命令解析的工程光 printf 相关代码就吃掉了 20KB 以上再加上堆栈和全局变量64KB 已经比较紧张了。如果你预计程序规模会超过内部 Block RAM最好在设计初期就在 Core Designer 里把外部 SDRAM 控制器加进去并把链接脚本的内存区域指向外部 RAM。这样在软件阶段就不用因为空间不足而大改代码了。内存地址映射地址可以在 Core Designer 里手动调整但我们需要保证生成的链接脚本和硬件地址一致。IDE 通常会帮你同步但如果你手工改过地址记得重新生成一次工程文件再写软件。3.3 外设挂载GPIO 和 UART 的配置细节CPU 和外设之间通过总线连接。Core Designer 里把 GPIO、UART、SPI、I2C 等 IP 拖到画布上连到总线上并分配好地址。GPIO 的位宽根据你的应用设置比如 8 位接一组 LED、16 位接按键和拨码开关。UART 配置里最重要的参数是波特率分频值它由输入时钟频率决定配置时要把实际时钟值填进去否则生成的波特率会有误差。Efinix 的 UART IP 通常支持中断控制你可以把 UART 接收中断连接到一个中断控制器这样主循环就不会因为等待串口数据而阻塞。中断的处理是软核 SoC 设计里最容易出问题的地方。SoftRISC-V 通常带有 PLIC 中断控制器可以管理多个外部中断源。在 Core Designer 里每个外设的中断输出线都要连接到 PLIC 的输入端口然后 PLIC 的输出再连接到 CPU 的中断引脚。配置完成后点 GenerateIDE 会生成整体 RTL并在工程里创建一个顶层模块。生成 RTL 之后回到主界面你需要把这个顶层模块设置为工程的顶层实体然后编写约束文件。这里提醒一下不要着急直接综合先把时钟约束、引脚约束全部做完再跑否则后续每次改引脚都要重新综合布局布线非常浪费时间。4. 第一个可调试工程从 C 代码到 FPGA 运行4.1 建立工程并处理顶层与约束在 Efinity IDE 里新建工程选择芯片型号这一步方向对了后面就顺。工程创建完成后Core Designer 生成的 RTL 会把顶层模块暴露出来进入 Sources 窗口右键设置这个模块为 top level。芯片的物理引脚要在这里做映射。比如板载 50MHz 晶振连接到 FPGA 的某个引脚你需要在约束中输入对应的时钟端口名并声明一个周期为 20ns 的时钟约束。GPIO 和 UART 引脚同理查阅开发板原理图找到对应网络号然后在引脚约束列表里逐个分配。我一般把约束文件分成两部分时钟相关约束放到 SDC 文件里引脚位置约束放在 IDE 的 Pin Planner 中处理。这样如果换了开发板只需要更新引脚分配不需要动时钟声明。第一版约束不用追求完美能把时钟和串口引脚分配对就够了因为我们要先跑通最小系统。4.2 创建软件工程BSP、启动文件与 main硬件综合通过之后就可以开始写软件了。在 Efinity IDE 中软件工程部分通常由配套的 SDK 环境承载。你需要在 IDE 里创建一个新的嵌入式软件工程并选择目标处理器为当前 Core Designer 里配置的 SoftRISC-V。SDK 会检查硬件生成的描述文件自动创建板级支持包包括启动代码、链接脚本、设备驱动库。如果你之前用过 MCU 开发这里会感觉很熟悉。生成的工程里有一个 startup 汇编文件负责初始化栈指针、清除 BSS 段、调用 main还有一个链接脚本规定了代码段、数据段、堆栈分别放在哪个地址区域。BSP 里一般已经包含了 GPIO、UART 等基础驱动我们只需要在 main 函数里调用对应的 API 就行。我习惯把第一个程序写成“点灯加打印”初始化 UART主循环里翻转 GPIO 控制 LED同时每隔一段时间通过串口发送一个计数值。这样一个最简单的程序就能验证 CPU 是否跑起来、内存是否正常、外设总线是否连通、串口是否工作四合一排查。4.3 编译、下载、看到输出软件代码写完后在 SDK 里执行编译生成的可执行文件通常是 ELF 格式。Efinity 的调试器支持把 ELF 直接下载到软核的存储区域运行。连接好 JTAG在 IDE 的调试配置里选择目标设备点击运行程序就会被加载到 FPGA 内部 RAM 并开始执行。到这里有一个关键分岔你是想通过串口看输出还是想通过调试器的 semihosting 方式在 IDE 控制台看输出semihosting 的意思是目标板上的程序通过一个特殊指令请求调试器代为执行主机端 I/O 操作这样一来你不需要额外接串口线printf 的输出就能显示在 IDE 的控制台上。但 semihosting 依赖调试器的实时交互在断点停下时它的输出也会暂停而且在发布版本中通常不应该启用。我建议开发阶段两条路都准备好强依赖调试器时用 semihosting 快速看数据常规流程里用硬件 UART 输出到串口工具。这样调试时灵活最后发布时只要把调试输出关掉即可。跑通第一个程序时看到串口里出现计数递增这基本就代表了整套软核链路已经打通剩下的就是深入调试和优化了。5. 深度调试单步之外那些有效率的事5.1 调试器连接失败的排查流程硬件调试器连接不上几乎是每个人都会遇到的第一道坎。Efinity IDE 调试视图里找不到目标设备时我一般按下面顺序排查先确认 JTAG 线连接可靠、开发板供电正常再打开设备管理器确认调试器被系统识别然后检查 IDE 的工程配置里选择的调试器型号是否正确最后看开发板上的模式跳线。有些板卡在 JTAG 调试模式和 Flash 启动模式之间需要通过跳线切换如果跳线位置不对CPU 可能被设置成从 Flash 启动外部 JTAG 就不能正常控制调试接口。还有一个容易忽略的因素调试器驱动冲突。如果你电脑里同时装过其他 FPGA 厂商的调试工具不同工具的 USB 驱动可能抢占设备。FTDI 驱动是所有板载 JTAG 方案里最常见的冲突源。遇到连接不稳定、时好时坏的情况可以尝试在设备管理器里手动指定驱动版本。5.2 断点、变量窗口、反汇编的组合用法很多从 MCU 开发转过来的工程师习惯了 IDE 里设置无限个软件断点。但在 FPGA 软核环境里硬件断点是很稀缺的资源。SoftRISC-V 的调试模块通常只提供有限的硬件断点比较器一般就几个。你想在十几个函数里都设断点大概率会失败或者只有前几个生效。我的策略是先用少量硬件断点定位大范围。比如在某个模块入口设一个断点确认程序是否进入这个模块再在关键函数末尾设一个断点确认执行路径完整。不要试图一次在多个循环内部都设断点。另有一个非常实用的技巧可以设置条件断点。当变量等于某个特定值时暂停这在排查特定异常状态时极为高效。变量窗口在优化开启时会显示“optimized out”这是新手最容易困惑的地方。为了调试体验软件工程可以单独使用 -O0 或 -Og 优化等级专门用于开发调试版本。等逻辑验证通过后再编译一个 -O2 的发布版本做性能验证。不要全程用 O0 测性能也不要全程用 O2 调逻辑。反汇编窗口是深度调试里的重武器。在 RISC-V 工程里你可以看到每一条 C 语句对应的汇编指令比如 lw、sw、jalr 这些。当怀疑编译结果有问题时我会切到反汇编窗口检查某个关键变量是不是真的被加载到了寄存器或者某个函数调用是不是被内联了。硬件乘法是否生效也可以直接在反汇编里看有没有 mul 指令——如果浮点库被静态链接你能看到一大段软件模拟浮点的循环这时候你就知道性能瓶颈在哪了。5.3 寄存器与内存视窗怎么观察 CSR 和异常现场程序员常用的寄存器是通用寄存器 r0-r31但真正在系统异常时有用的是控制和状态寄存器 CSR。mstatus 控制全局中断使能mtvec 保存异常入口地址mepc 保存异常发生时的程序计数器值。当程序跑飞或者进入异常时第一步应该查看这几个寄存器。在调试视图里打开寄存器窗口通常会列出当前线程的通用寄存器和部分 CSR。如果程序发生非法指令异常mepc 会指向触发异常的指令地址把这个地址和反汇编窗口里的代码行号对照就能快速定位是哪条汇编指令出了问题。如果是总线错误异常多半是访问了不存在的地址检查内存映射和外设地址范围就行。内存视窗则用来查看外设寄存器状态。外设寄存器在内存里有映射地址比如 UART 状态寄存器的某个位表示发送 FIFO 空不空。在内存窗口里输入外设基地址就能实时观察位翻转。这个能力在调试驱动初始化是否正确时特别有用不用在代码里加一堆临时变量。5.4 双通道调试软件断点配合硬件信号观察有些问题单看软件是不行的。比如电机控制里你怀疑一个中断服务程序执行时间过长导致时序异常但光靠计时器和断点很难说清楚。我常用的办法是“GPIO 打点”把某个空闲 GPIO 在关键代码路径的开始和结束处翻转然后把 GPIO 引脚引到示波器或者逻辑分析仪上这样代码执行时间的精确波形就出来了。这是嵌入式调试里最古老也最有效的办法。在 FPGA 环境里还能更进一步。Efinity IDE 有在线逻辑分析仪类的调试工具可以在不引出外部引脚的情况下观察 FPGA 内部信号。软件运行到某个函数时我们可以同时观察内部总线的读写周期、外设的片选信号是否正常。这样软硬件协同分析时能直接看到一次外设访问从 CPU 发起总线请求到外设返回数据的过程定位速度比盲猜快得多。调试到这里基本上已经脱离“能不能跑”的阶段进入“跑得好不好”的深水区了。6. 真实项目中的坑与优化一次完整排障记录6.1 第一个坑程序莫名其妙复位根因是栈溢出我调试一个数据采集程序时遇到了一个很诡异的现象程序运行十几秒后自动回到启动代码全局变量清零仿佛 CPU 被复位。刚开始我怀疑是看门狗溢出翻遍工程也没找到开启看门狗的地方。后来打开寄存器视窗发现 mepc 指向的地址在内存映射里根本不存在因为栈指针已经跑到分配的内存区域之外函数返回时跳到了一个非法地址触发了异常。根因是链接脚本里栈空间分配太小中断嵌套时栈不够用。解决办法分两步一是把链接脚本里的栈大小从 2KB 调整到 8KB二是用一个简单的栈水印检测函数在启动时把栈区域填入固定模式主循环定期检查如果尾部数据被改写就说明栈溢出临界。这个习惯我现在一直保留在资源受限的软核开发里非常重要。6.2 第二个坑GPIO 写操作被编译器“优化”掉了还有一次我写了一个标志位在中断里置 1主循环里清 0用来统计中断频率。结果发现主循环里清 0 的代码好像从来不执行逻辑分析仪抓到的波形也不对。打开反汇编一看编译器把那个变量的所有操作都优化没了因为它发现这个变量在全程序范围内没有任何读取操作就把它判定为死代码。解决办法是给这个标志变量加上 volatile 修饰告诉编译器这个变量可能被外部事件改变每次访问都必须从内存读取。硬件寄存器相关的变量在 BSP 头文件里通常已经加了 volatile但应用层的全局共享变量经常会被忽略。这个坑在开启 -O2 优化后特别容易出现并不是编译器蠢而是它在做正确的死代码消除。6.3 第三个坑中断一直进不去进了又出不来回用 PLIC 管理外部中断时,第一批新手都会遇到中断不触发的问题。我那次是按键 GPIO 中断配置了上升沿触发,但按键按下没反应。排查过程从硬件到软件检查了一遍GPIO 中断使能位有没有设、PLIC 的转发使能有没有开、CPU 全局中断使能有没有写。最后发现是 GPIO IP 的中断输出真值极性配置反了在 Core Designer 里是低电平有效我却在软件里按高电平判断。中断乱进的坑也遇到过。中断服务程序里处理完事件后忘了清 GPIO 挂起寄存器导致中断标志一直有效按下一次按键触发了七八次中断。处理这类问题没有捷径就是按链路顺序逐级确认外设挂起位、PLIC 挂起寄存器、CPU 的 mstatus 中断使能位逐项加断点或变量观察。6.4 性能调优让一个控制循环明显提速的实操调优案例来自一个需要快速响应外部数据的控制循环。最开始代码跑在 RV32IMC 内核上循环内部做了大量乘法运算编译时开了 -O2但我感觉响应周期还是不能接受。打开反汇编窗口后看到循环里很多乘法调用的是软乘库函数没有生成 mul 指令。原因是我在 SDK 里新建软件工程时没有把编译选项加上 -marchrv32imc编译器默认用基础指令集硬件 M 扩展没有被使用。把编译选项修正为匹配内核特性的参数后性能提升非常明显乘法相关的计算快了一个数量级。接着我又发现循环里有对结构体整体赋值的操作编译器生成了一个 memcpy 调用而这个结构体只有 6 个字段改成逐字段赋值后代码体积和栈开销都下去了。从这个案例里可以提炼出一个通用经验软核调试不只是看逻辑正确性还要看编译出来的指令是否真正用上了硬件特性。很多时候性能瓶颈不在 FPGA 频率跑不高而在于编译器没有针对目标 ISA 做代码生成。用反汇编窗口检查关键循环比用性能分析仪器来得更直接。这类经验和调试工具链本身一样值得积累未来换任何 RISC-V 平台都能复用。
返回列表