无人机PID参数自整定:从原理到工程实现

调PID这件事,玩过飞控的人都懂那种痛苦。参数给小了响应慢吞吞,给大了震荡停不下来,全靠手感一点点试。这篇文章聊聊怎么让飞控自己把PID参数调出来,省得我们对着波形发呆。


为什么需要自整定

手动调参的经典流程大家都熟悉:先把I和D清零,慢慢加P直到出现等幅震荡,然后回退一点,再加I消除静差,最后用D压一压超调。这套方法在实验室里调一台飞机没问题,但如果你要量产,每台机器的电机、桨叶、机架重量都有细微差异,挨个手调根本不现实。

自整定的核心思路是:让飞控主动施加一些激励信号,观察系统响应,然后根据响应特征反推出合适的PID参数。


继电反馈法:工程里最实用的方案

在各种自整定方法里,继电反馈法(Relay Feedback Method)是工程上用得最多的。它的原理不复杂:用一个继电器(bang-bang控制)替代PID控制器,系统会自发产生一个极限环震荡,通过测量这个震荡的幅值和周期,就能算出临界增益和临界周期,进而套用Ziegler-Nichols公式得到PID参数。

原理分析

假设我们的被控对象是一个典型的姿态环,继电器输出在 +d 和 -d 之间切换。当系统进入稳态震荡时,会呈现出近似正弦的周期运动。

设震荡的幅值为 aaa,周期为 TuT_uTu,则临界增益可以用描述函数法近似计算:

Ku=4dπaK_u = \frac{4d}{\pi a}Ku=πa4d

这个公式的物理意义是:继电器的等效增益等于其基波分量的幅值与输出幅值的比值。

拿到 KuK_uKu 和 TuT_uTu 之后,Ziegler-Nichols给出的经典整定公式是:

控制器类型 KpK_pKp TiT_iTi TdT_dTd
P 0.5Ku0.5K_u0.5Ku - -
PI 0.45Ku0.45K_u0.45Ku Tu/1.2T_u/1.2Tu/1.2 -
PID 0.6Ku0.6K_u0.6Ku Tu/2T_u/2Tu/2 Tu/8T_u/8Tu/8

转换成我们常用的增量式PID形式:

Ki=KpTi,Kd=Kp⋅TdK_i = \frac{K_p}{T_i}, \quad K_d = K_p \cdot T_dKi=TiKp,Kd=Kp⋅Td

无人机姿态环的特殊处理

直接把继电反馈法套到飞机上会遇到几个问题:

  1. 不能让飞机真的大幅震荡:继电器幅值要限制在安全范围内
  2. 需要在悬停状态下进行:有油门托底,姿态偏差不会导致炸机
  3. 三个轴要分开整定:Roll/Pitch/Yaw的动力学特性不同

实际工程中,我们通常在内环(角速度环)做自整定,外环(角度环)用相对保守的固定参数或者简单比例关系推算。


代码实现

下面是一个可以嵌入飞控的自整定模块,以单轴角速度环为例:

c 复制代码
#include <math.h>
#include <stdbool.h>

typedef struct {
    // 继电器参数
    float relay_amplitude;      // 继电器输出幅值
    float hysteresis;           // 迟滞量,防止高频抖动
    
    // 测量结果
    float oscillation_amplitude;
    float oscillation_period;
    float ku;                   // 临界增益
    float tu;                   // 临界周期
    
    // 整定得到的PID参数
    float kp;
    float ki;
    float kd;
    
    // 内部状态
    bool relay_state;           // true: +d, false: -d
    float last_zero_crossing;   // 上次过零时刻
    float peak_value;
    float valley_value;
    int half_period_count;
    float period_sum;
    float amplitude_sum;
    
    // 配置
    int periods_to_measure;     // 测量多少个周期取平均
    bool tuning_complete;
} AutoTuner;

void autotune_init(AutoTuner *at, float relay_amp, float hyst)
{
    at->relay_amplitude = relay_amp;
    at->hysteresis = hyst;
    at->relay_state = true;
    at->last_zero_crossing = 0;
    at->peak_value = -1e10f;
    at->valley_value = 1e10f;
    at->half_period_count = 0;
    at->period_sum = 0;
    at->amplitude_sum = 0;
    at->periods_to_measure = 6;  // 测3个完整周期
    at->tuning_complete = false;
}

