stm32驱动直流电机(HAL库)
电机作为一个大功率器件,不像LED那样单片机可以直接驱动,本文使用的电机驱动为DRV8833,它可以同时驱动2个电机,如果你的电机驱动与本文不同,也可以借鉴以下思路。1.管脚示意图2.管脚功能:3.控制逻辑:拿到一个驱动,最主要的就是搞清楚它输入和输出的逻辑关系,以此为例,当AIN1和AIN2电平相同时,电机停止,相反时,电机转动。比如,给AIN1提供一个PWM,调节pwm的占空比来改变电机的转速
前言
使用stm32f103c8t6作为主控,cubeMX创建工程,DRV8833作为电机驱动,一步一步实现pid控制电机转速。评论区上传了最终工程。
使用软件:cubeMX,keilMDK。
一、电机驱动介绍
电机作为一个大功率器件,不像LED那样单片机可以直接驱动,本文使用的电机驱动为DRV8833,它可以同时驱动2个电机,如果你的电机驱动与本文不同,也可以借鉴以下思路。
1.管脚示意图
2.管脚功能:
3.控制逻辑:
拿到一个驱动,最主要的就是搞清楚它输入和输出的逻辑关系,以此为例,当AIN1和AIN2电平相同时,电机停止,相反时,电机转动。
比如,给AIN1提供一个PWM,调节pwm的占空比来改变电机的转速。AIN2接单片机的任意gpio口,控制其电平翻转,实现改变电机的方向。实验中我把BIN1,BIN2也使用上了,他和AIN没有区别。下面我们就来实现。
二、cubeMX配置生成PWM
1.工程管理:
打开cubeMX,选择好自己的芯片后,打到工程管理一栏:
2.时钟配置:
3.DEBUG,一定要做,否则下载一次后芯片会锁定。
4、配置时钟3,生成pwm。值得一看喔!
记得勾选internal clock。
在这里顺便把输入AIN2和BIN2的gpio也配置了,我选择的是PA3(接AIN2)和PA4(BIN2)。这里只展示了AIN2,BIN2的配置相同。
然后我们就可以生成代码,看看有没有产生波形。注意,初始化完成后,时钟通道默认关闭,需要我们在程序中开启。代码如下,注意放用户代码的位置,否则更新工程后会被软件删除。
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1); //开启定时器3,通道1的pwm输出 右边电机
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2);//开启定时器3,通道2的pwm输出 左边电机
5.pwm展示:
周期2毫秒,占空比50,和预设的相同。然后按自己的引脚选择,参考驱动的逻辑接线,让电机转起来。
三、电机控制:
1.电机测速
电机测速需要使用编码器,实验中使用的是电机自带的霍尔编码器,轴转1圈会产生11个脉冲。在cubeMX中配置tim2和tim4为编码计数模式,记录编码器的脉冲。开启定时器3的中断,在中断回调函数中,把两个编码器的值转换成电机的转速。接着使用MDK自带的仿真器,观察电机转速,脉冲的变化。
1.1配置tim2,tim4为编码模式,开启定时器3的中断。
此处展示了tim4配置编码模式,tim2相同,不再展示。下图开启tim3的中断:根据前面的分析,2ms计数溢出,产生更新事件,会进一次中断。
然后生成代码,主函数中,添加以下代码:
HAL_TIM_Encoder_Start(&htim2,TIM_CHANNEL_ALL); //开启定时器编码 编码器1
HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL); //开启定时器编码 编码器1
打开工程后找到stm32f1xx_it.c文件,和中断相关的函数习惯写在这里。首先,用户私有变量区域定义以下变量:位置注意看宏:
/* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN PV */
uint16_t tim3ITcount = 0; //计算进入中断的次数
short Encoder1Count = 0; //编码器1的计数值
short Encoder2Count = 0; //编码器2的计数值
float motor1Speed = 0; //电机1速度
float motor2Speed = 0; //电机2
extern TIM_HandleTypeDef htim2;//这三个句柄在tim.c中有定义
extern TIM_HandleTypeDef htim3;
extern TIM_HandleTypeDef htim4;
/* USER CODE END PV */
接下来需要找到tim3的中断入口,然后再中断入口中找到中断处理函数,再给相应的中断回调函数做强定义。
1.2、中断处理
打开stm32f1xx_it.c,滑倒最下面,找到如下函数:这便是tim3的中断入口,其中调用了中断处理函数。
跳转一下定义,在其中找到更新事件中断下的HAL_TIM_PeriodElapsedCallback(htim),
在其中找到更新事件中断下的HAL_TIM_PeriodElapsedCallback(htim),右键跳转定义,可以发现他是一个弱定义函数。我们需要在stm32f1xx_it.c中重写该函数,注意位置,代码如下:
/* USER CODE BEGIN 1 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if(htim == &htim3){
tim3ITcount++;
if(tim3ITcount ==5){ //2ms进一次中断,
tim3ITcount=0;
Encoder1Count = (short)__HAL_TIM_GET_COUNTER(&htim2); //编码器1计数值
Encoder2Count = (short)__HAL_TIM_GET_COUNTER(&htim4); //编码器2计数值
__HAL_TIM_SET_COUNTER(&htim2,0);
__HAL_TIM_SET_COUNTER(&htim4,0);
motor1Speed = (float)Encoder1Count*100/21.3/11/4;
motor2Speed = -(float)Encoder2Count*100/21.3/11/4;
}
}
}
现在,对以上代码进行分析,读者可根据编码器来自己写函数。下图为获取编码器的脉冲数,由于我们设定了2ms进一次中断,进五次以后执行以下代码,也就是说,以下代码获取了编码器10ms的脉冲数。
计数值清0,防止溢出
计算电机的速度。笔者这里使用了减速电机,减速比为1:21.3,也就是说,电机轴转21.3圈,经变速齿轮后,假设变速齿轮上负载了一个轮子, 轮子转一圈,实际电机轴转了21.3圈。我使用的编码器轴转一圈,输出11个脉冲。定时器中,我们使用了双通道计数,会对脉冲四倍频。也就是说,轮转一圈,定时器的到的脉冲数为:21.3*11*4。如果我们获得1s的脉冲数,再除以一圈产生的脉冲数,能得到轮子1s转的圈数。上文,我们已经得到了10ms的脉冲数,*100不就是1s的脉冲数。
1.3、使用仿真查看轮子速度
2.控制速度:
2.1,手动修改占空比实现修改速度
在我们刚才开启定时器通道的代码后面添加以下代码,可以修改占空比,来改变电机的转速。此处为过渡阶段,帮助理解,可跳过。修改前后可以使用仿真查看速度变化。
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,20); //这两个宏可以在运行时改变pwm的占空比
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,20);
2.2、模块化化编程,添加motor.c,motor.h文件。
在工程文件夹下,建立HARDWARE文件夹,并在其下建立motor.c,motor.h文件。然后将其添加到工程中
添加头文件路径
//motor.c
#include "motor.h"
#include "tim.h"
pidtypedef pidmotor1;//定义一个结构体类型变量;
extern float motor1Speed ; //左电机
extern float motor2Speed; //右电机
int motor1PWM,motor2PWM;
/*
pid结构体初始化
参数:
目标值
比例,积分,微分系数
*/
void PID1_Init( float target_VAL,float KP,float KI,float KD){
pidmotor1.actual_val = 0.0;
pidmotor1.target_val = target_VAL;
pidmotor1.err = 0.0;
pidmotor1.err_last = 0.0;
pidmotor1.err_sum = 0.0;
pidmotor1.kp = KP;
pidmotor1.ki = KI;
pidmotor1.kd = KD;
}
/*
p,i控制函数
*/
float PI_realize(pidtypedef * Pid,float actual_val){
Pid->actual_val = actual_val;//传递真实值
Pid->err = Pid ->target_val - Pid->actual_val; //误差 = 目标值-实际值;
Pid->err_sum+=Pid->err; //误差累计值
//使用p,i控制,输出 = KP*当前误差+KI*误差累计值
Pid->actual_val = Pid->kp*Pid->err+Pid->ki*Pid->err_sum;
return Pid->actual_val;
}
/* 电机开环控制函数
根据参数设置电机1和电机2的转速和方向。
小于0,电机反转,大于0,电机正转。前进为正
电机1:ain1(PWM输入,定时器3通道2,PA7引脚),AIN2(电平控制,ain2_Pin),
电机2:bin1(PWM输入,定时器3通道1,PA6引脚),BIN2(电平控制bin2_Pin),
*/
void Motor_Set(int motor1,int motor2){
//设置方向
if(motor1>0){
AIN2_RESET;
}else{
if(motor1 == 0);
else AIN2_SET;
}
if(motor2>0){
BIN2_RESET;
}else{
if(motor2 == 0);
else BIN2_SET;
}
//设置占空比
if(motor1<0){
if(motor1<-99) motor1 = -99;
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2,100+motor1);
}else{ //>=0
if(motor1 >99) motor1 = 99;
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_2, motor1);
}
if(motor2<0){
if(motor2<-99) motor2 = -99;
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1,100+motor2);
}else{ //>=0
if(motor2 >99) motor2 = 99;
__HAL_TIM_SET_COMPARE(&htim3,TIM_CHANNEL_1, motor2);
}
}
/*电机闭环控制*/
void motor_closeloop_control(void){
if(motor1Speed<1.5) motor1PWM+=5;
if(motor1Speed>2.0) motor1PWM--;
if(motor2Speed<1.5) motor2PWM+=5;
if(motor2Speed>2.0) motor2PWM--;
Motor_Set(motor1PWM,motor2PWM);
}
//motor.h
#ifndef __MOTOR_H
#define __MOTOR_H
#include "main.h"
typedef struct{ //声明一个结构体类型
float target_val;//目标值
float actual_val;//实际值;
float err; //当前偏差
float err_last;//上一次偏差
float err_sum;//误差累计值
float kp,ki,kd;//比例,微分,积分系数
}pidtypedef;
// HAL_GPIO_WritePin(GPIOA, ain2_Pin|bin2_Pin, GPIO_PIN_RESET);
#define AIN2_SET HAL_GPIO_WritePin(GPIOA,ain2_Pin,GPIO_PIN_SET)
#define AIN2_RESET HAL_GPIO_WritePin(GPIOA,ain2_Pin,GPIO_PIN_RESET)
#define BIN2_SET HAL_GPIO_WritePin(GPIOA,bin2_Pin,GPIO_PIN_SET);
#define BIN2_RESET HAL_GPIO_WritePin(GPIOA,bin2_Pin,GPIO_PIN_RESET);
void Motor_Set(int motor1,int motor2);
void motor_closeloop_control(void);
void PID1_Init(float target_VAL,float KP,float KI,float KD);
float PI_realize(pidtypedef * Pid,float actual_val);
#endif
其中各函数都做了比较详细的注释,读者可以直接调用。 下面给出部分函数的使用范例。调用pid控制函数之前先调用pid初始化。
总结
这篇文章做的比较基础,初学者也能轻易的跟上。唯独motor.c中的函数没有做过多的讲解,如果有什么问题,欢迎评论,我会尽力解答。
更多推荐
所有评论(0)