如何获得一个丝滑的麦轮底盘(原理+代码详解)

本文详细介绍了基于STM32的麦轮底盘控制原理,包括麦轮的结构、运动学分析、运动学逆解、PID控制以及编码器数据的离散化处理。通过实例代码展示了如何将上位机指令转化为电机速度,实现全向移动、旋转等动作,并提供了完整的源码下载链接。
摘要由CSDN通过智能技术生成

        本文将用最直白的方式讲述麦轮底盘的控制原理,并且将附上全套stm32代码。

目录

一、准备工作

1. 麦轮简介

2. 安装底盘

二、原理分析

1. 先从一个轮子开始

2. 再到整个底盘

三、运动学逆解

1. 继续从整体分析

2. 最后回到一个轮子

四、离散化和PID

1. 数据离散化

2. 增量式PID

五、源码详解

1. 框架概述

2. 上位机协议

3. 第一层指令解析

4. 第二层指令解析

5. 底层电机驱动函数

六、源码下载地址


一、准备工作

1. 麦轮简介

        麦轮绝对是一个神奇的发明。其由轮毂和固定在外周的许多小辊子构成,轮轴和辊轴之间的夹角通常为 45°,每个轮子具有三个自由度,分别是绕轮轴转动,沿垂直于与地面接触的辊子的辊轴方向移动,绕轮子和地面的接触点转动。如图就是一个麦轮:

c1771fad2c26418cb7e9d31670955ddd.jpg

        麦轮特殊的构造使麦轮小车能有很多诡异的运动方式。其中最常见的就是全向移动了,那么我们该如何实现呢?

2. 安装底盘

        首先我们要将麦轮底盘装好。麦轮的安装方式有很多讲究,原理涉及很多力学和运动分解,这些具体的东西在下文再讲,这里就直接放出正确的安装方式了:

        麦轮安装主要看它上面的辊子的方向,下面这是正确安装方向的俯视图。

6b22fe6b118f411cadee90b1a56d1e94.JPG

         要注意下面这个图是正确方向的地面映射图,和上图安装方式是一样的。

3f1af878551346668a82dfcdb8a1a6ce.JPG

二、原理分析

1. 先从一个轮子开始

        如果你去看展开来分析麦轮单轮受力分析的文章,会发现其中的分解比较复杂。其实这些大部分还是高中的受力分解,高中的时候我们绝对能轻松应付。但是我们大部分都已经是大学生了,肯定已经看不懂这些了。

        但是不要慌,因为底层的原理对我们应用来说并不十分关键,我们只需了解几个结论:

f68cf91c98904568b3f4f7fbff45377a.JPG

 79dc002a1c184fffadeecd06b9a01c34.JPG

         这两张图就清楚展示了单个麦轮转动时的产生的速度方向。应该还是很好理解的,只要记住它的合速度与辊子方向平行就行了。

         然后再进行一个最简单的速度分解:

433d665e5737457ca0aed87924b12bd0.JPG

 7234b96382db4ee9b74c98957155d809.JPG

         这样我们就将每个轮子产生的速度分解到了xy两个方向,以便于下一步整车的运动分析。

2. 再到整个底盘

        那么理解了单个轮子的运动分析,再到了整辆车就比较简单了。

        首先我们要理解整个底盘的运动是由四个麦轮共同带动的,所以整车的速度方向取决于四个麦轮速度方向的合成。而四个麦轮转动速度与转动方向的不同组合就可以使小车以各种不同方式运动。

        下面先展示一下几种基本的运动情况是如何产生的:

① 前进后退

2dae387168c24e1799b18c3511ebb11c.JPG

60989f21e4f946b8867ad0c62f0415fa.JPG

         这里麦轮之间的横向分力互相抵消,竖向分力共同作用,就产生了前进后退的效果。

② 左右平移

df4bad26391c4e04b3b50b62cab81434.JPG

 3e823b90070547a6a713bac802bd2393.JPG

         相反,这里麦轮之间的竖向分力互相抵消,横向分力共同作用,就产生了左右平移的效果。

③ 斜向平移

b15bdb675c824d46a37543d8b09d2b08.JPG

 b5c1914f8e6a4c8fa4b577205d1a7739.JPG

94e48f5da0e143e0ab2b5212c2bb315f.JPG

 4459c97d8dc049c49dba0372a8efb717.JPG

         这里我的只让两个对角的麦轮转动,甚至不需要速度分解,我们就能很清楚的看出它们的合速度是怎么驱动小车斜向平移的。

④ 原地转圈

fb4336304aea4673a6860086a69037ac.JPG

