ELECTRONICS

Библиотека RF24

Обновлено

В менеджере библиотек
RF24 by TMRh20
Версия
1.6.2 · всего 39 версий, первая — 1.0.0
Платы
любые — автор не ограничивал
СкачатьRF24-1.6.2.zip · 5,6 МБ

Архивы — копии из официального индекса Arduino, контрольная сумма совпадает. Сверено 19.10.2026.

Радиомодули nRF24L01+: связь двух и более плат на 2,4 ГГц пакетами до 32 байт.

nRF24L01+ — дешёвый приёмопередатчик на 2,4 ГГц с дальностью от десятков метров до километра у версии с усилителем. Библиотека RF24 прячет регистры чипа за понятной моделью: у каждого получателя есть адрес-«труба», отправитель открывает трубу на запись и шлёт структуру, получатель слушает и читает.

Пакет — до 32 байт. Удобнее всего слать структуру целиком: sizeof(data) с обеих сторон должен совпадать.

Как скачать и установить

  1. Через менеджер — самый простой путь. В Arduino IDE: Инструменты → Управлять библиотеками (Ctrl+Shift+I). Введите RF24 и найдите в списке строку «RF24 by TMRh20» — одноимённых библиотек бывает несколько, отличаются они автором. Нажмите «Установить».
  2. Конкретная версия — если в чужом проекте указана не последняя. В той же карточке менеджера рядом с кнопкой есть выпадающий список версий: выберите нужную и нажмите «Установить».
  3. Из архива — если нет интернета на рабочем компьютере или менеджер не открывается. Скачайте ZIP выше, затем Скетч → Подключить библиотеку → Добавить .ZIP библиотеку и укажите файл. Распаковывать не нужно.

Файл RF24.h отдельно не скачивают: он внутри библиотеки и появляется после установки. Если среда пишет RF24.h: No such file or directory — библиотека не установлена или установлена не та. Как устроен менеджер и куда складываются файлы — в общей статье про библиотеки.

Строка подключения:

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

Создание объекта — обычно до setup():

RF24 radio(9, 10);                           // CE, CSN
const byte address[6] = "NODE1";             // адрес трубы, 5 символов
radio.begin();                               // в setup()
radio.openWritingPipe(address);              // передатчик
// или
radio.openReadingPipe(1, address);           // приёмник
radio.startListening();

Подключение к Arduino

Схема подключения nRF24L01+ к Arduino UNO: VCC к 3,3 В, GND к GND, CE к пину 9, CSN к пину 10, SCK к 13, MOSI к 11, MISO к 12
Схема с пояснением к каждому проводу — на странице модуля «Радиомодуль nRF24L01+».
VCC
UNO, Nano: 3,3 ВMega: 3,3 В

Питание. Только 3,3 В — от 5 В чип сгорает.

GND
UNO, Nano: GNDMega: GND

Земля.

CE
UNO, Nano: 9Mega: 9

Включает приём или передачу. Любой цифровой вывод — первый аргумент RF24 radio(9, 10).

CSN
UNO, Nano: 10Mega: 10

Выбор модуля на шине SPI. Любой цифровой — второй аргумент.

SCK
UNO, Nano: 13Mega: 52

Такт SPI — только этот вывод.

MOSI
UNO, Nano: 11Mega: 51

Данные к модулю — только этот вывод.

MISO
UNO, Nano: 12Mega: 50

Данные от модуля — только этот вывод.

IRQ
UNO, Nano: —Mega: —

Сигнал прерывания. В обычных скетчах не подключают.

Конденсатор обязателен

Электролит 10–100 мкФ между VCC и GND прямо на выводах модуля. Без него модуль сбрасывается в момент передачи: begin() проходит, а write() через раз отвечает false.

Сигналы 5 В — можно, питание 5 В — нельзя

Входы nRF24L01+ терпят 5-вольтовую логику, поэтому SPI от UNO подключают напрямую, без делителей. А вот VCC — только к 3,3 В.

Версия с антенной и усилителем

PA+LNA в передаче берёт до 115 мА — вывода 3,3 В платы не хватает. Её ставят на переходник с собственным стабилизатором: он питается от 5 В, а модуль получает свои 3,3 В.

Методы

Только то, что нужно в обычном скетче. Сигнатуры сверены с версией 1.6.2.

Запуск

