Bootstrap

STM32 HAL I2C(IIC)通信的序列传输(restart condition)

STM32 HAL I2C(IIC)通信的序列(Seq)传输函数(restart condition)

[email protected]

阅读本文需要你对I2C协议有基本的理解,包括收发过程,协议包的定义等。
参考资料和数据手册:

  1. I2C specification
  2. STM32 F4XX Reference Manual
  3. I2C Bus

我们知道,在I2C通信中,如果主机不想释放总线的归属权,希望在此次通信完成后继续给当前从机发送数据,或向另一个新的地址(另一个从机)发送数据,那么可以不发送STOP信号,而是继续占有总线,直接发送一个新的START信号,即I2C协议中所谓的Restart Condition。HAL自然提供了相应的函数支持,他们是一系列以HAL_I2C_Master/Slave_Seq_Transmit/Receive_IT/DMA()命名的函数。

但是HAL库的文档中对这些函数的使用并没有详细的介绍,国内外的网站上也没有相关函数的使用教程和信息,因此笔者在通过资料查阅和实验,以及花费了大量的时间阅读HAL的源码之后,总结出了这一篇参考教程。

本文重点介绍HAL的I2C序列传输主机(master)接口,即HAL库中下列函数的使用,和他们的参数XferOptoin的含义:

在这里插入图片描述
在这里插入图片描述

HAL库提供的8个用于I2C序列通信的接口

在这里插入图片描述

XferOption中的几种可选定义

XferOption的含义

我们按照定义的顺序进行讲解。

I2C_FIRST_AND_LAST_FRAME
I2C_FIRST_FRAME    
I2C_NEXT_FRAME 
I2C_FIRST_AND_NEXT_FRAME
I2C_LAST_FRAME
I2C_LAST_FRAME_NO_STOP             
I2C_OTHER_FRAME        
I2C_OTHER_AND_LAST_FRAME      
  1. I2C_FIRST_AND_LAST_FRAME

    如果在调用以上函数时传入了该参数,则和普通的I2C传输函数没有什么区别,字如其名,本次传输的是第一次也是最后一次,传输将会在发送或接收了函数参数指定的字节数后结束(主机会发送STOP结束通信)。

  2. I2C_FIRST_FRAME

    该参数在开始第一次传输的时候使用。即如果你需要在一次传输结束之后不释放总线马上开始另一次方向相同且目标从机都相同的传输(此次是transmit下次必须还是transmit,不可以转为receive,且传输的从机必须是同一个),那么就选择这个参数。使用此参数,会在传输结束之后让I2C硬件继续占用总线,直到下一次传输开始。

    传入这个参数后,I2C不会发出RESTART信号,只是传输数据之后保持对总线的占用,因此此次连接的从机会继续对总线保持监测。因此在下一次传输时,通信不能改变方向且必须和同一个从机进行;除非使用I2C_LAST_FRAME_NO_STOPI2C_OTHER_FRAMEI2C_OTHER_AND_LAST_FRAME参数。

  3. I2C_NEXT_FRAME

    字如其名。在之前已经调用过一次传输函数并且传入的参数为I2C_FIRST_FRAME之后,第二次调用同一个传输接口,并希望之后继续占用总线,不要释放(比如有第三次传输或更多次的传输),则使用此参数。注意,传输的方向必须和第一次相同。如第一次使用HAL_I2C_Master_Seq_Receive_IT()并传入了**I2C_FIRST_FRAME,那么本次也应该调用HAL_I2C_Master_Seq_Receive_IT(),同时传入I2C_NEXT_FRAME**参数。

    在完成本次传输之后,下一次的通信不能改变方向且必须和同一个从机进行;除非使用I2C_LAST_FRAME_NO_STOPI2C_OTHER_FRAMEI2C_OTHER_AND_LAST_FRAME参数。

  4. I2C_FIRST_AND_NEXT_FRAME

    该参数是2和3的结合。仅仅是为了代码的复用性和编程更加方便而添加的。在第一次和第二次调用传输接口的时候,都可以传入这个参数。即I2C占有总线之后,连续进行两次方向相同的传输,就可以使用这个参数,同时保持总线不释放,继续占用。

  5. I2C_LAST_FRAME

    在之前已经调用过传输接口函数,即我们已经占有了总线的情况下,希望在本次传输结束后终止,即释放总线发出STOP信号,则传入此参数。注意,传输方向需要和之前相同,地址也需要相同。在本次传输结束之后,主机将会发出STOP信号,释放总线。

  6. I2C_LAST_FRAME_NO_STOP

    注意,这是最关键的一个参数。此参数在已经完成一次传输并且希望在下一次传输中调换方向时使用。很多时候我们需要先向从机传送消息,写入“命令码”,从机会根据命令码准备反馈消息;完成写入后,主机需要立刻启动读取,将数据读回。为了防止其他主机占用总线,我们就需要这个接口。

    在之前一次传输中若调用了参数为I2C_FIRST_FRAMEI2C_NEXT_FRAMEI2C_FIRST_AND_NEXT_FRAME的传输函数,且在此次希望改变传输的方向,则在调用传输函数时传入此参数。

    I2C_LAST_FRAME_NO_STOP为参数的传输函数中,首先会发出restart,然后重新发送从机地址(当然包括读写位),再进行数据写入/读取。并且在此次传输结束之后,总线不会被释放如果希望在下一次传输中再次变换传输方向,可以继续传入此参数,或使用I2C_OTHER_FRAMEI2C_OTHER_AND_LAST_FRAME参数

