0%

STM32番外篇——oled温湿度计

从上一篇到这篇

上篇我们点亮了 OLED 屏幕(CH1116 驱动芯片 + 波特律动驱动库,学了页/列寻址、取模、帧缓冲);再往前,我们学会了用 I2C 读 AHT20 温湿度传感器

学完这两个,把 AHT20 的数据直接显示在 OLED 上,就是一台温湿度计。

和上一篇的区别 上一篇(OLED) 本篇(温湿度计)
总线上挂几个从机 1 个(OLED,0x7A) 2 个(AHT20,0x70 + OLED,0x7A)
数据方向 只写屏幕 读传感器 + 写屏幕
关键新知识 显存、取模 多从机共线、sprintf 浮点、组合工程

硬件原理:一条总线,两个从机

温湿度计的系统组成

1
2
3
4
5
6
7
8
9
10
11
12
            ┌────────────────────────────┐
│ STM32F103 │
│ (72MHz, I2C1 快速模式) │
└────────────┬───────────────┘
│ I2C1: SCL=PB6 SDA=PB7
┌───────────────┴───────────────┐
▼ ▼
┌────────────────┐ ┌────────────────┐
│ AHT20 传感器 │ │ OLED 屏幕 │
│ 地址 0x70 │ │ 地址 0x7A │
│ 读温度+湿度 │ │ 显示文字/图形 │
└────────────────┘ └────────────────┘

新知识点:I2C 总线的”多从机共线”

之前我们只在一根 I2C 总线上挂过一个设备。I2C 天生支持一条总线上挂多个从机——只要每个从机的7 位地址不同即可:

  • AHT20 的 I2C 地址是 0x70
  • OLED(CH1116)的 I2C 地址是 0x7A

类比:I2C 总线像一根”电话线”,从机是”座机”,地址是”电话号码”。STM32 给 0x70 打电话,AHT20 应答;给 0x7A 打电话,OLED 应答。互不干扰。

主设备发送数据时,第 8 位是读/写标志位,所以实际发送的字节是 地址<<1 | R/W——但 HAL 库的 HAL_I2C_Master_Transmit(&hi2c1, 地址, ...) 已经帮我们处理了移位,直接填 0x70 / 0x7A 就行。

两个设备共用总线还有个细节:AHT20 是”读”(MCU 收数据),OLED 是”写”(MCU 发数据)——一读一写,总线在时间上分时使用,不会冲突。


CubeMX 配置

配置和上篇 OLED 完全一样(SYS 的 Debug 选 Serial Wire、RCC 开 HSE、时钟树改 72MHz)

设置项 值 说明
I2C1 开关 I2C(快速模式) 学习板上温湿度模块与 OLED 都挂在 I2C1 上
Clock Speed 400000 快速模式 400kHz,比默认 100kHz 快 4 倍,刷屏更流畅

时钟树 72MHz 的配置和上一篇 OLED 博客完全一致(先 RCC 开 HSE,再把 HCLK 改成 72MHz),这里不再重复截图,可参考 上一篇:OLED屏幕。

另外别忘了 Project Manager → Code generator → 勾选 **”为每个外设生成一对 .c/.h 文件”**,方便我们后续手动添加驱动文件。


实操步骤

  1. CubeMX 新建工程:选芯片(学习板是 STM32F103C8T6)→ SYS 的 Debug 选 Serial Wire → RCC 开 HSE(Crystal/Ceramic Resonator)→ 时钟树改 72MHz → I2C1 开快速模式 → 生成代码

  2. 下载 OLED 驱动:打开波特律动文档站 → 驱动库 → 下载 CH1116 版 OLED 驱动 → 解压后把 oled.c、font.c 复制到 Core/Src/,oled.h、font.h 复制到 Core/Inc/

  3. 下载 AHT20 驱动:同样在驱动库下载 AHT20 驱动 → aht20.c → Core/Src/,aht20.h → Core/Inc/

  4. 修改 CMakeLists.txt(VS Code 工程专属,漏了会报 undefined reference):在 target_sources 中手动添加三个驱动文件,并在末尾加上浮点格式化链接参数(否则 %.1f 小数显示不出来,原因见下文”代码讲解·sprintf”):

    1
    2
    3
    4
    5
    6
    target_sources(${CMAKE_PROJECT_NAME} PRIVATE
    Core/Src/font.c
    Core/Src/oled.c
    Core/Src/aht20.c
    )
    target_link_options(${CMAKE_PROJECT_NAME} PRIVATE -u _printf_float)
  5. 编写 main.c:按下面的”核心代码”部分填写

  6. 中文取模:用波特律动取模助手输入”温湿度计”等文字 → 生成 16×16 字模 → 粘贴到 font.c 的字模数组中

  7. 编译烧录:cmake --build build 编译,成功后用 ST-Link 烧录


核心代码(main.c)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* USER CODE BEGIN Includes */
#include "oled.h"
#include "aht20.h"
#include <stdio.h> // sprintf 需要
/* USER CODE END Includes */

