【嵌入式学习-STM32F103-I2C】

I2C目录

  • 总体框图
  • 爱上半导体篇
  • 江科大篇
    • 概述
    • 要求
    • 项目要求
    • I2C的硬件规定
    • I2C的软件规定
      • 起始和终止条件
      • 发送一个字节时序
      • 接收一个字节的时序
      • 发送数据和接收数据的区别
      • 发送应答和接收应答
      • I2C-指定地址写
      • I2C时序-当前地址读(用不多)
      • I2C时序-指定地址读
        • 具体流程
    • 注意
    • MPU6050
      • 硬件接线图
      • 软件I2C读写MPU6050
        • 注意
        • 程序整体框架(分别为应用层-驱动层-协议层)
        • 具体步骤
        • 第一部分
        • 第二部分
          • 按位与操作,取出某一位或某几位
        • 问题1
        • 问题2
        • 检查从机是否有应答
        • 代码
          • 协议层
            • MyI2C.c
            • MyI2C.h
          • 驱动层
            • MPU6050.c
            • MPU6050.h
            • MPU6050_Reg.h
          • 应用层
            • main.c
            • 实验效果
      • 硬件I2C读写MPU6050
        • SDA部分
          • 发送流程
        • 接收流程
        • SCL部分
          • I2C输出
          • I2C输入
        • 软硬件波形对比
        • 代码实现
          • 步骤
          • 配置IIC外设
            • 50Khz(标准速度)
            • 100Khz(标准速度)
          • 杆子弹簧模型
            • 101Khz(快速状态)
            • 200Khz
            • 400Khz(快速模式)
          • 硬件I2C写寄存器
          • 硬件I2C读寄存器
          • 解决死循环问题
        • 代码
          • main.c
          • MPU6050.c
          • MPU6050.h
  • 补充
    • 1、为什么IIC采用开漏输出和弱上拉

总体框图

【嵌入式学习-STM32F103-I2C】_第1张图片

爱上半导体篇

IIC,Inter-Integrated Circuit芯片与芯片之间的通讯。
IIC通讯通常采用一主多从的形式。如单片机是主机,而其他设备是从机。通讯机理?

【嵌入式学习-STM32F103-I2C】_第2张图片

以单片机向从设备写信息为例
【嵌入式学习-STM32F103-I2C】_第3张图片
串口通讯的两根线分别是发送和接收
【嵌入式学习-STM32F103-I2C】_第4张图片
IIC的两根线分别是时钟线和数据线
【嵌入式学习-STM32F103-I2C】_第5张图片
当处于空闲状态时,数据线和时钟线都处于高电平状态
【嵌入式学习-STM32F103-I2C】_第6张图片
当开始传递信息时,比如传递第一位起始位。此时,必须要在时钟信号为高电平期间,数据信号完成由高到低的跳变,也就是下降沿。此时,起始信号发送完成。

【嵌入式学习-STM32F103-I2C】_第7张图片

【嵌入式学习-STM32F103-I2C】_第8张图片
接下来是7位设备地址码,为了区别和哪一个从设备通讯,需要发送7位设备地址码。

【嵌入式学习-STM32F103-I2C】_第9张图片
逻辑1和逻辑0的表示方法, 当时钟线为高电平时,数据线上的数据必须保持稳定。

【嵌入式学习-STM32F103-I2C】_第10张图片
【嵌入式学习-STM32F103-I2C】_第11张图片
写数据

读数据

应答信号:由从机发送给主机

【嵌入式学习-STM32F103-I2C】_第12张图片
如果从机接收到主机发送的数据会回复0,没收到或者读取完成回复1.
【嵌入式学习-STM32F103-I2C】_第13张图片

江科大篇

概述

第一块:介绍协议规则,然后用软件模拟的形式来实现协议。

第二块:介绍STM32的iic外设,然后用硬件来实现协议。

程序一现象:通过软件I2C通信,对MPU6050芯片内部的寄存器进行读写,写入到配置寄存器,就可以对外挂的这个模型进行配置,读出数据寄存器,就可以获取外挂模块的数据。I2通信的目的

串口通信没有时钟线的异步全双工的协议。

如果我们想要读写寄存器来控制硬件电路,我们至少需要定义两个字节的数据。一个字节是我们要读写哪个寄存器,也就是指定寄存器的地址,另一个字节就是这个地址下存储器的内容,写入内容就是控制电路,读出内容就是获取电路状态。

要求

1、全双工变为半双工;
2、应答机制(安全起见)
3、一根通讯线能够同时外接多个模块(单片机可以指定和任意一个模块进行通信,同时单片机在跟某个模块进行通信时,其他模块不能对正常的通信产生干扰。)
4、串口是异步时序,也就是发送方和接收方约定的传输速率是非常严格的,时钟不能有过大的偏差,也不能在传输过程中,单片机有事进入中断了,异步时序是不能暂停的,单片机一个字节发一半数据暂停了,接收方是不知道的,它仍然会按照原来的约定速率读取,最终导致传输出错。(异步时序的缺点是依赖硬件外设的支持,必须要有USART电路才能方便使用,如果没有USART硬件电路的支持,那么串口是很难用软件来模拟的),需要将该协议改为同步协议,另外加一条时钟线来指导对方读写。由于存在时钟线,对传输的时间要求就不高了,单片机可以随时暂停传输,去处理其他事情,因为暂停传输的同时,时钟线也暂停了,所以传输双方都能定格在暂停的时刻,可以过一段时间再继续,不会对传输造成影响。(同步时序的好处),使用同步时序可极大降低单片机对硬件电路的依赖。即使没有硬件电路的支持也可以很方便地用软件手动翻转电平来实现通信,而异步时序的好处是省一根时钟线,节省资源,缺点是对时间要求严格,对硬件电路的依赖比较严重。

