Привет хабровчане! Не так давно попалась мне в руки пара плат Arduino Nano со встроенным модулем NRF24L01, которые оказались достойной заменой популярной связки Arduino Nano + NRF24L01. Модуль NRF24L01 часто используется в различных проектах для обеспечения надежной беспроводной передачи данных. Небольшая цена, низкая задержка и энергопотребление, а также возможность выбора до128 каналов связи дает NRF24L01 преимущество перед другими радиочастотными модулями, такими как wifi, bluetooth, Zigbee и т.д.

В данной статье хочу поделиться с вами своим первым опытом работы как с Arduino RF, так и с NRF24L01 в целом.

Изображенную выше плату можно приобрести на Aliexpress. Данная плата является аналогом следующей схемы:

Для тестирования схемы я использую библиотеку RF24. В рамках данного обзора я рассмотрю:

  • передачу данных между платами Arduino RF;

  • передачу данных между Arduino RF и Raspberry Pi;

  • сравнение со связкой Arduino + модуль NRF24L01.

Передача данных между платами Arduino RF

Обе платы Arduino RF подключаются к портам одного ноутбука. Для работы с платами я использую среду Arduino Studio, в которой выполняю следующие настройки:

Tools -> Boards-> Arduino AVR Boards->Arduino Nano

Tools -> Processor-> ATmega328P (Old Bootloader)

Tools -> Managie Libraries-> "RF24" -> установка последней версии библиотеки RF24 by TMRh20 ( у меня версия 1.4.1). Также понадобятся библиотеки SPI.h и printf.h

работа с разными портами в Arduino Studio

Если у вас есть проблема одновременного открытия двух окон SerialMonitor, в которые выводится информация от двух Arduino, подключенных к разным портам, нужно сначала запустить ArduinoStuio в обычном режиме и отобразить информацию с одного порта, а потом запустить среду ArduinoStuio в режиме "от администратора"  и отобразить информацию с другого порта.

Для проверки плат использовался пример, поставляемый с библиотекой RF24, который нужно загрузить на обе платы Arduino.

Files-> Examples-> GettingStarted

код программы GettingStarted.ino
/*
 * See documentation at https://nRF24.github.io/RF24
 * See License information at root directory of this library
 * Author: Brendan Doherty (2bndy5)
 */

/**
 * A simple example of sending data from 1 nRF24L01 transceiver to another.
 *
 * This example was written to be used on 2 devices acting as "nodes".
 * Use the Serial Monitor to change each node's behavior.
 */
#include <SPI.h>
#include "printf.h"
#include "RF24.h"

// instantiate an object for the nRF24L01 transceiver
RF24 radio(7, 8); // using pin 7 for the CE pin, and pin 8 for the CSN pin

// Let these addresses be used for the pair
uint8_t address[][6] = {"1Node", "2Node"};
// It is very helpful to think of an address as a path instead of as
// an identifying device destination

// to use different addresses on a pair of radios, we need a variable to
// uniquely identify which address this radio will use to transmit
bool radioNumber = 1; // 0 uses address[0] to transmit, 1 uses address[1] to transmit

// Used to control whether this node is sending or receiving
bool role = false;  // true = TX role, false = RX role

// For this example, we'll be using a payload containing
// a single float number that will be incremented
// on every successful transmission
float payload = 0.0;

