做飞控主控最怕什么?不是算法看不懂,而是算力不够、实时性保不住。我最近用先楫半导体的HPM6750把一套四旋翼飞控完整跑了起来,这芯片是国内少见的非对称双核RISC-V MCU,主频拉满到816MHz,用在国产飞控系统里相当能打。先说结论:如果你也在纠结“要不要从STM32往高性能多核切换”,这篇内容很值得看完。我会从为什么选它、双核任务怎么切、核间怎么通信,到上手HPM SDK之后的典型坑,全部捋一遍,不是那种只贴规格表的吹水文。
我手里的这套系统,控制频率跑在1kHz,外设接口挂了一堆,功耗和启动时间也都还能接受。说实话,第一次把两个核同时点亮的时候,我心里是有底气的——因为非对称双核架构解决了我过去在单核上最头疼的事:姿态环必须实时、通信和日志又不能堵在中断里。下面我按自己的实操顺序展开讲。
1. 飞控主控选型:HPM6750到底凭的是什么
1.1 一块芯片顶两块用
先说硬件底子。HPM6750是双RISC-V 32位内核,最高主频816MHz,这个数据放在MCU领域属于第一梯队。它跟普通单核MCU最大的区别是,一颗芯片内部放了两个完整的处理器核心,同时又有大容量片上SRAM,支持外部NOR Flash和SDRAM扩展。我做飞控时最关心的东西,它都给了:多路UART、SPI、I2C、CAN/CAN-FD、PWM、正交解码、大量GPIO、定时器,还有千兆以太网、LCD控制器、摄像头接口和JPEG编解码器。
为了更直观,我整理了一份针对飞控场景看重的规格清单:
| 资源 | HPM6750能提供的价值 |
|---|---|
| 双核CPU | 一核管控制,一核管计算与通信 |
| 816MHz主频 | 高算力余量,跑EKF/VIO不再捉襟见肘 |
| 大容量片上SRAM | 避免外部PSRAM导致时序不确定性 |
| 多路PWM/定时器 | 直接驱动电调,支持高分辨率油门更新率 |
| CAN-FD | 对接机载设备、云台、动力电池BMS |
| 千兆以太网 | 挂机载电脑、图传链路非常方便 |
| 摄像头接口 | 给视觉定位/避障留了硬件入口 |
一个容易被忽略的点是,它的外设不光是“多”,而是“够独立”。比如多通道PWM定时器可以独立触发DMA传输,IMU数据可以通过SPI+DMA直接进内存,不需要CPU反复打断。这颗芯片不是一颗只管逻辑计算的CPU,而是一个完整的飞行控制器“周边生态”。
1.2 为什么这个算力水平对飞控很重要
很多人觉得飞控算法不复杂,PID加起来几十行代码,STM32F405完全够用。这话放在几年前没错,但现在的飞控早不是“陀螺仪+PID”的时代了。
现在的国产飞控系统通常要同时跑这几类任务:
- IMU数据读取、温度补偿、姿态解算;
- 加速度计/陀螺仪/磁力计/气压计/ GPS 的传感器融合,典型做法是EKF或无迹卡尔曼滤波;
- 位置控制、速度控制、高度控制,多层控制器级联;
- MAVLink等通信协议栈,无线数传双向收发;
- 黑匣子日志记录,掉电前的数据要落地;
- 视觉定位、激光雷达测距、避障算法(至少预留接口)。
这些任务叠在一起,单核MCU的算力和中断负载都会很紧张。尤其是传感器融合和高频控制放在同一个核上时,一旦EKF某个周期计算超时,IMU中断就会被延后,最终表现就是飞机“忽忽悠悠”然后掉高度。我实际踩过这个坑:单核方案里,把融合算法从100Hz升到200Hz,控制周期就开始抖动。
HPM6750这种非对称双核架构,恰恰把这块短板补上了。一个核干“硬实时”的事,另一个核干“重计算”的事,物理隔离,谁也拖不了谁的后腿。这也是我最后选择它当飞控主控的最直接原因。
2. 非对称双核架构的设计拆解
2.1 SMP与AMP:从对称总线到非对称分工
先厘清一个概念。双核处理器有两种主流工作模式:SMP对称多处理和AMP非对称多处理。SMP很好理解,两个核接受同一个操作系统调度,共同分担负载,比如手机里的八核处理器跑Android就是SMP。AMP则完全相反,两个核跑各自独立的系统或裸机程序,像两个独立的“人”在同一个身体里协同干活。
非对称双核架构属于AMP的范畴。关键点不是“两颗CPU塞进一颗芯片”,而是“两颗CPU干什么,在设计阶段就分好工”。在飞控这种安全敏感场景里,我特别推荐AMP方案,原因是:
- 控制任务要的是确定性,不能被其他任务抢占;
- 通信和日志任务要的是吞吐量,可以容忍一定延迟;
- SMP下两个核共享调度器和中断,会产生比较严重的相互干扰;
- AMP下核心职责清晰,故障隔离也更容易做。
如果拿生活类比,SMP就像两个厨师共用一口炒锅,谁有空谁炒,高峰期容易撞车;AMP就像主厨专门炒菜,副厨专门配菜、切菜、记账,各干各的,效率更高,而且主厨不会被配菜需求打断。
HPM6750把这种模式发挥到了极致:它允许开发者在两个核上分别烧写独立固件,通过共享内存和核间中断来配合。这也意味着飞控的“实时内核”和“应用内核”可以真正解耦。
2.2 两个核心怎么分配:飞控的两层大脑
我在这个项目里把双核分成两条明确的职责线:
Core0跑高频实时控制,我称它为“飞控核”。它专门负责:
- IMU中断响应和数据采样;
- 姿态解算和控制律计算;
- 混合器输出到电机PWM;
- 紧急状态下的直接解锁/上锁。
Core1跑应用与通信,我称它为“应用核”。它负责:
- EKF传感器融合、位置/速度估计;
- 外部指令协议解析,MAVLink打包解包;
- 日志写入Flash/SD卡;
- 跟机载视觉模块、光流传感器、数传模块进行数据交互。
简单说就是:Core0是飞控的“手和脚”,Core1是飞控的“眼睛和嘴”。手和脚必须稳、准、快,眼睛和嘴可以稍微慢半拍,但不能阻塞手脚动作。
这个划分不是拍脑袋定的,最核心的依据是“时延敏感度”。姿态环每2ms甚至每1ms就要算一次,差几十微秒都会在混控输出上体现出来;而通信一帧数据晚几毫秒完全无感。另一个依据是“故障隔离”:如果通信协议栈在Core1上卡死,Core0仍然能维持飞行器稳定,不会因为日志写入超时导致炸机。
3. 飞控系统软件架构与关键模块实现
3.1 固件整体分层
我搭这套飞控固件时,参考了不少开源飞控的分层思路,但为了适配双核,做了一些调整。整体上分四层:
第一层是板级支持包,负责时钟、PLL、GPIO、中断控制器、DMA的初始化;第二层是驱动层,包括IMU、气压计、磁力计、PWM电机输出、UART/DMA收发;第三层是功能层,包括姿态估计、位置估计、控制器、导航逻辑、日志;第四层是应用层,包括遥控器指令、地面站协议、任务规划。
这套分层里,第一、二层大部分跑在Core0上,因为核心外设的实时访问必须稳定。第三层里的实时控制器跑在Core0,而相对重型的滤波和导航算法跑在Core1。第四层主要在Core1上,遥控器协议这种对时序敏感的例外——我把SBUS解析放到Core0,因为遥控器丢帧直接关系到安全。
这里要提醒一个原则:外设所有权一定要明确。不能两个核同时初始化同一个UART,更不能两个核同时开同一个I2C总线。我在Core0做完外设初始化后,会通过核间消息把“设备句柄”或共享数据指针告诉Core1,而不是让Core1自作主张重新Init一遍。
3.2 核间通信:共享内存、邮箱中断和数据一致
双核的“沟通方式”是整个架构里最核心的技术点。这里直接用关键数据说话。
我定义了一个共享内存区,专门放IMU原始数据和姿态结果。结构体大致是这样:
typedef struct { uint32_t seq; // 数据序号,用于检测掉帧 uint32_t timestamp_us; // 采样时间戳 float gyro[3]; float accel[3]; float mag[3]; float pressure; } imu_sample_t; #define SHARED_MEM_BASE 0x01000000 // 实际地址换成你的链接脚本里预留的区域 #define SHARED_IMU_OFFSET 0x0000 imu_sample_t *imu_shared = (imu_sample_t *)(SHARED_MEM_BASE + SHARED_IMU_OFFSET);Core0每次采完IMU,把数据写入这个结构体,然后发一个核间中断通知Core1。Core1收到中断后,再读数据去做融合。写个通用流程是这样的:
// Core0:IMU数据就绪 void imu_isr(void) { imu_shared->seq++; imu_shared->timestamp_us = timer_get_us(); imu_shared->gyro[0] = gyro_raw[0]; imu_shared->gyro[1] = gyro_raw[1]; imu_shared->gyro[2] = gyro_raw[2]; /* ... accel, mag, pressure 赋值略 */ __DMB(); // 数据写完后,确保内存同步 ipc_trigger(IPC_CHANNEL_0); // 给Core1发核间中断 } // Core1:收到核间中断后读取 void ipc0_isr(void) { uint32_t seq = imu_shared->seq; if (seq != last_seq) { last_seq = seq; ekf_predict_update((const imu_sample_t *)imu_shared); } }两个细节必须说透。
第一是内存一致性。RISC-V内核和Cortex-M一样,在有缓存和乱序执行的情况下,不能保证CPU A写入的数据CPU B立刻就能看到。我的做法是:把共享内存区域配置成“强顺序/非缓存”访问,并在写完之后用__DMB()这类内存屏障指令保证可见性。如果你用默认缓存配置又不去管它,最典型的现象就是Core1读到的IMU数据偶尔是“旧”的,飞机自然飞不稳。
第二是核间中断机制。HPM SDK里提供了核间通信的底层接口,原理就是“CPU A往寄存器里写一个触发位,CPU B响应对应的中断”。接收方中断里第一件事就是读数据,不要做复杂处理,否则会拖长中断响应时间。我实测Core0到Core1的通知延迟可以在微秒级,这个量级对飞控完全够用。
4. 实操落地:从HPM SDK把工程搭起来
4.1 环境和工具链准备
我从零上手HPM6750,第一步就是搞定开发环境和工具链。HPM官方SDK做得比较完善,底层驱动和外设例程都齐,直接基于它来改比从寄存器裸写要省很多时间。
工具链方面,我用的是一套标准RISC-V GCC工具链,配合OpenOCD和调试器下载调试。工程构建方式采用的是Makefile加CMake的混合方式,HPM SDK本身自带一套构建系统,跟着仓库里的文档跑一遍sample就能把环境验证通。
如果你是第一次用,我建议先把官方的“双核点亮”例程跑通,别一上来就直接改自己的飞控逻辑。双核例程跑通的意义是验证两件事:第一,Core0能把Core1的固件加载起来;第二,两个核的共享内存能互相读写。这两件事一旦通了,后面开发就顺了。
4.2 双核启动流程与内存规划
双核启动是整个项目里最容易让人晕的地方。我当初调试时,Core0总是跑起来但Core1没动静,后来才知道核心原因在启动顺序和内存规划上。
典型的启动流程是这样的:
- Core0先复位,完成时钟、电源、存储控制器初始化;
- 把Core1的固件镜像从Flash加载到约定内存地址;
- 设置Core1的复位向量和栈指针;
- 通过寄存器/控制位释放Core1,让它进入运行状态;
- Core1启动后,先做自己的板级初始化,然后等待核间消息。
内存规划更重要。两个核必须“划地盘”,否则Core0的栈指针和Core1的堆指针碰在一起,系统就会离奇崩溃。我的做法是在链接脚本里明确划分三段内存:
- Core0私有用内存;
- Core1私有用内存;
- 两个核共享的通信区。
共享通信区单独划在固定地址,并且只放核间通信需要的结构体。链接脚本大致是这样:
MEMORY { RAM_CORE0 (rwx) : ORIGIN = 0x00000000, LENGTH = 512K RAM_CORE1 (rwx) : ORIGIN = 0x00080000, LENGTH = 512K SHARE_MEM (rw) : ORIGIN = 0x00100000, LENGTH = 128K }当然具体基地址以实际芯片内存映射为准。这个思路最重要:两个核的栈、堆、全局变量必须在各自的RAM区域里,不要让编译器默认把多个核的段重叠到一起。我遇到过一个很隐蔽的故障,Core0往数组里写数据,直接踩到了Core1的任务栈,飞控在飞行中突然重启,查了一天一夜才发现是链接脚本写错了。
4.3 实时周期与任务优先级调整
飞控核心是1kHz控制循环。我在Core0上的调度安排是这样的:
- 最高优先级:IMU数据就绪中断,触发顺序是IMU采样完成;
- 次高优先级:控制律定时器中断,执行姿态控制、位置控制;
- 中等优先级:PWM输出更新DMA;
- 低优先级:电机解锁/上锁、状态机切换。
一个重要的实操点是:把IMU中断到来的时间戳直接当作控制循环的基准时间,不要把控制循环放到一个独立定时器里。因为IMU中断和控制中断如果异步触发,会产生相位漂移,导致控制延迟出现周期性抖动。很多飞控芯片只有一个主控,很难做到这一点;HPM6750因为有独立定时器组和DMA通道,可以把IMU采样和PWM输出彻底错开,我实测下来控制周期稳定很多。
Core1上的优先级则完全不同。我让EKF融合任务周期跑200Hz,通过定时器触发;MAVLink收发放在稍低优先级;日志写入放到更低优先级,写日志还得避免阻塞融合。如果一个周期内计算没跑完,Core1会直接跳过本次融合,用上一帧数据,而不是让任务堆积。这个策略保证了Core1即使过载,Core0的控制仍然不受影响。
5. 实测心得与踩坑排查实录
5.1 一组可复现的实测数据
我把自己这套基于HPM6750双核飞控的实测表现列了个表。我的测试环境是:IMU通过SPI连接,SPI时钟20MHz,PWM输出频率拉到24kHz,控制频率1kHz。
| 项目 | 实测结果 |
|---|---|
| Core0控制周期任务抖动 | 约±1μs以内 |
| Core0到Core1核间中断延迟 | 微秒级 |
| Core0 CPU占用率 | 约60%~70% |
| Core1 CPU占用率 | 约40%~50% |
| IMU SPI读取单次耗时 | 约30~40μs |
| PWM输出频率 | 24kHz下稳定 |
这个数据说明一个事实:HPM6750跑飞控的算力余量非常充足。哪怕我把控制频率提到2kHz,也还能留出足够资源给更复杂的滤波算法。
顺嘴说一句,我调飞控时习惯把PWM输出频率拉到电机允许的上限附近,因为高频PWM能让电机响应更线性、噪声更小。HPM6750的高分辨率定时器在这块非常配。
5.2 高频问题排查速查表
以下是这轮项目里我遇到最多的四个问题,以及对应的排查思路和解决办法。
| 现象 | 可能原因 | 解决动作 |
|---|---|---|
| Core1读到的共享数据偶尔错乱 | 缓存一致性问题 | 将共享内存配置为强顺序/非缓存;写后加内存屏障 |
| 双核同时访问同一外设导致总线挂死 | 外设所有权未隔离 | 规定所有外设只由一个核初始化,另一个核通过核间消息访问 |
| 用调试器单步时Core1不启动 | 复位向量或栈指针设置错误 | 检查Core1镜像加载地址,确认启动前后端到端的地址一致 |
| 控制周期偶发大幅度抖动 | 高优先级中断里做了耗时操作 | 中断里只做数据拷贝和标记,复杂算法移到任务上下文 |
| 日志写入期间飞行状态异常 | 日志任务占用太多总线带宽 | 限制日志任务优先级,把写入频率降下来,或走DMA |
这几个问题我都亲手踩过,尤其是缓存一致性,它不像编译错误那样有明确报错,飞控表现就是数据时好时坏。排查时最好的工具是打时间戳,把Core0写入序号和Core1读取序号对齐,掉帧位置一目了然。
我自己还总结了一个土办法:在飞控起飞前,把两核之间跑一个通信心跳,比如Core0每秒发100个计数包给Core1,Core1收到后再回给Core0。如果心跳计数连续、无丢失、无重复,核间通信基本就稳了。这一步能过滤掉至少一半的偶发问题。
5.3 我个人的避坑心得
最后说几个普通文档里不会写的东西。
第一,双核调试很考验耐性。我调试时用的是多路串口日志方案,Core0和Core1各用一条独立UART打日志,再配合调试器看内存变量。不要试图用单串口同时看两个核的日志,那会把问题搅成一团。HPM6750的串口资源多,舍得用。
第二,做国产飞控系统一定要把BSP版本固定下来。HPM SDK更新比较快,头文件接口偶尔会调整,我一开始跟着最新版SDK跑,项目中途两次升级导致驱动代码大面积改版。后来果断锁住一个稳定BSP版本,业务逻辑单独建仓库,就不再受SDK变动影响了。
第三,不为“用双核而用双核”。非对称双核架构只有在任务划分清晰时才发挥价值。如果你的飞控只跑经典PID、不上EKF、不接视觉,那单核完全够用,引入双核只会增加调试成本。HPM6750只是给你多了一个选择,真正的飞控水平还是看控制逻辑和实验迭代,芯片永远是工具。
这套系统后续我准备往视觉避障和光流定位方向扩展,把Core1的算力吃得更狠一点。如果你也在折腾HPM6750或者高性能国产飞控,希望这篇实际记录能帮你少走几步弯路。