蓝牙控制STM32平衡车(一,硬件和程序实现)

一 硬件结构和原理

1.器件选型

主控芯片是F1系列的MCU

电机的话最好是那种精度较高的编码电机,当然淘宝那种霍尔的编码电机也是可以实现的;

电机驱动的话推荐TB6612驱动,带两个电机没有问题,并且体积比较小,可以直接画在PCB上;因为我手头有一个L298N的驱动,所以我采用的是L298N,当然驱动程序是可以通用的,注意好接线就行。

陀螺仪的话采用MPU6050完全满足,并且市面上资料比较多,用正点原子的DMP姿态解算非常方便;

蓝牙用的是HC-06的蓝牙模块,淘宝就有,当然HC-05主从一体的也可以;

显示屏使用0.96OLED就可以;

稳压的话可以直接淘宝买LM2595稳压就能满足,使用的时候要用电压表打下输出电压,刚买回来的输出电压一般不是5V;

电源建议买12V的锂电池组,不建议三个电池带一个电池盒,电池比较容易挂掉。

至于其他零件都比较随意了一般实验室都有,车模的话讲究重心低,结构紧凑最好,有利于后期的调参(减少痛苦

2.实现原理

        单片机通过获取陀螺仪的角度等数据判断当前姿态,另外编码电机提供的计数值,综合通过PID算法来控制PWM的输出作用到电机上,(即直立环和速度环)最终的控制都是加载到电机上,其中逻辑控制电机的正反转,PWM控制转速;当倾角越大转速越快,具体的控制效果在下章PID调参中具体讲解。

        另外就是其他外设,显示屏显示俯仰角或其他数据用于调试。通过蓝牙手机和单片机通信,控制车的前后左右。

3.硬件原理

      (1)编码电机

                编码电机有光电和霍尔的,其中的计数原理略有不同,但是最终实现的目的都是一样的,具体的原理可以网上了解。编码电机码盘上一般有6个接口,分别为M- VCC OA OB GND M+(不同的电机顺序可能不一样,不过市面上的直流减速编码电机都是这样),其中最外层的两个是电机的正负极,正接就是正转,反接反转,M+,M-接在电机驱动的输出上。VCC和GND是编码盘的5V供电,最内层的OA OB是编码电机的信号线,具体原理可以了解AB相正交解码,会用就行。

        只要电机轮子转动,编码器的计数就会增大或者减小,通过AB相将信号送给单片机,此时就能获取电机转动的数据。

       (2)电机驱动

                                        先贴张L298N的引脚图

        L298N可以带两个电机,左右马达输出就接在电机的正负极上,12V电源供电可以直接从电池引入,GND就没什么了;再向下以此为ENA IN1 IN2 IN3 IN4 ENB,我们使用时将ENA和ENB引脚的跳线帽拿掉,前三个是通道A的,同理后面三个是通道B的引脚,分别控制两个电机;ENA和ENB接单片机的PWM输出信号(控制电机的转速),IN1和IN2接从单片机输出的逻辑信号(控制电机的正反转),同理的IN3,IN4,ENB也是一样;

        通过IN引脚获得逻辑输入,EN引脚获得PWM信号最终改变马达输出端的电压以及电压的方向,从而实现电机的控制。

       (3)陀螺仪

        MPU6050陀螺仪内部带有三轴陀螺仪和三轴加速度器,和DMP数字运动处理器,有了陀螺仪和加速度数据就可以解算出欧拉角,俯仰角,横滚角,偏航角;根据DMP解算大大减轻了MCU的负担,我们就可以不太关心内部具体如何解算,根据函数调用直接获取数据便可;有想具体了解内部如何工作的可以查阅资料;引脚的话一般会有8个分别为VCC GND SCL SDA XDA XCL AD0 INT,其中我们用到的VCC和GND肯定要的(注意电源供电3.3V直接接5V可能会烧掉)。SCL和SDA是通信引脚,协议是IIC通信和后面的OLED一样,还有一个就是最后一个INT引脚,接在单片机的外部中断引脚上,DMP解算完成触发外部中断,在中断里更新数据信息。

       (4)显示

        显示的话使用OLED显示屏调试也是可以的,在上面显示小车的俯仰角,另外也可以用串口打印显示在电脑上也是可以的。OLED显示屏的电源可以接5V也可以3.3V,理论是要接3.3V 不过我的接的5V也可以使用。SCL和SDA同样是数据信号引脚,用的软件IIC。

       (5)蓝牙

        蓝牙用的是HC06模块,我的这个只能是从机模式被动连接,有HC05的主从一体更好。蓝牙的RXD和TXD接在单片机的USART3的引脚上,蓝牙和手机连接,至于蓝牙的设置可以网上搜索AT指令,蛮简单的,注意HC05和HC06的AT指令的格式是不一样的,将蓝牙配置为从机模式,设置名称,密码还有波特率(HC06默认9600,不调也可以)

蓝牙和手机连接成功之后就相当与信号线,作为信号传输。

二 程序实现

下面就贴几个重要的代码

  pwm.c

void PWM_Init_TIM1(u16 Psc,u16 Per)//PWM初始化
{
	GPIO_InitTypeDef GPIO_InitStruct;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	TIM_OCInitTypeDef TIM_OCInitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_TIM1 |        RCC_APB2Periph_AFIO,ENABLE);
	
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_8 |GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
	TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;
	TIM_TimeBaseInitStruct.TIM_Period=Per;
	TIM_TimeBaseInitStruct.TIM_Prescaler=Psc;
	TIM_TimeBaseInit(TIM1,&TIM_TimeBaseInitStruct);
	
	TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1;
	TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;
	TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;
	TIM_OCInitStruct.TIM_Pulse=0;
	TIM_OC1Init(TIM1,&TIM_OCInitStruct);

	TIM_OC4Init(TIM1,&TIM_OCInitStruct);
	
	TIM_CtrlPWMOutputs(TIM1,ENABLE);
	
	TIM_OC1PreloadConfig(TIM1,TIM_OCPreload_Enable);
	TIM_OC4PreloadConfig(TIM1,TIM_OCPreload_Enable);
	TIM_ARRPreloadConfig(TIM1,ENABLE);
	
	TIM_Cmd(TIM1,ENABLE);

motor.c

void Motor_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_12 |GPIO_Pin_13 |GPIO_Pin_14 |GPIO_Pin_15;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);	
}


