STM32的I2C通讯配置(硬件实现)——学习笔记(6-2)

文章内容根据野火学习教程进行整理,仅仅是学习记录。

开发板: 野火STM32F429-挑战者V2
官方固件库版本: STM32F4xx_DSP_StdPeriph_Lib_V1.8.0

这里以与EEPROM进行通讯的代码作为例子。


一、看电路图

1、要得知那些信息

从电路图上主要是要知道3点关键信息

  • I2C设备挂载在I2C1、I2C2、I2C3的哪一个总线上
  • SDA、SCL接的是哪两个GPIO
  • I2C设备地址(有的是可以选择的,比如这个EEPROM)
    STM32的I2C通讯配置(硬件实现)——学习笔记(6-2)_第1张图片

2、挂在哪个I2C总线以及哪个GPIO

从电路图上可以看出:

  • 挂载在I2C1上的
  • SDA接的是PB7引脚
  • SCL接的是PB6引脚

这个一般硬件工程师都会在电路图上标出来吧。

另外如果只标出了使用的GPIO引脚,也可以通过 《STM32F4xx中文数据手册》 查找相关GPIO的复用得知所挂的I2C总线。
STM32的I2C通讯配置(硬件实现)——学习笔记(6-2)_第2张图片

3、I2C设备地址

查看EPPROM的参考手册可以得知设备地址是由1010 A2A1A0一共位组合而成,而从电路图可以得知A2=0、A1=0、A0=0,所以最终的I2C地址位101 0000(0x50)。
其实在I2C总线上只要设备地址是唯一的就可以了。
STM32的I2C通讯配置(硬件实现)——学习笔记(6-2)_第3张图片

二、开始编码

由于是使用I2C对EEPROM进行读写,所以编码主要是分为I2C配置和对EEPROM读写两个部分。

1、I2C配置

(1)宏定义声明

根据电路连接情况就可以知道GPIO引脚以及哪个I2C总线啦。

#define I2C1_OwnAddress                         0X0A                           /* STM32的I2C1设备自身地址,自定义,与其他I2C设备地址不同即可 */
#define I2C1_Speed                              400000                         /* I2C1设备速率400KHz */
#define I2C1_GPIO_PORT                          GPIOB                          /* I2C1所接GPIO的端口 */
#define I2C1_SCL_PIN                            GPIO_Pin_6                     /* I2C1的SCL所接的GPIO引脚 */
#define I2C1_SDA_PIN                            GPIO_Pin_7                     /* I2C1的SDA所接的GPIO引脚 */
#define I2C1_SCL_SOURCE                         GPIO_PinSource6                /* I2C1的SCL所接的GPIO引脚序号 */
#define I2C1_SDA_SOURCE                         GPIO_PinSource7                /* I2C1的SDA所接的GPIO引脚序号 */
#define I2C1_GPIO_AF                            GPIO_AF_I2C1                   /* I2C1的SDA、SCL的GPIO引脚复用功能 */
  • I2C速率:有三种,标准模式(100kbit/s)、快速模式(400kbit/s)、Hs模式(3.4Mbit/s)。我自己试了一下速率过低是会有问题的,过高似乎没有什么问题,一般就填400K好了。
(2)使能GPIO和I2C总线时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);                      /* 初始化GPIO端口时钟 */

RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);                       /* 初始化I2C1的外围时钟 */

不管是使用GPIO还是I2C,相应的总线时钟都是要使能的。

