BBusI2C:高通用性的软件 I2C 驱动框架——适用STM32
本文介绍了一种标准化的软件I2C驱动框架设计,旨在解决嵌入式开发中多设备、多速度I2C通信的痛点。该方案通过GPIO模拟I2C时序,支持多总线并行工作,每条总线可独立配置通信速度(100kHz/400kHz等)。文章详细解析了I2C协议的关键时序(起始/停止信号、数据传输、应答机制)和硬件设计要点(开漏输出、上拉电阻)。该驱动框架采用分层设计,移植新设备只需修改硬件抽象层,核心逻辑无需改动,已在S
一、引言:为什么需要标准化的软件 I2C 驱动?
在嵌入式项目开发中,I2C 通信是传感器、存储芯片等外设的常用接口,但实际开发中常面临以下困境:
- 主控芯片需同时对接多个 I2C 设备(如 AHT30 温湿度传感器、EEPROM、OLED 屏),不同设备可能要求不同的通信速度
- 硬件 I2C 资源有限,软件 I2C 可通过 GPIO 模拟扩展多总线,无需占用硬件 I2C 外设
- 传统做法为每个设备单独编写 I2C 收发逻辑,导致代码冗余、维护困难,且难以适配不同 GPIO 引脚配置
本教程将呈现一个分层解耦、易扩展的软件 I2C 驱动框架,已在 STM32 等平台验证,支持多路 I2C 总线并行工作,移植新设备仅需修改硬件抽象层配置,无需改动核心逻辑,同时支持灵活调整总线速度、自定义通信流程。
二、软件 I2C 通信基础
2.1 什么是软件 I2C?
软件 I2C(Bit-Banging I2C)是通过通用 GPIO 引脚模拟 I2C 通信协议的时序,无需依赖芯片的硬件 I2C 外设。核心通过控制两根 GPIO 引脚(SDA 串行数据线、SCL 串行时钟线)的电平变化,实现设备间的数据交互,其通信时序需严格遵循 I2C 协议规范:
- 起始信号:SCL 为高电平时,SDA 从高电平拉低
- 停止信号:SCL 为高电平时,SDA 从低电平拉高
- 数据传输:SCL 为高电平时,SDA 电平保持稳定(表示 1 或 0);SCL 为低电平时,SDA 可切换电平
- 应答信号(ACK/NACK):接收方在数据传输完成后,拉低 SDA 表示应答(ACK),保持高电平表示不应答(NACK)
2.2 多设备多速度通信机制
软件 I2C 支持单总线挂载多个从设备(通过从设备地址区分),且支持为不同总线配置独立通信速度:
- 总线区分:通过 LUN(逻辑单元号)标识不同的软件 I2C 总线,每条总线对应一组独立的 SDA/SCL GPIO 引脚
- 速度适配:通过
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">bbus_i2c_set_delay_time</font>函数为每条总线设置延时时间,实现标准模式(100kHz)、快速模式(400kHz)等不同速度需求 - 通信模式:支持 “寄存器地址 + 数据” 的读写(适用于大多数传感器)和 “无寄存器地址” 的直接字节序列读取(适用于部分简单外设)
三、I2C 硬件设计与开漏输出
3.1 上拉电阻
- I2C 总线为开漏输出,必须外接上拉电阻(通常 4.7kΩ~10kΩ)。
- 上拉电阻确保总线在无设备驱动时保持高电平。
3.2 开漏输出模式
- 开漏输出只能拉低电平,无法主动输出高电平,需靠上拉电阻拉高。
- 优点:支持多设备“线与”,避免电平冲突;支持不同电压设备(通过上拉至目标电压)。
3.3 STM32 GPIO 配置
- 模式:开漏输出(Open-Drain)
- 上拉:使能内部上拉或外部上拉
- 初始状态:高电平
四、I2C 时序详解
I2C 通信基于严格的时序协议,以下是关键时序阶段:
4.1 起始条件(START)
- 当 SCL 为高电平时,SDA 由高电平跳变为低电平。
- 表示一次传输的开始,由主机发出。

4.2 停止条件(STOP)
- 当 SCL 为高电平时,SDA 由低电平跳变为高电平。
- 表示一次传输的结束,由主机发出。

4.3 数据有效性
- 在 SCL 为高电平期间,SDA 必须保持稳定。
- 数据变化只能在 SCL 为低电平时进行。

4.4 应答机制(ACK/NACK)
- 每传输完 8 位数据后,接收方需在第 9 个时钟脉冲期间拉低 SDA(ACK)表示应答。
- 若 SDA 保持高电平(NACK),表示非应答,通常用于结束读取或错误指示。
应答(红色线为SDA状态:由高变低):

非应答(红色线为SDA状态:一直高电平):

