DHT11:按严格时序读取温湿度

2026-09-10 00:00    #ESP32-S3   #ESP-IDF   #外设   #阶段3  

本课对应《DNESP32S3 使用指南(IDF 版)》第 30 章。配套完整工程:examples/25-dht11。

环境与验收边界: DNESP32S3 V1.2,ESP-IDF v5.5.5;本机 macOS 构建通过。Ubuntu 26、外设接线、屏幕效果和实际测量仍待实板验收。下文输出均为预期格式,不能当作已采集的硬件日志。

本课完成后能做什么

硬件连接与资源

信号或资源对应关系
DHT11 数据GPIO0,P5 IO0↔1WIRE_DQ
供电与上拉U4 的 3.3V、GND、4.7 kΩ
共用资源与 DS18B20 二选一;不要同时按 BOOT
周期上电等 1 秒,采样间隔至少 2 秒

以下为正点原子 DNESP32S3 V1.2 原理图的相关页,保留原厂标识;表格用于快速定位,遇到板卡版本差异以实际原理图为准。点击图片可查看引脚与网络名。

DNESP32S3 原理图第 1 页

DNESP32S3 原理图第 2 页

本课信号与数据处理流程

同样是一根线,协议却不同

V1.2 使用 U4 插座:DHT11 第 1 脚 VCC 对 U4 第 1 脚,第 2 脚 DATA 对 U4 第 2 脚,第 3 脚 NC 对第 3 脚,第 4 脚 GND 对第 4 脚。原理图中第 3、4 脚连地。教材某些段落写成 U6,应按本板 U4 丝印及网络名定位。

U4 插座的供电、数据和地

DHT11 和 DS18B20 共用板上插座,不代表命令兼容。DHT11 没有 0xCC/0x44 这样的命令流,而是主机给出足够长的起始低电平,再根据传感器脉冲时长读取 40 位。

主机拉低至少 18 ms,本例用 20 ms 任务延时。释放总线后,传感器发出约 80 μs 低、80 μs 高的响应;每个数据位先低约 50 μs,再以约 26~28 μs 高表示 0、约 70 μs 高表示 1。工程用 40 μs 作为分界。

40 位拆成哪五个字节

数据按高位先到,依次为湿度整数、湿度小数、温度整数、温度小数、校验和。前四字节相加取低 8 位,应等于第五字节。DHT11 的小数表现依模块版本而异,本例按十分位处理,不把显示小数位数当成真实精度。

每次等待电平变化都有 100 μs 的上限。遇到断线、电平持续为高或持续为低时,程序都会尽快返回失败,避免 CPU 永久停在 while 循环中。只有校验成功且范围合理,才将局部解码结果复制给调用者。

为什么不能用任务延时测量每一位

FreeRTOS tick 即使配置为 1 ms,也远长于几十微秒的高电平。若用 vTaskDelay(1) 等待一个位,采样回来时数据早已结束。本课使用短微秒忙等,并在几毫秒左右的接收窗口内进入临界区;所有失败路径都退出临界区。

这种实现便于讲清协议,但接收期间会推迟中断处理,不能无条件用于实时性要求高的项目。后续可用 RMT 测量脉宽。本例不启用无线任务,软件循环的测量偏差仍需实板检验。

工程如何组织

驱动放在 components/BSP/,应用放在 main/main.c。本工程包含 DHT 模块。ESP-IDF 会自动发现项目根目录下的 components/BSP。main 通过 PRIV_REQUIRES BSP 声明组件依赖,INCLUDE_DIRS 则导出编译时使用的头文件搜索路径,不涉及运行时文件查找。

固定引脚通过 CONFIG_LESSON_* 常量定义在本工程的 Kconfig.projbuild 中,构建后可在 sdkconfig 中核对。这些默认值对应本板连线,仅修改配置数字并不能改变硬件连接。

应用代码:入口与处理流程

下面代码直接取自配套工程的 main/main.c,头文件和 BSP 实现在仓库中一并提供。初学时先看 app_main(),再追踪它调用的模块。

 1#include <stdio.h>
 2#include <stdint.h>
 3#include <stdbool.h>
 4#include <string.h>
 5#include <stdlib.h>
 6#include <assert.h>
 7#include "esp_err.h"
 8#include "esp_log.h"
 9#include "esp_timer.h"
10#include "freertos/FreeRTOS.h"
11#include "freertos/task.h"
12#include "dht.h"
13void app_main(void)
14{
15    ESP_ERROR_CHECK(dht_init());
16    vTaskDelay(pdMS_TO_TICKS(1000)); // 上电稳定时间。
17    while(1) {
18        float temperature,humidity;
19        esp_err_t err=dht_read(&temperature,&humidity);
20        if(err==ESP_OK) ESP_LOGI("dht11","temperature=%.1f C humidity=%.1f %%",temperature,humidity);
21        else ESP_LOGW("dht11","%s",esp_err_to_name(err));
22        vTaskDelay(pdMS_TO_TICKS(2000)); // DHT11 不适合高速采样。
23    }
24}

