NRF24L01 Arduino 无线对传:接线、代码与那颗必装的电容

Cartoon of two black nRF24L01 radio modules with gold zig-zag antenna traces facing each other across teal and orange radio waves, a fingertip pressing a black button on a white breadboard beside the left module and a black electrolytic capacitor with a grey stripe standing beside the right one

两块 Arduino 靠一对 nRF24L01 模块对话,大约一小时就能通:两端模块照同一张表接线,两端写同一个五字节地址,一端跑负责发的那份代码,另一端跑负责听的那份。大多数人漏掉的一步,是在每块模块自己的 VCC 和 GND 引脚上跨一颗 10–100 µF 的电容 —— 少了它,模块在 SPI 上答得好好的,包却一个也送不出去。

要把两块 Arduino 无线连起来,需要哪些零件?

nRF24L01 模块是收发一体的,不是只管发的发射器 —— 同一块板既发也收,所以一条链路要两块模块,外加两块板子来带它们。按键只装在发送那一端;接收那一端在你还没给它一个真正可切换的负载之前,先用串口把结果报出来。不过两端都要一块面包板 —— 每块模块的电容,就是在那里把 VCC 桥到 GND 的。

选配 —— 距离升级、一个真正可切换的负载,以及那颗电容

ItemPriceQty
High Performance NRF24L01 RF Wireless Transceiver Module Antenna 2.4Ghz 1100m - NRF24L01 + ANTENNAHigh Performance NRF24L01 RF Wireless Transceiver Module Antenna 2.4Ghz 1100m - NRF24L01 + ANTENNANRF24ANRM14.25
Relay Module 3.3V 5V 12V 1/2/4/8 Ways Optocoupler Trigger Relay Module 1 2 4 8 Channel Relay Module - 5V RELAY MODULE(1WAY)Relay Module 3.3V 5V 12V 1/2/4/8 Ways Optocoupler Trigger Relay Module 1 2 4 8 Channel Relay Module - 5V RELAY MODULE(1WAY)1WRELAYRM3.15
Electronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KITElectronic Component Set Beginner Electrical Learning Kit For Arduino Beginner Learning - ELECTRICAL STARTER KITELECTRKRM39.95

带天线的版本是给两端一起升级的,绝不是只升一端:自动应答意味着远端那块也要发,所以只升级一头,链路仍旧被另一头卡住。继电器模块是给接收端一个真正可切换的东西。这个作品要的电容是一颗 10–100 µF 的电解电容 —— 入门套件里就能找到,因为它同时带了瓷片电容和电解电容,还配了电阻、LED 和面包板。

那八根引脚是 2×4 双排立起来的,所以 nRF24L01 没法直接插面包板 —— 两排会落在同一组连通孔上,直接短掉。公对母杜邦线才是对的:母头咬住模块的针,公头插进 Uno 的排母。公对公杜邦线用来在面包板上接按键,它裸露的针脚也正好压进继电器的螺丝端子。电脑跟 CH340 驱动过不去的话,就选 FT232 版 Uno

nRF24L01 怎么接到 Arduino Uno 上?

nRF24L01 走的是 SPI,所以它有三根脚落在 Uno 固定的 SPI 引脚上,动不得;CE 和 CSN 是普通输出脚,按惯例选。1 脚是背面焊盘为方形的那一根。两个节点接法完全一样 —— 谁发谁收,是代码里定的。

模块引脚 Uno 引脚 它做什么
1 GND GND 地,和电容的负极腿共用
2 VCC 3V3 只能 3.3 V。这里接 5 V 就是把芯片烧掉
3 CE D9 片使能 —— 让射频上电波。任意空闲脚都行
4 CSN D10 SPI 片选。任意空闲脚都行
5 SCK D13 SPI 时钟。固定
6 MOSI D11 Uno 发给模块。固定
7 MISO D12 模块发回 Uno。固定
8 IRQ 不接 中断输出。这两份代码用轮询,不用它

