stm32直流电机PID控制hal库(Cubemx),一步步手把手教你怎么配置cubemx怎么写代码。
未对pid就行深入解析,不过相信您通过配置和写代码以后大概可以知道pid的主要作用。
本人才疏学浅、文笔浅薄,对于pid调节和Cubemx的使用大多都是末学肤受,请您斧正!
这次我们使用的电机驱动芯片是TB213A;
我采用的芯片是stm32f401,使用的电机为MG513P10 12V,还需要20kΩ和33kΩ的电阻后面会说到。
本文使用的是Cubemx生成的基于hal库的开发,暂未采用Freertos操作系统。
PWM的频率大概是10KHZ,CK_CNT=TIMXCLK/(PSC+1)。所以我们将预分配系数为0,arr寄存器的值为8400-1;
对“auto-reload preload”的设定值的一个提示:
auto-reload preload=Disable:自动重载寄存器写入新值后,该计数值立刻生效,作为当前计数周期的溢出值。
auto-reload preload=Enable:自动重载寄存器写入新值后,存放在预装载寄存器中,该值不会马上生效,计数器按照原来旧的溢出值进行计数。当计数溢出后,该计数值才会生效(由预装载寄存器转入影子寄存器),开始新的计数周期
一般而言:预装载功能在多个定时器同时输出信号时比较有用,可以确保多个定时器的输出信号在同一个时刻变化,实现同步输出。单个定时器使用时,一般不开启预装载功能。
对于定时器而言除了红色位置需要修改以外其他地方不需要修改。(其他基础配置不做解析)。
TB213A需要两个引脚来控制电机的正反转以及停止所以我们在定义两个gpio为输出:
(为了方便我们使用user label)
TB213A还有一个STBY引脚他需要使能高电平,直接接入单片机的vcc+即可。
我们在MDK_ARM文件夹下创建一个user的文件夹(我们会在这个文件夹中加入自己写的相关文件),并在次文件夹下分别创建moto.c moto.h文件,打开工程并将文件添加在keil文件中(这里不在赘述如何添加)
在文件中我们找到89行:添加如下代码
并在moto.c中写入控制正反转的函数:
在主函数中开启pwm以及确定正反转就可以了
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 法。
如图所示我们打开一个新的定时器(因为stm32是不允许同一个定时器又用到pwm输出功能和编码器功能,所以开一个新的定时器,我用的是TIM4),我们开启encoder mode,然后选择四倍频,滤波我们填写10。
霍尔编码器会输出两路方波信号,如果只在通道A的上升沿计数,那就是1倍频;通道A的上升、下降沿计数,那就是2倍频;如果在通道A、B的上升、下降沿计数,那就是4倍频。
使用倍频可以最大程度地利用两路信号,提高测速的灵敏度
我们还需再打开一个定时器用来计时间隔
这里我们这样配置是为了每10ms(及100HZ)检测速度;并且要打开中断(NVIC)
由于我们现在需要通过串口打印出相应的代码,所以我们加入usart
在本实验中由于霍尔编码器的输出的高电平为5v为了保护单片机我们这里使用一个及其简单的分压电路即可。(一个33kΩ另一个20kΩ)
由于用到了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
并在魔法棒里面包括微库:
开启定时器以及编码器:
主程序中保持不变,我们在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.06000;中
13为旋转一圈所产生的脉冲数,即脉冲数/转(Pulse Per Revolution 或PPR)
10.0 是本减少直流电机的减速比
4.0是采用了双通道,几倍频就除以几
6000是本中断0.01s一次,*6000就可以得到一分钟的转速
pid的原理这里不再赘述,b站上有一个很棒的通俗易懂的解释:
b站
在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法。
实现正反转相对于单向转动稍微复杂一些,并且我们在这里进行一下输出的限幅,
在main.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 */
#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; //增量输出
}
在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个脉冲。
上一节中我们使用pi增量式来控制电机的转速,当然你也可以自己增加d项,这个也比较简单,这一章我们将使用pid的位置式来控制电机的位置(位置式并不是来控制位置的,相关的基础知识可在百度中查询)
有了上面的pi速度调节的经验后我们在实现pid位置调节的时候就可以轻车熟路了。
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; //增量输出
}
本例程式10ms读取一次编码器(100HZ),我使用的电机的减速比为10,霍尔编码器的精度为13,又我们式AB双相的所以我们是4倍频,那么电机转一圈编码器读取为 10×13×4=520 我的设置的为260,自然就是正向转动半圈。
感谢您能阅览本篇拙笔。本文仅仅简单介绍了使用cubemx来配置加上一些简单代码来实现,并未对pid就行深入探讨。