文章目录
- 介绍:
- 两种实现方式:
- I2C设备的常用连接方式:
- I2C协议
- 时序:
- STM32硬件I2C框架图
- I2C外设通讯过程
- **I2C读写EEPROM**(硬件I2C)
介绍:
-
两根通信线SCL(时钟线)、SDA(数据线)
-
同步半双工,支持总线挂载多设备
两种实现方式:
I2C有硬件I2C和软件I2C两种实现方式
硬件I2C:STM32 的 I2C 片上外设专门负责实现 I2C 通讯协议,只要配置好该外设,它就会自动根据协议要求产生通讯信号,收发数据并缓存起来,CPU 只要检测该外设的状态和访问数据寄存器,就能完成数据收发。这种由硬件外设处理 I2C 协议的方式减轻了CPU 的工作,且使软件设计更加简单
软件I2C:通过控制IO口高低电平模拟I2C协议所需电平,实现软件I2C直接控制 STM32 的两个 GPIO 引脚,分别用作 SCL 及 SDA,按照上述信号的时序要求,直接像控制 LED 灯那样控制引脚的输出 (若是接收数据时则读取 SDA 电平),就可以实现 I2C 通讯。同样,假如我们按照 USART 的要求去控制引脚,也能实现 USART 通讯。所以只要遵守协议,就是标准的通讯,不管您如何实现它,不管是 ST 生产的控制器还是 ATMEL 生产的存储器,都能按通讯标准交互。由于直接控制 GPIO 引脚电平产生通讯时序时,需要由 CPU 控制每个时刻的引脚状态,所以称之为“软件模拟协议”方式。
I2C设备的常用连接方式:
I2C协议
-
S代表起始位
-
SLAVE ADDRESS代表从机地址,表示主机要向哪个从机发送数据
-
RW表示读还是写,0表示主机发送数据,1表示主机接收数据
-
A代表应答位,主机向从机发送完从机地址后,被选中的从机会向主机发送一个应答位,表示接收到了主机发送的数据
-
DATA是数据,一次发送八位二进制数据,从机在接收到数据后会发送一个应答位,
-
P是停止位
-
I2C 使用 SDA 信号线来传输数据,使用 SCL 信号线进行数据同步。见图数据有效性 。SDA 数据线在 SCL 的每个时钟周期传输一位数据。传输时,SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL为低电平时,SDA的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。
-
I2C总线上每个设备都有自己的独立地址,主机发起通讯时通过SDA发送设备地址(SLAVE ADDRESS)来查找从机,I2C协议规定从机地址可以是7位也可以是10位,设备地址后一位是数据方向位,第八位或者第十一位,0表示主机发送,1表示主机接收
-
I2C 的数据和地址传输都带响应。响应包括“应答 (ACK)”和“非应答 (NACK)”两种信号。作为数据接收端时,当设备 (无论主从机) 接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答 (ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答 (NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
时序:
- 起始信号和停止信号
起始条件:SCL高电平期间,SDA从高电平切换到低电平
停止条件:SCL高电平期间,SDA从低电平切换到高电平
- 发送数据位
SCL低电平期间,主机将数据位放到SDA(高位先行),释放SCL(高电平),从机在SCL高电平期间读取数据位,重复八次发送一个字节
-
接收数据位
-
发送应答
主机在接收完一个字节后会在下一个时钟向从机发送一位数据,数据0表示应答,1表示非应答
- 接收应答
从机在接收完一个字节后会在下一个时钟向主机发送一位数据,数据0表示应答,1表示非应答(主机在接收之前需要释放SDA(高电平))
STM32硬件I2C框架图
-
STM32F4的I2C引脚橙色部分为I2C的外接通信线,SDA为数据线,SCL为时钟线。SMBA线用于 SMBUS 的警告信号,I2C 通讯没有使用。STM32有多个I2C外设,对应的引脚如下:
引脚 I2C 编号 I2C1 I2C2 I2C3 SCL PB6/PB10 PH4/PF1/PB10 PH7/PA8 SDA PB7/PB9 PH5/PF0/PB11 PH8/PC9 -
时钟控制逻辑电路
紫色部分为I2C的时钟控制逻辑电路,为I2C通信提供时钟。通过寄存器配置I2C的通信速率以及占空比。
-
I2C 的 SDA 信号主要连接到数据移位寄存器上,数据移位寄存器的数据来源及目标是数据寄存器 (DR)、地址寄存器 (OAR)、PEC 寄存器以及 SDA 数据线。当向外发送数据的时候,数据移位寄存器以“数据寄存器”为数据源,把数据一位一位地通过 SDA 信号线发送出去;当从外部接收数据的时候,数据移位寄存器把 SDA 信号线采样到的数据一位一位地存储到“数据寄存器”中。若使能了数据校验,接收到的数据会经过 PCE 计算器运算,运算结果存储在“PEC 寄存器”中。当 STM32 的 I2C 工作在从机模式的时候,接收到设备地址信号时,数据移位寄存器会把接收到的地址与 STM32 的自身的“I2C 地址寄存器”的值作比较,以便响应主机的寻址。STM32 的自身 I2C 地址可通过修改“自身地址寄存器”修改,支持同时使用两个 I2C 设备地址,两个地址分别存储在 OAR1 和 OAR2 中。
-
整体控制逻辑负责协调整个 I2C 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器 (SR1 和 SR2)”,我们只要读取这些寄存器相关的寄存器位,就可以了解 I2C 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 I2C 中断信号、DMA 请求及各种 I2C 的通讯信号(起始、停止、响应信号等)。
I2C外设通讯过程
作为主发送器
STM32的I2C在通信过程中会产生事件,标志位寄存器里面的数据通过不同的组合可以产生不同的事件。我们可以通过事件来判断I2C通信的信号是否发出。
EV5:SR1寄存器的SB位置1表示起始信号已经发送。
EV6:SR1寄存器的ADDR置1表示地址已经发送
EV8:SR1寄存器的TXE置1表示数据寄存器为空
EV8_2:TXE以及BTF置1,TXE置1表示数据寄存器为空,BTF置1表示通信结束
作为主接收器
EV7:SR1 寄存器的 RXNE 被置 1,表示接收数据寄存器非空。
这些事件发生之后对应的寄存器位需要软件清零,我们可以通过库函数的方式来监测事件并清零
I2C初始化结构体
typedef struct {
uint32_t I2C_ClockSpeed; /*!< 设置 SCL 时钟频率,此值要低于 40 0000*/
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读写EEPROM(硬件I2C)
EEPROM写时序
-
首先I2C发送一个起始信号表示开始通信,然后发送从机地址,表示主机想要和哪个设备进行通信,因为设备地址为7位,所以最后一位R/W表示读写位,1表示读,0表示写。
-
之后从机向主机发送一个应答信号,主机在接收到应答信号后可以发送数据了,EEPROM规定在发送数据之前需要先告诉EEPROM要读或者写入的地址。所以第一个字节用来表示EEPROM的内部地址,从机接收到地址后向主机发送应答表示从机收到,之后主机就可以向从机发送想要写入的数据了。
-
主机发送数据从机应答后主机就可以发送停止信号表示本次通信结束。
EEPROM读时序
-
向EEPROM读数据是一个复合时序,由一个写时序和一个读时序去掉中间的停止信号复合而成(停止信号也可以不用去)
-
首先主机发送设备地址及要读的地址后,EEPROM的指针便指向要读的地址。
-
然后主机重新发送一个起始信号,读写方向为读,EEPROM便向主机发送指针所指向的地址的数据,主机只需要应答,在读完后发送一个非应答信号表示主机不再接受,通信结束。
代码
代码部分只需要按照I2C的时序,一步一步来即可
发送一字节数据
uint32_t I2C_EE_ByteWrite(u8 WriteAddr,u8 data)
{
/* 发送开始条件 */
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测EV5事件并清零寄存器 */
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
}
/* 发送EEPROM地址 */
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_Address, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测EV6事件并清零寄存器 */
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
}
/* 发送EEPROM内部地址 */
I2C_SendData(EEPROM_I2C, WriteAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测EV8事件并清零寄存器 */
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
}
/* 发送要写入的数据 */
I2C_SendData(EEPROM_I2C, data);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测EV8事件并清零寄存器 */
while(!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
}
/* 停止条件 */
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
return 1;
}
这里设置了超时回调函数,方便快速定位到错误代码处,方便调试
超时回调函数
static uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* 使用串口 printf 输出错误信息,方便调试 */
printf("I2C 等待超时!errorCode = %d",errorCode);
return 0;
}
读取指定地址数据
uint8_t I2C_EE_ReadByte(u8 ReadAddr,u8 *data)
{
//检测I2C是否繁忙
while(I2C_GetFlagStatus(EEPROM_I2C, I2C_FLAG_BUSY))
{
if((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(9);
}
//开始
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
/* 设置超时等待时间 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(00);
}
//发送设备地址,方向为写
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_Address, I2C_Direction_Transmitter);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(11);
}
I2C_Cmd(EEPROM_I2C, ENABLE);
//发送要读的地址
I2C_SendData(EEPROM_I2C,ReadAddr);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_TRANSMITTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(22);
}
//重新开始
I2C_GenerateSTART(EEPROM_I2C, ENABLE);
/* 设置超时等待时间 */
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_MODE_SELECT))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(33);
}
//发送设备地址,方向为读
I2C_Send7bitAddress(EEPROM_I2C, EEPROM_Address, I2C_Direction_Receiver);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(44);
}
//关闭自动应答(在初始化I2C结构体的时候设置了自动应答)
I2C_AcknowledgeConfig(EEPROM_I2C, DISABLE);
I2CTimeout = I2CT_FLAG_TIMEOUT;
/* 检测 EV事件并清除标志 */
while (!I2C_CheckEvent(EEPROM_I2C, I2C_EVENT_MASTER_BYTE_RECEIVED))
{
if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(55);
}
*data = I2C_ReceiveData(EEPROM_I2C);
//结束
I2C_GenerateSTOP(EEPROM_I2C, ENABLE);
return 0;
}