当前位置: 首页 > news >正文

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 一次):

    1. 读取编码器:速度环里 Actual = Encoder_Get();(40 ms 内的脉冲数≈速度);位置环里 Actual += Encoder_Get();(把增量累加成位置)。

    2. 计算误差:Error0 = Target - Actual;

    3. 用位置式或增量式 PID 公式算 Out

    4. 限幅到 [-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, ErrorIntOut = Kp*e + Ki*∑e + Kd*(e-e₋₁)有,需要处理
#2 增量式·定速Actual = Encoder_Get()Error0, Error1, Error2Out += 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 迅速翻到另一侧。

http://www.lryc.cn/news/615630.html

相关文章:

  • 复现论文关于3-RPRU并联机器人运动学建模与参数优化设计
  • QT环境搭建
  • 功能测试中常见的面试题-二
  • 【Python 高频 API 速学 ⑥】
  • 09 【C++ 初阶】C/C++内存管理
  • [激光原理与应用-207]:光学器件 - 光纤种子源激光器常用元器件
  • Linux文件系统基石:透彻理解inode及其核心作用
  • 【高等数学】第八章 向量代数与空间解析几何——第四节 空间直线及其方程
  • 分析报告:基于字节连续匹配技术的KV缓存共享实施可能性及其扩展
  • 【机器学习深度学习】模型选型:如何根据模型的参数算出合适的设备匹配?
  • 202506 电子学会青少年等级考试机器人二级理论综合真题
  • 202506 电子学会青少年等级考试机器人三级器人理论真题
  • openvela之STM32开发板部署
  • LLM表征的提取方式
  • EP06:【DL 第二弹】动态计算图与梯度下降入门
  • UCMT部分复现
  • Chaos Monkey 故障注入工具使用介绍
  • Spring Boot Starter 自动化配置原理深度剖析
  • CentOS7编译安装GCC
  • C++高频知识点(十七)
  • C++ 虚函数、多重继承、虚基类与RTTI的实现成本剖析
  • AI大模型模态特征详解
  • 鸿蒙分布式任务调度深度剖析:跨设备并行计算的最佳实践
  • <PLC><汇川><字符转换>在汇川PLC中,如何进行字符串的转换与比较?
  • 从零开始理解编译原理:设计一个简单的编程语言
  • 二十、MySQL-DQL-条件查询
  • Kotlin初体验
  • DeepSeek智能考试系统智能体
  • 在 VS Code 或 Visual Studio 2022 上搭建 ESP32-CAM 开发环境
  • Vulnhub----Beelzebub靶场