单片机读写自己的寄存器,可以直接通过内部的数据总线来实现,直接用指针操作就行,不需要我们操心。但是,现在这个模块的寄存器在单片机的外面,那怎么实现单片机读写外部模块寄存器的操作呢

项目要求

通过通信线,实现单片机读写外挂模块寄存器的功能,其中至少要实现在指定的位置读寄存器和在指定的位置读寄存器两个功能。实现读写寄存器也就实现了对外挂模块的完全控制

【嵌入式学习-STM32F103-I2C】_第14张图片
同步时序稳定性比异步时序更高,然后只有一根SDA数据线,变全双工为半双工,一根线兼具发送和接收,最大化利用资源。一主多从:单片机作为主机,主导I2C总线的运行,挂在在I2C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制IIC总线,不能在未经允许的情况下去碰I2C总线,防止冲突。

I2C的硬件规定

主机对SCL线的完全控制,任何时候,都是主机完全掌控SCL线,另外在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机,这是主机的权利。

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

【嵌入式学习-STM32F103-I2C】_第15张图片
为了避免总线没协调好导致电源短路的问题,I2C的设计是禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构。

=

主机拥有SCL的绝对控制权,主机的SCL可以配置成推挽输出;所有从机的SCL都配置成浮空输入或者上拉输入;数据流向是主机发送,所有从机接收;由于半双工协议,所以主机的SDA在发送的时候是输出,在接收的时候是输入。同样,从机的SDA也会在输入和输出之间反复切换,如果能协调好输入输出的切换时机,如果总线时机没协调好,极有可能发生两个引脚同时处于输出的状态,如果正好此时一个输出低电平,一个输出高电平,那这个状态就是电源短路。所以,为了避免总线没有协调好导致电源短路。
【嵌入式学习-STM32F103-I2C】_第16张图片
【嵌入式学习-STM32F103-I2C】_第17张图片
I2C的设计是禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出(强下拉)的电路结构

【嵌入式学习-STM32F103-I2C】_第18张图片

包括CPU和被控IC引脚的内部结构都如下图所示

【嵌入式学习-STM32F103-I2C】_第19张图片
首先引脚的信号进来,都可以通过一个数据缓冲器或者施密特触发器进行输入,因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的,但是在输出的这部分,采用的是开漏输出的配置。(去掉强上拉的开关管,输出低电平时,下管导通,是强下拉,输出高电平时,下管断开,但是没有上管了,此时引脚处于浮空状态),因此所有设备只能输出低电平而不能输出高电平,为了避免高电平造成的引脚浮空,这时需要在总线外面各外置一个上拉电阻(弱上拉)。
【嵌入式学习-STM32F103-I2C】_第20张图片
推挽输出(上面导通输出高电平,下面导通输出低电平)因为这是通过开关管直接接到正极和负极的,所以这是强上拉和强下拉.
【嵌入式学习-STM32F103-I2C】_第21张图片
比喻
SCL或SDA就是一根杆子,为了防止有人向上推杆子,有人向下拉杆子,造成冲突,约定所有人不准向上推杆子,只能选择向下拉(低电平状态)或者放手(此时弹簧作用,有弱上拉的作用)

【嵌入式学习-STM32F103-I2C】_第22张图片
然后我们再外置一根弹簧向上拉,你要输出低电平,就往下拽,结果是弹簧被拉伸,杆子处于低电平状态,你要输出高电平,就放手,杆子在弹簧的拉力下,回弹到高电平(弱上拉的高电平),完全不影响数据传输。

【嵌入式学习-STM32F103-I2C】_第23张图片
好处:
1、完全杜绝了电源短路现象,保证电路的安全(无论拉杆子或者放手,杆子都不会处于一个被同时强拉或强推的状态,即使有多个人同时往下拉杆子也没问题);
2、避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能。你要是想输出,就去拉杆子或者放手,操作杆子变化即可,你要是想输入,就直接放手,然后观察杆子高低,因为开漏模式下,输出高电平就相当于断开引脚,所以在输入之前,可以直接输出高电平,不需要切换成输入模式;
3、“线与”,只要有任意一个或多个设备输出低电平,总线就处于低电平,只有所有的设备都输出高电平,总线才处于高电平。

I2C的软件规定

起始和终止条件

【嵌入式学习-STM32F103-I2C】_第24张图片
在高电平期间,是从机读取SDA的时候,SDA不允许变化。

发送一个字节时序

主机需要在SCL下降沿之后尽快把数据放在SDA上,但是主机有时钟的主导权,只需要在低电平的任意时刻把数据放在SDA上。数据放完后,主机再松手SCL,SCL高电平,从机开始读取数据。(主机拉低SCL,把数据放在SDA上,主机松开SCL,从机读取SDA的数据,在SCL的同步下,依次进行主机发送和从机接收,循环8次,就发送了8位数据,也就是1个字节)I2C时序:高位先行;串口时序:低位先行。

【嵌入式学习-STM32F103-I2C】_第25张图片
同步时序的好处:由于有时钟线进行同步,如果主机一个字节发送一半就进入了中断,不操作SCL和SDA,时序就会在中断的位置不断拉长,SCL和SDA电平暂停变化,传输完全暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题。

【嵌入式学习-STM32F103-I2C】_第26张图片

接收一个字节的时序

主机在接收之前,需要释放SDA(相当于切换为输入模式)所有设备包括主机都始终处于输入模式,当主机需要发送的时候就可以主动拉低SDA,而主机在被动接收的时候,就必须先释放SDA,不要动他,以免影响别人发送数据。因为总线是线与设备,任何一个设备拉低了,总线就是低电平,如果你接收数据的时候,还拽着SDA不放手,无论别人发送什么数据,总线始终是低电平。

实线部分表示主机控制的电平,虚线部分表示从机控制的电平

【嵌入式学习-STM32F103-I2C】_第27张图片

发送数据和接收数据的区别

