STM32F103标准库DHT11单总线驱动移植与时序避坑
发布时间:2026/10/30 10:22:51 锦皓数字建站

简介这份STM32F103 DHT11驱动程序面向使用最小系统板学习环境监测的嵌入式初学者与单片机开发者基于标准库实现单总线温湿度采集重点解决GPIO时序控制、定时器或软件延时、中断接收与40位数据解析等移植难点可直接用于课程实验、毕业设计和小型物联网终端原型。压缩包共211个文件约5.75MB包含50个h头文件与45个c源文件便于查看驱动接口和模块实现另有Keil工程文件d、o、crf、uvprojx、axf/hex下载文件及map、lst、sct等编译链接产物并附少量bat、txt辅助文件目录保留完整工程结构方便对照编译与调试。已有955人学习适合需要快速验证DHT11驱动、理解标准库配置并移植到其他STM32F103板卡的读者参考也能从中梳理传感器时序排错与工程组织思路。1. 为什么这块最小系统板配上 DHT11 驱动值得单独拆一遍手头有一块 stm32f103 最小系统板又买了个 DHT11 温湿度模块很多人第一步就卡住网上找的 dht11 驱动要么是 HAL 库命名法要么绑死在某个开发板的引脚上改个 GPIO 就编译一堆错。这份基于标准库的 dht11 驱动恰恰是针对这个痛点写的文件少、依赖少、引脚和延时全部靠宏导出挪到自己的工程里主要就是改三四个定义。它适合刚上手嵌入式单片机、想先把「单总线器件怎么读」这件事跑通的人也适合手里有老项目还在用标准库、不想为一个小传感器引入整套 HAL 的开发者。下面按工程骨架、时序实现、移植接口、踩坑排查、二次开发五段往下走。2. 工程骨架最小系统板接线与标准库目录怎么摆先说清楚这套代码运行在什么环境里。目标是 STM32F103C8T6 这类最小系统板72MHz 主频标准外设库StdPeriph Library工程编译工具链用 Keil 或 arm-none-eabi-gcc 都能过。整套驱动不依赖任何 RTOS不依赖 printf 重定向只要能提供一个微秒级延时和一个能被拉低拉高的 GPIO它就能跑起来。这一点决定了它「简单易移植」不是口号而是接口约束足够窄。2.1 硬件连接与上拉电阻这件事接线本身只有三根VCC、GND、DATA。DHT11 的供电范围宽3.3V 和 5V 都能工作但在 STM32 的 GPIO 只耐受 3.3V 的前提下直接给 5V 供电、数据线又直连 MCU 是有风险的。稳妥的做法是整体用 3.3V 供电数据线加上拉。信号DHT11 引脚最小系统板说明VCC13.3V与 MCU 同域避免电平不匹配DATA2PA1可改单总线需 4.7k~10k 上拉NC3悬空部分模块此脚为空GND4GND共地必接上拉电阻是最容易被忽略的一环。DHT11 的数据线是开漏结构总线释放后靠上拉拉高如果没有外部上拉只靠 MCU 内部弱上拉边沿会变得很软读出来的位宽判断就会飘。我一般直接在模块的 DATA 和 VCC 之间焊一个 4.7k 电阻比在代码里反复调延时靠谱得多。还有一点DATA 线不要和电机、继电器、开关电源走同一束线。DHT11 的位宽是几十微秒量级一根长线加一个干扰源足够让高电平 26us 和 70us 的区分失效。2.2 标准库工程的目录摆放这套驱动拆成两个文件更利于移植一个头文件放所有和硬件相关的宏与对外接口一个源文件放时序实现。标准库工程里通常这样摆Project/ ├── Libraries/ │ ├── CMSIS/ # 内核头文件与启动文件 │ └── STM32F10x_StdPeriph_Driver/ # 标准外设库 ├── Hardware/ │ ├── dht11.c # 时序实现不含板级细节 │ └── dht11.h # 引脚宏、对外接口、返回码 ├── User/ │ ├── main.c │ ├── stm32f10x_conf.h # 标准库头文件裁剪开关 │ ├── stm32f10x_it.c │ └── system_stm32f10x.c └── Output/关键在于dht11.c里不出现GPIOA、GPIO_Pin_1这类具体名字全部通过dht11.h里的宏间接引用。这样换板子的时候只动头文件源文件一个字不改代码评审时也容易看出改动范围。stm32f10x_conf.h里至少要让stm32f10x_gpio.h、stm32f10x_rcc.h、misc.h处于启用状态。标准库工程最常见的编译错误就是某个外设头文件被注释掉了然后报GPIO_InitTypeDef未定义跟驱动本身没关系。2.3 初始化顺序时钟、GPIO、延时标准库的初始化顺序有硬性依赖先开时钟再配 GPIO最后才能用延时。顺序反了不会立刻崩但会出现「第一次读失败、复位后又好了」这种玄学现象。/* dht11.h 中与硬件绑定的部分换板子只改这一段 */ #ifndef __DHT11_H #define __DHT11_H #include stm32f10x.h /* 引脚定义改这三行即可迁到其他 GPIO */ #define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_Pin_1 #define DHT11_GPIO_CLK RCC_APB2Periph_GPIOA /* 返回值约定调用方据此决定是否重试 */ #define DHT11_OK 0 #define DHT11_ERR_NO_RESPONSE 1 /* 起始信号后没有等到响应 */ #define DHT11_ERR_CHECKSUM 2 /* 校验和不匹配 */ #define DHT11_ERR_TIMEOUT 3 /* 位读取超时总线被拉死 */ void DHT11_Init(void); uint8_t DHT11_Read(uint8_t *temp, uint8_t *humi); #endif/* dht11.c 中的初始化注意时钟一定在最前面 */ void DHT11_Init(void) { GPIO_InitTypeDef gpio; /* 1. 使能 GPIO 时钟标准库必须先开时钟再配寄存器 */ RCC_APB2PeriphClockCmd(DHT11_GPIO_CLK, ENABLE); /* 2. 默认配成开漏输出总线空闲时由上拉拉高 */ gpio.GPIO_Pin DHT11_GPIO_PIN; gpio.GPIO_Mode GPIO_Mode_Out_OD; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, gpio); /* 3. 总线释放交给上拉电阻维持高电平 */ GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); /* 4. 上电后器件需要时间稳定这里留 1s别省 */ delay_ms(1000); }这段代码里有三个参数值得说清楚。GPIO_Mode_Out_OD选的是开漏输出而不是推挽原因是单总线在同一根线上要双向传输推挽输出在器件拉低总线时会形成对拉长期这么干容易损坏 IO。GPIO_Speed_50MHz对 DHT11 这种几十微秒级的信号其实用不上但开漏模式的上升沿靠上拉电阻速度配置高一点不会更差配低了反而边沿更软。最后的 1 秒延时是器件手册里要求的上电稳定时间跳过它就会出现「第一批数据永远是 0」的经典现象后面避坑章节还会展开。3. 单总线时序40 位数据是怎么一位一位抠出来的DHT11 的通信是典型的主从单总线主机拉低总线发起始信号器件应答然后器件主动输出 40 位数据。整个过程没有时钟线全靠双方约定的时间窗口所以这套驱动能不能用八成取决于微秒延时准不准和采样点选得对不对。这一章把时序拆成延时、起始、逐位读取三段讲。3.1 微秒级延时SysTick 是最省事的那条路标准库工程里通常没有现成的delay_us很多人第一反应是写个空循环void delay_us(uint32_t n) { while (n--) __NOP(); }这种写法在不同优化等级下循环次数完全不一样-O0能跑、-O2就短了一半属于典型的「我电脑上好好的」。要稳定用 SysTick 做轮询式短延时不占用中断。/* * 基于 SysTick 的微秒延时假定系统时钟 72MHz * SysTick 是 24 位递减计数器LOAD 最大 0xFFFFFF * 因此单次可延时的上限约 0xFFFFFF / 72 ≈ 233015 us */ void delay_us(uint32_t us) { uint32_t reload 72U * us; /* 72MHz 下 1us 计 72 个数 */ if (us 0U) return; if (reload 0xFFFFFFU) reload 0xFFFFFFU; /* 超长延时截断避免溢出 */ SysTick-LOAD reload - 1U; SysTick-VAL 0U; SysTick-CTRL 0x00000005U; /* 时钟源 HCLK使能计数不开中断 */ while ((SysTick-CTRL 0x00010000U) 0U); /* 等 COUNTFLAG 置位 */ SysTick-CTRL 0x00000000U; /* 关闭计数器恢复现场 */ SysTick-VAL 0U; } /* 毫秒延时直接叠加误差在百微秒级够用 */ void delay_ms(uint32_t ms) { while (ms--) delay_us(1000U); }逻辑上LOAD装的是重装载值写reload - 1是因为计数器从该值递减到 0 产生一次计数周期。CTRL写成0x05表示选择内核时钟、使能计数、不产生中断最后一位是ENABLE。轮询COUNTFLAG是第 16 位置位说明计数完成。参数说明us传入的值建议不要超过 200000超过会被截断成一次最大延时行为不是「延时不生效」而是「延时时长变短」这是最容易误判的地方。如果你的主频不是 72MHz把72U换成实际主频的 MHz 数即可这也是移植清单里的必改项。另外这个函数会独占 SysTick工程里如果已经用 SysTick 做系统节拍就得改成读 DWT 的 CYCCNT或者干脆用一个定时器做延时基准二者不要混用。3.2 起始信号与响应检测主机要发起一次读取动作是拉低总线至少 18ms然后释放等器件把总线拉低 80us 再拉高 80us 作为应答。这段代码里每一个delay_us的数值都直接对应手册里的时间窗。/* 发起始信号并等待器件应答返回 DHT11_OK 表示可以开始读数据 */ static uint8_t DHT11_StartAndWait(void) { /* 1. 主机拉低总线至少 18ms这里给 20ms 留余量 */ GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); delay_ms(20); /* 2. 释放总线20~40us 内器件会接管 */ GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); delay_us(30); /* 3. 等器件把总线拉低应答信号的第一段 80us 低电平 */ if (DHT11_WaitLevel(Bit_RESET, 100) ! DHT11_OK) return DHT11_ERR_NO_RESPONSE; /* 4. 等器件释放总线第二段 80us 高电平 */ if (DHT11_WaitLevel(Bit_SET, 100) ! DHT11_OK) return DHT11_ERR_NO_RESPONSE; /* 5. 再等这一段高电平结束之后就是 40 位数据 */ if (DHT11_WaitLevel(Bit_RESET, 100) ! DHT11_OK) return DHT11_ERR_NO_RESPONSE; return DHT11_OK; }逐段说明第 1 段的 20ms 是下限余量手册要求不小于 18ms写 20 是给自己留安全边界第 2 段释放后等 30us 再采样是因为器件从检测到上升沿到拉低有自己的响应时间太早采样会读到空闲高电平误判成「无响应」。第 3、4、5 段都用同一个等待函数把「等某个电平出现」抽象出来避免到处写死循环。DHT11_WaitLevel的实现要点是必须带超时/* 等待总线变成指定电平timeout_us 为最长等待微秒数 */ static uint8_t DHT11_WaitLevel(BitAction level, uint32_t timeout_us) { while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) ! level) { if (timeout_us-- 0U) return DHT11_ERR_TIMEOUT; /* 器件没响应或总线被拉死 */ delay_us(1); } return DHT11_OK; }参数说明level用标准库的BitAction枚举Bit_RESET是 0、Bit_SET是 1比裸写 0/1 可读性好。timeout_us这里给 100比任何一个 80us 的电平段都宽既能覆盖正常时序又能在器件损坏或线断时快速返回不至于卡在 while 里出不来。血泪经验是任何等待外部器件的循环都必须有超时否则一个焊点虚焊就能让整个固件假死。还有一处细节GPIO_ReadInputDataBit读的是输入数据寄存器它在开漏输出模式下依然反映引脚的真实电平所以这里不需要来回切方向。如果你的工程把引脚配成了推挽输出这个读法就会读到自己驱动的高电平时序全错——这也是后面避坑章节的一条。3.3 40 位逐位读取与校验应答结束后器件连续输出 40 位8 位湿度整数、8 位湿度小数、8 位温度整数、8 位温度小数、8 位校验和高位先出。每一位都以 50us 低电平开头随后是高电平高电平持续 26~28us 表示 0持续 70us 表示 1。区分这两种情况的经典做法是「低电平结束后延时 40us 再采样」。/* 读一个字节高位在前 */ static uint8_t DHT11_ReadByte(void) { uint8_t i, data 0U; for (i 0U; i 8U; i) { /* 1. 等这一位的 50us 低电平结束 */ if (DHT11_WaitLevel(Bit_SET, 100) ! DHT11_OK) return 0xFFU; /* 2. 延时 40us 后采样0 的高电平已结束1 的还没结束 */ delay_us(40); if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET) { data | (uint8_t)(1U (7U - i)); /* 高位先出注意移位方向 */ /* 3. 等这一位的高电平结束好让下一位从低电平开始 */ if (DHT11_WaitLevel(Bit_RESET, 100) ! DHT11_OK) return 0xFFU; } } return data; }采样点选 40us 是有讲究的。0 的高电平只有 26~28us40us 时早已回落到低电平1 的高电平约 70us40us 时还在高电平上。采样点落在两者中间偏 1 的一侧容错空间最大。如果把延时改成 30us0 的尾部还残留一点高电平就会读成 1改成 60us1 可能已经结束又会读成 0。这个 40 是我反复试出来的甜点值主频偏差较大时要重新标定。移位方向7 - i对应高位先出。写反了不会报错只会得到一串看似合理但完全对不上的数值湿度永远是 0、温度永远是个位数第一次遇到很容易怀疑传感器坏了。return 0xFF在这里当错误标记用因为正常数据里 0xFF 极少单独出现而且外层还有校验和兜底。严格一点的做法是改成返回状态码加出参但驱动保持简单也有它的价值。3.4 一次完整读取的封装把上面几段拼起来对外只暴露一个函数。返回值分三类成功、无响应、校验失败调用方据此决定是重试还是丢弃。/* * 读取一次温湿度 * temp、humi 为输出参数单位分别是摄氏度和百分比 RH * 返回 DHT11_OK 表示数据有效 */ uint8_t DHT11_Read(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i, sum; if (temp 0 || humi 0) return DHT11_ERR_TIMEOUT; if (DHT11_StartAndWait() ! DHT11_OK) return DHT11_ERR_NO_RESPONSE; for (i 0U; i 5U; i) buf[i] DHT11_ReadByte(); /* 校验和 前四个字节相加取低 8 位 */ sum (uint8_t)(buf[0] buf[1] buf[2] buf[3]); if (sum ! buf[4]) return DHT11_ERR_CHECKSUM; *humi buf[0]; /* 湿度整数部分 */ *temp buf[2]; /* 温度整数部分 */ return DHT11_OK; }数据格式对照表放在这里更直观字节序号含义说明buf[0]湿度整数单位 %RHbuf[1]湿度小数DHT11 恒为 0buf[2]温度整数单位 ℃buf[3]温度小数DHT11 恒为 0buf[4]校验和前四字节之和的低 8 位注意 DHT11 只给整数精度想要小数得换 DHT22 或 SHT 系列这不是驱动的问题。另外温度是 8 位无符号表示0~50℃ 范围内没问题如果以后换成分辨率更高、支持负温的型号这里要改成有符号处理别直接沿用。4. 移植接口换引脚、换主频、换工程只动三个地方「简单易移植」要落到可执行的检查清单上否则就是一句空话。这套驱动的移植面一共三处引脚宏、主频常量、延时函数实现。其他地方都和硬件无关这也是当初拆文件时刻意守住的边界。4.1 把硬件相关的东西全塞进一个头文件dht11.h里的宏分三类引脚、时钟、返回码。前两类是必须改的返回码一般不动。/* 迁到 PB5 的写法只改这三行 */ #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_PIN GPIO_Pin_5 #define DHT11_GPIO_CLK RCC_APB2Periph_GPIOBRCC_APB2Periph_GPIOB这个宏一定要和端口对应上写错不会报编译错误只会表现为「初始化成功但读不到数据」因为时钟压根没开写寄存器的操作全部无效。我在评审别人的移植代码时第一眼就看这三个宏是否成套。如果目标板用的是 GPIOE 这类挂在 APB2 上的端口宏名仍然是RCC_APB2Periph_GPIOE标准库已经封装好了。F103 上 GPIO 全在 APB2不存在 APB1 的情况这一点比 F4 系列省心。4.2 GPIO 方向切换的两种写法与取舍单总线需要双向所以「什么时候是输出、什么时候是输入」有两种实现路线。第一种是一直保持开漏输出靠写 ODR 释放总线读的时候读 IDR。好处是不用反复调GPIO_Init时序连贯中断打断的概率低缺点是要求外部必须有上拉否则释放后总线是浮空的。第二种是发完起始信号后把引脚切成上拉输入读完再切回输出/* 切换到上拉输入准备接收器件数据 */ static void DHT11_PinAsInput(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin DHT11_GPIO_PIN; gpio.GPIO_Mode GPIO_Mode_IPU; /* 上拉输入 */ gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, gpio); } /* 切回开漏输出准备下一次拉低总线 */ static void DHT11_PinAsOutput(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin DHT11_GPIO_PIN; gpio.GPIO_Mode GPIO_Mode_Out_OD; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(DHT11_GPIO_PORT, gpio); }两种写法都能跑差别在容错。板上没有外部上拉时第二种靠内部上拉能勉强工作但内部上拉是几十千欧量级边沿很软读出来偶发校验错第一种没有外部上拉则直接读不到数据。我的选择是硬件上必焊 4.7k 上拉软件上用第一种代码更短时序抖动更小。只有在改不了硬件的场合才退回第二种。GPIO_Init每次调用都会重写 CRL/CRH 寄存器本身不慢但它会把整个端口的配置重算一遍如果同一端口上还有别的引脚在用参数必须写全不能只写GPIO_Pin就完事——标准库的行为是只改你指定的引脚位这一点倒是安全的。4.3 换主频、换编译器时的检查清单移植翻车的第二大来源是主频。72MHz 的工程挪到 8MHz 内部时钟的板子上delay_us里的 72 没改所有延时会被拉长 9 倍时序彻底崩掉现象是「一直返回无响应」。检查清单按顺序走确认SystemCoreClock的实际值别信注释。在main开头打印或打断点看一眼。把delay_us里的72U换成SystemCoreClock / 1000000U让延时自适应主频。确认SysTick没有被 RTOS 或别的模块占用。占用了就换 DWT 或独立定时器。确认优化等级。-O2下如果延时函数被内联或优化掉时序会偏给延时函数加__attribute__((noinline))或volatile修饰关键变量。确认stm32f10x_conf.h里 GPIO、RCC、misc 三个头文件是打开的。第 4 条尤其值得说。有些工程为了让延时更准会把delay_us写成static inline结果编译器把整个循环优化成了常量计算实际延时变成 0。判断方法很简单把逻辑分析仪挂在 DATA 线上看起始信号的低电平宽度是不是 20ms 左右差一个数量级就说明延时没生效。换成 GCC 工具链时还有个小坑BitAction这个枚举在标准库里定义在stm32f10x_gpio.h某些裁剪过的库版本里会被条件编译掉报未定义。这时把DHT11_WaitLevel的参数类型改成uint8_t即可逻辑不变。5. 避坑与排查五个真实踩过的坑这一章按「现象 → 原因 → 解决」写都是这套驱动在实际项目里出过的问题。看完能省掉不少对着示波器发呆的时间。5.1 上电第一次读永远失败之后恢复正常现象冷启动后第一次调用DHT11_Read返回校验错或无响应复位一次之后又好了或者连续读几次就正常了。原因DHT11 上电后内部需要一段时间稳定手册要求不小于 1s而且第一次转换的数据本身不可靠。很多例程把DHT11_Init里的延时省掉或者只给 100ms就正好踩在这个窗口里。解决初始化里保留 1s 延时应用层再加一层「首帧丢弃」逻辑上电后第一次读取的结果直接扔掉从第二次开始采用。这个习惯我后来在所有单总线器件上都保留了包括 DS18B20。5.2 连续读取时好时坏间隔越短失败率越高现象单次读取正常放进 100ms 周期的定时任务里就频繁校验失败把周期改成 2s 又正常了。原因DHT11 的采样周期不能低于 1s实测低于 1.5s 就开始不稳定。它不是「读完就能立刻再读」的器件内部有一次完整的温湿度转换过程转换期间总线上的响应会异常。解决把读取周期固定在 2s 以上。如果业务上需要更快的数据刷新率别在这颗器件上死磕换 DHT22 或者 I2C 接口的传感器。用定时器节拍驱动读取不要放在主循环里裸跑容易因为别的地方耗时变化导致间隔抖动。5.3 校验和总是差一点点或者数据全 0现象能收到响应40 位也读完了但校验和对不上或者读出来湿度温度都是 0。原因最常见的是采样点偏了。delay_us(40)里的 40 是基于 72MHz 标定的如果主频改了没同步改延时采样点整体偏移0 和 1 判断错误校验和自然不对。其次是上拉电阻缺失或阻值过大边沿太软导致高电平宽度测量偏差。解决先用逻辑分析仪看一位数据的高电平宽度0 应该在 26~28us1 应该在 70us 左右偏差超过 20% 就调延时系数。没有分析仪就用串口把 5 个字节原样打印出来看是不是规律性地全 0 或全 0xFF前者多半是采样点太晚后者多半是太早。5.4 换了引脚就完全读不到代码看着没问题现象从 PA1 改到 PB5编译通过运行后一直返回无响应。原因三个宏只改了两个DHT11_GPIO_CLK还留着RCC_APB2Periph_GPIOAGPIOB 的时钟没开所有配置写不进去。标准库对未开时钟的 GPIO 写操作不报错属于静默失败。解决把三个宏写在一起改的时候整块替换。更稳妥的做法是把端口和时钟绑成一个宏用#define DHT11_GPIO_CLK RCC_APB2Periph_GPIO##PORT这种拼接虽然可读性差一点但不会漏。我个人还是倾向手写三行加一句注释「三个一起改」。5.5 加了串口打印之后就再也不成功现象单独跑驱动正常一旦在读取过程中调用printf打印调试信息读取就失败。原因printf重定向到串口是阻塞式的一个字符串打出去可能几百微秒到几毫秒直接打断了 40us 级别的位采样。同理任何中断服务程序如果在读取期间执行时间偏长也会造成同样的后果。解决读取函数执行期间不要打印把结果存到全局变量读完再统一输出。如果系统里有高优先级中断比如串口接收、定时器在DHT11_StartAndWait到读完 40 位这段临界区里关中断读完立刻开。这段临界区总时长约 5ms对多数应用可以接受如果中断实时性要求极高就改用状态机加定时器捕获的方式解析别用阻塞式延时。6. 二次开发把驱动接进自己的采集框架驱动跑通只是起点真正要用起来一般会做三件事统一接口、加滤波、加调度。我习惯先定义一个传感器抽象层把 DHT11 挂上去这样以后换型号只改一个文件。/* sensor.h统一接口返回值 0 表示成功 */ typedef struct { const char *name; uint8_t (*init)(void); uint8_t (*read)(float *temp, float *humi); } sensor_t; /* dht11_adapter.c把整型驱动包成浮点接口 */ static uint8_t dht11_init_adapter(void) { DHT11_Init(); return 0; } static uint8_t dht11_read_adapter(float *temp, float *humi) { uint8_t t, h; if (DHT11_Read(t, h) ! DHT11_OK) return 1; *temp (float)t; *humi (float)h; return 0; } const sensor_t sensor_dht11 { DHT11, dht11_init_adapter, dht11_read_adapter };这层适配看起来是多写了一层但收益在后面应用层只认sensor_t采集任务不用关心底层是单总线还是 I2C。第二件事是滤波。DHT11 的整数精度本身跳变就大直接显示会让人觉得「这传感器不准」。我一般做 5 点滑动平均再对温度做一阶低通/* 5 点滑动平均buf 由调用方维护idx 指向最旧数据 */ float moving_average(float *buf, uint8_t *idx, float new_val) { float sum 0.0f; uint8_t i; buf[*idx] new_val; *idx (uint8_t)((*idx 1U) % 5U); for (i 0U; i 5U; i) sum buf[i]; return sum / 5.0f; }滑动平均的窗口别开太大DHT11 两秒才出一个数开 10 点意味着 20 秒的滞后温度变化就跟不上了。5 点是经验值。第三件事是调度。用 SysTick 或者一个基本定时器产生 1ms 节拍在节拍里做计数每到 2000ms 触发一次读取读到的数据放进全局结构体主循环只管消费。这样读取的时序不受主循环里其他逻辑的影响稳定性会明显好一截。验证方法我通常用两步先把原始 5 字节通过串口按十六进制打出来和校验和一起看确认帧结构没问题再拿一个独立的温湿度计放在旁边做对比室温下两者差值在 2℃、5%RH 以内就算正常。如果差值大先别怀疑驱动检查传感器是不是被板子上的发热元件烤到了DHT11 贴着稳压芯片装温度读数能高出五六度。这套代码我前后在不同板子上移植过几次最省事的做法始终是新建工程时先把dht11.c和dht11.h原样拷进去改三个宏把延时函数的主频常量跟工程对齐然后拿逻辑分析仪看一眼起始信号的低电平宽度对了再往下走。从那以后我每次移植外设驱动都强制先量一个时序不再靠「编译过了应该没问题」这种侥幸希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。