直道抖动这件事占了我好几天。一开始我认定是微分系数给大了,在 Kd 上来回调了几轮,小车该摆还是摆,幅度不算大但特别扎眼,放地上跑两圈就没了继续调下去的耐心。
后来我把注意力挪回 app.c,翻到一半才想起来,死区这东西我早就写过,怎么一点动静都没有。
#define PID_DEADBAND 1.0f /* PID死区 */
使用处在 app_loop 里,放在 PID_Calc 调用之后:
float pid_output = PID_Calc(&position_pid, current_position, target_position);
if (fabs(pid_output) < PID_DEADBAND)
{
pid_output = 0.0f;
}
阈值 1.0f,而这一路位置环的输出限幅是 1000:
const float position_pid_params[3] = {8.0f, 0.0f, 1.0f};
PID_Init(&position_pid, PID_POSITION, position_pid_params, 1000.0f, 100.0f);
两组数放一起看就很清楚了。1.0f 是给量级在 1 附近的量准备的阈值,这里的输出量级是几百,中间差了三个数量级。
拿 Kp = 8 反推更直观。要让死区生效,pid_output 得小于 1.0,而比例项就是 8 乘位置偏差,光这一项就要求偏差小于 0.125,再算上微分项,条件只会更苛刻。位置值由八路灰度的加权质心算出来,权重是 7、5、3、1、-1、-3、-5、-7,压线的时候偏差以 1 为单位往上走,0.125 这种状态在实际跑道上基本不存在。死区写进程序了,那个 if 一次都没进去过。
顺手把这一路输出的真实范围也算了一遍。位置值取的是权重加权平均,权重最大到 7,所以 current_position 落在 -7 到 7 之间,误差最多 7。比例项 Kp = 8,顶天 56;微分项最多 14;积分项被 max_iout 限在 100。三项加起来也就一百七十上下,那个 1000 的限幅,按这套参数根本够不着。再往下一层还有个数,电机限幅 MOTOR_MAX_SPEED 是 800。死区 1.0、PID 限幅 1000、电机限幅 800,三个量级各说各话,全是同一批写下来的。
还有个更麻烦的地方。死区判断放在 PID_Calc 外面,能清掉的只有返回值,而积分项是在 pid.c 函数内部累加的:
pid->Iout += pid->Ki * pid->error[0];
现在这一路的 Ki 是 0,Iout 恒等于 0,所以问题还藏在下面。哪天真把 Ki 调上去,Iout 会一路累加到 PID_Init 里那个 100.0f 的积分限幅上,输出里就挂着一个 ±100 的常量,本身就超过死区了。死区这一拍把输出清零,下一拍积分又把它顶回来。拦得住输出,拦不住积分。
这个死区我最后没改,PID_DEADBAND 到现在还是 1.0f。要正经处理得做两件事,一是死区和限幅按同一把尺子给,比如取 max_out 的一个百分比,二是死区不能只砍输出,得同时把积分项按住,不然 Ki 一旦不为 0 就等于给自己埋雷。
这几天最直接的教训是,调参之前先把量级对一遍。我盯着 Kd 折腾了好几天,真正的问题在一行 #define 上,1.0f 对着 1000 的限幅,写和不写没区别。