← 返回教程库

读出第一个传感器数据:DHT11 温湿度

最后更新 2026-06-22
L2 · 传感与交互 ⏱ 约 18 分钟 🟡 涉接线/强电
你将学到
  • 把 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 填第一个 %.1fhumidity 填第二个。
  • 读失败那条用了 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"。)这一步的意义在于:你的设备从"被动读数"变成了"会根据现实做决定"。输入 + 判断 + 输出,这正是几乎所有嵌入式产品的骨架。


动手挑战

别只看,动手改一个:

  1. 让它每读 5 次算一个平均温度再打印,平滑掉单次抖动(提示:用一个累加变量和计数器,只在 err == ESP_OK 时累加)。
  2. 在变体二的基础上做"双阈值":湿度低于 40% 亮 LED,高于 70% 让它快闪,中间不亮。

卡住了?把你的完整代码、用的组件名和想要的效果一起描述清楚发给 AI 让它帮你改,比自己干瞪眼快得多。


小结 · 你现在掌握了什么

  • 你能用 ESP-IDF 组件把 DHT11 接到 ESP32-S3 并读出温度、湿度,用 ESP_LOGI 打到串口。
  • 你理解了为什么要检查 esp_err_t 返回码、为什么读取间隔不能太短、上拉电阻在单总线里干嘛、ESP32-S3 上怎么挑数据脚。
  • 你会用阈值判断把"读数"变成"会做决定的设备"。

这套"读传感器 → 判断 → 驱动输出"的思路,后面换成光照、距离、气体传感器都通用——区别只是换个组件、换个引脚。用组件而不是手搓时序,是产品级开发的常态:站在别人验证过的实现上,把精力留给你真正要做的事。

下一步:让你的温湿度数据走出这块板子。学会让 ESP32 连上 WiFi,把读数发到网上,你的第一个 IoT 设备就成型了。想看更多入门传感器选哪个,去传感器图鉴逛逛。

📄 来源 / 自校链接

本文为公开资料整理,非亲测。关键参数与代码请结合实物与下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为公开资料的学习整理,非亲测。涉接线/花钱/合规的步骤请结合实物与官方最新资料验证,风险自负。见免责声明