STM32之I2C通信

发布时间 2023-10-01 18:34:16作者: Sakura_Ji

I2C通信

学习资料:

前言

线与:连接在总线上的设备只要有一个输出低电平(0)总线就为低电平(0),只有全部设备都为高阻态时总线才是高电平(1)

I2C简介

I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线

  • 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
  • 同步,半双工
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从(可利用“线与”特性来执行时钟同步和总线仲裁))

I2C的硬件电路设计

I2C_Struct

  • 所有I2C设备的SCL连在一起,SDA连在一起

  • 设备的SCL和SDA均要配置成开漏输出模式

  • SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

我主要学习的是I2C一主多从的模式,所以下面以一主多从的模式(一主 即 MCU(STM32)来当作主机 ,其它设备当作从机)来讲解I2C的硬件电路是如何设计出来的

解释:

  • 对于 SCL 时钟线 来说 它是由主机控制的 所以它可以是 开漏输出 亦可以是 推挽输出 都可以 ,但为什么选择了 开漏输出模式 是因为 :多主多从模式下可利用“线与”特性来执行 时钟同步和总线仲裁
  • 对于 **SDA 时钟线 **来说 它是通信数据的线 不仅主机需要它进行 输出数据 还需要 接收从机发送过来的 数据 ,即在在此期间需要频繁的切换GPIO的输入输出模式 还要兼顾 不能在一个设备输出高电平 另一个设备输出低电平 导致的电源短路现象 所以为了防止总线没有协调好而导致此现象的产生 I2C设计是禁止所有设备输出强上拉的高电平 采用 外置弱上拉电阻加开漏输出模式
    • 那此时 你可能有所疑惑: 选择开漏输出模式 那岂不是不能输入了 其实然也 不知道你是否还记得 学习GPIO时,我们的STM32输出配置框图中 无论我们选择什么输出模式 都是可以进行输入的 不信?请看下图 STM32数据手册上写到 当I/O端口被配置为输出时 施密特触发输入被激活 如果你对GPIO有所遗忘 可以去看我的另一篇关于STM32之GPIO外设 - Sakura_Ji - 博客园 的笔记I2C_GPIOMode
  • 开漏输出模式: 设备输出1时是高阻态,在硬件电路设计图中即SCLKN1OUT/DATAN1OUT断开使引脚浮空,为了避免引脚浮空,通过外置的上拉电阻呈现弱上拉的高电平 但不影响数据的传输;设备输出0时是强下拉低电平
  • 所有任何设备在任何时候 是都可以进行输入的 都可以通过一个数据缓冲器或者是施密特触发器,进行输入

通过上文可知:

  • 设备在进行输出时: 低电平:强下拉的低电平 高电平: 弱上拉的高电平
  • 设备在进行输入时: 可直接输出高电平(相当于高阻态 断开引脚) 然后观察总线的高低电平即可

I2C的软件设计

  1. 主机可以访问总线上的任何一个设备
  2. 要与那个设备进行通信 主机在起始条件后 需要先发送 该设备的地址
  3. 所有设备都会对这个地址进行判断,如果和自己的不一样会认为没有访问自己,之后的时序就不管了,如果一样会向主机发送应答,并准备响应之后主机的读写操作
  4. 同一条的I2C总线上的从机的设备地址要求不能相同
  5. 从机设备地址在I2C协议标准里分为7位地址和10位地址,7位地址应用最为广泛
  6. 以7位作为示例:厂商一般规定高4位是固定死的,但低3位是可以通过电路进行改变的,这样地址就可以不同,所以I2C总线可以搭载相同的设备

I2C最大的一个特点就是有完善的应答机制,从机(主机)接收到主机(从机)的数据时,会回复一个应答信号来通知主机表示“我收到了”。

应答信号: 出现在1个字节传输完成之后,即第9个SCL时钟周期内,此时主机需要释放SDA总线,把总线控制权交给从机,由于上拉电阻的作用,此时总线为高电平,如果从机正确的收到了主机发来的数据,会把SDA拉低,表示应答响应。

