GPIO-I2C

引言

我们在前面学习完了USART,我们知道串口是一对一进行通信的。接下来我们学习的I2C是支持多对多和一对多的,但是我们学习过程中,只考虑一对多。

特点

两线制:一根SCL线用于传输时钟信号,一根SDA用于传输数据信号。

多主机/多从机:支持多对多和一对多,但是我们考虑的,和绝大多数情况都是一对多。

设备地址:一般为七位加一位读写位。

同步串行通信:所有数据都靠SCL时钟来控制。

半双工:同一时刻支持一个方向的传输。

速度模式:标准模式(100kbps),快速模式(400kbps)和快速模式+(1Mbps)

STM32F103ZET6和I2C

首先I2C是MCU上的片上外设,有两个I2C接口,其接线位置如下

和USART相同,在使用I2C的时候,需要复用GPIO接口

这里是对输出进行了复用,且使用开漏模式。但是在前面我们说一个GPIO的引脚,要么是输入,要么输出,为什么这里输入和输出用的是一个引脚呢?这里对输入并不是设置,只是当外部电压变化,内部可以感知到,并没有去配置输入模式之类的。

I2C基本通信原理

这个图中,我们除了SCL和SDA外,还看到了接VDD的两个上拉电阻,这个本质其实和I2C无关,主要是由于设置开漏模式导致的。​​​​​​​

这里在输出的时候就是和前面讲GPIO开漏输出是一样的,只是采用复用的方式。主要是对于输入大家可能有些疑问。这里输出后,其实在接收方,可以认为是导线直接连接,类似浮空输入,对输入进行整形,后复用。这里主要是可以感知到这个变化,如果需要就可以直接使用。

时钟与数据

那么时钟与数据是怎么配合协同工作的呢?​​​​​​​

这里规定,只有在时钟处于低电平期间,才可以作数据的跳变。在高电平期间会对数据进行读取。为了达到这个要求,也就要求数据的稳定时间要高于时钟高电平稳定时间,低电平。​​​​​​​

线与(Wried-AND)

我们刚才只在单个通信的角度看待I2C,现在我们在全局来看I2C

当有任何一个设备拉低SCL/SDA的时候,整条线都会接地。这个和逻辑与或者按位与是有点相似之处的。

线与的好处

时钟同步:比如主设发送数据过快,从设备来不及处理,就可以拉低SCL,来告诉主设备。这时主设备拉高电平也没有作用,知直到从设备释放SCL,才会继续发送。

总线仲裁:当有多台主设备竞争总线,如果自己发送1,由于其他设备发送0,会拉低总线,那么它就知道自己仲裁失败了。

为什么选择开漏

首先为什么推挽模式不可以:其一是因为当主设备输出1,然后从设备拉低电压,会造成短路,还有就是无法实现线与逻辑。而这两个问题正是开漏模式可以解决的。

I2C协议

①为空闲状态,空闲状态默认为高电平。②为开始状态,我们前面说只有在SCL为低电平SDA才可以跳变来改变数据,这里在SCL为高电平时SDA进行了跳变,表示开始。③为结束状态,同样在SCL为高电平的时候,SDA进行了从低向高的跳变,恢复空闲状态。​​​​​​​

Master发送从设备地址

在I2C中,一般由主设备产生时钟信号,也由主设备选定从设备。在最开始,会发送从设备的地址,发送后,如果从设备接收,会返回一个ACK,而没有匹配上的从设备不做任何回应。而这个地址一般为七位地址加一位读写位构成。​​​​​​​

Master写数据

开始位->发送地址和写->接收方应答->发送数据->接收方应答->...->停止

Master读数据

开始位->发送地址和读->接收方应答->接收方发送数据->主机应答->...->收方发送数据->主机不应答->停止

这里最后一个为什么是NACK,因为在写的时候,主机是知道自己要写多少,写完就结束,但是在读的时候,从机并不知道你要读多少,所以需要主机去进行控制。它在最后一个字节到达后不进行确认,从机就不会在发送数据了。

I2C功能结构图

将其简化,整合到全局​​​​​​​

首先,比较器是用来进行地址匹配的。然后控制逻辑电路输出时钟控制信息,数据控制包含移位寄存器和数据寄存器,数据通过APB1来从DR寄存器读取数据。不过这里的发送和USART不同,是MSB其余基本保持不变。

这里和前面还是一样,控制I2C其实就是控制它下面的寄存器。

DR寄存器用于存储数据,在发送的过程中如果TxE为表示不能写入,1表示可以写入。在接收过程中,RxNE为0表示不能读取数据,为1表示可以读取数据。

实战与源码

我们这里使用AT24C02来作从设备,我们要实现的是由USART来控制上位机,当写入的时候,把配置信息存到AT24C02当中。

