STM平台及GD平台 - 软件模拟I2C驱动实现
一、需知
- 本文不赘述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 移植
- 添加文件
- 驱动注册
- 驱动声明
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左右
4.1.4 通信波形
- 写数据(以O0优化为例)
- 读数据(以O0优化为例)
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左右
4.2.3 通信波形
- 写数据(以O0优化为例)
- 读数据(以O0优化为例)
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打开
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 的可以直接下载压缩包