75d3d52134394bca948a72b6d17b2858.JPG

         同样这里也不需要速度分解,我们也能很清楚的看出它们的合速度是怎么驱动小车转圈的。

三、运动学逆解

1. 继续从整体分析

        了解了最简单的原理,我们就可以考虑怎样去运用了。

        其实上文的原理分析就是一个运动学正解过程,也就是从每个轮子的运动去分析小车的运动。但是,这种方法在实际运用中用处是不大的。我们的需求是通过指定小车的运动方式,得到每个轮子的运动方式。这样我们才能方便的控制小车,也就是所谓的运动学逆解。

        我们最终的程序也是完成的这样的一个过程。例如我们传入小车前进的指令,程序解析出四个麦轮应运动的速度和方向,从而达到整车前进的效果。

        整个运动学逆解的推导过程还是比较复杂的。这里也展示一下吧:

 ececda33f63f48e7867a28578caf753e.jpeg

        相信你们也不想看这个。好消息是,这些也没必要去看。我们只需理解这个结论:

 VA轮 = Vx+Vy-Vz*(H/2+W/2)
 VB轮 = Vx-Vy-Vz*(H/2+W/2)
 VC轮 = Vx+Vy+Vz*(H/2+W/2)
 VD轮 = Vx-Vy+Vz*(H/2+W/2)

参数说明:
VABCD轮-> 麦轮A、B、C、D 的线速度,单位m/s。
Vx-> 机器人前后移动速度,前进为正,单位:m/s。
Vy-> 机器人左右移动速度,左移为正,单位:m/s。
Vz-> 机器人绕 O 点旋转速度,逆时针为正,单位:rad/s
W-> 轮距,机器人左右麦轮的距离,单位:m。
H-> 轴距,机器人前后麦轮的距离,单位:m。

        可以对照下面这个图理解,总体还是比较简单的。 

64deb792aa504885a13265bd09d2e707.png

         C语音实现示例:

// 整车移动量转换为单轮速度  x:前+后-  y:左+右-  z:逆+顺-
void Move_Transfrom(double Vx,double Vy,double Vz)
{
	TargetA=Vx+Vy-Vz*(Car_H/2+Car_W/2);
	TargetB=Vx-Vy-Vz*(Car_H/2+Car_W/2);
	TargetC=Vx+Vy+Vz*(Car_H/2+Car_W/2);
	TargetD=Vx-Vy+Vz*(Car_H/2+Car_W/2);
}

        这里还要注意一点,(Car_H/2+Car_W/2)这个值可以当作一个参数看待。在真正使用中没有必要去过度纠结它的值,我们可以随便的改变它,只要能达到最丝滑的旋转效果就好。

2. 最后回到一个轮子

        在上面的运动学逆解公式中,我们得到的是每个轮子的线速度,而且其单位是m/s。这个结果其实对我们而言并不是十分的友善。因为一个m/s的线速度在我们这种规格的小车上并不是一个非常清晰的概念,况且电机的空载转速与负载时的转速是有巨大差异的,所以我们很难通过电机的额定转速等信息推导出一个适合我们小车的车轮运动角速度。

        这时,我们可以使用一种全新的计量车轮速度的单位:编码器数据。编码器数据可以非常方便的直接读取,十分有利于我们对车速的预估和测量。同时在后面我们用pid对电机进行闭环控制时目标值的单位也是编码器数据,这里做到了单位的统一,对我们写程序也是十分友善的。

        关于编码器的原理和读取数据的方法就不多说了,这东西资料还是很多的。下面主要说一下如何进行编码器数据离散化和电机pid闭环控制。

四、离散化和PID

1. 数据离散化

        这名听起来挻高端。但是我们依然不用管其中的原理,我就直接说如何运用了。

        我们离散化的目的就是用编码器的数据时实表示轮子的速度值。首先我们要知道编码器的数据是什么样的。简单来说,当你往一个方向转动轮子,编码器的数据会一直自增,往另一个方向转动轮子,数据会一直自减。显然,这样的数据是无法表示速度的。

        但是我们如果每隔一段时间将这个数据取出来,然后让下一段时间的数据从0开始增减,那么这个取出来的数据就可以直接表示我们的速度大小和方向了。(这一段是原理可以不看: 我们的数据离散化就是把无限个的编码器数据映射到有限的空间内。可以理解为我们把在时间上连续的数据,通过一定的采样频率采集,用以代表我们在连续维度上的数据。)

        所以,在我们的stm32程序中,只需要再开一个定时器,在其中断函数中保存编码器数据值,然后再清空编码器计数器,用这个保存的数据代表实时速度,就可以实现所谓的离散化了。同时,要注意这个定时器的频率不易过低,实测10ms的间隔就是可以的。