(3)初始化GPIO
/**************************************************************************************************
**  函数名称:  gpio_cfg
**  功能描述:  I2C的GPIO配置
**  输入参数:  无
**  输出参数:  无
**  返回参数:  无
**************************************************************************************************/
static void gpio_cfg(void)
{
    GPIO_InitTypeDef GPIO_def;
    
    GPIO_def.GPIO_Pin = I2C1_SCL_PIN | I2C1_SDA_PIN;                           /* 要配置的GPIO的PIN脚 */
    GPIO_def.GPIO_Mode = GPIO_Mode_AF;                                         /* 要配置的GPIO模式(复用) */
    GPIO_def.GPIO_Speed = GPIO_Speed_50MHz;                                    /* 要配置的GPIO速率(50MHz) */
    GPIO_def.GPIO_OType = GPIO_OType_OD;                                       /* 输出类型(开漏) */
    GPIO_def.GPIO_PuPd = GPIO_PuPd_UP;                                         /* 引脚默认状态(上拉) */
    
    GPIO_Init(I2C1_GPIO_PORT, &GPIO_def);
    
    GPIO_PinAFConfig(I2C1_GPIO_PORT, I2C1_SCL_SOURCE, I2C1_GPIO_AF);           /* 配置复用类型 */
    GPIO_PinAFConfig(I2C1_GPIO_PORT, I2C1_SDA_SOURCE, I2C1_GPIO_AF);           /* 配置复用类型 */
}
  • GPIO类型必须配置成 开漏输出 ,因为这样I2C设备才能输出 低电平高阻态。因为 SCLSDA 连接上拉电阻到电源,那么GPIO输出低电平则拉低,输出高阻态则拉高。高阻态可类似看为开路。
(4)初始化I2C
/**************************************************************************************************
**  函数名称:  i2c_cfg
**  功能描述:  I2C配置
**  输入参数:  无
**  输出参数:  无
**  返回参数:  无
**************************************************************************************************/
static void i2c_cfg(void)
{
  I2C_InitTypeDef  I2C_def;
  
  I2C_def.I2C_Mode = I2C_Mode_I2C;                                             /* I2C模式选择 */
  I2C_def.I2C_DutyCycle = I2C_DutyCycle_2;                                     /* 低电平时间:高电平时间 = 2:1 */
  I2C_def.I2C_OwnAddress1 = I2C1_OwnAddress;                                   /* I2C设备地址 */
  I2C_def.I2C_Ack = I2C_Ack_Enable;                                            /* 使能应答 */
  I2C_def.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;              /* I2C的寻址模式 */
  I2C_def.I2C_ClockSpeed = I2C1_Speed;                                         /* 通信速率 */
  I2C_Init(I2C1, &I2C_def);                                                    /* I2C初始化 */
  
  I2C_Cmd(I2C1, ENABLE);                                                       /* 使能I2C */
  I2C_AcknowledgeConfig(I2C1, ENABLE);                                         /* 使能指定I2C总线的应答功能 */
}
  • SCL占空比:当I2C设置为快速模式时,可以配置SCL的占空比,有两个选项。一:I2C_DutyCycle_2(低电平:高电平=2:1),二:I2C_DutyCycle_16_9(低电平:高电平=16:9)。其实配哪个好像没啥区别。
  • I2C设备自身地址位数:有两种选择,可以选7位的也可以选10位的。其实都可以用,不过一般情况下是配置7个位的。
  • I2C设备自身地址:只要与I2C总线上其他设备的地址不同即可。前7位或10位有效,根据配置。

2、对EEPROM进行读写

(1)宏定义声明
#define EE_I2C                                  I2C1                           /* 所挂载的I2C总线 */

#define EE_I2C_ADDR                             0XA0                           /* EEPROM的I2C设备地址 1010000(0x50), (0x50 << 1)*/

#define EE_SHORT_TIMEOUT                        0x1000                         /* I2C检测短等待超时时间 */
#define EE_LONG_TIMEOUT                         0xA000                         /* I2C检测长等待超时时间 */
#define EE_WAIT_TIMES                           300                            /* I2C检测设备状态最大次数 */

#define TEST_ADDR                               0x30                           /* 要写入数据的EEPROM地址 */
#define TEST_DATA                               0x55                           /* 要写入的数据 */
  • 设备地址:由上面分析的结果可以的得知设备地址为101 0000(0x50),那么由于最后一位为读写位,所以需要左移1位,最终的到1010 0000(0XA0)。
