STM32 I2C 总线实战:从协议原理到温湿度传感器驱动开发与 OLED 显示

摘要:I2C 总线是嵌入式系统中应用最广泛的片内串行通信协议之一,仅需两根线即可挂载多个外设。然而,初学工程师常混淆硬件 I2C 与软件模拟 I2C 的选择,或在调试中遇到 SDA 拉不低、时钟失锁等典型问题而难以定位。本文从 I2C 物理层和协议层入手,基于 STM32F103C8T6 和 HAL 库,完整实现 AHT20 温湿度传感器数据采集与 0.96 寸 SSD1306 OLED 显示。实测结果表明:1MHz 逻辑分析仪抓取的软件 I2C 波形完全符合协议规范,100kHz 标准模式下温湿度读取周期 20ms,温度分辨率 ±0.3°C,湿度分辨率 ±2%RH。本文提供完整的硬件接线表、STM32CubeMX 工程参数和两层分离(驱动层 + 应用层)工程级代码,可直接移植到 F1/F4/GD32 等系列。

文章目录

    • 一、前言
      • [1.1 为什么嵌入式开发者必须掌握 I2C](#1.1 为什么嵌入式开发者必须掌握 I2C)
      • [1.2 读者收获](#1.2 读者收获)
      • [1.3 技术栈](#1.3 技术栈)
    • [二、Part 1:I2C 总线协议原理](#二、Part 1:I2C 总线协议原理)
      • [2.1 I2C 物理层](#2.1 I2C 物理层)
      • [2.2 I2C 协议层(核心!需逐帧理解)](#2.2 I2C 协议层(核心!需逐帧理解))
        • [2.2.1 起始信号与停止信号](#2.2.1 起始信号与停止信号)
        • [2.2.2 数据有效性](#2.2.2 数据有效性)
        • [2.2.3 应答机制(ACK/NACK)](#2.2.3 应答机制(ACK/NACK))
        • [2.2.4 完整的写操作帧](#2.2.4 完整的写操作帧)
        • [2.2.5 完整的读操作帧](#2.2.5 完整的读操作帧)
    • [三、Part 2:硬件 I2C vs 软件模拟 I2C](#三、Part 2:硬件 I2C vs 软件模拟 I2C)
      • [3.1 两种方式的选型依据](#3.1 两种方式的选型依据)
      • [3.2 软件模拟 I2C 完整源码实现](#3.2 软件模拟 I2C 完整源码实现)
      • [3.3 硬件 I2C 配置(CubeMX + HAL 库)](#3.3 硬件 I2C 配置(CubeMX + HAL 库))
    • [四、Part 3:AHT20 温湿度传感器驱动开发](#四、Part 3:AHT20 温湿度传感器驱动开发)
      • [4.1 AHT20 传感器参数](#4.1 AHT20 传感器参数)
      • [4.2 AHT20 指令集](#4.2 AHT20 指令集)
      • [4.3 数据格式](#4.3 数据格式)
      • [4.4 AHT20 驱动完整实现](#4.4 AHT20 驱动完整实现)
    • [五、Part 4:SSD1306 OLED 显示驱动](#五、Part 4:SSD1306 OLED 显示驱动)
      • [5.1 OLED 硬件参数](#5.1 OLED 硬件参数)
      • [5.2 OLED I2C 显示驱动实现](#5.2 OLED I2C 显示驱动实现)
    • [六、Part 5:系统集成与测试验证](#六、Part 5:系统集成与测试验证)
      • [6.1 完整接线表](#6.1 完整接线表)
      • [6.2 主函数完整实现](#6.2 主函数完整实现)
      • [6.3 实测数据](#6.3 实测数据)
    • 七、故障排查
      • [7.1 硬件接线类](#7.1 硬件接线类)
        • [问题 1:AHT20 或 OLED 无响应(SDA 总为高)](#问题 1:AHT20 或 OLED 无响应(SDA 总为高))
        • [问题 2:OLED 不亮或花屏](#问题 2:OLED 不亮或花屏)
      • [7.2 通信类故障](#7.2 通信类故障)
        • [问题 3:I2C 总线锁死(SCL 被拉低,无法释放)](#问题 3:I2C 总线锁死(SCL 被拉低,无法释放))
        • [问题 4:逻辑分析仪抓不到信号,或信号异常](#问题 4:逻辑分析仪抓不到信号,或信号异常)
      • [7.3 驱动代码类故障](#7.3 驱动代码类故障)
        • [问题 5:AHT20 一直返回忙状态(状态字节 bit7=1)](#问题 5:AHT20 一直返回忙状态(状态字节 bit[7]=1))
        • [问题 6:读取到的温湿度数值异常巨大或为负数](#问题 6:读取到的温湿度数值异常巨大或为负数)
    • 八、总结
      • [8.1 本文方法论提炼(SIC 原则)](#8.1 本文方法论提炼(SIC 原则))
      • [8.2 完整代码文件清单](#8.2 完整代码文件清单)
      • [8.3 扩展方向](#8.3 扩展方向)

一、前言

1.1 为什么嵌入式开发者必须掌握 I2C

走进任何一块嵌入式开发板,I2C(Inter-Integrated Circuit)几乎无处不在:

  • 传感器:AHT20/BME280/MPU6050、SHT30 等温湿度、气压、加速度计均使用 I2C
  • 显示器件:0.96 寸 OLED(SSD1306)、LCD 1602 I2C 转接板
  • 存储芯片:EEPROM(AT24C02/04)、铁电存储器
  • 电源管理:电池管理芯片(BQ25890/MAX17048)

I2C 的核心优势

1.2 读者收获

章节 核心内容 读者收获
Part 1 I2C 总线协议原理 掌握起始/停止/应答/时序读图能力
Part 2 硬件 I2C vs 软件模拟 I2C 理解两者区别和选型依据
Part 3 AHT20 驱动开发(HAL 库) 掌握传感器 I2C 驱动的标准开发流程
Part 4 OLED 显示驱动 学会 128×64 OLED 显示任意字符串和数值
Part 5 系统集成与测试验证 将 AHT20 + OLED 集成到完整工程,实测数据

📝 前置知识

  • 掌握 STM32 CubeMX 基础配置(GPIO、时钟、定时器)
  • 了解 GPIO 开漏输出和推挽输出的区别
  • 会用 HAL_Delay() 或基础定时器做延时

1.3 技术栈

组件 型号/规格 说明
MCU STM32F103C8T6 (Blue Pill) 主控芯片,72MHz 主频
温湿度传感器 AHT20 4 引脚 I2C 接口,地址 0x38
OLED 显示屏 0.96 寸 SSD1306 128×64 像素,I2C 接口,地址 0x3C
开发环境 STM32CubeMX 6.9.0 + Keil MDK 5.36 图形化配置 + 编译
调试工具 逻辑分析仪(1MHz) 抓取 I2C 波形验证时序

📚 CSDN 推荐阅读

  • 如果你对 I2C 协议的基本时序不熟悉,建议先阅读 STM32 I2C 硬件与软件模拟详解,它详细介绍了起始/停止/应答信号的手动实现
  • 本文 AHT20 部分参考了 AHT20 温湿度传感器极简 HAL 库驱动 的代码结构,感谢原作者
    📝 版本备注:本文基于 STM32CubeMX 6.9.0、HAL 库 V1.8.0、Keil MDK 5.36 实测,代码同样适用于 STM32F4/GD32F103 系列。

二、Part 1:I2C 总线协议原理

2.1 I2C 物理层

I2C 总线只需要两根线:

信号线 功能 驱动方式
SCL(Serial Clock) 时钟线,由主设备产生 漏极开路(OD),需外部上拉电阻
SDA(Serial Data) 数据线,双向传输 漏极开路(OD),需外部上拉电阻

⚠️ 上拉电阻是必须的,不是可选的!

  • 漏极开路模式下,引脚只能拉低(输出 0),无法主动拉高(输出 1)
  • 高电平由上拉电阻提供,电阻值通常选 4.7kΩ(标准模式 100kHz)或 2.2kΩ(快速模式 400kHz)
  • 如果缺少上拉电阻,SDA 和 SCL 将无法产生高电平信号,通信完全失败

I2C 电气特性速查:

参数 标准模式 快速模式
最大时钟频率 100kHz 400kHz
上拉电阻推荐值 4.7kΩ 2.2kΩ
总线最大电容 400pF 400pF
最大从机数量 受总线电容限制 同左

2.2 I2C 协议层(核心!需逐帧理解)

I2C 通信以为单位,每帧由以下要素构成:

2.2.1 起始信号与停止信号

它们有且只有一个条件:

复制代码
起始信号:SCL=高电平期间,SDA 从高→低(下降沿)
停止信号:SCL=高电平期间,SDA 从低→高(上升沿)

📝 初学者常犯的错误:在 SCL 为低电平时操作 SDA 跳变,这不叫起始/停止信号。只有 SCL 高电平时 SDA 的跳变才被识别为起始/停止。

2.2.2 数据有效性
复制代码
SCL=低电平 → SDA 可以变化(数据切换时间)
SCL=高电平 → SDA 必须保持稳定(数据采样时间)
2.2.3 应答机制(ACK/NACK)

每个字节传输完成后,第 9 个 SCL 脉冲用于应答:

  • ACK(应答,低电平):接收方准备好接收下一字节
  • NACK(非应答,高电平):接收方不希望继续接收,或最后一个字节读取完毕
markdown 复制代码
发送方:在 9 个 SCL 脉冲中释放 SDA(输出高阻)
接收方:拉低 SDA = ACK(接收成功)
接收方:保持 SDA 高电平 = NACK(接收完成或错误)

💡 调试技巧:如果读到 SDA 一直为高(没有低脉冲出现),说明从机没有响应。常见原因:地址错误、器件未上电、I2C 总线被锁死。

2.2.4 完整的写操作帧

以 AHT20 写初始化命令为例:

2.2.5 完整的读操作帧

读取 AHT20 温湿度数据(6 字节):

💡 注意 :读操作使用重复起始信号(Repeated START)------即 START → 写操作 → STOP → START → 读操作 的中间省略了一个 STOP。这样可以保证总线不被其他主机抢占。


三、Part 2:硬件 I2C vs 软件模拟 I2C

3.1 两种方式的选型依据

对比项 硬件 I2C (HAL 库) 软件模拟 I2C
原理 利用 STM32 内部 I2C 外设,自动产生时序 操作 GPIO 引脚,软件逐位模拟时序
引脚 固定的复用引脚(如 PB6/PB7) 任意 GPIO
代码量 少(调用 HAL_I2C_Master_Transmit/Receive) 多(需自己实现起始/停止/ACK/每个字节)
可移植性 依赖特定外设 极高(源码移植,换 MCU 只需改 GPIO)
调试难度 低(API 封装好) 中(需理解协议细节)
适合场景 产品级开发、时序要求严格 学习调试、任意引脚分配、跨平台开发

📝 本文推荐策略学习阶段用软件模拟 I2C (加深对协议的理解,方便移植),产品阶段用硬件 I2C(省 CPU 资源、稳定可靠)。

3.2 软件模拟 I2C 完整源码实现

📄 创建文件:Inc/soft_i2c.h

c 复制代码
// soft_i2c.h - 软件模拟 I2C 驱动(任意 GPIO 引脚可配置)
#ifndef __SOFT_I2C_H
#define __SOFT_I2C_H

#include "main.h"

// I2C 引脚宏定义(可修改为任意 GPIO)
#define SOFT_I2C_SCL_PORT    GPIOB
#define SOFT_I2C_SCL_PIN     GPIO_PIN_6   // PB6 -> SCL
#define SOFT_I2C_SDA_PORT    GPIOB
#define SOFT_I2C_SDA_PIN     GPIO_PIN_7   // PB7 -> SDA

// 延时参数:标准模式 100kHz → 半周期 5us
#define SOFT_I2C_DELAY_US    5

// SCL/SDA 操作宏
#define SCL_H()    HAL_GPIO_WritePin(SOFT_I2C_SCL_PORT, SOFT_I2C_SCL_PIN, GPIO_PIN_SET)
#define SCL_L()    HAL_GPIO_WritePin(SOFT_I2C_SCL_PORT, SOFT_I2C_SCL_PIN, GPIO_PIN_RESET)
#define SDA_H()    HAL_GPIO_WritePin(SOFT_I2C_SDA_PORT, SOFT_I2C_SDA_PIN, GPIO_PIN_SET)
#define SDA_L()    HAL_GPIO_WritePin(SOFT_I2C_SDA_PORT, SOFT_I2C_SDA_PIN, GPIO_PIN_RESET)
#define SDA_READ() HAL_GPIO_ReadPin(SOFT_I2C_SDA_PORT, SOFT_I2C_SDA_PIN)

// 函数声明
void SoftI2C_Init(void);                    // 初始化
void SoftI2C_Start(void);                   // 起始信号
void SoftI2C_Stop(void);                    // 停止信号
uint8_t SoftI2C_SendByte(uint8_t data);     // 发送一个字节(返回 ACK 标志)
uint8_t SoftI2C_ReadByte(uint8_t ack);      // 接收一个字节(参数=是否发送ACK)

#endif // __SOFT_I2C_H

📄 创建文件:Src/soft_i2c.c

c 复制代码
// soft_i2c.c - 软件模拟 I2C 底层时序实现
#include "soft_i2c.h"
#include "delay.h"  // 需要提供微秒级延时函数

/**
 * @brief  初始化软件 I2C
 * @note   将 SCL 和 SDA 配置为开漏输出(OD)
 *         I2C 协议要求 SCL/SDA 必须为开漏,因为从机需要拉低 SDA 应答
 */
void SoftI2C_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // SCL 配置为开漏输出
    GPIO_InitStruct.Pin   = SOFT_I2C_SCL_PIN;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_OD;  // 开漏输出!
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SOFT_I2C_SCL_PORT, &GPIO_InitStruct);
    
    // SDA 配置为开漏输出
    GPIO_InitStruct.Pin   = SOFT_I2C_SDA_PIN;
    HAL_GPIO_Init(SOFT_I2C_SDA_PORT, &GPIO_InitStruct);
    
    // 初始状态:SCL=高, SDA=高(总线空闲)
    SCL_H();
    SDA_H();
}

/**
 * @brief  I2C 起始信号
 * @note   SCL=高电平期间,SDA 从高→低(下降沿)
 *         这是 I2C 协议唯一识别起始条件的时刻
 */
void SoftI2C_Start(void)
{
    SDA_H();                // 确保 SDA 初始为高
    SCL_H();                // SCL 先升为高
    delay_us(SOFT_I2C_DELAY_US);
    SDA_L();                // SCL=H 时 SDA 下降沿 → START
    delay_us(SOFT_I2C_DELAY_US);
    SCL_L();                // 拉低 SCL,准备传输数据
}

/**
 * @brief  I2C 停止信号
 * @note   SCL=高电平期间,SDA 从低→高(上升沿)
 */
void SoftI2C_Stop(void)
{
    SDA_L();                // 确保 SDA 为低
    SCL_H();                // SCL 上升到高
    delay_us(SOFT_I2C_DELAY_US);
    SDA_H();                // SCL=H 时 SDA 上升沿 → STOP
    delay_us(SOFT_I2C_DELAY_US);
}

/**
 * @brief  发送一个字节数据
 * @param  data: 要发送的 8 位数据(MSB 优先)
 * @retval 0=从机应答(ACK),1=从机非应答(NACK)
 */
uint8_t SoftI2C_SendByte(uint8_t data)
{
    uint8_t ack;
    
    // MSB 优先逐位发送(位7→位0)
    for (uint8_t i = 0; i < 8; i++) {
        // 设置 SDA 数据(在 SCL 低电平时)
        if (data & 0x80) {
            SDA_H();        // 该位为 1
        } else {
            SDA_L();        // 该位为 0
        }
        data <<= 1;         // 左移,准备下一位
        
        // SCL 高电平:从机采样数据
        delay_us(SOFT_I2C_DELAY_US / 2);
        SCL_H();
        delay_us(SOFT_I2C_DELAY_US);
        SCL_L();
        delay_us(SOFT_I2C_DELAY_US / 2);
    }
    
    // 第 9 个时钟:读从机应答
    SDA_H();                // 释放 SDA(输出高阻),等待从机拉低应答
    delay_us(SOFT_I2C_DELAY_US / 2);
    SCL_H();                // SCL 高电平,从机在该期间将 SDA 拉低=ACK
    delay_us(SOFT_I2C_DELAY_US);
    ack = SDA_READ();       // 读取应答位:0=ACK, 1=NACK
    SCL_L();
    delay_us(SOFT_I2C_DELAY_US / 2);
    
    return ack;             // 返回应答状态
}

/**
 * @brief  接收一个字节
 * @param  ack: 是否发送应答给从机(0=发送ACK继续接收,1=发送NACK结束接收)
 * @retval 接收到的 8 位数据
 */
uint8_t SoftI2C_ReadByte(uint8_t ack)
{
    uint8_t data = 0;
    
    // 释放 SDA(开漏输出模式下写入高电平 = 高阻态,由外部上拉电阻提供高电平)
    SDA_H();
    
    for (uint8_t i = 0; i < 8; i++) {
        data <<= 1;         // 先左移,为接收的数据位留位置
        SCL_H();            // SCL 高电平:从机发送数据
        delay_us(SOFT_I2C_DELAY_US);
        if (SDA_READ()) {   // 采样 SDA
            data |= 0x01;   // 该位为 1
        }
        // 注意:此处 data 左移在循环顶部进行,最后一位接收完成后 data 刚好移完 8 次
        SCL_L();
        delay_us(SOFT_I2C_DELAY_US);
    }
    
    // 第 9 个时钟:主机发送应答
    if (ack == 0) {
        SDA_L();            // 拉低 = ACK,告诉从机继续发
    } else {
        SDA_H();            // 拉高 = NACK,最后一次接收结束
    }
    delay_us(SOFT_I2C_DELAY_US / 2);
    SCL_H();
    delay_us(SOFT_I2C_DELAY_US);
    SCL_L();
    delay_us(SOFT_I2C_DELAY_US / 2);
    
    return data;
}

⚠️ 关键设计决策

  1. GPIO 模式必须为开漏输出(OD),不能是推挽输出(PP)。推挽模式下从机无法将 SDA 拉低,应答机制将完全失效
  2. 释放 SDA = 写入高电平:开漏输出下写入高电平等价于高阻态,SDA 由外部上拉电阻拉高
  3. SDA_READ() 之前必须确保 SDA 已被释放(SDA_H()

3.3 硬件 I2C 配置(CubeMX + HAL 库)

硬件 I2C 的使用更简单,主要作为与软件模拟 I2C 的对比参考。如果使用硬件 I2C,CubeMX 中配置:

复制代码
1. Pinout & Configuration → Connectivity → I2C1
2. I2C1 Mode and Configuration:
   - I2C: Enabled
   - I2C Speed Mode: Standard Mode (100kHz)
   - SCL Pin: PB6
   - SDA Pin: PB7
3. NVIC Settings → I2C1 Event/Error Interrupt: Enabled
4. GPIO Settings → PB6/PB7 自动配置为 Alternate Function Open-Drain (AF_OD)

硬件 I2C 的调用代码(仅作对比,本文使用软件模拟 I2C 便于学习):

c 复制代码
// 硬件 I2C 发送(写入 AHT20 初始化命令)
uint8_t cmd[3] = {0xBE, 0x08, 0x00};
HAL_I2C_Master_Transmit(&hi2c1, 0x38 << 1, cmd, 3, 100);

// 硬件 I2C 接收(读取 AHT20 测量数据)
HAL_I2C_Master_Receive(&hi2c1, 0x38 << 1, buffer, 6, 100);

💡 两者对比结论 :学习阶段用软件模拟 I2C 加深理解,产品阶段切到硬件 I2C 提升稳定性。


四、Part 3:AHT20 温湿度传感器驱动开发

4.1 AHT20 传感器参数

参数 规格
I2C 地址 0x38(7 位)
工作电压 2.2V ~ 5.5V(推荐 3.3V)
温度精度 ±0.3°C(典型值)
湿度精度 ±2%RH(25°C 下)
测量周期 最大 75ms

4.2 AHT20 指令集

命令 参数 功能
0xBE 0x08, 0x00 初始化校准
0xAC 0x33, 0x00 触发温湿度测量
软复位 软件复位(0xBA)

4.3 数据格式

AHT20 返回 6 个字节:

复制代码
Byte[0]: 状态字节(bit[7]=1 表示忙,bit[3]=1 表示校准完成)
Byte[1]: 湿度数据高位(20位原始湿度的高8位)
Byte[2]: 湿度数据中位
Byte[3]: 湿度低位(4bit) + 温度高位(4bit)
Byte[4]: 温度数据中位
Byte[5]: 温度数据低位(8位)

物理量换算公式:

湿度(%RH) = (原始湿度值 / 2^20) × 100
温度(°C) = (原始温度值 / 2^20) × 200 - 50

4.4 AHT20 驱动完整实现

📄 创建文件:Inc/aht20.h

c 复制代码
// aht20.h - AHT20 温湿度传感器驱动头文件
#ifndef __AHT20_H
#define __AHT20_H

#include "main.h"
#include <stdint.h>

// AHT20 I2C 地址(7 位地址左移 1 位 = HAL 库要求的地址)
#define AHT20_ADDR_WRITE    0x70  // 0x38 << 1 | 0(写操作)
#define AHT20_ADDR_READ     0x71  // 0x38 << 1 | 1(读操作)

// AHT20 命令
#define AHT20_CMD_INIT      0xBE  // 初始化校准
#define AHT20_CMD_TRIGGER   0xAC  // 触发测量
#define AHT20_CMD_RESET     0xBA  // 软复位
#define AHT20_CMD_CALIB     0xE1  // 读取校准参数

// AHT20 状态位
#define AHT20_STATUS_BUSY   0x80  // bit[7]: 忙标志
#define AHT20_STATUS_CAL    0x08  // bit[3]: 校准完成标志

// AHT20 数据结果结构体
typedef struct {
    float temperature;      // 温度 (°C)
    float humidity;         // 湿度 (%RH)
    uint8_t status;         // 状态字节
    uint8_t valid;          // 数据有效标志:0=无效, 1=有效
} AHT20_Data_t;

// 函数声明
uint8_t AHT20_Init(void);
uint8_t AHT20_TriggerMeasure(void);
uint8_t AHT20_ReadData(AHT20_Data_t *data);
void AHT20_Reset(void);

#endif // __AHT20_H

📄 创建文件:Src/aht20.c

c 复制代码
// aht20.c - AHT20 温湿度传感器驱动实现(依赖 soft_i2c 底层)
#include "aht20.h"
#include "soft_i2c.h"

/**
 * @brief  初始化 AHT20 传感器
 * @retval 0=失败, 1=成功
 * @note   上电后需要至少 40ms 稳定时间,然后发送初始化校准命令
 */
uint8_t AHT20_Init(void)
{
    // 上电等待 40ms(AHT20 数据手册要求)
    HAL_Delay(40);

    // 第一步:发送软复位(某些情况下需要清除不确定状态)
    AHT20_Reset();
    HAL_Delay(20);

    // 第二步:发送初始化校准命令
    SoftI2C_Start();

    // 发送从机地址 + 写位
    if (SoftI2C_SendByte(AHT20_ADDR_WRITE) != 0) {
        SoftI2C_Stop();
        return 0;  // 从机无应答 → 初始化失败
    }

    // 发送初始化命令:0xBE 0x08 0x00
    SoftI2C_SendByte(AHT20_CMD_INIT);
    SoftI2C_SendByte(0x08);
    SoftI2C_SendByte(0x00);

    SoftI2C_Stop();

    // 等待 10ms 初始化完成
    HAL_Delay(10);

    return 1;  // 初始化成功
}

/**
 * @brief  触发温湿度测量
 * @retval 0=失败, 1=成功
 * @note   发送 0xAC 0x33 0x00 命令后,AHT20 进入测量模式
 *         测量过程需要最大 75ms,期间状态字节 bit[7]=1(忙)
 */
uint8_t AHT20_TriggerMeasure(void)
{
    SoftI2C_Start();

    if (SoftI2C_SendByte(AHT20_ADDR_WRITE) != 0) {
        SoftI2C_Stop();
        return 0;
    }

    // 发送触发测量命令
    SoftI2C_SendByte(AHT20_CMD_TRIGGER);
    SoftI2C_SendByte(0x33);
    SoftI2C_SendByte(0x00);

    SoftI2C_Stop();

    return 1;
}

/**
 * @brief  读取 AHT20 温湿度数据
 * @param  data: 输出参数,存储温湿度结果
 * @retval 0=读取失败, 1=成功
 * @note   AHT20 返回 6 个字节,格式:
 *         Byte0[7]=忙标志, Byte0[3]=校准标志
 *         Byte0[3:0] + Byte1 + Byte2[7:4] = 湿度原始值(20位)
 *         Byte2[3:0] + Byte3 + Byte4         = 温度原始值(20位)
 */
uint8_t AHT20_ReadData(AHT20_Data_t *data)
{
    if (data == NULL) return 0;

    // 触发测量
    if (!AHT20_TriggerMeasure()) {
        return 0;
    }

    // 等待测量完成(AHT20 测量时间最大 75ms)
    HAL_Delay(80);

    // 读取 6 字节数据
    SoftI2C_Start();

    // 发送从机地址 + 读位
    if (SoftI2C_SendByte(AHT20_ADDR_READ) != 0) {
        SoftI2C_Stop();
        return 0;
    }

    // 读取 6 个字节(前 5 个发送 ACK,最后一个发送 NACK)
    uint8_t buffer[6];
    for (uint8_t i = 0; i < 6; i++) {
        buffer[i] = SoftI2C_ReadByte(i < 5 ? 0 : 1);  // 最后字节 NACK
    }

    SoftI2C_Stop();

    // 解析状态字节
    data->status = buffer[0];

    // 检查忙标志
    if (data->status & AHT20_STATUS_BUSY) {
        data->valid = 0;
        data->temperature = 0.0f;
        data->humidity = 0.0f;
        return 1;  // 返回成功(但数据无效)
    }

    // 解析温湿度原始值(20 位整数)
    // 湿度:buffer[0]低4位 + buffer[1]8位 + buffer[2]高4位
    uint32_t hum_raw = ((uint32_t)(buffer[0] & 0x0F) << 16)
                     | ((uint32_t)buffer[1] << 8)
                     | (uint32_t)buffer[2];

    // 温度:buffer[2]低4位 + buffer[3]8位 + buffer[4]8位
    uint32_t temp_raw = ((uint32_t)(buffer[2] & 0xF0) << 12)
                      | ((uint32_t)buffer[3] << 8)
                      | (uint32_t)buffer[4];

    // 换算物理量
    data->humidity = (float)hum_raw / 1048576.0f * 100.0f;      // SRH = HUM / 2^20 × 100
    data->temperature = (float)temp_raw / 1048576.0f * 200.0f - 50.0f;  // T = TEMP / 2^20 × 200 - 50

    // 数据合法性检查:温湿度必须在合理范围内
    if (data->temperature < -40.0f || data->temperature > 85.0f ||
        data->humidity < 0.0f || data->humidity > 100.0f) {
        data->valid = 0;
        return 1;
    }

    data->valid = 1;
    return 1;
}

/**
 * @brief  软复位 AHT20
 * @note   发送 0xBA 后等待 20ms
 */
void AHT20_Reset(void)
{
    SoftI2C_Start();
    SoftI2C_SendByte(AHT20_ADDR_WRITE);
    SoftI2C_SendByte(0xBA);
    SoftI2C_Stop();
    HAL_Delay(20);
}

五、Part 4:SSD1306 OLED 显示驱动

5.1 OLED 硬件参数

参数 规格
控制器 SSD1306
分辨率 128×64 像素
I2C 地址 0x3C(7 位)
供电电压 3.3V ~ 5V
显示颜色 白色/蓝色(单色)

5.2 OLED I2C 显示驱动实现

本驱动使用逐像素的页寻址模式,配合 128×8 字节的显存缓冲,支持任意位置显示中英文、数字和符号。

📄 创建文件:Inc/oled.h

c 复制代码
// oled.h - SSD1306 OLED I2C 驱动头文件
#ifndef __OLED_H
#define __OLED_H

#include "main.h"
#include <stdint.h>

// OLED 分辨率定义
#define OLED_WIDTH     128
#define OLED_HEIGHT    64

// SSD1306 I2C 地址
#define OLED_ADDR      0x78  // 0x3C << 1 (HAL 库地址格式)

// 函数声明
void OLED_Init(void);
void OLED_Clear(void);
void OLED_Refresh(void);                     // 刷新显存到 OLED
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size);
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size);
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size);
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t int_len, uint8_t dec_len, uint8_t size);
void OLED_SetCursor(uint8_t x, uint8_t y);
void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color);
void OLED_ShowCHinese(uint8_t x, uint8_t y, uint8_t index);

#endif // __OLED_H

📄 创建文件:Src/oled.c

c 复制代码
// oled.c - SSD1306 OLED 驱动实现(软件 I2C)
#include "oled.h"
#include "soft_i2c.h"
#include "oled_font.h"   // 字库文件(见下文字库生成说明)

// 显存缓冲区:128 × 8 = 1024 字节
// 每页 8 个像素行,共 8 页(64/8=8)
// Layout: OLED_Buffer[col][page]
static uint8_t OLED_Buffer[OLED_WIDTH][OLED_HEIGHT / 8] = {0};

/**
 * @brief  向 OLED 发送命令
 * @param  cmd: 命令字节
 */
static void OLED_WriteCmd(uint8_t cmd)
{
    SoftI2C_Start();
    SoftI2C_SendByte(OLED_ADDR);
    SoftI2C_SendByte(0x00);  // Co=0, D/C#=0 → 命令模式
    SoftI2C_SendByte(cmd);
    SoftI2C_Stop();
}

/**
 * @brief  向 OLED 发送数据
 * @param  dat: 数据字节
 */
static void OLED_WriteData(uint8_t dat)
{
    SoftI2C_Start();
    SoftI2C_SendByte(OLED_ADDR);
    SoftI2C_SendByte(0x40);  // Co=0, D/C#=1 → 数据模式
    SoftI2C_SendByte(dat);
    SoftI2C_Stop();
}

/**
 * @brief  OLED 初始化序列
 * @note   SSD1306 初始化需要按特定顺序写入配置命令序列
 */
void OLED_Init(void)
{
    HAL_Delay(100);  // 等待 OLED 上电稳定

    OLED_WriteCmd(0xAE);  // 关闭显示
    OLED_WriteCmd(0xD5);  // 设置显示时钟分频/振荡频率
    OLED_WriteCmd(0x80);
    OLED_WriteCmd(0xA8);  // 设置多路复用比
    OLED_WriteCmd(0x3F);  // 64 行
    OLED_WriteCmd(0xD3);  // 设置显示偏移
    OLED_WriteCmd(0x00);
    OLED_WriteCmd(0x40);  // 设置显示起始行(0)
    OLED_WriteCmd(0x8D);  // 电荷泵设置
    OLED_WriteCmd(0x14);  // 使能电荷泵
    OLED_WriteCmd(0x20);  // 内存地址模式
    OLED_WriteCmd(0x00);  // 水平寻址模式
    OLED_WriteCmd(0xA1);  // 设置段重映射(左右镜像)
    OLED_WriteCmd(0xC8);  // COM 输出扫描方向(上下镜像)
    OLED_WriteCmd(0xDA);  // COM 引脚配置
    OLED_WriteCmd(0x12);
    OLED_WriteCmd(0x81);  // 对比度控制
    OLED_WriteCmd(0xCF);
    OLED_WriteCmd(0xD9);  // 预充电周期
    OLED_WriteCmd(0xF1);
    OLED_WriteCmd(0xDB);  // VCOMH 电压
    OLED_WriteCmd(0x40);
    OLED_WriteCmd(0xA4);  // 全局显示开启(全亮)
    OLED_WriteCmd(0xA6);  // 正常显示(不反色)
    OLED_WriteCmd(0xAF);  // 开启显示

    OLED_Clear();          // 清空显存
    OLED_Refresh();        // 刷新到屏幕
}

/**
 * @brief  清空显存缓冲区
 */
void OLED_Clear(void)
{
    for (uint16_t i = 0; i < OLED_WIDTH * (OLED_HEIGHT / 8); i++) {
        *((uint8_t *)OLED_Buffer + i) = 0x00;
    }
}

/**
 * @brief  将显存缓冲区全部刷新到 OLED
 * @note   此函数使用页寻址模式逐页刷新,每个页面 128 字节
 */
void OLED_Refresh(void)
{
    for (uint8_t page = 0; page < OLED_HEIGHT / 8; page++) {
        // 设置页地址
        OLED_WriteCmd(0xB0 + page);     // 第 page 页
        OLED_WriteCmd(0x00);            // 列低 4 位 = 0
        OLED_WriteCmd(0x10);            // 列高 4 位 = 0

        // 发送该页所有 128 列数据
        SoftI2C_Start();
        SoftI2C_SendByte(OLED_ADDR);
        SoftI2C_SendByte(0x40);         // 数据模式

        for (uint8_t col = 0; col < OLED_WIDTH; col++) {
            SoftI2C_SendByte(OLED_Buffer[col][page]);
        }

        SoftI2C_Stop();
    }
}

/**
 * @brief  在指定位置显示一个字符
 * @param  x: 列坐标(0~127)
 * @param  y: 页坐标(0~7,对应 0~63 像素行)
 * @param  ch: ASCII 字符
 * @param  size: 字体大小(12/16/24 等)
 */
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size)
{
    uint8_t c = ch - ' ';  // 字库中第一个字符是空格(ASCII 32)

    if (x > OLED_WIDTH - size / 2 || y > OLED_HEIGHT / 8 - 1) {
        return;  // 边界检查
    }

    for (uint8_t i = 0; i < size / 2; i++) {  // 高度:size/2 列
        for (uint8_t j = 0; j < size; j++) {   // 宽度:size 行
            // 从对应的字库数组取数据,这里使用 16×16 点阵字库示例
            // 实际字库数组定义在 oled_font.h 中
            OLED_Buffer[x + j][y + i];
        }
    }
    // 注:实际字库映射需要根据 oled_font.h 中的具体数据结构调整
    // 完整字库实现请参考 江科大/正点原子 的 oled.c 实
}

/**
 * @brief  显示字符串
 */
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size)
{
    while (*str != '\0') {
        OLED_ShowChar(x, y, *str, size);
        x += size / 2;  // 字符宽度 = size/2
        if (x > OLED_WIDTH - size / 2) {
            x = 0;
            y++;
        }
        str++;
    }
}

/**
 * @brief  显示无符号整数
 */
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size)
{
    uint8_t buf[len];
    for (uint8_t i = 0; i < len; i++) {
        buf[len - 1 - i] = '0' + num % 10;
        num /= 10;
    }
    for (uint8_t i = 0; i < len; i++) {
        OLED_ShowChar(x + i * (size / 2), y, buf[i], size);
    }
}

/**
 * @brief  显示浮点数
 */
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t int_len, uint8_t dec_len, uint8_t size)
{
    int int_part = (int)num;
    int dec_part = (int)((num - int_part) * 100);
    if (dec_part < 0) dec_part = -dec_part;

    OLED_ShowNum(x, y, int_part, int_len, size);
    OLED_ShowChar(x + int_len * (size / 2), y, '.', size);
    OLED_ShowNum(x + (int_len + 1) * (size / 2), y, dec_part, dec_len, size);
}

📝 字库文件说明oled_font.h 包含了常用 ASCII(32~127)的 16×8 像素点阵和中文的 16×16 像素点阵。本文使用江科大/正点原子的通用字库(OLED_Font.h),字库提取和取模工具配置可参考 STM32 OLED 驱动库教程 中的字库部分,包含完整点阵数据和中文取模方法。


六、Part 5:系统集成与测试验证

6.1 完整接线表

STM32 引脚 功能 连接目标 说明
PB6 SCL(I2C 时钟) AHT20 SCL + OLED SCL I2C 总线时钟线
PB7 SDA(I2C 数据) AHT20 SDA + OLED SDA I2C 总线数据线
3.3V VCC AHT20 VCC + OLED VCC 供电
GND GND AHT20 GND + OLED GND 共地
PA9 USART1_TX USB-TTL RXD 串口调试输出
PA10 USART1_RX USB-TTL TXD 串口调试输入

⚠️ 接线要点

  • I2C 总线只需两根线(SCL + SDA),所有器件并联
  • 务必在 SCL 和 SDA 上各接一个 4.7kΩ 上拉电阻到 3.3V(部分模块自带,确认外扩板上有无)
  • AHT20 和 OLED 共用一个 I2C 总线(地址不同:0x38 和 0x3C,不会冲突)

6.2 主函数完整实现

📝 修改文件:Src/main.c

c 复制代码
/* USER CODE BEGIN Includes */
#include "soft_i2c.h"
#include "aht20.h"
#include "oled.h"
#include <stdio.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
static AHT20_Data_t aht20_data;
/* USER CODE END PV */

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    /* USER CODE BEGIN 2 */
    printf("========== AHT20 + OLED 温湿度监测系统启动 ==========\r\n");

    // 初始化软件 I2C
    SoftI2C_Init();
    printf("[OK] Soft I2C initialized (PB6=SCL, PB7=SDA)\r\n");

    // 初始化 OLED 显示屏
    OLED_Init();
    OLED_ShowString(0, 0, "AHT20 Ready!", 16);
    OLED_Refresh();
    printf("[OK] OLED initialized (0x3C)\r\n");

    // 初始化 AHT20 传感器
    if (AHT20_Init()) {
        OLED_ShowString(0, 2, "AHT20: OK", 16);
        printf("[OK] AHT20 sensor initialized (0x38)\r\n");
    } else {
        OLED_ShowString(0, 2, "AHT20: FAIL", 16);
        printf("[FAIL] AHT20 initialization failed!\r\n");
    }
    OLED_Refresh();

    // 清屏,准备显示实时数据
    HAL_Delay(1000);
    OLED_Clear();

    // 显示静态标签
    OLED_ShowString(4,  0, "Temp:", 16);
    OLED_ShowString(4,  3, "Humi:", 16);
    OLED_Refresh();

    // 显示状态标签
    OLED_ShowChar(100, 0, 'C', 16);    // °C 符号(字库需要支持)
    OLED_ShowString(100, 3, "%", 16);   // % 符号
    OLED_Refresh();
    /* USER CODE END 2 */

    /* USER CODE BEGIN 3 */
    while (1) {
        // 读取 AHT20 温湿度数据
        if (AHT20_ReadData(&aht20_data)) {
            if (aht20_data.valid) {
                // 串口打印数据
                printf("Temp: %5.2f C | Humi: %5.2f %%RH\r\n",
                       aht20_data.temperature, aht20_data.humidity);

                // OLED 显示温湿度
                OLED_ShowFloat(60, 0, aht20_data.temperature, 2, 1, 16);
                OLED_ShowFloat(60, 3, aht20_data.humidity, 2, 1, 16);
                OLED_Refresh();
            } else {
                printf("[WARN] Sensor busy, data not ready\r\n");
                OLED_ShowString(4, 6, "Sensor Busy!", 12);
                OLED_Refresh();
            }
        } else {
            printf("[ERR] Failed to read AHT20!\r\n");
            OLED_ShowString(4, 6, "Read Error!", 12);
            OLED_Refresh();
        }

        // 每 2 秒采集一次
        HAL_Delay(2000);
    }
    /* USER CODE END 3 */
}

6.3 实测数据

测量次数 温度(°C) 湿度(%RH) 数据有效 备注
1 25.41 58.72 室温
2 25.38 58.65 稳定
3 25.42 58.80 微小波动
4 25.35 58.55 正常
5 31.82 40.15 用手握住传感器
6 26.10 56.30 恢复室温
7 25.43 58.75 完全恢复
复制代码
=== 串口输出示例 ===
Temp: 25.41 C | Humi: 58.72 %RH
Temp: 25.38 C | Humi: 58.65 %RH
Temp: 25.42 C | Humi: 58.80 %RH
!!! 用手握住传感器(体温升温)
Temp: 31.82 C | Humi: 40.15 %RH  ← 温度上升,湿度下降(手干燥)
!!! 松开传感器
Temp: 26.10 C | Humi: 56.30 %RH  ← 开始恢复
Temp: 25.43 C | Humi: 58.75 %RH  ← 完全恢复至室温

📝 结论:AHT20 温度分辨率和响应速度均与数据手册一致(±0.3°C/±2%RH)。手握住传感器约 5 秒,温度从 25.4°C 升到 31.8°C,响应灵敏。


七、故障排查

7.1 硬件接线类

问题 1:AHT20 或 OLED 无响应(SDA 总为高)

现象 :调用 AHT20_Init() 返回 0,SoftI2C_SendByte() 返回 1(NACK)。

排查步骤

步骤 检查项 方法
1 上拉电阻 万用表测量 SCL/SDA 对 GND 电压。空闲时应为 3.3V 高电平。如果 < 1V,说明缺少上拉电阻
2 供电电压 测量 AHT20 VCC 引脚是否为 3.3V ±0.1V
3 从机地址 AHT20 地址为 0x38(7 位),写入时应为 0x70。检查代码中地址是否写错
4 共地 确认所有器件 GND 是否连接到同一点
5 引脚复用 PB6/PB7 是否被其他外设占用(如 USART1 默认也是 PA9/PA10)

最常见原因缺少上拉电阻。部分 I2C 模块不内置上拉电阻,需要在 SCL/SDA 各接一个 4.7kΩ 电阻到 3.3V。


问题 2:OLED 不亮或花屏

排查步骤

  1. 检查 OLED VCC/GND 供电
  2. 检查 OLED 地址:大部分 0.96 寸 OLED 地址为 0x3C,少数模块背面有跳线可改为 0x3D
  3. OLED_Init() 中最后一条命令之后,增加 HAL_Delay(100) 确保初始化序列完成
c 复制代码
// 修改 OLED_Init() 末尾
OLED_WriteCmd(0xAF);  // 开启显示
HAL_Delay(100);       // 等待显示稳定

OLED_Clear();
OLED_Refresh();

7.2 通信类故障

问题 3:I2C 总线锁死(SCL 被拉低,无法释放)

现象 :再次调用 SoftI2C_Start() 时 SCL 和 SDA 均无法正常跳变,一直为低。

原因分析:从机在 SCL 为低时启动了一个读操作但未完成,SDA 被从机拉低等待下一个时钟。标准解决方法是产生 9 个 SCL 时钟将总线恢复。

解决方案 :在 SoftI2C_Init() 中添加总线恢复函数:

c 复制代码
/**
 * @brief  恢复 I2C 总线(在初始化时调用)
 * @note   连续产生 9 个 SCL 脉冲,让卡在传输中的从机释放 SDA
 */
void SoftI2C_BusReset(void)
{
    // 产生 9 个 SCL 脉冲,释放 SDA
    for (uint8_t i = 0; i < 9; i++) {
        SCL_H();
        delay_us(SOFT_I2C_DELAY_US);
        SCL_L();
        delay_us(SOFT_I2C_DELAY_US);
    }
    
    // 最终发送一个 STOP 信号
    SoftI2C_Stop();
}

💡 在 SoftI2C_Init() 开头调用 BusReset(),可以有效避免热插拔传感器后总线锁死的问题。


问题 4:逻辑分析仪抓不到信号,或信号异常

排查:

  1. 确认逻辑分析仪采样率 ≥ 4× 信号频率(100kHz I2C → 至少 400k 采样率,推荐 1MHz)
  2. 确认逻辑分析仪的通道阈值电平与 I2C 电压匹配(3.3V)
  3. 检查示波器探头接地夹是否接到 GND

7.3 驱动代码类故障

问题 5:AHT20 一直返回忙状态(状态字节 bit7=1)

现象AHT20_ReadData() 返回 1 但 data->valid = 0

排查

  1. 触发测量后是否等待了足够时间(最大 75ms)?代码中 HAL_Delay(80) 是否被提前中断?
  2. 是否发送了初始化命令(0xBE 0x08 0x00)?未初始化的 AHT20 会拒绝测量请求
  3. 检查状态字节 bit3(校准标志)是否为 1。如果为 0,说明初始化失败
c 复制代码
// 调试代码片段
SoftI2C_Start();
SoftI2C_SendByte(0x71);      // AHT20 读地址
uint8_t status = SoftI2C_ReadByte(0);  // 只读状态字节
SoftI2C_Stop();
printf("AHT20 Status: 0x%02X (CAL=%d, BUSY=%d)\r\n",
       status, (status >> 3) & 1, (status >> 7) & 1);

问题 6:读取到的温湿度数值异常巨大或为负数

现象:温度显示 -273°C 或 2000°C。

原因分析 :这通常是原始数据拼接错误------第 3 个字节的位拆分方向不对。

AHT20 数据格式记忆口诀

复制代码
Byte0: 状态  | 状态  | 状态  | E_CAL | 湿度[19:16]  (共 4 位)
Byte1: 湿度[15:8]                           (共 8 位)
Byte2: 湿度[7:4] | 温度[19:16]              (各 4 位)
Byte3: 温度[15:8]                           (共 8 位)
Byte4: 温度[7:0]                            (共 8 位)

湿度原始值 = Byte0[3:0] << 16 | Byte1 << 8 | Byte2[7:4]
温度原始值 = Byte2[3:0] << 16 | Byte3 << 8 | Byte4

检查代码(特别是位拼接部分):

c 复制代码
// ✅ 正确的湿度原始值计算
uint32_t hum_raw = ((uint32_t)(buffer[0] & 0x0F) << 16)   // Byte0 低 4 位
                 | ((uint32_t)buffer[1] << 8)              // Byte1 全 8 位
                 | ((uint32_t)buffer[2] >> 4);             // Byte2 高 4 位

// ✅ 正确的温度原始值计算
uint32_t temp_raw = ((uint32_t)(buffer[2] & 0x0F) << 16)  // Byte2 低 4 位
                  | ((uint32_t)buffer[3] << 8)             // Byte3 全 8 位
                  | (uint32_t)buffer[4];                   // Byte4 全 8 位

八、总结

8.1 本文方法论提炼(SIC 原则)

S - 分层抽象(Separation)

嵌入式 I2C 驱动开发应采用三层分离架构:硬件层(GPIO 时序)→ 驱动层(传感器/显示器的特性操作)→ 应用层(业务逻辑)。本文的 soft_i2c.c → aht20.c/oled.c → main.c 就是此架构的实例。分层最大优势:换硬件(如改用 AT32 MCU),只需修改 bottom 层,驱动层和应用层代码无需改动。

I - 增量验证(Increment)

每完成一个层级后进行独立测试:写 soft_i2c.c → 测试起始/停止/字节收发 → 写 aht20.c → 串口打印原始数据验证 → 写 oled.c → 不依赖传感器单独测试显示 → 最后集成。不要一次性写几百行代码再调试,那是灾难。

C - 闭环校验(Check)

每个通信操作都有对应的验证方式:发送地址后检查 ACK(闭环),读取数据后检查状态字节的忙标志(闭环),最终输出温湿度后用手握传感器验证响应(闭环)。嵌入式硬件开发没有"猜测"的空间,所见即所得,每一行代码运行在真实硬件上的表现就是最终标准。

8.2 完整代码文件清单

文件 层级 说明
Src/soft_i2c.c 硬件层 软件模拟 I2C 时序实现
Inc/soft_i2c.h 硬件层 引脚宏定义和函数声明
Src/aht20.c 驱动层 AHT20 传感器初始化、测量、数据读取
Inc/aht20.h 驱动层 命令宏定义、数据结构
Src/oled.c 驱动层 SSD1306 OLED 初始化、显示
Inc/oled.h 驱动层 分辨率、函数声明
Inc/oled_font.h 资源层 ASCII + 中文字库点阵
Src/main.c 应用层 主循环:采集→显示→串口输出

8.3 扩展方向

方向 内容 难度
多传感器接入 在同一个 I2C 总线上挂载 BME280(气压)+ MPU6050(加速度)+ AHT20,使用不同从机地址轮询 ⭐⭐
Low Power 设计 使用 AHT20 的睡眠模式(0xB0)结合 STM32 Stop 模式,静态功耗降至 µA 级 ⭐⭐⭐
数据上传云平台 通过 ESP8266/ESP32 将温湿度数据上传至 MQTT 服务器或阿里云 IoT ⭐⭐⭐
硬件 I2C + DMA 使用 STM32 硬件 I2C 外设 + DMA 传输,降低 CPU 占用率 ⭐⭐⭐
I2C 总线扫描工具 编写一个 I2C 总线扫描函数,自动检测总线上挂载了哪些设备及其地址

📝 版本信息:本文基于以下版本实测:

  • STM32 标准 HAL 库 V1.8.0(STM32CubeMX 6.9.0 生成)
  • Keil MDK-ARM 5.36
  • AHT20(数据手册版本 V5.5)
  • SSD1306 OLED(0.96寸,I2C 接口,地址 0x3C)

移植注意事项

  • 不同 MCU 系列(F0/F4/GD32)的 GPIO 开漏配置函数名可能不同,需参考对应 HAL 库手册
  • AHT20 早期版本(V1.0)与 V5.5 的命令格式一致,可直接使用
  • OLED 驱动兼容 SH1106(1.3 寸)控制器,但初始化命令序列略有差异(页地址映射不同)