You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PID控制器积分/微分项计算、参数整定及实现优化咨询

天文成像PID控制器设计问题及优化建议

声明

我已在其他社区提出相同问题,因本社区规模较大再次提问,若有问题关联功能我会使用该功能。

核心问题

  1. 积分控制器计算:应取所有历史误差的均值还是仅前n个样本的均值?若选后者,n如何取值?
  2. 微分控制器计算:应拟合前m个点的直线,还是对所有历史误差求导后通过低通滤波器(LPF)?若选后者,求C++中耗时≤0.3ms的快速实现方法。
  3. 参数整定:标准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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 22:57:00