自用 软件模拟I2C驱动 分享

软件I2C是一种无需依赖硬件I2C模块的通信方式,常用于资源受限或需要特殊控制的嵌入式系统中。本文分享 一个自写的软I2C驱动,设计思路参考stm32的HAL库i2c,亲测可用。


1.设计思路介绍

本驱动旨在提供一个可移植、易用、结构清晰的软件I2C,主要设计思路如下:

  • 遵循 HAL风格:命名统一,方便工程集成
  • 支持多实例:每个 I2C 使用独立句柄,适配多设备场景
  • 时序可调:支持传入自定义微秒延时函数,兼容不同主频平台
  • 简化用户调用:使用 SI2C_WriteSI2C_Read 等统一接口封装通信流程
  • 协议逻辑完整:实现起始条件、字节传输、ACK/NACK、停止条件等全部I2C协议行为

2.驱动程序源码分享

驱动包含两个文件:soft_i2c.hsoft_i2c.c。完整源码如下。

文件一:soft_i2c.h

/**
 * @file soft_i2c.h
 * @brief 软件I2C驱动接口(仿HAL风格,简洁命名)
 */

#ifndef __SOFT_I2C_H__
#define __SOFT_I2C_H__

#include "stdint.h"
#include "stm32f4xx_hal.h" // 根据实际平台替换为对应HAL头文件

/**
 * @brief 软件I2C状态定义
 */
typedef enum {
    SI2C_OK = 0x00,     ///< 操作成功
    SI2C_ERR = 0x01,    ///< 一般错误
    SI2C_BUSY = 0x02,   ///< 总线忙
    SI2C_TIMEOUT = 0x03 ///< 操作超时
} SI2C_Status;

/**
 * @brief 软件I2C初始化配置结构体
 */
typedef struct {
    GPIO_TypeDef *SCL_Port;       ///< SCL引脚对应的GPIO端口
    uint16_t SCL_Pin;             ///< SCL引脚编号
    GPIO_TypeDef *SDA_Port;       ///< SDA引脚对应的GPIO端口
    uint16_t SDA_Pin;             ///< SDA引脚编号
    uint32_t Frequency;           ///< I2C频率(单位:KHz)
    uint32_t DelayUs;             ///< 内部计算得到的延时(单位:微秒),无需用户赋值
    void (*DelayUsFunc)(uint32_t us); ///< 用户可选传入的微秒延迟函数指针(若为NULL则使用默认空循环)
} SI2C_InitTypeDef;

/*如果未传入us延迟函数,驱动内部将使用空循环完成延迟操作,如有需要请自行修改系统主频*/
#define SYSTEM_CORE_CLOCK_MHZ 168
#define SI2C_DELAY_FACTOR (SYSTEM_CORE_CLOCK_MHZ / 5)

/**
 * @brief 软件I2C句柄结构体
 */
typedef struct {
    SI2C_InitTypeDef Init; ///< 初始化参数
} SI2C_Handle;

/**
 * @brief 初始化软件I2C实例
 *
 * 根据指定的频率自动计算延时,配置SCL/SDA引脚为开漏输出,并设置默认高电平空闲状态。
 *
 * @param si2c 指向I2C句柄的指针
 * @return 操作状态(SI2C_OK表示成功)
 */
SI2C_Status SI2C_Init(SI2C_Handle *si2c);

/**
 * @brief 向I2C从设备发送数据
 *
 * 起始条件 -> 地址 -> 数据 -> 停止条件
 *
 * @param si2c 指向I2C句柄的指针
 * @param addr 7位从设备地址
 * @param data 发送数据缓冲区指针
 * @param size 要发送的字节数
 * @param timeout 超时时间(当前未使用)
 * @return 操作状态
 */
SI2C_Status SI2C_Write(SI2C_Handle *si2c, uint8_t addr, uint8_t *data, uint16_t size, uint32_t timeout);

/**
 * @brief 从I2C从设备读取数据
 *
 * 起始条件 -> 地址+读 -> 接收数据 -> 停止条件
 *
 * @param si2c 指向I2C句柄的指针
 * @param addr 7位从设备地址
 * @param data 接收数据缓冲区指针
 * @param size 要接收的字节数
 * @param timeout 超时时间(当前未使用)
 * @return 操作状态
 */
SI2C_Status SI2C_Read(SI2C_Handle *si2c, uint8_t addr, uint8_t *data, uint16_t size, uint32_t timeout);

#endif // __SOFT_I2C_H__


文件二:soft_i2c.c

/**
 * @file soft_i2c.c
 * @brief 软件I2C驱动实现文件
 */

#include "soft_i2c.h"

/**
 * @brief 软件I2C内部延时函数(单位:微秒)
 * @note 若用户提供了外部延时函数,则使用;否则使用空循环实现
 */
