免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

STM32驱动DHT11单总线时序:从标准库到HAL库的完整实现

STM32驱动DHT11单总线时序:从标准库到HAL库的完整实现 简介面向STM32学习者的DHT11温湿度采集工程包以标准库与HAL库双版本形式实现完整采集流程覆盖单总线起始信号、数据位读取、校验求和与串口打印等常见难点同时提供可直接编译运行的工程配置与示例代码适合初学者拿来即用或进阶者对比研究。压缩包文件数共计1167个整体约30.48MB其中589个C源文件与273个头文件构成核心代码同时收有Keil/IAR工程配置、hex/axf编译输出、CubeMX初始化文件ioc/mxproject、txt说明、PPT演示物料以及编译生成的库文件并保留大量o、crf、lst等中间文件便于查看编译链接细节目录结构清晰方便按模块查阅。目前已有3914人学习下载可辅助课程设计、毕业设计或日常嵌入式自学。通过对照两套实现能够直观看到标准库与HAL库在寄存器配置、延时策略、串口发送接口上的差异借助hex/axf文件可快速烧录与调试结合源码逐行梳理DHT11的握手时序、温湿度解码和校验处理利用map/lst文件还能分析代码存储布局从实践层面提升传感器外设开发能力。无论用于快速验证还是二次开发都能提供有效参考。1. DHT11单总线时序为什么总能让STM32项目栽在第一公里跑通LED点灯不等于会做STM32把DHT11温湿度传感器接上主控后程序卡死在while循环里出不来或者串口打印满是0xFF和校验错误这些场景几乎每个用STM32的人都在某个下午遇到过。DHT11没有SPI、I2C那样的时钟线它把40bit数据全部折叠在一根DATA线上主机必须靠微秒级的电平拉低、释放、采样窗口判断来完成读取。这件事对标准库和HAL库都一视同仁区别只在于GPIO模式切换的方式、微秒级延时的实现和串口发送的调用链。下面把两套实现完整过一遍从DHT11的时序参数到可复现驱动代码再到串口调试助手里看到连续可读的温湿度数据。2. DHT11单总线时序与STM32引脚规划先看懂40bit再动手2.1 单总线协议如何在一根线上传完40bitDHT11温湿度传感器和主机之间只有一根数据线所有信号都由主机发起。一次完整读取分为四段主机拉低总线至少18ms作为开始信号释放总线后等待从机应答DHT11检测到低电平跳变后先拉低80us响应再拉高80us随后连续发送40bit数据每个bit由50us低电平和一段高电平组成最后由一位低电平结束整帧传输。40bit的排列是8bit湿度整数、8bit湿度小数、8bit温度整数、8bit温度小数、8bit校验和校验和为前四个字节相加的低8位低位在前高位在后读取时用移位累加逐个拼出字节。判断逻辑0和逻辑1不能只看电平高低因为每个bit都以50us低电平开头差别落在高电平持续时长上。高电平约26-28us是0约70us是1。所以读取算法不是“等待电平为高”而是“在高电平窗口中间判断它是否仍然为高”。这也是DHT11这类单总线协议和标准I2C/SPI最大的不同没有SCK不能用上升沿锁存时序全部由主机软件模拟任何OS延时或中断抢占都可能让一帧数据直接错位。2.2 一次完整读操作的时序参数表与采样窗口把规格书里和实际示波器测量最相关的参数整理成表供后面写代码时对照。需要注意这些是典型值不同批次DHT11会有几微秒偏差代码里要留出余量。信号阶段持续时间说明主机拉低开始信号≥18ms工程里常用20ms太短传感器不响应主机释放总线20-40us释放后由外部上拉把总线拉高到3.3V从机应答低电平80us低电平起始处是整个采样窗口的参考零点从机应答高电平80us结束后进入40个数据位数据位逻辑0低50us高26-28us在低电平结束后延时30-40us采样应读到低数据位逻辑1低50us高70us同一采样点读数应仍为高相邻两次采集间隔≥1s密集连续读取会造成传感器无响应读位的窗口可以简化为每个bit从低电平上升沿开始先用循环等50us低电平结束再延时30-40us后读一次电平。读到低说明这是短高电平的0读到高说明这是宽高电平的1随后要把剩余高电平等完再进入下一位。30-40us这段延时是整个程序里最值得调的参数太短会把0高位误读成1太长又会在1的高电平还没结束时提前采样导致边沿判断混乱。提示不要用“等待电平下降”的方式区分0和1。连续收到多个0时总线在我们的采样点附近就会出现下降沿如果程序还在等上一次高电平结束会漏掉下一个bit的起始低电平导致整帧数据错位。2.3 引脚选型推挽模式切换还是开漏输出加上拉电阻DHT11数据的双向口必须既能被主机拉低也能被传感器拉低同时主机还要能读取电平。常见做法有两种。第一种是把STM32引脚配置成推挽输出开始信号拉低20ms再释放随后把GPIO立刻切换成输入上拉模式去读从机信号。标准库例程大多是这种思路每次读取都要调用两次GPIO_Init切换瞬间的电平毛刺也会影响响应窗口。第二种是开漏输出加外部上拉电阻。开漏模式下写0引脚真正拉低写1引脚进入高阻由外部上拉把总线抬到3.3V。这样读总线时不需要切换GPIO模式代码和逻辑都简洁很多。推荐用第二种接法DHT11的DATA脚接任意GPIO线路上并联4.7k欧电阻到VCCVCC与GND之间加100nF去耦电容。手头没有4.7k时10k也能跑但导线较长时上升沿会变缓30-40us采样窗口容易抖动。引脚建议选PB0这类带EXTI能力的脚后面如果要升级成输入捕获或外部中断方案不用重新改板。2.4 微秒级延时标准库和HAL库都绕不开的公共难题DHT11的整个通信过程以微秒计算最怕的就是用不靠谱的延时函数。标准库的Delay_ms基于SysTick循环HAL库的HAL_Delay依赖SysTick中断做毫秒计时这两个都不能直接用于DHT11。SysTick在毫秒延时结束后如果没有处理好重装载值和中断标志微秒延时循环就会变得不确定。更麻烦的是HAL_Delay依赖SysTick全局中断一旦被更高优先级中断打断20ms拉低时间误差会直接传导到后续采样窗口。工程里更稳的微秒延时方案是DWT也就是Cortex-M3内核里的数据观察点与跟踪单元。它的CYCCNT计数器以处理器时钟周期自增不占用SysTick、不需要中断、不受调度影响。DWT使能一次后之后就能用两条指令精确读取且和库版本无关标准库和HAL库完全通用。接下来的所有代码微秒延时都基于DWTHAL_Delay在DHT11驱动里只用作启动时的毫秒级延时。3. 标准库实现DHT11采集与串口显示GPIO模拟时序与printf重定向3.1 GPIO初始化和DWT微秒延时的基础工程标准库v3.5的工程模板建好后第一件事是确认系统主频为72MHz这决定DWT的周期数换算。DHT11数据脚接PB0GPIO时钟打开后先配置成推挽输出并把电平拉高因为发送开始信号之前总线必须保持空闲高电平#include stm32f10x.h #include stdio.h #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOB static void DHT11_GPIO_Init_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(DHT11_RCC, ENABLE); GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); }这里GPIO_WriteBit把初始电平拉高是为了避免上电时引脚默认为低电平让DHT11误判成一次开始信号。GPIO_Mode_Out_PP是推挽输出GPIO_Speed_50MHz保证引脚翻转足够快后续切换输入模式读取位时GPIO速度上不去会拉长数据线上电平边沿的建立时间直接压缩采样窗口。DHT11读取过程中模式切换会用到两次GPIO_Init进入输入上拉模式时也要带GPIO_Speed参数虽然输入模式其实不看速度但标准库初始化结构体里的这个字段仍然要正确赋值避免CRL寄存器余位配置错误。DWT微秒延时函数如下72MHz主频下每微秒72个周期static void DWT_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 wait us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) wait); }SystemCoreClock在启动文件里会被初始化为72000000wait就是每微秒72个周期。这里用无符号减法判断时间差即使CYCCNT自增溢出回绕也能得到正确结果。如果主频被CubeMX改成8MHz或使用内部HSI后没有正确配置PLLDWT换算出的等待时间会偏大DHT11读位会全部超时这是很多照着网上的代码却跑不通的第一原因。3.2 标准库DHT11完整驱动开始信号、40bit读取与校验和HAL库不同标准库在开漏输出上没有内部的推挽结构配合所以使用推挽输出加模式切换的经典方案。发送开始信号后一次性切到输入上拉模式读完一帧再切回推挽输出并拉高避免每个bit都重新调用GPIO_Init。单个字节读取函数如下static uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (int i 7; i 0; i--) { while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) 0); delay_us(40); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) 1) { data | (1 i); while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) 1); } } return data; }程序先停在低电平上等待位起始上升沿到来后延时40us再采样。如果这时总线已经被拉低说明这是高电平只有26-28us的0如果总线仍为高电平说明是宽高电平的1记下当前位后把剩余高电平等完。容易忽略的是为什么这里每个bit都要重新等待低电平到高电平的跳变而不是上一个bit结束时直接开始下一个bit。因为连续收到多个0时总线从高到低的下降沿正是下一个bit的起始低电平只有每次都等低电平结束才能保证不会漏掉半个bit导致整帧错位。40us这个值在72MHz下用DWT是稳定的如果用了较长杜邦线或改成慢速时钟可以往35us调。整帧读取封装成带校验的函数返回1表示成功0表示失败typedef struct { uint8_t humi_int; uint8_t humi_dec; uint8_t temp_int; uint8_t temp_dec; } DHT11_Data_t; uint8_t DHT11_ReadData(DHT11_Data_t *data) { uint8_t buf[5] {0}; GPIO_InitTypeDef GPIO_InitStructure; GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_RESET); delay_us(20000); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); delay_us(30); GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) 1) { GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); return 0; } while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) 0); while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) 1); for (int i 0; i 5; i) { buf[i] DHT11_ReadByte(); } GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); GPIO_WriteBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN, Bit_SET); if ((uint8_t)(buf[0] buf[1] buf[2] buf[3]) ! buf[4]) { return 0; } >int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }USART_FLAG_TXE表示发送数据寄存器为空说明刚才写入USART_SendData的字节已经从数据寄存器移动到移位寄存器开始发送。TXE置位不代表移位寄存器发完但对printf逐字节输出来说这时可以安全写入下一字节不会覆盖正在发送的数据。如果在大量连续发送时追求更高可靠性可以用USART_FLAG_TC做发送完成判断但要注意TC标志读出后需要软件清零否则下一次等待会立即返回。主循环里每2秒采集一次并打印时序上给足DHT11要求的采集间隔int main(void) { DHT11_Data_t dht11_data; DWT_Init(); DHT11_GPIO_Init_Output(); USART1_Init(115200); while (1) { if (DHT11_ReadData(dht11_data)) { printf(Humi: %d.%d%% Temp: %d.%dC\r\n, dht11_data.humi_int, dht11_data.humi_dec, dht11_data.temp_int, dht11_data.temp_dec); } else { printf(DHT11 read error\r\n); } delay_ms(2000); } }串口初始化函数USART1_Init内部设置波特率115200、8数据位、1停止位、无校验、无硬件流控。printf格式串里的回车换行用\r\n因为很多串口调试助手默认以\r\n作为一整条数据的分隔线只发\n在部分终端上不会换行。注意如果printf输出乱码先核对串口调试助手波特率是否115200再看系统时钟是否72MHz。串口波特率寄存器的分频依赖APB2时钟时钟源配置错误会让实际波特率偏差到无法识别此时即使代码完全正确收到的也全是乱码。3.4 标准库版本读到的数据固定为0xFF或频繁校验失败最典型的现象是三种。第一种是串口一直打印read error说明GPIO输入模式下总线一直为高DHT11没有把总线拉低。先查上拉电阻和接线再看传感器供电是不是在2.8V到5.5V范围内数据线有没有接反。第二种是读到的温湿度固定为0xFF方向是程序根本没等到传感器的应答低电平GPIO模式切换太慢错过80us响应窗口。第三种是偶尔正常、大多数时间校验失败几乎都指向时序没收到。特别是释放总线后切输入模式的时机标准库的GPIO_Init内部要写CRL寄存器释放后的延时建议从30us改成40us给GPIO_Init留出执行时间。还有一个非常容易被忽略的因素连续两次读取之间必须隔1秒以上。如果main循环里没有延时就连续调用DHT11会直接不响应表现为固定打印read error。这不是驱动代码的问题而是DHT11内部采样机制决定的外部看是传感器“拒绝工作”实际是它还在处理上一帧数据。4. HAL库实现DHT11采集与串口显示CubeMX配置与开漏读取4.1 CubeMX引脚配置清单与时钟树的三个关键项HAL库路线先要有一个CubeMX生成的工程骨架以下配置可以直接照抄。芯片选STM32F103C8T6System Core里的SYS配置Debug为Serial Wire这一步决定SWD下载口是否被保留很多新手在PB3/PB4上接了东西后才发现无法下载。RCC里HSE选择Crystal/Ceramic Resonator启用外部8MHz晶振。USART1选Asynchronous波特率1152008位数据、无校验、1位停止。DHT11数据脚选PB0GPIO模式配Output Open Drainpull-up选Pull-up。核心配置项整理如下配置项选项说明SYS - DebugSerial Wire保留SWD下载口避免一次下载后连不上RCC - HSECrystal/Ceramic Resonator启用外部8MHz晶振Clock ConfigurationHCLK 72MHzAPB272MHzAPB136MHz串口时钟源正确PB0 GPIO modeOutput Open Drain开漏输出外部上拉4.7kPB0 GPIO Pull-upPull-up内部上拉作补充空闲时总线为高USART1 ModeAsynchronous115200-8-N-1USART1 NVICglobal interrupt供串口中断接收使用时钟树配置最容易出错的是总线分频。USART1挂在APB2上最高72MHzUSART2、USART3挂在APB1上最高36MHz。CubeMX在HCLK输入72MHz后会自动分配预分频但如果手动改过APB1分频串口波特率就会按错误的时钟源计算。生成代码后在SystemClock_Config里确认SystemCoreClock是72MHz同时检查时钟树界面里APB2外围时钟是否为72MHz。Pinout视图里还要确认PB0右侧不显示为其他复用功能USART1的TX是PA9、RX是PA10。PA13和PA14是SWDIO和SWCLK如果被复用到其他外设Keil或ST-Link会报类似error: no stm32 target found的错误实际上芯片和目标板之间的调试链路已经断了不是代码问题。这也是为什么CubeMX里SYS-Debug必须选Serial Wire而不能选Disable。4.2 HAL库驱动DHT11开漏输出下不需要切换GPIO模式CubeMX初始化后的PB0是开漏上拉输出且初始为高电平这正好符合DHT11总线空闲状态。发送开始信号只需两步HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); DWT_Delay_us(20000); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); DWT_Delay_us(30);开漏模式下写RESET会真正拉低总线写SET则是让引脚进入高阻状态总线被外部4.7k上拉拉高。这和推挽输出写逻辑1有本质区别推挽模式下输出寄存器写1会主动输出高电平而开漏模式下只是“不驱动”靠上拉电阻完成电平恢复。正因为如此HAL库版本不需要像标准库那样切换GPIO模式也就不存在模式切换造成的电平毛刺和响应窗口错过问题。读取函数和标准库逻辑几乎一致但去掉了所有模式切换static uint8_t DHT11_ReadByte_HAL(void) { uint8_t data 0; for (int i 7; i 0; i--) { while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET); DWT_Delay_us(40); if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_SET) { data | (1 i); while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_SET); } } return data; }和标准库版本对照循环结构和延时采样点完全一样。区别在GPIO访问开销HAL_GPIO_ReadPin通过Handle结构体间接访问寄存器每调用一次比标准库的GPIO_ReadInputDataBit多几十个周期。在72MHz主频下50us低电平有3600个周期多出来的几十周期不影响采样但如果在低功耗场景把主频降到8MHz这些调用开销就会压缩采样余量。所以HAL库版本务必把编译器优化级别开到-O2不要在Debug模式下完全无优化运行。如果怀疑时序受优化影响用逻辑分析仪看PB0脚波形最直接数据位0的高电平区间应该稳定在26-28us数据位1在70us左右。DWT延时在HAL库工程里的写法static void DWT_Delay_us(uint32_t us) { if (!(CoreDebug-DEMCR CoreDebug_DEMCR_TRCENA_Msk)) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } uint32_t start DWT-CYCCNT; uint32_t wait us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) wait); }DWT_Delay_us里每次判断TRCENA位是为了防止调试器在Debug会话中把DWT停掉。CubeMX生成的main.c已经初始化SystemCoreClock变量直接用即可。如果工程里多处调用微秒延时可以把DWT_Init单独提出来只在启动时调用一次DWT_Delay_us只做取计数值和比较减少判断分支开销。4.3 HAL_UART_Transmit输出整帧字符串而不是逐字符hackHAL库发串口最直接的方式是HAL_UART_Transmit。把格式化后的整数放进缓冲区一次发送整帧uint8_t msg[64]; int len snprintf((char *)msg, sizeof(msg), Humi: %d.%d%% Temp: %d.%dC\r\n, dht11_data.humi_int, dht11_data.humi_dec, dht11_data.temp_int, dht11_data.temp_dec); HAL_UART_Transmit(huart1, msg, len, 100);HAL_UART_Transmit的四个参数依次是串口句柄、发送数据指针、发送长度和超时时间。超时单位是毫秒在115200波特率下64字节发送耗时约5ms100ms余量充足。如果程序卡在HAL_UART_Transmit内部不返回优先检查USART1使能位和GPIO复用配置PA9的AFIO配置错误时TX引脚是没有波形输出的。如果想要printf无缝工作HAL库下重写fputc同样可行但要注意HAL_UART_Transmit是按字节进入阻塞等待的printf打印一条日志会产生几十次UART调用。调试时够用生产环境建议改成攒帧后一次发送。HAL_UART_Transmit在发送期间会屏蔽同优先级中断如果系统里有高频中断处理串口打印那段代码会成为实时性的抖动源。4.4 标准库与HAL库的差异对照性能、移植与选型两个版本的读取逻辑完全同源差异集中在GPIO访问模型和串口发送链路。标准库直接操作寄存器代码更贴近底层调试时能很直观地看到引脚翻转HAL库把GPIO抽象成端口加引脚的Handle代码风格统一但调用链更长微秒级时序下需要依赖DWT和优化选项来弥补性能开销。对比项标准库 v3.5HAL库CubeMX生成GPIO模式切换每帧需要Init两次开漏模式整个读取无需切换微秒延时DWTDWT不能依赖HAL_Delay位读取函数GPIO_ReadInputDataBitHAL_GPIO_ReadPin串口发送USART_SendDataTXE轮询HAL_UART_Transmit时钟树配置手动RCC_APB2配置CubeMX生成并自动校验中断接收无特殊限制需要在回调中重新使能Receive_IT标准库虽然官方停止更新但存量资料和例程很多搜索stm32标准库新建工程和stm32f407adc标准库还能找到大量可参考模板对理解寄存器底层的帮助比HAL库版本大得多。HAL库则是ST当前主推方案CubeMX生成工程后外设初始化代码不会写错维护性更好。通用做法是先拿标准库版本跑通一遍完整理解开始信号、应答和位采样的时序再切到HAL库做同一份需求遇到问题时能分清是库封装导致的差异还是自己的逻辑缺陷。5. 串口调试与进阶排查乱码、中断只收一次与稳定的非阻塞采集5.1 串口显示前先把硬件闭环走通CH340驱动与串口调试助手温湿度数据能否在串口调试助手里显示最容易卡的环节其实不是代码而是USB转串口的环境。CH340驱动没有安装好时设备管理器里会出现带黄色感叹号的USB Serial设备串口调试助手根本找不到COM口。安装驱动后插上USB线在“端口COM和LPT”下能看到一个串口号右键属性里确认波特率115200、8数据位、1停止位、无校验。打开串口调试助手后如果读到乱码先把波特率切到9600或38400试发送看看内容是否变得可读这种换挡测试能快速区分是程序波特率配置错误还是串口线接线问题。5.2 STM32F103C8T6串口中断接收只收一次的修复在DHT11例程里加上串口命令控制时最常见的坑是首次能收到数据之后中断再也不触发。根因是HAL库的异步接收是一次性的HAL_UART_Receive_IT收到指定字节后会进入回调并关闭接收回调里不再次调用它外设就停止响应后续数据。修复方式是在回调末尾重新使能void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart huart1) { rx_buffer[rx_index 63] rx_data; rx_index; HAL_UART_Receive_IT(huart1, rx_data, 1); } }这里HAL_UART_Receive_IT的第三个参数是1表示每次只收1字节收到后进入回调再重新开启接收。rx_index按位与63实现环形缓冲下标约束64字节缓冲足够存放一条命令。如果改成一次接收整行例如长度参数写成缓冲区剩余空间则用户输入半条命令时回调永远不触发整个串口会看起来像死掉一样这是比“只收一次”更容易踩的坑。5.3 连续统计校验失败率是更可靠的验证方法判断DHT11采集稳不稳靠统计单次数值没有意义用连续100次读取的成功率说话。main循环里对读取次数计数校验失败时失败计数加1成功后累加湿度表整数值每100次打印一次平均值和失败次数然后清零重新统计。失败率超过5%时把读位采样延时从40us向30us方向每次调整2us再统计一轮。DHT11本身数据抖动很小读值看起来正常不代表时序裕量充足校验失败率才是量化采集可靠性的有效指标稳定设备应长期保持0次失败。5.4 用非阻塞状态机替代4ms死等前面所有实现都是整帧阻塞读取从拉低开始信号到读完40bit要占约4ms期间CPU无法响应其他任务。裸机轮询里可以接受但工程一旦叠加OLED刷新、按键扫描和串口协议解析4ms阻塞就会引起实时性抖动。可以把这个过程拆成状态机在SysTick的1ms周期回调里推进typedef enum { DHT11_IDLE, DHT11_START_LOW, DHT11_START_HIGH, DHT11_RESPONSE, DHT11_READ_BITS, DHT11_CHECK, DHT11_DONE } DHT11_State;每个状态用一个tick_start变量记录进入时间在定时器回调里判断是否满足当前状态持续时间满足则切换动作并进入下一状态。读位状态再拆成等待低电平结束、延时采样、判断高电平余量三个细粒度动作最终把4ms阻塞打散成若干次1ms内的快速判断。实际工程里这套状态机常与OLED刷新和串口协议栈配合使用温湿度采集间隔固定在2秒总线占用不再是系统实时性的瓶颈。本文还有配套的精品资源点击获取
返回列表