这次我们来看一个无人机控制领域的核心算法——串级PID。对于想深入无人机飞控开发、自己写控制代码的开发者来说,PID是绕不开的基础,而串级PID更是提升飞行器动态响应和稳定性的关键结构。它不是什么新概念,但能把原理讲透、代码写对、参数调好的人并不多。
这篇文章直接切入主题,不讲复杂的控制理论推导,重点解决三个问题:串级PID是什么结构、为什么比单级PID好、以及如何用代码实现一个可用的串级PID控制器。我们会从算法原理、代码结构、参数整定步骤到实际调试注意事项,完整走一遍。如果你关心如何在STM32、Arduino或者简单的仿真环境中实现无人机的姿态或位置控制,这篇内容可以直接参考。
1. 核心能力速览
在深入代码之前,我们先快速了解串级PID控制器的核心特性和应用场景。
| 能力项 | 说明 |
|---|---|
| 控制结构 | 两个PID控制器串联,内环(快环)和外环(慢环)分工协作。 |
| 主要优势 | 提升系统动态响应速度,增强抗干扰能力,改善控制精度。常用于无人机姿态(角速度/角度)、位置(速度/位置)控制。 |
| 硬件门槛 | 极低。算法本身不依赖特定硬件,从8位单片机到高性能处理器均可运行。关键在于传感器数据(如陀螺仪、加速度计)的获取频率和精度。 |
| 代码复杂度 | 中等。结构比单级PID清晰,但参数翻倍,整定更需章法。 |
| 适用场景 | 无人机、平衡车、云台、机器人等需要快速、稳定闭环控制的动态系统。 |
| 调试关键 | 遵循“先内环后外环”、“先比例后积分微分”的参数整定原则。 |
2. 串级PID原理:为什么是“串级”?
单级PID控制器直接根据目标值与实际值的误差进行计算输出。但在像无人机这样的复杂对象中,直接控制最终目标(如角度)可能响应不够快,抗干扰性差。
串级PID引入了两个控制环:
- 外环(慢环):计算“目标值”与“实际值”的误差,输出作为内环的“目标值”。例如,角度环:输入目标角度和当前角度,输出目标角速度。
- 内环(快环):接收外环的输出作为自己的目标,与更快速的“实际值”比较计算误差,并生成最终的控制量输出。例如,角速度环:输入目标角速度(来自角度环)和当前角速度(来自陀螺仪),输出电机PWM信号。
这种结构好比公司管理:外环是经理,制定战略目标(要到某个角度);内环是执行员工,负责快速执行战术动作(以多快的速度旋转)。经理不直接指挥电机,而是通过指挥员工来实现目标,响应更快,管理更精细。
核心思想:外环负责“准”,消除静差,跟踪大趋势;内环负责“快”,抑制内扰,快速响应。将一个大而慢的控制问题,分解为两个小而快的控制问题。
3. 适用场景与使用边界
串级PID并非万能,理解其适用边界能避免误用。
最适合的场景:
- 无人机姿态控制:最经典的案例。外环为角度环(Pitch/Roll/Yaw),内环为角速度环。内环快速响应陀螺仪数据,抑制机体震动;外环保证角度跟踪的准确性。
- 无人机位置控制:外环为位置环(X/Y/Z),内环为速度环。内环控制飞行速度,外环控制最终到达的点。
- 云台稳像:外环计算目标与实际角度的偏差,内环控制电机快速转动到指定角速度。
- 平衡车:外环为车身倾角环,内环为电机速度环。
不适用或需谨慎的场景:
- 被控对象模型简单、扰动小:如果用一个单级PID就能达到很好效果,增加串级只会增加调试复杂度。
- 内外环耦合极强的系统:如果两个环的动力学特性无法明显区分快慢,串级结构优势不明显。
- 对实时性要求极端苛刻:串级计算量约为单级的两倍,在计算资源极其有限的场合需评估。
- “黑箱”系统:如果完全不了解被控对象的内环(快速)状态如何获取(比如没有陀螺仪,只有角度传感器),则无法构建内环。
安全边界:在调试真实无人机时,务必在安全环境下进行,先进行软件仿真或硬件台架测试。参数不当可能导致系统剧烈震荡甚至失控。永远不要在未经验证的代码和参数下进行首次飞行。
4. 环境准备与前置条件
实现串级PID不需要特殊的软件环境,关键在于对硬件平台和工具链的熟悉。
硬件平台(三选一或自选):
- STM32系列(如F103、F405、F407):主流选择,资源丰富,社区支持好。需要准备调试器(ST-Link/J-Link)和IDE(Keil、STM32CubeIDE、PlatformIO)。
- Arduino(如Mega 2560、Due):入门友好,适合快速验证概念。但处理能力和定时器精度可能成为高性能应用的瓶颈。
- 仿真环境(如MATLAB/Simulink, Python):强烈推荐先在仿真中验证算法逻辑和参数范围,安全且高效。
传感器数据:
- 内环反馈:必须是高速、直接的测量值。例如角速度环需要陀螺仪(Gyroscope)数据,速度环需要编码器或光流等提供的速度信息。
- 外环反馈:相对低速、间接或积分得到的值。例如角度环可以由加速度计与陀螺仪融合(如互补滤波、卡尔曼滤波)得到,位置可以由速度积分或GPS得到。
- 关键点:内环数据的更新频率必须显著高于外环,通常内环控制频率在500Hz-1kHz,外环在100-250Hz。
软件准备:
- 嵌入式开发环境:配置好对应的编译、下载、调试工具。
- 数据可视化工具:调试PID离不开数据观察。可以是串口绘图(如SerialPlot、Vofa+)、OLED屏幕显示,或者仿真软件的Scope模块。
- 代码版本管理:建议使用Git,方便回溯参数修改。
5. 串级PID控制器代码实现
下面我们用一个C语言示例,展示一个通用的串级PID控制器结构。这个结构可以移植到大多数嵌入式平台。
首先,定义PID结构体。我们将分别定义内环和外环的结构体。
// pid_controller.h #ifndef PID_CONTROLLER_H #define PID_CONTROLLER_H typedef struct { float target; // 目标值 float measure; // 测量值 float error; // 当前误差 float error_last; // 上一次误差 float error_integral; // 误差积分项 float error_derivative; // 误差微分项 float kp, ki, kd; // PID参数 float output; // 控制器输出 float output_max; // 输出限幅上限 float output_min; // 输出限幅下限 float integral_max; // 积分限幅上限,防止积分饱和 // 可选:微分项滤波器系数,用于抑制高频噪声 float derivative_filter; float derivative_last; } PID_Controller; // 初始化PID控制器 void PID_Init(PID_Controller *pid, float kp, float ki, float kd, float out_max, float out_min, float i_max); // 执行PID计算(位置式) float PID_Calculate(PID_Controller *pid, float target, float measure); #endif // PID_CONTROLLER_H接下来是串级PID的核心结构。我们以“外环为角度,内环为角速度”为例。
// cascade_pid.h #ifndef CASCADE_PID_H #define CASCADE_PID_H #include "pid_controller.h" typedef struct { PID_Controller outer_loop; // 外环PID,例如角度环 PID_Controller inner_loop; // 内环PID,例如角速度环 // 串级计算的核心:外环的输出作为内环的目标 // 流程:外环计算 -> 结果作为内环目标 -> 内环计算 -> 最终输出 float final_output; } Cascade_PID_Controller; // 初始化串级PID void Cascade_PID_Init(Cascade_PID_Controller *cascade_pid, float outer_kp, float outer_ki, float outer_kd, float outer_out_max, float outer_out_min, float outer_i_max, float inner_kp, float inner_ki, float inner_kd, float inner_out_max, float inner_out_min, float inner_i_max); // 执行串级PID计算 // 参数:外环目标,外环测量值,内环测量值 float Cascade_PID_Calculate(Cascade_PID_Controller *cascade_pid, float outer_target, float outer_measure, float inner_measure); #endif // CASCADE_PID_H下面是具体的实现文件:
// cascade_pid.c #include "cascade_pid.h" void Cascade_PID_Init(Cascade_PID_Controller *cascade_pid, float outer_kp, float outer_ki, float outer_kd, float outer_out_max, float outer_out_min, float outer_i_max, float inner_kp, float inner_ki, float inner_kd, float inner_out_max, float inner_out_min, float inner_i_max) { // 初始化外环PID PID_Init(&(cascade_pid->outer_loop), outer_kp, outer_ki, outer_kd, outer_out_max, outer_out_min, outer_i_max); // 初始化内环PID PID_Init(&(cascade_pid->inner_loop), inner_kp, inner_ki, inner_kd, inner_out_max, inner_out_min, inner_i_max); cascade_pid->final_output = 0.0f; } float Cascade_PID_Calculate(Cascade_PID_Controller *cascade_pid, float outer_target, float outer_measure, float inner_measure) { // 1. 外环计算:根据角度误差,计算出目标角速度 float inner_target = PID_Calculate(&(cascade_pid->outer_loop), outer_target, outer_measure); // 2. 内环计算:根据(目标角速度 vs 实际角速度)误差,计算出最终控制量(如电机PWM) cascade_pid->final_output = PID_Calculate(&(cascade_pid->inner_loop), inner_target, inner_measure); return cascade_pid->final_output; }单级PID的计算实现(位置式,带积分抗饱和和输出限幅):
// pid_controller.c #include "pid_controller.h" #include <math.h> // 用于fabsf void PID_Init(PID_Controller *pid, float kp, float ki, float kd, float out_max, float out_min, float i_max) { pid->target = 0.0f; pid->measure = 0.0f; pid->error = 0.0f; pid->error_last = 0.0f; pid->error_integral = 0.0f; pid->error_derivative = 0.0f; pid->kp = kp; pid->ki = ki; pid->kd = kd; pid->output = 0.0f; pid->output_max = out_max; pid->output_min = out_min; pid->integral_max = i_max; pid->derivative_filter = 0.0f; // 0表示不使用滤波器 pid->derivative_last = 0.0f; } float PID_Calculate(PID_Controller *pid, float target, float measure) { pid->target = target; pid->measure = measure; pid->error_last = pid->error; pid->error = pid->target - pid->measure; // 计算误差 // 积分项:累加误差,并做抗饱和处理 pid->error_integral += pid->error; if (pid->integral_max > 0) { // 限制积分项幅值 if (pid->error_integral > pid->integral_max) pid->error_integral = pid->integral_max; if (pid->error_integral < -pid->integral_max) pid->error_integral = -pid->integral_max; } // 微分项:计算误差变化率,可选加入低通滤波 pid->error_derivative = pid->error - pid->error_last; // 简易微分,实际需除以dt if (pid->derivative_filter > 0) { // 一阶低通滤波,抑制高频噪声 pid->derivative_last = pid->derivative_filter * pid->derivative_last + (1 - pid->derivative_filter) * pid->error_derivative; pid->error_derivative = pid->derivative_last; } // PID输出 = P + I + D pid->output = (pid->kp * pid->error) + (pid->ki * pid->error_integral) + (pid->kd * pid->error_derivative); // 输出限幅 if (pid->output > pid->output_max) pid->output = pid->output_max; if (pid->output < pid->output_min) pid->output = pid->output_min; return pid->output; }代码关键点说明:
Cascade_PID_Calculate是核心:它清晰地展示了数据流向——外环输出成为内环目标。- 注意时间间隔:上述代码省略了微分项的时间间隔
dt。在实际嵌入式系统中,必须在固定的定时中断中调用PID计算函数,并根据真实的时间间隔(如0.002秒对应500Hz)来修正微分和积分项。通常ki和kd参数是已经考虑了dt的。 - 积分抗饱和:
integral_max非常重要,能防止系统在启动或遇到大误差时积分项过大,导致控制量饱和和超调。 - 微分滤波:微分项对噪声非常敏感,
derivative_filter提供了一个简单的低通滤波选项。
6. 功能测试与参数整定步骤
有了代码框架,下一步就是让它动起来并调好。参数整定是PID控制的“艺术”,对于串级PID,必须遵循严格的步骤。
6.1 测试环境搭建
- 硬件在环:将控制器代码烧录到飞控板,连接电机、电调和传感器。将飞控固定在测试台架上(非常重要!避免炸机)。通过遥控器或地面站发送角度指令。
- 软件仿真:在MATLAB/Simulink或Python中建立无人机姿态动力学模型,将上面的C代码改写成对应的仿真模块进行测试。这是最安全、成本最低的方法。
6.2 参数整定黄金法则:“先内后外,先P后I再D”
总原则:将外环断开,先调好内环;内环稳定后,再接入外环进行调试。
步骤一:整定内环(角速度环)
- 初始化:将外环PID参数全部设为0,使其输出为0。确保你能获取到可靠的角速度测量值(如陀螺仪数据)。
- 调 P(比例):
- 设定一个较小的目标角速度(例如 10 deg/s)。
inner_kp从0开始慢慢增大。- 观察现象:系统开始响应。继续增大
kp,直到系统出现持续、小幅度的等幅振荡。此时系统处于临界稳定状态。 - 记录:记下此时的
kp值为Ku(临界增益),并测量振荡周期Tu。
- 调 D(微分):
- 内环需要快速响应,微分项D对于抑制超调、增加阻尼非常有效。
- 根据经验公式,初步设定
kd = Ku * Tu / 8。 - 加入
kd后,振荡应该被抑制。微调kd,使系统能快速跟随目标角速度变化且无明显超调。
- 调 I(积分):
- 内环主要追求快速无静差跟踪,积分项I通常可以设为0,或者一个很小的值,用于消除稳态误差。
- 如果需要,加入一个很小的
ki,观察是否能消除静差。注意内环积分太强会导致系统反应迟钝。
理想的内环效果:给一个阶跃的目标角速度信号,实际角速度应能快速、平稳地跟上,超调小,调节时间短。
步骤二:整定外环(角度环)
- 连接:将调好的内环参数固定。现在,外环的输出将作为内环的目标。
- 调 P(比例):
- 设定一个目标角度(例如 10 度)。
outer_kp从0开始增大。- 观察现象:无人机会开始朝目标角度转动。继续增大
kp,直到角度响应出现轻微振荡或达到满意的响应速度。外环的振荡频率远低于内环。
- 调 D(微分):
- 外环的微分项有时可以不要,或者很小。如果需要,可以加入一点
kd来增加阻尼,抑制角度超调。
- 外环的微分项有时可以不要,或者很小。如果需要,可以加入一点
- 调 I(积分):
- 外环是加积分项的主要位置,用于消除角度静差(例如,在有风或重心不对称时仍能保持水平)。
- 加入一个较小的
outer_ki,观察角度能否稳定在目标值而无静差。积分项太强会引起系统缓慢振荡。
理想的串级效果:给定目标角度,无人机应平稳、快速地转动到该角度并保持稳定。内环像“肌肉”一样快速执行,外环像“大脑”一样精确指挥。
7. 资源占用与性能观察
串级PID本身计算量很小,在STM32F4等MCU上耗时通常小于10微秒,资源占用不是问题。真正的性能瓶颈和观察重点在于:
传感器数据频率与延迟:
- 内环频率:角速度环建议运行在500Hz以上(周期2ms)。确保陀螺仪的读取和滤波能跟上这个速度。
- 外环频率:角度环100-250Hz(周期4-10ms)通常足够。角度数据来自传感器融合(如互补滤波),需注意融合算法带来的延迟。
- 关键观察:用逻辑分析仪或翻转GPIO引脚的方式,测量从传感器数据就绪到PID计算完成的时间,必须小于控制周期。
控制输出延迟:
- PID计算出的PWM值,需要及时更新到定时器。确保PWM更新机制(如DMA、定时器中断)的延迟可控且稳定。
实时性保障:
- 将PID计算放在高优先级的定时器中断中。
- 避免在中断中进行浮点除法、三角函数等耗时操作。可以考虑使用
arm_math库(CMSIS-DSP)的浮点运算加速,或者使用定点数运算来进一步提升速度。
内存占用:
- 几个
float型变量和结构体,内存占用可忽略不计。
- 几个
性能测试方法:在代码中关键位置插入GPIO翻转语句,用示波器测量时间间隔,是评估实时性能最直接的方法。
8. 常见问题与排查方法
调试串级PID时,你会遇到各种现象。下表列出了典型问题及解决思路。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 系统剧烈发散振荡 | 1. 内环P值过大。 2. 内外环极性(正负反馈)错误。 | 1. 断开外环,单独测试内环是否稳定。 2. 检查误差计算符号 ( target - measure)。 | 1. 大幅降低内环P值,从零开始重调。 2. 确保控制器输出与系统响应方向正确。 |
| 内环稳定,加上外环后振荡 | 外环P值过大。 | 将外环P值减半观察。 | 遵循“先内后外”原则,降低外环P值,可能还需要微调外环D。 |
| 响应非常慢,像“软绵绵” | 1. 内环P值过小。 2. 积分项过强(尤其是外环I)。 | 1. 观察内环对阶跃信号的响应。 2. 暂时将外环I设为0。 | 1. 适当增加内环P值。 2. 减小积分系数,或加入积分限幅。 |
| 存在稳态误差(静差) | 缺少积分作用或积分作用太弱。 | 观察稳定后目标值与测量值的差值。 | 适当增加外环的I值。注意,应先尝试加大P值,若仍无法消除再加I。 |
| 电机发出高频啸叫 | 1. 控制频率进入人耳可闻范围(如8kHz以下)。 2. PWM频率与电机共振。 | 1. 测量控制循环实际频率。 2. 尝试改变PWM频率。 | 1. 确保控制频率足够高(>10kHz不现实,但输出滤波后可改善)。 2. 在PID输出后加入低通滤波器。 |
| 角度控制时缓慢漂移 | 1. 陀螺仪零漂未校准。 2. 加速度计数据受线性加速度污染。 | 1. 静止时读取陀螺仪输出是否为零。 2. 运动时观察角度融合结果是否合理。 | 1. 上电时进行陀螺仪零偏校准。 2. 优化传感器融合算法(如使用更可靠的DCM或四元数卡尔曼滤波)。 |
| 代码运行一次后卡死 | 1. 浮点运算溢出(如积分饱和后未限幅)。 2. 中断嵌套或优先级问题。 | 1. 检查所有变量范围,特别是积分项。 2. 检查中断服务函数是否过长或重入。 | 1. 加强变量限幅和异常值处理。 2. 简化中断服务程序,确保关键中断不被阻塞。 |
9. 最佳实践与使用建议
- 仿真先行:在MATLAB/Simulink或Python中搭建模型和控制器进行仿真。这能帮你快速理解参数影响,避免硬件损坏。
- 参数持久化:将调好的PID参数保存在飞控的Flash或EEPROM中,避免每次上电重新设置。
- 动态调整:可以考虑根据飞行模式(如自稳、定高、定点)切换不同的PID参数集。
- 加入死区:对于存在机械死区或静摩擦的系统,在PID输出端加入一个小的死区,可以避免电机持续微振。
- 输出滤波:在PID最终输出到执行器(如电机)之前,加一个一阶低通滤波器,可以平滑指令,减少机械磨损和噪声。
- 安全第一:
- 始终在台架或安全网内进行初步测试。
- 为所有控制量(角度、角速度、输出)设置软件限幅。
- 编写紧急停止机制,例如遥控器开关触发所有电机停转。
- 数据记录与回放:实现一个简单的飞行数据黑匣子功能,记录传感器数据、控制指令和PID输出。这对于分析异常飞行状态至关重要。
串级PID是无人机飞控的基石之一。理解其原理,掌握从代码实现到参数整定的完整流程,你就掌握了让无人机“听话”的核心技能。从调通第一个稳定的角速度环开始,到最终实现流畅的姿态控制,这个过程充满挑战,但解决问题的成就感也是巨大的。建议将本文的代码框架作为起点,结合具体的硬件平台和传感器,动手实践,积累自己的调试经验。