2. 增量式PID

       有了上面的公式和编码器数据,我们现在就可以将一个整车的速度转换为四个轮子各自的速度值了。但是这里又会有一个重要的问题,由于路面摩擦或电机本身等客观条件的影响,轮子并不会精准的依照我们给它的目标值运行。这将会导致小车的运动轨迹发生偏移,所以这里我们要为每个轮子添加PID闭环驱动。

        PID闭环控制的原理比较繁琐,这里也不多说了。同时PID的用途也非常广泛,我们这里用到的是简单的电机闭环控制,总结来说其作用就是让电机尽可能精准的按我们的预期运行。

        我们在应用时只需要带入PID的公式并了解传入的参数和输出的结果的意义就可以了。这里我使用的是增量式的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):上一次的偏差
e(k-2):上上次的偏差
Kp:比例项参数
Ki:积分项参数
Kd:微分项参数
Pwm:代表增量输出

        而我们的系统比较简单,电机数据突变值比较小,所以并没有使用D值。于是我们的公式就简化为了下面这个样子:

Pwm+=Kp[e(k)-e(k-1)]+Ki*e(k)
e(k):本次偏差
e(k-1):上一次的偏差
Kp:比例项参数
Ki:积分项参数
Pwm:代表增量输出

        在程序中,我的PID计算函数是这个样子:

//MA速度增量PID
int16_t Speed_PID_A(double Target)
{
	static float Bias,Pwm,Last_bias;//本次误差,累加输出,上次误差
	Bias=Target-Encoder_Result[0]/Speed_Proportion; //计算偏差
	Pwm+=Speed_PID_P*(Bias-Last_bias)+Speed_PID_I*Bias;//PID
	Last_bias=Bias; //保存上一次偏差
	return Pwm;
}

//注意:
//Target代表传入的目标速度
//Encoder_Result[0]/Speed_Proportion是实时的编码器测速值
//Pwm代表输出的电机驱动信号大小
//Speed_PID_P和Speed_PID_I是参数P和I的值

        使用时我们需要不断的调用这个函数,当你传入一个新的目标值时,PID公式就会帮你精准的控制每个轮子达到目标速度了。

        另外还要说一下,P和I的值是需要我们自己慢慢调节的,不同的情况下他们最合适的值都会不同,这就比较考验经验了。在一般情况下,P的值对系统的影响最大,其值一般会大一点,I值一般会小一点。而P或I过大容易造成系统强烈的震荡,过小会延长系统的反应时间。总之只能靠自己慢慢的调,小车才能实现最丝滑的状态。

        那么麦轮的控制原理就差不多说完了。总结一下就是通过公式将小车的整体速度转化为每一个轮子的速度,然后代入PID进行驱动。总体还是挺简单的。但是我们写代码时还要注意更多细节,下面详细说一下代码。

五、源码详解

1. 框架概述

        本套代码是stm32f103rct6驱动麦轮底盘的代码。

        首先来说我们的代码大概分为四层。第一层是与上位机的通信协议,这一层代码接收上位机发送的遥控指令包括移动刹车调速等,并下发到下一层。

        第二层代码是对指令的第一层解析,得到小车的xyz方向目标速度并下发到下一层。

        第三层代码是对指令的第二层解析,得到每个轮子的目标速度然后带入PID公式,将结果再次下发到下一层。

        第四层是底层电机驱动,将PID结果转换为驱动电机的PWM占空比,完成小车的驱动。

        当然,除了这些之外还有定时读取编码器值,接收蓝牙数据等其他代码共同起到作用。

        框架流程图:        

         其中底层驱动代码会因为不同的主控型号和不同的电机驱动芯片而不同,与上位机的通信协议也会有不同的地方,但中间最重要的解析过程是基本可以通用的。

2. 上位机协议

        这一层在main中,指令格式是 @+指令+/E ,是小程序的控制界面发送的。这一部分不是重点,没啥好说的,因为不同的项目中通信协议都是有很大的区别的。具体指令内容直接看源码吧。

        小程序操作界面:

        main.c: 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "LED.h"  						//控制LDE
#include "Serial.h"						//串口1与上位机通信
#include "Key.h"  						//按键控制
#include "MyI2CRev.h"					//IIC接收指令
#include "Move.h"						//底层移动控制
#include "Control.h"					//上层移动控制
#include "Encoder.h"					//编码器测速

uint8_t IICRxData;//接收的数据

