STM32及GD32 - 可移植软件模拟I2C驱动实现(支持时钟延展,400KHz快速模式,宏接口注册驱动)

一、需知

  • 本文不赘述I2C通信的协议栈和原理,默认阅读本文的读者已经知晓并会使用I2C通信
  • 本文的驱动以MCU为主机,且总线上只具有一个主机的场景进行实现

二、背景

  • 分配管脚时并不一定总能使用硬件I2C外设,对于引脚资源较紧张的情况,通过软件上控制GPIO模拟I2C通信是比较成熟且常见的解决方法。
  • 不过需要认识到的是,软件模拟I2C是不支持中断的,通信过程只能以阻塞的方式进行,同时如果业务代码内中断使用频繁,且中断占用率高,那么I2C时序被拉长是很常见的事。
  • 假设有一个20KHz即50us周期的定时器中断,每一次执行其中断函数需耗时20us,那么观察I2C通信波形就会频繁看到被拉长的时钟线,拉长的时间也差不多为中断函数的执行时间。
  • 此外,有些支持I2C通信的器件会在每一次主机发送或读取完一个字节、发送停止信号终止通信时,钳住时钟线,也即时钟延展(Clock Stretch),常见芯片如Type-C充电芯片或者TI的电芯控制芯片。
  • 硬件外设本身支持时钟延展,使能方式各有异(如TI的MSPM0芯片需要调用一个接口进行时钟延展使能),但是对于软件模拟I2C而言,要实现时钟延展只能调整时序。
  • 模拟I2C网络上可以参考的例子非常多,但大部分不支持时钟延展,且通信时序可能并不严格。
  • 因此本文基于NXP的协议标准,通过宏接口注册驱动,而不是移植后需频繁修改引脚定义的方式,实现了支持400KHz快速模式、同时支持时钟延展的在GD32F30X平台以及STM32F10X平台均可直接移植使用的软件模拟I2C驱动。

三、代码实现

3.1 延时函数

  • 对于模拟I2C驱动而言,实现其时序最基础的就是延时函数。
  • 直接通过软件延时的方式,是不一定精确的,本文只在GD32F303RET6 / GD32F305ZET6,STM32F103C8T6平台上进行过测试验证,以下延时函数可以达到目的。
  • 如果移植过后时序不对,可能需要手动调整延时函数,使其满足400KHz速率。
#if defined(SOFT_I2C_GD32F3_USED)
#define SOFT_I2C_DELAY_CYCLE     (10U)                  ///< 延时周期数
#elif defined(SOFT_I2C_STM32F1_USED)
#define SOFT_I2C_DELAY_CYCLE     (1U)                   ///< 延时周期数
#else
#define SOFT_I2C_DELAY_CYCLE     (1U)                   ///< 延时周期数
#endif

/**
 * @brief 内部调用 微秒延时函数
 */
static void soft_i2c_delay_us(void) {
    uint32_t i, j = 0;
    for (i = 0; i < 2; i++) {
        for (j = 0; j < SOFT_I2C_DELAY_CYCLE; j++) {
            __asm("NOP");
        }
    }
}

/**
 * @brief 内部调用 800纳秒延时函数
 */
static void soft_i2c_delay_800ns(void) {
    uint32_t i = 0;
#if defined(SOFT_I2C_GD32F3_USED)
    uint32_t j = 0;
    for (i = 0; i < 2; i++) {
        for (j = 0; j < (SOFT_I2C_DELAY_CYCLE * 4) / 5; j++) {
            __asm("NOP");
        }
    }
#elif defined(SOFT_I2C_STM32F1_USED)
    for (i = 0; i < 2; i++) {
        __asm("NOP");
    }
#else
    return;
#endif
}

3.2 时钟延展

  • 一般的软件模拟I2C默认只有主机具有控制SCL时钟线的能力,因此在具有时钟延展的场合,如发送一个字节后等待ACK、等待接收一个字节、发送停止信号时,在程序执行完拉起SCL的语句后便直接去执行读写SDA数据线的操作。
  • 然而实际上支持时钟延展的从机会钳住时钟线,当主机执行完拉高SCL的操作后,总线上SCL的电平可能仍为低。如果原来的程序设计中缺少考虑这种情况,那么很显然,执行拉起SCL后程序会认为此时SDA线上的所有数据都是有效的,这就会导致ACK接收错误、数据接收错误、或者停止信号的错误发出。
  • 因此,实现时钟延展的原理非常简单,就是在需要判断时钟延展的场合,主机第一次拉起SCL时钟线时,需要先回读此SCL时钟线的状态(开漏输出可以读取电平),当读到电平为高,才能认为时钟线被成功拉起,从机器件释放了时钟线,此时对SDA数据线进行操作才是有意义的。
  • 此处以发送停止信号为例,插入实现时钟延展的代码块
