STM32
直播中

石胜厚

7年用户 1065经验值
私信 关注
[问答]

浅析I2C物理层和协议层

I2C物理层的特点有哪些?
I2C通讯设备之间的常用连接方式是什么?
I2C协议层的基本读写过程是怎样的?

回帖(1)

李波

2021-9-29 14:58:08
  一 、I2C物理层
  I2C 通讯设备之间的常用连接方式见图:
  
  有以下特点:(参考数据手册:上拉电阻一般4.7k~10k ,一般4.7k)
  (1)由两条总线控制:一条双向串行数据线(SDA) ,一条串行时钟线 (SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
  (2)I2C总线上可挂在多个 I2C通讯的设备,如图所示。
  (3)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
  (4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
  (5)多个主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
  (6)具有三种传输模式:标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
  二、协议层
  1、I2C基本读写过程:阴影部分代表数据由主机传输至从机,无阴影部分相反
  (1)主机写数据到从机
  
  (2)主机由从机中读数据
  
  (3)I2C 通讯复合格式
  
  简单理解,当配置I2C完成后,发出开始信号(S)。主机开始广播某特定地址的从机(SLAVE ADDERSS),并给出要做出的操作(R/W),当从机收到主机的广播地址后,会通过内部地址比较器与自身地址比较,如果主机不是呼叫自己,那么自身保持“高阻”不接受应答。若从机发现主机呼叫的是自己,那么将SDA和SCL总线拉低,表示占用总线,接受响应并产生应答信号。执行读写操作。
  若执行的是写操作,参考图(1)当从机发出应答信号给主机,主机得到了应答后,开始给该从机发送数据,从机获取数据,应答主机(告诉主机 :自己收到了数据)。如此循环主机一直发送数据给从机,直到从机发出“非应答信号”(数据够了,我不要了,停止吧)主机发出停止信号,表示停止发送。
  若执行的是读操作,参考图(2)当从机发出应答信号给主机,并发送主机需要的数据DATA,当主机接受到来自从机的数据DATA后,会应答从机(表示:好的,你的数据我已接受,你继续)。如此循环,一直接受数据。知道主机发出“非应答信号”(表示:感谢,我的数据已经够了,你停把)。然后给从机发出停止信号。
  对于复合操作,参考(1)(2)。
  2、通讯的起始和停止位
  起始(S)和停止§信号是两种特殊的状态。当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA 线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
  
  3、数据有效性
  I2C 使用 SDA 信号线来传输数据,使用 SCL 信号线进行数据同步。SDA数据线在 SCL 的每个时钟周期传输一位数据。传输时,SCL 为高电平的时候 SDA 表示的数据有效,即此时的 SDA 为高电平时表示数据“1”,为低电平时表示数据“0”。当 SCL为低电平时,SDA 的数据无效,一般在这个时候 SDA 进行电平切换,为下一次表示数据做好准备。
  
  4、地址及数据方向
  I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。I2C 协议规定设备地址可以是 7 位或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/W),第 8 位或第 11 位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。(下图以七位地址为例)
  
  5、响应
  I2C 的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
  
  传输时主机产生时钟,在第 9 个时钟时,数据发送端会释放 SDA 的控制权,由数据接
  收端控制 SDA,若 SDA 为高电平,表示非应答信号(NACK),低电平表示应答信号(ACK)。
  三、 通讯过程
  对于实现I2C通讯,该部分十分重要。编程过程基本都是按照下图进行操作
  1、主发送器
  
  流程:
  (1)控制产生起始信号(S),它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
  (2)发送设备地址,若有从机应答,则产生事件“EV6”及“EV8”,这时 SR1 寄存器的“ADDR”位及“TXE”位被置 1,ADDR 为 1 表示地址已经发送,TXE 为 1 表示数据寄存器为空;
  (3)以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数据,这时 TXE 位会被重置 0,表示数据寄存器非空,I2C 外设通过SDA 信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,重复这个过程,可以发送多个字节数据;
  (4)当我们发送数据完成后,控制 I2C 设备产生一个停止信号(P),这个时候会产生EV8_2 事件,SR1 的 TXE 位及 BTF 位都被置 1,表示通讯结束。
  假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C 中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
  2、主接收器
  
  流程:
  (1) 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已经发送;
  (2) 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1 寄存器的“ADDR”位被置 1,表示地址已经发送。
  (3) 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1 寄存器的 RXNE 被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制 I2C 发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;
  (4) 发送非应答信号后,产生停止信号(P),结束传输。看到这里的小伙伴,恭喜你,你的耐力不错,竟然看完了这么多的理论知识,你可能也会感慨,这I2C也忒难搞了吧。一会发送这个东西,一会又检测另一个事件。要搞完一次 I2C通讯,岂不是要肝到天荒地老,哈哈哈哈。不要担心,其实很多工作,完全不需要我们做,官方已经为我们写好了大量的库,我们只需要进行“CV编程开发”即可,再加上一点小小的理解。就可以很轻松的实现I2C通讯了。
  按着步骤走很简单的:
  I2C_InitTypeDef 结构体:
  typedef struct
  {
  uint32_t I2C_ClockSpeed;
  uint16_t I2C_Mode;
  uint16_t I2C_DutyCycle;
  uint16_t I2C_OwnAddress1;
  uint16_t I2C_Ack;
  uint16_t I2C_AcknowledgedAddress;
  }I2C_InitTypeDef;
  把每个结构体成员都配置一下就好,具体怎么配置,.h文件里都有对应的宏,Ctrl+CV即可
  OKK说完这些,程序猿的事情,当然要代码交流了!
  (1)void I2C_GPIO_Config(void)
  void I2C_GPIO_Config(void)
  {
  GPIO_InitTypeDef GPIO_InitStruct;
  //初始化 IIC_GPIO 时钟
  RCC_APB2PeriphClockCmd(I2Cx_GPIO_SCL_Clk|I2Cx_GPIO_SDA_Clk,ENABLE);
  //初始化IIC_SCL
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
  GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SCL_Pin;
  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(I2Cx_GPIO_SCL_Port,&GPIO_InitStruct);
  //初始化IIC_SDA
  GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SDA_Pin;
  GPIO_Init(I2Cx_GPIO_SDA_Port,&GPIO_InitStruct);
  }
  (2)void I2C_GPIO_Config(void)
  void I2C_GPIO_Config(void)
  {
  GPIO_InitTypeDef GPIO_InitStruct;
  //初始化 IIC_GPIO 时钟
  RCC_APB2PeriphClockCmd(I2Cx_GPIO_SCL_Clk|I2Cx_GPIO_SDA_Clk,ENABLE);
  //初始化IIC_SCL
  GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
  GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SCL_Pin;
  GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(I2Cx_GPIO_SCL_Port,&GPIO_InitStruct);
  //初始化IIC_SDA
  GPIO_InitStruct.GPIO_Pin = I2Cx_GPIO_SDA_Pin;
  GPIO_Init(I2Cx_GPIO_SDA_Port,&GPIO_InitStruct);
  }
  (3)void I2C_Config(void)
  void I2C_Config(void)
  {
  I2C_InitTypeDef I2C_InitStruct;
  I2C_GPIO_Config();//配置SDA和SCL GPIO
  //初始化IIC外设时钟
  RCC_APB1PeriphClockCmd(DEBUG_I2Cx_Clk,ENABLE);
  //初始化IIC 结构体
  I2C_InitStruct.I2C_Ack = I2C_Ack_Enable; //使能应答
  I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //IIC 7 位寻址
  I2C_InitStruct.I2C_ClockSpeed = DEBUG_I2C_ClockSpeed;//通信速率 400k
  I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; //占空比 1/2
  I2C_InitStruct.I2C_Mode = I2C_Mode_I2C; //选择IIC模式
  I2C_InitStruct.I2C_OwnAddress1 = DEBUG_I2Cx_Addr;//输入主机地址,与从机有所区别即可正常通信
  I2C_Init(DEBUG_I2Cx_Port,&I2C_InitStruct);
  //使能IIC
  I2C_Cmd(DEBUG_I2Cx_Port, ENABLE);
  }
  (4)void I2C_ByteWrite(uint8_t pBuffer, uint8_t WriteAddr)
  void I2C_ByteWrite(uint8_t *pBuffer, uint8_t WriteAddr)
  {
  //读一个字节
  while(I2C_GetFlagStatus(DEBUG_I2Cx_Port, I2C_FLAG_BUSY));
  //发送Start信号
  I2C_GenerateSTART(DEBUG_I2Cx_Port,ENABLE);
  //等待EV5事件:IIC开始信号已经发出 (I2C_SR1内SB位置1)
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
  //发送7位“EEPROM地址”
  I2C_Send7bitAddress(DEBUG_I2Cx_Port,DEBUG_EEPROM_Addr,I2C_Direction_Transmitter);
  //等待EV6事件:表示地址已经发送
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
  //写入EEPROM内将要写入的地址数据
  I2C_SendData(DEBUG_I2Cx_Port,WriteAddr);
  //等待EV8事件:返回SET则数据寄存器DR为空
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
  //写入数据
  I2C_SendData(DEBUG_I2Cx_Port,*pBuffer);
  //等待EV8事件:返回SET则数据寄存器DR为空
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
  //一个字节发送完成,发送Stop信号
  I2C_GenerateSTOP(DEBUG_I2Cx_Port, ENABLE);
  }
  (5)void I2C_ByteRead(uint8_t pBuffer, uint8_t ReadAddr);
  /**
  * @brief 从EEPROM里面读取一块数据
  * @param
  * @arg pBuffer:存放从EEPROM读取的数据的缓冲区指针
  * @arg WriteAddr:接收数据的EEPROM的地址
  * @retval 无
  */
  void I2C_ByteRead(uint8_t *pBuffer, uint8_t ReadAddr)
  {
  //发送Start信号
  I2C_GenerateSTART(DEBUG_I2Cx_Port,ENABLE);
  //等待EV5事件:IIC开始信号已经发出 (I2C_SR1内SB位置1)
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
  //发送7位“EEPROM地址”
  I2C_Send7bitAddress(DEBUG_I2Cx_Port,DEBUG_EEPROM_Addr,I2C_Direction_Transmitter);
  //等待EV6事件:表示地址已经发送
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)==ERROR);
  //写入EEPROM内存“单元地址”
  I2C_SendData(DEBUG_I2Cx_Port,ReadAddr);
  //等待EV8事件:数据寄存器DR为空 ,地址数据已经发送
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_BYTE_TRANSMITTED)==ERROR);
  //重新发送Start信号
  I2C_GenerateSTART(DEBUG_I2Cx_Port,ENABLE);
  //等待EV5事件
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_MODE_SELECT)==ERROR);
  //发送7位“EEPROM地址”
  I2C_Send7bitAddress(DEBUG_I2Cx_Port,DEBUG_EEPROM_Addr,I2C_Direction_Receiver);//注意方向
  //等待EV6事件(接收):表示地址已经发送
  while(I2C_CheckEvent(DEBUG_I2Cx_Port,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)==ERROR);//注意方向
  //产生非应答
  I2C_AcknowledgeConfig(DEBUG_I2Cx_Port, DISABLE);
  //发送Stop信号
  I2C_GenerateSTOP(DEBUG_I2Cx_Port, ENABLE);
  //等待EV7事件, BUSY, MSL and RXNE flags
  while(I2C_CheckEvent(DEBUG_I2Cx_Port, I2C_EVENT_MASTER_BYTE_RECEIVED)==ERROR);
  *pBuffer = I2C_ReceiveData(DEBUG_I2Cx_Port);
  //重新初始化 为下次做准备
  I2C_AcknowledgeConfig(DEBUG_I2Cx_Port, ENABLE);
  }
  (6)函数测试
  #include “stm32f10x.h”
  #include “bsp_i2c.h”
  #include “bsp_usart.h”
  uint8_t I2C_Buf_Write[256];
  uint8_t I2C_Buf_Read[256];
  uint16_t i=0;
  void delay(uint32_t count)
  {
  while(count--);
  }
  int main(void)
  {
  USART_Config();
  I2C_Config();
  for(i=0;i《256;i++)
  {
  I2C_Buf_Write[i] = i;
  }
  printf(“rn 这是一个I2C外设(AT24C02)读写测试例程 rn”);
  I2C_ByteWrite(&I2C_Buf_Write[2],0x55);
  delay(0xffff);//因为STM32处理速度远大400k 所以,等待写入完成。
  I2C_ByteRead(&I2C_Buf_Read[2],0x55);
  printf(“0x%x ”, I2C_Buf_Read[2]);
  while(1);
  }
  //读写函数为测试学习使用,所有还有很多不完善的地方,改进中。..。.
  
举报

更多回帖

发帖
×
20
完善资料,
赚取积分