前文说到了12C的协议规定和通信意义,并且也用GPIO口模拟的I2C,实现了读写MPU6050的程序,在这个过程中可以发现,通信协议的时序是一个很重要的东西,只要理解清楚了这个时序的意,就可以按照协议的规定,去翻转通信引脚的高低电平,只要翻转产生的这个时序波形,满足了通信协议的规定,那通信双方就能理解并解析这个波形,这样,通信自然而然就实现了。
上一篇文章用的是软件 12C,手动拉低或释放时钟线,然后再手动对每个数据位进行判断,拉低或释放数据线,这样来产生时序的波形。由于12C是同步时序,这每一位的持续时间要求不严格,某一位的时间长点短点,或者中途暂停一会儿时序,影响都不大,所以I2C是比较容易用软件模拟的,在实际项目中,软件模拟的I2C也是非常常见的。但是作为一个协议标准,12C通信,也是可以有硬件收发电路的,就像之前的串口通信一样,先了解了这个串口的时序波形,但是在程序中,并没有用软件去手动翻转电平来实现这个波形,这是因为串口是异步时序,每一位的时间要求很严格,不能过长也不能过短,更不能中途暂停一会儿,所以串口时序虽然可以用软件模拟,但是操作起来比较困难,前文也没有说过软件模拟的串口,所以就没有手动翻转电平这个操作。另外,由于串口的硬件收发器在单片机中的普及程度非常高,基本上每个单片机都有串口的硬件资源,而且硬件实现的串口使用起来还非常简单,所以,串口通信,基本都是借助硬件收发器来实现的。
12C也可以有软件模拟和硬件收发器自动操作,这两种收发方式,对于串口这样的异步时序呢,软件实现,非常麻烦,硬件实现,非常简单,所以串口的实现基本是全都倒向硬件了,而对于12C这样的同步时序来说,软件实现,反而简单且灵活,硬件实现,相比之下,却并不能完全让人省心,所以12C的实现,软件模拟的情况还是非常多的,但是考虑到硬件12C也有很多独有的优势,比如,执行效率比较高,可以节省软件资源,功能比较强大,可以实现完整的多主机通信模型,时序波形规整,通信速率快,等等。如果只是简单应用,可以选择比较灵活的软件12C;如果对性能指标要求比较高,就可以考虑一下硬件12C。硬件 I²C 资源存在一定限制,因为它受到硬件电路支持的限制。而软件 I²C 资源的限制相对较小,只要代码存储空间足够,基本上可以随意开辟新的总线。
一、I2C外设简介
STM32 内部集成的 I2C 收发电路功能
-
STM32 内部集成了硬件 I2C 收发电路。这些电路能够自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能。通过硬件实现这些功能,可以减轻 CPU 的负担,另外由硬件来做这个事情,可以更加专注,时序生成的性能、效率也会更高。
-
硬件可以自动翻转引脚电平,软件呢,只需要写入控制寄存器CR和数据寄存器DR,就可以实现协议了。为了实时监控时序的状态,软件还得读取状态寄存器SR,来了解外设电路当前处于什么状态。
-
STM32有了库函数的封装之后,调用库函数给个参数,库函数就能自动配置或读取各种寄存器了。
硬件I2C的功能介绍
#### **支持多主机模型**
* STM32 的 I2C 通信分为主机(拥有主动控制总线的权利)和从机(只能在主机允许的情况下,才能控制总线),STM32 的 I2C外设支持多主机模型,这意味着在一个 I2C 总线上可以有多个主机设备进行通信。在 I²C 通信中,存在主机和从机的区分。在一主多重模型下,主机具有控制权,从机则听从主机的指令。而在进阶版的多主机模型中,有固定多主机和可变多主机模型。
* 固定多主机模式就是这条总线上,有两个或更多个固定的主机,这个状态,就像是在教室里,讲台上同时站了多个老师,下面坐的所有学生,可以被任意一个老师点名,老师可以主动发起对学生的控制,学生不能去控制老师,当两个老师同时想说话时,就是总线冲突状态,这时就要进行总线仲裁了,仲裁失败的一方让出总线控制权。
* STM32 采用的是可变多主机模型,总线上没有固定的主机和从机,这意味着任何设备在总线空闲时都可以成为主机,然后指定其他任何一个设备进行通信,当这个通信完成之后,这个跳出来的主机就要退回到从机的位置。这就像是在教室里,只有一堆学生,没有老师,默认情况下,所有学生都是从机,都不能说话,当有某个学生想说话时,就得跳出来,变成主机,然后指定其他任何一个学生进行通信,通信完成后,再坐下,变为从机,当有多个学生同时跳出来时,就是总线冲突状态,这时就要进行总线仲裁,仲裁失败的一方让出总线控制权。
#### **支持 7 位 / 10 位地址模式**
* 该 I2C 外设支持 7 位和 10 位地址模式。7 位地址模式较为常用,而 10 位地址模式在设备数量较多时可以提供更多的地址选择。
* 在 I²C 时序中,七位地址模式较为常用,它简单且常见,但是这种模式下只有 128 种地址情况。当设备数量较多时,七位地址模式可能就不够用了。针对这种情况,可以通过配置地址低位、开辟多条总线等方式来解决。而 STM32 的 I²C 总线支持十位地址模式,这种模式下最多有 1024 种可能。
* 而10位地址模式,它是通过规定起始后的前两个字节作为寻址来实现的,剩余的五位当做标志位。这样设计是为了表示在发送完第一个字节后表明后面的字节仍然是寻址,所以在第一个字节写个特定的数据,作为10位寻址模式的标志位,这个标志位就是11110,也就是如果你第二个字节也是寻址,那第一个字节的前5位就必须是11110。综上第一个字节的剩下两位和第二个字节的八位构成这十位地址。同样11110作为十位地址的标志位是不会作为七位地址的前五位出现的。
#### **支持不同的通讯速度**
* 支持不同的通讯速度,包括标准速度(高达 100 kHz)和快速速度(高达 400 kHz)。不同的速度可以根据实际应用需求进行选择。
* 速度,是协议规定的标准速度,也就是说,如果某个设备声称支持快速的12C,那它就支持最大400kHz的时钟频率。但是作为一个同步协议,这个时钟并不严格,所以只要不超过这个最大频率多少都是可以的,所以这个频率的具体值,一般关注不多。
#### **支持 DMA**
* 该 I2C 外设支持 DMA(直接内存访问),通过 DMA 可以在不占用 CPU 资源的情况下进行数据传输,在多字节传输的时候可以提高传输效率。
* 比如指定地址读多字节或写多字节的时序,如果想要连续读或写非常多的字节,那用一下DMA自动转运数据,这个过程的效率就会大大提升。如果只有几个字节,就没有必要使用DMA了。
#### **兼容 SMBus 协议**
* STM32 的 I2C 外设兼容 SMBus(System Management Bus 系统管理总线)协议,这使得它可以与符合 SMBus 协议的设备进行通信。
* SMBus是基于12C总线改进而来的,主要用于电源管理系统中,由于SMBus和I2C十分相似,所以STM32就顺便兼容了一下SMBus协议。
STM32F103C8T6 硬件 I2C 资源
-
对于 STM32F103C8T6 型号,其硬件 I2C 资源包括 I2C1 和 I2C2。这意味着该型号的芯片有两组硬件 I2C 接口可以使用。
-
这里资源的限制也是硬件12C和软件12C的区别之一,因为硬件12C必须要有硬件电路的支持,所以硬件12C的资源是有限的,但是对于软件I2C资源一般没有很大的限制,只需要复制一下代码就可以开辟一条新的I2C总线,所以软件12C,只要代码能存的下,基本上是想开几路就开几路,没有资源的限制。
二、I2C设计框图
I2C 功能框图概述
I2C 的功能框图是理解 I2C 工作原理的重要工具。图中展示了从数据输入到最终信号输出过程中,各个功能模块之间的连接与协作关系。
输入端口
SDA 与 SCL
-
在 I2C 功能框图的左侧,有两个关键的输入端口,即 SDA(数据线)和 SCL(时钟线)。这两条线是 I2C 通信的基础,所有的数据传输和控制操作都依赖于它们。
-
SDA 线负责数据的传输,它连接到一个名为 "DATA REGISTER" 的数据寄存器。视频中可能会提到,通过 SDA 线传输的数据会首先被暂存在数据寄存器中,等待后续处理。
-
SCL 线则负责提供时钟信号,它连接到 "时钟控制" 模块。时钟信号对于数据的同步传输至关重要,它决定了数据传输的速率和时序。
-
这种外设模块引出来的引脚,以般都是借用GPIO口的复用模式与外部世界相连的,具体的复用引脚需要查看引脚定义表。因为内部电路设计的时候,这些引脚就是连接好了的,所以如果想使用硬件I2C,就只能使用它连接好的指定引脚,不像软件12C那样,引脚可以任意指定,硬件I2C,引脚就是固定的这几个不能任意更改,所以硬件12C,对引脚的限制也比较大。I2C的两个输入引脚还有重映射的机会
中间的寄存器和控制模块
数据处理相关模块
-
从"数据寄存器(DATA REGISTER)"开始,数据通过箭头连接到 "数据控制" 模块。在这个过程中,数据会经过各种处理。(数据收发的核心部分)
-
"数据移位寄存器" 在数据传输过程中起到了关键作用。它能够对数据进行移位操作,是为了满足 I2C 协议对数据格式的要求。(数据收发的核心部分)
-
"比较器" 模块用于判断在STM32作为从机时所设置的自身地址,是否与收到的寻址相同。(本程序不需要使用,了解即可)
-
"帧错误校验 (PEC) 计算" 模块是保障数据传输正确性的重要部分。它通过特定的算法对多字节的数据帧硬件可以自动执行CRC校验计算,防止数据在传输过程中出现错误。(本程序不需要使用,了解即可)
-
**当需要发送数据时:**可以把一个字节数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步,转到移位寄存器里,在移位的过程中,就可以直接把下一个数据放到数据寄存器里等着了,一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空。
-
**当需要接受数据时:**输入的数据,一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空,这时就可以把数据从数据寄存器读出来了。
-
**总结:**这里的收发数据流程与串口那一节的流程基本一样,只不过串口是全双工,这里数据收和发是分开的;I2C是半双工,所以数据收发,是同一组数据寄存器和移位寄存器,但是这个数据寄存器和移位寄存器的配合,设计思路都是异曲同工。
地址相关寄存器
-
"自身地址寄存器" 和 "双地址寄存器" 在 I2C 通信中用于设备的寻址,是在从机模式下使用的,前文介绍了有关STM32多主机模式的内容,当STM32不进行通信的时候就是从机既然作为从机,它就应该可以被别人召唤,想被别人召唤,它就应该有从机地址,这个从机地址就是由自身地址寄存器指定,可以自定一个从机地址,写到这个寄存器,当STM32作为从机,在被寻址时,如果收到的寻址通过比较器判断,和自身地址相同,那STM32就作为从机,响应外部主机的召唤。并且这个STM32支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器,(本程序不需要使用,了解即可)
-
"帧错误校验 (PEC) 寄存器" 与帧错误校验 (PEC) 计算模块相关联,用于存储校验结果,以便后续检查数据帧是否存在错误,CRC算法在经过数据校验之后,然后会得到一个字节的校验位,附加在这个数据帧后面,在接收到这一帧数据后,STM32的硬件也可以自动执行校验的判定,如果数据在传输的过程中出错了,CRC校验算法就通不过,硬件就会置校验错误标志位。(本程序不需要使用,了解即可)
控制和状态寄存器
-
"时钟控制寄存器 (CCR)" 用于控制时钟的频率和其他相关参数。通过对 CCR 的设置,可以调整 I2C 通信的速度,以适应不同的应用场景。
-
"控制寄存器 (CR1&CR2)" 是整个 I2C 操作的核心控制部分,它可以决定数据传输的启动、停止、模式选择等操作。写入该寄存器,可以对整个电路进行控制。
-
"状态寄存器 (SR1&SR2)" 用于反映 I2C 当前的工作状态。通过读取状态寄存器,系统可以了解 I2C 是否处于空闲、传输中、出错等状态,从而做出相应的处理。
控制逻辑电路及相关模块
控制逻辑电路
-
在框图的右下角是 "控制逻辑电路" 模块。它是整个 I2C 功能框图的控制核心,协调各个模块的工作。
-
它连接到 "中断" 和 "DMA 请求与响应" 模块。当 I2C 操作过程中出现特定事件时,例如数据传输完成、出现错误等,会触发中断信号。视频中可能会介绍如何编写中断服务程序来处理这些中断事件,以确保 I2C 通信的稳定性和可靠性。
-
对于 "DMA 请求与响应" 模块,在需要进行大量数据传输时,通过 DMA(直接内存访问)可以提高数据传输效率。控制逻辑电路会根据需要向 DMA 模块发出请求,并处理相应的响应,视频中可能会有相关的演示或代码示例。
输出端口
SMBALERT 信号输出端口
- 在框图的最右侧有一个 "SMBALERT" 信号输出端口。这个端口是SMBus协议使用用于向外部系统发出特定的警报信号的。本文不涉及不需要过多了解。
三、I2C基本结构图
整体结构
I2C 基本结构概述
-
图中呈现了 I2C 的基本结构,主要由几个关键模块组成,包括时钟控制器、数据控制器、移位寄存器、数据寄存器 DR、开关控制以及与外部连接的 GPIO(General - Purpose Input/Output)端口。
各模块功能
时钟控制器(Clock Controller)
-
时钟控制器通过 GPIO 连接到 SCL(Serial Clock Line)引脚,控制时钟线。它负责产生 I2C 通信所需的时钟信号。由于图中是一主多从的模型,所以这里的时钟只有输出方向;如果是多主机模型,时钟线也是可以输入的。
-
在 I2C 通信中,时钟信号是至关重要的,它决定了数据传输的速率和时序。时钟控制器确保 SCL 线上的时钟信号符合 I2C 协议的要求,例如标准模式下时钟频率可达 100kHz,快速模式下可达 400kHz 等。
数据控制器(Data Controller)
-
数据控制器是另一个关键模块,它与数据寄存器 DR 和移位寄存器相连。
-
其主要功能是控制数据的传输和处理。它从数据寄存器 DR 中获取数据,并通过移位寄存器将数据逐位发送到 SDA(Serial Data Line)线上,或者从 SDA 线上接收数据并存储到数据寄存器 DR 中。
移位寄存器(Shift Register)
-
移位寄存器位于数据控制器和数据寄存器 DR 之间。
-
它的作用是在数据传输过程中对数据进行移位操作。在发送数据时,它将数据寄存器 DR 中的数据逐位移动到 SDA 线上;在接收数据时,它将 SDA 线上的逐位数据移动到数据寄存器 DR 中。
-
因为12C是高位先行,所以移位寄存器是向左移位,在发送的时候,最高位先移出去,然后是次高位,等等,SCL时钟,移位一次,移位8次,这样就能把一个字节,由高位到低位,依次放到SDA线上了。在接收的时候,数据通过GPIO口从右边依次移进来,最终移8次,一个字节就接收完成了。
数据寄存器 DR(Data Register DR)
-
数据寄存器 DR 是存储数据的地方。
-
它与数据控制器和移位寄存器紧密相连,用于临时存储要发送或已经接收的数据。在发送数据时,数据先被放入数据寄存器 DR,然后通过移位寄存器逐位发送;在接收数据时,从 SDA 线接收的数据先通过移位寄存器存储到数据寄存器 DR 中,再由数据控制器进行处理。
开关控制(Switch Control)
-
开关控制模块在图中也有显示。
-
它可能用于控制 I2C 通信的开启和关闭,或者在不同的操作模式之间进行切换。例如,在需要进行数据传输时打开 I2C 通道,在不需要时关闭以节省功耗等。
与外部连接
GPIO 端口
-
图中显示时钟控制器和数据控制器分别通过 GPIO 端口连接到 SCL 和 SDA 线。GPIO 端口提供了与外部 I2C 设备连接的接口,通过这些接口实现与外部设备的数据和时钟信号的交互。
-
使用硬件12C的时候,这里的两个GPIO口都要配置成复用开漏输出模式,复用,就是GPIO的状态是交由片上外设来控制的,开漏输出,这是I2C协议要求的端口配置。这里即使是开漏输出模式,GPIO口也是可以进行输入的。
-
输出数据,通过GPIO 连接到 SDA,输出到端口,输入数据,也是通过GPIO,输入到移位寄存器。那输入和输出数据的两个箭头连接在GPIO的哪里呢这里需要参考前面说过的复用开漏/推挽输出模式的结构图。对于输出部分,因为要使用开漏输出,所以这个P-MOS是没有的,其中移位寄存器输出的数据,通向GPIO接在图中"来自片上外设"这一部分,之后控制N-MOS的通断,进而控制这个I/O引脚,是拉低到低电平,还是释放悬空。然后对于输入部分,可以看到,虽然这是复用开漏输出,但是输入这一路仍然有效,I/O引脚的高低电平,通过"进入片上外设"这一部分,来进行复用功能输入。所以,I2C外设通向GPIO,输出接在"来自片上外设"这一部分,输入接在"进入片上外设"这一部分。
四、硬件I2C操作流程
下面两张图展示的是主机发送和主机接收的操作流程,这两张流程图告诉了我们要想产生这样的12C时序,什么时候需要哪些操作,这就是写程序需要参考的流程。
主机发送
当STM32想要执行指定地址写的时候,就要按照这个主发送器传送序列图来进行,这里有7位地址的主发送和10位地址的主发送,它们的区别就是,7位地址,起始条件后的一个字节是寻址,10位地址,起始条件后的两个字节都是寻址,其中前一个字节,这里写的是帧头,内容是5位的标志位1110+2位地址+1位读写位,然后后一个字节,内容就是纯粹的8位地址了,两个字节加一起,构成10位的寻址,但是最主要关注的还是7位地址寻址即可。
对于传送序列图,因为12C协议只规定了起始之后必须是寻址,至于后面数据的用途,并没有明确的规定,这些数据可以由各个芯片厂商自己来规定,比如MPU6050规定就是,寻址之后,数据1为指定寄存器地址,数据2为指定寄存器地址下的数据,之后的数据N,就是从指定寄存器地址开始,依次往后写,这就是一个典型的指定地址写的时序流程。
1. 主发送器传送序列图概述
图的标题为 " 主发送器传送序列图",此图专注于展示 I2C 主发送器在数据传输时的时序情况,且涵盖 7 位主发送和 10 位主发送这两种地址模式。
2. 7 位主发送详细过程
首先,初始化之后,总线默认空闲状态,STM32默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器,经过查看手册后得知,在控制寄存器CR1中,有个START位,在这一位写1,就可以产生起始条件了,当起始条件发出后,这一位可以由硬件清除,所以,只要在这一位写1,STM32就自动产生起始条件了。这就像写入控制寄存器,就像是踩油门刹车,来控制硬件运行。
之后,STM32由从模式转为主模式,也就是多主机模型下,STM32有数据要发,就要跳出来,这个意思。之后便开始进入传送序列图。
(一)起始条件(S)
发送操作
-
当 I2C 主发送操作开始时,首先产生起始条件(S)。这一条件如同数据传输的 "发令枪",标志着整个传输过程的正式启动。在硬件层面,这涉及到 I2C 总线的特定信号变化,确保从机能够识别到主机即将开始发送数据。
事件 EV5 处理
- 在发送起始条件后,会触发事件 EV5。这是因为在 I2C 通信的控制逻辑中,当起始条件产生(SB = 1)后,需要通过特定操作来确保后续数据传输的顺利进行。此时,操作要求读取 SR1(状态寄存器),然后将地址写入 DR 寄存器,以此清除事件 EV5。这一系列操作是为了保证 I2C 控制器能够正确地识别和处理下一个数据传输步骤。此外状态寄存器是不需要手动清除的
(二)地址(A)
发送操作
- 接着,主机发送 7 位地址,并在地址后紧跟一位读写位到数据寄存器(DR)中之后硬件电路就会自动把这一个字节,转到移位寄存器里,再把这一个字节发送到I2C总线上。由于是主发送操作,读写位为 0,表示写操作,硬件会自动接收应答并判断。
事件 EV6 处理
- 在寻址完成之后,会触发事件 EV6,就是ADDR标志位为1。因为在这种情况下,当地址已发送且收到从机的应答(ADDR = 1)时,需要通过读取 SR1 然后读取 SR2 的操作来清除事件 EV6,以保证 I2C 通信的正常进行。这一事件发生在地址发送阶段,确保主机能够确认从机已经正确接收到地址。经过查阅手册得知,ADDR标志位在主模式状态下就代表地址发送结束。
事件EV8_1处理
- EV6事件结束后,是EV8_1事件,EV8_1事件就是TxE标志位=1,移位寄存器空,数据寄存器空,这时需要我们写入数据寄存器DR进行数据发送了,一旦写入DR之后,因为移位寄存器也是空,所以DR会立刻转到移位寄存器进行发送,之后就会进入到EV8事件。
(三)数据 1 - 数据 N
逐个数据发送
- 随后,主机依次发送数据 1 到数据 N。在每发送一个数据字节的过程中,会产生事件 EV8。
事件 EV8 的处理机制
-
事件 EV8 的触发条件是数据寄存器为空(TxE = 1),移位寄存器非空,数据寄存器空。这意味着在当前数据字节发送完成后,数据寄存器已无数据可供继续发送。此时,需要等待数据寄存器重新被写入数据才能发送下一个字节,就是移位寄存器正在发数据的状态。这一机制确保了数据在 I2C 总线上能够有序、稳定地传输,避免数据冲突和传输错误。
-
EV8_1: 根据观察传送序列图可知,在EV8事件结束之后,紧接着的应答位也结束之后,数据2和EV8是同步发生的,可以得知,当EV8_1事件结束后,数据寄存器就把里面的数据1全部转移到移位数据寄存器一位一位的发送,与此同时的数据2也已经进入到了数据寄存器中,等移位寄存器清空之后,立马进入移位数据寄存器,并且同步发生EV8,继续写入数据3,以此类推。也表示一旦检测到EV8事件,就可以写入下一个数据了。
-
**EV8_2:**最后,当我想要发送的数据写完之后,这时就没有新的数据可以写入到数据寄存器了,当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,这就说明主机不想发了,这时就代表字节发送结束是时候停止了,所以在这里,当检测到EV8_2时,就可以产生终止条件了。
(四)停止条件(P)
当所有数据发送完毕后,主机产生停止条件(P)。这个停止条件标志着此次 I2C 主发送操作的数据传输部分结束。在硬件上,停止条件同样会引起 I2C 总线的特定信号变化,通知从机数据传输已经完成,I2C 总线可以进入空闲状态或准备下一次数据传输操作。
在控制寄存器(CR1)中操作STOP位置1即可。
(五)总结:
写入控制寄存器CR或者数据寄存器DR,可以控制时序单元的发生,比如产生起始条件,发送一个字节数据,时序单元发生后,检查相应的EV事件,其实就是检查状态寄存器SR,来等待时序单元发送完成,然后依次按照这个流程,操作,等待,操作,等待,等等等,这样就能实现时序了。
3. 事件详细说明
(一)事件 EV5
条件
- 当起始条件已产生,即 SB = 1 时,事件 EV5 被触发。这是 I2C 主发送操作开始时的一个重要事件,标志着主机开始准备发送地址。
操作
- 为了清除事件 EV5,需要进行读取 SR1 然后将地址写入 DR 寄存器的操作。这一操作确保了 I2C 控制器能够正确识别并处理后续的地址发送操作,维持数据传输的正常流程。
(二)事件 EV6
条件
- 当地址已发送且收到从机的应答,即 ADDR = 1 时,事件 EV6 被触发。这一事件发生在地址发送阶段,确保主机能够确认从机已经正确接收到地址。
操作
- 为了清除事件 EV6,需要读取 SR1 然后读取 SR2。这一操作序列能够使 I2C 控制器正确地处理后续的数据传输操作,避免因未处理的事件而导致的数据传输错误。
(三)事件 EV8
条件 1(EV8_1)
-
当数据寄存器为空(TxE = 1),且移位寄存器为空,数据寄存器为空时,事件 EV8_1 被触发。这意味着当前数据字节已经发送完毕,需要重新填充数据寄存器才能继续发送下一个字节。
-
为了清除事件 EV8_1,需要写入 DR 寄存器。这一操作能够使数据寄存器重新获得数据,从而继续数据传输。
条件 2(EV8_2)
-
当 TxE = 1 且 BTF(字节发送结束标志位) = 1(即数据字节传输完成且数据寄存器为空)时,事件 EV8_2 被触发。此时,请求设置停止位。
-
在这种情况下,TxE 和 BTF 位由硬件在产生停止条件时清除。这一机制确保了数据传输能够在合适的时候结束,并正确地产生停止条件通知从机。
(四)事件 EV9
条件
- 当 10 位地址已发送,即 ADDR10 = 1 时,事件 EV9 被触发。这一事件发生在 10 位地址发送过程中,确保主机能够正确处理后续的数据传输操作。
操作
- 为了清除事件 EV9,需要读取 SR1 然后写入 DR 寄存器。这一操作能够使 I2C 控制器在 10 位地址发送后顺利进入数据发送阶段。
4. 注意事项
(一)低电平时间
在 EV5、EV6、EV9、EV8_1 和 EV8_2 这些事件发生时,会拉低 SCL 的时间,直到对应的软件序列结束。这种机制确保了在处理这些关键事件时,I2C 总线的时钟信号能够与数据传输操作正确地同步。例如,在事件 EV5 发生时,拉低 SCL 直到读取 SR1 并将地址写入 DR 寄存器这一软件序列完成,防止在软件操作未完成时数据传输出现错误。
(二)软件序列完成时间
对于事件 EV8,其软件序列必须在当前字节传输结束之前完成。这是因为如果在当前字节传输结束后软件序列还未完成,可能会导致数据传输的中断或错误。例如,在数据寄存器为空(触发 EV8)后,如果没有及时处理软件序列,下一个数据字节可能无法及时发送,影响整个数据传输的效率和准确性。
主机接收
1. 主接收器传送序列图概览
这张 "图 246 主接收器传送序列图" 专注于展示 I2C 主接收器在数据传输过程中的时序。图中涵盖了 7 位主接收和 10 位主接收两种地址模式,为理解 STM32 在 I2C 主机接收操作提供了直观的参考。
2. 7 位主接收操作详解
这个序列图中省略了前面用于确定从机地址的一段时序。
(一)起始条件:开启数据接收之旅
发送操作
-
当 I2C 主接收操作启动时,首先写入控制寄存器的START位产生起始条件(S)。这个起始条件是整个数据接收过程的信号,它在 I2C 总线上以特定的电气信号变化呈现,就像吹响了接收数据的号角,通知总线上的从机主机即将开始接收数据。
事件 EV5
- 发送地址后,会触发事件 EV5,就代表起始条件已发送。这是因为在 I2C 的通信机制中,起始条件产生(SB = 1)后,需要通过特定操作来确保后续接收流程的正确性。此时,需要读取 SR1,然后将地址写入 DR 寄存器,以此清除事件 EV5,确保 I2C 控制器能够顺利进入数据接收阶段。
(二)地址发送与 EV5 事件处理
地址发送(寻址)+接收应答
- 主机发送 7 位地址,并在其后跟随一位读写位。因为是主接收操作,读写位为 1,表示读操作。
事件 EV6
-
发送地址后,会触发事件 EV6,代表寻址已完成。事件 EV6 在地址已发送且收到从机应答(ADDR = 1)时触发。需要读取 SR1 然后读取 SR2。
-
EV6_1: 没有对应的事件标志,只适于接收1个字节的情况。此时数据1还在移位,还没有收到所以没有标志位,当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去(A(1))。至于如何配置是否要给应答位,可以查看手册,其中控制寄存器CR1中的ACK(应答使能)。
-
此时序结束后,说明移位寄存器就已经成功移入一个字节的数据1了,这时,移入的一个字节就整体转移到数据寄存器,同时置RXNE标志位,表示数据寄存器非空,也就是收到了一个字节的数据(EV7)。
(三)数据接收与 EV7 事件处理
逐个数据接收
- 随后,主机依次接收数据 1 到数据 N。
事件 EV7
-
每接收一个数据字节,会产生事件 EV7。这是因为当数据寄存器非空(RxNE = 1)时,表示有数据可读,此时需要读取 DR 寄存器以获取数据。这一机制保证了数据能够及时从数据寄存器中取出,避免数据丢失或读取错误。当DR寄存器读走了数据寄存器的数据,该事件结束。
-
当第一个数据还没有被DE寄存器读走的时候,第二个数据就已经开始移入移位数据寄存器了,之后,第二个数据移位完成,收到第二个数据,再次产生EV7事件,读走第二个数据,EV7事件没有了,按这个流程可以一直接受数据,
-
EV7_1:最后,当不需要继续接收时,需要在最后时序单元发生时,提前把应答位控制寄存器ACK置0,并且设置终止条件请求,这就是EV7_1事件,后面由于设置了ACK=0,所以这里就会给出非应答,最后由于设置STOP位,所以产生终止条件。
(四)非应答与停止条件:结束接收过程
接收完最后一个数据字节后,主机产生非应答(NA),这是告诉从机主机不再需要接收数据了。接着产生停止条件(P),此停止条件标志着整个 7 位主接收的数据传输过程结束,I2C 总线恢复到相应的空闲状态。
(五)总结
流程和主机发送流程相似,写入控制寄存器CR和读取数据寄存器DR,产生时序单元,然后等待相应的事件,来确保时序单元完成,这就是这个主机接收的时序流程。
在写程序的时候就可以对应这两个流程图,实现硬件I2C的代码。
3. 事件处理机制深入剖析
(一)事件 EV5 处理
条件与意义
- 事件 EV5 的触发条件是 SB = 1,即起始条件已产生。这是整个 I2C 主接收操作开始时的关键事件,标志着主机开始准备进行地址发送操作。
操作流程
- 为了清除事件 EV5,需要执行读取 SR1 然后将地址写入 DR 寄存器的操作。这一系列操作确保了 I2C 控制器能够准确地识别和处理后续的接收操作,维持数据接收流程的正常运行。
(二)事件 EV6 处理
条件与意义
- 事件 EV6 在地址已发送且收到从机应答(ADDR = 1)时触发。这一事件在 10 位主接收操作中尤为重要,它确保了主机能够确认从机已经正确接收到地址。
操作流程
- 为了清除事件 EV6,需要读取 SR1 然后读取 SR2。同时,在 10 位主接收模式下,还要在 EV6 事件后设置 CR2 的 START = 1,这一整套操作能够保证 I2C 通信在 10 位主接收模式下顺利进行。
(三)事件 EV7 处理
条件与意义
- 事件 EV7 在数据寄存器非空(RxNE = 1)时触发。这意味着数据已经到达数据寄存器,主机需要及时读取数据,以保证数据传输的连贯性。
操作流程
- 当 RxNE = 1 时,操作是读取 DR 寄存器以获取数据。另外,在处理最后一个数据字节接收后的操作时,涉及 EV7_1 事件,需要设置 ACK = 0 和 STOP 请求,以正确地结束数据接收过程。
(四)事件 EV9 处理
条件与意义
- 事件 EV9 在 10 位地址已发送(ADDR10 = 1)时触发。这一事件发生在 10 位主接收操作中的帧头发送后,确保主机能够正确处理后续的数据接收操作。
操作流程
- 为了清除事件 EV9,需要读取 SR1 然后写入 DR 寄存器,使 I2C 控制器能够顺利进入数据接收阶段。
4. 注意事项与操作要点
(一)10 位主接收中的 EV6 特殊处理
在 10 位主接收模式下,EV6 事件后设置 CR2 的 START = 1 这一操作至关重要。这是因为在 10 位地址模式下,这种特殊处理能够确保 I2C 总线上的数据接收操作能够按照正确的时序和逻辑进行,避免因操作不当导致的数据接收错误或通信故障。
(二)EV7 相关处理要点
数据读取
- EV7 事件用于读取数据,当 RxNE = 1 时,及时读取 DR 寄存器是保证数据能够正确接收的关键操作。
最后数据字节接收后处理
- EV7_1 事件用于处理最后一个数据字节接收后的操作,包括设置 ACK = 0 和 STOP 请求。这些操作能够确保数据接收过程完整、有序地结束,防止出现数据接收不完整或 I2C 总线状态异常等问题。
五、软件I2C和硬件I2的波形对比
整体结构
图的标题和类型
-
图的标题是 "软件 / 硬件波形对比",表明这张图主要展示的是软件 I2C 和硬件 I2C 在数据传输过程中的波形差异,这是一段指定地址读的时序。
-
图中包含两组波形,上半部分是软件 I2C 的波形,下半部分是硬件 I2C 的波形。
波形分析
软件 I2C 波形
##### **波形颜色和标识**
* 图中使用了不同颜色的线条来表示不同的信号。例如,绿色线条表示时钟信号(SCL),黄色线条表示数据信号(SDA)。
* 红色的竖线用于标识特定的事件点,如起始条件(S)、停止条件(P)、数据传输的开始和结束等。
##### **波形特征**
* 软件 I2C 的波形在时序上会有一些抖动和不规则性。这是因为软件 I2C 是通过软件编程来模拟 I2C 协议的时序,受到 CPU 处理速度和其他软件任务的影响,不过由于12C是同步时序,这些不规整也没有影响。
* 因为,SCL低电平写,高电平读,虽然整个电平的任意时候都可,但是一般要求保证尽早的原则,所以可以直接认为是SCL下降沿写,上升沿读。在图中可以看到软件 I2C 的波形在数据传输过程中,时钟信号和数据信号的上升沿和下降沿可能不是非常整齐,因为操作端口之后有一些延时,所以图中SCL下降沿等了一会儿,才进行写入操作,后面的写入也都是这样。在读的时候,SDA的变化并不是是紧贴SCL上升沿进行的。
* 在应答结束时,从机在SCL下降沿立刻释放了SDA,但是软件12C的主机,过了一会儿才变换数据,所以这里就出现了一个短暂的高电平,
硬件 I2C 波形
##### **波形颜色和标识**
* 同样使用黄色线条表示时钟信号(SCL),黄色线条表示数据信号(SDA),红色竖线标识特定事件。
##### **波形特征**
* 硬件 I2C 的波形在时序上更加整齐和规则。这是因为硬件 I2C 是由硬件电路来生成 I2C 协议的时序,不受软件任务的干扰。
* 在图中可以看到硬件 I2C 的波形在数据传输过程中,时钟信号和数据信号的上升沿和下降沿更加整齐,间隔更加均匀。与软件不同,数据写入,都是紧贴SCL下降沿的,SCL下降沿,SDA立马就切换数据了,后面的写入也都是这样。在读的时候,SDA的变化也是紧贴SCL上升沿进行的。
* 在应答结束的时候,硬件I2C应答结束后,SCL下降沿,从机立刻释放SDA,同时主机也立刻拉低SDA,所以这里就出现了一个小尖峰。
具体事件分析
起始条件(S)
-
在软件 I2C 和硬件 I2C 的波形中,起始条件(S)都通过特定的波形变化来表示。通常是 SDA 线在 SCL 线为高电平时从高电平变为低电平。
-
软件 I2C 的起始条件波形可能会有一些延迟或抖动,而硬件 I2C 的起始条件波形则更加迅速和准确。
数据传输
-
在数据传输阶段,无论是软件 I2C 还是硬件 I2C,都按照 I2C 协议的规定,在 SCL 线的每个时钟周期内传输一位数据。
-
软件 I2C 的数据传输波形可能会因为软件处理的延迟而出现数据位之间的间隔不均匀,而硬件 I2C 的数据传输波形则更加均匀。
停止条件(P)
-
停止条件(P)在软件 I2C 和硬件 I2C 的波形中也是通过特定的波形变化来表示,通常是 SDA 线在 SCL 线为高电平时从低电平变为高电平。
-
软件 I2C 的停止条件波形可能会有一些延迟,而硬件 I2C 的停止条件波形则更加迅速和准确。
总结
性能对比
-
通过波形对比可以看出,硬件 I2C 在时序的准确性和稳定性方面优于软件 I2C。硬件 I2C 能够提供更精确的时钟信号和数据传输,减少数据传输错误的可能性。
-
软件 I2C 虽然在灵活性上有优势,但由于受到软件处理速度的影响,其波形在时序上可能会出现一些不规则性。
六、硬件I2C代码详解
1. 硬件电路
引脚选择原则
-
硬件 I2C 的通信引脚不能随意指定,必须查询引脚定义表来进行规划。例如在某些情况下,硬件 I2C1 可重映射,但由于特定板子上的引脚被占用,所以要选择硬件 I2C2。
I2C2 引脚示例
-
硬件 I2C2 的引脚为 PB10 和 PB11,且这两个引脚不能互换。这是由硬件电路设计决定的,在进行硬件连接时必须严格按照此规定操作,否则将无法正常实现硬件 I2C 的通信功能。
-
其中这里使用的硬件I2C引脚也是前文使用的软件I2C引脚,所以如果之后画板子设计电路的时候,不确定是使用硬件I2C还是软件I2C,就可以使用这种方法,就干脆直接接在硬件12C的引脚上,这样硬件I2C、软件I2C想使用哪个都可以。
2. 移除软件 I2C 模块
在新的工程里,最终的应用层,也就是主函数和程序现象,都是一样的,软件12C和硬件12C的区别就在通信的底层,也就是之前这里写的MyI2C.c这个文件,这里面都是用程序手动翻转引脚,也就是软件I2C,那我们有了硬件,这些底层的东西,就可以交给硬件完成,所以新的这个工程就不再需要这个MyI2C的模块了,所以需要移除软件 I2C 模块。
操作步骤:
-
选项卡操作:在工程中,首先要在选项卡中找到与软件 I2C 模块相关的设置项,进行关闭操作(右键 Close),确保从软件层面上不再调用软件 I2C 相关功能。
-
工程树文件操作:在工程树对应的文件位置进行移除操作,这一步是从工程结构中去除软件 I2C 模块的文件引用(右键 Remove),避免在编译等操作时出现错误。
-
工程目录清理:在完成上述两项操作后,需要在工程目录中手动删除软件 I2C 模块的相关文件,保证工程数和工程目录一致,实现整个工程在管理上的一致性,防止残留文件对后续开发和调试产生干扰。
之后MPU6050模块中调用MyI2C的代码都需要删除,并且用硬件I2C外设替换被删掉的代码,实现相同的功能。其中由于由于我们只替换最底层的通信层,所以后面这些基于通信层的芯片配置和读取数据,这些逻辑,都不需要更改。
3. 硬件 I2C 初始化步骤
配置I2C外设,对I2C2外设进行初始化,来替换这里的Myl2C_Init。
第一步,开启I2C外设和对应GPIO口的时钟
-
在 STM32 中,不同的外设由不同的时钟总线控制。I2C2 属于 APB1 总线,GPIOB 属于 APB2 总线。在使用这些外设之前,必须先开启相应的时钟。
-
这两行代码分别开启了 I2C2 和 GPIOB 的时钟,确保后续对 I2C2 通信和 GPIOB 引脚操作的正常进行。
cpp
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); //开启I2C2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
第二步,把I2C外设对应的GPIO口初始化为复用开漏模式
-
首先定义了一个
GPIO_InitTypeDef
类型的结构体变量GPIO_InitStructure
,用于配置 GPIO 引脚的参数。 -
GPIO_Mode_AF_OD
表示将引脚配置为复用开漏输出模式。对于 I2C 通信,其引脚通常需要这种模式,以实现数据的正确传输。开漏,这是I2C协议的设计要求,复用,就是GPIO的控制权要交给硬件外设,这是硬件12C,那控制引脚的任务肯定得交给外设来做了;如果是之前的软件12C的话,我们通过程序来控制引脚,那就是通用开漏模式。 -
GPIO_Pin_10 | GPIO_Pin_11
指定了要配置的引脚为 PB10 和 PB11,这两个引脚是 I2C2 的通信引脚。 -
GPIO_Speed_50MHz
设置了引脚的输出速度为 50MHz,确保数据传输的速度。 -
最后通过
GPIO_Init
函数将配置应用到 GPIOB 引脚。
cpp
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为复用开漏输出
第三步,使用结构体,对整个I2C进行配置
-
定义了
I2C_InitTypeDef
类型的结构体变量I2C_InitStructure
用于配置 I2C2 的参数。 -
I2C_Mode_I2C
明确了 I2C2 工作在标准的 I2C 模式。 -
I2C_ClockSpeed = 50000
将 I2C2 的时钟频率设置为 50kHz,这是一种常见的 I2C 通信速度设置。数值越大,SCL频率越高,数据传输就越快,但是不能超过400kHz。与前面的I2C外设支持不同通讯速度的知识点相关联 -
I2C_DutyCycle = I2C_DutyCycle_2
设置了 I2C 时钟的占空比为 Tlow/Thigh = 2,确保在数据传输过程中时钟信号的稳定性。这个时钟占空比参数,只有在时钟频率大于100kHz,也就是进入到快速状态时才有用,在小于等于100kHz的标准速度下,占空比是固定的1:1,也就是低电平时间比高电平时间,约等于1:1。且由于这里是标准速度,这里的占空比其实是没有用的。 -
I2C_Ack = I2C_Ack_Enable
使能了 I2C 的应答机制,在数据接收过程中会自动发送应答信号。 -
I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit
设置应答地址为 7 位,这是符合很多 I2C 从设备地址格式的。 -
I2C_OwnAddress1 = 0x00
设置了自身地址,不过在当前作为主设备与 MPU6050 通信的场景下,自身地址通常不太重要,这里设为 0x00,只要不和总线上其他设备重复即可。 -
通过
I2C_Init
函数将配置应用到 I2C2。
cpp
/*I2C初始化*/
I2C_InitTypeDef I2C_InitStructure; //定义结构体变量
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //模式,选择为I2C模式
I2C_InitStructure.I2C_ClockSpeed = 50000; //时钟速度,选择为50KHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //时钟占空比,选择Tlow/Thigh = 2
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //应答,选择使能
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //应答地址,选择7位,从机模式下才有效
I2C_InitStructure.I2C_OwnAddress1 = 0x00; //自身地址,从机模式下才有效
I2C_Init(I2C2, &I2C_InitStructure); //将结构体变量交给I2C_Init,配置I2C2
补充:I2C 时钟的占空比的相关知识
程序中的时钟占空比有16:9和2,2就是2:1,16:9,就是SCL时钟的,低电平时间和高电平时间是16:9的比例关系,2:1,就是低电平时间和高电平时间是2:1的比例关系,按理说同步时序,SCL高电平和低电平多长时间都应该没问题,那为什么还需要有占空比这个参数,其实,这个占空比是为了快速传输设计的。下面用示波器显示了最终程序在各个时钟频率下的波形。
首先是50kHz,上面是SCL,下面是SDA,最前面是起始信号,后面是数据传输,这里传输的数据是1101 0000,目前SCL频率50kHz,处于标准速度,所以这里占空比可以观察到,低电平比高电平,是1:1,也就是50%占空比的方波。
然后再加到100kHz频率,仍然是标准速度,所以时钟占空比仍然是1:1,不过,一个更重要的细节就是,这里可以观察查到,SCL和SDA的下降沿,变化是非常快的,但是在它们的上升沿,这个线它是缓慢上去的,这是为什么呢?按照之前的杆子弹簧模型来解释,这个线就是一根杆子,它由一根弹簧默认拉到高电平,当输出低电平时,要用强下拉,也就是无穷大的力,用力把杆子拽下来,那这时,可想而知,由于这个拽下来的力非常大,所以这个下降沿就非常的果断和迅速,但是输出高电平呢,是释放了杆子,杆子通过弹簧拉回至高电平,弹簧是一个弱上拉,所以这里,上升沿,就有一个回弹的过程,波形就会相对缓慢地上去,那这个缓慢变化的上升沿有什么影响呢?继续分析。
这张图是101kHz的波形,101kHz和100kHz频率差不多,但是 101kHz 就进入快速状态了,这时I2C会对SCL占空比进行调节,低电平比高电平,由原来的1:1变为大概2:1,增大了低电平时间占整个周期的比例,这样做是因为,低电平数据变化,高电平数据读取,数据变化,需要一定时间来翻转波形,尤其是这个数据的上升沿,变化比较慢,所以在快速传输的状态下,要给低电平多分配一些资源,要不然低电平数据变化来不及,高电平数据读取也没用,就像硬盘一般读取速度都是大于写入速度,所以要想快速传输,给低电平的写入时间多分配点资源,是合理的的,这就是标准速度下,时钟占空比接近1:1,快速状态下,时钟占空比接近2:1的原因。
这个时钟频率是200kHz,由于时间轴尺度进一步缩小,这时这个弯弯的上升沿就更加明显了,比如这个时钟的低电平期间,如果不给它多分配一些时间,它可能都来不及进行数据变化,当然,这个波形虽然是弯弯的,但是整体上还是非常清晰的,可以辨认出来。
最后这个是快速模式的极限速度400kHz,可以看出这个时钟的高电平,SCL还没完全回弹到高电平,就被立刻拉下来,传输下一个数据了,所以整个SCL波形就变成三角形了,这个和我们正常的理解是有出入的,但是在快速的数据传输中,这个弯弯就拖了后腿,限制了12C总线的最大传输速度,另外在下面SDA可以看到,SCL低电平期间,数据变化也不是完全贴到下降沿的,这也会有一些延时,所以这就更有必要在低电平多分配一些时间了。
第四步,I2C_Cmd,使能I2C
cpp
/*I2C使能*/
I2C_Cmd(I2C2, ENABLE); //使能I2C2,开始运行
下面的 MPU6050 寄存器初始化可以直接使用软件I2C中的初始化即可。
cpp
/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); //电源管理寄存器2,保持默认值0,所有轴均不待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); //采样率分频寄存器,配置采样率
MPU6050_WriteReg(MPU6050_CONFIG, 0x06); //配置寄存器,配置DLPF
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器,选择满量程为±2000°/s
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器,选择满量程为±16g
这些代码,是硬件12C的初始化,就可以替换掉这个软件12C的初始化,接下来继续替换写寄存器,也就是指定地址写一个字节的时序这个函数和读寄存器这两个函数的内容。
4. 需要使用的库函数的介绍
void I2C_DeInit(I2C_TypeDef* I2Cx)
函数功能:
-
这个函数用于将指定的 I2C 外设(由
I2Cx
参数指定)恢复到默认的复位状态。它会重置 I2C 模块的所有寄存器到默认值,包括配置寄存器、状态寄存器等,取消之前的所有配置。
使用场景:
- 当你需要重新初始化 I2C 外设,或者在配置过程中出现错误需要重新开始时,可以调用这个函数。例如,在切换不同的 I2C 通信模式或者改变通信速率之前,可能需要先将 I2C 外设复位。
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct)
函数功能:
-
该函数用于初始化指定的 I2C 外设(
I2Cx
)。它使用一个结构体(I2C_InitStruct
)来配置 I2C 的各种参数,这些参数包括但不限于:-
时钟频率(I2C 通信速率),例如标准模式 100kHz 或快速模式 400kHz。
-
地址模式(7 位或 10 位设备地址)。
-
应答使能 / 禁止(是否在接收数据时发送应答信号)。
-
双地址模式(某些设备支持两个不同的 I2C 地址)。
-
-
函数会根据结构体中的参数值来设置 I2C 模块的相应寄存器,从而完成初始化。
使用场景:
- 在使用 I2C 进行通信之前,必须先调用这个函数来正确配置 I2C 外设。例如,当你要与一个新的 I2C 设备进行通信时,需要根据该设备的要求来初始化 I2C 模块。
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct)
函数功能:
-
这个函数用于将一个
I2C_InitTypeDef
结构体变量初始化为默认值。它会将结构体中的各个成员变量设置为默认的初始状态,例如将时钟频率设置为默认值、应答模式设置为默认等。
使用场景:
- 在使用
I2C_Init
函数之前,通常需要先调用I2C_StructInit
来确保结构体中的参数有一个合理的初始值。这有助于避免因未初始化的变量而导致的错误配置。例如,当你定义了一个I2C_InitTypeDef
结构体变量后,应该先调用I2C_StructInit
,然后再根据具体需求修改结构体中的参数值,最后再调用I2C_Init
进行初始化。
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState)
函数功能:
-
该函数用于启用或禁用(由
NewState
参数决定)指定的 I2C 外设(I2Cx
)。当NewState
为ENABLE
时,I2C 模块将被使能,可以开始进行通信操作;当NewState
为DISABLE
时,I2C 模块将被禁用,停止所有的通信操作。
使用场景:
- 在初始化 I2C 模块后,如果不需要立即进行 I2C 通信,可以先调用
I2C_Cmd
函数将 I2C 模块禁用,以节省功耗或避免意外的数据传输。当准备好进行通信时,再调用I2C_Cmd
函数将 I2C 模块使能。例如,在一个低功耗应用中,当系统进入休眠模式时,可以禁用 I2C 模块,而在系统唤醒后需要进行 I2C 通信时,再重新使能 I2C 模块。
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
函数功能
###### **生成 I2C 起始条件**:
* 在 I2C 通信协议中,起始条件(START condition)是一个重要的信号,表示一次 I2C 通信的开始。当调用`I2C_GenerateSTART`函数且`NewState`参数为`ENABLE`时,函数会在指定的 I2C 总线上产生一个起始条件信号。这个起始条件信号通常是由将 SDA(串行数据线)从高电平拉低,同时 SCL(串行时钟线)保持高电平来实现的。
* 起始条件的生成用于通知连接在 I2C 总线上的从设备,主设备即将开始一次数据传输操作。例如,主设备可能要向从设备发送数据或者从从设备读取数据,在这些操作之前都需要先发送起始条件。
###### 函数参数
* **`I2Cx`**:
* 这是一个指向`I2C_TypeDef`结构体的指针。`I2C_TypeDef`是一个定义了 I2C 外设寄存器映射的结构体类型。通过这个参数,函数可以确定要操作的是哪个 I2C 外设。例如,在 STM32 中,可能有多个 I2C 接口(如 I2C1、I2C2 等),这个参数用于指定具体的 I2C 接口。
* **`NewState`**:
* 这个参数的类型是`FunctionalState`,它通常是一个枚举类型,用于表示使能或禁用状态。在这个函数中,`NewState`用于决定是否生成 I2C 起始条件。如果`NewState`为`ENABLE,`就把CR1寄存器的START位置1,函数将在指定的 I2C 外设上生成一个 I2C 起始条件;如果`NewState`为`DISABLE,`就把START位清0,则不会生成起始条件。
使用场景
###### **开始 I2C 通信**:
* 在使用 STM32 的 I2C 外设与其他 I2C 设备进行通信时,每当要开始一次新的通信操作(如读取传感器数据、写入配置数据等),都需要在合适的时候调用`I2C_GenerateSTART`函数来生成起始条件。例如,在初始化 I2C 外设并准备向一个 I2C 从设备发送数据时,首先要调用这个函数来生成起始条件,然后再进行后续的地址发送、数据传输等操作。
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
函数功能
###### **生成 I2C 停止条件**:
* 在 I2C 通信协议中,停止条件(STOP condition)是一个重要的信号,表示一次 I2C 通信的结束。当调用`I2C_GenerateSTOP`函数且`NewState`参数为`ENABLE`时,函数会在指定的 I2C 总线上产生一个停止条件信号。这个停止条件信号通常是由将 SDA(串行数据线)从低电平拉高,同时 SCL(串行时钟线)保持高电平来实现的。
* 停止条件的生成用于通知连接在 I2C 总线上的从设备,主设备即将结束当前的数据传输操作。例如,主设备在完成向从设备的数据发送或从从设备的数据读取后,需要调用这个函数来生成停止条件以结束本次通信。
###### 函数参数
* **`I2Cx`**:
* 这是一个指向`I2C_TypeDef`结构体的指针。`I2C_TypeDef`是一个定义了 I2C 外设寄存器映射的结构体类型。通过这个参数,函数可以确定要操作的是哪个 I2C 外设。例如,在 STM32 中,可能有多个 I2C 接口(如 I2C1、I2C2 等),这个参数用于指定具体的 I2C 接口。
* **`NewState`**:
* 这个参数的类型是`FunctionalState`,它通常是一个枚举类型,用于表示使能或禁用状态。在这个函数中,`NewState`用于决定是否生成 I2C 停止条件。如果`NewState`为`ENABLE`,函数将在指定的 I2C 外设上生成一个 I2C 停止条件;如果`NewState`为`DISABLE`,则不会生成停止条件。
使用场景
###### **结束 I2C 通信**:
* 在使用 STM32 的 I2C 外设与其他 I2C 设备进行通信时,每当要结束一次通信操作(如读取传感器数据、写入配置数据等),都需要在合适的时候调用`I2C_GenerateSTOP`函数来生成停止条件。例如,在完成向一个 I2C 从设备的数据发送后,需要调用这个函数来生成停止条件,以正确结束本次 I2C 通信。
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
函数功能
###### **I2C 应答功能配置**:
* 在 I2C 通信协议中,应答(ACK)是一个重要的机制。当主设备从从设备读取数据时,在接收到每个字节的数据后,主设备需要发送一个应答信号(ACK)来通知从设备可以继续发送下一个字节的数据。相反,如果主设备不发送应答信号(即发送非应答信号,NACK),则从设备将停止发送数据。
* `I2C_AcknowledgeConfig`函数用于配置指定 I2C 外设的应答功能。当`NewState`为`ENABLE`时,I2C 外设将在接收到数据字节后自动发送应答信号;当`NewState`为`DISABLE`时,I2C 外设将不发送应答信号,通常用于通知从设备停止发送数据。
###### 函数参数
* **`I2Cx`**:
* 这是一个指向`I2C_TypeDef`结构体的指针。`I2C_TypeDef`是一个定义了 I2C 外设寄存器映射的结构体类型。通过这个参数,函数可以确定要操作的是哪个 I2C 外设。例如,在 STM32 中,可能有多个 I2C 接口(如 I2C1、I2C2 等),这个参数用于指定具体的 I2C 接口。
* **`NewState`**:
* 这个参数的类型是`FunctionalState`,它通常是一个枚举类型,用于表示使能或禁用状态。在这个函数中,`NewState`用于决定是否使能 I2C 应答(Acknowledge,ACK)功能。如果`NewState`为`ENABLE`,函数将使能指定 I2C 外设的应答功能;如果`NewState`为`DISABLE`,则将禁用应答功能。
使用场景
###### **数据读取操作**:
* 在使用 STM32 的 I2C 外设从 I2C 从设备读取数据时,通常需要在初始化时调用`I2C_AcknowledgeConfig`函数来使能应答功能。这样,在每次接收到一个字节的数据后,I2C 外设会自动发送应答信号,确保数据的连续接收。
###### **数据读取结束操作**:
* 当主设备完成从从设备的数据读取操作,并且不希望从设备继续发送数据时,可以调用`I2C_AcknowledgeConfig`函数并将`NewState`设置为`DISABLE`,以发送非应答信号(NACK),从而通知从设备停止发送数据。然后,可以接着调用`I2C_GenerateSTOP`函数来生成 I2C 停止条件,正式结束本次数据读取操作。
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data)
函数功能
###### **I2C 数据发送**:
* 该函数的主要功能是通过指定的 I2C 外设(由`I2Cx`参数指定)发送一个字节的数据(由`Data`参数指定)。当调用这个函数时,它会将`Data`中的 8 位数据按照 I2C 协议的要求发送到 I2C 总线上(就是把Data这个数据,直接写入到DR寄存器,自动启动数据传输)。
##### 函数参数
*
###### **`I2Cx`**:
* 这是一个指向`I2C_TypeDef`结构体的指针。`I2C_TypeDef`是一个定义了 I2C 外设寄存器映射的结构体类型。通过这个参数,函数可以确定要操作的是哪个 I2C 外设。例如,在 STM32 中,可能有多个 I2C 接口(如 I2C1、I2C2 等),这个参数用于指定具体的 I2C 接口。![](https://i-blog.csdnimg.cn/direct/b5191f5a83a546b0888b64c800316c92.png)
*
###### **`Data`**:
* 这个参数的类型是`uint8_t`,表示无符号 8 位整数。它是要通过 I2C 总线发送的数据。在 I2C 通信中,每次发送的数据通常是一个字节(8 位),这个参数就是要发送的那个字节数据。
使用场景
###### **数据传输操作**:
* 在使用 STM32 的 I2C 外设与其他 I2C 设备进行通信时,当需要向从设备发送数据时,就会用到这个函数。例如,向 I2C 接口的 EEPROM 写入数据、向 I2C 传感器发送配置命令等操作都需要调用`I2C_SendData`函数来发送具体的数据字节。
###### **与其他函数配合使用**:
* 通常,`I2C_SendData`函数会与其他 I2C 操作函数配合使用。比如,在调用`I2C_GenerateSTART`函数生成起始条件后,接着发送从设备地址(可能使用`I2C_Send7bitAddress`函数),然后就可以使用`I2C_SendData`函数来发送具体的数据内容了。
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx)
函数功能
###### **I2C 数据接收**:
* 该函数的主要功能是从指定的 I2C 外设(由`I2Cx`参数指定)接收一个字节的数据。当调用这个函数时,它会从 I2C 总线上读取一个字节的数据,并将其作为函数的返回值返回(就是读取DR寄存器,接收数据)。
* 在 I2C 通信中,数据接收通常是在发送了起始条件、设备地址和相关读写控制位之后进行的。例如,主设备在发送了起始条件和从设备地址(并指定为读操作)后,就可以使用`I2C_ReceiveData`函数来接收从设备发送的数据字节。![](https://i-blog.csdnimg.cn/direct/d7ec2911073541c39c73bf0b43f5984a.png)
###### 函数参数
* **`I2Cx`**:
* 这是一个指向`I2C_TypeDef`结构体的指针。`I2C_TypeDef`是一个定义了 I2C 外设寄存器映射的结构体类型。通过这个参数,函数可以确定要操作的是哪个 I2C 外设。例如,在 STM32 中,可能有多个 I2C 接口(如 I2C1、I2C2 等),这个参数用于指定具体的 I2C 接口。
使用场景
###### **数据读取操作**:
* 在使用 STM32 的 I2C 外设与其他 I2C 设备进行通信时,当需要从从设备读取数据时,就会用到这个函数。例如,从 I2C 接口的传感器读取测量数据、从 I2C 接口的 EEPROM 读取存储的数据等操作都需要调用`I2C_ReceiveData`函数来接收具体的数据字节。
###### **与其他函数配合使用**:
* 通常,`I2C_ReceiveData`函数会与其他 I2C 操作函数配合使用。比如,在调用`I2C_GenerateSTART`函数生成起始条件后,接着发送从设备地址(并指定为读操作,可能使用`I2C_Send7bitAddress`函数),然后就可以使用`I2C_ReceiveData`函数来接收数据内容了。在接收多个字节数据时,可能还需要结合`I2C_AcknowledgeConfig`函数来控制应答位,以确保数据的正确接收。
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
函数功能
###### **发送 7 位地址和方向**:
* 该函数的主要功能是通过指定的 I2C 外设(由`I2Cx`参数指定)发送 7 位从设备地址(由`Address`参数指定)和通信方向(由`I2C_Direction`参数指定)。当调用这个函数时,它会按照 I2C 协议的要求,将地址和方向信息发送到 I2C 总线上。
* 在 I2C 通信中,发送从设备地址和方向是在生成起始条件之后进行的关键步骤。例如,主设备在生成起始条件后,就需要调用`I2C_Send7bitAddress`函数来指定要与之通信的从设备和通信方向(读或写)。
###### 函数参数
* **`I2Cx`**:
* 这是一个指向`I2C_TypeDef`结构体的指针。`I2C_TypeDef`是一个定义了 I2C 外设寄存器映射的结构体类型。通过这个参数,函数可以确定要操作的是哪个 I2C 外设。例如,在 STM32 中,可能有多个 I2C 接口(如 I2C1、I2C2 等),这个参数用于指定具体的 I2C 接口。
* **`Address`**:
* 这个参数的类型是`uint8_t`,表示无符号 8 位整数。它是要发送的 7 位从设备地址。在 I2C 通信中,从设备地址通常是 7 位长,这个参数就是要发送给从设备的地址。
* 该参数也是由DR发送,只不过是它在发送之前,设置了Address最低位的读写位,这里意思就是,如果Direction不是发送,就把Address的最低位置1,也就是读,否则,就把Address的最低位清0,也就是写,所以,在发送地址的时候,可以用一下这个函数。也可以直接调用上面的SendData函数来发送地址。
```cpp
if (I2C_Direction != I2C_Direction_Transmitter)
{
/* Set the address bit0 for read */
Address |= OAR1_ADD0_Set;
}
```
* **`I2C_Direction`**:
* 这个参数的类型也是`uint8_t`。它用于指定 I2C 通信的方向,即主设备是要从从设备读取数据(读操作)还是向从设备写入数据(写操作)。通常,这个参数可能有两个取值:
* 如果是写操作,取值为类似`I2C_Direction_Transmitter`(具体取值可能因库版本而异)。
* 如果是读操作,取值为类似`I2C_Direction_Receiver`。
I2C状态监控函数
STM32有的状态可能会同时置多个标志位,如果只检查某一个标志位就认为这个状态已经发生了,可能不太严谨;而如果用GetFlagStatus函数读多次,再进行判断,又可能比较麻烦,所以这里库函数就给了多种监控标志位的方案。
1. 基本状态监测 - I2C_CheckEvent () 函数
函数功能:
-
该函数通过比较状态寄存器(SR1 和 SR2)内容与给定事件(可以是一个或多个标志的组合)来进行状态监测。若当前状态包含给定标志,则返回 SUCCESS;若当前状态缺少一个或多个标志,则返回 ERROR。
-
这种方式就是同时判断一个或多个标志位,来确定EV几EV几这个状态是否发生,与前文的序列图是相对应的。
使用场景:
-
适用于大多数应用以及启动活动,因为相关事件在产品参考手册(RM0008)中有详细描述。也适用于需要自定义事件的用户。
局限性:
- 若出现错误(即除了被监测标志外还有错误标志被设置),I2C_CheckEvent () 函数可能仍返回 SUCCESS,尽管通信已中断或实际状态已损坏。这种情况下,建议使用错误中断来监测错误事件,并在中断 IRQ 处理程序中处理。同时还列出了用于错误管理的相关函数,如 I2C_ITConfig () 用于配置和使能错误中断,I2Cx_ER_IRQHandler () 在错误中断发生时被调用等。
2. 高级状态监测 - I2C_GetLastEvent () 函数
函数功能:
-
该函数返回一个 32 位字,包含两个状态寄存器(状态寄存器 2 的值左移 16 位并与状态寄存器 1 连接)的内容。
使用场景:
-
适用于与 I2C_CheckEvent () 函数相同的应用场景,但能克服 I2C_GetFlagStatus () 函数的局限性。其返回值可与库(stm32f10x_i2c.h)中已定义的事件或用户自定义值进行比较,适合同时监测多个标志的情况,并且允许用户自行决定何时接受一个事件(例如所有事件标志都设置且无其他标志设置,或者仅当所需标志设置时)。
局限性:
- 用户可能需要自己定义事件。若用户只检查常规通信标志而忽略错误标志,在错误管理方面也存在与上述函数类似的问题。
3. 基于标志的状态监测 - I2C_GetFlagStatus () 函数
函数功能:
-
该函数简单地返回单个标志(如 I2C_FLAG_RXNE 等)的状态。可以判断某一个标志位是否置1了。
使用场景:
-
可用于特定应用或调试阶段,适合只需要检查一个标志的情况(大多数 I2C 事件通过多个标志监测)。
局限性:
- 调用该函数时会访问状态寄存器,某些标志在访问状态寄存器时会被清除,所以检查一个标志的状态可能会清除其他标志,可能需要多次调用该函数来监测一个事件。
void I2C_ClearFlag(I2C_TypeDef* I2Cx, uint32_t I2C_FLAG);
函数功能:
-
该函数用于清除指定 I2C 外设(
I2Cx
)的特定标志(I2C_FLAG
)。在 I2C 通信中,某些标志在被读取后需要手动清除,以便下一次通信操作能够正确进行。
###### **函数参数**:
* **`I2Cx`** :指向`I2C_TypeDef`结构体的指针,指定要操作的 I2C 外设。
* **`I2C_FLAG`** :`uint32_t`类型,指定要清除的 I2C 标志。
使用场景:
- 例如,当处理完接收数据寄存器非空标志(
I2C_FLAG_RXNE
)后,需要调用I2C_ClearFlag
函数来清除该标志,以便下一次接收数据时能够正确检测到新的数据到来。
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT);
-
函数功能:
-
该函数用于获取指定 I2C 外设(
I2Cx
)的特定中断标志(I2C_IT
)的状态。它返回一个ITStatus
类型的值,通常是SET
(中断标志置位)或RESET
(中断标志复位)。 -
函数参数:
-
I2Cx
:指向I2C_TypeDef
结构体的指针,指定要操作的 I2C 外设。 -
I2C_IT
:uint32_t
类型,指定要获取状态的 I2C 中断标志。例如,可能包括I2C_IT_EVT
(事件中断标志)、I2C_IT_ERR
(错误中断标志)等。
-
-
-
使用场景:
- 在使用 I2C 中断驱动的通信中,需要检查中断标志来确定是否发生了特定的中断事件。例如,当 I2C 通信出现错误时,
I2C_IT_ERR
标志会置位,可以通过I2C_GetITStatus
函数来检查该标志,并在中断服务函数中进行相应的错误处理。
- 在使用 I2C 中断驱动的通信中,需要检查中断标志来确定是否发生了特定的中断事件。例如,当 I2C 通信出现错误时,
void I2C_ClearITPendingBit(I2C_TypeDef* I2Cx, uint32_t I2C_IT)
函数功能:
-
该函数用于清除指定 I2C 外设(
I2Cx
)的特定中断挂起位(I2C_IT
)。在处理完 I2C 中断事件后,通常需要清除相应的中断挂起位,以防止重复进入中断服务函数。
###### **函数参数**:
* **`I2Cx`** :指向`I2C_TypeDef`结构体的指针,指定要操作的 I2C 外设。
* **`I2C_IT`** :`uint32_t`类型,指定要清除的 I2C 中断挂起位。
使用场景:
- 例如,在 I2C 错误中断服务函数中,处理完错误后,需要调用
I2C_ClearITPendingBit
函数来清除I2C_IT_ERR
中断挂起位,确保系统能够正常恢复 I2C 通信。
七、解决代码提示框问题
问题分析:
-
存在因快捷键与输入法冲突导致代码提示框不显示的问题,在编程过程中,代码提示框对提高编程效率非常重要,这种冲突会导致提示框无法正常出现,给编程工作带来不便。
解决方法:
- 在输入法设置中取消中英文模式切换的快捷键勾选,然后在代码处按特定快捷键即可出现提示框,通过这种设置调整可以解决代码提示框不显示的问题,提升编程体验。
八、指定地址写的函数代码详解
对照着这个主机发送的序列图,来写程序。
第一步,生成起始条件
-
在 I2C 通信协议中,起始条件(START condition)标志着一次数据传输的开始。通过调用
I2C_GenerateSTART
函数并传入I2C2
和ENABLE
,在 I2C2 总线上产生起始条件信号。 -
产生起始条件后,调用
MPU6050_WaitEvent
函数等待I2C_EVENT_MASTER_MODE_SELECT
(文中定义为 EV5)事件。这一事件通常表示主设备已经获得了 I2C 总线的控制权,可以开始进行后续操作。 -
这一个GenerateSTART,就替换软件I2C的这个Start函数。
-
另外,软件12C的这些函数,内部都有Delay操作,是一种阻塞式的流程,也就是函数运行完成之后,对应的波形也肯定发送完毕了,所以上一个函数运行完之后,就可以紧跟下一个函数,但是下面的硬件12C函数,都不是阻塞式的,这些硬件12C函数只管给寄存器的位置1,或者只在DR写入数据,就结束,退出函数,至于波形是否发送完毕,它是不管的,所以对于这种非阻塞式的程序,在函数结束之后,我们都要等待相应的标志位,来确保这个函数的操作执行到位了。
-
在这个时序中,当起始条件的波形确实发出了,会产生EV5事件,所以在程序中,要等待EV5事件的到来,这里要用到状态监控函数,检测是否发生EV5事件。那在这里,为了等待事件的发生,需要套个while循环,如果检查EV5事件,不等于,SUCCESS,就一直空循环等待,否则,就跳出循环,这样就能实现功能了。
第二部, 发送从机地址
-
这里SendData和Send7bitAddress都可以完成这个功能,这里使用库函数
I2C_Send7bitAddress。
-
使用
I2C_Send7bitAddress
函数发送 MPU6050 的 7 位从机地址(MPU6050_ADDRESS
),同时指定通信方向为发送(I2C_Direction_Transmitter
)。这一步告诉 I2C 总线上的设备,主设备要向 MPU6050 发送数据。 -
这里的接受应答并不需要一个函数来操作,在这个库函数中,发送数据都自带了接收应答的过程,同样,接收数据也自带了发送应答的过程,如果应答错误,硬件会通过标志位和中断来提示,所以,发送地址之后,应答位就不需要处理了,直接等待事件即可
-
发送从机地址后,调用
MPU6050_WaitEvent
函数等待I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED
(文中定义为 EV6)事件。该事件表示从机已经被寻址,主设备可以开始向从机传输数据。 -
这里EV6事件之后,有个EV8_1事件,这个EV8_1事件,是告诉你,你该写入DR发送数据了,并不需要等待这个EV8_1事件,库函数的这个参数这里,也没有EV8_1事件的参数,所以这时,我们就是直接写入DR,发送数据。
第三步,发送寄存器地址
-
通过
I2C_SendData
函数将需要写入的寄存器地址(RegAddress
)发送到 I2C 总线上。 -
发送寄存器地址后,调用
MPU6050_WaitEvent
函数等待I2C_EVENT_MASTER_BYTE_TRANSMITTING
(文中定义为 EV8)事件。这表示主设备正在发送数据字节。
第四步,发送数据
-
再次调用
I2C_SendData
函数,这次发送要写入寄存器的数据(Data
)。 -
发送数据后,由于在时序中或者是最后一个字节,所以需要等待
I2C_EVENT_MASTER_BYTE_TRANSMITTED
(文中定义为 EV8_2)事件。这一事件表示主设备已经成功发送一个字节的数据。
**补充:**当需要多个字节的数据流时,中间的字节,写入DR之后,需要等待EV8事件,也就是BYTE_TRANSMITTING,最后一个字节,写入DR之后,需要等待EV8_2事件,也就是BYTE_TRANSMITTED,那在TRANSMITTED之后,就可以终止了。
第五步, 生成终止条件
- 当数据发送完成后,调用
I2C_GenerateSTOP
函数并传入I2C2
和ENABLE
,在 I2C2 总线上产生终止条件信号。这标志着本次 I2C 数据传输的结束,释放 I2C 总线,以便其他设备可以使用。
这样,就用硬件12C的代码,替换了上面软件12C的代码,两种方式产生的时序波形是一样的,完成的任务也是一样的。
cpp
/**
* 函 数:MPU6050写寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 参 数:Data 要写入寄存器的数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址,方向为发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //等待EV8
I2C_SendData(I2C2, Data); //硬件I2C发送数据
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTOP(I2C2, ENABLE); //硬件I2C生成终止条件
}
九、指定地址读的函数代码详解
第一步,第一次生成起始条件与等待事件
-
首先,通过
I2C_GenerateSTART
函数在 I2C2 总线上生成起始条件,这标志着 I2C 通信的开始。 -
然后调用
MPU6050_WaitEvent
函数等待I2C_EVENT_MASTER_MODE_SELECT
(EV5)事件,该事件表示主设备获取了 I2C 总线的控制权,可以进行后续操作。
第二步, 第一次发送从机地址与等待事件
-
使用
I2C_Send7bitAddress
函数发送 MPU6050 的 7 位从机地址(MPU6050_ADDRESS
),并指定通信方向为发送(I2C_Direction_Transmitter
),这表明主设备要向 MPU6050 发送数据(这里是寄存器地址)。 -
发送从机地址后,等待
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED
(EV6)事件,此事件表示从机已被寻址,主设备可以开始传输数据。
第三步,发送寄存器地址与等待事件
-
通过
I2C_SendData
函数将需要读取的寄存器地址(RegAddress
)发送到 I2C 总线上。 -
发送后等待
I2C_EVENT_MASTER_BYTE_TRANSMITTED
(EV8_2)事件,表示主设备已成功发送一个字节的数据(即寄存器地址)。 -
对于这里需要等待的事件是用TRANSMITTING还是TRANSMITTED呢?如果用TRANSMITTING,那实际这个事件发生时,RegAddress的波形其实还没有完全发送完毕,这时,再直接产生重复起始条件,会不会把这个数据截断呢?实际上并不会,当调用起始条件之后,如果当前还有字节正在移位,那这个起始条件将会延迟,等待当前字节发送完毕后,才能产生,所以这里的参数用 TRANSMITTING, 还是 TRANSMITTED, 都没问题。如果用TRANSMITTING.那下面重复起始条件之后将会等待,如果用TRANSMITTED,那等待就是在上面这里,等波形全都发完了,再产生重复起始条件,这里为了保险起见用TRANSMITTED。
第四步,第二次生成起始条件与等待事件
-
再次生成起始条件(这里是重复起始条件),用于切换主设备的操作模式,从发送寄存器地址切换到准备接收数据。
-
同样等待
I2C_EVENT_MASTER_MODE_SELECT
(EV5)事件,以确保主设备获得总线控制权。
第五步,第二次发送从机地址与等待事件(生成重复起始条件)
-
重新发送 MPU6050 的从机地址,但这次通信方向指定为接收(
I2C_Direction_Receiver
),表明主设备要从 MPU6050 接收数据。那使用Receiver的参数之后,函数内部就自动把这个地址最低位置1,就不再需要自己写一个|0x01了。 -
发送地址后等待
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED
(EV6)事件,该事件表示从机已被寻址且主设备准备好接收数据。 -
在接收一个字节时,有个EV6_1事件,这个事件没有标志位,也不需要我们等待,它是适合接收1个字节的情况,目前程序中也只需要接收一个字节,所以在EV6_1时,也就是EV6事件之后,要注意恰好在EV6之后,要清除响应和停止条件的产生,也就是说在代码的这个时刻,要把应答位ACK置0,同时把停止条件生成位STOP置1,那为什么连数据都没有收到为什么要产生终止条件,因为硬件I2C规定,在接收最后一个字节之前,就要提前把ACK置0,同时设置停止位STOP,因为目前是接收一个字节,所以在进入接收模式之后,就要立刻ACK置0,STOP置1,这样设计的原因是,如果不提前在数据还没收到的时候给ACK置0,那等时序到了这里,数据已经收到了,这时候在要求要置0,要给非应答,这时就晚了,数据收到之前,应答位就已经发送出去了,这时再给ACK置0,那只能是在下一个数据之后给非应答了,时序不等人,所以在最后一个数据之前,就要给ACK置0,同时,这里也建议提前设置STOP终止条件,这个终止条件,也不会截断当前字节,它会等当前字节接收完成后,再产生终止条件的波形。
-
所以总结一下就是,如果是读取多个字节,那直接等待EV7事件,读取DR,就能收到数据了,这样依次接收,在接收最后一个字节之前,也就是这里的EV7_1事件,需要提前把ACK置0,STOP置1;如果只需要读取一个字节,那在EV6事件之后,就要立刻ACK置0,STOP置1,要不然设置晚了,时序上就会多一个字节出来。所以有了下面一个代码
第六步,配置应答与生成停止条件
-
在接收数据前,调用
I2C_AcknowledgeConfig
函数将应答功能禁用(传入DISABLE
),这是因为在接收最后一个字节数据时不需要发送应答信号。 -
同时调用
I2C_GenerateSTOP
函数提前生成停止条件,因为在接收完最后一个字节后就可以结束本次通信。
第七步,接收数据与恢复应答设置
-
等待
I2C_EVENT_MASTER_BYTE_RECEIVED
(EV7)事件,表示主设备准备好接收一个字节的数据。 -
然后通过
I2C_ReceiveData
函数从 I2C2 接收数据,并将数据存储到变量Data
中。 -
最后调用
I2C_AcknowledgeConfig
函数将应答功能重新启用(传入ENABLE
),这样不会影响后续如果有读取多个字节数据操作时的正常进行。
cpp
/**
* 函 数:MPU6050读寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 返 回 值:读取寄存器的数据,范围:0x00~0xFF
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址,方向为发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成重复起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); //硬件I2C发送从机地址,方向为接收
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6
I2C_AcknowledgeConfig(I2C2, DISABLE); //在接收最后一个字节之前提前将应答失能
I2C_GenerateSTOP(I2C2, ENABLE); //在接收最后一个字节之前提前申请停止条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7
Data = I2C_ReceiveData(I2C2); //接收数据寄存器
I2C_AcknowledgeConfig(I2C2, ENABLE); //将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
return Data;
}
十、死循环问题的解释
目前可以看到程序中有大量死循环的问题,这种大量的死循环等待,在程序中是比较危险的,一旦有个事件一直没有产生,就会让整个程序卡死,所以对于这种死循环等待,可以给它加一个超时退出机制,用一个简单的计数等待就行了,为避免每一个while都加一个超时退出程序,所以可以编写一个函数将其封装起来。
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
1. 函数定义与参数
-
该函数用于等待特定的 I2C 事件发生。它接收两个参数:
-
I2Cx
:这是一个指向I2C_TypeDef
结构体的指针,用于指定要操作的 I2C 外设(例如 I2C1、I2C2 等)。 -
I2C_EVENT
:这是一个uint32_t
类型的参数,表示需要等待的 I2C 事件。这个事件会在I2C_CheckEvent
函数中进行检查。
-
2. 超时机制初始化
- 定义了一个
uint32_t
类型的变量Timeout
,并将其初始化为 10000。这个变量用于实现超时机制,即如果等待事件的时间过长,超过了设定的值,就会进行相应的处理,避免程序无限期地等待下去。
3. 等待事件循环
-
在
while
循环中,通过I2C_CheckEvent
函数来检查指定的 I2C 事件是否发生。如果I2C_CheckEvent
函数返回的值不等于SUCCESS
,表示事件还没有发生,就继续等待。 -
在每次循环中,
Timeout
变量会自减 1,表示等待时间在流逝。 -
当
Timeout
变量减到 0 时,表示等待时间已经超过了设定的上限,此时可以认为出现了某种问题(例如 I2C 通信故障等)。虽然代码中没有给出具体的错误处理代码(只给出了注释/*超时的错误处理代码,可以添加到此处*/
),但通常在这里可以添加一些处理 I2C 通信超时错误的代码,比如复位相关外设、设置错误标志等可以让代码更加完善。然后通过break
语句跳出while
循环,不再等待事件。
cpp
/**
* 函 数:MPU6050等待事件
* 参 数:同I2C_CheckEvent
* 返 回 值:无
*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout;
Timeout = 10000; //给定超时计数时间
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) //循环等待指定事件
{
Timeout --; //等待时,计数值自减
if (Timeout == 0) //自减到0后,等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break; //跳出等待,不等了
}
}
}
然后就可以将程序中的while语句都改成封装好的MPU6050_WaitEvent函数。