ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于HPM6750非对称双核AMP的国产飞控系统设计实践

基于HPM6750非对称双核AMP的国产飞控系统设计实践

这两年做无人机飞控的选型,我最大的感受就是“单核越来越不够用”。一方面姿态解算、控制环、混控输出这些实时任务压死一个核,另一方面遥测、日志、外设管理、甚至轻量视觉识别又全都挤在一块,经常出现控制周期被外部中断打断的情况。所以当我把目光转到国产芯片 HPM6750 上时,真正让我动心的不是它那颗主频 816MHz 的 RISC-V 核,而是它天生支持非对称双核架构(AMP)。这不是简单的“有两个核”,而是可以把飞控系统按任务性质拆成两个独立世界的硬件基础。

这篇文章就把我基于 HPM6750 做国产飞控系统的一些思路和实操过程整理出来。适合正在做飞控、机器人控制器,或者评估国产替代方案的工程师,也适合对异构多核嵌入式系统感兴趣、想看真实落地而不是PPT的人。我会把非对称双核的任务划分、核间通信、缓存一致性、双核启动这几个关键点都撕开讲,最后再把我踩过的一些坑直接列出来。

1. 项目概述:HPM6750 飞控的定位与选型思路

1.1 飞控算力需求变了:为什么单核不够用

以前做飞控,用一颗 STM32F405 或者 F427,把姿态解算、PID 控制、PWM 输出、遥控信号解析全塞进一个中断里,也能飞。那时候的飞控功能比较纯粹:稳定飞行、手动控制、简单定高。算力需求相对固定,单核在 168MHz 下勉强撑住,大部分时间其实花在等待外设和中断切换上。

但行业变化很快,现在常见的需求变成了这样:

  • 在飞控里同时跑 RT-Thread 或 FreeRTOS,主控制周期 1kHz 甚至 8kHz,控制实时性不能被打折。
  • 除了 IMU 数据,还要接光学流量计、激光雷达、气压计、空速管,这些传感器数据需要在各自的时间窗口内被采集和融合。
  • 数传链路不再只是发个 RPY,还需要传 OSD 信息、电池电压、GPS 状态、调试曲线,甚至远程改参。
  • 日志系统要求高频率、无丢帧,经常需要黑匣子一样的完整记录。

这些需求堆在一个单核上,唯一的办法就是打断优先级:高频快中断插在控制中断里,日志 DMA 霸占总线,然后控制器一抖,飞控就被带偏。我做过一个项目,就是因为 SD 卡日志写入抖动太大,导致电机输出出现周期性跳变,后来查了半天才发现是 FATFS 的写入占用了 CPU 时间。

这就是我转向双核的最根本原因:不是追求性能数字,而是要有物理隔离。控制任务占用一个核,读写日志、协议解析、外设轮询任务占用另一个核,谁也不能打断谁。

1.2 非对称双核架构:“非对称”到底是什么意思

对称多核(SMP)在嵌入式里也常见,两个核共享一个操作系统,由内核统一调度任务,任务可以在任意核之间迁移。这样做的好处是负载均衡,逻辑上还是一个系统。但在飞控这种硬实时场景,反而容易出问题:任务迁移切换会引入不确定的缓存失效和调度延迟,你没法保证 1kHz 控制循环里那一句关键是运行在哪个核的哪一行指令上。

非对称多核(AMP)则相反,两个核不是对等的。HPM6750 的这两个 CPU 核都有独立的执行单元、独立的中断控制器和私有定时器,但它们的角色分配由软件定义:一个核跑飞控主循环,另一个核跑外设管理和通信。两个核可以分别运行不同的操作系统,比如 CPU0 跑 ThreadX/FreeRTOS 保证确定性调度,CPU1 跑裸机循环或者 RT-Thread 做文件系统和协议栈。

这种架构核心优势有三点:

  • 强隔离:控制任务独占一个核,外部事件风暴再猛也影响不到主循环。
  • 调度确定性:不需要 SMP 那种锁和原子操作来保护共享任务队列。
  • 分区权限清晰:通信协议、日志、外设驱动的崩溃不会带走飞控。