/**
 * @brief 软件模拟 I2C 等待 SCL 释放
 *
 * @param [in]  p_i2c          I2C结构体指针
 * @param [in]  waitCnt        等待次数, 必须在前文声明
 *
 * @details 用于开启时钟延展功能, 等待从机释放时钟线
 */
#define SOFT_I2C_WAIT_SCL_RELEASE(p_i2c, waitCnt)                       \
        while (waitCnt > 0 &&                                           \
               soft_i2c_read_gpio(&p_i2c->scl) == SOFT_I2C_LEVEL_LOW) { \
            waitCnt--;                                                  \
        }

/**
 * @brief 内部调用 I2C停止信号
 * 
 * @param [in] p_i2c         I2C结构体指针
 * 
 * @return soft_i2c_err_t 
 *  @retval 0 成功, 其他值 失败 @ref SOFT_I2C_ERR_CODE
 * @details 特殊说明: 
 * @par eg:
 * @code
 *    
 * @endcode
 */
static soft_i2c_err_t soft_i2c_stop(P_SOFT_I2C_T p_i2c) {
    soft_i2c_write_gpio(&p_i2c->scl, SOFT_I2C_LEVEL_LOW);
    soft_i2c_write_gpio(&p_i2c->sda, SOFT_I2C_LEVEL_LOW);
    soft_i2c_delay_us();
    soft_i2c_write_gpio(&p_i2c->scl, SOFT_I2C_LEVEL_HIGH);
#if defined(__SOFT_I2C_CLOCK_STRECH_EN__)
    uint32_t waitCnt = 0xFFF;   ///< 等待计数
    SOFT_I2C_WAIT_SCL_RELEASE(p_i2c, waitCnt); // 等待SCL释放
    if (waitCnt == 0) {
        return SOFT_I2C_ERR_TIMEOUT;
    }
#endif
    soft_i2c_delay_us();
    soft_i2c_write_gpio(&p_i2c->sda, SOFT_I2C_LEVEL_HIGH);
    return SOFT_I2C_ERR_OK;
}

3.3 枚举及结构体定义

  • 将I2C驱动总线抽象为一个结构体,这个结构体只需要包含SCL时钟线、SDA数据线,以及一些监控变量
/**
 * @brief 软件模拟 I2C GPIO时钟枚举
 */
typedef enum _SOFT_I2C_GPIO_CLK_E {
    SOFT_I2C_GPIOA_CLK = 0x00000004U,   ///< GPIOA_CLK
    SOFT_I2C_GPIOB_CLK = 0x00000008U,   ///< GPIOB_CLK
    SOFT_I2C_GPIOC_CLK = 0x00000010U,   ///< GPIOC_CLK
    SOFT_I2C_GPIOD_CLK = 0x00000020U,   ///< GPIOD_CLK
    SOFT_I2C_GPIOE_CLK = 0x00000040U,   ///< GPIOE_CLK
    SOFT_I2C_GPIOF_CLK = 0x00000080U,   ///< GPIOF_CLK
    SOFT_I2C_GPIOG_CLK = 0x00000100U,   ///< GPIOG_CLK
} SOFT_I2C_GPIO_CLK_E;

/**
 * @brief 软件模拟 I2C 监控信息枚举
 */
typedef enum _SOFT_I2C_STA_E {
    SOFT_I2C_IDLE        = 0,   ///< 空闲
    SOFT_I2C_BUSY        = 1,   ///< 忙
    SOFT_I2C_WRITE_START = 2,   ///< 写开始
    SOFT_I2C_WRITE_END   = 3,   ///< 写结束
    SOFT_I2C_READ_START  = 4,   ///< 读开始
    SOFT_I2C_READ_END    = 5,   ///< 读结束
} SOFT_I2C_STA_E;

/**
 * @brief 软件模拟 I2C GPIO通用结构体
 */