接下来两个参数都是为切换从机准备的,即使用这两个参数允许此次通信的目标从机(地址)和之前不同。

  1. I2C_OTHER_FRAME

    只需要在之前启用了传输且没有发出STOP(只要没有释放总线),便可以使用此参数开启一次新的传输。并且在这次传输中可以选中其他从机,**即参数中的DevAddress可以不同。**在此次传输结束之后,不会发出STOP信号,之后可以继续传输。

    例如,首先调用HAL_I2C_Master_Seq_Receive_IT(),传入了从机1的地址,XferOptionI2C_FIRST_FRAME,从从机1处接收了10byte的数据;随后,调用HAL_I2C_Master_Seq_Transmit_IT(),传入了从机2的地址,XferOptionI2C_OTHER_FRAME,向从机2发送了5byte的数据,最后再次调用HAL_I2C_Master_Seq_Transmit_IT()XferOptionI2C_LAST_FRAME,向从机2发送了1byte数据之后发出STOP,释放总线。

    整个过程中,I2C总线的情况是这样的:

    1. 主机拉低SDA发送START信号,随后发送从机1的地址,从机1 ack之后主机连续发送10byte的数据(每个byte间从机都会ack应答)
    2. 主机拉低SDA发送RESTART信号,随后发送从机2的地址,从机2 ack后主机连续发送5byte数据
    3. 主机不会发送RESTART,继续向总线上发出1byte数据,从机2 ack后主机发出STOP,释放总线完成整个序列传输。
  2. I2C_OTHER_AND_LAST_FRAME

    此参数和第七个参数类似,只不过在传输结束后会发出STOP信号,结束通信让出总线的归属权。

P.S. : I2C的读写都必须由主机发起,也就是说,如果要在传输的途中改变传输方向,那么主机必须重新发出一包地址信息,因为地址的LSB(最低为)是读写位,只有从机收到地址才知道接下来一个字节是read or write。同时,要发送地址信息,之前必须要由RESTART信号,从机收到START or RESART,才会开始监听接下来一个byte的数据并将其当作地址处理,如果和自己的地址匹配,就接收接下来的信息。
10位地址模式也类似。

P.P.S. : STM32 F7系列就简化了序列通信的复杂度,将所有的参数归结为三种:AUTO_END,SOFT_END以及RELOAD_MODE,分别代表当前传输结束后发送STOP,当前传输结束后啥也不干继续占用总线以及传输前会发送RESTART信号。通过“或”运算( | )来组合三种标志,让用户灵活度大大提高。

HAL源码解析

I2C通信的函数实现其实大同小异,我们以STM32F4 HAL中的HAL_I2C_Master_Seq_Transmit_IT()为例,介绍I2C的Seq transfer序列传输过程。为了有最好的体验,建议将代码复制到VSCode中,提供高亮支持以便观看,或自行打开HAL中的对应文件(stm32f4xx_hal_i2c.c,在CubeMX生成的Driver/STM32F4xx_HAL_Driver/Src文件夹下)。

注意,ST不同的产品线HAL代码的实现不同,这是由于不同系列的硬件支持的特性不同,以及他们之间的兼容性不同。

