ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

baseflight飞控源码入门:从裸机主循环到混控输出的完整链路解析

baseflight飞控源码入门:从裸机主循环到混控输出的完整链路解析

简介:baseflight-master 是一套面向无人机飞控算法研究者的开源源码包,适合具备一定嵌入式与C语言基础的开发者学习或定制飞控特性。包内代码清晰展示飞控系统的核心运行逻辑:硬件初始化、传感器数据融合、PID姿态控制、命令解析、故障检测与低功耗管理等,通过跟踪源码可理解无人机如何利用陀螺仪、加速度计、磁力计等传感器计算姿态并驱动电机。资源压缩包共一百七十七个文件,以七十二个 C 源文件与六十个头文件为主,辅以三十四个汇编文件、HEX 固件、makefile、Keil 工程文件与链接脚本等,整体约六百七十一KB,便于在 Keil 环境中直接打开工程进行编译和调试。目前已有 301 人学习下载。学习该源码能获得飞控底层实现的全貌,也能基于代码修改 PID 参数或扩展特殊飞行模式,是研究嵌入式控制与传感器融合的一份实用素材。

1. 飞控源码入门选它:baseflight 是读懂整条控制链路的最短路径

想搞明白一台多旋翼飞控内部到底干了什么,最忌讳一上来就啃带操作系统的商业固件。baseflight-master 这套飞控源码恰好卡在了一个舒服的位置:再往前一步是任务调度器加抽象层的现代架构,往后退一步是 MultiWii 那种寄存器满天飞的远古代码。它没有 RTOS、没有复杂的驱动框架,从陀螺仪寄存器到电机 PWM 输出的完整链路,一两千行 C 代码里就能读完。它对多旋翼的需求聚焦得很干净:姿态解算、PID 控制、混控输出、遥控解析、电压保护全都有。适合两类人——想自己从零写飞控的人拿它当第一份研读源码,想给非标机架或新传感器做移植的开发者拿它当改造底板。

2. 架构拆解:无操作系统的裸机主循环如何串起传感器、PID 和混控

Baseflight 这一代固件最值得先看的地方,是它没有系统、没有调度器,整个飞控就靠一个while(1)裸奔。这个设计决定了后面所有模块的组织方式,也决定了你改任何功能时的思路。

2.1 目录结构:先认清这套 .c 文件各自守什么职责

以我手上这份 baseflight-master 为例,目录结构大致长这样:

baseflight-master/ ├── Makefile # 目标板选择、编译参数、链接脚本入口 └── src/ ├── main.c # 程序入口,主循环就藏在这里 ├── board.h # 目标板与硬件宏开关,改硬件先看它 ├── board.c # 时钟、GPIO、PWM 定时器初始化 ├── sensors.c # 陀螺仪/加速度计/磁力计驱动与校准 ├── imu.c # 姿态解算,陀螺积分与加速度计融合 ├── pid.c # 三轴 PID 控制器 ├── mixer.c # 多旋翼混控,决定电机与姿态的映射 ├── rx.c # 遥控协议解析(PWM/PPM/SBUS) ├── serial_cli.c # 串口命令行配置 └── battery.c # 电池电压检测与低电压保护

各文件的核心职责我一般会先列成一张表再往下读:

文件职责关键内容
board.h硬件配置总开关目标板定义、传感器型号宏、混控类型宏
main.c生命周期管理初始化顺序、主循环调用顺序
sensors.c原始数据来源寄存器读写、量程选择、零漂校准
imu.c姿态估计把角速度和加速度换算成欧拉角
pid.c控制核心三个轴的 P/I/D 计算
mixer.c输出分配混控表、电机限幅、转向

这套工程沿袭了早期多旋翼固件的组织方式,源码平铺在src/下,没有层层目录,读起来反而省心。GPS、气压计这类功能在完整版里也各占一个文件,属于可选模块,主循环里用宏包着,不编译就不会进固件。

2.2 主循环:固定周期采样下的六步调用链

Baseflight 没有定时器中断去驱动控制回路,主循环本身就是调度器。核心代码在 main.c 里,浓缩成可读版本是这样:

// main.c 主循环核心:裸机顺序执行,不依赖任何操作系统 while (1) { // 记录本帧起始 tick,用于控制回路周期对齐 uint32_t now = micros(); // 1. 先采样:拿到这一帧的陀螺仪/加速度计原始数据 sensors_update(); // 2. 再解析遥控器输入,把 PWM/PPM 换算成指令 rx_update(); // 3. 用刚才的传感器数据做姿态解算,得出当前角度 imu_update(); // 4. 遥控指令与姿态误差送进 PID,算出三轴控制量 pid_update(); // 5. 控制量按混控表分配到各电机通道 mixer_update(); // 6. 睡到下一个周期点,保证每一帧间隔一致 sleep_until(now + LOOPTIME_US); }

这个顺序是刻意排的:传感器采样必须放在最前面,保证姿态用的是本帧的新鲜数据;混控输出放在最后,保证计算完立刻写寄存器,输出延迟最小。LOOPTIME_US是循环周期宏,默认 3500us,换算过来大约 285Hz 的控制频率,当年很多飞手靠把 looptime 压到 1000us 来获得更快的响应,代价是 MCU 负载和陀螺仪噪声都上来了。

有个细节容易被忽略:sleep_until补的是上一帧的耗时差,如果某帧传感器读取超时,后面的周期会顺延,PID 积分项会按固定 dt 累加,于是实际周期和理想周期不一致时就容易产生振荡。这也是后来 cleanflight 引入任务调度器的动机之一,但 baseflight 这种线性写法对初学者更友好,每一行都能对应到实际行为。

2.3 配置体系:编译期宏和运行期 CLI 并存

Baseflight 的配置分两层。一层是编译期定死的,集中在 board.h 里:

// board.h 选段:目标板硬件开关 #define USE_MPU6050 // 使用 MPU6050 六轴传感器 #define USE_HMC5883L // 使用磁力计(可选) #define MIXER_QUADX // 默认机架类型为四轴 X 型 #define LOOPTIME_US 3500 // 主循环周期,单位微秒

改传感器型号、换机架类型、调 IO 映射,都要动这层宏然后重新编译。另一层是运行期可调的,走 serial_cli.c 提供的命令行:PID 参数、looptime、遥控通道映射、曲线设置都存在片上 flash 里,飞场调参时不用带编译器。

// serial_cli.c 的命令入口,解析字符串并写配置 void cliProcess(void) { if (strncmp(cmd, "set", 3) == 0) { // "set 参数名=值",例如 set pid_roll_p=60 lookupAndWriteConfig(cmd + 4); } else if (strncmp(cmd, "save", 4) == 0) { saveConfigToFlash(); // 写 flash 并重启 } }

我一般会先过一遍 board.h 里所有宏,搞清楚哪些硬件被编译进了固件,再连串口用 CLI 调运行期参数。分不清这两层,经常会出现"改了 PID 不生效"的困惑,实际是忘记save,或者说改了宏忘了重编。

3. 核心代码走读:从 MPU6050 原始数据到四路电机 PWM

这一章是整套源码的精华。我按数据流动的方向走一遍:传感器读出原始值,姿态解算算出角度,PID 算出控制量,混控器把控制量拆成四路输出。

3.1 传感器驱动:I2C 初始化、量程选择与零漂校准

飞控的传感器读取基本都是 I2C 接口,MPU6050 是当年最常见的六轴芯片,baseflight 对它的驱动写在 sensors.c 里:

// sensors.c:MPU6050 初始化选段 void gyro_init(void) { // 设备地址 0x68/0x69,先发复位指令让寄存器回到默认态 i2c_write(MPU6050_ADDR, 0x6B, 0x80); // DEVICE_RESET delay(50); i2c_write(MPU6050_ADDR, 0x6B, 0x00); // 唤醒并关闭休眠 // 陀螺量程选 ±2000°/s:寄存器 0x1B 写 0x18 // 对应灵敏度 16.4 LSB/(°/s),数值换算时直接除 16.4 i2c_write(MPU6050_ADDR, 0x1B, 0x18); // 加速度计量程选 ±8g:寄存器 0x1C 写 0x10 // 对应 4096 LSB/g,静止水平时 Z 轴读数约 4096 i2c_write(MPU6050_ADDR, 0x1C, 0x10); } // 读原始值:每个轴由高位和低位两个字节拼成一个 int16 int16_t gyro_get(int axis) { uint8_t buf[2]; // 陀螺 X/Y/Z 的高字节寄存器连续排列,按轴偏移取地址 i2c_read(REG_GYRO_XOUT_H + axis * 2, buf, 2); return (int16_t)((buf[0] << 8) | buf[1]); }

