【STM32】软件I2C的使用详解及附带代码

欢迎来到我的博客。今天我想向大家介绍一下STM32软件I2C功能。

首先,让我们来了解一下I2C(Inter-Integrated Circuit)总线。I2C是一种串行通信总线,最初由Philips公司开发。它允许多个设备使用同一条总线进行通信,并且每个设备都有唯一的地址。I2C通常用于连接微控制器、传感器和其他外设。

在STM32中,I2C总线被实现为硬件和软件两种方式。硬件I2C功能可以直接使用STM32芯片上的I2C外设,而软件I2C需要通过编程实现。由于某些应用场景不适宜使用硬件I2C功能,所以软件I2C在STM32中也变得非常重要。

STM32软件I2C功能与硬件I2C功能类似,它们之间的主要区别在于数据传输的过程。软件I2C需要使用GPIO口模拟I2C通信过程,因此实现起来相对复杂。但是软件I2C具有很高的灵活性,可以根据需要进行修改和扩展。

在STM32中,软件I2C驱动程序通常由以下几个部分组成:

初始化:这一步包括配置GPIO口、设置时序等操作,以确保I2C通信正常进行。

启动:启动信号是I2C总线上的一个信号,用于指示传输开始。为了在软件I2C中实现“启动”信号,我们需要将SDA(数据线)从高电平拉到低电平,然后将SCL(时钟线)从高电平拉到低电平。

停止:停止信号用于指示传输结束。在软件I2C中,我们需要将SCL从低电平拉到高电平,然后将SDA从低电平拉到高电平。

数据传输:数据传输通过向SDA写入位来完成。在传输数据之前,我们需要向SCL写入一个脉冲来获取ACK(应答)信号,以确保数据已被正确接收。

虽然软件I2C比硬件I2C更加复杂,但它具有很高的灵活性和可扩展性。此外,在某些情况下,软件I2C可以提供更好的性能和功耗优化。

下面上代码。根据野火例程修改而来,已验证。

bsp_i2c_gpio.c
/**
 ******************************************************************************
 * @file    bsp_i2c_ee.c
 * @version V1.0
 * @date    2023-4-12
 * @brief   用gpio模拟i2c总线, 适用于STM32系列CPU。该模块不包括应用层命令帧,仅包括I2C总线基本操作函数。
 ******************************************************************************
#include "bsp_i2c_gpio.h"
#include "stm32f4xx.h"
#include <stdio.h>

/*
*********************************************************************************************************
*	函 数 名: i2c_Delay
*	功能说明: I2C总线位延迟,最快400KHz
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
static void i2c_Delay(void)
{
    uint8_t i;

    /* 
        下面的时间是通过逻辑分析仪测试得到的。
    工作条件:CPU主频72MHz ,MDK编译环境,1级优化

        循环次数为10时,SCL频率 = 205KHz
        循环次数为7时,SCL频率 = 347KHz, SCL高电平时间1.5us,SCL低电平时间2.87us
        循环次数为5时,SCL频率 = 421KHz, SCL高电平时间1.25us,SCL低电平时间2.375us
    */
    for (i = 0; i < 10; i++)
        ;
}

/*
*********************************************************************************************************
*	函 数 名: i2c_Start
*	功能说明: CPU发起I2C总线启动信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_Start(void)
{
    /* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
    BSP_I2C_SDA_1();
    BSP_I2C_SCL_1();
    i2c_Delay();
    BSP_I2C_SDA_0();
    i2c_Delay();
    BSP_I2C_SCL_0();
    i2c_Delay();
}

/*
*********************************************************************************************************
*	函 数 名: i2c_Stop
*	功能说明: CPU发起I2C总线停止信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_Stop(void)
{
    /* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
    BSP_I2C_SDA_0();
    BSP_I2C_SCL_1();
    i2c_Delay();
    BSP_I2C_SDA_1();
}

/*
*********************************************************************************************************
*	函 数 名: i2c_SendByte
*	功能说明: CPU向I2C总线设备发送8bit数据
*	形    参:_ucByte : 等待发送的字节
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_SendByte(uint8_t _ucByte)
{
    uint8_t i;

    /* 先发送字节的高位bit7 */
    for (i = 0; i < 8; i++)
    {
        if (_ucByte & 0x80)
        {
            BSP_I2C_SDA_1();
        }
        else
        {
            BSP_I2C_SDA_0();
        }
        i2c_Delay();
        BSP_I2C_SCL_1();
        i2c_Delay();
        BSP_I2C_SCL_0();
        if (i == 7)
        {
            BSP_I2C_SDA_1(); // 释放总线
        }
        _ucByte <<= 1; /* 左移一个bit */
        i2c_Delay();
    }
}