typedef struct _SOFT_I2C_GPIO_COMM_T {
    uint32_t            gpioPort;  ///< GPIO端口, GD: GPIOx, STM32: GPIOx_BASE
    uint32_t            gpioPin;   ///< GPIO引脚, GD: GPIO_PIN_x, STM32: GPIO_Pin_x
    SOFT_I2C_GPIO_CLK_E gpioClk;   ///< GPIO时钟 @ref SOFT_I2C_GPIO_CLK_E
} SOFT_I2C_GPIO_COMM_T, *P_SOFT_I2C_GPIO_COMM_T;

/**
 * @brief 软件模拟 I2C 结构体
 */
typedef struct _SOFT_I2C_T {
    bool                 isValid; ///< 芯片是否有效
    bool                 isInit;  ///< 是否初始化
    SOFT_I2C_GPIO_COMM_T scl;     ///< 时钟线
    SOFT_I2C_GPIO_COMM_T sda;     ///< 数据线
    SOFT_I2C_STA_E       i2cSta;  ///< I2C监控状态
} SOFT_I2C_T, *P_SOFT_I2C_T;

3.4 对外接口

  • 使用类似注册的方式,定义I2C结构体,可以在一个统一的驱动源文件内使用,并在其头文件中声明,外部文件只需包含该头文件,即可操作I2C
  • 对外暴露的接口为总线初始化、写数据和读数据,并提供一个内联函数获取I2C总线状态。可以通过函数接口返回值及I2C总线状态判断当前通信异常发生的位置
#if defined(SOFT_I2C_GD32F3_USED) || defined(SOFT_I2C_STM32F1_USED)
/**
 * @brief 软件模拟 I2C 外部声明
 *
 * @param [in]  name        I2C结构体名称
 */
#define SOFT_I2C_EXT(name)  \
        extern void *const name
#else
#define SOFT_I2C_EXT(name)
#endif

#if defined(SOFT_I2C_GD32F3_USED)
/**
 * @brief 软件模拟 I2C 定义结构体
 *
 * @param [in]  name        I2C结构体名称
 * @param [in]  scl_port    时钟线端口, A ~ G
 * @param [in]  scl_pin     时钟线引脚, 0 ~ 15
 * @param [in]  sda_port    数据线端口, A ~ G
 * @param [in]  sda_pin     数据线引脚, 0 ~ 15
 */