供电脚和逻辑脚遵守的是两套不同的规矩,而把它们混为一谈,正是烧模块的原因。Nordic 数据手册给 VDD 的绝对范围是 1.9 V 到 3.6 V,所以 Uno 的 5 V 轨大约超了上限 40%。而它的数字引脚是单独按 5.25 V 定的,这正是 Uno 可以不加电平转换、直接把 CE、CSN、SCK 和 MOSI 推进模块的原因。供电取自 3V3,信号来自 5 V 逻辑脚:这两件事同时成立,都没错。

另一处要接的只有那颗轻触按键,而它藏着一个坑:四条腿其实是两两一组的,同一侧的两条腿在壳子里面是永久连通的。把它跨在面包板中间那道沟上,让每一组落进一半,然后从一条腿引到 D2,从它对角线那条腿引到 GND —— 对角线的那一对,永远是真正开合的那一对。转四分之一圈,每一组连通的腿就自己跨过了中缝,读出来就是一颗被永远按住的按键。INPUT_PULLUP 会用 ATmega 芯片内部的电阻把 D2 拉高,所以外面不用再加电阻。

动手之前,先看两块板真的把一条消息传过去,心里会踏实很多:

nRF24L01 为什么不工作,那颗电容又是怎么把它救回来的?

nRF24L01 最经典的那种坏法有一个很好认的特征:radio.begin() 成功,模块把自己的寄存器如实读回来,而 radio.write() 每一次都返回 false。SPI 那边一切正常,射频这边就是不行 —— 这说明接线是对的,只有在射频真正上电波的那几十微秒里,供电塌了。

待机空转时这颗芯片只吃 22 µA,nRF24L01+ 是 26 µA;而在 0 dBm 发射时是 11.3 mA。这 500 倍的跳变,发生在内部 PLL 稳定所需的那 130 µs 之内 —— 是一道近乎垂直的边沿,不是一段缓坡。下面两份代码跑在 RF24_PA_LOW 上,电流比这还小,跳变的高度是降了,陡峭程度却没有变:把稳压器打垮的是电流来得多快,而不是它总共有多大。

给它要的电荷算个数出来。一个数据包里有 8 位前导码、40 位地址、9 位控制字段、你自己那 8 位数据,再加 16 位 CRC:一共 81 位,在 1 Mbps 下就是 81 µs。再加上进入发射状态的那 130 µs,以及掉头去听应答的那段时间,整段爆发大约 0.4 ms,电流在 10 mA 的量级。电荷等于电流乘时间,所以这块模块要的,是在半毫秒之内拿到大约 4 µC。

电容上的电压会下降多少,等于取走的电荷除以容值,所以那 4 µC 直接把容值选出来了。模块上原本就有的那几颗瓷片电容,对这件事来说小得离谱:Nordic 给这颗芯片的官方参考设计,在供电脚旁边写的是 33 nF、10 nF 和 1 nF,而让 33 nF 独自扛下这段爆发,电压要掉 100 V 以上。它们是为纳秒级的边沿准备的,不是为这个 —— 所以同一份文档也叫你在它们旁边并上一颗大电容。10 µF 的电解电容掉 0.4 V;100 µF 掉 0.04 V。这个除法,就是满网都写 10–100 µF 的全部原因,也是为什么小的那一头是下限,而不是答案。

Uno 自己的稳压器补不上这个缺口,而它的标称值恰恰是最误导人的地方。Arduino 给 3V3 脚标的是 50 mA,这么看 11.3 mA 简直宽裕 —— 但 50 mA 是一个稳态数字。线性稳压器的反馈环要几十微秒才察觉到这一跳,而中间还隔着 20 cm 杜邦线带来的电阻和电感。稳压器永远赶到得太晚;而一颗焊在模块自己脚上的电容,本来就已经在现场了。

同一套算术也说得出这个坑有多深。附近只有几十纳法时,模块本地的那条电源轨在一段爆发里不是掉几百毫伏 —— 它是一路塌向芯片 1.9 V 的下限。nRF24L01 的功放和数字核心是由芯片内部的稳压器供电的,而任何稳压器一旦自己的输入没了,输出也就守不住,于是发出去的包是坏的,应答永远回不来,write() 报失败 —— 而 SPI 寄存器(是在两段爆发之间、以微安级的电流读的)却答得完美无缺。这也正是电容要跨在模块自己的引脚上、而不是 Uno 排针上的原因 —— 中间那段线,本身就是问题。