int main(void)
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组
	
	Delay_ms(1);//等待拓展板先初始化
	//LED_Init();
	//Key_Init();
	Serial_Init();
	OLED_Init();
	MyI2CRev_Init();
	Move_init();
	Encoder_Init();
	
	while(1)
	{	
		//IIC接收数据
		if(Get_I2CFlag()==1)
		{
			IICRxData=(uint16_t)Get_I2CData();
			if(IICRxData<=60)//小车全向移动(0~60映射到0~360)
			{
				Control_Straight_Move(IICRxData);
				Move_Flag=1;
			}
			else if(IICRxData==62){Move_StopAll();Move_Flag=0;}//停止
			else if(IICRxData==63){Move_BrakeAll();Move_Flag=0;}//急刹
			else if(IICRxData<=68){Control_Set_Speed(IICRxData-63);}//控速
			else if(IICRxData==69){Control_Circlr_Move(1);Move_Flag=1;}//左转
			else if(IICRxData==70){Control_Circlr_Move(0);Move_Flag=1;}//右转
			else if(IICRxData<=71){Move_StopAll();Move_Flag=0;}//停止
			
			RevFlag=0;//清除标志位
		}
	}
}

3. 第一层指令解析

        这里面的Control_Straight_Move函数将上位机传入的小车全向移动的平移角度指令转换为了xy两个方向的速度。

        Control_Circlr_Move函数将上位机传入的小车旋转指令转换为了z方向的速度。

        Control_Set_Speed函数提供了调速接口,要注意的是Max_Speed这个变量的值是通过实际测试得到的一些合适的速度值,采用的是和编码器数据一样的单位。

        Control.c:

#include "stm32f10x.h"                  // Device header
#include "Move.h"
#include "Math.h"

uint16_t Speed_Gear=3;//速度挡位1~5
double Max_Speed;//整车最大速度

//根据速度挡位转换整车最大速度
void Set_Max_Speed(void)
{
	//PWM=55编码器数据->GMR:1380  HR:28
	//PWM=60编码器数据->GMR:1720  HR:38
	//PWM=65编码器数据->GMR:2045  HR:46
	//PWM=70编码器数据->GMR:2360  HR:56
	//PWM=75编码器数据->GMR:2570  HR:64
	switch(Speed_Gear)
	{
		case 1:Max_Speed=1380/2/Speed_Proportion;break;
		case 2:Max_Speed=1720/2/Speed_Proportion;break;
		case 3:Max_Speed=2045/2/Speed_Proportion;break;
		case 4:Max_Speed=2360/2/Speed_Proportion;break;
		case 5:Max_Speed=2570/2/Speed_Proportion;break;
	}
}

//传入全向运动目标角度0~60,逆时针为正
void Control_Straight_Move(uint16_t Straight_Target)
{
	Straight_Target=(double)Straight_Target/60*360;//0~60映射0~360
	
	double pi=acos(-1.0);//派
	double Target_Angle=(double)Straight_Target/360*2*pi;//角度转弧度
	
	double Target_Speed_X=Max_Speed*cos(Target_Angle);//x方向目标速度
	double Target_Speed_Y=Max_Speed*sin(Target_Angle);//y方向目标速度
	
	Move_Transfrom(Target_Speed_X,Target_Speed_Y,0.0);//转换为每个电机的速度
}

//传入1或0,1左转0右转
void Control_Circlr_Move(uint16_t Circlr_Target)
{
	if(Circlr_Target==1)//左
	{
		Move_Transfrom(0.0,0.0,Max_Speed);//转换为每个电机的速度
	}
	else//右
	{
		Move_Transfrom(0.0,0.0,-Max_Speed);//转换为每个电机的速度
	}
}

//设置速度挡位
void Control_Set_Speed(uint16_t Set_Speed_Num)//传入1~5
{
	if(Set_Speed_Num<=5)
	{
		Speed_Gear=Set_Speed_Num;//赋值
		Set_Max_Speed();//转换最大速度
	}
}

         Control.h: 

#ifndef __CONTROL_H
#define __CONTROL_H

//根据速度挡位转换整车最大速度
void Set_Max_Speed(void);
//传入全向运动目标角度0~360,逆时针为正
void Control_Straight_Move(uint16_t Straight_Target);
//传入转向运动目标角度0~360,逆时针为正
void Control_Circlr_Move(uint16_t Circlr_Target);
//设置速度挡位
void Control_Set_Speed(uint16_t Set_Speed_Num);//传入1~5

#endif

4. 第二层指令解析

        这里面的Move_Transfrom函数接收上一层传入的小车xyz方向速度,转换为每个轮子的目标速度。

        Speed_PID_A,B,C,D函数是增量式PID计算公式。

        Move_Motor是调用PID驱动电机和限幅的函数。要注意的是这个函数在小车运动状态下是要一直循环调用的,具体实现在定时器的中断函数中,下面会展示。

        Move_StopAll和Move_BrakeAll是停止和刹车的接口函数。

        Move.c:

