STM32 IIC通信详解及实验教程(野火指南者)
一、简介
iic是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路 (IC) 间的通讯。
二、物理层
iic只由两条总线构成:
SDA(Serial Data)是数据线,D代表Data也就是数据,用来传输数据的串行总线
SCL(Serial Clock)是时钟线,C代表Clock也就是时钟,控制数据收发时序的串行总线
物理层特点:
(1)
它是一个支持设备的总线。“总线”指多个设备共用的信号线。在一个
I2C
通讯总线中,可连
接多个 I2C
通讯设备,支持多个通信主机及多个通讯从机。
(2)
一个
I2C
总线只使用两条总线线路,一条双向串行数据线
(SDA)
,一条串行时钟线
(SCL)
。数
据线即用来表示数据,时钟线用于数据收发同步。
(3) 每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访 问。
(4)
总线通过上拉电阻接到电源。当
I2C
设备空闲时,会输出高阻态,而当所有设备都空闲,都
输出高阻态时,由上拉电阻把总线拉成高电平。
(5)
多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
(6)
具有三种传输模式:标准模式传输速率为
100kbit/s
,快速模式为
400kbit/s
,高速模式下可达
3.4Mbit/s,但目前大多
II
C
设备尚不支持高速模式。
(7)
连接到相同总线的
IC
数量受到总线的最大电容
400pF
限制。
三、协议层
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
3.1基本读写过程
先简单看看 IIC
通讯过程的基本结构,它的通信过程见图
主机写数据到从机
、图
主机由从机中读数
据及图 IIC 通信复合格式 。
3.1.1 写数据
若配置的方向传输位为“写数据”方向,广播完地址,接收到应答信号后,主机开始正式向从机传输数据
(DATA)
,数据包的大小为
8
位,主机每发送完一个字节数据,都
要等待从机的应答信号
(ACK)
,重复这个过程,可以向从机传输
N
个数据,这个
N
没有大小限
制。当数据传输结束时,主机向从机发送一个停止传输信号
(P)
,表示不再传输数据。
3.1.2 读数据
若配置的方向传输位为“读数据”方向,广播完地址,接收到应答信号后,
从机开始向主机返回数据
(DATA)
,数据包大小也为
8
位,从机每发送完一个数据,都会等待主
机的应答信号
(ACK)
,重复这个过程,可以返回
N
个数据,这个
N
也没有大小限制。当主机希
望停止接收数据时,就向从机返回一个非应答信号
(NACK)
,则从机自动停止数据传输。
3.1.3 复合读写格式
除了基本的读写,I2C
通讯更常用的是复合格式,该传输过程有两次起始信
号
(S)
。一般在第一次传输中,主机通过
SLAVE_ADDRESS
寻找到从设备后,发送一段“数据”,
这段数据通常用于表示从设备内部的寄存器或存储器地址
(
注意区分它与
SLAVE_ADDRESS
的
区别
)
;在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读
写地址,第二次则是读写的实际内容。
3.2 起始信号和停止信号
前文中提到的起始 (S)
和停止
(P)
信号是两种特殊的状态,见下图
。当
SCL
线是高
电平时
SDA
线从高电平向低电平切换,这个情况表示通讯的起始。当
SCL
是高电平时
SDA
线由
低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
3.3 数据有效性
I2C 使用
SDA
信号线来传输数据,使用
SCL
信号线进行数据同步。见下图
。
SDA
数据
线在
SCL
的每个时钟周期传输一位数据。传输时,
SCL
为高电平的时候
SDA
表示的数据有效,
即此时的
SDA
为高电平时表示数据“
1
”,为低电平时表示数据“
0
”。当
SCL
为低电平时,
SDA
的数据无效,一般在这个时候
SDA
进行电平切换,为下一次表示数据做好准备。
3.4 地址及数据方向
I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS) 来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位 (R/),第 8 位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。
3.5 响应
I2C 的数据和地址传输都带响应。响应包括“应答
(ACK)
”和“非应答
(NACK)
”两种信号。作为
数据接收端时,当设备
(
无论主从机
)
接收到
I2C
传输的一个字节数据或地址后,若希望对方继
续发送数据,则需要向对方发送“应答
(ACK)
”信号,发送方会继续发送下一个数据;若接收端
希望结束数据传输,则向对方发送“非应答
(NACK)
”信号,发送方接收到该信号后会产生一个
停止信号,结束信号传输。
传输时主机产生时钟,在第 9
个时钟时,数据发送端会释放
SDA
的控制权,由数据接收端控制
SDA
,若
SDA
为高电平,表示非应答信号
(NACK)
,低电平表示应答信号
(ACK)
。
四、STM32 的 IIC 架构
STM32 的
I2C
外设可用作通讯的主机及从机,支持
100Kbit/s
和
400Kbit/s
的速率,支持
7
位、
10位设备地址,支持
DMA
数据传输,并具有数据校验功能。它的
I2C
外设还支持
SMBus2.0
协议,
SMBus
协议与
I2C
类似,主要应用于笔记本电脑的电池管理中,本文不展开。
下图为STM32 的 IIC 架构图,下文将分为四部分进行讲解。
4.1 通信引脚
II
C
的所有硬件架构都是根据图中左侧
SCL
线和
SDA
线展开的
(
其中的
SMBA
线用于
SMBUS
的警告信号,
I2C
通讯没有使用
)
。
STM32
芯片有多个
I2C
外设,它们的
I2C
通讯信号引出到不同的
GPIO
引脚上,使用时必须配置到这些指定的引脚。
4.2 时钟控制逻辑
SCL
线的时钟信号,由
II
C
接口根据时钟控制寄存器
(CCR)
控制,控制的参数主要为时钟频率。
配置
I2C
的
CCR
寄存器可修改通讯速率相关的参数:
信号线的输出时钟公式如下:
例如,我们的
PCLK1=36MHz
,想要配置
400Kbit/s
的速率,计算方式如下:
PCLK
时钟周期:
TPCLK1 = 1/36000000
目标
SCL
时钟周期:
TSCL = 1/400000
SCL
时钟周期内的高电平时间:
THIGH = TSCL/3
SCL
时钟周期内的低电平时间:
TLOW = 2*TSCL/3
计算
CCR
的值:
CCR = THIGH/TPCLK1 = 30
计算结果得出
CCR
为
30
,向该寄存器位写入此值则可以控制
IIC
的通讯速率为
400KHz
,其实即使配置出来的
SCL
时钟不完全等于标准的
400KHz
,
IIC
通讯的正确性也不会受到影响,因为所
有数据通讯都是由
SCL
协调的,只要它的时钟频率不远高于标准即可。
4.3 数据控制逻辑
I2C
的
SDA
信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存
器
(DR)
、地址寄存器
(OAR)
、
PEC
寄存器以及
SDA
数据线。当向外发送数据的时候,数据移位
寄存器以“数据寄存器”为数据源,把数据一位一位地通过
SDA
信号线发送出去;当从外部接收
数据的时候,数据移位寄存器把
SDA
信号线采样到的数据一位一位地存储到“数据寄存器”中。
若使能了数据校验,接收到的数据会经过
PCE
计算器运算,运算结果存储在“
PEC
寄存器”中。
当
STM32
的
I2C
工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到
的地址与
STM32
的自身的“
I2C
地址寄存器”的值作比较,以便响应主机的寻址。
STM32
的自
身
I2C
地址可通过修改“自身地址寄存器”修改,支持同时使用两个
I2C
设备地址,两个地址分
别存储在
OAR1
和
OAR2
中。
4.4 整体逻辑控制
整体控制逻辑负责协调整个 I2C
外设,控制逻辑的工作模式根据我们配置的“控制寄存器
(CR1/CR2)
”的参数而改变。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存
器
(SR1
和
SR2)
”,我们只要读取这些寄存器相关的寄存器位,就可以了解
I2C
的工作状态。除
此之外,控制逻辑还根据要求,负责控制产生
I2C
中断信号、
DMA
请求及各种
I2C
的通讯信号
(
起始、停止、响应信号等
)
。
4.5 具体通信过程
使用 I2C
外设通讯时,在通讯的不同阶段它会对“状态寄存器
(SR1
及
SR2)
”的不同数据位写入
参数,我们通过读取这些寄存器标志来了解通讯状态。
4.5.1 主发送器

