从上一篇到这篇
上篇我们点亮了 OLED 屏幕(CH1116 驱动芯片 + 波特律动驱动库,学了页/列寻址、取模、帧缓冲);再往前,我们学会了用 I2C 读 AHT20 温湿度传感器
学完这两个,把 AHT20 的数据直接显示在 OLED 上,就是一台温湿度计。
| 和上一篇的区别 | 上一篇(OLED) | 本篇(温湿度计) |
|---|---|---|
| 总线上挂几个从机 | 1 个(OLED,0x7A) | 2 个(AHT20,0x70 + OLED,0x7A) |
| 数据方向 | 只写屏幕 | 读传感器 + 写屏幕 |
| 关键新知识 | 显存、取模 | 多从机共线、sprintf 浮点、组合工程 |
硬件原理:一条总线,两个从机
温湿度计的系统组成
1 | ┌────────────────────────────┐ |
新知识点: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 文件”**,方便我们后续手动添加驱动文件。
实操步骤
CubeMX 新建工程:选芯片(学习板是 STM32F103C8T6)→ SYS 的 Debug 选 Serial Wire → RCC 开 HSE(Crystal/Ceramic Resonator)→ 时钟树改 72MHz → I2C1 开快速模式 → 生成代码
下载 OLED 驱动:打开波特律动文档站 → 驱动库 → 下载 CH1116 版 OLED 驱动 → 解压后把
oled.c、font.c复制到Core/Src/,oled.h、font.h复制到Core/Inc/下载 AHT20 驱动:同样在驱动库下载 AHT20 驱动 →
aht20.c→Core/Src/,aht20.h→Core/Inc/修改 CMakeLists.txt(VS Code 工程专属,漏了会报 undefined reference):在
target_sources中手动添加三个驱动文件,并在末尾加上浮点格式化链接参数(否则%.1f小数显示不出来,原因见下文”代码讲解·sprintf”):1
2
3
4
5
6target_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)编写 main.c:按下面的”核心代码”部分填写
中文取模:用波特律动取模助手输入”温湿度计”等文字 → 生成 16×16 字模 → 粘贴到
font.c的字模数组中编译烧录:
cmake --build build编译,成功后用 ST-Link 烧录
核心代码(main.c)
1 | /* USER CODE BEGIN Includes */ |
代码讲解
1. 初始化:先屏幕后传感器
1 | HAL_Delay(20); |
- **
HAL_Delay(20)**:OLED 上电速度比单片机慢,立刻初始化可能没响应,先等 20ms OLED_Init():发一串 CH1116 初始化指令(关显示 → 设页/列 → 开电荷泵 → 开显示)AHT20_Init():上电等 40ms,读状态寄存器,若 bit3 为 0(未校准)则发送 0xBE 校准命令
2. 主循环:读一次,画一帧,刷一次
1 | AHT20_Measure(); // ① 触发测量 |
AHT20 驱动内部:发送测量命令 {0xAC, 0x33, 0x00} → HAL_Delay(75) 等转换完成 → 读回 6 字节 → 用移位拼出 20 位湿度、20 位温度原始值 → 套换算公式:
1 | Humidity = humi * 100 / (1 << 20); // 20bit 湿度:0~2^20 映射到 0~100% |
3. 显示:帧缓冲三板斧
1 | OLED_NewFrame(); // 清空显存 |
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 | AHT20 内部寄存器(温度20bit/湿度20bit) |
总结:传感器读原始数据 → 解析成物理量 → 格式化字符串 → 查字模写显存 → 刷屏点亮
编译 & 烧录
老样子,此处不再赘述。
效果展示
总结
和上一篇(OLED)相比,本篇没有新外设,但把 I2C 的”读”与”写”、驱动库的”画字”与”刷屏”组合成了真正有用的东西。
