自用 软件模拟I2C驱动 分享,含源码
分享一个自用 软件模拟i2c驱动,模仿HAL风格,支持多实例
·
自用 软件模拟I2C驱动 分享
软件I2C是一种无需依赖硬件I2C模块的通信方式,常用于资源受限或需要特殊控制的嵌入式系统中。本文分享 一个自写的软I2C驱动,设计思路参考stm32的HAL库i2c,亲测可用。
1.设计思路介绍
本驱动旨在提供一个可移植、易用、结构清晰的软件I2C,主要设计思路如下:
- 遵循 HAL风格:命名统一,方便工程集成
- 支持多实例:每个 I2C 使用独立句柄,适配多设备场景
- 时序可调:支持传入自定义微秒延时函数,兼容不同主频平台
- 简化用户调用:使用
SI2C_Write、SI2C_Read等统一接口封装通信流程 - 协议逻辑完整:实现起始条件、字节传输、ACK/NACK、停止条件等全部I2C协议行为
2.驱动程序源码分享
驱动包含两个文件:soft_i2c.h 和 soft_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
更多推荐



所有评论(0)