【问题标题】:PID controller integral term causing extreme instability导致极端不稳定的 PID 控制器积分项
【发布时间】:2011-04-23 14:17:24
【问题描述】:

我有一个在机器人上运行的 PID 控制器,旨在使机器人转向罗盘方向。 PID 校正以 20Hz 的速率重新计算/应用。

虽然 PID 控制器在 PD 模式下运行良好(即积分项为零),但即使是最轻微的积分也会迫使输出不稳定,从而将转向执行器推向左侧或右极端。

代码:

        private static void DoPID(object o)
    {
        // Bring the LED up to signify frame start
        BoardLED.Write(true);

        // Get IMU heading
        float currentHeading = (float)RazorIMU.Yaw;

        // We just got the IMU heading, so we need to calculate the time from the last correction to the heading read
        // *immediately*. The units don't so much matter, but we are converting Ticks to milliseconds
        int deltaTime = (int)((LastCorrectionTime - DateTime.Now.Ticks) / 10000);

        // Calculate error
        // (let's just assume CurrentHeading really is the current GPS heading, OK?)
        float error = (TargetHeading - currentHeading);

        LCD.Lines[0].Text = "Heading: "+ currentHeading.ToString("F2");

        // We calculated the error, but we need to make sure the error is set so that we will be correcting in the 
        // direction of least work. For example, if we are flying a heading of 2 degrees and the error is a few degrees
        // to the left of that ( IE, somewhere around 360) there will be a large error and the rover will try to turn all
        // the way around to correct, when it could just turn to the right a few degrees.
        // In short, we are adjusting for the fact that a compass heading wraps around in a circle instead of continuing
        // infinity on a line
        if (error < -180)
            error = error + 360;
        else if (error > 180)
            error = error - 360;

        // Add the error calculated in this frame to the running total
        SteadyError = SteadyError + (error * deltaTime);

        // We need to allow for a certain amount of tolerance.
        // If the abs(error) is less than the set amount, we will
        // set error to 0, effectively telling the equation that the
        // rover is perfectly on course.
        if (MyAbs(error) < AllowError)
            error = 0;

        LCD.Lines[2].Text = "Error:   " + error.ToString("F2");

        // Calculate proportional term
        float proportional = Kp * error;

        // Calculate integral term
        float integral = Ki * (SteadyError * deltaTime);

        // Calculate derivative term
        float derivative = Kd * ((error - PrevError) / deltaTime);

        // Add them all together to get the correction delta
        // Set the steering servo to the correction
        Steering.Degree = 90 + proportional + integral + derivative;

        // We have applied the correction, so we need to *immediately* record the 
        // absolute time for generation of deltaTime in the next frame
        LastCorrectionTime = DateTime.Now.Ticks;

        // At this point, the current PID frame is finished
        // ------------------------------------------------------------
        // Now, we need to setup for the next PID frame and close out

        // The "current" error is now the previous error
        // (Remember, we are done with the current frame, so in
        // relative terms, the previous frame IS the "current" frame)
        PrevError = error;

        // Done
        BoardLED.Write(false);
    }

有谁知道为什么会发生这种情况或如何解决它?

【问题讨论】:

  • 120 个字符长的行? 80 (79) 请。
  • 你在运行什么? PID 是一个实时应用程序,但 .Net Micro 上的 C# 不具备实时能力,并且大多数目标没有 FPU,因此也不建议使用浮点实现。

标签: c# embedded control-theory


【解决方案1】:

看起来您将时基应用于积分三次。 误差已经是自上次采样以来的累积误差,因此您不需要将 deltaTime 乘以它。所以我会将代码更改为以下内容。

SteadyError += error ;

SteadyError 是误差的积分或总和。

所以积分应该只是 SteadyError * Ki

float integral = Ki * SteadyError;

编辑:

我再次检查了您的代码,除了上述修复之外,我还需要修复其他几项。

1) 您不希望增量时间以毫秒为单位。在正常的采样系统中,增量项将是 1,但您为 20Hz 的速率输入了 50 之类的值,这具有将 Ki 增加该因子并将 Kd 减小 50 因子的效果。如果您担心抖动,则需要将增量时间转换为相对采样时间。我会改用公式。

float deltaTime = (LastCorrectionTime - DateTime.Now.Ticks) / 500000.0

500000.0 是每个样本的预期滴答数,对于 20Hz 为 50ms。

2) 将积分项保持在一个范围内。

if ( SteadyError > MaxSteadyError ) SteadyError = MaxSteadyError;
if ( SteadyError < MinSteadyError ) SteadyError = MinSteadyError;

3) 更改以下代码,以便当错误在 -180 左右时,您只需稍作更改就不会出错。

if (error < -270) error += 360;
if (error >  270) error -= 360;

4) 验证 Steering.Degree 正在接收正确的分辨率和符号。

5) 最后,您可以将 deltaTime 全部删除,然后按以下方式计算微分项。

float derivative = Kd * (error - PrevError);

所有这些你的代码都变成了。