void setup() {

  Serial.begin(115200);
  while (!Serial) {
    // some boards need to wait to ensure access to serial over USB
  }

  // initialize the transceiver on the SPI bus
  if (!radio.begin()) {
    Serial.println(F("radio hardware is not responding!!"));
    while (1) {} // hold in infinite loop
  }

  // print example's introductory prompt
  Serial.println(F("RF24/examples/GettingStarted"));

  // To set the radioNumber via the Serial monitor on startup
  Serial.println(F("Which radio is this? Enter '0' or '1'. Defaults to '0'"));
  while (!Serial.available()) {
    // wait for user input
  }
  char input = Serial.parseInt();
  radioNumber = input == 1;
  Serial.print(F("radioNumber = "));
  Serial.println((int)radioNumber);

  // role variable is hardcoded to RX behavior, inform the user of this
  Serial.println(F("*** PRESS 'T' to begin transmitting to the other node"));

  // Set the PA Level low to try preventing power supply related problems
  // because these examples are likely run with nodes in close proximity to
  // each other.
  radio.setPALevel(RF24_PA_LOW);  // RF24_PA_MAX is default.

  // save on transmission time by setting the radio to only transmit the
  // number of bytes we need to transmit a float
  radio.setPayloadSize(sizeof(payload)); // float datatype occupies 4 bytes

  // set the TX address of the RX node into the TX pipe
  radio.openWritingPipe(address[radioNumber]);     // always uses pipe 0

  // set the RX address of the TX node into a RX pipe
  radio.openReadingPipe(1, address[!radioNumber]); // using pipe 1

  // additional setup specific to the node's role
  if (role) {
    radio.stopListening();  // put radio in TX mode
  } else {
    radio.startListening(); // put radio in RX mode
  }

  // For debugging info
  // printf_begin();             // needed only once for printing details
  // radio.printDetails();       // (smaller) function that prints raw register values
  // radio.printPrettyDetails(); // (larger) function that prints human readable data

} // setup

void loop() {

  if (role) {
    // This device is a TX node

    unsigned long start_timer = micros();                    // start the timer
    bool report = radio.write(&payload, sizeof(float));      // transmit & save the report
    unsigned long end_timer = micros();                      // end the timer

    if (report) {
      Serial.print(F("Transmission successful! "));          // payload was delivered
      Serial.print(F("Time to transmit = "));
      Serial.print(end_timer - start_timer);                 // print the timer result
      Serial.print(F(" us. Sent: "));
      Serial.println(payload);                               // print payload sent
      payload += 0.01;                                       // increment float payload
    } else {
      Serial.println(F("Transmission failed or timed out")); // payload was not delivered
    }

    // to make this example readable in the serial monitor
    delay(1000);  // slow transmissions down by 1 second

  } else {
    // This device is a RX node

    uint8_t pipe;
    if (radio.available(&pipe)) {             // is there a payload? get the pipe number that recieved it
      uint8_t bytes = radio.getPayloadSize(); // get the size of the payload
      radio.read(&payload, bytes);            // fetch payload from FIFO
      Serial.print(F("Received "));
      Serial.print(bytes);                    // print the size of the payload
      Serial.print(F(" bytes on pipe "));
      Serial.print(pipe);                     // print the pipe number
      Serial.print(F(": "));
      Serial.println(payload);                // print the payload's value
    }
  } // role

  if (Serial.available()) {
    // change the role via the serial monitor

    char c = toupper(Serial.read());
    if (c == 'T' && !role) {
      // Become the TX node

      role = true;
      Serial.println(F("*** CHANGING TO TRANSMIT ROLE -- PRESS 'R' TO SWITCH BACK"));
      radio.stopListening();

    } else if (c == 'R' && role) {
      // Become the RX node

      role = false;
      Serial.println(F("*** CHANGING TO RECEIVE ROLE -- PRESS 'T' TO SWITCH BACK"));
      radio.startListening();
    }
  }

} // loop

В рамках данного примера, одна плата настраивается как передатчик, а другая как получатель. В моем случае пины CE и CSN указываемые в конструкторе RF24 radio(CEpin, CSNpin) были 7 и 8 соответственно. После загрузки скетча на плату, в Serial monitor выводится строка:

Which radio is this? Enter '0' or '1'. Defaults to '0'

Ввожу "1" в окошке отправителя и "0" в окошке получателя. После вывода следующей строки

*** PRESS 'T' to begin transmitting to the other node

выбираю "T" для настройки одной из Arduino как отправителя и "R" как получателя.

