一、了解PID算法
PID 算法是自动控制算法中的经典之作,被广泛应用于温度控制、电机控制、飞行姿态控制等领域。简单来说就是用PID算法可以可以保持自动控制系统的稳定性(对关键参数进行自动控制,受到外界干扰而偏离正常状态时,能够被自动地调节回到目标范围内)。比如我们常用的烙铁,当烙铁头在焊接比较大的焊点时,设置的加热温度(350)会被拉低(280),这个时候有PID算法的介入,就可以让加热温度迅速回升到350。
二、算法入门
PID 分别是 Proportion(比例)、Integral(积分)、 Differential(微分)的首字母缩写。它是一种结合比例、积分和微分三个环节于一体的闭环控制算法。
控制流程:
核心就是不断的计算实际输出值与设置的值之间的误差,对这个误差进行比例、积分和微分三个环节的运算,将计算结果作用于执行机构,让实际输出值不算接近设置的值。
Proportion(比例):
比例环节可以成比例地反应控制系统的偏差信号,即输出与输入偏差成正比,可以用来减小系统的偏差。公式如下:
                                                                 u = Kp * e
                                              u:输出;Kp:比例系数;e:误差
假设设置温度为 350℃,而实际温度为 280℃,此时的偏差 e=70,由比例环节的公式可知,当 e 确定时,Kp 越大则输出u 越大,也就是温控系统的调节力度越大,这样就可以更快地达到目标温度;而当 Kp 确定时,偏差 e 越大则输出 u 越大,比例系数 Kp 和偏差 e 越大则系统消除偏差的时间越短
效果图:
Integral(积分):
积分环节可以对偏差 e 进行积分,只要存在偏差,积分环节就会不断起作用,主要用于消除静态误差,提高系统的无差度。引入积分环节后,比例+积分环节的公式如下:

                                            Ki:积分系数;\sume:累计偏差(偏差的和)

静态误差是什么:

假设我们现在需要设置温度350℃,而实际温度为340,此时偏差 e=10Kp 为固定值,如果此时的输出可以让烙铁头1s升温 10℃,而外部的温差的影响可以让烙铁1s内降温 10℃,也就是说,输出 u 的作用刚好被外部影响抵消了,这就使得偏差会一直存在。

只要系统还存在偏差,积分环节就会不断地累计偏差。当系统偏差为 0 的时候,说明已经达到目标值,此时的累计偏差不再变化,但是积分环节依旧在发挥作用(此时往往作用最大),这就很容易产生超调的现象了。因此,我们需要引入微分环节,提前减弱输出,抑制超调的发生。
Differential(微分):
微分环节可以反应偏差量的变化趋势,根据偏差的变化量提前作出相应控制,减小超调,克服振荡。比例+积分+微分环节的公式如下:

uk:第k次输出;
kp:比例系数;
ki:积分系数;
kd:微分系数;
ek:第 k 次偏差;
ek-1:第 k -1次偏差;
累计 k 次偏差
这就是最终得到的 PID 算法的一个公式,这个公式是位置式PID计算公式,借此可以推出增量式PID的公式(当然,这肯定不是我推的。到这里是不是已经懵完了?没关系,只要知道它是三条公式就行了):

位置式PID计算需要全部控制量参与,它的每一次输出都和过去的状态有关,需要对偏差进行累加,运算量大,而增量式 PID 的计算并不需要一直累计偏差,它的输出与近三次的偏差有很大关系,运算量少,实时性相对较好
三、通过代码实现PID算法
在代码中,我们可以通过一个结构体来管理公式这些控制量;
/*PID结构体*/
typedef struct
{
    volatile float  SetPoint;            /* 设置目标值 */
    volatile float  ActualValue;         /* 期望输出值/计算输出值 */
    volatile float  SumError;            /* 误差累计 */
    volatile float  Proportion;          /* 比例常数 P */
    volatile float  Integral;            /* 积分常数 I */
    volatile float  Derivative;          /* 微分常数 D */
    volatile float  Error;               /* 当前误差 */
    volatile float  LastError;           /* 上一次误差 */
    volatile float  PrevError;           /* 上上次误差 */
} PID_TypeDef;

增量式PID:

