简介:这是一套面向单片机爱好者、电子类专业学生与DIY玩家的温度采集与无线传输实践方案,基于51单片机搭配NRF905无线模块,解决短距离温度数据采集与无线收发的问题,适合具备一定电子技术基础、能看懂基础电路图并了解C语言设计原理的读者。压缩包共34个文件,约80KB,以C语言源文件、头文件、Keil工程文件(uvproj、uvopt)及编译产物(hex、obj、lst、m51)为主,另含大量bak备份文件,完整保留了发射机与接收机两套工程。资源围绕DS18B20温度采集与LCD1602显示展开,配合NRF905完成无线数据发送与接收,读者可通过查看电路学习电路设计原理,通过阅读代码理解代码编写逻辑,并借助编译输出文件快速验证程序运行效果。目前已有173人学习,适合作为课程设计、毕业设计或电子DIY项目的参考素材。
1. 从一块“哑巴”温度节点说起:51单片机配NRF905到底能跑出什么效果
前阵子帮一个做农业大棚环境监测的朋友看方案,他手里有一批老式温度采集节点,主控是51单片机,无线部分用的是NRF905。问题很典型:节点能采到温度,但数据传不回来,串口助手上一片空白。我拿过来一看,程序里NRF905的收发模式切换根本没做对,配置寄存器写进去的值和实际读出来的对不上。这个项目就是解决这类问题的——一套完整的51单片机温度采集加NRF905无线传输程序,包含发送端和接收端两套代码,能直接把DS18B20采到的温度通过NRF905发出去,接收端还原成可读数据。适合正在做无线传感节点、课程设计或者想搞懂NRF905底层时序的开发者。它不依赖复杂协议栈,全是寄存器级操作,跑通之后你对无线模块的理解会扎实很多。
2. 硬件链路与工程结构:先看清数据从传感器到空中的完整路径
2.1 发送端与接收端的硬件组成
发送端核心是51单片机最小系统加DS18B20温度传感器,再挂一片NRF905模块。DS18B20走单总线,接在任意一个IO口上,程序里用位操作模拟时序。NRF905和单片机之间是SPI接口,但51没有硬件SPI,所以用普通IO口模拟,这点很关键,后面代码里会看到。接收端同样是51单片机加NRF905,区别在于它不需要传感器,而是把收到的数据通过串口送到电脑,方便用串口助手看结果。
供电部分要注意,NRF905模块对电源纹波比较敏感,我一般会在模块VCC和GND之间并一个10uF电解电容加一个0.1uF瓷片电容。很多“发送正常但接收距离短”的问题,根源就在电源上。天线也要匹配,模块自带的弹簧天线在空旷环境能到几十米,室内穿墙会打折扣,这是物理规律,不是程序能解决的。
2.2 工程目录与文件职责
拿到源码包,目录结构通常是这样组织的:
NRF905_Temp/ ├── Sender/ │ ├── main.c // 主循环:初始化、读温度、发数据 │ ├── ds18b20.c // 单总线时序与温度换算 │ ├── nrf905.c // NRF905寄存器配置与收发 │ └── delay.c // 短延时与毫秒延时 ├── Receiver/ │ ├── main.c // 主循环:初始化、收数据、串口输出 │ ├── nrf905.c // 与发送端一致,保证配置匹配 │ ├── uart.c // 串口初始化与发送 │ └── delay.c └── Common/ └── nrf905_reg.h // 寄存器地址与命令字定义发送端和接收端的nrf905.c内容基本一致,因为NRF905的配置必须完全对称才能通信。常见做法是把配置参数抽到一个头文件里,两边都包含,避免改了一边忘了另一边。我一般会把频道、地址、有效数据宽度这几个参数单独拎出来,放在nrf905_reg.h顶部,改的时候一眼就能看到。
2.3 数据流与关键参数
整个链路的数据流是:DS18B20转换温度 -> 单片机读取 -> 格式化成一个字节数组 -> 写入NRF905发送缓冲区 -> 拉高TRX_CE触发发送 -> 接收端NRF905收到后拉低DR引脚 -> 单片机读缓冲区 -> 串口打印。
NRF905有几个参数必须两边一致:频道频率、接收地址、有效数据宽度。频道频率决定载波,地址相当于“门牌号”,数据宽度决定一次传几个字节。温度数据我一般用4个字节:两个字节放温度整数部分和小数部分,另外两个字节放节点ID和校验。这样接收端能区分是哪个节点发的,也能判断数据有没有出错。
3. 核心模块拆解:DS18B20时序与NRF905寄存器配置
3.1 DS18B20单总线时序实现
DS18B20最容易被忽略的是延时精度。51单片机在不同晶振下机器周期不同,延时函数必须根据实际晶振调整。下面这段是11.0592MHz晶振下的典型实现:
// ds18b20.c #include "ds18b20.h" #include "delay.h" // 复位DS18B20,检测存在脉冲 bit ds18b20_reset(void) { bit presence; DQ = 0; // 拉低总线 delay_us(480); // 保持至少480us DQ = 1; // 释放总线 delay_us(60); // 等待15-60us presence = DQ; // 读取存在脉冲,低电平表示存在 delay_us(420); // 等待复位完成 return presence; } // 写一个字节,低位先写 void ds18b20_write_byte(unsigned char dat) { unsigned char i; for(i = 0; i < 8; i++) { DQ = 0; delay_us(2); DQ = dat & 0x01; // 写数据位 delay_us(60); // 保持60us DQ = 1; // 释放总线 dat >>= 1; } } // 读一个字节 unsigned char ds18b20_read_byte(void) { unsigned char i, dat = 0; for(i = 0; i < 8; i++) { DQ = 0; delay_us(2); DQ = 1; // 拉高,准备读 delay_us(2); dat >>= 1; if(DQ) dat |= 0x80; // 读到1则置高位 delay_us(60); } return dat; }逻辑说明:复位函数先拉低总线480us以上,再释放,如果DS18B20存在会拉低总线60us左右,读DQ为0说明器件在线。写字节时每个位先拉低2us,然后放数据位,保持60us,最后释放。读字节时拉低2us后释放,在2us内采样总线电平。参数上,delay_us的精度直接决定成败,如果晶振换了,这三个延时值都要重新算。我见过有人用12MHz晶振但延时按11.0592MHz写,结果温度读出来永远是85度,这就是时序错位的典型翻车现场。
3.2 NRF905寄存器配置与收发切换
NRF905通过SPI接口配置,51用IO模拟SPI。下面这段是配置和发送的核心代码:
// nrf905.c #include "nrf905.h" #include "nrf905_reg.h" // 模拟SPI写一个字节 void nrf905_spi_write(unsigned char dat) { unsigned char i; for(i = 0; i < 8; i++) { MOSI = dat & 0x80; // 高位先出 SCK = 0; dat <<= 1; SCK = 1; // 上升沿写入 } } // 写配置寄存器 void nrf905_config(void) { CSN = 0; // 片选拉低 nrf905_spi_write(W_CONFIG); // 写配置命令 nrf905_spi_write(0x00); // 配置字节,根据实际设置 // 依次写入频道、地址、数据宽度等 nrf905_spi_write(CHANNEL); nrf905_spi_write(0xE7); // 接收地址,两边必须一致 nrf905_spi_write(0xE7); nrf905_spi_write(0xE7); nrf905_spi_write(0xE7); nrf905_spi_write(PAYLOAD_WIDTH); // 有效数据宽度 CSN = 1; } // 发送一包数据 void nrf905_tx_packet(unsigned char *buf, unsigned char len) { unsigned char i; CSN = 0; nrf905_spi_write(W_TX_PAYLOAD); // 写发送缓冲区命令 for(i = 0; i < len; i++) { nrf905_spi_write(buf[i]); } CSN = 1; TRX_CE = 1; // 触发发送 delay_ms(1); // 等待发送完成 TRX_CE = 0; }逻辑说明:nrf905_spi_write在SCK上升沿把数据移出,高位先出。配置时先发W_CONFIG命令,然后依次写配置字节、频道、地址、数据宽度。地址四个字节必须和接收端完全一致,否则收不到。发送时先写TX_PAYLOAD命令,再把数据逐个写入,最后拉高TRX_CE触发。参数上,PAYLOAD_WIDTH要和实际发送字节数匹配,我一般设成4,对应温度数据包。TRX_CE拉高后不要立刻拉低,给模块一点时间,1ms是保守值,实际可以更短,但没必要省这点时间。
3.3 接收端中断与串口输出
接收端用DR引脚判断是否有数据,可以用查询也可以用中断。下面是用查询方式的接收代码:
// Receiver/main.c 片段 void main(void) { unsigned char rx_buf[4]; unsigned char i; uart_init(); // 串口初始化,波特率9600 nrf905_config(); // 配置NRF905,与发送端一致 while(1) { if(DR == 1) // 收到数据 { CSN = 0; nrf905_spi_write(R_RX_PAYLOAD); // 读接收缓冲区命令 for(i = 0; i < 4; i++) { rx_buf[i] = nrf905_spi_read(); } CSN = 1; // 串口打印温度 uart_send_byte(rx_buf[0]); // 整数部分 uart_send_byte('.'); uart_send_byte(rx_buf[1]); // 小数部分 uart_send_byte('\r'); uart_send_byte('\n'); } } }逻辑说明:DR为高表示接收缓冲区有数据,先发R_RX_PAYLOAD命令,然后连续读4个字节。串口部分把整数和小数分开打印,中间加点号。参数上,串口波特率要和电脑端一致,9600是最常用的。如果DR一直不拉高,先检查发送端是否真的发出去了,再检查两边地址和频道是否一致。我一般会在发送端加一个LED闪烁指示发送动作,接收端加一个LED指示接收,这样调试时不用看串口也能判断链路是否通。
4. 避坑与排查:NRF905调试中最容易翻车的五个点
4.1 现象:发送端正常但接收端毫无反应
原因:最常见的是两边地址或频道不一致。NRF905的地址是4个字节,配置时如果只改了发送端忘了改接收端,或者频道值写错,模块根本不会响应。另一个可能是PAYLOAD_WIDTH设置不同,发送端设4接收端设2,数据对不上。
解决:把地址和频道参数抽到头文件里,两边包含同一个头文件。调试时先用固定地址0xE7E7E7E7和频道0x01,确认通了再改。用示波器看DR引脚,如果发送时接收端DR有脉冲但读不到数据,就是PAYLOAD_WIDTH的问题。
4.2 现象:温度读出来永远是85度
原因:DS18B20复位后默认输出85度,如果时序不对,单片机读到的就是默认值。常见于延时函数按12MHz写但实际用11.0592MHz晶振,或者delay_us精度不够。
解决:用示波器测DQ线上的复位脉冲宽度,确保拉低时间在480us到960us之间。延时函数用定时器做,不要用循环计数,循环计数在不同优化等级下时间不一样。我一般会在复位后读一次温度,如果连续三次都是85度,直接报传感器故障。
4.3 现象:通信距离短,隔一堵墙就断
原因:电源纹波大、天线匹配差、发射功率配置低。NRF905的发射功率可以通过配置寄存器调整,默认可能是最低档。另外模块供电如果和单片机共用一路,电机或继电器动作时电压跌落会导致模块复位。
解决:模块VCC并10uF加0.1uF电容,供电单独走一路LDO。配置寄存器里把发射功率调到最大,具体值看模块手册。天线尽量远离金属和人体,弹簧天线竖直放置效果最好。如果还不行,降低空中速率,速率越低距离越远,这是无线通信的基本规律。
4.4 现象:数据偶尔出错,串口打印乱码
原因:没有校验机制,NRF905本身有CRC但配置时可能没使能。另外电源波动导致某个字节翻转。
解决:在数据包里加一个校验字节,比如前三个字节异或。接收端算一下,不对就丢弃。NRF905的CRC使能位在配置寄存器里,建议打开。串口乱码还要检查波特率误差,11.0592MHz晶振配9600波特率误差为0,12MHz晶振会有误差,尽量用11.0592MHz。
4.5 现象:程序烧进去后单片机不跑
原因:51单片机的P0口作为普通IO时没有内部上拉,如果NRF905的SPI线接在P0口,没有外接上拉电阻,电平拉不高,SPI通信失败。另外看门狗没关,程序跑飞后不断复位。
解决:P0口接4.7K到10K上拉电阻,或者换到P1、P2口。看门狗在初始化时先关掉,调试完成再决定是否开启。烧录时注意晶振频率设置,有些下载软件默认12MHz,实际板子是11.0592MHz,烧进去延时全错。
5. 二次开发与改造:从固定节点到可配置传感网络
5.1 增加节点ID与多节点区分
原始程序是点对点,发送端固定,接收端固定。要改成多节点,最直接的办法是在数据包里加一个字节的节点ID。发送端从拨码开关或EEPROM读ID,接收端根据ID区分不同节点。下面是一个改造示例:
// 发送端数据包结构 typedef struct { unsigned char node_id; // 节点编号 unsigned char temp_int; // 温度整数 unsigned char temp_dec; // 温度小数 unsigned char checksum; // 校验 } temp_packet_t; // 填充数据包 temp_packet_t pkt; pkt.node_id = read_dip_switch(); // 从拨码开关读ID pkt.temp_int = temp >> 4; // DS18B20原始值转换 pkt.temp_dec = (temp & 0x0F) * 10 / 16; pkt.checksum = pkt.node_id ^ pkt.temp_int ^ pkt.temp_dec; nrf905_tx_packet((unsigned char *)&pkt, sizeof(pkt));逻辑说明:结构体打包后直接发送,接收端按同样结构体解析。node_id用拨码开关读取,3位拨码可以支持8个节点。checksum用异或,简单有效。参数上,sizeof(pkt)是4,和PAYLOAD_WIDTH一致。如果节点多了,可以改成轮询发送,每个节点分配不同时间片,避免空中碰撞。
5.2 接收端增加数据解析与上位机协议
接收端收到数据后,除了串口打印,还可以按一定协议打包发给上位机。我一般用简单的帧头加数据加帧尾格式:
// 接收端串口输出协议 void uart_send_packet(temp_packet_t *pkt) { uart_send_byte(0xAA); // 帧头 uart_send_byte(pkt->node_id); uart_send_byte(pkt->temp_int); uart_send_byte(pkt->temp_dec); uart_send_byte(pkt->checksum); uart_send_byte(0x55); // 帧尾 }逻辑说明:帧头0xAA和帧尾0x55用于上位机同步,中间是有效数据。上位机收到后先找0xAA,再读5个字节,最后验证0x55和校验和。这样即使串口有噪声也能恢复。参数上,帧头帧尾可以换成其他值,只要不和有效数据冲突就行。我一般会在上位机加一个超时重同步机制,超过一定时间没收到完整帧就重新找帧头。
5.3 低功耗改造思路
如果节点用电池供电,低功耗是必须的。51单片机本身功耗不低,但可以通过休眠模式加定时唤醒降低平均功耗。NRF905也有掉电模式,发送间隙可以关掉。改造步骤:把主循环改成定时器唤醒,唤醒后读温度、发数据、然后进入休眠。NRF905在发送完成后拉低TRX_CE和PWR_UP,进入掉电模式。注意DS18B20转换需要时间,可以在转换期间让单片机休眠,转换完成用外部中断唤醒。
5.4 验证方法与调试习惯
改造完成后怎么验证?我一般分三步:第一步,用串口助手看接收端输出,确认数据格式正确;第二步,用另一个NRF905模块做嗅探,看空中数据是否和发送端一致;第三步,长时间跑,看有没有丢包或校验错误。调试时我会在关键位置加LED指示,比如发送时闪一下,接收时闪一下,这样不用接串口也能判断程序有没有跑到那里。
从那以后我每次调NRF905,都强制先跑一遍地址和频道一致性检查,再确认电源纹波,最后才看代码逻辑。这个顺序帮我省了很多来回改代码的时间。希望帮到你。
本文还有配套的精品资源,点击获取