STM32读取MPU6050数据并通过角度值控制舵机运动(STM32、GY-521 MPU6050、SG90舵机、MG946舵机)

最终现象

STM32F103读取MPU6050数据控制舵机运动

一、MPU6050数据读取

使用软件IIC与MPU6050通信,这里可以直接参照之前的一篇博客:
https://blog.csdn.net/m0_71523511/article/details/135831042

二、舵机控制原理

通过输出占空比不同的PWM波就可以控制舵机转不同的角度。

①什么是PWM?

PWM全称脉冲宽度调制。通过对一系列脉冲的宽度进行调制来获得所需要的模拟参量,参用于电机控速等领域。
规定周期为Ts则频率为1/Ts,占空比为Ton/Ts(Ton为高电平时间)。如果频率为50Hz ,也就是说一个周期是20ms,那么一秒钟就有 50次PWM周期。
在这里插入图片描述
假设高电平为5V、低电平则为0V,那么要输出不同的模拟电压就要用到PWM。通过改变IO口输出的方波的占空比,从而获得使用数字信号模拟成的模拟电压信号。占空比为50%那就是高电平时间一半,低电平时间一半。在一定的频率下,就可以得到模拟的2.5V输出电压。那么75%的占空比,得到的电压就是3.75V,以此类推,如下图所示。
在这里插入图片描述

②STM32F103C8T6如何生成PWM?

想知道这部分原理直接看视频,视频最好理解:https://www.bilibili.com/video/BV1th411z7sn/?p=15&spm_id_from=pageDriver&vd_source=2a10d30b8351190ea06d85c5d0bfcb2a

③控制舵机需要什么样的PWM波?

在这里插入图片描述
舵机的控制需要MCU产生一个周期为20ms的脉冲信号,以0.5ms到2.5ms的高电平来控制舵机转动的角度。这里的角度根据自己的需求定,也可以是-90°-90°。
在这里插入图片描述
那么要产生周期为20ms的脉冲信号要怎么配置呢?
在这里插入图片描述
上图中ARR指的是定时器重载的周期,PSC指的是分频系数,CCR指的是输出比较寄存器的值。计算公式如下:
在这里插入图片描述
上面的CK_PSC为72Mhz,不同开发板的这个值是不一样的,也可以自己配置。比如要输出频率为1Khz,占空比为50%,分辨率为1%的PWM波,将这些值带入上面的公式可以得到ARR=100、PSC=720、CCR=50。
那么现在就可以来求舵机所需要的参数了,周期为20ms对应频率为1/0.02 = 50hz,在这里PSC和ARR的参数是不固定的,只要能满足第一个公式就可以,参考江协科技设置PSC为72-1,ARR为20000-1,这样设置的目的是此时CCR设置为500,那么占空比就是500/20000=0.025,即占空比为2.5%(看上面的控制参数);CCR设置为2500,那么占空比就是2500/20000=0.125,即占空比为12.5%(脉冲高电平时间2.5ms,转到180°),这样就很直观。

三、代码分析

1、main.c

#include "stm32f10x.h"              
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
#include "usart.h"
#include <math.h>
#include "Servo.h"
#include "PWM.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;
int16_t accel_agleX; 
int16_t accel_agleY; 

int main(void)
{
	OLED_Init();
	MPU6050_Init();
	uart_init(115200);
	Servo_Init();
	
	OLED_ShowString(1, 1, "ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1, 4, ID, 2);
	
	while (1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
		
		accel_agleX = (AX + 300)*1.2*1800/3.14/15384; // 这里我的转换是不标准的,因为我发现使用别人的公式舵机的位置无法达到预期,所以我就观察oled上采集到的数值进行缩放,只要能到达平的时候是0就好,但是其实不用改16384为15384也行,改前面那个300,但是我偷懒了,这样改的快,但是只要能达到目的公式不是很重要,
		accel_agleX = accel_agleX -18;//修改完上面的参数发现还是有偏差,所以在测试之后,这里减去偏差值,基本就能确保角度是在-90-90之间。
		OLED_ShowSignedNum(2, 1, accel_agleX, 5);
		
		accel_agleY = (AY + 300)*1.2*1800/3.14/15384;
		accel_agleY = accel_agleY -13;
		OLED_ShowSignedNum(3, 1, accel_agleY, 5);
		
		if(accel_agleY >= 0)
		{
			Servo_SetAngle(98-accel_agleY);
		}
		else if(accel_agleY <0)
		{
			int16_t a = ~accel_agleY;
			Servo_SetAngle(a+90);
		}
		Delay_ms(100);
		
		if(accel_agleX >= 0)
		{
			Servo_SetAngle2(98-accel_agleX);
		}
		else if(accel_agleX <0)
		{
			int16_t a = ~accel_agleX;
			Servo_SetAngle2(a+90);
		}
		Delay_ms(100);
	}
}

2、PWM.c

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:PWM初始化
  * 参    数:无
  * 返 回 值:无
  */
void PWM_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_1;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA1引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式
	
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;				//计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/ 
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);                         //结构体初始化,若结构体没有完整赋值
	                                                                //则最好执行此函数,给结构体所有成员都赋一个默认值
	                                                                //避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;               //输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;       //输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   //输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC2Init(TIM2, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC2Init,配置TIM2的输出比较通道2
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

