很多人在拿到DHT11这块蓝色小模块时,第一反应是照着网上的例程把代码抄一遍,结果读回来的温湿度不是0就是乱码,甚至干脆卡死在等待应答的循环里。我早期调这块传感器的时候也被折腾过几个晚上,后来把单总线这条链路上的每个环节彻底搞明白之后才发现,DHT11的"脾气"其实就是电气时序和通信协议的组合问题。这篇文章不打算简单甩一份代码,我想把器件原理、单总线协议、时序细节和排错方法放在一起完整拆开讲,让你不仅能跑通,还能在出问题时自己定位。
这篇文章适合正在学习单片机和嵌入式通信的开发者,无论你用STM32、51还是其他MCU,只要想真正搞懂DHT11乃至单总线通信机制的,接下来这部分内容应该都能帮到你。我会从单总线为什么存在讲起,一路拆到驱动代码里每个延时和每个判断条件,最后把实战中踩过的坑和排查链路全部摊开。
1. 单总线不是玄学:DHT11凭什么叫"单总线"
1.1 从三条线到一条线,省下的引脚是用时序换来的
传统数字传感器通信通常需要至少两条线,比如IIC的SCL和SDA,SPI的SCK、MOSI、MISO再加上片选信号。而DHT11这类器件用一根数据线就完成了主机和传感器之间的所有通信,这根线就是所谓的单总线(1-Wire,也叫单总线协议或单线协议)。
它的基本逻辑很简单:所有通信都通过这根线的高低电平变化以及变化的时间长度来表达。发送的是一位数据也好,接收的一个应答信号也好,本质上都是在约定好的时间段内拉高或拉低这根线,然后对方通过采样"这根线处于什么电平"来解读信息。
正因为所有信息都挤在一根线上,单总线通信对时序的敏感度非常高。IIC好歹还有时钟线同步,设备双方按照SCL的节拍来交换数据,而单总线没有独立的时钟线,全靠预先约定好的电平持续时间来区分"0"和"1"。这就好比两个人约定用敲墙编码交流,有人敲得长代表"1",敲得短代表"0",但如果敲的人手速不稳或者听的人走神半秒,信息就全乱了。
1.2 单总线与IIC、SPI的取舍:一个引脚换来的代价
很多初学者会问:单片机引脚又不缺,为什么非要选单总线?实际上这是成本和应用场景共同决定的结果。
单总线最大的优势就是节省IO资源。在一个GPIO紧张的MCU上,挂一个DHT11只占一个引脚,而且通信速率通常也就几十kbps,用于读取温湿度这种变化缓慢的数据完全够用。相比之下,SPI需要至少3根线(如果只读不写可以省掉MISO),IIC也需要两个上拉电阻和两根线,对于单纯读一个温湿度来说确实有点"杀鸡用牛刀"。
但单总线的代价也很明显:没有时钟同步机制,所有时序都必须靠延时函数硬抠;通信距离短,一般几米以内才稳定;抗干扰能力弱,线稍长或者环境有电磁干扰就容易失败。IIC和SPI在传输稳定性、速率和灵活度上都要强不少,这也是为什么工业级的传感器更多用RS485、Modbus协议而不是单总线。
1.3 DHT11是标准1-Wire吗:先把概念说清楚
严格来说,DHT11并不是Maxim公司定义的1-Wire协议。标准的1-Wire协议(代表性器件是DS18B20)支持在总线上挂接多个设备,每个设备有唯一的64位ROM序列号,主机会先发起ROM搜索命令来识别总线上有哪些设备,然后才能和指定设备通信。
而DHT11只是在电气特性和时序上类似单总线,它没有ROM ID,也不支持多设备寻址,一根线上只能挂一个DHT11。所以更准确的说法是:DHT11采用单总线通信方式,但不是标准1-Wire协议。网上很多文章把这两者混为一谈,导致有人试图在一根总线上挂多个DHT11,结果自然是失败的。
有些文章会用"DHT11单总线协议"来命名,这没问题,关键是你要理解它和DS18B20那种标准1-Wire是两回事。写代码的时候,DHT11的时序既不能套用DS18B20的驱动,也不能直接用Maxim 1-Wire库,必须单独实现。
2. 动手前的必备认知:DHT11内部在做什么
2.1 器件引脚与典型电路
DHT11常见的有4脚直插封装和模块两种形态。直插封装的引脚定义在正面丝印上有标注,一般从左到右依次是VCC、DATA、NC、GND。DATA引脚就是单总线通信的数据线,NC为空脚,不用连接。
模块形态通常已经把上拉电阻和滤波电容做好了,甚至有的模块还带了电源指示灯,接法非常省心:VCC接3.3V或5V,GND接地,DATA接MCU的一个GPIO。
如果你用裸片或者自己画板子,一定要记得在DATA引脚上加一个4.7kΩ左右的上拉电阻到VCC。这是因为DHT11的数据线是开漏/集电极开路结构,只能主动拉低,不能主动拉高,高电平完全靠外部上拉电阻提供。缺少上拉电阻的后果就是通信时电平浮空,读回来的数据乱七八糟。
供电范围方面,DHT11标称工作电压是3.3V到5.5V,但实际使用中3.3V系统需要特别注意上拉拉到的电压必须和MCU的IO电平兼容。温度在0到50度、湿度在20%到90%RH范围内,DHT11还能保证标称精度,超出区间误差会明显变大。
2.2 感湿与测温的原始思路
DHT11内部的感湿元件一般是一种高分子电阻式湿敏元件,感湿材料在吸收空气中的水分后电阻值会发生变化,通过内部电路转换为湿度数据。温度部分则是用一个热敏电阻或NTC,原理同样是电阻随温度变化,再经过ADC采样和内部逻辑换算成数字量。
因此DHT11输出的温湿度是经过内部校准和量化的数字信号,单片机上不需要再做公式换算,直接按协议读回数据帧,拆出5个字节就行。DHT11的精度并不高,湿度精度正负5%RH,温度精度正负2摄氏度,分辨率分别只有1%RH和1摄氏度,小数位通常读出来都是0。很多人在读到类似"25.0"的结果后会怀疑是不是数据错误,其实这就是DHT11的固有分辨率,它做不到更高的精度。
2.3 为什么时序参数这么关键
DHT11通信协议里最关键的就是一系列时间参数。主机起始信号要求低电平至少持续18ms,然后释放总线20到40us;传感器应答信号是低电平80us,再拉高80us;每位数据以低电平50us开始,之后的高电平持续时间决定这位的值是0还是1,0的高电平大约持续26到28us,1的高电平大约70us。
这些数值看起来只是几个延时参数,但决定了整个通信的成败。MCU的时钟频率不同、编译器优化等级不同、延时函数写法不同,都会导致实际延时和预期偏差。很多人的代码在开发板上能跑,换一块板子就不行,多半就是延时精度被"玄学"了。后面代码部分我会专门用一节来讲微秒延时怎么做得靠谱。
3. 总线上的一次完整温湿度读取
3.1 第一步:主机发起起始信号
单片机作为主机,想从DHT11读取数据时,必须先主动发起一个起始信号。
过程是这样的:主机先把数据线拉低,并且保持至少18ms,这段时间可以看作是"唤醒"DHT11的信号,DHT11检测到这个足够长的低电平后,就会进入准备发送数据的状态。之后主机释放总线(也就是把数据线拉高),让电平通过上拉电阻回到高电平,然后等待20到40us。
为什么起始信号要拉低18ms这么久?因为DHT11内部逻辑需要时间完成一次采样,18ms足够它稳定下来并准备好应答。如果你拉低时间太短,传感器可能根本没反应过来;太长一般问题不大,但会影响读取频率,因为DHT11本身采样周期就是1s左右,读得再快数据也不会更新。
起始信号结束后,总线处于高电平,主机需要立刻切换为输入模式(或者将引脚配置成开漏输出并置高,这样可以直接读取引脚电平,这在STM32上非常实用),等待DHT11的应答。
3.2 第二步:传感器应答与总线控制权交接
DHT11接收到起始信号后,会把总线拉低大约80us,表示"我收到你的请求了",然后再释放总线,让上拉电阻把电平拉高,大约维持80us。主机在这段时间里读到的是一个"低80us、高80us"的波形,这就相当于传感器对主机说:准备好,我开始发数据了。
在这里有一个容易忽略的细节:主机释放总线后,总线其实处于高电平空闲状态。但DHT11的应答和后续数据都表现为"主动拉低一段时间,然后释放让上拉电阻拉高",所以整个通信过程就是一个不断交替的低电平、高电平序列,主机只需要捕捉每次电平转变的持续时间,就能解析出数据。
如果在起始信号等待应答时,总线上一直没有出现低电平脉冲,那大概率是DHT11没有正常工作,可能是没上电、没加上拉、或者起始信号时序不达标。
3.3 第三步:40位数据怎么一个个传出来
应答信号之后,DHT11会连续发送40位数据。每个数据位的格式都一样:先拉低总线约50us,然后释放总线,之后高电平的持续时间决定这一位是0还是1。
- 如果高电平持续26到28us,表示这一位是0;
- 如果高电平持续大约70us,表示这一位是1。
主机怎么区分呢?常见做法是:检测到低电平结束后,启动一个计时器或循环计数,统计总线保持高电平的时间,超过某个阈值就判定为1,否则判定为0。这个阈值一般取40us到45us之间,因为0的高电平不到30us,1的高电平超过70us,中间留了足够大的余量。
40位数据的排列顺序是固定的:前16位是湿度数据,其中高8位是湿度整数部分,低8位是湿度小数部分;中间16位是温度数据,高8位是温度整数部分,低8位是温度小数部分;最后8位是校验和。
这里要特别指出,DHT11的小数位在大多数情况下是0,只有DHT22/AM2302这类高分辨率型号才会用到小数位。如果你强行把DHT11的小数位当作有效数字去精确显示,反而会引入无意义的波动。
3.4 校验和与数据还原
接收完40位数据后,主机还需要做一次校验,确保传输过程中没有误码。校验规则非常简单:把前四个字节加起来,取结果的低8位,如果等于第五个字节,就认为这次通信有效;否则丢弃本次数据,等待下一次读取。
举个例子,如果读到的原始数据是:
| 字节序号 | 值 |
|---|---|
| 湿度整数 | 0x1E |
| 湿度小数 | 0x00 |
| 温度整数 | 0x1A |
| 温度小数 | 0x00 |
| 校验和 | 0x38 |
那么前四个字节之和是0x1E + 0x00 + 0x1A + 0x00 = 0x38,低8位正好等于0x38,校验通过,温湿度分别为30%RH和26摄氏度。
实际数据还原公式是:
湿度 = 湿度整数 + 湿度小数 / 10
温度 = 温度整数 + 温度小数 / 10
对于DHT11,小数位通常为0,所以直接读整数值就可以用。
4. 手写驱动代码:每个函数为什么这样写
4.1 微秒延时:最容易翻车的环节
DHT11的时序对微秒级延时要求很高,但MCU上的HAL_Delay、delay_ms这类函数一般只能精确到毫秒级,没法处理20us、50us这种短延时。很多人直接把毫秒延时拿来用,结果读到的数据全是乱码。
最可靠的微秒延时方案是使用DWT(Data Watchpoint and Trace)模块的CYCCNT计数器。它是Cortex-M内核里的一个32位周期计数器,每个CPU时钟周期自增一次,精度高、不占用定时器资源。配合SystemCoreClock可以非常精确地实现微秒延时。
如果你的平台是51单片机,那延时方案会更朴素一些。12MHz晶振下,一个机器周期是1us(实际上8051一个机器周期包含12个时钟周期,12MHz时机器周期正好1us),所以延时1us约等于执行一条普通指令的时间,可以用_nop_函数或几个空循环拼凑。但空循环的实际时长受编译器优化影响很大,最好关掉优化或者用示波器实测校准。
4.2 GPIO模拟单总线的三种状态切换
单总线通信中,同一个GPIO既要输出又要输入。常见的处理方式有两种。
第一种是模式切换法:在主机发送起始信号时,把GPIO配置为推挽输出;发送完起始信号后,切换为浮空输入(因为外部有上拉电阻);读取数据时保持输入模式。这种方式逻辑清晰,但GPIO模式的切换需要时间,快速切换可能导致时序抖动。
第二种是开漏输出法:直接把GPIO配置为开漏输出,并开启内部上拉(或者依赖外部上拉电阻),输出0时引脚拉低,输出1时引脚处于高阻状态,由外部上拉电阻把电平拉高。这样在开漏模式下,GPIO同时也具备输入读取能力,HAL库的GPIO_ReadPin可以直接读到当前引脚电平。整个过程不需要切换模式,时序一致性更好。
我实际用下来,STM32上推荐第二种方式。开漏输出置1就相当于释放总线,置0就是主动拉低,读取数据时直接调ReadPin就能读到外部电路产生的电平,非常符合单总线的电气模型。
4.3 完整驱动代码与逐段讲解
下面以STM32F103 + HAL库为例,给出一个完整可用的DHT11驱动。GPIO选用PB12,采用开漏输出模式。
引脚初始化代码:
void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_12; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); }微秒延时函数,使用DWT周期计数器:
static void DWT_Delay_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000U); while ((DWT->CYCCNT - start) < ticks); }毫秒延时可以复用HAL_Delay,或者也用DWT实现。起始信号以及读取间隔需要毫秒级延时,直接用HAL_Delay(20)就行。
起始信号和应答检测:
uint8_t DHT11_Start(void) { // 拉低总线至少18ms HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); HAL_Delay(20); // 释放总线,等待20~40us HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); delay_us(30); // 此时DHT11应拉低总线作为应答 if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_SET) { return 1; // 总线仍为高,说明无应答 } // 等待应答低电平结束(约80us),带超时保护 uint32_t timeout = 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_RESET) { if (--timeout == 0) return 2; } // 等待应答高电平结束(约80us),带超时保护 timeout = 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_SET) { if (--timeout == 0) return 3; } return 0; }超时保护很有必要。如果DHT11未连接或者损坏,主机会一直卡在while循环里,整个程序就假死了。所以等待电平变化的循环一定要加超时退出机制,否则在调试时非常痛苦。
读取一个数据位:
uint8_t DHT11_ReadBit(void) { uint32_t timeout; uint32_t high_duration = 0; // 每个数据位都以50us低电平开始,先等低电平结束 timeout = 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_RESET) { if (--timeout == 0) return 0; } // 测量高电平持续时间,用循环计数近似时间 timeout = 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) == GPIO_PIN_SET) { if (--timeout == 0) break; high_duration++; } // 0的高电平约26~28us,1的高电平约70us,阈值取40次循环计数 return (high_duration > 40) ? 1 : 0; }这种读取方式是用空循环的次数来衡量高电平时间,不是严格的微秒计时间。它的优点是代码简单,配合DWT延时初始化后,循环计数的次数和实际时间有一个相对稳定的对应关系;缺点是不同优化等级下阈值可能要微调。更精确的做法是在高电平期间读取DWT->CYCCNT的差值,然后和微秒阈值比较,但这组代码用于学习和大多数工程已经足够了。
读取一个字节:
uint8_t DHT11_ReadByte(void) { uint8_t data = 0; for (int i = 0; i < 8; i++) { data <<= 1; data |= DHT11_ReadBit(); } return data; }读取完整温湿度数据并校验:
uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] = {0}; if (DHT11_Start() != 0) { return 1; // 起始失败 } for (int i = 0; i < 5; i++) { data[i] = DHT11_ReadByte(); } // 校验和 = 前四字节之和的低8位 if (data[4] != ((data[0] + data[1] + data[2] + data[3]) & 0xFF)) { return 2; // 校验失败 } *humidity = data[0]; *temperature = data[2]; return 0; }DHT11数据帧格式中,data[1]和data[3]分别是湿度和温度的小数部分。对DHT11来说它们基本都是0,所以主函数里可以直接读取整数部分。如果你想做更通用的驱动,可以把小数部分也返回:
float humidity = data[0] + data[1] * 0.1f; float temperature = data[2] + data[3] * 0.1f;4.4 在main函数里读温湿度的正确姿势
DHT11从完成一次采样到数据稳定需要时间,官方手册给出的采样周期是1s左右。这意味着你读数据的频率最好不要超过1Hz,每次读取之间至少间隔1s以上。频繁读取不仅拿不到新数据,还可能因为传感器还在忙而导致通信失败。
正确的调用方式是在主循环里加一个1s或者更长的延时,每次都重新发起起始信号并读取。很多程序跑着跑着输出就固定不动了,往往不是因为死机,而是读取频率太快,DHT11根本来不及更新数据。
另外要注意,DHT11上电后需要等待1到2秒才能稳定工作。如果你在系统上电后立刻读取,很可能失败一两次。比较稳妥的做法是在初始化完成后先延时2秒,然后再进入主循环读取。
5. 实战排错:读不到数据时的完整排查链路
5.1 现象一:读回来全是0xFF或0x00
如果读回来的字节全是0xFF,本质上是主机把所有数据位都判成了1,说明在每个数据位的后半段,总线一直是高电平,或者传感器根本没有拉低总线。常见原因有三个:
第一,上拉电阻缺失。模块一般自带,但如果你自己接裸片而漏了上拉,总线在传感器释放后会一直处于不确定状态,读回高电平的概率很高,从而判定为1。解决办法是加一个4.7kΩ上拉到VCC。
第二,GPIO没有配置成输入或开漏模式。如果你用推挽输出,发送完起始信号后仍然保持推挽输出并且输出高电平,那传感器拉低总线时就是在和MCU的推挽输出"打架",电平可能会被强行拉高,导致时序完全错乱。
第三,GPIO模式切换太慢。切换模式时如果引入了额外的us级延迟,可能会错过传感器的应答窗口,后面读到的自然全是高电平。
如果读回来全是0x00,那就是所有位都被判成0,说明主机在高电平阶段读到了低电平,通常是引脚内部配置为下拉,或者外部电路把总线强制拉低了。检查一下GPIO的上下拉配置,确保测量高电平时引脚是浮空或上拉状态。
5.2 现象二:只有第一次能读到,后续失败
这种问题多半是状态没有复位。DHT11的数据读取是一次性的,读完40位后总线恢复到空闲状态。如果第二次读取之前没有正确发送起始信号,或者起始信号之后没有等足应答时间,传感器就不会响应。
另一个常见原因是主循环里读取间隔太短。DHT11需要大约1s时间完成一次采样,你在500ms内连续读了两次,第二次去读时传感器可能还在刷新内部数据,应答波形不完整,导致校验失败。
还有一种隐蔽情况:前端读取代码在中断里执行,被其他高频中断打断。数据位的高电平窗口只有几十微秒,如果刚好在测量高电平时间时被一个优先级更高的中断抢占了几十微秒,返回的判断结果就会出错,连续读几次之后数据就对不上了。解决思路是在读取DHT11的临界区临时关中断,或者把DHT11读取任务放到不会被频繁打断的线程里。
5.3 现象三:湿度值跳动异常
湿度值虽然会随着环境变化,但如果反复读到比如20%和60%这样大幅跳动的数据,首先怀疑数据校验是否通过。没有通过校验的数据不能被使用,有些程序在校验失败后仍然用旧数组里的垃圾数据算温湿度,就会产生跳变。
排除校验因素后,考虑供电和干扰。DHT11对电源纹波比较敏感,如果传感器离电机、继电器、电源模块很近,或者连接线比较长,就容易受到干扰。可以在VCC和GND之间加一个100nF去耦电容,并尽量缩短DATA线的长度。
如果模块是插在面包板上的,检查一下面包板的接触是否可靠。氧化、松动、以及杜邦线内部的断裂,都会造成时通时断,这类硬件问题往往比软件问题更难排查。
5.4 没有示波器时怎么定位:逻辑分析仪与排除法
最好用的是逻辑分析仪。便宜的逻辑分析仪就能抓到DHT11的完整波形,把数据分析仪接到DATA引脚上,抓取一次读取过程,然后对比协议时序图,一眼就能看出是起始信号不够长、应答缺失还是数据位的0/1时长不对。
没有逻辑分析仪时,可以像剥洋葱一样用排除法:
- 先用万用表量DATA引脚在空闲时的电平,正常应该接近VCC,如果接近0V说明上拉缺失或者MCU引脚被拉低。
- 检查起始信号后能否看到传感器拉低总线的应答,可以在读应答处加一个反例判断:如果等待低电平超时,就打印一个错误码。通过错误码能快速区分是"无应答"还是"数据读出异常"。
- 用固定波特率的串口打印每次读到的原始5字节数据,把校验和一起打出来。如果数据每次都不同,说明通信不稳定;如果数据固定但校验不过,说明协议解析有误。
我调试时最喜欢打印原始数据帧,因为只看最终温湿度会被代码逻辑掩盖很多问题,原始帧能直接告诉你传感器到底发出来什么。举例来说,如果前四个字节和校验和一直是0xFF 0xFF 0xFF 0xFF 0xFF,那不用怀疑解析代码,直接去查硬件电路。
6. 从DHT11出发,单总线在传感器选型中的定位
6.1 DHT11与DHT22/AM2302:别只看价格
DHT22(也叫AM2302)同样是单总线传感器,数据格式和DHT11非常像,也是5字节,前两字节湿度、中间两字节温度、最后一字节校验和。区别在于:DHT22的湿度分辨率是0.1%RH,温度分辨率也是0.1摄氏度,测量范围更大,整体精度比DHT11高了一个量级。
时序上两者略有差异。DHT22的起始信号同样要求拉低至少1ms,一般用10ms也能触发,但要用DHT11的18ms时序去读DHT22,通常也能正常工作。不过最稳妥的做法还是分别根据手册调参数。驱动函数的改造点主要在起始信号的拉低时间、数据位判定阈值和超时时间上,整体数据结构可以复用。
价格上DHT11优势非常明显,适合对精度要求不高的项目,比如简单的室内温湿度显示、孵化箱的粗略监控、智能家居演示板。一旦你开始关心湿度的变化趋势或者需要做相对精确的露点计算,至少应该升级到DHT22,或者干脆用IIC接口的SHT系列。
6.2 单总线、IIC、SPI在传感器应用中的取舍
标题里提到了很多通信协议,包括SPI、IIC、Modbus等,这里可以放在一起做个横向对比,帮助你把DHT11和单总线放在整个通信生态里理解。
| 通信方式 | 典型传感器 | 引脚占用 | 传输速率 | 抗干扰能力 | 典型场景 |
|---|---|---|---|---|---|
| 单总线 | DHT11、DHT22、DS18B20 | 1根数据线,部分需上拉 | 低速(kbps级) | 一般 | 板内短距离温湿度采集 |
| IIC | SHT30、BMP280、MPU6050 | 2根线(SCL+SDA) | 100kbps~400kbps | 较好 | 板内多传感器连接 |
| SPI | 部分传感器、Flash、显示屏 | 3~4根线 | 数十Mbps | 较好 | 高速数据传输 |
| RS485/Modbus | 工业温湿度变送器 | 2根差分线 | 低速~数Mbps | 强 | 工业现场远距离通信 |
| CAN | 汽车/工业传感器节点 | 2根差分线 | 最高1Mbps | 强 | 车载、工控实时网络 |
单总线的优势是省IO,劣势同样明显:速率上不去、距离拉不长、无法像Modbus那样通过地址挂载多个设备。所以当你看到"单总线通信实战"的时候,一定要理解它是特定场景下的产物,不是因为所有传感器都该这么连。
6.3 我的选型建议与使用心得
从DHT11开始学单总线是很合适的路径,因为它协议简单、代码量少、出错时也好定位,相比直接上手标准1-Wire协议要友好得多。你在单片机学习阶段花的那些时间,并不会白费:单总线中"通过电平持续时间来编码信息"的思想,在后续理解红外遥控、数字温度传感器、甚至一些自定义协议时都会反复用到。
实际开发中,我的建议是:临时验证和教学项目用DHT11模块就好,便宜又皮实;产品原型阶段如果空间允许,尽量选择IIC接口的高精度传感器,因为它们不需要微秒级时序,代码更健壮,不依赖具体MCU的主频和延时实现。工业现场则直接考虑RS485和Modbus,单总线在这种场景下基本没有生存空间。
如果你确实需要在单总线上用DHT11做产品,记得在硬件设计阶段就把4.7kΩ上拉电阻、电源去耦电容、ESD保护都加上,并且不要让单片机在读取期间处理其他高优先级中断。时序敏感的器件,硬件给一点余量,软件就少一份玄学。
我自己最初调DHT11时,最大的教训就是差点被"代码不对所以改了三天"这个概念迷惑。最后用逻辑分析仪一看,波形从头到尾都是对的,问题根本出在GPIO配置成了推挽输出,导致传感器拉低总线时被单片机强行拉高。从那以后我排问题不再是盯着代码反复改,而是先抓波形,再从协议和电气上找原因。这种思维比多会几个驱动函数要值钱得多。