AT24C02在读取当作一个一维数组来读取即可,但是在写入的时候不能跨页写入,会造成回卷。

我们整体的架构分为硬件,HAL库,板级驱动,控制逻辑,调用逻辑。

我们创建两个组,一个APP用于控制逻辑,BSP_Drivers为板级驱动,HAL库和调用逻辑(main)生成中是带好的。整体逻辑为,HAL库控制寄存器来控制硬件,板级驱动调用HAL库实现各自功能,控制逻辑通过调用板级驱动相关函数实现具体项目功能,最后main函数调用控制逻辑。

源码解析

初始化

这里和USART一样,也是使用了句柄。

cpp 复制代码
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(hi2c->Instance==I2C2)
  {
    /* USER CODE BEGIN I2C2_MspInit 0 */

    /* USER CODE END I2C2_MspInit 0 */

    __HAL_RCC_GPIOB_CLK_ENABLE();
    /**I2C2 GPIO Configuration
    PB10     ------> I2C2_SCL
    PB11     ------> I2C2_SDA
    */
    GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* Peripheral clock enable */
    __HAL_RCC_I2C2_CLK_ENABLE();
    /* USER CODE BEGIN I2C2_MspInit 1 */

    /* USER CODE END I2C2_MspInit 1 */

  }

}

首先,开启GPIO时钟,初始化GPIO,随后打开I2C2的时钟。

随后先关闭I2C的使能,进行配置,这里不是时钟配置,这里是设备自己的配置。随后配置寄存器信息,最后打开设备使能。

HAL_I2C_IsDeviceReady

cpp 复制代码
      tmp1 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR);
      tmp2 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF);
      while ((hi2c->State != HAL_I2C_STATE_TIMEOUT) && (tmp1 == RESET) && (tmp2 == RESET))
      {
        if (((HAL_GetTick() - tickstart) > Timeout) || (Timeout == 0U))
        {
          hi2c->State = HAL_I2C_STATE_TIMEOUT;
        }
        tmp1 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_ADDR);
        tmp2 = __HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_AF);
      }

这里主要来判断,有没有收到正确的应答。这两位互斥不可能全为1或全为0,ADDR为1表示成功,ADDR为0表示失败,AF则相反。随后循环尝试设定次数。

cpp 复制代码
  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;
    }

这里是检查设备状态是否就绪。

HAL_I2C_Mem_Write

写操作其实和检查状态类似,只不过后面要在加要写入的数据。

cpp 复制代码
 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 */
          SET_BIT(hi2c->Instance->CR1, I2C_CR1_STOP);
        }
        return HAL_ERROR;
      }

      /* Write data to DR */
      hi2c->Instance->DR = *hi2c->pBuffPtr;

      /* Increment Buffer pointer */
      hi2c->pBuffPtr++;

      /* Update counter */
      hi2c->XferSize--;
      hi2c->XferCount--;

      if ((__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == SET) && (hi2c->XferSize != 0U))
      {
        /* Write data to DR */
        hi2c->Instance->DR = *hi2c->pBuffPtr;

        /* Increment Buffer pointer */
        hi2c->pBuffPtr++;

        /* Update counter */
        hi2c->XferSize--;
        hi2c->XferCount--;
      }
    }

最下面连写入两次,相当于双缓冲区。主要靠TxE判断数据寄存器是否为空。

HAL_I2C_Mem_Read

读这里比较麻烦,做了很多的分类,包括为回复NACK专门设置标志位。本质都是为了实现最后一个回复是NACK。

相关推荐
振南的单片机世界2 小时前
PVD掉电中断——让单片机“咽气前”写完遗书
arm开发·stm32·单片机·嵌入式硬件
续写烂尾5 小时前
SD NAND存储实测:正点原子STM32精英板SDIO读写验证全流程
stm32·单片机·嵌入式硬件
zlinear数据采集卡7 小时前
USB HS 480M高速传输与SRAM记录仪:DABM-D223数据存储系统全解析
arm开发·嵌入式硬件·fpga开发·开源
wuyk5557 小时前
1.栈:后进先出的线性数据结构
c语言·数据结构·stm32·单片机
Crkylin8 小时前
Cortex-M SysTick系统滴答定时器详解
单片机·嵌入式硬件
Hello_Damon_Nikola8 小时前
AC7911从入门到精通
开发语言·嵌入式硬件·物联网·php
gscsded9 小时前
AUTOSAR CanTp 模块学习笔记
单片机·嵌入式硬件·汽车
Hello_Damon_Nikola9 小时前
扰流筋从原理到应用
人工智能·单片机·嵌入式硬件
某林2129 小时前
从“仿真能跑”到“真机能走”:一套轮腿机械狗强化学习系统的工程化实践
人工智能·stm32·嵌入式硬件·架构·机器人·机械狗