Arduino и NRF24L01 в одной плате. Первое знакомство

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

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

5d4189caa4ebbbbd69cf028e9d707af7.jpg

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

image-loader.svg

Для тестирования схемы я использую библиотеку 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 
#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):

image-loader.svg

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

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

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

image-loader.svg

Для настройки 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.
ie: RF24 radio(, *10+); 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
        # "

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

image-loader.svg

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

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

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

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

image-loader.svg

Результат:

image-loader.svg

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

image-loader.svgкод Arduino US.ino


#include 
#include "nRF24L01.h"
#include "RF24.h"
#include 
//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: "));

}

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

image-loader.svg

Заключение

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

© Habrahabr.ru