STM32——软硬件I2C

目录

#I2C通信协议#

一、I2C通信

[1.1 I2C简介](#1.1 I2C简介)

[1.2 硬件电路(硬件规定)](#1.2 硬件电路(硬件规定))

[1.3 I2C时序基本单元(软件规定)](#1.3 I2C时序基本单元(软件规定))

[1.3.1 起始条件S:](#1.3.1 起始条件S:)

[1.3.2 终止条件P:](#1.3.2 终止条件P:)

[1.3.3 发送一个字节:主机发送数据,从机读取数据](#1.3.3 发送一个字节:主机发送数据,从机读取数据)

[1.3.4 接收一个字节:从机发送数据,主机读取数据](#1.3.4 接收一个字节:从机发送数据,主机读取数据)

[1.3.5 发送应答(发送一位):](#1.3.5 发送应答(发送一位):)

[1.3.6 接收应答(接收一位):](#1.3.6 接收应答(接收一位):)

*解释:

*问题:

为什么应答0(低电平)是ACK?为什么能看到?

为什么设计成低电平是ACK?

1.3.7总结一下应答过程(主机发送数据给从机为例):

[1.4 I2C完整时序(时序基本单元拼接)](#1.4 I2C完整时序(时序基本单元拼接))

[1.4.1 核心前提:](#1.4.1 核心前提:)

[1.4.2 指定地址写(Random Write/Byte Write)](#1.4.2 指定地址写(Random Write/Byte Write))

[1.4.3当前地址读(Current Address Read)](#1.4.3当前地址读(Current Address Read))

[1.4.4指定地址读(Random Read/Sequential Read)](#1.4.4指定地址读(Random Read/Sequential Read))

1.4.5总结

#MPU6050外设#

二、MPU6050外设

[2.1 MPU6050简介](#2.1 MPU6050简介)

[2.2 MPU6050参数](#2.2 MPU6050参数)

[2.3 硬件电路](#2.3 硬件电路)

2.4MPU6050框图

2.5重要寄存器了解

2.5.1寄存器一览表

2.5.2关键寄存器详解

三、软件I2C读写MPU6050

[3.1 I2C代码](#3.1 I2C代码)

3.1.1程序中疑惑的点

3.2MPU6050.c代码

[3.3 main.c代码](#3.3 main.c代码)

3.4MPU6050硬件初始化代码

3.4.1MPU6050相关寄存器地址的宏定义

3.4.2MPU6050.c硬件代码初始化

3.4.3main.c代码

四、I2C外设

[4.1 I2C外设简介](#4.1 I2C外设简介)

[4.2 I2C外设模块框图](#4.2 I2C外设模块框图)

[4.3 I2C外设基本结构](#4.3 I2C外设基本结构)

4.4硬件I2C操作流程

4.4.1主机发送(指定地址写)

4.4.2主机接收(当前地址读)

4.4.3过程总结:

五、软硬件I2C的对比

5.1软件I2C优缺点

5.1.1优点

5.1.2缺点

[5.2硬件I2C 优缺点](#5.2硬件I2C 优缺点)

5.2.1优点

5.2.2缺点

5.3如何选择

六、硬件I2C读写MPU6050

6.1硬件I2C控制MPU6050

6.2硬件I2C相关库函数

6.3MPU6050.c

6.4代码出现的问题


#I2C通信协议#

一、I2C通信

1.1 I2C简介

  • I2C通信目的:

通过软件I2C通信,对MPU6050芯片内部寄存器进行读写。

写入配置寄存器,可以对外挂模块进行配置;

读出数据寄存器,可以获取外挂模块的数据。

  • I2C通信实现:

芯片内的众多外设,通过读写寄存器控制运行(寄存器本身也是存储器的一种),芯片内的所有寄存器被分配到一个线性的存储空间,若想读写寄存器控制硬件电路,至少需要定义俩个字节数据;

一个字节:指定哪一个寄存器(指定寄存器的地址)

一个字节:地址下存储器存储的内容

写入内容是控制电路;读出内容是获取电路状态

流程与51单片机CPU操作外设原理一致


但:51单片机读写自己寄存器,可直接通过内部数据总线实现,直接用指针操作即可;

而:模块寄存器在STM32单片机外面,不能直接把单片机内部数据总线扯出与芯片结合,因此需要设计通信协议,连接少量电线,实现单片机读写外部模块寄存器功能


实现思路:

通过串口HEX数据包通信。定义3个字节数据包,从单片机向外挂模块发送。第一个字节:读写,读1写0;第二个字节:地址;第三个字节:数据。若发送数据包0x00,0x06,0xAA:在0x06地址写入0xAA,模块接收后执行写入操作;若发送数据包0x01,0x06,0x00:读取0x06地址下的数据,第三个字节无效,模块接收后,再发送一个字节,返回0x06地址下的数据即可。


缺点:

①串口为两根通信线的全双工协议,但是需要的只是一根信号线的双向写入或者读取数据,如此一根通信线会处于空闲状态,浪费资源;

②没有应答机制,每发送/接收一个字节,需要一个应答;

③一根通信线无法同时接多个模块;

④串口是异步时序,对于传输速率严格,容易在传输出错。


改良:

使用同步协议,加时钟线指导读写,减少对硬件电路的依赖


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

  • 两根通信线:SCL(Serial Clock串行时钟线)、SDA(Serial Data串行数据线)

  • 同步,半双工

  • 带数据应答

  • 支持总线挂载多设备(一主多从、多主多从)

  • 使用I2C协议的模块

①MPU6050模块,可进行姿态测量

②OLED模块,可显示字符图片等信息

③AT24C02存储器模块,可存储数据

④DS3231实时时钟模块


I2C如何实现功能?硬件和软件上如何规定?

1.2 硬件电路(硬件规定)

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

  • 设备的SCL和SDA均要配置成开漏输出模式(可输出引脚电平,高电平为高阻态,低电平接VSS)

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

  • 一主多从模式:

CPU(单片机)为总线主机,任何时候对SCL完全控制,空闲状态时可主动对SDA控制,只有在从机发送数据和从机应答时,主机才会转交SDA控制权给从机。

从机权利小,在任何时刻对SCL时钟线被动读取,不允许主动对SDA数据线控制,只有主机发送读取从机命令后或者从机应答时,从机才可短暂获取SDA控制权。

1.3 I2C时序基本单元(软件规定)

**空闲状态:**SCL和SDA由外挂上拉电阻拉高至高电平状态

主机产生起始和终止条件

1.3.1 起始条件S:

SCL高电平期间,SDA从高电平切换到低电平

从机捕获SCL高电平,SDA下降沿信号时,进行自身复位,等待主机召唤;在SDA低电平状态,主机将SCL拽到低电平方便占用总线和拼接时序基本单元

1.3.2 终止条件P:

SCL高电平期间,SDA从低电平切换到高电平

SCL先回弹至高电平,SDA再回弹至高电平,产生上升沿触发终止条件,之后SDA和SCL都是高电平,回归空闲状态

1.3.3 发送一个字节:主机发送数据,从机读取数据

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

发送一个字节,主机发送,整个时序中SDA和SCL全权主机掌控,从机被动读取。

①最开始,SCL低电平 ,主机发送0拉低SDA至低电平,发送1放手SDA回弹至高电平(主机将数据放在SDA上(在SCL低电平期间,允许改变SDA电平)
②当SDA高/低电平稳定,主机松手时钟线,SCL回弹至高电平,在SCL高电平 期间,从机读取SDA (一般在SCL上升沿时刻,从机已经读取完成),因此SDA不允许变化
③SCL处于高电平一段时间后,主机继续拉低SCL,SCL低电平 ,传输下一位,主机 需要在SCL下降沿之后把数据放在SDA上(主机对SCL有控制权,因此只需要在SCL低电平任意时刻把数据放在SDA上即可)
④数据放在SDA之后,主机再次松手SCL,SCL回弹至高电平,从机读取SDA上数据,之后,主机拉低SCL至低电平,将数据放在SDA上,主机松开SCL,从机读取SDA数据......
⑤在SCL同步下,依次进行主机发送和从机接收,循环8次,发送8位数据(一个字节)
高位先行,因此,第一位是一个字节的最高位B7,最后发送最低位B0(与串口不同:低位先行)
⑦由于时钟线SCL进行同步,若主机一个字节发送一半,SDA和SCL电平不变,传输暂停。

1.3.4 接收一个字节:从机发送数据,主机读取数据

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

(主机在接收之前,需要释放SDA至高电平)

①释放SDA=切换成输入模式(所有设备包括主机都始终处于输入模式,当主机需要发送时,主动拉低SDA;当主机被动接收时,必须先释放SDA,避免影响其他发送进程)

②总线是线与特征:任何一个设备拉低,总线为低电平


SCL全程由主机控制,SDA主机在接收前释放交由从机控制

发送一个字节:SCL低电平从机放数据 ,SCL高电平主机读数据

①主机接收之前释放SDA至高电平,从机获取SDA控制权,从机拉低SDA发送0,放手SDA发送1

②由于SCL时钟是主机控制,因此从机数据变换基本上在SCL下降沿进行,主机在SCL高电平任意时刻读取

1.3.5 发送应答(发送一位):

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

接收一个字节之后,给从机发送一个应答位,用来判断从机是否继续发送。若主机应答,从机继续发送;若主机不应答,从机释放SDA,交出SDA控制权,以便主机之后的操作

1.3.6 接收应答(接收一位):

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

发送一个字节之后,紧跟着接收应答的时序,用来判断从机是否收到数据。若从机收到数据,在应答位(主机释放SDA时)从机把SDA下拉,在SCL高电平期间,主机读取应答位(0接收到,1未接受到)


*解释:

①SCL低电平:发送ACK应答,从机就下拉SDA线到低电平;如果从机发送NACK非应答,从机就释放SDA线(让上拉电阻拉高)

②SCL高电平:关键采样时刻,刚刚释放SDA的主机在这个期间读取SDA状态:

SDA低电平0:应答ACK

SDA高电平1:非应答NACK


*问题:
为什么应答0(低电平)是ACK?为什么能看到?
  1. 特定时刻: 你担心"如果SDA数据为0,应答不就看不到了吗?" 这个担心混淆了数据位应答位发生的时间点。在传输8位数据时,SDA上的0确实是数据0。但在第9个时钟脉冲的高电平期间,SDA上的0有完全不同的含义,它不是数据位,它是专门的应答信号位。发送器知道现在是在读应答,而不是数据。

  2. 角色反转与强制拉低: 在第9个时钟脉冲期间,主机主动释放了SDA线(让它默认是高)。如果从机想要ACK,它必须主动地、用力地把SDA线拉低到0。这个动作是强制的、明确的。

①主机释放SDA -> SDA 默认 变高 (1 - 表示NACK)。

②从机想要ACK -> 从机主动拉低 SDA -> SDA 变低 (0)。

③主机在第9个SCL高电平时,看到SDA是低 (0),就知道:"哦,从机用力拉低了线,它在明确地告诉我ACK!" 它不会把这个0误认为是之前的数据0,因为它知道现在是在应答时隙。

④如果从机什么都不做(释放SDA),上拉电阻会把SDA拉高(1),发送器就解读为NACK。


为什么设计成低电平是ACK?

①物理实现简单: 在开漏输出的I2C总线 上,拉低信号(产生一个强制的0)比驱动一个强制的1更容易、更可靠(靠上拉电阻自然回高就是1)。从机只需在需要ACK时短暂地"接地"一下(拉低)即可。

②明确性: 低电平是一个"主动动作"(需要器件驱动),而高电平是"被动状态"(靠电阻拉回)。用主动动作来表示"确认收到"更符合逻辑。


1.3.7总结一下应答过程(主机发送数据给从机为例):

1.主机发送完8位数据(控制SCL,在SCL低时设置SDA)。

2.主机产生第9个SCL脉冲(低电平)。

3.主机释放SDA线(内部切换到输入模式,不再驱动SDA,SDA靠上拉电阻变高)。

4.从机(作为接收方)在SCL低电平时:

①如果成功收到字节 -> 拉低SDA线 (准备发送ACK)。

②如果没收到/出错/不是地址 -> 不拉低SDA线 (释放它,让它保持高,准备发送NACK)。

5.SCL线被主机拉高(第9个脉冲的高电平)。

6.主机在这个SCL高电平期间读取SDA线:

①读到 0 (低电平) -> 从机拉低了,ACK!继续传输。

②读到 1 (高电平) -> 从机没拉低,NACK!停止传输(可能重试或报错)。

7.主机拉低SCL,为下一次传输(或停止条件)做准备。

8.从机释放SDA线(如果它之前拉低了的话)。


1.4 I2C完整时序(时序基本单元拼接)

1.4.1 核心前提:

1**.**主机: 发起通信、控制时钟(SCL)的设备(通常是 STM32)。

2.**从机:**响应主机请求的设备(如 EEPROM、传感器等),有唯一的 7 位或 10 位地址。

  1. 地址字节: 主机发送的第一个字节总是地址字节。它包含:

从机地址 (7位或高8位中的7位): 指定要和哪个从机通信。

读写位 (1位,最低位 - LSB):

0: 表示主机写数据到从机 (Write)。

1: 表示主机要从从机读数据 (Read)。

4.ACK/NACK: 每个字节(包括地址字节和每个数据字节)传输后,接收方必须发送一个 ACK (0) 或 NACK (1) 信号。

5.起始 (S) 和停止 (P) 条件: 由主机产生,标志通信的开始和结束。

1.4.2 指定地址写(Random Write/Byte Write)

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


  • 目的: 将数据写入从机存储空间的某个特定地址。

  • 时序步骤:

1.主机产生 START 条件 (S)。

2.主机发送地址字节 (Address Byte):

  • 7 位从机地址 + 写位 (0) 。告诉目标从机 :"我要向你写数据!"

  • 从机收到地址匹配且是写操作,在第9个时钟脉冲 发送 ACK (0)

3.主机发送内存地址字节 (Memory Address Byte):

  • 告诉从机 :"我要把数据写到你的哪个位置?" (例如 EEPROM 的 0x0050)。

  • 从机收到地址字节,发送 ACK (0)。 (如果地址空间大,可能需要2个地址字节,每个后都有ACK)。

4.主机发送数据字节 (Data Byte):

  • 发送要写入该指定地址的数据

  • 从机收到数据字节,发送 ACK (0)

5.(可选) 主机可以继续发送更多数据字节:

如果从机支持顺序写 (Sequential Write),内部地址指针会自动递增,下一个数据字节会写到下一个地址。 每个数据字节后从机都发送 ACK。

6.主机产生 STOP 条件 (P):

结束写操作。从机收到 STOP 后,通常会开始将数据写入非易失存储器(如 EEPROM),此时总线空闲。

  • 通俗理解: 就像寄快递

①S起始条件: 你(主机)拿起电话打给快递公司(总线)。

②地址字节(写): 你报出仓库(从机)的地址和说"我要寄件"(写位0)。仓库确认收到请求(ACK)。

③内存地址字节: 你告诉仓库管理员"请把这个包裹存到A区3号货架"(特定地址)。管理员确认位置有效(ACK)。

④数据字节: 你把包裹(数据)交给管理员。管理员签收(ACK)。

⑤P: 你挂断电话。管理员开始把包裹存放到指定货架。

对于指定从机地址为1101000的设备,在其内部0x19地址寄存器中,写入0xAA数据

1.4.3当前地址读(Current Address Read)

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


  • 目的: 从从机内部地址指针当前指向的位置读取数据。这个指针通常是上次读写操作完成后的位置 + 1

  • 时序步骤:

1.主机产生 START 条件 (S)。

2.主机发送地址字节 (Address Byte):

  • 7 位从机地址 + 读位 (1)。告诉目标从机 :"我要从你那里读数据!"

  • 从机收到地址匹配且是读操作,在第9个时钟脉冲 发送 ACK (0)

3.从机发送数据字节 (Data Byte):

  • 从机立即从它内部地址指针当前指向的位置读取一个字节的数据 ,并通过 SDA 线发送给主机。

  • 主机收到数据字节后,决定是否继续读取:

  • 如果还想读下一个字节,主机在第9个时钟脉冲发送 ACK (0) 。从机看到 ACK,会自动递增内部地址指针 ,并发送下一个地址的数据。

  • 如果这是最后一个字节或不想再读,主机在第9个时钟脉冲发送 NACK (1)。告诉从机:"够了,不用再发了"。

4.主机产生 STOP 条件 (P):

结束读操作。

  • 通俗理解: 就像去图书馆按顺序借书:

①S: 你(主机)走进图书馆(总线)。

②地址字节(读): 你出示借书证(从机地址)说"我要借书"(读位1)。管理员确认是你(ACK)。

③数据字节: 管理员(从机)直接从当前书架上(内部指针位置)拿下一本书(数据)递给你。

④主机ACK/NACK:

你接过书说"我还要下一本"(ACK),管理员就去拿下一本书(指针自动+1)。

你接过书说"就这些了"(NACK),管理员就不拿了。

⑤P: 你带着借到的书离开图书馆(结束通信)。下次你来借书,管理员会默认从这次结束后的下一本书开始拿(指针已更新)。

1.4.4指定地址读(Random Read/Sequential Read)

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


  • 目的: 从从机存储空间的某个特定地址开始读取一个或多个数据。这是最常用也稍微复杂一点的读操作。

  • 时序步骤:

1.主机产生 START 条件 (S1)。

2.主机发送地址字节 (Address Byte - Write):

  • 7 位从机地址 + 写位 (0)。告诉从机 :"我要先'写'点东西给你!" (这里写的是从机地址+写操作0)

  • 从机收到地址匹配且是写操作, 发送 ACK (0)。

3.主机发送内存地址字节 (Memory Address Byte):

  • 告诉从机:"我接下来想从你的哪个位置开始读数据?" (例如 EEPROM 的 0x0050)。(从机内部地址指针)

  • 从机收到地址字节, 发送 ACK (0)。 (同样,地址可能不止1字节)。

4.主机产生一个重复起始条件 (Sr / Repeated Start):

主机不产生 STOP 条件,而是再次产生一个 START 条件。这非常关键!它告诉总线:"通信没完,但我现在要切换方向了(从写切换到读)",同时保持总线占用权,避免其他主机抢走总线。

5.主机再次发送地址字节 (Address Byte - Read):

  • 7 位从机地址 + 读位 (1) 。告诉从机:"好了,现在请从刚才我告诉你的那个位置开始,把数据读给我吧!"(读取从机地址内部地址指针)

  • 从机收到地址匹配且是读操作, 发送 ACK (0)

6.从机发送数据字节 (Data Byte):

  • 从机从步骤3 指定的内存地址读取一个字节的数据发送给主机。

  • 主机收到数据字节后,决定是否继续读取:

  • 如果还想读下一个字节,主机在第9个时钟脉冲发送 ACK (0)。从机看到 ACK,会自动递增内部地址指针,并发送下一个地址的数据。

  • 如果这是最后一个字节或不想再读,主机在第9个时钟脉冲发送 NACK (1)。

7.主机产生 STOP 条件 (P):

结束整个读操作。

  • 通俗理解: 就像去图书馆指定借某本书,并可能续借后面的:

①S1: 你(主机)走进图书馆(总线)。

②地址字节(写): 你出示借书证(从机地址)说"我要填个索书单"(写位0)。管理员确认是你(ACK)。

③内存地址字节: 你在索书单上写下"我想借《STM32指南》第3章"(特定地址)递给管理员。管理员确认书在馆(ACK)。

④Sr: 你立刻对管理员说:"等等,别走开!我现在就要借这本书!" (重复起始,切换方向)。

⑤地址字节(读): 你再次出示借书证说"请把刚才我要的那本书给我"(读位1)。管理员确认(ACK)。

⑥数据字节: 管理员找到《STM32指南》第3章(指定地址)递给你。

⑦主机ACK/NACK:

  • 你接过书说"我还要下一章"(ACK),管理员就把第4章(指针+1)也递给你。

  • 你接过书说"就这些了"(NACK),管理员就不拿了。

⑧P: 你带着借到的书离开图书馆(结束通信)。

1.4.5总结

|-----------|----------------|------------------|---------------|--------------|--------------|
| 操作 | 关键目的 | 第一个地址字节方向 | 需要发送内存地址? | 需要重复起始 ? | 主机在数据后回应 |
| 指定地址写 | 写数据到特定地址 | 写 (0) | | | (主机是发送方) |
| 当前地址读 | 从上次位置+1读数据 | 读 (1) | | | ACK/NACK |
| 指定地址读 | 从特定地址开始读数据 | 先写(0) 后读 (1) | 是 (在写阶段) | | ACK/NACK |

  • ACK/NACK 无处不在: 地址字节后、内存地址字节后、每一个数据字节后(无论是主机发送还是从机发送),接收方都必须发送 ACK 或 NACK。这是 I2C 协议保证可靠性的基石。

  • 重复起始 (Sr) 是精髓: 指定地址读利用 Sr 在不释放总线控制权的情况下,无缝地从"写地址"切换到"读数据"。

  • 内部地址指针: 从机通常维护一个内部地址指针。写操作(包括指定地址读的第一步"伪写")会设置这个指针。读操作(无论是当前读还是指定读)后,如果主机回复 ACK,指针会自动递增,为顺序读做准备。

  • 顺序操作: 在指定地址写或指定地址读过程中,如果主机持续发送 ACK,可以连续写入或读取多个字节(顺序写/顺序读),地址指针会自动递增。

#MPU6050外设#

二、MPU6050外设

2.1 MPU6050简介

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

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

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

  • 集成3轴磁场传感器:测量XYZ轴磁场强度---9轴姿态传感器

  • 再次集成气压传感器:测量气压大小(反应垂直地面的高度信息)---10轴姿态传感器

2.2 MPU6050参数

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

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

量程越大精度越低(如±2g时精度0.0001g,±16g时只有0.001g)

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

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

  • 可配置的时钟源

  • 可配置的采样分频

  • MUP6050ID号:0x68(也有其它ID号)

读出ID号,可检测I2C读取数据功能是否正常

  • I2C从机地址( 二进制):1101000(AD0=0)0x68

1101001(AD0=1)0x69

从机地址表示:

①若0x68/0x69是从机地址,发送第一个字节时,需要将0x68左移一位0x68<<1再|或读写位

②若0xD0/0xD1是从机地址,发送第一个字节,直接将其作为第一个字节

2.3 硬件电路

|---------|----------------------------------|
| 引脚 | 功能 |
| VCC、GND | 电源(3.3V) |
| SCL、SDA | I2C通信引脚 |
| XCL、XDA | 主机I2C通信引脚,用于扩展芯片 (通常用于外接磁力计/气压计) |
| AD0 | 从机地址最低位(引脚悬空低电平) |
| INT | 中断信号输出 |

电源若用5V系统,需要使用LDO降压到3.3V

2.4MPU6050框图

芯片内部传感器:XYZ轴的加速度计和陀螺仪+内部温度传感器------本质为可变电阻,分压后输出模拟电压,通过ADC进行模数转换,数据存在数据寄存器。读取数据寄存器就可得到传感器测量值。

②每一个传感器都有自测单元,用来验证芯片好坏。当启动自测后,芯片内部会模拟一个外力施加在传感器上,从而导致传感器数据比平时大。如何自测?使能自测------读取数据R1------失能自测------读取数据R2,|R1-R2|=自测响应,在一定范围内,芯片完好。

电荷泵****/ 充电泵,CPOUT引脚外接一个电容。电荷泵说是升压电路(陀螺仪内部需要高电压支持)

中断状态控制器:控制内部事件到中断引脚的输出

FIFO:先入先出寄存器:对数据流进行缓存

⑥**配置寄存器:**对内部各个电路进行配置

传感器寄存器:数据寄存器,存储各个传感器数据

⑧**工厂校准:**对内部传感器进行校准

⑨**数字运动处理器DMP:**芯片内部自带的姿态解算的硬件算法,配合官方DMP库,可进行姿态解算

FSYNC帧同步

⑩①通信接口部分 :上面一部分是从机的IC和****SPI 通信接口,用于和STM32通信,AUX_CL和AUX_DA是主机的IC通信接口,用于和MPU6050扩展的设备进行通信,旁边的寄存器是接口旁路选择器=开关:上拨:总线合并,STM32控制所有;下拨:总线分开,MPU6050控制扩展设备

内部框图解析:

工作流程比喻:

①感官采集(加速度计+陀螺仪)
→ 如同皮肤感受晃动(加速度)+ 耳朵感受旋转(陀螺仪)

②信号转换(ADC)
→ 把模拟信号变成数字语言(如把"很晃"转化为具体数值)

③智能处理(DMP)
→ 内置"运动专家"自动计算姿态(省去主控80%运算)

④数据暂存(FIFO)
→ 像快递柜临时存放数据,等单片机来取

⑤对外通信(I²C)
→ 通过"SDA/SCL电线"向单片机报告结果

2.5重要寄存器了解

2.5.1寄存器一览表

|--------------------------------|-------|-------------|----------------------|-----------------------------------------------------|
| 寄存器名称 | 地址 | 核心功能 | 通俗比喻 | 关键配置项说明 |
| 采样时钟分频器(SMPLRT_DIV) | 0x19 | 控制数据刷新速度 | 传感器"眨眼频率" (数值越大反应越慢) | 分频值 = 0:默认1kHz刷新 分频值 = 9:降速到100Hz |
| 配置寄存器(CONFIG) | 0x1A | 设置数字滤波器 | 抗干扰"防抖滤镜" (数值越大画面越稳) | DLPF_CFG[2:0]: • 0=关闭(260Hz带宽) • 6=最强滤波(5Hz带宽) |
| 陀螺仪配置寄存器 (GYRO_CONFIG) | 0x1B | 设置陀螺仪量程和精度 | 旋转检测"灵敏度档位" | FS_SEL[1:0]: • 00=±250°/s(最精确) • 11=±2000°/s(最粗糙) |
| 加速度计配置寄存器(ACCEL_CONFIG) | 0x1C | 设置加速度计量程和精度 | 晃动检测"灵敏度档位" | AFS_SEL[1:0]: • 00=±2g(最精确) • 11=±16g(最粗糙) |
| 电源管理寄存器1(PWR_MGMT_1) | 0x6B | 开关传感器/选择时钟源 | 总电源开关+心脏起搏器 | SLEEP=0:唤醒 CLKSEL[2:0]=001:用X轴陀螺做时钟(最稳) |
| 电源管理寄存器2(PWR_MGMT_2) | 0x6C | 关闭指定传感器省电 | 部件独立开关 | DISABLE_ZG=1:关闭Z轴陀螺仪(省电) |
| 数据寄存器(XXXX_X/Y/ZOUT_H/L) | 0x3B起 | 存储实时检测数据 | 传感器"体检报告单" | 每轴2字节(高8位+低8位) |
| 器件ID寄存器(WHO_AM_I) | 0x75 | 验证芯片真伪/通信状态 | 身份证号码 | 固定值0x68(读不到说明接线错误) |

2.5.2关键寄存器详解

1️⃣ 采样时钟分频器 (0x19) - "数据刷新率调节器"

计算公式:实际采样率 = 1kHz (陀螺仪采样频率)/ (1 + SMPLRT_DIV)

uint8_t SMPLRT_DIV = 9; // 此时采样率 = 1000/(1+9) = 100Hz

  • 应用场景:平衡小车用100Hz足够,无人机需500Hz

2️⃣ 配置寄存器 (0x1A) - "抗干扰滤镜"

典型配置:中等滤波(带宽20Hz)

uint8_t CONFIG = 0x04; // DLPF_CFG=100

  • 滤波档位选择

    • 0:关闭滤波(响应快但易受干扰)

    • 6:最强滤波(延迟大但超稳定)


3️⃣ 陀螺仪配置 (0x1B) - "旋转灵敏度"

设置量程±500°/s(灵敏度65.5 LSB/°/s)

uint8_t GYRO_CONFIG = 0x08; // FS_SEL=01

  • 量程与精度关系

    • ±250°/s → 131 LSB/°/s(精度高)

    • ±2000°/s → 16.4 LSB/°/s(精度低)


4️⃣ 加速度计配置 (0x1C) - "震动灵敏度"

设置量程±4g(灵敏度8192 LSB/g)

uint8_t ACCEL_CONFIG = 0x08; // AFS_SEL=01


5️⃣ 电源管理1 (0x6B) - "总控开关"

唤醒设备 + 使用X轴陀螺时钟

uint8_t PWR_MGMT_1 = 0x01; // SLEEP=0, CLKSEL=001

  • 避坑指南
    必须写0x00唤醒设备! 刚上电默认休眠

6️⃣ 数据寄存器 - "体检报告单"

数据 地址 字节组合 计算公式
加速度X 0x3B,0x3C ACCEL_XOUT_H + ACCEL_XOUT_L `ax = (高8<<8
温度 0x41,0x42 TEMP_OUT_H + TEMP_OUT_L T = (原始值/340.0) + 36.53
陀螺仪Z 0x47,0x48 GYRO_ZOUT_H + GYRO_ZOUT_L `gz = (高8<<8

温度传感器妙用

检测芯片工作状态,温度每升高1℃陀螺仪漂移增加0.01°/s

三、软件I2C读写MPU6050

端口不受限,可任意指定

3.1 I2C代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "I2C.H"
//对操作端口的函数进行封装,以便修改和延时
//SCL写函数
void IIC_Write_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB ,GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}




//SDA写函数
void IIC_Write_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB ,GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}




//SDA读函数
uint8_t IIC_Read_SDA(void)
{
	uint8_t BitValue;
	BitValue=GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}




void IIC_init(void)
{
	//使用软件I2C,库函数不使用,下只需要GPIO口读写函数即可
	//SCL PB10 SDA PB11
	/*

	软件I2C初始化:
	①SCL和SDA初始化为开漏输出模式,可输出引脚电平,高电平为高阻态,低电平接VSS
	②SCL和SDA置高电平
	③I2C6个时序基本单元:起始条件;发送一个字节,接受一个字节,发送应答,接收应答;终止条件

	*/
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_OD;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
}

//起始条件
void IIC_Start(void)
{
	/*
	确保SCL和SDA都是高电平
	先拉低SDA至低电平
	再拉低SCL至低电平
	为方便修改端口号,将函数进行封装
	*/
	IIC_Write_SDA(1);
	IIC_Write_SCL(1);
	
	IIC_Write_SDA(0);
	IIC_Write_SCL(0);

}

//终止条件
void IIC_Stop(void)
{
	/*
	先拉低SDA至低电平
	再释放SCL至高电平
	再释放SDA至高电平
	*/
	IIC_Write_SDA(0);
	IIC_Write_SCL(1);
	
	IIC_Write_SDA(1);
}

//发送一个字节
void IIC_SendByte(uint8_t Byte)
{
	/*
	SCL低电平时(起始条件时SCL是低电平,方便时序连接),发送数据(高位先行)
	释放SCL高电平,从机自动读取数据
	......
	*/
//	IIC_Write_SDA(Byte&0x80);//最高位数据
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
//	IIC_Write_SDA(Byte&0x40);
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
//	IIC_Write_SDA(Byte&0x20);
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
//	IIC_Write_SDA(Byte&0x10);
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
//	IIC_Write_SDA(Byte&0x08);
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
//	IIC_Write_SDA(Byte&0x04);
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
//	IIC_Write_SDA(Byte&0x02);
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
//	IIC_Write_SDA(Byte&0x01);
//	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
//	IIC_Write_SCL(0);//继续存放下一个数据
	
	uint8_t pos;//数据位置
	for(pos=0;pos<8;pos++)
	{
		IIC_Write_SDA(Byte&(0x80>>pos));
		IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
		IIC_Write_SCL(0);//继续存放下一个数据	
	}
}

//接受一个字节
uint8_t IIC_ReadByte(void)
{
	/*
	SCL低电平时,数据放到SDA上(高位先行)
	释放SCL高电平,主机自动读取SDA数据
	主机在接收前,先释放SDA
	*/
	uint8_t Byte=0x00;
//	IIC_Write_SDA(1);//SCL是低电平,从机放数据到SDA上
//	IIC_Write_SCL(1);//主机释放SCL至高电平,主机读取数据
//	if(IIC_Read_SDA()==1)
//	{
//		Byte|=0x80;
//	  IIC_Write_SCL(0);
//	}//循环八次,与发送一个字节类似
	uint8_t pos;
	IIC_Write_SDA(1);//主机释放SDA,SCL是低电平,从机放数据到SDA上
	for(pos=0;pos<8;pos++)
	{
		IIC_Write_SCL(1);//主机释放SCL至高电平,主机读取数据
		if(IIC_Read_SDA()==1)
		{
			Byte|=(0x80>>pos);
		}
		
		IIC_Write_SCL(0);

	}
	return Byte;
}

//发送应答
void IIC_SendACK(uint8_t AckBite)
{
	IIC_Write_SDA(AckBite);
	IIC_Write_SCL(1);//驱动时钟走一个脉冲,从机将SDA上数据读走
	IIC_Write_SCL(0);//继续下一个单元
}

//接收应答
uint8_t IIC_ReceiveAck(void)
{
	uint8_t AckBite;
	IIC_Write_SDA(1);//SCL是低电平,主机释放SDA,从机放数据到SDA上
	IIC_Write_SCL(1);//主机释放SCL至高电平,主机读取数据
	AckBite=IIC_Read_SDA();
	IIC_Write_SCL(0);//进入下一个时序单元
	return AckBite;
}

3.1.1程序中疑惑的点

Q1:在程序中,主机把SDA置1,之后再读取SDA,读取的应答位不是1吗?

A:①I2C引脚是开漏输出+弱上拉配置,主机输出1,并不是强置SDA为高电平,而是释放SDA

②I2C进行通信,主机释放SDA,从机若要使用SDA,会拉低SDA:SDA=0,从机应答;SDA=1,从机未应答

Q2:在接收一个字节模块中,不断读取SDA,又不写SDA,那SDA值一直是同一个值啊?

A:I2C进行通信,主机不断驱动SCL时钟时,从机有义务去改变SDA电平,因此,每次主机循环读取SDA时,读取到的数据是从机控制的且是从机想要发送的数据

总结:进行通信,通信有主机和从机,是有时序的,有些引脚的值不同时序读出的结果是不同的

3.2MPU6050.c代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "I2C.h"
#include "MPU6050.h"
//根据指定地址写和指定地址读,拼接一个完整的时序


//指定地址写
void IIC_Random_Write_address(uint8_t addr,uint8_t Data)
{
	/*
	起始条件
	发送字节地址
	接收应答
	发送内存地址字节
	接收应答
	发送数据字节
	接收应答
	终止条件
	*/
	IIC_Start();
	IIC_SendByte(0xD0);//从机地址
	IIC_ReceiveAck();
	IIC_SendByte(addr);//睡眠模式写入寄存器无效
	IIC_ReceiveAck();
	IIC_SendByte(Data);
	IIC_ReceiveAck();
	IIC_Stop();
}
	
//指定地址读
uint8_t IIC_Random_Read_address(uint8_t addr)
{
	/*
	起始条件
	发送字节地址
	接收应答  
	发送内存地址字节
	接收应答

	起始条件
	发送字节地址
	发送应答

	发送数据字节
	发送不应答
	终止条件
	*/
	uint8_t Data;
	IIC_Start();
	IIC_SendByte(0xD0);//从机地址写
	IIC_ReceiveAck();
	IIC_SendByte(addr);//睡眠模式写入寄存器无效
	IIC_ReceiveAck();

	IIC_Start();
	IIC_SendByte(0xD1);//从机地址读
	IIC_ReceiveAck();
	Data=IIC_ReadByte();
	IIC_SendACK(1);
	IIC_Stop();
	return Data;
}


void MPU6050_Init(void)
{
	IIC_init();
}

3.3 main.c代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
//#include "I2C.h"
#include "MPU6050.h"
uint8_t ID;
int main(void)
{	
	
    OLED_Init();
	//将MPU6050当成存储器使用
	MPU6050_Init();
	//验证读寄存器功能,WHO_AM_I寄存器地址0x75
    ID=IIC_Random_Read_address(0x75);
	OLED_ShowHexNum(1,1,ID,2);
	//验证写寄存器功能,需要解除芯片睡眠模式,否则写入无效
	IIC_Random_Write_address(0x6B,0x00);
	//睡眠模式是电源管理器1的SLEEP控制,可直接写入0x00,寄存器地址是0x6B
	IIC_Random_Write_address(0x19,0xAA);//分频寄存器
	//写入是否有效?再读出地址下的数据
	uint8_t Fre=IIC_Random_Read_address(0x19);
	OLED_ShowHexNum(1,4,Fre,2);
	while(1)
	{
	}
}

3.4MPU6050硬件初始化代码

3.4.1MPU6050相关寄存器地址的宏定义

cpp 复制代码
#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

3.4.2MPU6050.c硬件代码初始化

cpp 复制代码
#include "MPU6050_Reg.h"
void MPU6050_Init(void)
{
	IIC_init();
	/*MPU6050硬件电路初始化
	①配置电源管理寄存器1和2
	②配置分频寄存器,采样分频10
	③配置寄存器
	④陀螺仪配置寄存器
	⑤加速度计配置寄存器
	*/
	IIC_Random_Write_address(MPU6050_PWR_MGMT_1,0x01);//不复位,解除睡眠,选择陀螺仪时钟
	IIC_Random_Write_address(MPU6050_PWR_MGMT_2,0x00);//循环模式唤醒频率不需要,每一个轴待机位都为0,不需要待机
	IIC_Random_Write_address(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率,这里是10
	IIC_Random_Write_address(MPU6050_CONFIG, 0x06);			//配置寄存器,外部同步不需要,数字低通滤波器110,最平滑的滤波
	IIC_Random_Write_address(MPU6050_GYRO_CONFIG, 0x18);	//陀螺仪配置寄存器,选择满量程11,为±2000°/s,,最大量程
	IIC_Random_Write_address(MPU6050_ACCEL_CONFIG, 0x18);	//加速度计配置寄存器,选择满量程11,为±16g,高通滤波器用不到00
}

//获取数据寄存器,XYZ多个返回值函数,使用指针地址传递
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	//分别读取6个轴数据寄存器的高位和低位,拼接成16位数据,再通过指针变量返回
	uint16_t DataH, DataL;//数据高8位和低8位
	
	DataH = IIC_Random_Read_address(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;//加速度计X轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;//加速度计Y轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;	//加速度计Z轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;//陀螺仪X轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;//陀螺仪Y轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;//陀螺仪Z轴的数据
}

3.4.3main.c代码

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
//#include "I2C.h"
#include "MPU6050.h"
uint8_t ID;
int16_t AX,AY,AZ,GX,GY,GZ;
int main(void)
{	
	
    OLED_Init();
	//将MPU6050当成存储器使用
	MPU6050_Init();
	//验证读寄存器功能,WHO_AM_I寄存器地址0x75
    ID=IIC_Random_Read_address(0x75);
	OLED_ShowHexNum(1,1,ID,2);
	//验证写寄存器功能,需要解除芯片睡眠模式,否则写入无效
	IIC_Random_Write_address(0x6B,0x00);
	//睡眠模式是电源管理器1的SLEEP控制,可直接写入0x00,寄存器地址是0x6B
	IIC_Random_Write_address(0x19,0xAA);//分频寄存器
	//写入是否有效?再读出地址下的数据
	uint8_t Fre=IIC_Random_Read_address(0x19);
	OLED_ShowHexNum(1,4,Fre,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外设

4.1 I2C外设简介

  • STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担(软件只需写入控制寄存器CR+数据据寄存器DR+读取状态寄存器SR)

  • 模块默认地工作于从模式。接口在生成起始条件后自动地从从模式切换到主模式:当仲裁丢失或产生停止信号时,则从主模式切换到从模式。允许多主机功能。

  • 支持多主机模型

**通俗解释:**想象这个"悄悄话"圈子里,不只STM32一个人能发起对话。房间里可能有多个"话事人"(主机),比如另一个STM32或者一个专门的I2C主控芯片。


**好处:**系统更灵活。比如,平时STM32负责问传感器数据(STM32是主机),但某个时刻,另一个设备(比如一个系统管理器芯片)需要紧急向STM32发送命令(这时管理器芯片成了主机)。


关键机制: STM32的I2C硬件能检测总线冲突(两个主机同时想说话),并有一套仲裁规则来决定谁最终能"说"下去(通常是先发低电平0的赢),失败的主机会自动退出,等下次机会。STM32硬件帮你处理了复杂的冲突判断。

  • 支持7位/10位地址模式

通俗解释: 每个想听"悄悄话"的设备(从机)都有一个"门牌号"(地址)。STM32既认识短门牌号(7位),也认识长门牌号(10位)。


7位地址: 最常用。理论上可以给 2^7 = 128 个设备编号(0-127)。但有些地址是保留的,实际可用少一些。

10位地址: 用于设备特别多的场景。理论上可以给 2^10 = 1024 个设备编号。地址需要分两次发送。


好处: 兼容市面上绝大多数I2C设备。你可以自由选择连接使用哪种地址格式的设备

  • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)

通俗解释: "悄悄话"可以说得快一点,也可以说得慢一点。STM32可以灵活设置指挥家打拍子(SCL时钟)的快慢。


常见速度:

标准模式 (Standard-mode): 最高 100 kHz。速度慢,稳定可靠,走线长点也没问题。

快速模式 (Fast-mode): 最高 400 kHz。速度较快,常用。

快速模式+ (Fast-mode Plus): 最高 1 MHz。速度更快。

高速模式 (High-speed mode): 最高 3.4 MHz(需要特定硬件支持)。速度非常快。


好处: 可以根据你连接的设备能力和实际需求(需要多快传数据)来选择合适的通讯速度。在初始化时配置一下寄存器即可。

  • 支持DMA

通俗解释: 想象STM32的CPU是老板。老板很忙,不想每次都亲自去收发每一个"悄悄话"的字(字节)。DMA就像一个能干的秘书。


DMA怎么工作:

老板(CPU)告诉秘书(DMA):去把传感器(I2C设备)说的悄悄话(数据)存到仓库(内存)的这个位置X,一共N个字。或者把仓库位置Y的M个字发给那个设备。

老板就可以去干其他重要任务了(比如算数学、控制电机)。

秘书(DMA)自动地、一个字一个字地通过I2C硬件收发数据,搬进/搬出仓库(内存)。

搬完了或者出错了,秘书(DMA)会报告老板(产生中断)。


好处: 大大减轻CPU负担! 尤其在需要传输大量数据时(比如读写大容量EEPROM),CPU可以解放出来做其他事情,提高系统效率。

  • 兼容SMBus协议

**通俗解释:**SMBus是基于I2C发展出来的一种更"严格"的"悄悄话"协议,主要用于电脑主板上的电源管理、电池监控等。它增加了一些规则(比如超时限制、特定的命令格式)。


STM32的兼容性: STM32的I2C硬件在设计时考虑了这些SMBus规则,可以通过配置启用SMBus特有的功能(如超时检测、PEC包校验)。


好处: 如果你需要连接遵循SMBus标准的设备(比如智能电池、系统管理芯片),STM32可以直接支持,无需额外模拟。

  • STM32F103C8T6 硬件I2C资源:I2C1、I2C2

4.2 I2C外设模块框图

关键部件解释(结合框图):

**SDA/SCL 引脚:**复用开漏输出模式, 物理连接线(固定),连接到外部设备的SDA/SCL线。

**SMBALERT:**SMBus使用


数据移位寄存器: 核心搬运工。它负责把CPU/DMA送来的并行数据(比如8位),一位一位地按照时钟节拍(SCL)串行送到SDA线上(发送)。反过来,把从SDA线上串行收到的一位位数据,攒够一个字节(8位)后,并行交给数据寄存器(接收)。完成串行<->并行转换。


数据寄存器 (DR): CPU或DMA读写I2C数据的窗口。要发送的数据,CPU/DMA写到这里;接收到的数据,CPU/DMA从这里读取。当移位寄存器没有移位,数据寄存器值会进一步转到移位寄存器(TXE=1:发送寄存器空),在移位过程中,可直接把下一个数据放到数据寄存器等着,一旦前一个数据移位完成,下一个数据可以无缝衔接继续发送;输入数据一位一位地从引脚移入到移位寄存器中,当一个字节收齐之后,数据整体从移位寄存器转到数据寄存器(RXNE=1,接收寄存器非空),此时可以i把数据从数据寄存器读出


地址比较器: 当STM32工作在从机模式时,它时刻监听SDA线上广播的"门牌号"(地址)。收到地址后,它和自己预先设置好的从机地址(自身地址寄存器)比较。如果匹配上了,就告诉控制逻辑:"有人叫我!",STM32就会响应这个主机。STM32支持同时响应俩个从机地址(双地址寄存器)。


控制寄存器 (CR1, CR2): CPU通过写这些寄存器来配置整个I2C部门:

  • 选择主机/从机模式

  • 设置通讯速度(时钟频率)

  • 选择7位/10位地址

  • 使能应答(ACK)

  • 使能DMA

  • 使能中断

  • 产生起始(START)/停止(STOP)信号

  • 等等... 这是你编程时打交道最多的部分!


状态寄存器 (SR1, SR2): I2C部门的工作状态报告板。CPU通过读取这些寄存器,可以知道:

  • 总线忙不忙?

  • 地址发送/接收完成没?

  • 数据发送/接收完成没?

  • 有没有收到应答(ACK)?

  • 仲裁丢失了没?

  • 发生错误没? 编程时,你需要不断查询这些状态来决定下一步做什么。


时钟产生与控制: 根据你在控制寄存器里设置的通讯速度,内部产生对应频率的SCL时钟信号(当STM32是主机时),并精确控制时钟的高低电平时间。它也负责检测总线上的时钟同步(多主机时)。


状态机: I2C通讯过程有严格的步骤(起始条件 -> 发送地址 -> 读/写位 -> 应答 -> 发送/接收数据 -> ... -> 停止条件)。状态机就像一个自动化的流程控制器,硬件根据当前状态和输入(如状态寄存器值),自动执行下一步操作,大大简化了CPU的控制逻辑。


中断/DMA控制: 当发生特定事件时(数据准备好、传输完成、收到地址、出错等),这个模块会产生中断信号给CPU,或者向DMA控制器发出请求信号,让DMA来搬运数据。这是实现高效、非阻塞通讯的关键!


CRC计算单元 (部分型号): 如果启用了SMBus的PEC(Packet Error Checking)功能,硬件会自动计算和校验数据的CRC值。

4.3 I2C外设基本结构

4.4硬件I2C操作流程

  1. 每一步操作后,必须检查特定的状态标志位(在SR1和SR2寄存器中),确认该步骤成功完成,才能进行下一步。

  2. 操作数据寄存器DR会清除某些状态标志(如TXE/RXNE),操作SR1寄存器(读或写)会清除事件标志(如ADDR, BTF)。 清除标志是告诉状态机:"我知道这件事完成了,我们继续吧"。

  3. 起始(START)和停止(STOP)条件由硬件在软件置位相应控制位后自动产生。

4.4.1主机发送(指定地址写)

操作步骤详解

  1. 配置与初始化 (Setup):

    • 配置I2C时钟(在RCC寄存器中使能I2C外设时钟)。

    • 配置SDA和SCL引脚为复用开漏输出模式(通常需要上拉电阻)。

    • 初始化I2C外设:

      • 设置时钟速度(CR2中的频率值,影响I2C_InitStructure.I2C_ClockSpeed)。

      • 设置自身设备地址(作为主机发送时通常不需要,但如果是多主机或可能做从机时需要,I2C_InitStructure.I2C_OwnAddress1)。

      • 设置ACK使能(I2C_Ack_Enable,主机一般需要发送ACK给从机)。

      • 设置地址识别模式(7位/10位,I2C_InitStructure.I2C_AcknowledgedAddress)。

      • 使能I2C外设(I2C_Cmd(I2Cx, ENABLE)

  2. 产生起始条件 (Generate START):

    • 设置控制寄存器CR1的START位为1 (I2C_GenerateSTART(I2Cx, ENABLE)

    • 等待事件标志: 轮询状态寄存器SR1的**SB**(Start Bit) 标志位,直到它被硬件置1。这表示START条件已成功发送到总线上。

    • 关键动作: 读取SR1寄存器(硬件自动清除SB标志)。

  3. 发送从机地址 + 写方向 (Send Slave Address + Write Bit):

    • 7位从机地址左移1位最低位设置为0 (表示写操作),然后写入数据寄存器DR (I2C_SendData(I2Cx, (SlaveAddress << 1) | I2C_Direction_Transmitter)

    • 等待事件标志: 轮询状态寄存器SR1的**ADDR** (Address sent) 标志位,直到它被置1。这表示地址帧(地址+方向)已发送,并且收到了从机的应答(ACK) 。如果从机无应答(NACK),ADDR不会被置位,AF (Acknowledge Failure) 标志会被置位(表示错误)。

    • 关键动作: 读取SR1寄存器(清除ADDR标志)并紧接着读取SR2寄存器(读SR2是为了清除内部锁存,虽然SR2的内容可能不重要,但步骤必须做)

  4. 发送数据字节 (Send Data Byte(s)):

    • 将要发送的第一个数据字节写入数据寄存器DR (I2C_SendData(I2Cx, DataByte)

    • 等待事件标志: 轮询状态寄存器SR1的TXE (Transmit Data Register Empty) 标志位,直到它被置1。这表示DR中的数据已被移入移位寄存器(可以发送下一个字节了),并且上一个字节(地址或数据)已发送完成并收到了从机的ACK

    • 关键动作: 一旦EV8事件被检测,TXE=1,就可以写入下一个字节到DR。重复此步骤发送所有数据字节。

    • 注意: 在发送最后一个字节时,步骤稍有不同(见下一步)。

  5. 发送最后一个字节并产生停止条件 (Send Last Byte & Generate STOP):

    • 写入最后一个数据字节到DR。

    • 等待**TXE置1**(表示最后一个字节已移入移位寄存器,等待发送)。

    • 关键等待: 等待状态寄存器SR1的EV8_2事件,BTF (Byte Transfer Finished) 标志置1。BTF=1表示移位寄存器也已变空 (最后一个字节的最后一位已发送到SDA线上),并且收到了从机对该最后一个字节的ACK。此时总线暂时空闲。

    • 关键动作:BTF=1后,立即 设置控制寄存器CR1的STOP位为1 (I2C_GenerateSTOP(I2Cx, ENABLE)),产生STOP条件(必须在下一个START产生前或总线超时前产生STOP)

4.4.2主机接收(当前地址读)

操作步骤详解 (轮询)

  1. 配置与初始化 (Setup): 同主机发送步骤1。

  2. 产生起始条件 (Generate START): 同主机发送步骤2。等待SB置位并清除。

  3. 发送从机地址 + 读方向 (Send Slave Address + Read Bit):

    • 7位从机地址左移1位最低位设置为1 (表示读操作),然后写入数据寄存器DR (I2C_SendData(I2Cx, (SlaveAddress << 1) | I2C_Direction_Receiver)

    • 等待事件标志: 轮询状态寄存器SR1的**ADDR标志位,直到它被置1** 。这表示地址帧(地址+方向)已发送,并且收到了从机的应答(ACK)

    • 关键动作: 读取SR1寄存器(清除ADDR标志)并紧接着读取SR2寄存器(同样,读SR2是必须的步骤)注意: 清除ADDR后,硬件会自动使能ACK(如果之前配置了),准备接收第一个数据字节。

  4. 接收数据字节 (Receive Data Byte(s)):

    • 对于非最后一个字节:

      • 等待事件标志: 轮询状态寄存器SR1的**RXNE (Receive Data Register Not Empty) 标志位,直到它被置1。** 这表示一个完整的数据字节 已经从从机接收完毕,并已从移位寄存器转移到数据寄存器DR中,且主机在SDA上发送了ACK信号给从机(表示继续发送)。

      • 关键动作: 读取数据寄存器DR (ReceivedByte = I2C_ReceiveData(I2Cx))。读取DR会清除RXNE标志。重复此步骤接收所有非最后一个字节。

    • 对于最后一个字节:

      • 在读取倒数第二个字节的DR之后、等待最后一个字节的RXNE之前: 必须关闭ACK !设置控制寄存器CR1的**ACK位为0** (I2C_AcknowledgeConfig(I2Cx, DISABLE))。这是告诉从机:"下一个字节是最后一个了,发完就别发了"。

      • 关键动作: 在读取最后一个字节之前,必须先产生STOP条件! 设置控制寄存器CR1的STOP位为1 (I2C_GenerateSTOP(I2Cx, ENABLE))。(也可以在RXNE置位后立即产生STOP,但提前产生更常见)

      • 等待事件标志: 轮询RXNE标志置1(表示最后一个字节已收到并在DR中)。

      • 关键动作: 读取数据寄存器DR (LastByte = I2C_ReceiveData(I2Cx))。此时,主机在SDA上发送的是NACK信号(因为ACK已关闭)。


4.4.3过程总结:

  1. 流程固定: START -> 地址(方向) -> 数据(发送/接收) -> STOP。主机发送和接收的主要区别在于方向位最后一个字节的处理(ACK/NACK、STOP时机)

  2. 状态驱动: 每做一步关键操作(写DR、置位START/STOP),必须等待并清除 特定的状态标志(SB, ADDR, TXE, RXNE, BTF),才能进行下一步。这是保证硬件状态机同步的关键。

  3. 地址处理: 发送地址时,一定要把7位地址左移1位,并根据是读还是写操作设置最低位(0=写,1=读)。

  4. 接收关键点:

    • 非最后一个字节:收到后主机发ACK (ACK=1)。

    • 最后一个字节:在接收前要关ACK (ACK=0),并在读取前发STOP 。这样主机在收到最后一个字节后会回NACK,并释放总线。

五、软硬件I2C的对比

5.1软件I2C优缺点

5.1.1优点

  1. 极高的灵活性 (GPIO选择):

    • 最大优势! 你可以使用任意两个空闲的GPIO引脚作为SCL和SDA。不受芯片引脚复用功能的限制。这在硬件设计自由度低、引脚资源紧张时非常有用。
  2. 代码透明,易于理解和调试:

    • 协议逻辑完全在你的代码控制之下,每一步(拉低SCL、拉高SDA、延时、检测ACK等)都清晰可见。对于理解I2C底层协议非常有帮助。

    • 调试时,更容易在逻辑分析仪上看到代码执行步骤和对应波形的关系。

  3. 规避特定芯片的硬件I2C缺陷:

    • 在STM32早期型号(尤其是F1系列)中,硬件I2C模块有时被诟病存在一些设计缺陷或"坑"(如特定时序下的总线挂死、中断处理复杂等)。软件模拟可以完全绕过这些潜在问题。(注:新型号STM32的硬件I2C已大幅改进和稳定)
  4. 简单场景下实现快:

    • 对于速度要求很低 (如100kHz以下)且通信量极小(偶尔读写几个字节)的场景,快速写一个简单的软件I2C驱动可能比配置复杂的硬件I2C寄存器更快。

5.1.2缺点

  1. 速度慢且不稳定:

    • 致命缺点! 速度受限于CPU执行指令和软件延时的精度。很难达到标准模式(100kHz)的稳定速度,更别提快速模式(400kHz)或以上。即使能达到,速度也会因中断干扰、代码路径变化而波动。
  2. 极高的CPU占用率:

    • CPU必须全程参与 每一位(bit)数据的发送和接收,在通信期间基本被"阻塞",无法执行其他任务。使用延时循环(for/while延时)阻塞尤其严重。即使使用定时器中断来产生SCL,CPU中断开销仍然很大。
  3. 时序精度难以保证:

    • 软件延时容易受到中断(尤其是高优先级中断)、总线负载、CPU主频变化(如电源管理降频)等因素干扰,导致SCL高低电平时间、建立/保持时间等时序参数偏差。这可能造成通信不稳定,在高速或长距离总线时尤其突出。
  4. 功能支持有限:

    • 实现多主机仲裁 (Arbitration) 非常复杂且不可靠,软件模拟几乎无法实用。

    • 支持时钟同步 (Clock Synchronization) 很困难。

    • 实现SMBus超时检测等功能需要额外工作。

  5. 开发复杂度和维护成本高:

    • 需要开发者深刻理解I2C协议细节(包括所有时序要求)。

    • 编写一个健壮的、能处理各种异常情况(总线忙、无应答、被意外拉低等)的软件I2C驱动并非易事,代码量可能不小。

    • 在不同型号STM32或更换主频时,可能需要调整延时参数。

5.2硬件I2C 优缺点

5.2.1优点

  1. 高速且稳定:

    • 最大优势! 由专用硬件电路产生精确的时钟和时序,不受CPU负载影响。轻松达到标准模式(100kHz)、快速模式(400kHz),部分型号支持快速模式+(1MHz)甚至高速模式(3.4MHz)。通信速度稳定可靠。
  2. 极低的CPU开销:

    • CPU只需在关键事件(如启动传输、发送完地址、发送/接收完一个数据块、传输结束、出错)时介入(通过查询标志位或中断)。

    • 结合DMA,CPU在数据传输过程中几乎完全解放,只需在传输开始和结束时处理一下,效率极高。这是大数据量传输(如读写大容量EEPROM、持续读取传感器流)的首选方案。

  3. 高可靠性和协议完整性:

    • 硬件自动处理复杂的协议细节:精确的START/STOP条件、ACK/NACK生成与检测、时钟拉伸支持、7/10位地址匹配、多主机仲裁 (Arbitration)时钟同步 (Clock Synchronization) 等。大大降低了通信错误率。

    • 内置丰富的错误检测标志(总线错误、仲裁丢失、ACK错误、过载/欠载、超时等),便于错误处理。

  4. 功能强大且完善:

    • 原生支持多主机模式

    • 支持7位/10位地址

    • 支持SMBus协议(包括PEC校验、超时检测、告警响应等可选功能)。

    • 支持时钟延展 (Clock Stretching)(从机拉低SCL要求主机等待)。

  5. 开发相对高效(使用库/HAL):

    • 使用STM32标准外设库(SPL)、HAL库或LL库,通过调用封装好的函数(如HAL_I2C_Master_Transmit(), HAL_I2C_Mem_Read())进行配置和操作,可以快速搭建通信,无需深入理解每个寄存器位(但理解原理对调试有益)。

    • 库函数通常处理了状态机流程和错误检查。

5.2.2缺点

  1. 引脚固定:

    • SCL和SDA引脚由芯片设计固定(查阅芯片数据手册的"Alternate function mapping"表格)。如果这些引脚被占用或布局不方便,可能需要改动硬件设计。
  2. 配置相对复杂:

    • 需要理解I2C外设的寄存器结构和状态机流程(如之前讲解的主机发送/接收步骤)。配置时钟频率、地址模式、中断/DMA、可能的中断优先级等步骤比软件模拟初始设置稍显繁琐。

    • 遇到通信问题时,调试硬件I2C可能比软件模拟更棘手,需要结合逻辑分析仪波形和状态寄存器值来分析。

  3. 潜在的历史问题(主要在老旧型号):

    • 如前所述,早期STM32型号(如F1系列)的硬件I2C模块在某些边界条件下可能存在Bug或行为不符合预期(例如特定顺序操作导致总线锁死)。需要查阅对应芯片的勘误手册和应用笔记。(强烈建议:对于新设计,优先选用新型号STM32,其硬件I2C已非常成熟稳定)。
  4. 中断管理:

    • 为了高效利用硬件I2C,通常需要使能和使用中断。这增加了中断服务程序编写的复杂度,并需要考虑中断优先级和嵌套问题。

5.3如何选择

特性 软件I2C (Bit-Banging) 硬件I2C 推荐场景
速度 低 (<100kHz),不稳定 (100kHz - 3.4MHz+),稳定 高速传输必选硬件
CPU占用 非常高 (阻塞CPU) 非常低 (尤其配合DMA) 低功耗、实时性要求高必选硬件
时序精度/稳定性 低,易受干扰 ,硬件保证 长线、干扰环境、高速必选硬件
GPIO灵活性 极高 (任意GPIO) 低 (固定复用引脚) 引脚受限时选软件
多主机/仲裁 极难实现 原生支持 多主机系统必选硬件
开发难度(基础) 低 (简单读写) 中 (需理解状态机/寄存器/库) 初学者理解协议可选软件
开发难度(健壮/高级) 高 (需处理所有异常/时序) 中低 (库函数处理大部分) 产品级应用优先硬件
功能完整性 有限 (需自行实现) 丰富 (协议/SMBus/DMA) 需要完整协议支持选硬件
调试透明度 (代码直接对应波形) 中低 (需结合波形和状态寄存器) 深度调试协议细节软件直观
规避硬件Bug (完全软件控制) 仅当目标芯片有已知严重I2C硬件Bug时考虑

六、硬件I2C读写MPU6050

6.1硬件I2C控制MPU6050

1.配置I2C外设,对I2C外设进行初始化:

①开启I2C外设和对应GPIO口时钟

②将I2C外设对应的GPIO口初始化为复用开漏模式

③用结构体,对整个I2C进行配置

④使能I2C

2.控制外设电路,实现指定地址写的时序
3.控制外设电路,实现指定地址读的时序

6.2硬件I2C相关库函数

①void I2C_DeInit(I2C_TypeDef* I2Cx);

重置默认值


②void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);

初始化


③void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);

结构体


④void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

使能


⑤void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);

生成起始条件


⑥void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

生成终止条件


⑦void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);

配置CR1的ACK位,从机应答(1应答,0非应答)


⑧void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);

发送数据到DR寄存器,自动启动数据传输


⑨uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);

读取DR数据,接收数据

接受移位完成时,收到的一个字节由移位寄存器转到数据寄存器,读取数据寄存器,就可接收一个字节;

在下一个字节收到前,及时把上一个字节移走,防止数据覆盖


⑩void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);

发送7位地址专用函数


监控标志位的方案,来确定EV-X状态是否发生
Ⅰ基本状态监控I2C_CheckEvent()(推荐)

ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);

同时判断一个或多个标志位
Ⅱ高级状态监控I2C_GetLastEvent()

uint32_t I2C_GetLastEvent(I2C_TypeDef* I2Cx);

将SR1和SR2数据拼接成16位再处理
Ⅲ基于标志位的状态监控I2C_GetFlagStatus()

FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);

判断某个标志位是否置1


标志位的读取和清除

读取标志位

FlagStatus I2C_GetFlagStatus(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);

清除标志位

void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);

读取中断标志位

ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);

清除中断标志位

void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT);

6.3MPU6050.c

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"
#define MPU6050_Address 0xD0
//根据指定地址写和指定地址读,拼接一个完整的时序

//指定地址写
void IIC_Random_Write_address(uint8_t addr,uint8_t Data)
{
	/*
	起始条件->EV5事件
	发送从机字节地址->EV6事件->EV8_1事件(没有,直接写入发送从机地址)
	发送内存地址字节->EV8事件,之后,可直接再次写入数据,非最后一个字节可直接写入下一个数据
	发送数据字节,非最后一个字节可直接写入下一个数据,发送最后一个字节->EV8_2事件
	终止条件
	*/
	I2C_GenerateSTART(I2C2,ENABLE);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT)!=SUCCESS);
	I2C_Send7bitAddress(I2C2,MPU6050_Address,I2C_Direction_Transmitter);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)!=SUCCESS);
	I2C_SendData(I2C2,addr);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING)!=SUCCESS);
	I2C_SendData(I2C2,Data);//最后一个字节,发送完毕要终止
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED)!=SUCCESS);
	I2C_GenerateSTOP(I2C2,ENABLE);
}
	
//指定地址读
uint8_t IIC_Random_Read_address(uint8_t addr)
{
	/*
	起始条件->EV5事件
	发送从机字节地址->EV6事件->EV8_1事件(没有,直接写入发送从机地址)
	发送内存地址字节->EV8事件,之后,可直接再次写入数据,非最后一个字节可直接写入下一个数据

	重复起始条件->EV5事件
	发送从机读字节地址->EV6事件(进入主机接受模式,开始接收从机发送的数据波形)->接收一个字节时,有EV6_1事件(没有标志位,适合接收一个字节的情况,在EV6之后,清除响应和停止条件的产生位)
	终止条件(在接收最后一个字节之前,提前把ACK置0,同时设置停止位,等待EV7事件:接收到一个字节后产生)EV7->一个字节的数据在DR中,读取DR即可拿出字节->ACK置0
	若接收多个字节,直接等待EV7事件,读取DR,可接收到数据,在接收最后一个字节之前,在EV7_1事件,需要提前将ACK置0,STOP置1
	若接收一个字节,在EV6事件后,立刻ACK置0,STOp置1
	默认状态下ACK=1,给从机应答,在接收最后一个字节之前,把ACK=0,给非应答,因此在接收函数最后,恢复ACk的默认值1
	*/
	uint8_t Data;
	I2C_GenerateSTART(I2C2,ENABLE);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT)!=SUCCESS);
	I2C_Send7bitAddress(I2C2,MPU6050_Address,I2C_Direction_Transmitter);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)!=SUCCESS);
	I2C_SendData(I2C2,addr);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED)!=SUCCESS);

	I2C_GenerateSTART(I2C2,ENABLE);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT)!=SUCCESS);
	I2C_Send7bitAddress(I2C2,MPU6050_Address,I2C_Direction_Receiver);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)!=SUCCESS);
	I2C_AcknowledgeConfig(I2C2,DISABLE);
	I2C_GenerateSTOP(I2C2,ENABLE);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED)!=SUCCESS);
	Data=I2C_ReceiveData(I2C2);
	I2C_AcknowledgeConfig(I2C2,ENABLE);//方便接收多个字节
	/*
	若接收多个字节将这四个函数循环即可
	I2C_AcknowledgeConfig(I2C2,DISABLE);
	I2C_GenerateSTOP(I2C2,ENABLE);
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED)!=SUCCESS);
	Data=I2C_ReceiveData(I2C2);
	在接收前面字节时,只执行后面俩行,再if,若计数到最后一个字节,if成立,就四行全部执行
	I2C_AcknowledgeConfig(I2C2,DISABLE);
	I2C_GenerateSTOP(I2C2,ENABLE);
	代码如下:
	if(num=0;num<8;num--)
	{
		I2C_AcknowledgeConfig(I2C2,DISABLE);
		I2C_GenerateSTOP(I2C2,ENABLE);
		if(num==8)
		{
			I2C_AcknowledgeConfig(I2C2,DISABLE);
			I2C_GenerateSTOP(I2C2,ENABLE);
			while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED)!=SUCCESS);
			Data=I2C_ReceiveData(I2C2);
		}

	}

	*/
	return Data;
}