static void si2c_delay(SI2C_Handle *si2c) {
    if (si2c->Init.DelayUsFunc) {
        si2c->Init.DelayUsFunc(si2c->Init.DelayUs);
    } else {
		for (volatile uint32_t i = 0; i < si2c->Init.DelayUs * SI2C_DELAY_FACTOR; ++i);
    }
}

// 设置SDA引脚为输出模式(开漏)
static void sda_out(SI2C_Handle *si2c) {
    GPIO_InitTypeDef GPIO_Init = {
        .Pin = si2c->Init.SDA_Pin,
        .Mode = GPIO_MODE_OUTPUT_OD,
        .Speed = GPIO_SPEED_FREQ_HIGH
    };
    HAL_GPIO_Init(si2c->Init.SDA_Port, &GPIO_Init);
}

// 设置SDA引脚为输入模式
static void sda_in(SI2C_Handle *si2c) {
    GPIO_InitTypeDef GPIO_Init = {
        .Pin = si2c->Init.SDA_Pin,
        .Mode = GPIO_MODE_INPUT
    };
    HAL_GPIO_Init(si2c->Init.SDA_Port, &GPIO_Init);
}

// 发送起始条件
static void i2c_start(SI2C_Handle *si2c) {
    sda_out(si2c);
    HAL_GPIO_WritePin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_SET);
    si2c_delay(si2c);
    HAL_GPIO_WritePin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin, GPIO_PIN_RESET);
    si2c_delay(si2c);
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_RESET);
}

// 发送停止条件
static void i2c_stop(SI2C_Handle *si2c) {
    sda_out(si2c);
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin, GPIO_PIN_RESET);
    si2c_delay(si2c);
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin, GPIO_PIN_SET);
    si2c_delay(si2c);
}

// 等待从设备ACK应答
static uint8_t i2c_wait_ack(SI2C_Handle *si2c) {
    uint8_t err_time = 0;
    sda_in(si2c);
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_SET);
    si2c_delay(si2c);
    while (HAL_GPIO_ReadPin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin)) {
        if (++err_time > 250) {
            i2c_stop(si2c);
            return 1;
        }
    }
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_RESET);
    return 0;
}

// 发送ACK或NACK信号
static void i2c_send_ack(SI2C_Handle *si2c, uint8_t ack) {
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_RESET);
    sda_out(si2c);
    HAL_GPIO_WritePin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin, ack ? GPIO_PIN_SET : GPIO_PIN_RESET);
    si2c_delay(si2c);
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_SET);
    si2c_delay(si2c);
    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_RESET);
}

// 发送一个字节
static void i2c_send_byte(SI2C_Handle *si2c, uint8_t byte) {
    sda_out(si2c);
    for (int i = 0; i < 8; ++i) {
        HAL_GPIO_WritePin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin, (byte & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET);
        byte <<= 1;
        si2c_delay(si2c);
        HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_SET);
        si2c_delay(si2c);
        HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_RESET);
    }
}

// 接收一个字节
static uint8_t i2c_read_byte(SI2C_Handle *si2c, uint8_t ack) {
    uint8_t data = 0;
    sda_in(si2c);
    for (int i = 0; i < 8; ++i) {
        data <<= 1;
        HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_SET);
        si2c_delay(si2c);
        if (HAL_GPIO_ReadPin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin)) {
            data |= 0x01;
        }
        HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_RESET);
        si2c_delay(si2c);
    }
    i2c_send_ack(si2c, !ack);
    return data;
}

/**
 * @brief 初始化软件I2C
 *
 * 自动根据设定的频率(KHz)计算延时(us)
 *
 * @param si2c 指向SI2C句柄
 * @return 操作状态
 */
