在学习江协科技PID课程时,做一些笔记,对应视频3-1,对应代码:13
13-双环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"
#include "PID.h"uint8_t KeyNum;int16_t Speed, Location; //速度,位置/*定义PID结构体变量*/
PID_t Inner = { //内环PID结构体变量,定义的时候同时给部分成员赋初值.Kp = 0.3, //比例项权重.Ki = 0.3, //积分项权重.Kd = 0, //微分项权重.OutMax = 100, //输出限幅的最大值.OutMin = -100, //输出限幅的最小值
};PID_t Outer = { //外环PID结构体变量,定义的时候同时给部分成员赋初值.Kp = 0.3, //比例项权重.Ki = 0, //积分项权重.Kd = 0.4, //微分项权重.OutMax = 20, //输出限幅的最大值.OutMin = -20, //输出限幅的最小值
};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, "2*PID Control");OLED_Update();while (1){/*按键修改目标值*//*解除以下注释后,记得屏蔽电位器旋钮修改目标值的代码*//*调节内环目标值使用Inner.Target,调节外环目标值使用Outer.Target*/
// KeyNum = Key_GetNum(); //获取键码
// if (KeyNum == 1) //如果K1按下
// {
// Inner.Target += 10; //目标值加10
// }
// if (KeyNum == 2) //如果K2按下
// {
// Inner.Target -= 10; //目标值减10
// }
// if (KeyNum == 3) //如果K3按下
// {
// Inner.Target = 0; //目标值归0
// }/*解除下面一段代码的注释,进行内环PID调参*//*进行内环PID调参时,请注释掉外环控制内环的部分代码*//*RP_GetValue函数返回电位器旋钮的AD值,范围:0~4095*//* 除4095.0可以把AD值归一化,再乘上一个系数,可以调整到一个合适的范围*/
// Inner.Kp = RP_GetValue(1) / 4095.0 * 2; //修改Kp,调整范围:0~2
// Inner.Ki = RP_GetValue(2) / 4095.0 * 2; //修改Ki,调整范围:0~2
// Inner.Kd = RP_GetValue(3) / 4095.0 * 2; //修改Kd,调整范围:0~2
// Inner.Target = RP_GetValue(4) / 4095.0 * 300 - 150; //修改目标值,调整范围:-150~150
//
// /*OLED显示*/
// OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", Inner.Kp); //显示Kp
// OLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", Inner.Ki); //显示Ki
// OLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", Inner.Kd); //显示Kd
//
// OLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", Inner.Target); //显示目标值
// OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", Inner.Actual); //显示实际值
// OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", Inner.Out); //显示输出值
//
// OLED_Update(); //OLED更新,调用显示函数后必须调用此函数更新,否则显示的内容不会更新到OLED上
//
// Serial_Printf("%f,%f,%f\r\n", Inner.Target, Inner.Actual, Inner.Out); //串口打印目标值、实际值和输出值
// //配合SerialPlot绘图软件,可以显示数据的波形/*解除下面一段代码的注释,进行外环PID调参*//*内环PID调参完成后,加上外环控制内环的部分代码,再进行外环PID调参*//*RP_GetValue函数返回电位器旋钮的AD值,范围:0~4095*//* 除4095.0可以把AD值归一化,再乘上一个系数,可以调整到一个合适的范围*/
// Outer.Kp = RP_GetValue(1) / 4095.0 * 2; //修改Kp,调整范围:0~2
// Outer.Ki = RP_GetValue(2) / 4095.0 * 2; //修改Ki,调整范围:0~2
// Outer.Kd = RP_GetValue(3) / 4095.0 * 2; //修改Kd,调整范围:0~2Outer.Target = RP_GetValue(4) / 4095.0 * 816 - 408; //修改目标值,调整范围:-408~408/*OLED显示*/OLED_Printf(0, 16, OLED_8X16, "Kp:%4.2f", Outer.Kp); //显示KpOLED_Printf(0, 32, OLED_8X16, "Ki:%4.2f", Outer.Ki); //显示KiOLED_Printf(0, 48, OLED_8X16, "Kd:%4.2f", Outer.Kd); //显示KdOLED_Printf(64, 16, OLED_8X16, "Tar:%+04.0f", Outer.Target); //显示目标值OLED_Printf(64, 32, OLED_8X16, "Act:%+04.0f", Outer.Actual); //显示实际值OLED_Printf(64, 48, OLED_8X16, "Out:%+04.0f", Outer.Out); //显示输出值OLED_Update(); //OLED更新,调用显示函数后必须调用此函数更新,否则显示的内容不会更新到OLED上Serial_Printf("%f,%f,%f\r\n", Outer.Target, Outer.Actual, Outer.Out); //串口打印目标值、实际值和输出值//配合SerialPlot绘图软件,可以显示数据的波形}
}void TIM1_UP_IRQHandler(void)
{/*定义静态变量(默认初值为0,函数退出后保留值和存储空间)*/static uint16_t Count1, Count2; //分别用于内环和外环的计次分频if (TIM_GetITStatus(TIM1, TIM_IT_Update) == SET){/*每隔1ms,程序执行到这里一次*/Key_Tick(); //调用按键的Tick函数/*内环计次分频*/Count1 ++; //计次自增if (Count1 >= 40) //如果计次40次,则if成立,即if每隔40ms进一次{Count1 = 0; //计次清零,便于下次计次/*获取实际速度值和实际位置值*//*Encoder_Get函数,可以获取两次读取编码器的计次值增量*//*此值正比于速度,所以可以表示速度,但它的单位并不是速度的标准单位*//*此处每隔40ms获取一次计次值增量,电机旋转一周的计次值增量约为408*//*因此如果想转换为标准单位,比如转/秒*//*则可将此句代码改成Speed = Encoder_Get() / 408.0 / 0.04;*/Speed = Encoder_Get(); //获取编码器增量,得到实际速度Location += Speed; //实际速度累加,得到实际位置/*以下进行内环PID控制*//*内环获取实际值*/Inner.Actual = Speed; //内环为速度环,实际值为速度值/*PID计算及结构体变量值更新*/PID_Update(&Inner); //调用封装好的函数,一步完成PID计算和更新/*内环执行控制*//*内环输出值给到电机PWM*/Motor_SetPWM(Inner.Out);}/*外环计次分频*/Count2 ++; //计次自增if (Count2 >= 40) //如果计次40次,则if成立,即if每隔40ms进一次{Count2 = 0; //计次清零,便于下次计次/*以下进行外环PID控制*//*外环获取实际值*/Outer.Actual = Location; //外环为位置环,实际值为位置值/*PID计算及结构体变量值更新*/PID_Update(&Outer); //调用封装好的函数,一步完成PID计算和更新/*外环执行控制*//*外环的输出值作用于内环的目标值,组成串级PID结构*/Inner.Target = Outer.Out;}TIM_ClearITPendingBit(TIM1, TIM_IT_Update);}
}
PID.c:
#include "stm32f10x.h" // Device header
#include "PID.h"/*** 函 数:PID计算及结构体变量值更新* 参 数:PID_t * 指定结构体的地址* 返 回 值:无*/
void PID_Update(PID_t *p)
{/*获取本次误差和上次误差*/p->Error1 = p->Error0; //获取上次误差p->Error0 = p->Target - p->Actual; //获取本次误差,目标值减实际值,即为误差值/*外环误差积分(累加)*//*如果Ki不为0,才进行误差积分,这样做的目的是便于调试*//*因为在调试时,我们可能先把Ki设置为0,这时积分项无作用,误差消除不了,误差积分会积累到很大的值*//*后续一旦Ki不为0,那么因为误差积分已经积累到很大的值了,这就导致积分项疯狂输出,不利于调试*/if (p->Ki != 0) //如果Ki不为0{p->ErrorInt += p->Error0; //进行误差积分}else //否则{p->ErrorInt = 0; //误差积分直接归0}/*PID计算*//*使用位置式PID公式,计算得到输出值*/p->Out = p->Kp * p->Error0+ p->Ki * p->ErrorInt+ p->Kd * (p->Error0 - p->Error1);/*输出限幅*/if (p->Out > p->OutMax) {p->Out = p->OutMax;} //限制输出值最大为结构体指定的OutMaxif (p->Out < p->OutMin) {p->Out = p->OutMin;} //限制输出值最小为结构体指定的OutMin
}
PID.h:
#ifndef __PID_H
#define __PID_Htypedef struct {float Target;float Actual;float Out;float Kp;float Ki;float Kd;float Error0;float Error1;float ErrorInt;float OutMax;float OutMin;
} PID_t;void PID_Update(PID_t *p);#endif
一、双环 PID 实现原理
结构
外环(位置环):
直接对位置误差(目标位置 − 实际位置)进行 PID 计算,输出的结果不是直接驱动电机,而是作为内环速度环的目标值。内环(速度环):
对速度误差(目标速度 − 实际速度)进行 PID 控制,输出 PWM 信号驱动电机。
工作流程
外环根据位置误差计算一个期望速度(正负代表转动方向和快慢)。
内环将期望速度作为目标,与实时测得的速度比较后,用 PID 算法快速调整 PWM,使实际速度跟随期望速度。
最终实现位置精确控制的同时,速度变化过程平滑、快速。
代码对应关系
1.外环
Outer.Actual = Location;
PID_Update(&Outer);
Inner.Target = Outer.Out; // 外环输出作为内环目标速度
2.内环:
Inner.Actual = Speed;
PID_Update(&Inner);
Motor_SetPWM(Inner.Out);
二、双环 PID 的优点(相对单环 PID)
稳态精度高
外环处理位置误差,能消除位置的长期偏差(稳态误差小)。
内环快速消除速度误差,让外环的输出更快收敛到目标。
动态性能好
内环抑制了速度的突变,提高系统响应速度。
外环不直接控制 PWM,避免了位置环大幅度输出导致的振荡。
抗干扰能力强
内环对速度的快速调节可以抵消外部扰动(负载变化、摩擦力变化等)。
抑制超调与振荡
外环输出受内环限幅(
OutMax
/OutMin
)保护,避免因位置误差大而直接全速输出造成冲过头。
三、调参技巧(先内环后外环)
1. 为什么要先调内环
内环(速度环)是系统快速响应部分,直接影响外环效果。
如果内环速度跟随不准,外环即使参数再好,也会出现震荡、超调甚至失稳。
2. 调内环步骤
关掉外环(直接给内环一个固定的速度目标值)。
先调 Kp:从小到大增加,直到速度响应快且基本无振荡。
再调 Ki:消除速度稳态误差,但不要太大,防止低频振荡。
视情况加 Kd:抑制速度快速变化的振荡。
确保内环在目标速度变化时能快速平稳跟随。
3. 调外环步骤
开启外环,内环保持调好的参数。
外环先调 Kp:从小到大增加,观察位置响应速度和超调情况。
若存在位置稳态误差,可适当增加 Ki(位置环 Ki 一般很小)。
外环 Kd 用于抑制位置变化过快时的超调(相当于对速度的进一步约束)。
外环输出限幅 (
OutMax
) 要合理,一般设为电机中速范围,防止位置误差大时直接给满速。