☰
基于51单片机与NRF905的无线温度采集系统设计
2026/10/17 7:22:02 网站建设 项目流程

简介:基于51单片机的温度采集NRF905无线传输程序,面向单片机爱好者、电子类专业学生与电子DIY玩家,可帮助初学者熟悉DS18B20温度采集、NRF905无线收发和LCD1602显示的综合开发流程。压缩包共34个文件,大小仅80KB,含发射机与接收机两套Keil工程,包括C语言源程序、LCD1602与DS18B20驱动头文件、HEX烧录文件,以及备份和编译中间文件,可直接打开工程查看代码或烧录验证硬件。当前已有172人学习下载。其价值在于对照发射与接收两端代码,能理解温度数据采集、编码、无线发送与接收显示的完整链路;结合驱动头文件可掌握模块化编程思路。工程文件齐全,便于在Proteus仿真或实物上二次修改调试,适合作为单片机无线通信课程设计或入门练习的参考资料,建议具备一定电子技术基础和C语言设计基础后使用。

1. 先把51单片机温度采集这事说透:NRF905无线传输能帮你省掉多少事

做温度监控最烦的不是读温度,而是布线。传感器装在冷库、温室或者配电房里,主机放在几十米外的值班室,中间拉一根几十米长的信号线,线损、干扰、施工成本全来了。这个项目用51单片机做温度采集,配NRF905无线模块做无线传输,把温度数据从采集端发到接收端,整套链路不拉一根信号线。采集端用DS18B20测温度,数值通过NRF905在433MHz频段发出去,接收端另一片51单片机收下来,在1602液晶屏上显示,实测在开阔环境下传个三五十米很轻松。适合拿来做电子设计竞赛的无线测温题、单片机课程设计,或者工厂车间多点温度监控的入门原型。

2. 硬件架构与选型:DS18B20怎么接、NRF905怎么连、收发两端如何分工

2.1 采集端选型:为什么是DS18B20而不是热电偶或热敏电阻

温度传感器这块,DS18B20几乎是51单片机项目里的默认选项。它走单总线协议,一根数据线既能供电又能传数据,测量范围-55°C到+125°C,在-10°C到+85°C范围内精度±0.5°C,对学生项目和工业原型来说完全够用。比起热电偶要接冷端补偿电路、比起热敏电阻要自己算分压和查表,DS18B20直接输出数字量,省掉一堆模拟前端电路。

硬件上要注意它的工作模式。DS18B20支持寄生供电和外部供电两种方式,寄生供电只接两根线,但转换电流大时容易出问题,我一般建议用外部供电,VDD接3.3V或5V,DQ数据线接单片机IO口,同时必须接一个4.7kΩ上拉电阻。这个上拉电阻是很多新手翻车的地方,不接的话单总线时序根本拉不高电平,读回来的温度永远是85°C或者乱码。

采集端的主控我用STC89C52RC,5V供电,和DS18B20直接兼容,不需要额外电平转换。NRF905模块的IO口也是5V耐受的,所以整个采集端小板子就三样东西:一片51单片机、一个DS18B20、一个NRF905模块,再加几个去耦电容和电阻,元件成本极低。

2.2 NRF905无线模块工作机制:433MHz频段和ShockBurst模式

NRF905是北欧半导体公司出的单片射频收发芯片,工作在433/868/915MHz三个ISM频段,我用的是433MHz版本,绕射能力强,穿一堵墙还有余量。它内置了完整的射频前端和协议引擎,单片机只需要通过SPI接口读写它的内部寄存器,发送时把数据扔进去,接收时从中取出来,射频部分完全不用操心。

这个芯片最核心的是ShockBurst技术,可以理解为硬件帮你把数据帧的导码、CRC校验、地址匹配全干完了。你只要配置好发送地址和接收地址,写入要发的数据,拉高CE引脚,芯片就自动加前导码、算CRC、按GFSK调制发出去。接收端配置相同的地址和频段,CE拉高进入接收模式,数据到达后DR引脚拉高,单片机SPI读回来就行。这种设计大大降低了单片机负担,不像nRF24L01需要软件去折腾自动应答和重发。