bool begin()

Включить модуль и выставить настройки по умолчанию: канал 76, 1 Мбит/с, 15 повторов по 1,5 мс, пакет 32 байта. Вызывают в setup() раньше любых других методов.

Возвращает: true — модуль ответил по SPI. false — не отвечает: питание не 3,3 В, перепутаны CE и CSN или отходит провод.

if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }

bool isChipConnected()

Проверить, что чип на месте, в любой момент работы — begin() смотрит только при запуске.

Возвращает: true — регистры чипа читаются.

if (!radio.isChipConnected()) Serial.println("Модуль отвалился");

bool isPVariant()

Узнать, какой чип на модуле: nRF24L01+ или старый без плюса. От этого зависит, доступны ли 250 кбит/с и testRPD().

Возвращает: true — nRF24L01+ или совместимый клон.

Serial.println(radio.isPVariant() ? "nRF24L01+" : "без плюса");

Адреса и режим

void openWritingPipe(const uint8_t* address)

Задать адрес получателя — туда уйдут все следующие write().

address
пять байт адреса. Удобно записать строкой: const byte address[6] = "NODE1" — шесть байт с нулём в конце, библиотека берёт первые пять. В старых скетчах адрес записан числом, 0xF0F0F0F0E1LL: так ещё работает, но в версии 1.6 этот вариант помечен устаревшим.
radio.openWritingPipe(address);

void openReadingPipe(uint8_t number, const uint8_t* address)

Слушать адрес на одной из труб. Труб шесть, поэтому один приёмник принимает до шести передатчиков сразу.

number
номер трубы 0–5. Трубу 0 модуль занимает под подтверждения, пока передаёт, но библиотека сама возвращает ей адрес приёма при каждом startListening() — так что открывать её на приём можно.
address
у трубы 1 — полный адрес из пяти байт. У труб 2–5 свой только первый байт, остальные четыре берутся от трубы 1: "1NODE", "2NODE" и так далее.
radio.openReadingPipe(1, address);

void closeReadingPipe(uint8_t pipe)

Перестать слушать трубу. Можно звать и для трубы, которая не открывалась, — ошибки не будет.

pipe
номер трубы 0–5; другие числа молча игнорируются.
radio.closeReadingPipe(2);

void setAddressWidth(uint8_t a_width)

Длина адреса. Короче адрес — короче пакет в эфире, но выше шанс принять шум за свой пакет. Задаётся одинаково на обеих платах.

a_width
3, 4 или 5 байт, по умолчанию 5.
radio.setAddressWidth(4);

void startListening()

Перейти в приём. Только после него available() начинает видеть пакеты.

radio.startListening();

void stopListening()

Выйти из приёма. Перед write() обязательно: модуль, который слушает, не передаёт.

radio.stopListening();

Передача и приём

bool write(const void* buf, uint8_t len)

Отправить пакет и дождаться подтверждения от приёмника. Пока идут повторы, скетч стоит — до 60–70 мс при настройках по умолчанию.

buf
что отправить: &переменная или имя массива.
len
сколько байт — обычно sizeof(переменной), не больше 32.

Возвращает: true — приёмник подтвердил получение. false — подтверждения нет после всех повторов. Если подтверждения выключены setAutoAck(false), всегда true, даже когда никто не принял.

bool ok = radio.write(&p, sizeof(p));

bool available()

Проверить, есть ли принятый пакет. Опрашивают в loop(); в очереди модуля помещается три пакета, дальше новые теряются.

Возвращает: true — в очереди есть пакет.

if (radio.available()) radio.read(&p, sizeof(p));

bool available(uint8_t* pipe_num)

То же, плюс номер трубы, по которой пришёл пакет, — так приёмник узнаёт, от какого передатчика.

pipe_num
адрес переменной, куда записать номер трубы 0–5.

Возвращает: true — в очереди есть пакет, номер трубы записан.

uint8_t pipe;
if (radio.available(&pipe)) Serial.println(pipe);

void read(void* buf, uint8_t len)

Забрать пакет из очереди в переменную. Ничего не возвращает — есть ли пакет, проверяют заранее через available().

buf
куда положить: &переменная того же типа, что шлёт передатчик.
len
sizeof(переменной). Меньше пакета — пакет останется в очереди; больше — хвост заполнится повтором последнего байта.
radio.read(&p, sizeof(p));