发送:低电平主机放数据,高电平从机读数据

接收:低电平从机放数据,高电平主机读数据

发送应答和接收应答

主机在接收数据之前,需要释放SDA。在调用发送一个字节后,就要紧跟接收应答的时序,用来判断从机有没有收到刚才给他的数据。如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来为什么要立刻拉下来

【嵌入式学习-STM32F103-I2C】_第28张图片

一般I2C的从机设备地址,高位都是厂商确定的,低位可由引脚来灵活切换。即使相同型号的芯片,挂载在同一个总线上,也可以通过切换地址低位的方式,保证每个设备的地址都不一样。

【嵌入式学习-STM32F103-I2C】_第29张图片

I2C-指定地址写

【嵌入式学习-STM32F103-I2C】_第30张图片

发送从机地址,就是确定通信的对象

发送读写位,就是确认我接下来是要写入还是要读出

在这里,SCL低电平期间,SDA变换数据,SCL高电平期间,从机读取SDA(如读取1)
【嵌入式学习-STM32F103-I2C】_第31张图片
以下图片波形有以下意义:

0表示,之后的时序主机要进行写入操作

1表示,之后的时序主机要进行读出操作

字节数据转化为16进制数据,且为高位先行。

【嵌入式学习-STM32F103-I2C】_第32张图片

根据协议内容,紧接着是接收从机的应答位,此时,主机要释放SDA。

所以如果单看主机的波形,释放SDA之后,引脚电平回弹到高电平。
【嵌入式学习-STM32F103-I2C】_第33张图片

根据协议规定,从机要在这个位拉低SDA,所以单看从机的波形,该应答的时候,从机立刻拽住SDA,然后应答结束之后,从机再放开SDA。(由从机发送应答位,因此SDA暂时由从机控制)
【嵌入式学习-STM32F103-I2C】_第34张图片
综合两者的波形,结合“线与”的特性,在主机释放SDA后,由于SDA也被从机拽住,所以主机松手后,SDA并没有回弹高电平,这个过程代表从机产生了应答。最终高电平期间,主机读取SDA发现是0,说明我(主机)进行寻址,有人给我应答,传输没问题。
【嵌入式学习-STM32F103-I2C】_第35张图片
如果主机读取SDA,发现是1,说明我进行寻址,应答期间,我松手了,但是没人拽住它,没人给我应答,那就直接产生停止条件,并提示一些信息。这就是应答位。
【嵌入式学习-STM32F103-I2C】_第36张图片
以下是从机释放SDA产生的上升沿,从机交出了SDA的控制权,因为从机要在低电平尽快变换数据,所以这个SDA的上升沿和SCL的下降沿几乎同时发生。
【嵌入式学习-STM32F103-I2C】_第37张图片
第二个字节,就可以送到指定设备的内部,从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址或者是指令控制字等。以下是主机向从机发送了0X19这个数据,在MPU6050里,就表示主机要操作0x19地址下的寄存器,接着同样是从机的应答。

【嵌入式学习-STM32F103-I2C】_第38张图片
以下为主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位位0,表示收到了从机的应答。

【嵌入式学习-STM32F103-I2C】_第39张图片

同样流程如下图,这个字节就是主机想要写入到0X19地址下寄存器的内容,即在0x19地址下,写入0xAA,最后是接收应答位0。

【嵌入式学习-STM32F103-I2C】_第40张图片
如果主机不需要传输了,就可以产生停止位,在停止条件之前,先拉底SDA,为后续SDA上升沿做准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间,SDA上升沿。
【嵌入式学习-STM32F103-I2C】_第41张图片

I2C时序-当前地址读(用不多)

【嵌入式学习-STM32F103-I2C】_第42张图片

当前地址指针是什么意思呢?主机读取从机的数据,可执行以上时序。

最开始,SCL高电平期间,拉低SDA,产生起始条件。

【嵌入式学习-STM32F103-I2C】_第43张图片
起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位。

【嵌入式学习-STM32F103-I2C】_第44张图片
紧跟着,发送一个字节后,接收一下从机应答位,从机应答0,代表从机收到了第一个字节。

从以下位置开始,数据的传输方向就要反过来。因为刚才主机发出了读的命令,因此,在这之后,主机就不能继续发送,需要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作。

【嵌入式学习-STM32F103-I2C】_第45张图片
从机得到主机的允许,可以在SCL低电平期间写入SDA

【嵌入式学习-STM32F103-I2C】_第46张图片

然后主机在SCL高电平期间读取SDA,最终在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据。

I2C时序-指定地址读

【嵌入式学习-STM32F103-I2C】_第47张图片
【嵌入式学习-STM32F103-I2C】_第48张图片

具体流程

【嵌入式学习-STM32F103-I2C】_第49张图片
主机接收一个字节,0xAA就是0x19地址下的数据,主机从0xD1里面的数据0xAA到0x19,后面如果还需要继续读的话,0x19自增为0x1A

首先,启动条件,然后发送一个字节,进行设备寻址,这里指定从机地址是1101000,读写标志位是0,代表我要进行写的操作,经过从机应答0后,发送第二个字节,用来指定地址,这个数据就写入到了从机的地址指针里,即从机接收到这个数据后,它的寄存器指针就指向了0x19这个位置,之后我们要写入的数据不给他发,而是直接来一个起始条件,Sr(Start Repeat)重复起始条件,相当于另起时序,因为指定读写标志位只能是跟着起始条件的第一个字节,如果想要切换读写方向,只能再来一个起始条件,然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表主机开始从从机读数据。

官方规定的数据帧:起始+重复起始+停止

进阶版本:指定地址写多个字节
【嵌入式学习-STM32F103-I2C】_第50张图片
①写入到0x19,②写入到0x1A,③写入到0x1B…
在指定的地址开始,按顺序写入多个字节
同理当前位置读指定位置读也可以多次执行最后一部分时序