void MPU6050_Init(void)
{
	//硬件I2C初始化
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_OD;//复用:GPIO控制权交给硬件外设
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Ack=I2C_Ack_Enable;
	I2C_InitStructure.I2C_AcknowledgedAddress=I2C_AcknowledgedAddress_7bit;
	I2C_InitStructure.I2C_ClockSpeed=50000;//0~400KHz
	I2C_InitStructure.I2C_DutyCycle=I2C_DutyCycle_2;//始终占空比,只有在f大于100KHz,进入快速状态才有用,始终占空比大概2;1,其余基本是1:1,始终占空比没用
	I2C_InitStructure.I2C_Mode=I2C_Mode_I2C;
	I2C_InitStructure.I2C_OwnAddress1=0x00;//自身地址7_7bit,10_10Bit,STM32做从机被使唤
	I2C_Init(I2C2,&I2C_InitStructure);
	
	I2C_Cmd(I2C2,ENABLE);
	/*MPU6050硬件电路初始化
	①配置电源管理寄存器1和2
	②配置分频寄存器,采样分频10
	③配置寄存器
	④陀螺仪配置寄存器
	⑤加速度计配置寄存器
	*/
	IIC_Random_Write_address(MPU6050_PWR_MGMT_1,0x01);//不复位,解除睡眠,选择陀螺仪时钟
	IIC_Random_Write_address(MPU6050_PWR_MGMT_2,0x00);//循环模式唤醒频率不需要,每一个轴待机位都为0,不需要待机
	IIC_Random_Write_address(MPU6050_SMPLRT_DIV, 0x09);		//采样率分频寄存器,配置采样率,这里是10
	IIC_Random_Write_address(MPU6050_CONFIG, 0x06);			//配置寄存器,外部同步不需要,数字低通滤波器110,最平滑的滤波
	IIC_Random_Write_address(MPU6050_GYRO_CONFIG, 0x18);	//陀螺仪配置寄存器,选择满量程11,为±2000°/s,,最大量程
	IIC_Random_Write_address(MPU6050_ACCEL_CONFIG, 0x18);	//加速度计配置寄存器,选择满量程11,为±16g,高通滤波器用不到00
}