/*
 * 运行自整定
 * error: 当前角速度误差(目标 - 实际)
 * current_time: 当前时间戳(秒)
 * 返回值: 控制器输出
 */
float autotune_update(AutoTuner *at, float error, float current_time)
{
    if (at->tuning_complete) {
        return 0;  // 整定完成后不再输出
    }
    
    // 记录峰值和谷值
    if (error > at->peak_value) {
        at->peak_value = error;
    }
    if (error < at->valley_value) {
        at->valley_value = error;
    }
    
    // 继电器切换逻辑(带迟滞)
    bool should_switch = false;
    if (at->relay_state && error < -at->hysteresis) {
        should_switch = true;
    } else if (!at->relay_state && error > at->hysteresis) {
        should_switch = true;
    }
    
    if (should_switch) {
        // 每次切换意味着过了半个周期
        float half_period = current_time - at->last_zero_crossing;
        at->last_zero_crossing = current_time;
        
        // 忽略第一次切换(数据不完整)
        if (at->half_period_count > 0) {
            at->period_sum += half_period * 2;
            
            // 计算本次半周期的幅值
            float amp;
            if (at->relay_state) {
                // 从正切到负,记录的是正半周期的峰值
                amp = at->peak_value;
            } else {
                // 从负切到正,记录的是负半周期的谷值
                amp = -at->valley_value;
            }
            at->amplitude_sum += amp;
        }
        
        at->half_period_count++;
        
        // 重置峰谷值
        at->peak_value = -1e10f;
        at->valley_value = 1e10f;
        
        // 切换继电器状态
        at->relay_state = !at->relay_state;
        
        // 检查是否测量完成
        if (at->half_period_count >= at->periods_to_measure + 1) {
            autotune_calculate(at);
        }
    }
    
    // 输出继电器信号
    return at->relay_state ? at->relay_amplitude : -at->relay_amplitude;
}

void autotune_calculate(AutoTuner *at)
{
    int valid_samples = at->half_period_count - 1;
    
    // 计算平均周期和幅值
    at->oscillation_period = at->period_sum / (valid_samples / 2);
    at->oscillation_amplitude = at->amplitude_sum / valid_samples;
    
    // 计算临界增益
    at->tu = at->oscillation_period;
    at->ku = (4.0f * at->relay_amplitude) / (M_PI * at->oscillation_amplitude);
    
    // Ziegler-Nichols PID公式
    at->kp = 0.6f * at->ku;
    at->ki = at->kp / (at->tu / 2.0f);
    at->kd = at->kp * (at->tu / 8.0f);
    
    at->tuning_complete = true;
}

// 获取整定结果
bool autotune_get_result(AutoTuner *at, float *kp, float *ki, float *kd)
{
    if (!at->tuning_complete) {
        return false;
    }
    *kp = at->kp;
    *ki = at->ki;
    *kd = at->kd;
    return true;
}

调用示例

c 复制代码
AutoTuner roll_tuner;
float gyro_roll;           // 来自陀螺仪的角速度
float roll_rate_target;    // 期望角速度
float motor_output;

// 初始化:继电器幅值设为满油门的15%,迟滞量5deg/s
autotune_init(&roll_tuner, 0.15f, 5.0f);

// 在姿态控制循环中调用(替代原PID控制器)
void attitude_control_loop(float dt, float current_time)
{
    float error = roll_rate_target - gyro_roll;
    
    if (!roll_tuner.tuning_complete) {
        motor_output = autotune_update(&roll_tuner, error, current_time);
    } else {
        // 整定完成,切换到正常PID控制
        float kp, ki, kd;
        autotune_get_result(&roll_tuner, &kp, &ki, &kd);
        motor_output = pid_calculate(kp, ki, kd, error, dt);
    }
}

ZN公式的局限与改进

经典Ziegler-Nichols公式整定出来的参数,超调量通常偏大(可能到25%),对于飞行器这种对平稳性要求高的场合,直接用往往不太合适。

几种常见的改进方案:

Tyreus-Luyben公式

比ZN保守一些,超调更小:

Kp=0.45Ku,Ti=2.2Tu,Td=Tu/6.3K_p = 0.45K_u, \quad T_i = 2.2T_u, \quad T_d = T_u/6.3Kp=0.45Ku,Ti=2.2Tu,Td=Tu/6.3