void Load(int moto1,int moto2)
{
	
	if(moto1>0)	Ain1=1,Ain2=0;
	else 		Ain1=0,Ain2=1;
	TIM_SetCompare1(TIM1,GFP_abs(moto1));
	if(moto2>0)	Bin1=0,Bin2=1;
	else 		Bin1=1,Bin2=0;	
	TIM_SetCompare4(TIM1,GFP_abs(moto2));
}

exti.c

void MPU6050_EXTI_Init(void)
{
	EXTI_InitTypeDef EXTI_InitStruct;
	GPIO_InitTypeDef GPIO_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO,ENABLE);
	
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU;/**¡¾1¡¿**///GPIO_Mode_AF_PP
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;
	GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStruct);	
	
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource5);//
	
	EXTI_InitStruct.EXTI_Line=EXTI_Line5;
	EXTI_InitStruct.EXTI_LineCmd=ENABLE;
	EXTI_InitStruct.EXTI_Mode=EXTI_Mode_Interrupt;
	EXTI_InitStruct.EXTI_Trigger=EXTI_Trigger_Falling;
	EXTI_Init(&EXTI_InitStruct);
}

encoder.c


void Encoder_TIM2_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	TIM_ICInitTypeDef TIM_ICInitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN_FLOATING;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_0 |GPIO_Pin_1;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	
	TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
	TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;
	TIM_TimeBaseInitStruct.TIM_Period=65535;
	TIM_TimeBaseInitStruct.TIM_Prescaler=0;
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
	
	TIM_EncoderInterfaceConfig(TIM2,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
	
	TIM_ICStructInit(&TIM_ICInitStruct);
	TIM_ICInitStruct.TIM_ICFilter=10;
	TIM_ICInit(TIM2,&TIM_ICInitStruct);
	
	TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);
	
	TIM_SetCounter(TIM2,0);
	
	TIM_Cmd(TIM2,ENABLE);
}


void Encoder_TIM4_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStruct;
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
	TIM_ICInitTypeDef TIM_ICInitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4,ENABLE);
	
	GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IN_FLOATING;
	GPIO_InitStruct.GPIO_Pin=GPIO_Pin_6 |GPIO_Pin_7;
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	
	TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
	TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;
	TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;
	TIM_TimeBaseInitStruct.TIM_Period=65535;
	TIM_TimeBaseInitStruct.TIM_Prescaler=0;
	TIM_TimeBaseInit(TIM4,&TIM_TimeBaseInitStruct);
	
	TIM_EncoderInterfaceConfig(TIM4,TIM_EncoderMode_TI12,TIM_ICPolarity_Rising,TIM_ICPolarity_Rising);
	
	TIM_ICStructInit(&TIM_ICInitStruct);
	TIM_ICInitStruct.TIM_ICFilter=10;
	TIM_ICInit(TIM4,&TIM_ICInitStruct);
	
	TIM_ClearFlag(TIM4,TIM_FLAG_Update);
	TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE);
	
	TIM_SetCounter(TIM4,0);

	TIM_Cmd(TIM4,ENABLE);
}