/**
 * @brief       pid闭环控制
 * @param       *PID:PID结构体变量地址
 * @param       Feedback_value:当前实际值
 * @retval      期望输出值/计算值
 */
int32_t increment_pid_ctrl(PID_TypeDef *PID,float Feedback_value)
{
    PID->Error = (float)(PID->SetPoint - Feedback_value);                   /* 计算偏差 */
    
    PID->ActualValue += (PID->Proportion * (PID->Error - PID->LastError))                          /* 比例环节 */
                        + (PID->Integral * PID->Error)                                             /* 积分环节 */
                        + (PID->Derivative * (PID->Error - 2 * PID->LastError + PID->PrevError));  /* 微分环节 */
    
    PID->PrevError = PID->LastError;                                        /* 存储偏差,用于下次计算 */
    PID->LastError = PID->Error;
    
    return ((int32_t)(PID->ActualValue));                                   /* 返回计算后输出的数值 */
}

看代码是不是非常简单,信心一下子回来。接下来举一个电机控制电流环的例子(电流不需要其他传感器,只需采样电阻,但是采样的误差很影响算法结果)。

控制流程:

1.ADC采集电流


/* 多通道ADC采集 DMA读取 */
ADC_HandleTypeDef ADC1_CUR_HANDLER;                 /* ADC句柄 */
DMA_HandleTypeDef DMA1_ADC1_HANDLER;                 /* 与ADC关联的DMA句柄 */

uint16_t g_adc_value[ADC_CH_NUM * ADC_COLL] = {0};      /* 存储ADC原始值 */


void adc_init(void)
{
    ADC_ChannelConfTypeDef sConfig = {0};

    ADC1_CUR_HANDLER.Instance = ADC_ADCX;                                       /* ADCx */
    ADC1_CUR_HANDLER.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;            /* 4分频,ADCCLK = PCLK2/4 = 84/4 = 21Mhz */
    ADC1_CUR_HANDLER.Init.Resolution = ADC_RESOLUTION_12B;                      /* 12位模式 */
    ADC1_CUR_HANDLER.Init.ScanConvMode = ENABLE;                                /* 扫描模式 多通道使用 */
    ADC1_CUR_HANDLER.Init.ContinuousConvMode = ENABLE;                          /* 连续转换模式,转换完成之后接着继续转换 */
    ADC1_CUR_HANDLER.Init.DiscontinuousConvMode = DISABLE;                      /* 禁止不连续采样模式 */
    ADC1_CUR_HANDLER.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; /* 使用软件触发 */
    ADC1_CUR_HANDLER.Init.ExternalTrigConv = ADC_SOFTWARE_START;                /* 软件触发 */
    ADC1_CUR_HANDLER.Init.DataAlign = ADC_DATAALIGN_RIGHT;                      /* 右对齐 */
    ADC1_CUR_HANDLER.Init.NbrOfConversion = ADC_CH_NUM;                         /* 使用转换通道数,需根据实际转换通道去设置 */
    ADC1_CUR_HANDLER.Init.DMAContinuousRequests = ENABLE;                       /* 开启DMA连续转换请求 */
    ADC1_CUR_HANDLER.Init.EOCSelection = ADC_EOC_SEQ_CONV;
    HAL_ADC_Init(&ADC1_CUR_HANDLER);

    /* 配置使用的ADC通道,采样序列里的第几个转换,增加或者减少通道需要修改这部分 */
    sConfig.Channel = ADC_ADCX_CH0;
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES;
    HAL_ADC_ConfigChannel(&ADC1_CUR_HANDLER, &sConfig);

    sConfig.Channel = ADC_ADCX_CH1;
    sConfig.Rank = 2;
    HAL_ADC_ConfigChannel(&ADC1_CUR_HANDLER, &sConfig);

    sConfig.Channel = ADC_ADCX_CH2;
    sConfig.Rank = 3;
    HAL_ADC_ConfigChannel(&ADC1_CUR_HANDLER, &sConfig);
    
}