非应答信号:当第9个SCL时钟周期时,SDA保持高电平,表示非应答信号。

非应答信号可能是主机产生也可能是从机产生,产生非应答信号的情况主要有以下几种:

  • I2C总线上没有主机所指定地址的从机设备;
  • 从机正在执行一些操作,处于忙状态,还没有准备好与主机通讯;
  • 主机发送的一些控制命令,从机不支持;
  • 主机接收从机数据时,主机产生非应答信号,通知从机数据传输结束,不要再发数据了;

I2C的时序机制

  • 总线空闲状态: SCL和SDK同时处于高电平

  • 起始条件: SCL高电平期间,SDA从 高电平 切换到 低电平

  • 终止条件: SCL高电平期间,SDA从 低电平 切换到 高电平

  • 发送一个字节: SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节

  • 接收一个字节: SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)

  • 发送应答:主机在接收完一个字节之后,在下一个 时钟发送一位数据,数据0表示应答,数据1表示非应答

  • 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

I2C_TimeSequence

I2C_DataChange

I2C的数据帧格式

I2C的数据帧格式有:指定地址写,当前地址读,指定地址读

I2C_DataFlow

指定地址写

对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)

I2C_WriteReg

  1. 总线由空闲状态转为起始位:在SCL高电平的期间,SDA下降沿触发

  2. 主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  3. 主机释放SDA 接收从机的应答位 从机控制SDA :

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
  4. 主机得到从机的应答 从机释放SDA 主机控制SDA 发送寄存器地址(8位)

    • 因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  5. 主机释放SDA 接收从机的应答位 从机控制SDA :

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
  6. 主机得到从机的应答 从机释放SDA 主机控制SDA 发送数据(8位)

    • 因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  7. 主机释放SDA 接收从机的应答位 从机控制SDA :

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
  8. 主机得到从机的应答 从机释放SDA 主机控制SDA 产生停止条件

    • 因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的
    • 数据变化:在SCL低电平期间,SDA进行数据变化
      • 如果主机想结束通讯 就可以产生停止条件 在停止条件之前 ,先拉低SDA,为后续SDA的上升沿做准备
      • 如果主机想要继续传输 就可以继续发送数据 它的数据会自动写入下一个寄存器地址的位置(单独的记录地址的指针变量会自增 在下文当前地址读中有解释为什么)
    • 数据稳定:在SCL高电平期间,SDA保持不动
  9. 在SCL高电平的期间,SDA上升沿触发 产生停止位 转向 总线空闲状态

当前地址读

对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)

I2C_ReadPresent

  1. 总线由空闲状态转为起始位:在SCL高电平的期间,SDA下降沿触发

  2. 主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  3. 主机释放SDA 接收从机的应答位 从机控制SDA :

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
  4. 主机得到从机的应答 从机继续控制SDA 从机发送数据(8位) -- 数据传输方向 变换

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

    • 那么问题来了 -- 此时从机是从将哪个寄存器的数据 传给主机的呢? 在I2C协议的规定中,在主机进行寻址时,一旦读写标志位给了1,下一个字节要立马转为读的时序,所以主机还来不及指定,我要读那个寄存器,就要开始接收数据了,所以这里没有指定地址这个环节,那从机该发哪一个寄存器的数据呢?

      在从机中,所有的寄存器都被分配到了一个线性区域中,并且会有一个单独的记录地址的指针变量,指示着其中的一个寄存器,这个指针上电默认指向0地址,并且每写入一个字节和读出一个字节后,这个指针会自动自增一次,移动到下一个位置

      那么在调用当前地址读的时序时,主机没有指定要读那个地址,从机就会返回当前 记录地址的那个指针变量的 指针指向的寄存器的值,举例

      1. 上一步刚刚调用了指定地址写的时序,在0X19的位置写入了0XAA,那么 记录地址的那个指针变量 就会自动加一 移动到 0X1A的位置
      2. 之后再调用当前地制读的时序,读取的就是0X1A这个地址的寄存器中的值
      3. 再继续读数据,读取的就是0X1B这个地址的寄存器中的值
      4. ···
      5. 以此类推
  5. 从机释放SDA 接收主机的应答位 主机控制SDA 产生停止条件

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当从机释放SDA后 由于主机是非应答 所以总线依旧处于上拉状态 所以在SA(Send Ack)处显示的像未被释放过一样
    • 当从机未得到主机的应答时,从机将不会再继续发送给主机数据,由主机控制SDA
    • 数据变化:在SCL低电平期间,SDA进行数据变化
      • 如果主机想要结束通讯 就可以产生停止条件 在停止条件之前 ,先拉低SDA,为后续SDA的上升沿做准备
      • 如果主机想要继续读取 上面就得应答从机 然后可以继续读取下一个寄存器地址中的数据
    • 数据稳定:在SCL高电平期间,SDA保持不动
  6. 在SCL高电平的期间,SDA上升沿触发 产生停止位 转向 总线空闲状态

