PID算法(简单明了)
·
一、了解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:积分系数;e:累计偏差(偏差的和)
静态误差是什么:
假设我们现在需要设置温度350℃,而实际温度为340℃,此时偏差 e=10,Kp 为固定值,如果此时的输出可以让烙铁头1s升温 10℃,而外部的温差的影响可以让烙铁1s内降温 10℃,也就是说,输出 u 的作用刚好被外部影响抵消了,这就使得偏差会一直存在。

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

uk:第k次输出;
kp:比例系数;
ki:积分系数;
kd:微分系数;
ek:第 k 次偏差;
ek-1:第 k -1次偏差;
这就是最终得到的 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):
- 调节比例增益Kp:逐渐增加Kp的值,直到系统开始出现持续的振荡(速度在设定值附近上下波动)。此时记录下Kp的值。
- 调节积分增益Ki:将Kp设置为刚刚Kp值的一半,然后逐渐增加Ki的值,直到系统的静态误差(速度与设定值的差距)变得很小。过大的Ki值可能导致系统响应过慢或振荡,此时需要适当减小Ki值。
- 调节微分增益Kd:保持当前的Kp和Ki值不变,逐渐增加Kd的值,直到系统的动态性能满足要求(例如,要求速度在设定值附近的波动范围和响应时间)。过大的Kd值可能导致系统响应过于敏感或噪声敏感,此时需要适当减小Kd值。
- 微调参数:以上步骤得到的Kp,Ki,Kd值可能仍然不是最优解,因此还需要根据实际系统的性能要求进行微调。通常情况下,可以先调整Kp和Ki,然后再调整Kd。在调整过程中,需要关注系统的稳定性、阻尼、响应速度、过冲等性能指标。
更多推荐



所有评论(0)