注意

【嵌入式学习-STM32F103-I2C】_第51张图片

如果只想读一个字节就停止,在读完一个字节之后,主机一定要给从机发个非应答,(Send Ack,SA),非应答就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,就代表主机没有应答,从机收到非应答后,就知道主机不想要继续接收数据,从机就会释放总线,把SDA控制权交还给主机。

如果主机读完仍然给从机应答,SA=0,那么从机就会认为主机还想要数据,就会继续发送下一个数据,而这时主机如果想产生停止条件,SDA可能就会因为被从机拽住,而不能正常弹回高电平。

如果主机想连续读取多个字节,就需要在最后一个字节给非应答,而之前所有的字节都要给应答。

主机给应答,从机就会继续发,主机给非应答,从机就不会再发,交出SDA的控制权,从机控制SDA发送一个字节的权力,开始于读写标志位为1,结束语主机给应答为1

MPU6050

硬件接线图

【嵌入式学习-STM32F103-I2C】_第52张图片

软件I2C读写MPU6050

注意

端口不受限,可以任意指定
在使用软件模拟的I2C通信时,理论上可以将SCL(时钟线)和SDA(数据线)连接到单片机的任意引脚。由于软件实现了I2C通信的协议和时序,因此不依赖于特定的硬件引脚。但是需要注意的是,选择的引脚应具备足够的GPIO功能,包括输入输出控制、上拉电阻等。同时,还需要在软件中正确配置和操作这些引脚,以确保I2C通信的正确性和稳定性。因此,在选择引脚时,需要考虑到单片机的引脚功能和软件实现的复杂度,以及可能的干扰和布线问题。

程序整体框架(分别为应用层-驱动层-协议层)

【嵌入式学习-STM32F103-I2C】_第53张图片

具体步骤

1、I2C,建立I2C通信层的.c和.h模块,在通讯层里写好I2C底层的GPIO初始化和6个时序基本单元(起始、终止、发送一个字节、接收一个字节、发送应答和接收应答)

2、MPU6050,建立MPU6050的.c和.h模块,在这一层,我们将基于I2C通信的模块,来实现指定地址读、指定地址写,再实现写寄存器对芯片进行配置,读寄存器得到传感器数据

3、最终在main.c调用MPU6050的模块,初始化,拿到数据,显示数据

第一部分

完成软件I2C协议时序

/*单步
1、写SCL
2、写SDA
3、读SDA
*/

/*步骤
1、起始位
2、终止位
3、发送一个字节
4、接收一个字节
5、发送应答位
6、接收应答位
*/

第二部分

基于I2C协议,读写寄存器,来操控MPU6050

【嵌入式学习-STM32F103-I2C】_第54张图片
SCL低电平,变换数据,高电平,保持数据稳定

【嵌入式学习-STM32F103-I2C】_第55张图片
由于高位先行,所以变换数据的时候,按照先放最高位,再放次高位等,最后最低位。依次把一个字节的每一位放在SDA线上,每放完一位后,执行释放SCL,拉低SCL的操作,驱动时钟运转。

按位与操作,取出某一位或某几位

【嵌入式学习-STM32F103-I2C】_第56张图片
写入一个字节
【嵌入式学习-STM32F103-I2C】_第57张图片

//发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));  //按位与的方式,取出数据的某一位或某几位
		MyI2C_W_SCL(1);  //释放SCL,从机就会立刻把我刚才放在SDA的数据读走,
		MyI2C_W_SCL(0);  //拉低SCL
	}
}

接收一个字节

【嵌入式学习-STM32F103-I2C】_第58张图片

接收一个字节时序开始时,SCL低电平,此时从机需要把数据放到SDA上,为了防止主机干扰从机写入数据,主机需要先释放SDA,释放SDA相当于切换为输入模式。在SCL低电平时,从机会把数据放到SDA上,如果从机想发1,就释放SDA,如果从机想发0,就拉低SDA。然后,主机释放SCL,在SCL高电平期间,读取SDA,再拉低SCL,低电平期间,从机就会把下一位数据放到SDA上。

【嵌入式学习-STM32F103-I2C】_第59张图片

以上过程重复8次,主机就能读到一个字节了。

读写分离:SCL低电平变换数据,高电平读取数据。低电平时间,定义为写的时间,高电平时间定义为读的时间。一二三木头人,主机控制SCL,低电平相当于123(可动SDA),高电平相当于木头人(不可动SDA),而起始终止SCL高电平必须动SDA。

【嵌入式学习-STM32F103-I2C】_第60张图片
发送应答和接收应答

【嵌入式学习-STM32F103-I2C】_第61张图片

问题1

【嵌入式学习-STM32F103-I2C】_第62张图片
先将SDA置1,在读取SDA,那应答位肯定是1,这样做有意义吗?

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

2、I2C是在进行通信,主机释放SDA,从机又不是在外面看戏,从机如果在的话,它是有义务在此时把SDA再拉低,所以即使上图主机把SDA置1,之后再读取SDA,读到的值可能为0.读到0,代表从机给应答,否则,没给应答。

问题2

【嵌入式学习-STM32F103-I2C】_第63张图片
上图不断读取SDA,但是for循环又没写过SDA,那SDA读出来应该始终是一个值?有意义吗?
I2C是在进行通信,通信是有从机的,当主机不断驱动SCL时钟时,从机有义务改变SDA的电平,所以主机每次循环读取SDA时,读取到的数据是从机控制的。这个数据也是从机想要给主机发送的数据。

检查从机是否有应答
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
#include "MyI2C.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;

int main(void)
  
  
  
{   
	/* 测试代码 */
	OLED_Init();
	
	MyI2C_Init();
	
	MyI2C_Start();
	MyI2C_SendByte(0xA0);  //1101 000 0
	uint8_t Ack = MyI2C_ReceiveAck();
	MyI2C_Stop();
	
	OLED_ShowNum(1,1,Ack,3);
	while(1)
	{
	
	}
}