bool rxFifoFull()

Проверить, не забита ли очередь приёма. Когда заняты все три места, новые пакеты модуль отбрасывает — значит, loop() читает медленнее, чем шлют.

Возвращает: true — все три места заняты.

if (radio.rxFifoFull()) Serial.println("Не успеваю читать");

Ответ приёмника

void enableAckPayload()

Разрешить приёмнику класть данные в подтверждение. Так передатчик получает ответ, не переключая обе платы между приёмом и передачей. Вызывают на обеих платах в setup().

radio.enableDynamicPayloads();
radio.enableAckPayload();

bool writeAckPayload(uint8_t pipe, const void* buf, uint8_t len)

Зарядить ответ на приёмнике. Он уйдёт со следующим принятым пакетом, поэтому первый ответ заряжают заранее, в setup(), а после каждого приёма заряжают новый. Передатчик читает ответ обычными available() и read() сразу после write().

pipe
труба, по которой придёт пакет от передатчика, — та же, что в openReadingPipe().
buf
что ответить: &переменная.
len
sizeof(переменной), до 32 байт.

Возвращает: true — ответ встал в очередь. false — очередь занята (ждать отправки могут не больше трёх ответов) или не вызван enableAckPayload().

radio.writeAckPayload(1, &reply, sizeof(reply));

void enableDynamicPayloads()

Пакеты переменной длины на всех трубах: в эфир уходит столько байт, сколько передано в write(), а не всегда 32. Для ответов приёмника обязательно. Включают на обеих платах.

radio.enableDynamicPayloads();

uint8_t getDynamicPayloadSize()

Длина пришедшего пакета — когда включены пакеты переменной длины. Зовут после available() и до read().

Возвращает: число байт в пакете, 1–32.

uint8_t n = radio.getDynamicPayloadSize();

Настройка связи — одинаково на обеих платах

void setPALevel(uint8_t level, bool lnaEnable = 1)

Мощность передатчика. Для отладки на столе — минимальная: на максимальной модули в полуметре друг от друга глушат приём. Для дальности — максимальная, но тогда без конденсатора по питанию модуль сбрасывается.

level
RF24_PA_MIN (−18 дБм), RF24_PA_LOW (−12), RF24_PA_HIGH (−6), RF24_PA_MAX (0 дБм). Если не задать — остаётся та, с которой чип включился, обычно максимальная.
lnaEnable
усилитель приёма. Влияет только на клоны на чипе Si24R1, обычно не указывают.
radio.setPALevel(RF24_PA_LOW);

bool setDataRate(rf24_datarate_e speed)

Скорость в эфире. Чем медленнее, тем дальше: при той же мощности 250 кбит/с бьёт заметно дальше 2 Мбит/с.

speed
RF24_1MBPS — по умолчанию, RF24_2MBPS — быстрее и ближе, RF24_250KBPS — медленнее и дальше.

Возвращает: true — скорость принята. false — модуль её не умеет: 250 кбит/с есть только у nRF24L01 с плюсом.

radio.setDataRate(RF24_250KBPS);

void setChannel(uint8_t channel)

Частота: 2400 МГц плюс номер канала. Wi-Fi занимает нижнюю часть диапазона, поэтому помехи уходят на каналах от 100.

channel
0–125, по умолчанию 76 (2476 МГц).
radio.setChannel(108);

void setRetries(uint8_t delay, uint8_t count)

Сколько раз повторять пакет без подтверждения и с какой паузой. Больше повторов — надёжнее, но write() дольше держит скетч.

delay
0–15, пауза (delay + 1) × 250 мкс. По умолчанию 5 — 1,5 мс.
count
0–15, по умолчанию 15. 0 — не повторять вовсе.
radio.setRetries(5, 15);

void setPayloadSize(uint8_t size)

Размер пакета. Короче пакет — быстрее доходит. Должен совпадать на передатчике и приёмнике, иначе пакеты не принимаются.

size
1–32 байта, по умолчанию 32.
radio.setPayloadSize(sizeof(Packet));

void setAutoAck(bool enable)

Включить или выключить подтверждения на всех трубах. Без подтверждений нет повторов и ответов приёмника, а write() всегда отвечает true. Выключают для широковещания на много приёмников и в сканере эфира.