После выполнения вышеописанных настроек, получился следующий результат (время передачи пакета и пакет с числом 0.0, увеличивающимся с шагом 0.01):

Время передачи в среднем заняло всего 552 микросекунды

Передача данных между Arduino RF и Raspberry Pi

Далее пусть в качестве передатчика снова выступает микроконтроллер Arduino Nano RF, а в качестве приемника - Raspberry Pi 4 с модулем NRF24L01, подключённым по следующей схеме:

Для настройки Raspberry в качестве приемника, я выполнила следующие шаги:

Для удаленного подключения к Raspberry, определяю IP адрес Raspberry с помощью программы Aadvanced Ip scanner (альтернативный способ - через список подключенных устройств на странице роутера).

С помощью Putty, подключаюсь к Raspberry по ssh, указывая Ip адрес Raspberry и порт 22 (по умолчанию логин "pi", пароль "raspberry" ).

P.S. Для удобства работы через графический интерфейс, можно скачать программу VNCviewer, после чего ввести в консоль Raspberry команду vncserver.

В консоли Raspbrry для настройки SPI выполняю следующую команду

sudo raspi-config

В появившемся окне выбираю 5. Interfacting options -> SPI -> Enabledtparam=spi=on

Изначально, моя Raspberry Pi поставлялась в комплекте с дисплеем, подключаемым к тем же портам, что и NRFL01 модуль. После того, как дисплей убран, нужно отредактировать файл boot/config.txt , закоментировав строки, относящиеся к дисплею. В моем случае незакоментированной осталась только строка

dtparam=spi=on

Перезагружаюсь и обновляюсь

sudo reboot
sudo apt-get update

Далее устанавливаю библиотеку RF24 (например по инструкции на github или medium)

установка библиотиеки RF24 на Raspberry
  1. Install prerequisites if there are any (MRAA, LittleWire libraries, setup SPI device etc)

  2. Download the install.sh file from http://tmrh20.github.io/RF24Installer/RPi/install.sh

    wget http://tmrh20.github.io/RF24Installer/RPi/install.sh
  3. Make it executable

    chmod +x install.sh
  4. Run it and choose your options

    ./install.sh
  5. Run an example from one of the libraries

    cd rf24libs/RF24/examples_linux

    Edit the gettingstarted example, to set your pin configuration

    nano gettingstarted.cpp
    make
    sudo ./gettingstarted

В качестве примера также использую файл gettingstarted.py, после выполнения которого выбираю номер модуля "1" и режим "R".

код программы gettingstarted.py
"""
A simple example of sending data from 1 nRF24L01 transceiver to another.
This example was written to be used on 2 devices acting as 'nodes'.
"""
import sys
import argparse
import time
import struct
from RF24 import RF24, RF24_PA_LOW


parser = argparse.ArgumentParser(
    description=doc,
    formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
    "-n",
    "--node",
    type=int,
    choices=range(2),
    help="the identifying radio number (or node ID number)"
)
parser.add_argument(
    "-r",
    "--role",
    type=int,
    choices=range(2),
    help="'1' specifies the TX role. '0' specifies the RX role."
)
########### USER CONFIGURATION ###########
See https://github.com/TMRh20/RF24/blob/master/pyRF24/readme.md
Radio CE Pin, CSN Pin, SPI Speed
CE Pin uses GPIO number with BCM and SPIDEV drivers, other platforms use
their own pin numbering
CS Pin addresses the SPI bus number at /dev/spidev<a>.<b>
ie: RF24 radio(<ce_pin>, <a>*10+<b>); spidev1.0 is 10, spidev1.1 is 11 etc..
Generic:
radio = RF24(22, 0)
################## Linux (BBB,x86,etc) #########################
See http://nRF24.github.io/RF24/pages.html for more information on usage
See http://iotdk.intel.com/docs/master/mraa/ for more information on MRAA
See https://www.kernel.org/doc/Documentation/spi/spidev for more
information on SPIDEV
using the python keyword global is bad practice. Instead we'll use a 1 item
list to store our float number for the payloads sent/received
payload = [0.0]
def master():
    """Transmits an incrementing float every second"""
    radio.stopListening()  # put radio in TX mode
    failures = 0
    while failures < 6:
        # use struct.pack() to packet your data into the payload
        # "<f" means a single little endian (4 byte) float value.
        buffer = struct.pack("<f", payload[0])
        start_timer = time.monotonic_ns()  # start timer
        result = radio.write(buffer)
        end_timer = time.monotonic_ns()  # end timer
        if not result:
            print("Transmission failed or timed out")
            failures += 1
        else:
            print(
                "Transmission successful! Time to Transmit: "
                "{} us. Sent: {}".format(
                    (end_timer - start_timer) / 1000,
                    payload[0]
                )
            )
            payload[0] += 0.01
        time.sleep(1)
    print(failures, "failures detected. Leaving TX role.")
