读出第一个传感器数据:DHT11 温湿度
- 把 DHT11 接到 ESP32-S3 上
- 用 ESP-IDF 组件读出温度和湿度并打印到串口
- 避开"数据一直读成 0 或 nan"这个经典坑
你已经会让 LED 听话地闪了——那是"输出",是你的程序在往外发指令。这一节反过来:让 ESP32-S3 第一次接收现实世界的信息。你想做个能看温湿度的小东西,一搜,新手教程全是 DHT11。十块钱不到、三根线,它确实是入门传感器的标准答案。但它也有个坑能卡你一晚上,我们最后专门拆开讲。
读这篇前,你需要已经跑通过点亮第一个 LED:知道怎么用 idf.py set-target esp32s3 选芯片、idf.py build flash monitor 一条龙编译烧录看日志。如果这几步还生,先回去补,因为本节不再重复这些操作细节。
本节涉及给传感器供电、接 GPIO。VCC 一律接 3.3V(下面会说为什么别接 5V),动线前断开 USB,接好再上电。
第一步:它怎么接
DHT11 通常是三根脚(带底板的模块版):
- VCC → ESP32-S3 的 3.3V
- GND → GND
- DATA → 任意一个普通 GPIO,比如 GPIO4
就这么简单,单总线传感器只占一个数据脚。接好后大概长这样:
ESP32-S3 3V3 ──→ VCC
ESP32-S3 GND ──→ GND
ESP32-S3 GPIO4 ──→ DATA
市面上 DHT11 有两种封装:三脚的模块板(带蓝色或绿色小 PCB,已经焊好上拉电阻)和四脚的裸传感器(蓝色塑料壳,第三脚悬空不接)。本节按三脚模块讲。如果你手里是四脚裸件,记住第 3 脚不接、DATA 是第 2 脚,并且必须自己补一个上拉电阻——后面"为什么"那节会说清楚。
VCC 接 3.3V,别接 5V。DHT11 标称支持 3~5.5V,但 ESP32-S3 的 GPIO 是 3.3V 逻辑,如果你用 5V 供电传感器,它的 DATA 脚可能输出接近 5V 的高电平,长期灌进 3.3V 的引脚有损伤风险。同电压域最省心。具体电平阈值以你手里 ESP32-S3 模组的官方手册为准。
第二步:上代码
DHT11 走的是单总线时序协议——主机先拉低数据线发"起始信号",传感器回一段"响应",然后把 40 个 bit(湿度高 8 位、湿度低 8 位、温度高 8 位、温度低 8 位、校验和)一位一位地用"高电平持续多少微秒"编码送回来。这套微秒级时序自己写极容易写错,而且 ESP-IDF 没有内置 DHT 驱动——它的 driver/gpio.h 只管单根引脚的电平,不替你解析这种私有时序。
所以正路是用现成的组件,让别人把数微秒、校验这些脏活封好。ESP-IDF 5.x 自带组件管理器,一条命令就能从 ESP Component Registry 拉乐鑫官方维护的 espressif/dht:
idf.py add-dependency "espressif/dht"
跑完它会在你工程的 main/ 下生成(或更新)一个 idf_component.yml,内容大致是:
# main/idf_component.yml ——组件管理器据此拉依赖
dependencies:
espressif/dht: "^1.0.0"
idf:
version: ">=5.0"
下面这段是参考实现,函数名/参数顺序按 espressif/dht 写。但社区里还有 UncleRus 的 esp-idf-lib dht 组件 等不同实现,API 名和返回类型略有差异。请以你实际拉到的那个组件 README 为准自校一遍,别照抄就上传。
下面这段是按 espressif/dht 写的完整可跑代码,整段放进 main/main.c:
// 读 GPIO4 上的 DHT11,每 2 秒打印一次温湿度
#include "dht.h" // 来自 espressif/dht 组件
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "dht11";
#define DHT_GPIO GPIO_NUM_4 // DATA 接在哪个 GPIO
#define DHT_KIND DHT_TYPE_DHT11 // DHT22 改成 DHT_TYPE_AM2301
void app_main(void)
{
while (1) {
float humidity = 0, temperature = 0;
// 一次读出温湿度;返回 ESP_OK 才是真数据
esp_err_t err = dht_read_float_data(DHT_KIND, DHT_GPIO,
&humidity, &temperature);
if (err == ESP_OK) {
ESP_LOGI(TAG, "温度 %.1f°C 湿度 %.1f%%", temperature, humidity);
} else {
ESP_LOGW(TAG, "读取失败 (%s),检查接线", esp_err_to_name(err));
}
vTaskDelay(pdMS_TO_TICKS(2000)); // DHT11 至少隔 1 秒读一次
}
}
在工程目录下一条命令编译、烧录、看日志:
idf.py build flash monitor
(第一次用别忘了先 idf.py set-target esp32s3。)
你应该看到什么
烧录成功后自动进入串口监视(monitor)。正常的话,每两秒滚出一行带时间戳和 TAG 的日志,像这样:
I (2312) dht11: 温度 24.0°C 湿度 53.0%
I (4318) dht11: 温度 24.0°C 湿度 52.0%
I (6324) dht11: 温度 25.0°C 湿度 54.0%
具体数字取决于你房间的实际温湿度。想验证它真在工作?对着传感器哈一口气,下一两次刷新里湿度会明显跳上去(常常蹿到 70%、80%),过几秒又慢慢落回来。这一下,就是你的程序第一次"摸到"了现实世界。按 Ctrl + ] 退出监视。
如果滚出来的全是 读取失败,或者温湿度一直是 0.0——别急,这正是那个经典坑,第五步专门治它。
第三步:把这几行代码讲透
例子能跑只是起点,看懂它你才能改它、复用它。逐块拆:
#include "dht.h" // 组件头文件,时序与校验它替你处理
static const char *TAG = "dht11"; // ESP-IDF 日志的"标签",区分是谁打的
#define DHT_GPIO GPIO_NUM_4 // DATA 脚接在哪个 GPIO
#define DHT_KIND DHT_TYPE_DHT11 // 告诉组件是 DHT11(DHT22 时序不同)
#include "dht.h":把组件的功能拉进来。这个头文件是idf.py add-dependency之后由组件管理器放到搜索路径里的,所以你能直接 include。没有它,下面的dht_read_float_data不认识。TAG:ESP-IDF 不用Serial.print,而是用ESP_LOGI/ESP_LOGW这套分级日志,每条都带一个 TAG。好处是以后日志多了,你能按 TAG 过滤、按级别(I 信息 / W 警告 / E 错误)调整,产品调试时这套比"一把梭打印"省心得多。#define:给数字/常量起名字。改接到别的脚,只改DHT_GPIO这一行;换 DHT22 只改DHT_KIND。
esp_err_t err = dht_read_float_data(DHT_KIND, DHT_GPIO,
&humidity, &temperature);
- 这是核心动作——向传感器要一次数据。组件内部替你完成"发起始信号 → 等响应 → 收 40 bit → 校验和核对"整套时序,你只管调一次函数。
- 注意它不是返回温湿度,而是返回一个
esp_err_t错误码,温湿度通过指针(&humidity、&temperature)写回你的两个变量。这是 ESP-IDF 的惯用风格:返回值专门用来报告成功/失败,数据走出参。 - 为什么必须检查返回值:物理世界的读取本来就会偶尔失败(时序被打断、校验和不过)。
err == ESP_OK才说明这次读到的是真数据;否则就是一坨失败,别拿去用。养成每次读传感器都判断返回码的习惯,不然迟早被一次失败读出的脏值坑到。
ESP_LOGI(TAG, "温度 %.1f°C 湿度 %.1f%%", temperature, humidity);
ESP_LOGI是"打一条 Info 级日志"。格式串和printf一样:%.1f表示"按浮点、保留 1 位小数";%%转义出一个真正的%号。temperature填第一个%.1f,humidity填第二个。- 读失败那条用了
ESP_LOGW(Warning 级),并用esp_err_to_name(err)把错误码翻译成可读名字(比如ESP_ERR_TIMEOUT),方便你一眼看出失败原因。
vTaskDelay(pdMS_TO_TICKS(2000)); // 等 2 秒再读下一次
vTaskDelay是 FreeRTOS 的"让当前任务睡一会儿",睡的这段时间 CPU 让给别的任务(这点 L1 讲过)。pdMS_TO_TICKS(2000)把"2000 毫秒"换算成系统节拍数。- 这行不是随便写的 2 秒,它是 DHT11 能不能正常工作的关键,下一节细说。
第四步:"为什么"再深一层
抄通了不算懂。把下面两个"为什么"想明白,你以后碰到别的单总线传感器、别的组件,都能举一反三。
为什么读取间隔不能太短
DHT11 是个"慢性子"。它内部自己测温测湿、做模数转换需要时间,它的数据更新周期约 1 秒(DHT22 更慢,约 2 秒)——你读得比它产出还快,它根本来不及给你新值,于是 dht_read_float_data 直接返回失败码。代码里写 2 秒就是留足余量。把 vTaskDelay 改成 pdMS_TO_TICKS(200) 试试,你大概率会看到一堆 读取失败。这不是 bug,是你在跟硬件的物理节奏抢时间。
别在 DHT11 上追求"实时"。温湿度本来就是缓变量,房间温度不会在半秒内跳几度,2 秒一刷已经绰绰有余。需要更快采样的场合(比如气流、呼吸监测),DHT11 本身就不是对的器件。
为什么 DATA 脚要上拉电阻
DHT11 和 ESP32-S3 之间只有一根数据线,平时谁都不说话时,这根线必须有个确定的"默认电平"——靠一个上拉电阻把它轻轻拉到高电平(3.3V)。没有它,空闲时这根线会"飘"(电平不确定),通信时序就乱了,表现就是时好时坏地读失败。
通信时,双方靠把这根线拉低、保持特定的微秒数来传 0 和 1(这就是"单总线协议",一根线既收又发,全靠时间长短编码)。组件替你数这些微秒,但前提是线本身有个干净的高电平基准——这正是上拉电阻干的事。想把单总线、上拉这些底层原理彻底吃透,看上拉电阻原理。
ESP32-S3 的 GPIO 内部也有可配置的上拉,组件初始化时可能帮你打开。但内部上拉阻值偏大(几十 kΩ),线一长、干扰一大就不够稳。裸传感器仍建议外接一个 10kΩ 实体上拉到 3.3V,这是最稳的做法,别只指望内部上拉。
数据老是读成 0 或读取失败? 八成是这四件事之一:① DHT11 采样慢,读取间隔别低于 1 秒(DHT22 别低于 2 秒),读太快它来不及给数;② DATA 脚和 VCC 之间最好加一个 10kΩ 上拉电阻(很多模块板已自带,裸传感器要自己加);③ 供电不稳 / 杜邦线太长信号衰减——换短线、插紧、确认 3V3 供得上;④ GPIO 选了仅输入脚或特殊功能脚(ESP32-S3 上 GPIO19/20 默认是 USB、部分脚是 SPI Flash 专用),单总线要求引脚能输入也能输出,挑个普普通通的 GPIO(如 GPIO4)。
逐条对应着排:现象①是节奏问题,②是电气问题,③是物理连接问题,④是选脚问题。新手九成的"读不出来"都落在这四类里。
第五步:故障排查表
照现象对号入座,从上往下查:
| 现象 | 最可能的原因 | 怎么办 |
|---|---|---|
monitor 一片空白,啥日志都没有 |
没烧进去 / 没进 monitor / 芯片没选对 | 确认 idf.py set-target esp32s3 选过;重跑 idf.py flash monitor |
一直 读取失败 |
读取间隔太短,或上拉缺失 | 确认 vTaskDelay(pdMS_TO_TICKS(2000));裸传感器补 10kΩ 上拉到 3V3 |
| 温湿度一直纹丝不动是 0.0 | DATA 接错脚 / 没接好 | 核对 DATA 真接在 GPIO4;哈气测试看数会不会动 |
| 偶尔成功、偶尔失败 | 杜邦线接触不良 / 面包板松 / 线太长 | 换短线、插紧,模块虚焊的重焊 |
| 数据明显离谱(如 -100°C) | 型号选错(接的是 DHT22 当 DHT11 读) | 把 DHT_KIND 改成你实际的型号(DHT22 用 DHT_TYPE_AM2301) |
编译报找不到 dht.h |
没拉组件,或 idf_component.yml 没生效 |
重跑 idf.py add-dependency "espressif/dht",再 idf.py reconfigure |
DHT11 精度一般(温度 ±2°C、湿度 ±5%),够玩够学。哈气能跳、放着稳定,就说明它在正常工作,别纠结那一两度的误差。想要更准,看图鉴里的 DHT22 或更高规格的型号。这些误差范围以官方数据手册为准,下载链接见本页底部的资料来源。
玩出花样:两个变体
读到数据只是半成品,把数据"用起来"才有意思。
变体一:换成 DHT22,更准更慢
手里是 DHT22(也叫 AM2301,白色方壳)?接线一模一样,代码只改一行——把型号换掉,并把读取间隔放到 2 秒以上:
#define DHT_KIND DHT_TYPE_AM2301 // DHT22 在多数组件里就叫 AM2301
// ...
vTaskDelay(pdMS_TO_TICKS(2500)); // DHT22 更慢,间隔放宽到 2.5 秒更稳
DHT22 量程更宽、精度更高(温度 ±0.5°C、湿度 ±2%),但更贵、更慢。具体类型枚举名以你拉到的组件 README 为准(有的写 DHT_TYPE_DHT22,有的写 DHT_TYPE_AM2301,含义相同)。
变体二:加一个阈值报警
让它不只是"显示",还会"判断"。比如湿度低于 40% 就点亮你上一节接的 LED 提醒该加湿了——这就用上了你已经会的 GPIO 输出能力:
// 在 app_main 开头(while 之前)先把报警 LED 设成输出:
// gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
// 然后在读到有效数据(err == ESP_OK)的分支里加这段判断:
if (humidity < 40) {
gpio_set_level(GPIO_NUM_2, 1); // 湿度偏低,点亮 LED 报警
} else {
gpio_set_level(GPIO_NUM_2, 0);
}
(记得在文件顶部 #include "driver/gpio.h"。)这一步的意义在于:你的设备从"被动读数"变成了"会根据现实做决定"。输入 + 判断 + 输出,这正是几乎所有嵌入式产品的骨架。
动手挑战
别只看,动手改一个:
- 让它每读 5 次算一个平均温度再打印,平滑掉单次抖动(提示:用一个累加变量和计数器,只在
err == ESP_OK时累加)。 - 在变体二的基础上做"双阈值":湿度低于 40% 亮 LED,高于 70% 让它快闪,中间不亮。
卡住了?把你的完整代码、用的组件名和想要的效果一起描述清楚发给 AI 让它帮你改,比自己干瞪眼快得多。
小结 · 你现在掌握了什么
- 你能用 ESP-IDF 组件把 DHT11 接到 ESP32-S3 并读出温度、湿度,用
ESP_LOGI打到串口。 - 你理解了为什么要检查
esp_err_t返回码、为什么读取间隔不能太短、上拉电阻在单总线里干嘛、ESP32-S3 上怎么挑数据脚。 - 你会用阈值判断把"读数"变成"会做决定的设备"。
这套"读传感器 → 判断 → 驱动输出"的思路,后面换成光照、距离、气体传感器都通用——区别只是换个组件、换个引脚。用组件而不是手搓时序,是产品级开发的常态:站在别人验证过的实现上,把精力留给你真正要做的事。
下一步:让你的温湿度数据走出这块板子。学会让 ESP32 连上 WiFi,把读数发到网上,你的第一个 IoT 设备就成型了。想看更多入门传感器选哪个,去传感器图鉴逛逛。