private static void DoPID(object o)
{
    // Bring the LED up to signify frame start
    BoardLED.Write(true);

    // Get IMU heading
    float currentHeading = (float)RazorIMU.Yaw;


    // Calculate error
    // (let's just assume CurrentHeading really is the current GPS heading, OK?)
    float error = (TargetHeading - currentHeading);

    LCD.Lines[0].Text = "Heading: "+ currentHeading.ToString("F2");

    // We calculated the error, but we need to make sure the error is set 
    // so that we will be correcting in the 
    // direction of least work. For example, if we are flying a heading 
    // of 2 degrees and the error is a few degrees
    // to the left of that ( IE, somewhere around 360) there will be a 
    // large error and the rover will try to turn all
    // the way around to correct, when it could just turn to the right 
    // a few degrees.
    // In short, we are adjusting for the fact that a compass heading wraps 
    // around in a circle instead of continuing infinity on a line
    if (error < -270) error += 360;
    if (error >  270) error -= 360;

    // Add the error calculated in this frame to the running total
    SteadyError += error;

    if ( SteadyError > MaxSteadyError ) SteadyError = MaxSteadyError;
    if ( SteadyError < MinSteadyError ) SteadyError = MinSteadyError;

    LCD.Lines[2].Text = "Error:   " + error.ToString("F2");

    // Calculate proportional term
    float proportional = Kp * error;

    // Calculate integral term
    float integral = Ki * SteadyError ;

    // Calculate derivative term
    float derivative = Kd * (error - PrevError) ;

    // Add them all together to get the correction delta
    // Set the steering servo to the correction
    Steering.Degree = 90 + proportional + integral + derivative;

    // At this point, the current PID frame is finished
    // ------------------------------------------------------------
    // Now, we need to setup for the next PID frame and close out

    // The "current" error is now the previous error
    // (Remember, we are done with the current frame, so in
    // relative terms, the previous frame IS the "current" frame)
    PrevError = error;

    // Done
    BoardLED.Write(false);
}

【讨论】:

  • 虽然是一个错误,但不会像他观察到的那样导致饱和。
  • 我不知道你为什么在设置点的 AllowError 范围内将错误归零。这会引入一个死区,并且会在接近零时导致路线来回徘徊(因为没有错误信号来纠正错误)。特别是,积分项确实希望这个小错误能够使事情准确无误。当系统开始偏离设定值并将其拉回时,它会将小误差累加起来。比例项无法做到这一点。
  • if (MyAbs(error) &lt; AllowError) error = 0; 在原始代码中,但是是的,将它与一个完整的术语结合使用会达到目的。
  • 我同意,即使它是在错误被添加到积分之后并且它来自我删除它的原始代码。
  • 就“死区”和 PID 控制器而言,可能的意图(尽管有些被删除)是让车辆指向任何方向然后关闭......这个逻辑似乎并不真正完全可以通过,但也许就是这样,然后被其他一些不知情的编码员“清理”了。
【解决方案2】:

你在初始化SteadyError(奇怪的名字……为什么不是“积分器”)?如果它在启动时包含一些随机值,它可能永远不会返回接近零 (1e100 + 1 == 1e100)。

您可能正在遭受integrator windup 的困扰,它通常应该会消失,但如果减少所需的时间比您的车辆完成完整旋转(并再次启动积分器)所需的时间更长,则不会。简单的解决方案是对积分器施加限制,但如果您的系统需要,也有 more advanced solutions (PDF, 879 kB)。

Ki 的符号是否正确?

我会强烈不鼓励将浮点数用于 PID 参数,因为它们的精度是任意的。使用整数(可能是fixed point)。您将不得不进行限制检查,但这比使用浮点数要明智得多。

【讨论】:

  • 使用浮点进行 PID 计算是常态,不知道您为什么反对。这里没有任意精度类型。
【解决方案3】:

积分项已经随时间累积,乘以 deltaTime 将使其以时间平方的速率累积。事实上,由于 SteadyError 已经通过将误差乘以 deltaTime 计算得出错误,因此是时间立方的!

在 SteadyError 中,如果您试图补偿非周期性更新,最好修复非周期性。但是,无论如何,计算都是有缺陷的。您已经以错误/时间为单位进行了计算,而您只需要错误单位。如果确实需要,补偿时序抖动的算术正确方法是:

SteadyError += (error * 50.0f/deltaTime);

如果 deltaTime 仍以毫秒为单位,并且标称更新速率为 20Hz。但是,如果您尝试检测的是时序抖动,则 deltaTime 最好计算为浮点数或根本不转换为毫秒;您不必要地丢弃了精度。无论哪种方式,您都需要通过标称时间与实际时间的比率来修改误差值。

一个很好的阅读是PID without a PhD

【讨论】:

  • 感谢没有博士学位的 PID 链接。正是我想要的。
【解决方案4】:

我不确定您的代码为什么不起作用,但我几乎可以肯定您也无法对其进行测试以了解原因。您可以注入一个计时器服务,以便模拟它并查看发生了什么:

public interace ITimer 
{
     long GetCurrentTicks()
}

public class Timer : ITimer
{
    public long GetCurrentTicks() 
    {
        return DateTime.Now.Ticks;
    }
}

public class TestTimer : ITimer
{
    private bool firstCall = true;
    private long last;
    private int counter = 1000000000;

    public long GetCurrentTicks()
    {
        if (firstCall)
            last = counter * 10000;
        else
            last += 3500;  //ticks; not sure what a good value is here

        //set up for next call;
        firstCall = !firstCall;
        counter++;

        return last;
    }
}

然后,将两个对DateTime.Now.Ticks 的调用替换为GetCurrentTicks(),然后您可以单步执行代码并查看值的样子。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-07
    • 1970-01-01
    • 1970-01-01
    • 2019-02-27
    • 1970-01-01
    • 1970-01-01
    • 2020-05-05
    • 1970-01-01
    相关资源
    最近更新 更多