/*ADC DMA读取 初始化函数*/
void adc_nch_dma_init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
      
    ADC_ADCX_CHY_CLK_ENABLE();                                  /* 使能ADCx时钟 */
    ADC_ADCX_CH0_GPIO_CLK_ENABLE();                             /* 开启GPIO时钟 */
    ADC_ADCX_CH1_GPIO_CLK_ENABLE();
    ADC_ADCX_CH2_GPIO_CLK_ENABLE();
    
    /* AD采集引脚模式设置,模拟输入 */
    GPIO_InitStruct.Pin = ADC_ADCX_CH0_GPIO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(ADC_ADCX_CH0_GPIO_PORT, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = ADC_ADCX_CH1_GPIO_PIN;
    HAL_GPIO_Init(ADC_ADCX_CH1_GPIO_PORT, &GPIO_InitStruct);
    
    GPIO_InitStruct.Pin = ADC_ADCX_CH2_GPIO_PIN;   
    HAL_GPIO_Init(ADC_ADCX_CH2_GPIO_PORT, &GPIO_InitStruct); 
    
    adc_init();                                                 /* 先初始化ADC */
    
    if ((uint32_t)ADC_ADCX_DMASx > (uint32_t)DMA2)              /* 大于DMA1_Channel7, 则为DMA2的通道了 */
    {
        __HAL_RCC_DMA2_CLK_ENABLE();                            /* DMA2时钟使能 */
    }
    else 
    {
        __HAL_RCC_DMA1_CLK_ENABLE();                            /* DMA1时钟使能 */
    }

    /* DMA配置 */
    DMA1_ADC1_HANDLER.Instance = ADC_ADCX_DMASx;                             /* 数据流x */
    DMA1_ADC1_HANDLER.Init.Channel = ADC_ADCX_DMASx_Chanel;                  /* DMA通道x */
    DMA1_ADC1_HANDLER.Init.Direction = DMA_PERIPH_TO_MEMORY;                 /* DIR = 1 ,外设到存储器模式 */
    DMA1_ADC1_HANDLER.Init.PeriphInc = DMA_PINC_DISABLE;                     /* 外设非增量模式 */
    DMA1_ADC1_HANDLER.Init.MemInc = DMA_MINC_ENABLE;                         /* 存储器增量模式 */
    DMA1_ADC1_HANDLER.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;    /* 外设数据长度:16位 */
    DMA1_ADC1_HANDLER.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;       /* 存储器数据长度:16位 */
    DMA1_ADC1_HANDLER.Init.Mode = DMA_CIRCULAR;                              /* 外设流控模式 */
    DMA1_ADC1_HANDLER.Init.Priority = DMA_PRIORITY_MEDIUM;                   /* 中等优先级 */
    HAL_DMA_Init(&DMA1_ADC1_HANDLER);
 
    __HAL_LINKDMA(&ADC1_CUR_HANDLER,DMA_Handle,DMA1_ADC1_HANDLER);       /* 把ADC和DMA关联,用DMA传输ADC数据 */
    
    /* ADC DMA中断配置 */
    HAL_NVIC_SetPriority(ADC_ADCX_DMASx_IRQn, 2, 1);
    HAL_NVIC_EnableIRQ(ADC_ADCX_DMASx_IRQn);
    
    HAL_ADC_Start_DMA(&ADC1_CUR_HANDLER,(uint32_t *)g_adc_value,ADC_SUM);    /* 开启ADC的DMA传输 */
}

/*DMA2 数据流4中断服务函数*/
void ADC_ADCX_DMASx_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&DMA1_ADC1_HANDLER);
}

uint16_t g_adc_val[ADC_CH_NUM];                     /*ADC平均值存放数组*/