#include "stm32f10x.h"                  // Device header
#include "Motor.h"
#include "Encoder.h"
#include "Control.h"

#define PWM_MAX    420 //PWM最大限幅(1020-600)

double Speed_PID_P=0.6;//P
double Speed_PID_I=0.01;//I

//电机分布:从左下顺时针开始ABCD,对应M1234

double Speed_Proportion=2.0;//空转速度与负载速度比值

double Car_HW=1; //小车旋转参数比例值

double TargetA; // A轮目标速度
double TargetB; // B轮目标速度
double TargetC; // C轮目标速度
double TargetD; // D轮目标速度

void Move_init(void)
{	
	Motor_Init();
	
	Set_Max_Speed();//设置初始速度
}

// 整车移动量转换为单轮速度  x:前+后-  y:左+右-  z:逆+顺-
void Move_Transfrom(double Vx,double Vy,double Vz)
{
	TargetA=Vx+Vy-Vz*Car_HW;
	TargetB=Vx-Vy-Vz*Car_HW;
	TargetC=Vx+Vy+Vz*Car_HW;
	TargetD=Vx-Vy+Vz*Car_HW;
}

//MA速度增量PID
int16_t Speed_PID_A(double Target)
{
	static float Bias,Pwm,Last_bias;//本次误差,累加输出,上次误差
	Bias=Target-Encoder_Result[0]/Speed_Proportion; //计算偏差
	Pwm+=Speed_PID_P*(Bias-Last_bias)+Speed_PID_I*Bias;//PID
	Last_bias=Bias; //保存上一次偏差
	return Pwm;
}
//MB速度增量PID
int16_t Speed_PID_B(double Target)
{
	static float Bias,Pwm,Last_bias;//本次误差,累加输出,上次误差
	Bias=Target-Encoder_Result[1]/Speed_Proportion; //计算偏差
	Pwm+=Speed_PID_P*(Bias-Last_bias)+Speed_PID_I*Bias;//PID
	Last_bias=Bias; //保存上一次偏差
	return Pwm;
}
//MC速度增量PID
int16_t Speed_PID_C(double Target)
{
	static float Bias,Pwm,Last_bias;//本次误差,累加输出,上次误差
	Bias=Target-Encoder_Result[2]/Speed_Proportion; //计算偏差
	Pwm+=Speed_PID_P*(Bias-Last_bias)+Speed_PID_I*Bias;//PID
	Last_bias=Bias; //保存上一次偏差
	return Pwm;
}
//MD速度增量PID
int16_t Speed_PID_D(double Target)
{
	static float Bias,Pwm,Last_bias;//本次误差,累加输出,上次误差
	Bias=Target-Encoder_Result[3]/Speed_Proportion; //计算偏差
	Pwm+=Speed_PID_P*(Bias-Last_bias)+Speed_PID_I*Bias;//PID
	Last_bias=Bias; //保存上一次偏差
	return Pwm;
}

// 控制电机转动
void Move_Motor(void)
{
	double PWM_A=Speed_PID_A(TargetA);//PID
	double PWM_B=Speed_PID_B(TargetB);
	double PWM_C=Speed_PID_C(TargetC);
	double PWM_D=Speed_PID_D(TargetD);
	
	//驱动
	if(PWM_A>PWM_MAX){Motor1_Speed(PWM_MAX);}//限幅
	else if(PWM_A<-PWM_MAX){Motor1_Speed(-PWM_MAX);}//限幅
	else{Motor1_Speed((int16_t)PWM_A);}//正常输出
	
	if(PWM_B>PWM_MAX){Motor2_Speed(PWM_MAX);}
	else if(PWM_B<-PWM_MAX){Motor2_Speed(-PWM_MAX);}
	else{Motor2_Speed((int16_t)PWM_B);}
	
	if(PWM_C>PWM_MAX){Motor3_Speed(PWM_MAX);}
	else if(PWM_C<-PWM_MAX){Motor3_Speed(-PWM_MAX);}
	else{Motor3_Speed((int16_t)PWM_C);}
	
	if(PWM_D>PWM_MAX){Motor4_Speed(PWM_MAX);}
	else if(PWM_D<-PWM_MAX){Motor4_Speed(-PWM_MAX);}
	else{Motor4_Speed((int16_t)PWM_D);}
}

//速度值清零
void Target_Clear(void)
{
	TargetA=0.0;
	TargetB=0.0;
	TargetC=0.0;
	TargetD=0.0;
}

//全部停止
void Move_StopAll(void)
{
	Motor1_Stop();
	Motor2_Stop();
	Motor3_Stop();
	Motor4_Stop();
	Target_Clear();
}

