PID控制器的积分项导致极端不稳定性。

13
我在一台机器人上运行了一个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);
    }

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


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

9
看起来你将时间基准应用到了积分三次。误差已经是自上次采样以来累积的误差,所以你不需要将 deltaTime 乘以它。因此我会将代码更改为以下内容。
SteadyError += error;
SteadyError 是误差的积分或总和。
因此,积分应该只是 SteadyError * Ki。
float integral = Ki * SteadyError;
编辑:
我再次查看了你的代码,除了上述修复外,还有其他几个需要修复的问题。
1)你不想要毫秒级的 delta 时间。在正常的采样系统中,delta 项的值应该为 1,但是你却输入了一个像 50 这样的值,对于 20Hz 的速率,这会增加 Ki 这个因子并将 Kd 减小 50 倍。如果你担心抖动,那么你需要将 delta 时间转换为相对采样时间。我会使用以下公式。
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);
}

就“死区”和PID控制器而言,也许意图(虽然有些遥远)是让车辆指向任何方向,然后关闭...但这种逻辑似乎并没有真正体现出来,但也许当时是这样的,然后被其他无意识的编码人员“清理”了。 - Nick T
谢谢!这解决了很多问题!初步测试看起来成功,但是路上落满了秋叶,所以很难准确地看到机器人在做什么。 - chris12892
在PID循环中引入死区以避免输出响应是非线性时的“寻找”是常见的做法。 - Clifford
我喜欢这样 - 从PID计算中减少时间。DoPID(0)函数应在特定时间间隔内调用。您应该能够缓慢增加I增益并看到校正时间增加。 - BrianK
“1)你不需要毫秒级的增量时间。在一个正常的采样系统中,增量项应该为1,但是...”这里的“1”是什么意思?是一秒钟吗?积分计算中增量时间应该使用什么单位?显然,毫秒会导致更大的乘数,从而对输出的积分项产生不利影响。考虑到处理器可能会执行其他.NET任务,因此无法确保处理器每50ms精确地执行您的方法。我正在运行一个72MHz的时钟,但我不想假设时间,而是要进行计算。 - Gineer
显示剩余7条评论

5

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

您可能正在遭受 积分风扇效应 的困扰,它通常会消失,但如果它所需时间比您的车辆完成一次完整旋转(并再次引起积分器风扇效应)所需的时间长,则不会消失。普通解决方案是对积分器施加限制,但如果您的系统需要,还有更高级的解决方案(PDF,879 kB)。

Ki 是否具有正确的符号?

强烈反对使用浮点数作为 PID 参数,因为它们具有任意精度。使用整数(也许是 定点数)将不得不进行限制检查,但它比使用浮点数要合理得多。


使用浮点数进行PID计算是常态,不确定您为什么反对。这里没有任何任意精度类型。 - David Heffernan

4
积分项已经随时间累积,乘以deltaTime将使其以时间平方的速度累积。实际上,由于SteadyError已经通过将误差乘以deltaTime错误地计算出来,因此它是时间立方!
在SteadyError中,如果您正在尝试补偿非周期性更新,则最好修复非周期性。但是,在任何情况下,该计算都存在缺陷。您已经按照误差/时间单位进行了计算,而您只想要误差单位。如果真的有必要补偿定时抖动的算术正确方法应该是:
SteadyError += (error * 50.0f/deltaTime);

如果deltaTime仍以毫秒为单位,而名义更新率为20Hz。然而,如果您试图检测时间抖动,则最好将deltaTime计算为浮点数或根本不进行毫秒转换;您正在不必要地丢弃精度。无论哪种方式,您需要通过名义时间与实际时间的比例来修改误差值。

一篇很好的文章是《没有博士学位的PID控制》


谢谢提供《没有博士学位的PID》的链接,正是我所需要的。 - Drew Noakes

1

我不确定为什么你的代码不能正常工作,但我几乎可以肯定你无法测试它以查明原因。你可以注入一个计时器服务,这样你就可以模拟它并查看发生了什么:

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(),这样你就可以逐步执行代码并查看值的外观。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接