/*ADC采集中断回调函数*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{

    float temp_c = 0.0;
    static float add_adc = 0;
    static float init_adc_value = 0;
    static uint8_t adc_count1 = 0;
    
    if ( hadc->Instance == ADC_ADCX )                               /* 判断是不是ADC1_CUR_HANDLER */
    { 
        adc_count1++;
        HAL_ADC_Stop_DMA(&ADC1_CUR_HANDLER);                    /* 关闭DMA转换 */
        
        calc_adc_val(g_adc_val);                                    /* 计算ADC的平均值 */
        add_adc += g_adc_val[2];                                    /* 取出电流通道对应的ADC值进行累计 */

        if (adc_count1 >= 15)                                       /* 累计15次 */
        {
            add_adc = (float)(add_adc / adc_count1);                /* 取平均值 */
                temp_c = (add_adc - init_adc_value) * ADC2CURT;                                                 /* 计算电流 */
                
                g_motor_data.current = (float)((g_motor_data.current * (float)0.60) + ((float)0.40 * temp_c));  /* 一阶低通滤波 */

                if (g_motor_data.current <= 20)                                                                 /* 过滤掉微弱浮动电流 */
                {
                    g_motor_data.current = 0.0;
                }

            add_adc = 0;
            adc_count1 = 0;
        }
        HAL_ADC_Start_DMA(&ADC1_CUR_HANDLER, (uint32_t *)&g_adc_value, (uint32_t)(ADC_SUM));                /* 启动DMA转换 */
    }
}

2.PID初始化

PID_TypeDef  g_current_pid;             /* 电流环PID参数结构体 */

/*pid初始化*/
void pid_init(void)
{
    g_current_pid.SetPoint = 10;        
    g_current_pid.ActualValue = 0.0;   
    g_current_pid.SumError = 0.0;      
    g_current_pid.Error = 0.0;          
    g_current_pid.LastError = 0.0;     
    g_current_pid.PrevError = 0.0;     
    g_current_pid.Proportion = KP;      
    g_current_pid.Integral = KI;        
    g_current_pid.Derivative = KD;      
}



int32_t increment_pid_ctrl(PID_TypeDef *PID,float Feedback_value)
{
    PID->Error = (float)(PID->SetPoint - Feedback_value);                   /* 计算偏差 */
    
    PID->ActualValue += (PID->Proportion * (PID->Error - PID->LastError))                          /* 比例环节 */
                        + (PID->Integral * PID->Error)                                             /* 积分环节 */
                        + (PID->Derivative * (PID->Error - 2 * PID->LastError + PID->PrevError));  /* 微分环节 */
    
    PID->PrevError = PID->LastError;                                        /* 存储偏差,用于下次计算 */
    PID->LastError = PID->Error;
    
    return ((int32_t)(PID->ActualValue));                                   /* 返回计算后输出的数值 */
}

3.PWM初始化

/* 定时器配置句柄 定义 */
TIM_HandleTypeDef TIM_PWM_HANDLER;                              /* 定时器x句柄 */

/**
 * @brief       高级定时器TIMX 互补输出 初始化函数(使用PWM模式1)
 * @note
 *              配置高级定时器TIMX 互补输出, 一路OCy 一路OCyN, 并且可以设置死区时间
 *
 *              高级定时器的时钟来自APB2, 而PCLK2 = 168Mhz, 我们设置PPRE2不分频, 因此
 *              高级定时器时钟 = 168Mhz
 *              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
 *              Ft=定时器工作频率, 单位 : Mhz
 *
 * @param       arr: 自动重装值。
 * @param       psc: 时钟预分频数
 */

void atim_timx_cplm_pwm_init(uint16_t arr, uint16_t psc)
{
    TIM_OC_InitTypeDef sConfigOC ;

    TIM_PWM_HANDLER.Instance = ATIM_TIMX_CPLM;                      /* 定时器x */
    TIM_PWM_HANDLER.Init.Prescaler = psc;                           /* 定时器预分频系数 */
    TIM_PWM_HANDLER.Init.CounterMode = TIM_COUNTERMODE_UP;          /* 向上计数模式 */
    TIM_PWM_HANDLER.Init.Period = arr;                              /* 自动重装载值 */
    TIM_PWM_HANDLER.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;    /* 时钟分频因子 */
    TIM_PWM_HANDLER.Init.RepetitionCounter = 0;                     /* 重复计数器寄存器为0 */
    TIM_PWM_HANDLER.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;        /* 使能影子寄存器TIMx_ARR */
    HAL_TIM_PWM_Init(&TIM_PWM_HANDLER) ;

    /* 设置PWM输出 */
    sConfigOC.OCMode = TIM_OCMODE_PWM1;                                     /* PWM模式1 */
    sConfigOC.Pulse = 0;                                                    /* 比较值为0 */
    sConfigOC.OCPolarity = TIM_OCPOLARITY_LOW;                              /* OCy 低电平有效 */
    sConfigOC.OCNPolarity = TIM_OCNPOLARITY_LOW;                            /* OCyN 低电平有效 */
    sConfigOC.OCFastMode = TIM_OCFAST_ENABLE;                               /* 不使用快速模式 */
    sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;                          /* 主通道的空闲状态 */
    sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;                        /* 互补通道的空闲状态 */
    HAL_TIM_PWM_ConfigChannel(&TIM_PWM_HANDLER, &sConfigOC, ATIM_TIMX_CPLM_CHY);    /* 配置后默认清CCER的互补输出位 */   
}