指定地址读

对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

I2C_ReadReg

  1. 总线由空闲状态转为起始位:在SCL高电平的期间,SDA下降沿触发

  2. 主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  3. 主机释放SDA 接收从机的应答位 从机控制SDA :

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
  4. 主机得到从机的应答 从机释放SDA 主机控制SDA 发送寄存器地址(8位)

    • 因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  5. 主机释放SDA 接收从机的应答位 从机控制SDA :

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
  6. 主机得到从机的应答 从机释放SDA 主机控制SDA 重新起始SR(Start Repeat)

    • 因为从机要在低电平尽快变化数据(释放SDA),所以SCL的下降沿和SDA的上升沿几乎是同时发生的
    • 数据变化:在SCL低电平期间,SDA进行数据变化
    • 产生重新起始条件,从机释放SDA后,主机依旧保持SDA上拉状态,为后续SDA的下降沿做准备
    • 那么问题来了 -- 重新起始是什么鬼?为什么这么操作,上文说过 有一个单独的记录地址的指针变量 我们先像那个指定的寄存器地址进行写入操作(但不真正的写入 所以指针不会自增),重新起始,然后进行当前地址读操作,这样不就完美的实现了 指定地址读操作嘛 看不懂去看当前地址读中的举例
      • 重新起始相当于另起一个时序,因为读写标志位只能是跟着起始条件的第一个字节,因此想要切换读写操作,只能再启动一次时序即重新起始
    • 起始位:在SCL高电平的期间,SDA下降沿触发
  7. 主机控制SDA 发送从机设备地址(7位地址) + 发送 读(1)/写(0)指令(1位):

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  8. 主机释放SDA 接收从机的应答位 从机控制SDA :

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当主机释放SDA后 立刻被从机拉下并控制 所以在RA(Receive Ack)处显示的像未被释放过一样
  9. 主机得到从机的应答 从机继续控制SDA 从机发送数据(8位) -- 数据传输方向 变换

    • 数据变化:在SCL低电平期间,SDA进行数据变化

    • 数据稳定:在SCL高电平期间,SDA保持不动

  10. 从机释放SDA 接收主机的应答位 主机控制SDA 产生停止条件

    • 从机回复 0是应答 1为非应答
    • 由于线与机制,当从机给予应答,从总线的现象上看 当从机释放SDA后 由于主机是非应答 所以总线依旧处于上拉状态 所以在SA(Send Ack)处显示的像未被释放过一样
    • 当从机未得到主机的应答时,从机将不会再继续发送给主机数据,由主机控制SDA
    • 数据变化:在SCL低电平期间,SDA进行数据变化
      • 如果主机想结束通讯 就可以产生停止条件 在停止条件之前 ,先拉低SDA,为后续SDA的上升沿做准备
      • 如果主机想要继续读取 上面就得应答从机 然后可以继续读取下一个寄存器地址中的数据
    • 数据稳定:在SCL高电平期间,SDA保持不动
  11. 在SCL高电平的期间,SDA上升沿触发 产生停止位 转向 总线空闲状态

