基于STM32与NRF24L01的无线温度采集系统设计与实现
发布时间:2026/11/3 9:00:17 锦皓数字建站

1. 项目缘起与整体设计思路无线温度计这个东西听起来好像没什么技术含量无非就是测个温度然后无线发出去。但真动手做起来你会发现里面藏着不少门道。我前后做过三四个版本的无线温度采集方案从最早的51单片机加NRF24L01到后来的STM32F103C8T6配DS18B20和OLED踩过的坑比想象中多得多。这个S003D方案的核心思路很明确用STM32做为主控DS18B20负责温度采集NRF24L01完成无线数据传输OLED做本地显示再加一颗FLASH芯片做数据存储。五个模块各司其职组合起来就是一个完整的无线温度监测系统。为什么选STM32而不是其他MCU这个问题我被问过很多次。STM32F103C8T6这颗芯片72MHz主频、64KB Flash、20KB RAM对于温度采集这种任务来说性能绰绰有余。更重要的是它的生态好HAL库把底层寄存器操作封装得很完善你不用花大量时间去啃参考手册就能把外设跑起来。而且这颗芯片价格便宜淘宝上几块钱就能买到对于做毕业设计或者个人项目来说成本极低。相比之下如果你用ESP32或者树莓派Pico虽然性能更强但要么功耗偏高要么开发环境配置更复杂对于只需要采集温度并无线传输的场景来说属于杀鸡用牛刀。DS18B20这个温度传感器也是经典选择了。它是单总线数字温度传感器测温范围-55°C到125°C精度在-10°C到85°C范围内可以达到±0.5°C。最关键的是它直接输出数字信号不需要额外的ADC转换电路一根数据线就能完成通信。很多人第一次用DS18B20会觉得时序很麻烦确实单总线协议的读写时序对时间要求比较严格但一旦调通了稳定性非常好。我实测下来同一根总线上挂三四个DS18B20都没问题每个器件有唯一的64位ROM地址可以分别读取。NRF24L01是2.4GHz频段的无线收发芯片SPI接口最高空中速率2Mbps。它的优势在于功耗低、体积小、成本低一对模块也就十几块钱。通信距离在空旷环境下大约30到50米室内穿墙的话大概10到20米具体取决于墙体材质和周围2.4GHz频段的干扰情况。OLED我选的是0.96寸I2C接口的SSD1306分辨率128x64显示温度数值和状态信息足够了。FLASH用的是W25Q648MB容量SPI接口用来存储历史温度数据方便后续做趋势分析。整个系统的数据流是这样的STM32通过单总线协议读取DS18B20的温度值经过处理后一方面送到OLED显示另一方面通过NRF24L01发送出去同时按照一定的时间间隔写入FLASH存储。接收端可以用另一块STM32加NRF24L01接收数据也可以直接连电脑通过串口查看。这个架构的好处是模块化程度高每个部分都可以独立调试出了问题也容易定位。2. 核心硬件选型与电路设计要点2.1 主控芯片的引脚分配与最小系统STM32F103C8T6是LQFP48封装48个引脚看着多但真正能用的GPIO也就三十来个。做这个项目的时候引脚分配需要提前规划好不然后面改起来很麻烦。我一般会先把所有外设列出来然后按照功能分组分配引脚。DS18B20用PA0配置为开漏输出加外部上拉电阻。这里有个细节要注意DS18B20的单总线需要4.7kΩ的上拉电阻我试过用10kΩ的短距离通信没问题但线拉长到一两米以上就容易出现数据错误。NRF24L01用SPI1接口SCK接PA5MISO接PA6MOSI接PA7CSN接PA4CE接PB0IRQ接PB1。OLED用I2C1SCL接PB6SDA接PB7。W25Q64也用SPI可以跟NRF24L01共用SPI1总线通过不同的CS引脚来区分W25Q64的CS接PA3。最小系统部分8MHz晶振接PD0和PD1复位电路用10kΩ上拉电阻加100nF电容BOOT0通过10kΩ电阻接地BOOT1也接地。电源部分用AMS1117-3.3把5V转成3.3V输入输出各加一个10uF和一个100nF的滤波电容。这些看起来是基础操作但我见过不少人因为电源滤波没做好导致NRF24L01通信不稳定的情况。注意STM32F103C8T6的PA13、PA14、PA15、PB3、PB4这几个引脚默认是JTAG调试接口如果要用作普通GPIO需要先关闭JTAG功能。我一般用SWD调试只占用PA13和PA14剩下的PA15、PB3、PB4可以通过配置AFIO_MAPR寄存器来释放。2.2 DS18B20的接线方式与抗干扰处理DS18B20有三种封装TO-92直插、防水探头和贴片封装。做温度计的话推荐用防水探头版本测量液体温度或者户外温度都方便。接线就三根VCC接3.3VGND接地DQ接PA0。如果是 parasite power 模式VCC可以不接直接从数据线取电但这种模式下转换期间总线必须保持高电平时序控制更复杂不建议新手使用。抗干扰方面如果传感器线缆超过一米建议在DQ和GND之间并联一个100nF的电容同时在VCC和GND之间也加一个0.1uF的退耦电容。我做过一个鱼缸温度监测的项目传感器线拉了大概三米一开始数据偶尔会跳变加了电容之后稳定了很多。另外如果总线上挂多个DS18B20上拉电阻可以适当减小到2.2kΩ增强驱动能力。2.3 NRF24L01的电源处理与天线布局NRF24L01对电源噪声非常敏感这是很多人调试时容易忽略的点。模块的工作电压是1.9V到3.6V但瞬间发射电流可以达到十几毫安如果电源纹波大通信距离会明显缩短甚至完全无法通信。我的做法是在NRF24L01的VCC和GND引脚旁边直接焊一个10uF的钽电容和一个100nF的陶瓷电容越近越好。天线部分PCB板载天线的效果一般如果对距离有要求建议选带SMA接口外接天线的版本。模块的摆放方向也会影响通信效果PCB天线最好平行于地面两个模块的天线方向一致时信号最强。我实测过同样一对模块天线平行放置比垂直放置的距离能差一倍以上。2.4 OLED与FLASH的接口选择OLED用I2C接口只需要两根线接线简单但刷新率受限于I2C的400kHz速率。如果你需要显示动态波形或者频繁刷新SPI接口的OLED会更合适速率可以到10MHz以上。不过对于温度显示这种应用I2C完全够用一秒钟刷新两次都绰绰有余。W25Q64用SPI接口最高支持80MHz时钟STM32的SPI1可以跑到36MHz72MHz的二分频。实际使用中我一般设置成18MHz或者9MHz速度足够稳定性也更好。FLASH的读写要注意页编程和扇区擦除的区别写入数据前必须先擦除对应的扇区4KB擦除后整个扇区变成0xFF然后才能按页256字节写入。这个特性跟EEPROM不同EEPROM可以按字节擦写FLASH不行。3. 软件架构与关键驱动实现3.1 DS18B20单总线时序的精确控制DS18B20的通信协议包括复位脉冲、存在脉冲、写时隙、读时隙四种基本操作。复位脉冲是主机拉低总线至少480微秒然后释放如果DS18B20在线它会在15到60微秒后拉低总线60到240微秒作为应答。写时隙分写0和写1写0是拉低总线60到120微秒然后释放写1是拉低总线1到15微秒然后释放。读时隙是主机拉低总线1到15微秒然后在15微秒内采样总线电平。这些时间参数看起来复杂但用STM32的微秒级延时函数实现并不难。关键是要保证延时的准确性如果系统时钟配置不对延时函数就会偏差很大。我一般用SysTick定时器做微秒延时配置成1微秒中断一次然后在中断里递减计数器。也可以用DWTData Watchpoint and Trace单元的CYCCNT寄存器做更精确的延时不过对于DS18B20来说微秒级精度已经足够了。// DS18B20复位函数 uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_DQ_OUT(); // 配置为输出 DS18B20_DQ_LOW(); // 拉低总线 delay_us(480); // 保持至少480us DS18B20_DQ_HIGH(); // 释放总线 delay_us(30); // 等待15-60us DS18B20_DQ_IN(); // 配置为输入 presence DS18B20_DQ_READ(); // 读取存在脉冲 delay_us(420); // 等待时序结束 return presence; // 0表示存在1表示不存在 }温度转换命令是0x44读取暂存器命令是0xBE。发送0x44后DS18B20开始转换12位精度下需要750毫秒。转换期间总线可以释放也可以保持高电平。转换完成后读取9个字节的暂存器数据前两个字节就是温度值低字节在前高字节在后。温度值的格式是低4位是小数部分高12位是整数部分补码形式。比如读到的值是0x0191二进制是0000 0001 1001 0001整数部分是0000 0001 1001 25小数部分是0001 1所以温度是25.0625°C。3.2 NRF24L01的SPI驱动与通信协议NRF24L01的寄存器操作通过SPI接口完成读写命令格式是读操作发送寄存器地址0x00到0x1F写操作发送0x20加上寄存器地址。比如要写CONFIG寄存器地址0x00发送0x20 | 0x00 0x20然后发送要写入的值。要读STATUS寄存器地址0x07发送0x07然后读取返回的数据。初始化流程一般是这样的先拉低CSN发送0x20 | 0x00写入0x0F使能CRC、16位CRC、上电、接收模式然后配置EN_AA自动应答、EN_RXADDR接收地址使能、SETUP_AW地址宽度、SETUP_RETR自动重传、RF_CH射频通道、RF_SETUP发射功率和速率、RX_ADDR_P0接收地址、TX_ADDR发送地址、RX_PW_P0接收数据宽度。这些配置看起来多但大部分用默认值就行主要改RF_CH和地址。发送数据时先拉低CE写入TX_ADDR和要发送的数据到TX FIFO然后拉高CE至少10微秒模块就会自动发送。发送完成后STATUS寄存器的TX_DS位会置1如果收到应答RX_DR位也会置1。如果超过重传次数还没收到应答MAX_RT位会置1。我一般会在发送后检查STATUS寄存器如果MAX_RT置1就清标志并重发。// NRF24L01发送数据 uint8_t NRF24L01_TxPacket(uint8_t *txbuf) { uint8_t state; NRF24L01_CE_LOW(); NRF24L01_Write_Buf(WR_TX_PLOAD, txbuf, TX_PLOAD_WIDTH); NRF24L01_CE_HIGH(); delay_us(15); while(NRF24L01_IRQ_READ()); // 等待发送完成 state NRF24L01_Read_Reg(STATUS); NRF24L01_Write_Reg(STATUS, state); // 清除标志 if(state MAX_RT) { NRF24L01_Write_Reg(FLUSH_TX, 0xFF); // 清空TX FIFO return MAX_RT; } if(state TX_DS) { return TX_DS; } return 0; }3.3 OLED显示驱动的移植与优化OLED驱动芯片是SSD1306I2C地址一般是0x78写或0x79读。初始化序列包括设置显示时钟分频、多路复用率、显示偏移、起始行、电荷泵、内存寻址模式、段重映射、扫描方向、对比度、预充电周期、VCOMH取消选择级别等。这些参数在SSD1306数据手册里都有推荐值直接照搬就行。显示汉字需要取模我一般用PCtoLCD2002这个软件设置成阴码、逐列式、顺向、十六进制格式。取出来的数组直接放到代码里通过OLED_Set_Pos设置起始位置然后逐个字节写入。显示温度值的话把整数部分和小数部分分开处理整数部分用sprintf转成字符串小数部分乘以10取整后显示一位小数。实操心得OLED的I2C通信速率不要设太高我试过400kHz有时候会花屏降到200kHz就稳定了。另外OLED初始化之前最好延时100毫秒以上等电源稳定后再发命令否则偶尔会出现不亮的情况。3.4 W25Q64的读写与数据管理W25Q64的容量是64Mbit也就是8MB分成128个块每块64KB每块又分成16个扇区每个扇区4KB。擦除的最小单位是扇区写入的最小单位是页256字节。读取没有限制可以任意地址读取。写数据之前必须先擦除。我一般把FLASH分成两部分前4MB用来存储历史温度数据后4MB留作扩展。每条记录包含时间戳4字节和温度值2字节一共6字节。按照每分钟记录一次计算4MB可以存大约70万条记录够用很长时间了。// W25Q64页写入 void W25Q64_PageWrite(uint32_t addr, uint8_t *buf, uint16_t len) { W25Q64_WriteEnable(); W25Q64_CS_LOW(); SPI1_ReadWriteByte(W25Q64_PAGE_PROGRAM); SPI1_ReadWriteByte((addr 16) 0xFF); SPI1_ReadWriteByte((addr 8) 0xFF); SPI1_ReadWriteByte(addr 0xFF); while(len--) { SPI1_ReadWriteByte(*buf); } W25Q64_CS_HIGH(); W25Q64_WaitBusy(); }4. 系统联调与常见问题排查4.1 无线通信不稳定的排查思路NRF24L01通信不稳定是最常见的问题表现包括接收端收不到数据、数据丢包严重、通信距离短。排查的时候按照以下顺序来先检查电源用示波器看VCC引脚上的纹波如果峰峰值超过100mV就要加电容或者换LDO。然后检查SPI通信读一下CONFIG寄存器看能不能正确读回写入的值。如果SPI都读不对那就是接线或者时序问题。接下来检查地址配置发送端和接收端的地址必须完全一致包括地址宽度。RF_CH也要一样如果周围有WiFi路由器建议避开常用的1、6、11信道选2、4、8这些。空中速率方面250kbps的距离最远但速率慢2Mbps速率快但距离近1Mbps是比较均衡的选择。我一般用1Mbps加0dBm发射功率室内能稳定穿两堵墙。还有一个容易被忽略的点是CE引脚的控制时序。发送模式下CE拉高至少10微秒才能触发发送如果拉高时间不够数据发不出去。接收模式下CE要保持高电平才能持续接收。我见过有人把CE接在PWM输出上结果通信时好时坏这就是CE时序不对导致的。4.2 DS18B20读取失败的几种情况DS18B20读不到数据首先用示波器看DQ线上的波形。正常的复位脉冲应该是主机拉低约500微秒然后释放DS18B20在约30微秒后拉低约120微秒作为应答。如果看不到应答脉冲可能是上拉电阻没接、传感器坏了、或者接线反了。如果复位有应答但读出来的温度一直是85°C这是DS18B20的上电默认值说明温度转换命令没有成功执行。检查一下发送0x44之后有没有等待足够的时间12位精度需要750毫秒如果只等了100毫秒就读取读到的就是默认值。另外如果总线上挂了多个传感器发送0x44之前要先发0xCC跳过ROM匹配否则只有第一个传感器会执行转换。温度值偶尔跳变到-127°C或者125°C一般是通信误码导致的。可以在读取后做校验CRC校验字节是暂存器的第9个字节如果CRC不对就丢弃这次数据重新读取。也可以连续读三次取中间值简单有效。4.3 OLED显示异常的快速定位OLED不亮或者花屏先检查I2C地址对不对。有些模块的地址是0x78有些是0x7A跟SA0引脚的电平有关。用I2C扫描程序扫一下总线看看能不能找到设备。如果找不到检查SCL和SDA有没有接反上拉电阻有没有接一般模块自带上拉但有些便宜的模块没有。花屏的话大概率是初始化序列不对或者通信速率太高。把I2C速率降到100kHz试试如果好了就是速率问题。如果还是花屏检查初始化命令里的多路复用率和显示时钟设置这两个参数跟屏幕的扫描方式有关设错了就会显示错乱。显示内容偏移或者只显示一半检查一下显示起始行和页地址范围。SSD1306的显存是128x64位分成8页每页8行。如果只初始化了前4页那就只显示上半屏。这个在初始化序列里设置多路复用率为0x3F64行就能解决。4.4 FLASH读写失败的典型原因W25Q64读不到ID或者读写数据不对先检查SPI模式。W25Q64支持模式0和模式3STM32的SPI要配置成对应的CPOL和CPHA。我一般用模式0CPOL0CPHA0也就是时钟空闲低电平第一个边沿采样。如果模式不对读出来的数据全是0xFF或者0x00。写入数据后读出来不对检查有没有先擦除。FLASH只能把1写成0不能把0写成1所以写入之前必须擦除擦除后所有位变成1。如果没擦除就直接写写进去的数据是原数据和新数据的按位与肯定不对。另外页写入不能跨页如果从地址250开始写10个字节会写到下一页去导致数据错乱。写之前算一下当前页还剩多少字节不够的话先写完当前页再从下一页继续。注意W25Q64的擦除和写入操作需要时间扇区擦除典型值45毫秒页编程典型值0.7毫秒。操作完成后要读状态寄存器判断BUSY位是否清零不能直接进行下一步操作否则会失败。4.5 常见问题速查表问题现象可能原因排查方法解决方案NRF24L01通信距离短电源纹波大示波器测VCC纹波加10uF钽电容100nF陶瓷电容NRF24L01收不到数据地址或信道不一致读寄存器对比配置统一地址和RF_CHDS18B20读数85°C转换未完成检查延时等待750ms后再读取DS18B20读数跳变通信误码检查CRC加CRC校验或多次读取取中值OLED不亮I2C地址错误I2C扫描确认地址0x78或0x7AOLED花屏通信速率过高降低I2C速率设为100kHz或200kHzFLASH读出全0xFFSPI模式错误检查CPOL/CPHA配置为模式0FLASH写入失败未擦除检查擦除操作写前先擦除扇区5. 低功耗优化与数据存储策略5.1 系统功耗分析与优化方向这个系统如果一直全速运行电流大概在30到50毫安用电池供电的话撑不了多久。如果要做成电池供电的便携版本低功耗优化是必须的。功耗大头在三个地方STM32本身、NRF24L01发射时的瞬时电流、OLED显示。STM32F103C8T6在72MHz全速运行时的电流大约30毫安如果降到8MHz电流可以降到10毫安左右。再进一步在两次采集之间进入Stop模式电流可以降到几十微安。Stop模式下所有的时钟都停了只有外部中断和RTC还能唤醒。我一般用RTC的闹钟功能每分钟唤醒一次采集完温度发送出去后继续睡。NRF24L01在发射时的电流大约12毫安但发射时间很短每次也就几百微秒。接收模式下电流约13毫安如果一直开着接收功耗就上去了。所以接收端也可以用轮询方式每隔一段时间唤醒一次发送端发送数据时连续发几次确保接收端能收到。OLED的功耗跟显示内容有关全亮的时候大概20毫安显示少量文字的话10毫安左右。如果对功耗要求高可以在不需要显示的时候关闭OLED通过命令0xAE关闭显示需要的时候再0xAF打开。5.2 温度数据的存储与导出方案FLASH里存的数据怎么导出来最简单的办法是在接收端加一个串口转USB模块把接收到的数据直接打印到电脑上用串口助手保存成文本文件。如果数据存在发送端的FLASH里可以加一个按键按下后把FLASH里的数据全部通过串口发出来。数据格式我一般用CSV每行一条记录格式是“时间戳,温度值”。时间戳可以用RTC提供如果没有RTC就用开机后的毫秒数。温度值保留一位小数比如“25.3”。这样导出来直接可以用Excel打开画曲线图。如果要存的数据量很大可以考虑用文件系统比如FatFs。把W25Q64格式化成FAT文件系统数据以文件形式存储通过USB Mass Storage或者SD卡导出。不过FatFs移植起来稍微麻烦一点需要实现磁盘读写接口对于简单的温度记录来说有点重了。5.3 无线数据帧格式设计无线传输的数据帧我一般设计成固定长度比如16字节。格式如下帧头2字节0xAA 0x55、设备ID1字节、温度整数部分1字节、温度小数部分1字节、电池电压1字节、状态标志1字节、保留7字节、校验和1字节。固定长度的好处是接收端处理简单不需要解析变长协议。校验和可以用简单的累加和把所有字节加起来取低8位。虽然不如CRC可靠但对于短帧数据来说够用了。如果环境干扰大可以用CRC8计算量也不大。接收端收到数据后先检查帧头帧头不对就丢弃。然后检查校验和校验不对也丢弃。都通过了再解析温度值。这样可以有效过滤掉干扰导致的错误数据。6. 项目扩展与个人实操体会6.1 从单点测温到多点组网单个温度计只能测一个位置的温度如果要做多点监测比如大棚里不同位置的温度就需要组网。NRF24L01支持6个接收通道可以同时接收6个不同地址的发送端数据。每个发送端设置不同的地址接收端轮流切换通道接收或者用不同的管道地址同时接收。如果节点更多可以用中继的方式每个节点既发送自己的数据也转发其他节点的数据。不过NRF24L01本身不支持自动中继需要自己写协议。更简单的方案是换用LoRa模块传输距离远组网也方便但成本会高一些。6.2 数据可视化与远程查看本地OLED只能显示当前温度要看历史曲线还得把数据导到电脑上。如果想远程查看可以在接收端加一个ESP8266或者ESP32把数据上传到云平台。不过这就涉及到网络配置的问题对于简单的温度监测来说有点复杂了。我个人的做法是接收端用STM32加一个串口屏把温度曲线直接画在屏幕上。串口屏用起来很简单发指令就能画线、显示数字比自己做GUI省事多了。或者用上位机软件通过串口接收数据用Python的matplotlib实时画图效果也不错。6.3 外壳设计与安装注意事项做好的电路板总要装个外壳。3D打印是最方便的选择我一般用PLA材料打印温度210°C热床60°C。外壳设计的时候注意留出传感器开孔和USB接口的位置。如果是测环境温度外壳上要开通风孔否则传感器测的是外壳内部的温度跟环境温度有偏差。DS18B20如果是防水探头版本可以直接放在被测环境里比如水里、土壤里。但要注意探头线缆的防水接头处用热缩管封好否则水汽进去会导致短路。我做过一个土壤温度监测的项目探头埋在地下20厘米处线缆从PVC管里穿出来接头用防水胶密封运行了半年多没出问题。6.4 我踩过的几个坑第一个坑是NRF24L01的电源。最早做的时候直接从STM32的3.3V引脚取电结果通信距离只有几米。后来在模块旁边加了10uF电容距离立马翻倍。这个电容的位置很关键一定要靠近模块的VCC引脚远了效果就打折扣。第二个坑是DS18B20的延时。一开始用for循环做微秒延时没考虑编译器优化结果延时时间不对读出来的温度全是85°C。后来改用SysTick定时器问题解决。所以延时函数一定要用硬件定时器软件循环不可靠。第三个坑是OLED的I2C地址。买了一批0.96寸OLED有的地址是0x78有的是0x7A程序里写死了0x78结果一半的屏幕不亮。后来改成上电时自动扫描地址兼容性就好了。第四个坑是FLASH的扇区擦除。一开始没注意擦除时间擦除命令发出去后马上就读状态结果一直显示BUSY。后来加了等待循环等BUSY清零后再操作就正常了。W25Q64的扇区擦除最长可能要400毫秒等待时间要留够。这个项目整体难度不大但细节很多。每个模块单独调通不难难的是把它们整合在一起稳定运行。我的建议是分模块调试先调通DS18B20再调OLED然后调NRF24L01最后调FLASH每个模块都确认没问题了再整合。整合的时候先跑通数据流再优化功耗和稳定性。这样出了问题容易定位不至于一团乱麻。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。