5.5 时序波形图解析
图片来源:江科大STM32入门PPT
(1) 指定地址写

| 阶段 | 物理动作 | 电平变化逻辑 | 总线状态 |
|---|---|---|---|
| ① START | 主机发起 | SCL=1期间,SDA产生↓下降沿 | 总线占用 |
| ② 地址帧 | 主机发送0xD0(写) | SCL↓时改变SDA,SCL↑时从机采样 | 主机驱动 |
| ③ ACK采样 | 主机读取应答 | 主机释放SDA,SCL↑时读取SDA=0(从机拉低) | 从机驱动 |
| ④ 寄存器地址 | 主机发送0x19 | SCL↓时改变SDA,SCL↑时从机采样 | 主机驱动 |
| ⑤ ACK采样 | 主机读取应答 | 主机释放SDA,SCL↑时读取SDA=0(从机拉低)确认从机就绪 | 从机驱动 |
| ⑥ 数据帧 | 主机发送0x19 | SCL↓时改变SDA,SCL↑时从机采样 | 主机驱动 |
| ⑦ ACK采样 | 主机读取应答 | 主机释放SDA,SCL↑时读取SDA=0(从机拉低),确认写入成功 | 从机驱动 |
| ⑧ STOP | 主机释放 | SCL=1期间,SDA产生↑上升沿 | 总线空闲 |
(2) 指定地址读

| 阶段 | 物理动作 | 电平变化逻辑 | 总线状态 |
|---|---|---|---|
| ① START | 主机发起 | SCL=1期间,SDA产生↓下降沿 | 总线占用 |
| ② 写地址帧 | 主机发送0xD0(写) | SCL↓时改变SDA,SCL↑时从机采样 | 主机驱动 |
| ③ ACK采样 | 主机读取应答 | 主机释放SDA,SCL↑时读取SDA=0(从机拉低) | 从机驱动 |
| ④ 寄存器地址 | 主机发送0x19 | SCL↓时改变SDA,SCL↑时从机采样 | 主机驱动 |
| ⑤ ACK采样 | 主机读取应答 | 主机释放SDA,SCL↑时读取SDA=0(从机拉低) | 从机驱动 |
| ⑥ REPEAT START | 主机再次发起 | SCL=1期间,SDA产生↓下降沿 | 总线占用 |
| ⑦ 读地址帧 | 主机发送0xD1(读) | SCL↓时改变SDA,SCL↑时从机采样 | 主机驱动 |
| ⑧ ACK采样 | 主机读取应答 | 主机释放SDA,SCL↑时读取SDA=0(从机拉低) | 从机驱动 |
| ⑨ 数据接收 | 从机驱动SDA | SCL↓时从机改变SDA,SCL↑时主机采样 | 从机驱动 |
| ⑩ NACK | 主机发送非应答 | SCL=0时主机置SDA=1,表示读取完成 | 主机驱动 |
| ⑪ STOP | 主机释放 | SCL=1期间,SDA产生↑上升沿 | 总线空闲 |
五、驱动架构设计:分层解耦思想
5.1 代码架构
本驱动采用两层架构设计,实现硬件无关性,确保核心逻辑可跨平台复用:
应用层(用户代码,如AHT30驱动)
↑↓
核心驱动层(bbus_i2c.c/h)—— 通用逻辑,无需修改
↑↓
硬件抽象层(bbus_i2c_port.c/h)—— 需根据平台适配
5.2 核心组件
| 组件 | 功能 | 实现位置 |
|---|---|---|
| 总线配置 | 管理多 I2C 总线(LUN),初始化引脚电平 | bbus_i2c_port.c |
| 引脚操作 | 控制 SDA/SCL 引脚的电平、输入 / 输出模式切换 | bbus_i2c_port.c |
| 延时控制 | 提供微秒级延时,适配不同通信速度 | bbus_i2c_port.c(调用 delay_us) |
| 临界区 | 保护多任务环境下的总线操作,防止冲突 | bbus_i2c_port.c(用户实现) |
| 时序生成 | 实现起始 / 停止信号、数据收发、ACK/NACK 处理 | bbus_i2c.c |
| 超时机制 | 接收应答时的超时保护,避免死等 | bbus_i2c.c(依赖系统滴答定时器) |
六、工程配置
6.1 STM32CubeMX 配置
(1)选择芯片型号:

①输入芯片的型号名字,这里选择STM32F103RCT6
②搜索后,一般情况下选择第一个芯片即可
(2)引脚外设功能配置:

①选择外部高速晶振:在 System Core -> RCC -> HSE -> Crystal/Ceramic Resonator
②配置SW调试模式:在System Core -> SYS -> Debug -> Serial Wire
③信息打印串口1配置:设置异步通信Connectivity -> USART1 -> Mode -> Asynchronous
然后串口参数配置USART1 -> Parameter Settings: 115200 Bits/s, 8 Bits, None, 1
④配置两组软件I2C引脚,SDA:PB7 , SCL:PB6 ,SDA:PB9 , SCL:PB8
⑤配置引脚参数:
在System Core -> GPIO -> GPIO -> 对应的引脚 -> GPIO mode -> Output Open Drain,修改输出模式为开漏输出
在System Core -> GPIO -> GPIO -> 对应的引脚 -> GPIO Pull-up/Pull-down -> Pull-up,修改为上拉
(3)时钟树调整:

①时钟配置页面
②前面使能了外部低速晶振,这里选择这个LSE通道
③前面使能了外部高速晶振,这里输入最高主频:72,然后按下回车确定
(4)工程配置:

①项目名字:这里示例项目名<font style="background-color:rgba(0, 0, 0, 0.06);">BBus_I2C</font>,一般情况名字默认叫project
②项目位置:这里项目路径不要包含中文名
③开发环境:选择 MDK-ARM,也就是keil5,后面的版本默认即可
(5)代码生成配置:

①代码生成选项
②只生成必要库文件:勾选Copy only the necessary library files
③为每个外设生成一对.c.h文件,便于管理:勾选Generate peripheral initalization as a pair of '.c/.h' files per peripheral
最后点击右上角的生成代码即可
6.2 keil5配置
步骤1:创建文件夹结构与四个文件,然后在文章末尾将源码分别复制进去:
Project/
├── Core/
├── Drivers/
├── BBusI2C/ ← 新增
│ ├── bbus_i2c_port.c
│ ├── bbus_i2c_port.h
│ ├── bbus_i2c.c
│ └── bbus_i2c.h
└── MDK-ARM/
步骤2:Keil5添加文件
- 添加一个组 命名为
AT_Driver,并添加at_driver.c和at_port.c和头文件

- 点击"Options for Target" → “C/C++” → “Include Paths” → 添加
AT文件夹路径

步骤3:重定向printf(必须勾选微库)
// 在main.c中添加,用于AT_LOG输出
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
⚠️ 重要:Keil选项中勾选"Use MicroLIB"

步骤4:添加自己的delay us级别程序
本驱动依赖微秒级延时函数 delay_us()。
请确保已实现该函数,可参考 STM32 delay us 微秒延时驱动 :
6.3 工程移植适配
(1)在bbus_i2c_port.h文件中,设置实际的软件i2c总线个数,例如本例程使用了两个总线
同时可以选择是否启动软件i2c信息打印,可以在i2c通信错误时打印错误信息