//全部刹车
void Move_BrakeAll(void)
{
	Motor1_Brake();
	Motor2_Brake();
	Motor3_Brake();
	Motor4_Brake();
	Target_Clear();
}

        Move.h:

#ifndef __MOVE_H
#define __MOVE_H

extern double Speed_Proportion;//空转速度与负载速度比值

void Move_init(void);
void Move_Transfrom(double Vx,double Vy,double Vz);
void Move_Motor(void);
void Move_StopAll(void);
void Move_BrakeAll(void);

#endif

        下面是编码器的数据读取函数,其中也包含一个定时器的中断函数,在这个中断中要读取并清除编码器数据并调用电机PID驱动函数。读取到的编码器速度值将直接作为PID公式中的当前速度值。

        Encoder.c:

#include "stm32f10x.h"                  // Device header
#include "Encoder_Timer.h"
#include "Move.h"

int16_t Encoder_Result[4]; //存储编码器实时数据
uint8_t Move_Flag=0;//是否运动标志位,0静止1运动

void Encoder_Init(void)
{
	//初始化定时器7,10ms采集频率
	Encoder_Timer_Init();
	
	//Timer2的编码器接口(PB3,PA15)remap
	//Timer3的编码器接口(PA6,PA7)
	//Timer4的编码器接口(PB6,PB7)
	//Timer5的编码器接口(PA0,PA1)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//开启GPIOB时钟
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);//开启Timer2时钟->M1
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);//开启Timer3时钟->M4
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);//开启Timer4时钟->M2
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);//开启Timer5时钟->M3
	
	GPIO_InitTypeDef GPIO_InitStructure;//初始化GPIO
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 |GPIO_Pin_1 | GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_15;
 	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_6 | GPIO_Pin_7;
 	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	//Timer2的输出通道1,2重映射在PA15,PB3
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//开启AFIO时钟
	GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);//引脚重映射
	GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//解除调试功能
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//时基单元初始化
	TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波器不分频
	TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数(此参数没有用,由编码器控制)
	TIM_TimeBaseInitStruct.TIM_Period=65536-1;//ARR自动重装器(65536满量程计数,防止溢出,方便换算为负数)
	TIM_TimeBaseInitStruct.TIM_Prescaler=1-1;//PSC预分频器(不分频,编码器时钟直接驱动计数器)
	TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
	TIM_TimeBaseInit(TIM4, &TIM_TimeBaseInitStruct);
	TIM_TimeBaseInit(TIM5, &TIM_TimeBaseInitStruct);
	
	TIM_ICInitTypeDef TIM_ICInitStruct;//初始化输入捕获单元
	TIM_ICStructInit(&TIM_ICInitStruct);//赋初始值
	TIM_ICInitStruct.TIM_Channel=TIM_Channel_1;//选择输入捕获通道1
	TIM_ICInitStruct.TIM_ICFilter=0xF;//滤波器
	TIM_ICInit(TIM2, &TIM_ICInitStruct);//初始化通道1
	TIM_ICInit(TIM3, &TIM_ICInitStruct);
	TIM_ICInit(TIM4, &TIM_ICInitStruct);
	TIM_ICInit(TIM5, &TIM_ICInitStruct);
	
	TIM_ICInitStruct.TIM_Channel=TIM_Channel_2;//选择输入捕获通道2
	TIM_ICInitStruct.TIM_ICFilter=0xF;//滤波器
	TIM_ICInit(TIM2, &TIM_ICInitStruct);//初始化通道2
	TIM_ICInit(TIM3, &TIM_ICInitStruct);
	TIM_ICInit(TIM4, &TIM_ICInitStruct);
	TIM_ICInit(TIM5, &TIM_ICInitStruct);
	
	//配置编码器接口
	//在TI1和TI2都计数  通道1不反向  通道2不反向
	TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Falling, TIM_ICPolarity_Rising);
	TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Falling, TIM_ICPolarity_Rising);
	TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
	TIM_EncoderInterfaceConfig(TIM5, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
	
	TIM_Cmd(TIM2,ENABLE);//开启定时器
	TIM_Cmd(TIM3,ENABLE);
	TIM_Cmd(TIM4,ENABLE);
	TIM_Cmd(TIM5,ENABLE);
}

