Привет хабровчане! Не так давно попалась мне в руки пара плат 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
Install prerequisites if there are any (MRAA, LittleWire libraries, setup SPI device etc)
-
Download the install.sh file from http://tmrh20.github.io/RF24Installer/RPi/install.sh
wget http://tmrh20.github.io/RF24Installer/RPi/install.sh
-
Make it executable
chmod +x install.sh
-
Run it and choose your options
./install.sh
-
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) < 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 "<f"
# buffer[:4] truncates padded 0s in case payloadSize was not set
payload[0] = struct.unpack("<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 & 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) > 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(); "<f" means a little endian unsigned float
radio.payloadSize = len(struct.pack("<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)
FridayJew
29.08.2021 23:12одна плата настраивается как передатчик, а другая как отправитель.
Хм
RiddickABSent
30.08.2021 00:47А приёмником у них будет плата номер полтора)
Интересно, что по этому поводу сказал бы гуру АлехГувер, он же AlexGyver..
agalakhov
30.08.2021 01:16nRF52832 может больше и стоит дешевле.
jaha33
30.08.2021 10:24+1Этого быть не может. NRF24 китайцы давно научились подделывать, поэтому он копеешный, NRF52 копировать еще не научились, ардуины с NRF52 раза в 2-3 будут дороже
igrushkin
30.08.2021 11:09в том то и дело, что не нужны "ардуины С nrf5", тк в nrf И ардуина, И передатчик/приемник. Все в одном чипе
mkurilov
30.08.2021 10:42Прекрасный пример как китайцы продают устаревшее железо несведущим людям..
nRF5, конечно, хороши (больше пинов, мощнее cpu, лучшая энергоэффективность, и много ещё чего). Единственный минус - порог вхождения. Не хочешь париться - бери adafruit на том же 52840 чипе.
balamutang
30.08.2021 15:54NRF24L01 требует для питания стабильного 3.3вольта, которых на ардуине из коробки нет.
Те костыли, когда питание берется с внутреннего стабилизатора ардуины не в счет, тк связь получается очень нестабильной. Для NRF24L01 даже отдельные адаптеры есть чтобы заставить ее работать нормально, гуглятся по "nrf24l01 power adapter" и ничего подобного им (стабилизатор 3.3 и электролит) на этой ардуине RF я не вижу.
Соответственно и работать оно будет очень условно
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 не шло от кристала.
ewavr
"Время передачи в среднем заняло всего 552 наносекунды или 0.000552 миллисекунды!"
На ваших же скринах 552 микросекунды. Откуда взяться наносекундам, когда максимальная скорость передачи 2 Мбит/секунда?
SpiritErr Автор
спасибо что нашли ошибку