目录
本文已更新,加上曲线调试,更好效果,更多内容,详情:
编码电机PID调试(速度环|位置环|跟随)_桃成蹊2.0的博客-CSDN博客_编码器pid
串级控制系统介绍:
串级控制系统是改善控制质量的有效方法之一,在过程控制中得到了广泛的应用。所谓串级控制,就是采用两个控制器串联工作,外环控制器的输出作为内环控制器的设定值,由内环控制器的输出去操纵控制阀,从而对外环被控量具有更好的控制效果。这样的控制系统被称为串级系统。PID串级控制就是串级控制中的两个控制器均为PID控制器。
限于时间和篇幅,这篇仅记录用通用的pid方式进行感觉性的调参,等回家了有时间在把改进pid加上去试试效果
先把原理贴一遍
PID控制,就是对偏差进行比例、积分和微分的控制。PID由3个单元组成,分别是比例(P)单元、积分(I)单元、微分(D)单位。在工程实践中,一般P是必须的,所以衍生出许多组合的PID控制器,如PD、PI、PID等。
因为单片机是通过软件实现其控制算法的,所以必须对模拟调节器进行离散化处理,这样它只需根据釆样时刻的偏差值计算控制量。因此,我们需要使用离散的差分方程代替连续的微分方程。
通俗理解,用比例积分微分运算来消除误差,但是这个过程是连续的,周期性(一般是ms级的)的一次次计算来消除误差。
1、PID框图
速度环pid
位置环pid
2、pid控制器的表达式
经典的PID计算式如下所示:
将上述公式离散化,结果如下:
在使用ki和kd来代替积分就是下面的我们最常用的公式了(这个是比较常用的位置式pid版本)
之后我们再修改一种版本,如下所示(这种称为增量式pid)
可以看出这种仅统计当前误差和上一次误差,而上面的位置式统计了自起始以来所有的误差项,而上面的位置式版本输出后直接作为控制器输出值,而增量式则作为增加量叠加进入控制器的输出中。
3、传感器数据获取
霍尔码盘结构图:
编码电机上如下:
怎么读数据看下面这个,一张图一张表,对应着读取就知道了
这里注意:stm32用硬件编码器模式,这个读取的过程他是自动进行的,只要进行配置编码器模式就行了,但是其他没有硬件编码器模式的需要软件上模拟实现类似的功能,怎么模拟-就是按照下面的那个表,使用gpio中断加上if条件判读即可。
对应的上下信号说明如下所示:
读不懂就看口诀,立马懂:
CNT计数+:
- A上升沿,B逻辑低
- B上升沿,A逻辑高
- B下降沿,A逻辑低
- A下降沿,B逻辑高
CNT计数-:
- A下降沿,B逻辑低
- B下降沿,A逻辑高
- B上升沿,A逻辑低
- A上升沿,B逻辑高
4、硬件设计
硬件其实没什么要求,画了块板子只是为了使用方便,这块板子接口上是直接兼容编码电机的,市面上几款我都试过了,基本不需要改线,直接插上就可以使用。
关注一下这个电机部分的接口吧,毕竟就这个有点用了
5、工程配置
可能有些没用的,主要看电机的pwm接口,编码器捕获接口,还有电机方向的接口把
定时器8 1 3 2采用编码器模式,配置如下
pwm设置,这里一个定时器就够了,重装载设置为7199,那pwm最大就是7200了,这就这样OK
基本时间配置,这里我没有采用操作系统的写法,直接用一个定时器中断了(毕竟是老工程了,拿过来直接用比较方便),使用定时器6,可以看出定时时间为1ms一次。
综上:资源配置如下
接口类型 | 外设资源 | 模式 |
编码器口 | TIM8 TIM1 TIM3 TIM2 | 编码器模式(T12) |
电机接口 | M11 M12 M21 M22 M31 M32 M41 M42 | 看原理图 |
PWM口 | TIM4 (CH1 ~ 4) | 变化范围0-7200 |
基本定时 | TIM6 | 1ms一次 |
6、软件部分程序配置
1、电机配置,这里把每一个电机封装成一个函数,内函限幅,电机换向,PWM设置,是比较方便的,墙裂推荐hhh
void AAC_MotorFL_Run(int16_t speed)
{
if(speed > 0) {GPIOE->BRR = m11_Pin;GPIOE->BSRR = m12_Pin;}
else {speed = -speed;GPIOE->BSRR = m11_Pin;GPIOE->BRR = m12_Pin;}
if(speed > 7100) speed = 7100;
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_1,speed);
}
void AAC_MotorFR_Run(int16_t speed)
{
if(speed > 0) {GPIOE->BRR = m21_Pin;GPIOE->BSRR = m22_Pin;}
else {speed = -speed;GPIOE->BSRR = m21_Pin;GPIOE->BRR = m22_Pin;}
if(speed > 7100) speed = 7100;
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_2,speed);
}
void AAC_MotorBL_Run(int16_t speed)
{
if(speed > 0) {GPIOC->BRR = m31_Pin;GPIOC->BSRR = m32_Pin;}
else {speed = -speed;GPIOC->BSRR = m31_Pin;GPIOC->BRR = m32_Pin;}
if(speed > 7100) speed = 7100;
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_3,speed);
}
void AAC_MotorBR_Run(int16_t speed)
{
if(speed > 0) {GPIOC->BRR = m41_Pin;GPIOC->BSRR = m42_Pin;}
else {speed = -speed;GPIOC->BSRR = m41_Pin;GPIOC->BRR = m42_Pin;}
if(speed > 7100) speed = 7100;
__HAL_TIM_SetCompare(&htim4,TIM_CHANNEL_4,speed);
}
2、编码器测速,定时器的编码器模式是特殊的计数模式,测量值还是保存在cnt中的,因此只要读取cnt的值就可以获取编码器当前的计数值了。
int Read_Encoder(uint8_t TIMX)
{
int Encoder_TIM;
switch(TIMX)
{
case 2: Encoder_TIM = (short)TIM2 -> CNT; TIM2 -> CNT=0;break;
case 3: Encoder_TIM = (short)TIM3 -> CNT; TIM3 -> CNT=0;break;
case 1: Encoder_TIM = (short)TIM1 -> CNT; TIM1 -> CNT=0;break;
case 8: Encoder_TIM = (short)TIM8 -> CNT; TIM8 -> CNT=0;break;
default: Encoder_TIM = 0;
}
return Encoder_TIM;
}
3、测量值大小问题 市面上常用的编码电机有两种,由电机+减速箱+编码器组成,电机为最内部的主体,前端套筒为减速箱,最尾部的为编码器,捕获到的值由编码器和减速箱共同决定。
减速比可由电机上的贴纸或者型号获取,比如贴了10F,就是减速比为10:1的意思
下面说明了常见霍尔编码器和光电编码器的编码器线束
可以看出关电编码器的线数是远大于霍尔编码器的,这使得光电编码器更适合高精度的应用,那么最终公式为
主动轴一圈=减速比*编码器线数
4、定时器创建任务周期,前面已经创建了1ms一次的定时器中断,这里在中断服务函数中加入判断,这里根据经验判断如下(为啥呢,这样取得值比较合理,大概pwm满载的时候速度最大100多):
- 光电编码器:2ms读取一次数据
- 霍尔编码器:10ms读取一次数据
//定时器任务周期
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
static int time;
if(htim->Instance == htim6.Instance)
{
if(time % 10 == 0)
{
//10ms执行一次
}
if(time >= 1000)
{
time = 0;
HAL_GPIO_TogglePin(GPIOD, led_Pin);
}
}
}
5、pid公式
- 这是直接根据离散公式还原来的
typedef struct
{
float Kp, Ki, Kd;
float P, I, D;
float Error_Last;
}PositionPID_t;
// pid计算
int Position_PID( PositionPID_t *pid, float set_value, float now_value )
{
pid->P = set_value - now_value;
pid->I += pid->P;
pid->D = pid->P - pid->Error_Last;
pid->Error_Last = pid->P;
pid->I=pid->I>10000?10000:(pid->I<(-10000)?(-10000):pid->I);
if( set_value == 0 ) pid->I = 0;
return( pid->Kp*pid->P + pid->Ki*pid->I + pid->Kd*pid->D );
}
- 平衡小车之家抄来的
//位置式PID控制器
int Position_PI (int Encoder, int Target)
{
// float Kp=0.02,Ki=0.0002;
static int Bias, Pwm;
static long Integral_bias;
Bias = Encoder - Target; //计算偏差
Integral_bias += Bias; //求出偏差的积分
if(Integral_bias > 1500000) Integral_bias = 1500000; //积分限幅
if(Integral_bias < -1500000) Integral_bias = -1500000; //积分限幅
Pwm = Position_Kp * Bias + Position_Ki * Integral_bias; //位置式PI控制器
return Pwm; //增量输出
}
//增量PI控制器
int Incremental_PI (int Encoder, int Target)
{
// float Kp=20,Ki=30;
static int Bias, Pwm, Last_bias;
Bias = Encoder - Target; //计算偏差
Pwm += Incremental_Kp * (Bias - Last_bias) + Incremental_Ki * Bias; //增量式PI控制器
Last_bias = Bias; //保存上一次偏差
return Pwm; //增量输出
}
- 大疆robomaster官方例程
pid.c
#include "pid.h"
#include "main.h"
#define LimitMax(input, max) \
{ \
if (input > max) \
{ \
input = max; \
} \
else if (input < -max) \
{ \
input = -max; \
} \
}
void PID_init(pid_type_def *pid, uint8_t mode, const fp32 PID[3], fp32 max_out, fp32 max_iout)
{
if (pid == NULL || PID == NULL)
{
return;
}
pid->mode = mode;
pid->Kp = PID[0];
pid->Ki = PID[1];
pid->Kd = PID[2];
pid->max_out = max_out;
pid->max_iout = max_iout;
pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
pid->error[0] = pid->error[1] = pid->error[2] = pid->Pout = pid->Iout = pid->Dout = pid->out = 0.0f;
}
fp32 PID_calc(pid_type_def *pid, fp32 ref, fp32 set)
{
if (pid == NULL)
{
return 0.0f;
}
pid->error[2] = pid->error[1];
pid->error[1] = pid->error[0];
pid->set = set;
pid->fdb = ref;
pid->error[0] = set - ref;
if (pid->mode == PID_POSITION)
{
pid->Pout = pid->Kp * pid->error[0];
pid->Iout += pid->Ki * pid->error[0];
pid->Dbuf[2] = pid->Dbuf[1];
pid->Dbuf[1] = pid->Dbuf[0];
pid->Dbuf[0] = (pid->error[0] - pid->error[1]);
pid->Dout = pid->Kd * pid->Dbuf[0];
LimitMax(pid->Iout, pid->max_iout);
pid->out = pid->Pout + pid->Iout + pid->Dout;
LimitMax(pid->out, pid->max_out);
}
else if (pid->mode == PID_DELTA)
{
pid->Pout = pid->Kp * (pid->error[0] - pid->error[1]);
pid->Iout = pid->Ki * pid->error[0];
pid->Dbuf[2] = pid->Dbuf[1];
pid->Dbuf[1] = pid->Dbuf[0];
pid->Dbuf[0] = (pid->error[0] - 2.0f * pid->error[1] + pid->error[2]);
pid->Dout = pid->Kd * pid->Dbuf[0];
pid->out += pid->Pout + pid->Iout + pid->Dout;
LimitMax(pid->out, pid->max_out);
}
return pid->out;
}
void PID_clear(pid_type_def *pid)
{
if (pid == NULL)
{
return;
}
pid->error[0] = pid->error[1] = pid->error[2] = 0.0f;
pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
pid->out = pid->Pout = pid->Iout = pid->Dout = 0.0f;
pid->fdb = pid->set = 0.0f;
}
pid.h
#ifndef PID_H
#define PID_H
#include "struct_typedef.h"
enum PID_MODE
{
PID_POSITION = 0,
PID_DELTA
};
typedef struct
{
uint8_t mode;
//PID 三参数
fp32 Kp;
fp32 Ki;
fp32 Kd;
fp32 max_out; //最大输出
fp32 max_iout; //最大积分输出
fp32 set;
fp32 fdb;
fp32 out;
fp32 Pout;
fp32 Iout;
fp32 Dout;
fp32 Dbuf[3]; //微分项 0最新 1上一次 2上上次
fp32 error[3]; //误差项 0最新 1上一次 2上上次
} pid_type_def;
extern void PID_init(pid_type_def *pid, uint8_t mode, const fp32 PID[3], fp32 max_out, fp32 max_iout);
extern fp32 PID_calc(pid_type_def *pid, fp32 ref, fp32 set);
extern void PID_clear(pid_type_def *pid);
#endif
大疆官例写的比较直观,有面向对象的感觉了,用起来舒服,但是也是直接套公式,没有加入一些优化pid的方法,下一版本我将用完善版本的。
7、调参过程记录
先放经典图,参数的作用看图
速度单环:周期中代码如下
enc = Read_Encoder(8);
pwm = PID_calc(&motor_speed_pid,enc,target);//速度环
AAC_MotorFL_Run(pwm);
位置单环:周期中代码如下
enc += Read_Encoder(8);
pwm = PID_calc(&motor_angle_pid,enc,target);//位置环
AAC_MotorFL_Run(pwm);
位置速度双环:周期中代码如下
enc += Read_Encoder(8);
pwm = PID_calc(&motor_angle_pid,enc,target);//位置环
pwm = PID_calc(&motor_speed_pid,Read_Encoder(8),pwm);//速度环
AAC_MotorFL_Run(pwm);
角度控制效果 ,调的不是很好,下次一定hhh
说明:做角度控制其实单独使用位置环已经有一定效果了,但是没有串起来效果好,也没有串起来稳定,所以建议还是双闭环,角度仅仅作为一个调参练习即可。
源码我已上传到csdn,链接如下