def slave(timeout=6):
    """Listen for any payloads and print the transaction
:param int timeout: The number of seconds to wait (with no transmission)
    until exiting function.
"""
radio.startListening()  # put radio in RX mode

start_timer = time.monotonic()
while (time.monotonic() - start_timer) &lt; timeout:
    has_payload, pipe_number = radio.available_pipe()
    if has_payload:
        # fetch 1 payload from RX FIFO
        buffer = radio.read(radio.payloadSize)
        # use struct.unpack() to convert the buffer into usable data
        # expecting a little endian float, thus the format string "&lt;f"
        # buffer[:4] truncates padded 0s in case payloadSize was not set
        payload[0] = struct.unpack("&lt;f", buffer[:4])[0]
        # print details about the received packet
        print(
            "Received {} bytes on pipe {}: {}".format(
                radio.payloadSize,
                pipe_number,
                payload[0]
            )
        )
        start_timer = time.monotonic()  # reset the timeout timer

print("Nothing received in", timeout, "seconds. Leaving RX role")
# recommended behavior is to keep in TX mode while idle
radio.stopListening()  # put the radio in TX mode

def set_role():
    """Set the role using stdin stream. Timeout arg for slave() can be
    specified using a space delimiter (e.g. 'R 10' calls slave(10))
:return:
    - True when role is complete &amp; app should continue running.
    - False when app should exit
"""
user_input = input(
    "*** Enter 'R' for receiver role.\n"
    "*** Enter 'T' for transmitter role.\n"
    "*** Enter 'Q' to quit example.\n"
) or "?"
user_input = user_input.split()
if user_input[0].upper().startswith("R"):
    if len(user_input) &gt; 1:
        slave(int(user_input[1]))
    else:
        slave()
    return True
elif user_input[0].upper().startswith("T"):
    master()
    return True
elif user_input[0].upper().startswith("Q"):
    radio.powerDown()
    return False
print(user_input[0], "is an unrecognized input. Please try again.")
return set_role()

if name == "main":
args = parser.parse_args()  # parse any CLI args

# initialize the nRF24L01 on the spi bus
if not radio.begin():
    raise RuntimeError("radio hardware is not responding")

# For this example, we will use different addresses
# An address need to be a buffer protocol object (bytearray)
address = [b"1Node", b"2Node"]
# It is very helpful to think of an address as a path instead of as
# an identifying device destination

print(sys.argv[0])  # print example name

# to use different addresses on a pair of radios, we need a variable to
# uniquely identify which address this radio will use to transmit
# 0 uses address[0] to transmit, 1 uses address[1] to transmit
radio_number = args.node  # uses default value from `parser`
if args.node is None:  # if '--node' arg wasn't specified
    radio_number = bool(
        int(
            input(
                "Which radio is this? Enter '0' or '1'. Defaults to '0' "
            ) or 0
        )
    )

# set the Power Amplifier level to -12 dBm since this test example is
# usually run with nRF24L01 transceivers in close proximity of each other
radio.setPALevel(RF24_PA_LOW)  # RF24_PA_MAX is default

