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

news2024/11/18 19:42:14

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

    • 最终现象
    • 一、MPU6050数据读取
    • 二、舵机控制原理
      • ①什么是PWM?
      • ②STM32F103C8T6如何生成PWM?
      • ③控制舵机需要什么样的PWM波?
    • 三、代码分析
    • 四、完整工程代码

最终现象

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);	//设置占空比
												//将角度线性变换,对应到舵机要求的占空比范围上
}

四、完整工程代码

我用夸克网盘分享了「MPU6050控制舵机.rar」,点击链接即可保存。打开「夸克APP」,无需下载在线播放视频,畅享原画5倍速,支持电视投屏。
链接:https://pan.quark.cn/s/a07f1cbd8320
提取码:PV24

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1417639.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

看图说话:Git图谱解读

很多新加入公司的同学在使用Git各类客户端管理代码的过程中对于Git图谱解读不太理解&#xff0c;我们常用的Git客户端是SourceTree&#xff0c;配合P4Merge进行冲突解决基本可以满足日常工作大部分需要。不同的Git客户端工具对图谱展示会有些许差异&#xff0c;以下是SourceTre…

jenkins对接K8S

创建连接K8S的凭据 查看需要使用到的命名空间 [rootk8s ~]# kubectl get ns |grep arts-system arts-system Active 16d创建service accounts [rootk8s ~]# kubectl create sa jenkins-k8s -n arts-system serviceaccount/jenkins-k8s created [rootk8s ~]# kubectl…

使用vscode查bug

具体操作 修改CMakeList.txt # set(CMAKE_BUILD_TYPE "Release")//注释Release模式 set(CMAKE_BUILD_TYPE "Debug")//设置为Debug模式 # set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -g")//注释*这行代码是用来设置 CMake 构建系统中 Release 模式…

Go Zero微服务个人探究之路(十)实战走通微服务前台请求调用的一套流程model->rpc微服务->apiHTTP调用

前言 Go语言凭借低占用&#xff0c;高并发等优秀特性成为后台编程语言的新星&#xff0c;GoZero框架由七牛云技术副总裁团队编写&#xff0c;目前已经成为Go微服务框架里star数量最多的框架 本文记录讲述笔者一步步走通前台向后台发出请求&#xff0c;后台api调用rpc服务的相…

verilog编程之乘法器的实现

知识储备 首先来回顾一下乘法是如何在计算机中实现的。 假设现在有两个32位带符号定点整数x和y&#xff0c;我们现在要让x和y相乘&#xff0c;然后把乘积存放在z中&#xff0c;大家知道&#xff0c;两个32位数相乘&#xff0c;结果不会超过64位&#xff0c;因此z的长度应该为64…

总结6(循环(for))

循环 定义&#xff1a; 某些代码会被重复执行 分类&#xff1a; for 1.格式 for(1; 2; 3) 语句A; 2.执行的流程&#xff08;1,2,A,3 2,A,3 2,A,3..........&#xff09; 单个for循环的使用 多个for循环的嵌套使用 1). for&#xff08;1; 2; 3&#xff09; for&#xff0…

用通俗易懂的方式讲解:一种全新的大模型检索增强生成方法

如何使大型语言模型更加事实、正确和可靠&#xff1f; 检索增强生成&#xff08;RAG&#xff09;是一种有效的方法&#xff0c;可以缓解大型语言模型的基本局限性&#xff0c;如幻觉和缺乏最新知识。 然而&#xff0c;如果您曾尝试过RAG&#xff0c;您会同意我所说的RAG易于原…

【每日一题】4.LeetCode——环形链表

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有限&#xff0c;欢迎各位大佬指点&…

RK3568平台开发系列讲解(Linux系统篇)互斥锁使用

🚀返回专栏总目录 文章目录 一、互斥锁API二、使用互斥锁的步骤三、互斥锁使用规则四、使用案例沉淀、分享、成长,让自己和他人都能有所收获!😄 一、互斥锁API 在Linux中,你可以使用互斥锁(Mutex)来实现多线程或多进程之间的互斥访问。互斥锁用于确保在同一时间只有一…

24. 两两交换链表中的节点(力扣LeetCode)

文章目录 24. 两两交换链表中的节点题目描述解题思路只使用一个临时节点使用两个临时节点 24. 两两交换链表中的节点 题目描述 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff0…