(2)检测I2C事件的封装函数

在I2C通讯中根据协议会需要检测EV5、EV6、EV7、EV8、EV9等事件。
我自己把检测I2C事件函数封装了一下,加入了超时跳出的机制,方便调用。

/**************************************************************************************************
**  函数名称:  i2c_check_event
**  功能描述:  检测I2C设备的事件
**  输入参数:  无
**  输出参数:  无
**  返回参数:  检测成功返回0,失败返回-1
**************************************************************************************************/
static s32 i2c_check_event(u32 i2c_event)
{
    __IO u32 timeout;

    timeout = EE_SHORT_TIMEOUT;
    while(SUCCESS != I2C_CheckEvent(EE_I2C, i2c_event)) {
        timeout--;
        if (timeout == 0) {
            return -1;
        }
    }
    
    return 0;
}
(3)检测I2C寄存器标志位封装函数

在I2C通讯中会有需要检测寄存器标志位状态的时候。
我把检测标志位的函数封装了一下,加入了期望值匹配状态以及超时机制,也是为了方便使用。

/**************************************************************************************************
**  函数名称:  i2c_check_flag
**  功能描述:  检测I2C设备的标志
**  输入参数:  i2c_flag:要检测的标志
**           expect:期望的状态,RESET或者SET
**  输出参数:  无
**  返回参数:  匹配返回0,不匹配返回-1
**************************************************************************************************/
static s32 i2c_check_flag(u32 i2c_flag, FlagStatus expect)
{
    __IO u32 timeout;
    
    timeout = EE_LONG_TIMEOUT;
    while(I2C_GetFlagStatus(EE_I2C, i2c_flag) != expect) {
        timeout--;
        if (timeout == 0) {
            return -1;
        }
    }
    
    return 0;
}

(4)检测I2C设备是否处于待命状态的封装函数

在I2C通讯过程当中,如果从设备内部正在紧张地处理着一些事情,比如果读写啥的是没有空理会I2C总线上主机设备发送的命令的。所以要保证I2C从设备能够正常的接收到命令有时候就需要去探寻一下从设备是不是有空了。
我把这个探寻机制封装了一下。
大致流程如下:

  • 向I2C发送一个要进行通讯的从设备地址。
  • 检测I2C的 SR1 寄存器的 地址位ADDR 是否被置1,如果是说明要通讯的地址可以用了,那么清除 地址位ADDR 然后可以做后续的通讯。(清除 地址位ADDR 通过先读取SR1再读取SR2来实现)
  • 检测I2C的 SR1 寄存器的 应答位AF 是否被置1,如果是说明应答失败了,那么就将 应答位AF 清零然后继续发起请求。
  • 一直循环发起请求直到I2C的 SR1 寄存器的 地址位ADDR 是否被置1,或者超时跳出。
