当前位置: 首页 > web >正文

STM32——软硬件I2C

目录

#I2C通信协议#

一、I2C通信

1.1 I2C简介

1.2 硬件电路(硬件规定)

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

1.3.1 起始条件S:

1.3.2 终止条件P:

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

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

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

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

*解释:

*问题:

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

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

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

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

1.4.1 核心前提:

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

1.4.3当前地址读(Current Address Read)

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

1.4.5总结

#MPU6050外设#

二、MPU6050外设

2.1 MPU6050简介

2.2 MPU6050参数

2.3 硬件电路

2.4MPU6050框图

2.5重要寄存器了解

2.5.1寄存器一览表

2.5.2关键寄存器详解

三、软件I2C读写MPU6050

3.1 I2C代码

3.1.1程序中疑惑的点

3.2MPU6050.c代码

3.3 main.c代码

3.4MPU6050硬件初始化代码

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

3.4.2MPU6050.c硬件代码初始化

3.4.3main.c代码

四、I2C外设

4.1 I2C外设简介

4.2 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.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地址下的数据即可。


缺点:

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

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

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

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


改良:

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


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

  • 两根通信线:SCLSerial Clock串行时钟线)、SDASerial 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 位地址。

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

从机地址 (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轴姿态传感器,可以测量芯片自身XYZ轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(欧拉角 ),常应用于平衡车、飞行器等需要检测自身姿态的场景

  • 3轴加速度计(静态稳定)(Accelerometer):测量XYZ轴的加速度

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

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

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

2.2 MPU6050参数

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

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

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

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

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

  • 可配置的时钟源

  • 可配置的采样分频

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

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

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

                                                  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_CLAUX_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️⃣ 数据寄存器 - "体检报告单"

数据地址字节组合计算公式
加速度X0x3B,0x3CACCEL_XOUT_H + ACCEL_XOUT_Lax = (高8<<8 | 低8) / 16384.0 (±2g)
温度0x41,0x42TEMP_OUT_H + TEMP_OUT_LT = (原始值/340.0) + 36.53
陀螺仪Z0x47,0x48GYRO_ZOUT_H + GYRO_ZOUT_Lgz = (高8<<8 | 低8) / 131.0 (±250°/s)

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

三、软件I2C读写MPU6050

端口不受限,可任意指定

3.1 I2C代码

#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代码

#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代码

#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寄存器地址0x75ID=IIC_Random_Read_address(0x75);OLED_ShowHexNum(1,1,ID,2);//验证写寄存器功能,需要解除芯片睡眠模式,否则写入无效IIC_Random_Write_address(0x6B,0x00);//睡眠模式是电源管理器1的SLEEP控制,可直接写入0x00,寄存器地址是0x6BIIC_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相关寄存器地址的宏定义

#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硬件代码初始化

#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);		//采样率分频寄存器,配置采样率,这里是10IIC_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代码

#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寄存器地址0x75ID=IIC_Random_Read_address(0x75);OLED_ShowHexNum(1,1,ID,2);//验证写寄存器功能,需要解除芯片睡眠模式,否则写入无效IIC_Random_Write_address(0x6B,0x00);//睡眠模式是电源管理器1的SLEEP控制,可直接写入0x00,寄存器地址是0x6BIIC_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资源:I2C1I2C2

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),必须等待并清除特定的状态标志(SBADDRTXERXNEBTF),才能进行下一步。这是保证硬件状态机同步的关键。

  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

#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~400KHzI2C_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);		//采样率分频寄存器,配置采样率,这里是10IIC_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死循环等待,对程序有危险,若一个事件没有产生,程序会卡死,因此,我们可以给死循环加一个超时退出机制,一个简单的计数等待

	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函数

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;}}}

http://www.xdnf.cn/news/18210.html

相关文章:

  • 8月17日星期天今日早报简报微语报早读
  • 解锁Java开发神器:XXL-Job从入门到精通
  • java如何使用正则提取字符串中的内容
  • Go语言实战案例-使用ORM框架 GORM 入门
  • Centos 更新/修改宝塔版本
  • GaussDB 数据库架构师修炼(十三)安全管理(5)-全密态数据库
  • 【架构师从入门到进阶】第五章:DNSCDN网关优化思路——第十二节:网关安全-信息过滤
  • 哈希表与unorder_set,unorder_map的学习
  • 【Linux系列】常见查看服务器 IP 的方法
  • 深入了解 Filesystem Hierarchy Standard (FHS) 3.0 规范
  • 17.5 展示购物车缩略信息
  • 【Linux】文件基础IO
  • Google Earth Engine | (GEE)逐月下载的MODIS叶面积指数LAI
  • Rust 入门 生命周期(十八)
  • 【牛客刷题】字符串按索引二进制1个数奇偶性转换大小写
  • C#高级语法_委托
  • java基础(十)sql的mvcc
  • 字节 Golang 大模型应用开发框架 Eino简介
  • 进程互斥的硬件实现方法
  • 私人AI搜索新突破:3步本地部署Dify+Ollama+QwQ,搜索能力MAX
  • 《动手学深度学习v2》学习笔记 | 1. 引言
  • Nacos 注册中心学习笔记
  • C++入门自学Day11-- String, Vector, List 复习
  • Kafka 面试题及详细答案100道(23-35)-- 核心机制2
  • 3D打印——给开发板做外壳
  • 最新技术论坛技术动态综述
  • XF 306-2025 阻燃耐火电线电缆检测
  • 【Linux | 网络】高级IO
  • JMeter(进阶篇)
  • (一)Python + 地球信息科学与技术 (GeoICT)=?