代码
协议层
MyI2C.c
 #include "stm32f10x.h"                  // Device header
#include "Delay.h"


//给定参数1或0,就可以释放或拉低SCL
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
	Delay_us(10);  //引脚延时10us
}
//给定参数1或0,就可以释放或拉低SDA
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
	Delay_us(10);
}
//读SDA
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
	//读完后延时10us
	Delay_us(10);
	//返回读到SDA线的电平
	return BitValue;
}
/*软件I2C初始化(两个任务)
1、把SCL和SDA都初始化为开漏输出模式
2、把SCL和SDA置高电平
*/
void MyI2C_Init(void)
{
	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; //10是时钟线,11是数据线
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//把SCL和SDA置高电平,即释放总线,此时IIC总线处于空闲状态
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}

//注意:若不断调用SetBits和ResetBits来手动翻转高低电平,但这样做会在后面的程序出现非常多的地方来指定这个GPIO端口号
//一方面,语义不明显;另一方面,如果我们之后需要换一个端口,改动地方非常多,因此需要在程序开头定义,将端口号同一替换名字
//这样无论是语义和端口号修改都非常清晰

/* I2C 6个基本时序 */
//起始条件
void MyI2C_Start(void)
{
	//首先把SCL和SDA都确保释放
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	//先拉低SDA再拉低SCL
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}
//终止条件
void MyI2C_Stop(void)
{
	//为了确保SDA能释放SDA能产生上升沿,我们要在时序单元开始时,先拉低SDA,然后再释放SCL,释放SDA
	//终止条件后,SCL和SDA都回归到高电平
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

//发送一个字节时序开始时,SCL是低电平,
//实际上,除了终止条件,SCL以高电平结束,所有单元我们都会保证SCL以低电平结束,方便各个单元的拼接

//发送一个字节
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));  //按位与的方式,取出数据的某一位或某几位
		MyI2C_W_SCL(1);  //释放SCL,从机就会立刻把我刚才放在SDA的数据读走,
		MyI2C_W_SCL(0);  //拉低SCL
	}
}
//接收一个字节
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	//主机释放SDA,从机把数据放到SDA
	MyI2C_W_SDA(1);
	for (i = 0; i < 8; i ++)
	{
		//主机释放SCL,SCL高电平,主机就能读取数据,
		MyI2C_W_SCL(1);
		if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}  //如果第一次读SDA为1,则Byte |= 0x80,把Byte最高位置1,否则默认为0x00,如此类推
		//读取一位之后再把SCL拉低,从机就会把下一位数据放到SDA上
		MyI2C_W_SCL(0);
	}
	//最后把数据返回
	return Byte;
}
/*发送应答和接收应答*/
void MyI2C_SendAck(uint8_t AckBit)
{
	//函数进来时,SCL低电平,主机把AckBit放到SDA上,
	MyI2C_W_SDA(AckBit);
	//SCL高电平,从机读取应答位
	MyI2C_W_SCL(1);
	//SCL低电平,进入下一个时序单元
	MyI2C_W_SCL(0);
}

uint8_t MyI2C_ReceiveAck(void)
{
	//函数进来时,SCL低电平,
	uint8_t AckBit;
	//主机释放SDA,防止干扰从机, 同时从机把应答位放在SDA上
	MyI2C_W_SDA(1);
	//SCL高电平,主机读取应答位,
	MyI2C_W_SCL(1);
	AckBit = MyI2C_R_SDA();
	//SCL低电平,进入下一个时序单元
	MyI2C_W_SCL(0);
	//返回应答位
	return AckBit;
}

MyI2C.h
#ifndef __MYI2C_H
#define __MYI2C_H

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

驱动层
MPU6050.c
#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS		0xD0
//指定地址写
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)  //参数是 8 位的寄存器地址 和 8 位 的数据
{
	//起始
	MyI2C_Start();
	//发送从机地址
	MyI2C_SendByte(MPU6050_ADDRESS);
	//接收应答,这里不处理应答位了
	MyI2C_ReceiveAck();
	//寻址找到从机后,发送第二个字节,指定寄存器地址
	MyI2C_SendByte(RegAddress);
	//接收应答
	MyI2C_ReceiveAck();
	//发送第三个字节,指定写入指定寄存器的数据
	MyI2C_SendByte(Data);
	//接收应答
	MyI2C_ReceiveAck();
	//终止时序
	MyI2C_Stop();
}
//指定地址读
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();
	//发送一个字节,指定从机地址和读写位(此时为读->1)
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);
	//接收应答,接收应答后,总线控制权交给从机
	MyI2C_ReceiveAck();
	//从机开始发送一个字节,主机就调用接收一个字节,函数返回值就是接收到的数据
	Data = MyI2C_ReceiveByte();
	//主机接收到一个字节后,要给从机发送一个应答
	MyI2C_SendAck(1);
	//终止时序
	MyI2C_Stop();
	//将读到的数据返回
	return Data;
}
//MPU6050初始化
void MPU6050_Init(void)
{
    //在MPU6050初始化开始时就需要把底层通讯协议初始化号,再去初始化MPU6050硬件的寄存器
	MyI2C_Init();
	//配置电源管理寄存器1,解除睡眠,选择陀螺仪时钟
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	//配置电源管理寄存器2,6个轴均不待机
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
	//采样率分频,采样分频为10
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
	//配置寄存器,滤波参数给最大
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
	//陀螺仪配置寄存器,最大量程
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
	//加速度计配置寄存器,最大量程
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
	/*自此,配置完后
	陀螺仪内部就在连续不断地进行数据转换,输出的数据就存放在这里的数据寄存器里
	接下来,要想获取数据,只需要再写一个后去数据寄存器的函数即可*/
}

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
//获取数据的函数
//C语言中,函数的返回值只能有一个
/* 特殊操作来实现返回6个值的任务 */
/*
方法一:定义6个全局变量,子函数读到的数据直接写到全局变量里,
然后6个全局变量在主函数里进行共享

方法二:用指针,进行变量的地址传递,来实现多返回值

方法三:用结构体,对多个变量进行打包,然后再统一进行传递
*/
//函数里的6个参数,均是int16_t 的指针类型,之后在主函数定义变量,通过指针,
//把主函数的变量的地址传递到子函数来
//子函数中,通过传递过来的地址,操作主函数的变量
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
	uint8_t DataH, DataL;  //高8位,低8位
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL;  //高8位左移8位,移出去的数据不会丢失,它会自动进行数据转换得到16位数据 再 或上 低8位  得到一个16位的数据
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}

