STM32标准库I2C协议与MPU6050-2

简介: STM32标准库I2C协议与MPU6050

STM32标准库I2C协议与MPU6050-1

https://developer.aliyun.com/article/1508409


四、硬件I2C读写MPU6050

1.I2C外设简介

2.I2C框图

  • 首先左边这里是外设的通信引脚SDA和SCL,下面SMBALERT是SMBus用到的,像这种外设模块引出来的引脚,一般都是借助GPIO口的复用模式与外部世界相连的,具体是复用在了哪个GPIO口呢,还是查询这个引脚定义表,在复用功能这两栏里找一下,因为内部电路设计的时候引脚就是连接好了的,所以如果想使用硬件I2C,就只能使用它连接好的指定硬件,不像软件I2C那样引脚可以任意指定。(这里选用I2C2 起对应的引脚是Pin10和Pin11)
  • 数据控制部分,数据收发的核心部分是这里的数据寄存器和数据移位寄存器,当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步转到移位寄存器里,在移位的过程中我们就可以直接把下个数据放到数据寄存器器里等着了,一旦前个数据移位完成,下一个数据就可以无缝衔接继续发送,当数据由数据寄存器器转到移位寄存器时,就会置位状态寄存器的TXE位为1,表示发送寄存器器为空,那在接收时也是这一路,输入的数据一位一位的从引脚移入到移位寄存器里,当一个字节的数据收起之后,数据就整体从移位寄存器转到数据寄存器,同时置位标志位RXNE表示接收寄存器器非空,这时候我们就可以把数据从数据寄存器读出来了,这个流程和之前串口是一样的,只不过串口是全双工,这里是半双工。
  • 比较器和地址寄存器,这是从机模式使用的,stm32是基于可变多主机模型设计的,不进行通信的时候就是从机,就可以被别人召唤,想被别人召唤,它就应该有从机地址,就可以由这个自身地址寄存器制定,我们可以自定义一个从机地址写到这个寄存器,当stm32作为从机在被寻址时,如果收到的寻址通过比较器判断和自身地址相同,那stm32就作为从机响应外部主机的召唤,并且这个stm32 支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器。


3.I2C基本结构

移位进寄存器和数据寄存器DR的配合是通信的核心部分,这里因为I2C是高位先行,所以这个移位寄存器是向左移位,在发送的时候最高位先移出去,一个SCL时钟移位一次,八次这样就能把一个字节由高位到低位依次放到SDA线上了,那在接收的时候呢,数据通过GPIO口从右边依次移进来,最终移8次1个字节就接收完成了,使用硬件I2C的时候GPIO口都要配置成复用开漏输出的模式,交由片上外设来控制,这里即使是开漏输出模式,GPIO口也是可以进行输入的,然后SCL这里时钟控制器,通过GPIO去控制时钟线,这里我简化成一主多从的模型了,所以时钟这里只画了输出的方向,实际上前面这里如果是多主机的模型,时钟线也是会进行输入的。


4.主机发送


  • 七位地址的主发送和十位地址的主发送,它们的区别就是七位地址,起始条件后的一个字节是寻址,十位地址,起始条件后的两个字节都是寻址,其中前一个字节,这里写的是帧头,内容是五位的标志位,11110+2位地址+1位读写位,然后后一个字节内容就是纯粹的八位地址了,两个字节加一起构成十位的寻址,这是十位地的选择模式啊,我们主要关注七位地址的就行了。
  • 首先初始化之后,总线默认空闲状态,stm32 默认是从模式,为了产生一个起始条件,stm32 需要写入控制寄存器,这个得看一下手册的寄存器描述,在控制寄存器中有个START位,在这一位写1就可以产生起始条件了,当起始条件发出后,这一位可以由硬件清除,所以只要在这一位写1,stm32就自动产生起始条件了,之后stm32 由从模式转为主模式,也就是多主机模型下,stm32有数据要发就要跳出来这个意思。

5.主机接收