/*
*********************************************************************************************************
*	函 数 名: i2c_ReadByte
*	功能说明: CPU从I2C总线设备读取8bit数据
*	形    参:无
*	返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t i2c_ReadByte(void)
{
    uint8_t i;
    uint8_t value;

    /* 读到第1个bit为数据的bit7 */
    value = 0;
    for (i = 0; i < 8; i++)
    {
        value <<= 1;
        BSP_I2C_SCL_1();
        i2c_Delay();
        if (BSP_I2C_SDA_READ())
        {
            value++;
        }
        BSP_I2C_SCL_0();
        i2c_Delay();
    }
    return value;
}

/*
*********************************************************************************************************
*	函 数 名: i2c_WaitAck
*	功能说明: CPU产生一个时钟,并读取器件的ACK应答信号
*	形    参:无
*	返 回 值: 返回0表示正确应答,1表示无器件响应
*********************************************************************************************************
*/
uint8_t i2c_WaitAck(void)
{
    uint8_t re;

    BSP_I2C_SDA_1(); /* CPU释放SDA总线 */
    i2c_Delay();
    BSP_I2C_SCL_1(); /* CPU驱动SCL = 1, 此时器件会返回ACK应答 */
    i2c_Delay();
    if (BSP_I2C_SDA_READ()) /* CPU读取SDA口线状态 */
    {
        re = 1;
    }
    else
    {
        re = 0;
    }
    BSP_I2C_SCL_0();
    i2c_Delay();
    return re;
}

/*
*********************************************************************************************************
*	函 数 名: i2c_Ack
*	功能说明: CPU产生一个ACK信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_Ack(void)
{
    BSP_I2C_SDA_0(); /* CPU驱动SDA = 0 */
    i2c_Delay();
    BSP_I2C_SCL_1(); /* CPU产生1个时钟 */
    i2c_Delay();
    BSP_I2C_SCL_0();
    i2c_Delay();
    BSP_I2C_SDA_1(); /* CPU释放SDA总线 */
}

/*
*********************************************************************************************************
*	函 数 名: i2c_NAck
*	功能说明: CPU产生1个NACK信号
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_NAck(void)
{
    BSP_I2C_SDA_1(); /* CPU驱动SDA = 1 */
    i2c_Delay();
    BSP_I2C_SCL_1(); /* CPU产生1个时钟 */
    i2c_Delay();
    BSP_I2C_SCL_0();
    i2c_Delay();
}