STM32学习笔记(二) —— 调试串口

我们在调试程序时&#xff0c;经常会使用串口打印相关的调试信息&#xff0c;但是单片机串口不能直接与 PC 端的 USB 接口通讯&#xff0c;需要用到一个USB转串口的芯片来充当翻译的角色。我们使用的开发板上有这个芯片&#xff0c;所以在打印调试信息的时候直接使用USB线连接开…

每日一题——LeetCode1365.有多少小于当前数字的数字

方法一 暴力循环 对于数组里的没一个元素都遍历一遍看有多少元素小于当前元素 var smallerNumbersThanCurrent function(nums) {let n nums.length;let ret [];for (let i 0; i < n; i) {let count 0;for (let j 0; j < n; j) {if (nums[j] < nums[i]) {count…

【C语言】数组的应用:三子棋游戏

由于代码较长&#xff0c;为了增加可读性&#xff0c;我们把代码分别写到game.h&#xff0c;game.c&#xff0c;test.c&#xff0c;里面&#xff0c;其中game.h用来声明函数&#xff0c;实现函数功能的代码在game.c&#xff0c;测试游戏的代码在test.c 为了方便后续的更改&…

qt学习:http+访问百度智能云api实现车牌识别

目录 登录到百度智能云&#xff0c;找到文字识别 完成操作指引 开通 查看车牌识别的api文档 ​编辑​编辑 查看自己应用的api key 查看回应的数据格式 编程步骤 ui界面编辑 添加模块&#xff0c;头文件和定义变量 新建两个类&#xff0c;一个图像Image类&#xff0c…

深度学习之卷积神经网络进阶版

上一讲我们介绍了卷积神经网络和多层感知机&#xff0c;也就是全链接网络。他们在网络架构上是串行的结构&#xff0c;也就是在每一层与每一层之间&#xff0c;前面一层的输出&#xff0c;是后面一层的输入。 在神经网络里面&#xff0c;我们可能会有更加复杂的结构&#xff0…

Pycharm连接云算力远程服务器(AutoDL)训练深度学习模型全过程

前言&#xff1a;在上一篇windows搭建深度学习环境中&#xff0c;我试图使用笔记本联想小新air14的mx350显卡训练一个图像检测的深度学习模型&#xff0c;但是训练时长大概需要几天时间远超我的预期&#xff0c;所以我便选择租用GPU进行训练&#xff0c;在对多家平台对比后找到…

[BT]小迪安全2023学习笔记(第19天:Web开发-.NET项目)

第19天 名词解释 .NET 是一种由Microsoft开发的软件框架&#xff0c;用于构建和运行Windows操作系统上的应用程序。它提供了一个广泛的工具和库集合&#xff0c;支持多种编程语言&#xff0c;包括C#、VB.NET、F#等。.NET的主要目标是提供一种一致的开发平台&#xff0c;使开发…

《HTML 简易速速上手小册》第1章:HTML 入门(2024 最新版)

文章目录 1.1 HTML 简介与历史&#xff08;&#x1f609;&#x1f310;&#x1f47d;踏上神奇的网页编程之旅&#xff09;1.1.1 从过去到现在的华丽蜕变1.1.2 市场需求 —— HTML的黄金时代1.1.3 企业中的实际应用 —— 不只是个网页1.1.4 职业前景 —— 未来属于你 1.2 基本 H…

SQL注入:二次注入

SQL注入系列文章&#xff1a; 初识SQL注入-CSDN博客 SQL注入&#xff1a;联合查询的三个绕过技巧-CSDN博客 SQL注入&#xff1a;报错注入-CSDN博客 SQL注入&#xff1a;盲注-CSDN博客 目录 什么是二次注入&#xff1f; 二次注入演示 1、可以注册新用户 2、可以登录->…

数据湖技术之平台建设篇2

数据湖技术之平台建设篇1&#xff0c;主要介绍了湖仓平台建设的前三个主要工作&#xff0c;本次主要继续上次的建设工作介绍&#xff0c;聊一聊一站式湖仓服务平台的相关管理能力建设以及针对小文件的处理。 一. 一站式湖仓服务平台的相关管理能力 主要是将相关能力落地到平台…