这段代码里最值得记住的是量程和灵敏度的换算关系。±2000°/s 意味着每个 LSB 代表 16.4°/s,PID 里拿到原始值后要先除这个系数,否则控制量会整体偏大或偏小,表现出来就是"同样参数下这台飞控特别容易超调"。零漂校准也是这一层干的活:上电后静止采几十帧求平均,作为陀螺零偏,飞行时在每个采样值里减掉它。常见做法是把这一步放在解锁前的初始化里,确保每次起飞前都重新校准。

3.2 姿态解算:互补滤波把陀螺积分和加速度计夹角揉在一起

姿态解算是飞控的"感觉"来源。陀螺仪积分出来的角度短时间很准,但会随时间漂移;加速度计能直接测出重力方向,但受震动干扰大。baseflight 的处理思路是互补滤波,把两者按权重融合:

// imu.c:互补滤波 —— 陀螺仪短时间可信,加速度计长时间可信 // 取上一次姿态角,叠加陀螺角速度积分,再用加速度计角度做校正 float dt = LOOPTIME_US / 1000000.0f; // 每帧间隔约 0.0035s float k = 0.98f; // 陀螺积分权重 // 加速度计算重力方向夹角:atan2(y, z) 得到 roll 角 float accAngle = atan2f(accel.y, accel.z) * RAD2DEG; // 融合:陀螺积分占大头,加速度计只负责拉回漂移 attitude.roll = k * (attitude.roll + gyro.roll * dt) + (1 - k) * accAngle;

k取 0.98 意味着 98% 信任陀螺积分,2% 信任加速度计。k 越接近 1,角度越平滑但漂移越明显;k 调小,抗漂移强但震动会直接串进姿态。实际工程里 baseflight 内部用的是方向余弦矩阵加误差校正实现的,比我这里展开的标量版本更完整,但融合思想一样——陀螺负责短期,加速度计负责长期。我读源码时建议先看懂这个简化模型,再回去看矩阵版本,会轻松很多。

3.3 PID 控制器:rate 环的 P/I/D 是怎么落到三个轴上的

Baseflight 的 PID 是角速度环,也叫 rate 环:遥控杆量给出期望角速度,陀螺仪给出实测角速度,两者误差进 PID。核心代码在 pid.c:

// pid.c:三个轴的 PID 计算,以 roll/pitch/yaw 循环展开 void pid_controller(void) { for (int axis = 0; axis < 3; axis++) { // 误差 = 期望角速度(来自遥控杆) - 实测角速度(来自陀螺) int32_t error = rcCommand[axis] - gyroRate[axis]; // P:误差直接放大,决定响应速度 int32_t P = error * pid[axis].P / 100; // I:累积误差,用于消除长时间偏航,必须限幅防积分饱和 iState[axis] = constrain(iState[axis] + error, -I_LIMIT, I_LIMIT); int32_t I = iState[axis] * pid[axis].I / 100; // D:对陀螺测量值做差分,相当于阻尼,抑制超调 int32_t D = (gyroRate[axis] - lastGyroRate[axis]) * pid[axis].D / 100; // 三部分叠加,送到混控器 axisPID[axis] = P + I + D; } }

三个参数各管一件事:P 管响应快慢,太低飘、太高抖;D 管刹车力度,专治超调;I 管持续偏差,比如重心偏一侧导致的长期下垂。我调参的习惯顺序是先只开 P 调到机身轻抖,再加 D 压住超调,最后加一点 I 消除静态偏差——一上来就三个参数一起拧,基本是玄学现场。针对满油门振荡,后来的 cleanflight/Betaflight 加了 TPA(油门相关衰减),思路是按油门位置线性压低 P,baseflight 时期还没有,你可以把这个思路留到二次开发时自己去实现。

3.4 混控器:三轴指令怎么变成四路电机输出

PID 算出来的是 roll/pitch/yaw 三个控制量,但电机有四路,怎么分配是混控器的活。以最常见的四轴 X 型为例,mixer.c 里维护一张表:

// mixer.c:四轴 X 型混控表 // 每行一个电机,三个值分别是该电机对 roll / pitch / yaw 的参与系数 // 系数正负取决于电机在机架上的安装方位 static const int16_t mixerQuadX[4][3] = { { -1, +1, -1 }, // 右后电机:负 roll、正 pitch、逆时针修正 { +1, +1, +1 }, // 右前电机:正 roll、正 pitch { +1, -1, -1 }, // 左前电机:正 roll、负 pitch { -1, -1, +1 }, // 左后电机:负 roll、负 pitch }; void mixer_motors(void) { for (int i = 0; i < 4; i++) { // 油门作基底,三轴控制量按系数叠加到对应电机 motor[i] = throttle + axisPID[ROLL] * mixerQuadX[i][0] + axisPID[PITCH] * mixerQuadX[i][1] + axisPID[YAW] * mixerQuadX[i][2]; // 限幅保护,防止输出超过电调可接受范围 motor[i] = constrain(motor[i], MIN_MOTOR, MAX_MOTOR); } }

混控表本质是一张「电机数 × 三轴系数」的矩阵。正负号由安装方位决定,比如右前电机要提供正 roll 力矩,系数就是 +1。这里最容易翻车的是电机转向和桨型没配合好:同侧电机转向必须相反,否则 yaw 方向的控制量会互相抵消,飞机一解锁就原地打转。改机架布局本质就是改这张表——给六轴加两行,给八轴加四行,Y 型机要额外处理舵机混控。看到这里你就明白了,二次开发最常见的切入点就是这张表。

4. 编译烧录与联调:把固件跑起来并完成首次上电验证

源码读通了,下一步是让它跑在真实硬件上。这个工程是 Makefile 驱动的嵌入式项目,没有 IDE 工程文件,工具链和烧录流程都要自己准备。

4.1 工具链:arm-none-eabi-gcc 与 make 编译

先装交叉编译工具链。以 Ubuntu/Debian 为例:

# 安装 ARM 交叉编译工具链和 make sudo apt install gcc-arm-none-eabi make # 编译固件,TARGET 换成你手上板子对应的配置名 make TARGET=xxx # 编译产物在根目录或 obj/ 下,通常是 .hex 和 .bin ls baseflight.hex baseflight.bin

TARGET是 Makefile 里切换目标板的变量,对应 board.h 里一套完整的引脚和传感器宏定义。选错 TARGET 的后果是编译能过、上电没反应——后面避坑章会专门讲。老工程对编译器版本敏感,我一般会先看一眼工程附带的说明里要求的 gcc 版本,太新的编译器容易在链接阶段报奇怪的错,这不是代码问题,纯粹是工具链代沟。编译成功后会生成baseflight.hex(烧录用)和baseflight.bin(部分工具只认 bin),两者内容等价。

4.2 烧录:串口 bootloader 与 SWD 两种方式

这个年代的飞控板一般留了两种写入通道。一种是板载串口 bootloader,把 BOOT0 跳线拉高再上电,MCU 会进入系统引导区,然后用串口工具写入:

# 方式一:串口内置 bootloader,进入后直接写 bin stm32flash /dev/ttyUSB0 -w baseflight.bin # 方式二:SWD 调试口,板上带调试座时更省事 st-flash write baseflight.bin 0x08000000

stm32flash走的是 UART,st-flash走的是 SWD 四线调试口。SWD 的优势是不用动跳线、可以反复擦写,串口方式则依赖 BOOT0 的时序。烧完别急着上电,先通过串口用version命令确认固件版本和当前目标板,这一步能提前发现"烧进去但没跑对"的问题。常见做法是把烧录命令写进 Makefile 的 flash 目标里,以后改代码重编重烧一条命令搞定。

4.3 首次上电:CLI 检查、解锁条件与无桨测试

固件起来后,第一步不是上桨,而是接串口做配置核对。串口 115200 波特率连接,回车进入 CLI:

version # 查看固件版本、目标板名称 set looptime=1000 # 把循环周期从默认 3500us 提到 1000us set pid_roll_p=60 # 现场改 roll 轴 P 值 save # 写入 flash,重启生效

不同分支的命令名略有差异,输入 help 能看到完整清单。解锁前要过一遍条件:油门摇杆最低、遥控接收机信号正常、陀螺仪和加速度计读数不为零且不飘、解锁开关处于关闭位。我自己的流程是先把所有电机插头拔掉,用遥控器走一遍解锁和加锁,观察飞控 LED 状态变化;确认无误后再接电机,但先不装桨,推油门到 20% 左右用手感知每个电机是否按混控方向响应。这一步值得多花十分钟,它能拦住大部分"一解锁就满油"的事故。

提示:任何带螺旋桨的测试,务必先拆桨再做,这是这条赛道里唯一的铁律。