首先写入控制寄存器器的start位,产生起始条件,然后等待Ev5事件,下面解释和刚才一样,Ev5事件就代表起始条件已发送,最后是寻址、接收应答、结束后产生Ev6事件,这数据1这一块代表数据正在通过移位寄存器进行输入,Ev6_1事件,下面解释是没有对应的事件标志,只适于接收一个字节的情况,这个Ev6_1可以看到数据1其实还正在移位,还没收到呢,所以这个事件就没有标志位,之后当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去,如何配置是否要给应答呢,也是手册控制净寄存器CR1里,这里有一位ACK应答使能,如果写1在接收一个字节后就返回一个应答,写零就是不给应答,就是应答位的配置,之后继续当这个时序单元结束后,就说明移位寄存器器已经成功移入一个字节的数据1了,这时移入的一个字节就整体转移到数据寄存器,同时置RxNE标志位,表示数据寄存器非空,也就是收到了一个字节的数据,这个状态就是Ev7事件,下面解释是RxNE等于1,数据寄存器非空,读DR寄存器清除该事件,也就是收到数据了,当把这个数据读走之后,这个事件就没有了,上面这里Ev7事件没有了,说明此时数据1被读走,当然数据1还没读走的时候啊,数据2就可以直接移入移位寄存器了,之后数据2移位完成,收到数据2产生Ev7事件,读走数据2Ev7 事件没有了,然后按照这个流程就可以一直接收数据了,最后当我们不需要继续接收时,需要在最后一个时序单元发生时,提前把刚才说的应答位控制寄存器ack置0,并且设置终止条件请求,这就是Ev7_1事件。


6.软件/硬件波形对比

7.硬件I2C读写MPU6050

硬件I2C读写MPU6050有固定引脚如下:


MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"
 
#define MPU6050_ADDRESS   0xD0    //MPU6050的I2C从机地址
 
/**
  * 函    数:MPU6050等待事件
  * 参    数:同I2C_CheckEvent
  * 返 回 值:无
  */
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
  uint32_t Timeout;
  Timeout = 10000;                  //给定超时计数时间
  while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)  //循环等待指定事件
  {
    Timeout --;                   //等待时,计数值自减
    if (Timeout == 0)               //自减到0后,等待超时
    {
      /*超时的错误处理代码,可以添加到此处*/
      break;                    //跳出等待,不等了
    }
  }
}
 
/**
  * 函    数:MPU6050写寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 参    数:Data 要写入寄存器的数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
  I2C_GenerateSTART(I2C2, ENABLE);                    //硬件I2C生成起始条件
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);          //等待EV5
  
  I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);  //硬件I2C发送从机地址,方向为发送
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);  //等待EV6
  
  I2C_SendData(I2C2, RegAddress);                     //硬件I2C发送寄存器地址
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);      //等待EV8
  
  I2C_SendData(I2C2, Data);                       //硬件I2C发送数据
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);       //等待EV8_2
  
  I2C_GenerateSTOP(I2C2, ENABLE);                     //硬件I2C生成终止条件
}
 
/**
  * 函    数:MPU6050读寄存器
  * 参    数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
  * 返 回 值:读取寄存器的数据,范围:0x00~0xFF
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
  uint8_t Data;
  
  I2C_GenerateSTART(I2C2, ENABLE);                    //硬件I2C生成起始条件
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);          //等待EV5
  
  I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);  //硬件I2C发送从机地址,方向为发送
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);  //等待EV6
  
  I2C_SendData(I2C2, RegAddress);                     //硬件I2C发送寄存器地址
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);       //等待EV8_2
  
  I2C_GenerateSTART(I2C2, ENABLE);                    //硬件I2C生成重复起始条件
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);          //等待EV5
  
  I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);   //硬件I2C发送从机地址,方向为接收
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);   //等待EV6
  
  I2C_AcknowledgeConfig(I2C2, DISABLE);                 //在接收最后一个字节之前提前将应答失能
  I2C_GenerateSTOP(I2C2, ENABLE);                     //在接收最后一个字节之前提前申请停止条件
  
  MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);        //等待EV7
  Data = I2C_ReceiveData(I2C2);                     //接收数据寄存器
  
  I2C_AcknowledgeConfig(I2C2, ENABLE);                  //将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
  
  return Data;
}
 
/**
  * 函    数:MPU6050初始化
  * 参    数:无
  * 返 回 值:无
  */
