玩ESP8266这条线的朋友十有八九都绕不开DHT11这颗温湿度传感器。便宜、好买、资料多不管你是做小气象站、温控风扇还是给屋子搞一个湿度监测基本第一块传感器就是它。而“咸鱼ZTMR”这块小板子本质上是某厂商出的ESP8266核心板引脚布局和经典的NodeMCU基本兼容网上也习惯叫它“咸鱼板”。这两样搭配起来就是一个非常典型的入门级环境数据采集项目。这篇文章不聊那些花里胡哨的应用就把“咸鱼ZTMR DHT11”从接线到读数的完整过程扒开讲清楚。你会看到DHT11的单总线时序到底是怎么一回事为什么读回来的数据偶尔是0或者显示不出来以及代码里那些延时参数能不能随便改。适合刚拿到板子、或者之前玩Arduino但第一次接触ESP8266的读者也适合那些用DHT库读不到数据、想搞明白底层原理的人。1. 项目拆解这个标题背后究竟要解决什么问题1.1 咸鱼ZTMR是什么为什么它适合配DHT11先把这个板子的底细捋一捋。ZTMR是一块基于ESP8266的Wi-Fi开发板网上卖得便宜经常能在二手平台见到它。它的引脚排布基本照搬NodeMCU所以绝大多数NodeMCU教程里的接线和代码拿到它上面都能直接跑。板载的ESP8266芯片自带Wi-Fi这意味着一颗DHT11的数据读回来之后不只是能用串口看还能通过无线网络发到其他地方去。选它配DHT11理由很现实ZTMR是3.3V逻辑的开发板而DHT11的数据引脚虽然标称是5V供电但实际上很多模块在3.3V下也能正常工作。加上DHT11只占一个GPIO口剩下的引脚还能干别的比如接OLED屏、接继电器完全不冲突。对新手来说整个项目的硬件成本可以压在十几块以内非常适合做第一个物联网小项目。1.2 单总线的数据链路从传感器到串口的三步DHT11的数据读取逻辑核心就在“单总线”这三个字上。它不像I2C那样有SCL和SDA两条线也不像SPI那样有专门的主从时钟它只用一根DATA线既要给传感器发命令又要从传感器收数据。整个链路拆开是这样的主机也就是ESP8266先把数据线拉低一段时间这是给DHT11的“起床信号”DHT11收到后回一个响应电平接着就开始往这根线上吐40位数据。这40位里湿度整数、湿度小数、温度整数、温度小数各占8位最后8位是校验和。ESP8266要做的事情就是按规定的时序把这40位一个bit一个bit地抠出来再拼成温度和湿度。值得提前说的是DHT11的采样是有节制的。它内部用的传感器刷新周期大概在1到2秒也就是说你就算每秒读100次它给你的数据也基本不变频繁读取反而容易让总线时序混乱导致读失败。这一点后面写代码的时候会反映在延时设计上。1.3 明确这个项目的输入、输出与目标做任何项目之前先想清楚需求和边界。这个实例的目标很清晰输入DHT11测量到的环境温湿度数据格式为40位二进制串。输出通过串口打印出可读的“湿度xx%温度xx℃”。中间处理ESP8266按单总线时序发起通信、读取数据位、做校验、解析数值。适合谁如果你只是想复制代码、点个灯那用现成的DHT库就行一篇这几百字就够但如果想弄明白“为什么DHT库能读出数据”“为什么别人代码里的延时跟我写的不一样”那你需要把下面的时序拆解看一遍。文章后面两个方案都会讲。2. 硬件连接先解决电源、引脚和上拉这三件大事2.1 物料清单与常见版本差异我这里用的物料很简单咸鱼ZTMR开发板一块DHT11模块一个蓝色四针模块板载上拉电阻那种杜邦线三根Micro USB数据线一根注意区分充电线很多“只能充电不能传数据”的线会让人怀疑人生这里要提醒一个容易踩的坑DHT11有两种常见形态。一种是我上面说的蓝色成品模块板子上已经焊好了电阻和电容VCC、GND、DATA三个引脚引出插上就能用另一种是裸的DHT11传感器四只引脚分别是VCC、DATA、NC、GND其中NC是空脚而且DATA引脚是必须外接一个上拉电阻的。你如果买的是裸传感器别直接怼到开发板上读数据大概率会失败。2.2 接线对照表咸鱼ZTMR与DHT11模块ZTMR的引脚名沿用了NodeMCU的命名习惯比如D1、D2、D3这类编号。接线非常直白ZTMR引脚DHT11模块引脚说明3V3VCC给传感器供电3.3V可用GNDGND共地D3DATA数据线本文示例用D3也就是GPIO0关于供电电压多说一句。DHT11的数据手册上写的是5V供电但实测下来3.3V供电也完全可以工作因为传感器返回的响应信号是“拉低电平”判断起来毫无压力。唯一需要注意的是如果你用的是老版本、品质一般的模块3.3V供电时测量结果可能稍微偏干一点。如果遇到读数异常可以把VCC换到开发板的5V引脚再试一次数据线仍保持3.3V逻辑即可。2.3 上拉电阻为什么DATA线不能悬空很多第一次接触单总线的人会困惑为什么DHT11的DATA引脚要接一个电阻到VCC是不是多余的这不是多余而是单总线协议的基础。DHT11的DATA引脚本质上是开漏输出什么叫开漏你把它想象成一个只能“把线拉低”的开关它自己没法主动把线拉高。要让数据线在不传输的时候保持高电平就得靠外部电阻把它“轻轻的拽上去”。这个电阻就是上拉电阻阻值一般在4.7k到10k之间。如果缺少这个上拉电阻数据线在空闲状态下是悬空的。悬空的电平是不确定的可能是高也可能是低时序就全乱了。成品模块上已经焊了上拉电阻所以插上就能用如果自己用裸传感器做法就是在DATA引脚和VCC之间接一个4.7k电阻。3. 编译烧录开发板配置和驱动方案选型3.1 Arduino IDE 下添加ESP8266支持咸鱼ZTMR本质上就是一块ESP8266开发板所以在Arduino IDE里操作并不复杂。先准备好串口驱动板子插上电脑如果设备管理器里能看到串口那驱动的部分就过了看不到的话找一下CH340或者CP2102的驱动这是最基础的“板子连不上电脑”问题。然后在Arduino IDE里打开“文件—首选项—附加开发板管理器网址”填上ESP8266的包地址。这一步各个教程里都有不再赘述。添加之后在“工具—开发板”里能看到ESP8266系列选择NodeMCU 1.0 (ESP-12E Module)这一项就行了虽然ZTMR不是NodeMCU但引脚定义和Flash大小都兼容选这个板型最省事。烧录的时候波特率选115200Flash大小默认4M即可。点上传之后串口会有一堆乱码输出不用担心这是正常的boot信息。如果上传经常失败大概率是板子自动下载逻辑没触发按一下板子上的RST按键再试一次或者拔插USB线重新让板子上电。3.2 驱动方案选型现成库还是手写时序读DHT11有两种路线。一种是直接用别人写好的库比如DHT sensor library几行代码就能拿到温湿度省心省力另一种是自己写单总线时序驱动把底层每个电平翻转周期都控制好读原始数据再解析。我的建议是如果你只想做一个能用的项目直接上库如果你想彻底搞懂“为什么能读到数据”自己写一遍非常值得。这个实例的核心是后者因为很多所谓“读不出来”的问题恰恰是因为不懂时序看库源码也看不懂。把底层逻辑跑通了以后换DHT22、DS18B20这些单总线设备时上手速度会快很多。4. 核心代码把DHT11的40位数据完整读回来4.1 引脚定义与初始化先定义一个读取引脚。我用的D3在NodeMCU的引脚映射里它就是GPIO0。注意在Arduino代码中如果你写digitalRead(0)它读的就是GPIO0而ZTMR板子丝印上的D3对应这个GPIO。#define DHT11_PIN 0 // 对应板子上的D3 void setup() { Serial.begin(115200); delay(1000); Serial.println(DHT11 Reader Starting...); }这里有个细节启动时不要立刻去读传感器。DHT11上电之后需要大概1秒钟来完成内部的自检和数据准备刚上电就发读取命令它可能还没缓过来。所以setup里加一个延时或者在loop开头加一个delay(2000)是稳妥的习惯。4.2 单总线起始信号与响应检测DHT11的通信第一步是主机拉低总线。时序上有明确要求拉低时间至少18毫秒。这个时间就是为了让DHT11能认出“这是一个开始信号”太短它会忽略掉。具体的代码实现可以这样写// 发送起始信号 void sendStartSignal() { pinMode(DHT11_PIN, OUTPUT); digitalWrite(DHT11_PIN, HIGH); delay(1); // 空闲状态先保持高 digitalWrite(DHT11_PIN, LOW); delay(20); // 拉低至少18ms这里是20ms留足余量 digitalWrite(DHT11_PIN, HIGH); delayMicroseconds(30); // 拉高30微秒后释放总线 pinMode(DHT11_PIN, INPUT); // 释放总线等待DHT11响应 }拉低时间为什么是20毫秒不是18毫秒因为18是下限你稍微多一点不影响但少了就会失败。而拉高到等待响应之间的30微秒则是DHT11识别你“结束起始信号”的时间窗。如果这个时间太短或者太长后面的数据都读不到。起始信号发完之后主机会把总线释放也就是把引脚设为输入模式此时的电平由上拉电阻维持为高。DHT11会在一段时间后主动把总线拉低表示“我收到你的信号了”这就是响应信号。判断响应是否成功的代码// 等待DHT11响应信号返回true表示检测到 boolean waitForResponse() { unsigned long timeout 1000; // 等待上限1ms unsigned long start micros(); // 等待总线变低也就是DHT11开始应答 while (digitalRead(DHT11_PIN) HIGH) { if (micros() - start timeout) return false; } // 检测到低电平后再等待80微秒的低电平结束 delayMicroseconds(80); // 接着应该会跳回高电平 return (digitalRead(DHT11_PIN) HIGH); }响应信号的时间参数大概是先拉低80微秒再拉高80微秒。80这个数字不是随便写的它是厂家定义好的主机只要判断到“先低后高”这个特征就确认传感器在线了。如果卡死在等待低电平的状态要么是接线有问题要么是上拉电阻缺失。4.3 如何通过电平持续时间区分0和1这是DHT11读取里最值得玩味的地方。DHT11的数据线在传输每一位的时候都会先产生一个50微秒的低电平作为“位起始信号”。之后紧跟着的是一个可变长度的高电平。关键就在这个高电平的持续时间如果高电平持续大约26到28微秒这位是0。如果高电平持续大约70微秒这位是1。也就是说每一位都是从低到高跳变开始你只要在检测到上升沿之后测量高电平持续了多久就能判断这一位是0还是1。代码实现如下// 读取一个bit int readBit() { // 等待上升沿 while (digitalRead(DHT11_PIN) LOW); // 等低电平结束 delayMicroseconds(40); // 在40us时采样 if (digitalRead(DHT11_PIN) HIGH) { // 采样点仍然是高说明是高电平长脉宽是1 return 1; } else { // 采样点已经变低说明高电平脉宽短是0 return 0; } }这里用了一个很常见的“40微秒采样法”位起始低电平50微秒后电平跳高。如果是0高电平只维持26~28微秒就掉下去了。等40微秒再去读引脚0的高电平已经结束读到低电平如果是1高电平维持70微秒40微秒时还在高电平期间所以读到高电平。用这种“错峰采样”的方式比精确测脉宽更省事而且对时序误差容忍度更高。4.4 读取完整40位数据并校验有了readBit接下来读取8位字节就顺理成章了。一个字节由8个bit组成执行8次readBit把结果拼起来。// 读取一个字节8位 byte readByte() { byte val 0; for (int i 0; i 8; i) { val 1; // 左移一位腾出位置 val | readBit(); // 把读到的bit放到最低位 } return val; }然后完成40位的完整读取。40位里共有5个字节湿度整数、湿度小数、温度整数、温度小数、校验和。需要注意DHT11的小数位在很多传感器上恒为0但你也得把它读出来因为校验和的计算包含了这5个字节的字节操作。先定义好数据结构byte data[5] {0};然后依次读取五个字节。这里要特别强调DHT11发送每一位时低电平的持续时间是固定的但每个字节之间几乎没有停顿连在一起刚好40位。因此读取过程中不能有任何额外操作打断时序。如果在读取过程中去执行了串口打印哪怕只是几微秒也可能错过数据位的切换导致读到0或者校验失败。读取完整代码boolean readDHT11Data() { sendStartSignal(); if (!waitForResponse()) { return false; } // 跳过响应信号后的80微秒高电平 delayMicroseconds(80); // 读取5个字节 for (int i 0; i 5; i) { data[i] readByte(); } // 校验 byte checksum data[0] data[1] data[2] data[3]; if (checksum ! data[4]) { return false; } return true; }校验的思路很简单DHT11发送的前四个字节相加结果的低8位应该等于第五个字节。如果等于说明传输过程中没有数据错位或丢位如果不等于整包数据作废重新再来。这比你拿到一个数据就信以为真要靠谱得多。最后在loop里解析并打印void loop() { if (readDHT11Data()) { float humidity data[0] data[1] / 10.0; float temperature data[2] data[3] / 10.0; Serial.print(Humidity: ); Serial.print(humidity); Serial.print(% ); Serial.print(Temperature: ); Serial.print(temperature); Serial.println(°C); } else { Serial.println(Read error, retrying...); } delay(2000); }这里delay(2000)是为了保证DHT11的采样间隔。DHT11的采样周期约1秒设置2秒的读取间隔既保证数据是新的又给DHT11留出余量。死循环快速读取反而会经常失败。4.5 更省事的库方案DHT sensor library如果你不想关心时序细节只是想快点看到数据那用库就够了。Arduino IDE的库管理器里搜DHT sensor library安装同时装上Adafruit Unified Sensor库它依赖这个然后#include DHT.h #define DHTPIN 0 #define DHTTYPE DHT11 DHT dht(DHTPIN, DHTTYPE); void setup() { Serial.begin(115200); dht.begin(); delay(1000); } void loop() { float h dht.readHumidity(); float t dht.readTemperature(); if (isnan(h) || isnan(t)) { Serial.println(Failed to read from DHT sensor!); } else { Serial.print(Humidity: ); Serial.print(h); Serial.print(% ); Serial.print(Temperature: ); Serial.print(t); Serial.println(°C); } delay(2000); }这个方案能应付大多数场景但前提是你的接线没问题。一旦读数异常排查起来就得回到前面讲的时序和上拉阻值问题。所以核心的底层逻辑还是值得过一遍的不然出了问题只能干瞪眼。5. 联调与排错串口读数不正常按这三步排查5.1 上电后的第一手信息串口输出了什么把这套代码烧进去打开串口监视器波特率选115200你会看到两类结果。正常的输出是每两秒一行“Humidity: xx% Temperature: xx°C”不正常的输出则有很多种花样常见的包括一直打印“Read error, retrying...”湿度显示0.0%温度显示0.0°C温度和湿度读数几乎不变永远卡在一个奇怪的值上偶尔正常偶尔报错每一种表现背后的原因不太一样。全部报错优先怀疑接线和供电读数全是0但没报错通常说明读取到的数据位全是0很可能是等待响应的环节出了问题导致后面读到的不是真正数据数据卡在不合理值上比如湿度显示99.9%可能是传感器已经损坏或者接错引脚。5.2 排查流程从硬件到时序再到库我的习惯是从物理层往上查。第一步检查接线。把杜邦线拔下来重新插一遍尤其是DATA线不要用那种很松的母对母杜邦线插在面包板上接触不良是万恶之源。有条件的话用万用表量一下VCC和GND之间是否有3.3V或5V。第二步确认模块是成品模块还是裸传感器。裸传感器没加上拉电阻的直接换成品模块再试。这一步能排除大量问题。第三步看时序。用逻辑分析仪是最专业的没有逻辑分析仪也可以先用代码里的超时机制判断如果waitForResponse一直返回false说明DHT11压根没有应答问题在起始信号或者供电上如果响应正常但校验失败那传输过程受干扰或读取时序有问题可以尝试把读bit里的采样时间从40微秒跳到45微秒或35微秒看是否有所改善。我把常见的异常表现和方向整理成了表现象可能原因检查方向一直报错读不到任何数据接线错误、供电不足、模块损坏量电压、重新插线、换模块读回0.0%和0.0°C响应检测失灵导致数据位全为0检查起始信号时序、确认上拉电阻湿度温度偶尔跳到峰值数据传输被中断校验失败检查中断和PIN中断冲突数据完全不变传感器采样未更新或代码delay太短保证读取间隔2秒以上用DHT库读到NaN库的读取失败通常是硬件问题和上面一样先查硬件5.3 一个很多人忽略的坑GPIO中断与读取冲突ESP8266的Wi-Fi协议栈是跑在单核上的和你的程序共享同一个CPU。如果在读取DHT11的过程中底层发生了Wi-Fi中断那40位数据的实时性会被打断就可能出现校验失败。这就是为什么有的项目在没开Wi-Fi时一切都好一连接路由器就开始报错。解决办法有几个方向读取DHT11的短暂过程中可以临时禁止其他中断不过在ESP8266的Arduino环境里操作起来稍微复杂更实际的做法是选择不受Wi-Fi影响的高优先级硬件定时器但这对新手不友好。最务实的方法就是把读取频率放低比如5秒读一次因为每次读取耗时不过几十毫秒Wi-Fi中断碰上的概率会降低很多。如果项目对数据实时性要求高DHT11本身就有点吃力可以考虑换I2C接口的传感器。5.4 实测心得关于模块品质和供电在项目里摸过不少DHT11模块有几点经验比较主观但很实用。便宜的模块个体差异很大同一批两个模块可能一个读数稳定另一个怎么读都校验失败淘宝上很多蓝色模块标称“DHT11模块”但板载电阻阻值有的用4.7k有的用10k都算正常范围如果读数边缘化可以考虑自己飞线换电阻。另外如果模块的VCC接的是3.3V但持续校验失败换成5V试试往往就好了。这个“试试”看起来很玄学但实际上DHT11在3.3V供电下高电平输出幅度更接近3.3V对某些板子来说判断逻辑电平会处于临界状态5V供电能拉大信噪比。6. 扩展成实用小气象站还要做的事6.1 接OLED屏彻底摆脱串口线数据只有串口能看到用起来还是不太方便。常见的扩展是接一个0.96英寸的OLED屏幕I2C接口只要两根线SDA和SCL。在ZTMR上D1和D2分别对应I2C的SCL和SDA。搭配U8g2库代码里在每次读到有效数据后刷新屏幕。需要注意的是OLED和DHT11共用3.3V供电电流不大不会有问题。我自己做过一版把温度和湿度分别显示在两行刷新间隔2秒看起来和小气象站没什么区别。这个方案的优点是不用连电脑上电就能看。6.2 把数据记录成CSV做简单趋势分析如果你想把数据留下来分析最省事的方式是用串口输出CSV格式每隔一分钟写一行。Serial.println(String(millis()) , String(temperature) , String(humidity));然后在电脑上用串口助手把数据存成文件导入表格工具里画个曲线。这个方法很适合做短期监测比如测一下某个房间一天的湿度变化。如果采集时间要横跨几小时甚至几天那就不能一直开着电脑了更合适的做法是在ESP8266本地用文件系统存数据或者定期上报到局域网里的MQTT服务再接一个简单的数据面板。6.3 上报数据前先想清楚采样节奏和功耗DHT11本身并不适合高频上报。它1秒才能刷新一次数据就算你1秒上报10条内容也是重复的。设计一个温湿度上报逻辑时采样间隔和上报间隔应该分开本地2秒读一次用于显示云端5分钟上报一次用于记录。这样既能保证曲线平滑又不会产生大量冗余数据。如果项目要靠着电池或者充电宝长期跑那得考虑耗电问题。ESP8266本身的Wi-Fi功耗就比较可观DHT11反而是省电的那部分。用定时唤醒加deep sleep的方式能大幅降低平均功耗但要注意DHT11唤醒后也需要时间稳定输出通常唤醒后等1秒再读第一次数据比较保险。这部分的扩展其实才是“咸鱼ZTMR实例”最有意思的地方用十几块的硬件拼出一个能感知环境并联网上报的小系统。DHT11作为整个链路的起点读准它、读稳它后面的一切才站得住脚。我在实际调这套组合时最深的感触是DHT11的时序并不难但容错性确实一般。那些“读不到数据”的夜晚到最后几乎都是物理层问题杜邦线接触不良、上拉电阻没接、供电电压卡在临界。把物理层伺候好了软件层面就是一个延时参数的事。如果你也遇到数据读不稳定先把手从键盘上拿开去动动那三根杜邦线。