void Encoder_Get(void)//更新编码器数据
{
	Encoder_Result[0]=TIM_GetCounter(TIM2);//Timer2->M1
	TIM_SetCounter(TIM2, 0);//CNT清零,方便下次读取速度
	
	Encoder_Result[1]=TIM_GetCounter(TIM4);//Timer4->M2
	TIM_SetCounter(TIM4, 0);//CNT清零,方便下次读取速度
	
	Encoder_Result[2]=TIM_GetCounter(TIM5);//Timer5->M3
	TIM_SetCounter(TIM5, 0);//CNT清零,方便下次读取速度
	
	Encoder_Result[3]=TIM_GetCounter(TIM3);//Timer3->M4
	TIM_SetCounter(TIM3, 0);//CNT清零,方便下次读取速度
}

void TIM7_IRQHandler(void)
{
	if(TIM_GetITStatus(TIM7, TIM_IT_Update)==SET)//判断标志位是否正确
	{
		Encoder_Get();//更新编码器数据
		if(Move_Flag==1)
		{
			Move_Motor();//定时驱动电机
		}
		TIM_ClearITPendingBit(TIM7, TIM_IT_Update);//清除标志位
	}
}

        Encoder.h:

#ifndef __ENCODER_H
#define __ENCODER_H

extern uint8_t Move_Flag;//是否运动标志位,0静止1运动

extern int16_t Encoder_Result[];

void Encoder_Init(void);

#endif

  5. 底层电机驱动函数

        由于底层驱动代码会因为不同的主控型号和不同的电机驱动芯片而不同,而且这里涉及原理不是本文的重点,所以就不多说啥了。

        Motor.c:

#include "stm32f10x.h"                  // Device header