MPU6050.h
#ifndef __MPU6050_H
#define __MPU6050_H

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);

void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

#endif

MPU6050_Reg.h
/*用宏定义,将寄存器的地址都用一个字符串来表示
模块的东西一般加模块当作前缀
*/
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

#define	MPU6050_SMPLRT_DIV		0x19
#define	MPU6050_CONFIG			  0x1A
#define	MPU6050_GYRO_CONFIG		0x1B
#define	MPU6050_ACCEL_CONFIG	0x1C

#define	MPU6050_ACCEL_XOUT_H	0x3B
#define	MPU6050_ACCEL_XOUT_L	0x3C
#define	MPU6050_ACCEL_YOUT_H	0x3D
#define	MPU6050_ACCEL_YOUT_L	0x3E
#define	MPU6050_ACCEL_ZOUT_H	0x3F
#define	MPU6050_ACCEL_ZOUT_L	0x40
#define	MPU6050_TEMP_OUT_H		0x41
#define	MPU6050_TEMP_OUT_L		0x42
#define	MPU6050_GYRO_XOUT_H		0x43
#define	MPU6050_GYRO_XOUT_L		0x44
#define	MPU6050_GYRO_YOUT_H		0x45
#define	MPU6050_GYRO_YOUT_L		0x46
#define	MPU6050_GYRO_ZOUT_H		0x47
#define	MPU6050_GYRO_ZOUT_L		0x48

#define	MPU6050_PWR_MGMT_1		0x6B
#define	MPU6050_PWR_MGMT_2		0x6C
#define	MPU6050_WHO_AM_I		0x75

#endif

应用层
main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
#include "MyI2C.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;

int main(void)
{
	OLED_Init();
	MPU6050_Init();
	
	OLED_ShowString(1, 1, "ID:");
	//获取芯片ID号
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1, 4, ID, 2);
	
	while (1)
	{
		//在主循环里不断读取数据,不断刷新
		//读取6个变量的值
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
		//用OLED显示
		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);
	}
	
	
//	/* 测试代码 1*/
//	OLED_Init();
//	
//	MyI2C_Init();
//	
//	MyI2C_Start();
//	MyI2C_SendByte(0xD0);  //1101 000 0
//	uint8_t Ack = MyI2C_ReceiveAck();  //判断ACK的值就知道从机有没有给主机应答,0->应答,1->非应答
//	MyI2C_Stop();
//	
//	OLED_ShowNum(1,1,Ack,3);
//	while(1)
//	{
//	
//	}

/*把MPU6050当成一个存储器来使用,写某个存储器和读某个存储器,但是寄存器的每一位数据都对应了硬件电路的状态,寄存器和外设的硬件电路是可以进行互动的*/  
/*测试代码->读*/

//	OLED_Init();
//	MPU6050_Init();
//	uint8_t ID = MPU6050_ReadReg(0x75);
//	
//	OLED_ShowNum(1,1,ID,3);

/* 测试代码->写(需要接触MPU6050睡眠模式)*/
//	OLED_Init();
//	MPU6050_Init();
//	
//	MPU6050_WriteReg(0x6B,0x00);
//	
//	MPU6050_WriteReg(0x19,0x66);
//	
//	uint8_t ID = MPU6050_ReadReg(0x19);
//	
//	OLED_ShowHexNum(1,1,ID,2);

}

实验效果

【嵌入式学习-STM32F103-I2C】_第64张图片
由于初始化配置满量程是最大16g
【嵌入式学习-STM32F103-I2C】_第65张图片
x就是测量值,为0.95g,机z轴的加速度值是0.95g,标准答案为1g

硬件I2C读写MPU6050

像以下这种外设模块引出来的引脚,一般借助GPIO口的复用模式与外部世界相连,具体复用在那个GPIO口呢?查询引脚定义表。

【嵌入式学习-STM32F103-I2C】_第66张图片
因为内部电路设计的时候,这些引脚已经连接好。如果要使用硬件I2C,就只能使用它连接好的指定引脚。

【嵌入式学习-STM32F103-I2C】_第67张图片

SDA部分

数据收发的核心部分是数据寄存器和数据移位寄存器
【嵌入式学习-STM32F103-I2C】_第68张图片

发送流程

当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR里,当移位寄存器没有移位时,这个数据寄存器的值就会进一步转到移位寄存器里面来。在移位过程中,我们可以直接把下一个数据放到数据寄存器里面等着。一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器TXE位为1,表示发送数据寄存器为空

【嵌入式学习-STM32F103-I2C】_第69张图片

接收流程

输入的数据,一位一位地从引脚移入到数据移位寄存器。
【嵌入式学习-STM32F103-I2C】_第70张图片
当一个字节的数据接收之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空。这是我们就可以把数据从数据寄存器里读出来。
【嵌入式学习-STM32F103-I2C】_第71张图片

SCL部分

