STC15单片机nRF24L01远距离发射实验例程源码详解
简介围绕nRF24L01远距离无线发射场景的STC15单片机实验例程源码包适合正在做无线通信课程设计、单片机入门或物联网小项目的开发者。内容覆盖2.4GHz工作频段、2Mbps数据速率、SPI接口连接、发射功率调整与寄存器配置等关键知识点并结合STC15增强型8051内核的I/O、中断与低电压特性展示完整的数据发射代码流程标签提及的ESP8266 APStation组网思路也可作为无线网络扩展参考。压缩包共14个文件、约48KB以Keil工程配置、C语言源码、头文件、hex烧录文件及编译中间文件为主工程结构完整便于直接阅读SPI时序、寄存器设置和无线收发逻辑m51/lst等中间文件还可帮助核对编译映射和排查链接问题。目前已有114人学习适合作为单片机课程设计、毕业设计或物联网入门项目的底层参考。1. 给 STC15 配 nRF24L01 做远距离发射难点不在接线而在参数取舍如果你手里有一套 STC15 单片机想做一个能打到几百米甚至上公里的无线遥控、数据透传或遥控小车nRF24L01 无线发射是绕不开的方案。2.4GHz 频段、GFSK 调制、SPI 接口、几块钱一片看起来比 433M 模块“高级”但很多人照着例程接好线却发现室内隔一堵墙就丢包。问题通常不出在硬件而是你没理解“远距离发射”这句话背后的三个变量数据速率、发射功率和自动重发策略。这个标题里的“实验例程源码”本质上就是用 STC15 的 SPI 去操作 nRF24L01 的寄存器把它配置成发送端PTX然后把一包数据发出去。适合正在做课程设计、毕设或小型无线项目的开发者。接下来我直接从发射端的视角把寄存器配置、发送时序和距离优化一次讲透。2. 远距离发射的物理基础250kbps 速率和 PA 模块才是距离的来源2.1 nRF24L01PA 模块和裸模块的差距到底在哪市面上常见的有两种板子一种是裸 nRF24L01板上只有一颗芯片和 PCB 天线发射功率 0dBm空旷距离大约 30~50 米另一种是带 PA功率放大器和 LNA低噪声放大器的版本常见功放芯片是 RFX2401C 或 PA2411发射功率标称 20dBm空旷距离可以到 800~1000 米条件是两端都用 PA 模块、天线垂直、地面无遮挡。参数裸 nRF24L01nRF24L01PA/LNA发射功率0dBm1mW20dBm100mW接收灵敏度250kbps-94dBm-94dBmLNA 通常不改变灵敏度标称值空旷通信距离30~50m500~1000m峰值电流约 13mA约 100~130mA体积小略大需要外部天线注意PA 模块上的 RF 开关在接收时不走 PA 而是走 LNA所以接收灵敏度没有因为放大而变好多少距离优势主要来自发射端功率提高。换句话说你想让“远距离发射”成立至少发射端必须用 PA 模块接收端是否用 PA 则决定接收灵敏度极限。2.2 为什么 250kbps 比 2Mbps 传得更远nRF24L01 支持三种空中速率250kbps、1Mbps、2Mbps。数据手册给出的灵敏度分别为约 -94dBm、-85dBm、-82dBm。灵敏度数字越小越好250kbps 比 2Mbps 多了约 12dB 的链路预算。用自由空间路径损耗公式估算路径损耗 20×log10(d) 20×log10(f) 32.44单位是 dB距离 d 的单位是 km频率 f 的单位是 MHz。以 2440MHz 计算0dBm 发射功率、-94dBm 灵敏度的理想最大距离约为 4 公里实际因为地面反射、天线效率和干扰能到 1/10 就不错。若改用 2Mbps灵敏度降到 -82dBm理想距离缩水到约 1 公里实际可能只剩几十米到一百米。所以做远距离发射第一件事就是把 RF_SETUP 寄存器里的速率位设成 250kbps。代价是空中传输时间变长一包 32 字节在 250kbps 下大约需要 1ms在 2Mbps 下只要 0.13ms前者被 2.4G 频段内 WiFi 信号碰撞的概率更高。这是个典型的“要距离还是要吞吐”的取舍。2.3 寄存器命令格式和地址匹配逻辑nRF24L01 用一根 CSN 线做片选所有操作都是通过 SPI 发一个命令字节再加若干数据字节。命令字节的高 5 位是命令码低 3 位是要操作的寄存器地址。常用的读命令是0x00 | reg写命令是0x20 | reg发送负载是0xA0清空发送 FIFO 是0xE1。#define REG_CONFIG 0x00 #define REG_EN_AA 0x01 #define REG_EN_RXADDR 0x02 #define REG_SETUP_AW 0x03 #define REG_SETUP_RETR 0x04 #define REG_RF_CH 0x05 #define REG_RF_SETUP 0x06 #define REG_STATUS 0x07 #define REG_TX_ADDR 0x10 #define REG_RX_ADDR_P0 0x0A #define TX_PLOAD 0xA0 #define FLUSH_TX 0xE1地址匹配是新手最容易忽略的发送端写进 TX_ADDR 的 5 字节地址必须和接收端 RX_ADDR_P0 的 5 字节地址完全一致否则即使射频参数全部相同也收不到。建议不要在地址里用连续的 0xAA、0x55 或者全 0这类地址容易在干扰下产生错误匹配。3. STC15 与 nRF24L01 的接线和 SPI 底层驱动3.1 电平匹配和最小接线nRF24L01 的 VCC 范围是 1.9~3.6V典型 3.3V。STC15 系列是宽压单片机供电范围 2.4~5.5V所以最省事的方案是把 STC15 也放在 3.3V 下运行两者共用一组 3.3V 电源IO 电平完全一致。如果你非要让 STC15 工作在 5V那么 CE、CSN、SCK、MOSI 这四个输出引脚必须串电阻分压。常见做法是串联 1kΩ 电阻到 nRF24L01 引脚利用模块引脚对地阻抗自然分压但这样做时序边沿会变慢SPI 速率太高时可能出错。我一般建议直接 3.3V 共供电少一层转换就少一个坑。STC15 引脚nRF24L01 引脚说明P1.3CE片选使能高电平进入收发状态P1.2CSNSPI 片选低电平有效P1.1SCKSPI 时钟P1.0MOSI主机输出从机输入P1.4MISO主机输入从机输出P1.5IRQ可选中断脚低电平有效注意 CSN 与 CE 不要接反或共用。CSN 负责 SPI 事务的打开和关闭CE 负责控制 nRF24L01 进入发送或接收模式两个引脚功能完全不同。3.2 用软件 SPI 读写寄存器的标准写法STC15 系列部分型号带硬件 SPI例如 STC15W4K32S4但寄存器映射与 STC89C52 不同工程里复制粘贴的例程很容易跑偏。为了通用性先用软件 SPI 把时序跑通逻辑上完全等价且不受硬件 SPI 引脚映射限制。#include stc15.h #include intrins.h typedef unsigned char u8; typedef unsigned int u16; sbit CE P1^3; sbit CSN P1^2; sbit SCK P1^1; sbit MOSI P1^0; sbit MISO P1^4; void SPI_Delay(void) { _nop_(); _nop_(); } void SPI_WriteByte(u8 dat) { u8 i; for (i 0; i 8; i) { if (dat 0x80) MOSI 1; else MOSI 0; SCK 1; SPI_Delay(); dat 1; SCK 0; SPI_Delay(); } } u8 SPI_ReadByte(void) { u8 i, dat 0; for (i 0; i 8; i) { SCK 1; SPI_Delay(); dat 1; if (MISO) dat | 0x01; SCK 0; SPI_Delay(); } return dat; } u8 NRF_ReadReg(u8 reg) { u8 val; CSN 0; SPI_WriteByte(0x00 | reg); val SPI_ReadByte(); CSN 1; return val; } u8 NRF_WriteReg(u8 reg, u8 val) { u8 status; CSN 0; status SPI_WriteByte(0x20 | reg); SPI_WriteByte(val); CSN 1; return status; }软件 SPI 的时序核心是“SCK 高电平期间采样 MISO、低电平期间更新 MOSI”上面的代码正是这个节拍。两个_nop_()在 12MHz 1T 模式下约产生 0.17us 延时远小于 nRF24L01 要求的 100ns SPI 时钟最小周期。如果单片机主频更高可以适当多插几个_nop_()。硬件 SPI 用户可以这样初始化STC15 的硬件 SPI 在寄存器 SPCTL 中把 SSIG 位置 1MSTR 位置 1选择 CPOL1、CPHA1再把 SPDAT 当数据寄存器用。两种方式最终写进 nRF24L01 的字节序列完全一致所以可以先拿软件 SPI 调试稳定后再切硬件 SPI 提升速度。3.3 读 STATUS 是排查一切问题的入口STATUS 寄存器地址 0x07bit6 是 RX_DRbit5 是 TX_DSbit4 是 MAX_RT。这三个标志位写 1 清零。发送一包数据后读 STATUS 就能知道是发成功了、还是重发次数耗尽放弃发送。u8 NRF_GetStatus(void) { return NRF_ReadReg(REG_STATUS); }这里的返回值要和 0x70 做与操作再判断如果 TX_DS 位为 1说明数据发出并且收到了接收端的 ACK如果只有 MAX_RT 为 1说明接收端没回应触发了最大重发次数。要注意MAX_RT 置位后发送 FIFO 不会被自动清空需要手动发 FLUSH_TX 命令把残留数据清掉否则下一包数据会排旧数据的队。4. 发射端例程源码初始化配置与发送一包数据的完整时序4.1 发送端寄存器配置的推荐值远距离发射场景下推荐配置是 250kbps、0dBm 芯片功率PA 模块会对外放大、2 字节 CRC、使能自动重发。RF_SETUP 寄存器的值需要自己拼bit6 写 1 选 250kbpsbit3~bit2 写 11 选 0dBm所以 RF_SETUP 0x4C。u8 txAddr[5] {0x7A, 0x3C, 0x55, 0x12, 0xAC}; void NRF_TX_Init(void) { CE 0; CSN 1; NRF_WriteReg(REG_CONFIG, 0x0E); // PWR_UP1, EN_CRC1, CRCO1, PRIM_TX0 NRF_WriteReg(REG_EN_AA, 0x01); // 数据管道0自动ACK NRF_WriteReg(REG_EN_RXADDR, 0x01); // 只开数据管道0 NRF_WriteReg(REG_SETUP_AW, 0x03); // 5字节地址宽度 NRF_WriteReg(REG_SETUP_RETR, 0x1A); // 重发延时500us, 重发10次 NRF_WriteReg(REG_RF_CH, 0x32); // 信道50, 频率2450MHz NRF_WriteReg(REG_RF_SETUP, 0x4C); // 250kbps, 0dBm NRF_WriteReg(REG_TX_ADDR, txAddr[0]); // 低字节先写? 见下文说明 NRF_WriteReg(REG_RX_ADDR_P0, txAddr[0]); CE 1; }上面的代码里 REG_TX_ADDR 只写了一个字节实际地址是 5 字节必须连续写完。地址写入顺序是先从最低字节开始这一点在寄存器地址连续递增时体现为循环写入void NRF_WriteAddr(u8 reg, u8 *addr) { u8 i; CSN 0; SPI_WriteByte(0x20 | reg); for (i 0; i 5; i) { SPI_WriteByte(addr[i]); } CSN 1; }4.2 发送一包数据的时序CE 拉高再拉低nRF24L01 的发送流程并不复杂但 CE 的时序必须精确先把 CE 拉低进入待机状态写完地址和负载后把 CE 拉高至少 10us再拉低芯片才开始发射。很多人一上来就把 CE 一直拉高导致模块一直处于等待状态数据始终没有发出去。void NRF_SendPacket(u8 *buf, u8 len) { u8 status; u8 i; CE 0; SPI_WriteByte(FLUSH_TX); // 清空发送FIFO残留 NRF_WriteAddr(REG_TX_ADDR, txAddr); // 发送地址 NRF_WriteAddr(REG_RX_ADDR_P0, txAddr); // 接收ACK的地址 CSN 0; SPI_WriteByte(TX_PLOAD); // 写发送负载命令 for (i 0; i len; i) { SPI_WriteByte(buf[i]); // 逐字节写入负载 } CSN 1; CE 1; for (i 0; i 10; i) _nop_(); // CE高电平维持至少10us CE 0; while (1) { status NRF_GetStatus(); if (status 0x70) break; // TX_DS 或 MAX_RT 置位即结束 } NRF_WriteReg(REG_STATUS, status 0x70); // 写1清中断标志 if (status 0x10) { // TX_DS: 发送成功且收到ACK } else if (status 0x08) { // MAX_RT: 超过重发次数, 执行FLUSH_TX SPI_WriteByte(FLUSH_TX); } }这里用一个忙等循环等待发送结束适合例程演示。注意while (1)里的判断条件用status 0x70因为 STATUS 的三个中断标志位分别落在 bit6、bit5、bit4 上只要任意一个置位就说明一次发送事务完成。MAX_RT 对应的状态位是 bit4数值是 0x08所以在判断处写的是 0x08 而不是 0x04别搞混。4.3 重发参数 SETUP_RETR 的选取逻辑SETUP_RETR 寄存器高 4 位是自动重发延时 ARD低 4 位是最大重发次数 ARC。ARD 以 250us 为步进0x1A 表示 500us 延时、10 次重发。这个参数直接影响远距离下的可靠性距离越远ACK 往返时间越长如果 ARD 设置得太短本端在 ACK 还没回来之前就重发旧包反而把信道占满。我一般建议在 300 米以上距离把 ARD 设到 750us 甚至 1000us也就是 SETUP_RETR 的值为 0x3A 或 0x4ACRC 重发次数保持 10 次。用 250kbps 速率时一包 8 字节数据的空中时间约 0.5ms1000us 的 ARD 足够覆盖一次完整的发送和 ACK 往返。太大会让链路反应迟钝太小则重发风暴。4.4 轮询发送和中断发送怎么选例程里用轮询 STATUS 最简单但主循环会被阻塞在while (1)里。实际项目中可以把 IRQ 引脚接 STC15 的外部中断引脚 INT0在下降沿触发的中断里读 STATUS这样单片机可以在等待 ACK 期间干别的事。void INT0_Isr(void) interrupt 0 { u8 status NRF_GetStatus(); NRF_WriteReg(REG_STATUS, status 0x70); if (status 0x10) { txOkFlag 1; } else if (status 0x08) { SPI_WriteByte(FLUSH_TX); txResendFlag 1; } }中断方式的好处是 MCU 不用在发送期间空转代价是 IRQ 引脚必须接线。STC15 的 INT0 在 P3.2默认电平是上拉输入nRF24L01 的 IRQ 默认高、事件发生时拉低正好匹配下降沿触发。5. 把发射距离顶到极限的三个实测技巧5.1 降速、缩包、调重发先确认你已经把 RF_SETUP 设成 250kbps这是最有效的一步。然后看负载长度nRF24L01 的负载最多 32 字节但远距离场景里每一字节都是在空中多待的时间。实测 32 字节负载在 250kbps 下大约占用 1ms如果链路本身差重发会把信道打满。建议遥控类应用把单包负载压缩到 8 字节以内一条命令一包发完不要为了“省事”把一整帧结构体塞进去。5.2 判断链路质量不要用丢包率要看重发次数nRF24L01 没有 RSSI 寄存器不能直接读信号强度但你可以利用 STATUS 里的 ARC_CNT 位段。它在自动重发结束后记录实际重发了多少次链路越差这个数字越高。把每次发送后的 status 里低 4 位取出来累加用串口打印出来比盲目看 LED 闪烁精确得多。u8 linkQuality status 0x0F; // ARC_CNT如果这个值长期接近 10说明链路已经到临界状态再调天线朝向、高度或者换信道比继续堆发射功率更有效。5.3 天线净空和供电是最后的瓶颈PA 模块对电源非常敏感。发送瞬间电流能冲到 120mA 以上普通 3.3V 稳压器如果压降太大RF 输出功率会明显下降。在模块电源引脚旁边放 100uF 钽电容加 0.1uF 陶瓷电容导线尽量短不要用杜邦线从面包板远距离飞线供电。天线位置同样关键nRF24L01 模块用的 2.4GHz 天线是 1/4 波长单极子裸长大约 31mm尽量垂直地面、远离金属平面和人体。实测中发现模块贴着桌面和悬空 30cm 摆放距离能差出 30% 以上。如果需要固定安装把模块用尼龙柱架起来不要把天线贴在外壳金属面板上。最后PA 模块在 250kbps、0dBm 配置下的实际电流约 100mA比裸模块高一个数量级长时间发射会让模块表面明显发热。例程里连续发送时建议在每包之间加 10ms 以上延时既降低功耗也减少在恶劣环境下无谓的重发风暴。用这种方式配合前面的配置空旷环境下把一包数据从 200 米打到 600 米以上是完全可以复现的。本文还有配套的精品资源点击获取