卡通对比图:两条电源电压曲线,上面一条深蓝色的线塌进一个很深的凹坑并配一个橙色箭头,上方那块黑色 nRF24L01 模块被打上禁止符号;下面一条青色的线只轻微下凹,旁边立着一颗带灰色竖条的黑色电解电容,同一块模块正在发出青色电波
同一段发射爆发,画了两遍。身边没有储能时,模块自己的供电在整个包的时间里都是塌的;有了它,同一段爆发只花掉几十毫伏。

电解电容是有极性的:外壳上那道竖条标的是负极腿,这条腿接 GND。装反了它会发热、会坏。任何 10–100 µF、耐压 6.3 V 以上的都能用;入门套件里除了瓷片电容,也带了电解电容。

怎么把它装上去,是大多数教程跳过的一步,因为模块的引脚上已经插着杜邦线的母头了。最好的做法:把电容的两条腿直接焊在 VCC 和 GND 引脚上 —— 手法看我们那篇排针焊接教程。免焊的做法:把模块的 VCC 和 GND 两根线引到面包板上,再把电容跨在这两排孔之间,这样它离芯片只有几厘米,而不是一整根杜邦线再加上 Uno 板上的走线。最差的做法:插在 Uno 的 3V3GND 孔上 —— 那等于把你本来想绕开的那整根杜邦线,又留在了路径里。两个节点各要一颗,这也是材料清单里有两块面包板的原因。

地址和管道到底在做什么?

RF24 库里的 openWritingPipe()openReadingPipe() 不是网络里的 socket。管道地址就是随每一个数据包一起飞过去的五个字节,接收端的芯片会在硬件里先比对它们,比对不上根本不会把 Arduino 叫醒。两端共用同一个地址,而同一个房间里的两条链路,靠使用不同的地址各走各的。它是一个过滤器,不是密码:任何人只要地址对得上,就能听见你。

角色来自两个函数调用,不是来自接线。stopListening() 让一个节点成为发送端,startListening() 让它成为接收端,把这两行在两份代码之间对调,硬件一根线都不用动。

应答这件事,是大多数人脑子里那张图崩掉的地方。当一个包到达并通过 CRC 校验,接收端会自动打开自己的发射机回一个应答。所以两端都在发,两端都需要那颗电容,而 write() 返回 true 是一份「送达回执」,不是一个「已发出」的标志。这也顺带解释了管道编号:0 号管道被留给这些应答,所以做集线的那一端用的是 1 到 5 号。这四个管道共用 1 号管道地址的前四个字节,只在最后一个字节上不同 —— 因为芯片只为这一组存了一个完整的五字节地址,之后每个管道只存一个字节。

卡通图:两块黑色 nRF24L01 模块各自标着一排完全相同的五个彩色方块,一支青色箭头把消息从一块送到另一块,一支橙色箭头把应答送回来;旁边第三块颜色变淡的模块标着另一串颜色,被一个橙色禁止符号挡住
地址就是随每一个包一起带过去的五个字节。手里拿着同样五个字节的两个节点会认出彼此、互相回应;拿着别的字节的第三个节点,在硬件里就被滤掉了,它的 Arduino 根本不会醒。

正是最后这个细节,能把这条双节点链路变成一群传感器向同一块板汇报的网络:

nRF24L01 的发送端和接收端代码长什么样?

两份代码都用 TMRh20 写的 RF24 库 —— 打开库管理器,搜 RF24,安装。把发送端烧进带按键的那块板,接收端烧进另一块,然后把两个串口监视器都开在 115200 —— 要开两个 IDE 窗口,因为一个窗口只盯一个串口。两块板一分开,就各自需要自己的电源:一根 USB 线、一个充电头,或者一个充电宝,每端一份。除此之外两个节点之间什么都不共享,连地线都不共享。

/*
  nRF24L01 远程按键 - 发送端节点(Arduino Uno + TMRh20 的 RF24 库)。

  nRF24L01 VCC -> Uno 3V3  (绝对不能 5V)      GND -> Uno GND
  CE  -> D9      CSN -> D10
  SCK -> D13     MOSI -> D11      MISO -> D12      IRQ -> 不接
  10-100uF 电解电容跨在模块自己的 VCC 和 GND 引脚上,带竖条那条腿接 GND。

  按键接在 D2 和 GND 之间。用 INPUT_PULLUP,所以不用外接电阻。
  状态一变就发一个字节,另外每 200 ms 再重发一次当前状态,这样
  接收端才能分得清「还按着」和「链路断了」。
*/