HPM6750 的双核设计是真的为 AMP 做足了准备:片上 2MB SRAM 可以很方便地划出非缓存的内存区域给核间通信使用,每个核又有独立中断入口,还有硬件邮件中断做核间触发。这些细节后面我会展开讲。

1.3 技术栈与关键参数

我用的这套飞控系统的完整技术栈如下:

模块选型
MCUHPM6750,双核 RISC-V,主频 816MHz
实时系统CPU0:ThreadX / CPU1:裸机环形循环(后期可跑 RT-Thread)
飞控框架自研控制调度器,1kHz 控制中断驱动
IMU 驱动SPI 中断模式,双传感器冗余
通信核间共享内存 + 邮箱中断
调试JTAG + 串口控制台 + 共享内存状态池

选择 ThreadX 而不是 FreeRTOS 的一个原因是它调度器的确定性更好,双核场景下我们不需要复杂的 IPC,任务全部绑定在各自核上。

2. 核心细节解析:HPM6750 飞控的骨架与核间设计

2.1 CPU0/CPU1 的任务划分实例

飞控系统的任务天然可以切成两类:

CPU0 负责“硬实时控制链”,这一类任务周期必须严格、抖动必须极小:

  • IMU 数据读取和姿态解算(互补滤波或者 EKF,视项目需求而定);
  • PID 控制循环和混控输出;
  • 遥控信号解析和模式切换逻辑;
  • 急停和失效保护(fail-safe)。

CPU1 负责“软实时外设链”,这一类任务允许毫秒级延迟,但不能长期阻塞:

  • 数传协议栈:MAVLink 或自定义协议解析,以及遥测打包发送;
  • SD 卡日志写入:黑匣子日志队列、故障记录、掉电保存;
  • 电源管理:电压电流采集、电池参数监测、加热/继电器控制等;
  • 外部视觉/高速传感器数据初步处理,比如光流、激光雷达那类高频但有突发性的传感器。

划分的要点是:让 CPU0 的世界尽量干净,它只和 IMU、PWM 或电机驱动这类控制外设直接通信。所有可能引起不确定延迟的操作全部丢给 CPU1。CPU0 不直接访问 SD 卡,不直接处理数传协议栈,它只需要把关键状态写入共享内存,然后通过邮箱中断通知 CPU1 取走。

2.2 核间通信机制:共享内存、邮箱中断与读指针

AMP 双核通信最朴素也最可靠的方案就是“共享内存 + 硬件中断”。

我在项目里设计了一个核间通信区,放在 HPM6750 的片上 SRAM 中,并且映射为非缓存属性。这部分内存专门用来做控制核和通信核之间的数据交换。核心数据结构分为两部分:

控制区(CPU0 写,CPU1 读):

  • 飞行状态:姿态四元数、角速度、欧拉角、高度;
  • 控制输出:油门、横滚、俯仰、偏航通道值;
  • 系统状态:控制模式、电池电压、剩余油量/电量;
  • 故障标志:传感器失效、GPS 信号弱、imu 溢出等。

指令区(CPU1 写,CPU0 读):

  • 遥控指令:解锁/上锁、模式切换、目标速度;
  • 参数配置:PID 参数、传感器校准值、日志使能;
  • 外部命令:外部计算机通过数传下发的任务指令。

两个核之间通过邮箱中断做“新数据到达”通知,而不是靠轮询共享内存标志。比如 CPU0 每次控制循环结束,把状态数据写入共享结构体,然后触发一次核间中断,CPU1 的接收中断里把数据拷贝到自己的本地缓存,再去处理。反过来也一样。

这里有个重要经验:共享内存只做瞬时快照,不做长期队列。控制流是周期性的,通信核只需要最新的状态,不需要把每一个中间状态都记录下来。如果需要历史数据,由 CPU1 自己按时间戳追加写入自己的日志队列,而不是让 CPU0 为了“保持同步”而阻塞。

2.3 缓存一致性与内存映射设计:一个绝不能错的字

多核嵌入式系统最容易翻车的点就是缓存一致性。HPM6750 的两个核各自有 L1 I-Cache 和 D-Cache,如果你把共享内存映射成普通可缓存类型,CPU0 写了一个数据,这个数据可能还留在自己的 D-Cache 里没有写回物理内存,CPU1 去读就拿到了旧值。飞控里这种错误是致命的。

