首页 / 资讯中心 / 文章详情

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

基于HPM6750非对称双核AMP的国产飞控系统设计实践 ★ FEATURED ARTICLE
这两年做无人机飞控的选型我最大的感受就是“单核越来越不够用”。一方面姿态解算、控制环、混控输出这些实时任务压死一个核另一方面遥测、日志、外设管理、甚至轻量视觉识别又全都挤在一块经常出现控制周期被外部中断打断的情况。所以当我把目光转到国产芯片 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实时系统CPU0ThreadX / 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 和启动文件涉及太多细节手工搭建容易在启动阶段就卡住。关键步骤从 SDK 中复制amp_emulation或者类似的双核示例工程至少保留一个 CPU0 一个 CPU1 两个子工程。CPU0 工程的链接脚本里需要预留 CPU1 要使用的 SRAM 区域通常分成各自独立的数据段。CPU1 工程编译出的镜像和入口地址需要被 CPU0 知道因为 CPU0 负责将 CPU1 镜像引导起来。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 到控制环再到日志下面的流程是飞控主循环的一个典型连接CPU0 上定时器中断以 1kHz 频率触发控制循环。控制循环第一步读取 SPI IMU 的缓存数据通过传感器融合得到姿态。姿态数据经过 PID 控制器生成电机混控输出指令通过高级定时器 PWM 直接输出。控制循环最后把一组关键状态写入共享内存结构体并触发 CPU1 的邮箱中断。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。排查步骤确认 CPU0 调用了唤醒 CPU1 的接口并且参数中的启动地址是否和实际链接地址一致。链接脚本修改后CPU1 的入口地址很容易和实际不符。检查 CPU1 工程是否被正确编译并链接到 CPU0 期望的位置。有些工程把 CPU1 镜像放在片外 FlashCPU0 需要先把 CPU1 镜像拷贝到 SRAM 或 Flash 指定区域再唤醒。如果 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 的核有多快而是它给了我从根本上重新设计飞控软件结构的自由。国产芯片这几年已经不只是“能用”的水平双核架构上的完整度也超出了我最初的预期。如果手里刚好有一个对计算实时性和外设复杂度要求都比较高的机载项目这套方案值得直接被拿来参考。
阅读完成 · 觉得有帮助?
咨询建站