调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
无人机姿态环的特殊处理
直接把继电反馈法套到飞机上会遇到几个问题:
- 不能让飞机真的大幅震荡:继电器幅值要限制在安全范围内
- 需要在悬停状态下进行:有油门托底,姿态偏差不会导致炸机
- 三个轴要分开整定: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倍作为起点。
小结
继电反馈自整定是一种简单有效的方法,核心就是三步:施加继电器激励、测量震荡特征、套公式算参数。代码实现不复杂,关键是要结合实际飞行器做好安全保护和参数边界处理。
整定得到的参数可以作为一个不错的起点,如果对性能要求很高,后续还可以在此基础上手动微调,或者结合频域辨识做更精细的优化。