5. 避坑指南:编译失败、解锁异常与飞行振荡的排查记录

这章全是实际跑这个工程时翻过的车。每条按现象、原因、解决三条线写,你踩到哪个坑就直接对号入座。

5.1 编译与烧录阶段的坑

现象一:命令行执行 make 直接报arm-none-eabi-gcc: command not found。原因:工具链装了但没进 PATH,或者压根没装。解决:先which arm-none-eabi-gcc确认,装过的话把工具链的 bin 目录写进~/.bashrc并重新打开终端;没装就按 4.1 的 apt 命令装一遍。

现象二:编译报错undefined reference to 'xxx',看着像代码缺失。原因:十有八九是 Makefile 里 TARGET 对应的源码文件列表和你改动的文件对不上,新增的 .c 文件没有加进编译列表。解决:打开 Makefile 找到目标板的源文件清单,把新文件加进去后重新 make。这个坑在加新传感器驱动时必踩,我后来养成习惯,每加一个文件第一件事就是同步改 Makefile。

现象三:烧录时工具提示无法连接或同步失败。原因:BOOT0 没拉高就上电、串口被上位机占用、或者波特率不匹配。解决:断电,确认 BOOT0 跳线位置,关闭所有占用串口的软件,重新上电再烧。SWD 方式如果也连不上,大概率是板上调试口没供电或接线虚焊,用万用表量一下 3.3V 和 GND 最靠谱。

5.2 上电与飞行表现异常的坑

现象四:CLI 里看陀螺仪读数全是 0,或者数值漂得离谱。原因:I2C 通信失败,常见是 SCL/SDA 缺少上拉电阻,或者传感器供电不稳;二手传感器翻新的也不少,内部寄存器响应异常。解决:先查板子上 I2C 上拉电阻是不是 4.7k 左右,用示波器看 SCL/SDA 波形是否干净;换一颗已知正常的传感器做排除,能省半天排查时间。

现象五:解锁后某个电机猛转,其他电机没反应。原因:混控方向配错了——电机转向或者桨型装反,导致解锁自检时飞控检测到姿态异常,试图用全力修正。解决:拆桨,按 4.3 的无桨流程逐电机测试,核对每个电机的转向和混控表系数是否一致;转向不对就调换电机三根线的任意两根。血泪经验是别在装桨状态下做这个测试。

现象六:悬停时机身高频振荡,收油后消失。原因:P 值过高,或者机架本身震动大被陀螺仪放大;也可能是 looptime 调太低导致采样抖动。解决:先把 PID 的 P 降到当前值的一半试飞,振荡消失说明是 P 的问题;还抖就检查机架电机座螺丝松紧,并观察陀螺仪波形里有没有高频毛刺。有条件就开日志记录,黑匣子数据比手感判断客观得多,它能告诉你振荡频率是落在姿态环还是电机响应上。

6. 二次开发思路:给 baseflight 加一张自定义混控表

最后落到改造。绝大多数人拿到这套源码不是为了刷一个原版固件,而是手上有台非标机架——比如双层八轴、异形布局,标准混控表里没有对应项。改造切入点就是 mixer.c 的混控表。

假设你要加一个 X8 布局,上下两层各四电机、同轴同向。做法是在混控选择入口加一个 case,把下层四个电机的系数复用四轴 X 型的表:

// mixer.c:在混控选择函数里增加自定义类型 case MIXER_CUSTOM_X8: // 先复制标准 quad X 的四行到前四个电机 memcpy(motorMixer, mixerQuadX, sizeof(mixerQuadX)); // 后四个电机对应下层,系数与上层一致 memcpy(&motorMixer[4], mixerQuadX, sizeof(mixerQuadX)); motorCount = 8; // 让混控循环遍历 8 个电机 break;

改完重新 make,烧录,然后强制走一遍无桨验证:解锁后逐个电机给固定油门,确认八个电机的转向、转速响应和上下层对应关系都对,再上桨试悬停。这套验证流程不只是为这一次改造服务——从那以后我每动一次机架或混控,都强制按"电机编号 → 安装方向 → 混控系数 → 无桨解锁空转"四步过一遍,确实省掉了好几次拆桨的尴尬。

换新传感器的思路同理:在 sensors.c 里加一个xxx_init和xxx_get,board.h 里打开对应宏,主循环不用动,因为采样函数已经按抽象接口在调了。这套裸机代码的开放性就在这里——没有框架约束,改哪里都很直接。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进