//获取数据寄存器,XYZ多个返回值函数,使用指针地址传递
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	//分别读取6个轴数据寄存器的高位和低位,拼接成16位数据,再通过指针变量返回
	uint16_t DataH, DataL;//数据高8位和低8位
	
	DataH = IIC_Random_Read_address(MPU6050_ACCEL_XOUT_H);		//读取加速度计X轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_ACCEL_XOUT_L);		//读取加速度计X轴的低8位数据
	*AccX = (DataH << 8) | DataL;//加速度计X轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_ACCEL_YOUT_H);		//读取加速度计Y轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_ACCEL_YOUT_L);		//读取加速度计Y轴的低8位数据
	*AccY = (DataH << 8) | DataL;//加速度计Y轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_ACCEL_ZOUT_H);		//读取加速度计Z轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_ACCEL_ZOUT_L);		//读取加速度计Z轴的低8位数据
	*AccZ = (DataH << 8) | DataL;	//加速度计Z轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_GYRO_XOUT_H);		//读取陀螺仪X轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_GYRO_XOUT_L);		//读取陀螺仪X轴的低8位数据
	*GyroX = (DataH << 8) | DataL;//陀螺仪X轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_GYRO_YOUT_H);		//读取陀螺仪Y轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_GYRO_YOUT_L);		//读取陀螺仪Y轴的低8位数据
	*GyroY = (DataH << 8) | DataL;//陀螺仪Y轴的数据
	
	DataH = IIC_Random_Read_address(MPU6050_GYRO_ZOUT_H);		//读取陀螺仪Z轴的高8位数据
	DataL = IIC_Random_Read_address(MPU6050_GYRO_ZOUT_L);		//读取陀螺仪Z轴的低8位数据
	*GyroZ = (DataH << 8) | DataL;//陀螺仪Z轴的数据
}

