在学习江协科技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.pswp.cn/pingmian/92418.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/92418.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/92418.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++入门学习3

10.类和对象 C语言结构体中只能定义变量&#xff0c;在C中&#xff0c;结构体内不仅可以定义变量&#xff0c;也可以定义函数。 C中定义类&#xff08;结构体&#xff09;的语法&#xff1a; class className {// 类体&#xff1a;由成员函数和成员变量组成}; // 一定要注意…

奇偶校验码原理与FPGA实现

奇偶校验原理与FPGA实现写在前面一、基础原理2.1 奇校验2.2 偶校验2.3 缺点二、举个例子3.1 奇校验例子3.2 偶校验例子3.3 检测出错例子三、FPGA实现写在后面写在前面 奇偶校验码是一种简单的检错码&#xff0c;主要用于数据传输或存储过程中检测奇数个比特错误或者偶数个比特错…

Python中的Lambda函数详解

Lambda函数&#xff08;匿名函数&#xff09;是Python中一种简洁的函数定义方式&#xff0c;它允许你快速创建小型、一次性的函数对象而无需使用标准的def关键字。1. Lambda函数的基本语法lambda arguments: expressionlambda&#xff1a;定义匿名函数的关键字arguments&#x…

进阶向:Python编写网页爬虫抓取数据

Python网页爬虫入门指南&#xff1a;从零开始抓取数据在当今数据驱动的时代&#xff0c;网络爬虫已成为获取公开信息的重要工具。Python凭借其丰富的库和简洁的语法&#xff0c;成为编写网络爬虫的首选语言。本文将详细介绍如何使用Python编写一个基础的网页爬虫。什么是网页爬…

客服Agent革命:智能客服系统的技术实现与效果评估

客服Agent革命&#xff1a;智能客服系统的技术实现与效果评估 &#x1f31f; Hello&#xff0c;我是摘星&#xff01; &#x1f308; 在彩虹般绚烂的技术栈中&#xff0c;我是那个永不停歇的色彩收集者。 &#x1f98b; 每一个优化都是我培育的花朵&#xff0c;每一个特性都是我…

C++-红黑树

1、红黑树的概念红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红黑树确保没有一条路 径会比其他路径长出俩倍&#xff0c;…

在Python中避免使用`None`表示特殊情况:函数返回值与异常处理的最佳实践 (Effective Python 第20条)

在Python编程中&#xff0c;函数的设计与实现直接影响代码的可读性、可维护性和健壮性。一个常见的问题是如何处理函数的返回值&#xff0c;尤其是在需要表示某种特殊或异常情况时。许多开发者习惯性地使用None来表示这些特殊情况&#xff0c;但这种方法往往会导致意想不到的错…

从反射到方法句柄:深入探索Java动态编程的终极解决方案

&#x1f31f; 你好&#xff0c;我是 励志成为糕手 &#xff01; &#x1f30c; 在代码的宇宙中&#xff0c;我是那个追逐优雅与性能的星际旅人。 ✨ 每一行代码都是我种下的星光&#xff0c;在逻辑的土壤里生长成璀璨的银河&#xff1b; &#x1f6e0;️ 每一个算法都是我绘制…

算法_python_学习记录_01

人心的成见是一座大山。一旦有山挡在面前&#xff0c;则很难到达下一站。所需要做的&#xff0c;是穿过这座山。 偶然间看了一个视频&#xff0c;说的是EMASMA的自动交易策略&#xff0c;这个视频做的很用心&#xff0c;在入场的时间不仅要看EMA的金叉&#xff0c;还需要看其他…

机器翻译中的语言学基础详解(包括包括语法、句法和语义学等)

文章目录一、语法&#xff08;Grammar&#xff09;&#xff1a;语言规则的底层框架1.1 传统语法理论的应用1.2 生成语法&#xff08;Generative Grammar&#xff09;1.3 依存语法&#xff08;Dependency Grammar&#xff09;二、句法&#xff08;Syntax&#xff09;&#xff1a…

