转载:关于STM32硬件I2C读写EEPROM代码实现原理的理解与总结

http://home.eeworld.com.cn/my/space-uid-716241-blogid-655190.html
一、I2C协议简介
I2C是两线式串行总线,用于连接微控制器及其外围设备。两根信号线分别是:
时钟信号线SCL和数据信号线SDA。
 
二、I2C总线传输时序
2.1 I2C传输协议的三种信号
I2C在数据传输过程中有三种信号类型,分别是:起始信号、结束信号和应答信号。
①起始信号:在时钟信号SCL为高电平时,数据线SDA由高电平跳变为低电平,开始传输数据;
②结束信号:在时钟信号SCL为高电平时,数据线SDA由低电平跳变为高电平,数据传输结束;
③应答信号:接收数据的IC在接收8位(一个字节)数据后,向发送数据的IC发出特定的低电平信号,表示已经收到数据。准确的说法是:发送设备在时钟信号SCL的8个脉冲的驱动下发送了8个bit,即一个字节后,在SCL第九个脉冲到来前将SDA线释放(即将其电平拉高),由接收设备在SCL第9个脉冲期间返回一个应答信号,即将SDA电平拉低。要注意的是:在SCL第9个脉冲高电平期间,SDA的低电平信号必须保持稳定。(实际上I2C协议中,数据传输时,SCL每个脉冲的高电平期间,SDA上的信号都必须保持稳定,只有在SCL为低电平期间,SDA上的数据才能变化。)
 
2.2 I2C协议规定
①什么时候总线为空闲状态?
SDA和SCL同时为高电平时,规定总线为空闲状态。即只要SDA和SCL中有一个为低电平,则总线为忙状态。
②关于应答信号说明
应答信号为低电平时,规定为有效应答,简称ACK,表示已接收到数据;应答信号为高电平时,规定为非应答,简称NACK,一般表示数据接收没有成功,有时根据相关操作时序,也可能需要产生一个NACK,这个后面会有说明。
2.3 传输协议数据包构成图示

三、以I2C读写EEPROM(AT24C02)为例进行说明

3.1 针对EEPROM的基本操作

①写操作

②读操作

针对EEPROM的写操作和读操作也可以细分,如:

写操作:

①往EEPROM中写入一个字节的数据;

②往EEPROM中批量写入数据;

读操作:

①从EEPROM中指定地址读取一个字节数据;

②从EEPROM中批量读取数据;

下面结合上面4种读写操作来具体说明:

1、往EEPROM中写入一个字节数据

1)写入时序

2)时序分析

①第一步:主机产生起始信号,开始传输;

②第二步:发送挂载在I2C总线下的从机设备地址(这里由于是针对EEPROM,其地址是7位),注意:EEPROM的地址高四位已经固定,为1010,低三位由自己的硬件设计决定,一般都将EEPROM的这三根地址线直接接地,所以低三位地址就为000,这样该EEPROM的设备地址就为1010 000。由于这里是写入数据,所以R/W位就为0,这一位与前面的7位地址组成一个字节,所以写入数据时,发送的从机地址就为0xA0;

③第三步:发送完从机地址后,主机会释放SDA线,等待从机给一个低电平应答信号ACK,主机收到ACK后,进行下一步;

④第四步:发送将要写入数据的地址,这里要搞清楚,这个地址不是上面的从机地址,而是我们要写入数据到EEPROM中具体地址;(两者的关系可以打个比方:我们假设I2C总线就是一条叫香樟大道的马路,EEPROM就是这条马路上某个地方的一栋大楼,地址是香樟大道99号,而写入数据的地址就是香樟大道99号这栋楼里的某个具体房间号。)

⑤发送完写入数据地址后,释放SDA总线并等待从机给一个低电平的应答信号ACK;

⑥主机收到应答信号ACK后,开始向指定的地址中写入一个字节数据,写完一个字节数据后释放SDA总线;

⑦从机接受到一个字节数据后,返回一个应答信号给主机,主机接受到应答信号后,发送一个结束信号来结束本次数据传输。

3)根据时序分析调用STM32库函数编写具体的程序
/*
 * 函数名:I2C_EEPROM_ByteWrite
 * 描述:向EEPROM中写入一个字节数据
 * 输入参数:pBuffer—指针变量,指向我们要发送的数据
 *           WriteAddr—要写入数据的地址
 * 说明:程序中EEPROM_ADDRESS是一个宏定义,是从机地址,为0xA0
 */
void I2C_EEPROM_ByteWrite(uint8_t *pBuffer, uint8_t WriteAddr)
{
 /*①调用库函数,产生起始信号 */
 I2C_GenerateSTART(I2C1, ENABLE);
 /*检测EV5事件(即SB=1,表示起始信号已发送)*/
 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
 
 /*②调用库函数发送从机地址*/
 I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
 /*检测EV6事件(即ADDR=1,表示从机设备地址已经发送,并且收到从机的应答)*/
 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
 
 /*③调用库函数发送要写入数据的地址*/
 I2C_SendData(I2C1, WriteAddr);
 /*检测EV8事件(即TxE=1,数据寄存器空,就是地址已经发送完成)*/
 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING));
 
 /*④调用库函数发送要写入的数据*/
 I2C_SendData(I2C1, *pBuffer);
 /*检测EV8_2事件(即TxE=1, BTF=1,请求设置停止位)*/
 while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
 
 /*⑤调用库函数发送停止信号结束传输 */
 I2C_GenerateSTOP(I2C1, ENABLE);
}
 
