STM32 I2C
I2C简介
I2C(Inter-Integrated Circuit,集成电路总线)是一种通用的总线协议。由Philips公司(2006年迁移到NXP)在1980年代初开发的一种简单、双线双向的同步串行总线,它利用一根时钟线和一根数据线在连接总线的两个器件之间进行信息的传递,为设备之间数据交换提供了一种简单高效的方法。++每个连接到总线上的器件都有唯一的地址,任何器件既可以作为主机也可以作为从机,但同一时刻只允许有一个主机。++
I2C详解
物理层
SCL: 串行时钟线,用于通信双方时钟的同步。
SDA: 串行数据线,用于收发数据。
- I2C的特点:
- 只需要两条总线;
- 没有严格的波特率要求,例如使用RS232,主设备生成总线时钟;
- 所有组件之间都存在简单的主/从关系,连接到总线的每个设备均可通过唯一地址进行软件寻址;
- I²C是真正的多主设备总线,可提供仲裁和冲突检测;
- 传输速度;
- 标准模式:Standard Mode = 100 Kbps
- 快速模式:Fast Mode = 400 Kbps
- 高速模式: High speed mode = 3.4 Mbps
- 超快速模式: Ultra fast mode = 5 Mbps
- 连接到相同总线的 IC 数量受到总线的最大电容 400pF 限制。
协议层
I2C的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
I2C 数据在消息中传输,消息被分解为数据帧。
每条消息包含:
- 开始条件
- 停止条件
- 读取和写入位
- ACK/NACK 位
- 从机地址
- 数据帧
ACK/NACK:应答信号,由接收设备在每个帧后发送,以向发送方发送信号是否成功接收到数据帧 (ACK) 或未成功接收 (NACK)
- 基本读写过程:
- 开始条件
- 地址传送
- 数据传送
- 停止条件
开始和停止条件:
开始条件:
- 当主设备将==SDA线从高电平切换到低电平,然后将SCL线从高电平切换到低电平 #EE3F4D==时,传输将开始。
- 向其他从属设备发出信号,表明传输即将发生。
- 如果两个主机同时发送启动条件想要获得总线的所有权,那么++谁先将 SDA 拉低,谁就“获胜”++。
停止条件:
- 所有数据帧发送完毕后,将发送停止条件。
- ==SCL线先从低电平切换到高电平,然后SDA线从低电平切换到高电平 #EE3F4D==
- 在正常数据写入操作期间,++当 SCL 为高电平时,SDA 上的值不应更改++,因为这可能会导致错误停止情况。
当两个起始信号之间没有停止信号时,即产生了重复起始信号。主机采用这种方法与另一个从机或相同的从机以不同传输方向进行通信(例如:从写入设备到从设备读出)而不释放总线。
地址传送:
开始条件或者重新开始条件后面的帧是地址帧(一个字节),用于指定主机通信的对象地址,在发送停止条件之前,指定的从机一直有效。
I2C 通常有 7 位地址,并且只有 127 个不同的 I2C 设备。然而,实际上,I2C 设备的类型要多得多,并且 ++I2C 设备很有可能在总线上具有相同的地址++。为了克服这一限制,许多器件通过外部配置引脚使用双地址以及 10 位地址方案。
10 位地址方案对普通 I2C 协议有两个影响:
- 地址帧现在有两个字节而不是 1 个字节。
- 第一个字节的前五个最高有效位用于标识 10 位地址,约定为“11110”。
7 位寻址模式:
- 地址帧(++8bit++)的==高 7 位为从机地址 #EE3F4D==,地址帧==第 8 位来决定数据帧传送的方向 #EE3F4D==:++7 位从机地址 + 1位 读/写位++,读/写位控制从机的数据传输方向(++0:写 1:读++)
10 位寻址模式:
- 地址帧有两字节(两帧),第一帧发送头序列(11110XX0,其中 XX 表示 10 位地址的高两位),然后第二帧发送低八位从机地址。
数据传送:
- 发送地址帧并且主设备从从设备接收到 ACK 位后,将开始传输 8 位长的数据。
- 当主设备定期生成时钟脉冲时,数据由主设备或从设备根据读/写位在 SDA 上发送。
- 每个数据帧后面都有一个 ACK/NACK 位,以表明数据是否已成功接收。在发送下一个数据帧之前,主机或从机必须接收到 ACK 位
- 此过程完成后,主设备将向从设备发送停止条件,从而结束传输。
数据有效性
I2C 使用 SDA 信号线来传输数据,使用 SCL 信号线进行数据同步。见图数据有效性。SDA 数据线在 SCL 的每个时钟周期传输一位数据。传输时,SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL 为低电平时,SDA的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。
每次数据传输都以字节为单位,每次传输的字节数不受限制。
综合如下:
读数据
若配置的方向传输位为“读数据”方向,广播完地址,接收到应答信号后,从机开始向主机返回数据,数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号,重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号,则从机自动停止数据传输。
写数据
若配置的方向传输位为“写数据”方向,广播完地址,接收到应答信号后,主机开始正式向从机传输数据 ,数据包的大小为 8 位,主机每发送完一个字节数据,都要等待从机的应答信号,重复这个过程,可以向从机传输 N 个数据,这个 N 没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号,表示不再传输数据。
读写数据
除了基本的读写,I2C 通讯更常用的是复合格式,该传输过程有两次起始信号 (S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址 (注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。
STM32的I2C外设
本节内容来自CSDN博主iiCube的一篇文章,写的十分详细
在讲硬件I2C之前不得不吐槽一下这个硬件I2C外设,有时候就突然会卡在某个事件的检测,需要关闭电源重新启动才有用,不过虽然可能硬件I2C可能会有问题,可能以后不一定用的到但是我们主要是学习如何用硬件实现I2C协议,对我们以后学别的协议肯定会有帮助。
硬件 I2C:是指直接
利用 STM32 芯片中的硬件 I2C 外设
,该硬件 I2C 外设跟 USART串口外设类似,只要配置好对应的寄存器,外设就会产生标准串口协议的时序。使用它的I2C 外设则可以方便地通过外设寄存器来控制硬件I2C外设产生 I2C 协议方式的通讯,而不需要内核直接控制引脚的电平
。软件模拟I2C:即直接使用CPU内核按照 I2C 协议的要求控制GPIO输出高低电平。如控制产生 I2C 的起始信号时,先控制作为 SCL 线的 GPIO 引脚输出高电平,然后控制作为 SDA 线的GPIO引脚在此期间完成由高电平至低电平的切换,最后再控制SCL 线切换为低电平,这样就输出了一个标准的 I2C 起始信号。
硬件 I2C 直接使用外设来控制引脚,可以减轻 CPU 的负担。不过使用硬件I2C 时必须使用某些固定的引脚作为 SCL 和 SDA,软件模拟 I2C 则可以使用任意 GPIO 引脚,相对比较灵活。
I2C外设功能框图(重点)
1.通信引脚
STM32中有两个I2C外设,硬件I2C必须要使用这些引脚,因为这些引脚才连接到I2C引脚,就比如说PB6与PB7引脚就连接到芯片内部的I2C1外设(正点原子STM32Mini板)。
2.时钟控制逻辑
时钟控制寄存器
这里解释一下为什么是用Tpclk1,因为I2C1外设是挂载在APB1总线上的
这里只是演示一下这么计算寄存器写入的值,用库函数我们只要配置好相应寄存器的参数,库函数会帮我计算自动写入的,不要慌。
3.数据控制逻辑
当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过SDA信号线发送出去;
当从外部接收数据的时候,数据移位寄存器把SDA信号线采样到的数据一位一位地存储到“数据寄存器”中。
然后通过CPU或DMA向数据寄存器写入或者读出数据(一般保存在一个数组当中)。
数据寄存器DR
自身地址寄存器1
4.整体控制逻辑
这里挑一些重点的寄存器位,我们只需配置好寄存器就可以让I2C外设硬件逻辑自动控制SDA,SCL总线去产生I2C协议的时序如:起始信号、应答信号、停止信号等等
接下来就是了解的知识:
- 总线错误(BERR)
在一个地址或数据字节传输期间,当I2C接口检测到一个外部的停止或起始条件则产生总线错误。此时:
● BERR位被置位为’1’;如果设置了ITERREN位,则产生一个中断;
● 在从模式情况下,数据被丢弃,硬件释放总线:
─ 如果是错误的开始条件
,从设备认为是一个重启动,并等待地址或停止条件。
─ 如果是错误的停止条件
,从设备按正常的停止条件操作,同时硬件释放总线。
● 在主模式情况下,硬件不释放总线,同时不影响当前的传输状态。此时由软件决定是否要中止当前的传输
主机模式与从机模式
- 应答错误(AF)
当STM32检测到一个无应答位时,产生应答错误。此时:
● AF位被置位,如果设置了ITERREN位,则产生一个中断;
● 当发送器接收到一个NACK时,必须复位通讯:
─ 如果是处于从模式,硬件释放总线。
─ 如果是处于主模式,软件必须生成一个停止条件
。
- 过载/欠载错误(OVR)
在从模式下,如果禁止时钟延长,I2C接口正在接收数据时,当它已经接收到一个字节(RxNE=1),但在DR寄存器中前一个字节数据还没有被读出,则发生过载错误。此时:
● 最后接收的数据被丢弃;
● 在过载错误时,软件应清除RxNE位,发送器应该重新发送最后一次发送的字节。
在从模式下,如果禁止时钟延长,I2C接口正在发送数据时,在下一个字节的时钟到达之前,新的数据还未写入DR寄存器(TxE=1),则发生欠载错误。此时:
● 在DR寄存器中的前一个字节将被重复发出;
● 用户应该确定在发生欠载错时,接收端应丢弃重复接收到的数据。发送端应按I2C总线标准在规定的时间更新DR寄存器。
在发送第一个字节时,必须在清除ADDR之后并且第一个SCL上升沿之前写入DR寄存器;如果不能做到这点,则接收方应该丢弃第一个数据
STM32做为从机时写入数据和读出数据时应该连续,取个例子主机要10个字节的数据而你只发5个字节此时就发生欠载错误:在下一个字节的时钟到达之前,新的数据还未写入DR寄存器
5.STM32的I2C外设通信过程(超级重要)
I2C模式选择:
接口可以下述4种模式中的一种运行:
● 从发送器模式
● 从接收器模式
● 主发送器模式
● 主接收器模式
该模块默认地工作于从模式。接口在生成起始条件后自动地从从模式切换到主模式
;当仲裁丢失或产生停止信号时,则从主模式切换到从模式。允许多主机功能。
- 主模式:STM32作为主机通信(发送器与接收器)
- 从模式:STM32作为从机通信(发送器与接收器)
这里我主要将STM32做为主机通信
I2C主模式:
默认情况下,I2C接口总是工作在从模式。从从模式切换到主模式,需要产生一个起始条件。
在主模式时,I2C接口启动数据传输并产生时钟信号
。串行数据传输总是以起始条件开始并以停止条件结束。当通过START位在总线上产生了起始条件,设备就进入了主模式
。
主发送器
- EV5事件
起始条件当BUSY=0时,设置START=1,I2C接口将产生一个开始条件并切换至主模式(M/SL位置位)
一旦发出开始条件,我们需要检测SB是否置1,判断是否成功发送起始信号
● SB位被硬件置位,如果设置了ITEVFEN位,则会产生一个中断。
然后主设备等待读SR1寄存器,紧跟着将从地址写入DR寄存器
- EV6事件
从机地址的发送
● 在7位地址模式时,只需送出一个地址字节。
一旦该地址字节被送出,
─ ADDR位被硬件置位,如果设置了ITEVFEN位,则产生一个中断。
随后主设备等待一次读SR1寄存器,跟着读SR2寄存器。
根据送出从地址的最低位,主设备决定进入发送器模式还是进入接收器模式
● 在7位地址模式时,
─ 要进入发送器模式,主设备发送从地址时置最低位为’0’。
─ 要进入接收器模式,主设备发送从地址时置最低位为’1’
从机地址发送完成从机应答之后检测EV6事件:
确保从机应答,之后才传输下一个数据,如果你不检测万一地址发送失败或者从机无应答,直接就开始传输数据那传给谁??
EV8_1事件:
这个检测是地址发送完之后进行检测,其实我们只要检测EV6事件就可以了,因为EV6事件成功之后就已经代表地址(数据)发送出去,而且从机还应答了,地址已经发送完成那肯定数据寄存器,与移位寄存器肯定为空呐,所以不检测也可以。EV8事件
我们在发送完一个数据之后必须判断数据寄存器是否为空,数据寄存器为空(TXE),才能向数据寄存器写入新的数据,不然上一个数据们还没有转移到移位寄存器,CPU又写入一个数据则会覆盖上一个数据。
EV8_2事件
在我们发送完最后一个字节之后我们应该检测EV8_2事件,主要检测BTF位。
为什么呢,主要是检测数据移位寄存器的数据全部发送完成,则才算最后一个字节全部发送完毕。关闭通信
在DR寄存器中写入最后一个字节后,通过设置STOP位产生一个停止条件,然后I2C接口将自动回到从模式(M/S位清除)。
主接收器
因为虽然STM32做为接收器,但是STM32是主机,起始信号与发送从机地址都是必须由主机干的活,所以前面EV5,EV6,EV6_1事件与主接收器是一模一样
- EV7事件
主机使能ACK位就可以自动接收完数据产生应答信号。
接收数据之前,判断数据寄存器是否有数据,也就数据寄存器非空(RNXE),CPU就可以读取数据寄存器中的数据啦。
- EV7_1事件
关闭通信主设备在从设备接收到最后一个字节后发送一个NACK
。接收到NACK后,从设备释放对SCL和SDA线的控制;主设备就可以发送一个停止/重起始条件。
● 为了在收到最后一个字节后产生一个NACK脉冲,在读倒数第二个数据字节之后(在倒数第二个RxNE事件之后)必须清除ACK位。
● 为了产生一个停止/重起始条件,软件必须在读倒数第二个数据字节之后(在倒数第二个RxNE事件之后)设置STOP/START位。
● 只接收一个字节时,刚好在EV6之后(EV6_1时,清除ADDR之后)要关闭应答和停止条件的产生位。在产生了停止条件后,I2C接口自动回到从模式(M/SL位被清除)
这里产生一个NACK其实就是清除ACK位,将ACK位置0,后面接收的一个字节不在产生应答就是非应答咯
然后主机产生停止信号
然后通过判断EV7事件,CPU向数据寄存器读取最后一个字节数据
硬件I2C写代码必须熟练掌握和理解主发送器和主接收器的过程,只要你理解了写代码还不是信手拈来,简简单单,然后写代码你会发送就是上面的过程一模一样
6.I2C初始化结构体
- I2C_ClockSpeed
设置I2C的传输速率,我们写入的这个参数值不得高于400KHz。
在调用初始化函数时,函数会根据我们输入的数值,以及后面输入的占空比参数,经过运算后把时钟因子写入到I2C的时钟控制寄存器CCR。
CCR寄存器不能写入小数类型的时钟因子,影响到SCL的实际频率可能会低于本成员设置的参数值,这时除了通讯稍慢一点以外,不会对I2C的标准通讯造成其它影响。
初始化函数
- I2C_Mode
选择I2C的使用方式,有I2C模式(I2C_Mode_I2C )和SMBus主、从模式(I2C_Mode_SMBusHost、 I2C_Mode_SMBusDevice ) 。
- I2C_DutyCycle
设置I 2 C的SCL线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为2:1 ( I2C_DutyCycle_2)和16:9(I2C_DutyCycle_16_9)。
这个模式随便选反正区别不大。
- I2C_OwnAddress1
配置STM32的I2C设备自己的地址,每个连接到I2C总线上的设备都要有一个自己的地址,作为主机也不例外。
地址可设置为7位或10位,只要该地址是I2C总线上唯一
的即可。
其实可以有两个地址,这里是设置的第一个地址。
第二个地址要另外用库函数设置而且只能是7位
- I2C_Ack_Enable
配置I 2 C应答是否使能,设置为使能则可以发送响应信号。一般配置为允许应答(I2C_Ack_Enable)若STM32接收一个字节数据自动产生应答,必须要使能
- I2C_AcknowledgeAddress
选择I2C的寻址模式是7位还是10位地址。这需要根据实际连接到I2C总线上设备的地址进行选择,这个成员的配置也影响到I2C_OwnAddress1成员,只有这里设置成10位模式时,I2C_OwnAddress1才支持10位地址。
配置完成之后调用一下I2C初始化函数就搞定
记得使能I2C外设
标准库I2C
库函数
/*初始化结构体*/
typedef struct {
uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 400000*/
uint16_t I2C_Mode; /*!< 指定工作模式,可选 I2C 模式及 SMBUS 模式 */
uint16_t I2C_DutyCycle; /* 指定时钟占空比,可选 low/high = 2:1 及 16:9 模式*/
uint16_t I2C_OwnAddress1; /*!< 指定自身的 I2C 设备地址 */
uint16_t I2C_Ack; /*!< 使能或关闭响应 (一般都要使能) */
uint16_t I2C_AcknowledgedAddress; /*!< 指定地址的长度,可为 7 位及 10 位 */
} I2C_InitTypeDef;
/*
I2C初始化函数
参数1:要初始化的I2C
参数2:用于初始化的结构体
*/
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
/*
I2C状态设置
参数1:要设置的I2C
参数2:目标状态
*/
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);
/*
I2C产生开始信号/停止信号
参数1:要设置的I2C
参数2:目标状态
*/
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);
/*
I2C应答设置,收到一个字节后是否给从机应答
参数1:要设置的I2C
参数2:目标状态
*/
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);
/*
设置主机地址(STM32当从机使用时配置)
参数1:要设置的I2C
参数2:地址
*/
void I2C_OwnAddress2Config(I2C_TypeDef* I2Cx, uint8_t Address);
/*
发送数据和接收数据
参数1:要收发数据的I2C
参数2:发送的数据,一个字节
接收函数的返回值即为接收数据
*/
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);
/*
发送从机地址(I2C_SendData也可以发送)
参数1:要发送地址的I2C
参数2:从机地址
参数3:数据方向
*/
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);
/*
检查应答位
参数1:要检查的I2C
参数2:要检查的标志位
*/
void I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
模拟I2C
由于I^2^C占用引脚少,协议相对简单,所以可以++用两个IO口来模拟一个I2C++外设
一、基本接口定义
为了提高代码的可移植性和使用方便性,定义了一些宏和结构体,下面介绍一下这些宏和结构体。
1、结构体
I2C总线有SDA和SCL两个引脚,所以构造一个结构体来定义表示这两个引脚的基本信息:
typedef struct {
uint32_t SDA_RCC_APB2Periph;// SDA脚时钟
GPIO_TypeDef* SDA_Port;//SDA脚Port
uint16_t SDA_Pin;//SDA脚Pin
uint32_t SCL_RCC_APB2Periph;//SCL脚时钟
GPIO_TypeDef* SCL_Port;//SCL脚Port
uint16_t SCL_Pin;//SCL脚Pin
} sw_i2c_gpio_t;
2、宏定义
#define I2C_USE_7BIT_ADDR //如果使用的从机地址是7Bit模式,则打开这个宏,否则注释掉这个宏
#define I2C_DELAY 50 // I2C每个Bit之间的延时时间,延时越小I2C的速率越高
#define SW_I2C_SCL_LOW GPIO_ResetBits(gpio->SCL_Port,gpio->SCL_Pin) // I2C SCL脚输出0
#define SW_I2C_SCL_HIGH GPIO_SetBits(gpio->SCL_Port,gpio->SCL_Pin) // I2C SCL脚输出1
#define SW_I2C_SDA_LOW GPIO_ResetBits(gpio->SDA_Port,gpio->SDA_Pin) // I2C SDA脚输出0
#define SW_I2C_SDA_HIGH GPIO_SetBits(gpio->SDA_Port,gpio->SDA_Pin) // I2C SDA脚输出1
#define SW_I2C_SDA_INPUT sw_i2c_set_sda_input(gpio) // 将SDA脚方向设置为输入
#define SW_I2C_SDA_OUTPUT sw_i2c_set_sda_output(gpio) // 将SDA脚方向设置为输出
#define SW_I2C_SDA_STATUS sw_i2c_sda_status(gpio) // 获取SDA脚输入电平状态
#define i2c_delay_us(a) SystemDelayUs(a) // 获取SDA脚输入电平状态
一、I2C基本操作实现
1、SDA脚输入输出切换及输入状态读取
/**************************************************************************
*** 读取SDA脚的状态 ***
***************************************************************************/
static uint8_t sw_i2c_sda_status(sw_i2c_gpio_t *gpio)
{
uint8_t sda_status;
sda_status = GPIO_ReadInputDataBit(gpio->SDA_Port,gpio->SDA_Pin);
return sda_status?1:0;
}
/**************************************************************************
*** 设置SDA脚为输入 ***
***************************************************************************/
static void sw_i2c_set_sda_input(sw_i2c_gpio_t *gpio)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = gpio->SDA_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init (gpio->SDA_Port, & GPIO_InitStructure );
}
/**************************************************************************
*** 设置SDA脚为输出 ***
***************************************************************************/
static void sw_i2c_set_sda_output(sw_i2c_gpio_t *gpio)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = gpio->SDA_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init (gpio->SDA_Port, & GPIO_InitStructure );
}
2、I2C启动
static void sw_i2c_start(sw_i2c_gpio_t *gpio)
{
// I2C 开始时序:SCL=1时,SDA由1变成0.
SW_I2C_SDA_HIGH;
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_HIGH;
i2c_delay_us(I2C_DELAY);
SW_I2C_SDA_LOW;
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_LOW;
i2c_delay_us(I2C_DELAY);
}
3、I2C停止
static void sw_i2c_stop(sw_i2c_gpio_t *gpio)
{
// I2C 开始时序:SCL=1时,SDA由0变成1.
SW_I2C_SDA_LOW;
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_HIGH;
i2c_delay_us(I2C_DELAY);
SW_I2C_SDA_HIGH;
}
4、等待数据接收方反馈ACK
static uint8_t sw_i2c_wait_ack(sw_i2c_gpio_t *gpio)
{
uint8_t sda_status;
uint8_t wait_time=0;
uint8_t ack_nack = 1;
//先设置SDA脚为输入
SW_I2C_SDA_INPUT;
//等待SDA脚被从机拉低
while(SW_I2C_SDA_STATUS)
{
wait_time++;
//如果等待时间过长,则退出等待
if (wait_time>=200)
{
ack_nack = 0;
break;
}
}
// SCL由0变为1,读入ACK状态
// 如果此时SDA=0,则是ACK
// 如果此时SDA=1,则是NACK
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_HIGH;
i2c_delay_us(I2C_DELAY);
//再次将SCL=0,并且将SDA脚设置为输出
SW_I2C_SCL_LOW;
i2c_delay_us(I2C_DELAY);
SW_I2C_SDA_OUTPUT;
i2c_delay_us(I2C_DELAY);
return ack_nack;
}
5、发送ACK给数据发送方
static void sw_i2c_send_ack(sw_i2c_gpio_t *gpio)
{
// 发送ACK就是在SDA=0时,SCL由0变成1
SW_I2C_SDA_LOW;
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_HIGH;
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_LOW;
i2c_delay_us(I2C_DELAY);
}
6、发送NACK给数据发送方
static void sw_i2c_send_nack(sw_i2c_gpio_t *gpio)
{
// 发送NACK就是在SDA=1时,SCL由0变成1
SW_I2C_SDA_HIGH;
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_HIGH;
i2c_delay_us(I2C_DELAY);
SW_I2C_SCL_LOW;
i2c_delay_us(I2C_DELAY);
}
7、主设备向从设备写一个字节
static void sw_i2c_write_byte(sw_i2c_gpio_t *gpio,uint8_t aByte)
{
uint8_t i;
for (i=0;i<8;i++)
{
//先将SCL拉低;
SW_I2C_SCL_LOW;
i2c_delay_us(I2C_DELAY);
//然后在SDA输出数据
if(aByte&0x80)
{
SW_I2C_SDA_HIGH;
}
else
{
SW_I2C_SDA_LOW;
}
i2c_delay_us(I2C_DELAY);
//最后将SCL拉高,在SCL上升沿写入数据
SW_I2C_SCL_HIGH;
i2c_delay_us(I2C_DELAY);
aByte = aByte<<1;//数据位移
}
//写完一个字节只后要将SCL拉低
SW_I2C_SCL_LOW;
i2c_delay_us(I2C_DELAY);
}
8、主设备从从设备读一个字节
static uint8_t sw_i2c_read_byte(sw_i2c_gpio_t *gpio)
{
uint8_t i,aByte;
//先将SDA脚设置为输入
SW_I2C_SDA_INPUT;
for (i=0;i<8;i++)
{
//数据位移
aByte = aByte << 1;
//延时等待SDA数据稳定
i2c_delay_us(I2C_DELAY);
//SCL=1,锁定SDA数据
SW_I2C_SCL_HIGH;
i2c_delay_us(I2C_DELAY);
//读取SDA状态
if(SW_I2C_SDA_STATUS)
{
aByte |= 0x01;
}
//SCL=0,解除锁定
SW_I2C_SCL_LOW;
}
//读完一个字节,将SDA重新设置为输出
SW_I2C_SDA_OUTPUT;
return aByte;
}
二、I2C传输数据函数实现
1、模拟I2C初始化
void sw_i2c_init(sw_i2c_gpio_t *gpio)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd ( gpio->SCL_RCC_APB2Periph, ENABLE );
GPIO_InitStructure.GPIO_Pin = gpio->SCL_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init (gpio->SCL_Port, & GPIO_InitStructure );
RCC_APB2PeriphClockCmd ( gpio->SDA_RCC_APB2Periph, ENABLE );
GPIO_InitStructure.GPIO_Pin = gpio->SDA_Pin;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init (gpio->SDA_Port, & GPIO_InitStructure );
SW_I2C_SCL_HIGH;
SW_I2C_SDA_HIGH;
}
2、主设备向从设备写N个字节数据
void sw_i2c_write_nBytes(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint8_t *data,uint8_t len)
{
uint8_t j;
//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDR
i2c_addr = i2c_addr<<1;
#endif
//启动I2C
sw_i2c_start(gpio);
//写I2C从机地址,写操作
sw_i2c_write_byte(gpio,i2c_addr);
//如果从机响应ACC则继续,如果从机未响应ACK则停止
if (!sw_i2c_wait_ack(gpio))
goto err;
//开始写n个字节数据
for (j=0;j<len;j++)
{
sw_i2c_write_byte(gpio,data[j]);
// 每写一个字节数据后,都要等待从机回应ACK
if (!sw_i2c_wait_ack(gpio))
goto err;
}
//停止I2C
err:
sw_i2c_stop(gpio);
}
3、主设备从从设备读取N个字节数据
void sw_i2c_read_nBytes(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint8_t *buf,uint8_t len)
{
uint8_t j;
//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDR
i2c_addr = i2c_addr<<1;
#endif
//启动I2C
sw_i2c_start(gpio);
//写I2C从机地址,读操作
sw_i2c_write_byte(gpio,i2c_addr|0x01);
//如果从机响应ACC则继续,如果从机未响应ACK则停止
if (!sw_i2c_wait_ack(gpio))
goto err;
//开始读n个字节数据
for (j=0;j<len;j++)
{
buf[j]=sw_i2c_read_byte(gpio);
// 每读一个字节数据后,都要发送ACK给从机
sw_i2c_send_ack(gpio);
}
//停止I2C
err:
sw_i2c_stop(gpio);
}
4、主设备向从设备16Bit长度的寄存器地址读取N个字节
void sw_i2c_send2read_16bit(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint16_t reg,uint8_t *buf,uint8_t len)
{
uint8_t j;
//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDR
i2c_addr = i2c_addr<<1;
#endif
//启动I2C
sw_i2c_start(gpio);
//写I2C从机地址,写操作
sw_i2c_write_byte(gpio,i2c_addr);
//如果从机响应ACC则继续,如果从机未响应ACK则停止
if (!sw_i2c_wait_ack(gpio))
goto err;
//写寄存器地址高8位
sw_i2c_write_byte(gpio,(reg>>8)&0xff);
if (!sw_i2c_wait_ack(gpio))
goto err;
//写寄存器地址低8位
sw_i2c_write_byte(gpio,reg&0xff);
if (!sw_i2c_wait_ack(gpio))
goto err;
//重新启动I2C
sw_i2c_start(gpio);
//写I2C从机地址,读操作
sw_i2c_write_byte(gpio,i2c_addr|0x01);
if (!sw_i2c_wait_ack(gpio))
goto err;
//开始读n个字节数据
for (j=0;j<len;j++)
{
buf[j]=sw_i2c_read_byte(gpio);
// 每读一个字节数据后,都要发送ACK给从机
sw_i2c_send_ack(gpio);
}
//停止I2C
err:
sw_i2c_stop(gpio);
}
5、主设备向从设备8Bit长度的寄存器地址读取N个字节
void sw_i2c_send2read_8bit(sw_i2c_gpio_t *gpio,uint8_t i2c_addr,uint8_t reg,uint8_t *buf,uint8_t len)
{
uint8_t j;
//如果使用的是7bit地址,需要左位移一位
#ifdef I2C_USE_7BIT_ADDR
i2c_addr = i2c_addr<<1;
#endif
//启动I2C
sw_i2c_start(gpio);
//写I2C从机地址,写操作
sw_i2c_write_byte(gpio,i2c_addr);
//如果从机响应ACC则继续,如果从机未响应ACK则停止
if (!sw_i2c_wait_ack(gpio))
goto err;
//写寄存器地址
sw_i2c_write_byte(gpio,reg);
if (!sw_i2c_wait_ack(gpio))
goto err;
//重新启动I2C
sw_i2c_start(gpio);
//写I2C从机地址,读操作
sw_i2c_write_byte(gpio,i2c_addr|0x01);
if (!sw_i2c_wait_ack(gpio))
goto err;
//开始读n个字节数据
for (j=0;j<len;j++)
{
buf[j]=sw_i2c_read_byte(gpio);
// 每读一个字节数据后,都要发送ACK给从机
sw_i2c_send_ack(gpio);
}
//停止I2C
err:
sw_i2c_stop(gpio);
}