STM32驱动DHT11温湿度传感器:从时序到代码的完整实战指南
写 STM32 的人十有八九绕不开 DHT11 这颗温湿度传感器。它便宜、好买、时序简单学完就能摸到单总线通信的门道做小项目、课设、毕设都经常用它。DHT11 本质是一个“用一根引脚讲完所有数据”的数字传感器配合 STM32 的 GPIO 模拟时序20 行核心代码就能读回温度和湿度。这篇文章我会从硬件接线、通信时序、代码实现一直聊到常见报错比如大家搜烂了的 error: no stm32 target found!把能踩的坑提前给你排一遍。网上 DHT11 的教程很多但大部分只贴代码不讲为什么导致很多人移植到自己板子上就翻车。我写这篇的定位是“照着能跑跑了能懂”不仅给你标准库和 HAL 库两套可直接用的代码还会把时序参数背后的容错逻辑、上拉电阻的作用、以及“为什么读回来的数据永远是 0”这类问题一次性讲透。无论你是刚点亮 LED 的入门选手还是被项目周期逼着赶工的老手这篇都值得存一份。1. DHT11 到底是什么为什么它适合入门1.1 硬件长什么样引脚怎么认DHT11 常见的封装有三种四脚直插、三脚贴片、以及焊在模块上的成品。四脚直插的引脚定义最容易搞混实物上丝印如果看不清记住一条经验把有网孔的那面朝自己引脚朝下从左到右依次是 VCC、DATA、NC、GND。三脚贴片和模块则通常是 VCC、DATA、GND 三根线中间那根永远是数据线。供电范围是 3.3V 到 5V这给 STM32 用非常方便直接接 3.3V 就行数据线不用做电平转换。上电后传感器会先进入 1 秒左右的不稳定状态这个时间段不要去读它等它内部完成自检。很多人在初始化后立刻读数据读回来的都是 0其实就是没等够这 1 秒。功耗方面 DHT11 正常工作时电流只有 0.5mA 到 1mA 左右待机更是微安级别对电池供电的小项目很友好。测量范围是湿度 20% 到 90% RH、温度 0 到 50 摄氏度精度分别是正负 5% RH 和正负 2 摄氏度。这个精度做环境监测够了但别拿它当精密仪器用我见过有人拿 DHT11 测体温然后质疑读数不准那是选型问题不是传感器问题。1.2 数据格式和校验原理DHT11 一次完整传输会返回 40 位数据分成 5 个字节湿度整数、湿度小数、温度整数、温度小数、校验和。跟你想的可能不一样DHT11 的小数位在实际数据里通常全是 0因为它的分辨率本身就只有 1% 和 1 摄氏度小数位是为了协议兼容才保留的。校验规则很简单前四个字节相加取低 8 位如果等于第五个字节说明这次传输有效。比如读回的数据是 00101100 00000000 00011010 00000000 01000110湿度就是 44%温度是 26 度校验和 0x46 正好等于 0x2C 0x00 0x1A 0x00。这个校验小时候大家可能觉得多余但实际用起来非常重要。单总线协议没有 CRC 之外的纠错机制而 GPIO 模拟时序在受到中断干扰时很容易读错位没有校验的话你会拿到一个看起来合理但其实是错的数据。所以代码里校验那一步必须写并且校验失败要主动丢弃这次数据而不是硬着头皮用。2. 硬件接线别小看这根数据线的上拉电阻2.1 三根线怎么接接线图非常简单VCC 接 3.3VGND 接 GNDDATA 接 STM32 的任意一个 GPIO。这里要注意代码里你把引脚配置成开漏输出还是推挽输出直接决定你需不需要外加上拉电阻。如果用推挽输出MCU 能主动驱动高电平和低电平但 DHT11 拉低总线的时候要和 MCU 的高电平输出打架必须让 MCU 抢不过传感器否则时序全乱。更稳妥的做法是把 GPIO 配置成开漏输出模式同时在数据线上加一个 4.7kΩ 到 10kΩ 的上拉电阻到 3.3V。开漏输出下MCU 想拉低就拉低想释放总线就靠上拉电阻把电平拉高这和 DHT11 内部的开漏驱动结构完全一致双方不会冲突。这是很多新手翻车的第一大坑推挽模式配 DHT11能通但偶发错乱开漏模式配 4.7k 上拉才是标准答案。模块版的 DHT11 大多数已经帮你把上拉电阻焊好了你直接接杜邦线就能用。散件版 DHT11 就必须要自己加电阻不加的话数据线浮空读回来的数据十有八九是乱的。判断模块有没有上拉电阻的方法很简单看原理图或者拿万用表量 DATA 引脚对 3.3V 是否有几十千欧的阻值。2.2 电源滤波和长线传输的讲究DHT11 对电源的纹波不算敏感但如果你把传感器放在电机、继电器这种大电流负载旁边建议在 VCC 和 GND 之间加一个 100nF 的陶瓷电容位置尽量靠近传感器引脚。这个电容能滤掉电机启停瞬间的电压跌落避免传感器内部逻辑判断错乱。数据线长度控制在 20 厘米以内基本不用考虑信号完整性问题。超过这个长度尤其是走杜邦线飞线寄生电容会改变信号的上升沿和下降沿导致 STM32 采样到的脉宽失真。我试过用 50 厘米的杜邦线接 DHT11读 10 次大概会错 2 到 3 次校验和都拦不住。如果项目非要长线传输建议换 DHT22 或者直接走 I2C 接口的 SHT30别在 DHT11 上死磕。电源方面还有个小细节不要用 STM32 的 3.3V LDO 输出同时带一堆传感器。DHT11 虽然电流小但上电瞬间的浪涌可能会让 3.3V 出现短暂跌落如果你的 MCU 也在同一路电源上可能导致复位。我习惯在传感器电源入口串一个 10Ω 的电阻配合 100nF 电容组成一个简易 RC 滤波实测很稳。3. 通信时序拆解单总线协议的精髓3.1 主机起始信号DHT11 的通信由主机主动发起。主机先把数据线拉低保持至少 18ms然后再拉高 20 到 40 微秒这就是起始信号。18ms 这个数字很多人不理解为什么这么长原因是 DHT11 内部用的是低频晶振或者 RC 振荡器需要足够长的低电平时间才能唤醒内部逻辑。如果你拉低时间不够传感器可能根本不会响应。拉高 20 到 40 微秒这个窗口比较讲究。太短了DHT11 可能还没准备好开始应答太长了传感器会认为这是一次误触发。实际操作中我用一个 30us 左右的延时稳定性和兼容性都很好。注意这里说的延时必须相对准确SysTick 或者其他定时器都可以但别用空循环加 volatile 变量被编译器优化掉。起始信号发完后主机要把数据线释放开漏模式下就是直接不驱动靠上拉电阻自动拉高然后立刻把 GPIO 切换成输入模式准备读取 DHT11 的响应信号。这一步很多人忘导致总线一直被主机占着传感器根本没法应答读回来的数据自然全是 0xFF 或者卡死在等待状态。3.2 响应信号与 40 位数据DHT11 收到起始信号后会先拉低数据线 80us再拉高 80us表示“我准备好了开始发数据”。主机判断响应的方式就是读取引脚的低电平时间看是否在合理范围内。不用精确到微秒级只要检测到低电平时间在 40us 到 120us 之间基本可以认为是有效响应。响应之后紧接着就是 40 位数据。每一位的传输格式都是先拉低 50us然后拉高。拉高的时间长短决定这一位是 0 还是 1高电平持续 26 到 28us 表示 0高电平持续 70us 表示 1。所以读位的核心操作就是等待引脚变低再等待引脚变高然后测量高电平的持续时间。这里有个非常关键的容错细节DHT11 的时序参数在数据手册上标注的是一个范围实际芯片之间可能有比较大的差异。比如有的芯片每位之间的低电平只有 40us有的能到 60us。所以判断 0 和 1 的阈值不要卡死在某个点上我建议用 50us 作为分界线高电平持续小于 50us 记为 0大于 50us 记为 1。这样即使芯片个体有偏差也能准确识别。3.3 时序参数汇总与容错策略给出一张我实测过多个批次 DHT11 后整理的典型时序参数表新手对照这个写代码基本不会出问题信号阶段最小时间典型时间最大时间主机拉低起始18ms20ms30ms主机拉高等待20us30us40us传感器响应低电平40us80us120us传感器响应高电平40us80us120us数据位低电平40us50us60us数据位 0 高电平22us26us30us数据位 1 高电平60us70us90us传输结束释放40us50us60us容错策略上我写代码时会加“超时退出”机制。比如等待引脚变低如果超过 200us 还没等到就直接判定通信失败返回错误码而不是无限循环等下去。这个策略在工程上非常重要因为一旦 DHT11 没接好或者损坏你的程序会卡死在读取函数里整个系统都跟着瘫痪。4. 代码实现标准库和 HAL 库完整方案4.1 引脚配置与延迟函数先讲 GPIO 配置。标准库和 HAL 库的写法不同但核心思想一致先把引脚配置为开漏输出方便主机拉低起始信号然后发送起始信号时手动控制高低电平等待响应和数据时切换为输入模式。为了省事我通常直接把引脚配置为开漏输出因为开漏输出模式下把输出寄存器写 1 就等效于释放总线引脚会被上拉电阻拉高再用 GPIO 读取函数检测电平不需要真的在输入和输出模式之间来回切换。微秒级延时是 DHT11 驱动的基础。HAL 库的 HAL_Delay 只支持毫秒级所以必须自己写微秒延时。最靠谱的方式是用 DWTData Watchpoint and Trace单元它有一个 32 位的 CYCCNT 计数器每个时钟周期加一精度非常高。// 使用 DWT 实现微秒延时兼容 Cortex-M3/M4/M7 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 / 1000000); while ((DWT-CYCCNT - start) ticks); }这个写法在 STM32F1、F4、F7 上都能用前提是系统时钟频率配置正确。如果你用的是 SysTick 做系统心跳也可以在中断里维护一个微秒级的 tick 变量但 DWT 更省心不占用中断资源。4.2 标准库版本完整驱动代码标准库版本目前依然有大量存量用户尤其是 STM32F103 系列的初学者。下面是一套我整理过的、可以直接跑的标准库 DHT11 驱动核心逻辑全部用宏定义和静态函数封装方便移植到不同引脚。// dht11.h #ifndef __DHT11_H #define __DHT11_H #include stm32f10x.h #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_0 #define DHT11_GPIO_CLK RCC_APB2Periph_GPIOB #define DHT11_HIGH() GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN) #define DHT11_LOW() GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN) #define DHT11_READ() GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) #define DHT11_OUTPUT() { GPIO_InitTypeDef gpio; \ gpio.GPIO_Pin DHT11_GPIO_PIN; \ gpio.GPIO_Mode GPIO_Mode_Out_PP; \ gpio.GPIO_Speed GPIO_Speed_50MHz; \ GPIO_Init(DHT11_GPIO_PORT, gpio); } #define DHT11_INPUT() { GPIO_InitTypeDef gpio; \ gpio.GPIO_Pin DHT11_GPIO_PIN; \ gpio.GPIO_Mode GPIO_Mode_IPU; \ GPIO_Init(DHT11_GPIO_PORT, gpio); } uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature); #endif// dht11.c #include dht11.h #include delay.h static uint8_t DHT11_WaitPinLevel(uint8_t level, uint32_t timeout_us) { while (DHT11_READ() ! level) { if (timeout_us-- 0) return 0; delay_us(1); } return 1; } uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] {0, 0, 0, 0, 0}; uint8_t i, j; // 引脚配置为输出拉低起始信号至少 18ms DHT11_OUTPUT(); DHT11_LOW(); delay_ms(20); DHT11_HIGH(); delay_us(30); DHT11_INPUT(); // 检查响应信号先低后高 if (!DHT11_WaitPinLevel(0, 200)) return 1; if (!DHT11_WaitPinLevel(1, 200)) return 1; // 读取 40 位数据 for (i 0; i 5; i) { for (j 0; j 8; j) { if (!DHT11_WaitPinLevel(0, 100)) return 2; delay_us(40); // 跳过 50us 低电平 buf[i] 1; if (DHT11_READ() 1) { buf[i] | 1; if (!DHT11_WaitPinLevel(0, 100)) return 3; } } } // 校验和判断 if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) return 4; *humidity buf[0]; *temperature buf[2]; return 0; }这段代码有两个设计细节值得说明。第一读取每一位数据时我用的是“等待低电平结束后延时 40us 再读高电平”的策略而不是测量高电平的宽度。原因是在 GPIO 模拟时序下精确测量一个 26us 到 70us 的高电平宽度需要频繁轮询容易受中断影响而延时 40us 后如果引脚还是高电平说明这是一位 1否则是 0。这个做法牺牲了一点精确度换来了极强的抗干扰性。第二每个等待环节都加了超时退出返回值用不同的数字区分错误类型方便调试。4.3 HAL 库版本完整驱动代码如果你用的是 CubeMX 生成的工程HAL 库版本的核心逻辑和标准库完全一样只有 GPIO 读写函数的封装不同。CubeMX 里把 DHT11 数据引脚配置为 GPIO_Output 开漏模式用户标签可以命名为 DHT11_DATA然后在代码里手动切换时钟源即可。// dht11_hal.c 关键函数 #define DHT11_DATA_PORT GPIOB #define DHT11_DATA_PIN GPIO_PIN_0 void DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] {0, 0, 0, 0, 0}; uint8_t i, j; uint32_t timeout; // 开漏输出模式此时写 1 等效于释放总线 GPIO_InitTypeDef gpio {0}; gpio.Pin DHT11_DATA_PIN; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_DATA_PORT, gpio); // 发送起始信号 HAL_GPIO_WritePin(DHT11_DATA_PORT, DHT11_DATA_PIN, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_DATA_PORT, DHT11_DATA_PIN, GPIO_PIN_SET); delay_us(30); // 切换到输入模式等待响应 gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_DATA_PORT, gpio); timeout 200; while (HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) GPIO_PIN_SET) if (--timeout 0) return; timeout 200; while (HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) GPIO_PIN_RESET) if (--timeout 0) return; // 数据位读取逻辑与标准库一致 for (i 0; i 5; i) { for (j 0; j 8; j) { timeout 100; while (HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) GPIO_PIN_SET) if (--timeout 0) return; delay_us(40); buf[i] 1; if (HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) GPIO_PIN_SET) { buf[i] | 1; timeout 100; while (HAL_GPIO_ReadPin(DHT11_DATA_PORT, DHT11_DATA_PIN) GPIO_PIN_SET) if (--timeout 0) return; } } } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) buf[4]) { *humidity buf[0]; *temperature buf[2]; } }HAL 库版本有一个和标准库不同的地方标准库里我通过切换 GPIO_Mode_Out_PP 和 GPIO_Mode_IPU 来切换输入输出HAL 库里直接复用同一个 GPIO_InitTypeDef 结构体改模式重新 Init 即可。另外 HAL 库的开漏输出模式下没有额外配置内部上拉因为开漏输出时内部上拉根本不起作用必须靠外部 4.7k 电阻。4.4 主函数调用与防误判处理主函数里的调用逻辑很简单但有几个细节要注意。DHT11 的采样周期是 1 秒也就是说你两次读取之间的间隔不要小于 1 秒否则传感器还在内部刷新读回来的数据可能是上一次的缓存。这个特性和很多传感器不同新手容易忽略。我习惯用定时器做 2 秒一次的软件轮询既能保证数据及时刷新又能避开传感器忙状态。// main.c 示例 int main(void) { uint8_t humidity 0; uint8_t temperature 0; uint8_t status 0; uint32_t last_read 0; SystermClock_Init(); DWT_Delay_Init(); UART_Init(115200); while (1) { if (HAL_GetTick() - last_read 2000) { last_read HAL_GetTick(); status DHT11_Read_Data(humidity, temperature); if (status 0) { printf(湿度: %d.%d%%, 温度: %d.%dC\r\n, humidity, 0, temperature, 0); } else { printf(DHT11 读取失败, 错误码: %d\r\n, status); } } } }我见过不少人把读取失败的错误码直接忽略只在 printf 里打串口日志。这种做法在调试时问题不大但用在正式项目里就非常危险。我的习惯是连续读取失败超过 3 次就置一个传感器故障标志让系统走降级逻辑连续失败超过 10 次直接判定传感器离线触发报警。毕竟温湿度数据在很多项目里是控制逻辑的输入你不能拿一个错的数据去控制继电器或者风扇。5. 常见问题与排查技巧实录5.1 error: no stm32 target found! 到底是什么问题这个报错几乎每个用 ST-Link 的人都会遇到它和 DHT11 本身没关系但很多人在调试 DHT11 程序时第一次碰到它以为是自己代码把芯片弄坏了。实际上这个报错是调试器连接不上 STM32 芯片的 SWD 接口原因五花八门。最常见的原因是 ST-Link 的 SWDIO、SWCLK、GND 三根线没接对或者接触不良。SWDIO 是数据线SWCLK 是时钟线不能接反。其次是芯片供电问题目标板必须自己供电ST-Link 自带的 3.3V 输出只能给极低功耗的电路供电带不动 STM32 和 DHT11。还有一个容易被忽略的坑如果 STM32F4 系列芯片的 VCAP 引脚没有接 2.2uF 电容芯片根本不会正常运行调试器自然也连不上。另外一个高频原因是在调试器设置里选了错误的连接模式。Keil 的 Debug 设置里ST-Link 的 Port 要选 SW不是 JTAGMax Clock 不要设太高建议 4MHz 以下。如果你用的是高云、兆易创新这些国产替代芯片SWD 时序要求可能更苛刻把时钟降到 1MHz 甚至 500kHz很多“连不上”的问题就消失了。还有种情况是芯片被禁用了调试接口比如程序里把 SWDIO 和 SWCLK 引脚复用成普通 GPIO 了。解决办法是用 ST-Link Utility 或者 STM32CubeProgrammer 的 Connect Under Reset 模式在复位期间抢先把连接建立起来然后擦除 Flash。具体操作是按住目标板复位键不放点击连接连接成功后松开复位。这个方法在 90% 的“芯片锁死”场景下都能救回来。5.2 数据全是 0 或 255大概率是时序问题DHT11 读回来全是 0最常见的原因是起始信号没发出去。检查一下你的 GPIO 配置如果引脚被配置成输入模式就发不出低电平自然唤醒不了传感器。另一个可能是你的延时函数有问题比如 delay_us 被编译器优化成空操作或者系统时钟配置错误导致延时时间完全不对。读回来全是 0xFF 或者 0xFF 和随机数据交替出现通常是数据线上的上拉电阻缺失或者引脚在读取阶段被配置成了推挽输出导致信号被强制拉高。还有一种可能是传感器供电不足VCC 电压低于 3V 时DHT11 的高电平输出可能达不到 STM32 的高电平阈值读到的就全是 0 或者乱跳。如果数据偶尔对偶尔错重点排查中断干扰。你如果在用定时器中断、串口中断而且中断处理函数里执行时间过长就会打断 DHT11 的位读取过程。DHT11 的一位高电平只有 26 到 70us这个窗口非常窄一个稍微长一点的中断响应就能让它被误判。解决办法是在 DHT11 读取过程中关闭可屏蔽中断或者把 DHT11 读取放到最高优先级的中断里执行。5.3 温湿度数值不变是不是传感器坏了传感器数值长时间不变先别急着换硬件。DHT11 在 1 秒采样周期内的输出本来就是不变的如果你读取间隔小于 1 秒读到的就是内部缓存的旧数据。我见过有人在 while 循环里不加延时拼命读 DHT11结果读出来的温度和湿度永远是一个值还以为是代码写错了。排除采样周期后检查你是否一直在读同一个变量地址。有的同学把 DHT11 读取函数写在中断里主循环里显示但两个地方操作的不是同一个全局变量数据自然不会更新。更隐蔽的情况是编译器做了优化把读取结果缓存到了寄存器里导致变量永远不刷新这种问题需要给共享变量加上 volatile 修饰。还有一个容易误导人的情况DHT11 正常工作但数据全部是 0x00。这种时候大概率是传感器已经损坏或者内部晶振停振了。用示波器看数据线如果起始信号之后 DHT11 完全没有响应信号基本可以判定传感器本体坏了。DHT11 价格便宜手里的库存可能放了很久建议换一颗新的对比测试。5.4 间接排查用示波器和逻辑分析仪定位问题排查 DHT11 问题效率最高的方法不是反复改代码而是直接把数据线接到示波器或者逻辑分析仪上看实际的波形。逻辑分析仪二三十块钱就能买到采样率 24MHz 以上就够用是排查单总线时序的最佳工具。抓波形时主要看三个地方起始信号的低电平宽度是否达到 18msDHT11 的响应信号是否出现数据位的 0 和 1 高电平宽度是否符合 26us/70us 的预期。如果起始信号正常但没有响应信号问题在传感器一侧如果响应正常但数据位宽度乱跳问题在代码的延时和采样逻辑上。这里分享一个我用逻辑分析仪总结出的经验很多 DHT11 驱动代码在读取数据位时用的是“先等低电平再延时 40us 读高电平”的策略这个策略在时序正常时几乎没有误差。但如果你的系统主频比较高比如 168MHz 的 F4GPIO 读取本身的耗时加上循环判断的开销可能已经超过好几个微秒导致实际延时不等于预期延时。解决方法是实测一下你的 delay_us(40) 到底实际延时了多少用示波器量一遍比瞎猜效率高太多。6. 工程化建议与后续扩展6.1 多次采样取平均数据稳定性翻倍DHT11 本身精度有限但在实际项目中你完全可以通过软件方法让输出数据更稳定。最简单的做法是连续读取 3 到 5 次去掉最大值和最小值然后取平均。虽然这样会增加总耗时但换来的是更平滑的数据曲线尤其在湿度变化比较剧烈的环境里效果明显。如果项目对响应速度有要求可以采用滑动平均的方式维护一个长度为 5 的环形缓冲区每次读取新数据就替换最旧的数据然后输出缓冲区平均值。这样不需要每次读多遍传感器也能达到平滑效果内存开销只有 5 个字节对 STM32 来说微不足道。取平均的时候要特别注意一点只对通过校验的数据取平均。如果某次读取校验失败直接丢弃不要用错误数据参与计算。另外不要把平均值回写覆盖原始变量而不保留原始值因为有些控制逻辑可能需要对突变敏感平滑过度反而会掩盖真实的环境变化。6.2 多传感器扩展与总线冲突避免一个 STM32 可以同时接多个 DHT11每个 DHT11 占用一个独立的 GPIO代码上复制一份驱动、改一下引脚宏定义即可。但要注意多个 DHT11 不能像 DS18B20 那样挂载在同一条总线上用 ROM 寻址区分DHT11 的协议里没有地址识别机制所以必须一芯一线。多传感器采样时建议错开读取时间避免多个传感器同时发起起始信号导致电源瞬间电流过大。虽然每个传感器的电流不大但如果你接了四五个同时读取时的浪涌不可忽视尤其是用电池供电的小系统。我的做法是两两之间间隔 100ms 以上优先保证系统稳定。如果你需要采集多个点位的温湿度我更推荐用 I2C 接口的 SHT30 或者 AHT20它们的地址可以通过引脚配置切换一根总线上可以挂多个传感器而且精度比 DHT11 高一个量级。DHT11 更适合单点、低功耗、低成本的应用场景。6.3 从 DHT11 到 DHT22 的平滑升级当你发现 DHT11 的精度满足不了项目需求时可以考虑升级到 DHT22也叫 AM2302。DHT22 在引脚定义和通信协议上和 DHT11 高度相似只是数据格式从 8 位整数变成了 16 位数据精度也提升到了正负 2% RH 和正负 0.5 摄氏度。从代码角度看升级 DHT22 只需要改两部分数据解析从每个字节扩展为每个参数占两个字节起始信号的拉低时间从 18ms 缩短到 1ms 左右。其余时序结构完全兼容这也就是为什么很多人说“学会 DHT11DHT22 就是白送”。如果你的项目后续有升级计划写代码时把数据解析函数单独封装升级时只改这一个函数就够了。升级后别忘了调整读取频率。DHT11 是 1 秒采样周期DHT22 是 2 秒如果沿用原来的 2 秒轮询读到的可能一直是旧数据。我习惯把读取间隔设置成 2.5 秒给传感器留出足够的刷新余量。6.4 用 vscode CMake 管理工程告别 Keil 依赖最后聊一个可能引发争议但确实提高效率的话题开发环境。DHT11 驱动本身不复杂但如果你被 Keil 的各种编译错误、下载器配置问题反复折腾真心建议试试 vscode 加 CMake 加 ARM GCC 的组合。配置好之后写代码、编译、烧录、调试的体验比 Keil 舒服太多。vscode 下开发 STM32 的核心是用 EIDE 或 CMake 插件来管理工程编译器用 arm-none-eabi-gcc烧录用 pyOCD 或 ST-Link 的命令行工具。DHT11 这种纯 GPIO 驱动的代码迁移到 vscode 工程几乎零成本只需要把 stm32f1xx.h 之类的头文件路径配置正确。调试可以用 cortex-debug 插件加 ST-Link 的 OpenOCD 或 pyOCD 后端断点、变量查看、外设寄存器查看都支持。当然如果你是跟着网课初学先别急着折腾环境切换。Keil 虽然有各种毛病但资料多、问人方便适合入门阶段把精力集中在代码本身。等你能独立写完一个完整的 DHT11 温湿度采集程序再考虑迁移环境顺便把 Makefile 或者 CMakeLists 搞明白那是进阶的必修课。DHT11 这颗传感器带给我最大的收获不是“读温湿度”这个功能本身而是让我彻底理解了单总线通信的时序敏感性和容错设计的重要性。写驱动的时候多想想“为什么这里要延时 40us”“为什么读不到数据要加超时退出”比单纯复制粘帖代码有用得多。等你把 DHT11 吃透了再去碰 I2C 的 SHT30、SPI 的 MAX6675 或者 OneWire 的 DS18B20会发现底层逻辑都是相通的无非就是“拉低、释放、测电平、解析数据”这几板斧。换个传感器换个心情祝你调通顺利。