HPM6750双核RISC-V开发实战:从固件合并到双核启动全流程解析
1. 项目概述与核心思路
最近在折腾一块基于HPM6750双核RISC-V处理器的开发板,这块板子挺有意思,它集成了两个完全独立的RISC-V核心(hart0和hart1)。在嵌入式开发里,双核系统能做的事情就多了,比如一个核跑实时控制,另一个核处理网络协议栈或者图形界面,效率能提升不少。但随之而来的一个现实问题就是:怎么把两个核心的程序(固件)烧录进去?这可不是简单的“一键下载”就能搞定的事儿。
我用的开发环境是AWorksLP SDK,它对外设做了高度抽象,同一类外设的接口是统一的,这让写跨平台应用轻松了不少。但官方文档里关于双核烧录的步骤写得比较零散,新手照着做很容易卡住。经过一番摸索和踩坑,我总算把整个流程跑通了。这篇文章,我就以SDK里自带的hello双核例程为例,手把手带你走一遍HPM6750双核工程的创建、编译、固件合并与烧录的全过程。无论你是刚接触双核MCU,还是从其他平台(比如ARM Cortex-M系列的双核)转过来,这篇详尽的实操记录应该都能帮你避开我走过的弯路。
简单来说,HPM6750的双核烧录,核心思路是“主核带从核”。hart0(核心0)是主核,它总是首先启动,并且它的程序是固化在Flash里的。hart1(核心1)是从核,它自己不能独立启动,必须由hart0来“唤醒”和加载。因此,我们的最终目标,是生成一个包含了hart0(Flash程序)和hart1(SDRAM程序)所有代码的单一固件文件,然后通过调试器(比如J-Link)一次性下载到芯片里。听起来有点绕?别急,下面我们一步步拆解。
2. 环境准备与工程结构解析
工欲善其事,必先利其器。在开始动手之前,我们需要把环境和工程结构搞清楚。
2.1 开发环境与SDK
首先,你需要准备好AWorksLP SDK和对应的集成开发环境(IDE),官方通常推荐基于Eclipse的定制版本。确保你的SDK版本支持HPM6750和双核特性。将SDK解压到一个没有中文和空格的路径下,这是避免各种奇怪编译问题的第一步。
SDK的目录结构很有讲究,理解它有助于后续操作。关键路径如下:
{SDK}\demos\multi-core\:这里存放了所有的双核示例工程。我们重点关注的hello例程就在里面。{SDK}\platforms\platform-hpm-aworks-lp\boards\EPC6750-AWI-muti\:这是针对我们使用的“EPC6750-AWI-muti”这块多核评估板的板级支持包(BSP)目录。里面包含了该板卡的所有硬件配置文件、驱动和链接脚本。{SDK}\platforms\platform-hpm-aworks-lp\boards\EPC6750-AWI-muti\source\:这个source文件夹是关键中的关键。我们后续需要把从核(hart1)编译生成的二进制文件(.bin)拷贝到这里,主核工程在编译时会来这里寻找并“打包”这个文件。
注意:务必确认你使用的板卡型号。本文以
EPC6750-AWI-muti为例,如果你的板子是EPC6750-AWI-L或其他型号,部分配置(尤其是调试串口)会有所不同,需要相应调整。
2.2 双核工程本质:两个独立的项目
这是理解整个流程的基础。HPM6750的两个RISC-V核心是完全独立的,它们有各自的内存映射、中断向量表和运行上下文。因此,在软件开发层面,双核工程并不是一个“工程”里包含两个“任务”,而是两个完全独立的单核工程。
在hello例程目录下,你会清晰地看到两个文件夹:
hart0/:这是主核(Core 0)的工程目录。hart1/:这是从核(Core 1)的工程目录。
你需要分别用IDE打开、配置和编译这两个工程。它们之间的关联,仅仅是通过一个“约定”:主核工程在编译的最终阶段,会把从核工程生成的二进制文件“包含”进来,合并成一个最终的可执行文件。
2.3 运行内存规划:Flash vs. SDRAM
两个核心的运行位置也不同,这是由硬件特性和启动顺序决定的:
- hart0 (主核):其程序代码通常链接到芯片的内部Flash地址空间。芯片上电复位后,直接从Flash的起始地址开始执行hart0的代码。
- hart1 (从核):其程序代码将被加载到外部SDRAM中运行。这是因为hart1的启动由hart0控制,hart0需要有能力将一段二进制代码搬运到SDRAM,然后跳转过去执行。SDRAM容量大、速度快,适合存放从核的应用程序。
所以,在编译配置上,两个工程也有显著区别:
- hart0工程:编译目标通常是“Flash调试”,链接脚本指向Flash地址。
- hart1工程:编译目标需要选择“SDRAM调试”,链接脚本指向SDRAM的特定地址(例如0x8000 0000)。并且,它的输出文件格式必须是Raw Binary (.bin),而不是默认的ELF文件,因为二进制格式更适合主核进行简单的内存搬运。
3. 实操步骤详解:从编译到烧录
理论铺垫完毕,现在进入实战环节。我们按照“先编译从核,再编译主核,最后下载”的顺序进行。
3.1 步骤一:创建与导入工程
首先,在IDE中创建或导入工程。
- 选择
File->New->AWorksLP Project(或类似选项)。 - 在弹窗中,选择“从现有代码创建工程”或直接定位到
{SDK}\demos\multi-core\hello目录。 - 关键选择:在板卡选择页面,必须选择
EPC6750-AWI-muti。即使你手头的物理板子是EPC6750-AWI-L,在创建这个双核示例工程时,为了使用多核的BSP配置,也需要先选择muti版本。串口等差异我们后续再调整。 - 分别对
hart0和hart1文件夹执行上述操作,在IDE中建立两个独立的工程。
3.2 步骤二:编译hart1(从核)固件
我们先处理从核,因为它的输出是主核的“原材料”。
- 打开hart1工程:在IDE的工程浏览器中,切换到
hart1工程。 - 配置编译目标:
- 找到工程属性(通常右键工程 ->
Properties)。 - 定位到
C/C++ Build->Settings->Tool Settings选项卡。 - 找到
GNU RISC-V Cross Create Flash Image或类似的“生成镜像”工具设置。 - 将
Output file format(输出文件格式)从默认的Intel Hex或ELF改为Raw binary。这一步至关重要,它告诉编译器生成一个纯粹的、无头信息的二进制镜像,方便主核直接加载。
- 找到工程属性(通常右键工程 ->
- 选择编译配置:在IDE的工具栏,找到编译配置下拉菜单。你会看到多个配置选项,如
flash_debug,sdram_hart1_debug等。- 为hart1工程选择
sdram_hart1_debug。这个配置预定义了正确的链接脚本,将代码和数据定位到SDRAM地址。
- 为hart1工程选择
- 执行编译:点击编译按钮。编译成功后,去工程目录下寻找生成的文件。路径通常是:
{你的工作空间}\hello\hart1\project_eclipse\sdram_hart1_debug\在这个目录下,你应该能找到生成的二进制文件,名字类似HPM6750-MULTI-HART1.bin。请记下这个完整的文件名。
3.3 步骤三:准备hart1固件供主核使用
编译出.bin文件只是第一步,接下来要把它“交给”主核工程。
- 拷贝bin文件:将上一步生成的
HPM6750-MULTI-HART1.bin文件,复制到SDK的板级包指定目录:{SDK}\platforms\platform-hpm-aworks-lp\boards\EPC6750-AWI-muti\source\如果source文件夹不存在,就手动创建一个。 - 核对汇编文件:用文本编辑器打开
source文件夹下的一个关键文件:hpm_hart1_image.S。这个文件的内容通常如下:
你的任务就是检查.section .hart1_image, "a" .global hart1_image_start .global hart1_image_end .align 4 hart1_image_start: .incbin "HPM6750-MULTI-HART1.bin" /* 确保这里的文件名与你拷贝的bin文件完全一致 */ hart1_image_end:.incbin指令后面的双引号内的文件名,是否与你刚刚拷贝进去的bin文件名称完全一致,包括大小写。incbin是汇编器指令,作用是在当前位置“包含”一个二进制文件。主核工程在链接时,会把这个二进制数据段链接到最终的ELF文件中。
实操心得:这是最容易出错的一步。很多人编译失败,报错找不到符号
hart1_image_start,十有八九是因为这里的文件名没对上,或者bin文件没有成功拷贝到这个目录。我建议在拷贝完成后,在IDE里刷新一下工程,确保文件系统同步。
3.4 步骤四:编译hart0(主核)固件
处理好从核的“粮草”,现在来编译主力部队。
- 切换工程:在IDE中,将活动工程切换到
hart0。 - 选择编译配置:在编译配置下拉菜单中,为hart0工程选择
flash_debug。这个配置会将主核程序链接到Flash。 - 执行编译:点击编译按钮。这次编译过程会做一件重要的事:链接器会读取
hpm_hart1_image.S,将我们准备好的hart1的bin文件作为一段数据,整合进最终生成的hart0的ELF文件中。 - 定位最终文件:编译成功后,在hart0工程的输出目录(如
project_eclipse\flash_debug\)下,会生成最终的可执行ELF文件(例如hello.elf)和可供烧录的二进制文件(例如hello.bin或hello.hex)。这个文件已经包含了两个核心的所有代码。
3.5 步骤五:硬件连接与调试串口配置
在烧录之前,需要正确连接硬件和配置调试串口,以便观察两个核心的运行打印。
- 硬件连接:
- 使用调试器(如J-Link)连接板子的SWD接口,用于下载程序和调试。
- 连接板子的串口到PC。这里需要特别注意:根据板卡型号和工程配置,两个核心可能使用不同的串口。
- 串口配置解析:打开设备树文件:
{SDK}\platforms\platform-hpm-aworks-lp\boards\EPC6750-AWI-muti\EPC6750-AWI-muti.dts在文件中搜索uart,你会找到类似下面的配置:
这意味着在默认的&uart0 { status = "okay"; core = <0>; /* hart0 使用 uart0 */ }; &uart13 { status = "okay"; core = <1>; /* hart1 使用 uart13 */ };EPC6750-AWI-muti配置中:- hart0的
aw_kprintf输出到UART0。 - hart1的
aw_kprintf输出到UART13。
- hart0的
- 针对EPC6750-AWI-L评估板的调整:如果你手头是
EPC6750-AWI-L评估板,情况稍有不同。该板子的UART13硬件连接到了RS485收发器,默认不是简单的TTL UART。你有两种选择:- 选项A(使用UART5):修改
EPC6750-AWI-muti.dts文件,将hart1的core = <1>;从&uart13节点移到&uart5节点,并确保uart5的status = “okay”;。UART5在板子的排针上有引出,更方便连接USB转TTL模块。 - 选项B(使用UART13的485功能):不修改DTS,但需要确保板载的485功能已使能(可能需要跳线或GUI配置),并使用RS485转USB适配器来接收数据。个人建议新手使用选项A,修改DTS后重新编译工程,这样直接用UART5的TTL电平,接线和调试都更简单。
- 选项A(使用UART5):修改
3.6 步骤六:烧录与运行
万事俱备,只欠烧录。
- 配置调试会话:在IDE中,为
hart0工程创建一个调试配置(Debug Configuration)。选择正确的调试器(如J-Link),目标芯片选择HPM6750。 - 下载程序:启动调试会话。IDE会通过调试器,将我们最终生成的、包含了双核代码的ELF文件下载到芯片的Flash中。
- 复位与观察:程序下载完成后,按一下板子上的复位按钮(Reset)。这是必须的步骤,因为上电或下载后,hart1可能处于不确定状态,需要一次硬件复位让hart0重新执行启动流程。
- 打开串口终端:在PC上打开两个串口终端软件窗口(如Putty、MobaXterm、SecureCRT等)。
- 终端1:连接hart0使用的串口(如UART0),配置正确的波特率(通常是115200)。
- 终端2:连接hart1使用的串口(如修改后的UART5或原始的UART13),同样配置波特率。
- 查看打印信息:复位后,你应该能在两个串口终端中看到交替打印的信息:
- hart0终端:会先打印
”open multi_core ok!”,然后每隔1秒打印”hart0: hello world!”。 - hart1终端:会打印
”application Start……”,然后每隔1秒打印”hart1: hello world!”。
- hart0终端:会先打印
当你同时看到两个核心的“hello world”在各自的串口上有规律地打印时,恭喜你,双核烧录与运行就成功了!
4. 核心代码与机制深度解析
看到现象只是第一步,理解背后的代码逻辑才能举一反三。我们来深入看看hello例程里最关键的两段代码。
4.1 hart1(从核)程序分析
从核的程序非常简单,它就是一个独立的、无限循环的C应用。
int aw_main() { aw_kprintf("\r\napplication Start.............. \r\n"); while(1) { aw_kprintf("hart1: hello world!\n"); aw_mdelay(1000); // 延时1秒 } return 0; }aw_main():这是AWorksLP应用程序的入口函数,类似于标准C的main()。aw_kprintf():AWorksLP提供的打印函数,输出会重定向到该核心配置的调试串口。aw_mdelay():毫秒级延时函数。
关键点:从核程序完全不知道自己是被谁、如何加载到SDRAM的。它只关心从它的入口地址开始执行自己的逻辑。这个入口地址在链接脚本里定义,必须和主核加载它的目标地址(SDRAM中的地址)一致。
4.2 hart0(主核)程序分析
主核的程序承担了启动从核的重任。
static void __start_hart1(void) { int fd; fd = aw_open("/dev/multi_core", AW_O_RDWR, 0); if (fd < 0) { aw_kprintf("open error, fd: %d\n", fd); } aw_kprintf("open multi_core ok!\n"); } int aw_main() { aw_kprintf("\r\napplication Start.............. \r\n"); __start_hart1(); // 启动hart1 while(1) { aw_kprintf("hart0: hello world!\n"); aw_mdelay(1000); } return 0; }__start_hart1()函数是核心。它通过aw_open(“/dev/multi_core”, …)这个操作来启动从核。”/dev/multi_core”:这是一个虚拟设备,是AWorksLP SDK为多核通信与启动抽象出来的一个设备节点。打开这个设备,底层驱动会执行一系列硬件操作:- 配置hart1的启动向量地址(指向SDRAM中hart1程序的起始地址)。
- 将hart1从复位状态释放。
- hart1检测到释放信号后,开始从指定的SDRAM地址执行指令。
aw_open的返回值是一个文件描述符fd,这个fd在后续的多核通信(如OpenAMP或RPC)中会用到,用于在两个核心之间传递消息或数据。在简单的hello例程中,我们打开设备后并没有进行实际的数据通信。
机制总结:主核通过操作系统提供的抽象接口(aw_open)触发硬件机制来启动从核。从核的二进制镜像已经作为数据段被包含在主核的固件中,主核的启动代码会在系统初始化早期,自动将这段数据从Flash拷贝到SDRAM的指定位置,为aw_open操作做好准备。
5. 常见问题与深度排查指南
双核调试比单核复杂,遇到问题是常态。下面是我总结的一些常见坑点及其解决方案。
5.1 编译与链接问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
编译hart0时,链接错误,提示undefined reference to ‘hart1_image_start’ | 1.hpm_hart1_image.S文件中的.incbin文件名与实际的bin文件名不匹配。2. bin文件未拷贝到 …/source/目录,或目录错误。3. 未在hart0工程的链接脚本或工程配置中包含 hpm_hart1_image.S文件(通常SDK已配置好)。 | 1.仔细核对文件名:检查hpm_hart1_image.S中的字符串与source文件夹下bin文件的名字,确保一字不差,包括后缀.bin。2.确认路径:确认bin文件在 {SDK}\…\EPC6750-AWI-muti\source\下。3.清理并重建:尝试清理(Clean)hart0和hart1工程,然后按顺序重新编译(先hart1,拷贝bin,再hart0)。 |
| hart1工程编译失败,提示地址溢出或链接错误 | hart1工程的链接地址(SDRAM地址)与硬件不符,或SDRAM未正确初始化。 | 1.检查编译配置:确保hart1工程选择的是sdram_hart1_debug等与SDRAM相关的配置。2.检查链接脚本:查看该配置使用的链接脚本(.ld文件),确认代码段(.text)的起始地址是否为SDRAM的有效地址(如 0x80000000)。3.确认SDRAM初始化:主核的启动代码必须包含SDRAM的初始化。确保使用的BSP和板级支持包是正确的。 |
| 编译通过,但生成的最终bin/elf文件异常小 | hart1的bin文件没有被成功包含进去。 | 查看最终生成的ELF文件大小,如果只有几十KB(仅hart0代码),说明包含失败。请回到步骤三,严格检查bin文件拷贝和汇编文件引用的每一步。 |
5.2 运行与调试问题
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 只有hart0串口有打印,hart1串口无任何输出 | 1. hart1程序未成功启动。 2. hart1使用的串口配置错误或硬件连接错误。 3. hart1程序在SDRAM中运行崩溃。 | 1.检查启动代码:在主核aw_open(“/dev/multi_core”)后,是否打印了成功信息?如果没有,说明打开多核设备失败,检查驱动和硬件配置。2.检查串口配置:确认设备树(.dts)中hart1核心对应的串口引脚配置是否正确,是否与你实际连接的串口线匹配。对于EPC6750-AWI-L板,强烈建议将hart1的调试串口改为UART5。 3.使用调试器:尝试在hart1的入口函数( aw_main)处设置断点。如果能停住,说明启动成功,问题在串口输出;如果停不住或无法调试,问题在启动或加载环节。 |
| 两个核心的打印信息混乱,或只有一个核心在打印 | 两个核心的串口配置成了同一个物理串口。 | 检查设备树,确保core = <0>和core = <1>分别指向不同的uart节点(如uart0和uart5)。 |
| 程序运行一段时间后死机或行为异常 | 1. 双核访问共享资源(如内存、外设)未做同步,导致竞态条件。 2. SDRAM访问不稳定(时钟、电气问题)。 3. 堆栈空间不足。 | 1.隔离问题:先分别测试两个核心的单核程序是否稳定。 2.检查共享资源:在 hello例程中,两个核心仅通过串口打印,没有共享变量。如果你修改了代码并引入了共享数据,必须使用互斥锁、信号量等机制进行保护。3.调整内存配置:检查hart1工程的链接脚本,为其分配足够的堆(heap)和栈(stack)空间,尤其是在SDRAM中运行。 |
| 无法通过调试器连接到hart1进行调试 | 调试器配置或IDE设置不支持多核调试,或hart1的调试接口未使能。 | 1.确认IDE支持:查看你的IDE和调试插件是否支持RISC-V多核调试。可能需要特定的调试配置。 2.检查调试脚本:在调试配置中,可能需要加载特定的多核调试脚本(.cfg文件),以确保在复位后能正确识别和连接两个核心。 3.先确保hart0能调试:多核调试通常以主核为入口。确保能正常对hart0进行单步、断点调试。 |
5.3 高级技巧与优化建议
自定义hart1的加载地址:默认的SDRAM地址(如
0x80000000)是BSP预设的。如果你需要调整,需要修改两处:- hart1工程的链接脚本:修改
MEMORY区域定义和SECTIONS的起始地址。 - 主核的加载代码:
hpm_hart1_image.S文件只是包含了数据,实际的搬运代码在BSP的底层启动文件里。你需要找到负责从Flash拷贝数据到SDRAM的函数(通常叫copy_hart1_image或类似),并修改其目标地址参数。这需要深入阅读SDK启动代码。
- hart1工程的链接脚本:修改
验证hart1固件完整性:在复杂项目中,可以在主核启动从核前,增加一个校验步骤,比如计算hart1 bin文件的CRC,并与预存值对比,确保加载到SDRAM的数据是正确的。
从核程序不限于
while(1):从核可以运行一个完整的实时操作系统(RTOS)任务。AWorksLP本身支持多核,你可以在hart1上也创建一个AWorks任务,并通过OpenAMP或共享内存与hart0进行复杂通信。demos/multi-core/下的openamp和rpc例程就是更高级的框架。性能考量:hart1运行在SDRAM,其访问速度比hart0的Flash快,但延迟比内部TCM高。对于极端性能要求的代码,可以考虑将hart1的关键代码段通过主核加载到内部RAM中执行,但这需要更精细的内存管理和链接脚本控制。
整个流程走下来,你会发现双核烧录的核心在于“分离编译,合并打包,主核引导”。只要理解了incbin包含机制和aw_open(“/dev/multi_core”)这个启动开关,剩下的就是细致的工程配置和问题排查了。希望这篇超详细的指南能帮你顺利打通HPM6750双核开发的任督二脉。