带衰减系数的修正

引入一个衰减系数来降低激进程度:

c 复制代码
float damping_factor = 0.7f;  // 0.5~0.8之间调整
at->kp = 0.6f * at->ku * damping_factor;
at->ki = at->kp / (at->tu / 2.0f) * damping_factor;
at->kd = at->kp * (at->tu / 8.0f);

基于相位裕度的整定

如果要更精细的控制,可以不用ZN公式,而是直接根据期望的相位裕度来计算。继电反馈法本质上测量的是系统在临界频率点的信息,结合一些额外假设可以推算其他频率点的特性。这块涉及的数学比较多,有兴趣可以查阅Åström和Hägglund的相关论文。


飞控中的实际整合

把自整定功能做进飞控,还需要考虑以下几点:

状态机设计

c 复制代码
typedef enum {
    TUNE_IDLE,
    TUNE_INIT,
    TUNE_ROLL,
    TUNE_PITCH,
    TUNE_YAW,
    TUNE_COMPLETE,
    TUNE_FAILED
} TuneState;

三个轴依次整定,每个轴整定期间其他轴使用保守的默认参数维持稳定。

安全保护

c 复制代码
// 在autotune_update中加入保护
if (fabsf(error) > MAX_SAFE_ERROR) {
    at->tuning_complete = true;  // 强制退出
    return 0;  // 输出置零,让默认控制器接管
}

// 超时保护
if (current_time - at->start_time > MAX_TUNE_TIME) {
    // 可能系统不稳定或参数选择不当
    return TUNE_FAILED;
}

参数存储

整定完成后,把结果写入EEPROM或Flash,下次上电直接使用。


调试技巧

实际调试时,把以下数据通过串口或日志输出,方便分析:

c 复制代码
typedef struct {
    float timestamp;
    float error;
    float relay_output;
    bool relay_state;
    float peak;
    float valley;
} TuneLog;

观察波形是否接近正弦、周期是否稳定。如果震荡很不规则,可能是继电器幅值太小(激励不够)或者太大(非线性太强)。

迟滞量的选择也很关键:太小会导致继电器在噪声作用下高频抖动,太大会让震荡波形失真。一般取传感器噪声幅值的3~5倍作为起点。


小结

继电反馈自整定是一种简单有效的方法,核心就是三步:施加继电器激励、测量震荡特征、套公式算参数。代码实现不复杂,关键是要结合实际飞行器做好安全保护和参数边界处理。

整定得到的参数可以作为一个不错的起点,如果对性能要求很高,后续还可以在此基础上手动微调,或者结合频域辨识做更精细的优化。

相关推荐
Coovally AI模型快速验证5 小时前
计算机视觉的 2026:从“堆算力”竞赛,到“省算力”智慧
人工智能·深度学习·算法·yolo·计算机视觉·无人机
小O的算法实验室15 小时前
2024年ESWA SCI1区TOP,基于遗传算法的多无人机同时到达和资源约束的协同任务分配,深度解析+性能实测
无人机·论文复现·智能算法·智能算法改进
wheeldown1 天前
【数学建模】用代码搞定无人机烟幕:怎么挡导弹最久?
数学建模·无人机
陈奕昆1 天前
保姆级教程!零基础解锁大疆无人机开发:MSDK/PSDK/ 上云 API 实战指南[特殊字符]
无人机·sdk·大疆·企业级大疆二次开发
StarChainTech1 天前
无人机租赁平台:开启智能租赁新时代
大数据·人工智能·微信小程序·小程序·无人机·软件需求
WinstonJQ1 天前
AirSim无人机仿真入门(一):实现无人机的起飞与降落
python·机器人·游戏引擎·ue4·无人机
抬头望远方1 天前
【无人机】无人机群在三维环境中的碰撞和静态避障仿真(Matlab代码实现)
开发语言·支持向量机·matlab·无人机
无人装备硬件开发爱好者1 天前
无人机 5.8G 模拟图传电路设计方案及性能分析
无人机·图传硬件电路设计·硬件电路设计·5.8g图传
matlab科研助手1 天前
【路径规划】基于遗传算法的农药无人机在多边形区域的路径规划研究附Matlab代码
开发语言·matlab·无人机