目录

1、PID框图

2、pid控制器的表达式

3、传感器数据获取

4、硬件设计

5、工程配置

6、软件部分程序配置

7、调参过程记录


本文已更新,加上曲线调试,更好效果,更多内容,详情:

编码电机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
基本定时TIM61ms一次

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,链接如下 

直流编码电机速度位置双闭环-制造文档类资源-CSDN文库

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