STM32+NRF24L01+DHT11无线温湿度采集系统实战解析 做过几年嵌入式开发的人大概率都绕不开两块东西一块是 STM32另一块就是 2.4G 无线模块。我之前用 STM32F103VE 配合 NRF24L01 和 DHT11 做过一套无线温湿度采集系统说实话这套组合在工业和 HVAC 场景里非常典型比如机房温湿度监控、实验室环境记录、冷冻仓库货架温度巡检甚至一些老式空调项目的改造预留都能看到类似方案。整套系统看着简单——DHT11 采温湿度STM32 通过 SPI 把数据喂给 NRF24L01接收端再把数据还原出来——但真跑起来丢包、误码、湿度偶尔读出来是 0%、从机莫名复位问题一个接一个。这篇想把完整的工程经验做一次整理从器件选型、电路连接、驱动编写、无线链路调试到最后的常见问题排查尽量写得实在一些。如果你正打算用 STM32F103VE 做低成本无线温湿度采集或者纯粹想入门 2.4GHz 无线通信的实战项目这套方案值得参考。1. 方案解析STM32F103VE、NRF24L01、DHT11 为什么能搭在一起1.1 三个核心器件的分工与选型理由STM32F103VE 属于增强型系列Cortex-M3 内核512KB Flash、64KB SRAM资源对这个项目来说其实很富余。你可能会问既然资源这么足为什么还要选它我的看法是这套方案的核心价值不在“算”而在“管”——管 SPI 时序、管单总线时序、管无线重发逻辑。F103VE 的 SPI 主模式最高能跑到 18MbpsGPIO 翻转速度也够快用来模拟 DHT11 的单总线读取完全没有压力。更重要的是它 3.3V 供电和 NRF24L01、DHT11 在同一条电源轨上不用做电平转换简化了电路设计。NRF24L01 是 Nordic 的 2.4GHz 收发芯片市面上最常见的模块是带 PALNA 的版本和裸片版。这个项目里用裸片版就够了它的特点是支持 250Kbps、1Mbps、2Mbps 三档速率6 路数据管道内置自动应答和自动重发通信距离在空旷环境下 30 米左右没问题。工业 HVAC 场景中只要不是穿过好几层钢筋混凝土这个距离基本够用。DHT11 则是典型的单总线数字温湿度传感器精度±2°C、±5%RH放在对精度要求不高的监控场合很合适而且价格低、引脚少焊起来也省事。有人可能会问为什么不用 DHT22 或者 SHT30如果预算允许SHT30 当然更好I2C 接口、精度高、抗干扰强但它的单价是 DHT11 的好几倍。DHT11 的定位就是“够用且便宜”在很多工业现场改造项目里老板要的是能看出趋势、能报警不是做计量级测量。所以选型这个东西本质上是个 trade-off别盲目追高。器件接口精度供电典型成本适合场景DHT11单总线±2°C / ±5%RH3.3V~5.5V很低环境趋势监测、低成本采集DHT22单总线±0.5°C / ±2%RH3.3V~5.5V中等对湿度精度有一定要求SHT30I2C±0.3°C / ±2%RH3.3V~5.5V较高计量级、长期稳定性要求高1.2 系统拓扑与数据流设计整套系统的数据流并不复杂发送端 STM32F103VE 定时读取 DHT11得到 40 位温湿度数据经过简单校验后封装成协议帧通过 SPI 写入 NRF24L01 的 TX FIFO然后拉高 CE 引脚触发发送。NRF24L01 发送后等待接收端自动应答如果没收到 ACK模块内部按预设的重发次数和间隔自动重传。接收端 NRF24L01 工作在 RX 模式收到数据后通过 SPI 把 Payload 读出来再经串口打印或者驱动 LCD 显示。这里面有个设计点容易忽略发送端和接收端的角色不是固定死的。NRF24L01 支持收发模式切换所以这套系统完全可以让一块板子既做采集又做中继或者主机轮询多个从机。工业 HVAC 场景里我见过不少设计是把接收端做成网关转发到 RS485 总线或者以太网但底层无线采集节点还是这套方案。所以我建议一开始写驱动时就别把收发逻辑焊死留出模式切换的接口后面扩展会省很多事。2. 硬件搭接细节电源、SPI、单总线每一根线都有讲究2.1 引脚分配与连接关系STM32F103VE 的 SPI1 可以挂在 PA5(SCK)、PA6(MISO)、PA7(MOSI)片选用 PA4CE 我习惯用 PA3。DHT11 的数据线放在 PB0串口调试用 USART1。这样分配的思路是SPI1 和 DHT11 数据线分属不同端口改线时互不干扰调试时可以单独测 SPI 回环也可以单独测 DHT11 波形问题定位快。信号STM32F103VE 引脚外设引脚SPI1_SCKPA5NRF24L01 SCKSPI1_MISOPA6NRF24L01 MISOSPI1_MOSIPA7NRF24L01 MOSISPI1_CSNPA4NRF24L01 CSNCEPA3NRF24L01 CEDHT11_DATAPB0DHT11 DATAUSART1_TX/RXPA9/PA10调试串口NRF24L01 的 IRQ 引脚我建议接一个空闲 GPIO不要悬空。虽然可以用查询方式读状态寄存器来判断发送是否完成但遇到自动重发正在频繁进行时查询会占用大量 CPU 时间用外部中断或者轮询 IRQ 的方式更高效。IRQ 可以放到 PB1配置成下降沿触发。这里的下降沿是 NRF24L01 在事件发生时主动拉低的和很多模块的高电平触发不一样我第一次用的时候反了结果中断风暴直接把系统卡死后面会细说。2.2 电源完整性是无线模块的命门NRF24L01 的电源质量直接影响发射功率和接收灵敏度这一点很多人栽过跟头。模块正常工作电流在发射模式下可能到 13mA 左右但内部 PA 启动瞬间会有比较陡的电流变化。如果直接用 STM32 板载的 3.3V LDO 供电且 LDO 输入输出压差不够或者滤波电容不足很容易出现发射瞬间电压跌落导致无线模块复位或者数据错乱。我的做法是在 NRF24L01 的 VCC 引脚附近就近放一个 10uF 的钽电容和一个 0.1uF 的陶瓷电容两者并联。钽电容负责瞬态电流陶瓷电容负责高频去耦。如果你用的是带 PA LNA 的 NRF24L01 模块电流需求会更大建议单独用一颗 AMS1117-3.3 从 5V 取电不要和 MCU 共用 LDO。这个细节直接影响通信稳定性尤其当传输距离拉远后发射功率增大掉压问题会成倍放大。DHT11 的供电是 3.3V 到 5.5V 都能工作但注意DHT11 的数据线输出高电平是跟着 VCC 走的也就是说如果你用 5V 给 DHT11 供电那它数据线的高电平就是 5VSTM32F103VE 的 GPIO 是 5V 容忍但 NRF24L01 不是。为了避免电平打架我建议整套系统统一用 3.3V 供电DHT11 也接 3.3V省去电平转换的麻烦。2.3 DHT11 上拉电阻与布线注意事项DHT11 是单总线协议数据线在空闲时被上拉到高电平。STM32 的 GPIO 内部有上拉但内部上拉太弱加上导线本身的寄生电容会导致上升沿变缓严重时主机读到的“1”电平不够高出现误码。我实测下来DHT11 数据线上最好外接一个 4.7kΩ 或者 10kΩ 的上拉电阻到 3.3V。如果你的 DHT11 模块已经带了上拉电阻那就不用再加但要确认一下阻值有些模块用的是 1kΩ也问题不大只是稍微费一点电。布线方面DHT11 的线越短越好。我之前做过一次实验把 DHT11 用 30 厘米的杜邦线接到板子上读取成功率急剧下降波形上能看到上冲和下冲。原因很简单DHT11 的时序是微秒级的线太长加上分布电容和电感会直接把边沿搞坏。工业现场如果传感器必须拉远我建议用带屏蔽的双绞线屏蔽层单端接地或者干脆改用 I2C 接口的 SHT30抗干扰能力完全不是一个级别。3. 软件驱动SPI 配置、单总线时序、无线重发一个都不能少3.1 基于标准库的 SPI 初始化和 NRF24L01 寄存器配置STM32F103VE 的 SPI1 初始化我用的是标准外设库时钟源选 PCLK2 的二分频配置成主模式、8 位数据、CPOL0、CPHA0。NRF24L01 对 SPI 时序要求不挑剔4MHz 以内的 SCK 都能稳定跑我习惯把 SPI 分频配成 4 分频也就是 9MHz留一点裕量。要注意的一点是NRF24L01 的 CSN 片选信号在每次 SPI 事务之前必须拉低事务结束后拉高且 CSN 低电平期间 SCK 的跳变不能有毛刺否则很容易把模块的状态机搞乱。初始化 NRF24L01 的寄存器顺序有讲究。我的经验是先关 CE让模块进入待机模式然后按顺序设置 EN_AA、EN_RXADDR、SETUP_AW、SETUP_RETR、RF_CH、RF_SETUP接着写发送地址和接收地址再写 RX_PW_P0 设置接收载荷宽度最后清空 FIFO 并重新拉高 CE。这里有个容易踩的坑EN_AA 和 EN_RXADDR 如果不匹配自动应答功能会失效。比如你只使能了数据管道 0但 EN_AA 的对应位写错了模块收发数据时不会产生 ACK发送端会一直重发直到超时。void NRF24L01_Init(void) { uint8_t buf[5]; NRF24L01_CE_LOW(); // 写配置寄存器屏蔽中断开启 CRC使能收发器 NRF24L01_WriteReg(REG_CONFIG, 0x0E); // 使能自动应答只针对数据管道0 NRF24L01_WriteReg(REG_EN_AA, 0x01); // 使能接收地址只使能数据管道0 NRF24L01_WriteReg(REG_EN_RXADDR, 0x01); // 设置地址宽度为5字节 NRF24L01_WriteReg(REG_SETUP_AW, 0x03); // 自动重发延时1500us重发10次 NRF24L01_WriteReg(REG_SETUP_RETR, 0x1A); // 选择射频通道 40240040 2440MHz NRF24L01_WriteReg(REG_RF_CH, 40); // 发射速率2Mbps发射功率0dBm NRF24L01_WriteReg(REG_RF_SETUP, 0x0E); // 发送地址 buf[0] 0xE7; buf[1] 0xE7; buf[2] 0xE7; buf[3] 0xE7; buf[4] 0xE7; NRF24L01_WriteReg(REG_TX_ADDR, buf, 5); // 接收地址与发送地址一致 NRF24L01_WriteReg(REG_RX_ADDR_P0, buf, 5); // 数据管道0有效数据宽度为8字节温湿度帧 NRF24L01_WriteReg(REG_RX_PW_P0, 8); // 清空发送和接收FIFO NRF24L01_FlushTx(); NRF24L01_FlushRx(); NRF24L01_CE_HIGH(); }上面这几个参数不是随便写的。RF_CH 用了 40对应 2440MHz这个频点在实际测试中干扰相对较少。如果你所在的现场有其他 2.4G 设备比如 Wi-Fi 或者无线键鼠最好换个信道。RF_SETUP 里的 2Mbps 速率是我有意选择的速率越高单位比特在空中停留时间越短抗干扰能力反而越好代价是接收灵敏度会低 2-3dBm。室内场景优先选 2Mbps室外空旷场景可以降成 1Mbps 换更远距离。3.2 DHT11 单总线读取时序的实现要点DHT11 的通信协议是单总线主机先拉低数据线至少 18ms然后释放并延时等待传感器响应。传感器响应后拉低 80us 再拉高 80us随后输出 40 位数据每位数据以 50us 低电平开始之后的高电平持续 26-28us 表示“0”持续 70us 左右表示“1”。读取的关键在时延精度STM32F103VE 主频 72MHz使用循环延时或者 SysTick 延时都可以但注意别用带操作系统的延时函数DHT11 的时序容差只允许几十微秒的抖动。我写 DHT11 驱动时更倾向于用 GPIO 读取和简单的循环延时而不是依赖定时器捕获。原因在于单总线时序中高低电平的翻转很快定时器捕获虽然准确但代码复杂中断频繁反而可能干扰其他逻辑。简单的方式是先用一个函数读出当前电平然后短暂延时再读下一个状态。实测下来只要延时函数本身校准过并且没有跑操作系统读出 40 位的成功率可以做到 95% 以上。uint8_t DHT11_ReadByte(void) { uint8_t i, data 0; for (i 0; i 8; i) { while (DHT11_DATA_IN() 0); // 等待50us低电平结束 DelayUs(30); if (DHT11_DATA_IN() 1) { data | (1 (7 - i)); while (DHT11_DATA_IN() 1); // 等待高电平结束 } } return data; }这段代码里有几个细节值得说。第一个细节是读取函数里的 while 等待虽然能保证时序对齐但如果传感器故障或者掉线while 循环会死等程序直接卡死。所以我在实际项目里加了超时判断用 SysTick 计数超过 100us 就退出并返回错误标志。第二个细节是每个字节先读高位DHT11 输出的数据格式是湿度整数、湿度小数、温度整数、温度小数、校验和其中小数部分在 DHT11 上固定为 0但你还是得按协议读完因为校验和是包含这 4 个字节的。第三个细节是读取完成后要检查校验和即前四个字节相加的低 8 位是否等于第五个字节不相等就丢弃本次数据留到下一轮再采。uint8_t DHT11_ReadData(int16_t *temp, uint8_t *humidity) { uint8_t buf[5]; uint8_t i; uint16_t sum; DHT11_DATA_OUT(); DHT11_DATA_LOW(); DelayMs(20); // 起始信号大于18ms DHT11_DATA_IN(); DelayUs(40); // 释放总线后等待响应 if (DHT11_DATA_IN() 0) { while (DHT11_DATA_IN() 0); // 等待80us低电平结束 while (DHT11_DATA_IN() 1); // 等待80us高电平结束 for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } sum buf[0] buf[1] buf[2] buf[3]; if ((sum 0xFF) ! buf[4]) { return 0; // 校验失败 } *humidity buf[0]; *temp buf[2]; return 1; } return 0; }这里还有个容易被忽略的点DHT11 的读数不是持续更新的。手册上写的典型采样周期是 1-2 秒如果你采集间隔小于 1 秒读出来的数据大概率是上一次缓存的值。所以我会在主循环里用 2 秒的软件定时器去触发读取既省电又保证数据有效性。3.3 无线数据包的封装与自动重发机制数据在空中不是裸传的我定义了一个 8 字节的协议帧结构是帧头 0xAA、节点 ID、温度高字节、温度低字节、湿度字节、状态字节、校验和、帧尾 0x55。温度用 int16 表示带符号位这样负温度也能表达。状态字节记录发送端自检状态比如 DHT11 是否读失败、电池电压是否低。发送端在每次发送前先清空 RX FIFO再写 TX FIFO拉高 CE 至少 10us 触发发送。发送完成后读取 STATUS 寄存器如果 TX_DS 位置位说明收到 ACK发送成功如果 MAX_RT 位置位说明自动重发超过了设定次数需要做异常处理。这里我强烈建议每次发送后都手动清一下 STATUS 寄存器否则中断标志会一直挂在那里干扰下一次发送。uint8_t NRF24L01_SendPacket(uint8_t *data, uint8_t len) { uint8_t status; NRF24L01_CE_LOW(); NRF24L01_FlushTx(); NRF24L01_WritePayload(data, len); NRF24L01_CE_HIGH(); DelayUs(15); NRF24L01_CE_LOW(); while (NRF24L01_IRQ_PIN() ! 0); // 等待IRQ拉低 status NRF24L01_ReadReg(REG_STATUS); NRF24L01_WriteReg(REG_STATUS, status); // 清状态标志 if (status (1 4)) // MAX_RT { NRF24L01_FlushTx(); return 2; // 重发超时 } if (status (1 5)) // TX_DS { return 1; // 发送成功 } return 0; }这个函数里用了一个 while 等 IRQ 引脚拉低同样有死等风险。实际使用中我给这个循环加了一个超时计数超过 50ms 就退出并返回超时避免无线模块异常时把系统挂死。重发次数和重发延时这两个参数也值得讲一下SETUP_RETR 寄存器低 4 位是重发次数高 4 位是重发延时。延时设得越长总耗时越高但两个模块之间冲突的概率也越小。我的配置是 1500us 延时、重发 10 次在大多数室内环境下足够可靠。接收端的处理相对简单NRF24L01 工作在 RX 模式CE 拉高后持续监听。收到数据后 RX_DR 中断标志置位读取 RX FIFO 即可。如果是多节点轮询接收端可以通过数据帧里的节点 ID 字段区分数据来源把不同节点的温湿度分别缓存。4. 联调实录丢包、乱码、DHT11 读不到我是怎么一步步排查的4.1 无线丢包与 ACK 超时的处理第一次联调时我遇到的现象是发送端每隔几秒报一次 MAX_RT 超时接收端收到的数据有明显缺口。排查过程花了整整一个下午最后发现原因有两个。第一个原因是发送地址和接收地址不匹配。NRF24L01 的地址是 5 字节我收发两端写地址时一个高位在前、一个低位在前虽然看起来写的内容一样但实际在空中接收时地址完全不同。这个问题用逻辑分析仪看 SPI 波形才能发现普通串口打印根本看不出来。第二个原因是接收端的 Rx 模式还没真正建立发送端就开始发数据了。NRF24L01 从 TX 模式切到 RX 模式需要一点时间如果切换太频繁模块的状态机可能来不及稳定。解决方法是把模式切换函数做得干净利落先 CE 拉低等待 SPI 写完成再写 CONFIG 寄存器切换 PWR_UP 和 PRIM_RX 位最后 CE 拉高整个过程不要插入其他 SPI 操作。4.2 DHT11 读取失败起始信号、上拉电阻、干扰源DHT11 读取失败的问题更普遍。我遇到过三种典型情况第一种是起始信号时间不够我最初用 DelayMs(18)但因为代码里其他中断干扰实际延时可能被压缩到 10ms 左右传感器不响应。后来我把起始信号延长到 25ms同时关掉了不必要的定时器中断读取成功率立刻上来了。第二种是数据线上的上拉电阻太小模块上没有配电阻我用 1kΩ 上拉后波形上的高电平被拉得不够高STM32 读到错误电平。换回 4.7kΩ 就好了。第三种是来自 2.4G 模块的干扰NRF24L01 的天线离 DHT11 太近发射瞬间的高频能量耦合到数据线上导致 DHT11 读取失败。解决办法是把天线朝向另一侧或者在 DHT11 数据线上串联一个几百欧的电阻来抑制干扰。4.3 通信距离不足与天线布局的实测工业现场对无线距离的要求往往比实验室苛刻。我实测过裸片 NRF24L01 在 2Mbps 速率下室内穿一堵墙的距离大概是 10-15 米室外空旷能到 30 米左右。如果你需要更远距离可以考虑带 PA 的模块或者降低速率到 250Kbps。但要注意带 PA 的模块对电源要求更高而且辐射功率大在某些应用场景里可能会有合规问题选型时得想清楚。天线布局上我建议 NRF24L01 的天线区域一般是 PCB 板边那个蛇形走线区域不能有覆铜也不能被塑料外壳完全包死。有些外壳开模避开了天线位置但把天线贴在金属板上通信距离直接减半这种坑最好在设计阶段就规避。5. 常见问题速查与几个值得长期保留的优化习惯5.1 问题排查速查表现象可能原因排查思路与解决办法发送端一直 MAX_RT收发地址不匹配、信道不一致、接收端未进入 RX检查地址字节序、RF_CH 是否一致用示波器看 CE 和 IRQ 波形接收端收不到数据但有 ACK接收地址错误或 RX_PW_P0 设置不对核对 RX_ADDR_P0 与发送端 TX_ADDR确认载荷宽度DHT11 读取超时起始信号太短、上拉电阻不对、传感器损坏延长起始信号到 25ms检查上拉阻值最直接的办法是换一颗传感器湿度读到 0% 或温度乱跳校验和不对、线太长、干扰大加校验失败重读机制缩短单总线线长尝试串电阻通信距离明显变短天线被遮挡、电源电压跌落、速率太高检查天线区域用独立 LDO 供电降低速率至 1Mbps 或 250Kbps模块发热且不休眠CE 引脚悬空或电平不对确保 CE 有确定的 GPIO 控制待机时 CE 拉低5.2 我个人的三条经验这套系统跑通之后我留了几个习惯虽然不起眼但相当管用。第一个习惯是在每一包数据里都带一个自增序列号。接收端拿到序列号后就能计算丢包率这个指标在判断无线链路健康度时比单纯的 RSSI 更直观。第二个习惯是定时重启 NRF24L01因为模块长时间运行后偶尔会进入无法收发数据的状态简单粗暴地在定时器里检测通信失败次数超过阈值就重新初始化模块代价是几毫秒的复位时间换来的是系统长期稳定。第三个习惯是接收端把数据同时打到串口和本地 Flash这样即使上位机断开数据也不会丢。最后再分享一个小技巧联调时先不要同时上 DHT11 和 NRF24L01先把 STM32 的 SPI 用回环测试跑通——MISO 和 MOSI 短接发一个字节看能不能收到。SPI 没问题之后再接 NRF24L01这时如果有问题问题一定出在无线配置而不是 SPI 时序。同样的逻辑DHT11 可以先单独读把数据打印到串口确认读数稳定后再把两个模块整合到一起。这样做的好处是每次只引入一个新变量遇到问题五分钟就能定位。这套项目如果只是搭个 Demo可能一天就能跑通但要做到长期稳定运行建议你还是按这个思路把每个环节都验证一遍。