直接上源码,笔者已经添加了注释并将无用的部分省略(通过//注释以及//…),很详细:

HAL_StatusTypeDef HAL_I2C_Master_Seq_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t XferOptions)
{
	// 临时变量申明和赋值,参数检查等
    // ...

  if (hi2c->State == HAL_I2C_STATE_READY) // 判断I2C外设是否READY,有没有其他线程占用
  {
    /* 如果不是第一次调用接口(不是首次开启传输),要通过I2C的状态寄存器检查I2C硬件是否BUSY */
    if ((READ_BIT(hi2c->Instance->CR1, I2C_CR1_STOP) == I2C_CR1_STOP) || (XferOptions == I2C_FIRST_AND_LAST_FRAME) || (XferOptions == I2C_FIRST_FRAME))
    {
      /* Wait until BUSY flag is reset */
      count = I2C_TIMEOUT_BUSY_FLAG * (SystemCoreClock / 25U / 1000U);
      do  
      {
          //这部分都是超时检查,如果超时会返回HAL_ERROR错误...
      }
      while (__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BUSY) != RESET); // 轮询,直到I2C_FLAG_BUSY被硬件RESET
    }

    // 传输的初始化配置,包括给I2C硬件加锁,配置传输字节数和传输方向等
	// ... 

    /* 如果传输的方向没有改变,并且XferOption并不是I2C_OTHER_FRAME或I2C_OTHER_AND_LAST_FRAME,那么就不会产生RESTART */
    if ((Prev_State != I2C_STATE_MASTER_BUSY_TX) || (IS_I2C_TRANSFER_OTHER_OPTIONS_REQUEST(XferOptions) == 1)) // Prev_State当中保存了上一次传输的状态(Tx or Rx)
    {
      /* Generate Start */
      SET_BIT(hi2c->Instance->CR1, I2C_CR1_START);
    }
      
	// 解锁I2C硬件,开启中断等
	// ...
      
    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

HAL_I2C_Master_Seq_Transmit_IT()实际上是中断和任务前端分离的设计,调用此函数只是设定了一些标志位,随后更多处理会在中断中进行,即HAL_I2C_EV_IRQHandler()函数,这里同样略去不重要的部分:

void HAL_I2C_EV_IRQHandler(I2C_HandleTypeDef *hi2c)
{
	// 临时变量和参数的初始化与赋值
	// ...
    
  /* Master or Memory mode selected */
  if ((CurrentMode == HAL_I2C_MODE_MASTER) || (CurrentMode == HAL_I2C_MODE_MEM))
  {
    sr2itflags   = READ_REG(hi2c->Instance->SR2);
    sr1itflags   = READ_REG(hi2c->Instance->SR1);

    /* Exit IRQ event until Start Bit detected in case of Other frame requested */
    if ((I2C_CHECK_FLAG(sr1itflags, I2C_FLAG_SB) == RESET) && (IS_I2C_TRANSFER_OTHER_OPTIONS_REQUEST(CurrentXferOptions) == 1U))
    {
      return; // 如果start位还没有产生,直接退出,等到其被置位
    }

    /* SB Set ,起始位已经产生----------------------------------------------------------------*/
    if ((I2C_CHECK_FLAG(sr1itflags, I2C_FLAG_SB) != RESET) && (I2C_CHECK_IT_SOURCE(itsources, I2C_IT_EVT) != RESET))
    { // 会通过这个判断说明之前已经产生了RESTART位,在之前的分析中我们知道,只有第一次传输或传入了I2C_OTHER_FRAME的情况下,会发出RESTART信号
      // 也就是说,如果我们在第一次调用传输接口时传入I2C_FIRST_FRAME,第二次使用I2C_NEXT_FRAME的话,是不会进到这里的,也不会重新发送地址,而是会直接进入下方的/* I2C in mode Transmitter */部分.
      /* Convert OTHER_xxx XferOptions if any */
      I2C_ConvertOtherXferOptions(hi2c); // 这个宏会把OTHER_FRAME转化成其他参数,方便后续处理,我们不用管

      I2C_Master_SB(hi2c); // 此函数会根据调用HAL_I2C_Master_Seq_Transmit_IT()时设定的配置,并发送从机地址
    }
    /* ADDR Set,从机已经接收到地址发回ACK --------------------------------------------------------------*/
    else if ((I2C_CHECK_FLAG(sr1itflags, I2C_FLAG_ADDR) != RESET) && (I2C_CHECK_IT_SOURCE(itsources, I2C_IT_EVT) != RESET))
    { // 会通过这个判断进入到这里,说明从机已经接收到主机发送的地址,在之前的分析中我们知道,只有FIRST_FRAME或另外两个OTHER_FRAME会发出RESTART并发送从机地址,传入其他参数则不会重新发送地址.
      I2C_Master_ADDR(hi2c); // 此函数会处理ADDR等相关标志位,并启动数据位的发送
    }
    /* I2C in mode Transmitter,说明已经处在数据发送状态下-----------------------------------------------*/
    else if (I2C_CHECK_FLAG(sr2itflags, I2C_FLAG_TRA) != RESET)
    {  // !!!在使用I2C_NEXT_FRAME,I2C_FIRST_AND_NEXT_FRAME,I2C_LAST_FRAME的时候,进入数据传输阶段都会直接跑到这里,不会进入SB判断和ADDR的判断!!! 也就是说,使用这些参数不会发出RESTART信号,而是继续数据的传输.
      /* 如果启用了DMA传输,那么不能干扰DMA,会跳过这个判断语句.这里我们以IT传输为例,不用关心这句判断,会直接满足条件进入 */
      if (READ_BIT(hi2c->Instance->CR2, I2C_CR2_DMAEN) != I2C_CR2_DMAEN)
      {
        // 补充:BTF=Byte Transfer Finished, TXE=Transmit data register Empty
        /* TXE set and BTF reset ,TXE=1,BTF=0说明发送数据寄存器为空(上一次发送已经完成),新的一次发送尚未开始-----------------------------------------------*/
        if ((I2C_CHECK_FLAG(sr1itflags, I2C_FLAG_TXE) != RESET) && (I2C_CHECK_IT_SOURCE(itsources, I2C_IT_BUF) != RESET) && (I2C_CHECK_FLAG(sr1itflags, I2C_FLAG_BTF) == RESET))
        {
          I2C_MasterTransmit_TXE(hi2c); // 这个函数会判断当前是否已经发送完所有字节,如果发送完,则根据传入的XferOption是否包含LAST决定要不要发送STOP位
        }
        /* BTF set -------------------------------------------------------------*/
        else if ((I2C_CHECK_FLAG(sr1itflags, I2C_FLAG_BTF) != RESET) && (I2C_CHECK_IT_SOURCE(itsources, I2C_IT_EVT) != RESET))
        {
          if (CurrentState == HAL_I2C_STATE_BUSY_TX)
          {
            I2C_MasterTransmit_BTF(hi2c); // 这个函数会判断是否已经发送完所有字节,如果没发送完,则将数据写入DR进行发送;如果发送完,则处理同上.
          }
        }
      }
    }
    else
    {
      /* I2C in mode Receiver --------------------------------------------------*/
      // 我们以发送为例,接收就不讲解了,有兴趣的同学可以自行查看源码.当然,需要配合寄存器和功能说明手册
    }
  }
}

// 补充: I2C的CurrenState(工作状态)有以下几种,含义如注释所示:
typedef enum
{
  HAL_I2C_STATE_RESET             = 0x00U,   /*!< Peripheral is not yet Initialized         */
  HAL_I2C_STATE_READY             = 0x20U,   /*!< Peripheral Initialized and ready for use  */
  HAL_I2C_STATE_BUSY              = 0x24U,   /*!< An internal process is ongoing            */
  HAL_I2C_STATE_BUSY_TX           = 0x21U,   /*!< Data Transmission process is ongoing      */
  HAL_I2C_STATE_BUSY_RX           = 0x22U,   /*!< Data Reception process is ongoing         */
  HAL_I2C_STATE_LISTEN            = 0x28U,   /*!< Address Listen Mode is ongoing            */
  HAL_I2C_STATE_BUSY_TX_LISTEN    = 0x29U,   /*!< Address Listen Mode and Data Transmission
                                                 process is ongoing                         */
  HAL_I2C_STATE_BUSY_RX_LISTEN    = 0x2AU,   /*!< Address Listen Mode and Data Reception
                                                 process is ongoing                         */
  HAL_I2C_STATE_ABORT             = 0x60U,   /*!< Abort user request ongoing                */
  HAL_I2C_STATE_TIMEOUT           = 0xA0U,   /*!< Timeout state                             */
  HAL_I2C_STATE_ERROR             = 0xE0U    /*!< Error                                     */
} HAL_I2C_StateTypeDef;

实验验证

回学校后补充示波器图像。

;