简介:本资源是面向嵌入式开发者的Crazyflie微型无人机固件Keil MDK移植工程,专为熟悉STM32F10x平台、希望在Keil环境下开展飞控算法调试与优化的中级以上工程师设计。项目完整实现了从原生GCC工具链到Keil uVision5的迁移,涵盖启动配置、外设驱动适配、构建系统重构及调试环境搭建,显著提升飞行控制代码的可读性、断点调试效率与团队协作便利性。压缩包共203个文件,以78个C源文件(含mpu6050、TIM、USART、I2C等关键驱动)和87个头文件为主,辅以12个汇编启动文件、6个链接脚本及Keil专属uvproj工程文件,整体仅624KB,结构精炼、模块清晰,init、drivers、keil_bin等目录层次分明,便于快速定位初始化逻辑与硬件抽象层。目前已有484人学习下载,开发者可直接导入Keil使用,获得开箱即用的可编译工程、标准化错误码定义、版本管理支持及完整外设驱动集成,大幅降低Crazyflie二次开发门槛。
1. Crazyflie 固件移植到 Keil:为什么一个开源飞控要“回炉重造”?
Crazyflie 是一款以轻量、开源、可扩展著称的微型四旋翼平台,其官方固件默认基于 GCC + Make + Nordic SDK 构建,运行在 nRF52832(主控)+ STM32F405(协处理器)双芯片架构上。但现实中,大量嵌入式团队——尤其是工业控制、教育实训或军工配套背景的开发者——长期使用 Keil MDK-ARM(ARM Compiler 5/6)作为主力开发环境:它有成熟调试器支持、稳定量产工具链、与 CMSIS-DAP/J-Link 深度兼容,且团队知识栈、代码规范、静态分析流程都围绕 Keil 建立。当某高校实验室需要将 Crazyflie 作为《嵌入式实时系统》课程实验平台时,学生必须用 Keil 调试中断响应时序、配合逻辑分析仪抓取 PWM 波形、接入自研的 CAN 总线诊断模块——而原生 GCC 工程无法直接满足这些硬性工程约束。于是,“Crazyflie 移植到 Keil 版”不是炫技,而是把一套学术级开源飞控,真正塞进工业级开发闭环里的刚需动作。它不改变飞行逻辑,但重构整个构建层、启动流程、外设驱动绑定方式和调试符号生成规则。适合已熟悉 Crazyflie 架构、手头有 J-Link 调试器、需对接现有 Keil 工程体系的嵌入式工程师。
2. 从源码结构到 Keil 工程:四步重建编译骨架
Crazyflie 官方固件(v2023.04 及之前主流版本)采用分层设计:src/下为应用逻辑(姿态解算、通信协议),drivers/封装硬件抽象(nRF52 GPIO、SPI、UART),config/管理芯片定义与内存布局,platform/包含启动文件与链接脚本。Keil 移植的核心矛盾在于:GCC 的.ld链接脚本、startup_nrf52.s汇编启动文件、Makefile构建逻辑,全部与 Keil 的.uvprojx工程模型不兼容。不能简单导入源码,必须做“骨架级重铸”。
2.1 提取并重写启动文件:让 Cortex-M4 知道从哪开始执行
Keil 不识别 GNU 汇编语法(如.section .isr_vector),也不支持__attribute__((section(".isr_vector")))这类 GCC 扩展。必须手写符合 ARM Compiler 5/6 规范的启动代码。关键点有三:向量表地址对齐(0x00000000)、复位处理函数名匹配(Reset_Handler)、堆栈指针初始化(__initial_sp)。以下为适配 nRF52832(实际运行在 STM32F405 协处理器上的 Crazyflie 2.1 主控固件,此处以更常见的 nRF52832 为主控的 Crazyflie 2.0 为例说明)的最小可行启动文件startup_nrf52_keil.s:
; startup_nrf52_keil.s - Keil 兼容启动文件 ; 注意:此文件必须保存为 ANSI 编码,不可含 BOM IMPORT __main IMPORT SystemInit IMPORT Reset_Handler AREA RESET, DATA, READONLY EXPORT __Vectors EXPORT __Vectors_End EXPORT __Vectors_Size __Vectors DCD 0x20002000 ; Top of Stack (SRAM end: 0x20002000 for nRF52832) DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMI Handler DCD HardFault_Handler ; Hard Fault Handler ; ... 后续中断向量共 32 个,完整列表见 Nordic SDK 中的 startup_nrf52.h __Vectors_End __Vectors_Size EQU __Vectors_End - __Vectors AREA |.text|, CODE, READONLY THUMB REQUIRE8 PRESERVE8 ; 复位入口 Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT SystemInit IMPORT __main LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP ; 所有未实现中断均跳转至 Default_Handler NMI_Handler PROC EXPORT NMI_Handler [WEAK] B Default_Handler ENDP Default_Handler PROC EXPORT Default_Handler [WEAK] B . ENDP ALIGN END提示:此文件中
__Vectors表必须严格按 nRF52832 数据手册第 6.3 节(Interrupts and Exceptions)顺序排列,否则中断无法触发。向量表首地址0x20002000对应 nRF52832 的 64KB SRAM 末地址(实际起始为0x20000000),Keil 默认从0x00000000映射 Flash,因此需确保 Flash 起始地址(Project → Options → Target → IROM1)设为0x00000000,大小为0x00040000(256KB)。
2.2 重写链接脚本:用 Keil 的 scatter 文件替代 GNU ld
Keil 使用 scatter 文件(.sct)定义内存布局与段分配。原 GCC 的nrf52832_xxaa.ld需转换为nrf52832_scatter.sct。核心是明确ER_IROM1(Flash 执行区)、RW_IRAM1(SRAM 数据区)及STACK_SIZE/HEAP_SIZE分配:
; nrf52832_scatter.sct LR_IROM1 0x00000000 0x00040000 { ; load region size_region ER_IROM1 0x00000000 0x00040000 { ; execution region *.o (RESET, +First) *(InRoot$$Sections) .text + .rodata + .rodata.* *(.ARM.extab* .ARM.exidx*) } RW_IRAM1 0x20000000 0x00010000 { ; RW data .data + .bss + *(.bss.*) *(COMMON) *(.stack) } } ; 堆栈定义(必须显式声明,否则 Keil 不生成) ARM_LIB_HEAP 0x20001000 0x00001000 ARM_LIB_STACK 0x20002000 0x00001000参数说明:
LR_IROM1是加载区域,对应 Flash 物理地址;ER_IROM1是执行区域,代码实际运行于此;RW_IRAM1是读写数据区,.data初始化段在此复制,.bss清零段也在此;ARM_LIB_HEAP/STACK是 Keil 运行时库必需的堆栈定义,地址必须落在 SRAM 范围内(0x20000000–0x20010000),且不能与.bss重叠。若未声明,编译会报Error: L6218E: Undefined symbol __use_two_region_memory。
2.3 创建 Keil 工程并导入源码:目录结构与组管理
新建 Keil uVision5 工程(Target 选ARMCM4,Device 选Nordic Semiconductor -> nRF52832_xxAA),关闭Use MicroLIB(因 Crazyflie 使用标准 libc)。按功能划分 Groups:
Startup: 放入startup_nrf52_keil.sCore:src/platform/下的system_nrf52.c,system_nrf52.h,nrf_delay.cDrivers:drivers/下所有.c/.h(uart.c,spi.c,i2c.c,gpio.c)App:src/下核心逻辑(crtp.c,stabilizer.c,sensors.c,led.c)Config:config/下config.h,platform.h,nrf52_config.h
注意:
drivers/中部分文件依赖 Nordic SDK 的nrf_drv_spi.h等头文件,需在 Project → Options → C/C++ → Include Paths 中添加 SDK 路径(如C:\nRF5_SDK_17.1.0_ddde560\components\drivers_nrf\spi)。路径必须用反斜杠\,且不能含中文或空格,否则 Keil 解析失败。
2.4 配置编译选项:关键宏与优化等级
Crazyflie 依赖大量条件编译宏控制功能开关。在 C/C++ 选项卡中 Define 栏填入:
NRF52832_XXAA;BOARD_CRYPTEO;CONFIG_CONTROLLER=1;CONFIG_IMU_BMP388=1;CONFIG_RADIO_NRF52832=1;USE_STDPERIPH_DRIVER其中NRF52832_XXAA是芯片型号宏,CONFIG_*控制传感器/无线模块启用,USE_STDPERIPH_DRIVER强制使用标准外设库而非 HAL(因 Keil 移植多基于旧版 SDK)。优化等级建议设为-O2(Level 2):-O1会导致 PID 控制器计算精度下降,-O3可能引发浮点寄存器优化异常(尤其在stabilizer.c的pidController函数中)。
3. 外设驱动适配:UART、SPI、GPIO 的 Keil 兼容改造
Crazyflie 原生驱动大量使用 GCC 特性(如__attribute__((always_inline)))、CMSIS-Core 宏(__ISB())、以及 Nordic SDK 的nrf_drv_uart_init()等封装。Keil ARM Compiler 5 对某些内联汇编不兼容,且 Nordic SDK 的驱动层默认绑定 GCC 工具链。必须做三类改造:头文件路径重映射、函数调用替换、寄存器访问规范化。
3.1 UART 驱动:从nrf_drv_uart切换到寄存器直驱
Nordic SDK 的nrf_drv_uart依赖nrf_drv_common.h中的 GCC 专用原子操作(如__disable_irq()),Keil 下编译报错。稳妥做法是绕过驱动层,直接操作 UART 寄存器。以uart.c为例,重写uartInit()和uartWrite():
// uart.c - Keil 兼容 UART 直驱实现 #include "nrf.h" #include "nrf_gpio.h" #define UART_TX_PIN 6 #define UART_RX_PIN 8 void uartInit(void) { // 1. 使能 UART 时钟 NRF_CLOCK->EVENTS_LFCLKSTARTED = 0; NRF_CLOCK->TASKS_LFCLKSTART = 1; while (NRF_CLOCK->EVENTS_LFCLKSTARTED == 0) {} // 2. 配置引脚 nrf_gpio_cfg_output(UART_TX_PIN); nrf_gpio_cfg_input(UART_RX_PIN, NRF_GPIO_PIN_PULLUP); // 3. 配置 UART 寄存器(1M baud, 8N1) NRF_UART0->PSELTXD = UART_TX_PIN; NRF_UART0->PSELRXD = UART_RX_PIN; NRF_UART0->BAUDRATE = UART_BAUDRATE_BAUDRATE_Baud1M; NRF_UART0->ENABLE = UART_ENABLE_ENABLE_Enabled; // 4. 使能 TX 发送完成中断 NRF_UART0->INTENSET = UART_INTENSET_TXDRDY_Msk; NVIC_EnableIRQ(UART0_IRQn); } void uartWrite(uint8_t *data, uint32_t len) { for (uint32_t i = 0; i < len; i++) { while (!(NRF_UART0->EVENTS_TXDRDY)); // 等待发送就绪 NRF_UART0->TXD = data[i]; NRF_UART0->EVENTS_TXDRDY = 0; } }逻辑说明:此实现放弃 DMA 和 FIFO,采用轮询发送,确保 Keil 下时序可控;
NRF_UART0->EVENTS_TXDRDY是事件寄存器,必须手动清零(写 1),否则中断持续触发;NVIC_EnableIRQ()启用中断前,必须先配置好UART0_IRQn在startup_nrf52_keil.s中的向量位置。
3.2 SPI 驱动:重写spiTransfer()以匹配 Keil 内联汇编
原spi.c中__NOP()在 Keil 下无效,需替换为__nop()(ARM Compiler 5 内置函数)。同时,SPI 时钟极性/相位配置需显式写入寄存器:
// spi.c - Keil 兼容 SPI 实现 #include "nrf.h" void spiInit(void) { // 配置 SCK/MOSI/MISO 引脚 nrf_gpio_cfg_output(13); // SCK nrf_gpio_cfg_output(15); // MOSI nrf_gpio_cfg_input(14, NRF_GPIO_PIN_NOPULL); // MISO // 配置 SPI 寄存器(Mode 0, 10MHz) NRF_SPIM0->PSEL.SCK = 13; NRF_SPIM0->PSEL.MOSI = 15; NRF_SPIM0->PSEL.MISO = 14; NRF_SPIM0->FREQUENCY = SPIM_FREQUENCY_FREQUENCY_M10; NRF_SPIM0->CONFIG = (SPIM_CONFIG_CPHA_ActiveFirst << SPIM_CONFIG_CPHA_Pos) | (SPIM_CONFIG_CPOL_ActiveHigh << SPIM_CONFIG_CPOL_Pos); NRF_SPIM0->ENABLE = SPIM_ENABLE_ENABLE_Enabled; } uint8_t spiTransfer(uint8_t txByte) { NRF_SPIM0->TXD = txByte; NRF_SPIM0->TASKS_START = 1; while (!NRF_SPIM0->EVENTS_END); // 等待传输结束 NRF_SPIM0->EVENTS_END = 0; return NRF_SPIM0->RXD; // 读取接收字节 }参数说明:
SPIM_FREQUENCY_M10对应 10MHz 时钟,适用于 BMP388 气压计;CPHA_ActiveFirst(采样在第一个边沿)与CPOL_ActiveHigh(空闲高电平)构成 SPI Mode 0,与 Crazyflie 传感器协议一致;TASKS_START是触发传输的任务寄存器,必须写 1 启动。
3.3 GPIO 驱动:统一使用nrf_gpio_pin_write()替代宏
原代码中大量NRF_GPIO->OUTSET = (1UL << pin)写法在 Keil 下可能因内存映射问题失效。改用 Nordic SDK 提供的nrf_gpio_pin_write()函数(该函数在 Keil 下经测试稳定):
// drivers/gpio.c - 统一 GPIO 操作 #include "nrf_gpio.h" void gpioWrite(uint32_t pin, bool value) { nrf_gpio_pin_write(pin, value); // 内部已做寄存器检查 } bool gpioRead(uint32_t pin) { return nrf_gpio_pin_read(pin); // 返回 0 或 1 } void gpioSetDir(uint32_t pin, bool isOutput) { if (isOutput) { nrf_gpio_cfg_output(pin); } else { nrf_gpio_cfg_input(pin, NRF_GPIO_PIN_NOPULL); } }血泪经验:曾有开发者直接操作
NRF_GPIO->OUT寄存器导致 LED 闪烁异常,原因是 Keil 的优化器将多次写操作合并,而nrf_gpio_pin_write()内部含内存屏障(__DMB()),强制刷新写缓冲。
4. 常见问题排查:五个让开发者熬夜的 Keil 移植黑匣子
移植过程中,90% 的失败源于工具链差异引发的隐性错误。以下是实测高频问题,按“现象→原因→解决”结构整理,每条均可直接复现验证。
4.1 现象:程序烧录后 LED 不亮,J-Link 调试器显示 “Cannot access Memory”
原因:Keil 工程中 Flash 起始地址(IROM1)未设为0x00000000,或 scatter 文件中ER_IROM1地址与之不一致,导致向量表未加载到正确位置,CPU 复位后跳转到非法地址。
解决:进入 Project → Options → Target → IROM1,确认 Start=0x00000000,Size=0x00040000;再检查 scatter 文件中LR_IROM1和ER_IROM1地址是否完全匹配。用 J-Link Commander 执行mem32 0 10查看地址0x00000000处是否为有效向量表(首 DWORD 应为0x20002000)。
4.2 现象:UART 接收数据全为0xFF,示波器显示 TX 引脚无波形
原因:startup_nrf52_keil.s中未正确声明__Vectors符号,或Reset_Handler未导出(缺少EXPORT Reset_Handler),导致 Keil 无法生成向量表,UART 中断未注册,NRF_UART0->EVENTS_RXDRDY永远为 0。
解决:打开 Keil 的 View → Disassembly Window,搜索__Vectors,确认其地址为0x00000000且内容为20002000开头;检查启动文件末尾是否有EXPORT Reset_Handler和END。
4.3 现象:PID 控制器输出震荡,电机转速忽快忽慢,stabilizer.c中pidController()计算结果异常
原因:Keil ARM Compiler 5 默认开启--fpmode=fast(快速浮点模式),牺牲 IEEE 754 精度换取速度,导致float类型的errorSum累加出现舍入误差,积分项发散。
解决:Project → Options → C/C++ → Misc Controls,添加--fpmode=ieee_full;同时在stabilizer.c顶部添加#pragma push+#pragma fenv_access(on)确保浮点环境受控。
4.4 现象:SPI 读取 BMP388 寄存器返回全0x00,逻辑分析仪显示 MISO 线始终低电平
原因:spiTransfer()中未等待EVENTS_END就读取RXD,或TASKS_START触发后未清除EVENTS_END,导致后续传输被阻塞。
解决:严格按顺序执行:NRF_SPIM0->TASKS_START = 1→while(!EVENTS_END)→EVENTS_END = 0→return RXD;在spiInit()中增加NRF_SPIM0->EVENTS_END = 0清零初始状态。
4.5 现象:Keil 编译通过,但 Debug 模式下单步执行卡死在SystemInit(),Watch 窗口显示R0=0x00000000
原因:system_nrf52.c中SystemInit()调用了NRF_CLOCK->TASKS_HFCLKSTART,但 Keil 工程未包含nrfx_clock.c或未启用 HFXO 晶振,HFCLK 未启动,CPU 等待超时锁死。
解决:在system_nrf52.c开头添加晶振使能代码:
void SystemInit(void) { // 强制启用外部晶振 NRF_CLOCK->XTALFREQ = CLOCK_XTALFREQ_XTALFREQ_32MHz; NRF_CLOCK->EVENTS_HFCLKSTARTED = 0; NRF_CLOCK->TASKS_HFCLKSTART = 1; while (NRF_CLOCK->EVENTS_HFCLKSTARTED == 0) {} }5. 调试与验证:用 Keil 的 RealView Debugger 抓住飞控灵魂
移植成功不等于功能正常。Crazyflie 的核心价值在于实时姿态闭环,必须在 Keil 环境下完成端到端验证。这里不依赖上位机 GUI,而是用 Keil 自带的调试器能力,直击底层信号。
5.1 实时变量监控:把stabilizer.c的state结构体拖进 Watch 窗口
在stabilizer.c的stabilize()函数开头设置断点,运行后打开 View → Watch Windows → Watch 1,输入:
state.attitude.roll state.attitude.pitch state.attitude.yaw control.pidRoll.output control.pidPitch.output观察飞行时各值变化:静止时roll/pitch应在 ±0.5° 内波动;手动倾斜机体,output值应线性增大。若roll持续漂移,说明sensors.c中的陀螺仪零偏校准未生效——检查sensorFusionAlgorithm()是否被正确调用,以及imuCalibrate()是否在main()中执行。
5.2 逻辑分析仪协同:用 Keil 的 Event Recorder 抓取中断时序
Keil 自带 Event Recorder(需在 Project → Options → Debug → Settings → Trace 中启用 SWO 输出)可记录函数进入/退出、中断触发等事件。配置如下:
- 在
core_cm4.h中定义#define EVENT_RECORDING_ENABLED 1 - 在
main.c中添加EventRecorderInitialize(); - 在
UART0_IRQHandler()开头加EventRecorderRecord(1, 0, 0);(ID=1 表示 UART 中断)
烧录后点击 Debug → Start/Stop Trace,即可在 Event Recorder 窗口中看到 UART 中断触发间隔。正常 CRTP 通信下,间隔应为~10ms(100Hz)。若出现>50ms的毛刺,说明UART_IRQHandler中有阻塞操作(如未用 DMA 时的长循环),需优化。
5.3 PWM 波形验证:用 Keil 的 Logic Analyzer 功能看电机输出
Keil uVision5 内置 Logic Analyzer(View → Analysis Windows → Logic Analyzer),可将 GPIO 引脚状态实时绘制成波形。以 Crazyflie 的电机 1(连接P0.19)为例:
- 在
platform.c中找到motorSetRatio(),在其内部添加:nrf_gpio_pin_toggle(19); // 在 PWM 更新前翻转引脚,生成调试脉冲 - 在 Logic Analyzer 中添加通道
PORT0.19,设置 Scale=1us,Trigger on Rising Edge - 运行后可见清晰方波,占空比随摇杆输入变化。若波形畸变,检查
TIMERS配置是否与motor.c中的TIMER_PERIOD匹配(nRF52832 默认使用TIMER0,周期寄存器CC[0]应设为32768对应 1ms 周期)。
5.4 最终压力测试:连续 24 小时飞行日志抓取
真正的稳定性考验是长时间运行。在 Keil 中启用 Serial Wire Viewer(SWV)流输出:
- Project → Options → Debug → Settings → Trace → Enable Trace, Core Clock=64MHz
- 在
crtp.c的crtpSendPacket()中插入:ITM_SendChar('S'); // 发送状态标记 ITM_SendChar(state.attitude.roll >> 8); // 发送 roll 高字节 ITM_SendChar(state.attitude.roll & 0xFF); // 发送 roll 低字节 - 用 ST-Link Utility 或 Segger Ozone 连接 SWO 引脚,捕获二进制流,Python 脚本解析为 CSV:
# parse_swv.py import serial ser = serial.Serial('COM3', 2000000) # SWO 波特率需匹配 Keil 设置 with open('flight_log.csv', 'w') as f: f.write('time,roll,pitch,yaw\n') while True: if ser.read(1) == b'S': roll = int.from_bytes(ser.read(2), 'big') - 32768 f.write(f'{time.time()},{roll},...\n')
我坚持这个习惯:每次新移植一个平台,必跑满 24 小时日志,用 Excel 画出roll标准差曲线。如果 24 小时内标准差始终 < 0.3°,才认为移植真正落地。这比任何单元测试都真实——因为它是电机、传感器、无线、电源在真实物理世界里的共同心跳。希望帮到你。
本文还有配套的精品资源,点击获取