void MPU6050_Init(void)
{
  /*开启时钟*/
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);    //开启I2C2的时钟
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);   //开启GPIOB的时钟
  
  /*GPIO初始化*/
  GPIO_InitTypeDef GPIO_InitStructure;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStructure);          //将PB10和PB11引脚初始化为复用开漏输出
  
  /*I2C初始化*/
  I2C_InitTypeDef I2C_InitStructure;            //定义结构体变量
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;        //模式,选择为I2C模式
  I2C_InitStructure.I2C_ClockSpeed = 50000;       //时钟速度,选择为50KHz
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;    //时钟占空比,选择Tlow/Thigh = 2
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;       //应答,选择使能
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //应答地址,选择7位,从机模式下才有效
  I2C_InitStructure.I2C_OwnAddress1 = 0x00;       //自身地址,从机模式下才有效
  I2C_Init(I2C2, &I2C_InitStructure);           //将结构体变量交给I2C_Init,配置I2C2
  
  /*I2C使能*/
  I2C_Cmd(I2C2, ENABLE);                  //使能I2C2,开始运行
  
  /*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
  MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);       //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
  MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);       //电源管理寄存器2,保持默认值0,所有轴均不待机
  MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);       //采样率分频寄存器,配置采样率
  MPU6050_WriteReg(MPU6050_CONFIG, 0x06);         //配置寄存器,配置DLPF
  MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);      //陀螺仪配置寄存器,选择满量程为±2000°/s
  MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);     //加速度计配置寄存器,选择满量程为±16g
}
 
 
/**
  * 函    数:MPU6050获取ID号
  * 参    数:无
  * 返 回 值:MPU6050的ID号
  */
uint8_t MPU6050_GetID(void)
{
  return MPU6050_ReadReg(MPU6050_WHO_AM_I);   //返回WHO_AM_I寄存器的值
}
 
/**
  * 函    数:MPU6050获取数据
  * 参    数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 参    数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
  * 返 回 值:无
  */
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
            int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
  uint8_t DataH, DataL;               //定义数据高8位和低8位的变量
  
  DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);    //读取加速度计X轴的高8位数据
  DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);    //读取加速度计X轴的低8位数据
  *AccX = (DataH << 8) | DataL;           //数据拼接,通过输出参数返回
  
  DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);    //读取加速度计Y轴的高8位数据
  DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);    //读取加速度计Y轴的低8位数据
  *AccY = (DataH << 8) | DataL;           //数据拼接,通过输出参数返回
  
  DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);    //读取加速度计Z轴的高8位数据
  DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);    //读取加速度计Z轴的低8位数据
  *AccZ = (DataH << 8) | DataL;           //数据拼接,通过输出参数返回
  
  DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);   //读取陀螺仪X轴的高8位数据
  DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);   //读取陀螺仪X轴的低8位数据
  *GyroX = (DataH << 8) | DataL;            //数据拼接,通过输出参数返回
  
  DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);   //读取陀螺仪Y轴的高8位数据
  DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);   //读取陀螺仪Y轴的低8位数据
  *GyroY = (DataH << 8) | DataL;            //数据拼接,通过输出参数返回
  
  DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);   //读取陀螺仪Z轴的高8位数据
  DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);   //读取陀螺仪Z轴的低8位数据
  *GyroZ = (DataH << 8) | DataL;            //数据拼接,通过输出参数返回
}


MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H
 
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);
 
void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
            int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);
 
#endif


MPU6050.Reg.h

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H
 
#define MPU6050_SMPLRT_DIV    0x19
#define MPU6050_CONFIG      0x1A
#define MPU6050_GYRO_CONFIG   0x1B
#define MPU6050_ACCEL_CONFIG  0x1C
 
#define MPU6050_ACCEL_XOUT_H  0x3B
#define MPU6050_ACCEL_XOUT_L  0x3C
#define MPU6050_ACCEL_YOUT_H  0x3D
#define MPU6050_ACCEL_YOUT_L  0x3E
#define MPU6050_ACCEL_ZOUT_H  0x3F
#define MPU6050_ACCEL_ZOUT_L  0x40
#define MPU6050_TEMP_OUT_H    0x41
#define MPU6050_TEMP_OUT_L    0x42
#define MPU6050_GYRO_XOUT_H   0x43
#define MPU6050_GYRO_XOUT_L   0x44
#define MPU6050_GYRO_YOUT_H   0x45
#define MPU6050_GYRO_YOUT_L   0x46
#define MPU6050_GYRO_ZOUT_H   0x47
#define MPU6050_GYRO_ZOUT_L   0x48
 
#define MPU6050_PWR_MGMT_1    0x6B
#define MPU6050_PWR_MGMT_2    0x6C
#define MPU6050_WHO_AM_I    0x75
 
#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
 
uint8_t ID;               //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ;     //定义用于存放各个数据的变量
 
