前言

使用stm32f103c8t6作为主控,cubeMX创建工程,DRV8833作为电机驱动,一步一步实现pid控制电机转速。评论区上传了最终工程。

使用软件:cubeMX,keilMDK。

一、电机驱动介绍

 电机作为一个大功率器件,不像LED那样单片机可以直接驱动,本文使用的电机驱动为DRV8833,它可以同时驱动2个电机,如果你的电机驱动与本文不同,也可以借鉴以下思路。

1.管脚示意图

819d13b0a363458fa4ec07cbc28f38c1.png

2.管脚功能:

f491fa2258e746988ea60860439f6fe5.png

3.控制逻辑:

fa783a1e26a44a50b8c13c8515e3a46c.png

拿到一个驱动,最主要的就是搞清楚它输入和输出的逻辑关系,以此为例,当AIN1和AIN2电平相同时,电机停止,相反时,电机转动。

比如,给AIN1提供一个PWM,调节pwm的占空比来改变电机的转速。AIN2接单片机的任意gpio口,控制其电平翻转,实现改变电机的方向。实验中我把BIN1,BIN2也使用上了,他和AIN没有区别。下面我们就来实现。

 

二、cubeMX配置生成PWM

1.工程管理:

打开cubeMX,选择好自己的芯片后,打到工程管理一栏:

19c6bfb3c316493b8e7daf548d10ae36.png

dba325709ba444f7a71f9f161dd78ec7.png

2.时钟配置:

0573b572a4f0446dbf3664c4bc987eaa.png

fa03159e89804c8b863cde75d3668eff.png

3.DEBUG,一定要做,否则下载一次后芯片会锁定。

e4518650bbc8478185db2641207c7064.png

4、配置时钟3,生成pwm。值得一看喔!

记得勾选internal clock。

69ab43907900452ba91f6ba82c9c2c53.png

在这里顺便把输入AIN2和BIN2的gpio也配置了,我选择的是PA3(接AIN2)和PA4(BIN2)。这里只展示了AIN2,BIN2的配置相同。

9addf796289a44e5a061dcd7ccfb29e2.png

然后我们就可以生成代码,看看有没有产生波形。注意,初始化完成后,时钟通道默认关闭,需要我们在程序中开启。代码如下,注意放用户代码的位置,否则更新工程后会被软件删除。

HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_1); //开启定时器3,通道1的pwm输出 右边电机
HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_2);//开启定时器3,通道2的pwm输出  左边电机

037564a1b35f46e2af5425d28115789a.png

5.pwm展示:

周期2毫秒,占空比50,和预设的相同。然后按自己的引脚选择,参考驱动的逻辑接线,让电机转起来。

60609e5149cb4b47885217a9fad35ba1.jpeg

 

 

三、电机控制:

1.电机测速

电机测速需要使用编码器,实验中使用的是电机自带的霍尔编码器,轴转1圈会产生11个脉冲。在cubeMX中配置tim2和tim4为编码计数模式,记录编码器的脉冲。开启定时器3的中断,在中断回调函数中,把两个编码器的值转换成电机的转速。接着使用MDK自带的仿真器,观察电机转速,脉冲的变化。

1.1配置tim2,tim4为编码模式,开启定时器3的中断。

183ac56b54f44569a12e00c1d997b956.png

此处展示了tim4配置编码模式,tim2相同,不再展示。下图开启tim3的中断:根据前面的分析,2ms计数溢出,产生更新事件,会进一次中断。

6f43d7ab432b43cb9dfb78d986905255.png

然后生成代码,主函数中,添加以下代码:

HAL_TIM_Encoder_Start(&htim2,TIM_CHANNEL_ALL); //开启定时器编码  编码器1
HAL_TIM_Encoder_Start(&htim4,TIM_CHANNEL_ALL); //开启定时器编码  编码器1

1ff7bc8201934a7d85b5422a249a7f87.png

打开工程后找到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的中断入口,其中调用了中断处理函数。

50befaebc72b4fff9caff3a186a3df63.png

跳转一下定义,在其中找到更新事件中断下的HAL_TIM_PeriodElapsedCallback(htim),

e21e74638eb049189e1e5c464d131218.png

在其中找到更新事件中断下的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的脉冲数。

145e46570df14ab5a69a35d8a481d4ce.png

计数值清0,防止溢出

05b51044b9ed427b9c73c9030364467f.png

计算电机的速度。笔者这里使用了减速电机,减速比为1:21.3,也就是说,电机轴转21.3圈,经变速齿轮后,假设变速齿轮上负载了一个轮子, 轮子转一圈,实际电机轴转了21.3圈。我使用的编码器轴转一圈,输出11个脉冲。定时器中,我们使用了双通道计数,会对脉冲四倍频。也就是说,轮转一圈,定时器的到的脉冲数为:21.3*11*4。如果我们获得1s的脉冲数,再除以一圈产生的脉冲数,能得到轮子1s转的圈数。上文,我们已经得到了10ms的脉冲数,*100不就是1s的脉冲数。

c853110c46c6482db303eb28ecf66e25.png

1.3、使用仿真查看轮子速度

cb40d1420828466fb66c807083aa2304.png

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文件。然后将其添加到工程中

d2c4209ebe7c4e23a63e95905ab481fd.png

添加头文件路径

c552aa4e8d414fd7b15e90d03de5332a.png

//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初始化。

aa625e2889ed45b3b332cb00adf49428.png

f8007fb51131474a8c7c4bf17f0f7142.png


总结

这篇文章做的比较基础,初学者也能轻易的跟上。唯独motor.c中的函数没有做过多的讲解,如果有什么问题,欢迎评论,我会尽力解答。

 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