int Read_Speed(int TIMx)
{
	int value_1;
	switch(TIMx)
	{
		case 2:value_1=(short)TIM_GetCounter(TIM2);TIM_SetCounter(TIM2,0);break;
		case 4:value_1=(short)TIM_GetCounter(TIM4);TIM_SetCounter(TIM4,0);break;
		default:value_1=0;
	}
	return value_1;
}

最重要的control.c

#include "control.h"
#include "main.h"
extern Flag_typedef Flag;
float Target_velocity=0;
float Turn_velocity=0;

#define SPEED_Y 500 //前后最大速度
#define SPEED_Z 100//左右最大速度
float Med_Angle=9.5;//机械中值
float
	Vertical_Kp=540,//550,	直立环系数
	Vertical_Kd=7.5 ;//5.8;
float
	Velocity_Kp=-0.021,//0.01,	//速度环系数
	Velocity_Ki=-0.000105;
float 
	Turn_Kd=0,//转向环稀释	
	Turn_Kp=5;

int Vertical_out,Velocity_out,Turn_out=0;

int Vertical(float Med,float Angle,float gyro_Y);
int Velocity(float target ,int encoder_left,int encoder_right);
int Turn(int gyro_Z,int RC);

void EXTI9_5_IRQHandler(void)//整个姿态都是在外部中断进行,PID需要严格的时间,在MPU.C中设置10MS
{
	if(EXTI_GetITStatus(EXTI_Line5)!=0)
	{
		int PWM_out,PWM_dead;
		if(PBin(5)==0)
		{
			EXTI_ClearITPendingBit(EXTI_Line5);
			
			
			Encoder_Left=-Read_Speed(2);//读取编码器速度,相对安装,其中一个符号取反
			Encoder_Right=Read_Speed(4);
			mpu_dmp_get_data(&Pitch,&Roll,&Yaw);//获取陀螺仪,加速度和DMP角度
			MPU_Get_Gyroscope(&gyrox,&gyroy,&gyroz);	
			MPU_Get_Accelerometer(&aacx,&aacy,&aacz);	
	
			if((Flag.Go==0)&&(Flag.Buck==0))Target_velocity=0;
			if(Flag.Go==1)Target_velocity-=2;//蓝牙控制,前进标志被置位,期望速度减小
			if(Flag.Buck==1)Target_velocity+=2;
			Target_velocity=Target_velocity>SPEED_Y?SPEED_Y:(Target_velocity<-SPEED_Y?(-SPEED_Y):Target_velocity);//限幅
			if((Flag.Left==0)&&(Flag.Right==0))Turn_velocity=0;
			if(Flag.Left==1)Turn_velocity++;
			if(Flag.Right==1)Turn_velocity--;
			Turn_velocity=Turn_velocity>SPEED_Z?SPEED_Z:(Turn_velocity<-SPEED_Z?(-SPEED_Z):Turn_velocity);

			
			//数据压入	PID速度环输出给到直立环输入,串级PID
			Velocity_out=Velocity(Target_velocity,Encoder_Left,Encoder_Right);	
			Vertical_out=Vertical(Med_Angle+Velocity_out,Pitch,gyroy);				
			PWM_out=Vertical_out;
			
			Turn_out=Turn(gyroz,Turn_velocity);																

//		OLED_Num4(0,0,PWM_out);
					if(PWM_out>0){PWM_dead=Dead_value(0);}//死区电压
					else if(PWM_out<0){PWM_dead=-Dead_value(0);}
			MOTO1=PWM_dead+PWM_out+Turn_out;//PID输出加上死区电压和转向环,最终输出
			MOTO2=PWM_dead+PWM_out-Turn_out;
			
			Limit(&MOTO1,&MOTO2);//限幅		
			Load(MOTO1,MOTO2);//加载到电机
		Motor_off(&Med_Angle,&Pitch);//电机异常关闭
	
		}
	}
}


int Dead_value(int value)
{
	return value;	
}
/*************
直立环,输入机械中值,真实角度,真实角速度
**************/
int Vertical(float Med,float Angle,float gyro_Y)
{
	int PWM_out;
	PWM_out=Vertical_Kp*(Angle-Med)+Vertical_Kd*(gyro_Y-0);
	return PWM_out;
}



