STM32和GD32可移植软件模拟I2C驱动实现,支持时钟延展和400KHz快速模式

STM平台及GD平台 – 软件模拟I2C驱动实现

  • 一、需知
  • 二、背景
  • 三、代码实现
  • 3.1 延时函数
  • 3.2 时钟延展
  • 3.3 枚举及结构体定义
  • 3.4 对外接口
  • 四、使用示例
  • 4.1 GD32F303RET6核心板
  • 4.1.1 移植
  • 4.1.2 使用
  • 4.1.3 资源占用
  • 4.1.4 通信波形
  • 4.2 STM32F103C8T6核心板
  • 4.2.1 使用
  • 4.2.2 资源占用
  • 4.2.3 通信波形
  • 4.3 时钟延展波形
  • 五、驱动获取方式
  • 5.1 百度网盘
  • 5.2 GitHub(推荐)
  • 一、需知

  • 本文不赘述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下载
  • 物联沃分享整理
    物联沃-IOTWORD物联网 » STM32和GD32可移植软件模拟I2C驱动实现,支持时钟延展和400KHz快速模式

    发表回复