I2C协议简介:
I2C(inter-integrated Circuit)协议是由Phiilps公司开发的,因为它的引脚少,硬件实现简单,可扩展性强(不像USART,CAN的外部收发设备),如今被广泛的使用在系统内多个集成电路(IC)之间的通信.
(根据I2C总线协议,我们可以把I2C分为物理层和协议层)
物理层:
1,使用两条总线线路,一条双向串行数据线(SDA),一条串行时钟线(SCL).
2,每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问.
3,多主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线.
4,具有三种传输模式:
标准模式传输速率为100Kbit/s
快速模式传输速率为400Kbit/s
高速模式传输速率为3.4Mbit/s(但是目前大多的I2C设备都不支持高速模式)
5,片上的滤波器可以滤去总线数据线上的毛刺波保证数据完整.
6,连接到相同总线的IC数量受到总线的最大电容400pF限制.
协议层:
I2C的协议包括起始和停止条件,数据有效性,响应,仲裁,时钟同步和地址广播等.(STM32有集成的硬件I2C接口,所以不需要用软件去模拟SDA和SCL时序).
主机写数据到从机
主机由从机读数据
(这两幅图表示的是主机和从机通讯时,SDA线的数据包序列)
S:传输开始信号
SLAVE ADDRESS:从机的地址
R/~W:传输方向选择 1为读,0为写
A/~A:应答或者非应答信号
DATA(带阴影的):数据由主机传输到从机
DATA(空白的):数据由从机传输到主机
P:停止传输信号
起始信号S产生后,所有从机就开始等待主机接下来广播的从机地址信号SLAVE ADDRESS,在I2C总线上,每个设备的地址都是唯一的.当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备会直接忽略之后的数据信号.(根据I2C总线协议,这个从机地址可是是7位或10位).
地址位的后面,是传输方向的选择(读写R/~W),从机接收到匹配的地址后,主机或从机会返回一个应答(A)或者非应答(~A)信号,只有接收到应答信号后,主机才能继续发送或接收数据.(根据I2C总线协议,之后的每一个操作都需要返回一个应答信号)
如果配置方向传输位是写数据:
广播完地址,接收到应答信号后,主机开始向从机传输数据(DATA),数据包的大小为8位,主机每发送一个数据,都要等待从机的应答信号(A)(重复这个过程可以向从机传输N个数据,这个N没有大小限制),当数据传输结束时,主机向从机发送一个停止传输信号(P),表示传输数据停止.
如果配置方向传输位是读数据:
广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包的大小为8位,从机每发送一个数据,都会等待主机的应答信号(A)(重复这个过程可以返回N个数据,这个N没有大小限制),当主机希望停止接收数据时,就向从机返回一个非应答信号(~A),从机自动停止数据传输.
STM32的I2C特性和架构:
I2C接口特性:
1,STM32的中等容量和大容量型号的芯片均有2个的I2C总线接口.
2,能够工作于多主模式或从模式,分别为主接收器,主发送器,从接收器,从发送器.
3,支持标准模式100Kbit/s和快速模式400Kbit/s,不支持高速模式.
4,支持7位或10位寻址.
5,内置了硬件CRC发生器/校验器.
6,I2C的接收和发送都可以使用DMA操作.
7,支持系统管理总线(SMBus)2.0版
I2C架构:
可以看到,I2C所有的硬件架构都是根据SCL和SDA展开的(其中的SMBALERT用于SMBUS).
SCL线的时序就是I2C协议中的时钟信号,他由I2C接口根据时钟控制寄存器(CCR)控制,控制的参数主要是时钟频率.
SDA线的信号是通过一系列数据控制架构,在将要发送的数据的基础上,根据协议添加各种起始信号,应答信号,地址信号,实现以I2C协议的方式发送出去.读取数据则从SDA线上的信号中取出接收到的数据值.(发送和接收的数据都是被保存在数据寄存器(DR)上的).
I2C实例(EEPROM):
对EEPROM读写最常用的方式是使用I2C协议.STM32F103VET6它有2个I2C接口,本实验使用I2C1,对应地接到EEPROM(AT24CO2)的SCL和SDA线,实现I2C通讯,对EEPROM进行读写.
本例子是采用主模式,分别用作主发送器和主接收器.通过查询事件的方式来确保正常通讯.
main函数:
int main(void)
{
USART1_Config();//串口初始化
I2C_EE_Init();//I2C的EEPROM的初始化
USART1_printf(USART1, "\r\n ("__DATE__ "-"__TIME__ ") \r\n");
I2C_Test();
while (1)
{
}
}
初始化完成后由串口向终端输出调试信息,接着是调用用户函数I2C_Test(),把配置好的I2C接口向EEPROM写入数据,再把这些数据读取出来进行校验,检查我们I2C是否正常工作.
I2C接口的初始化:
void I2C_EE_Init(void)
{
I2C_GPIO_Config();
I2C_Mode_Config();
/* 根据头文件 i2c_ee.h 中的定义来选择 EEPROM 要写入的地址 */
#ifdef EEPROM_Block0_ADDRESS
/* 选择 EEPROM Block0 来写入 */
EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
#endif
#ifdef EEPROM_Block1_ADDRESS
/* 选择 EEPROM Block1 来写入 */
EEPROM_ADDRESS = EEPROM_Block1_ADDRESS;
#endif
#ifdef EEPROM_Block2_ADDRESS
/* 选择 EEPROM Block2 来写入 */
EEPROM_ADDRESS = EEPROM_Block2_ADDRESS;
#endif
#ifdef EEPROM_Block3_ADDRESS
/* 选择 EEPROM Block3 来写入 */
EEPROM_ADDRESS = EEPROM_Block3_ADDRESS;
#endif
}
这里是先调用了I2C_GPIO_Config()配置好I2C所用的I/O端口,然后再调用 I2C_Mode_Config()配置I2C的工作模式,并且使能相关外设的时钟.
EEPROM的地址:
根据I2C协议,每一个连接到I2C总线上的设备都需要唯一的地址,EEPROM当然也有(找到EEPROM的数据手册,可以找到每一个块的地址)
这里使用的EEPROM的型号是AT24C02,容量是2K bits,图中EEPROM的高四位是做了硬性规定,最低位为R/W(读写位),能改变的位只有A2,A1,A0(在硬件设计中可以根据需求改变,这里都为接地,也就是0).所以根据读写位的不同,EEPROM的地址为0xA0或者0xA1.
GPIO端口的初始化:
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能与 I2C1 有关的时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
/* PB6-I2C1_SCL、PB7-I2C1_SDA*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
在这个函数里面,使能了GPIO时钟,I2C1的时钟,并且对I2C1复用的两个引脚进行了初始化设置,模式设置为开漏输出(不记得工作方式的,可以看之前的博客,或者直接查询《STM32数据手册》的引脚定义部分和《STM32参考手册》的GPIO章节)
I2C模式初始化:
static void I2C_Mode_Configu(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 =I2C1_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
/* I2C1 初始化 */
I2C_Init(I2C1, &I2C_InitStructure);
/* 使能 I2C1 */
I2C_Cmd(I2C1, ENABLE);
}
和所有外设初始化一样,I2C模式的初始化还是对他的初始化结构体进行参数配置。
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
选择I2C的使用方式,这里面有I2C模式(I2C_Mode_I2C)和SMBus模式(I2C_Mode_SMBusDevice或者I2C_Mode_SMBusHost),这里选择了I2C模式
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
这里是配置I2C的SCL线时钟的占空比.在STM32的I2C占空比配置中有两个选择:(这里使用了2:1)
高电平和低电平时间比 16:9(I2C_DutyCycle_16_9)
高电平和低电平时间比 2:1 ( I2C_DutyCycle_2)
(SCL线的时钟信号的高电平时间与低电平时间是没必要相同的,由于SDA线是在SCL线维持在高电平时读取或者写入数据的,而在SCL的低电平期间SDA的数据发生了变化,所以高电平时间较长就不容易出现数据错误.根据I2C协议,在快速模式和高速模式下SCL的高低电平时间可以不同)
I2C_InitStructure.I2C_OwnAddress1 =I2C1_OWN_ADDRESS7;
配置STM32的I2C设备自己的地址,根据I2C协议,每一个连接到I2C总线上的设备都需要唯一的地址(主机也一样)这里设置为自定义宏I2C1_OWN_ADDRESS7(0x0A)在STM32数据手册I2C章节可以找到.
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
这里是配置为允许应答(I2C_Ack_Enable),这是绝大多数遵循I2C标准设备通讯的要求(如果改为禁止应答(I2C_Ack_Disable)往往会导致通讯错误).
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
这里是选择I2C的寻址模式是7位还是10位.根据实际连接到I2C总线上设备的地址进行选择(这里是和EEPROM进行通讯,所以使用7位寻址模式(I2C_AcknowledgedAddress_7bit;)).
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
这里是设置I2C的传输速率,在调用初始化函数时,函数会根据我们输入的数字经过运算后把分频值写入到I2C的时钟控制寄存器(我们写入的这个值不能高于400KHz),这里自定义宏I2C_Speed(400000)
(实际上由于I2C使用的APB1时钟为36MHz,不是10MHz的整数倍,因此最终分频后输出的SCL线时钟并不是精确的400KHz)
对结构体赋值完毕后,就用I2C_Init()进行初始化,并调用I2C_Cmd()使能I2C外设.
EEPROM的读写:
void I2C_Test(void)
{
u16 i;
printf(“写入的数据\n\r”);
for ( i=0; i<=255; i++ ) //填充缓冲
{
I2c_Buf_Write[i] = i;
printf(“0x%02X “, I2c_Buf_Write[i]);
if(i%16 == 15)
printf(“\n\r”);
}
//将 I2c_Buf_Write 中顺序递增的数据写入 EERPOM 中
I2C_EE_BufferWrite( I2c_Buf_Write, EEP_Firstpage, 256);
printf(“\n\r 读出的数据\n\r”);
//将 EEPROM 读出数据顺序保持到 I2c_Buf_Read 中
I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256);
//将 I2c_Buf_Read 中的数据通过串口打印
for (i=0; i<256; i++)
{
if(I2c_Buf_Read[i] != I2c_Buf_Write[i])
{
printf(“0x%02X “, I2c_Buf_Read[i]);
printf(“错误:I2C EEPROM 写入与读出的数据不一致\n\r”);
return;
}
printf(“0x%02X “, I2c_Buf_Read[i]);
if(i%16 == 15)
printf(“\n\r”);
}
printf(“I2C(AT24C02)读写测试成功\n\r”);
}
这个函数实现的功能是把数值0~255按顺序填入缓冲区数组,并通过串口打印到终端,接着通过函数I2C_EE_BufferWrite()把缓冲区的数据写入EEPROM,写入成功后,再用I2C_EE_BufferRead把数据读取出来,进行校验,判断数据是否正确写入.
I2C_EE_BufferWrite()以及I2C_EE_BuffRead()具有三个输入参数,缓冲区指针(I2c_Buf_Write)和(I2c_Buf_Read),写入到或者读出EEPROM的存储地址(EEP_Firstpage),以及字节数.
EEPROM的数据组织形式:
EEPROM设备把它的存储矩阵进行了分页处理:
型号是AT24C02的EEPROM分为32页,每一页可以存储8个字节的数据,若在同一页写入超过8字节,则超过的部分会被写在该页的起始地址(也就是一开始写好的部分会被覆盖).
为了把连续的缓冲区数组按页写入到 EEPROM ,就需要对缓冲区进行分页处理.I2C_EE_BufferWrite()是根据输入的缓冲区大小参数 NumByteToWrite,计算出需要写入多少页,计算写入位置。
分页处理好之后,调用 I2C_EE_PageWrite(),这个函数是与 EEPROM进行I2C通讯的最底层函数(里面都是调用STM32库函数)
EEPROM写入 I2C_EE_BufferWrite();
u8 I2c_Buf_Write[256];
//#define EEP_Firstpage 0x00(加个//免得字体放大)
void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr, u16 NumByteToWrite)
{
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
/*计算出要写的页数和分页*/
Addr = WriteAddr % I2C_PageSize;
count = I2C_PageSize - Addr;
NumOfPage = NumByteToWrite / I2C_PageSize;
if(Addr == 0)
{
if(NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
I2C_EE_WaitEepromStandbyState(); //检测是否为Standby状态,才可以进行下一步操作
}
else
{
while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer,WriteAddr,I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
if(NumOfSingle!=0)
{
I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
else
{
if(NumOfPage == 0)
{
I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
else
{
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
if(count != 0)
{
I2C_EE_PageWrite(pBuffer,WriteAddr,count)
I2C_EE_WaitEepromStandbyState();
WriteAddr += count;
pBuffer += count;
}
while(NumOfPage--)
{
I2C_EE_PageWrite(pBuffer,WriteAddr,I2C_PageSize);
I2C_EE_WaitEepromStandbyState();
WriteAddr += I2C_PageSize;
pBuffer += I2C_PageSize;
}
if(NumOfSingle != 0)
{
I2C_EE_PageWrite(pBuffer,WriteAddr,NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
}
}
}
这里每次调用完 I2C_EE_PageWrite()后,都调用了一个I2C_EE_WaitEepromStandbyState();
void I2C_EE_WaitEepromStandbyState(void)
{
vu16 SR1_Tmp = 0;
do
{
I2C_GenerateSTART(I2C1, ENABLE);
SR1_Tmp = I2C_ReadRegister(I2C1, I2C_Register_SR1);
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
}while(!(I2C_ReadRegister(I2C1, I2C_Register_SR1) & 0x0002));
I2C_ClearFlag(I2C1, I2C_FLAG_AF);
I2C_GenerateSTOP(I2C1, ENABLE);
}
这里是利用了 EEPROM在接收完数据后,启动了周期写入数据的时间内不会对主机的请求做出应答的特性,利用这个库函数循环发送起始信号,若检测到 EEPROM的应答,则说明 EEPROM已经完成上一步的数据写入,进入了Standby状态,可以进行下一步操作。
EEPROM进行I2C通讯的最底层函数I2C_EE_PageWrite()
void I2C_EE_PageWrite(u8* pBuffer, u8 WriteAddr, u8 NumByteToWrite)
{
/确保SDA总线空闲时再做I2C通讯/
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_SendData(I2C1, WriteAddr);
while(! I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
while(NumByteToWrite--)
{
I2C_SendData(I2C1, *pBuffer);
pBuffer++;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMIT)TED));
}
I2C_GenerateSTOP(I2C1, ENABLE);
}
这个 EEPROM 的页写入就是根据 EEPROM的页写入时序来写的:
在 I2C_EE_PageWrite()函数中,先不管while语句的循环检测,就可以很清晰的看到整个代码流程就是 EEPROM 的页写入时序流程
I2C_GenerateSTART(I2C1, ENABLE);
产生I2C的通讯起始信号S。
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
把前面条件变异中赋值的变量 EEPROM_ADDRESS 地址通过I2C1接口发送出去,数据传输方向为ST32的 I2C发送数据(I2C_Direction_Transmitter).
(这里的 EEPROM_ADDRESS 地址是 EEPROM作为挂载在I2C总线上设备的寻址,并不是 EEPROM 内存存储矩阵的地址)
I2C_SendData(I2C1, WriteAddr);
这里是把数据传输到数据寄存器,再由I2C模块根据I2C协议发送出去,但是要注意这里的输入参数是 WriteAddr,根据 EEPROM 的页写入时序,发送完I2C的地址后的第一个数据,并不一定要写入 EEPROM的数据.EEPROM对这个数据解释为将要对存储矩阵写入的地址,WriteAddr是在 I2C_EE_PageWrite()函数时作为参数输入的,在这个实例里是由 I2C_EE_BufferWrite()计算出来的.
I2C_SendData(I2C1, *pBuffer);
这里是向 EEPROM发送要写如的数据,根据 EEPROM 的页写入时序,这些数据被写入到前面发送的页地址中,如果连续写入超过一页的最大字节数(这里是8个),则多出来的数据会重新从该页的起始地址连续写入,覆盖前面的数据.
I2C_GenerateSTOP(I2C1, ENABLE);
产生I2C的传输结束信号,完成一次I2C通讯.
I2C事件检测:
在 I2C_EE_PageWrite里面还有很多的事件检测,这些都是必须的,根据STM32参考手册的序列图可以看到,在I2C的通讯过程中,会产生一系列的事件,出现时间后相应的寄存器中会产生标志位.
这个图的意思为:
1,发出了起始信号,会产生事件 5(EV5),即STM32的I2C成为主机模式;
2,发送完成I2C设备寻址并得到应答后,会产生 EV6,即STM32的I2C成为数据发送端;
3,发送数据完成会产生EV8;
所以在做出I2C通讯操作时,可以通过循环调用库函数 I2C_CheckEvent()进行事件查询,确保上一操作完成后才进行下一操作.
例如:在确定SDA总线空闲之后,作为主发送器的STM32发出起始信号,成功后会产生EV5,所以使用语句:
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
来检测这个事件,确保检测到之后再执行下一操作,I2C_EVENT_MASTER_MODE_SELECT则是事件的类型,在keil环境下可以查找到所有相关事件
EEPROM读取函数 I2C_EE_BufferRead();
void I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)
{
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
I2C_GenerateSTART(I2C1, ENABLE);//第一次发送起始信号
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
I2C_Cmd(I2C1, ENABLE);
I2C_SendData(I2C1, ReadAddr);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
I2C_GenerateSTART(I2C1, ENABLE);//这里是第二次发送起始信号了
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
while(NumByteToRead)
{
if(NumByteToRead == 1)
{
/* Disable Acknowledgement */
I2C_AcknowledgeConfig(I2C1, DISABLE);
/* Send STOP Condition */
I2C_GenerateSTOP(I2C1, ENABLE);
}
/* Test on EV7 and clear it */
if(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
{
/* Read a byte from the EEPROM */
*pBuffer = I2C_ReceiveData(I2C1);
/* Point to the next location where the byte read will be
pBuffer++;
/* Decrement the read bytes counter */
NumByteToRead--;
}
}
}
这里也是利用 I2C_CheckEvent()来确保通讯正常进行的,要注意的是读取数据根据I2C标准协议,主发送器STM32要发出两次起始信号I2C信号才能建立通讯.
PS:
总结一下I2C读写流程:
配置I/0端口,确定并配置I2C的模式,使能GPIO和I2C时钟.(首先必须做的)
写:
1,检测SDA是否空闲;
2,按I2C协议发出起始信号;
3,发出7位期间地址和写模式;
4,要写如的存储区的首地址;
5,用页写入方式或者字节写入方式写入数据;
6,发送I2C通讯结束信号;
(每个操作之后要检测事件确定是否成功,写完检测 EEPROM是否进入了Standby状态)
读:
1,检测SDA是否空闲;
2,按I2C协议发出起始信号;
3,发出7位期间地址和写模式(伪写);
4,发出要读取的存储首地址;
5,重发起始信号(记住);
6,发出7为器件地址和读模式;
7,接收数据;
(每个操作之后要检测事件确定是否成功)