“实现精准控制:使用STM32 HAL库及Cubemx进行PID调节直流电机”

stm32直流电机PID控制hal库(Cubemx),一步步手把手教你怎么配置cubemx怎么写代码。
未对pid就行深入解析,不过相信您通过配置和写代码以后大概可以知道pid的主要作用。

文章目录

  • 前言
  • 一、进行pwm输出和相关引脚的配置
  • 1.PWM输出配置
  • 2.电机控制引脚配置
  • 3.用户代码文件编写
  • 二、通过encoder来获取当前转速
  • 1.编码器encouder配置
  • 2.定时器中断配置
  • 3.串口发送配置
  • 4.霍尔编码器输出说明
  • 5.用户代码编写
  • 三、PI控制速度
  • 1.简单验证并调试
  • 2.实现电机的正反转
  • 再次修改it.c文件中的中断服务函数
  • 修改control.c文件如下:(齐全代码):
  • 3.通过上位机打印波形
  • 四、pid控制位置
  • 1.对相关函数进行编写
  • 2.控制电机转动位置的公式
  • 总结

  • 前言

    本人才疏学浅、文笔浅薄,对于pid调节和Cubemx的使用大多都是末学肤受,请您斧正!

    这次我们使用的电机驱动芯片是TB213A;
    我采用的芯片是stm32f401,使用的电机为MG513P10 12V,还需要20kΩ和33kΩ的电阻后面会说到。
    本文使用的是Cubemx生成的基于hal库的开发,暂未采用Freertos操作系统。


    一、进行pwm输出和相关引脚的配置

    1.PWM输出配置

    PWM的频率大概是10KHZ,CK_CNT=TIMXCLK/(PSC+1)。所以我们将预分配系数为0,arr寄存器的值为8400-1;

    对“auto-reload preload”的设定值的一个提示:
    auto-reload preload=Disable:自动重载寄存器写入新值后,该计数值立刻生效,作为当前计数周期的溢出值。
    auto-reload preload=Enable:自动重载寄存器写入新值后,存放在预装载寄存器中,该值不会马上生效,计数器按照原来旧的溢出值进行计数。当计数溢出后,该计数值才会生效(由预装载寄存器转入影子寄存器),开始新的计数周期
    一般而言:预装载功能在多个定时器同时输出信号时比较有用,可以确保多个定时器的输出信号在同一个时刻变化,实现同步输出。单个定时器使用时,一般不开启预装载功能。

    2.电机控制引脚配置

    对于定时器而言除了红色位置需要修改以外其他地方不需要修改。(其他基础配置不做解析)。
    TB213A需要两个引脚来控制电机的正反转以及停止所以我们在定义两个gpio为输出:
    (为了方便我们使用user label)

    TB213A还有一个STBY引脚他需要使能高电平,直接接入单片机的vcc+即可。

    3.用户代码文件编写

    我们在MDK_ARM文件夹下创建一个user的文件夹(我们会在这个文件夹中加入自己写的相关文件),并在次文件夹下分别创建moto.c moto.h文件,打开工程并将文件添加在keil文件中(这里不在赘述如何添加)
    在文件中我们找到89行:添加如下代码

    并在moto.c中写入控制正反转的函数:

    在主函数中开启pwm以及确定正反转就可以了


    二、通过encoder来获取当前转速

    stm32芯片有硬件编码器功能,所以得到转速很方便。得到转速有几种方法:
    常用的编码器测速方法一般有三种:M 法、T 法和M/T 法。

    M 法:又叫做频率测量法。这种方法是在一个固定的定时时间内(以秒为单位),统计这段时间的编码器脉冲数,计算速度值。设编码器单圈总脉冲数为C,在时间T0 内,统计到的编码器脉冲数为M0,则转速n 的计算公式为:N=M0/(C*T0)
    公式中的编码器单圈总脉冲数C 是常数,所以转速n 跟M0 成正比。这就使得在高速测量时M0变大,可以获得较好的测量精度和平稳性,但是如果速度很低,低到每个T0 内只有少数几个脉冲,此时算出的速度误差就会比较大,并且很不稳定。也有一些方法可以改善M 法在低速测量的准确性,上一节提到的增量式编码器倍频技术就是其中一种,比如原本捕获到的脉冲M0 只有4 个,经过4 倍频后,相同电机状态M0 变成了16 个,也就提升了低速下的测量精度。

    T 法:又叫做周期测量法。这种方法是建立一个已知频率的高频脉冲并对其计数,计数时间由捕获到的编码器相邻两个脉冲的间隔时间TE 决定,计数值为M1。设编码器单圈总脉冲数为C,高频脉冲的频率为F0,则转速n 的计算公式为:

    公式中的编码器单圈总脉冲数C 和高频脉冲频率F0 是常数,所以转速n 跟M1 成反比。从公式可以看出,在电机高转速的时候,编码器脉冲间隔时间TE 很小,使得测量周期内的高频脉冲计数值M1 也变得很少,导致测量误差变大,而在低转速时,TE 足够大,测量周期内的M1 也足够多,所以T 法和M 法刚好相反,更适合测量低速。

    M/T 法:这种方法综合了M 法和T 法各自的优势,既测量编码器脉冲数又测量一定时间内的高频脉冲数。在一个相对固定的时间内,计数编码器脉冲数M0,并计数一个已知频率为F0 的高频脉冲,计数值为M1,计算速度值。设编码器单圈总脉冲数为C,则转速n 的计算公式为:

    由于M/T 法公式中的F0 和C 是常数,所以转速n 就只受M0 和M1 的影响。电机高速时,M0 增大,M1 减小,相当于M 法,低速时,M1 增大,M0 减小,相当于T 法。

    1.编码器encouder配置


    如图所示我们打开一个新的定时器(因为stm32是不允许同一个定时器又用到pwm输出功能和编码器功能,所以开一个新的定时器,我用的是TIM4),我们开启encoder mode,然后选择四倍频,滤波我们填写10。

    霍尔编码器会输出两路方波信号,如果只在通道A的上升沿计数,那就是1倍频;通道A的上升、下降沿计数,那就是2倍频;如果在通道A、B的上升、下降沿计数,那就是4倍频。
    使用倍频可以最大程度地利用两路信号,提高测速的灵敏度

    2.定时器中断配置

    我们还需再打开一个定时器用来计时间隔

    这里我们这样配置是为了每10ms(及100HZ)检测速度;并且要打开中断(NVIC)

    3.串口发送配置

    由于我们现在需要通过串口打印出相应的代码,所以我们加入usart

    4.霍尔编码器输出说明

    在本实验中由于霍尔编码器的输出的高电平为5v为了保护单片机我们这里使用一个及其简单的分压电路即可。(一个33kΩ另一个20kΩ)

    5.用户代码编写

    由于用到了usart输出为了方便起见我们加入微库中的printf函数,我们为他重定义一下
    在usart.c文件的末尾的 /* USER CODE BEGIN 1 */ 处加入以下代码:

    int fputc(int ch,FILE *f)
    {
    	HAL_UART_Transmit(&huart1,(uint8_t*)&ch,1,1000);
    	return (ch);
    }
    int fgetc(FILE *f)
    {
    	uint8_t ch;
    	HAL_UART_Receive(&huart1,(uint8_t *)&ch,sizeof(ch),0xffff);
    	return ch;
    }
    

    这里我们需要这个文件的上方加入#include <stdio.h>
    并在魔法棒里面包括微库:

    开启定时器以及编码器:


    主程序中保持不变,我们在it.c(中断文件),最下方找到void TIM3_IRQHandler(void) 函数我们将在这里写相关函数(没有用回调函数)在 /* USER CODE END TIM3_IRQn 0 */ 的后面写入如下的代码:

    	int encoder_count;
    	encoder_count= TIM4->CNT;
    	TIM4->CNT=0;
    	printf("脉冲数:%d\n",encoder_count);
    

    然后将线连接在编码器的输出口:
    (保证此时电机是正传)

    此时串口打印出来的数据可能是很大数据,也可能是较小的数据,我们规定正转时时小的数据,所以如若此时打印出来的是很大的数据,那我们就需要把两根线互换一下即可。

    为了减少误差,以及防止引入浮点数等问题,我建议在底层层面上都使用脉冲数。

    下面我们通过编写相关函数来输出正反转以及,此时的转速(RPM)
    将 /* USER CODE END TIM3_IRQn 0 / 的后面写入如下的代码:

    在rpm=encoder_count/13.0/10.0/4.0
    6000;中
    13为旋转一圈所产生的脉冲数,即脉冲数/转(Pulse Per Revolution 或PPR)
    10.0 是本减少直流电机的减速比
    4.0是采用了双通道,几倍频就除以几
    6000是本中断0.01s一次,*6000就可以得到一分钟的转速


    三、PI控制速度

    pid的原理这里不再赘述,b站上有一个很棒的通俗易懂的解释:
    b站

    1.简单验证并调试

    在user文件夹下创建control.h和control.c并放入keil文件中
    在main.c文件中加入一些变量:

    注意主函数中原来的代码
    在中断服务函数(在it.c)中只打印现在的脉冲数,并加入Control_function(); 函数:

    随后在control.c文件中进行编写:

    #include "control.h"
    #include "main.h"
    #include "moto.h"
    #include "tim.h"
    
    extern int Encoder_count,Target_Velocity; //编码器的脉冲计数,现在和目标
    extern float Velocity_KP,Velocity_KI,Velocity_KD; //pid参数
    
    void Control_function(void)
    {
    	Moto_pwm=Incremental_PI(Encoder_count,Target_Velocity);//希望有30个脉冲
    	Set_Pwm(Moto_pwm);
    }
    /*
    *	@函数功能:增量PI控制器
    *	入口参数:编码器测量值,目标速度
    *	返回值:电机PWM
    *	根据增量式离散PID公式 
    *	pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)+Kd[e(k)-2e(k-1)+e(k-2)]
    *	e(k)代表本次偏差 
    *	e(k-1)代表上一次的偏差  以此类推 
    *	pwm代表增量输出
    *	在我们的速度控制闭环系统里面,只使用PI控制
    *	pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)
    */
    int Incremental_PI (int Encoder,int Target)
    { 	
    	 static float Bias,Pwm,Last_bias;
    	 Bias=Target-Encoder;                                  //计算偏差
    	 Pwm+=Velocity_KP*(Bias-Last_bias)+Velocity_KI*Bias;   //增量式PI控制器
    	 Last_bias=Bias;	                                   	 //保存上一次偏差 
    	 return Pwm;                                           //增量输出
    }
    /**
    *	函数功能:赋值给PWM寄存器
    *	入口参数:PWM
    *	返回值:无
    */
    void Set_Pwm(int moto)
    {
    	int pwm_abs;
    	{Moto(0);}//正传
    			
    	pwm_abs=myabs(moto);		
    	__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,pwm_abs);
    	
    }
    
    

    加入后运行程序,程序中的p I 的值是需要根据自己的电机进行相应的调整
    常见问题有:
    电机转的超级慢:大概是p太小了(最好把i也设进去,一般i先设为1)
    电机完全不转或者转的超级快:多半是Moto();这个函数(控制正反转)中的1或者0填错了

    例如我的电机我设置的预期转速为 300 rpm,由上面的一个公式(rpm=encoder_count/13.0/10.0/4.0*6000)我们可以知道,当encoder_count为1的时候我们的rpm为11.538,所以我们的精度也只能停留在11.538rpm,如若超调量需要的更加精细那就可以从公式入手,或者采用T法。

    2.实现电机的正反转

    实现正反转相对于单向转动稍微复杂一些,并且我们在这里进行一下输出的限幅,
    在main.c中再加入一些全局变量

    再次修改it.c文件中的中断服务函数

      /* USER CODE BEGIN TIM3_IRQn 1 */
    
    	int encode_tim;
    	encode_tim= TIM4->CNT;
    	if(encode_tim>0xefff)
    	{
    		encode_tim=encode_tim-0xffff;
    	}
    	
    	Encoder_count= encode_tim;        //这样就可以得到负的脉冲数,当是负的时候就说明是反转
    	printf("现在的脉冲数为%d\n",Encoder_count);
    	
    	
    
    	float rpm_;
    	rpm_=Encoder_count/13.0/10.0/4.0*6000;
    	printf("PMR= %.3f\n",rpm_);
    	
    	
    	TIM4->CNT=0;
    	Control_function();//在contr.c文件中进行数据的处理
      /* USER CODE END TIM3_IRQn 1 */
    
    

    修改control.c文件如下:(齐全代码):

    #include "control.h"
    #include "main.h"
    #include "moto.h"
    #include "tim.h"
    extern int Encoder_count,Target_Velocity; //编码器的脉冲计数,现在和目标
    extern float Velocity_KP,Velocity_KI,Velocity_KD; //pid参数
    extern int target_rpm;//目标每分钟的圈数
    extern int Moto_pwm;
    void Control_function(void)
    {
    	Target_Velocity =(int)(target_rpm/6000.0*4*10*13);//将目标rpm转化为编码器需要的脉冲计数转化为int类型的
    	Moto_pwm=Incremental_PI(Encoder_count,Target_Velocity);
    	limiting_Pwm();
    	Set_Pwm(Moto_pwm);
    }
    /*
    *	函数功能:取绝对值
    *	入口参数:int 
    *	返回值:无 unsingned int 
    */
    int myabs(int num)
    {
    	int temp;
    	if(num<0)	temp=-num;
    	else temp =num;
    	return temp;
    }
    /**
    *	函数功能:赋值给PWM寄存器
    *	入口参数:PWM
    *	返回值:无
    */
    void Set_Pwm(int moto)
    {
    	int pwm_abs;
      	if(moto<0)
    	{Moto(1);}//反转				
    	else
    	{Moto(0);}//正传
    	pwm_abs=myabs(moto);		
    	__HAL_TIM_SET_COMPARE(&htim1,TIM_CHANNEL_1,pwm_abs);
    }
    /*
    *	函数功能:限制PWM赋值 
    *	入口参数:无
    *	返回值:无
    */
    void limiting_Pwm(void)
    {	
    	  int maximum=8000;    //===PWM满幅是8400 限制在8000
    	  if(Moto_pwm<-maximum) Moto_pwm=-maximum;	
    		if(Moto_pwm>maximum)  Moto_pwm=maximum;		
    }
    /*
    *	@函数功能:增量PI控制器
    *	入口参数:编码器测量值,目标速度
    *	返回值:电机PWM
    *	pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)+Kd[e(k)-2e(k-1)+e(k-2)]
    *	e(k)代表本次偏差 
    *	e(k-1)代表上一次的偏差  以此类推 
    *	pwm代表增量输出
    *	在我们的速度控制闭环系统里面,只使用PI控制
    *	pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)
    */
    int Incremental_PI (int Encoder,int Target)
    { 	
    	 static float Bias,Pwm,Last_bias;
    	 Bias=Target-Encoder;                                  //计算偏差
    	 Pwm+=Velocity_KP*(Bias-Last_bias)+Velocity_KI*Bias;   //增量式PI控制器
    	 Last_bias=Bias;	                                   	 //保存上一次偏差 
    	 return Pwm;                                           //增量输出
    }
    
    

    3.通过上位机打印波形

    在user文件夹下创建四个文件分别为:show.c show.h DataScop_DP.c DataScop_DP.h
    并将文件置于keil文件中。
    将程序移植到相关的文件中:

    show.c

    #include "show.h"
    #include "main.h"
    
    #include "DataScop_DP.h"
    
    extern int Encoder_count,Target_Velocity; //编码器的脉冲计数,现在和目标
    
    /**************************************************************************
    函数功能:虚拟示波器往上位机发送数据 
    入口参数:无
    返回  值:无
    **************************************************************************/
    void DataScope(void)
    {   
    	int Send_Count,i;//计数需要的变量
    	
    	DataScope_Get_Channel_Data( Encoder_count, 1 );      
    	DataScope_Get_Channel_Data( Target_Velocity, 2 );      
    //	DataScope_Get_Channel_Data( 0, 3 );              
    //	DataScope_Get_Channel_Data( 0 , 4 );   
    //	DataScope_Get_Channel_Data(0, 5 ); //用您要显示的数据替换0就行了
    //	DataScope_Get_Channel_Data(0 , 6 );//用您要显示的数据替换0就行了
    //	DataScope_Get_Channel_Data(0, 7 );
    //	DataScope_Get_Channel_Data( 0, 8 ); 
    //	DataScope_Get_Channel_Data(0, 9 );  
    //	DataScope_Get_Channel_Data( 0 , 10);//一共可以打印10个数据查看波形
    	Send_Count = DataScope_Data_Generate(2);//打印几个数据就在这里改为几
    	for( i = 0 ; i < Send_Count; i++) 
    	{
    	while((USART1->SR&0X40)==0);  
    	USART1->DR = DataScope_OutPut_Buffer[i]; 
    	}
    }
    
    

    show.h

    #ifndef __SHOW_H
    #define __SHOW_H
    
    void DataScope(void);
    #endif
    
    

    DataScop_DP.c

    #include "DataScop_DP.h"
    
    unsigned char DataScope_OutPut_Buffer[42] = {0};	   //串口发送缓冲区
    //函数说明:将单精度浮点数据转成4字节数据并存入指定地址 
    //附加说明:用户无需直接操作此函数 
    //target:目标单精度数据
    //buf:待写入数组
    //beg:指定从数组第几个元素开始写入
    //函数无返回 
    void Float2Byte(float *target,unsigned char *buf,unsigned char beg)
    {
        unsigned char *point;
        point = (unsigned char*)target;	  //得到float的地址
        buf[beg]   = point[0];
        buf[beg+1] = point[1];
        buf[beg+2] = point[2];
        buf[beg+3] = point[3];
    }
    //函数说明:将待发送通道的单精度浮点数据写入发送缓冲区
    //Data:通道数据
    //Channel:选择通道(1-10)
    //函数无返回 
    void DataScope_Get_Channel_Data(float Data,unsigned char Channel)
    {
    	if ( (Channel > 10) || (Channel == 0) ) return;  //通道个数大于10或等于0,直接跳出,不执行函数
      else
      {
         switch (Channel)
    		{
          case 1:  Float2Byte(&Data,DataScope_OutPut_Buffer,1); break;
          case 2:  Float2Byte(&Data,DataScope_OutPut_Buffer,5); break;
    		  case 3:  Float2Byte(&Data,DataScope_OutPut_Buffer,9); break;
    		  case 4:  Float2Byte(&Data,DataScope_OutPut_Buffer,13); break;
    		  case 5:  Float2Byte(&Data,DataScope_OutPut_Buffer,17); break;
    		  case 6:  Float2Byte(&Data,DataScope_OutPut_Buffer,21); break;
    		  case 7:  Float2Byte(&Data,DataScope_OutPut_Buffer,25); break;
    		  case 8:  Float2Byte(&Data,DataScope_OutPut_Buffer,29); break;
    		  case 9:  Float2Byte(&Data,DataScope_OutPut_Buffer,33); break;
    		  case 10: Float2Byte(&Data,DataScope_OutPut_Buffer,37); break;
    		}
      }	 
    }
    //函数说明:生成 DataScopeV1.0 能正确识别的帧格式
    //Channel_Number,需要发送的通道个数
    //返回发送缓冲区数据个数
    //返回0表示帧格式生成失败 
    unsigned char DataScope_Data_Generate(unsigned char Channel_Number)
    {
    	if ( (Channel_Number > 10) || (Channel_Number == 0) ) { return 0; }  //通道个数大于10或等于0,直接跳出,不执行函数
      else
      {	
    	 DataScope_OutPut_Buffer[0] = '$';  //帧头
    		
    	 switch(Channel_Number)   
       { 
    		 case 1:   DataScope_OutPut_Buffer[5]  =  5; return  6;  
    		 case 2:   DataScope_OutPut_Buffer[9]  =  9; return 10;
    		 case 3:   DataScope_OutPut_Buffer[13] = 13; return 14; 
    		 case 4:   DataScope_OutPut_Buffer[17] = 17; return 18;
    		 case 5:   DataScope_OutPut_Buffer[21] = 21; return 22;  
    		 case 6:   DataScope_OutPut_Buffer[25] = 25; return 26;
    		 case 7:   DataScope_OutPut_Buffer[29] = 29; return 30; 
    		 case 8:   DataScope_OutPut_Buffer[33] = 33; return 34; 
    		 case 9:   DataScope_OutPut_Buffer[37] = 37; return 38;
         case 10:  DataScope_OutPut_Buffer[41] = 41; return 42; 
       }	 
      }
    	return 0;
    }
    
    

    DataScop_DP.h

    #ifndef __DATA_PRTOCOL_H
    #define __DATA_PRTOCOL_H
     
    extern unsigned char DataScope_OutPut_Buffer[42];	   //待发送帧数据缓存区
    
    
    void DataScope_Get_Channel_Data(float Data,unsigned char Channel);    // 写通道数据至 待发送帧数据缓存区
    
    unsigned char DataScope_Data_Generate(unsigned char Channel_Number);  // 发送帧数据生成函数 
     
     
    #endif 
    
    

    这里我们需要上位机软件,软件在群文件中可以获取;
    为了防止发生串口发送和上位机解析的错误,我们将it.c的定时器中断中串口发送注释。


    然后可以对自己的pid进行调整,本例中pid调节达到预期效果设置每0.01s产生26个脉冲。


    四、pid控制位置

    上一节中我们使用pi增量式来控制电机的转速,当然你也可以自己增加d项,这个也比较简单,这一章我们将使用pid的位置式来控制电机的位置(位置式并不是来控制位置的,相关的基础知识可在百度中查询)
    有了上面的pi速度调节的经验后我们在实现pid位置调节的时候就可以轻车熟路了。

    1.对相关函数进行编写

    main.c

    /*
    *位置控制的相关全局变量
    *
    */
    int CurrentPosition,Target_Position=260;//当前位置(从转动位置开始现在的总脉冲数+加-减),预期位置
    int moto_position;//电机pwm变量
    float Position_KP=120,Position_KI=0.1,Position_KD=500; //PID系数
    

    it.c
    在中断服务函数中我们把原来的Control_function() 函数注释掉换为:Control_Position();

    control.c

    我们先外部声明(extern)一下刚刚在main.c中创建的全局变量
    现在我们编写位置控制函数:

    /*
    *	函数功能: 位置算法
    *	入口参数:无
    *	返回值:无
    */
    void Control_Position(void)
    {
    	CurrentPosition+=Encoder_count;//将编码器读取的(速度)积分得到位置
    	moto_position=Position_PID(CurrentPosition,Target_Position);    //===位置PID控制器
    	limiting_Pwm();																					 //===PWM限幅
    	Set_Pwm(moto_position);
    	
    }
    
    /*
    *	函数功能:位置式PID控制器
    *	入口参数:编码器测量位置信息,目标位置
    *	返回  值:电机PWM
    *	根据位置式离散PID公式 
    *	pwm=Kp*e(k)+Ki*∑e(k)+Kd[e(k)-e(k-1)]
    *	e(k)代表本次偏差 
    *	e(k-1)代表上一次的偏差  
    *	∑e(k)代表e(k)以及之前的偏差的累积和;其中k为1,2,,k;
    *	pwm代表输出
    **************************************************************************/
    int Position_PID (int position,int target)
    { 	
    	 static float Bias,Pwm,Integral_bias,Last_Bias;
    	 Bias=target-position;                                  //计算偏差
    	 Integral_bias+=Bias;	                                 //求出偏差的积分
    	 if(Integral_bias>3000)Integral_bias=3000;				//对积分 限幅
    	 if(Integral_bias<-3000)Integral_bias=-3000;		//积分限幅 防止到达目标位置后过冲
    	 Pwm=Position_KP*Bias+Position_KI*Integral_bias+Position_KD*(Bias-Last_Bias);       //位置式PID控制器
    	 Last_Bias=Bias;                                       //保存上一次偏差 
    	 return Pwm;                                           //增量输出
    }
    
    

    2.控制电机转动位置的公式

    本例程式10ms读取一次编码器(100HZ),我使用的电机的减速比为10,霍尔编码器的精度为13,又我们式AB双相的所以我们是4倍频,那么电机转一圈编码器读取为 10×13×4=520 我的设置的为260,自然就是正向转动半圈。


    总结

    感谢您能阅览本篇拙笔。本文仅仅简单介绍了使用cubemx来配置加上一些简单代码来实现,并未对pid就行深入探讨。

    物联沃分享整理
    物联沃-IOTWORD物联网 » “实现精准控制:使用STM32 HAL库及Cubemx进行PID调节直流电机”

    发表评论