stm32直流电机PID控制hal库(Cubemx)

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:自动重载寄存器写入新值后,存放在预装载寄存器中,该值不会马上生效,计数器按照原来旧的溢出值进行计数。当计数溢出后,该计数值才会生效(由预装载寄存器转入影子寄存器),开始新的计数周期
一般而言:预装载功能在多个定时器同时输出信号时比较有用,可以确保多个定时器的输出信号在同一个时刻变化,实现同步输出。单个定时器使用时,一般不开启预装载功能。

stm32直流电机PID控制hal库(Cubemx)_第1张图片

2.电机控制引脚配置

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

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

3.用户代码文件编写

我们在MDK_ARM文件夹下创建一个user的文件夹(我们会在这个文件夹中加入自己写的相关文件),并在次文件夹下分别创建moto.c moto.h文件,打开工程并将文件添加在keil文件中(这里不在赘述如何添加)
在文件中我们找到89行:添加如下代码
在这里插入图片描述
并在moto.c中写入控制正反转的函数:
stm32直流电机PID控制hal库(Cubemx)_第2张图片
在主函数中开启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直流电机PID控制hal库(Cubemx)_第3张图片
如图所示我们打开一个新的定时器(因为stm32是不允许同一个定时器又用到pwm输出功能和编码器功能,所以开一个新的定时器,我用的是TIM4),我们开启encoder mode,然后选择四倍频,滤波我们填写10。

霍尔编码器会输出两路方波信号,如果只在通道A的上升沿计数,那就是1倍频;通道A的上升、下降沿计数,那就是2倍频;如果在通道A、B的上升、下降沿计数,那就是4倍频。
stm32直流电机PID控制hal库(Cubemx)_第4张图片使用倍频可以最大程度地利用两路信号,提高测速的灵敏度

2.定时器中断配置

我们还需再打开一个定时器用来计时间隔
stm32直流电机PID控制hal库(Cubemx)_第5张图片
这里我们这样配置是为了每10ms(及100HZ)检测速度;并且要打开中断(NVIC)

3.串口发送配置

由于我们现在需要通过串口打印出相应的代码,所以我们加入usart
stm32直流电机PID控制hal库(Cubemx)_第6张图片

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
并在魔法棒里面包括微库:
stm32直流电机PID控制hal库(Cubemx)_第7张图片
开启定时器以及编码器:

stm32直流电机PID控制hal库(Cubemx)_第8张图片
主程序中保持不变,我们在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);

然后将线连接在编码器的输出口:
(保证此时电机是正传)
stm32直流电机PID控制hal库(Cubemx)_第9张图片
此时串口打印出来的数据可能是很大数据,也可能是较小的数据,我们规定正转时时小的数据,所以如若此时打印出来的是很大的数据,那我们就需要把两根线互换一下即可。

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

下面我们通过编写相关函数来输出正反转以及,此时的转速(RPM)
将 /* USER CODE END TIM3_IRQn 0 / 的后面写入如下的代码:
stm32直流电机PID控制hal库(Cubemx)_第10张图片
在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(); 函数:

stm32直流电机PID控制hal库(Cubemx)_第11张图片

随后在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的定时器中断中串口发送注释。

stm32直流电机PID控制hal库(Cubemx)_第12张图片
然后可以对自己的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就行深入探讨。

你可能感兴趣的:(电机,PID,stm32,stm32,单片机,嵌入式硬件)