简介:这份资源面向嵌入式与物联网方向的初学者及开发者,围绕STM32 HAL库展开,解决DHT11温湿度采集与OLED实时显示联动的实践问题。包内共193个文件,以h头文件、c源文件、d依赖文件、o目标文件及crf编译记录为主,另含ioc工程配置、uvprojx与uvoptx工程文件、hex烧录固件、map映射表等,压缩包约6.9MB,完整覆盖从驱动源码到编译产物的工程结构。资源基于STM32F0系列,涉及GPIO、I2C、UART、TIM等HAL模块,可帮助读者理解单总线时序读取40位数据、SSD1306驱动OLED显示温湿度的完整链路。目前已有772人学习下载,适合作为课程设计、毕业设计或智能家居小项目的参考模板,便于快速移植与二次开发。
1. 从一块小板子说起:DHT11 和 OLED 到底能帮你做出什么
你可能刚拿到一块单片机开发板,看着上面密密麻麻的引脚,心里想的是“我连灯都没点亮过,怎么可能做出一个能显示温湿度的东西”。别急,我第一次接触硬件时也有同样的感觉,觉得这玩意儿是玄学,线一插错就冒烟。其实你手里那块板子加上 DHT11 和 OLED,就是一个完整的“感知—处理—显示”小系统,它不神秘,只是把三件事串起来了:DHT11 负责测量空气里的温度和湿度,单片机负责读取数据,OLED 屏幕负责把数字显示出来。整个过程就像你用体温计测体温,然后自己看一眼读数,只不过这里换成了电子元件自动完成。你不需要懂半导体物理,也不需要会画电路板,只要会插线、会写几行代码,就能让这块屏幕亮起来并显示你房间的温湿度。这就是物联网最基础的雏形——让物体自己“说话”。适合谁学?适合所有想动手做点东西、但被“嵌入式”三个字吓到的新手。你不需要先学完模电数电,直接从这个小项目切入,反而更容易理解那些抽象概念到底在说什么。
2. 为什么值得从 DHT11 和 OLED 入手:新手的第一块敲门砖
2.1 门槛低到超乎想象:只需要三根线就能跑起来
很多新手一听到“传感器”和“显示屏”,脑子里立刻浮现出复杂的电路图和一堆看不懂的时序图。但 DHT11 和 OLED 这两个模块,恰恰是嵌入式入门里最友好的组合之一。DHT11 只有三个有效引脚:VCC、GND、DATA。OLED 通常用 I2C 接口,也只需要四根线:VCC、GND、SCL、SDA。你甚至不需要自己焊电路,用杜邦线插在开发板上就能用。我第一次接的时候,总共就插了七根线,其中三根还是电源和地。这种“插上就能用”的体验,对新手来说太重要了——它能让你在半小时内看到结果,而不是花三天调一个看不见的波形。你不需要理解 DHT11 内部怎么把湿度转换成数字,也不需要知道 OLED 的驱动芯片怎么扫描像素,你只需要知道:给它们供电,然后按照协议去读、去写。这就是入门阶段最需要的正反馈。
2.2 一个项目串起三个核心概念:GPIO、时序、显示
别看这个项目小,它其实把嵌入式开发里最常用的三个技能点都覆盖了。第一是 GPIO 的控制,你要学会把某个引脚设置成输出模式去驱动 OLED,或者设置成输入模式去读取 DHT11 的数据。第二是时序的理解,DHT11 是单总线协议,它用一根线的高低电平变化来传递 0 和 1,你需要知道什么时候拉低、什么时候释放、什么时候去采样。第三是显示刷新,OLED 需要你把温湿度数值转换成字符,再按照坐标写到屏幕的显存里。这三个概念在任何一个嵌入式项目里都会反复出现,你在这个小项目里踩过的坑,以后做更复杂的东西时都会变成经验。比如你在这里学会了“读数据前先等传感器响应”,以后读其他传感器时就不会再犯“上来就猛读”的错误。
2.3 从“能跑”到“能用”:一个可以带走的完整案例
很多教程只教你点亮一个灯,或者只让屏幕显示一行字,但 DHT11 加 OLED 是一个完整的闭环。你做完之后,得到的是一个能放在桌上、实时显示当前环境温湿度的小设备。你可以把它放在书桌前,随时看一眼房间干不干、热不热。这种“做出来就能用”的成就感,比单纯点亮一个 LED 要强得多。而且这个项目有很强的扩展性:你可以加一个按键切换显示摄氏度还是华氏度,可以加一个蜂鸣器做高温报警,也可以把数据通过串口传到电脑上画曲线。这些扩展都不需要你重新学一套东西,只是在现有代码上加几行。所以它既是一个入门练习,也是一个可以继续生长的原型。你学到的不是“怎么驱动 DHT11”,而是“怎么把一个传感器和一个显示器组合成一个有用的小系统”。
3. 动手之前先搞懂:DHT11 和 OLED 到底怎么跟单片机说话
3.1 DHT11 的单总线协议:一根线怎么传 40 个 bit
DHT11 和单片机之间只有一根数据线,这根线既要传命令,又要传数据,还要传时钟信号,听起来很神奇,其实原理很简单。它靠的是“时间长短”来区分 0 和 1。具体来说,单片机先把数据线拉低至少 18 毫秒,然后释放,DHT11 检测到这个信号后,会先拉低 80 微秒,再拉高 80 微秒,表示“我准备好了”。接下来 DHT11 会连续发送 40 个 bit 的数据,每个 bit 都以一个 50 微秒的低电平开始,然后如果是 0,高电平持续 26 到 28 微秒;如果是 1,高电平持续 70 微秒。你只需要在每次低电平结束后等 40 微秒左右去读引脚电平,就能判断这一位是 0 还是 1。这 40 个 bit 里,前 16 位是湿度整数和小数,接着 16 位是温度整数和小数,最后 8 位是校验和。校验和等于前四个字节相加取低八位,用来判断数据有没有传错。我第一次读的时候,总是读出来 255,后来发现是等待时间不对,把采样点往后挪了一点就正常了。所以如果你也读不到正确值,先检查你的延时函数准不准。
3.2 OLED 的 I2C 通信:两根线搞定显示
OLED 模块通常用 I2C 接口,只需要 SCL 和 SDA 两根线。I2C 是一种总线协议,你可以把多个设备挂在同一组线上,靠设备地址来区分。OLED 的地址一般是 0x3C 或 0x3D,具体看模块背面。通信过程是这样的:单片机先发送起始信号,然后发送地址加写标志,接着发送命令或数据,最后发送停止信号。命令用来设置对比度、显示模式、光标位置等,数据就是你要显示的字符的点阵信息。你不需要自己写 I2C 的底层时序,因为 HAL 库已经提供了HAL_I2C_Master_Transmit这样的函数,你只需要把要发的命令和数据填进缓冲区,调用函数就行。但你要知道,OLED 显示字符不是直接写 ASCII 码,而是要把字符对应的点阵数据写进显存。比如你要显示数字“2”,得先查字模表,找到“2”的 8x16 点阵数据,然后按页和列写到 OLED 的 GDDRAM 里。听起来麻烦,但网上有现成的字模软件,你只需要生成数组,复制到代码里就行。
3.3 数据怎么变成屏幕上的数字:从传感器到显示的完整链路
整个链路是这样的:单片机先给 DHT11 发送起始信号,然后读取 40 位数据,解析出温度和湿度的整数值。接着把这些整数拆成单个数字,比如温度 26 度,就拆成字符‘2’和‘6’。然后去字模数组里找到这两个字符的点阵数据,再通过 I2C 把点阵数据写到 OLED 的对应位置。OLED 收到数据后,会自动刷新屏幕,你就能看到“Temp: 26 C”这样的字样。这个过程中,你需要注意两点:一是 DHT11 读取间隔不能太短,官方建议至少 1 秒一次,否则数据会不准;二是 OLED 刷新时,如果你每次都全屏清除再写,屏幕会闪,最好是只更新变化的区域,或者用双缓冲。我第一次做的时候,每 100 毫秒读一次 DHT11,结果屏幕上的数字乱跳,后来改成 2 秒读一次,就稳定了。所以时序和刷新频率这两个参数,你调一次就能记住。
4. 第一个能跑的例子:从零写一个温湿度显示程序
4.1 准备工作:硬件连接和软件环境
先确认你手头的东西:一块 STM32 开发板(比如 F103C8T6 最小系统板)、一个 DHT11 模块、一个 0.96 寸 OLED 模块(I2C 接口)、若干杜邦线。接线如下:DHT11 的 VCC 接 3.3V,GND 接 GND,DATA 接 PA0(随便选一个 GPIO 就行)。OLED 的 VCC 接 3.3V,GND 接 GND,SCL 接 PB6,SDA 接 PB7(这是 I2C1 的默认引脚)。软件方面,你需要安装 STM32CubeMX 和 Keil MDK(或者 STM32CubeIDE)。用 CubeMX 新建工程,选好芯片型号,然后配置 PA0 为 GPIO 输入,PB6 和 PB7 为 I2C1 的 SCL 和 SDA。接着配置时钟树,把系统时钟设成 72MHz。最后生成代码,打开工程。如果你用的是其他型号,引脚可能不同,但思路一样:一个普通 GPIO 读 DHT11,一组 I2C 引脚驱动 OLED。
4.2 写 DHT11 读取函数:微秒延时是成败关键
DHT11 的读取对延时精度要求比较高,所以你需要一个微秒级的延时函数。HAL 库自带的HAL_Delay是毫秒级的,不够用。你可以用系统滴答定时器或者通用定时器来实现微秒延时。下面是一个简单的微秒延时函数,基于 SysTick:
// 微秒延时函数,基于 SysTick 计数器 void delay_us(uint32_t us) { uint32_t start = SysTick->VAL; uint32_t ticks = us * (SystemCoreClock / 1000000); while (1) { uint32_t now = SysTick->VAL; uint32_t elapsed; if (now <= start) { elapsed = start - now; } else { elapsed = start + (SysTick->LOAD - now); } if (elapsed >= ticks) break; } }这个函数通过读取 SysTick 的当前值来计算经过了多少个时钟周期。SystemCoreClock是系统时钟频率,比如 72MHz,那么每个微秒就是 72 个时钟周期。ticks就是你要延时的微秒数对应的周期数。注意 SysTick 是向下计数的,所以计算 elapsed 时要分两种情况。这个函数不是绝对精确,但对于 DHT11 来说足够了。如果你发现读出来的数据总是校验错,可以试着把延时稍微调大一点,比如把 40 微秒改成 50 微秒。
接下来是 DHT11 的读取函数:
// 读取 DHT11 温湿度,成功返回 0,失败返回 1 uint8_t DHT11_Read(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] = {0}; uint8_t i, j; // 主机发送起始信号:拉低至少 18ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); delay_us(30); // 切换为输入模式,等待 DHT11 响应 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 等待 DHT11 拉低 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) return 1; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET); // 读取 40 位数据 for (i = 0; i < 5; i++) { for (j = 0; j < 8; j++) { while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET); delay_us(40); buf[i] <<= 1; if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET) { buf[i] |= 1; } while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET); } } // 校验 if (buf[0] + buf[1] + buf[2] + buf[3] != buf[4]) return 1; *humi = buf[0]; *temp = buf[2]; return 0; }这个函数先拉低数据线 20 毫秒,然后释放,再等 30 微秒,接着把引脚切成输入模式。之后等待 DHT11 的响应信号:先等它拉低,再等它拉高,再等它拉低,然后开始读 40 位。每一位都是先等低电平结束,然后延时 40 微秒,再读引脚电平,如果是高就记 1,否则记 0。最后校验和。注意这里用了HAL_GPIO_ReadPin和HAL_GPIO_WritePin,你需要根据实际引脚修改。另外,读取成功后,湿度和温度分别存在buf[0]和buf[2]里,小数部分在buf[1]和buf[3],但 DHT11 的小数部分通常为 0,所以直接取整数就行。
4.3 写 OLED 显示函数:把数字变成屏幕上的字符
OLED 驱动需要你先初始化,然后才能写数据。初始化序列包括设置显示时钟、复用率、对比度、显示模式等。网上有现成的 OLED 初始化代码,你可以直接拿来用。这里重点说怎么显示字符。假设你已经有了一个OLED_ShowString函数,它接受 x 坐标、y 坐标和字符串指针。你需要先把温湿度整数转换成字符串,比如用sprintf:
char disp_buf[32]; uint8_t temp, humi; if (DHT11_Read(&temp, &humi) == 0) { sprintf(disp_buf, "Temp: %d C", temp); OLED_ShowString(0, 0, (uint8_t *)disp_buf); sprintf(disp_buf, "Humi: %d %%", humi); OLED_ShowString(0, 2, (uint8_t *)disp_buf); } else { OLED_ShowString(0, 0, (uint8_t *)"DHT11 Error"); }这里OLED_ShowString的第二个参数是页地址,0 表示第一页,2 表示第三页(每页 8 行)。sprintf把格式化的字符串写到disp_buf里,然后传给 OLED 显示函数。注意%在 sprintf 里要写成%%才能输出一个百分号。如果你没有OLED_ShowString,可以自己写一个:先设置光标位置,然后逐个字符查字模,把点阵数据通过 I2C 写过去。字模可以用 PCtoLCD2002 软件生成,取模方式选“阴码、逐列式、顺向”,然后复制数组到代码里。
4.4 主循环:每两秒读一次,稳定显示不闪烁
主循环里不要频繁读取 DHT11,否则数据会跳变。建议每 2 秒读一次,中间让 OLED 保持显示。代码如下:
while (1) { if (DHT11_Read(&temp, &humi) == 0) { sprintf(disp_buf, "Temp: %d C", temp); OLED_ShowString(0, 0, (uint8_t *)disp_buf); sprintf(disp_buf, "Humi: %d %%", humi); OLED_ShowString(0, 2, (uint8_t *)disp_buf); } else { OLED_ShowString(0, 0, (uint8_t *)"DHT11 Error"); } HAL_Delay(2000); }这样每两秒更新一次屏幕,数字不会乱跳,也不会闪。如果你觉得两秒太长,可以改成 1 秒,但不要低于 1 秒。另外,如果读取失败,显示错误信息,方便你排查。第一次跑的时候,如果屏幕一直显示 Error,先检查接线,再检查延时函数,最后检查 DHT11 是不是坏了(虽然概率很低)。我遇到过因为杜邦线接触不良导致读不到数据的情况,换一根线就好了。
5. 新手最容易踩的五个坑:排查与避坑指南
5.1 现象:OLED 完全不亮,或者只有背光没有字符
原因:I2C 地址不对,或者 SCL/SDA 接反了,或者初始化序列没发对。解决:先用 I2C 扫描程序确认设备地址,通常是 0x3C 或 0x3D。如果扫描不到,检查接线是否松动,或者换一组 I2C 引脚。如果地址对了但屏幕不亮,检查初始化代码里有没有设置显示开启命令(一般是 0xAF)。我第一次用的时候,地址写成了 0x78,结果一直没反应,后来改成 0x3C 就正常了。
5.2 现象:DHT11 读出来全是 255 或者校验错
原因:微秒延时不准,或者采样点太早或太晚。解决:用示波器或者逻辑分析仪看波形是最直接的,但没有的话可以试着调整delay_us(40)这个值,改成 30 或 50 试试。另外,确保在读取每一位之前,先等待低电平结束,否则会读到错误的高电平。还有一个常见原因是 GPIO 模式切换后没有重新初始化,导致引脚状态不对。
5.3 现象:屏幕上的数字跳来跳去,或者显示乱码
原因:读取频率太高,或者字符串缓冲区溢出,或者 OLED 刷新时没有清除旧数据。解决:把读取间隔改成 2 秒以上。检查disp_buf大小是否足够,sprintf写入的字符数不要超过缓冲区。如果显示乱码,可能是字模取反了,或者页地址和列地址算错了。建议先用固定的字符串测试显示,比如“Hello”,确认显示正常后再换成动态数据。
5.4 现象:程序下载后没反应,或者一上电就死机
原因:堆栈溢出,或者中断冲突,或者电源供电不足。解决:检查delay_us里有没有死循环,比如 SysTick 配置不对导致elapsed永远不够。检查 DHT11 和 OLED 的电源是不是都接了 3.3V,有些模块 5V 也能用,但最好统一 3.3V。如果用了 FreeRTOS,注意任务堆栈大小。我第一次跑的时候,因为delay_us里用了SystemCoreClock但没包含正确的头文件,编译通过了但运行不对,后来加上#include "stm32f1xx.h"就好了。
5.5 现象:换一块板子或者换一个 DHT11 就不好使了
原因:不同厂家的 DHT11 时序略有差异,或者不同板子的晶振频率不同导致延时变化。解决:如果你的代码是基于 72MHz 写的,换到 48MHz 的板子上,延时就会变长,需要重新调整delay_us里的计算。另外,有些 DHT11 模块自带上拉电阻,有些没有,如果没有,你需要在数据线上接一个 4.7k 到 10k 的上拉电阻。我换过一块最小系统板,发现它的 3.3V 输出电流不够,导致 OLED 闪烁,后来单独给 OLED 供了 5V 才稳定。
6. 下一步学什么:从这个小项目往外延伸的三条路
6.1 把数据传到电脑上:串口打印与曲线绘制
你现在已经能在 OLED 上看到温湿度了,但如果你想记录一整天的变化,OLED 就不够用了。下一步可以学串口通信,把数据通过 UART 发送到电脑,然后用串口助手或者 Python 脚本接收并画成曲线。具体做法是:在 CubeMX 里开启 USART1,配置波特率 115200,然后在主循环里用printf重定向到串口。你需要重写fputc函数,让printf输出到 USART1。之后在电脑上用串口助手打开对应端口,就能看到数据流。如果想画曲线,可以用 Python 的 pyserial 和 matplotlib,每读一行就更新一次图。这个过程中你会学到中断接收、DMA 传输等进阶概念,但都是从你已经熟悉的代码上长出来的。
6.2 加上报警功能:按键、蜂鸣器和阈值设置
另一个实用的扩展是加一个按键和蜂鸣器。按键用来设置温湿度阈值,蜂鸣器在超过阈值时报警。你可以用 GPIO 输入接按键,用 GPIO 输出接蜂鸣器。按键需要消抖,可以用软件延时或者外部中断。蜂鸣器可以用 PWM 控制声音频率。代码上,你需要在主循环里轮询按键状态,如果按下就进入设置模式,用 OLED 显示当前阈值,再按一下切换调整。这个过程中你会学到状态机的写法,以及如何用非阻塞的方式处理多个任务。我建议你先用最简单的阻塞式按键,等跑通了再改成中断,这样不容易一开始就卡住。
6.3 换成更精确的传感器:SHT30 或 AHT20 的迁移思路
DHT11 的精度是 ±2°C 和 ±5%RH,对于日常看看够用,但如果你想做更精确的记录,可以换成 SHT30 或 AHT20。这两个都是 I2C 接口,精度更高,而且不需要微秒延时,直接读寄存器就行。迁移的时候,你只需要把 DHT11 的读取函数换成对应的 I2C 读取函数,OLED 显示部分完全不用改。这样你就能体会到“分层设计”的好处:传感器层和显示层分开,换传感器不影响显示。我建议你在做完 DHT11 之后,至少试一次换传感器,这样你以后做项目时就会下意识地把硬件相关的代码封装起来,而不是到处复制粘贴。
6.4 我的习惯:每次改完代码先看三件事
从那以后我每次改完代码,都强制走一遍这三步:第一,检查所有延时函数的单位是不是微秒,有没有和毫秒混用;第二,用万用表量一下所有电源引脚是不是 3.3V,地是不是共地;第三,在串口里打印关键变量的值,比如 DHT11 读出来的原始 40 位数据,确认校验和对不对。这三步花不了两分钟,但能省下你几个小时瞎猜的时间。希望帮到你,也祝你的第一个温湿度显示器一次点亮。
本文还有配套的精品资源,点击获取