/*********************
速度环,输入期望速度,左右电机速度
*********************/
extern int EnC;
int Velocity(float target ,int encoder_left,int encoder_right)
{
	static float Moment;
	static int PWM_out,Encoder_Err,Encoder_S,EnC_Err_Lowout,EnC_Err_Lowout_last;
	float a=0.6;


	Encoder_Err=(encoder_left+encoder_right)-target;
	EnC_Err_Lowout=(1-a)*Encoder_Err+a*EnC_Err_Lowout_last;//低通滤波
	EnC_Err_Lowout_last=EnC_Err_Lowout;//
	
	
	Encoder_S+=EnC_Err_Lowout;//误差累积
	Encoder_S=Encoder_S;


	Encoder_S=Encoder_S>10000?10000:(Encoder_S<(-10000)?(-10000):Encoder_S);//限幅
	PWM_out=Velocity_Kp*EnC_Err_Lowout+Velocity_Ki*Encoder_S;//
	if(Motor_off(&Med_Angle,&Pitch)==1){Encoder_S=0;}//电机异常关闭积分清零
	return PWM_out;
}



/*********************
转向环,Z轴角速度
*********************/
int Turn(int gyro_Z,int T)
{
	int PWM_out;
	
	PWM_out=Turn_Kd*gyro_Z+Turn_Kp*T;
	return PWM_out;
}

后续会出PID讲解和调参

链接:https://pan.baidu.com/s/11pMVbMSqpbYqGfJ5TEQGRA 
提取码:1213

  • 9
    点赞
  • 81
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
两轮自平衡特点: 小底盘使用的是一体成型的钣金件,且表面做了黑色阳极化处理,更耐脏,更坚固,而非其他的使用亚克力固定电机座的做法。 上两层使用黑色亚克力,与底盘浑然一体,更加时尚美观。 电机光栅码盘有保护盖,避免了小行进碰撞导致损坏光栅,如果光栅损坏了,小想再站起来就不可能了。 使用的是减速电机而非步进电机,反应更迅速。 电路完全自主设计成单模式,而非模块拼凑。 使用安卓蓝牙APP进行遥控。 电路控制使用双主控,与现有市面上的载人两轮自平衡方案相同,一颗用于运动控制,一颗用于姿态解算,具备更高的可靠性。 电路提供了2部分3.3V电源,一个用于姿态传感器单独供电,另一个用于除姿态传感器其他的所有部分3.3V电源,避免了电源交叉影响,给姿态解算带来了更高的精确度。 小硬件组成: 双主控:运动控制STM32F103RCT6)、姿态解算(STM32F103C8T6) 姿态传感器:陀螺仪+加速度传感器(MPU6050)、磁场传感器,用于磁场补偿(HMC5883L) 电机驱动:TB6612FNG,对比L298N,带来更高的效率和更低发热量 遥控数据接收:主从一体蓝牙模块(汇承HC-05) 体构成:黑色阳极化钣金底盘和2块3mm厚度黑色亚克力 带光栅码盘减速电机,轮胎转一圈输出6280个脉冲 安卓控制程序: 两轮自平衡演示视频: 两轮自平衡附件包含原理图、源代码、APP、视频
基于STM32的可控两轮自平衡可以通过蓝牙进行控制蓝牙模块使用的是HC06,它被设置为从机模式被动连接。蓝牙的RXD和TXD引脚连接到STM32的USART3引脚上。要连接蓝牙手机,可以使用AT指令进行配置,设置名称、密码和波特率。注意,HC05和HC06的AT指令格式不同。将蓝牙配置为从机模式后,可以通过蓝牙控制的运动。\[2\] 此外,小的电机驱动使用了L298N模块,它可以驱动两个电机。左右马达的输出连接到电机的正负极上,12V电源可以直接从电池引入。ENA和ENB引脚用于控制电机的转速,它们连接到单片机的PWM输出信号。IN1和IN2用于控制电机的正反转,IN3、IN4和ENB也是类似的。通过控制这些引脚的逻辑信号,可以实现对电机的控制,从而控制的运动。\[3\] 因此,通过STM32蓝牙模块的配合,可以实现对基于STM32的可控两轮自平衡蓝牙控制。 #### 引用[.reference_title] - *1* [69、基于STM32单片机智能两轮双轮自平衡 蓝牙app控制系统设计](https://blog.csdn.net/2301_76924958/article/details/129558571)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [蓝牙控制STM32平衡(一,硬件程序实现)](https://blog.csdn.net/DXRES/article/details/124408197)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值