enable
true — подтверждать, по умолчанию; false — не подтверждать. Должно совпадать на обеих платах.
radio.setAutoAck(false);

void setAutoAck(uint8_t pipe, bool enable)

То же для одной трубы. Трубу 0 модуль использует при передаче, поэтому, если передатчику нужны подтверждения, на ней их не выключают.

pipe
номер трубы 0–5.
enable
true — подтверждать, false — нет.
radio.setAutoAck(2, false);

void setCRCLength(rf24_crclength_e length)

Длина контрольной суммы пакета. По ней модуль отбрасывает пакеты, испорченные помехой.

length
RF24_CRC_16 — по умолчанию, RF24_CRC_8 — пакет на байт короче, RF24_CRC_DISABLED — без проверки. Выключить не получится, пока включены подтверждения.
radio.setCRCLength(RF24_CRC_8);

Текущие настройки

uint8_t getChannel()

Прочитать канал из чипа. Удобно сверить две платы, когда связи нет.

Возвращает: 0–125.

Serial.println(radio.getChannel());

uint8_t getPALevel()

Прочитать мощность из чипа.

Возвращает: RF24_PA_MIN, RF24_PA_LOW, RF24_PA_HIGH или RF24_PA_MAX — числа 0–3.

if (radio.getPALevel() == RF24_PA_MAX) Serial.println("Максимум");

rf24_datarate_e getDataRate()

Прочитать скорость из чипа.

Возвращает: RF24_1MBPS, RF24_2MBPS или RF24_250KBPS.

if (radio.getDataRate() != RF24_250KBPS) Serial.println("Скорость не та");

uint8_t getPayloadSize()

Прочитать размер пакета, заданный setPayloadSize().

Возвращает: 1–32.

Serial.println(radio.getPayloadSize());

Питание от батарейки

void powerDown()

Усыпить модуль. В сне он берёт около 0,9 мкА вместо 13,5 мА в режиме приёма. Настройки при этом не теряются.

radio.powerDown();

void powerUp()

Разбудить модуль после powerDown(). Просыпается до 5 мс — библиотека выжидает это время сама.

radio.powerUp();

Эфир: помехи и сканер

bool testRPD()

Есть ли на канале сигнал сильнее −64 дБм — чей угодно, не только наш. Так строят сканер эфира: слушают канал 128 мкс и проверяют. Только у nRF24L01+.

Возвращает: true — на канале кто-то есть.

radio.setChannel(ch);
radio.startListening();
delayMicroseconds(128);
radio.stopListening();
bool busy = radio.testRPD();

bool testCarrier()

То же для старого чипа без плюса: была ли несущая за время последнего прослушивания.

Возвращает: true — несущая была.

bool busy = radio.testCarrier();

Быстрая передача и очереди

bool writeFast(const void* buf, uint8_t len)

Поставить пакет в очередь передачи и не ждать подтверждения — пока в очереди есть место, скетч не стоит. Для потока данных; после серии обязательно txStandBy().

buf
что отправить: &переменная.
len
sizeof(переменной), до 32 байт.

Возвращает: true — пакет в очереди. false — предыдущий пакет так и не доставлен; зовите txStandBy().

for (int i = 0; i < 10; i++) radio.writeFast(&p, sizeof(p));
bool ok = radio.txStandBy();

bool txStandBy()

Дождаться, пока очередь передачи опустеет, и перевести модуль в покой. Вариант txStandBy(1000) повторяет недоставленное до тайм-аута в миллисекундах.

Возвращает: true — все пакеты доставлены. false — какой-то не подтверждён.

if (!radio.txStandBy()) Serial.println("Часть не дошла");

uint8_t flush_tx()

Очистить очередь передачи — все три места. Нужно, если пакет застрял и мешает следующим.

Возвращает: регистр состояния чипа; обычно не нужен.

radio.flush_tx();

uint8_t flush_rx()

Очистить очередь приёма: выбросить непрочитанные пакеты, например накопившиеся за время настройки.

Возвращает: регистр состояния чипа; обычно не нужен.

radio.flush_rx();

Отладка

void printDetails()

Напечатать все регистры чипа. Печатает через printf, поэтому без #include <printf.h> и printf_begin() в setup() молчит. Нули или FF во всех строках — чип не отвечает.

printf_begin();
radio.printDetails();

void printPrettyDetails()