6.4代码出现的问题

代码中出现大量While死循环等待,对程序有危险,若一个事件没有产生,程序会卡死,因此,我们可以给死循环加一个超时退出机制,一个简单的计数等待

cpp 复制代码
	uint32_t TimeOut;
	I2C_GenerateSTART(I2C2,ENABLE);
	TimeOut=10000;
	while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT)!=SUCCESS)
	{
		if(--TimeOut==0)
		{
			break;
		}
	}

每一个While函数增加一个退出机制,太过于麻烦,因此,可以在checkEvent函数上改装成带有超时退出机制的WaitEvent函数

cpp 复制代码
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t TimeOut=10000;
	while(I2C_CheckEvent(I2Cx,I2C_EVENT)!=SUCCESS)
	{
		if(--TimeOut==0)
		{
			break;
		}
	}

}
相关推荐
Ronin-Lotus3 小时前
嵌入式硬件篇---电感串并联
嵌入式硬件
Wallace Zhang4 小时前
STM32 - Embedded IDE - GCC - 显著减少固件的体积
stm32·单片机·嵌入式硬件
fengfuyao98514 小时前
STM32如何定位HardFault错误,一种实用方法
stm32·单片机·嵌入式硬件
爱学习的颖颖15 小时前
EXTI外部中断的执行逻辑|以对射式红外传感器计次为例
单片机·嵌入式硬件·exti中断
keer_zu16 小时前
STM32L051 RTC闹钟配置详解
stm32·嵌入式硬件
AI精钢16 小时前
H20芯片与中国的科技自立:一场隐形的博弈
人工智能·科技·stm32·单片机·物联网
etcix19 小时前
implement copy file content to clipboard on Windows
windows·stm32·单片机
谱写秋天19 小时前
在STM32F103上进行FreeRTOS移植和配置(STM32CubeIDE)
c语言·stm32·单片机·freertos
globbo1 天前
【嵌入式STM32】I2C总结
单片机·嵌入式硬件