目录
控制系统
对生产中某些关键性参数进行自动控制,使它们在受到外界干扰(扰动)的影响而偏离正常状态时,能够被自动地调节而回到工艺所要求地数值范围内。
自动控制系统分为:开环、闭环。
闭环自动控制系统原理
闭环控制是负反馈控制。系统组成包括:传感器、控制装置、执行机构。
传感器检测被控对象的状态信息(输出量),并将其转变成电信号传给控制装置。
控制装置比较被控对象当前状态(输出量)和希望状态(给定量)的偏差,产生一个控制信号,通过执行机构驱动被控对象运行,使其运动状态接近希望状态。
开环自动控制系统原理
开环控制不能检测误差,不能够校正误差,只能按照事先确定好的程序和产生信号的条件,依次去控制对象并且无抑制的干扰能力。
PID
PID(Proportional比例、Integral积分、Differential微分)结合了比例、积分和微分,是闭环控制算法,是目前为止在连续控制系统中最为成熟的一种控制算法。
PID控制的实质是对目标值和实际值误差进行比例、积分、微分运算后的结果用来作用在输出上。
P、I、D是三种不同的调节作用,既可以单独使用(P、I、D),也可以两个两个用(PI、PD),也可以三个一起用(PID)。
PID最基本的三个参数:Kp、Ki、Kd。
PID控制是个对偏差的控制过程。
如果偏差为0,则比例环节不起作用,只有存在偏差时比例环节才起作用。
积分环节主要是用来消除系统稳定后输出值和设定值间的差值,积分环节实际上是偏差累计的过程,把累计的误差加到原有系统上以抵消系统造成的差值。
微分信号反应了偏差信号的变化规律(变化趋势),根据偏差信号的变化趋势来进行超前调节,从而增加了系统的预知性。
增量式与位置式区别
增量式算法不需要对积分项累加,控制量增量只与近几次的误差有关,计算误差对控制量计算的影响较小。而位置式算法要对近几次的偏差进行积分累加,容易产生较大的累加误差。
增量式算法控制输出的是控制量增量(例如在阀门控制中,只输出阀门开度的变化部分,误动作影响小,必要时还可通过逻辑判断限制或禁止本次输出,不会严重影响系统的工作)。而位置式算法的输出直接对应对象的输出,因此对系统影响较大。
增量式算法控制输出的是控制量增量,并无积分作用,因此适用于执行机构带积分部件的对象,如步进电机等。而位置式算法适用于执行机构不带积分部件的对象,如电液伺服阀。
在进行PID控制时,位置式PID需要有积分限幅和输出限幅。而增量式PID只需输出限幅。
位置式PID优缺点
优点:非递推式算法,可直接控制执行机构(如平衡小车),u(k)的值和执行机构的实际位置(如小车当前角度)是一一对应的,因此在执行机构不带积分部件的对象中可以很好的应用。
缺点:每次输出都和过去的状态有关,计算时要对e(k)进行累加,运算工作量大。
增量式PID优缺点
优点:
误动作时影响小,必要时可用逻辑判断的方法去除出错数据。
手动/自动切换时冲击小,便于实现无扰动切换。
算式中不需要累加。控制增量△u(k)的确定仅与最近3次的采样值有关。在速度闭环控制中有很好的实时性。
缺点:
积分截断效应大,有稳态误差。
溢出影响大。有的被控对象用增量式则不太好。
Kp、Ki、Kd的作用
举例水箱注水,要维持水箱水位在1m的高度。
假设刚开始水箱的水位是0.2m。则偏差为0.8m。现确定往里注水,取加水力度Kp为0.5。
则第一次加水时,加水量△u = 0.5 * 0.8 = 0.4m。接着第二次加水,当前水位是0.6m,则偏差为0.4m,△u = 0.5 * 0.4 = 0.2m,达到0.8m的水位量。
如此循环,可以预见最终水位会达到我们需要的1m。
一般情况下增大比例系数可以加快系统的响应,有助于减小静差。
再增加一些条件,水箱的水是要给别人使用的。假设每次加水前,别人都会使用掉0.1m的水位。假设Kp还是0.5,水位0.2m。
则第一次加水时,加水量△u = 0.5 * 0.8 = 0.4m。接着第二次加水,当前水位是0.5m,则偏差为0.5m,△u = 0.5 * 0.5 = 0.25m,达到0.75m的水位量。接着第三次加水,当前水位是0.65m,则偏差为0.35m,△u = 0.5 * 0.35 = 0.175m,达到0.825m的水位量。接着第四次加水,当前水位是0.725m,则偏差为0.275m,△u = 0.5 * 0.275 = 0.1375m,达到0.8625m的水位量。接着第五次加水,当前水位是0.7625m,则偏差为0.2375m,△u = 0.5 * 0.2375 = 0.11875m,达到0.88125m的水位量。如此循环,每次加水前都贴合0.8m,达到了平衡,水位将不再变化。
此时产生的误差就是稳态误差。此时稳态误差为0.2m。
一般情况下增大积分时间有利于减小超调,使系统稳定性增加,但是会增长消除静差的时间。
为了避免系统达到稳态却仍然不能达到目标值的情况,引入积分环节来消除稳态误差。
引入积分环节,相当于再加了一个小水龙头,给水箱注水。Ki项作用在整个环节累计的误差上,只要有误差,那么控制的力度就会不断的增大。
应用在实例中,与上述纯比例环节情况不同,引入积分环节后,即使比例环节达到了稳态,但是由于积分环节的存在,如果误差仍然存在时,那么小水龙头还会继续拧大,增加注水量,直到误差消除,此时积分项不再增大,输出维持不变。意思是在消除了误差的情况下,小水龙头的注水量和用水量也达到了平衡。
微分环节延续上面的系统,注水时突然来了个大爷给水箱加了一桶水,假设现在还没有超过预定的水位,那么在大爷加水前系统的偏差是不是比大爷加水后的偏差要大(如大爷加水前偏差水位是0.5m,加水后偏差是0.1米)。假设增加水位的速度是0.3m/次,那么下次加水就很容易漫出来。引入了微分环节的话,微分环节就会帮我们把水龙头给拧小,甚至会帮我们把水弄出去一些。
一般情况下微分环节具有超前调整的作用,抑制震荡。
PID调试一般原则
在输出不振荡时,增大比例增益P。
在输出不振荡时,减小积分时间常数Ti。
在输出不振荡时,增大微分时间常数Td。
确定P、I、D参数的一般步骤
确定比例增益P
首先去掉PID的积分项和微分项,一般是令Ti=0、Td=0,使PID为纯比例调节。
输入设定为系统允许的最大值的60%~70%,由0逐渐加大比例增益P,直到系统出现振荡。
然后在从此时的比例增益P逐渐减小,直到系统振荡消失。
记录此时的比例增益P,设定PID的比例增益P为当前值的60%~70%。比例增益P调试完成。
确定积分时间常数Ti
比例增益P确定后,设定一个较大的积分时间常数Ti的初值,然后逐渐减小Ti,直至系统出现振荡。
然后逐渐加大Ti,直到系统振荡消失。
记录此时的Ti,设定PID的积分时间常数Ti为当前值的150%~180%。积分时间常数Ti调试完成。
确定积分时间常数Td
积分时间常数Td一般不用设定,为0即可。若要设定,与前面方法一致,取不振荡时的30%。
系统空载、带载联调,再对PID参数进行微调,直至满足要求。
PID算法代码
位置式按键修改目标值
基本定时器配置TIM6
默认20ms为定时器周期,每20ms进一次中断处理函数。
/* 以下两宏仅适用于定时器时钟源TIMxCLK=84MHz,预分频器为:1680-1 的情况 */
#define SET_BASIC_TIM_PERIOD(T) __HAL_TIM_SET_AUTORELOAD(&TIM_TimeBaseStructure, (T)*50 - 1) // 设置定时器的周期(1~1000ms)
#define GET_BASIC_TIM_PERIOD() ((__HAL_TIM_GET_AUTORELOAD(&TIM_TimeBaseStructure)+1)/50.0) // 获取定时器的周期,单位ms
/**
* @brief 初始化基本定时器定时,默认20ms产生一次中断
* @param 无
* @retval 无
*/
void TIMx_Configuration(void)
{
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 1, 3);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
__TIM6_CLK_ENABLE();
// 80MHz/1680 -- 50kHz
// 定时器周期:20ms
TIM_TimeBaseStructure.Instance = TIM6;
TIM_TimeBaseStructure.Init.Period = 1000 - 1;
TIM_TimeBaseStructure.Init.Prescaler = 1680 - 1;
TIM_TimeBaseStructure.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM_TimeBaseStructure.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&TIM_TimeBaseStructure);
HAL_TIM_Base_Start_IT(&TIM_TimeBaseStructure);
#if defined(PID_ASSISTANT_EN)
// 计算定时器周期,单位ms。20ms
uint32_t temp = GET_BASIC_TIM_PERIOD();
set_computer_value(SEND_PERIOD_CMD, CURVES_CH1, &temp, 1); // 给通道 1 发送目标值
#endif
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim == (&TIM_TimeBaseStructure))
{
// 默认每20ms执行一次该函数
time_period_fun();
}
}
PID相关
初始化PID参数并向上位机发送PID参数,每20ms传入实际值执行一次pid算法,输出值(实际值)整数输出到上位机。
typedef struct
{
float target_val; //目标值
float actual_val; //实际值
float err; //定义偏差值
float err_last; //定义上一个偏差值
float Kp,Ki,Kd; //定义比例、积分、微分系数
float integral; //定义积分值
}_pid;
_pid pid;
void PID_param_init()
{
/* 初始化参数 */
pid.target_val = 0.0; //目标值
pid.actual_val = 0.0; //实际值
pid.err = 0.0; //定义偏差值
pid.err_last = 0.0; //定义上一个偏差值
pid.integral = 0.0; //定义积分值
pid.Kp = 0.01; //定义比例系数
pid.Ki = 0.80; //定义积分系数
pid.Kd = 0.04; //定义微分系数
#if defined(PID_ASSISTANT_EN)
float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
#endif
}
/**
* @brief 设置比例、积分、微分系数
* @param p:比例系数 P
* @param i:积分系数 i
* @param d:微分系数 d
* @note 无
* @retval 无
*/
void set_p_i_d(float p, float i, float d)
{
pid.Kp = p; // 设置比例系数 P
pid.Ki = i; // 设置积分系数 I
pid.Kd = d; // 设置微分系数 D
}
/**
* @brief PID算法实现
* @param val 实际值
* @note 无
* @retval 通过PID计算后的输出
*/
float PID_realize(float temp_val)
{
/*计算目标值与实际值的误差*/
pid.err = pid.target_val - temp_val;
/*误差累积*/
pid.integral += pid.err;
/*PID算法实现*/
pid.actual_val = pid.Kp * pid.err + pid.Ki * pid.integral + pid.Kd * (pid.err - pid.err_last);
/*误差传递*/
pid.err_last = pid.err;
/*返回当前实际值*/
return pid.actual_val;
}
/**
* @brief 定时器周期调用函数
* @param 无
*@note 无
* @retval 无
*/
int pid_status = 0;
void time_period_fun(void)
{
if (!pid_status)
{
float val = PID_realize(pid.actual_val);
// 上位机需要整数参数,转换一下
int temp = val;
set_computer_value(SEND_FACT_CMD, CURVES_CH1, &temp, 1); // 给通道 1 发送实际值
}
}
协议相关
后面会讲。
主体函数
float set_point = 0.0;
int main(void)
{
int run_i = 0;
int temp = set_point; // 上位机需要整数参数,转换一下
初始化工作
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &temp, 1); // 给通道 1 发送目标值
while (1)
{
/* 接收数据处理 */
receiving_process();
/* 按一次,预设目标值200 */
/* 再按一次,预设目标值0 */
if (Key_Scan(KEY2_GPIO_PORT, KEY2_PIN) == KEY_ON)
{
if (run_i % 2 == 0)
{
set_point = 200;
pid.target_val = set_point;
}
else
{
set_point = 0;
pid.target_val = set_point;
}
run_i++;
temp = set_point; // 上位机需要整数参数,转换一下
set_computer_value(SEND_TARGET_CMD, CURVES_CH1, &temp, 1); // 给通道 1 发送目标值
}
/* 按一次,停止pid调节,同步上位机的停止按钮状态 */
/* 再按一次,开始pid调节,同步上位机的启动按钮状态 */
if (Key_Scan(KEY3_GPIO_PORT, KEY3_PIN) == KEY_ON)
{
pid_status = !pid_status;
if (!pid_status)
{
set_computer_value(SEND_START_CMD, CURVES_CH1, NULL, 0); // 同步上位机的启动按钮状态
}
else
{
set_computer_value(SEND_STOP_CMD, CURVES_CH1, NULL, 0); // 同步上位机的停止按钮状态
}
}
}
}
速度式按键修改目标值
基本定时器配置TIM6
和上同。
PID相关
初始化PID参数并向上位机发送PID参数,每20ms传入实际值执行一次pid算法,输出值(实际值)整数输出到上位机。
typedef struct
{
float target_val; // 目标值
float actual_val; // 实际值
float err; // 定义当前偏差值
float err_next; // 定义下一个偏差值
float err_last; // 定义最后一个偏差值
float Kp, Ki, Kd; // 定义比例、积分、微分系数
}_pid;
_pid pid;
void PID_param_init()
{
/* 初始化参数 */
pid.target_val = 0.0; // 目标值
pid.actual_val = 0.0; // 实际值
pid.err = 0.0; // 定义偏差值
pid.err_last = 0.0; // 定义上一个偏差值
pid.err_next = 0.0; // 定义最后一个偏差值
pid.Kp = 0.20; //定义比例系数
pid.Ki = 0.20; //定义积分系数
pid.Kd = 0.0; //定义微分系数
float pid_temp[3] = {pid.Kp, pid.Ki, pid.Kd};
set_computer_value(SEND_P_I_D_CMD, CURVES_CH1, pid_temp, 3); // 给通道 1 发送 P I D 值
}
/**
* @brief 设置比例、积分、微分系数
* @param p:比例系数 P
* @param i:积分系数 i
* @param d:微分系数 d
* @note 无
* @retval 无
*/
void set_p_i_d(float p, float i, float d)
{
pid.Kp = p; // 设置比例系数 P
pid.Ki = i; // 设置积分系数 I
pid.Kd = d; // 设置微分系数 D
}
/**
* @brief PID算法实现
* @param val 目标值
* @note 无
* @retval 通过PID计算后的输出
*/
float PID_realize(float temp_val)
{
/*计算目标值与实际值的误差*/
pid.err = pid.target_val - temp_val;
/*PID算法实现*/
float increment_val = pid.Kp * (pid.err - pid.err_next) + pid.Ki * pid.err + pid.Kd * (pid.err - 2 * pid.err_next + pid.err_last);
/*累加*/
pid.actual_val += increment_val;
/*传递误差*/
pid.err_last = pid.err_next;
pid.err_next = pid.err;
/*返回当前实际值*/
return pid.actual_val;
}
/**
* @brief 定时器周期调用函数
* @param 无
*@note 无
* @retval 无
*/
int pid_status = 0;
void time_period_fun(void)
{
if (!pid_status)
{
float val = PID_realize(pid.actual_val);
// 上位机需要整数参数,转换一下
int temp = val;
set_computer_value(SEND_FACT_CMD, CURVES_CH1, &temp, 1); // 给通道 1 发送实际值
}
}
协议相关
后面会讲。
主体函数
和上同。