【嵌入式学习-STM32F103-I2C】_第72张图片
【嵌入式学习-STM32F103-I2C】_第73张图片
以上两个GPIO口都要配置成复用开漏输出的模式。复用,就是GPIO口的状态是交由片上外设来控制的,开漏输出,这时I2C协议要求的端口配置。

I2C输出

我们要使用开漏输出,所以P-Mos是没有的,移位寄存器输出的数据,通向GPIO,就接在(来自片上外设)的位置,之后控制N-Mos的通断,进而控制I/O引脚,是拉低到低电平还是释放悬空。
在这里插入图片描述
【嵌入式学习-STM32F103-I2C】_第74张图片

I2C输入

【嵌入式学习-STM32F103-I2C】_第75张图片

软硬件波形对比

【嵌入式学习-STM32F103-I2C】_第76张图片
SCL低电平写,高电平读。

代码实现
步骤

1、配置I2C外设,对I2C2外设进行初始化

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

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

配置I2C外设,参考以下两张图
【嵌入式学习-STM32F103-I2C】_第77张图片
【嵌入式学习-STM32F103-I2C】_第78张图片
实现读写时序,参考一下两张图,主机发送和主机接收的流程图
【嵌入式学习-STM32F103-I2C】_第79张图片
【嵌入式学习-STM32F103-I2C】_第80张图片

配置IIC外设

1、开启I2C外设和对应GPIO口的时钟

2、把I2C外设对应的GPIO初始化为复用开漏模式

3、使用结构体对整个I2C进行配置

4、I2C_Cmd,使能I2C

ctrl+alt+空格:代码提示

50Khz(标准速度)

1101 0000 ,低电平比高电平是1:1,即50%占空比方波

【嵌入式学习-STM32F103-I2C】_第81张图片

100Khz(标准速度)

可以观察到SCL和SDA的下降沿变化是非常快的,但是在他们的上升沿,缓慢上升
【嵌入式学习-STM32F103-I2C】_第82张图片

杆子弹簧模型

我们这个线就是一根杆子,它由一根弹簧默认拉到高电平。当我们输出低电平时,我们要强下拉,也就无穷大的力,因此下降沿迅速且果断。但是输出高电平,我们是释放了杆子,杆子通过弹簧拉回到高电平。弹簧是一个弱上拉,上升沿就有一个回弹的过程。波形就会相对缓慢地上去。缓慢变化的上升沿有什么影响呢

101Khz(快速状态)

此时I2C会对SCL占空比进行调节,低电平比高电平,由原来的1:1变为大概2:1.增大了低电平时间占整个周期的比例,为什么增大低电平比例呢?因为低电平数据变化,高电平数据读取,数据变化需要一定时间来翻转波形。尤其是数据的上升沿变化比较慢。所以在快速传输的状态下,需要给低电平多分配一些资源。要不然低电平数据变化来不及,高电平数据读取也没用。

【嵌入式学习-STM32F103-I2C】_第83张图片

200Khz

由于时间轴尺度进一步缩小,这时弯弯的上升沿就更加明显。如果SCL时钟低电平时间不充足,它可能都来不及进行数据变化。
【嵌入式学习-STM32F103-I2C】_第84张图片
【嵌入式学习-STM32F103-I2C】_第85张图片

400Khz(快速模式)

从这里释放SCL,SCL还没完全回弹到高电平,就立刻拉下来传输下一个数据。这个弯弯拖后腿,限制了I2C总线的最大传输速度。
【嵌入式学习-STM32F103-I2C】_第86张图片

硬件I2C写寄存器

【嵌入式学习-STM32F103-I2C】_第87张图片

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	//1、产生起始条件
	I2C_GenerateSTART(I2C2, ENABLE);
	//对于硬件I2C(非阻塞式),在函数结束后,都要等待相应的标志位,来确保函数的操作执行到位
	//while循环加多了,一旦总线出问题,就很容易造成整个程序卡死
	//while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	//2、发送从机地址,接收应答;在库函数中,发送数据都自带了接收应答的过程,同样接收数据也自带了发送应答的过程,如果应答错误
	//硬件会通过标志位和中断来提示我们
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	//EV6事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	//程序写完DR后,还是需要例行检查EV8事件(尽管该过程很快)
	I2C_SendData(I2C2, RegAddress);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);  //EV8_1
	//发送data
	I2C_SendData(I2C2, Data);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);  //EV8_2
	//由于data是最后一个字节,发送完data后就需要产生终止条件
	I2C_GenerateSTOP(I2C2, ENABLE);
}
硬件I2C读寄存器

【嵌入式学习-STM32F103-I2C】_第88张图片

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	
	I2C_SendData(I2C2, RegAddress);
	//如果用Transmitting,那实际这个事件发生时,
	//regaddress的波形其实还没有完全发送完毕,这时再直接产生重复起始条件
	//会不会把该数据截断呢-->不会
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	
	
	//在指定地址后,需要生成重复起始条件
	I2C_GenerateSTART(I2C2, ENABLE);
	//等待EV5事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	//发送从机地址,读写位是读的方向,I2C_Direction_Receiver自动将MPU6050_ADDRESS最低位置1
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
	//等待EV6(主机接收)
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
	
	//进入到主机接收的模式后,就开始接收从机发的数据波形了
	//再接收最后一个字节之前,就要提前把ACK置0,同时设置停止位STOP(接收一个字节的情况)
	//在进入接收模式之后,就要立刻ACK置0,STOP置1
	I2C_AcknowledgeConfig(I2C2, DISABLE);
	I2C_GenerateSTOP(I2C2, ENABLE);
	
	//等待EV7事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
	//读取DR的数据
	Data = I2C_ReceiveData(I2C2);
	//最后将ACK置1,默认状态下ACK为1,给从机应答,在接收最后一个字节之前,临时把ACK置0,给非应答,方便指定地址接收多个字节
	I2C_AcknowledgeConfig(I2C2, ENABLE);
	
	return Data;
}
解决死循环问题