4) 关于3)中向EEPROM中写入一个字节数据函数的详细解析
看了上面的函数,可能有些人会有疑问,比如为什么你这函数要这么写?发送完起始信号为什么要检测EV5?后面的操作中又为什么要检测EV6、EV8或者EV8_2?等等问题。下面我结合《STM32中文参考手册_V10》中有关I2C的相关内容来进行说明。
首先,我们来看看《STM32中文参考手册_V10》P498中有关主发送器的相关时序说明:

图4中我们要看的是7位主发送的序列图,下面结合图4的主发送器传送序列图来解释上面写一个字节数据到EEPROM函数:

①主机产生起始信号,起始信号发送后,会产生EV5事件,EV5即表示SB=1,SB是I2C状态寄存器I2C_SR1的bit0,如下图5:

看上面的图5可以知道,如果起始条件已发送,SB会被置“1”,即产生事件EV5,所以我们要检测该位确认起始条件已发送;

②发送从机设备地址,从机应答后,会产生EV6事件,即ADDR=1,ADDR是I2C_SR1寄存器的bit1,说明如下图6:

通过图6我们可以知道,当地址发送完成,主机收到从机的应答后,ADDR位被置“1”,我们通过检测EV6事件来判断地址已发送完成并清除相关位;

③发送要写入数据的地址(地址也是数据),从机收到数据后应答,通过图4传送序列图可以看出,在第一个数据(第一个数据是要写入数据的地址)发送完并收到应答后,EV8事件早已经产生,EV8表示TxE=1,移位寄存器非空,数据寄存器空,写入DR寄存器将清除该事件。数据寄存器空,移位寄存器非空说明我们要发送的数据已经由数据寄存器转入移位寄存器,数据一旦转入移位寄存器就会在时钟信号的驱动下自动一位一位的将数据发送出去,所以我们在发送完地址后检测EV6事件,确认数据已经由数据寄存器转移到移位寄存器中发送,可以向数据寄存器中写入新的数据;

④发送我们要写入到EEPROM中的数据,从机应答后,检测EV8_2,这里为什么是检测EV8_2而不是EV8?因为我们这个函数只向EEPROM中写入一个字节数据,所以这个字节数据就是最后一个要发送的数据,通过图4传送序列图可以看出,最后一个数据传输完成后产生事件EV8_2,表示TxE=1,BTF=1,请求设置停止位。至于为什么产生的是EV8_2而不是EV8,我们来看看图7中有关BTF的说明:BTF表示字节发送结束,在发送时,当一个新数据被发送且数据寄存器还未被写入新的数据(TxE=1)时,BTF会被置“1”,由于我们这个函数只向从机发送一个字节数据(不包括地址),所以在这个字节数据被发送后并没有新的数据写入到数据寄存器,因此符合了上面的条件,BTF被置“1”。在TxE=1、BTF=1的情况下,就产生EV8_2;

⑤检测到EV8_2后,主机就可以产生停止信号来结束本次传输了。

有关写入一个字节数据到EEPROM中函数的分析到这里就结束了,下面来说一些从EEPROM中读取一个字节数据的相关代码以及分析。

2、从EEPROM中指定地址读取一个字节数据
1)读取时序

2)时序分析

①第一步:主机产生起始信号;

②第二步:主机发送从机设备地址,写选通(即R/W为‘0’),从机接收到地址后应答;

③第三步:主机发送要读取数据在EEPROM中的地址,从机接收到后应答;

④第四步:主机再次产生起始信号;

⑤第五步:主机发送从机设备地址,读选通(即R/W为‘1’),从机接收到后应答;

⑥第六步:从机发送数据,主机接收,接收到最后一个数据后非应答;

⑦第七步:主机产生停止信号结束传输。

3)根据时序分析调用STM32库函数编写具体的程序
/*
 * 函数名:I2C_EEPROM_ByteRead
 * 描述:从I2C EEPROM中读取一个字节数据
 * 输入:pBuffer—指针变量,*pBuffer用来存放读取到的数据
 *       ReadAddr:读取数据的地址
 * 输出:无
*/
void I2C_EEPROM_ByteRead(uint8_t *pBuffer, uint8_t ReadAddr)
{
        /*等待EEPROM准备好*/
I2C_EE_WaitEepromStandbyState();
/*①产生起始信号*/
I2C_GenerateSTART(I2C1, ENABLE);
/*检测EV5并清除标志*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
/*②发送从机地址,写选通*/
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
/*检测EV6并清除标志*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
/*③发送要读取数据的地址*/
I2C_SendData(I2C1, ReadAddr);
/*检测EV8并清除标志*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
/*④重新发送起始信号 */
I2C_GenerateSTART(I2C1, ENABLE);
/*检测EV5并清除标志*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
/*⑤发送从机设备地址,读选通*/
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);
/*检测EV6并清除标志*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
/*⑥检测EV7,然后读取数据清除标志*/
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); 
*pBuffer = I2C_ReceiveData(I2C1);
/*⑦非应答*/
I2C_AcknowledgeConfig(I2C1, DISABLE);
        /*⑧产生停止信号*/