BSP 关键实现:DHT

以下是本课对应驱动的完整源文件。对外声明见 dht.h。

 1#include "sdkconfig.h"
 2#include "dht.h"
 3#include "driver/gpio.h"
 4#include "esp_rom_sys.h"
 5#include "freertos/FreeRTOS.h"
 6#include "freertos/task.h"
 7#define DQ CONFIG_LESSON_ONEWIRE
 8static portMUX_TYPE s_lock=portMUX_INITIALIZER_UNLOCKED;
 9esp_err_t dht_init(void)
10{
11    gpio_config_t c={.pin_bit_mask=1ULL<<DQ,.mode=GPIO_MODE_INPUT_OUTPUT_OD,.pull_up_en=true};
12    esp_err_t err=gpio_config(&c); gpio_set_level(DQ,1); return err;
13}
14// 记录当前电平维持了多少微秒;每一步都有上限,断线不会永久卡死。
15static int pulse(int level,int max_us)
16{
17    int t=0;
18    while(gpio_get_level(DQ)==level) {
19        if(t>=max_us) return -1;
20        esp_rom_delay_us(1); t++;
21    }
22    return t;
23}
24esp_err_t dht_read(float *temperature,float *humidity)
25{
26    uint8_t b[5]={0};
27    gpio_set_level(DQ,0);
28    vTaskDelay(pdMS_TO_TICKS(20)); // 主机起始信号至少 18 ms,任务可让出 CPU。
29    bool ok=true;
30    portENTER_CRITICAL(&s_lock);
31    gpio_set_level(DQ,1);
32    // 先等响应低电平,再跳过约 80 us 低、80 us 高的响应前导。
33    if(pulse(1,100)<0 || pulse(0,100)<0 || pulse(1,100)<0) ok=false;
34    for(int bit=0;ok && bit<40;bit++) {
35        if(pulse(0,100)<0) { ok=false; break; }
36        int high=pulse(1,100);
37        if(high<0) { ok=false; break; }
38        b[bit/8]=(b[bit/8]<<1)|(high>40);
39    }
40    portEXIT_CRITICAL(&s_lock); // 包括失败路径,也必须离开临界区。
41    if(!ok) return ESP_ERR_TIMEOUT;
42    if((uint8_t)(b[0]+b[1]+b[2]+b[3])!=b[4]) return ESP_ERR_INVALID_CRC;
43    float h=b[0]+b[1]*0.1f;
44    float t=(b[2]&0x7f)+b[3]*0.1f;
45    if(b[2]&0x80) t=-t;
46    if(h>100 || t<0 || t>50) return ESP_ERR_INVALID_RESPONSE;
47    *humidity=h; *temperature=t; // 校验成功之后才更新调用者的数据。
48    return ESP_OK;
49}

CMake 与中文注释

1# 私有依赖只供本组件源码使用。
2idf_component_register(SRCS "main.c" INCLUDE_DIRS "."
3    PRIV_REQUIRES esp_timer BSP )

每个工程有自己的 project() 名称和 sdkconfig.defaults。VS Code 已关闭普通 CMake 自动配置,C/C++ 语言服务从 build/compile_commands.json 读取编译信息。首次构建前,这个文件还不存在,需要先激活 IDF 并完成构建,无需手工拼接头文件路径。

编译、烧录与观察

 1# 激活 EIM 安装的 IDF;每个新终端都需要激活。
 2source ~/.espressif/tools/activate_idf_v5.5.5.sh
 3# 进入单个示例根目录,不是在 examples 总目录运行。
 4cd ~/mycode/esp32-blog/examples/25-dht11
 5# 首次配置目标;已有正确配置时不用每次重复 set-target。
 6idf.py set-target esp32s3
 7# 编译会生成 build/compile_commands.json,供 VS Code 使用。
 8idf.py build
 9# 把这个值换成实际连接开发板的端口;macOS 通常为 /dev/cu.*。
10PORT=/dev/ttyUSB0
11idf.py -p "$PORT" flash monitor
12# 串口监视退出:Ctrl+]。

预期输出或屏幕内容(示意;数值、日志前缀和启动时间依现场而变):

1dht11: temperature=25.0 C humidity=60.0 %
2# 数值是格式示例,不是实测保证。

故障定位与逐项验收

先确认目标为 esp32s3,再检查实际端口、供电,并按本文引脚表核对跳线。编译成功只能说明源码与工具链兼容,模块型号、物理线路和时序仍需实板确认。

遇到通信错误,先检查错误码和对应模块,不要用“错误时返回 0”掩盖异常。文中的预期日志只供阅读程序时参考;实板验收时,应记录实际现象,保存日志和接线照片。

与教材和官方实现对照

对照教材第 30 章正文:保留 DHT11 起始与 40 位时序,增加每个等待阶段上限、校验和和错误隔离;不与 DS18B20 同时运行。

本地验证记录与参考

返回课程目录