STM32之I2C外设

I2C的外设应用

MPU6050的介绍

MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角,常应用于平衡车、飞行器等需要检测自身姿态的场景

  • 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度

  • 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度

MPU6050

MPU6050的参数:

  • 16位ADC采集传感器的模拟信号,量化范围:-32768~32767

  • 加速度计满量程选择:±2、±4、±8、±16(g)

  • 陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec)

  • 可配置的数字低通滤波器

  • 可配置的时钟源

  • 可配置的采样分频

  • I2C的地址

    • 1101000(AD0=0)
    • 1101001(AD0=1)

未待完续之更详细的MPU6050的笔记

I2C的实战演习

为什么分 软件读写 和 硬件读写 ?

  • 软件读写 是完全利用I2C的基本原理 时序来写的 ,也就是说无论使用那个 GPIO口都可以实现本操作,甚至你可以使用其它型号的MCU都可以的,只要逻辑和软件读写的一样,当然还要看一下双方支持最大的引脚接收翻转的频率
  • 硬件读写 因为在这里学习的是STM32单片机,而且它内部已经拥有I2C外设,这样很方便快捷我们的操作,所以在了解I2C是什么的基础上我们学习STM32的I2C外设会更加快速方便助我们使用 -- 库函数

软件模拟I2C之MPU6050

MyI2C.h

#ifndef  __MYI2C_H__//如果没有定义了则参加以下编译
#define  __MYI2C_H__//一旦定义就有了定义 所以 其目的就是防止模块重复编译

#include "stm32f10x.h"
#include "Delay.h"

void MyI2C_W_SCL(uint8_t BitValue);
void MyI2C_W_SDA(uint8_t BitValue);
uint8_t MyI2C_R_SDA(void);
void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);

#endif  //结束编译

MyI2C.c

#include "MyI2C.h"
/*
  PB10 -- SCL
  PB11 -- SDA
*/
//以下这些引脚操作 可使用带参宏定义
/**
  * @brief  主机发送时钟SCL
  * @param  
  * @retval 
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);//如果单片机主频比较快 可增加引脚延时 
}
/**
  * @brief  主机发送数据SDA -- 按位 每次都是改变GPIO的高低电平
            BitValue 即使传入的是0X80 -- (BitAction)转成1 传入0X00 -- (BitAction)转成0 
  * @param  
  * @retval 
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}
/**
  * @brief  主机接收数据SDA -- 按位 每次都是GPIO接收外部电平的变化
  * @param  
  * @retval 
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}
/**
  * @brief  I2C引脚初始化
  * @param  
  * @retval 
  */
void MyI2C_Init(void)
{
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

    //为初始化函数做准备
  GPIO_InitTypeDef GPIO_InitStructure;//定义结构体
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10|GPIO_Pin_11;//设置PB10,PB11引脚
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD ;//设置输出模式为开漏输出(也是可以输入的 先输出1 再读取输入数据寄存器就可)
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz ;//设置输出速度为50MHZ
  //初始化函数↓
  GPIO_Init(GPIOB,&GPIO_InitStructure);//初始化
  GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);//初始化为高电平 即总线空闲状态
}

