简介:这是一份面向嵌入式与无人机开发学习者的C语言开源项目源代码包,完整覆盖无人机硬件控制、传感器数据采集、飞行控制算法实现等核心环节,代码结构清晰、注释丰富,适合具备一定单片机基础、希望深入理解无人机系统设计及PID控制算法的开发者参考学习。压缩包共1740个文件,约19.23MB,以h/c源文件、launch工程配置、cproject/project工程文件、ld链接脚本及hex固件等为主,同时附带html帮助文档、pdf说明文件和png示意图,便于按模块阅读源码、快速定位核心代码及搭建调试环境。目前已有870人学习下载。项目通过微控制器驱动电机、GPS与遥控器模块,借助加速度计、陀螺仪、气压计采集姿态与位置信息,并基于PID算法实现飞行姿态、速度和高度的闭环控制;额外集成的图像处理与无线通信模块,可支撑图像传输、远程控制等扩展应用,整体设计兼顾教学与二次开发需求。资料目录结构清晰,文件类型覆盖从源码、工程配置到编译产物与说明文档的完整链路,具有较好的参考价值和实用性,无论是课程设计、毕业设计拓展,还是入门级飞控研究,都能从中获得可复用的工程参考。
1. C语言嵌入式无人机项目:源码结构、设计文档与学习路径
很多开发者拿到一套飞控源码,打开目录看到几十个.c和.h文件就直接劝退了。这个 C 语言嵌入式无人机开源项目,把飞控的完整链路——传感器采集、姿态解算、PID 控制、PWM 输出、无线通信——全部用 C 语言实现,不依赖复杂操作系统,裸机跑在常见 ARM 单片机上。它附带了详细的设计说明文档,每个模块怎么工作、为什么这样设计都有交代,正好适合两类人:一是想搞懂多旋翼飞控原理的嵌入式学习者,二是有一定基础、想基于开源飞控做二次开发的从业者。这次我把整个仓库拆了一遍,从源码布局、核心控制算法到编译烧录和常见坑,逐个讲清楚。
2. 读懂飞控源码:从main()开始的模块化拆解
2.1 源码目录结构与代码规模
项目的顶层目录划分很标准,一眼就能看出设计者的工程习惯。核心代码集中在Src、Inc和Drivers三个目录,Src放主逻辑,Inc放头文件,Drivers放外设驱动。另外还有Docs目录专门存放设计文档,包含系统架构图、引脚分配表、PID 调参记录表,这些资料对二次开发特别有用,省去了自己反推硬件连接和时序的功夫。
代码量上,核心模块大约 3000 行,不是那种动辄几万行的大型开源飞控,读起来压力小很多。主控采用的是常见的 STM32F103 系列,时钟配置、GPIO 初始化、定时器配置全部通过标准外设库完成,没有用更复杂的 HAL 库,代码可读性更强,也更容易跟踪调用关系。整个工程的模块划分如下表:
| 目录 | 主要内容 | 职责 |
|---|---|---|
Src/main.c | 主循环、任务调度 | 完成传感器读取、姿态解算、控制输出 |
Src/MPU6050.c | 陀螺仪+加速度计驱动 | 通过 I2C 读取原始数据,完成零偏校准 |
Src/BMP280.c | 气压计驱动 | 通过 I2C 读取气压与温度,计算高度 |
Src/pid.c | PID 控制器 | 实现姿态角内环与角速度外环控制 |
Src/motor.c | 电机控制映射 | 将控制量映射为四路 PWM 输出 |
Src/uart.c | 串口通信 | 与上位机通信,发送遥测数据 |
Drivers/ | 标准外设库 | GPIO、定时器、I2C、USART 等底层驱动 |
2.2 传感器驱动层:MPU6050和BMP280的底层实现
传感器是飞控的「眼睛」,数据准不准直接影响控制效果。MPU6050 的驱动实现采用的是软件 I2C 而非硬件 I2C,这样做的好处是引脚任意、移植方便,代价是占用 CPU 时间。初始化函数先做复位,然后配置量程——加速度计设成 ±2g,陀螺仪设成 ±500°/s,这个配置在四轴这种机动性场景够用,还兼顾了分辨率。
void MPU6050_Init(void) { I2C_WriteByte(MPU6050_ADDR, PWR_MGMT_1, 0x00); // 退出睡眠模式 I2C_WriteByte(MPU6050_ADDR, SMPLRT_DIV, 0x07); // 采样频率 1kHz/(1+7) = 125Hz I2C_WriteByte(MPU6050_ADDR, CONFIG, 0x06); // 低通滤波截止频率约 5Hz I2C_WriteByte(MPU6050_ADDR, ACCEL_CONFIG, 0x00); // 加速度量程 ±2g I2C_WriteByte(MPU6050_ADDR, GYRO_CONFIG, 0x08); // 陀螺仪量程 ±500°/s }这里有个参数需要特别注意:SMPLRT_DIV设置为 7,配合内部 1kHz 采样率,实际输出频率是 125Hz。而低通滤波配置CONFIG设为 0x06 对应约 5Hz 截止频率,主要滤掉机体高频振动。实际调试时如果发现数据过于平滑、响应迟钝,优先把截止频率往上调。读取函数在MPU6050_Read里通过memcpy一次性取出 6 轴原始数据,再依据量程换算成物理量——陀螺仪用500/32768的灵敏度系数,加速度计则用2/32768。
2.3 姿态解算与PID控制:核心代码逐行分析
姿态解算用的是互补滤波,没有上卡尔曼这种复杂算法。互补滤波的原理很直接:加速度计在静态时能给出准确的重力方向,但动态响应慢;陀螺仪积分短时间很准,但长期会漂移。把两者以合适的比例融合,就得到了既平稳又跟手的姿态角。
void Attitude_Update(float gx, float gy, float gz, float ax, float ay, float az, float dt) { float roll_est, pitch_est, roll_meas, pitch_meas; // 陀螺仪积分得到当前角度 roll_est = roll + gx * dt; pitch_est = pitch + gy * dt; // 由加速度计计算静态角度 roll_meas = atan2f(ay, az) * 57.3f; pitch_meas = atan2f(-ax, sqrtf(ay * ay + az * az)) * 57.3f; // 互补滤波融合 roll = 0.95f * roll_est + 0.05f * roll_meas; pitch = 0.95f * pitch_est + 0.05f * pitch_meas; }滤波系数0.95/0.05是关键参数。0.95 分配给陀螺仪的积分结果,保证动态响应快;0.05 留给加速度计,纠正长时间漂移。如果电机震动大,可以把加速度计权重降到 0.02,但要注意姿态角会变得不够「硬」,回中感变弱。紧接着 PID 控制器拿到目标姿态角和当前姿态角,先算外环角度误差,输出作为内环角速度的设定值,再经过一次比例和微分运算,最终输出电机控制量。这里内环用 D 项抑制角速度突变,对飞行稳定性至关重要。
3. 搭建编译与烧录环境:从零跑通工程
3.1 工具链选型:Keil与GCC两种路径
源码工程默认配套的 IDE 是 Keil MDK,打开Project.uvprojx就能直接编译。Keil 的优点是开箱即用,调试器集成度高,对新手友善。但如果你的电脑上没装 Keil,或者所在开发环境不便于安装破解,可以换成 GCC 工具链配合 Makefile 来编译,仓库里附带了一份Makefile示例。
CC = arm-none-eabi-gcc OBJCOPY = arm-none-eabi-objcopy CFLAGS = -mcpu=cortex-m3 -mthumb -DSTM32F10X_MD \ -IInc -IDrivers/Inc -ISrc LDFLAGS = -T STM32F103C8T6_FLASH.ld --specs=nano.specs OBJS = Src/main.o Src/MPU6050.o Src/BMP280.o Src/pid.o Src/motor.o Src/uart.o all: firmware.hex %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ firmware.hex: $(OBJS) $(CC) $(LDFLAGS) $^ -o firmware.elf $(OBJCOPY) -O ihex firmware.elf firmware.hex编译参数里,-mcpu=cortex-m3 -mthumb指定了内核架构为 Cortex-M3 且使用 Thumb 指令集,这必须和芯片匹配——F103 就是 M3 核,如果你打算移植到 F103 之外的其他型号,一定要先查芯片内核再改这个参数。-DSTM32F10X_MD这个宏定义会告诉标准库当前芯片是中等密度型号,它影响 flash 和 RAM 的地址分配,别删,否则外设注册会报错。整个工程编译下来大约 30 秒左右就能出.hex文件,用 ST-Link 配合st-flash或STM32_Programmer即可烧录。
3.2 烧录配置与串口日志
烧录前要检查 BOOT0 引脚的跳线帽。BOOT0 接地是正常运行模式,从主 flash 启动;BOOT0 接高电平是系统存储器模式,方便串口 ISP 下载。平时保持 BOOT0 为低,用 ST-Link 的 SWD 接口下载最方便,只需要 SWDIO、SWCLK、GND 三根线。我是习惯先用烧录器刷进去再上电看现象,避免一边供电一边下载导致电压不稳。
# 使用 st-flash 工具烧录 firmware.hex st-flash --connect-under-reset write firmware.hex 0x08000000烧录完成后接上串口,波特率 115200,8N1,就能看到飞控的启动日志和遥测数据。日志格式在uart.c里定义,包括姿态角、电机油门值和传感器原始值。这些数据是调参时判断问题的重要依据,比如解算出来的 roll 角是否和实际倾斜方向一致、油门输出会不会突然跳变,这些在日志里都能直接观察到。
3.3 参数调整:PID与传感器校准
飞控上电前要先做传感器校准,否则陀螺仪零偏误差累积几秒就会让姿态角飞速漂移。具体做法是:把无人机平放在桌面上,保持静止 5 秒以上,让 MPU6050 完成零偏采样。源码里MPU6050_Calibrate函数会采集 200 次数据求平均值,把结果存到全局变量gyro_offset中。校准后如果静止状态下姿态角变化率小于每秒 0.5°,说明零偏处理到位了。
PID 参数在pid.h中通过宏定义给出初始值,包括内环角速度 P、I、D 三项和外环角度 P 项。F103 这种主频 72MHz 的芯片上,姿态解算 500Hz、控制环 250Hz 已经足够;频率再往上加 CPU 占用率高,收益却不大。调参时有个容易忽略的点:外环角度 P 的值很大,通常在 4 到 6 之间,内环角速度 P 则小得多,在 0.1 到 0.3 之间,两个环的输出方向相反,搞混了飞机会直接翻转。
4. 避坑指南:编译与调试阶段的五个典型问题
4.1 MPU6050数据跳变:I2C上拉电阻缺失
现象:飞控平放不动,上位机里看到 roll 角突然跳变到几十度再弹回来,伴随陀螺仪数据异常。
原因:软件 I2C 的 SCL 和 SDA 引脚没有配置内部上拉,或者外部上拉电阻没焊,导致总线上高电平驱动能力不足。数据在传输过程中发生位错误,解算出来的角度自然就飞了。
解决:在 I2C 初始化时把 GPIO 配置为开漏输出并开启内部上拉,或者干脆焊两颗 4.7kΩ 外部上拉电阻。我在一次焊接时为了省事没接上拉,排查了一整天才发现是硬件问题。如果你是拿来学习,直接改代码开内部上拉最快。
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7); // 拉高,使能上拉4.2 PWM输出无信号:定时器映射配置错位
现象:四个电机完全没反应,用示波器查 PWM 引脚看不到波形,但代码逻辑看起来没问题。
原因:定时器的通道映射没有正确配置。比如 TIM2 的 PWM 输出可以映射到 PA0~PA3,也可以重映射到 PA15、PB3 等引脚。飞控源码默认用的是 PA0~PA3,但你的飞控板如果 PCB 走线把电机接到了其他引脚,就会出现这种情况。
解决:打开motor.c看定时器初始化里的GPIO_PinAFConfig或GPIO_RemapConfig配置。如果板子是自定义的,先查原理图确认电机接口接到哪个引脚,再核对定时器通道映射表。一个常见的习惯做法是:先用 GPIO 翻转测试确认引脚本身没问题,再用定时器输出,把问题分开定位。
4.3 串口输出乱码:晶振频率与波特率不匹配
现象:串口助手收到的是乱码,不是可读的文本日志。
原因:飞控板上实际焊接的晶振是 8MHz,但工程配置里把HSE_VALUE定义成了 12MHz,或者反过来。波特率计算基于是错的时钟频率,串口收发自然对不上。
解决:检查system_stm32f10x.c或stm32f10x.h中HSE_VALUE宏定义,改成和板子实际晶振一致的值。确认后再用逻辑分析仪或者示波器看 TX 引脚的波形时间,验证波特率实际是 115200 还是别的值。这个方法比盲调管用得多。
4.4 电池电压读取不准:分压电阻与ADC参考电压
现象:电量监测显示满电电压只有 7V,而万用表实测是 11.1V。
原因:电池电压经过分压电阻接入 ADC 引脚,但分压比例计算错误,或者 ADC 的参考电压选择不对。F103 的 ADC 默认参考电压是 VDD(3.3V),如果电池电压 12V 直接接入,不仅读数不准,还会烧掉引脚。
解决:先用万用表测量分压点电压,确认在 ADC 输入范围内。然后按实际分压电阻值反推比例系数,修改Battery_Read函数里的换算公式。我一般会给 ADC 通道配置一个软件滤波器,连续采 10 次去掉最大最小值再平均,读数稳很多。
4.5 电机油门输出不一致:电机行程校准未做
现象:解锁推油门时,四个电机转速明显不一致,其中一个明显偏慢或快。
原因:电调没有执行行程校准,PWM 信号的最小值和最大值与电调的响应范围不匹配。不同批次的电调个体差异会导致同样的 PWM 占空比对应不同转速。
解决:拔掉螺旋桨,给飞控上电,把油门打到最大,再给电调上电,听到「滴滴」提示音后把油门拉到最小,完成行程标定。这个过程要选个靠谱的步骤顺序——先卸桨,再校行程,顺序反了有安全隐患。项目文档里也写了完整的校准流程,照着走一遍就能解决。
5. 进阶:从看懂到改飞控的自定义扩展
5.1 添加GPS模块:串口中断接收与解析
飞控默认只靠 MPU6050 和 BMP280 实现姿态与高度感知,没有定位能力。如果想加 GPS 模块做定点模式,路径已经很清晰了。常见的 GPS 模块输出 NMEA 协议数据,通过串口和飞控连接。飞控这边需要开一个串口中断接收,把数据放到环形缓冲区,然后主循环解析$GPGGA帧提取经纬度和定位状态。
void USART1_IRQHandler(void) { uint8_t byte; if (USART_GetITStatus(USART1, USART_IT_RXNE)) { byte = USART_ReceiveData(USART1); if (ringbuffer_full(&buf)) { // 这里要处理缓冲区满的情况,我通常是直接把最老的数据丢弃 ringbuffer_remove(&buf); } ringbuffer_append(&buf, byte); } }串口中断接收的关键在于环形缓冲区的容量设置,我习惯至少设 256 字节。NMEA 语句最长的$GPGGA也就 80 个字符左右,256 字节足够容纳多条语句排队处理。主循环里每次从缓冲区取一个字符,按照$开头、,分隔的规则解析,做完校验求和就更新一次定位数据。注意解析频率不要太高,1Hz 的刷新率对定点模式已经够了,处理快了也只是增加 CPU 负载。
5.2 修改控制律:从单级PID改成串级结构
源码默认的姿态控制在四轴上已经能稳定飞行,但想要更好的抗风性和手感,可以改成串级 PID。外环控制角度,内环控制角速度,两个环都各自有比例和微分项。
// 外环:角度误差 -> 期望角速度 float roll_angle_err = target_roll - roll; float target_roll_rate = Kp_angle * roll_angle_err; // 内环:角速度误差 -> 输出修正量 float roll_rate_err = target_roll_rate - gyro_x; float roll_output = Kp_rate * roll_rate_err + Kd_rate * (roll_rate_err - last_rate_err) / dt;这段代码里最值得琢磨的是Kp_angle和Kp_rate的数值关系——它们的乘积太大,飞控会高频震荡,电机发出滋滋声;太小则飞机软绵绵的,受点风就晃。我从这个项目里学到的经验是:先单独调内环,让飞控在锁定角速度时不振荡;再加外环,逐步加大Kp_angle,直到用手轻推机身能感到明显的回中力但又不会来回抖。串级结构改动并不大,但调参的维度从两个变成了三个,难度上了一个台阶,好处是飞行品质上限明显更高。
5.3 编写自定义通信协议:把飞控数据发到地面站
源码自带的 UART 输出格式比较简单,如果你想在电脑端做可视化监控,建议自定义一个数据帧协议。一个实用做法是采用类似 MAVLink 的思路:帧头0xAA 0x55、长度字节、数据类型字节、有效负载、校验字节。
void Send_Telemetry(uint8_t msg_type, int16_t roll, int16_t pitch, int16_t yaw, uint16_t throttle) { uint8_t frame[10]; frame[0] = 0xAA; // 帧头1 frame[1] = 0x55; // 帧头2 frame[2] = 6; // 负载长度 frame[3] = msg_type; // 数据类型 frame[4] = (uint8_t)(roll >> 8); frame[5] = (uint8_t)(roll & 0xFF); frame[6] = (uint8_t)(pitch >> 8); frame[7] = (uint8_t)(pitch & 0xFF); frame[8] = (uint8_t)(throttle >> 8); frame[9] = (uint8_t)(throttle & 0xFF); // 发送之前补一个校验字节,我这里用简单的累加取低8位,够用了 uint8_t sum = 0; for (int i = 0; i < 9; i++) sum += frame[i]; UART_SendBytes(frame, 9); UART_SendByte(sum); }习惯上我会在每个数据帧末尾追加一个校验字节,而不是依赖串口自身的波特率纠错。上位机收到一帧后先算累加和,和校验字节对比,不通过就丢掉这一帧。这样就算偶尔有丢包,地面站上看到的曲线也不会出现毛刺跳变。这个扩展技巧不仅用于飞控,在后续做地面站、做无线数传时也能复用到同一套思路。
5.4 从仿真环境到真机验证的切换细节
光在电脑上编译通过不代表飞控就能上天。实际验证有个环节特别容易忽略:从仿真器切换到真实传感器数据时,传感器方向的定义可能不对。MPU6050 的安装方向如果和代码里的轴定义不一致,飞控表现出来的行为会特别诡异——推油门往一边倒、打副翼反而俯仰。这个项目文档里给了一张传感器安装方向与代码坐标系对齐的对照表,改硬件或者换机架之后务必重新核对一次。
我从第一次调通到第二次再飞,中间隔了两个多月,再打开工程时完全不记得电机方向和陀螺仪方向的对应关系,全靠文档里的这张表救回来。从那以后我每次拿到新飞控板都会强制走一遍流程:先核传感器安装方向、再核电机转向、最后解锁小油门逐项验证,顺序一步不乱。希望这份拆解能帮你少走几步弯路,顺利用上手这套 C 语言嵌入式无人机项目。
本文还有配套的精品资源,点击获取