STC15单片机nRF24L01远距离发射实验例程源码详解
2026/9/15 14:25:21 网站建设 项目流程

简介:围绕nRF24L01远距离无线发射场景的STC15单片机实验例程源码包,适合正在做无线通信课程设计、单片机入门或物联网小项目的开发者。内容覆盖2.4GHz工作频段、2Mbps数据速率、SPI接口连接、发射功率调整与寄存器配置等关键知识点,并结合STC15增强型8051内核的I/O、中断与低电压特性,展示完整的数据发射代码流程;标签提及的ESP8266 AP+Station组网思路也可作为无线网络扩展参考。压缩包共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 nRF24L01+PA 模块和裸模块的差距到底在哪

市面上常见的有两种板子:一种是裸 nRF24L01+,板上只有一颗芯片和 PCB 天线,发射功率 0dBm,空旷距离大约 30~50 米;另一种是带 PA(功率放大器)和 LNA(低噪声放大器)的版本,常见功放芯片是 RFX2401C 或 PA2411,发射功率标称 +20dBm,空旷距离可以到 800~1000 米,条件是两端都用 PA 模块、天线垂直、地面无遮挡。

参数裸 nRF24L01+nRF24L01+PA/LNA
发射功率0dBm(1mW)+20dBm(100mW)
接收灵敏度(250kbps)-94dBm-94dBm(LNA 通常不改变灵敏度标称值)
空旷通信距离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 位置 1,MSTR 位置 1,选择 CPOL=1、CPHA=1,再把 SPDAT 当数据寄存器用。两种方式最终写进 nRF24L01 的字节序列完全一致,所以可以先拿软件 SPI 调试,稳定后再切硬件 SPI 提升速度。

3.3 读 STATUS 是排查一切问题的入口

STATUS 寄存器地址 0x07,bit6 是 RX_DR,bit5 是 TX_DS,bit4 是 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 选 250kbps,bit3~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_UP=1, EN_CRC=1, CRCO=1, PRIM_TX=0 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 或 0x4A,CRC 重发次数保持 10 次。用 250kbps 速率时,一包 8 字节数据的空中时间约 0.5ms,1000us 的 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 米以上,是完全可以复现的。

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

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

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

立即咨询