【嵌入式学习-STM32F103-I2C】_第89张图片
大量的死循环等待,在程序中是比较危险的,一旦有一个事件一直没有产生,就会让整个程序卡死。对于死循环等待,我们可以给他加一个超时退出机制。

【嵌入式学习-STM32F103-I2C】_第90张图片
封装以上函数

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t Timeout;
	Timeout = 10000;
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
}

【嵌入式学习-STM32F103-I2C】_第91张图片

代码
main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;

int main(void)
{
	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);
	}
}

MPU6050.c
#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS		0xD0

void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t Timeout;
	Timeout = 10000;
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
	{
		Timeout --;
		if (Timeout == 0)
		{
			break;
		}
	}
}

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	//1、产生起始条件
	I2C_GenerateSTART(I2C2, ENABLE);
	//对于硬件I2C(非阻塞式),在函数结束后,都要等待相应的标志位,来确保函数的操作执行到位
	//while循环加多了,一旦总线出问题,就很容易造成整个程序卡死
	//while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	//2、发送从机地址,接收应答;在库函数中,发送数据都自带了接收应答的过程,同样接收数据也自带了发送应答的过程,如果应答错误
	//硬件会通过标志位和中断来提示我们
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	//EV6事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	//程序写完DR后,还是需要例行检查EV8事件(尽管该过程很快)
	I2C_SendData(I2C2, RegAddress);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);  //EV8_1
	//发送data
	I2C_SendData(I2C2, Data);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);  //EV8_2
	//由于data是最后一个字节,发送完data后就需要产生终止条件
	I2C_GenerateSTOP(I2C2, ENABLE);
}

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	I2C_GenerateSTART(I2C2, ENABLE);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
	
	I2C_SendData(I2C2, RegAddress);
	//如果用Transmitting,那实际这个事件发生时,
	//regaddress的波形其实还没有完全发送完毕,这时再直接产生重复起始条件
	//会不会把该数据截断呢-->不会
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
	
	
	//在指定地址后,需要生成重复起始条件
	I2C_GenerateSTART(I2C2, ENABLE);
	//等待EV5事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
	
	//发送从机地址,读写位是读的方向,I2C_Direction_Receiver自动将MPU6050_ADDRESS最低位置1
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
	//等待EV6(主机接收)
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
	
	//进入到主机接收的模式后,就开始接收从机发的数据波形了
	//再接收最后一个字节之前,就要提前把ACK置0,同时设置停止位STOP(接收一个字节的情况)
	//在进入接收模式之后,就要立刻ACK置0,STOP置1
	I2C_AcknowledgeConfig(I2C2, DISABLE);
	I2C_GenerateSTOP(I2C2, ENABLE);
	
	//等待EV7事件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
	//读取DR的数据
	Data = I2C_ReceiveData(I2C2);
	//最后将ACK置1,默认状态下ACK为1,给从机应答,在接收最后一个字节之前,临时把ACK置0,给非应答,方便指定地址接收多个字节
	I2C_AcknowledgeConfig(I2C2, ENABLE);
	
	return Data;
}
/*
### 配置IIC外设

1、开启I2C外设和对应GPIO口的时钟

2、把I2C外设对应的GPIO初始化为复用开漏模式

3、使用结构体对整个I2C进行配置

4、I2C_Cmd,使能I2C
*/

void MPU6050_Init(void)
{
	//1、开启I2C外设和对应GPIO口的时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	//把I2C外设对应的GPIO初始化为复用开漏模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;//开漏是I2C协议的设计要求,复用就是GPIO的控制权要交给硬件外设
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	//初始化I2C2外设
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;  //I2C模式
	I2C_InitStructure.I2C_ClockSpeed = 50000;   //时钟速度,数值越大,SCL频率越高,数据传输就越快,此处设置50KHz
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;  //时钟占空比,只有在时钟频率大于100Khz,进入到快速状态时才有用,此处设置2,就是低电平时间和高电平时间是2:1的比例关系
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;  //指定STM32作为从机,可以响应几位的地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00;  //stm32作为从机使用的自身地址
	I2C_Init(I2C2, &I2C_InitStructure);
	//使能I2C2
	I2C_Cmd(I2C2, ENABLE);  
	
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
	MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}

uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

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;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}

MPU6050.h
#ifndef __MPU6050_H
#define __MPU6050_H

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);

void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

#endif

补充

1、为什么IIC采用开漏输出和弱上拉

I2C(Inter-Integrated Circuit)总线采用开漏输出和弱上拉电阻的设计有以下原因:

1、开漏输出:I2C总线是一种多主机、多从机的串行通信协议,多个设备可以共享同一条总线。开漏输出意味着输出信号可以被多个设备进行共享,当设备输出低电平时,它可以将总线拉低,但当设备输出高电平时,它并不强制将总线拉高,而是通过总线上的外部上拉电阻使总线恢复到高电平。这种设计使得多个设备能够共享总线,并且设备之间可以进行逻辑“与”操作,保证总线上的信号传输可靠。

2、弱上拉电阻:I2C总线的时钟和数据线都采用了弱上拉电阻。这些上拉电阻提供了默认的高电平信号,当总线上没有设备输出低电平时,总线会被上拉到高电平状态。弱上拉电阻的作用是保证总线在空闲状态时保持高电平,避免出现信号冲突。当设备需要将总线拉低时,它会通过开漏输出的方式将总线拉低,而不会与上拉电阻产生直接的冲突。

综上所述,I2C总线采用开漏输出和弱上拉电阻的设计是为了在多设备共享总线的情况下,实现可靠的信号传输和避免信号冲突。开漏输出允许多个设备共享总线并进行逻辑“与”操作,而弱上拉电阻提供默认的高电平信号,保证总线在空闲状态时保持高电平。

你可能感兴趣的:(嵌入式学习-STM32,stm32,单片机,学习)