天天看点

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

一、I2C协议简介

1、I2C物理层

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

1、在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。

2、一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。

3、每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。

4、总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。 

2、I2C协议层 

1、I2C读写过程 

主机写数据到从机: 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

        若配置的方向传输位为“写数据”方向,广播完地址,接收到应答信号后, 主机开始正式向从机传输数据,数据包的大小为 8 位,主机每发送完一个字节数据,都要等待从机的应答信号(ACK),重复这个过程,可以向从机传输 N 个数据。当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。 

主机由从机中读数据: 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

        若配置的方向传输位为“读数据”方向, 广播完地址,接收到应答信号后, 从机开始向主机返回数据,数据包大小也为 8 位,从机每发送完一个数据,都会等待主机的应答信号(ACK),重复这个过程,可以返回 N 个数据,这个 N 也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(NACK),则从机自动停止数据传输。

复合通讯: 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

       该传输过程有两次起始信号(S)。一般在第一次传输中,主机通过 SLAVE_ADDRESS 寻找到从设备后,发送一段“数据”,这段数据通常用于表示从设备内部的寄存器或存储器地址(注意区分它与 SLAVE_ADDRESS 的区别);在第二次的传输中,对该地址的内容进行读或写。也就是说,第一次通讯是告诉从机读写地址,第二次则是读写的实际内容。

 2、通讯的起始和停止信号 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

        当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。 

3、地址及数据方向

        I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向数据方向位,第 8 位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。读数据方向时,主机会释放对 SDA 信号线的控制,由从机控制 SDA 信号线,主机接收信号,写数据方向时, SDA 由主机控制,从机接收信号。 

4、响应 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

        I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接

收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。 

二、 I2C架构

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

引脚:

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

 时钟控制逻辑:

 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信号线输出时钟计算: 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

通讯过程 :

1、主发送器 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

2、主接收器 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

三、I2C初始化结构体 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

四、读写EEPROM 

        EEPROM芯片的设备地址一共有 7 位,其中高 4 位固定为:1010 b,低 3 位则由 A0/A1/A2 信号线的电平决定,R/W 是读写方向位,与地址无关。 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

        按照我们此处的连接, 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 单字节写入时序:

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写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 页写入时序: 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写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、利用页写入的方式快速写入多字节 

首地址对齐到页时的情况: 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 

首地址未对齐到页时的情况

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写EEPROM 
#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 读取时序: 

STM32学习笔记9(I2C)一、I2C协议简介二、 I2C架构三、I2C初始化结构体 四、读写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);
}