/* USER CODE BEGIN 2 */
HAL_Delay(20); // OLED 上电比 STM32 慢,等它"睡醒"
OLED_Init(); // 初始化屏幕
AHT20_Init(); // 初始化传感器
float tem, humi; // 存放温度、湿度
char message[50]; // 存放格式化后的字符串
/* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
while (1)
{
AHT20_Measure(); // 触发一次测量
tem = AHT20_Temperature(); // 取温度
humi = AHT20_Humidity(); // 取湿度

OLED_NewFrame(); // 新建一帧画面(清空显存)
OLED_PrintString(0, 0, "温湿度计", &font16x16, OLED_COLOR_NORMAL); // 标题(中文)

sprintf(message, "T:%.1f", tem); // 格式化:保留 1 位小数
OLED_PrintString(32, 16, message, &font16x16, OLED_COLOR_NORMAL); // 温度

sprintf(message, "H:%.1f", humi); // 湿度同理
OLED_PrintString(32, 32, message, &font16x16, OLED_COLOR_NORMAL);
OLED_ShowFrame(); // 把这一帧刷到屏幕上

HAL_Delay(1000); // 1 秒刷新一次,不用刷太快
}
/* USER CODE END WHILE */

代码讲解

1. 初始化:先屏幕后传感器

1
2
3
HAL_Delay(20);
OLED_Init();
AHT20_Init();
  • **HAL_Delay(20)**:OLED 上电速度比单片机慢,立刻初始化可能没响应,先等 20ms
  • OLED_Init():发一串 CH1116 初始化指令(关显示 → 设页/列 → 开电荷泵 → 开显示)
  • AHT20_Init():上电等 40ms,读状态寄存器,若 bit3 为 0(未校准)则发送 0xBE 校准命令

2. 主循环:读一次,画一帧,刷一次

1
2
3
AHT20_Measure();          // ① 触发测量
tem = AHT20_Temperature();
humi = AHT20_Humidity();

AHT20 驱动内部:发送测量命令 {0xAC, 0x33, 0x00} → HAL_Delay(75) 等转换完成 → 读回 6 字节 → 用移位拼出 20 位湿度、20 位温度原始值 → 套换算公式:

1
2
Humidity = humi * 100 / (1 << 20);            // 20bit 湿度:0~2^20 映射到 0~100%
Temperature = temp * 200 / (1 << 20) - 50; // 20bit 温度:映射到 -50~150℃

3. 显示:帧缓冲三板斧

1
2
3
4
OLED_NewFrame();                                                  // 清空显存
OLED_PrintString(0, 0, "温湿度计:", &font16x16, OLED_COLOR_NORMAL);
...
OLED_ShowFrame(); // 刷屏

OLED_PrintString 是驱动库封装好的”画字”函数,它会把每个字符/汉字去 font.c 里查字模,逐字节写入 1KB 的显存 OLED_GRAM。注意三个细节:

  • 中文必须存在于 font.c 的字模里(用取模助手生成后粘贴进去),不能随便写
  • 字体变量要取地址 &font16x16
  • 坐标:标题在 (0,0),温度在 (32,16),湿度在 (32,32)——16×16 字体一行 16 像素高,错开 16 像素正好不重叠

4. sprintf:把数字变成字符串

OLED 只能显示字符,不能直接显示数字,所以要把 float 转成字符串:

1
sprintf(message, "T:%.1f", tem);
  • %.1f:保留一位小数的浮点格式,比如 28.3
  • newlib-nano 默认不链接浮点格式化代码(为了省 Flash)——编译不报错,但 %f 显示空白/0.0,需要在 CMakeLists.txt 加一行 -u _printf_float 强制拉回(见实操步骤第 4 步)
  • message 是 char[50]——必须用 char 数组,不能用 uint8_t,否则 sprintf 的类型警告烦死你

数据流全链路

从传感器读数到屏幕亮字,完整走一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AHT20 内部寄存器(温度20bit/湿度20bit)
│ ① AHT20_Measure() 发送 0xAC 测量命令,等待 75ms
▼
HAL_I2C_Master_Receive 读回 6 字节(从机 0x70)
│ ② 移位拼接 + 换算公式(×100/2²⁰、×200/2²⁰-50)
▼
float tem/humi(程序内存)
│ ③ sprintf("T:%.1f") 格式化
▼
char message[50] 字符串
│ ④ OLED_PrintString 查 font.c 字模表(16×16 点阵)
▼
OLED_GRAM 显存(128×8 字节帧缓冲)
│ ⑤ OLED_ShowFrame 遍历显存
▼
HAL_I2C_Master_Transmit(从机 0x7A,0x40 数据门牌)
│ ⑥ CH1116 芯片按页/列寻址点亮像素
▼
OLED 屏幕显示「温湿度计:T:28.3 H:65.2」

总结:传感器读原始数据 → 解析成物理量 → 格式化字符串 → 查字模写显存 → 刷屏点亮


编译 & 烧录

老样子,此处不再赘述。


效果展示

IMG_20260805_215509


总结

和上一篇(OLED)相比,本篇没有新外设,但把 I2C 的”读”与”写”、驱动库的”画字”与”刷屏”组合成了真正有用的东西。


参考资源