主发送器发送流程及事件说明如下:
(1)
控制产生起始信号
(S)
,当发生起始信号后,它产生事件“
EV5
”,并会对
SR1
寄存器的“
SB
”位置
1
,表示起始信号已经发送;
(2)
紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“
EV6
”及“
EV8
”,这时SR1
寄存器的“
ADDR
”位及“
TXE
”位被置
1
,
ADDR
为
1
表示地址已经发送,
TXE
为
1 表示
数据寄存器为空;
(3)
以上步骤正常执行并对
ADDR
位清零后,我们往
I2C
的“数据寄存器
DR
”写入要发送的数据,这时
TXE
位会被重置
0
,表示数据寄存器非空,
I2C
外设通过
SDA
信号线一位位把数据发送
出去后,又会产生“
EV8
”事件,即
TXE
位被置
1
,重复这个过程,就可以发送多个字节数据了;
(4)
当我们发送数据完成后,控制
I2C
设备产生一个停止信号
(P)
,这个时候会产生
EV8_2
事件,SR1
的
TXE
位及
BTF
位都被置
1
,表示通讯结束。
假如我们使能了 I2C
中断,以上所有事件产生时,都会产生
I2C
中断信号,进入同一个中断服务
函数,到
I2C
中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
4.5.2 主接收器
主接收器接收流程及事件说明如下:
(1)
同主发送流程,起始信号
(S)
是由主机端产生的,控制发生起始信号后,它产生事件“
EV5
”,并会对
SR1
寄存器的“
SB
”位置
1
,表示起始信号已经发送;
(2)
紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“
EV6
”这时
SR1
寄存器
的“
ADDR
”位被置
1
,表示地址已经发送。
(3)
从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“
EV7
”事件,
SR1
寄存器的
RXNE
被置
1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据
寄存器清空,以便接收下一次数据。此时我们可以控制
I2C
发送应答信号
(ACK)
或非应答信号
(NACK)
,若应答,则重复以上步骤接收数据,若非应答,则停止传输;
(4)
发送非应答信号后,产生停止信号
(P)
,结束传输。在发送和接收过程中,有的事件不只是标志了我们上面提到的状态位,还可能同时标志主机状态
之类的状态位,而且读了之后还需要清除标志位,比较复杂。我们可使用
STM32
标准库函数来
直接检测这些事件的复合标志,降低编程难度。
五、IIC 通信实验
5.1 IIC结构体
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;
(1) I2C_ClockSpeed
本成员设置的是 I2C 的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把时钟因子写入到 I2C 的时钟控制寄存器 CCR。而我们写入的这个参数值不得高于 400KHz。实际上由于 CCR 寄存器不能写入小数类型的时钟因子,影响到 SCL 的实际频率可能会低于本成员设置的参数值,这时除了通讯稍慢一点以外,不会对 I2C 的标准通讯造成其它影响。
(2) I2C_Mode
本成员是选择 I
2
C
的使用方式,有
I
2
C
模式
(I2C_Mode_I2C)
和
SMBus
主、从模式
(I2C_Mode_SMBusHost
、
I2C_Mode_SMBusDevice )
。
I2C
不需要在此处区分主从模式,直接设
置
I2C_Mode_I2C
即可。
(3) I2C_DutyCycle
本成员设置的是 I
2
C
的
SCL
线时钟的占空比。该配置有两个选择,分别为低电平时间比高电平时间为
2
:
1 ( I2C_DutyCycle_2)
和
16
:
9 (I2C_DutyCycle_16_9)
。其实这两个模式的比例差别并不
大,一般要求都不会如此严格,这里随便选就可以。
(4) I2C_OwnAddress1
本成员配置的是 STM32
的
I2C
设备自己的地址,每个连接到
I2C
总线上的设备都要有一个自己
的地址,作为主机也不例外。地址可设置为
7
位或
10
位
(
受下面
I2C_AcknowledgeAddress
成员决
定
)
,只要该地址是
I2C
总线上唯一的即可。
STM32
的
I2C
外设可同时使用两个地址,即同时对两个地址作出响应,这个结构成员
I2C_OwnAddress1
配置的是默认的、
OAR1
寄存器存储的地址,若需要设置第二个地址寄存器
OAR2
,可使用
I2C_OwnAddress2Config
函数来配置,
OAR2
不支持
10
位地址,只有
7
位。
(5) I2C_Ack_Enable
本成员是关于 I
2
C
应答设置,设置为使能则可以发送响应信号。本实验配置为允许应答
(I2C_Ack_Enable)
,这是绝大多数遵循
I
2
C
标准的设备的通讯要求,改为禁止应答
(I2C_Ack_Disable)
往往会导致通讯错误。
(6) I2C_AcknowledgeAddress
本成员选择 I2C
的寻址模式是
7
位还是
10
位地址。这需要根据实际连接到
I2C
总线上设备的地址进行选择,这个成员的配置也影响到
I2C_OwnAddress1
成员,只有这里设置成
10
位模式时,
I2C_OwnAddress1
才支持
10
位地址。
5.2 核心代码
我使用的是野火的指南者开发板,代码仅供参考。
5.2.1 硬件IIC
以 STM32 I2C1外设为例,它连接了一个EEPROM,硬件 IIC 即直接使用片上IIC外设,但是硬件 IIC 不稳定,有时会莫名卡住,还是推荐使用软件IIC。
/**
* @brief I2C I/O配置
* @param 无
* @retval 无
*/
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能与 I2C 有关的时钟 */
EEPROM_I2C_APBxClock_FUN ( EEPROM_I2C_CLK, ENABLE );
EEPROM_I2C_GPIO_APBxClock_FUN ( EEPROM_I2C_GPIO_CLK, ENABLE );
/* I2C_SCL、I2C_SDA*/
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(EEPROM_I2C_SDA_PORT, &GPIO_InitStructure);
}
/**
* @brief I2C 工作模式配置
* @param 无
* @retval 无
*/
static void I2C_Mode_Config(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
/* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 =I2Cx_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
/* I2C的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
/* 通信速率 */
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
/* I2C 初始化 */
I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
/* 使能 I2C */
I2C_Cmd(EEPROM_I2Cx, ENABLE);
}
/**
* @brief 从EEPROM里面读取一块数据
* @param
* @arg pBuffer:存放从EEPROM读取的数据的缓冲区指针
* @arg WriteAddr:接收数据的EEPROM的地址
* @arg NumByteToWrite:要从EEPROM读取的字节数
* @retval 无
*/
uint32_t I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
//*((u8 *)0x4001080c) |=0x80;
while(I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
}
/* Send START condition */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
//*((u8 *)0x4001080c) &=~0x80;
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV5 and clear it */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(10);
}
/* Send EEPROM address for write */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV6 and clear it */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
}
/* Clear EV6 by setting again the PE bit */
I2C_Cmd(EEPROM_I2Cx, ENABLE);
/* Send the EEPROM's internal address to write to */
I2C_SendData(EEPROM_I2Cx, ReadAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV8 and clear it */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(12);
}
/* Send STRAT condition a second time */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV5 and clear it */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(13);
}
/* Send EEPROM address for read */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Receiver);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV6 and clear it */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(14);
}
/* While there is data to be read */
while(NumByteToRead)
{
if(NumByteToRead == 1)
{
/* Disable Acknowledgement */
I2C_AcknowledgeConfig(EEPROM_I2Cx, DISABLE);
/* Send STOP Condition */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
}
/* Test on EV7 and clear it */
I2CTimeout = I2CT_LONG_TIMEOUT;
while(I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)==0)
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
{
/* Read a byte from the EEPROM */
*pBuffer = I2C_ReceiveData(EEPROM_I2Cx);
/* Point to the next location where the byte read will be saved */
pBuffer++;
/* Decrement the read bytes counter */
NumByteToRead--;
}
}
/* Enable Acknowledgement to be ready for another reception */
I2C_AcknowledgeConfig(EEPROM_I2Cx, ENABLE);
return 1;
}
/**
* @brief 在EEPROM的一个写循环中可以写多个字节,但一次写入的字节数
* 不能超过EEPROM页的大小,AT24C02每页有8个字节
* @param
* @arg pBuffer:缓冲区指针
* @arg WriteAddr:写地址
* @arg NumByteToWrite:写的字节数
* @retval 无
*/
uint32_t I2C_EE_PageWrite(u8* pBuffer, u8 WriteAddr, u8 NumByteToWrite)
{
I2CTimeout = I2CT_LONG_TIMEOUT;
while(I2C_GetFlagStatus(EEPROM_I2Cx, I2C_FLAG_BUSY))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(4);
}
/* Send START condition */
I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV5 and clear it */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(5);
}
/* Send EEPROM address for write */
I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV6 and clear it */
while(!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(6);
}
/* Send the EEPROM's internal address to write to */
I2C_SendData(EEPROM_I2Cx, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV8 and clear it */
while(! I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(7);
}
/* While there is data to be written */
while(NumByteToWrite--)
{
/* Send the current byte */
I2C_SendData(EEPROM_I2Cx, *pBuffer);
/* Point to the next byte to be written */
pBuffer++;
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* Test on EV8 and clear it */
while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(8);
}
}
/* Send STOP condition */
I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);
return 1;
}
5.2.2软件 IIC
就是用两个 GPIO 模拟 IIC 的 SDA 和 SCL 总线,用软件拉高拉低的它们的电平,模拟出 IIC 协议。
/*
*********************************************************************************************************
* 函 数 名: i2c_Delay
* 功能说明: I2C总线位延迟,最快400KHz
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
static void i2c_Delay(void)
{
uint8_t i;
/*
下面的时间是通过逻辑分析仪测试得到的。
工作条件:CPU主频72MHz ,MDK编译环境,1级优化
循环次数为10时,SCL频率 = 205KHz
循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us
循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us
*/
for (i = 0; i < 10; i++);
}
/*
*********************************************************************************************************
* 函 数 名: i2c_Start
* 功能说明: CPU发起I2C总线启动信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Start(void)
{
/* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
EEPROM_I2C_SDA_1();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SDA_0();
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
}
/*
*********************************************************************************************************
* 函 数 名: i2c_Stop
* 功能说明: CPU发起I2C总线停止信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Stop(void)
{
/* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
EEPROM_I2C_SDA_0();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SDA_1();
}
/*
*********************************************************************************************************
* 函 数 名: i2c_SendByte
* 功能说明: CPU向I2C总线设备发送8bit数据
* 形 参:_ucByte : 等待发送的字节
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_SendByte(uint8_t _ucByte)
{
uint8_t i;
/* 先发送字节的高位bit7 */
for (i = 0; i < 8; i++)
{
if (_ucByte & 0x80)
{
EEPROM_I2C_SDA_1();
}
else
{
EEPROM_I2C_SDA_0();
}
i2c_Delay();
EEPROM_I2C_SCL_1();
i2c_Delay();
EEPROM_I2C_SCL_0();
if (i == 7)
{
EEPROM_I2C_SDA_1(); // 释放总线
}
_ucByte <<= 1; /* 左移一个bit */
i2c_Delay();
}
}
/*
*********************************************************************************************************
* 函 数 名: i2c_ReadByte
* 功能说明: CPU从I2C总线设备读取8bit数据
* 形 参:无
* 返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t i2c_ReadByte(void)
{
uint8_t i;
uint8_t value;
/* 读到第1个bit为数据的bit7 */
value = 0;
for (i = 0; i < 8; i++)
{
value <<= 1;
EEPROM_I2C_SCL_1();
i2c_Delay();
if (EEPROM_I2C_SDA_READ())
{
value++;
}
EEPROM_I2C_SCL_0();
i2c_Delay();
}
return value;
}
/*
*********************************************************************************************************
* 函 数 名: i2c_WaitAck
* 功能说明: CPU产生一个时钟,并读取器件的ACK应答信号
* 形 参:无
* 返 回 值: 返回0表示正确应答,1表示无器件响应
*********************************************************************************************************
*/
uint8_t i2c_WaitAck(void)
{
uint8_t re;
EEPROM_I2C_SDA_1(); /* CPU释放SDA总线 */
i2c_Delay();
EEPROM_I2C_SCL_1(); /* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
i2c_Delay();
if (EEPROM_I2C_SDA_READ()) /* CPU读取SDA口线状态 */
{
re = 1;
}
else
{
re = 0;
}
EEPROM_I2C_SCL_0();
i2c_Delay();
return re;
}
/*
*********************************************************************************************************
* 函 数 名: i2c_Ack
* 功能说明: CPU产生一个ACK信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_Ack(void)
{
EEPROM_I2C_SDA_0(); /* CPU驱动SDA = 0 */
i2c_Delay();
EEPROM_I2C_SCL_1(); /* CPU产生1个时钟 */
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
EEPROM_I2C_SDA_1(); /* CPU释放SDA总线 */
}
/*
*********************************************************************************************************
* 函 数 名: i2c_NAck
* 功能说明: CPU产生1个NACK信号
* 形 参:无
* 返 回 值: 无
*********************************************************************************************************
*/
void i2c_NAck(void)
{
EEPROM_I2C_SDA_1(); /* CPU驱动SDA = 1 */
i2c_Delay();
EEPROM_I2C_SCL_1(); /* CPU产生1个时钟 */
i2c_Delay();
EEPROM_I2C_SCL_0();
i2c_Delay();
}
作者:嵌功尽弃