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

这篇具有很好参考价值的文章主要介绍了如何获得一个丝滑的麦轮底盘(原理+代码详解)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

目录

一、准备工作

1. 麦轮简介

2. 安装底盘

二、原理分析

1. 先从一个轮子开始

2. 再到整个底盘

三、运动学逆解

1. 继续从整体分析

2. 最后回到一个轮子

四、离散化和PID

1. 数据离散化

2. 增量式PID

五、源码详解

1. 框架概述

2. 上位机协议

3. 第一层指令解析

4. 第二层指令解析

5. 底层电机驱动函数

六、源码下载地址


一、准备工作

1. 麦轮简介

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

麦轮,stm32

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

2. 安装底盘

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

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

麦轮,stm32

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

麦轮,stm32

二、原理分析

1. 先从一个轮子开始

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

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

麦轮,stm32

 麦轮,stm32

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

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

麦轮,stm32

 麦轮,stm32

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

2. 再到整个底盘

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

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

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

① 前进后退

麦轮,stm32

麦轮,stm32

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

② 左右平移

麦轮,stm32

 麦轮,stm32

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

③ 斜向平移

麦轮,stm32

 麦轮,stm32

麦轮,stm32

 麦轮,stm32

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

④ 原地转圈

麦轮,stm32

麦轮,stm32

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

三、运动学逆解

1. 继续从整体分析

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

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

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

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

 麦轮,stm32

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

 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。

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

麦轮,stm32

         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占空比,完成小车的驱动。

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

        框架流程图:        麦轮,stm32

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

2. 上位机协议

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

        小程序操作界面:

麦轮,stm32

        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的全部源码和小程序蓝牙操作界面的源码。 文章来源地址https://www.toymoban.com/news/detail-752926.html

到了这里,关于如何获得一个丝滑的麦轮底盘(原理+代码详解)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【ComfyUI进阶1】5分钟制作丝滑的AI视频-+AnimateDiff

    用AnimateDiff Prompt Travel video-to-video搭配ComfyUI制作AI视频,效果丝滑 Ai跳舞教学案例视频 AnimateDiff可以搭配扩散模型算法(Stable Diffusion)来生成高质量的动态视频,其中动态模型(Motion Models)用来实时跟踪人物的动作以及画面的改变。 这里我们使用ComfyUI来搭配AnimateDiff做视频转

    2024年02月05日
    浏览(41)
  • Android打造丝滑的Activity recreate重建(主题切换)过渡动画

    当应用程序支持多种语言或主题时,切换语言或主题通常需要重新启动 Activity 以重新加载配置。虽然 recreate 是一种常用的重建 Activity 方法,但它不支持像在 Activity 之间切换时那样使用过渡动画。特别是在切换 浅色/深色 主题时,由于缺乏过渡动画而显得很生硬。为了提升改

    2024年02月22日
    浏览(48)
  • Android应用-Flutter实现丝滑的滑动删除、移动排序等-Dismissible控件详解

    Dismissible 是 Flutter 中用于实现可滑动删除或拖拽操作的一个有用的小部件。主要用于在用户对列表项或任何其他可滑动的元素执行删除或拖动操作时,提供一种简便的实现方式。 列表项删除: 允许用户在列表中通过滑动手势删除某个项。 左右滑动: 提供可自定义的背景,当

    2024年02月04日
    浏览(50)
  • 游戏有延迟?如何获得一个好的Ping

    在多人游戏世界中,玩家要想获得良好的游戏体验,需要做很多事情——尤其是如果这种良好的体验取决于你的想法。 在线多人游戏,如FPS(第一人称射击游戏)、赛车和一些MMORPG类型的游戏,尤其取决于玩家对游戏反馈的及时有效反应。这个反馈和反应链有很多环节,其中

    2023年04月27日
    浏览(52)
  • 分享一个菜单标签页动画,切换丝滑无比

    先上效果图: 代码如下,复制粘贴大法拿走即可使用:

    2023年04月26日
    浏览(29)
  • 非计算机科班如何丝滑转码?(本人就是有点不丝滑)

    自学 报班 有师傅带 游戏开发?后台研发?爬虫工程师?前端程序员?数据分析师? 或者 仅仅是想做一个【程序员】?? 或者被影视所影响,感觉程序员好酷、好牛逼 所以你要想清楚!!! C语言?C++?C#?Java?Python?H5?JS?甚至是Mysql? 所以你要考虑清楚,当然我还是推荐P

    2024年02月10日
    浏览(48)
  • Java:定义一个学生类(Student),属性包括:学号,姓名,性别,年龄;方法包括:获得学号,获得姓名,获得性别,获得年龄,修改学号,修改姓名,修改性别,修改年龄。定义并创建一个学生数组对象。

       定义一个学生类(Student),属性包括:学号,姓名,性别,年龄;方法包括:获得学号,获得姓名,获得性别,获得年龄,修改学号,修改姓名,修改性别,修改年龄。定义并创建一个学生数组对象,长度可自定,最后在控制台输出学生信息。 结果:  

    2024年02月11日
    浏览(59)
  • WouoUI-PageVersion 一个用于快速构建具有丝滑OLED_UI动画的项目

    简介致谢 Air001的TestUI例子的b站的演示视频 Air001的LittleClock例子的b站演示视频: https://www.bilibili.com/video/BV1J6421g7H1/ Stm32的TestUI例子的b站演示视频: https://www.bilibili.com/video/BV1mS421P7CZ/ 所有演示的工程文件都使用zip压缩包上传在对应的文件夹下。 本项目的Github链接为:https://githu

    2024年02月22日
    浏览(36)
  • 非计算机科班如何丝滑转码

    近年来,很多人想要从其他行业跳槽转入计算机领域。非计算机科班如何丝滑转码? 对于非计算机科班的人来说,想要在计算机领域实现顺利的转码并不是一件容易的事情,但也并非不可能。以下是一些建议和观点: 学习编程基础:计算机领域的核心是编程。对于非科班背

    2024年02月12日
    浏览(37)
  • 非计算机科班如何丝滑转码?

    转码,也就转行为程序员,已成为当今数字化时代的一种重要技能。随着科技的发展,越来越多的人开始意识到掌握编程技能的重要性,而非计算机科班出身的朋友们,想要丝滑转码,也许可以从以下几个方面入手。 在开始学习转码之前,首先需要明确自己的目标和动机。为

    2024年02月13日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包