直流电机PID学习

学习记录:

参考野火的电机开发,最近尝试了一下直流电机的驱动。电机的型号是https://item.taobao.com/item.htm?spm=a1z2k.11010449.931864.2.362e509dj90qpJ&scm=1007.13982.82927.0&id=45347924687&last_time=1616899617
在这里插入图片描述
驱动电路是最常见的L298n,单路最大2A输出。


学习内容:

PID调试准备
1、编码器使用(获取转速)
2、 PWM电机驱动
3、 PID移植
4、 上位机调试


编码器使用:

这里使用的是霍尔编码器,1:30减速比,编码器线数13ppr。相当于电机一圈有390个信号出现。
编码器原理比较简单,就是比较常见的正交解码。通过两个方波信号进行判断正反转和速度。正反通过相位差判断(90度还是270度),速度通过频率判断,这里我们使用STM32的正交解码功能。每一次经过编码器就会有四个边沿变化。


在这里插入图片描述

可以看到定时器上面的TF1和TF2的编码器接口,然后我们要进行四分频,否则得到的计数值为四倍。当然也可以发现他会使用CNT计数器进行计数,这也代表我们的PWM就不能输出。意思是为了一个编码,这个定时器基本就没有其他可以用的了。所以不要希望还能有PWM输出了。

首先cube配置只展示关键部分
CUBE配置
主要是四分频,选择两个通道,上升沿触发,分频4-1,滤波器1-15随便选,现在没看到。
之后进行启动旧可以了。

  HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL);//开始编码器器
  __HAL_TIM_GET_COUNTER(&htim4)//获取计数值

如果每有一次信号,CNT就会加一或者减一,当然有人会发现CNT16位,意味着溢出是很有可能的。因此处理溢出很重要。这里有两种方法可以实现。
首先是野火的处理方式,这个是最完善的,可以处理多圈溢出。

//使用定时器溢出来获取,这种是最完善的
 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
 {
	 /* 判断当前计数器计数方向 */
	 if(__HAL_TIM_IS_TIM_COUNTING_DOWN(&TIM_EncoderHandle))
	 /* 下溢 */
	 Encoder_Overflow_Count--;
	 else
	 /* 上溢 */
	 Encoder_Overflow_Count++;
 }

这是我的处理方式,比较简单粗暴,通过计数值来判断正反转,考虑到我的电机转速上限不高,并且我获取速度还挺快的,所以使用了这个。

//这个是我实现的方法,缺点是转速不能过快,因为我这个算法溢出不能多次,当然经我实测,一般不会出现,所以我采用了。
int32_t get_speed(void)
{
      int32_t temp;
      static uint16_t last_cnt;
      uint16_t now_cnt=__HAL_TIM_GET_COUNTER(&htim4);
      //判断计数变多还是变少
      if(now_cnt>last_cnt)
      {
          //判断是溢出导致还是正常情况
          if(last_cnt<10000 && now_cnt>55535)
          {//溢出了导致的
              temp=0xffff-now_cnt+last_cnt;
              last_cnt=now_cnt;
              return temp;
          }
          else
          {
              temp=last_cnt-now_cnt;
              last_cnt=now_cnt;
              return temp;
          }
      }
      else
      {
          //判断是溢出导致还是正常情况
          if(now_cnt<10000 && last_cnt>55535)
          {//溢出了导致的
              temp= 0xffff-now_cnt+last_cnt;
              last_cnt=now_cnt;
              return temp;
          }
          else
          {
              temp= last_cnt-now_cnt;
              last_cnt=now_cnt;
              return temp;
          }       
      }
}

到此为止,计数就完成了。

PWM电机驱动:

在这里插入图片描述

电机驱动是H桥电路,原理不细讲。这里是L298N的驱动方法。比较简单,我们只要对两个信号进行PWM输出就可以了。
在这里插入图片描述

void  set_pwm(float period)
{
   //我的周期的999,所以上限制也是999
    if(period>0)
    {
        uint16_t temp=period; 
        if(temp>999)
          temp=999;
        TIM3->CCR1=temp;
        TIM3->CCR2=0;
    }
    
    else
    {
        uint16_t temp=-1*period; 
        if(temp>999)
          temp=999;
        TIM3->CCR1=0;
        TIM3->CCR2=temp;
    }
      /**TIM3 GPIO Configuration
    PA6     ------> TIM3_CH1
    PA7     ------> TIM3_CH2
    PB0     ------> TIM3_CH3
    PB1     ------> TIM3_CH4
    */

}

PID移植:

移植野火的PID算法。

typedef struct
{
    float target_val; //目标值
    float actual_val; //实际值
    float err; //定义偏差值
    float err_last; //定义上一个偏差值
    float Kp,Ki,Kd; //定义比例、积分、微分系数
    float integral; //定义积分值
}_pid1;


