
本节目标理解 DHT11 的单总线协议与微秒级时序用 RMT 接收通道把脉冲串解析成温湿度读数一、本节要解决的问题温湿度采集是物联网设备最常见的感知需求。DHT11 便宜、量程够用、直接输出数字量不需要像热敏电阻那样做 ADC 标定很适合入门。但它的通信方式很抠门只有一根数据线主机要拉低它发起始信号从机再用同一根线把数据回给主机而且每一位的 0/1 不是电平本身而是高电平持续了多久时间尺度是微秒级。如果用 GPIO 轮询加软件计时一次中断或任务调度就会把测量结果打乱读数经常校验失败。所以本节的做法是让 RMT 接收通道的硬件去测量每个边沿的电平和时长把它们记录成一组符号软件只负责解析这组符号——把最难的测时间交给硬件。二、硬件基础DHT11 有 4 个引脚其中 1 个悬空不用引脚功能VCC供电 3~5.5V超过可能烧毁DATA单总线数据脚双向NC不接GND接地接线要点DATA 与 VCC 之间接一个约5K 的上拉电阻买到带转接板的模块一般已经焊好数据线空闲时靠上拉保持高电平通信时主机和从机轮流把它拉低开发板上信号脚接GPIO25仓库dht11/main/main.c里对应#defineDHT11_GPIO25// DHT11引脚定义注意GPIO25 与教程一致。但如果买的是带转接板的 DHT11引脚顺序可能被重新排列有的把 DATA 放中间务必按丝印核对VCC 接反是最常见的完全读不出数据原因。另外规格书要求上电后等待 1 秒以上再开始测量。三、知识点3.1 数据包40 bit 的结构DHT11 一次通信回传 5 个字节字节内容第 1 字节湿度整数部分第 2 字节湿度小数部分DHT11 恒为 0第 3 字节温度整数部分第 4 字节温度小数部分DHT11 恒为 0第 5 字节校验和 前 4 字节相加后只取低 8 位传输时高位在前、低位在后。因为两个小数字节恒为 0DHT11 实际只有整数精度湿度 ±5%RH、温度 ±2℃量程是湿度 5%~95%RH、温度 -20℃~60℃。教程勘误教程原文写DHT11 的通信包数据长度为 40 个字节正确说法是40 个二进制位bit 5 个字节。教程后面自己列的8bit 8bit 8bit 8bit 8bit也说明它是 40 位。3.2 一次完整通信的时序阶段谁在驱动电平与时长起始信号主机拉低 18ms然后拉高 20~40us响应信号从机先拉低 80us再拉高 80us数据位前导从机每一位数据前都固定拉低 50us数据 0从机高电平约 23~27us数据 1从机高电平约 68~74us结束从机数据发完再拉低 50us之后总线被上拉回高注意这里的套路和上一节 WS2812 一模一样0 和 1 靠高电平的持续时间区分而不是电平本身。3.3 为什么需要微秒级延时FreeRTOS 的时基默认是 1msvTaskDelay/pdMS_TO_TICKS的最小粒度就是毫秒根本表达不了 20us、80us 这种长度所以主机发起始信号时用的是ets_delay_us()它是忙等CPU 空转着数时间精度在微秒级但不会让出 CPU代码里最长的一次忙等是 20ms起始信号的低电平对本例这种单任务演示无所谓但正式产品里要意识到这 20ms 内其他任务都得不到调度。3.4 为什么用 RMT 接收而不是 GPIO 轮询RMT 接收通道会把总线上的每个跳变自动记录成一个符号rmt_symbol_word_t时间基准就是通道配置里的resolution_hz代码把它设成1MHz即 1 tick 1us而 DHT11 最短的脉冲也有 20us 量级精度绰绰有余测量由硬件完成软件即使晚一点来取数据已经测好的脉宽也不会丢一帧 DHT11 数据大约 3~4msRMT 用内部接收内存块攒够一帧后通过中断通知软件比关中断死等友好得多。3.5 判决门限 35us 是怎么来的0 码的高电平约 23~27us1 码约 68~74us两者中间有一大段空白。代码统一用35us作为分界线duration 35 ? 0 : 1往两边各有约 8us 和 33us 的余量即使有轻微抖动也不会误判。四、代码分析4.1 工程结构dht11/ ├── CMakeLists.txt ├── main/ │ ├── CMakeLists.txt # idf_component_register(SRCS main.c dht11.c INCLUDE_DIRS .) │ ├── dht11.c # RMT 接收通道 接收回调 符号解析 │ ├── dht11.h # 对外只暴露 Init 和 StartGet 两个接口 │ └── main.c # app_main()每秒读一次并打印 ├── sdkconfig # CONFIG_IDF_TARGETesp32 └── README.md4.2 初始化DHT11_Initrmt_rx_channel_config_trx_chan_config{.clk_srcRMT_CLK_SRC_APB,// 选择时钟源.resolution_hz1000*1000,// 1 MHz 滴答分辨率即 1 滴答 1 µs.mem_block_symbols64,// 内存块大小即 64 * 4 256 字节.gpio_numdht11_pin,// GPIO 编号.flags.invert_infalse,// 不反转输入信号.flags.with_dmafalse,// 不需要 DMA 后端(ESP32S3才有)};//创建rmt接收通道ESP_ERROR_CHECK(rmt_new_rx_channel(rx_chan_config,rx_chan_handle));//新建接收数据队列rx_receive_queuexQueueCreate(20,sizeof(rmt_rx_done_event_data_t));assert(rx_receive_queue);//注册接收完成回调函数ESP_LOGI(TAG,register RX done callback);rmt_rx_event_callbacks_tcbs{.on_recv_doneexample_rmt_rx_done_callback,};ESP_ERROR_CHECK(rmt_rx_register_event_callbacks(rx_chan_handle,cbs,rx_receive_queue));//使能RMT接收通道ESP_ERROR_CHECK(rmt_enable(rx_chan_handle));和上一节 WS2812 的发送通道对照着看结构体名字从rmt_tx_channel_config_t变成rmt_rx_channel_config_t多了invert_in是否反相输入和with_dma是否用 DMA 后端两个标志位少了发送队列深度。rx_receive_queue是中断 → 任务之间的桥梁接收完成回调在中断上下文里执行不能做耗时的事情所以它只把数据塞进队列主流程在任务上下文里等这个队列。4.3 接收完成回调只做搬运staticbool IRAM_ATTRexample_rmt_rx_done_callback(rmt_channel_handle_tchannel,constrmt_rx_done_event_data_t*edata,void*user_data){BaseType_t high_task_wakeuppdFALSE;QueueHandle_t rx_receive_queue(QueueHandle_t)user_data;// send the received RMT symbols to the parser taskxQueueSendFromISR(rx_receive_queue,edata,high_task_wakeup);returnhigh_task_wakeuppdTRUE;}IRAM_ATTR把函数放进 IRAM保证中断期间一定能执行到函数在中断里执行操作队列必须用xQueueSendFromISR这种带 FromISR 后缀的版本返回值high_task_wakeup告诉内核这次入队是否唤醒了更高优先级的任务需要的话内核会在中断退出时立刻切换任务。4.4 发起一次测量DHT11_StartGet先由主机发出起始信号再把引脚切成输入然后启动 RMT 接收//发送20ms开始信号脉冲启动DHT11单总线gpio_set_direction(DHT11_PIN,GPIO_MODE_OUTPUT);gpio_set_level(DHT11_PIN,1);ets_delay_us(1000);gpio_set_level(DHT11_PIN,0);ets_delay_us(20000);//拉高20usgpio_set_level(DHT11_PIN,1);ets_delay_us(20);//信号线设置为输入准备接收数据gpio_set_direction(DHT11_PIN,GPIO_MODE_INPUT);gpio_set_pull_mode(DHT11_PIN,GPIO_PULLUP_ONLY);先输出高电平 1ms是让总线在起始信号前有一个稳定的高电平起点拉低 20ms满足规格书18ms的要求拉高 20us 后立刻切回输入并打开内部上拉把总线交给 DHT11 驱动。紧接着配置接收过滤窗口并启动接收rmt_receive_config_treceive_config{.signal_range_min_ns100,//最小脉冲宽度(0.1us),信号长度小于这个值视为干扰.signal_range_max_ns1000*1000,//最大脉冲宽度(1000us)信号长度大于这个值视为结束信号};staticrmt_symbol_word_traw_symbols[128];//接收缓存staticrmt_rx_done_event_data_trx_data;//实际接收到的数据ESP_ERROR_CHECK(rmt_receive(rx_chan_handle,raw_symbols,sizeof(raw_symbols),receive_config));// wait for RX done signalif(xQueueReceive(rx_receive_queue,rx_data,pdMS_TO_TICKS(1000))pdTRUE){// parse the receive symbols and print the resultreturnparse_items(rx_data.received_symbols,rx_data.num_symbols,humidity,temp_x10);}return0;signal_range_min_ns比它还窄的脉冲被视为干扰、直接滤掉。这里设 0.1us相当于不过滤1MHz 分辨率下一个 tick 就是 1ussignal_range_max_ns超过它认为总线空闲接收结束。DHT11 最长脉冲是 80us设 1000us 有充分余量rmt_receive()也是异步的启动后马上返回真正等结果的地方是xQueueReceive超时给了 1000ms——正常情况下整帧数据只有几毫秒超时说明传感器没响应raw_symbols和rx_data声明为static生命周期覆盖整个程序。因为一旦超时返回RMT 通道可能还在后台往这块内存写数据放在栈上就有被踩坏的风险。提示与教程不一致教程 PDF 里的片段是signal_range_min_ns 10001us、signal_range_max_ns 200*1000200usraw_symbols是 64 个符号的局部数组仓库改成了 0.1us / 1000us 的过滤窗口和static的128 个符号。放宽滤波是为了不让任何有效边沿被当成干扰丢掉加大缓冲是留余量——一帧的符号数并不是固定值和 RMT 从哪个边沿开始捕获有关教程正文说的42 个符号只是理想情况。以仓库代码为准。4.5 解析parse_items解析函数把一串 RMT 符号还原成温湿度if(item_num41){// 检查是否有足够的脉冲数return0;}if(item_num41)item;// 跳过开始信号脉冲for(i0;i16;i,item)// 提取湿度数据{uint16_tduration0;if(item-level0)durationitem-duration0;elsedurationitem-duration1;rh(rh1)(duration35?0:1);}三个关键点为什么先判断符号个数40 位数据 起始/响应 结束理论上 42 个符号左右数量明显不对说明这一帧没接收完整直接返回失败不浪费后面的计算为什么用if(item-level0)选 duration一个 RMT 符号包含前半段 后半段高电平究竟落在前半段还是后半段取决于接收窗口从哪里对齐。先看哪一段的 level 是 1就取那一段的时长作为高电平时间这样最稳妥这也是教程原文强调的这样最保险(duration 35 ? 0 : 1)就是 3.5 节说的 35us 判决门限配合rh (rh 1) ...左移一位拼出新的一 bit16 次循环后正好拼成 16 位的湿度字。温度和校验同理循环次数分别是 16 次和 8 次。校验与取值部分// 检查校验if((((temp8)temp(rh8)rh)0xFF)!checksum){ESP_LOGI(TAG,Checksum failure %4X %4X %2X\n,temp,rh,checksum);return0;}// 返回数据rhrh8;temp(temp8)*10(temp0xFF);//判断数据合法性if(rh100)*humidityrh;if(temp600)*temp_x10temp;return1;(temp 8) temp (rh 8) rh就是前 4 个字节相加因为temp、rh都是 16 位高位字节和低位字节正好是整数与小数两部分 0xFF只取低 8 位与校验和比较温度被组合成0.1℃ 为单位整数部分 × 10 小数部分。DHT11 的小数字节恒为 0实际就是整数 ×10但这样写可以兼容以后换 DHT22rh 100、temp 600是仓库新增的合法性判断只有通过校验且落在合理范围内才写回输出参数防止把明显的坏数据当成读数。提示与教程不一致教程代码是if (item_num 42) return 0;然后无条件item;仓库改成了if (item_num 41)if(item_num 41) item;。原因如上所述RMT 启动接收与从机应答之间存在时间竞争接收到的符号数可能是 41 也可能是 42仓库的写法两种情况都能兼容教程的写法在 41 个符号时会直接判失败。另外教程版本解析完直接赋值没有范围检查。4.6 应用main.c 主循环DHT11_Init(DHT11_GPIO);while(1){if(DHT11_StartGet(temp,hum)){ESP_LOGI(TAG,temp-%i.%i C hum-%i%%,temp/10,temp%10,hum);}vTaskDelay(1000/portTICK_PERIOD_MS);}DHT11_StartGet返回 1 才打印返回 0超时或校验失败就静默跳过下一次再读temp / 10和temp % 10打印出25.3这样的一位小数间隔 1000ms 不是随便写的DHT11 规格书要求两次采样间隔不少于 1 秒读太快会拿到重复值甚至直接失败app_main开头的nvs_flash_init()和那几行启动日志是仓库模板自带的跟 DHT11 本身无关。五、关键 API 速查API作用所在头文件rmt_new_rx_channel()创建 RMT 接收通道GPIO、分辨率、内存块driver/rmt_rx.hrmt_rx_register_event_callbacks()注册接收完成回调driver/rmt_rx.hrmt_receive()异步启动一次接收指定符号缓冲、长度和过滤配置driver/rmt_rx.hrmt_enable()使能通道收发前必须调用driver/rmt_rx.hxQueueCreate()/xQueueReceive()创建队列、任务上下文里等待数据freertos/queue.hxQueueSendFromISR()中断上下文里往队列发数据freertos/queue.hgpio_set_direction()切换引脚为输入/输出单总线共用一根线时必用driver/gpio.hgpio_set_level()/gpio_set_pull_mode()输出电平、设置上拉driver/gpio.hets_delay_us()微秒级忙等延时esp32/rom/ets_sys.h六、编译与现象验证cdesp32-board/dht11 idf.py build# sdkconfig 中目标已是 esp32idf.py-pCOM端口flash monitor烧录后串口应每秒打印一行类似I (1234) DHT11_Demo: temp-25.3 C hum-60%对着传感器轻轻呵气湿度数字会明显上升如果读到坏数据会看到Checksum failure日志且这一秒不打印温湿度。另外注意DHT11 上电后需要约 1 秒稳定时间第一行读数偏慢或缺失属正常。