То же словами: канал, скорость, мощность, адреса труб. Удобнее для чтения, но занимает больше памяти. Условие то же — printf_begin().

radio.printPrettyDetails();

uint8_t getARC()

Сколько повторов понадобилось последнему write(). Грубая оценка связи: 0 — отлично, под 15 — на пределе.

Возвращает: 0–15.

radio.write(&p, sizeof(p));
Serial.println(radio.getARC());

Примеры

Связь двух плат

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

RF24 radio(9, 10);
const byte address[6] = "NODE1";
const bool TRANSMITTER = true;      // на второй плате — false

// типы фиксированной ширины: у UNO и ESP32 int разной длины
struct Packet { uint32_t ms; int16_t value; };

void setup() {
  Serial.begin(9600);
  if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  if (TRANSMITTER) {
    radio.openWritingPipe(address);
    radio.stopListening();
  } else {
    radio.openReadingPipe(1, address);
    radio.startListening();
  }
}

void loop() {
  if (TRANSMITTER) {
    Packet p = { millis(), (int16_t)analogRead(A0) };
    bool ok = radio.write(&p, sizeof(p));
    Serial.println(ok ? "Доставлено" : "Нет ответа");
    delay(500);
  } else if (radio.available()) {
    Packet p;
    radio.read(&p, sizeof(p));
    Serial.print(p.ms); Serial.print(": "); Serial.println(p.value);
  }
}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Один скетч на обе платы: меняется только константа TRANSMITTER. Так проще держать структуру пакета одинаковой.

Монитор порта:

Передатчик:
Доставлено
Доставлено
Нет ответа        ← приёмник выключен или далеко

Приёмник — время отправки и показание A0:
10503: 512
11006: 511
11509: 514

Приём от нескольких передатчиков

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

RF24 radio(9, 10);
// у передатчиков различается только первый байт адреса
const byte addresses[][6] = { "1NODE", "2NODE", "3NODE" };
const uint8_t MY_ID = 0;   // 0 — приёмник, 1, 2, 3 — передатчики

struct Packet { uint8_t id; int16_t value; };

void setup() {
  Serial.begin(9600);
  if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
  radio.setPALevel(RF24_PA_LOW);
  if (MY_ID == 0) {
    for (uint8_t i = 0; i < 3; i++) radio.openReadingPipe(i + 1, addresses[i]);
    radio.startListening();
  } else {
    radio.openWritingPipe(addresses[MY_ID - 1]);
    radio.stopListening();
  }
}

void loop() {
  if (MY_ID == 0) {
    uint8_t pipe;
    if (radio.available(&pipe)) {
      Packet p;
      radio.read(&p, sizeof(p));
      Serial.print("Труба "); Serial.print(pipe);
      Serial.print(", плата "); Serial.print(p.id);
      Serial.print(": "); Serial.println(p.value);
    }
  } else {
    Packet p = { MY_ID, (int16_t)analogRead(A0) };
    radio.write(&p, sizeof(p));
    delay(1000 + MY_ID * 70);   // разный период — передатчики реже совпадают в эфире
  }
}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Один приёмник слушает три трубы, у каждого передатчика свой адрес. Номер трубы из available(&pipe) говорит, кто прислал пакет, даже если id в пакете не передавать. Труб шесть — столько же передатчиков можно подключить к одному приёмнику.

Монитор порта:

Труба 1, плата 1: 512
Труба 2, плата 2: 347
Труба 3, плата 3: 1023
Труба 1, плата 1: 515

Ответ приёмника без смены ролей

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

RF24 radio(9, 10);
const byte address[6] = "NODE1";
const bool TRANSMITTER = true;   // на второй плате — false

int16_t counter = 0;   // передатчик: что шлём
int16_t reply = 0;     // приёмник: чем отвечаем

void setup() {
  Serial.begin(9600);
  if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
  radio.setPALevel(RF24_PA_LOW);
  radio.enableDynamicPayloads();   // ответы бывают только переменной длины
  radio.enableAckPayload();
  if (TRANSMITTER) {
    radio.openWritingPipe(address);
    radio.stopListening();
  } else {
    radio.openReadingPipe(1, address);
    radio.writeAckPayload(1, &reply, sizeof(reply));   // первый ответ — заранее
    radio.startListening();
  }
}

