一、引言:为什么需要标准化的软件 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) 指定地址写

IIC指定地址写 —— 波形图

阶段 物理动作 电平变化逻辑 总线状态
① 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) 指定地址读

IIC指定地址写 —— 波形图

阶段 物理动作 电平变化逻辑 总线状态
① 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:PB6SDA: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添加文件

  1. 添加一个组 命名为AT_Driver,并添加at_driver.cat_port.c和头文件

  1. 点击"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 微秒延时驱动

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 驱动框架通过分层设计实现了以下核心优势:

  1. 硬件无关性:移植仅需修改<font style="color:rgb(0, 0, 0);background-color:rgba(0, 0, 0, 0);">bbus_i2c_port.c/h</font>,适配不同 MCU 平台
  2. 多总线多速度:支持多路 I2C 总线并行工作,每条总线可独立配置通信速度
  3. 灵活易用:提供 3 个通用对外接口,同时支持底层函数自定义通信流程
  4. 鲁棒性强:内置超时保护、ACK 校验、临界区保护(RTOS 适配)
  5. 资源占用低:静态内存分配,无动态内存依赖,适合嵌入式小型系统

十、源码

扫描下方二维码加入嵌入式技术交流群,即可获取源码压缩包,群内同步答疑驱动开发、移植问题及后续版本更新,同时有更多嵌入式问题也欢迎讨论~ +q:181921938


源码遵循开源TIM协议,如果这个项目对你的物联网开发有帮助,请给它一个 ⭐ !:

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

Logo

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

更多推荐