(2)在bbus_i2c_port.c文件中,适配自己的延时,tick,gpio读取函数即可
/**
* @brief 软件I2C延时函数
* @param xus: 延时时间,单位us
* @retval 无
*/
void bbus_i2c_port_delay_us(uint32_t xus)
{
delay_us(xus);
}
/**
* @brief 获取当前系统时间,单位ms
* @param 无
* @retval 当前系统时间,单位ms
*/
uint32_t bbus_i2c_port_tick_get(void)
{
return HAL_GetTick();
}
(3)初始化内容
/**
* @brief 软件I2C端口初始化
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_init(uint8_t lun)
{
switch (lun)
{
case 0:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
break;
case 1:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
break;
default:
break;
}
}
(4)配置sda与scl引脚控制函数
/**
* @brief 设置I2C SDA引脚电平
* @param lun: I2C总线号
* @param level: SDA引脚电平,1:高电平,0:低电平
* @retval 无
*/
void bbus_i2c_port_sda_set(uint8_t lun, uint8_t level)
{
switch (lun)
{
case 0:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
case 1:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
default:
break;
}
}
/**
* @brief 设置I2C SCL引脚电平
* @param lun: I2C总线号
* @param level: SCL引脚电平,1:高电平,0:低电平
* @retval 无
*/
void bbus_i2c_port_scl_set(uint8_t lun, uint8_t level)
{
switch (lun)
{
case 0:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
case 1:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
default:
break;
}
}
(5)当使用rtos时,建议加上临界区
/**
* @brief 进入临界区
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_enter_critical(uint8_t lun)
{
switch (lun)
{
case 0:
break;
case 1:
break;
default:
break;
}
}
/**
* @brief 退出临界区
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_exit_critical(uint8_t lun)
{
switch (lun)
{
case 0:
break;
case 1:
break;
default:
break;
}
}
七、使用示例
7.1 初始化与扫描总线上的iic设备
记得初始化延时函数
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
delay_init(72);
bbus_i2c_init();
printf("Scanning I2C bus...\n");
for (uint8_t dev_addr = 0x01; dev_addr <= 0x7F; dev_addr++)
{
if (bbus_i2c_check_address(0, dev_addr, 20) == 0)
{
printf("Found device at address: 0x%02X\n", dev_addr);
}
}
while (1)
{
}
}
7.2 调用写入函数
static inline uint8_t AHT30_I2C_WRITE(uint8_t addr, const uint8_t *data, uint8_t len) /* 写数据函数 */
{
bbus_i2c_set_delay_time(0, 100);//设置iic延时
return bbus_i2c_write_data(0, AHT30_ADDR, addr, data, len, 100);
}
static inline uint8_t AHT30_I2C_READ(uint8_t *data, uint8_t len)/* 读数据函数 */
{
bbus_i2c_set_delay_time(0, 100);//设置iic延时
return bbus_i2c_read_seq(0, AHT30_ADDR, data, len, 100);
}
例如上面是aht30的i2c连续写函数:
- 第一步:先设置当前总线的延时us,传入100,默认0
- 第二步:调用
bbus_i2c_write_data连续写函数,传入总线号,i2c地址,寄存器地址,数据,长度,超时时间
读取函数同理
7.3 基础对外接口
这些基础iic接口,当连续读写函数不满足通信要求时,可以自行使用这些接口实现通信
/**
* @brief 产生I2C起始信号
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_start(uint8_t lun);
/**
* @brief 产生I2C停止信号
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_stop(uint8_t lun);
/**
* @brief 等待应答信号到来
* @param lun: I2C总线号
* @param timeout: 超时时间ms
* @retval 1,接收应答失败, 0,接收应答成功
*/
uint8_t bbus_i2c_wait_ack(uint8_t lun, uint32_t timeout);
/**
* @brief 产生ACK应答
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_ack(uint8_t lun);
/**
* @brief 不产生ACK应答
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_nack(uint8_t lun);
/**
* @brief I2C发送一个字节
* @param lun: I2C总线号
* @param data: 要发送的数据
* @retval 无
*/
void bbus_i2c_send_byte(uint8_t lun, const uint8_t data);
/**
* @brief I2C读取一个字节
* @param lun: I2C总线号
* @param ack: ack=1时,发送ack; ack=0时,发送nack
* @retval 接收到的数据
*/
uint8_t bbus_i2c_read_byte(uint8_t lun, uint8_t ack);
7.4 核心对外接口
驱动提供 3 个主要对外函数,覆盖绝大多数 I2C 设备通信场景:
(1)连续写数据(带寄存器地址)
uint8_t bbus_i2c_write_data(uint8_t lun, uint8_t slave_addr, uint8_t reg_address, const uint8_t *data, uint8_t len, uint32_t timeout);
- 适用场景:向设备指定寄存器写入多个字节(如配置传感器参数)
- 流程:起始信号 → 发送从设备地址(写)→ 等待 ACK → 发送寄存器地址 → 等待 ACK → 发送数据 → 等待 ACK → 停止信号
(2)连续读数据(带寄存器地址)
uint8_t bbus_i2c_read_data(uint8_t lun, uint8_t slave_addr, uint8_t reg_address, uint8_t *data, uint8_t len, uint32_t timeout);
- 适用场景:从设备指定寄存器读取多个字节(如读取传感器数据)
- 流程:起始信号 → 发送从设备地址(写)→ 等待 ACK → 发送寄存器地址 → 等待 ACK → 重新发送起始信号 → 发送从设备地址(读)→ 等待 ACK → 读取数据(最后一字节发 NACK)→ 停止信号
(3)直接读 N 字节序列(无寄存器地址)
uint8_t bbus_i2c_read_seq(uint8_t lun, uint8_t slave_addr, uint8_t *data, uint8_t len, uint32_t timeout);
- 适用场景:设备无需指定寄存器,直接返回数据(如部分 EEPROM、简单 ADC)
- 流程:起始信号 → 发送从设备地址(读)→ 等待 ACK → 读取数据 → 停止信号
八、运行效果
示例代码展示扫描总线上的所有iic设备,并显示出来,这里可以看到检测出了0x70地址设备

九、总结
本软件 I2C 驱动框架通过分层设计实现了以下核心优势:
- 硬件无关性:移植仅需修改
<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">bbus_i2c_port.c/h</font>,适配不同 MCU 平台 - 多总线多速度:支持多路 I2C 总线并行工作,每条总线可独立配置通信速度
- 灵活易用:提供 3 个通用对外接口,同时支持底层函数自定义通信流程
- 鲁棒性强:内置超时保护、ACK 校验、临界区保护(RTOS 适配)
- 资源占用低:静态内存分配,无动态内存依赖,适合嵌入式小型系统
十、源码
扫描下方二维码加入嵌入式技术交流群,即可获取源码压缩包,群内同步答疑驱动开发、移植问题及后续版本更新,同时有更多嵌入式问题也欢迎讨论~ +q:181921938