模块引脚不算多,常用的是CE(发射/接收使能)、CSN(SPI片选)、SCK、MOSI、MISO、DR(数据就绪)、TX_EN(收发方向控制),还有VCC和GND。TX_EN和CE的组合决定工作状态:CE低、TX_EN任意是待机,CE高、TX_EN高是发送,CE高、TX_EN低是接收。

2.3 收发两端硬件接线表:照着连就能跑

下面这张表是我在实验板上验证过的接法,发送端和接收端各用一片STC89C52RC,NRF905模块的SPI引脚都接在P1口。

发送端(温度采集节点)接线:

器件引脚单片机引脚说明
DS18B20 DQP2.0单总线数据线,需接4.7kΩ上拉
DS18B20 VDD+5V外部供电
DS18B20 GNDGND共地
NRF905 CEP1.0收发使能控制
NRF905 TX_ENP1.1发送/接收方向选择
NRF905 CSNP1.2SPI片选,低有效
NRF905 SCKP1.3SPI时钟
NRF905 MOSIP1.4SPI主机输出
NRF905 MISOP1.5SPI主机输入
NRF905 DRP1.6数据就绪中断引脚

接收端接线除了不接DS18B20,其余NRF905的引脚接法完全一样,然后把1602液晶的RS、RW、EN分别接P0.7、P0.6、P0.5,数据口D0-D7接P0.0-P0.4和P2.0-P2.2(或者用4位模式省引脚)。我实际做的时候为了省事,接收端液晶用了4位模式,只占6个IO口。

供电方面有个容易忽略的坑:NRF905发射瞬间电流能到30mA左右,如果用凌乱杜邦线从单片机5V引脚引电,压降会导致模块复位。我一般给NRF905单独加一个100μF电解电容并在VCC和GND之间并一个0.1μF陶瓷电容,实测发射距离能稳定不少。

3. 工程结构与主流程:从温度值到无线帧的代码是怎么组织的

3.1 工程目录与模块划分:Keil下怎么拆文件

整套代码用Keil C51开发,工程分了发送端和接收端两个独立项目,各自维护一套代码。目录结构是这样组织的:

temp_nrf905/ ├── tx_node/ # 发送端工程(温度采集节点) │ ├── main.c # 主循环,采集-打包-发送 │ ├── ds18b20.c # DS18B20驱动 │ ├── ds18b20.h │ ├── nrf905.c # NRF905驱动 │ ├── nrf905.h │ └── delay.c # 晶振11.0592MHz延时函数 └── rx_node/ # 接收端工程(显示节点) ├── main.c # 主循环,接收-解析-显示 ├── nrf905.c # 与发送端共用同一份驱动,避免配置不一致 ├── nrf905.h ├── lcd1602.c # 1602液晶驱动 ├── lcd1602.h └── delay.c

这种拆法的好处很直接:NRF905驱动在收发两端完全共用源文件,寄存器配置写死在一个头文件里,改频率或者改地址只需要动一处。很多新手习惯把全部代码堆在一个main.c里,收发两端各改各的,最后地址对不上、频段不一致,无线链路就是不通,排查起来头疼。

3.2 发送端主流程:四步循环,状态机思想在哪里

发送端的核心逻辑不复杂,但要用状态机的思路去组织,而不是写成一坨顺序执行。整个主循环分为四个状态:测量温度、读取结果、打包数据、无线发射。

// main.c - 发送端主循环伪代码 void main(void) { uint8_t i; int16_t temp; NRF905_Init(); // 配置SPI、设置收发地址和频段 while (1) { DS18B20_Convert(); // 发送温度转换指令 delay_ms(750); // 转换时间约750ms,等待完成 temp = DS18B20_GetTemp(); // 读取16位温度值 tx_buf[0] = temp & 0xFF; // 低温字节 tx_buf[1] = temp >> 8; // 高温字节 tx_buf[2] = 0xAA; // 帧头标记 tx_buf[3] = crc8(tx_buf, 2); // 简单校验 NRF905_TxPacket(tx_buf, 4); // 发射4字节数据 delay_ms(2000); // 每2秒采集一次,避免发射过于频繁 } }