解决手段有两种:

第一,把这段核间共享内存映射成非缓存类型。HPM SDK 的链接脚本和内存映射文件里可以单独划分一块区域,配置为 Cache 禁用。这样每次写入都会直接落在内存里,没有不一致风险,代价是访问速度略低,但对于几十字节的状态快照,这个代价可以忽略不计。

第二,如果必须使用缓存区域,则需要手动执行 cache clean/invalidate 操作。也就是写入侧在写完关键数据后要把 D-Cache 对应区域做 clean,读取侧在读取前做 invalidate。真实项目里这个流程特别容易漏,一旦漏一次,数据错乱就是完全随机的。

我做了一个折中方案,把所有核间通信相关全局变量全部放在一个自定义段.shared_mem里,在链接脚本中让它落在非缓存区域的地址空间。这样我在写代码的时候不需要时刻提醒自己“这里要不要加 barrier”,因为编译器知道这段内存不可缓存。HPM SDK 对非缓存区域的处理已经比较成熟,只需要在 memory map 的 attribute 里配置好。

提示:如果飞控突然出现姿态跳变或者控制量突变的诡异故障,先把核间通信缓存一致性检查一遍。很多时候不是算法出了问题,而是数据本身是坏的。

2.4 中断优先级与核间中断隔离

HPM6750 的中断控制器支持每个核独立配置中断。CPU0 和 CPU1 都有各自的中断入口地址和中断优先级配置。这意味着你可以把 IMU 的 SPI 接收中断、PWM 定时器中断绑定到 CPU0,而把串口 DMA 接收中断、定时器心跳中断绑定到 CPU1。

这样做的直接好处是:当 CPU1 上的 UART 数据风暴导致中断风暴时,CPU0 的中断响应时间完全不受影响。在单核系统里,这是做不到的,一切中断都在争抢同一个 CPU 时间片。

实际配置的时候需要注意 CPU1 的中断控制器可能默认初始状态和 CPU0 不一致,需要仔细看 SDK 里每个实例的初始化代码。我自己就踩过一个问题:CPU0 可以正常接收 SPI 中断,但 CPU1 上 UART 中断一直不触发,后来发现 CPU1 的优先级掩码寄存器默认把所有中断屏蔽了,必须显式设置。

3. 实操过程:从 HPM SDK 到双核协同跑起来

3.1 建立双核工程:从 SDK 模板说起

HPM SDK 已经提供了 AMP 双核的工程模板。我建议直接基于模板修改,而不是从零搭工程,因为双核的 link script 和启动文件涉及太多细节,手工搭建容易在启动阶段就卡住。

关键步骤:

  1. 从 SDK 中复制amp_emulation或者类似的双核示例工程,至少保留一个 CPU0 一个 CPU1 两个子工程。
  2. CPU0 工程的链接脚本里,需要预留 CPU1 要使用的 SRAM 区域,通常分成各自独立的数据段。
  3. CPU1 工程编译出的镜像和入口地址,需要被 CPU0 知道,因为 CPU0 负责将 CPU1 镜像引导起来。
  4. SDK 通常会提供一个prepare_for_running_core1()类似的接口,它内部完成把 CPU1 复位释放、设置为指定启动地址的操作。

这里要说一下 HPM6750 的启动流程。芯片上电后由片内 ROM 代码检查启动模式,然后运行 CPU0 的镜像。CPU0 初始化到一定阶段后,主动执行一条唤醒函数让 CPU1 从复位状态解除,CPU1 再运行它自己的入口代码。这种方式不像 SoC 那种由 BootROM 自动加载多核,而是完全由 CPU0 主导,所以双核的上电顺序是可控的。

在地址规划上,我建议把 CPU0 和 CPU1 的运行代码段、数据段彻底隔开。CPU0 区域放在 SRAM 的低端,CPU1 区域放在高端,中间留一片共享内存缓冲区。这样可以避免两个核的局部变量栈空间发生碰撞。

3.2 CPU0 引导 CPU1:唤醒时序与入口函数注意事项

CPU1 的入口函数有几个隐藏约束。

