☰
C语言裸机飞控实战:从传感器读取到串级PID完整实现
2026/10/17 11:58:14 网站建设 项目流程

简介:这套以C语言实现的嵌入式无人机开源工程,面向无人机控制与嵌入式开发者,覆盖硬件驱动、传感器采集、飞行控制算法、图像处理与无线通信等模块,可帮助读者系统理解飞控软件架构与PID控制原理。压缩包约19.23MB、共1740个文件,包含278个h与258个c源文件,并配有HTML文档、launch配置、PNG图片、PDF手册及链接脚本等,目录分类明确,便于按模块查阅。已有870人学习/下载,适合有一定C语言基础、希望上手完整无人机项目的开发者参考。在源码中可看到基于STM32的定时器驱动、传感器数据采集、PID控制算法以及图像处理与无线通信等实现,配套详细设计和说明文档,注释丰富、结构清晰,对掌握嵌入式无人机开发流程、PID整定和外设驱动有实际参考价值。

1. 为什么是C语言裸机飞控:一个课程设计要解决的三个问题

做无人机课程设计的开发者,通常第一个念头是想用手头最熟的平台:要么Arduino封装好的库,要么直接上FreeRTOS跑多任务。但真正面对C语言嵌入式无人机这个方向时,我第一反应是劝你把这两个选项都放一放。原因是Arduino库把传感器读取、PWM生成都封装成黑匣子,答辩时被问到底层寄存器配置,答不上来;而上RTOS,对于只有姿态解算、PID控制、电机驱动这几个任务的飞控来说,又明显杀鸡用牛刀——任务切换开销、优先级配置的复杂度,反而成了新手翻车重灾区。这套C语言裸机飞控方案要解决的核心问题有三个:第一,用纯C在裸机上跑通「传感器读取—姿态解算—PID控制—PWM输出」的完整闭环;第二,把每一层拆开讲清楚,让你能解释每个参数为什么是这个值;第三,给出可验收的测试方法和答辩演示路径。适合正在做嵌入式飞控课设、或者想入门无人机底层开发的同学,照着这套结构做,能省下大量踩坑时间。

2. 总体架构设计:硬件选型与四层软件栈的职责划分

2.1 硬件平台的选型思路

无人机飞控的主控芯片不需要追求高端,课程设计场景下,一颗Cortex-M3或M4内核的芯片就足够了。通常的做法是选一款主频在72MHz以上、内置硬件I2C和定时器PWM输出的芯片,这样传感器读取和电机控制都可以不走软件模拟,既稳定又减少CPU占用。传感器方面,核心是六轴惯性测量单元,兼顾加速度计和陀螺仪,航向校正再加一个磁力计;气压计用来做定高,属于加分项,裸机模式下可不接。电机驱动采用PWM信号输入的电调,四旋翼对应四路PWM输出,频率一般设在50Hz到500Hz之间。骨架选常见的X型或十字型机架,轴距250mm左右比较适合课设演示。电池用3S锂电池,搭配稳压模块给主控和传感器供电——注意电机和主控的电源必须分开走,否则电机启动瞬间的压降会让单片机直接复位。

2.2 软件模块划分:驱动层、算法层、任务层与通信层

整体软件结构按四层划分:最底层是设备驱动,负责I2C读取IMU原始数据、定时器产生PWM波形、串口收发调试数据;往上一层是算法层,包含姿态解算和控制律计算;再往上是任务层,用一个大循环配合定时器中断,把传感器采集、姿态更新、PID计算、PWM输出按固定时序调度起来;最上面是通信层,通过串口把姿态角、目标值和PID输出打包发给上位机。这里推荐把控制频率固定为1kHz,也就是每1毫秒执行一次完整的姿态解算和控制输出;传感器读取频率可以降为500Hz,通过滑动窗口取最新值。

任务调度的实现不需要操作系统,用一个定时器中断置标志位,主循环轮询即可。主循环代码骨架如下:

volatile uint8_t control_tick = 0; void TIM1_IRQHandler(void) { if (TIM_GetITStatus(TIM1, TIM_IT_Update)) { control_tick = 1; // 1ms 控制周期标志 TIM_ClearITPendingBit(TIM1, TIM_IT_Update); } } int main(void) { imu_init(); motor_init(); uart_init(); pid_init(); while (1) { if (control_tick) { control_tick = 0; imu_read_raw(); attitude_update(); // 姿态解算 pid_control(); // 串级PID pwm_update(); // 输出电机控制 telemetry_send(); // 串口上报 } } }

这个时序保证了不管传感器数据何时到达,控制周期都严格是1kHz。需要注意,这里的imu_read_raw()只是把I2C的原始数据读进缓冲区,姿态解算和PID才放在主循环里做,避免在中断服务函数里做浮点运算,这是裸机飞控一个常见的速度优化点。定时器中断里只置标志位,不做任何计算,中断处理时间可以控制在几微秒以内。

2.3 三层状态机:待机、解锁、飞行与异常保护

飞控不能上电就直接转电机,必须用状态机把行为约束住。这里设计三个核心状态:待机(LOCKED)、解锁(UNLOCKED)、飞行(FLYING),外加一个异常状态(FAULT)。待机状态下,系统只采集传感器数据、解算姿态、更新遥控器输入,但不输出PWM控制信号;解锁状态需要满足两个条件才会进入:一是遥控器摇杆执行特定解锁动作,二是机体倾角小于设定阈值(通常30°),防止起飞时姿态偏差过大。飞行状态下才允许电机响应PID输出。

异常状态的判定和恢复策略很简单:当电池电压低于安全阈值、陀螺仪数据异常、或者遥控器信号丢失时,立即进入FAULT状态,PWM输出强制为零。判定的实现不复杂,在PID控制函数开头加一段保护逻辑,一旦触发异常标志,无论PID输出是多少,输出值直接写安全值。状态机的好处是验收演示时逻辑清晰,评审问「电机为什么不会上电就转」「突然丢信号会怎样」,可以直接指代码说明。

3. 核心模块实现:姿态融合、PID闭环与PWM输出的完整代码

3.1 姿态解算:互补滤波融合陀螺仪与加速度计

姿态解算是整个飞控里最容易被卡住的环节。陀螺仪积分得到姿态角,短时间准确,但积分漂移会随时间累积;加速度计直接测量重力方向,长期稳定,但振动环境下噪声很重。互补滤波就是把两者按频率加权组合:低频采用加速度计的数据来抑制漂移,高频采用陀螺仪的数据来保证动态响应。算法结构清晰,适合裸机实现,计算量比卡尔曼滤波小一个数量级,在72MHz主频上浮点运算完全没有性能压力。

typedef struct { float dt; // 采样周期(秒) float tau; // 滤波系数,通常取 0.5~1.0 float roll; // 横滚角(rad) float pitch; // 俯仰角(rad) } complementary_filter_t; void complementary_update(complementary_filter_t *f, float gx, float gy, float ax, float ay, float az) { // 由加速度计计算姿态角,atan2f 避免除零问题 float acc_roll = atan2f(ay, az); float acc_pitch = atan2f(-ax, sqrtf(ay * ay + az * az)); // 陀螺仪角速度积分 + 加速度计修正 float alpha = f->tau / (f->tau + f->dt); f->roll = alpha * (f->roll + gx * f->dt) + (1.0f - alpha) * acc_roll; f->pitch = alpha * (f->pitch + gy * f->dt) + (1.0f - alpha) * acc_pitch; }

这段代码的关键参数是tau,它决定陀螺仪和加速度计的信任权重。tau越大,滤波结果越平滑,但响应变慢;tau太小则加速度计的振动噪声会直接串进姿态角。实际调参时,我一般从tau=0.5起步,观察上位机姿态曲线,滚转时若看到高频抖动就逐步增大到0.8左右。还有一个容易忽略的细节:atan2f的返回值范围是-π到π,而陀螺仪积分是无限累积的,所以在计算误差前要先把两者规范到-π到π区间,否则翻滚超过180°时会出现姿态角跳变,PID输出瞬间反向。

3.2 控制器设计:内环角速度环与外环角度环串级PID

姿态控制采用串级PID结构,外环把目标角度与当前姿态角的误差转换成期望角速度,内环把期望角速度与陀螺仪实测角速度的误差转换成电机输出。为什么要串级?因为只做单环角度PID时,飞机受外力扰动后恢复太慢,甚至会振荡;加了角速度内环后,相当于先控制转动快慢,再控制转到哪里,抗风性和稳定性都好很多。

typedef struct { float kp, ki, kd; float integral_max; // 积分限幅,防积分饱和 float out_max; // 输出限幅 float integral; float last_error; } pid_t; float pid_update(pid_t *pid, float target, float current, float dt) { float err = target - current; float d_err = (err - pid->last_error) / dt; pid->integral += err * dt; // 积分限幅与输出限幅 if (pid->integral > pid->integral_max) pid->integral = pid->integral_max; if (pid->integral < -pid->integral_max) pid->integral = -pid->integral_max; float out = pid->kp * err + pid->ki * pid->integral + pid->kd * d_err; if (out > pid->out_max) out = pid->out_max; if (out < -pid->out_max) out = -pid->out_max; pid->last_error = err; return out; }

串级时外环输出内环的期望角速度,内环输出直接叠加到油门基值上。比如横滚通道:

float expect_rate = pid_update(&roll_outer_pid, target_roll, imu.roll, dt); float roll_out = pid_update(&roll_inner_pid, expect_rate, imu.gyro_x, dt);

内环PID参数通常比外环大一个数量级——角速度环相当于阻尼,负责让飞行更「硬」,角度环负责把飞机拉回目标角度。先在悬停状态调内环kp,让它从零慢慢加上去,观察飞行器是否出现高频抖动;再调角度环kp,目标是要有干脆的跟随而不过冲。

3.3 控制信号输出:PWM映射与电机方向配置

PID输出是-1到1之间的归一化数值,需要映射到电机的实际PWM占空比。以常见的电调为例,50Hz刷新率时油门PWM脉宽通常在1000μs到2000μs之间,1000对应电机停转,2000对应满油门。油门基值在解锁后固定在一个怠速值(比如1050μs),PID输出乘以一个缩放系数后叠加在基值上。四个电机的组合关系按X型机架定义:

// X型机架电机输出组合,roll/pitch/yaw均为PID输出 // 前方两个电机与后方两个电机的方向相反抵消扭矩 motor_pwm[0] = throttle_base + roll_out + pitch_out - yaw_out; motor_pwm[1] = throttle_base - roll_out + pitch_out + yaw_out; motor_pwm[2] = throttle_base - roll_out - pitch_out - yaw_out; motor_pwm[3] = throttle_base + roll_out - pitch_out + yaw_out;

这段组合逻辑是飞控里最关键的经验值之一。如果某个电机的旋转方向装反,或者PWM输出接错通道,飞行器通电后就会直接侧翻。所以驱动初始化时要单独写一个电机测试函数,依次给四个电机发送固定脉宽,确认每个电机的旋转方向和安装方向与代码假设一致。飞控运行时PWM更新频率与控制频率保持一致,也就是1kHz,保证每个控制周期的新输出立刻生效。

4. 调试链路设计:串口协议、上位机查看与参数整定流程

4.1 串口协议设计:帧头、校验与数据打包

裸机飞控调试最依赖的就是串口日志。但把原始浮点数直接printf出来,会产生大量无格式数据,上位机难解析。建议设计一个固定长度的二进制数据帧,用帧头、长度、数据体和校验字段组成。核心思路是:解析时先找帧头,再按长度接收,校验通过后按结构体直接映射到字段。上面例子里的帧长度固定,这样解析代码可以省去长度字段,进一步简化。

// 飞行状态数据结构体,由串口发送给上位机 #pragma pack(1) typedef struct { uint8_t frame_head; // 固定 0xAA uint8_t status; // 状态机当前状态 float roll; // 横滚角(rad) float pitch; // 俯仰角(rad) float gyro_x; // 滚转角速度(rad/s) float motor0; // 0号电机PWM脉宽 float motor1; float motor2; float motor3; uint16_t crc; // CRC16校验 } telemetry_frame_t; #pragma pack()

串口波特率建议用到460800或更高。因为每帧约30字节,如果以100Hz频率发送,460800波特率传输时间只有约5ms,不会影响控制主循环。注意printf浮点数格式化会占用大量CPU时间,尽量避免在主循环里用printf,而是直接做内存拷贝,把结构体指针丢给串口DMA发送。DMA发送的好处是主循环不用等数据发完,发完自动进下一个周期。

4.2 上位机可视化:把姿态和响应曲线变成图表

光看串口十六进制数据看不出问题,需要配合上位机把数据变成直观的曲线。上位机的选择可以分两级:课程设计阶段用串口调试助手加自绘波形,或者直接用开源的绘图脚本读取串口数据并实时绘制姿态角和PID输出曲线。更完整的做法是写一个桌面端小工具,接收串口帧,按通道分离数据,显示三轴姿态角、角速度以及四个PWM输出,同时支持把数据记录到本地文件,方便事后回放。这部分工作在答辩时很占优势:评审问「你怎么证明你的飞控是稳定的」,直接调出悬停时的姿态角曲线,峰值不超过±3°,比任何口头解释都有说服力。

4.3 PID参数整定的标准路径

PID调参是飞控里最容易玄学的环节,很多开发者上来就同时调四个通道,结果飞控完全失控。标准路径是先单通道、后多通道、先内环、后外环。第一步,把飞机固定在一个只允许一个方向转动的测试台上,单独给横滚通道加阶跃信号,调内环kp直到响应快速且无明显振荡;第二步,给内环目标角速度设为零,用手掰动飞机再松开,观察它是否自己能回中,此时内环ki可以处理静差;第三步,调外环角度环kp,让飞机能够跟随目标角度,最后再引入kd抑制过冲。这个过程中始终通过上位机观察响应曲线,不要用眼睛看飞机姿态来估算。

参数整定还有一个容易被忽视的步骤:目标值变化也要缓慢,不能直接给一个大的阶跃角度。遥控器输入经过限幅和斜坡处理后再送进外环,否则飞机会猛地甩头,甚至翻转。通常在代码里加一个输入限速器,让目标角度的变化率不超过180°/s。

5. 常见问题排查:五个最容易翻车的坑与解决路径

5.1 现象:电机转速异常跳变,飞机抖动加剧

原因排查下来通常是两个:一是加速度计的振动噪声直接串进了姿态解算,互补滤波的tau值太小;二是内环kp过大,导致角速度环出现极限环振荡。解决方法是先在上位机里看原始加速度波形,确认是否有明显的周期性振动峰值;然后把tau从当前值往上调,直到输出曲线平滑为止;最后降低内环kp,观察高频抖动是否消失。注意电机旋转带来的振动频率通常在几百赫兹到几千赫兹,如果滤波时间常数在10ms量级,振动噪声衰减程度有限,可以在加速度计进滤波器之前加一个滑动平均窗口,但窗口长度不要太大,否则会引入额外延迟。

5.2 现象:飞行或调试中I2C读取超时,主循环被卡住

I2C读取IMU时,如果总线被拉死或设备无响应,默认的阻塞等待会让主循环死等,控制中断得不到响应,飞控失去可控性。原因通常是连接线松动、I2C上拉电阻缺失、或者中断服务函数与主循环同时对I2C外设进行操作。解决方案是在I2C读取函数里加超时机制,超过一定时间就放弃本次读取并返回失败,主循环中如果连续多次读取失败就进入异常状态并停机,同时把读取操作完全放在主循环中,定时器中断里只置标志位,不做任何I2C操作。硬件上给I2C总线加上4.7kΩ上拉电阻,并把线长控制在10cm以内。

5.3 现象:电机发出刺耳啸叫,电流噪声明显

PWM频率和刷新率设置不当是常见原因。电调对PWM频率有适用范围,50Hz是标准航模信号频率,但部分电调在新固件下支持250Hz以上信号,如果两者不匹配,电调内部无法正确解出占空比,就会表现为啸叫和电机响应不一致。解决方法是查阅电调说明书,确认它支持的PWM频率范围;若不确定,统一用50Hz和1000μs到2000μs脉宽,这是最通用的配置。另外电机在怠速脉宽附近也可能发出轻微噪声,这是正常的,但如果高转速下啸叫,就要检查电调校准是否做过——上电前把油门拉到最大,听到提示音后拉到最小,完成行程校准。

5.4 现象:电池电压一旦下降,飞行器迅速失控

原因不是PID参数问题,而是电池内阻导致电机大电流拉低主控供电电压,如果供电稳压器压差不足,主控复位或传感器数据异常。解决方法是给主控单独用低压差稳压器供电,并测量电机全速运转时主控供电电压是否稳定在额定值;同时代码里实现电压监测,低于安全阈值时限制最大油门输出,而不是直接切断。实际做法是把电池电压经分压电阻接入ADC引脚,在主循环里周期采样做平均,电压低于阈值时分级降功率,保留足够的电压让飞控安全降落。

5.5 现象:起飞瞬间往一个方向侧翻

原因几乎都是电机方向或旋向配置错误。每个电机的旋转方向必须与机架对角线上的另一个相反以抵消扭矩,装机时只要有一个电机方向不对,起飞时就会绕该轴偏转。排查方法是把四个电机的PWM线依次接到固定通道上,用测试程序让每个电机单独转,确认旋转方向与设计一致;同时注意螺旋桨的正反装,旋转方向和桨型必须匹配。另外一个隐蔽原因是加速度计安装方向与代码坐标系不一致,导致起飞前姿态角就偏了几十度,解锁直接被状态机拦截——验证方法是把飞机水平放置,上位机显示的roll和pitch应该在±1°以内。

6. 验收与答辩准备:测试用例设计、演示流程与讲解侧重点

课程设计验收和答辩的本质是让评审确信你不仅把代码跑起来了,还理解了背后的原理。所以要提前准备一张测试用例表,覆盖地面测试、飞行测试和安全测试三类场景:地面测试包括传感器数据是否正常、电机按序转动、PWM脉宽与预期一致;飞行测试包括悬停时姿态角偏差、俯仰滚转阶跃响应是否超调、油门加减时飞行器是否平稳;安全测试包括遥控器丢信号是否会停机、低电压保护是否触发、异常按键是否能紧急停止。

答辩演示建议按这个时间轴推进:先花一分钟展示开机自检流程,说明状态机如何阻止意外启动;然后打开上位机,展示解锁后实时姿态曲线,强调悬停时roll和pitch波动幅度不超过设定范围;接着做一个俯仰方向的阶跃响应,指出超调量和调节时间这两个关键指标;最后演示保护机制,比如用手挡住下方通道让螺旋桨堵转、或者直接关闭遥控器,展示飞控在数毫秒内进入FAULT状态并停止电机。

讲解侧重点不要放在「我做了什么」,而要放在「我为什么这么做」。比如评审问为什么不用卡尔曼滤波时,回答补上滤波在计算开销和精度之间更适合裸机;问为什么控制周期选1kHz时,回答这是主流开源飞控的经验值,并且与传感器带宽和电机响应频率匹配;问PID参数怎么定出来的,回答按内环先行、观察曲线逐步逼近的方式,而不是猜参数。有一个第一人称的习惯可以分享:从那以后,我每次做无人机原型,都强制走一遍「传感器原始数据检查 → 电机转向验证 → 桌面固定调参 → 小幅度试飞 → 安全触发测试」的完整测试用例表,不跳过任何一步。希望这篇方案能帮你把课程设计做成一份能讲清楚原理、也能稳定演示的项目。

这套完整源码的代码可以直接参考这套结构去实现,拿到手之后先对照状态机和PID参数看一遍,再动手改自己的机架参数,比自己从头写能省下大量时间。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询