PID控制器积分/微分项计算、参数整定及实现优化咨询
天文成像PID控制器设计问题及优化建议
声明
我已在其他社区提出相同问题,因本社区规模较大再次提问,若有问题关联功能我会使用该功能。
核心问题
- 积分控制器计算:应取所有历史误差的均值还是仅前n个样本的均值?若选后者,n如何取值?
- 微分控制器计算:应拟合前m个点的直线,还是对所有历史误差求导后通过低通滤波器(LPF)?若选后者,求C++中耗时≤0.3ms的快速实现方法。
- 参数整定:标准PID形式中积分时间Ti和微分时间Td如何选择?用于抑制大气视宁度振荡(天文成像中,大气折射率随机变化导致焦平面图像位置振荡,典型频率500Hz,振幅1-6角秒),控制器帧率655Hz。
当前PID实现说明
全局变量说明:
- Kp、Ki、Kd:PID参数
- Ni:积分误差计算点数,<0则取所有历史点,否则取前Ni个点
- Nd:拟合直线的点数,斜率为微分值
- XTemp、YTemp:当前图像相对参考图像的位移量,校正值为其负值
- limMinInt、limMaxInt:积分输出的最小/最大值(暂未明确逻辑及取值)
- limMin、limMax:PID总输出的最小/最大值(暂未明确逻辑及取值)
- correctionShiftX、correctionShiftY:待求解的校正位移
- *correctionX、*correctionY:经转换矩阵后输出给执行器的校正值
当前实现仅能维持约3分钟的振荡抑制效果,需优化逻辑及参数。
代码实现
inline int getCorrection(double XTemp, double YTemp, deque<double> &integralErrorX, deque<double> &integralErrorY, deque<double> &derivativeErrorX, deque<double> &derivativeErrorY, uint64_t *integralCounter, uint64_t *derivativeCounter, double *correctionX, double *correctionY, uint64_t curr_count, uint64_t nbImagesReceived ) { uint64_t i; double toCorrectXTemp, toCorrectYTemp; double xMean = (double) (Nd + 1) / (double) 2; double xixbaryiybarX = 0, xixbaryiybarY = 0; double xixbarsq = 0; double integralErrorVoltageX = 0; double integralErrorVoltageY = 0; double derivativeErrorVoltageX = 0; double derivativeErrorVoltageY = 0; toCorrectXTemp = -1 * XTemp; toCorrectYTemp = -1 * YTemp; double correctionShiftX, correctionShiftY; if (Ni > 0) { if (integralErrorX.size() == Ni) { integralErrorX.pop_front(); integralErrorY.pop_front(); } integralErrorX.push_back(toCorrectXTemp); integralErrorY.push_back(toCorrectYTemp); if (integralErrorX.size() < Ni) { integralErrorVoltageX = 0; integralErrorVoltageY = 0; } else { integralErrorVoltageX = accumulate(integralErrorX.end() - Ni, integralErrorX.end(), 0.0) / Ni; integralErrorVoltageY = accumulate(integralErrorY.end() - Ni, integralErrorY.end(), 0.0) / Ni; } } else { if (integralErrorX.empty()) { integralErrorX.push_back(toCorrectXTemp); integralErrorY.push_back(toCorrectYTemp); integralErrorVoltageX = 0; integralErrorVoltageY = 0; } else { double sumX = integralErrorX.front(); double sumY = integralErrorY.front(); integralErrorX.pop_front(); integralErrorY.pop_front(); sumX += toCorrectXTemp; sumY += toCorrectYTemp; integralErrorX.push_back(sumX); integralErrorY.push_back(sumY); integralErrorVoltageX = sumX / (curr_count + 1); integralErrorVoltageY = sumY / (curr_count + 1); } } if (derivativeErrorX.size() == Nd) { derivativeErrorX.pop_front(); derivativeErrorY.pop_front(); } derivativeErrorX.push_back(toCorrectXTemp); derivativeErrorY.push_back(toCorrectYTemp); if (derivativeErrorX.size() < Nd) { derivativeErrorVoltageX = 0; derivativeErrorVoltageY = 0; } else { double meanDerivativeErrorVoltageX = 0; double meanDerivativeErrorVoltageY = 0; meanDerivativeErrorVoltageX = accumulate(derivativeErrorX.end()-Nd, derivativeErrorX.end(), 0.0) / Nd; meanDerivativeErrorVoltageY = accumulate(derivativeErrorY.end()-Nd, derivativeErrorY.end(), 0.0) / Nd; i = 0; for (auto it=derivativeErrorX.cbegin(); it!=derivativeErrorX.cend(); it++) { xixbaryiybarX += (*it - meanDerivativeErrorVoltageX) * (i + 1 - xMean); xixbarsq += ((i + 1 - xMean) * (i + 1 - xMean)); i += 1; } i = 0; for(auto it=derivativeErrorY.cbegin(); it!=derivativeErrorY.cend(); it++){ xixbaryiybarY += (*it - meanDerivativeErrorVoltageY) * (i + 1 - xMean); i += 1; } derivativeErrorVoltageX = xixbaryiybarX / xixbarsq; derivativeErrorVoltageY = xixbaryiybarY / xixbarsq; } double integralContributionX = Ki * integralErrorVoltageX; double integralContributionY = Ki * integralErrorVoltageY; if (integralContributionX <= limMinInt) { integralContributionX = limMinInt; } if (integralContributionX >= limMaxInt) { integralContributionX = limMaxInt; } if (integralContributionY <= limMinInt) { integralContributionY = limMinInt; } if (integralContributionY >= limMaxInt) { integralContributionY = limMaxInt; } correctionShiftX = Kp * toCorrectXTemp + integralContributionX + Kd * derivativeErrorVoltageX; correctionShiftY = Kp * toCorrectYTemp + integralContributionX + Kd * derivativeErrorVoltageY; if (correctionShiftX <= limMin) { correctionShiftX = limMin; } if (correctionShiftX >= limMax) { correctionShiftX = limMax; } if (correctionShiftY <= limMin) { correctionShiftY = limMin; } if (correctionShiftY >= limMax) { correctionShiftY = limMax; } *correctionX = A00*correctionShiftX + A01*correctionShiftY; *correctionY = A10*correctionShiftX + A11*correctionShiftY; sprintf( logString, "%ld, %ld, %lf, %lf, %lf, %lf, %lf, %lf, %lf, %lf, %lf, %lf", curr_count, nbImagesReceived, toCorrectXTemp, toCorrectYTemp, integralContributionX, integralContributionY, derivativeErrorVoltageX, derivativeErrorVoltageY, correctionShiftX, correctionShiftY, *correctionX, *correctionY ); shift_uncorected_log(logString); return 0; }
问题解答
1. 积分项计算方式选择
- 不建议取所有历史误差的均值:运行时间增加后,旧误差累积会导致积分项响应滞后,甚至引发积分饱和,这可能是当前仅能维持3分钟抑制效果的核心原因之一。
- 优先选择前n个样本的滑动窗口均值:本质是实现积分遗忘特性,让积分项更关注近期误差,避免饱和。
- n的取值:针对500Hz振荡、655Hz帧率,建议n对应0.10.5秒的样本数(65327个点)。噪声大时适当增大n,需快速响应时减小n,最终通过实验调整,以缓解积分饱和且保证稳态误差足够小为目标。
2. 微分项计算方式选择
- 拟合前m个点的直线:对噪声有一定抑制,但计算量随m增大而增加,m过大时会产生响应滞后。
- 求导后加低通滤波器(LPF):更适合快速系统,实现简单且耗时极低,推荐采用这种方式。
- C++快速LPF实现(一阶IIR低通,耗时远低于0.3ms):
一阶IIR公式:y(k) = α*y(k-1) + (1-α)*x(k),其中α为滤波系数,α = exp(-T/Tc),T是采样周期(约1.527ms,1/655Hz),Tc为时间常数。针对500Hz振荡,可设Tc为0.52ms(对应截止频率31879Hz,抑制高频噪声)。
代码实现(无需队列,仅需保存历史值):
该实现仅需基础加减乘运算,耗时微秒级,完全满足时间要求。// 全局变量,保存上一次的滤波结果和误差值 double prevDerivX = 0.0, prevDerivY = 0.0; double prevToCorrectX = 0.0, prevToCorrectY = 0.0; const double alpha = exp(-1.527e-3 / 1e-3); // 示例:Tc=1ms // 计算微分项 double rawDerivX = toCorrectXTemp - prevToCorrectX; double rawDerivY = toCorrectYTemp - prevToCorrectY; derivativeErrorVoltageX = alpha * prevDerivX + (1 - alpha) * rawDerivX; derivativeErrorVoltageY = alpha * prevDerivY + (1 - alpha) * rawDerivY; // 更新历史值 prevToCorrectX = toCorrectXTemp; prevToCorrectY = toCorrectYTemp; prevDerivX = derivativeErrorVoltageX; prevDerivY = derivativeErrorVoltageY;
3. Ti和Td参数选择(针对大气视宁度振荡)
标准PID中,Ki = Kp / Ti,Kd = Kp * Td,需先确定Kp,再推导Ti和Td:
- Kp整定:从0开始逐步增大,直到系统出现轻微振荡,取该值的0.6~0.8倍(临界比例度法)。初始值可从能让校正位移抵消一半振荡振幅开始测试。
- Ti(积分时间):需大于主要振荡周期(2ms),避免积分项放大振荡,建议取5~20ms。若积分饱和严重,可增大Ti。
- Td(微分时间):需小于振荡周期的1/4,避免过度响应噪声,建议取0.2~1ms。Td过小则微分作用弱,过大则易受干扰。
- 调整顺序:先调Kp至系统稳定且响应合理,再调Ki消除稳态误差,最后调Td抑制振荡。若系统不稳定,可适当减小Kd或增大Ti。
额外优化建议
- 积分分离:当误差绝对值超过阈值时暂停积分,小于阈值时再启用,比单纯限幅更有效抑制积分饱和。
- 输出限幅:limMin和limMax需根据执行器最大行程设置,避免输出超出硬件能力。
- 修复代码bug:计算correctionShiftY时误用了integralContributionX,应改为integralContributionY,该错误会直接影响Y轴控制效果。
内容的提问来源于stack exchange,提问作者Harsh M
相关产品推荐
相关产品推荐