/*pid*/
typedef struct
{
    float target_val; //目标值
    float actual_val; //实际值
    float err; //定义当前偏差值
    float err_next; //定义下一个偏差值
    float err_last; //定义最后一个偏差值
    float Kp, Ki, Kd; //定义比例、积分、微分系数
}_pid2;
//位置PID
void PID1_param_init(_pid1* pid)
{
   /* 初始化参数 */
   pid->target_val=0.0;
   pid->actual_val=0.0;
   pid->err=0.0;
   pid->err_last=0.0;
   pid->integral=0.0;
   pid->Kp = 0.31;
   pid->Ki = 0.070;
   pid->Kd = 0.3;
}
float PID1_realize(float temp_val,_pid1* pid)
{
   /* 计算目标值与实际值的误差 */
   pid->err=pid->target_val-temp_val;
   /* 误差累积 */
   pid->integral+=pid->err;
   /*PID 算法实现 */
   pid->actual_val=pid->Kp*pid->err+pid->Ki*pid->integral+pid->Kd*(pid->err-pid->err_last);
   /* 误差传递 */
   pid->err_last=pid->err;
   /* 返回当前实际值 */
   return pid->actual_val;
}
//增量PID
void PID2_param_init(_pid2* pid)
{
  /* 初始化参数 */
  pid->target_val=0.0;
  pid->actual_val=0.0;
  pid->err = 0.0;
  pid->err_last = 0.0;
  pid->err_next = 0.0;
  pid->Kp = 0.21;
  pid->Ki = 0.80;
  pid->Kd = 0.01;
}


float PID2_realize(float temp_val,_pid2* pid)
{
  /* 计算目标值与实际值的误差 */
   pid->err=pid->target_val-temp_val;
   /*PID 算法实现 */
   float increment_val = pid->Kp*(pid->err - pid->err_next) + pid->Ki*pid->err+ pid->Kd*(pid->err - 2 * pid->err_next + pid->err_last);
   /* 累加 */
   pid->actual_val += increment_val;
   /* 传递误差 */
   pid->err_last = pid->err_next;
   pid->err_next = pid->err;
   /* 返回当前实际值 */
   return pid->actual_val;
}

放到定时器进行循环计算输出。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{//50ms的定时器中断
    if (htim->Instance==TIM6)
    {
      float speed=1.0*get_speed();
      set_pwm(10*PID2_realize(speed,&pid2[0]));   
      set_computer_channel(0,speed);
      set_computer_channel(1,pid2[0].target_val);
      set_computer_channel(2,pid2[0].actual_val*10.0);
    }
}

上位机调试:

这里使用现成的PID调试软件,有三个。野火的PID软件上位机莫名其妙丢帧,不知道谁可以帮我解决。无名的协议太复杂,移植费劲。这里选择的VOFA+软件,可以自己编写命令,非常好用啊!
在这里插入图片描述
首先发送P I D直接发送字母,然后的第二个字节代表PID通道,后面四个字节浮点数代表实际的数据。然后接入滑动条。
在这里插入图片描述
这样调参就完成了。

//
void set_computer_channel(uint8_t pid_channel,float data)
{
  float temp=data;
   if(s_tx_buf[4*channel_num+3] !=0x7f)
   {
      s_tx_buf[4*channel_num]=0x00;
      s_tx_buf[4*channel_num+1]=0x00;
      s_tx_buf[4*channel_num+2]=0x80;
      s_tx_buf[4*channel_num+3]=0x7f;
   }
   memcpy(s_tx_buf+4*pid_channel,&temp,4);
   HAL_UART_Transmit_DMA(&huart1,s_tx_buf,sizeof(s_tx_buf));
}

void set_pid1(_pid1* pid,uint8_t data_type,float data)
{
   switch (data_type)
   {
        case 'P':pid->Kp=data;break;
        case 'I':pid->Ki=data;break;
        case 'D':pid->Kd=data;break;
        case 'T':pid->target_val=data;break; 
   }
}


void set_pid2(_pid2* pid,uint8_t data_type,float data)
{
   switch (data_type)
   {
        case 'P':pid->Kp=data;break;
        case 'I':pid->Ki=data;break;
        case 'D':pid->Kd=data;break;
        case 'T':pid->target_val=data;break; 
   }
}

void get_computer_channel(uint8_t* data,uint8_t len)
{       
   pid_channel_data* p= (pid_channel_data*)data;
   switch ((*p).header)
   {
        case 'P':set_pid2(&pid2[(*p).channel],'P',(*p).data);break;
        case 'I':set_pid2(&pid2[(*p).channel],'I',(*p).data);break;
        case 'D':set_pid2(&pid2[(*p).channel],'D',(*p).data);break;
        case 'T':set_pid2(&pid2[(*p).channel],'T',(*p).data);break;
   }
}

在这里插入图片描述
效果如图,因为空载,所以PID很容易调个差不多。
在这里插入图片描述

  • 13
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值