一、I2C协议简介
1、I2C物理层
1、在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
2、一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。
3、每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
4、总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
2、I2C协议层
1、I2C读写过程
主机写数据到从机:
若配置的方向传输位为“写数据”方向,广播完地址,接收到应答信号后, 主机开始正式向从机传输数据,数据包的大小为 8 位,主机每发送完一个字节数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输 N 个数据。当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。
主机由从机中读数据:
若配置的方向传输位为“读数据”方向, 广播完地址,接收到应答信号后, 从机开始向主机返回数据,数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。
复合通讯:
该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。
2、通讯的起始和停止信号
当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
3、地址及数据方向
I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向数据方向位,第 8 位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,写数据方向时, SDA 由主机控制,从机接收信号。
4、响应
I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接
收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。
二、 I2C架构
引脚:
时钟控制逻辑:
SCL 线的时钟信号,由 I2C 接口根据时钟控制寄存器(CCR)控制。
1、可选择 I2C 通讯的“标准/快速”模式,这两个模式分别 I2C 对应 100/400Kbit/s 的通讯速率。
2、在快速模式下可选择 SCL 时钟的占空比,可选 Tlow/Thigh=2 或 Tlow/Thigh=16/9模式。
3、STM32的I2C外设挂载在APB1总线上,使用APB1的时钟源PCLK1。
SCL信号线输出时钟计算:
通讯过程 :
1、主发送器
2、主接收器
三、I2C初始化结构体
四、读写EEPROM
EEPROM芯片的设备地址一共有 7 位,其中高 4 位固定为:1010 b,低 3 位则由 A0/A1/A2 信号线的电平决定,R/W 是读写方向位,与地址无关。
按照我们此处的连接, A0/A1/A2均为 0,所以 EEPROM的7位设备地址是: 101 0000b ,即 0x50。由于 I2C 通讯时常常是地址跟读写方向连在一起构成一个 8 位数,且当 R/W 位为0 时,表示写方向,所以加上 7 位地址,其值为“0xA0”,常称该值为 I2C 设备的“写地址”;当 R/W 位为 1 时,表示读方向,加上 7 位地址,其值为“0xA1”,常称该值为“读地址”。
EEPROM 芯片中还有一个 WP 引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时,可写入数据,我们直接接地,不使用写保护功能。
1、 初始化I2C的GPIO
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能与 I2C 有关的时钟 */
RCC_APB1PeriphClockCmd ( RCC_APB1Periph_I2C1, ENABLE );
RCC_APB2PeriphClockCmd ( RCC_APB2Periph_GPIOB, ENABLE );
/* I2C_SCL、 I2C_SDA*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
2 、配置I2C模式
static void I2C_Mode_Configu(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
/* 高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 */
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
//I2C主机地址,随机
I2C_InitStructure.I2C_OwnAddress1 =0x5F;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
/* I2C 的寻址模式 */
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
/* 通信速率 */
I2C_InitStructure.I2C_ClockSpeed = 400000;
/* I2C 初始化 */
I2C_Init(I2C1, &I2C_InitStructure);
/* 使能 I2C */
I2C_Cmd(I2C1, ENABLE);
}
3、向EEPROM写入一个字节的数据
EEPROM 单字节写入时序:
EPROM 的单字节时序规定,向它写入数据的时候,第一个字节为内存地址,第二个字节是要写入的数据内容。
//EEPROM内存256K,内存地址设置为8位uint8_t
void EEPROM_Byte_Write(uint8_t addr,uint8_t data)
{
//产生起始信号
I2C_GenerateSTART(I2C1, ENABLE);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//EV5时间被检测到,发送设备地址
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
//EV6事件被检测到,发送要操作的存储单元地址
I2C_SendData(I2C1, addr);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR);
//EV8事件被检测到,发送要存储的数据
I2C_SendData(I2C1, data);
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
//数据传输完成
I2C_GenerateSTOP(I2C1, ENABLE);
}
4、EEPROM的页写入
EEPROM 页写入时序:
向连续地址写入多个数据的时候,只要告诉 EEPROM 第一个内存地址 address1,后面的数据按次序写入到 address2、 address3… 这样可以节省通讯的时间,加快速度。根据页写入时序,第一个数据被解释为要写入的内存地址 address1,后续可连续发送 n个数据,这些数据会依次写入到内存中。其中 AT24C02 型号的芯片页写入时序最多可以一次发送 8 个数据(即 n = 8 ),该值也称为页大小。
//向EEPROM写入多个字节(页写入),每次写入不能超过8个
//EEPROM内存256K,内存地址设置为8位uint8_t
void EEPROM_Page_Write(uint8_t addr,uint8_t *data,uint8_t numByteToWrite)
{
//产生起始信号
I2C_GenerateSTART(I2C1, ENABLE);
//检测EV5事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//发送设备地址
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
//检测EV6事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
//发送要操作的存储单元地址
I2C_SendData(I2C1, addr);
//检测EV8事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR);
while(numByteToWrite)
{
//发送要存储的数据
I2C_SendData(I2C1, *data);
//检测EV8事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
data++;
numByteToWrite--;
}
//数据传输完成
I2C_GenerateSTOP(I2C1, ENABLE);
}
5、利用页写入的方式快速写入多字节
首地址对齐到页时的情况:
首地址未对齐到页时的情况
#define I2C_PageSize 8
/**
* @brief 将缓冲区中的数据写到 I2C EEPROM 中
* @param
* @arg pBuffer:缓冲区指针
* @arg WriteAddr:写地址
* @arg NumByteToWrite:写的字节数
* @retval 无
*/
void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr,u16 NumByteToWrite)
{
uint8_t NumOfPage=0,NumOfSingle=0,Addr =0,count=0;
/*mod 运算求余,若 writeAddr 是 I2C_PageSize 整数倍,运算结果 Addr 值为 0*/
Addr = WriteAddr % I2C_PageSize;
/*差 count 个数据值,刚好可以对齐到页地址*/
count = I2C_PageSize - Addr;
/*计算出要写多少整数页*/
NumOfPage = NumByteToWrite / I2C_PageSize;
/*mod 运算求余,计算出剩余不满一页的字节数*/
NumOfSingle = NumByteToWrite % I2C_PageSize;
// Addr=0,则 WriteAddr 刚好按页对齐 aligned
// 这样就很简单了,直接写就可以,写完整页后
// 把剩下的不满一页的写完即可
if (Addr == 0) {
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage == 0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitForWritingEnd();
}
/* 如果 NumByteToWrite > I2C_PageSize */
else {
/*先把整数页都写了*/
while (NumOfPage--) {
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitForWritingEnd();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle!=0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitForWritingEnd();
}
}
}
// 如果 WriteAddr 不是按 I2C_PageSize 对齐
// 那就算出对齐到页地址还需要多少个数据,然后
// 先把这几个数据写完,剩下开始的地址就已经对齐
// 到页地址了,代码重复上面的即可
else {
/* 如果 NumByteToWrite < I2C_PageSize */
if (NumOfPage== 0) {
if (NumOfSingle > count) {
I2C_EE_PageWrite(pBuffer, WriteAddr,NumOfSingle);
I2C_EE_WaitForWritingEnd();
WriteAddr += count;
pBuffer += count;
}
}
/* 如果 NumByteToWrite > I2C_PageSize */
else {
/*地址不对齐多出的 count 分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
/*先把 WriteAddr 所在页的剩余字节写了*/
if (count != 0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, count);
I2C_EE_WaitForWritingEnd();
/*WriteAddr 加上 count 后,地址就对齐到页了*/
WriteAddr += count;
pBuffer += count;
}
/*把整数页都写了*/
while (NumOfPage--) {
I2C_EE_PageWrite(pBuffer, WriteAddr, I2C_PageSize);
I2C_EE_WaitForWritingEnd();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle != 0) {
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitForWritingEnd();
}
}
}
}
6、从EEPROM读取数据
EEPROM 读取时序:
从 EEPROM 读取数据是一个复合的 I2C 时序,它实际上包含一个写过程和一个读过程。读时序的第一个通讯过程中,使用 I2C 发送设备地址寻址(写方向),接着发送要读取的“内存地址”;第二个通讯过程中,再次使用 I2C 发送设备地址寻址,但这个时候的数据方向是读方向;在这个过程之后, EEPROM 会向主机返回从“内存地址”开始的数据,一个字节一个字节地传输,只要主机的响应为“应答信号”,它就会一直传输下去,主机想结束传输时,就发送“非应答信号”,并以“停止信号”结束通讯,作为从机的 EEPROM也会停止传输。
/**
* @brief 从EEPROM 里面读取一块数据
* @param data:存放从 EEPROM 读取的数据的缓冲区指针
* @param addr:接收数据的 EEPROM 的地址
* @param numByteToRead:要从 EEPROM 读取的字节数
*/
void EEPROM_Read(uint8_t addr,uint8_t *data,uint8_t numByteToRead)
{
//产生起始信号
I2C_GenerateSTART(I2C1, ENABLE);
//检测EV5事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//发送设备地址
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
//检测EV6事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
//发送要操作的存储单元地址
I2C_SendData(I2C1, addr);
//检测EV8事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)==ERROR);
//第二次起始信号
I2C_GenerateSTART(I2C1, ENABLE);
//检测EV5事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
//发送设备地址 最后一位为读 方向为接收
I2C_Send7bitAddress(I2C1, 0xA1, I2C_Direction_Receiver);
//检测EV6事件
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)==ERROR);
while(numByteToRead)
{
if(numByteToRead==1)
{
//如果为最后一个字节
I2C_AcknowledgeConfig(I2C1, DISABLE);
}
//EV7事件被检测到,即数据寄存器有新的有效数据
while(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)==ERROR);
//接收数据
*data=I2C_ReceiveData(I2C1);
data++;
numByteToRead--;
}
//数据传输完成
I2C_GenerateSTOP(I2C1, ENABLE);
//使能应答,方便下次I2C传输
I2C_AcknowledgeConfig(I2C1, DISABLE);
}
7、状态等待函数
void I2C_EE_WaitForWritingEnd()
{
do
{
I2C_GenerateSTART(I2C1, ENABLE);
//检查EV5事件
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_SB)==RESET);
I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter);
}
//检查EV6事件
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_ADDR)==RESET);
I2C_AcknowledgeConfig(I2C1, DISABLE);
}