MQTT:Dashboard访问授权

目录一、认证1.1 创建认证器1.2 多认证器二、授权2.1 ACL文件授权配置2.2 使用内置数据库授权配置一、认证 认证&#xff1a;就是验证客户端的身份。 1.1 创建认证器 选择认证方式配置数据源配置数据源的相关参数 认证器创建之后&#xff0c;在使用客户端连接Dashboard时&am…

Serper注册无反应

google邮箱才行&#xff0c;163邮箱注册无反应&#xff0c;其他邮箱没试过 在尝试websailor系列的时候&#xff0c;需要注册serper&#xff0c;获取Google Search Key serper.dev/dashboard

聊聊经常用的微服务

聊聊微服务 架构演变 单体架构&#xff1a; All in One&#xff0c;所有的功能模块都在一个工程里。 SOA架构&#xff1a; 这个架构当不当正不正&#xff0c;对于现在来说&#xff0c;有点老&#xff0c;甚至需要ESB&#xff0c;WebService之类的&#xff0c;基本不会使用了。…

第十四届蓝桥杯青少年组省赛 编程题真题题解

明天我就要考蓝桥杯省赛了&#xff0c;本蒟蒻已瑟瑟发抖&#xff0c;所以现在写一篇文章。 题目分别为&#xff1a; 1.​​​​​​B4270 [蓝桥杯青少年组省赛 2023] 特殊运算符 2.B4271 [蓝桥杯青少年组省赛 2023] 四叶玫瑰数 3.B4272 [蓝桥杯青少年组省赛 2023] 质因数的…

HTML全景效果实现

我将为您创建一个精美的360度全景效果页面&#xff0c;使用Three.js库实现沉浸式全景体验&#xff0c;并提供用户友好的控制界面&#xff0c;完整代码看文章末尾。 设计思路 使用Three.js创建全景球体 添加控制面板用于切换不同场景 实现自动旋转和手动控制选项 添加加载状…

Python 属性描述符(描述符用法建议)

描述符用法建议 下面根据刚刚论述的描述符特征给出一些实用的结论。 使用特性以保持简单 内置的 property 类创建的其实是覆盖型描述符&#xff0c;__set__ 方法和 __get__ 方法都实现了&#xff0c;即便不定义设值方法也是如此。特性的 __set__ 方法默认抛出 AttributeError: …

Milvus 向量数据库内存使用相关了解

1、支持 MMap 的数据存储在 Milvus 中&#xff0c;内存映射文件允许将文件内容直接映射到内存中。这一功能提高了内存效率&#xff0c;尤其是在可用内存稀缺但完全加载数据不可行的情况下。这种优化机制可以增加数据容量&#xff0c;同时在一定限度内确保性能&#xff1b;但当数…

C++编程之旅-- -- --默认成员函数(全详解)

目录前言构造函数构造函数形式&#xff1a;构造函数的特性&#xff1a;explicit关键字析构函数析构函数的概念析构函数的特性含有类类型的成员变量的类析构函数的调用拷贝构造函数拷贝构造函数的概念拷贝构造函数的特性浅拷贝和深拷贝&#xff1a;拷贝构造函数典型调用场景&…

Linux网络编程:TCP的远程多线程命令执行

目录 前言&#xff1a; 一、前文补充 二、服务端的修改 三、Command类的新增 前言&#xff1a; 好久不见&#xff0c;最近忙于其他事情&#xff0c;就耽误了咱们的Linux的网络部分的学习。 今天咱们先来给之前所学的TCP的部分进行一个首尾工作&#xff0c;主要是给大家介绍…

重学React(三):状态管理

背景&#xff1a; 继续跟着官网的流程往后学&#xff0c;之前已经整理了描述UI以及添加交互两个模块&#xff0c;总体来说还是收获不小的&#xff0c;至少我一个表面上用了四五年React的前端小卡拉米对React的使用都有了新的认知。接下来就到了状态管理&#xff08;React特地加…