#include <SPI.h>
#include <RF24.h>

RF24 radio(9, 10);                 // CE, CSN

const byte address[6] = "MHUB1";   // 5 个字节;两个节点必须完全一致

const uint8_t  PIN_BUTTON   = 2;
const uint16_t DEBOUNCE_MS  = 25;  // 触点几毫秒就稳了;取 25 稳妥
const uint16_t HEARTBEAT_MS = 200; // 每隔这么久重发一次当前状态

uint8_t  stableState = 0;          // 1 = 按下
uint8_t  lastRaw     = 0;
uint32_t lastEdgeAt  = 0;
uint32_t lastSendAt  = 0;

void setup() {
  Serial.begin(115200);
  pinMode(PIN_BUTTON, INPUT_PULLUP);

  if (!radio.begin()) {
    // begin() 会读回一个寄存器。卡在这里就是接线或供电的问题,
    // 绝不是距离问题 —— 另一个节点这时候还没参与进来。
    Serial.println(F("radio not responding: check 3.3V, CE/CSN, capacitor"));
    while (1) {}
  }

  // RF24_PA_LOW 是 -12 dBm - 对一条虚弱的 3.3V 电源轨比 0 dBm 的
  // RF24_PA_MAX 温柔得多,等供电扎实了再换成后者。
  radio.setPALevel(RF24_PA_LOW);
  // RF24_250KBPS 用速率换接收灵敏度,但这个速率只有 nRF24L01+ 的
  // 芯片才有;被拒绝时 setDataRate() 会返回 false。
  radio.setDataRate(RF24_1MBPS);
  radio.setChannel(76);             // 2476 MHz - 在频段内,压在 Wi-Fi 1/6/11 之上
  radio.setPayloadSize(sizeof(stableState));
  radio.openWritingPipe(address);
  radio.stopListening();            // 这个节点负责说

  Serial.println(F("transmitter ready"));
}

void loop() {
  uint8_t  raw = (digitalRead(PIN_BUTTON) == LOW);  // 上拉着:LOW = 按下
  uint32_t now = millis();

  if (raw != lastRaw) {             // 只要抖一下,就把稳定计时重新开始
    lastRaw    = raw;
    lastEdgeAt = now;
  }

  bool changed = false;
  if (raw != stableState && (now - lastEdgeAt) >= DEBOUNCE_MS) {
    stableState = raw;
    changed     = true;
  }

  if (changed || (now - lastSendAt) >= HEARTBEAT_MS) {
    lastSendAt = now;

    // 只有接收端的自动应答回来了,write() 才返回 true,
    // 所以它是一份送达回执,不是「已发出」的标志。
    bool acked = radio.write(&stableState, sizeof(stableState));

    if (changed) {
      Serial.print(stableState ? F("pressed  ") : F("released "));
      Serial.println(acked ? F("- ack received") : F("- NO ack"));
    }
  }
}
/*
  nRF24L01 远程按键 - 接收端节点(Arduino Uno + TMRh20 的 RF24 库)。

  接线和发送端完全一样:
  VCC -> 3V3, GND -> GND, CE -> D9, CSN -> D10, SCK -> D13, MOSI -> D11,
  MISO -> D12,另加那颗跨在模块 VCC 和 GND 上的 10-100uF 电容。

  D3 是输出脚。它去驱动继电器模块的 IN 端子,或者一颗串了限流电阻
  的 LED。这里 D13 不能拿来做指示灯:在 Uno 上那支脚是 SPI 的 SCK,
  已经被射频占用了。
*/

#include <SPI.h>
#include <RF24.h>

RF24 radio(9, 10);                 // CE, CSN

const byte address[6] = "MHUB1";   // 和发送端同样的五个字节

const uint8_t  PIN_OUT     = 3;
const uint16_t FAILSAFE_MS = 1000; // 连丢 5 次心跳 = 链路没了