void PWM2_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);							//将PA1引脚初始化为复用推挽输出	
																	//受外设控制的引脚,均需要配置为复用模式
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM3);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;     //时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; //计数器模式,选择向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;				//计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;            //重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);             //将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元
	
	/*输出比较初始化*/ 
	TIM_OCInitTypeDef TIM_OCInitStructure;							//定义结构体变量
	TIM_OCStructInit(&TIM_OCInitStructure);                         //结构体初始化,若结构体没有完整赋值
	                                                                //则最好执行此函数,给结构体所有成员都赋一个默认值
	                                                                //避免结构体初值不确定的问题
	TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;               //输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;       //输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   //输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC1Init(TIM3, &TIM_OCInitStructure);                        //将结构体变量交给TIM_OC2Init,配置TIM2的输出比较通道2
	
	/*TIM使能*/
	TIM_Cmd(TIM3, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)
  */
void PWM_SetCompare2(uint16_t Compare)
{
	TIM_SetCompare2(TIM2, Compare);		//设置CCR2的值
}

void PWM_SetCompare22(uint16_t Compare)
{
	TIM_SetCompare1(TIM3, Compare);		//设置CCR2的值
}

实际上想驱动多个电机用一个定时器的多个输出通道就好了,但是我想试试用两个定时器,上面就是用两定时器不同输出通道的代码。如果是想用一个定时器的话之间加一个代码就行:
在这里插入图片描述
想要通道几就加通道几的代码。STM32F103C8T6的引脚图如下,想要使用定时器的输出通道,在对gpio进行初始化的时候需要选择复用推挽输出才行。
在这里插入图片描述
3、servo.c
驱动舵机直接调用前面封装好的底层代码即可。

#include "stm32f10x.h"                  // Device header
#include "PWM.h"

/**
  * 函    数:舵机初始化
  * 参    数:无
  * 返 回 值:无
  */
void Servo_Init(void)
{
	PWM_Init();									//初始化舵机的底层PWM
	PWM2_Init();
}

/**
  * 函    数:舵机设置角度
  * 参    数:Angle 要设置的舵机角度,范围:0~180
  * 返 回 值:无
  */
void Servo_SetAngle(float Angle)
{
	PWM_SetCompare2(Angle / 180 * 2000 + 500);	//设置占空比
												//将角度线性变换,对应到舵机要求的占空比范围上
}
void Servo_SetAngle2(float Angle)
{
	PWM_SetCompare22(Angle / 180 * 2000 + 500);	//设置占空比
												//将角度线性变换,对应到舵机要求的占空比范围上
}

四、完整工程代码

