STM32F103标准库驱动DHT11:单总线时序与稳定读取 简介面向嵌入式初学者与STM32开发者的DHT11驱动工程基于STM32F103最小系统板和标准库编写重点解决单总线温湿度传感器的GPIO方向切换、微秒级延时、40位数据解析与校验等实操问题。压缩包共211个文件、约5.75MB以50个.h头文件和45个.c源文件为核心另含.o、.crf等编译中间文件及hex、axf、map、uvprojx等工程与烧录产物驱动层与硬件配置相对分离修改数据引脚和延时接口即可迁移到同类F103板卡。工程内还带有串口、OLED等常用外设代码便于在最小系统板上直接联调。已有955人学习下载读者可据此对照标准库GPIO配置、启动信号与位判别时序快速搭建温湿度采集并排查通信失败问题。1. 从最小系统板跑通 DHT11标准库驱动先解决“能读”而不是“好看”手里只有一块 STM32F103 最小系统板和一只四脚 DHT11 时最怕的不是代码多而是时序差几微秒就读回 0 或 255。这份 DHT11 驱动走标准库路线把 GPIO 方向切换、SysTick 微秒延时、40 位数据校验压在一个很小的模块里接线只要 VCC、GND、DATA 三根线换引脚时改几个宏就能移植。它适合刚把最小系统板跑起来、想先把温湿度读稳的嵌入式新手也适合需要把 DHT11 塞进旧标准库工程的单片机从业者。先跑通“能读”再谈滤波和低功耗顺序反了容易在时序玄学里打转。2. 单总线时序拆到微秒DHT11 的 40 位数据怎么从 GPIO 里抠出来DHT11 不是 I2C也不是 SPI它只有一根 DATA 线主机和从机轮流拉低拉高。标准库工程里最容易翻车的地方是把“读数据”写成“读电平”DHT11 的 0 和 1 不是静态电平而是高电平持续时间不同。先把时序拆开再落到 GPIO 读位上代码才不会变成黑匣子。2.1 起始信号与响应窗口18ms、20-40us、80us 分别卡在哪主机先拉低 DATA 线至少 18ms常见做法是拉低 20ms给足余量。然后释放总线释放后 2040us 内 DHT11 会拉低 80us 作为响应再拉高 80us接着进入第一位数据的 50us 低电平。这里的关键是“释放”不是“输出高电平一直顶着”而是把 GPIO 切成输入模式让外部上拉电阻把线拉高。很多读不到响应的现场问题就出在释放后没有切输入或者上拉电阻根本没焊。阶段典型时间说明主机起始低电平≥18ms常用20ms主机主动拉低主机释放高电平2040us常用30us切输入靠上拉拉高DHT11响应低电平80us从机拉低DHT11响应高电平80us从机拉高每位起始低电平50us每位都以低电平开头位0高电平2628us短高电平位1高电平70us长高电平提示DHT11 的 DATA 线必须外接 4.7k10k 上拉电阻MCU 内部上拉只能兜底长线或 3.3V 供电时更容易失败。起始信号可以单独封装成一个函数返回值只表达“有没有等到响应”不急着读数据。这样后面排查时能区分“没响应”和“响应了但校验错”现场定位会快很多。下面的代码用标准库的 GPIO_Init 切换方向输出用推挽输入用上拉释放总线后等三次边沿等低、等高、等低。#define DHT11_PORT GPIOA #define DHT11_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOA static void DHT11_SetOutput(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(DHT11_RCC, ENABLE); gpio.GPIO_Pin DHT11_PIN; gpio.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出主机主动拉低 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT, gpio); } static void DHT11_SetInput(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin DHT11_PIN; gpio.GPIO_Mode GPIO_Mode_IPU; // 上拉输入释放总线 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT, gpio); } static uint8_t DHT11_WaitLevel(uint8_t level, uint16_t timeout_us) { while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) ! level) { if (--timeout_us 0) { return 1; // 超时 } delay_us(1); } return 0; }这段代码的逻辑是先把 DATA 线切成输出主机才有能力拉低释放时切成上拉输入外部上拉和内部上拉一起把线拉高。DHT11_WaitLevel 的 level 参数表示要等待的电平timeout_us 是超时上限单位微秒。超时值不要设得太小80us 响应窗口加上中断抖动给 100200us 比较稳。delay_us 必须是真的微秒延时不能拿空循环凑后面第 3 章会把这个延时落实。起始信号本身不读 40 位数据只负责把总线从空闲状态带进响应状态。代码里先拉低 20ms再拉高 30us然后切输入接着等低、等高、等低。第三次等低是在等 80us 响应高电平结束也就是第一位数据的 50us 低电平开始。这个顺序写错后面读出来的字节会整体错位。static uint8_t DHT11_Start(void) { DHT11_SetOutput(); GPIO_ResetBits(DHT11_PORT, DHT11_PIN); delay_ms(20); // 至少18ms留2ms余量 GPIO_SetBits(DHT11_PORT, DHT11_PIN); delay_us(30); // 20~40us常用30us DHT11_SetInput(); if (DHT11_WaitLevel(0, 200) ! 0) return 1; // 等DHT11拉低 if (DHT11_WaitLevel(1, 200) ! 0) return 1; // 等80us低结束 if (DHT11_WaitLevel(0, 200) ! 0) return 1; // 等80us高结束 return 0; }DHT11_Start 返回 0 表示已经看到响应返回 1 表示超时。这里没有判断“响应低电平持续多久”因为标准库轮询很难精确测宽先把边沿顺序等对读位阶段再用采样点区分 0 和 1。20ms 和 30us 是两个可以调的参数20ms 不要低于 18ms30us 不要贴着 20us 下限给晶振和中断留一点空间。等低、等高、等低的超时给 200us比 80us 宽能容忍一点中断抖动。2.2 位判定与字节拼接50us 低电平之后的 26us 和 70us进入读位阶段后每一位都以 50us 低电平开头然后是 2628us 的高电平表示 070us 的高电平表示 1。常见做法是等 50us 低电平结束延时 40us 左右再采样此时位 0 的高电平已经结束读到低位 1 的高电平还在读到高。采样太早0 和 1 都可能是高采样太晚1 也可能已经变低。40us 落在 28us 和 70us 中间是比较稳的折中。static uint8_t DHT11_ReadBit(uint8_t *bit) { if (DHT11_WaitLevel(1, 200) ! 0) return 1; // 等50us低电平结束 delay_us(40); // 0的高电平约26~28us1约70us *bit GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN); if (DHT11_WaitLevel(0, 200) ! 0) return 1; // 等高电平结束 return 0; }DHT11_ReadBit 的 bit 参数是输出读到 0 或 1。函数先等低电平结束也就是等上升沿延时 40us 后采样再等高电平结束为下一位的 50us 低电平做准备。这里的 40us 是经验值不是 DHT11 手册规定的固定采样点。不同主频、不同优化等级、不同延时实现都会影响这个值逻辑分析仪抓一次波形最稳。没有逻辑分析仪时可以用 30us、40us、50us 各试一轮看哪一组校验成功率最高。读字节时DHT11 是高位在前。每读一位把结果左移一位再或进去循环 8 次得到一个字节。读 5 个字节就是 40 位湿度整数、湿度小数、温度整数、温度小数、校验和。DHT11 的小数部分通常为 0但协议里仍然占一个字节解析时不要跳过否则校验和会对不上。static uint8_t DHT11_ReadByte(uint8_t *data) { uint8_t i, bit, val 0; for (i 0; i 8; i) { if (DHT11_ReadBit(bit) ! 0) return 1; val (uint8_t)((val 1) | bit); } *data val; return 0; }DHT11_ReadByte 的 data 参数是输出字节返回值 0 表示成功1 表示某一位超时。val 先清零每读一位左移保证第一次读到的最高位最终落在 bit7。DHT11 的 40 位数据没有起始位和停止位完全靠前面的响应窗口对齐响应窗口一旦等错后面 8 次循环读出来的就是错位字节校验和会直接暴露问题。2.3 校验与温湿度换算第五字节不是“随便加的”5 个字节读完后前 4 个字节相加取低 8 位应该等于第 5 个字节。校验不过时不要急着做温湿度换算先把原始 5 个字节打印出来。现场常见的是湿度整数正常、温度整数漂移或者校验和偶尔差 1这类问题多半不是 DHT11 坏了而是读位阶段被中断打断某一位的高电平采样点偏了。typedef enum { DHT11_OK 0, DHT11_ERR_TIMEOUT, DHT11_ERR_CHECKSUM } DHT11_Status; typedef struct { uint8_t humi_int; uint8_t humi_dec; uint8_t temp_int; uint8_t temp_dec; } DHT11_Data; DHT11_Status DHT11_Read(DHT11_Data *out) { uint8_t buf[5], i; if (DHT11_Start() ! 0) { return DHT11_ERR_TIMEOUT; } for (i 0; i 5; i) { if (DHT11_ReadByte(buf[i]) ! 0) { return DHT11_ERR_TIMEOUT; } } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) { return DHT11_ERR_CHECKSUM; } out-humi_int buf[0]; out-humi_dec buf[1]; out-temp_int buf[2]; out-temp_dec buf[3]; return DHT11_OK; }这段代码把“起始、读 40 位、校验、解析”串成一次完整读取。out 参数是调用者传入的结构体指针函数不负责分配内存。buf[0] 是湿度整数buf[1] 是湿度小数buf[2] 是温度整数buf[3] 是温度小数buf[4] 是校验和。DHT11 的温度范围通常是 050℃湿度 20%90%RH超出这个范围的读数先怀疑时序和供电不要直接当成真实环境值。校验通过后湿度值可以用 humi_int 加 humi_dec 的十分之一拼出来温度同理。DHT11 的小数位多数时候是 0显示时保留整数就够如果项目要求小数点做显示换算时再拼不要在驱动里提前乘 10否则后面换 DHT22 或别的传感器又要改一遍。驱动层只负责“原始字节 校验状态”业务层再做滤波和展示移植时边界清楚。3. 标准库工程落地GPIO、SysTick 与方向切换的五个配置点标准库工程里DHT11 驱动能不能稳定跑往往不取决于读位函数写得多漂亮而取决于五个配置点时钟有没有开、引脚模式切得对不对、微秒延时准不准、读 40 位时有没有被中断打断、读取间隔够不够。下面按工程落地顺序拆开每个点都给可抄的代码和参数说明。3.1 时钟与引脚初始化推挽输出和上拉输入的切换顺序STM32F103 的 GPIO 挂在 APB2 上用标准库要先开 RCC_APB2PeriphClockCmd。DHT11 的 DATA 线在输出和输入之间来回切初始化时不用纠结“到底配输出还是输入”因为每次读取前都会重新配。我一般会在 DHT11_Init 里先配一次上拉输入让总线空闲时保持高电平真正拉低前再切输出。这样上电后不会出现 DATA 线悬空、DHT11 被反复触发的现象。void DHT11_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(DHT11_RCC, ENABLE); gpio.GPIO_Pin DHT11_PIN; gpio.GPIO_Mode GPIO_Mode_IPU; // 空闲时上拉输入 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT, gpio); }DHT11_Init 只做一件事开时钟把 DATA 线置成上拉输入。GPIO_Mode_IPU 是内部上拉输入外部 4.7k10k 上拉仍然要焊。GPIO_Speed_50MHz 对 DHT11 这种低速单总线不是必须但标准库的 GPIO_Init 需要填统一填 50MHz 省得后面改。初始化不拉低总线避免上电瞬间给 DHT11 发一个不完整起始信号。输出模式的配置放在 DHT11_SetOutput 里输入模式放在 DHT11_SetInput 里两个函数都调用 GPIO_Init。有人为了省事直接改 GPIOx-CRL 寄存器速度确实快但换引脚时要算引脚号对应的 4 位配置位移植性差。标准库的 GPIO_Init 多几条指令对 DHT11 的毫秒级起始信号没有影响对读位阶段的微秒级延时有一点影响但读位延时靠 SysTick不靠 GPIO_Init 的耗时。注意每次读取前都要确保 DHT11_SetOutput 里重新开时钟或至少保证时钟已开不要假设别的模块已经开过 GPIOA 时钟。换到 GPIOB、GPIOC 时RCC 宏也要跟着换。引脚选择上最小系统板常见把 DHT11 接在 PA0、PA1、PB5 这类空闲脚。避开 JTAG 占用的 PA13、PA14、PA15、PB3、PB4也避开串口下载脚 PA9、PA10。DATA 线不要和电机、继电器、蜂鸣器共用一组电源DHT11 对电源纹波敏感电机一转就读 255 的现场并不少见。接线短一点杜邦线超过 20cm 时把上拉电阻靠近 DHT11 一端放。3.2 微秒延时SysTick 轮询写法与中断写法的边界DHT11 的读位窗口是几十微秒delay_us 不准后面全白搭。标准库工程里常见两种延时SysTick 轮询和 SysTick 中断。轮询写法不依赖中断读位期间不会被 SysTick 中断打断适合 DHT11 这种时序敏感的场合。中断写法会给系统提供一个毫秒时基但 DHT11 读位时如果正好进 SysTick 中断40us 采样点可能被推后校验和就会偶尔失败。static uint32_t fac_us; void Delay_Init(void) { fac_us SystemCoreClock / 1000000; SysTick-LOAD 0xFFFFFF; SysTick-VAL 0; SysTick-CTRL SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; } void delay_us(uint32_t us) { uint32_t ticks us * fac_us; uint32_t told SysTick-VAL; uint32_t tnow, tcnt 0; while (1) { tnow SysTick-VAL; if (tnow ! told) { if (tnow told) { tcnt told - tnow; } else { tcnt told (SysTick-LOAD - tnow); } told tnow; if (tcnt ticks) { break; } } } } void delay_ms(uint32_t ms) { while (ms--) { delay_us(1000); } }Delay_Init 里 fac_us 是 1 微秒对应的 SysTick 计数72MHz 下是 72。SysTick-LOAD 设成 0xFFFFFF用最大重装载值delay_us 自己累计跨重装载的计数。delay_us 的 us 参数是一次延时的微秒数ticks 是目标计数told 记录上一次 VALtnow 是当前 VALtcnt 累计走过的计数。delay_ms 用循环调 delay_us不要直接算毫秒计数避免一次延时超过 SysTick 的 24 位范围。这套延时是轮询不产生 SysTick 中断所以不会在 DHT11 读位中间插一脚。代价是没有系统毫秒时基如果工程里还要用 SysTick 做别的定时可以把 DHT11 的延时改成 TIM 定时器轮询或者读 40 位期间临时关中断。不同主频下SystemCoreClock 必须是实际主频改了 PLL 没更新这个变量delay_us 会整体偏快或偏慢DHT11 直接读回 0 或 255。提示用逻辑分析仪测 DHT11_Start 里拉低时间20ms 应该看到约 20ms如果明显偏短先查 SystemCoreClock 和 fac_us。3.3 读一个字节的临界区关中断不是万能药DHT11 读完 40 位大约 4ms 左右这段时间如果被串口接收中断、定时器中断打断某一位的高电平采样点就会偏。常见做法是在 DHT11_Read 前后关中断读完再开。关中断能解决大部分抖动但不是万能药关太久会影响串口接收和系统响应关太短又护不住整个读位过程。我的习惯是只包住“起始信号之后的 40 位读取”起始的 20ms 拉低不关中断因为那段时间不怕打断。DHT11_Status DHT11_Read_NoIrq(DHT11_Data *out) { DHT11_Status st; __disable_irq(); st DHT11_Read(out); __enable_irq(); return st; }DHT11_Read_NoIrq 把 DHT11_Read 包在 __disable_irq 和 __enable_irq 之间out 参数原样传给底层读取。这个函数适合调试阶段验证“是不是中断打断导致校验失败”。如果关中断后成功率明显上升说明工程里有中断在抢时间下一步不是长期关中断而是调整中断优先级或缩短中断服务函数。长期关中断会带来串口丢字节、按键不响应属于用后悔药换稳定量产工程要慎重。另一种做法是给 DHT11 读位期间提高自己的中断优先级或者把串口接收改成 DMA。对最小系统板验证阶段关中断最直接对多任务工程优先保证 DHT11 读位不被高优先级中断打断同时把其他中断服务函数压短。临界区不是越长越好能包住 40 位读取就够起始信号的 20ms 和读取间隔的 1s 都不需要关中断。4. 把驱动做成可移植模块三个宏和一层接口就够同一份 DHT11 代码有人换块板子就要重写一遍问题通常不是时序而是引脚、时钟、延时全写死在函数里。把驱动拆成“宏配置 接口层 延时适配”三层换 STM32F103 的最小系统板只改三行宏换主频只改 Delay_Init读业务不用动。下面这套结构不追求大而全只保留 DHT11 真正需要的部分。4.1 头文件抽象换引脚只改三行宏头文件里放三件事引脚宏、状态码、数据结构。引脚宏至少包含端口、引脚号、端口时钟。有人还会把上拉电阻有无做成宏方便在内部上拉和外部上拉之间切换。状态码把“超时”和“校验错”分开调用者才能决定是重试还是丢弃。数据结构只放原始字节不放浮点数避免不同编译器对浮点格式的处理差异。#ifndef __DHT11_H #define __DHT11_H #include stm32f10x.h #define DHT11_PORT GPIOA #define DHT11_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOA #define DHT11_EXT_PULLUP 1 // 外部4.7k~10k上拉必须为1 typedef enum { DHT11_OK 0, DHT11_ERR_TIMEOUT, DHT11_ERR_CHECKSUM } DHT11_Status; typedef struct { uint8_t humi_int; uint8_t humi_dec; uint8_t temp_int; uint8_t temp_dec; } DHT11_Data; void DHT11_Init(void); DHT11_Status DHT11_Read(DHT11_Data *out); DHT11_Status DHT11_Read_NoIrq(DHT11_Data *out); #endifDHT11_PORT、DHT11_PIN、DHT11_RCC 三个宏决定驱动挂在哪根脚上换到 PB5 就把三行一起改不要只改 PIN 不改 RCC。DHT11_EXT_PULLUP 是给人看的提醒代码里不一定要参与编译但它能防止后来的人把外部上拉删掉。DHT11_Status 的枚举值从 0 开始DHT11_OK 为 0调用者可以写 if (st ! DHT11_OK) 统一处理错误。DHT11_Data 用 uint8_t 存四个原始字节DHT11 的小数位多数为 0仍然保留校验和解析时不能省。头文件不要包含 DHT11.c 里的静态函数声明读位、读字节、等电平这些内部函数用 static 关在 .c 里。对外只暴露初始化、读取、带临界区读取三个接口。换传感器时比如从 DHT11 换到 DHT22接口层可以保持 DHT11_Read 的名字不变内部换协议业务层少改代码。名字叫 DHT11 但结构留出扩展空间比一开始就写死“读湿度”“读温度”两个函数更好移植。4.2 接口层设计初始化、读取、错误码分开接口层的核心是“一次读取返回一个明确状态”不要在读取函数里直接 printf 或点亮 LED。初始化只负责 GPIO 和延时读取只负责起始、40 位、校验、填结构体。错误码分三层超时表示没等到响应或某一位没读完校验错表示 40 位读完了但和对不上OK 表示数据可用。调用者拿到校验错可以重试一次拿到超时可以先查接线处理策略不一样。DHT11_Status DHT11_Read(DHT11_Data *out) { uint8_t buf[5], i; if (out 0) { return DHT11_ERR_TIMEOUT; // 空指针按错误处理避免硬 fault } if (DHT11_Start() ! 0) { return DHT11_ERR_TIMEOUT; } for (i 0; i 5; i) { if (DHT11_ReadByte(buf[i]) ! 0) { return DHT11_ERR_TIMEOUT; } } if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) { return DHT11_ERR_CHECKSUM; } out-humi_int buf[0]; out-humi_dec buf[1]; out-temp_int buf[2]; out-temp_dec buf[3]; return DHT11_OK; }这段接口层代码先判空指针再走起始信号再读 5 个字节最后校验。out 为空时直接返回超时错误虽然不是最精确的错误码但能避免解引用空指针。buf 是局部数组5 个字节刚好放湿度整数、湿度小数、温度整数、温度小数、校验和。校验和用 uint8_t 强转让加法自然溢出取低 8 位和 DHT11 的校验规则一致。返回 DHT11_ERR_CHECKSUM 时out 里的旧数据不要用调用者应该丢弃这一次结果。业务层可以按这个状态做重试超时重试一次校验错重试一次连续两次失败就上报传感器故障。不要无限重试DHT11 的读取间隔至少 1s无限重试会把间隔压短反而读到旧数据。重试之间至少隔 1s或者直接等下一次周期任务。接口层不负责重试只负责把状态说清楚这样换到 RTOS 或裸机轮询都能接。4.3 移植到不同主频延时校准与超时计数换主频是 DHT11 移植里最容易翻车的一步。72MHz 的工程搬到 48MHzdelay_us 还按 72 算实际延时变长读位采样点全部偏移。标准库工程里 SystemCoreClock 通常由 system_stm32f10x.c 维护但有人手动改 PLL 后忘了更新delay_us 就不准。移植时先确认 SystemCoreClock 打印出来和实际主频一致再看 delay_us(1000) 在逻辑分析仪上是不是 1ms 左右。void DHT11_PortInit(void) { RCC_APB2PeriphClockCmd(DHT11_RCC, ENABLE); } uint8_t DHT11_CheckInterval(uint32_t last_ms, uint32_t now_ms) { if ((uint32_t)(now_ms - last_ms) 1000) { return 0; // 间隔不足1s不读 } return 1; }DHT11_PortInit 把端口时钟单独拎出来移植时只改 DHT11_RCC。DHT11_CheckInterval 用无符号减法处理毫秒计数溢出last_ms 是上次读取的毫秒时间now_ms 是当前毫秒时间返回 0 表示间隔不够返回 1 表示可以读。DHT11 的数据更新频率不高读取间隔低于 1s 容易读到同一帧或触发内部忙状态常见做法是 2s 读一次显示刷新用缓存值。超时计数也不要写死。DHT11_WaitLevel 里的 timeout_us 在 72MHz 和 8MHz 下都应该按微秒算但如果 delay_us 本身不准超时计数只能兜住“完全没响应”兜不住“响应了但采样点偏”。移植后先跑 100 次读取统计超时和校验错的比例比例明显高于原平台先查主频和延时再查中断。不要一上来改 DHT11 的时序参数时序参数是协议定的延时实现才是变量。5. 避坑与排查读数 0、255、校验失败的五种现场DHT11 的坑很集中接线、上拉、延时、中断、读取间隔。现场表现看起来都是“读不出来”但原因不一样排查顺序也不一样。下面五种现场按出现频率排每条按现象、原因、解决写遇到问题可以顺着查不要一上来就换传感器。5.1 现象一一直返回超时读数全是 0 或 255现象是 DHT11_Read 每次都返回 DHT11_ERR_TIMEOUT串口打印出来湿度 0、温度 0或者原始字节全是 255。原因常见有三个DATA 线没接对引脚、外部上拉电阻没焊、起始信号拉低时间不够。最小系统板上 PA0 和 PB0 长得像宏里写 PA0 实际接 PB0代码等不到响应上拉电阻缺失时总线释放后拉不高DHT11 不响应起始低电平只有 1msDHT11 根本没识别到。解决顺序是先拿万用表量 DATA 线空闲电压应该是 3.3V 左右再把 DHT11 拔掉用代码拉低 DATA 线 20ms 再释放量电压能不能回到 3.3V。回不去查上拉和引脚配置回得去查 DHT11 接线和供电。起始信号用逻辑分析仪看拉低时间必须 ≥18ms释放后 2040us 内应该看到 DHT11 拉低 80us。没有逻辑分析仪就把 delay_ms(20) 改成 delay_ms(30) 试一次排除边界问题但不要长期用 30ms 掩盖延时不准。5.2 现象二湿度正常温度明显偏高或偏低现象是湿度读数在合理范围温度却比实际高十几度或者温度整数偶尔跳变。原因多半是字节解析顺序错了或者把校验和当成了温度小数。DHT11 的 5 个字节顺序是湿度整数、湿度小数、温度整数、温度小数、校验和有人把 buf[2] 当湿度小数、buf[3] 当温度整数温度就会离谱。另一种情况是 DHT11 的小数位恒为 0业务层却按“温度 temp_int temp_dec / 10”算看起来没错但把校验和误读成小数时就会跳。解决是把原始 5 个字节一起打印校验通过后再解析。湿度整数 buf[0] 范围 2090湿度小数 buf[1] 通常 0温度整数 buf[2] 范围 050温度小数 buf[3] 通常 0buf[4] 是校验和不参与温湿度换算。DHT11 不支持负温读到温度整数最高位为 1 的情况先怀疑时序错位不要当成零下温度。换 DHT22 时字节定义不同温度支持负值驱动要另写不要把 DHT11 的解析直接套过去。5.3 现象三校验和偶尔失败重启后又正常现象是连续读 10 次其中两三次返回 DHT11_ERR_CHECKSUM重启或重新插拔杜邦线后好一阵过一会儿又出现。原因通常是读 40 位期间被中断打断或者电源纹波导致某一位高电平变窄。串口接收中断、定时器中断、SysTick 中断都可能落在 40us 采样点附近把位 0 读成位 1校验和自然对不上。杜邦线太长、DHT11 和电机共用 5V 电源也会让波形边沿变缓。解决分两步先用 DHT11_Read_NoIrq 关中断读 100 次看校验失败率有没有降到 0如果降了调整中断优先级或缩短中断服务函数不要长期全局关中断。再查电源DHT11 的 VCC 和 GND 之间加 100nF 去耦电容Data 线上拉电阻靠近 DHT11 一端线长控制在 20cm 以内。校验失败时不要用上一次的值硬顶连续两次失败就上报故障让上层知道传感器当前不可信。5.4 现象四上电第一次成功后面每次都不对现象是复位后第一次读 DHT11 正常之后每隔 100ms 读一次读数不变或者校验错。原因是读取间隔太短DHT11 内部需要时间更新数据低于 1s 的间隔可能读到同一帧或者从机还没准备好。另一种情况是 DHT11_SetInput 之后没有重新配置第二次读取时引脚还停在上拉输入DHT11_Start 里虽然调了 SetOutput但时钟或模式被别的代码改掉了。解决是把读取间隔拉到 2s至少不低于 1s用 DHT11_CheckInterval 做门限。每次读取前重新走 DHT11_Start 里的 SetOutput不要假设引脚状态。如果工程里还有别的模块操作同一个 GPIO 端口检查那些模块有没有把整个端口重新初始化把 DHT11 的引脚模式覆盖掉。上电第一次成功、后面失败优先查间隔和引脚模式再查传感器供电是否在第一次读取后被拉低。5.5 现象五换主频或换优化等级后时序全乱现象是同一份代码在 72MHz 工程里能读搬到 48MHz 或把编译器优化等级从 -O0 改成 -O2 后读数全是 255 或校验错。原因是 delay_us 基于 SystemCoreClock主频变了但 fac_us 没更新或者空循环延时被优化器删掉实际延时变短。DHT11 的 40us 采样点对延时很敏感差 10us 就可能把 0 读成 1。解决是统一用 SysTick 轮询延时不靠空循环。换主频时确认 SystemCoreClock 与实际主频一致Delay_Init 重新算 fac_us。优化等级改变后用逻辑分析仪复测起始低电平 20ms 和读位采样点不要只看代码有没有变。延时函数里用 volatile 或直接读 SysTick-VAL避免循环变量被优化掉。移植到新工程时先把 delay_us(1000) 测准再跑 DHT11 读取顺序不要反。6. 进阶验证用逻辑分析仪和滑动窗口把 DHT11 稳定性量化DHT11 读通一次不算完能不能连续读稳才是边界。我一般会做两步验证先用逻辑分析仪抓 DATA 线看起始低电平是不是 20ms、响应低/高是不是各 80us、位 0 高电平是不是 2628us、位 1 高电平是不是 70us 左右再跑滑动窗口连续读 100 次统计成功、校验错、超时各多少次。逻辑分析仪看的是单次波形滑动窗口看的是长期稳定性两个都过才敢说这份驱动在最小系统板上可用。验证项观察点合格线起始低电平DHT11_Start 拉低段≥18ms常见20ms释放高电平切输入后到响应前2040us响应低/高DHT11 拉低再拉高各约80us位0高电平50us低之后2628us位1高电平50us低之后约70us读取间隔两次 DHT11_Read 之间≥1s建议2s滑动窗口代码不用复杂把状态计数打印出来就够。下面这段放在主循环里每 2s 读一次读满 100本文还有配套的精品资源点击获取