uint8_t  lastState = 0;
uint32_t lastHeard = 0;

void setup() {
  Serial.begin(115200);
  pinMode(PIN_OUT, OUTPUT);
  digitalWrite(PIN_OUT, LOW);      // 别的事情之前,先把输出压到关断

  if (!radio.begin()) {
    Serial.println(F("radio not responding: check 3.3V, CE/CSN, capacitor"));
    while (1) {}
  }

  radio.setPALevel(RF24_PA_LOW);    // 下面每一项设置都必须和发送端一致
  radio.setDataRate(RF24_1MBPS);
  radio.setChannel(76);
  radio.setPayloadSize(sizeof(lastState));
  radio.openReadingPipe(1, address);  // 1 号管道守在这个共用地址上
  radio.startListening();             // 这个节点负责听

  Serial.println(F("receiver listening"));
}

void loop() {
  if (radio.available()) {
    uint8_t state = 0;
    radio.read(&state, sizeof(state));
    lastHeard = millis();

    if (state != lastState) {
      lastState = state;
      digitalWrite(PIN_OUT, state ? HIGH : LOW);
      Serial.println(state ? F("output ON") : F("output OFF"));
    }
  }

  // 一整秒没听见动静:发送端要么关了,要么出了距离,要么挂了。
  // 与其把输出一直卡在开着,不如放掉它。
  if (lastState && (millis() - lastHeard) > FAILSAFE_MS) {
    lastState = 0;
    digitalWrite(PIN_OUT, LOW);
    Serial.println(F("link lost - output released"));
  }
}

在 Uno 上,发送端编译出来是 3,802 字节 Flash 和 244 字节 RAM;接收端是 3,424 和 239。输出脚是 D3,而且是刻意不用 D13 的:在 Uno 上那支脚是 SPI 时钟,已经被射频占着,板载 LED 会跟着通信一起乱闪,根本没法当指示灯。把 D3 接到继电器模块IN 螺丝端子,同时也别忘了给模块本身供电:DC+ 接 Uno 的 5 V,DC- 接 GND。板子没供上电,IN 上再有信号也切不动任何东西。H/L 触发的跳线帽要设在 H,因为这份代码的关断状态是 D3 为 LOW —— 上电时就主动驱动成 LOW,失效保护一触发也回到 LOW —— 而在 L 那一档,上面每一个「关断」的瞬间反而会让继电器吸合,于是链路一断,负载不是被关掉而是被打开。切换的那一侧请只走低压直流;市电有它自己那一整套关于外壳、爬电距离和线材规格的要求,动手前先读我们那篇继电器模块安全指南

心跳和失效保护是一对:发送端每 200 ms 重发一次按键状态,接收端在静默一秒之后放掉自己的输出,所以电池耗尽、或者人走出了距离,负载是被关掉,而不是就那样一直卡在开着。把按键换成摇杆,同一套结构就能带两个轴。

PCB 天线版还是外接天线版 —— 该买哪一个?

NRF24RF(PCB 天线) NRF24AN(外接天线版)
天线 板上蚀刻出来的金色走线 SMA 座,胶棒天线拧上去
发射 裸芯片,最高 0 dBm 带功率放大器,约 +20 dBm
接收 裸芯片 芯片前面多一级低噪声放大器
发射峰值电流 11.3 mA 大约 115 mA
Uno 的 3V3 脚带得动吗 能,配上那颗电容 不能 —— 要单独给它一路 3.3 V
适合 同一个房间、隔壁房间、一间小屋子里 跨过一片空地、楼与楼之间

NRF24AN 的优势是算出来的,不是吹出来的。功放在发射端大约加 20 dB,低噪声放大器在接收端大约加 10 dB,所以整条链路预算改善约 30 dB。距离每翻一倍,自由空间损耗涨 6 dB —— 因为同样的功率摊到了四倍的面积上 —— 所以 30 dB 就是五次翻倍,大约三十倍的距离。这正是为什么空旷场地的数字动辄几百米,而普通模块只有几十米。那些最大值都是清晰通视条件下量的,而墙体和人体,每一样都要从这份预算里吃掉好几个 dB。