/*
*********************************************************************************************************
*	函 数 名: i2c_CfgGpio
*	功能说明: 配置I2C总线的GPIO,采用模拟IO的方式实现
*	形    参:无
*	返 回 值: 无
*********************************************************************************************************
*/
void i2c_CfgGpio(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    I2Cx_SCL_GPIO_CLK_ENABLE();
    I2Cx_SDA_GPIO_CLK_ENABLE();
    /**I2C2 GPIO Configuration
    PB10     ------> I2C2_SCL
    PB9     ------> I2C2_SDA
    */
    GPIO_InitStruct.Pin = BSP_I2C_SCL_PIN | BSP_I2C_SDA_PIN;
    ;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    HAL_GPIO_Init(BSP_GPIO_PORT_I2C, &GPIO_InitStruct);
    /* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
    i2c_Stop();
}

bsp_i2c_gpio.h
#ifndef _BSP_I2C_GPIO_H
#define _BSP_I2C_GPIO_H

#include <inttypes.h>

#define BSP_I2C_WR 0 /* 写控制bit */
#define BSP_I2C_RD 1 /* 读控制bit */

/* 定义I2C总线连接的GPIO端口时钟控制 */

#define I2Cx_SDA_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2Cx_SCL_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()

/* 定义I2C总线连接的GPIO端口, 用户只需要修改下面3行代码即可任意改变SCL和SDA的引脚 */
#define BSP_GPIO_PORT_I2C GPIOB    /* GPIO端口 */
#define BSP_I2C_SCL_PIN GPIO_PIN_8 /* 连接到SCL时钟线的GPIO */
#define BSP_I2C_SDA_PIN GPIO_PIN_9 /* 连接到SDA数据线的GPIO */

/* 定义读写SCL和SDA的宏,已增加代码的可移植性和可阅读性 */
#if 0                                                                /* 条件编译: 1 选择GPIO的库函数实现IO读写 */
#define BSP_I2C_SCL_1() digitalH(BSP_GPIO_PORT_I2C, BSP_I2C_SCL_PIN) /* SCL = 1 */
#define BSP_I2C_SCL_0() digitalL(BSP_GPIO_PORT_I2C, BSP_I2C_SCL_PIN) /* SCL = 0 */

#define BSP_I2C_SDA_1() digitalH(BSP_GPIO_PORT_I2C, BSP_I2C_SDA_PIN)         /* SDA = 1 */
#define BSP_I2C_SDA_0() digitalL(BSP_GPIO_PORT_I2C, BSP_I2C_SDA_PIN)         /* SDA = 0 */
	
	//#define BSP_I2C_SDA_READ()  GPIO_ReadInputDataBit(BSP_GPIO_PORT_I2C, BSP_I2C_SDA_PIN)	/* 读SDA口线状态 */
#define BSP_I2C_SDA_READ() ((BSP_GPIO_PORT_I2C->IDR & BSP_I2C_SDA_PIN) != 0) /* 读SDA口线状态 */

#else                                                                              /* 这个分支选择直接寄存器操作实现IO读写 */
/* 注意:如下写法,在IAR最高级别优化时,会被编译器错误优化 */
#define BSP_I2C_SCL_1() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SCL_PIN        /* SCL = 1 */
#define BSP_I2C_SCL_0() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SCL_PIN << 16U /* SCL = 0 */

#define BSP_I2C_SDA_1() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SDA_PIN        /* SDA = 1 */
#define BSP_I2C_SDA_0() BSP_GPIO_PORT_I2C->BSRR = (uint32_t)BSP_I2C_SDA_PIN << 16U /* SDA = 0 */

#define BSP_I2C_SDA_READ() ((BSP_GPIO_PORT_I2C->IDR & BSP_I2C_SDA_PIN) != 0) /* 读SDA口线状态 */
#endif

/* 直接操作寄存器的方法控制IO */
#define digitalH(p, i) \
    {                  \
        p->BSRR = i;   \
    } // 设置为高电平
#define digitalL(p, i)               \
    {                                \
        p->BSRR = (uint32_t)i << 16; \
    } // 输出低电平

void i2c_CfgGpio(void);
void i2c_Start(void);
void i2c_Stop(void);
void i2c_SendByte(uint8_t _ucByte);
uint8_t i2c_ReadByte(void);
uint8_t i2c_WaitAck(void);
void i2c_Ack(void);
void i2c_NAck(void);

#endif

最后,不要忘记在主程序中调用 i2c_CfgGpio(); 完成用于模拟I2C的GPIO初始化。

应用实例

这里我使用的是lis2dw12加速度传感器,在数据手册中给出了I2C通信时序如下。

I2C通讯时序图
Master:主机
Slave:从机
ST:起始信号 START signal
SAD:从机地址 Slave Address
SAK:从机应答 slave acknowledge
DATA :8位的数据内容
SP:停止信号 STOP signal
NMAK :非主机应答 No Master Acknowledge
SUB:8位的子地址 8-bit sub-address
W :读操作
R:写操作

软件模拟这个流程就能实现通讯,对照时序图和程序的每一个步骤阅读,方便理解。

使用前记得包含头文件

#include "bsp_i2c_gpio.h"
软件模拟读操作

I2C读操作时序图

/*
 * @brief  Read generic device register (platform dependent)
 *
 * @param  handle    customizable argument. In this examples is used in
 *                   order to select the correct sensor bus handler.
 * @param  reg       register to read
 * @param  bufp      pointer to buffer that store the data read
 * @param  len       number of consecutive register to read
 *
 */
static int32_t platform_read(void *handle, uint8_t reg, uint8_t *bufp,
                             uint16_t len)
{
  uint16_t i;
  /* 第1步:发起I2C总线启动信号 */
  i2c_Start();
  /* 第2步:发送控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
  i2c_SendByte(BSP_I2C_ADD | BSP_I2C_WR); /* 写指令 */
  /* 第3步:等待ACK */
  if (i2c_WaitAck() != 0)
  {
    goto cmd_fail;
  }
  /* 第4步: 发送SUB */
  i2c_SendByte(reg);
  /* 第5步: 等待ACK */
  if (i2c_WaitAck() != 0)
  {
    goto cmd_fail;
  }
  /* 第6步: 发送SR (repeated START) */
  i2c_Start();
  /* 第7步: 发送控制字节 */
  i2c_SendByte(LIS2DW12_I2C_ADD | LIS2DW12_I2C_RD); /* 读指令 */
                                                    /* 第8步: 发送ACK */
  if (i2c_WaitAck() != 0)
  {
    goto cmd_fail;
  }
  /* 第9步: 循环读取数据 */
  for (i = 0; i < len; i++)
  {
    bufp[i] = i2c_ReadByte(); /* 读1个字节 */

    /* 每读完1个字节后,需要主机发送ACK,最后一个字节发送NACK */
    if (i != len - 1)
    {
      i2c_Ack(); /* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
    }
    else
    {
      i2c_NAck(); /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
    }
  }
  /* 第10步:发送停止信号 */
  i2c_Stop();
  return 0;
cmd_fail:
  i2c_Stop();
  return 1;
}
软件模拟写操作

I2C写操作时序图

/*
 * @brief  Write generic device register (platform dependent)
 *
 * @param  handle    customizable argument. In this examples is used in
 *                   order to select the correct sensor bus handler.
 * @param  reg       register to write
 * @param  bufp      pointer to buffer that store the data read
 * @param  len       number of consecutive register to read
 *
 */
static int32_t platform_write(void *handle, uint8_t reg, const uint8_t *bufp,
                              uint16_t len)
{
  uint16_t i;
  /* 第0步: 发送停止信号 */
  i2c_Stop();
  /* 第1步: 发起I2C总线启动信号 */
  i2c_Start();
  /* 第2步: 发送控制字节 */
  i2c_SendByte(LIS2DW12_I2C_ADD | LIS2DW12_I2C_WR);
  /* 第3步: 等待ACK */
  if (i2c_WaitAck() != 0)
  {
    goto cmd_fail;
  }
  /* 第4步: 发送SUB */
  i2c_SendByte(reg);
  /* 第5步: 等待ACK */
  if (i2c_WaitAck() != 0)
  {
    goto cmd_fail;
  }
  /* 第6步: 循环发送DATA */
  for (i = 0; i < len; i++)
  {
    i2c_SendByte(bufp[i]); /* 发一个数据 */
    i2c_Ack();             /*发完一个数据后等待ACK*/
  }
  /* 第7步: 发送停止信号 */
  i2c_Stop();
  return 0;
cmd_fail:
  i2c_Stop();
  return 1;
}

总而言之,STM32软件I2C是一种非常重要的通信方式,尤其适用于那些不适合使用硬件I2C的应用场景。希望本文对你了解STM32软件I2C功能有所帮助,如果有不理解的地方欢迎私信留言。

物联沃分享整理
物联沃-IOTWORD物联网 » 【STM32】软件I2C的使用详解及附带代码

发表评论