资讯详情

Crazyflie飞控固件移植到Keil MDK:从架构解析到工程落地

📅 2026/10/18 14:03:25 | 华诺云谱 👁 阅读
Crazyflie飞控固件移植到Keil MDK:从架构解析到工程落地
简介这是一份面向嵌入式开发者的Crazyflie微型无人机固件移植资料目标是将原有工程完整迁移到Keil MDK环境便于利用Keil的调试与编译特性进行二次开发。资料共203个文件压缩包约624KB以C源码78个、头文件87个为主同时包含汇编启动文件、链接脚本、Makefile和Keil工程文件覆盖从底层寄存器配置到项目构建的完整链路。目前已有484人学习浏览。内容除了STM32F10x标准外设驱动外还涉及MPU6050姿态传感器驱动、任务调度模块及多类系统文件说明对于理解Crazyflie固件结构很有帮助。借助这些材料开发者可以了解移植过程中Makefile调整、启动脚本适配、驱动编译验证等关键步骤并能直接打开Keil工程进行断点调试与性能分析大幅缩短环境搭建和移植调试周期。1. crazyflie移植到Keil一份让飞控入门不再依赖Linux的命令行固件工程很多搞四轴、玩飞控的开发者手里都有一块STM32开发板和一套趁手的Keil MDK结果第一次打开crazyflie源码时直接懵了仓库里没有.uvprojx工程文件只有一堆Makefile、链接脚本和GCC专用语法装Windows版的arm-none-eabi工具链还得在命令行里敲指令。这份“crazyflie移植到Keil版”就是干这件事的——把那套基于FreeRTOS GNU工具链的固件完整搬进Keil MDK让习惯在uVision里点Download按钮、看变量Watch窗口的开发者也能跑起这套开源飞控。适合三类人想把crazyflie当学习蓝本做姿态解算实验的学生、要在STM32F405平台上二次开发飞控的工程师、以及想绕开Linux编译环境的四轴爱好者。2. 架构先行原版固件的构建体系与各层模块到底怎么分工2.1 Makefile与FreeRTOS的双重结构原工程为什么难以下手先看原版固件的构建方式。crazyflie整套固件并非单一目录它通过git submodule引入FreeRTOS、CMSIS、无线协议栈等第三方代码顶层是一个大型Makefile体系。编译时执行make命令Makefile会根据目标平台比如crazyflie_cli自动选择链接脚本、启动文件和编译参数工具链是arm-none-eabi-gcc。这种结构在Linux下很顺畅但到了Windows上问题就来了路径分隔符、工具链安装、环境变量配置每一项都能劝退新手。原工程的另一个特点是FreeRTOS深度定制系统 tick 配置、内存堆管理、任务优先级都在FreeRTOSConfig.h里同时源码里大量使用GCC的__attribute__语法和asmlinkage这样的编译器特性。移植到Keil版时这些都要逐条适配ARMCC编译器的规则否则几百个编译错误瞬间砸过来。理解这一点再去看Keil版工程里多出来的那些配置项就不觉得多余了。2.2 目录结构解剖app / modules / drivers / utils 各层的职责边界打开Keil工程左侧Project栏的目录分组基本还原了原版固件的层次关系。源码根目录下最核心的四个目录分别是app、modules、drivers和utils外加一个platform目录存放板级初始化代码。我一般会让读者先看app因为飞控的主入口就在这里。crazyflie-firmware/ ├── app/ # 应用层main函数、系统初始化、任务创建 │ └── src/ │ ├── main.c │ ├── system.c │ └── app_main.c ├── modules/ # 核心逻辑姿态估计、控制器、通信、参数系统 │ ├── inc/ │ ├── src/ │ │ ├── stabilizer.c # 控制器主任务 │ │ ├── attitude_estimator.c │ │ ├── estimator.c │ │ ├── param.c # 参数系统实现 │ │ ├── log.c # 日志系统实现 │ │ └── crtp.c # 通信协议 ├── drivers/ # 底层驱动传感器、无线、存储 │ ├── src/ │ │ ├── mpu9250.c │ │ ├── lps25h.c │ │ └── nrf51.c ├── platform/ # 板级支持电源管理、LED、初始化 └── utils/ # 通用工具队列、定时器、位操作这个分层的核心思想是驱动可换、逻辑稳定。drivers层只做传感器寄存器读写把原始数据扔给上层modules层所有飞控算法不关心具体硬件型号输入的是统一格式的传感器结构体app层负责搭任务、调调度。Keil版也沿用了这种结构你在工程里看到的文件夹层级就是源码目录的映射。2.3 模块间通信CRTP协议、参数系统与日志系统的协作关系crazyflie的无线通信不像普通串口透传那么简单它跑了一个叫CRTPCrazy RealTime Protocol的轻量协议数据包通过板载nRF51822无线MCU转发。CRTP协议里包含几个关键口参数口param、日志口log、控制口cmd、平台口platform。参数系统和日志系统是整个固件的灵魂它们让开发者可以在飞行途中不重新编译就调整PID系数。// param.c 核心每个参数注册进一个全局链表 typedef struct param_s { const char *name; const char *type; void *addr; struct param_s *next; } param_t; // 注册参数时用宏自动拼接名字 #define PARAM_ADD(type, name, var) \ static param_t param_##name { #name, #type, var, NULL }; \ __attribute__((constructor)) void param_init_##name(void) \ { param_register(param_##name); }参数系统的价值在于param.c维护一张参数注册表上位机通过CRTP协议把“参数名数值”发送过来固件在链表里查找对应内存地址直接写入。这意味着stabilizer.c里控制环路的kp、ki、kd都是变量而不是宏定义改参就像改数据库记录一样。日志系统则是反向通道把需要观察的内部变量注册进日志表上位机按需订阅固件周期性通过无线发送快照。这个机制极大方便了飞控调试——飞行姿态、电机输出、传感器原始值都不用仿真器去看无线就能拉出来。2.4 硬件平台速览主控、IMU、气压计与板载无线MCU的驱动关系主控是STM32F405Cortex-M4F内核带硬件浮点单元主频168MHz。这套硬件跑FreeRTOS加姿态解算加控制算法负载并不高。IMU用的是InvenSense MPU9250九轴传感器三轴加速度计、三轴陀螺仪、三轴磁力计通过SPI接口与主控通信数据读取频率最高可达1kHz。气压计常见的是LPS25H通过I2C挂在总线上用于高度估计。板载还有一个nRF51822作为无线协处理器它自己跑BLE和私有2.4GHz协议栈与主控之间用串口连接。主控把CRTP数据包扔给nRF51发出去PC端接收后由Python客户端解析。3. 核心模块拆解姿态解算与控制环路在源码里是这么写的3.1 传感器读数与对齐MPU9250加速度计/陀螺仪数据的姿态解算输入不管上层算法多高级传感器数据质量决定飞控上限。MPU9250的驱动代码负责配置量程、设置采样率、读取六轴原始值。陀螺仪量程默认±2000dps加速度计量程±2g这些在驱动头文件里可以通过宏修改。读完的原始数据是两个int16_t数组但姿态解算需要的是物理单位所以驱动层要做一步换算。// mpu9250.c 数据读取与单位换算 void mpu9250_read_gyro(float *gyro) { uint8_t data[6]; spi_read(MPU9250_SPI, MPU9250_GYRO_XOUT_H, data, 6); int16_t raw[3]; raw[0] (data[0] 8) | data[1]; // 高位在前大小端要反转 raw[1] (data[2] 8) | data[3]; raw[2] (data[4] 8) | data[5]; // 满量程 2000dps对应的灵敏度为 16.4 LSB/dps gyro[0] raw[0] / 16.4f; gyro[1] raw[1] / 16.4f; gyro[2] raw[2] / 16.4f; }这里有两个参数要特别注意SPI字节序和灵敏度系数。MPU9250的数据寄存器默认大端模式读出来的高字节在低地址。灵敏度系数与所选量程一一对应如果驱动配置里把量程改成了±500dps灵敏度就要换成65.5 LSB/dps。这种量程与换算系数不匹配的错位是飞控传感器最常见的翻车点。3.2 姿态估计器实现互补滤波的核心步骤与参数调法crazyflie原生固件里姿态估计器用的是互补滤波思路核心逻辑用陀螺仪积分做短期姿态预测再用加速度计重力方向做长期修正。之所以不用纯积分是因为陀螺仪有零漂积分几秒后姿态就飘了而加速度计在飞行时噪声很大不能直接求倾角。互补滤波就是让两者取长补短。// attitude_estimator.c 核心公式误差修正 四元数积分 void attitude_estimator_update(float dt, const float *gyro, const float *acc) { float q[4]; // 当前姿态四元数 float error[3]; // 1. 把加速度计归一化得到重力参考方向 float norm sqrtf(acc[0]*acc[0] acc[1]*acc[1] acc[2]*acc[2]); float ax acc[0] / norm, ay acc[1] / norm, az acc[2] / norm; // 2. 用当前四元数算出预测的重力方向求误差叉积 error[0] ay * q[1] - az * q[0]; error[1] az * q[2] - ax * q[1]; error[2] ax * q[0] - ay * q[2]; // 3. 误差经PI修正后叠加到陀螺仪角速度上 gyro[0] error[0] * kp; gyro[1] error[1] * kp; gyro[2] error[2] * kp; // 4. 用修正后的角速度做四元数一阶积分 q[0] 0.5f * (-q[1]*gyro[0] - q[2]*gyro[1] - q[3]*gyro[2]) * dt; q[1] 0.5f * ( q[0]*gyro[0] - q[3]*gyro[1] q[2]*gyro[2]) * dt; q[2] 0.5f * ( q[3]*gyro[0] q[0]*gyro[1] - q[1]*gyro[2]) * dt; q[3] 0.5f * (-q[2]*gyro[0] q[1]*gyro[1] q[0]*gyro[2]) * dt; // 5. 重新归一化防止四元数漂移 float n sqrtf(q[0]*q[0] q[1]*q[1] q[2]*q[2] q[3]*q[3]); q[0] / n; q[1] / n; q[2] / n; q[3] / n; }代码里kp是互补滤波的比例修正系数典型值取0.3~0.5。kp越大加速度计修正越强动态响应快但容易把高频噪声引进来kp越小陀螺仪积分占主导姿态更平滑但抗漂移能力弱。调试时先让飞机静止观察姿态角是否在±1度内小幅度抖动如果抖动严重就调小kp。四元数积分这一段用到了dt这个值是姿态估计任务的实际调度周期。crazyflie固件里姿态估计频率通常设为500Hz也就是dt 0.002s。如果移植后任务频率变化记得同步修改dt参数否则姿态积分的角速度累积会偏快或偏慢表现为悬停时飞机缓慢自转。3.3 三层控制链路速率PID环、姿态PID环、位置PID环的串联关系看stabilizer.c会发现控制链路是内环嵌套外环的结构。最内层是角速率环中间是姿态角环最外层是位置环。每个外环的输出就是内环的设定值位置环输出期望加速度姿态环输出期望姿态角速率环输出期望角速度。// stabilizer.c 控制链路的简化结构 void stabilizer_attitude_loop(float dt) { // 1. 姿态环当前姿态角 - 期望角速度 float rate_setpoint[3]; pid_attitude_update(pid_attitude_roll, attitude_roll, attitude_setpoint_roll); rate_setpoint[0] pid_attitude_roll.out; // 2. 速率环期望角速度 - 期望力矩 pid_rate_update(pid_rate_roll, gyro_roll, rate_setpoint[0]); // 3. 期望力矩 - 电机PWM占空比 mixer_update(pid_rate_roll.out, pid_rate_pitch.out, pid_rate_yaw.out); }PID对象在crazyflie里不是分散的常量而是一个包含误差、积分项、微分项和上下限的结构体。每个PID都有自己的输出限幅因为电机PWM占空比有物理上限。姿态环的输出限幅通常是角速度限幅速率环的输出限幅是力矩限幅。调参顺序也有讲究先调速率环再调姿态环最后调位置环。你直接改姿态环PID往往看不出效果因为可能被内环速率环的保守参数压制住了。我把这个叫做“顺序调参铁律”违反必翻车。3.4 参数注册机制为什么PID参数能在飞行途中在线修改这部分是实现细节里的精华。stabilizer.c里每个PID系数都通过PARAM_ADD宏注册进参数系统注册时指定参数ID。比如陀螺仪PID的kp参数注册后上位机发送param set PID_ROLL_RATE.kp 0.05数据包经过CRTP协议到达主控param模块在链表里找到对应地址把值写进去。下一次控制循环读到的pid_rate_roll.kp就已经是新值了。这个机制让整机调试非常高效——不用反复编译烧录飞机在空中就能调参。但有个前提参数值存在RAM里断电就丢。要持久化保存需要用到flash存储模块在参数系统基础上做配置存档恢复。4. 移植实施从GCC/CLion构建迁到Keil MDK的完整步骤4.1 环境准备MDK版本、设备支持包与源码依赖开始移植前先确认环境Keil MDK版本建议5.23以上因为MDK5的设备支持包DFP机制把CMSIS和芯片启动文件分离开较新版本在浮点支持上更省心。在PACK Installer里安装对应厂商的F4系列DFP。源码方面如果用git拉取原生仓库记得先更新子模块因为FreeRTOS和CMSIS都是独立仓库漏掉子模块会导致Keil里找不到FreeRTOS的port文件。我建议移植时不要把整个源码目录里的东西一股脑加进工程而是手工建立分组、按需添加文件。这样能清晰控制每个文件所属的编译单元出了链接错误也容易定位。Windows路径下的中文用户名也会引发奇怪的编译错误最好把工程放在纯英文路径下。4.2 FreeRTOS移植把GCC port换成ARMCC的ARM_CM4F portFreeRTOS在不同编译器下使用不同的port实现这是Keil移植中最关键的一步。原生crazyflie用的是portable/GCC/ARM_CM4F这个port里的portASM.s是GNU汇编语法ARMCC编译器不认——它需要ARMASM语法的汇编文件同时portmacro.h里的内联汇编也依赖GCC关键字。# 原生目录GCC编译器使用 FreeRTOS/Source/portable/GCC/ARM_CM4F/ port.c portmacro.h portASM.s # 需要切换到Keil对应目录ARMCC编译器使用 FreeRTOS/Source/portable/ARMCC/ARM_CM4F/ port.c portmacro.h portASM.s我在Keil工程里把portable分组下的文件从GCC目录切到ARMCC目录后编译直接从两百多个错误降为零。这个文件路径的切换是整个移植工程的胜负手。FreeRTOSConfig.h也要微调。原生配置里configTOTAL_HEAP_SIZE设得比较大因为GCC链接脚本给堆预留了足够空间。Keil的分散加载文件如果没同步放大RAM堆区FreeRTOS的pvPortMalloc会申请失败表现为任务创建后不运行系统卡死在断言里。一般configTOTAL_HEAP_SIZE设为24KB在F405上够用。4.3 启动文件、宏定义与分散加载文件ARMCC编译器下的工程适配Keil的工程选项C/C选项卡里需要配置Define宏这几个宏决定编译哪些代码分支、开启哪些硬件特性STM32F40XX # 选型宏使能F4系列外设库 USE_STDPERIPH_DRIVER # 使用标准外设库 ARM_MATH_CM4 # 启用CMSIS-DSP库的Cortex-M4优化路径 __FPU_PRESENT1 # 告诉编译器目标芯片带FPU __FPU_USED1 # 启用硬浮点涉及寄存器保存策略分散加载文件是ARMCC版链接脚本的替代品。原生GCC用的是.ld链接脚本Keil用.sct文件描述ROM/RAM布局。在Options的Linker选项卡里取消勾选默认配置使用自定义sct文件。典型的F405布局是LR_IROM1起始0x08000000长度1MB或按实际Flash容量调整RW_IRAM1起始0x20000000长度128KB如果启用了CCM RAM另加一段起始0x10000000长度64KB的段。; 自定义 scatter file适配 STM32F405 LR_IROM1 0x08000000 0x00100000 { ER_IROM1 0x08000000 0x00100000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } }这里有个容易被忽略的坑C库重定向。GCC工具链默认用newlib它的printf实现和微库不同。Keil里裸机运行建议勾选Use MicroLIB它占用的RAM/ROM更小串口重定向也更简单把fputc函数重写一下就能用printf输出调试信息。4.4 编译、烧录与调试J-Link下载与RTOS任务可视化工程配置完成后编译优化级别推荐选-O1或-O2。不要用-O0FreeRTOS在-O0下调度延时明显增大控制环路的时序会出问题。烧录前在Flash Download选项卡里勾选Reset and Run复位后直接运行。调试层面Keil对FreeRTOS有专门的RTOS窗口但需要安装调试组件。用J-Link连接后在Debug菜单里打开RTOS任务窗口可以看到每个任务的运行状态、堆栈余量。这个功能对定位“某个任务卡死导致整机失控”特别有用。如果烧录后程序跳进HardFault_Handler先在Fault Report窗口看异常类型再检查FPU是否在启动代码里使能。5. 避坑记录crazyflie工程在Keil下编译与运行的常见问题排查5.1 编译报错“cannot open source file core_cm4.h”现象工程里所有使用CMSIS头文件的源文件都报找不到core_cm4.h。原因Keil工程没有把CMSIS的Include路径添加进来。原生GCC工程通过Makefile的-I参数指定了路径Keil需要在Options的C/C选项卡Include Paths里手动添加。如果装了DFPCMSIS库通常在Keil安装目录下的ARM/PACK/CMSIS路径里。解决在Include Paths里添加CMSIS核心头文件目录同时确认stm32f4xx.h所在目录也在路径里。添加后重新编译即可。5.2 链接报L6220E上百个未定义符号现象链接阶段报大量未定义符号集中在vTaskStartScheduler、xPortStartScheduler这类FreeRTOS调度符号上。原因FreeRTOS的port文件选错编译器版本。Keil工程里错误引入了portable/GCC/ARM_CM4F下的portASM.sGNU汇编语法ARMCC不识别汇编文件被静默排除导致调度器入口函数缺失。解决将portable目录下的文件整体替换为portable/ARMCC/ARM_CM4F。注意port.c也要替换GCC版本的port.c里用了__attribute__和GCC内联汇编ARMCC编译会报语法错误或产生错误符号。5.3 烧录后直接跑进HardFault_Handler现象程序能烧进去但一运行就进入硬件错误中断Fault报错指向一个内存访问异常。原因最常见的是FPU未使能。Cortex-M4F的FPU默认不开启如果Keil工程主要特性里选了FPU: Single Precision但启动文件里没有执行CP10、CP11协处理器使能任何浮点指令都会触发HardFault。另一常见原因是定时器中断优先级配置超过FreeRTOS要求的范围导致在中断里调用了FreeRTOS API。解决在SystemInit()里加入FPU使能代码void SystemInit(void) { // 使能FPU协处理器 SCB-CPACR | ((3UL 10*2) | (3UL 11*2)); /* 其余初始化代码 */ }同时在FreeRTOSConfig.h里检查configMAX_SYSCALL_INTERRUPT_PRIORITY的值确保外设中断优先级数值不小于该值。5.4 任务调度不运行系统卡死在汇编死循环现象工程编译通过、烧录成功但LED不闪在调试器里暂停发现PC停在Default_Handler或某条B .死循环指令。原因FreeRTOS依赖SysTick产生系统节拍如果工程里没有把SysTick_Handler映射到xPortSysTickHandler调度器永远等不到时间片。原版GCC工程通过链接脚本的--wrap或宏映射处理Keil里需要用中断向量表或者FreeRTOSConfig.h里配置。解决在中断处理文件里实现SysTick_Handler转发void SysTick_Handler(void) { if (xTaskGetSchedulerState() ! taskSCHEDULER_NOT_STARTED) { xPortSysTickHandler(); } }注意xTaskGetSchedulerState()在调度器启动前也会被调用需要先做状态判断否则空指针会直接崩溃。5.5 串口printf输出乱码现象用printf打印调试信息PC串口助手显示内容全是乱码但程序运行正常。原因串口波特率与实际RCC配置不匹配。crazyflie主频是168MHz原版代码里外设总线分频系数和Keil工程默认配置不一样。usart.c里初始化的波特率寄存器是基于某个时钟频率计算的如果系统时钟频率没改到位实际波特率就偏离了配置值。解决用示波器或逻辑分析仪测一下串口TX引脚的实际波特率反向推断系统时钟。或者直接在调试器里读RCC-CFGR寄存器确认时钟树配置。一般情况下把SystemCoreClock设成168000000然后重新计算USART分频即可。6. 二次开发拿到Keil工程后从哪里下手改第一行代码6.1 用参数系统改PID找到注册表并修改对应项飞控到手第一件事把PID调稳。在stabilizer.c的初始化代码里找到PARAM_ADD段里面注册了PID_ROLL_RATE.kp、PID_ROLL_RATE.ki、PID_ROLL_RATE.kd等参数。先不要直接改数值打开Crazyflie客户端连接无线用参数面板实时调整。观察姿态曲线是否超调、是否振荡再逐步收敛。// stabilizer.c 参数注册示例 PARAM_ADD(PARAM_FLOAT, pid_roll_rate_kp, pid_rate_roll.kp); PARAM_ADD(PARAM_FLOAT, pid_roll_rate_ki, pid_rate_roll.ki); PARAM_ADD(PARAM_FLOAT, pid_roll_rate_kd, pid_rate_roll.kd);通过无线在线改参确认最优值后再把数值固化到代码里的默认值重新编译烧录。这样调出来的参数比盲改代码高效得多。6.2 自己加一个日志变量把实时姿态数据拉出来分析调试飞控时总有几个变量想观察比如电机PWM原始值、气压计原始高度、电池电压。用日志系统加变量很简单在任意模块里定义变量用LOG_ADD宏注册上位机就能按需订阅。// 在自定义模块里注册一个日志变量 static float custom_battery_voltage; void custom_log_init(void) { logVarId_t id logGetCreateId(custom, batt_voltage); logAddFloat(id, custom_battery_voltage); }日志频率不要设太高1Hz~10Hz足够看趋势。频率太高会占满无线带宽反过来影响控制指令的实时性。6.3 加一个自定义任务比如自定义LED灯效或外部传感器的融合FreeRTOS加任务的模式很固定创建任务时指定栈大小和优先级。控制链路任务的优先级最高对应configMAX_PRIORITIES-1其余任务优先级递减。加LED灯效任务时优先级放最低保证不影响姿态控制。// 自定义任务LED 呼吸灯 void led_effect_task(void *param) { while (1) { for (int i 0; i 100; i) { led_set_brightness(i); vTaskDelay(M2T(20)); } } } // 在应用初始化的地方创建任务 xTaskCreate(led_effect_task, LED, configMINIMAL_STACK_SIZE 64, NULL, 1, NULL);这类的改造可以从一个最不起眼的小任务入手验证FreeRTOS的任务创建、延时节拍、优先级抢占全链路通畅再往里填自己的逻辑。说起来我自己每次拿到移植后的工程都会强制走一遍“编译 → 烧录 → 看任务调度 → 看姿态曲线”的流程确认飞控算法真的在裸机环境下跑起来了才敢开始改业务代码。移植类的工程最怕的是“看起来能编译跑起来就失控”所以烧录后先用无线日志拉一遍姿态数据和电机PWM数据确认传感器方向没反、PID输出方向正确、电机响应跟得上指令再考虑二次开发。希望这套排查思路能帮你少走点弯路。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