/**************************************************************************************************
**  函数名称:  i2c_wait_standbystate
**  功能描述:  检测指定的I2C设备处于待命状态
**  输入参数:  addr:要检测的I2C设备的地址
**  输出参数:  无
**  返回参数:  等待成功返回0,失败返回-1
**************************************************************************************************/
static s32 i2c_wait_standbystate(u8 addr)
{
    __IO u16 tmpSR1;                                                           /* SR1寄存器的值 */
    __IO u32 timeout;                                                          /* 超时计数器 */
    __IO u32 trials;                                                           /* 等待次数计数器 */

    timeout = 0;
    
    if (0 != i2c_check_flag(I2C_FLAG_BUSY, RESET)) {
        printf("%s, i2c is busy !\r\n", __FUNCTION__);
        return -1;
    }

    while ((timeout++) < EE_LONG_TIMEOUT ) {
        I2C_GenerateSTART(EE_I2C, ENABLE);                                     /* 发送开始命令 */
        if (0 != i2c_check_event(I2C_EVENT_MASTER_MODE_SELECT)) {              /* EV5 */
            printf("%s, i2c start error !\r\n", __FUNCTION__);
            return -1;
        }
        
        I2C_Send7bitAddress(EE_I2C, addr, I2C_Direction_Transmitter);          /* 配置I2C从设备的地址 */
        
        trials = 0;
        do {
            trials++;
            tmpSR1 = I2C_ReadRegister(EE_I2C, I2C_Register_SR1);               /* 获取当前SR1寄存器的值 */
        } while (((tmpSR1 & (I2C_SR1_ADDR | I2C_SR1_AF)) == 0) && (trials < EE_WAIT_TIMES));
        
        if (tmpSR1 & I2C_SR1_ADDR) {                                           /* 检查ADDR标志位是否被设置 */
            I2C_ReadRegister(EE_I2C, I2C_Register_SR2);                        /* 清除ADDR标志,先读SR1再读SR2可以清除ADDR标志 */
            I2C_GenerateSTOP(EE_I2C, ENABLE);
            return 0;
        }
        
        if (tmpSR1 & I2C_SR1_AF) {                                             /* 检查AF标志位是否被设置 */
            I2C_ClearFlag(EE_I2C, I2C_FLAG_AF);                                /* 清除AF标志位 */
        }
    }
    
    return -1;
}
(5)向EEPROM写入一个字节

大致流程如下:

  • 检测I2C总线是否可用
  • 发送起始位
  • 发送要通讯的I2C从设备地址,以及通讯方向为写入
  • 发送要进行操作的EEPROM的地址
  • 发送要写入的数据
  • 发送停止位
/**************************************************************************************************
**  函数名称:  i2c_ee_writebyte
**  功能描述:  写一个字节到I2C的EEPROM中
**  输入参数:  dev_addr:I2C设备地址
**           addr:要写入数据的地址
**           data:要写入的数据
**  输出参数:  无
**  返回参数:    写入成功返回0,失败返回-1
**************************************************************************************************/
s32 i2c_ee_writebyte(u8 dev_addr, u8 addr, u8 data)
{
    if (0 != i2c_check_flag(I2C_FLAG_BUSY, RESET)) {
        printf("%s, i2c is busy !\r\n", __FUNCTION__);
        return -1;
    }

    I2C_GenerateSTART(EE_I2C, ENABLE);                                         /* 发送开始命令 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_MODE_SELECT)) {                  /* EV5 */
        printf("%s, i2c start error !\r\n", __FUNCTION__);
        return -1;
    }

    I2C_Send7bitAddress(EE_I2C, dev_addr, I2C_Direction_Transmitter);       /* 配置EEPROM设备的地址,写方向 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {    /* EV6 */
        printf("%s, i2c send 7bit address error, write !\r\n", __FUNCTION__);
        return -1;
    }
    
    I2C_SendData(EE_I2C, addr);                                                /* 发送要写入的地址 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {             /* EV8 */
        printf("%s, i2c send a byte error !\r\n", __FUNCTION__);
        return -1;
    }

    I2C_SendData(EE_I2C, data);                                                /* 发送要写入的数据 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {             /* EV8 */
        printf("%s, i2c send a byte error !\r\n", __FUNCTION__);
        return -1;
    }
    
    I2C_GenerateSTOP(EE_I2C, ENABLE);                                          /* 发送停止位 */

    return 0;
}

(6)向EEPROM写入一个字节

大致流程如下:

  • 检测I2C总线是否可用
  • 发送起始位
  • 发送要通讯的I2C从设备地址,以及通讯方向为写入
  • 发送要进行操作的EEPROM的地址
  • 重新发起起始位
  • 发送要通讯的I2C从设备地址,以及通讯方向为读取
  • 读取之前设置的要进行操作的EEPROM的地址的数据
  • 发送停止位