SI2C_Status SI2C_Init(SI2C_Handle *si2c) {
    if (si2c->Init.Frequency == 0) return SI2C_ERR;
    si2c->Init.DelayUs = 500 / si2c->Init.Frequency; // 一位信号持续500us/freq

    GPIO_InitTypeDef GPIO_Init = {
        .Mode = GPIO_MODE_OUTPUT_OD,
        .Speed = GPIO_SPEED_FREQ_HIGH
    };
    GPIO_Init.Pin = si2c->Init.SCL_Pin;
    HAL_GPIO_Init(si2c->Init.SCL_Port, &GPIO_Init);

    GPIO_Init.Pin = si2c->Init.SDA_Pin;
    HAL_GPIO_Init(si2c->Init.SDA_Port, &GPIO_Init);

    HAL_GPIO_WritePin(si2c->Init.SCL_Port, si2c->Init.SCL_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(si2c->Init.SDA_Port, si2c->Init.SDA_Pin, GPIO_PIN_SET);

    return SI2C_OK;
}

SI2C_Status SI2C_Write(SI2C_Handle *si2c, uint8_t addr, uint8_t *data, uint16_t size, uint32_t timeout) {
    i2c_start(si2c);
    i2c_send_byte(si2c, addr << 1);
    if (i2c_wait_ack(si2c)) return SI2C_ERR;
    for (uint16_t i = 0; i < size; ++i) {
        i2c_send_byte(si2c, data[i]);
        if (i2c_wait_ack(si2c)) return SI2C_ERR;
    }
    i2c_stop(si2c);
    return SI2C_OK;
}

SI2C_Status SI2C_Read(SI2C_Handle *si2c, uint8_t addr, uint8_t *data, uint16_t size, uint32_t timeout) {
    i2c_start(si2c);
    i2c_send_byte(si2c, (addr << 1) | 0x01);
    if (i2c_wait_ack(si2c)) return SI2C_ERR;
    for (uint16_t i = 0; i < size; ++i) {
        data[i] = i2c_read_byte(si2c, (i != (size - 1)));
    }
    i2c_stop(si2c);
    return SI2C_OK;
}


3.源码解析

3.1初始化与配置

SI2C_Status SI2C_Init(SI2C_Handle *si2c)
  • 自动计算频率对应的延时
  • 配置 GPIO 开漏输出
  • 设置初始高电平空闲状态

3.2写数据流程(主->从)

SI2C_Write(handle, addr, data, size, timeout)

封装完整写入流程,调用后由主机向从机发送数据:

  • 发送起始位(START)

  • 发送从设备地址(带写标志)

  • 依次写入数据字节,每个字节后等待从机ACK

  • 发送停止位(STOP)

  • 返回值为 SI2C_OK 表示写入成功,SI2C_ERR 表示失败(如无应答等)。

3.3读数据流程(主<-从)

SI2C_Read(handle, addr, buf, size, timeout)
  • 封装读取过程,由主机读取从机发来的数据:

  • 发送起始位(START)

  • 发送从设备地址(带读标志)

  • 连续接收字节,并在每个字节后发送ACK

  • 最后一个字节后发送NACK

  • 发送停止位(STOP)

  • 支持任意长度读操作,返回状态指示是否成功接收。

3.4 支持错误码返回

/**
 * @brief 软件I2C状态定义
 */
typedef enum {
    SI2C_OK = 0x00,     ///< 操作成功
    SI2C_ERR = 0x01,    ///< 一般错误
    SI2C_BUSY = 0x02,   ///< 总线忙
    SI2C_TIMEOUT = 0x03 ///< 操作超时
} SI2C_Status;

3.5 延迟策略

  • 可通过 Init.DelayUsFunc 传入用户微秒延时函数
  • 未传入则使用空循环配合 SI2C_DELAY_FACTOR 打拍

4.调用示例以及注意事项

4.1调用示例

SI2C_Handle i2c1 = {
    .Init = {
        .SCL_Port = GPIOB,
        .SCL_Pin = GPIO_PIN_6,
        .SDA_Port = GPIOB,
        .SDA_Pin = GPIO_PIN_7,
        .Frequency = 100  // 100KHz
    }
};

int main(void) {
    HAL_Init();

	if(SI2C_Init(&i2c1) == SI2C_OK){
		printf ("i2c1 init ok\r\n");
	}
	
	/*驱动内部已经集成了读写地址的移位操作,所以直接传入器件地址即可*/
	
	/*Write*/
    uint8_t tx[2] = {0x01, 0x02};
	if( SI2C_Write(&i2c1, 0x50, tx, 2, 100) != SI2C_OK){
		printf ("i2c1 Write err\r\n");
	}
	/*Read  这里是单纯读寄存器,一般的i2c器件读之前,还需要Write一个需要访问的寄存器地址*/
    uint8_t rx[2];
   	if( SI2C_Read(&i2c1, 0x50, rx, 2, 100) != SI2C_OK){
		printf ("i2c1 Read  err\r\n");
	}
	
}

4.2 注意事项

  • 波形异常?确认连接上拉电阻
  • 延时不准?驱动内部默认使用的是空循环延迟实现,主频变化后需调整SYSTEM_CORE_CLOCK_MHZ ,个人推荐传入自己的延迟函数,如 DWT 延时
void My_DelayUs(uint32_t us) {
    // 可基于 DWT、定时器、NOP 等实现
}
i2c1.Init.DelayUsFunc = My_DelayUs;

4.3 平台替换

  • 其他平台移植可以替换平台.h和内部的io读写函数就行
/*替换以下这些就行*/
#include "stm32f4xx_hal.h" // 根据实际平台替换
GPIO_TypeDef  // 根据实际平台替换
HAL_GPIO_WritePin  // 根据实际平台替换
HAL_GPIO_ReadPin  // 根据实际平台替换

结语

如果你觉得本驱动对你有所帮助,欢迎点赞、收藏或评论交流。


作者:Jafi


Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