距离是用功耗换来的。115 mA 是 Uno 那个 3V3 脚额定值的两倍多,所以把它直接挂上去只会让塌压更严重,而不是更好:让它跑在 RF24_PA_LOW,电容至少上 100 µF,再给它一路专用的 3.3 V 电源,地和 Uno 的地接在一起。两端一起升级,因为有应答就意味着远端那块也要发 —— 然后再把两端拉开距离才去评判效果,因为低噪声放大器是为微弱信号设计的,两块带放大器的模块隔着一掌宽会把彼此的前端灌爆。第一次测试就给它们隔开几米。还有一个细节直接决定天线能不能用 —— 模块上的座子是标准 SMA,而市面上多数闲置的 Wi-Fi 天线是 RP-SMA。反极性换的是里面的触点、不是螺纹,所以两边都成了「孔」,拧得上去,本该有针的地方却只有空气。

卡通俯视对比图:两块黑色 nRF24L01 模块 —— 左边那块较短,带一条金色之字形 PCB 天线走线和几道较短的青色电波;右边那块较长,带一个金色 SMA 座、一根黑色可拧上的棒状天线、两颗芯片,以及长得多的青色电波
从上往下看这两个 nRF24L01 版本:左边是蚀刻在板子上的金色天线,右边是金色 SMA 座加一根拧上去的黑色天线。长的那块上有两颗芯片,因为放大器本身就是第二颗芯片。

什么时候 nRF24L01 是选错了?

nRF24L01 是一条近距离、高速率的链路,有三类活儿更适合别的东西。要跨几公里、每分钟只送几个字节 —— 一整片园丘那头的水箱、一条长车道尽头的门磁 —— 那是 LoRa 的地盘:一种用数据率换距离的亚 GHz 扩频电台,是另一种设计,不是「更大号的 nRF24」,而这个我们没有卖。

如果链路有一端是手机,那你要的是 Bluetooth,而不是一种手机根本没有的电台 —— 配对怎么做,看我们那篇 HC-05 教程,而蓝牙遥控小车就是同一套东西再去驱动电机。如果两端本来就都是 ESP32,那 ESP-NOW 用的是你已经付过钱的 Wi-Fi 电路,一个额外模块都不用加。nRF24L01 赢的正是这一格:两颗单片机,隔着一个房间或者一间屋子,而且便宜到两端都装得起。

我们从真实客户那里看到的常见错误

把 5 V 接到 VCC 脚上。逻辑脚确实是耐 5 V 的;供电脚的上限却是 3.6 V。插错一个排针孔,这块模块就再也不会应 begin() 了。

因为「在桌上是好的」就把电容省掉。近距离的一个短包,撑得住一条正在塌的电源轨;同一条链路隔三个房间就撑不住了。

两份代码对不上。频道、数据率、地址和负载长度都必须一致,而两端都调 startListening(),就成了两个接收端、没有发送端。这两种错都是无声的 —— 不报任何错,只是接收端永远什么都不报。

常见问题

为什么 radio.write() 总是返回 false?

因为没有应答回来:要么接收端的设置不一样,要么发送端的供电在那段爆发里塌了。先把电容装上,再去对两份代码。

能用 Arduino 的 5 V 脚给 nRF24L01 供电吗?

不能。VCC 的绝对最大值是 3.6 V。请用 3V3 —— 信号脚耐 5 V,供电脚不耐。

电容应该选多大?

10–100 µF 电解电容,耐压 6.3 V 以上,跨在模块自己的 VCC 和 GND 引脚上,带竖条那条腿接 GND。外接天线版请用 100 µF。

两块模块必须是同一款吗?

不必,它们能好好配对。但距离是由弱的那一端定的,所以只升级一头,买到的效果远比想象的少。

两块模块在室内能通多远?

一对 PCB 天线的模块,覆盖一个房间绰绰有余,通常还能再穿过一两堵墙。我们这里的房子是砖和钢筋混凝土,不是木架加石膏板,所以同样一堵墙,吃掉的信号比外国视频里那堵墙更多。如果比这还早就断,先查供电,再去怪距离。

最后更新:2026 年 8 月。有问题?WhatsApp 联系我们

Leave a Reply

Your email address will not be published. Required fields are marked *