通过STM32F103C8T6读取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); //设置占空比
//将角度线性变换,对应到舵机要求的占空比范围上
}
四、完整工程代码
提取码:PV24