I2C_GenerateSTOP(I2C1, ENABLE);
}
 
4)从EEPROM中读取一个字节数据的函数分析
与之前的写一个字节函数一样,会发现这里除了我们根据读取时序列出的步骤之外,还多了很多事件EVx的检测,而且开头还有一个等待EEPROM准备好的函数。下面同样根据EEPROM的读取时序结合《STM32中文参考手册_V10》中有关I2C相关章节的说明来解释一下,首先我们来看看STM32的I2C主接收器传送序列图(这里看7位主接收):

问题说明:

问题1:为什么要加一句等待EEPROM准备好的函数:

EEPROM的写入是需要时间的,只有当前面的写入操作完成,它才能继续响应后面的读取操作,否则在它还没有准备好的情况下,进行读取肯定是不成功的。关于等待EEPROM函数准备好的函数,它的实现原理就是通过向EEPROM发送从机地址,看从机是否成功应答,如果应答了就表示EEPROM已经准备好,如果应答失败就说明EEPROM还没准备好,然后主机继续发送从机地址来呼叫EEPROM等待应答,直到从机应答成功就退出该函数执行下一步,其代码如下(这段代码是我在野火的工程代码上修改了一点点,大家可以直接去看野火的官方例程,一定会有所收获的):

void I2C_EE_WaitEepromStandbyState(void)      
{
      vu16 SR1_Tmp = 0;
      do
     {
           /* Send START condition */
          I2C_GenerateSTART(I2C1, ENABLE);
          /* Read I2C1 SR1 register */
          SR1_Tmp = I2C_ReadRegister(I2C1, I2C_Register_SR1);
          /* Send EEPROM address for write */
           I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
      }while(!(I2C_ReadRegister(I2C1, I2C_Register_SR1) & 0x0002));
  
      /* Clear AF flag */
      I2C_ClearFlag(I2C1, I2C_FLAG_AF);
}
问题2:
 
可能到这里有人会有疑问,一个是STM32的I2C的主接收序列,一个是EEPROM的读时序,那写代码时应该依照哪一个啊?我的理解是,我们要两者结合,按照EEPROM的读时序来搞清楚读一个数据需要那几步,按照STM32的I2C主发送和主接收序列来搞清楚每一步需要检测什么事件(本质就是检测哪些寄存器的哪些位来判断相应的步骤已经顺利完成)。
 
下面结合主接收和主发送的序列图以及EEPROM的读时序图来分析代码为什么这样写:
①等待从机准备好函数,这个上面已经解释过了,这里不再赘述;
②根据图8 EEPROM读取一个字节时序图可知,首先我们要产生一个起始信号S,然后根据图4主发送序列图可知,在成功发送起始条件后,会产生事件EV5,因此我们通过检测事件EV5来判断起始条件是否发送成功并清除寄存器;
③根据图8 EEPROM读取一个字节时序图可知,发送起始信号后,我们接下来要发送从机的设备地址(写选通,即R/W为‘0’),根据图4主发送序列图可知,如果成功发送从机设备地址并收到应答信号后,会产生时间EV6,因此我们通过检测EV6来判断从机设备地址的发送情况; 
④根据图8 EEPROM读取一个字节时序图可知,发送完从机设备地址后,我们接下来要发送读取数据的地址,根据图4主发送序列图可知(这里由于依然是主机在向从机发送数据,所以要参考图4主发送序列图),如果成功发送地址后,可以通过检测事件EV8来判断;
⑤根据图8 EEPROM读取一个字节时序图可知,接下来要重新发送起始信号(从这一步开始就要参考图9主接收序列图),同样检测事件EV5;
⑥根据图8 EEPROM读取一个字节时序图可知,接下来要发送从机设备地址,读选通(即R/W为‘1’),根据图9主接收序列图可知,如果读取地址发送成功,会产生事件EV6,因此我们通过检测EV6来判断和清除标志;
⑦根据图9主接收序列图可知,接下来就是从机发送数据,主机接收数据,在主机接收一个数据后,会产生事件EV7,我们通过判断EV7来查看数据接收是否完成,如果接收完成,就将数据寄存器中的数据读取出来;
⑧根据图9主接收序列图可知,读取一个数据后,主机要产生一个非应答信号,然后产生一个停止信号来结束本次传输(因为我们只读取一个字节数据,所以读取的第一个数据也是最后一个数据,最后一个数据接收完成后产生非应答信号)。
 
到这里,有关STM32的I2C写入和读取EEPROM操作就结束了,以上的内容都是我在学习相关知识内容时的一点总结和理解,希望能给需要的人带来一些帮助,如果有什么理解不对说明错误的地方,还请大家不吝批评指正,我会感激不尽,谢谢!
posted @ 2019-11-25 22:26  星空下聆听  阅读(2141)  评论(0编辑  收藏  举报