摘要: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;
}
⚠️ 关键设计决策:
- GPIO 模式必须为开漏输出(OD),不能是推挽输出(PP)。推挽模式下从机无法将 SDA 拉低,应答机制将完全失效
- 释放 SDA = 写入高电平:开漏输出下写入高电平等价于高阻态,SDA 由外部上拉电阻拉高
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 不亮或花屏
排查步骤:
- 检查 OLED VCC/GND 供电
- 检查 OLED 地址:大部分 0.96 寸 OLED 地址为 0x3C,少数模块背面有跳线可改为 0x3D
- 在
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:逻辑分析仪抓不到信号,或信号异常
排查:
- 确认逻辑分析仪采样率 ≥ 4× 信号频率(100kHz I2C → 至少 400k 采样率,推荐 1MHz)
- 确认逻辑分析仪的通道阈值电平与 I2C 电压匹配(3.3V)
- 检查示波器探头接地夹是否接到 GND
7.3 驱动代码类故障
问题 5:AHT20 一直返回忙状态(状态字节 bit7=1)
现象 :AHT20_ReadData() 返回 1 但 data->valid = 0。
排查:
- 触发测量后是否等待了足够时间(最大 75ms)?代码中
HAL_Delay(80)是否被提前中断? - 是否发送了初始化命令(
0xBE 0x08 0x00)?未初始化的 AHT20 会拒绝测量请求 - 检查状态字节 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 寸)控制器,但初始化命令序列略有差异(页地址映射不同)