小麦大叔
认证:普通会员
所在专题目录 查看专题
STM32 使用st-link调试遇到写保护 Flash Timeout 问题的解决思路
天哪!原来PWM这么简单
小小舵机,大大玄机
UART协议快速扫盲(图文并茂+超详细)
I2C协议快速扫盲
原来SPI并没有我想的那么简单
作者动态 更多
新手学STM32的话,先学标准库还是HAL库?
10-18 15:09
一个超级实用的源码阅读小技巧
07-31 10:28
Sourcetail 一款代码编辑神器,让看源码如丝般顺滑
07-30 09:24
真正的机器人为什么都需要SLAM?
07-29 13:12
又踩坑了!这次败给CAN总线了
07-16 13:59

I2C协议快速扫盲

本文将带你了解I²C协议。我们将学习I²C协议背景,硬件层,数据传输协议,实际上如何工作,单个主设备连接多个从机,多个主设备连接多个从机,以及如何编程。如果觉得不错,欢迎关注、分享、收藏、点赞。希望能帮助到大家,如有错误敬请指出,谢谢!

    目录

    • 背景
    • 硬件层
    • 数据传输协议
    • 实际上如何工作?
    • 单个主设备连接多个从机
    • 多个主设备连接多个从机
    • 如何编程?
    • 总结

    一、背景

    I²CInter-Integrated Circuit),中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,是由飞利浦公司在1980年代初设计的,方便了主板、嵌入式系统或手机与周边设备组件之间的通讯。由于其简单性,它被广泛用于微控制器与传感器阵列,显示器,IoT设备,EEPROM等之间的通信。

    I²C最重要的功能包括:

    • 只需要两条总线;
    • 没有严格的波特率要求,例如使用RS232,主设备生成总线时钟;
    • 所有组件之间都存在简单的主/从关系,连接到总线的每个设备均可通过唯一地址进行软件寻址;
    • I²C是真正的多主设备总线,可提供仲裁和冲突检测;
    • 传输速度;
      • 标准模式:Standard Mode = 100 Kbps
      • 快速模式:Fast Mode = 400 Kbps
      • 高速模式: High speed mode = 3.4 Mbps
      • 超快速模式: Ultra fast mode = 5 Mbps
    • 最大主设备数:无限制;
    • 最大从机数:理论上是127;

    以上是I²C的一些重要特点,下面会进一步对I²C进行介绍。

    二、硬件层

    I²C协议仅需要一个SDA和SCL引脚。SDA是串行数据线的缩写,而SCL是串行时钟线的缩写。这两条数据线需要接上拉电阻。

    设备间的连接如下所示:

    使用I²C,可以将多个从机(Slave)连接到单个主设备(Master),并且还可以有多个主设备(Master)控制一个或多个从机(Slave)。

    假如希望有多个微控制器(MCU)将数据记录到单个存储卡或将文本显示到单个LCD时,这个功能就非常有用。

    I²C总线(SDASCL)内部都使用漏极开路驱动器(开漏驱动),因此SDASCL 可以被拉低为低电平,但是不能被驱动为高电平,所以每条线上都要使用一个上拉电阻,默认情况下将其保持在高电平;

    拉电阻的值取决于许多因素。德州仪器TI 建议 使用以下公式来计算正确的上拉电阻值:

    具体如下所示:

    从上表可知,使用I2C设备必须在的灌电流下工作,

    这里不难发现需要在做选型需要满足几个条件;

    所以根据上述公式可以计算,对于5V的电源,每个上拉电阻必须至少具有1.53kΩ,而对于3.3V的电源,每个电阻必须至少具有967Ω。

    如果觉得计算电阻值比较麻烦,也可以使用典型值 4.7kΩ

    上述推导过程可以参考 TI的文档《I2C Bus Pullup Resistor Calculation》 https://www.ti.com/lit/an/slva689/slva689.pdf

    最终在调试的时候,当我们测量SDA或SCL信号并且逻辑LOW上的电压高于0.4V时,我们就知道可以知道灌电流太高了;

    当然,这并不意味着每当灌电流超过3mA时,设备就会立即停止工作。但是,在操作超出其规格的设备时,应始终小心,因为它可能导致通信故障,缩短其使用寿命甚至甚至永久损坏设备。

    三、数据传输协议

    主设备和从设备进行数据传输时遵循以下协议格式。数据通过一条SDA数据线在主设备和从设备之间传输01的串行数据。串行数据序列的结构可以分为,开始条件,地址位,读写位,应答位,数据位,停止条件,具体如下所示;

    开始条件

    当主设备决定开始通讯时,需要发送开始信号,需要执行以下动作;

    • 先将SDA线从高压电平切换到低压电平;
    • 然后将SCL从高电平切换到低电平;

    在主设备发送开始条件信号之后,所有从机即使处于睡眠模式也将变为活动状态,并等待接收地址位

    具体如下图所示;

    地址位

    通常地址位占7位数据,主设备如果需要向从机发送/接收数据,首先要发送对应从机的地址,然后会匹配总线上挂载的从机的地址;

    I2C还支持10位寻址;

    读写位

    该位指定数据传输的方向;

    • 如果主设备需要将数据发送到从设备,则该位设置为 0
    • 如果主设备需要往从设备接收数据,则将其设置为 1

    ACK / NACK

    主机每次发送完数据之后会等待从设备的应答信号ACK

    • 在第9个时钟信号,如果从设备发送应答信号ACK,则SDA会被拉低;
    • 若没有应答信号NACK,则SDA会输出为高电平,这过程会引起主设备发生重启或者停止;

    数据块

    传输的数据总共有8位,由发送方设置,它需要将数据位传输到接收方。

    发送之后会紧跟一个ACK / NACK位,如果接收器成功接收到数据,则设置为0。否则,它保持逻辑“ 1”。

    重复发送,直到数据完全传输为止。

    停止条件

    当主设备决定结束通讯时,需要发送开始信号,需要执行以下动作;

    • 先将SDA线从低电压电平切换到高电压电平;
    • 再将SCL线从高电平拉到低电平;

    具体如下图所示;

    四、实际上如何工作?

    第一步:起始条件

    主设备通过将SDA线从高电平切换到低电平,再将SCL线从高电平切换到低电平,来向每个连接的从机发送启动条件 :

    第二步:发送从设备地址

    主设备向每个从机发送要与之通信的从机的7位或10位地址,以及相应的读/写位

    第三步:接收应答

    每个从设备将主设备发送的地址与其自己的地址进行比较。如果地址匹配,则从设备通过将SDA线拉低一位以表示返回一个ACK位

    如果来自主设备的地址与从机自身的地址不匹配,则从设备将SDA线拉高,表示返回一个NACK位

    第四步:收发数据

    主设备发送或接收数据到从设备;

    第五步:接收应答

    在传输完每个数据帧后,接收设备将另一个ACK位返回给发送方,以确认已成功接收到该帧:

    第六步:停止通信

    为了停止数据传输,主设备将SCL切换为高电平,然后再将SDA切换为高电平,从而向从机发送停止条件;

    五、单个主设备连接多个从机

    I2C总线上的主设备使用7位地址对从设备进行寻址,可以使用128()个从机地址。

    请使用4.7K上拉电阻将SDA和SCL线连接到Vcc;

    六、多个主设备连接多个从机

    多个主设备可以连接到一个或多个从机;

    当两个主设备试图通过SDA线路同时发送或接收数据时,同一系统中的多个主设备就会出现问题。

    为了解决这个问题,每个主设备都需要在发送消息之前检测SDA线是低电平还是高电平

    • 如果SDA线为低电平,则意味着另一个主设备可以控制总线,并且主设备应等待发送消息。

    • 如果SDA线为高电平,则可以安全地发送消息。

      要将多个主设备连接到多个从机,请使用下图,其中4.7K上拉电阻将SDA和SCL线连接到Vcc:

    七、如何编程?

    ==Talk is cheap. Show me the code.==

    参考了STM32的HAL库中I2C驱动,主设备发送函数HAL_I2C_Master_Transmit()具体如下:

    /**
      * @brief  Transmits in master mode an amount of data in blocking mode.
      * @param  hi2c Pointer to a I2C_HandleTypeDef structure that contains
      *                the configuration information for the specified I2C.
      * @param  DevAddress Target device address: The device 7 bits address value
      *         in datasheet must be shifted to the left before calling the interface
      * @param  pData Pointer to data buffer
      * @param  Size Amount of data to be sent
      * @param  Timeout Timeout duration
      * @retval HAL status
      */
    HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, 
                                              uint16_t DevAddress, 
                                              uint8_t *pData, 
                                              uint16_t Size, 
                                              uint32_t Timeout){
      uint32_t tickstart = 0x00U;
    
      /* Init tickstart for timeout management*/
      tickstart = HAL_GetTick();
    
      if(hi2c->State == HAL_I2C_STATE_READY){
        /* Wait until BUSY flag is reset */
        if(I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK){
          return HAL_BUSY;
        }
    
        /* Process Locked */
        __HAL_LOCK(hi2c);
    
        /* Check if the I2C is already enabled */
        if((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE){
          /* Enable I2C peripheral */
          __HAL_I2C_ENABLE(hi2c);
        }
    
        /* Disable Pos */
        hi2c->Instance->CR1 &= ~I2C_CR1_POS;
    
        hi2c->State     = HAL_I2C_STATE_BUSY_TX;
        hi2c->Mode      = HAL_I2C_MODE_MASTER;
        hi2c->ErrorCode = HAL_I2C_ERROR_NONE;
    
        /* Prepare transfer parameters */
        hi2c->pBuffPtr    = pData;
        hi2c->XferCount   = Size;
        hi2c->XferOptions = I2C_NO_OPTION_FRAME;
        hi2c->XferSize    = hi2c->XferCount;
    
        /* Send Slave Address */
        if(I2C_MasterRequestWrite(hi2c, DevAddress, Timeout, tickstart) != HAL_OK){
          if(hi2c->ErrorCode == HAL_I2C_ERROR_AF){
            /* Process Unlocked */
            __HAL_UNLOCK(hi2c);
            return HAL_ERROR;
          }else{
            /* Process Unlocked */
            __HAL_UNLOCK(hi2c);
            return HAL_TIMEOUT;
          }
        }
    
        /* Clear ADDR flag */
        __HAL_I2C_CLEAR_ADDRFLAG(hi2c);
    
        while(hi2c->XferSize > 0U){
          /* Wait until TXE flag is set */
          if(I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK){
            if(hi2c->ErrorCode == HAL_I2C_ERROR_AF){
              /* Generate Stop */
              hi2c->Instance->CR1 |= I2C_CR1_STOP;
              return HAL_ERROR;
            }else{
              return HAL_TIMEOUT;
            }
          }
          /* Write data to DR */
          hi2c->Instance->DR = (*hi2c->pBuffPtr++);
          hi2c->XferCount--;
          hi2c->XferSize--;
    
          if((__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == SET) 
             && (hi2c->XferSize != 0U)){
            /* Write data to DR */
            hi2c->Instance->DR = (*hi2c->pBuffPtr++);
            hi2c->XferCount--;
            hi2c->XferSize--;
          }
          /* Wait until BTF flag is set */
          if(I2C_WaitOnBTFFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK){
              
            if(hi2c->ErrorCode == HAL_I2C_ERROR_AF){
              /* Generate Stop */
              hi2c->Instance->CR1 |= I2C_CR1_STOP;
              return HAL_ERROR;
            }else{
              return HAL_TIMEOUT;
            }
          }
        }
    
        /* Generate Stop */
        hi2c->Instance->CR1 |= I2C_CR1_STOP;
    
        hi2c->State = HAL_I2C_STATE_READY;
        hi2c->Mode = HAL_I2C_MODE_NONE;
        
        /* Process Unlocked */
        __HAL_UNLOCK(hi2c);
    
        return HAL_OK;
      }else{
        return HAL_BUSY;
      }
    }

    八、总结

    本文主要介绍I2C的入门基础知识,从I2C协议的硬件层,协议层进行了简单介绍;作者能力有限,难免存在错误和纰漏,请大佬不吝赐教。


    声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
    觉得内容不错的朋友,别忘了一键三连哦!
    赞 5
    收藏 5
    关注 138
    成为作者 赚取收益
    全部留言
    0/200
    成为第一个和作者交流的人吧