void Motor_Init(void)
{
	//Timer1,8输出PWM(频率30kHZ) 初始化Motor1(PC6,PC7),Motor2(PC8,PC9),Motor3(PA8,PA11),Motor4(PB0,PB1)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);//开启GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//开启GPIOB时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);//开启GPIOC时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);//开启Timer1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE);//开启Timer8时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;//初始化GPIO
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//一定要用复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;//Timer1 CH1 Motor3
 	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;//Timer1 CH2 Motor4
 	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;//Timer1 CH3 Motor4
 	GPIO_Init(GPIOB, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;//Timer1 CH4 Motor3
 	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//Timer8 CH1 Motor1
 	GPIO_Init(GPIOC, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;//Timer8 CH2 Motor1
 	GPIO_Init(GPIOC, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;//Timer8 CH3 Motor2
 	GPIO_Init(GPIOC, &GPIO_InitStructure);
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;//Timer8 CH4 Motor2
 	GPIO_Init(GPIOC, &GPIO_InitStructure);
	
	TIM_InternalClockConfig(TIM1);//选择内部时钟为时钟源
	TIM_InternalClockConfig(TIM8);//选择内部时钟为时钟源
	
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;//时基单元初始化(30KHZ)
	TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波器不分频
	TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数
	TIM_TimeBaseInitStruct.TIM_Period=1200-1;//ARR自动重装器 600~1200为有效驱动值
	TIM_TimeBaseInitStruct.TIM_Prescaler=2-1;//PSC预分频器
	TIM_TimeBaseInitStruct.TIM_RepetitionCounter=0;//重复计数器
	TIM_TimeBaseInit(TIM1, &TIM_TimeBaseInitStruct);
	TIM_TimeBaseInit(TIM8, &TIM_TimeBaseInitStruct);
	
	TIM_OCInitTypeDef TIM_OCInitStruct;//初始化输出比较
	TIM_OCStructInit(&TIM_OCInitStruct);//先给结构体赋初始值,防止使用高级定时器时参数配置不完全导致无法正常输出PWM
	TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1;//PWM模式1(常用)
	TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;//TIM_OCPolarity_High REF极性不翻转
	TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;//输出使能
	TIM_OCInitStruct.TIM_Pulse=0;//设置CCR的值(占空比)
	//Timer8通道初始化
	TIM_OC1Init(TIM8, &TIM_OCInitStruct);//初始化TIM8输出比较单元1 Motor1
	TIM_OC2Init(TIM8, &TIM_OCInitStruct);//初始化TIM8输出比较单元2 Motor1
	TIM_OC3Init(TIM8, &TIM_OCInitStruct);//初始化TIM8输出比较单元3 Motor2
	TIM_OC4Init(TIM8, &TIM_OCInitStruct);//初始化TIM8输出比较单元4 Motor2
	//Timer1通道初始化
	TIM_OCInitStruct.TIM_OutputNState=TIM_OutputNState_Enable;//特殊配置Timer1 CHN2,CHN3
	TIM_OCInitStruct.TIM_OCNPolarity=TIM_OCNPolarity_Low;
	TIM_OCInitStruct.TIM_OCNIdleState=TIM_OCNIdleState_Set;
	TIM_OC1Init(TIM1, &TIM_OCInitStruct);//初始化TIM1输出比较单元1 Motor3
	TIM_OC4Init(TIM1, &TIM_OCInitStruct);//初始化TIM1输出比较单元4 Motor3
	TIM_OC2Init(TIM1, &TIM_OCInitStruct);//初始化TIM1输出比较单元2 Motor4
	TIM_OC3Init(TIM1, &TIM_OCInitStruct);//初始化TIM1输出比较单元3 Motor4
	
	//Timer1的输出通道2,3的引脚重映射在PB0,PB1上)
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);//开启AFIO时钟
	GPIO_PinRemapConfig(GPIO_PartialRemap_TIM1, ENABLE);//引脚重映射
	
	TIM_CtrlPWMOutputs(TIM1, ENABLE);
	TIM_CtrlPWMOutputs(TIM8, ENABLE);
	
	TIM_Cmd(TIM1, ENABLE);//使能计数器
	TIM_Cmd(TIM8, ENABLE);//使能计数器
}


//Motor1:
void Motor1_Speed(int16_t Compare)//调用函数可以更改占空比(传入-600~600)
{
	if(Compare>=0)//前进
	{
		TIM_SetCompare2(TIM8, 0);
		TIM_SetCompare1(TIM8, Compare+600);
	}
	else//后退
	{
		TIM_SetCompare1(TIM8, 0);
		TIM_SetCompare2(TIM8, -Compare+600);
	}
}

void Motor1_Stop(void)//自然停止
{
	TIM_SetCompare1(TIM8, 0);
	TIM_SetCompare2(TIM8, 0);
}

void Motor1_Brake(void)//急刹
{
	TIM_SetCompare1(TIM8, 1200);
	TIM_SetCompare2(TIM8, 1200);
}


//Motor2:
void Motor2_Speed(int16_t Compare)//调用函数可以更改占空比(传入-600~600)
{
	if(Compare>=0)//前进
	{
		TIM_SetCompare4(TIM8, 0);
		TIM_SetCompare3(TIM8, Compare+600);
	}
	else//后退
	{
		TIM_SetCompare3(TIM8, 0);
		TIM_SetCompare4(TIM8, -Compare+600);
	}
}

void Motor2_Stop(void)//自然停止
{
	TIM_SetCompare4(TIM8, 0);
	TIM_SetCompare3(TIM8, 0);
}

void Motor2_Brake(void)//急刹
{
	TIM_SetCompare4(TIM8, 1200);
	TIM_SetCompare3(TIM8, 1200);
}


//Motor3:
void Motor3_Speed(int16_t Compare)//调用函数可以更改占空比(传入-600~600)
{
	if(Compare>=0)//前进
	{
		TIM_SetCompare4(TIM1, 0);
		TIM_SetCompare1(TIM1, Compare+600);
	}
	else//后退
	{
		TIM_SetCompare1(TIM1, 0);
		TIM_SetCompare4(TIM1, -Compare+600);
	}
}

void Motor3_Stop(void)//自然停止
{
	TIM_SetCompare4(TIM1, 0);
	TIM_SetCompare1(TIM1, 0);
}

void Motor3_Brake(void)//急刹
{
	TIM_SetCompare4(TIM1, 1200);
	TIM_SetCompare1(TIM1, 1200);
}


//Motor4:
void Motor4_Speed(int16_t Compare)//调用函数可以更改占空比(传入-600~600)
{
	if(Compare>=0)//前进
	{
		TIM_SetCompare2(TIM1, 0);
		TIM_SetCompare3(TIM1, Compare+600);
	}
	else//后退
	{
		TIM_SetCompare3(TIM1, 0);
		TIM_SetCompare2(TIM1, -Compare+600);
	}
}

void Motor4_Stop(void)//自然停止
{
	TIM_SetCompare2(TIM1, 0);
	TIM_SetCompare3(TIM1, 0);
}

void Motor4_Brake(void)//急刹
{
	TIM_SetCompare2(TIM1, 1200);
	TIM_SetCompare3(TIM1, 1200);
}

        Motor.h:

#ifndef __MOTOR_H
#define __MOTOR_H

void Motor_Init(void);

void Motor1_Speed(int16_t Compare);
void Motor2_Speed(int16_t Compare);
void Motor3_Speed(int16_t Compare);
void Motor4_Speed(int16_t Compare);

void Motor1_Stop(void);
void Motor1_Brake(void);
void Motor2_Stop(void);
void Motor2_Brake(void);
void Motor3_Stop(void);
void Motor3_Brake(void);
void Motor4_Stop(void);
void Motor4_Brake(void);

#endif

六、源码下载地址

https://download.csdn.net/download/2303_76380160/87878664

文件包包含stm32的全部源码和小程序蓝牙操作界面的源码。 

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值