4.配置一个定时器中断定时计算PID并将结果输出到pwm

/**
 * @brief       定时器更新中断回调函数
 * @param        htim:定时器句柄指针
 * @note        此函数会被定时器中断函数共同调用的
 * @retval      无
 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
     int32_t motor_pwm_temp = 0;
    
     if (htim->Instance == TIM6)
     {
                /* PID计算,输出比较值(占空比),再进行一阶低通滤波 */
                motor_pwm_temp = increment_pid_ctrl(&g_current_pid, g_motor_data.current);
                g_motor_data.motor_pwm = (int32_t)((g_motor_data.motor_pwm * 0.5) + (motor_pwm_temp * 0.5));

                if (g_motor_data.motor_pwm >= 8200)                     /* 限制占空比 */
                {
                    g_motor_data.motor_pwm = 8200;
                }
                else if (g_motor_data.motor_pwm <= 0)
                {
                    g_motor_data.motor_pwm = 0;
                }


                motor_pwm_set(g_motor_data.motor_pwm);                                                  /* 设置占空比(电机转速) */            
     }
}

如果用stm32,外设驱动都可以在cubmax中生成。

四、参数(kp、ki、kd)整定

采样周期:

根据控制对象突变能力选择。假设电机当前转速为 20RPM,我们需要提高它的转速到 30RPM,此电机的转速在 1s 之内最大可以突变 10RPM(即电机速度的突变能力)。

kp、ki、kd:

结合系统的具体情况以及经验,先试凑几组合理的 PID 系数,同时需要观察系统的曲线变化,确定每一个系数对于整个系统曲线的大致影响,然后再根据具体的曲线进行调整。
1.先是比例(P),再积分(I),最后是微分(D);
2.按纯比例系统整定比例系数,使其得到比较理想的调节过程曲线,然后再把比例系数缩小 1.2 倍左右,将积分系数从小到大改变,使其得到较好的调节过程曲线;
3.在这个积分系数下重新改变比例系数,再看调节过程曲线有无改善;
4.如有改善,可将原整定的比例系数减少,改变积分系数,这样多次的反复,就可得到合适的比例系数和积分系数;
5. 如果存在外界的干扰,系统的稳定性不好,可把比例、积分系数适当减小,使系统足够稳定;
6. 如果系统存在小幅度超调,可以将整定好的比例系数和积分系数适当减小,增大微分系数,以得到超调量最小、调节作用时间最短的系统曲线;
也可以简单点,以调节速度为例(初始参数Kp、Ki、Kd为0):
  1. 调节比例增益Kp:逐渐增加Kp的值,直到系统开始出现持续的振荡(速度在设定值附近上下波动)。此时记录下Kp的值。
  2. 调节积分增益Ki:将Kp设置为刚刚Kp值的一半,然后逐渐增加Ki的值,直到系统的静态误差(速度与设定值的差距)变得很小。过大的Ki值可能导致系统响应过慢或振荡,此时需要适当减小Ki值。
  3. 调节微分增益Kd:保持当前的Kp和Ki值不变,逐渐增加Kd的值,直到系统的动态性能满足要求(例如,要求速度在设定值附近的波动范围和响应时间)。过大的Kd值可能导致系统响应过于敏感或噪声敏感,此时需要适当减小Kd值。
  4. 微调参数:以上步骤得到的Kp,Ki,Kd值可能仍然不是最优解,因此还需要根据实际系统的性能要求进行微调。通常情况下,可以先调整Kp和Ki,然后再调整Kd。在调整过程中,需要关注系统的稳定性、阻尼、响应速度、过冲等性能指标。

更多推荐