源码遵循开源TIM协议,如果这个项目对你的物联网开发有帮助,请给它一个 ⭐ !:
- 源码仓库(GitHub):ZeroOneLab/BBusI2C
- 源码仓库(Gitee):零壹实验室-ZeroOneLab/BBusI2C
10.1 bbus_i2c.c
#include "bbus_i2c.h"
static uint8_t delay_time[BBUS_I2C_BUS_NUM];
#define SDA_OUT(lun) bbus_i2c_port_sda_set_out(lun)
#define SDA_IN(lun) bbus_i2c_port_sda_set_in(lun)
#define SDA_SET(lun, level) bbus_i2c_port_sda_set(lun, level)
#define SDA_GET(lun) bbus_i2c_port_sda_get(lun)
#define SCL_SET(lun, level) bbus_i2c_port_scl_set(lun, level)
#define DELAY_US(delay_time) bbus_i2c_port_delay_us(delay_time)
#define ENTER_CRITICAL(lun) bbus_i2c_port_enter_critical(lun)
#define EXIT_CRITICAL(lun) bbus_i2c_port_exit_critical(lun)
/**
* @brief 初始化软件I2C
* @param 无
* @retval 无
*/
void bbus_i2c_init(void)
{
for (uint8_t i = 0; i < BBUS_I2C_BUS_NUM; i++)
{
bbus_i2c_port_init(i);
delay_time[i] = 0;
}
}
/**
* @brief 设置I2C延时时间
* @param lun: I2C总线号
* @param xus: 延时时间 (单位: us)
* @retval 无
*/
void bbus_i2c_set_delay_time(uint8_t lun, uint32_t xus)
{
delay_time[lun] = xus;
}
/**
* @brief 产生I2C起始信号
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_start(uint8_t lun)
{
SDA_OUT(lun);
SDA_SET(lun, 1);
SCL_SET(lun, 1);
DELAY_US(delay_time[lun]);
SDA_SET(lun, 0); /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
DELAY_US(delay_time[lun]);
SCL_SET(lun, 0); /* 钳住I2C总线,准备发送或接收数据 */
}
/**
* @brief 产生I2C停止信号
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_stop(uint8_t lun)
{
SDA_OUT(lun);
SDA_SET(lun, 0); /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
SCL_SET(lun, 0); /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
DELAY_US(delay_time[lun]);
SCL_SET(lun, 1);
SDA_SET(lun, 1); /* 发送I2C总线结束信号 */
DELAY_US(delay_time[lun]);
}
/**
* @brief 等待应答信号到来
* @param lun: I2C总线号
* @param timeout: 超时时间ms
* @retval 1,接收应答失败
* 0,接收应答成功
*/
uint8_t bbus_i2c_wait_ack(uint8_t lun, uint32_t timeout)
{
uint32_t wait_time = bbus_i2c_port_tick_get();
SDA_IN(lun); /* 设置SDA为输入模式 */
SDA_SET(lun, 1); /* 主机释放SDA线(此时外部器件可以拉低SDA线) */
DELAY_US(delay_time[lun]);
SCL_SET(lun, 1); /* SCL=1, 此时从机可以返回ACK */
DELAY_US(delay_time[lun]);
while (SDA_GET(lun)) /* 等待应答 */
{
if ((bbus_i2c_port_tick_get() - wait_time) >= timeout)
{
bbus_i2c_stop(lun);
return 1;
}
}
SCL_SET(lun, 0); /* SCL=0, 结束ACK检查 */
return 0;
}
/**
* @brief 产生ACK应答
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_ack(uint8_t lun)
{
SCL_SET(lun, 0); /* SCL 0 -> 1 时 SDA = 0,表示应答 */
SDA_OUT(lun);
DELAY_US(delay_time[lun]);
SDA_SET(lun, 0); /* SCL 0 -> 1 时 SDA = 0,表示应答 */
DELAY_US(delay_time[lun]);
SCL_SET(lun, 1); /* 产生一个时钟 */
DELAY_US(delay_time[lun]);
SCL_SET(lun, 0);
}
/**
* @brief 不产生ACK应答
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_nack(uint8_t lun)
{
SCL_SET(lun, 0); /* 产生一个时钟 */
SDA_OUT(lun);
SDA_SET(lun, 1); /* SCL 0 -> 1 时 SDA = 1,表示不应答 */
DELAY_US(delay_time[lun]);
SCL_SET(lun, 1); /* 产生一个时钟 */
DELAY_US(delay_time[lun]);
SCL_SET(lun, 0);
}
/**
* @brief I2C发送一个字节
* @param lun: I2C总线号
* @param data: 要发送的数据
* @retval 无
*/
void bbus_i2c_send_byte(uint8_t lun, const uint8_t data)
{
SDA_OUT(lun);
SCL_SET(lun, 0); /* 产生一个时钟 */
for (uint8_t i = 0; i < 8; i++)
{
SDA_SET(lun, (((data << i) & 0x80) >> 7));
DELAY_US(delay_time[lun]);
SCL_SET(lun, 1);
DELAY_US(delay_time[lun]);
SCL_SET(lun, 0);
}
}
/**
* @brief I2C读取一个字节
* @param lun: I2C总线号
* @param ack: ack=1时,发送ack; ack=0时,发送nack
* @retval 接收到的数据
*/
uint8_t bbus_i2c_read_byte(uint8_t lun, uint8_t ack)
{
uint8_t i, receive = 0;
SDA_SET(lun, 1);
SDA_IN(lun); /* 设置SDA为输入模式 */
for (i = 0; i < 8; i++) /* 接收1个字节数据 */
{
SCL_SET(lun, 0);
DELAY_US(delay_time[lun]);
SCL_SET(lun, 1);
receive <<= 1; /* 高位先输出,所以先收到的数据位要左移 */
if (SDA_GET(lun))
{
receive++;
}
DELAY_US(delay_time[lun]);
}
if (!ack)
{
bbus_i2c_nack(lun); /* 发送nACK */
}
else
{
bbus_i2c_ack(lun); /* 发送ACK */
}
return receive;
}
/**
* @brief 检查从设备地址是否正确
* @param lun: I2C总线号
* @param slave_addr: 从设备地址
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_check_address(uint8_t lun, uint8_t slave_addr, uint32_t timeout)
{
ENTER_CRITICAL(lun);
bbus_i2c_start(lun);
bbus_i2c_send_byte(lun, slave_addr & 0xFE);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Check][ERROR]: Wait ACK failed for address 0x%02X\n", slave_addr);
return 1; // 接收应答失败
}
bbus_i2c_stop(lun);
EXIT_CRITICAL(lun);
return 0;
}
/**
* @brief 软件I2C连续写数据
* @param lun: I2C总线号
* @param salve_adress: 从设备地址
* @param reg_address: 寄存器地址
* @param data: 存储读取数据的缓冲区
* @param len: 要读取的数据长度
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_write_data(uint8_t lun, uint8_t slave_addr, uint8_t reg_address, const uint8_t *data, uint8_t len, uint32_t timeout)
{
ENTER_CRITICAL(lun);
// 产生起始信号
bbus_i2c_start(lun);
// 发送从设备地址 + 写命令
bbus_i2c_send_byte(lun, slave_addr & 0xFE);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Write][ERROR]: Wait ACK failed for address 0x%02X\n", slave_addr);
return 1; // 接收应答失败
}
// 发送寄存器地址
bbus_i2c_send_byte(lun, reg_address);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Write][ERROR]: Wait ACK failed for register 0x%02X\n", reg_address);
return 1; // 接收应答失败
}
// 发送数据
for (uint8_t i = 0; i < len; i++)
{
bbus_i2c_send_byte(lun, data[i]);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Write][ERROR]: Wait ACK failed for data 0x%02X\n", data[i]);
return 1; // 接收应答失败
}
}
// 产生停止信号
bbus_i2c_stop(lun);
EXIT_CRITICAL(lun);
return 0;
}
/**
* @brief 软件I2C连续读数据
* @param lun: I2C总线号
* @param salve_adress: 从设备地址
* @param reg_address: 寄存器地址
* @param data: 存储读取数据的缓冲区
* @param len: 要读取的数据长度
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_read_data(uint8_t lun, uint8_t slave_addr, uint8_t reg_address, uint8_t *data, uint8_t len, uint32_t timeout)
{
ENTER_CRITICAL(lun);
// 产生起始信号
bbus_i2c_start(lun);
// 发送从设备地址 + 写命令
bbus_i2c_send_byte(lun, slave_addr & 0xFE);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Read][ERROR]: Wait ACK failed for address 0x%02X\n", slave_addr);
return 1; // 接收应答失败
}
// 发送寄存器地址
bbus_i2c_send_byte(lun, reg_address);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Read][ERROR]: Wait ACK failed for register 0x%02X\n", reg_address);
return 1; // 接收应答失败
}
// 产生起始信号
bbus_i2c_start(lun);
// 发送从设备地址 + 读命令
bbus_i2c_send_byte(lun, slave_addr | 0x01);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Read][ERROR]: Wait ACK failed for address 0x%02X in read mode\n", slave_addr);
return 1; // 接收应答失败
}
// 读取数据
for (uint8_t i = 0; i < len; i++)
{
data[i] = bbus_i2c_read_byte(lun, i < len - 1 ? 1 : 0);
}
// 产生停止信号
bbus_i2c_stop(lun);
EXIT_CRITICAL(lun);
return 0; // 读取成功
}
/**
* @brief 直接读 N 字节序列(无寄存器地址阶段)
* @param lun: I2C总线号
* @param salve_adress: 从设备地址
* @param data: 存储读取数据的缓冲区
* @param len: 要读取的数据长度
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_read_seq(uint8_t lun, uint8_t slave_addr, uint8_t *data, uint8_t len, uint32_t timeout)
{
ENTER_CRITICAL(lun);
// 产生起始信号
bbus_i2c_start(lun);
// 发送从设备地址 + 读命令
bbus_i2c_send_byte(lun, slave_addr | 0x01);
if (bbus_i2c_wait_ack(lun, timeout))
{
bbus_i2c_stop(lun);
BBUS_I2C_LOG("[I2C Read][ERROR]: Wait ACK failed for address 0x%02X in read mode\n", slave_addr);
return 1; // 接收应答失败
}
// 读取数据
for (uint8_t i = 0; i < len; i++)
{
data[i] = bbus_i2c_read_byte(lun, i < len - 1 ? 1 : 0);
}
// 产生停止信号
bbus_i2c_stop(lun);
EXIT_CRITICAL(lun);
return 0; // 读取成功
}
10.2 bbus_i2c.h
#ifndef BBUS_I2C_H
#define BBUS_I2C_H
#include "bbus_i2c_port.h"
#include <stdint.h>
/**
* @brief 初始化软件I2C
* @param 无
* @retval 无
*/
void bbus_i2c_init(void);
/**
* @brief 设置I2C延时时间
* @param lun: I2C总线号
* @param xus: 延时时间 (单位: us)
* @retval 无
*/
void bbus_i2c_set_delay_time(uint8_t lun, uint32_t xus);
/**
* @brief 产生I2C起始信号
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_start(uint8_t lun);
/**
* @brief 产生I2C停止信号
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_stop(uint8_t lun);
/**
* @brief 等待应答信号到来
* @param lun: I2C总线号
* @param timeout: 超时时间ms
* @retval 1,接收应答失败, 0,接收应答成功
*/
uint8_t bbus_i2c_wait_ack(uint8_t lun, uint32_t timeout);
/**
* @brief 产生ACK应答
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_ack(uint8_t lun);
/**
* @brief 不产生ACK应答
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_nack(uint8_t lun);
/**
* @brief I2C发送一个字节
* @param lun: I2C总线号
* @param data: 要发送的数据
* @retval 无
*/
void bbus_i2c_send_byte(uint8_t lun, const uint8_t data);
/**
* @brief I2C读取一个字节
* @param lun: I2C总线号
* @param ack: ack=1时,发送ack; ack=0时,发送nack
* @retval 接收到的数据
*/
uint8_t bbus_i2c_read_byte(uint8_t lun, uint8_t ack);
/**
* @brief 检查从设备地址是否正确
* @param lun: I2C总线号
* @param slave_addr: 从设备地址
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_check_address(uint8_t lun, uint8_t slave_addr, uint32_t timeout);
/**
* @brief 软件I2C连续写数据
* @param lun: I2C总线号
* @param salve_adress: 从设备地址
* @param reg_address: 寄存器地址
* @param data: 存储读取数据的缓冲区
* @param len: 要读取的数据长度
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_write_data(uint8_t lun, uint8_t slave_addr, uint8_t reg_address, const uint8_t *data, uint8_t len, uint32_t timeout);
/**
* @brief 软件I2C连续读数据
* @param lun: I2C总线号
* @param salve_adress: 从设备地址
* @param reg_address: 寄存器地址
* @param data: 存储读取数据的缓冲区
* @param len: 要读取的数据长度
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_read_data(uint8_t lun, uint8_t slave_addr, uint8_t reg_address, uint8_t *data, uint8_t len, uint32_t timeout);
/**
* @brief 直接读 N 字节序列(无寄存器地址阶段)
* @param lun: I2C总线号
* @param salve_adress: 从设备地址
* @param data: 存储读取数据的缓冲区
* @param len: 要读取的数据长度
* @param timeout: 超时时间ms
* @retval 0,读取成功;1,读取失败
*/
uint8_t bbus_i2c_read_seq(uint8_t lun, uint8_t slave_addr, uint8_t *data, uint8_t len, uint32_t timeout);
#endif
10.3 bbus_i2c_port.c
#include "bbus_i2c_port.h"
#include "main.h"
#include "delay.h"
/**
* @brief 软件I2C延时函数
* @param xus: 延时时间,单位us
* @retval 无
*/
void bbus_i2c_port_delay_us(uint32_t xus)
{
delay_us(xus);
}
/**
* @brief 获取当前系统时间,单位ms
* @param 无
* @retval 当前系统时间,单位ms
*/
uint32_t bbus_i2c_port_tick_get(void)
{
return HAL_GetTick();
}
/**
* @brief 软件I2C端口初始化
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_init(uint8_t lun)
{
switch (lun)
{
case 0:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
break;
case 1:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
break;
default:
break;
}
}
/**
* @brief 设置I2C SDA引脚电平
* @param lun: I2C总线号
* @param level: SDA引脚电平,1:高电平,0:低电平
* @retval 无
*/
void bbus_i2c_port_sda_set(uint8_t lun, uint8_t level)
{
switch (lun)
{
case 0:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
case 1:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
default:
break;
}
}
/**
* @brief 设置I2C SCL引脚电平
* @param lun: I2C总线号
* @param level: SCL引脚电平,1:高电平,0:低电平
* @retval 无
*/
void bbus_i2c_port_scl_set(uint8_t lun, uint8_t level)
{
switch (lun)
{
case 0:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
case 1:
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, (level == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
break;
default:
break;
}
}
/**
* @brief 获取I2C SDA引脚电平
* @param lun: I2C总线号
* @retval SDA引脚电平,1:高电平,0:低电平
*/
uint8_t bbus_i2c_port_sda_get(uint8_t lun)
{
uint8_t ret = 0;
switch (lun)
{
case 0:
ret = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_SET) ? 1 : 0;
break;
case 1:
ret = (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_9) == GPIO_PIN_SET) ? 1 : 0;
break;
default:
break;
}
return ret;
}
/**
* @brief 设置I2C SDA引脚为输出模式
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_sda_set_out(uint8_t lun)
{
switch (lun)
{
case 0:
break;
case 1:
break;
default:
break;
}
}
/**
* @brief 设置I2C SDA引脚为输入模式
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_sda_set_in(uint8_t lun)
{
switch (lun)
{
case 0:
break;
case 1:
break;
default:
break;
}
}
/**
* @brief 进入临界区
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_enter_critical(uint8_t lun)
{
switch (lun)
{
case 0:
break;
case 1:
break;
default:
break;
}
}
/**
* @brief 退出临界区
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_exit_critical(uint8_t lun)
{
switch (lun)
{
case 0:
break;
case 1:
break;
default:
break;
}
}
10.4 bbus_i2c_port.h
#ifndef BBUS_I2C_PORT_H
#define BBUS_I2C_PORT_H
#include <stdint.h>
#include <stdio.h>
#define BBUS_I2C_LOG(...) printf(__VA_ARGS__)
#define BBUS_I2C_BUS_NUM 2 // 总共支持的 I2C 总线数量
/**
* @brief 软件I2C端口初始化
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_init(uint8_t lun);
/**
* @brief 软件I2C延时函数
* @param xus: 延时时间,单位us
* @retval 无
*/
void bbus_i2c_port_delay_us(uint32_t xus);
/**
* @brief 获取当前系统时间,单位ms
* @param 无
* @retval 当前系统时间,单位ms
*/
uint32_t bbus_i2c_port_tick_get(void);
/**
* @brief 设置I2C SDA引脚电平
* @param lun: I2C总线号
* @param level: SDA引脚电平,1:高电平,0:低电平
* @retval 无
*/
void bbus_i2c_port_sda_set(uint8_t lun, uint8_t level);
/**
* @brief 设置I2C SCL引脚电平
* @param lun: I2C总线号
* @param level: SCL引脚电平,1:高电平,0:低电平
* @retval 无
*/
void bbus_i2c_port_scl_set(uint8_t lun, uint8_t level);
/**
* @brief 获取I2C SDA引脚电平
* @param lun: I2C总线号
* @retval SDA引脚电平,1:高电平,0:低电平
*/
uint8_t bbus_i2c_port_sda_get(uint8_t lun);
/**
* @brief 设置I2C SDA引脚为输出模式
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_sda_set_out(uint8_t lun);
/**
* @brief 设置I2C SDA引脚为输入模式
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_sda_set_in(uint8_t lun);
/**
* @brief 进入临界区
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_enter_critical(uint8_t lun);
/**
* @brief 退出临界区
* @param lun: I2C总线号
* @retval 无
*/
void bbus_i2c_port_exit_critical(uint8_t lun);
#endif
更多推荐




所有评论(0)