很多教程只讲顺序执行,但我建议用一个简单的枚举状态机去管理,尤其是以后要做多节点轮询的时候。比如定义STATE_IDLE、STATE_MEASURE、STATE_READY、STATE_SEND四个状态,主循环里用一个switch跳转,采集周期和发送节奏分开控制。这样改造成多节点上报模式时,只需要在STATE_SEND里切换目标地址或者信道,不用重写整个循环。

3.3 接收端主流程:等待中断还是轮询DR引脚

接收端的结构比发送端简单,核心问题是怎么知道数据到了。NRF905收到有效数据后DR引脚会拉高,所以有两种处理方式:一是把DR接到单片机外部中断引脚,数据一到就打断主程序;二是主循环里轮询DR引脚电平。第一种实时性好,第二种代码简单。这个项目里我用的是轮询,因为温度数据2秒才来一次,轮询完全够用,还能省一个中断资源。

// main.c - 接收端主循环伪代码 void main(void) { uint8_t rx_buf[4]; NRF905_Init(); LCD1602_Init(); LCD1602_ShowString(0, 0, "Temp Monitor"); while (1) { if (NRF905_RxPacket(rx_buf)) // 收到一帧数据返回1 { if (rx_buf[2] == 0xAA && crc8(rx_buf, 2) == rx_buf[3]) { // 校验通过,解析温度并显示 int16_t temp = (rx_buf[1] << 8) | rx_buf[0]; float t = temp * 0.0625; // 12位分辨率,每个LSB代表0.0625°C LCD1602_ShowTemp(t); } } delay_ms(50); } }

这里有个细节值得注意:NRF905的接收函数内部要处理DR引脚边沿变化,我的做法是CSN拉低进入SPI读操作之前,先等DR稳定,然后一次把RX_PAYLOAD寄存器读空。如果读了一半DR就被硬件清掉,下一帧数据就会覆盖当前缓冲区,这就是后文要讲的乱码来源之一。

4. 核心代码拆解:温度读取、无线发射、接收显示的三段关键代码

4.1 DS18B20时序驱动:复位、跳过ROM、读温度

DS18B20的驱动是整套代码里最讲究时序的部分。51单片机没有硬件单总线控制器,全靠延时函数卡时间点。我用的晶振是11.0592MHz,每个机器周期约1.085μs,写延时函数时必须按这个算。

先看复位函数,这是每次通信的起点:

// ds18b20.c - 复位脉冲 bit DS18B20_Reset(void) { bit presence; DQ = 0; // 拉低总线 delay_us(480); // 保持480us以上产生复位脉冲 DQ = 1; // 释放总线 delay_us(60); // 等待60us后采样存在脉冲 presence = DQ; // 0表示器件存在,1表示没有器件 delay_us(420); // 剩余复位时间 return presence; }

逻辑说明:主机拉低480μs以上再释放,DS18B20收到复位脉冲后会在60μs左右拉低总线作为存在脉冲,所以主机释放总线后延时60μs采样,读到低电平说明传感器在线。这个函数在调试时非常有用,如果返回1,说明硬件连线有问题,直接去查上拉电阻和VDD供电。

再看写字节和读字节,这两个函数是对时序要求最严的地方:

// ds18b20.c - 写一个字节 void DS18B20_WriteByte(uint8_t byte) { uint8_t i; for (i = 0; i < 8; i++) { DQ = 0; // 产生写起始时序 delay_us(10); // 写隙起始后延时10us DQ = byte & 0x01; // 输出数据位,先低字节位 delay_us(50); // 保持数据时间 DQ = 1; // 释放总线 delay_us(2); // 恢复时间 byte >>= 1; } }

写时序的要点是每个写隙至少60μs,两个写隙之间至少1μs恢复时间,数据位是从低位开始。代码里先拉低总线10μs,然后写入当前位并保持50μs,加起来正好一个完整的写时隙。

读字节的代码逻辑相通,区别在于主机拉低1μs后必须释放总线,然后在15μs内采样DS18B20输出的电平:

// ds18b20.c - 读一个字节 uint8_t DS18B20_ReadByte(void) { uint8_t i, byte = 0; for (i = 0; i < 8; i++) { byte >>= 1; DQ = 0; // 产生读起始时序 delay_us(2); // 保持低电平2us DQ = 1; // 释放总线 delay_us(8); // 等待数据稳定,约8us后采样 if (DQ) byte |= 0x80; delay_us(60); // 完成整个读时隙 } return byte; }

温度读取的完整流程是复位、发0xCC跳过ROM匹配、发0x44启动温度转换、等待750ms、再复位、发0xCC、发0xBE读取暂存器、连续读9个字节。前两个字节是温度值,低字节在前。12位分辨率下,温度值乘以0.0625就是实际的摄氏度值。

4.2 NRF905初始化与发送:SPI配置和收发地址是关键

NRF905的驱动分三层:底层是SPI字节交换,中间是寄存器读写,上层是发送和接收包函数。先看初始化里最核心的SPI配置:

// nrf905.c - SPI初始化 void NRF905_SPI_Init(void) { SCK = 0; CSN = 1; MOSI = 1; MISO = 1; } // SPI字节交换 uint8_t NRF905_SpiRW(uint8_t byte) { uint8_t i; for (i = 0; i < 8; i++) { SCK = 0; if (byte & 0x80) // 先发最高位 MOSI = 1; else MOSI = 0; byte <<= 1; SCK = 1; // 上升沿锁存输入数据 if (MISO) // 同时读回MISO电平 byte |= 0x01; } SCK = 0; return byte; }

逻辑说明:这个软件SPI完全靠IO口模拟,时钟极性是空闲低电平、上升沿采样。MOSI先输出当前数据位的最高位,然后拉高SCK产生上升沿,NRF905在这个沿上锁存数据;同时读MISO引脚,接收芯片返回的数据位。整个字节交换完成后,byte里既包含发出的数据,也包含收到的数据,一箭双雕。

接下来是初始化函数,配置收发地址、频段和功率:

// nrf905.c - NRF905初始化 void NRF905_Init(void) { uint8_t config[10]; // 进入配置模式:CE=0, CSN=0 CE = 0; CSN = 0; // 配置寄存器(10字节) config[0] = 0x01; // 频段配置,433MHz config[1] = 0x0C; // 发送功率10dBm,433MHz频段 config[2] = 0x00; // 接收地址宽度4字节 config[3] = 0x00; // 发送地址宽度4字节 config[4] = 0x00; // 接收地址低字节 config[5] = 0x00; // 接收地址高字节 config[6] = 0x00; // 发送地址低字节 config[7] = 0x00; // 发送地址高字节 config[8] = 0x04; // CRC校验使能,8位校验 config[9] = 0x00; // 收发模式配置 // 写入配置 NRF905_SpiRW(0x00); // W_CONFIG 命令 for (i = 0; i < 10; i++) { NRF905_SpiRW(config[i]); } // 设置发送/接收地址为相同值 // 地址值放在全局变量 TX_ADDRESS[4] 和 RX_ADDRESS[4] 中 }

这里必须提醒一个关键点:NRF905的接收地址和发送地址在寄存器里各有一份,发送时芯片自动在数据帧头部加上TX地址,接收端只有RX地址匹配才能收到数据。所以收发两端的地址必须完全一致,否则DR引脚永远不会拉高,看起来就像模块坏了。

发送一个数据包的代码逻辑:

// nrf905.c - 发送一包数据 void NRF905_TxPacket(uint8_t *data, uint8_t len) { uint8_t i; // 切换到发送模式 TX_EN = 1; CE = 1; delay_us(100); // 等待模式切换稳定 // 写TX地址和TX数据 CSN = 0; NRF905_SpiRW(0x20); // W_TX_ADDRESS 命令 for (i = 0; i < 4; i++) NRF905_SpiRW(TX_ADDRESS[i]); CSN = 1; CSN = 0; NRF905_SpiRW(0x21); // W_TX_PAYLOAD 命令 for (i = 0; i < len; i++) NRF905_SpiRW(data[i]); CSN = 1; // CE保持高电平,触发发送 delay_us(100); CE = 0; // 发送完成后拉低CE,回到待机 // 等待发送完成 while (DR == 0); // DR拉高表示发送完成 }