# set the TX address of the RX node into the TX pipe
radio.openWritingPipe(address[radio_number])  # always uses pipe 0

# set the RX address of the TX node into a RX pipe
radio.openReadingPipe(1, address[not radio_number])  # using pipe 1

# To save time during transmission, we'll set the payload size to be only
# what we need. A float value occupies 4 bytes in memory using
# struct.pack(); "&lt;f" means a little endian unsigned float
radio.payloadSize = len(struct.pack("&lt;f", payload[0]))

# for debugging, we have 2 options that print a large block of details
# (smaller) function that prints raw register values
# radio.printDetails()
# (larger) function that prints human readable data
# radio.printPrettyDetails()

try:
    if args.role is None:  # if not specified with CLI arg '-r'
        while set_role():
            pass  # continue example until 'Q' is entered
    else:  # if role was set using CLI args
        # run role once and exit
        master() if bool(args.role) else slave()
except KeyboardInterrupt:
    print(" Keyboard Interrupt detected. Exiting...")
    radio.powerDown()
    sys.exit()

Получился аналогичный предыдущему пункту результат (на изображении показан вывод в IDE ArduinoStudio и Thonny):

В данном случае время передачи одного из пакетов значительно выше. Такая ситуация повторилась несколько раз.

Сравнение со связкой Arduino Leonardo + модуль NRF24L01

Данный краткий обзор был бы совсем кратким, не выполни я пример gettingstarted на стандартной связке Arduino + NRFL01 и Raspberry + NRFL01

Схема подключения NRFL01 к Arduino Nano изображена в посте выше. У меня не было под рукой Arduino Nano, но была Arduino Leonardo, у которой SPI пины вынесены сбоку платы.

Результат:

В конце поста, также покажу результат передачи информации о расстоянии до объекта, полученной с помощью имеющегося в наличии ультразвукового датчика, подключенного по схеме ( как подключается NRF24L01 модуль показано выше):

код Arduino US.ino

#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"
#include <NewPing.h>
//static char send_payload[256];

#define TRIGGER_PIN 4      //Trig pin
#define ECHO_PIN  3        //Echo pin
#define MAX_DIST 400

const int min_payload_size = 4;
const int max_payload_size = 32;
const int payload_size_increments_by = 1;
int next_payload_size = min_payload_size;
float send_payload = 0.0;


NewPing sonar(TRIGGER_PIN, ECHO_PIN, MAX_DIST);
RF24 radio(7, 8); //
//const int role_pin = 5;

// Radio pipe addresses for the 2 nodes to communicate.
uint8_t address[][6] = {"1Node", "2Node"}; //
char receive_payload[max_payload_size + 1];
void setup()
{
  Serial.begin(115200);
  radio.begin();
  radio.enableDynamicPayloads();
  radio.setRetries(5, 15);
  radio.openWritingPipe(address[0]); //
  radio.openReadingPipe(1, address[1]); //
  radio.startListening();
}

void loop(void)
{
  int water_level = sonar.ping_cm();
  Serial.print("Sending Data :");
  Serial.print(water_level);
  Serial.println(" cm");
  //delay(1000);
  String water = String(water_level);
 
 radio.stopListening();
  send_payload = water_level;
  unsigned long start_timer = micros();                    // start the timer
  radio.write(&send_payload, sizeof(float)); 
  unsigned long end_timer = micros();                      // end the timer
  Serial.print(F("Time to transmit = "));
  Serial.print(end_timer - start_timer);                 // print the timer result
  Serial.print(F(" us. Sent: "));

}

Результат выполнения показан ниже. Время передачи значительно выше. С ходу не хватает знаний понять, почему так вышло и как улучшить результат.

Заключение

