ESP32手写DHT11驱动:单总线时序与ESP-IDF实战 📅 发布时间:2026/9/16 6:50:09 👁 浏览次数: 简介面向物联网嵌入式开发者的ESP32实战例程基于ESP-IDF框架与VSCode环境编写演示如何通过C语言读取DHT11数字温湿度传感器数据。压缩包共25个文件以C源码7个.c、7个.h为主配合4个json配置VSCode调试与任务设置、3个txt说明、sdkconfig、分区表csv及CMakeLists等构建文件整体仅47KB目录结构清晰适合ESP32入门及项目原型验证。代码在ESP32-S3上运行验证各引脚接线已在源码中定义并附有详细注释、README及技术答疑入口便于对照硬件调整和排查问题。目前已有675人学习浏览对想快速上手ESP-IDF开发或集成DHT11传感器的开发者具有直接参考价值。1. 物联网嵌入式开发里ESP32 读 DHT11 为什么值得自己写一遍物联网嵌入式开发中的环境监测项目十个里有八个会选 DHT11三根引脚、单总线、单价够低几乎成了“入门传感器”的代名词。可一旦把主控从 Arduino 换成 ESP32并在 ESP-IDF VSCode 编程环境下裸写驱动很多人第一次读到的不是温湿度而是无穷无尽的超时和 CRC 错误。原因在于 DHT11 的数据线需要芯片在微秒级配合先拉低 20ms 发开始信号再释放总线接着逐位读取 40bit 数据0 和 1 的区别只是高电平的宽度差了几十微秒。Arduino 生态有现成库ESP-IDF 却没有官方 DHT11 组件自己实现一遍反而是理解单总线协议最快的方式。这篇文章就按我实际调试的顺序把时序、硬件接线、工程模板、GPIO 驱动和验证手段讲透给刚从点灯进阶到写驱动的人一条能照做的路径。2. 决定成败的前置DHT11 协议时序、上拉电阻和 ESP32 引脚电平2.1 单总线时序DHT11 的 40bit 数据帧和 0/1 判别DHT11 的数据引脚是开漏结构外部上拉电阻把总线默认拉高。主机发起一次通信要先拉低总线至少 18ms再释放并拉高 20-40us然后转入接收。传感器收到这个开始信号后会先拉低约 80us 响应再拉高 80us之后才开始发送数据。这 40bit 的排列非常固定8bit 湿度整数、8bit 湿度小数、8bit 温度整数、8bit 温度小数、8bit 校验和。校验和等于前四个字节相加后取低 8 位。每一位都由 50us 左右的低电平开头数据 0 的后续高电平只有约 26-28us数据 1 的后续高电平则到 70us。所以判别办法可以很简单在每位低电平结束后的第 40us 左右去采样总线读到高就是 1读到低就是 0。这个采样点选在 30us 和 55us 之间都安全也是大多数软件驱动采用固定延时的依据。下表是调试时必须记牢的时序参数阶段电平典型时长说明主机开始信号低18-20ms长度要够不能少于 1ms主机释放后拉高高20-40us随后进入接收状态传感器响应低低80us从机拉低总线传感器响应高高80us之后进入数据位数据位前导低50us每一位固定都有数据 0高26-28us在 40us 处采样为低数据 1高70us在 40us 处采样为高结束位低50us之后总线恢复高参数说明不同批次 DHT11 的手册写的上下限略有差异但采样点放在 40us 附近基本能通吃。不要试图用 GPIO 中断对每一位做精确计时ESP32 的中断响应抖动对单总线来说并不可控任务里轮询反而更稳定。相比把高电平宽度测出来再和 30us 比较我更推荐固定延时采样原因在第四章的代码里可以看到。2.2 ESP32 GPIO 输入输出切换、上拉电阻与最低接线引脚选择上DHT11 的 DATA 建议接 GPIO4、GPIO5 或 GPIO16 这类普通 IO避开 GPIO12、GPIO15 等影响启动模式或连接 flash 引脚的位置。供电直接给 3.3V不要图省事接到 5VDHT11 数据线的高电平和供电电压相关5V 供电时数据线可能被拉到 5V这会击穿 ESP32 的 GPIO。模块版通常已经带了一颗 4.7k 或 10k 上拉电阻买裸芯片的话要在 DATA 和 3.3V 之间外接一颗 4.7k-10k 的电阻。没有上拉电阻时ESP32 内部弱上拉勉强能读但线一长就容易出现校验错误。接线关系如下DHT11 引脚接到 ESP32注意事项VCC3.3V不要用 5VDATAGPIO4避开下载 boot 相关引脚GNDGND必须共地初始化代码用 ESP-IDF 的 GPIO 驱动可以写成开漏模式#include driver/gpio.h #define DHT11_GPIO GPIO_NUM_4 void dht11_init(void) { gpio_set_direction(DHT11_GPIO, GPIO_MODE_INPUT_OUTPUT_OD); gpio_set_pull_mode(DHT11_GPIO, GPIO_PULLUP_ONLY); gpio_set_level(DHT11_GPIO, 1); }逻辑说明GPIO_MODE_INPUT_OUTPUT_OD是开漏模式向引脚写 0 会拉低总线写 1 则释放总线交给上拉电阻拉高gpio_set_pull_mode打开内部弱上拉作为外部电阻不足时的补充。接下来所有收发都在这一个模式下完成不需要来回切换方向和重新配置。参数说明如果模块上已经有 4.7k 上拉可以把内部上拉关掉改成GPIO_PULLUP_DISABLE。内部弱上拉只有约几十千欧孤立使用时容易受寄生电容干扰所以正规做法还是外部加电阻。3. 在 VSCode 中搭建 ESP-IDF 工程并用最小模板跑通编译链路3.1 安装 ESP-IDF 插件、设置工具链并解决 idf.py 路径问题VSCode 里做 ESP-IDF 开发最省事的是安装 Espressif 官方发布的 ESP-IDF 扩展。安装后按CtrlShiftP执行 “ESP-IDF: Configure ESP-IDF Extension”选择自动下载工具链。工具链和 SDK 的安装路径不要出现中文、空格Windows 下建议装在C:\Espressif。新手遇到最多的报错是状态栏提示 “The path for ESP-IDF is not valid: /tools/idf.py not found.”。这通常是插件的 IDF 路径没指对要么手动配置环境变量要么在工程目录的.vscode/settings.json里写明路径{ idf.espIdfPath: C:/Espressif/frameworks/esp-idf-v5.3, idf.toolsPath: C:/Espressif, idf.port: COM3 }参数说明espIdfPath指向包含idf.py的 esp-idf 根目录toolsPath指向工具链目录port填烧录串口。路径分隔符统一用正斜杠Windows 下直接写反斜杠会在解析时出问题。命令行里也可以验证echo $IDF_PATH ls $IDF_PATH/idf.pyWindows PowerShell 用echo $env:IDF_PATH。如果命令找不到idf.py先执行 SDK 目录下的export.batLinux/Mac 是source export.sh再开 VSCode。3.2 用 idf.py create-project 创建工程并理解最小目录结构打开 VSCode 集成终端执行以下命令创建工程idf.py create-project esp32_dht11 cd esp32_dht11 idf.py set-target esp32创建后的目录结构如下esp32_dht11/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt │ └── main.c └── sdkconfig顶层CMakeLists.txt由create-project自动生成正常情况下不用改。真正要动的是main/CMakeLists.txt它通过idf_component_register描述本组件要编译哪些源文件。因为 DHT11 驱动会直接放在 main 目录需要把dht11.c加进去idf_component_register(SRCS dht11.c main.c INCLUDE_DIRS .)逻辑说明SRCS是要编译的源文件INCLUDE_DIRS是头文件搜索路径填.就表示当前目录。这样main.c才能#include dht11.h。如果之后把驱动抽成独立组件这里的写法还要再调整。3.3 编译、烧录、监视三条命令串起来ESP-IDF 的命令行工作流非常固定idf.py build idf.py -p COM3 flash monitorbuild负责编译整个工程flash把固件烧进芯片monitor打开串口监视器Ctrl]退出。第一次编译会检查 ESP-IDF 版本和依赖需要联网下载部分组件。如果 flash 时报找不到串口先确认设备管理器的 COM 口号再回看.vscode/settings.json里的idf.port配置。在 VSCode 里也可以直接点击底部状态栏的芯片图标触发 build编译日志里的错误行可以直接点击跳转到源码比命令行查 output 更顺手。环境准备到这里下一步就是写驱动。4. 手写 DHT11 驱动ESP-IDF 的 GPIO 开漏读写与精确延时4.1 微秒延时选择为什么用 esp_rom_delay_us 而不是 vTaskDelayDHT11 的位宽在几十微秒级FreeRTOS 的vTaskDelay最小粒度是一个系统 tick默认 10ms根本没法用。ESP-IDF 里做阻塞式微秒延时最直接的是esp_rom_delay_us()它来自 ROM 中的ets_delay_us函数不依赖 FreeRTOS。#include esp_rom_sys.h static inline void dht11_delay_us(uint32_t us) { esp_rom_delay_us(us); }逻辑说明esp_rom_delay_us会占用 CPU循环等待指定微秒适合 DHT11 这种短时序收发。它的精度受 CPU 频率和中断影响但 DHT11 对时序的容差足够大固定延时采样方案里并不要求误差小于 1us。参数说明传入单位是微秒不要传毫秒。dht11_delay_us(20000)才是延时 20ms 的正确写法。如果编辑器把esp_rom_sys.h标红多半是 include path 没配置全编译能过就是正常的。另一个常见选择是用esp_timer_get_time()测量时间差适合做超时判断不适合做阻塞延时。因为轮询时间差会引入gpio_get_level的调用开销测量起点和终点不稳。驱动里的 20ms 开始信号用esp_rom_delay_us响应等待用esp_timer_get_time做超时两者分工明确。4.2 完整驱动实现dht11.h 和 dht11.c驱动头文件保持最小接口#pragma once #include esp_err.h typedef struct { float humidity; float temperature; } dht11_data_t; void dht11_init(void); esp_err_t dht11_read(dht11_data_t *out);接口说明dht11_init负责配置 GPIOdht11_read发起一次完整读取成功返回ESP_OK超时或校验失败返回对应的esp_err_t。核心实现在dht11.c#include dht11.h #include driver/gpio.h #include esp_rom_sys.h #include esp_timer.h #define DHT11_GPIO GPIO_NUM_4 #define DHT11_TIMEOUT_US 500 static int dht11_wait_level(int level, int timeout_us) { int64_t deadline esp_timer_get_time() timeout_us; while (gpio_get_level(DHT11_GPIO) ! level) { if (esp_timer_get_time() deadline) { return -1; } } return 0; } void dht11_init(void) { gpio_set_direction(DHT11_GPIO, GPIO_MODE_INPUT_OUTPUT_OD); gpio_set_pull_mode(DHT11_GPIO, GPIO_PULLUP_ONLY); gpio_set_level(DHT11_GPIO, 1); } static esp_err_t dht11_start(void) { gpio_set_level(DHT11_GPIO, 0); esp_rom_delay_us(20 * 1000); gpio_set_level(DHT11_GPIO, 1); esp_rom_delay_us(30); // 等待传感器响应低电平 if (dht11_wait_level(0, DHT11_TIMEOUT_US)) return ESP_ERR_TIMEOUT; // 等待响应高电平 if (dht11_wait_level(1, DHT11_TIMEOUT_US)) return ESP_ERR_TIMEOUT; // 等待高电平结束进入第一个数据位的低电平 if (dht11_wait_level(0, DHT11_TIMEOUT_US)) return ESP_ERR_TIMEOUT; return ESP_OK; } static uint8_t dht11_read_byte(void) { uint8_t byte 0; for (int i 7; i 0; i--) { // 等待当前位的低电平结束 if (dht11_wait_level(1, 100)) return 0xFF; // 在 40us 处采样读到高即为 1 esp_rom_delay_us(40); if (gpio_get_level(DHT11_GPIO) 1) { byte | (1 i); } // 等待当前位的高电平结束回到低电平 if (dht11_wait_level(0, 100)) return 0xFF; } return byte; } esp_err_t dht11_read(dht11_data_t *out) { esp_err_t err dht11_start(); if (err ! ESP_OK) return err; uint8_t data[5]; for (int i 0; i 5; i) { data[i] dht11_read_byte(); } if (dht11_wait_level(1, 100)) return ESP_ERR_TIMEOUT; uint8_t sum (data[0] data[1] data[2] data[3]) 0xFF; if (sum ! data[4]) return ESP_ERR_INVALID_CRC; out-humidity data[0] data[1] * 0.1f; out-temperature data[2] data[3] * 0.1f; return ESP_OK; }逻辑说明dht11_wait_level用esp_timer_get_time设置绝对截止时间轮询直到引脚电平匹配超时返回 -1。dht11_start先拉低 20ms再释放 30us之后三次等待分别对应响应低、响应高、响应结束后进入数据位。dht11_read_byte对每一位先等待低电平结束然后固定延时 40us 采样数据 0 的高电平此时已经结束读到低电平数据 1 的高电平还在持续读到高电平。采样后等待位结束确保下一轮从低电平状态开始。dht11_read读出 5 个字节后先做校验和再换算成浮点温湿度。参数说明DHT11_TIMEOUT_US设为 500us是因为响应阶段每个沿最多约 80us500us 足够宽松dht11_read_byte内部每步等 100us因为每一位的周期约 100-120us如果等待高电平超过 100us 说明协议错乱。超时返回的 0xFF 最终会因校验和不匹配被拦截不会把脏数据放出去。4.3 main.c 调用驱动加日志确认读数在主程序里用 2 秒周期循环读取#include stdio.h #include freertos/FreeRTOS.h #include freertos/task.h #include esp_log.h #include dht11.h static const char *TAG dht11; void app_main(void) { dht11_init(); vTaskDelay(pdMS_TO_TICKS(1000)); // 传感器上电稳定 dht11_data_t dht; while (1) { vTaskDelay(pdMS_TO_TICKS(2000)); // 两次读取间隔至少 1s esp_err_t err dht11_read(dht); if (err ESP_OK) { ESP_LOGI(TAG, hum%.1f%% temp%.1fC, dht.humidity, dht.temperature); } else { ESP_LOGW(TAG, read failed: %s, esp_err_to_name(err)); } } }逻辑说明app_main先初始化 GPIO再延时 1 秒等 DHT11 内部上电完成。循环每 2 秒读一次满足 DHT11 手册要求的读取间隔。失败时把ESP_ERR_TIMEOUT或ESP_ERR_INVALID_CRC打出来方便区分问题方向。参数说明pdMS_TO_TICKS(2000)是 FreeRTOS 的毫秒转 tick 宏默认 tick 频率 100Hz 时等于 200 tick。如果日志里持续出现ESP_ERR_TIMEOUT优先检查接线和上拉电阻出现ESP_ERR_INVALID_CRC则说明时序被干扰可能是线太长、供电纹波大或读取过程被任务抢占。5. 读取结果的验证逻辑分析仪实测位宽和 FreeRTOS 下的稳定轮询5.1 用逻辑分析仪验证数据 0 和数据 1 的采样点如果手头有逻辑分析仪把它接到 GPIO4 和 GND采样率设为 10MHz 以上触发条件选下降沿。抓一次完整的 20ms 开始信号加 40bit 数据帧重点量每个高电平宽度。数据 0 应在 26-28us 附近数据 1 在 70us 附近且每一位前都有约 50us 低电平。如果测出来数据 0 的宽度超过 35us说明总线电容大或采样点太靠后可以缩短示例代码里的esp_rom_delay_us(40)到 35us 再试。没有逻辑分析仪时可以在dht11_read_byte里临时把高电平宽度通过ESP_LOGI打出来但要注意esp_timer_get_time统计本身会引入偏差只能作为参考。5.2 用 vTaskSuspendAll 避免任务抢占破坏接收时序默认情况下app_main里的循环会因为其他 FreeRTOS 任务的 tick 中断被抢占。ESP32 的 tick 中断很短通常不影响 DHT11 读取但如果日志里偶发ESP_ERR_INVALID_CRC可以考虑在读取期间挂起调度器vTaskSuspendAll(); err dht11_read(dht); xTaskResumeAll();参数说明vTaskSuspendAll只禁止任务上下文切换不会关中断20ms 的开始信号期间 WiFi 等系统中断仍能运行因此不会破坏连接。读取过程约几十毫秒挂起调度器对应用层影响可忽略。如果这样做后 CRC 错误清零说明问题确实出在任务抢占。如果做低功耗物联网设备方向是延长读取周期并配合深度睡眠每次唤醒后先等 1 秒让 DHT11 稳定再读取读完保存数据后进入esp_light_sleep_start。DHT11 本身测量周期长不适合做高频采集连续读取时传感器自身发热也会让温度读数偏高2 秒间隔已经是它的工作上限真要追求精度同价位选 SHT30 或 AHT20 是更合适的方案。本文还有配套的精品资源点击获取