void loop() {
  if (TRANSMITTER) {
    counter++;
    if (!radio.write(&counter, sizeof(counter))) {
      Serial.println("Нет связи");
    } else if (radio.available()) {
      int16_t answer;
      radio.read(&answer, sizeof(answer));
      Serial.print("Отправил "); Serial.print(counter);
      Serial.print(", ответ "); Serial.println(answer);
    } else {
      Serial.println("Доставлено, ответа нет");
    }
    delay(1000);
  } else if (radio.available()) {
    int16_t got;
    radio.read(&got, sizeof(got));
    reply = got * 10;                                  // уйдёт со следующим пакетом
    radio.writeAckPayload(1, &reply, sizeof(reply));
  }
}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Приёмник отвечает прямо в подтверждении — передатчику не нужно переходить в приём. Ответ заряжается заранее и уходит со следующим пакетом, поэтому в выводе он отстаёт на шаг: на 2 приходит ответ на 1. Если ответы не нужны свежими, это не мешает; если нужны — передатчик шлёт второй короткий пакет-запрос.

Монитор порта:

Отправил 1, ответ 0
Отправил 2, ответ 10
Отправил 3, ответ 20
Отправил 4, ответ 30

Сканер эфира: какой канал выбрать

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

RF24 radio(9, 10);
const uint8_t CHANNELS = 126;
uint8_t hits[CHANNELS];

void setup() {
  Serial.begin(115200);
  if (!radio.begin()) { Serial.println("Модуль не найден"); while (1); }
  radio.setAutoAck(false);   // не отвечать на чужие пакеты
  radio.startListening();
  radio.stopListening();
  radio.flush_rx();
  // шапка: номер канала сверху вниз — сотни, десятки, единицы
  for (uint8_t i = 0; i < CHANNELS; i++) Serial.print(i / 100);
  Serial.println();
  for (uint8_t i = 0; i < CHANNELS; i++) Serial.print(i / 10 % 10);
  Serial.println();
  for (uint8_t i = 0; i < CHANNELS; i++) Serial.print(i % 10);
  Serial.println();
}

void loop() {
  memset(hits, 0, sizeof(hits));
  for (uint8_t pass = 0; pass < 20; pass++) {
    for (uint8_t ch = 0; ch < CHANNELS; ch++) {
      radio.setChannel(ch);
      radio.startListening();
      delayMicroseconds(128);
      radio.stopListening();
      if (radio.testRPD()) hits[ch]++;
    }
  }
  // строка на проход: «-» — тихо, цифра — сколько раз из 20 канал был занят (9 — девять и больше)
  for (uint8_t ch = 0; ch < CHANNELS; ch++) {
    if (hits[ch] == 0) Serial.print('-');
    else Serial.print(min(hits[ch], (uint8_t)9));
  }
  Serial.println();
}
Запустить в симулятореСхема с этим скетчем — запустить в браузере

Нужна одна плата с модулем. Столбец — канал, строка — один проход по всем 126: прочерк — тихо, цифра — сколько раз из двадцати канал был занят. Выбирайте канал, где столбец из прочерков. testRPD() есть только у nRF24L01+; на старом чипе замените на testCarrier(). В выводе ниже — как выглядит роутер на 6-м канале Wi-Fi: он занимает 2426–2448 МГц, то есть каналы 26–48.

Монитор порта:

000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111111111111111111111111
000000000011111111112222222222333333333344444444445555555555666666666677777777778888888888999999999900000000001111111111222222
012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345
--------------------------216339999999999999353-1-----------------------------------------------------------------------------
--------------------------1166399999999999994361------------------------------------------------------------------------------
--------------------------11363999999999999943422-----------------------------------------------------------------------------

На чём попадаются

begin() возвращает false или printDetails() печатает нули

Модуль не виден по SPI. Питание строго 3,3 В, CE и CSN как в конструкторе, провода короткие.

Работает рядом, теряет пакеты на расстоянии

Просадка питания при передаче. Конденсатор 10–100 мкФ прямо на выводах питания модуля — обязательная деталь.

Приёмник молчит

Адреса, канал и скорость должны совпадать на обеих платах, а приёмник — вызвать startListening().

Пакет приходит с мусором

Разный размер структуры на платах — например, int на UNO и на ESP32 разной длины. Используйте типы фиксированной ширины: int16_t, uint32_t.

Где используется на сайте

Что дальше