К сожалению мне сходу не удалось найти в интернете подробных гайдов по работе с Arduino RF, поэтому пришлось пару недель повозиться. Знакомство с библиотекой Mirf как-то сразу не задалось. После многих попыток разобраться в теме, получился вот такой вот гайд. Оказалось, что работать с Arduino RF интересно и не так уж и трудно. Надеюсь что мой опыт пригодится новичкам и желающим построить какой-либо проект на базе Arduino RF. Также хочу выразить благодарность авторам постов про NRF24L01, которых набралось уже не мало :)

Комментарии (11)


  1. ewavr
    29.08.2021 21:48
    +2

    "Время передачи в среднем заняло всего 552 наносекунды или 0.000552 миллисекунды!"

    На ваших же скринах 552 микросекунды. Откуда взяться наносекундам, когда максимальная скорость передачи 2 Мбит/секунда?


    1. SpiritErr Автор
      29.08.2021 21:48

      спасибо что нашли ошибку


  1. FridayJew
    29.08.2021 23:12

    одна плата настраивается как передатчик, а другая как отправитель. 

    Хм


    1. RiddickABSent
      30.08.2021 00:47

      А приёмником у них будет плата номер полтора)

      Интересно, что по этому поводу сказал бы гуру АлехГувер, он же AlexGyver..


  1. agalakhov
    30.08.2021 01:16

    nRF52832 может больше и стоит дешевле.


    1. sav13
      30.08.2021 05:41

      А 52840 еще и Zigbee умеет. И мощность выше


    1. jaha33
      30.08.2021 10:24
      +1

      Этого быть не может. NRF24 китайцы давно научились подделывать, поэтому он копеешный, NRF52 копировать еще не научились, ардуины с NRF52 раза в 2-3 будут дороже


      1. igrushkin
        30.08.2021 11:09

        в том то и дело, что не нужны "ардуины С nrf5", тк в nrf И ардуина, И передатчик/приемник. Все в одном чипе


  1. mkurilov
    30.08.2021 10:42

    Прекрасный пример как китайцы продают устаревшее железо несведущим людям..

    nRF5, конечно, хороши (больше пинов, мощнее cpu, лучшая энергоэффективность, и много ещё чего). Единственный минус - порог вхождения. Не хочешь париться - бери adafruit на том же 52840 чипе.


  1. balamutang
    30.08.2021 15:54

    NRF24L01 требует для питания стабильного 3.3вольта, которых на ардуине из коробки нет.

    Те костыли, когда питание берется с внутреннего стабилизатора ардуины не в счет, тк связь получается очень нестабильной. Для NRF24L01 даже отдельные адаптеры есть чтобы заставить ее работать нормально, гуглятся по "nrf24l01 power adapter" и ничего подобного им (стабилизатор 3.3 и электролит) на этой ардуине RF я не вижу.

    Соответственно и работать оно будет очень условно


    1. daggert
      31.08.2021 00:57
      +1

      Есть 3v3 на том что представил автор, видно на обратной стороне - 1 стаб на атмегу и 1 на NRF.

      На обычных 3в бордах стоит AMS1117-3V3, точно такой-же LDO как на адаптере питания для NRF24L01. На всяких pro mini - MIC5205 или SPX3819, дающие до 150mA. На нормальной уно - LP2985-33DBVR, тоже на 150mA по линии 3V.3. У меня сейчас на столе лежит pro mini, nano (3v3), RoLa Node, китайская бюджетная версия атмеги328 и какая-то поделка вроде lilly pad - все из них успешно тянут NRF через свой штатный LDO в режиме максимальной производительности.

      Вопрос ведь всегда в навешивании на нее допов, если глобально. Если у вас 2-3 модуля по 10-15mA + дуина на 20mA + NRF, который по даташиту умещается в 13mA в 2 мегабита скорости - будет все стабильно работать и хватит штатных кондеров даже.

      Если-же вы довешиваете десяток светодиодов на 20mA или еще какую дичь - то да, не будет стабильного 3V3. Ну и совсем китайский китай не юзайте, если борда 5В - ищите второй стаб, чтоб 3V3 не шло от кристала.