首先是它的入口函数不能像普通裸机函数那样随便声明。CPU1 在从复位状态唤醒时,栈指针尚未初始化,需要入口代码优先设置sp再设置全局指针。HPM SDK 的 startup 文件会处理这件事,但在你确定启动文件正确之前,不要假定 C 环境能直接用。

其次是 CPU1 从低功耗状态唤醒后,外设时钟状态可能和 CPU0 不完全一致。官方的启动流程一般已经保证系统时钟在 CPU0 侧配置完成,CPU1 不需要重新配置 PLL,否则容易造成时钟源冲突。

我遇到的一个情况是:CPU1 入口函数第一行就调用了printf调试,结果串口没有输出,代码卡在打印等待。后来排查发现是 CPU1 在实现printf重定向时依赖的是 CPU0 已经初始化好的外设,但 CPU1 启动时又重置了 UART 时钟域相关寄存器把它关掉了。解决方案是 CPU1 入口函数保持极简,必要的调试信息通过共享内存状态变量来观测。

3.3 核心数据流打通:IMU 到控制环再到日志

下面的流程是飞控主循环的一个典型连接:

  1. CPU0 上,定时器中断以 1kHz 频率触发控制循环。
  2. 控制循环第一步读取 SPI IMU 的缓存数据,通过传感器融合得到姿态。
  3. 姿态数据经过 PID 控制器,生成电机混控输出,指令通过高级定时器 PWM 直接输出。
  4. 控制循环最后把一组关键状态写入共享内存结构体,并触发 CPU1 的邮箱中断。
  5. CPU1 收到邮件中断后,把共享内存里的状态拷贝到自己的日志缓冲区。同时 CPU1 上的数传协议任务会定时打包这个状态,通过串口发往地面站。

整个过程里 CPU0 和 CPU1 之间只有一次内存拷贝和一次中断开销,单次耗时可控制在微秒级,远低于控制周期 1ms 的预算。

需要注意,控制循环里尽量避免 CPU0 主动等待 CPU1 的回复。如果你的设计里 CPU0 发一条核间消息之后,必须等 CPU1 处理完才继续飞控,那控制实时性直接就被拉垮了。非对称双核的本质是异步并行,不是“另一个核帮我分担锁”。

这里我对数据体积做过一次测算:每次状态快照大约 128 字节,通过非缓存共享内存写入,一次写入耗时约 2 微秒。在 1kHz 周期里,这只占 0.2%,几乎可以忽略不计。而同样的数据如果走 UART 同步发出去,至少几个毫秒,完全没法比。

3.4 调试架构:双核各留一条命门

双核调试和单核最大的区别是:你不能再用一个全局断点去查看“所有变量”,因为两个核各有独立执行流。

我的做法是:

  • CPU0 保留一个真实 JTAG 调试口,用来打硬实时控制环路的断点。
  • CPU1 通过串口控制台输出 Debug 信息,用来观察通信任务和日志任务运行状态。
  • 共享内存里专门留一块debug_status区域,两个核都向这个区域写入自己的“心跳”和最近一次异常前的位置ID。

这个debug_status区域在故障定位上极其好用。比如飞控突然不输出了,我可以先检查 CPU1 心跳是不是还在跑,然后看 CPU0 在哪个位置ID停住,基本能定位到是中断没触发还是死循环了。

4. 常见问题与排查技巧实录

4.1 双核启动异常:CPU1 卡死不再跑

症状:CPU0 运行正常,串口控制台有输出,但 CPU1 一点动静没有。共享内存里 CPU1 侧写入的版本号永远是0。

排查步骤:

  1. 确认 CPU0 调用了唤醒 CPU1 的接口,并且参数中的启动地址是否和实际链接地址一致。链接脚本修改后,CPU1 的入口地址很容易和实际不符。
  2. 检查 CPU1 工程是否被正确编译并链接到 CPU0 期望的位置。有些工程把 CPU1 镜像放在片外 Flash,CPU0 需要先把 CPU1 镜像拷贝到 SRAM 或 Flash 指定区域再唤醒。
  3. 如果 CPU1 使用的是独立链接脚本,确认它的初始栈指针设置的区域没有被 CPU0 的数据段覆盖。

排障时可以用一个“眨眼程序”验证:让 CPU1 入口函数只做一件事,反复翻转一个 GPIO。如果 GPIO 没反应,那就是启动流程的问题;如果 GPIO 有反应但你的应用逻辑异常,那就回到应用层排查。

