PID学习笔记1
在学习江协科技PID课程时,做一些笔记,对应视频1-4,对应代码:02,03,04,05
02-位置式PID定速控制
main.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "RP.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"uint8_t KeyNum;/*定义变量*/
float Target, Actual, Out; //目标值,实际值,输出值
float Kp, Ki, Kd; //比例项,积分项,微分项的权重
float Error0, Error1, ErrorInt; //本次误差,上次误差,误差积分int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化Key_Init(); //非阻塞式按键初始化Motor_Init(); //电机初始化Encoder_Init(); //编码器初始化RP_Init(); //电位器旋钮初始化Serial_Init(); //串口初始化,波特率9600Timer_Init(); //定时器初始化,定时中断时间1ms/*OLED打印一个标题*/OLED_Printf(0, 0, OLED_8X16, "Speed Control");OLED_Update();while (1){/*按键修改目标值*//*解除以下注释后,记得屏蔽电位器旋钮修改目标值的代码*/
// KeyNum = Key_GetNum(); //获取键码
// if (KeyNum == 1) //如果K1按下
// {
// Target += 10; //目标值加10
// }
// if (KeyNum == 2) //如果K2按下
// {
// Target -= 10; //目标值减10
// }
// if (KeyNum == 3) //如果K3按下
// {
// Target = 0; //目标值归0
// }/*电位器旋钮修改Kp、Ki、Kd和目标值*//*RP_GetValue函数返回电位器旋钮的AD值,范围:0~4095*//* 除4095.0可以把AD值归一化,再乘上一个系数,可以调整到一个合适的范围*/Kp = RP_GetValue(1) / 4095.0 * 2; //修改Kp,调整范围:0~2Ki = RP_GetValue(2) / 4095.0 * 2; //修改Ki,调整范围:0~2Kd = RP_GetValue(3) / 4095.0 * 2; //修改Kd,调整范围:0~2Target = RP_GetValue(4) / 4095.0 * 300 - 150; //修改目标值,调整范围:-150~150/*OLED显示*/OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", Kp); //显示KpOLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", Ki); //显示KiOLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", Kd); //显示KdOLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", Target); //显示目标值OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", Actual); //显示实际值OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", Out); //显示输出值OLED_Update(); //OLED更新,调用显示函数后必须调用此函数更新,否则显示的内容不会更新到OLED上Serial_Printf("%f,%f,%f\r\n", Target, Actual, Out); //串口打印目标值、实际值和输出值//配合SerialPlot绘图软件,可以显示数据的波形}
}void TIM1_UP_IRQHandler(void)
{/*定义静态变量(默认初值为0,函数退出后保留值和存储空间)*/static uint16_t Count; //用于计次分频if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET){/*每隔1ms,程序执行到这里一次*/Key_Tick(); //调用按键的Tick函数/*计次分频*/Count ++; //计次自增if (Count >= 40) //如果计次40次,则if成立,即if每隔40ms进一次{Count = 0; //计次清零,便于下次计次/*获取实际速度值*//*Encoder_Get函数,可以获取两次读取编码器的计次值增量*//*此值正比于速度,所以可以表示速度,但它的单位并不是速度的标准单位*//*此处每隔40ms获取一次计次值增量,电机旋转一周的计次值增量约为408*//*因此如果想转换为标准单位,比如转/秒*//*则可将此句代码改成Actual = Encoder_Get() / 408.0 / 0.04;*/Actual = Encoder_Get();/*获取本次误差和上次误差*/Error1 = Error0; //获取上次误差Error0 = Target - Actual; //获取本次误差,目标值减实际值,即为误差值/*误差积分(累加)*//*如果Ki不为0,才进行误差积分,这样做的目的是便于调试*//*因为在调试时,我们可能先把Ki设置为0,这时积分项无作用,误差消除不了,误差积分会积累到很大的值*//*后续一旦Ki不为0,那么因为误差积分已经积累到很大的值了,这就导致积分项疯狂输出,不利于调试*/if (Ki != 0) //如果Ki不为0{ErrorInt += Error0; //进行误差积分}else //否则{ErrorInt = 0; //误差积分直接归0}/*PID计算*//*使用位置式PID公式,计算得到输出值*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*输出限幅*/if (Out > 100) {Out = 100;} //限制输出值最大为100if (Out < -100) {Out = -100;} //限制输出值最小为100/*执行控制*//*输出值给到电机PWM*//*因为此函数的输入范围是-100~100,所以上面输出限幅,需要给Out值限定在-100~100*/Motor_SetPWM(Out);}TIM_ClearITPendingBit(TIM1, TIM_IT_Update);}
}
03-增量式PID定速控制
main.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "RP.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"uint8_t KeyNum;/*定义变量*/
float Target, Actual, Out; //目标值,实际值,输出值
float Kp, Ki, Kd; //比例项,积分项,微分项的权重
float Error0, Error1, Error2; //本次误差,上次误差,上上次误差int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化Key_Init(); //非阻塞式按键初始化Motor_Init(); //电机初始化Encoder_Init(); //编码器初始化RP_Init(); //电位器旋钮初始化Serial_Init(); //串口初始化,波特率9600Timer_Init(); //定时器初始化,定时中断时间1ms/*OLED打印一个标题*/OLED_Printf(0, 0, OLED_8X16, "Speed Control");OLED_Update();while (1){/*按键修改目标值*//*解除以下注释后,记得屏蔽电位器旋钮修改目标值的代码*/
// KeyNum = Key_GetNum(); //获取键码
// if (KeyNum == 1) //如果K1按下
// {
// Target += 10; //目标值加10
// }
// if (KeyNum == 2) //如果K2按下
// {
// Target -= 10; //目标值减10
// }
// if (KeyNum == 3) //如果K3按下
// {
// Target = 0; //目标值归0
// }/*电位器旋钮修改Kp、Ki、Kd和目标值*//*RP_GetValue函数返回电位器旋钮的AD值,范围:0~4095*//* 除4095.0可以把AD值归一化,再乘上一个系数,可以调整到一个合适的范围*/Kp = RP_GetValue(1) / 4095.0 * 2; //修改Kp,调整范围:0~2Ki = RP_GetValue(2) / 4095.0 * 2; //修改Ki,调整范围:0~2Kd = RP_GetValue(3) / 4095.0 * 2; //修改Kd,调整范围:0~2Target = RP_GetValue(4) / 4095.0 * 300 - 150; //修改目标值,调整范围:-150~150/*OLED显示*/OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", Kp); //显示KpOLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", Ki); //显示KiOLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", Kd); //显示KdOLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", Target); //显示目标值OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", Actual); //显示实际值OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", Out); //显示输出值OLED_Update(); //OLED更新,调用显示函数后必须调用此函数更新,否则显示的内容不会更新到OLED上Serial_Printf("%f,%f,%f\r\n", Target, Actual, Out); //串口打印目标值、实际值和输出值//配合SerialPlot绘图软件,可以显示数据的波形}
}void TIM1_UP_IRQHandler(void)
{/*定义静态变量(默认初值为0,函数退出后保留值和存储空间)*/static uint16_t Count; //用于计次分频if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET){/*每隔1ms,程序执行到这里一次*/Key_Tick(); //调用按键的Tick函数/*计次分频*/Count ++; //计次自增if (Count >= 40) //如果计次40次,则if成立,即if每隔40ms进一次{Count = 0; //计次清零,便于下次计次/*获取实际速度值*//*Encoder_Get函数,可以获取两次读取编码器的计次值增量*//*此值正比于速度,所以可以表示速度,但它的单位并不是速度的标准单位*//*此处每隔40ms获取一次计次值增量,电机旋转一周的计次值增量约为408*//*因此如果想转换为标准单位,比如转/秒*//*则可将此句代码改成Actual = Encoder_Get() / 408.0 / 0.04;*/Actual = Encoder_Get();/*获取本次误差、上次误差和上上次误差*/Error2 = Error1; //获取上上次误差Error1 = Error0; //获取上次误差Error0 = Target - Actual; //获取本次误差,目标值减实际值,即为误差值/*PID计算*//*使用增量式PID公式,计算得到输出值*/Out += Kp * (Error0 - Error1) + Ki * Error0+ Kd * (Error0 - 2 * Error1 + Error2);/*输出限幅*/if (Out > 100) {Out = 100;} //限制输出值最大为100if (Out < -100) {Out = -100;} //限制输出值最小为100/*执行控制*//*输出值给到电机PWM*//*因为此函数的输入范围是-100~100,所以上面输出限幅,需要给Out值限定在-100~100*/Motor_SetPWM(Out);}TIM_ClearITPendingBit(TIM1, TIM_IT_Update);}
}
04-位置式PID定位置控制
main.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "RP.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"uint8_t KeyNum;/*定义变量*/
float Target, Actual, Out; //目标值,实际值,输出值
float Kp, Ki, Kd; //比例项,积分项,微分项的权重
float Error0, Error1, ErrorInt; //本次误差,上次误差,误差积分int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化Key_Init(); //非阻塞式按键初始化Motor_Init(); //电机初始化Encoder_Init(); //编码器初始化RP_Init(); //电位器旋钮初始化Serial_Init(); //串口初始化,波特率9600Timer_Init(); //定时器初始化,定时中断时间1ms/*OLED打印一个标题*/OLED_Printf(0, 0, OLED_8X16, "Location Control");OLED_Update();while (1){/*按键修改目标值*//*解除以下注释后,记得屏蔽电位器旋钮修改目标值的代码*/
// KeyNum = Key_GetNum(); //获取键码
// if (KeyNum == 1) //如果K1按下
// {
// Target += 10; //目标值加10
// }
// if (KeyNum == 2) //如果K2按下
// {
// Target -= 10; //目标值减10
// }
// if (KeyNum == 3) //如果K3按下
// {
// Target = 0; //目标值归0
// }/*电位器旋钮修改Kp、Ki、Kd和目标值*//*RP_GetValue函数返回电位器旋钮的AD值,范围:0~4095*//* 除4095.0可以把AD值归一化,再乘上一个系数,可以调整到一个合适的范围*/Kp = RP_GetValue(1) / 4095.0 * 2; //修改Kp,调整范围:0~2Ki = RP_GetValue(2) / 4095.0 * 2; //修改Ki,调整范围:0~2Kd = RP_GetValue(3) / 4095.0 * 2; //修改Kd,调整范围:0~2Target = RP_GetValue(4) / 4095.0 * 816 - 408; //修改目标值,调整范围:-408~408/*OLED显示*/OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", Kp); //显示KpOLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", Ki); //显示KiOLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", Kd); //显示KdOLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", Target); //显示目标值OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", Actual); //显示实际值OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", Out); //显示输出值OLED_Update(); //OLED更新,调用显示函数后必须调用此函数更新,否则显示的内容不会更新到OLED上Serial_Printf("%f,%f,%f\r\n", Target, Actual, Out); //串口打印目标值、实际值和输出值//配合SerialPlot绘图软件,可以显示数据的波形}
}void TIM1_UP_IRQHandler(void)
{/*定义静态变量(默认初值为0,函数退出后保留值和存储空间)*/static uint16_t Count; //用于计次分频if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET){/*每隔1ms,程序执行到这里一次*/Key_Tick(); //调用按键的Tick函数/*计次分频*/Count ++; //计次自增if (Count >= 40) //如果计次40次,则if成立,即if每隔40ms进一次{Count = 0; //计次清零,便于下次计次/*获取实际位置值*//*Encoder_Get函数,可以获取两次读取编码器的计次值增量*//*计次值增量进行累加,即可得到计次值本身(即实际位置)*//*这里先获取增量,再进行累加,实际上是绕了个弯子*//*如果只需要得到编码器的位置,而不需要得到速度*//*则Encode_Get函数内部的代码可以修改为return TIM_GetCounter(TIM3);直接返回CNT计数器的值*//*修改后,此处代码改为Actual = Encoder_Get();直接得到位置,就不再需要累加了,这样更直接*/Actual += Encoder_Get();/*获取本次误差和上次误差*/Error1 = Error0; //获取上次误差Error0 = Target - Actual; //获取本次误差,目标值减实际值,即为误差值/*误差积分(累加)*//*如果Ki不为0,才进行误差积分,这样做的目的是便于调试*//*因为在调试时,我们可能先把Ki设置为0,这时积分项无作用,误差消除不了,误差积分会积累到很大的值*//*后续一旦Ki不为0,那么因为误差积分已经积累到很大的值了,这就导致积分项疯狂输出,不利于调试*/if (Ki != 0) //如果Ki不为0{ErrorInt += Error0; //进行误差积分}else //否则{ErrorInt = 0; //误差积分直接归0}/*PID计算*//*使用位置式PID公式,计算得到输出值*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*输出限幅*/if (Out > 100) {Out = 100;} //限制输出值最大为100if (Out < -100) {Out = -100;} //限制输出值最小为100/*执行控制*//*输出值给到电机PWM*//*因为此函数的输入范围是-100~100,所以上面输出限幅,需要给Out值限定在-100~100*/Motor_SetPWM(Out);}TIM_ClearITPendingBit(TIM1, TIM_IT_Update);}
}
05-增量式PID定位置控制
main.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"
#include "Timer.h"
#include "Key.h"
#include "RP.h"
#include "Motor.h"
#include "Encoder.h"
#include "Serial.h"uint8_t KeyNum;/*定义变量*/
float Target, Actual, Out; //目标值,实际值,输出值
float Kp, Ki, Kd; //比例项,积分项,微分项的权重
float Error0, Error1, Error2; //本次误差,上次误差,上上次误差int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化Key_Init(); //非阻塞式按键初始化Motor_Init(); //电机初始化Encoder_Init(); //编码器初始化RP_Init(); //电位器旋钮初始化Serial_Init(); //串口初始化,波特率9600Timer_Init(); //定时器初始化,定时中断时间1ms/*OLED打印一个标题*/OLED_Printf(0, 0, OLED_8X16, "Location Control");OLED_Update();while (1){/*按键修改目标值*//*解除以下注释后,记得屏蔽电位器旋钮修改目标值的代码*/
// KeyNum = Key_GetNum(); //获取键码
// if (KeyNum == 1) //如果K1按下
// {
// Target += 10; //目标值加10
// }
// if (KeyNum == 2) //如果K2按下
// {
// Target -= 10; //目标值减10
// }
// if (KeyNum == 3) //如果K3按下
// {
// Target = 0; //目标值归0
// }/*电位器旋钮修改Kp、Ki、Kd和目标值*//*RP_GetValue函数返回电位器旋钮的AD值,范围:0~4095*//* 除4095.0可以把AD值归一化,再乘上一个系数,可以调整到一个合适的范围*/Kp = RP_GetValue(1) / 4095.0 * 2; //修改Kp,调整范围:0~2Ki = RP_GetValue(2) / 4095.0 * 2; //修改Ki,调整范围:0~2Kd = RP_GetValue(3) / 4095.0 * 2; //修改Kd,调整范围:0~2Target = RP_GetValue(4) / 4095.0 * 816 - 408; //修改目标值,调整范围:-408~408/*OLED显示*/OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", Kp); //显示KpOLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", Ki); //显示KiOLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", Kd); //显示KdOLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", Target); //显示目标值OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", Actual); //显示实际值OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", Out); //显示输出值OLED_Update(); //OLED更新,调用显示函数后必须调用此函数更新,否则显示的内容不会更新到OLED上Serial_Printf("%f,%f,%f\r\n", Target, Actual, Out); //串口打印目标值、实际值和输出值//配合SerialPlot绘图软件,可以显示数据的波形}
}void TIM1_UP_IRQHandler(void)
{/*定义静态变量(默认初值为0,函数退出后保留值和存储空间)*/static uint16_t Count; //用于计次分频if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET){/*每隔1ms,程序执行到这里一次*/Key_Tick(); //调用按键的Tick函数/*计次分频*/Count ++; //计次自增if (Count >= 40) //如果计次40次,则if成立,即if每隔40ms进一次{Count = 0; //计次清零,便于下次计次/*获取实际位置值*//*Encoder_Get函数,可以获取两次读取编码器的计次值增量*//*计次值增量进行累加,即可得到计次值本身(即实际位置)*//*这里先获取增量,再进行累加,实际上是绕了个弯子*//*如果只需要得到编码器的位置,而不需要得到速度*//*则Encode_Get函数内部的代码可以修改为return TIM_GetCounter(TIM3);直接返回CNT计数器的值*//*修改后,此处代码改为Actual = Encoder_Get();直接得到位置,就不再需要累加了,这样更直接*/Actual += Encoder_Get();/*获取本次误差、上次误差和上上次误差*/Error2 = Error1; //获取上上次误差Error1 = Error0; //获取上次误差Error0 = Target - Actual; //获取本次误差,目标值减实际值,即为误差值/*PID计算*//*使用增量式PID公式,计算得到输出值*/Out += Kp * (Error0 - Error1) + Ki * Error0+ Kd * (Error0 - 2 * Error1 + Error2);/*输出限幅*/if (Out > 100) {Out = 100;} //限制输出值最大为100if (Out < -100) {Out = -100;} //限制输出值最小为100/*执行控制*//*输出值给到电机PWM*//*因为此函数的输入范围是-100~100,所以上面输出限幅,需要给Out值限定在-100~100*/Motor_SetPWM(Out);}TIM_ClearITPendingBit(TIM1, TIM_IT_Update);}
}
这套工程里“编码电机 + 编码器 + TIM1中断 + PID”分成 4 个控制回路:
#1 位置式 PID 的“定速控制”(速度环)
#2 增量式 PID 的“定速控制”(速度环)
#3 位置式 PID 的“定位控制”(位置环)
#4 增量式 PID 的“定位控制”(位置环)
对每个回路说明:回路怎么形成、公式长什么样、代码里“误差/采样/执行”具体在哪儿发生,以及调 Kp/Ki/Kd 对电机波形会造成的具体影响。
共同的运行节奏(四种情况都一样)
采样周期:在
TIM1_UP_IRQHandler
里用Count
做了 40 次 1 ms 的分频,等效控制周期 Ts = 40 ms。执行流程(每 40 ms 一次):
读取编码器:速度环里
Actual = Encoder_Get();
(40 ms 内的脉冲数≈速度);位置环里Actual += Encoder_Get();
(把增量累加成位置)。计算误差:
Error0 = Target - Actual;
用位置式或增量式 PID 公式算 Out。
限幅到 [-100, 100],再
Motor_SetPWM(Out)
(PB12/PB13 控向,TIM2_CH1 输出占空比)。
注:没把差分/积分显式除以 Ts,因此 Kp/Ki/Kd 都是“包含了采样周期”的离散增益。日后若把 40 ms 改成别的,增益需要重整定。
① 位置式 PID ——定速控制(Speed Control)
回路对象:速度。
测量量:Actual = Encoder_Get()
,单位=“脉冲/40 ms”,与转速成正比。
误差:e(k)=Target-Actual
。
控制律(离散位置式):
代码里:Out = Kp*Error0 + Ki*ErrorInt + Kd*(Error0-Error1);
特性与波形影响(速度阶跃 T=0→T≠0)
Kp↑:上升更快,稳态误差更小;但超调↑、振荡↑。
波形:速度曲线更陡,超过目标后在目标附近来回摆幅度更大。
Ki↑:消除稳态误差更快;但积分累积→容易“冲过头”,低频振荡↑,爬行时抖动↑。
波形:接近目标时二次抬头明显,且进入目标后缓慢拉回。
Kd↑:抑制超调,提高相位裕度;但编码器量化+摩擦会使差分项对噪声敏感,PWM 抖动↑。
波形:峰值降低、整定更快,但曲线顶端会出现细小“毛刺/锯齿”。
代码层面的注意
有积分项
ErrorInt
,但没做抗积分饱和;当Out
被限幅时,建议同步钳制ErrorInt
,否则会“解饱和后猛冲”。
② 增量式 PID ——定速控制(Speed Control)
回路对象:速度。
控制律(离散增量式):
代码里:Out += Kp*(Error0-Error1) + Ki*Error0 + Kd*(Error0 - 2*Error1 + Error2);
特性与波形影响
与位置式相比,不显式累加 ErrorInt,输出是平滑累计,对执行器饱和更“耐受”,不易风up。
Kp↑:速度响应快、超调↑;但由于是“增量加法”,Out 不会一次跳很大,更平滑。
Ki↑:同样消除稳态误差,但增量式对饱和更温和;仍会带来低频摆动。
Kd↑:抑制超调、加快收敛;对速度计数噪声同样敏感,但增量公式的二阶差分对高频更敏感,Kd 过大时高频 PWM 颤动更明显。
典型波形对比(与①相同调参幅度)
上升沿接近,但峰值更低、恢复更稳;阶梯感更弱,趋稳时“细抖”更少。
③ 位置式 PID ——定位控制(Location Control)
回路对象:位置(编码器计数)。
测量量:Actual += Encoder_Get()
(累计位置)。
误差:目标位置 - 实际位置。
控制律:同①(位置式),但 Target
的范围改成 ±408(≈1 圈≈408 脉冲)。
物理意义
这是单环位置控制,没有速度前馈或内环速度闭环。
当误差大时,
Out
会迅速饱和到 ±100(相当于“全速冲过去”),靠 Kd、Ki 在临近目标时刹车/消差。
波形影响(目标位置阶跃 0→N 脉冲)
Kp↑:到位更快,但过冲角度↑,可能“来回摆几次”才稳住。
Ki↑:消除残余位置误差;但若 Kp 已经让系统接近目标,Ki 会把“摩擦/死区”补掉→慢速爬行到零误差;过大依旧引发低频振荡(前后来回“找零点”)。
Kd↑:相当于对位置误差的差分=速度估计,在接近目标时提供制动,明显降低越位角、加快整定;过大时在低速段对编码器量化很敏感,抖动/齿格感↑。
典型位置波形
好整定:一次略超,回落后 1~2 次小摆动收敛;差整定(Kp/Ki 大):锯齿状来回扫,长时间达不到 ±1 脉冲的“静差带”。
④ 增量式 PID ——定位控制(Location Control)
回路对象:位置。
控制律:与②同为增量式,但误差是位置误差。
特性
输出按增量积累,靠近目标时刹车更柔,饱和退出后的“冲返”更小,整体“顺手”。
Kp↑:定位更快,但仍可能越位;
Ki↑:去静差;比位置式更不易 windup,但 Ki 过大仍会慢速摆动;
Kd↑:降低越位,减少回摆次数,但编码器分辨率/齿槽摩擦会让高 Kd 带来微抖。
典型位置波形对比(与③相同调参幅度)
峰值越位更小、回摆次数更少;在目标附近的波形更圆滑,不容易“撞饱和再反弹”。
调参对“曲线”会出现的具体可见变化
用“目标阶跃→响应曲线”的语言描述你会在串口波形/屏幕上看到什么(四种都通用):
Kp 从 0.5 → 1.5:
速度环:爬升斜率明显变陡,峰值超调从 ~5% 增到 ~20%,稳定前的振荡周期缩短、幅度增大。
位置环:上电一脚油门冲向目标,越位角从几脉冲到几十脉冲,回摆 1→3 次。
Ki 从 0 → 0.8:
- 速度/位置环:原本到稳态仍有少量偏差(速度慢 3–5 脉冲/40 ms 或位置差 2–5 脉冲),加 Ki 后能拉回到 0;但接近目标处会出现“速度曲线抬头”,进入目标后会缓慢起伏 1~2 个周期才贴合。
- 如果 Out 经常顶在 ±100,随后松开会看到“再次猛冲”(积分饱和/释放)。建议加抗积分饱和(在饱和时暂停积分或给积分项限幅)。
Kd 从 0 → 1.0:
速度环:超调从 ~20% 降到 <10%,整定时间缩短;但曲线顶部出现细齿状抖动(编码器计数量化、负载扰动导致)。
位置环:越位明显下降、回摆次数从 3 次变 1 次;临近目标(误差 <10 脉冲)时,PWM 会出现高频细变,肉眼可见“轻抖”。
这些“幅度/次数”的数字只为量感示例,真值取决于电机、负载、供电、电机驱动与摩擦。
小贴士(让波形更好看、调参更顺)
速度测量去抖:对
Encoder_Get()
的 40 ms 增量做 3~5 点滑动平均,能大幅降低 Kd 带来的噪声抖动。积分抗饱和(位置式 PID 必加):
if (Out==±100 && sgn(Out)==sgn(Error0))
时暂停积分,或对ErrorInt
加上clamp(-Imax, Imax)
。
死区补偿:小误差下 PWM 不动,可在 |Out|<DeadZone 时给最小可动 PWM。
两环更丝滑(扩展):位置环给速度目标,内层速度环控 PWM(你现在是单环)。
四套 Main 的核心区别
定速 vs 定位:误差
e(k)
分别是“速度差”与“位置差”(速度=增量、位置=累计)。位置式 vs 增量式:
位置式:一次性给出绝对
Out(k)
,积分显式,容易 windup,但实现简单;增量式:给出
ΔOut
再累加,对饱和更友好,动作更平滑,对噪声差分更敏感。
四套 Main 的区别:
共同点(四套都一样)
采样:
TIM1_UP_IRQHandler
内用 1 ms 基础 + 40 次计数 → 控制周期 Ts = 40 ms。流程:读编码器 → 算误差 → 计算 PID → 限幅 [-100,100] →
Motor_SetPWM(Out)
。
A. 被控量获取:速度环 vs 位置环
速度环(#1、#2)
// 每40 ms取一次增量,把它直接当“速度”
Actual = Encoder_Get(); // 单位≈ 脉冲/40ms
位置环(#3、#4)
// 把每40 ms的增量累计成位置
Actual += Encoder_Get(); // 单位≈ 脉冲计数(相对位移)
这就是“定速”和“定位”的本质差别:速度=增量;位置=增量的累加。
B. 目标量映射(电位器 → 目标)
速度环
Target = RP_GetValue(4) / 4095.0 * 300 - 150; // 约 -150 ~ +150(脉冲/40ms 的量纲)
位置环
Target = RP_GetValue(4) / 4095.0 * 816 - 408; // 约 -408 ~ +408(≈±1转,按你注释约408脉冲/圈)
C. 误差历史/状态变量:位置式 vs 增量式
位置式(#1、#3):需要“上次误差 + 误差积分”
float Error0, Error1, ErrorInt; // 本次/上次误差 + 积分
Error1 = Error0;
Error0 = Target - Actual;
if (Ki != 0) ErrorInt += Error0; else ErrorInt = 0; // 你做了可开关的积分
增量式(#2、#4):需要“上次 & 上上次误差”,不显式存积分
float Error0, Error1, Error2; // 本次/上次/上上次误差
Error2 = Error1;
Error1 = Error0;
Error0 = Target - Actual;
D. PID 计算语句(核心差异)
#1 / #3:位置式 PID(一次给出“绝对”输出)
Out = Kp * Error0+ Ki * ErrorInt+ Kd * (Error0 - Error1);
#2 / #4:增量式 PID(给出“增量”,再叠加到输出)
Out += Kp * (Error0 - Error1)+ Ki * Error0+ Kd * (Error0 - 2*Error1 + Error2);
一眼区分法:
看到
Out = ...
(覆盖赋值)+ErrorInt
→ 位置式。看到
Out += ...
(增量累加)+Error2
→ 增量式。
E. 抗积分饱和(只有位置式需要考虑)
位置式(#1/#3)里你有显式积分
ErrorInt
,但目前没有做反风up;增量式(#2/#4)没有显式积分项积累(虽然 Ki·e(k) 仍有“低频作用”),对饱和更温和。若要补:在
Out
饱和时暂停积分或对ErrorInt
夹紧。
if ((Out >= 100 && Error0 > 0) || (Out <= -100 && Error0 < 0)) {// 暂停积分或做夹紧
} else {ErrorInt += Error0;
}
场景 | 被控量 Actual | 误差状态 | 计算式 | 积分风up风险 |
---|---|---|---|---|
#1 位置式·定速 | Actual = Encoder_Get() | Error0, Error1, ErrorInt | Out = Kp*e + Ki*∑e + Kd*(e-e₋₁) | 有,需要处理 |
#2 增量式·定速 | Actual = Encoder_Get() | Error0, Error1, Error2 | Out += Kp*(e-e₋₁)+Ki*e+Kd*(e-2e₋₁+e₋₂) | 低 |
#3 位置式·定位 | Actual += Encoder_Get() | Error0, Error1, ErrorInt | 同 #1 | 有,需要处理 |
#4 增量式·定位 | Actual += Encoder_Get() | Error0, Error1, Error2 | 同 #2 | 低 |
windup = 积分饱和/积分累积过量。
在 PID 里,积分项会把长期存在的误差一直累加。如果执行器(这是 Motor_SetPWM(Out)
)因为限幅(±100)或硬件能力到顶,Out 再怎么加也出不去,但积分还在不停累加;一旦脱离饱和(比如负载放开或误差变小),“巨大的积分存量”会把系统猛推向反方向,造成大超调、长时间回摆,这就是 integral windup(积分饱和、积分累积)。
为什么“增量式更不易 windup”?
位置式有显式
ErrorInt
(积分状态),饱和时它会继续变大,释放后就“二次猛冲”;增量式没有单独的积分状态(虽然有 Ki·e 的低频作用),而且我们通常先把 Out 计算好再夹到限幅,被夹掉的增量不会“存起来”,所以风up程度明显更轻。
在代码里,windup 的“症状”
长时间目标很大(或被人为卡住轴),
Out
贴着 +100/-100,同时ErrorInt
继续变大;一旦放开或误差变号,速度/位置严重越过目标,要好几次摆动才回到目标;
串口波形能看到:
Out
长时间饱和,Actual
变化很慢;解除后Out
迅速翻到另一侧。