int main(void)
{
  /*模块初始化*/
  OLED_Init();    //OLED初始化
  MPU6050_Init();   //MPU6050初始化
  
  /*显示ID号*/
  OLED_ShowString(1, 1, "ID:");   //显示静态字符串
  ID = MPU6050_GetID();       //获取MPU6050的ID号
  OLED_ShowHexNum(1, 4, ID, 2);   //OLED显示ID号
  
  while (1)
  {
    MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);    //获取MPU6050的数据
    OLED_ShowSignedNum(2, 1, AX, 5);          //OLED显示数据
    OLED_ShowSignedNum(3, 1, AY, 5);
    OLED_ShowSignedNum(4, 1, AZ, 5);
    OLED_ShowSignedNum(2, 8, GX, 5);
    OLED_ShowSignedNum(3, 8, GY, 5);
    OLED_ShowSignedNum(4, 8, GZ, 5);
  }
}
相关实践学习
容器服务Serverless版ACK Serverless 快速入门:在线魔方应用部署和监控
通过本实验,您将了解到容器服务Serverless版ACK Serverless 的基本产品能力,即可以实现快速部署一个在线魔方应用,并借助阿里云容器服务成熟的产品生态,实现在线应用的企业级监控,提升应用稳定性。
云原生实践公开课
课程大纲 开篇:如何学习并实践云原生技术 基础篇: 5 步上手 Kubernetes 进阶篇:生产环境下的 K8s 实践 相关的阿里云产品:容器服务&nbsp;ACK 容器服务&nbsp;Kubernetes&nbsp;版(简称&nbsp;ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
12天前
使用STM32F103标准库实现定时器控制LED点亮和关闭
通过这篇博客,我们学习了如何使用STM32F103标准库,通过定时器来控制LED的点亮和关闭。我们配置了定时器中断,并在中断处理函数中实现了LED状态的切换。这是一个基础且实用的例子,适合初学者了解STM32定时器和中断的使用。 希望这篇博客对你有所帮助。如果有任何问题或建议,欢迎在评论区留言。
47 2
|
12天前
|
IDE 开发工具
使用STM32F103标准库实现自定义键盘
通过本文,我们学习了如何使用STM32F103标准库实现一个简单的自定义键盘。我们首先初始化了GPIO引脚,然后实现了一个扫描函数来检测按键状态。这个项目不仅能够帮助我们理解STM32的GPIO配置和按键扫描原理,还可以作为进一步学习中断处理和低功耗设计的基础。希望本文对你有所帮助,祝你在嵌入式开发的道路上不断进步!
45 4
|
12天前
|
存储 数据采集 数据安全/隐私保护
使用STM32F103读取TF卡并模拟U盘:使用标准库实现
通过以上步骤,你可以实现用STM32F103将TF卡内容变成U盘进行读取。这种功能在数据采集、便携式存储设备等应用中非常有用。如果你有更多的需求,可以进一步扩展此项目,例如添加文件管理功能、加密存储等。希望这篇博客能帮到你,如果有任何问题,欢迎在评论区留言讨论!
17 1
|
13天前
|
传感器
【经典案例】STM32F407使用HAL库配置I2C详解
STM32F407是一个强大的微控制器,广泛应用于嵌入式系统中。在许多应用中,我们需要使用I2C总线来与传感器、EEPROM、显示屏等外设进行通信。本文将详细介绍如何使用STM32 HAL库来配置和使用I2C接口。
26 2
|
13天前
|
开发者
【经典案例】使用HAL库配置STM32F407的SPI外设
在嵌入式系统开发中,STM32F407是一款广泛应用的微控制器,而SPI(Serial Peripheral Interface)是一种常用的通信接口。本文将详细介绍如何使用STM32的硬件抽象层(HAL)库配置STM32F407的SPI外设,并提供完整的代码示例。
30 1
|
1月前
|
缓存 网络协议 算法
[蓝桥杯嵌入式]hal库 stm32 PWM的使用(随时修改占空比,随时修改频率)
[蓝桥杯嵌入式]hal库 stm32 PWM的使用(随时修改占空比,随时修改频率)
|
1月前
【STM32】基于HAL库的360度编码器、摇杆代码编写
【STM32】基于HAL库的360度编码器、摇杆代码编写
|
1月前
|
传感器 算法
【STM32】I2C练习,HAL库读取MPU6050角度陀螺仪
【STM32】I2C练习,HAL库读取MPU6050角度陀螺仪
|
1月前
|
传感器