4.2 缓存一致性导致数据错乱:玄学故障的元凶

症状:飞控运行十几分钟后,姿态解算偶尔跳一下,或者电机控制量出现瞬间到极限值的毛刺。概率随机,重启后可能就不再出现。

这种问题最难查,因为它不像中断冲突那样固定触发。我两次遇到过类似的case,最后都指向了同一个原因:共享内存被分配成可缓存区域,CPU0 写了一个值,CPU1 读到的是旧值,几微秒后又恢复正常。

解决办法就是前面说过的:把共享内存区域明确放到非缓存区间。如果一开始就用链接脚本划好区,后续就不用反复怀疑内存一致性问题。

注意:不要相信“多写几遍 cache clean 就能解决”的思路。缓存一致性问题本质上是要在架构层面消除,而不是靠碰运气刷缓存。

4.3 外设冲突:两个核都想独占外设

飞控上有些外设天然不适合共享,比如串口控制台、SPI总线上的多个传感器、PWM输出定时器。最危险的是,CPU0 和 CPU1 如果都想访问同一个外设的寄存器,而这两个核没有加互斥,结果就是寄存器更新被覆盖。

一个典型的例子是串口:CPU0 想在调试时打印控制循环数据,CPU1 也在打印日志,两个串口其实共用同一个 UART,就会出现乱码和丢数据。

我的原则:一个外设原则上只归属一个核。如果非要跨核访问,请在应用层通过“代理机制”解决:真正拥有这个外设的核接收另一个核的请求,代为执行访问,不要去直接操作对方的外设寄存器。

4.4 中断风暴与优先级反转

在非对称双核里,中断优先级反转仍然存在,但已经被限制在单个核内部。比如 CPU0 上低优先级 UART 中断长期霸占嵌套层级,导致高优先级 TIMER 中断延迟增加。实际上控制中断本来应该很快执行,但因为 CPU0 自己的中断嵌套管理没做好,还是会出问题。

我的做法是给 CPU0 的一切非必要中断设置最低优先级,甚至可以做到“控制周期内允许屏蔽非关键中断,控制计算结束后再恢复”。因为这是 AMP 架构,通信任务的实时中断全部放在 CPU1 处理,CPU0 只保留 IMU 和 PWM 相关的中断,优先级反转范围被缩小很多。

4.5 排查技巧速查表

现象首要怀疑点快速检查方法
CPU1 无输出启动地址/唤醒时序错误GPIO 翻转测试
随机数据毛刺共享内存缓存属性不对查看链接脚本内存段属性
日志数据乱码两个核同时访问串口外设归属梳理
控制中断出现抖动CPU0 上有强占用中断关掉可疑中断逐个排查
共享内存数据不对版本结构体有未同步字段编译期检查字段大小两端一致

5. 项目扩展思路与个人体会

这套基于 HPM6750 的非对称双核飞控方案,后续做扩展也比较方便。CPU1 既然空出了大量算力,除了跑通信和日志,还可以尝试接一些需要图像处理的应用,比如光流测距传感器数据处理、超简单图像识别,或者做双冗余传感器管理。再往下,如果 CPU0 上还有剩余性能,可以考虑把部分控制算法升级成高频率的内环控制,比如 4kHz 甚至 8kHz 的电流环控制,这在电机驱动类项目中能明显提升响应带宽。

最后再分享一个我个人的体会:做双核飞控最难的不是把代码跑起来,而是改变单核时代的思维模式。在单核上,所有任务都是抢同一个执行时间片,你习惯性地用优先级去排队;到了非对称双核,你要学会的是“任务分家”,把实时性要求不同的任务放到不同的世界里,并用一种轻量、可预测的通信方式把它们连接起来。

用 HPM6750 搭这样一个系统,我最大的收获并不是那颗 816MHz 的核有多快,而是它给了我从根本上重新设计飞控软件结构的自由。国产芯片这几年,已经不只是“能用”的水平,双核架构上的完整度也超出了我最初的预期。如果手里刚好有一个对计算实时性和外设复杂度要求都比较高的机载项目,这套方案值得直接被拿来参考。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进