参数说明:data是待发送的数据缓冲区,len是数据长度,NRF905单个数据包最多32字节。这里发送完成后DR引脚会拉高,代码里用while等待DR变为高电平,确保发送完成后再切换状态。实际使用时建议加一个超时退出,不然芯片异常会卡死在循环里。

接收函数对称:

// nrf905.c - 接收一包数据,返回1表示有数据 uint8_t NRF905_RxPacket(uint8_t *data) { uint8_t i; if (DR == 0) // DR为低说明没有数据到达 return 0; // 切换到接收模式 TX_EN = 0; CE = 1; delay_us(100); // 读取RX数据 CSN = 0; NRF905_SpiRW(0x22); // R_RX_PAYLOAD 命令 for (i = 0; i < 4; i++) data[i] = NRF905_SpiRW(0x00); CSN = 1; CE = 0; // 读完立即关闭接收,防止覆盖 return 1; }

这里有个容易踩的坑:读完数据后CE拉低的时机要快,最好在读到最后一个字节后立即拉低CE,否则芯片可能又进入接收状态,下一帧数据会把缓冲区冲掉,导致逻辑上"丢数据"。

4.3 接收端温度解析与1602显示:把字节拼成温度再格式化输出

接收端把无线帧里的两个温度字节还原成温度值,再显示到LCD上。温度值是有符号数,最高位是符号位,负温度时高字节的高位是1:

// main.c - 温度解析 int16_t temp_raw = (rx_buf[1] << 8) | rx_buf[0]; // 判断正负 if (temp_raw & 0x8000) // 负温度 { temp_raw = ~temp_raw + 1; // 取补码 is_negative = 1; } // 转换为实际温度,0.0625°C/LSB float temp = temp_raw * 0.0625;

解释一下:DS18B20输出的16位温度值在12位分辨率下,低12位是有效数据,高4位是符号扩展位。负温度以补码形式存储,所以需要先判断最高位是否为1,是的话按补码取反加一得到绝对值。乘0.0625是因为12位分辨率下1个LSB代表0.0625°C,这个值来自DS18B20数据手册的分辨率表。

显示部分用1602的4位模式,驱动函数要处理使能脉冲和忙标志检测。我在温度显示前先清屏,然后把整数部分和小数部分分开转换:

// lcd1602.c - 显示温度数值 void LCD1602_ShowTemp(float temp, bit negative) { int int_part; int dec_part; if (negative) LCD1602_ShowChar(0, 1, '-'); else LCD1602_ShowChar(0, 1, ' '); int_part = (int)temp; dec_part = (int)((temp - int_part) * 100); // 取小数点后两位 LCD1602_ShowNumber(1, 1, int_part); // 整数部分 LCD1602_ShowChar(5, 1, '.'); LCD1602_ShowNumber(6, 1, dec_part); // 小数部分 LCD1602_ShowChar(8, 1, 'C'); }

有些教程直接调用sprintf格式化浮点数,在51上千万别这么干,Keil C51的浮点printf库能吃掉好几K的代码空间,而且51模拟浮点运算慢得很。手写整数和小数拆解,既快又省空间,是嵌入式开发的常见做法。

5. 避坑与排查:NRF905无线链路最常见的几个翻车现场

5.1 现象:温度读回来永远是85°C或者0xFF

原因:DS18B20的复位脉冲或读写时序不对。85°C是DS18B20上电后的默认温度值,如果程序没有正确发送转换指令,芯片就一直返回默认值。另外上拉电阻缺失会导致单总线通信失败,读到的高电平不稳定,最终结果就是0xFF。

解决:先用逻辑分析仪或示波器看DQ引脚的波形,确认复位脉冲宽度(480μs低电平)和存在脉冲是否正常。排除硬件问题后,用编写的DS18B20_Reset()函数返回值做判断,如果返回1,基本可以确定是连线或上拉电阻问题。我在调试时习惯在复位失败后点个LED,快速定位,不用每次串口打印。

5.2 现象:发送端DS18B20能读到温度,但接收端液晶一直显示初始值