/**
  * @brief  I2C起始条件 --  SCL高电平期间,SDA从 高电平 切换到 低电平
  * @param  
  * @retval 
  */
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);//SDA在前 为了确保SDA上升沿
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);//起始条件后 将SCL拉低 拼接发送发送数据格式
}
/**
  * @brief  I2C停止条件 -- SCL高电平期间,SDA从 低电平 切换到 高电平
  * @param  
  * @retval 
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);//SDA在前 为了确保SDA下降沿
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}
/**
  * @brief  I2C发送字节 -- SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,
            从机将在SCL高电平期间读取数据位
  * @param  
  * @retval 
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));//按位操作 依次取最高位 -- 第一个对应上 起始条件的SCL低电平
		MyI2C_W_SCL(1);//高电平期间发送数据
		MyI2C_W_SCL(0);//低电平期间数据变换
	}
}
/**
  * @brief  I2C接收字节 -- SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,
            主机将在SCL高电平期间读取数据位
  * @param  
  * @retval 
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);//为了防止主机干扰从机的数据发送 主机将开启并保持高阻态 总线SDA只能由从机控制 也就相当于开启了输入模式
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SCL(1);//①接受完应答位后 先把时钟线拉高 来读取从机发送过来的数据 -- 高电平期间接收数据
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//那个字节是 就把 那一位置1 高位先行
		MyI2C_W_SCL(0);//低电平期间数据变换
	}
	return Byte;
}
/**
  * @brief  I2C主机发送应答 -- 数据0表示应答,数据1表示非应答
  * @param  
  * @retval 
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);//是否应答
	MyI2C_W_SCL(1);//高电平期间发送数据
	MyI2C_W_SCL(0);//拉低SCL
}
/**
  * @brief  I2C主机接收应答 -- 数据0表示应答,数据1表示非应答
  * @param  
  * @retval 
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;
	MyI2C_W_SDA(1);//主机释放SDA 从机控制SDA
	MyI2C_W_SCL(1);//高电平期间接收数据
	AckBit = MyI2C_R_SDA();//接收从机发送过来的应答位
	MyI2C_W_SCL(0);//拉低SCL
	return AckBit;//返回 从机是否应答
}

可以在主函数中使用下面的代码 来检测I2C的代码逻辑是否有误 没有则 MPU6050会传给你一个0应答

  OLED_Init();
  MyI2C_Init();
  MyI2C_Start();
  MyI2C_SendByte(0XD0);
  uint8_t ACK = MyI2C_ReceiveAck();
  OLED_ShowNum(1,1,ACK,2);

MPU6050.h

#ifndef  __MPU6050_H__//如果没有定义了则参加以下编译
#define  __MPU6050_H__//一旦定义就有了定义 所以 其目的就是防止模块重复编译

#include "stm32f10x.h"
#include "MyI2C.h"
#define  MPU6050_ADDRESS         0xD0 //默认为写操作 1101 000 0
#define  MPU6050_SMPLRT_DIV      0x19 //采样率分频寄存器地址 -- 地址内容就是采样分频
#define  MPU6050_CONFIG          0x1A //配置寄存器 -- Bit5~3(外部同步 000不需要) Bit2~0(数字低通滤波器 -- 110最平滑的滤波)
#define  MPU6050_GYRO_CONFIG     0x1B //陀螺仪寄存器 -- Bit7~5(自测使能 000不自测)Bit4~3(满量程选择 11最大量程)后三位为无关位
#define  MPU6050_ACCEL_CONFIG    0x1C //加速度计配置寄存器 -- Bit7~5(自测使能 000不自测)Bit4~3(满量程选择 11最大量程)Bit2~0(高通滤波器 000不使用)
      
#define  MPU6050_ACCEL_XOUT_H    0x3B  //加速度计X 高8位
#define  MPU6050_ACCEL_XOUT_L    0x3C  //加速度计X 低8位
#define  MPU6050_ACCEL_YOUT_H    0x3D  //加速度计Y 高8位
#define  MPU6050_ACCEL_YOUT_L    0x3E  //加速度计Y 低8位
#define  MPU6050_ACCEL_ZOUT_H    0x3F  //加速度计Z 高8位
#define  MPU6050_ACCEL_ZOUT_L    0x40  //加速度计Z 低8位
#define  MPU6050_TEMP_OUT_H      0x41  //温度 高8位
#define  MPU6050_TEMP_OUT_L      0x42  //温度 低8位
#define  MPU6050_GYRO_XOUT_H     0x43  //陀螺仪计X 高8位
#define  MPU6050_GYRO_XOUT_L     0x44  //陀螺仪计X 低8位
#define  MPU6050_GYRO_YOUT_H     0x45  //陀螺仪计Y 高8位
#define  MPU6050_GYRO_YOUT_L     0x46  //陀螺仪计Y 低8位
#define  MPU6050_GYRO_ZOUT_H     0x47  //陀螺仪计Z 高8位
#define  MPU6050_GYRO_ZOUT_L     0x48  //陀螺仪计Z 低8位
                                 
#define  MPU6050_PWR_MGMT_1      0x6B   //电源管理 --设备复位(不复位),睡眠模式(0解除睡眠),循环模式(0不循环),无关位(给0),温度传感器(0不失能),时钟(000选择内部时钟 001陀螺仪时钟)
#define  MPU6050_PWR_MGMT_2      0x6C   //电源管理 --(前两位)循环模式和唤醒频率(00不需要) 后6位每一个轴的待机位(全给0 不需要待机)
#define  MPU6050_WHO_AM_I        0x75   //查询芯片ID号 -- 0X68  0 110 1000 其实就是7地址

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.c

#include "MPU6050.h"

//此代码优化处 可处理是否应答了 
/**
  * @brief  指定地址写 -- 对于指定设备(Slave Address),在指定地址(Reg Address)下,写入指定数据(Data)
  * @param  
  * @retval 
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
  MyI2C_Start();
  MyI2C_SendByte(MPU6050_ADDRESS);//指定设备·写
  MyI2C_ReceiveAck();
  MyI2C_SendByte(RegAddress);//指定地址
  MyI2C_ReceiveAck();
  
  //想要写入多个字节 可以 在把下面两行代码 加入一个for循环 然后参数输入一个数组即可
  MyI2C_SendByte(Data);//写入数据 1字节
  MyI2C_ReceiveAck();
  
  MyI2C_Stop();
}
/**
  * @brief  指定地址读 -- 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
  * @param  
  * @retval 
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
  uint8_t Data;

  MyI2C_Start();
  MyI2C_SendByte(MPU6050_ADDRESS);//指定设备·写
  MyI2C_ReceiveAck();
  MyI2C_SendByte(RegAddress);//指定地址
  MyI2C_ReceiveAck();

  MyI2C_Start();//重新起始
  MyI2C_SendByte(MPU6050_ADDRESS | 0x01);////指定设备·读
  MyI2C_ReceiveAck();
  //想要写入多个字节 可以 在把下面代码 加入一个for循环 读出来的数据保存到一个数组内
  //同时要改为 应答从机 MyI2C_SendAck(0); 在读完最后一个 不应答 MyI2C_SendAck(1);
  Data = MyI2C_ReceiveByte();//读取数据 1字节
 
  MyI2C_SendAck(1);
  MyI2C_Stop();
  return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);    //电源管理1
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);    //电源管理2
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);    //采样率分频 
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);        //配置寄存器 
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);   //陀螺仪寄存器 
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);  //加速度计配置寄存器
}
/**
  * @brief  获取MPU6050的ID号
  * @param  
  * @retval 
  */
uint8_t MPU6050_GetID(void)
{
  return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
/**
  * @brief  获取加速度计,陀螺仪数据
  * @param  
  * @retval 
  */
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;

  DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
  DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
  *AccX = (DataH << 8) | DataL;//加速度计X  16位的数据 PS:虽然DataH是8位的 然后左移8位 由于运算时计算结果并不存储在data变量中  所以最后赋值给16位的也没什么影响

  DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
  DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
  *AccY = (DataH << 8) | DataL;//加速度计Y

  DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
  DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
  *AccZ = (DataH << 8) | DataL;//加速度计Z

  DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
  DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
  *GyroX = (DataH << 8) | DataL;//陀螺仪X

  DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
  DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
  *GyroY = (DataH << 8) | DataL;//陀螺仪Y

  DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
  DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
  *GyroZ = (DataH << 8) | DataL;//陀螺仪Z
}

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"
#include "MPU6050.h"
uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;

int main(void)
{
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
  OLED_Init();
  MPU6050_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);
      OLED_ShowSignedNum(2, 1, AX, 5);
      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);
  }
}

硬件读写I2C之MPU6050