第18章 硬件I2C

第十八章 硬件I2C

EEPROM是一种掉电后数据不丢失的存储器,常用来存储一些配置信息,以便系统重新上电的时候加载之。 EEPOM芯片最常用的通讯方式就是I2C协议, 本小节以EEPROM的读写实验为大家讲解STM32的I2C使用方法。 实验中STM32的I2C外设采用主模式,分别用作主发送器和主接收器,通过查询事件的方式来确保正常通讯。

1. 硬件设计

本实验板中的EEPROM芯片(型号:AT24C02)的SCL及SDA引脚连接到了STM32对应的I2C引脚中,结合上拉电阻, 构成了I2C通讯总线,它们通过I2C总线交互。EEPROM芯片的设备地址一共有7位,其中高4位固定为:1010 b, 低3位则由A0/A1/A2信号线的电平决定,图中的R/W是读写方向位,与地址无关。

按照我们此处的连接,A0/A1/A2均为0,所以EEPROM的7位设备地址是:101 0000b,即0x50。 由于I2C通讯时常常是地址跟读写方向连在一起构成一个8位数,且当R/W位为0时,表示写方向, 所以加上7位地址,其值为“0xA0”,常称该值为I2C设备的“写地址”;当R/W位为1时,表示读方向, 加上7位地址,其值为“0xA1”,常称该值为“读地址”。

EEPROM芯片中还有一个WP引脚,具有写保护功能,当该引脚电平为高时,禁止写入数据,当引脚为低电平时, 可写入数据,我们直接接地,不使用写保护功能。

2. 软件设计

2.1 编程目的

  1. 配置通讯使用的目标引脚为开漏模式;

  2. 使能I2C外设的时钟;

  3. 配置I2C外设的模式、地址、速率等参数并使能I2C外设;

  4. 编写基本I2C按字节收发的函数;

  5. 编写读写EEPROM存储内容的函数;

  6. 编写测试程序,对读写数据进行校验。

2.2 代码分析

  • I2C硬件相关宏定义
/**************************I2C参数定义,I2C1或I2C2********************************/
#define             EEPROM_I2Cx                                I2C1                   // 我们这里使用I2C1
#define             EEPROM_I2C_APBxClock_FUN                   RCC_APB1PeriphClockCmd // 使能I2C时钟
#define             EEPROM_I2C_CLK                             RCC_APB1Periph_I2C1    // I2C时钟源
#define             EEPROM_I2C_GPIO_APBxClock_FUN              RCC_APB2PeriphClockCmd // 使能GPIO时钟
#define             EEPROM_I2C_GPIO_CLK                        RCC_APB2Periph_GPIOB   // GPIO时钟源  
#define             EEPROM_I2C_SCL_PORT                        GPIOB                  // SCL引脚所在GPIO端口
#define             EEPROM_I2C_SCL_PIN                         GPIO_Pin_6             // 使用PB6引脚
#define             EEPROM_I2C_SDA_PORT                        GPIOB                  // SDA引脚所在GPIO端口
#define             EEPROM_I2C_SDA_PIN                         GPIO_Pin_7             // 使用PB7引脚

// STM32 I2C 快速模式
#define I2C_Speed              400000

// 这个地址只要与STM32外挂的I2C器件地址不一样即可
#define I2Cx_OWN_ADDRESS7      0X0A   

// AT24C01/02每页有8个字节
#define I2C_PageSize           8

以上代码根据硬件连接,把与EEPROM通讯使用的I2C号 、引脚号都以宏封装起来,并且定义了自身的I2C地址及通讯速率,以便配置模式的时候使用。

  • 初始化I2C的GPIO

利用上面的宏,编写I2C GPIO引脚的初始化函数

// IC2的IO基础配置
static void I2C_GPIO_Config(void)
{
  // 1.定义一个结构体
  GPIO_InitTypeDef  GPIO_InitStructure; 
  // 2.使能与 I2C 有关的时钟
  EEPROM_I2C_APBxClock_FUN(EEPROM_I2C_CLK, ENABLE ); // I2C时钟使能
  EEPROM_I2C_GPIO_APBxClock_FUN(EEPROM_I2C_GPIO_CLK, ENABLE ); // GPIO时钟使能
  // 3.配置I2C_SCL、I2C_SDA
  GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN; // 选择引脚-PB6
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 选择速度50MHz
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;   // 使用开漏输出
  GPIO_Init(EEPROM_I2C_SCL_PORT, &GPIO_InitStructure);

  GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SDA_PIN; // 选择引脚-PB7
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 选择速度50MHz
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;      // 开漏输出
  GPIO_Init(EEPROM_I2C_SDA_PORT, &GPIO_InitStructure);        
}