关注私聊

  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,需要了解MPU6050的数据格式和寄存器映射。MPU6050是一个六轴加速度计和陀螺仪,可以通过I2C接口读取数据。其中,加速度计的数据包括X、Y、Z三个轴的加速度,陀螺仪的数据包括X、Y、Z三个轴的角速度。这些数据都是通过读取MPU6050内部的寄存器获得的。 下面是一份基于STM32的代码,用于读取MPU6050数据并计算位移量: ``` C #include "stm32f10x.h" #include "stdio.h" #include "math.h" #define MPU6050_ADDRESS 0xD0 #define SMPLRT_DIV 0x19 #define CONFIG 0x1A #define GYRO_CONFIG 0x1B #define ACCEL_CONFIG 0x1C #define ACCEL_XOUT_H 0x3B #define ACCEL_YOUT_H 0x3D #define ACCEL_ZOUT_H 0x3F #define TEMP_OUT_H 0x41 #define GYRO_XOUT_H 0x43 #define GYRO_YOUT_H 0x45 #define GYRO_ZOUT_H 0x47 #define PWR_MGMT_1 0x6B float AcX,AcY,AcZ,Tmp,GyX,GyY,GyZ; float gyro_x, gyro_y, gyro_z; float accel_x, accel_y, accel_z; float gyro_x_old, gyro_y_old, gyro_z_old; float accel_x_old, accel_y_old, accel_z_old; float dt = 0.01; float angle_x = 0, angle_y = 0, angle_z = 0; float distance_x = 0, distance_y = 0, distance_z = 0; void I2C_Configuration(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_Init(GPIOB,&GPIO_InitStructure); I2C_InitStructure.I2C_ClockSpeed = 100000; I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = MPU6050_ADDRESS; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockStretching = I2C_ClockStretching_Enable; I2C_Init(I2C1,&I2C_InitStructure); I2C_Cmd(I2C1,ENABLE); } void MPU6050_Init(void) { I2C_Configuration(); I2C_GenerateSTART(I2C1,ENABLE); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1,MPU6050_ADDRESS,I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1,PWR_MGMT_1); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1,0x00); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1,ENABLE); } void MPU6050_Read(void) { I2C_GenerateSTART(I2C1,ENABLE); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1,MPU6050_ADDRESS,I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1,ACCEL_XOUT_H); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTART(I2C1,ENABLE); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1,MPU6050_ADDRESS,I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); AcX = (float)I2C_ReceiveData(I2C1)<<8; while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); AcX |= (float)I2C_ReceiveData(I2C1); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); AcY = (float)I2C_ReceiveData(I2C1)<<8; while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); AcY |= (float)I2C_ReceiveData(I2C1); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); AcZ = (float)I2C_ReceiveData(I2C1)<<8; while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); AcZ |= (float)I2C_ReceiveData(I2C1); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); GyX = (float)I2C_ReceiveData(I2C1)<<8; while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); GyX |= (float)I2C_ReceiveData(I2C1); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); GyY = (float)I2C_ReceiveData(I2C1)<<8; while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); GyY |= (float)I2C_ReceiveData(I2C1); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); GyZ = (float)I2C_ReceiveData(I2C1)<<8; while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); GyZ |= (float)I2C_ReceiveData(I2C1); while(!I2C_CheckEvent(I2C1,I2C_EVENT_MASTER_BYTE_RECEIVED)); I2C_GenerateSTOP(I2C1,ENABLE); accel_x = AcX / 16384.0f; accel_y = AcY / 16384.0f; accel_z = AcZ / 16384.0f; gyro_x = GyX / 131.0f; gyro_y = GyY / 131.0f; gyro_z = GyZ / 131.0f; } int main(void) { MPU6050_Init(); while(1) { MPU6050_Read(); // 计算角度 gyro_x = gyro_x - 0.0038 * gyro_x_old; gyro_y = gyro_y - 0.0038 * gyro_y_old; gyro_z = gyro_z - 0.0038 * gyro_z_old; accel_x = accel_x - 0.0015 * accel_x_old; accel_y = accel_y - 0.0015 * accel_y_old; accel_z = accel_z - 0.0015 * accel_z_old; angle_x += gyro_x * dt; angle_y += gyro_y * dt; angle_z += gyro_z * dt; angle_x = 0.99 * (angle_x + accel_x * dt) + 0.01 * angle_x; angle_y = 0.99 * (angle_y + accel_y * dt) + 0.01 * angle_y; angle_z = 0.99 * (angle_z + accel_z * dt) + 0.01 * angle_z; // 计算位移量 distance_x += angle_x * dt; distance_y += angle_y * dt; distance_z += angle_z * dt; gyro_x_old = gyro_x; gyro_y_old = gyro_y; gyro_z_old = gyro_z; accel_x_old = accel_x; accel_y_old = accel_y; accel_z_old = accel_z; } } ``` 在上述代码中,MPU6050_Init()函数用于初始化I2C接口,MPU6050_Read()函数用于读取MPU6050数据。计算角度和位移量的代码在主函数中。其中,dt为采样时间,可以根据自己的需求进行调整。这份代码只是一个示例,需要根据实际情况进行修改和完善。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值