/**************************************************************************************************
**  函数名称:  i2c_ee_readbyte
**  功能描述:  读一个字节
**  输入参数:  dev_addr:I2C设备地址
**           addr:要读取数据的地址
**  输出参数:  无
**  返回参数:  读取到的字节,默认为0
**************************************************************************************************/
u8 i2c_ee_readbyte(u8 dev_addr, u8 addr)
{
    u8 value;

    if (0 != i2c_check_flag(I2C_FLAG_BUSY, RESET)) {
        printf("%s, i2c is busy !\r\n", __FUNCTION__);
        return 0;
    }

    I2C_GenerateSTART(EE_I2C, ENABLE);                                         /* 发送开始命令 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_MODE_SELECT)) {                  /* EV5 */
        printf("%s, i2c start error !\r\n", __FUNCTION__);
        return 0;
    }

    I2C_Send7bitAddress(EE_I2C, dev_addr, I2C_Direction_Transmitter);          /* 配置EEPROM设备的地址,写方向 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) {    /* EV6 */
        printf("%s, i2c send 7bit address error, write !\r\n", __FUNCTION__);
        return 0;
    }

    I2C_SendData(EE_I2C, addr);                                                /* 发送要读取的地址 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_BYTE_TRANSMITTED)) {             /* EV8 */
        printf("%s, i2c send a byte error !\r\n", __FUNCTION__);
        return 0;
    }

    I2C_GenerateSTART(EE_I2C, ENABLE);                                         /* 发送开始命令 */
    if (0 != i2c_check_event(I2C_EVENT_MASTER_MODE_SELECT)) {                  /* EV5 */
        printf("%s, i2c start error !\r\n", __FUNCTION__);
        return 0;
    }

    I2C_Send7bitAddress(EE_I2C, dev_addr, I2C_Direction_Receiver);
     if (0 != i2c_check_event(I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) {      /* EV6 */
        printf("%s, i2c send 7bit address error, read !\r\n", __FUNCTION__);
        return 0;
    }   

    if (0 != i2c_check_event(I2C_EVENT_MASTER_BYTE_RECEIVED)) {                /* EV7 */
        printf("%s, i2c read data error !\r\n", __FUNCTION__);
        return 0;
    }
    
    value = I2C_ReceiveData(EE_I2C);                                           /* 读取数据 */

    I2C_AcknowledgeConfig(EE_I2C, DISABLE);                                    /* 非应答 */

    I2C_GenerateSTOP(EE_I2C, ENABLE);                                          /* 发送停止位 */

    return value;
}
(7)测试函数调用

大致流程如下:

  • 向EEPROM写入的一个指定地址写入数据。
  • 等待EEPROM内部数据写入完成。
  • 读取EEPROM的指定地址的数据。
/**************************************************************************************************
**  函数名称:  bsp_eeprom_init
**  功能描述:  EEPROM初始化
**  输入参数:  无
**  输出参数:  无
**  返回参数:  无
**************************************************************************************************/
void bsp_eeprom_init(void)
{
    u32 value;
    
    i2c_ee_writebyte(EE_I2C_ADDR, TEST_ADDR, TEST_DATA);
    
    i2c_wait_standbystate(EE_I2C_ADDR);
    
    value = i2c_ee_readbyte(EE_I2C_ADDR, TEST_ADDR);
    
    printf("value = %x\r\n", value);
}

总结

1、这里只实现了简单的对EEPRON的单字节的读写,学习了一下I2C的流程,EEPROM还有可以一个page一个page读写的就先不去研究了。把每一行代码研究透,然后对各种bug修修改改也是挺磨人的。
2、写的I2C通讯流程使用的一般通讯结构,读的I2C通过流程使用的是复合通讯结构,具体的协议说明可以看一下介绍基础知识的文章 STM32的I2C基础知识——学习笔记(6-1)

你可能感兴趣的:(STM32)