原因:收发两端NRF905配置不一致,最常见的是收发地址不同、频段寄存器写错、地址宽度设置不对。NRF905的DR引脚需要接收地址完全匹配才会拉高,任何一位对不上都不行。

解决:把发送端和接收端的nrf905.c直接共用同一个文件,地址数组和频段配置全写在头文件里。我见过一个案例,发送端用433MHz、接收端用了868MHz的配置寄存器值,两只模块频率不同,怎么折腾都收不到。另外检查CE和TX_EN的组合逻辑,发送时TX_EN=1、CE=1,接收时TX_EN=0、CE=1,这个顺序错了芯片会一直待在待机模式。

5.3 现象:能收到数据,但隔一堵墙就丢包严重

原因:发射功率不足或天线问题。NRF905的发射功率由配置寄存器的低两位控制,默认可能是最低功率档。另外模块天线的焊接质量、天线长度不匹配、供电电压偏低都会大幅缩短有效距离。

解决:把配置寄存器改成10dBm档,加大模块供电电容。天线部分,433MHz频段的1/4波长天线理论上约17.3cm,柱状天线可以直接买成品SMA接口的,但注意有些模块是PCB天线,不要用金属物体遮挡。实测在相同配置下,供电稳定与否能让传输距离差出10到20米,这个差距肉眼可见。

5.4 现象:接收端偶尔显示乱码,温度值跳变很随机

原因:没有做数据校验。NRF905虽然硬件上做了CRC校验,但CRC只保证无线帧的比特没有传错,不能保证业务数据的语义正确。比如发送端在温度还没转换完就发了旧数据,或者接收端读到的是一个半包,这里面就有脏数据。

解决:在业务层加一个帧头标记和CRC校验。我用的方案是4字节帧:2字节温度值、1字节固定0xAA帧头、1字节CRC8校验。接收端先校验帧头再校验CRC,帧头不对直接丢弃,防止脏数据误显示。这样改动很小,但可靠性提升明显,丢包和乱码基本绝迹。

5.5 现象:发送模块发一会儿就不工作了,必须断电重启

原因:大概率是SPI通信总线卡死,或者发送函数里等待DR拉高的while循环跳不出来。NRF905在某些异常状态下DR引脚一直不拉高,程序就永远卡在等待循环里,看起来就像死机了。

解决:给发送完成等待加超时机制。比如设置一个变量计数,循环等待DR拉高时最多等1000次,超时后强制拉低CE复位模块状态,然后重新初始化。不要小看这个细节,实际连续运行十几个小时后,模块状态的稳定性就是靠这种防御性代码撑起来的。

6. 二次开发:从单点测温改成多节点轮询系统

这个项目最自然的改造方向是把一个发送端扩展成多个采集节点,接收端按地址轮询,做成一套多点温度监控系统。改造思路是:每个发送节点用拨码开关配置自己的地址和节点编号,接收端定时循环发送查询命令,节点收到查询后才上报温度,避免多个节点同时发射造成数据碰撞。

具体做法是给每帧数据增加一个节点ID字段,4字节的数据包扩展成6字节:1字节节点ID、2字节温度值、1字节帧头、1字节CRC校验、1字节状态位。接收端解析时先看节点ID是几号,再决定把这个温度显示到液晶的第几行。如果换成带彩色屏幕和按键的接收端,还能做成翻页查看每个节点的历史温度曲线。

低功耗改造也是常见需求。采集节点用电池供电时,让51单片机和NRF905大部分时间处于休眠状态,定时器每30秒唤醒一次,采完温度、发完数据立刻再睡。NRF905的待机电流只有微安级别,51进入掉电模式后功耗也极低,整套系统两节AA电池撑几个月没问题。发送节点之间通过地址区分,车辆载具上多个节点同时采集也不会有冲突。

我自己的习惯是每次改完代码,先跑一个24小时连续收发测试,统计丢包率,再考虑加不加自动重发机制。从那以后每次做无线项目,我都会强制走一遍"发射功率拉满、天线悬空、连续跑24小时"的验证流程,这个习惯帮我排掉过不少隐性bug。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询