开启相关的时钟并初始化GPIO引脚,函数执行流程如下:

  1. 使用GPIO_InitTypeDef定义GPIO初始化结构体变量, 以便下面用于存储GPIO配置;

  2. 调用库函数RCC_APB1PeriphClockCmd(代码中为宏EEPROM_I2C_APBxClock_FUN)使能I2C外设时钟, 调用RCC_APB2PeriphClockCmd(代码中为宏EEPROM_I2C_GPIO_APBxClock_FUN)来使能I2C引脚使用的GPIO端口时钟, 调用时我们使用“|”操作同时配置两个引脚。

  3. 向GPIO初始化结构体赋值,把引脚初始化成复用开漏模式, 要注意I2C的引脚必须使用这种模式。

  4. 使用以上初始化结构体的配置, 调用GPIO_Init函数向寄存器写入参数,完成GPIO的初始化。

  • 配置I2C的模式

以上只是配置了I2C使用的引脚,还不算对I2C模式的配置,下面我们来配置一下:

// I2C工作模式配置
static void I2C_Mode_Configu(void)
{
  // 1.首先定义一个结构体
  I2C_InitTypeDef  I2C_InitStructure; 
  // 2.I2C 配置
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // 使用stm32库函数-stm32f10x_i2c.h中定义的I2C模式
  // 3.使用高电平数据稳定,低电平数据变化 SCL 时钟线的占空比 
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 使用stm32库函数-stm32f10x_i2c.h中定义的I2C时钟占空比
  I2C_InitStructure.I2C_OwnAddress1 = I2Cx_OWN_ADDRESS7; // 选择自己的地址
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // 使能应答
  // 4.选择I2C的寻址模式
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 7位地址模式
  // 5.选择通信速率
  I2C_InitStructure.I2C_ClockSpeed = I2C_Speed; // 高速模式-400000Hz
  // 6.I2C 初始化
  I2C_Init(EEPROM_I2Cx, &I2C_InitStructure);
  I2C_Cmd(EEPROM_I2Cx, ENABLE); // 使能I2C
}

熟悉STM32 I2C结构的话,这段初始化程序就十分好理解,它把I2C外设通讯时钟SCL的低/高电平比设置为2,使能响应功能, 使用7位地址I2C_OWN_ADDRESS7以及速率配置为I2C_Speed(前面在bsp_i2c_ee.h定义的宏)。最后调用库函数I2C_Init把这些配置写入寄存器, 并调用I2C_Cmd函数使能外设。

当然了,出现了新的库函数,我们是要解释一下的:

I2C_InitTypeDef 结构体

这个结构体用于配置 I2C 外设的工作模式。主要字段如下:

  1. I2C_Mode
  • 功能:选择 I2C 模式。
  • 可选值
    • I2C_Mode_I2C:标准 I2C 模式。
    • I2C_Mode_SMBusDevice:SMBus 设备模式(仅适用于某些 STM32 设备)。
  1. I2C_DutyCycle
  • 功能:设置时钟线的占空比。
  • 可选值
    • I2C_DutyCycle_1:占空比为 1/16(标准模式,100 kHz 时钟频率)。
    • I2C_DutyCycle_2:占空比为 1/8(标准模式,400 kHz 时钟频率)。
  1. I2C_OwnAddress1
  • 功能:配置 I2C 外设的自定义地址。
  • 类型:这是一个 7 位或 10 位的地址,根据选择的地址模式。
  • 示例值0x00(如果不使用从设备地址)。
  1. I2C_Ack
  • 功能:选择应答功能。
  • 可选值
    • I2C_Ack_Enable:启用应答。
    • I2C_Ack_Disable:禁用应答。
  1. I2C_AcknowledgedAddress
  • 功能:选择地址模式。
  • 可选值
    • I2C_AcknowledgedAddress_7bit:7 位地址模式。
    • I2C_AcknowledgedAddress_10bit:10 位地址模式。
  1. I2C_ClockSpeed
  • 功能:设置 I2C 时钟频率。
  • 示例值:可以设置为 100000(100 kHz)或 400000(400 kHz)。

函数配置示例:

void I2C_Config(void)
{
    I2C_InitTypeDef I2C_InitStructure;

    // 配置 I2C 模式
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

    // 设置时钟占空比
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 高速模式下推荐

    // 设置自己的地址
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;

    // 启用应答
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;

    // 设置地址模式
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

    // 配置时钟速度
    I2C_InitStructure.I2C_ClockSpeed = 100000; // 100 kHz

    // 初始化 I2C
    I2C_Init(I2C1, &I2C_InitStructure);
    I2C_Cmd(I2C1, ENABLE);
}
---

为方便调用,我们把I2C的GPIO及模式配置都用I2C_EE_Init函数封装起来。

  • 有关EEPROM的函数,我们只需知道就行了,在这里分析一个

初始化好I2C外设后,就可以使用I2C通讯,我们看看如何向EEPROM写入一个字节的数据

// 通讯等待超时时间
#define I2CT_FLAG_TIMEOUT   ((uint32_t)0x1000)
#define I2CT_LONG_TIMEOUT   ((uint32_t)(10 * I2CT_FLAG_TIMEOUT))

/**
* @brief  I2C等待事件超时的情况下会调用这个函数来处理
* @param  errorCode:错误代码,可以用来定位是哪个环节出错.
* @retval 返回0,表示IIC读取失败.
*/
static  uint32_t I2C_TIMEOUT_UserCallback(uint8_t errorCode)
{
    /* 使用串口printf输出错误信息,方便调试 */
    EEPROM_ERROR("I2C 等待超时!errorCode = %d",errorCode);
    return 0;
}
/**
* @brief   写一个字节到I2C EEPROM中
* @param   pBuffer:缓冲区指针
* @param   WriteAddr:写地址
* @retval  正常返回1,异常返回0
*/
uint32_t I2C_EE_ByteWrite(u8* pBuffer, u8 WriteAddr)
{
    /* 产生I2C起始信号 */
    I2C_GenerateSTART(EEPROM_I2Cx, ENABLE);

    /*设置超时等待时间*/
    I2CTimeout = I2CT_FLAG_TIMEOUT;
    /* 检测 EV5 事件并清除标志*/
    while (!I2C_CheckEvent(EEPROM_I2Cx, I2C_EVENT_MASTER_MODE_SELECT))
    {
        if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(0);
    }

    /* 发送EEPROM设备地址 */
    I2C_Send7bitAddress(EEPROM_I2Cx, EEPROM_ADDRESS,
                        I2C_Direction_Transmitter);

    I2CTimeout = I2CT_FLAG_TIMEOUT;
    /* 检测 EV6 事件并清除标志*/
    while (!I2C_CheckEvent(EEPROM_I2Cx,
                        I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED))
    {
        if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(1);
    }

    /* 发送要写入的EEPROM内部地址(即EEPROM内部存储器的地址) */
    I2C_SendData(EEPROM_I2Cx, WriteAddr);

    I2CTimeout = I2CT_FLAG_TIMEOUT;
    /* 检测 EV8 事件并清除标志*/
    while (!I2C_CheckEvent(EEPROM_I2Cx,
                        I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    {
        if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(2);
    }
    /* 发送一字节要写入的数据 */
    I2C_SendData(EEPROM_I2Cx, *pBuffer);

    I2CTimeout = I2CT_FLAG_TIMEOUT;
    /* 检测 EV8 事件并清除标志*/
    while (!I2C_CheckEvent(EEPROM_I2Cx,
                        I2C_EVENT_MASTER_BYTE_TRANSMITTED))
    {
        if ((I2CTimeout--) == 0) return I2C_TIMEOUT_UserCallback(3);
    }

    /* 发送停止信号 */
    I2C_GenerateSTOP(EEPROM_I2Cx, ENABLE);

    return 1;
}

先来分析I2C_TIMEOUT_UserCallback函数,它的函数体里只调用了宏EEPROM_ERROR,这个宏封装了printf函数, 方便使用串口向上位机打印调试信息,阅读代码时把它当成printf函数即可。在I2C通讯的很多过程,都需要检测事件, 当检测到某事件后才能继续下一步的操作,但有时通讯错误或者I2C总线被占用,我们不能无休止地等待下去, 所以我们设定每个事件检测都有等待的时间上限,若超过这个时间,我们就调用I2C_TIMEOUT_UserCallback函数输出调试信息(或可以自己加其它操作),并终止I2C通讯。

了解了这个机制,再来分析I2C_EE_ByteWrite函数,这个函数实现了前面讲的I2C主发送器通讯流程

(1) 使用库函数I2C_GenerateSTART产生I2C起始信号, 其中的EEPROM_I2C宏是前面硬件定义相关的I2C编号;

(2) 对I2CTimeout变量赋值为宏I2CT_FLAG_TIMEOUT,这个I2CTimeout变量在下面的while循环中每次循环减1, 该循环通过调用库函数I2C_CheckEvent检测事件,若检测到事件,则进入通讯的下一阶段,若未检测到事件则停留在此处一直检测, 当检测I2CT_FLAG_TIMEOUT次都还没等待到事件则认为通讯失败,调用前面的I2C_TIMEOUT_UserCallback输出调试信息,并退出通讯;

(3) 调用库函数I2C_Send7bitAddress发送EEPROM的设备地址, 并把数据传输方向设置为I2C_Direction_Transmitter(即发送方向), 这个数据传输方向就是通过设置I2C通讯中紧跟地址后面的R/W位实现的。发送地址后以同样的方式检测EV6标志;

(4) 调用库函数I2C_SendData向EEPROM发送要写入的内部地址, 该地址是I2C_EE_ByteWrite函数的输入参数,发送完毕后等待EV8事件。 要注意这个内部地址跟上面的EEPROM地址不一样,上面的是指I2C总线设备的独立地址,而此处的内部地址是指EEPROM内数据组织的地址, 也可理解为EEPROM内存的地址或I2C设备的寄存器地址;

(5) 调用库函数I2C_SendData向EEPROM发送要写入的数据, 该数据是I2C_EE_ByteWrite函数的输入参数,发送完毕后等待EV8事件;

(6) 一个I2C通讯过程完毕, 调用I2C_GenerateSTOP发送停止信号。

在这个通讯过程中,STM32实际上通过I2C向EEPROM发送了两个数据,但为何第一个数据被解释为EEPROM的内存地址,这是由EEPROM的自己定义的单字节写入时序

EEPROM的单字节时序规定,向它写入数据的时候,第一个字节为内存地址,第二个字节是要写入的数据内容。 所以我们需要理解:命令、地址的本质都是数据,对数据的解释不同,它就有了不同的功能。

  • main函数

完成基本的读写函数后,接下来我们编写一个读写测试函数来检验驱动程序

#include "stm32f10x.h"
#include "./led/bsp_led.h"
#include "./usart/bsp_usart.h"
#include "./i2c/bsp_i2c_ee.h"
#include <string.h>

#define  EEP_Firstpage      0x00 // 定义EEPROM的起始页地址
uint8_t I2c_Buf_Write[256];      // 定义写入缓冲区
uint8_t I2c_Buf_Read[256];       // 定义读取缓冲区
uint8_t I2C_Test(void);

int main(void)
{ 
  LED_GPIO_Config(); // LED初始化
  LED_BLUE; // 蓝灯亮
  USART_Config(); // USART初始化
  printf("\r\n 这是一个I2C外设(AT24C02)读写测试例程 \r\n");

  // I2C 外设初(AT24C02)始化
  I2C_EE_Init();
  printf("\r\n 这是一个I2C外设(AT24C02)读写测试例程 \r\n");    

  //EEPROM 读写测试
  if(I2C_Test() == 1)
  {
      LED_GREEN; // 绿灯亮
  }
  else
  {
      LED_RED; // 错误,红灯亮
  }
  while (1)
  {      
  }
}

// I2C EEPROM读写测试函数
uint8_t I2C_Test(void)
{
    uint16_t i;
    printf("写入的数据\n\r");
    for(i=0; i <= 255; i++) //填充缓冲
    {   
        I2c_Buf_Write[i] = i; // 写入数据
        printf("0x%02X ", I2c_Buf_Write[i]); // 打印写入数据
        if(i % 16 == 15) // 每行打印16个数据   
            printf("\n\r");    
   }
  //将I2c_Buf_Write中顺序递增的数据写入EERPOM中 
  I2C_EE_BufferWrite(I2c_Buf_Write, EEP_Firstpage, 256); // 写入EEPROM函数参数:写入缓冲区,起始页地址,写入长度
  EEPROM_INFO("\n\r写成功\n\r"); // 宏定义,打印提示信息 
  EEPROM_INFO("\n\r读出的数据\n\r"); // 宏定义,打印提示信息 
  //将EEPROM读出数据顺序保持到I2c_Buf_Read中
  I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256); // 读取EEPROM函数参数:读取缓冲区,起始页地址,读取长度

  // 将I2c_Buf_Read中的数据通过串口打印
  for(i = 0; i < 256; i++)
  {    
      if(I2c_Buf_Read[i] != I2c_Buf_Write[i]) // 判断写入与读出的数据是否一致
      {
            EEPROM_ERROR("0x%02X ", I2c_Buf_Read[i]);
            EEPROM_ERROR("错误:I2C EEPROM写入与读出的数据不一致\n\r");
            return 0;
      }
    printf("0x%02X ", I2c_Buf_Read[i]); // 打印读出数据
    if(i % 16 == 15)    // 每行打印16个数据   
        printf("\n\r");
    }
  EEPROM_INFO("I2C(AT24C02)读写测试成功\n\r");
  return 1;
}

代码中先填充一个数组,数组的内容为1,2,3至N,接着把这个数组的内容写入到EEPROM中,写入时可以采用单字节写入的方式或页写入的方式。 写入完毕后再从EEPROM的地址中读取数据,把读取得到的与写入的数据进行校验,若一致说明读写正常,否则读写过程有问题或者EEPROM芯片不正常。 其中代码用到的EEPROM_INFO跟EEPROM_ERROR宏类似,都是对printf函数的封装,使用和阅读代码时把它直接当成printf函数就好。 具体的宏定义在“bsp_i2c_ee.h文件中”,在以后的代码我们常常会用类似的宏来输出调试信息。

3. 小结

啊,说了怎么多,本质还是一个配置的事,因为stm32标准库的强大,我们想要使用功能只用配置就好了,接下来的一个模拟I2C可能就没那么顺利了。根据传统,现在我们需要总结一下程序了:

  • 首先是要配置好串口和led了,目的是方便观察实验效果,串口和led我们都已经见过啦

  • 接下来就是I2C和外设的宏定义了,和以往不同的是,我们还宏定义了信息输出模板

/*信息输出*/
#define EEPROM_DEBUG_ON         0

#define EEPROM_INFO(fmt,arg...)           printf("<<-EEPROM-INFO->> "fmt"\n",##arg)
#define EEPROM_ERROR(fmt,arg...)          printf("<<-EEPROM-ERROR->> "fmt"\n",##arg)
#define EEPROM_DEBUG(fmt,arg...)          do{\
                                          if(EEPROM_DEBUG_ON)\
                                          printf("<<-EEPROM-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
                                          }while(0)
  • 正式开始配置:GPIO基础配置、I2C基础配置、I2C工作模式配置、外设(EEPROM)初始化

  • 配置里面当然包含外设有关函数:写入数据、写入字节、循环写入、取出数据(这些我们复制粘贴就好了)

  • 主函数测试:调用我们写好的函数,再写一个测试函数,外设正常绿灯亮、错误红灯亮

// I2C EEPROM读写测试函数
uint8_t I2C_Test(void)
{
    uint16_t i;
    printf("写入的数据\n\r");
    for ( i=0; i <= 255; i++) //填充缓冲
  {   
    I2c_Buf_Write[i] = i; // 写入数据
    printf("0x%02X ", I2c_Buf_Write[i]); // 打印写入数据
    if(i % 16 == 15) // 每行打印16个数据   
        printf("\n\r");    
   }

  //将I2c_Buf_Write中顺序递增的数据写入EERPOM中 
  I2C_EE_BufferWrite(I2c_Buf_Write, EEP_Firstpage, 256); // 写入EEPROM函数参数:写入缓冲区,起始页地址,写入长度
  EEPROM_INFO("\n\r写成功\n\r"); // 宏定义,打印提示信息 
  EEPROM_INFO("\n\r读出的数据\n\r"); // 宏定义,打印提示信息 
  //将EEPROM读出数据顺序保持到I2c_Buf_Read中
  I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256); // 读取EEPROM函数参数:读取缓冲区,起始页地址,读取长度

  // 将I2c_Buf_Read中的数据通过串口打印
    for(i = 0; i < 256; i++)
    {    
        if(I2c_Buf_Read[i] != I2c_Buf_Write[i]) // 判断写入与读出的数据是否一致
        {
            EEPROM_ERROR("0x%02X ", I2c_Buf_Read[i]);
            EEPROM_ERROR("错误:I2C EEPROM写入与读出的数据不一致\n\r");
            return 0;
        }
    printf("0x%02X ", I2c_Buf_Read[i]); // 打印读出数据
    if(i % 16 == 15)    // 每行打印16个数据   
        printf("\n\r");
    }
  EEPROM_INFO("I2C(AT24C02)读写测试成功\n\r");

  return 1;
}


2024.9.4 第一次修订,后期不再维护

posted @ 2024-09-04 10:28  hazy1k  阅读(6)  评论(0编辑  收藏  举报