#define SOFT_I2C_DEF(name, scl_port, scl_pin, sda_port, sda_pin)                 \
        SOFT_I2C_T soft_i2c_##name = {                                           \
            false, false,                                                        \
            {GPIO##scl_port, GPIO_PIN_##scl_pin, SOFT_I2C_GPIO##scl_port##_CLK}, \
            {GPIO##sda_port, GPIO_PIN_##sda_pin, SOFT_I2C_GPIO##sda_port##_CLK}, \
            SOFT_I2C_IDLE                                                        \
        };                                                                       \
        void *const name = &soft_i2c_##name                                      \

#elif defined(SOFT_I2C_STM32F1_USED)
/**
 * @brief 软件模拟 I2C 定义结构体
 *
 * @param [in]  name        I2C结构体名称
 * @param [in]  scl_port    时钟线端口, A ~ G
 * @param [in]  scl_pin     时钟线引脚, 0 ~ 15
 * @param [in]  sda_port    数据线端口, A ~ G
 * @param [in]  sda_pin     数据线引脚, 0 ~ 15
 */
#define SOFT_I2C_DEF(name, scl_port, scl_pin, sda_port, sda_pin)                        \
        SOFT_I2C_T soft_i2c_##name = {                                                  \
            false, false,                                                               \
            {GPIO##scl_port##_BASE, GPIO_Pin_##scl_pin, SOFT_I2C_GPIO##scl_port##_CLK}, \
            {GPIO##sda_port##_BASE, GPIO_Pin_##sda_pin, SOFT_I2C_GPIO##sda_port##_CLK}, \
            SOFT_I2C_IDLE                                                               \
        };                                                                              \
        void *const name = &soft_i2c_##name                                             \

#else
#define SOFT_I2C_DEF(name, scl_port, scl_pin, sda_port, sda_pin)
#endif

/**
 * @brief 软件模拟 I2C 初始化
 * 
 * @param [in] p_i2c         I2C结构体指针
 * 
 * @return uint32_t
 *  @retval 0 成功, 其他值 失败 @ref SOFT_I2C_ERR_CODE
 * @details 特殊说明: 
 * @par eg:
 * @code
 *    
 * @endcode
 */
extern soft_i2c_err_t soft_i2c_init(P_SOFT_I2C_T p_i2c);

/**
 * @brief 软件模拟 I2C 写数据
 * 
 * @param [in] p_i2c         I2C结构体指针
 * @param [in] slaveAddr     从机地址
 * @param [in] regAddr       寄存器地址
 * @param [in] regAddrLen    寄存器地址长度 @ref SOFT_I2C_REG_ADDR_LEN_1 , @ref SOFT_I2C_REG_ADDR_LEN_2
 * @param [in] p_data        数据指针
 * @param [in] dataLen       数据长度(字节)
 * 
 * @return soft_i2c_err_t 
 *  @retval 0 成功, 其他值 失败 @ref SOFT_I2C_ERR_CODE
 * @details 特殊说明: 
 * @par eg:
 * @code
 *    
 * @endcode
 */
extern soft_i2c_err_t soft_i2c_write(P_SOFT_I2C_T p_i2c, uint32_t slaveAddr, uint32_t regAddr, uint32_t regAddrLen, uint8_t *p_data, uint32_t dataLen);

/**
 * @brief 软件模拟 I2C 读数据
 * 
 * @param [in] p_i2c         I2C结构体指针
 * @param [in] slaveAddr     从机地址
 * @param [in] regAddr       寄存器地址
 * @param [in] regAddrLen    寄存器地址长度 @ref SOFT_I2C_REG_ADDR_LEN_1 , @ref SOFT_I2C_REG_ADDR_LEN_2
 * @param [in] p_data        数据指针
 * @param [in] dataLen       数据长度(字节)
 * 
 * @return soft_i2c_err_t 
 *  @retval 0 成功, 其他值 失败 @ref SOFT_I2C_ERR_CODE
 * @details 特殊说明: 
 * @par eg:
 * @code
 *    
 * @endcode
 */
extern soft_i2c_err_t soft_i2c_read(P_SOFT_I2C_T p_i2c, uint32_t slaveAddr, uint32_t regAddr, uint32_t regAddrLen, uint8_t *p_data, uint32_t dataLen);

四、使用示例

  • 参照5.2节获取已移植I2C驱动的示例工程

  • 使能时钟延展功能需在 bsp_soft_i2c.h 文件内取消注释__SOFT_I2C_CLOCK_STRECH_EN__
    时钟延展使能

  • 或者在工程的全局宏中添加__SOFT_I2C_CLOCK_STRECH_EN__
    在这里插入图片描述

4.1 GD32F303RET6核心板

4.1.1 移植

  • 添加文件
    GD移植
  • 驱动注册
    驱动注册
  • 驱动声明
    驱动声明

4.1.2 使用

#include "gd32f30x.h"
#include "gd32f303e_eval.h"
#include "systick.h"
// 引入软件模拟I2C头文件
#include "soft_i2c_dev.h"
#include <string.h>

#define SLAVE_ADDR  0xA0    ///< 从机地址
#define REG_ADDR    0x00    ///< 寄存器地址

uint8_t w_buf[8] = {0xAA,0xA5,0x5A,0xFF,0xFA,0xAF,0xDD,0xEE};
uint8_t r_buf[8] = {0};
uint8_t flag = 1;
soft_i2c_err_t ret = SOFT_I2C_ERR_OK;
bool equal = false;

int main(void)
{  
    gd_eval_led_init(LED);
    systick_config();

    // 初始化结构体对象
    ret = soft_i2c_init(I2C_DEV);
    
    while(1){
        /* turn on LED1 */
        gd_eval_led_on(LED);
        /* insert 200 ms delay */
        delay_1ms(200);
        memset(r_buf, 0, 8);
        if (flag) {
            ret = soft_i2c_write(I2C_DEV, SLAVE_ADDR, REG_ADDR, 1, w_buf, 8);
            // if (ret != SOFT_I2C_ERR_OK) {
            //     while(1);
            // }
            equal = false;
        } else {
            ret = soft_i2c_read(I2C_DEV, SLAVE_ADDR, REG_ADDR, 1, r_buf, 8);
            // if (ret != SOFT_I2C_ERR_OK) {
            //     while(1);
            // }
            equal = (memcmp(r_buf, w_buf, 8) == 0) ? true : false;
        }
        flag = !flag;
        /* turn off LEDs */
        gd_eval_led_off(LED);
        /* insert 200 ms delay */
        delay_1ms(200);
    }
}

4.1.3 资源占用

  • 以O0优化为例,占用大小1KB左右
    GD占用

4.1.4 通信波形

  • 写数据(以O0优化为例)
    GD写数据
  • 读数据(以O0优化为例)
    GD读取数据

4.2 STM32F103C8T6核心板

  • 移植和驱动注册声明省略,与GD平台一致

4.2.1 使用

#include "stm32f10x.h"
#include "stm32_eval.h"
#include "soft_i2c_dev.h"
#include <stdio.h>
#include <string.h>

#define SLAVE_ADDR  0xA0    ///< 从机地址
#define REG_ADDR    0x00    ///< 寄存器地址
uint8_t w_buf[8] = {0xAA,0xA5,0x5A,0xFF,0xFA,0xAF,0xDD,0xEE};
uint8_t r_buf[8] = {0};
uint8_t flag = 1;
bool equal = false;
soft_i2c_err_t ret = SOFT_I2C_ERR_OK;

int main(void)
{
  STM_EVAL_LEDInit(LED1);   // PC13
  STM_EVAL_LEDOn(LED1);
  ret = soft_i2c_init(I2C_DEV);
  while (1)
  {
      STM_EVAL_LEDOn(LED1);
      block_delay();
      {
        memset(r_buf, 0, 8);
        if (flag) {
            ret = soft_i2c_write(I2C_DEV, SLAVE_ADDR, REG_ADDR, 1, w_buf, 8);
            // if (ret != SOFT_I2C_ERR_OK) {
            //     while(1);
            // }
            equal = false;
        } else {
            ret = soft_i2c_read(I2C_DEV, SLAVE_ADDR, REG_ADDR, 1, r_buf, 8);
            // if (ret != SOFT_I2C_ERR_OK) {
            //     while(1);
            // }
            equal = (memcmp(r_buf, w_buf, 8) == 0) ? true : false;
        }
        flag = !flag;
      }
      STM_EVAL_LEDOff(LED1);
      block_delay();
  }
}

4.2.2 资源占用

  • 以O0优化为例,占用大小1KB左右
    STM 占用

4.2.3 通信波形

  • 写数据(以O0优化为例)
    STM写数据
  • 读数据(以O0优化为例)
    STM读数据

4.3 时钟延展波形

时钟延展

五、驱动获取方式

  • 最新版本请通过 GitHub 获取,百度网盘不定期更新,可能具有一定延后性
  • 如有BUG,请联系 arthurcai_c@163.com
  • 目录树如下
Soft_I2C_Driver                                             主文件夹
|
├─ software_i2c                                             子文件夹
|  |
│  ├─ dsview                                                波形文件夹
│  │
│  ├─ lib                                                   底层库
|  |  |
│  │  ├─ CMSIS                                              CMSIS-Cortex-M4
│  │  │
│  │  ├─ GD32F30x_standard_peripheral                       GD32F30X标准库
│  │  │
│  │  └─ STM32F10x_StdPeriph_Lib_V3.6.0                     STM32F10X标准库
│  │
│  ├─ bsp_soft_i2c.c                                        软件模拟I2C源文件
|  |
│  ├─ bsp_soft_i2c.h                                        软件模拟I2C头文件
|  |
│  ├─ bsp_soft_i2c_private.h                                软件模拟I2C私有头文件
|  |
│  └─ readme.txt                                            readme文件
│
└─ test_project                                             示例工程
   │
   ├─ GD32F303_PRJ                                          GD32F303示例工程
   │
   └─ STM32F1_PRJ                                           STM32F103示例工程
  • 文件结构如下,波形文件需下载DSView打开
    demo工程

文件结构

5.1 百度网盘

  • 最后更新时间:2024年1月30日
  • 链接:https://pan.baidu.com/s/1BjvmiNVWNeajOn__YWfHAg?pwd=jtkn
  • 提取码:jtkn

5.2 GitHub(推荐)

  • 最后更新时间:2024年2月2日
  • 链接:https://github.com/Arthur-cai/Soft_I2C_Driver
  • 不会 Git Clone 的可以直接下载压缩包
    Git下载
  • 22
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值