Прошу помочь исправить скетч или алгоритм работы радиоуправления NRF24L01

Добрый день, уважаемые форумчане!
Прошу помочь выявить и устранить проблему, а также подсказать более/наиболее удачный алгоритм работы системы, где пульт должен управлять цветом и частотой синхронного(!) мигания 10 автономных лент.
Собираю систему, состоящую из 1-го пульта и 10 приёмников.
Пульт и приёмники работают на Arduino Nano, передача сигналов осуществляется при помощи радиомодулей NRF24L01+PA+LNA на пульте и на приёмниках. Радиомодули питаются через специальные адаптеры питания для NRF24.
Алгоритм работы. На пульте есть два вращающихся потенциометра, при помощи которых настраивается цвет и частота мигания. Мигание осуществляется следующим образом: передатчик через раз передаёт на пульты в соответствующей ячейке массива то «0», то «1» и в зависимости от этого значения пульты либо гасят свою светодиодную ленту, либо включают. Частота мигания определяется частотой передачи сигналов «0» и «1», которая определяется значением, выдаваемым одним из потенциометров. Значение цвета определяется значением, выдаваемым вторым потенциометром.
Так как приёмников много, было решено отключить функцию проверки доставки сигнала и, что бы сообщения точно доходили до всех приёмников, просто слать по 20 раз подряд одно и то же сообщение: сначала 20 раз «включи такой-то цвет», а потом 20 раз подряд «погасни».
Но при таком топорном алгоритме возникла проблема с миганием ленты на приёмнике (пока только на одном испытываю). Во-первых, как ни старайся, невозможно увеличит частоту мигания ленты выше какого-то предела (раз пять в секунду, наверное). Во-вторых, судя по показателям монитора порта, частенько пропадают пакеты и мигание становится нерегулярным.
При питании через USB кабели от ПК проблем с потерей пакетов было меньше. Когда собрал на автономном питании с DC-DC преобразователями, банками 18650 и освободил платы Arduino от лишних проводов, пересадив питание радиомодулей и светодиодных лент на шины, проблема с пропуском мигания усугубилась крайне.
Ниже привожу скетчи и схемы приёмника и передатчика.
Заранее благодарю за конструктивные ответы.

Скетч пульта:

#include <nRF24L01.h>                                          // Подключаем файл настроек из библиотеки RF24.
#include <RF24.h>                                              // Подключаем библиотеку для работы с nRF24L01+.
RF24     radio(7, 10);                                         // Создаём объект radio для работы с библиотекой RF24, указывая номера выводов модуля (CE, SS).

int potValue[4];// Объявляем массив для хранения и передачи данных (до 32 байт включительно).

boolean FlagSveta = true;//флаг, меняющий с каждым циклом значение на противоположное для создания мигания ленты

int NomerPaketa = 0;

#define color_pin A0 //сюда подключена средняя нога потенциометра
#define freq_pin A1 //сюда подключена средняя нога потенциометра

void setup(){                                                  //
    Serial.begin(9600);
    radio.begin           ();                                  // Инициируем работу модуля nRF24L01+.
    radio.setChannel      (66);                                // был канал 27. Указываем канал передачи данных (от 0 до 125), 27 - значит передача данных осуществляется на частоте 2,427 ГГц.
    radio.setDataRate     (RF24_1MBPS);                        // Указываем скорость передачи данных (RF24_250KBPS, RF24_1MBPS, RF24_2MBPS), RF24_1MBPS - 1Мбит/сек.
    radio.setPALevel      (RF24_PA_MAX);                       // Указываем уровень усиления передатчика (RF24_PA_MIN=-18dBm, RF24_PA_LOW=-12dBm, RF24_PA_HIGH=-6dBm, RF24_PA_MAX=0dBm).
    radio.enableDynamicAck();                                  // Разрешаем выборочно отключать запросы подтверждения приема данных.
    //radio.setAutoAck(1, false); // Запретить приёмнику отправлять пакеты подтверждения приема передатчику использующему адрес 1 трубы.
    radio.openWritingPipe (0xAABBCCDD11LL);                    // Открываем трубу с адресом 0xAABBCCDD11 для передачи данных (передатчик может одновременно вещать только по одной трубе).

    pinMode (2, OUTPUT); //выдаём с 2 ноги 5 Вольт для потенциометра цвета
    pinMode (3, OUTPUT); //выдаём с 3 ноги 5 Вольт для потенциометра частоты
}

void loop(){

  digitalWrite (2, HIGH); // включаем подачу питания 2 ногу для потенциометра цвета
  digitalWrite (3, HIGH); // включаем подачу питания 3 ногу для потенциометра частоты

  byte color = analogRead(color_pin)/4; //переменная "color" (цвет) типа байт считывает с аналоговой ноги потенциометра
  if (color > 200) color = 200; else {} //ограничиваем диапазон значений цвета, что бы красный был только с одного крайнего положения потенциометра

  byte freq = analogRead(freq_pin)/80+1; //переменная "freq" (частота) типа байт считывает с аналоговой ноги потенциометра
  
  potValue[0] = FlagSveta;//записываем значение первой ячейки массива. Попеременно должно быть то 0, то 1.
  potValue[1] = color;//записываем значение второй ячейки массива. Там будет шифр цвета ленты.
  potValue[2] = freq;//записываем значение третьей ячейки массива. Там будет "сырая" частота.
  potValue[3] = NomerPaketa;//записываем значение порядкового номера пакета для отслеживания потерь

  for (byte CountTriesTrans = 0; CountTriesTrans < 20; CountTriesTrans++){ //определяем количество попыток отправки
  radio.write(&potValue, sizeof(potValue), true); // Отправляем данные из массива potValue указывая весь размер массива в байтах.
  //третий аргумент "true" работает совместно с "radio.enableDynamicAck();" из войд сетапа
  }


  FlagSveta = !FlagSveta;

  NomerPaketa++;

//  delay(50);                                                 // Устанавливаем задержку на 50 мс. В этом скетче нет смысла слать данные чаще.
// Так же задержка нужна для того, что бы приёмник успел выполнить свои действия над полученными данными (если таковые необходимы) до следующей передачи.
  delay(1000/freq); //определяем долю секунды, которую нужно ждать, что бы послать следующий сигнал
//для отладки:
Serial.print(potValue[0]); Serial.println(" - должно чередоваться");
Serial.print(potValue[1]); Serial.println(" - цвет с потенциометра");
Serial.print(potValue[2]); Serial.println(" - сырая частота с потенциометра");
Serial.print(potValue[3]); Serial.println(" - порядковый номер пакета");
Serial.println("=============");
}```

**Скетч приемника:**

```#include <SPI.h>                                               // Подключаем библиотеку для работы с шиной SPI.
#include <nRF24L01.h>                                          // Подключаем файл настроек из библиотеки RF24.
#include <RF24.h>                                              // Подключаем библиотеку для работы с nRF24L01+.
RF24 radio(7, 10);                                         // Создаём объект radio для работы с библиотекой RF24, указывая номера выводов модуля (CE, SS).
int potValue[4];  // Массив для хранения значений потенциометров
uint8_t  pipe;                                                 // Объявляем переменную в которую будет сохраняться номер трубы по которой приняты данные.

#include <FastLED.h> //подключаем библиотеку для светодиодов
#define NUM_LEDS 144 // How many leds in your strip?
// For led chips like WS2812, which have a data line, ground, and power, you just
// need to define DATA_PIN.  For led chipsets that are SPI based (four wires - data, clock,
// ground, and power), like the LPD8806 define both DATA_PIN and CLOCK_PIN
// Clock pin only needed for SPI based chipsets when not using hardware SPI
#define DATA_PIN 2
//#define CLOCK_PIN 3
CRGB leds[NUM_LEDS];// Define the array of leds


void setup(){
    Serial.begin(9600);
    radio.begin();                                             // Инициируем работу nRF24L01+.
    radio.setChannel      (66);                                // Указываем канал передачи данных (от 0 до 125), 27 - значит приём данных осуществляется на частоте 2,427 ГГц.
    radio.setDataRate     (RF24_1MBPS);                        // Указываем скорость передачи данных (RF24_250KBPS, RF24_1MBPS, RF24_2MBPS), RF24_1MBPS - 1Мбит/сек.
    radio.setPALevel      (RF24_PA_MAX);                       // Указываем мощность передатчика (RF24_PA_MIN=-18dBm, RF24_PA_LOW=-12dBm, RF24_PA_HIGH=-6dBm, RF24_PA_MAX=0dBm).
    radio.openReadingPipe (1, 0xAABBCCDD11LL);                 // Открываем 1 трубу с адресом 1 передатчика 0xAABBCCDD11, для приема данных.
    //radio.setAutoAck (1, false); // Запретить приёмнику отправлять пакеты подтверждения приема передатчику использующему адрес 1 трубы.
    radio.startListening  ();                                  // Включаем приемник, начинаем прослушивать открытые трубы.

FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);  // GRB ordering is assumed

pinMode (3, OUTPUT);
}

void loop(){
    digitalWrite (3, 1); //включаем пин 3 для подачи напряжения на ленту. При автономном питании от батареек не нужно.
    if(radio.available(&pipe)){ // Если в буфере имеются принятые данные, то получаем номер трубы по которой эти данные пришли в переменную pipe.
        radio.read( &potValue, sizeof(potValue) ); // Читаем данные из буфера в массив potValue указывая сколько всего байт может поместиться в массив.
        if(pipe==1){ // Если данные пришли от 1 передатчика (по 1 трубе), то можно выполнить соответствующее действие ...
          for (int i = 0; i < 4; i++) {
          Serial.println(potValue[i]);
          }
          Serial.println("=====================");
         
        byte FlagSveta = potValue[0]; //получаем значение флага для определения команды ленте вкл. или выкл.
        byte color = potValue[1]; //получаем значение цвета светодиодов, ограничение диапазона и деление показателя потенциометра на 4 произведено на передатчике
        
        byte MaxCountLed = 144; //На случай отладки при питании с Ардуино, когда тока мало, включаем мало диодов, если питание соответствует ленте, то все диоды включаем

        if (FlagSveta==0){ //если переменная FlagSveta равна 1, то гасим число диодов равное MaxCountLed
          for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){
          leds[CountLED] = CRGB::Black; //гасим светодиоды
          }
          FastLED.show(); //функция передачи команды на ленту
        }
        else { //если переменная FlagSveta НЕ равна 0, то включаем число диодов равное MaxCountLed
        for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){ //определяем сколько диодов из ленты включаем
        leds[CountLED] = CHSV(color,255,100); //(1й знак - цвет, 2й знак - насыщенность ноль это белый, 3й знак - яркость)
        }
        FastLED.show(); //функция передачи команды на ленту. В данному случае на включение.
        }
         }
      else {Serial.println("Ничего не пришло по 1 трубе");}
    }
    else {
      //Serial.println("в буфере пусто");
      //delay(1000);
      }
//Serial.println("=====================");
}```

Совершенно неудивительно…
Вы не пробовали хотя бы приблизительно прикинуть, сколько у вас занимает печать вот этих сообщений?

я вас удивлю - как раз примерно одну пятую секунды и занимают…

А вы эту длительность вообще в расчете частоты не учитываете.

1 лайк

1.Так вроде модулю NRF24L01+PA+LNA надо 3.3в напряжение питания? Или это какой-то клон?
2.Добавьте конденсатор по питанию, как можно ближе к модулю, желательно low esr ~100uF

1 лайк
  1. Модуль NRF не клон. Ему нужно 3,3 Вольта, но можно поставить блок питания, которые часто идут в комплекте с ним. Типа такого как на картинке
  2. Спасибо! Как раз думал, что может помех от ленты много.

Благодарю! Даже не знал, что это такой длительный процесс. Думал, что это вообще не отнимает времени. Попробую убрать вообще.
Я так понимаю, что Ардуина пытается слать сообщения в порт и тратит на это время даже если не подключена по USB к ПК?

Нашёл информацию, что мигающая лента (у меня RGB лента WS2812 144 диода) создаёт довольно серьёзные помехи в сети и от них тоже нужно избавляться конденсаторами. Поставлю ещё и на питании их. Может в этом причина ещё.

Не “пытается”, а просто шлет. И время, необходимое на это, не зависит от того, принимает эти сообщения кто-нибудь на другом конце. или нет.
Длительность вывода сообщений в порт очень легко подсчитать, исходя из заданной скорости порта:

При такой скорости печать каждого символа требует примерно 1 мс. У вас, навскидку, около сотни символов. Однако это кириллица, каждая буква которой требует два байта - вот почему я написал. что вывод этих сообщений занимает порядка 200мс - одной пятой секунды.

Кстати, если выбор скорости 9600 не задан какими-то особыми условиями - рекомендую поднять скорость порта до 115200, чтобы разгрузить программу от пустой работы.

1 лайк

Большое Вам спасибо!

Закомментил все участки скетча с отправкой в порт и саму инициацию монитора в скетчах приёмника и передатчика, и всё заработало как надо. Частота мигания на приёмнике стала соответствовать частоте, задаваемой пультом.

Остались лишь редкие “заикания” при мигании ленты.

На приёмнике, на шине питания макетной платы поставил два конденсатора: к Ардуине поближе 470мкф16в Teapo и поближе к ленте 2200мкф16в Teapo. Такой совет нашёл у Алекса Гайвера. Вроде бы, конденсаторы и этот момент подправили, но надо ещё понаблюдать. Может, стоит ещё поставить 470мкф*16в Teapo рядом с Ардуиной на пульте, где-то там помехи ещё могут быть?..

Также добавил при запуске единоразовое мигание красным, зелёным и синим цветом при включении пульта, что бы можно было понять без включения пульта, всё ли нормально с лентой. Думал, что из-за этого ещё могло “заикаться” моргание, но по идее, проверка выполнения условия поднятого флага не должно так сильно грузить Ардуину в моей программе.

#include <SPI.h>                                               // Подключаем библиотеку для работы с шиной SPI.
#include <nRF24L01.h>                                          // Подключаем файл настроек из библиотеки RF24.
#include <RF24.h>                                              // Подключаем библиотеку для работы с nRF24L01+.
RF24 radio(7, 10);                                         // Создаём объект radio для работы с библиотекой RF24, указывая номера выводов модуля (CE, SS).
int potValue[4];  // Массив для хранения значений потенциометров
uint8_t  pipe;                                                 // Объявляем переменную в которую будет сохраняться номер трубы по которой приняты данные.

#include <FastLED.h> //подключаем библиотеку для светодиодов
#define NUM_LEDS 144 // How many leds in your strip?
// For led chips like WS2812, which have a data line, ground, and power, you just
// need to define DATA_PIN.  For led chipsets that are SPI based (four wires - data, clock,
// ground, and power), like the LPD8806 define both DATA_PIN and CLOCK_PIN
// Clock pin only needed for SPI based chipsets when not using hardware SPI
#define DATA_PIN 2
//#define CLOCK_PIN 3
CRGB leds[NUM_LEDS];// Define the array of leds

boolean MigZapuska = true;//первичное включение ленты для проверки

void setup(){
    //Serial.begin(9600); //включаем возможность отправки данных в порт
    radio.begin();                                             // Инициируем работу nRF24L01+.
    radio.setChannel      (66);                                // Указываем канал передачи данных (от 0 до 125), 27 - значит приём данных осуществляется на частоте 2,427 ГГц.
    radio.setDataRate     (RF24_1MBPS);                        // Указываем скорость передачи данных (RF24_250KBPS, RF24_1MBPS, RF24_2MBPS), RF24_1MBPS - 1Мбит/сек.
    radio.setPALevel      (RF24_PA_MAX);                       // Указываем мощность передатчика (RF24_PA_MIN=-18dBm, RF24_PA_LOW=-12dBm, RF24_PA_HIGH=-6dBm, RF24_PA_MAX=0dBm).
    radio.openReadingPipe (1, 0xAABBCCDD11LL);                 // Открываем 1 трубу с адресом 1 передатчика 0xAABBCCDD11, для приема данных.
    //radio.setAutoAck (1, false); // Запретить приёмнику отправлять пакеты подтверждения приема передатчику использующему адрес 1 трубы.
    radio.startListening  ();                                  // Включаем приемник, начинаем прослушивать открытые трубы.

FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS);  // GRB ordering is assumed

pinMode (3, OUTPUT);
}

void loop(){

    digitalWrite (3, 1); //включаем пин 3 для подачи напряжения на ленту. При автономном питании от батареек не нужно.

    byte MaxCountLed = 144; //На случай отладки при питании с Ардуино, когда тока мало, включаем мало диодов, если питание соответствует ленте, то все диоды включаем
    
    if(MigZapuska == true){//пробное включение ленты на 1 сек. единожны при включении пульта
    for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){ //определяем сколько диодов из ленты включаем
        leds[CountLED] = CHSV(85,255,100); //(1й знак - цвет, 2й знак - насыщенность ноль это белый, 3й знак - яркость)
        }
        FastLED.show();
      delay (500);
    for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){ //определяем сколько диодов из ленты включаем
        leds[CountLED] = CHSV(170,255,100); //(1й знак - цвет, 2й знак - насыщенность ноль это белый, 3й знак - яркость)
        }
        FastLED.show();
      delay (500);
    for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){ //определяем сколько диодов из ленты включаем
        leds[CountLED] = CHSV(255,255,100); //(1й знак - цвет, 2й знак - насыщенность ноль это белый, 3й знак - яркость)
        }
        FastLED.show();
      delay (500);
    for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){
          leds[CountLED] = CRGB::Black; //гасим светодиоды
        }
        FastLED.show(); //функция передачи команды на ленту
    }
    else {}
    MigZapuska = false;//выключаем флаг первичного мигания*/

    if(radio.available(&pipe)){ // Если в буфере имеются принятые данные, то получаем номер трубы по которой эти данные пришли в переменную pipe.
        radio.read( &potValue, sizeof(potValue) ); // Читаем данные из буфера в массив potValue указывая сколько всего байт может поместиться в массив.
        if(pipe==1){ // Если данные пришли от 1 передатчика (по 1 трубе), то можно выполнить соответствующее действие ...
          //for (int i = 0; i < 4; i++) { //цикл фор для выдачи данных в порт
          //Serial.println(potValue[i]);
          //}
          //Serial.println("=====================");
         
        byte FlagSveta = potValue[0]; //получаем значение флага для определения команды ленте вкл. или выкл.
        byte color = potValue[1]; //получаем значение цвета светодиодов, ограничение диапазона и деление показателя потенциометра на 4 произведено на передатчике
                
        if (FlagSveta==0){ //если переменная FlagSveta равна 1, то гасим число диодов равное MaxCountLed
          for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){
          leds[CountLED] = CRGB::Black; //гасим светодиоды
          }
          FastLED.show(); //функция передачи команды на ленту
        }
        else { //если переменная FlagSveta НЕ равна 0, то включаем число диодов равное MaxCountLed
        for (byte CountLED = 0; CountLED < MaxCountLed; CountLED++){ //определяем сколько диодов из ленты включаем
        leds[CountLED] = CHSV(color,255,100); //(1й знак - цвет, 2й знак - насыщенность ноль это белый, 3й знак - яркость)
        }
        FastLED.show(); //функция передачи команды на ленту. В данному случае на включение.
        }
         }
      else {}//Serial.println("Ничего не пришло по 1 трубе"); - внутри элс
    }
    else {
      //Serial.println("в буфере пусто");
      //delay(1000);
      }
//Serial.println("=====================");
}

NRF24L01

Спойлер

И от “макетки на штырьках” надо уходить.
Паяльник незаменим ИМХО

Согласен, что от макеток надо уходить. Как только всю систему более-менее отлажу, стану паять. Но прежде хочу проверить плату “Arduino RF-Nano” со встроенным радиоприёмником-передатчиком. Там без усиления, поэтому он до 100 метров, но мне больше и не надо.

Также хочу попробовать простую RGB ленту, не адресную.

Если тебе больше не надо, сходи, отнеси в ведро все свои NRF, купи HC-12 и забудь об извращенном секасе.

HC-12 умеет в “звезда”?

Как настроишь. Главное - протокол, который ты волен придумать сам, а не полагаться на номера труб NRF. У меня 1 приёмник на управляющем модуле + 8 датчиков протечки на Tiny85+HC12. Пробовал JDY-40 применить, но его пакеты на 2.4 мегагерца частенько терялись, а у HC-12 частота 433МГц, она понадежнее через бетонные стены пробивает.
Датчики просыпались раз в 4 секунды, если всё норм, то раз в минуту передавали управляйке свой ID, 1 или 2 байта, не помню, сотояние батареи 18650 или BL5, и Аларм или Все спокойно + CRC. Работает 4 года как часы.
Даже заголовочник нашел, с описанием

Спойлер
/*
    Name:       LeakSensor.ino
    Created:	23.08.18 8:57:14
    Author:     DtS
*/
/*
   Даччик протечки, работает автономно, постоянно в режиме Deep Sleep, работает от 18650
   Просыпается раз в 4 секунды, опрашивает даччик влажности
   Шлёт вышестоящему контроллеру информацию о состоянии, если всё ок, то раз в минуту, если нет, 
   то раз в секунду
   Даччики отличаются по своему ID, который жестко прописан в прошивке
   Светодиод моргает при просыпании, раз в 8 секунд (два просыпания), информируя наблюдателя о том, 
   что даччик работает
   Информация о состоянии: 
   ID даччика, State = OK | Alarm, Battery = VBat, CRC
   VBat = целое, умноженное на 10 напряжение батареи, т.е 38 - 3.8 Вольта

   При протечке издает звуковой сигнал в течение минуты, 500мс пищит, 250 мс не пищит
*/
1 лайк

Спасибо! Почему-то упустил из виду модуль HC-12. Я немного посмотрел в интернете по нему и понял, что там тоже разбираться надо. Купил, включил и забыл там тоже не сработает, как я понял. Да и “собственный протокол” звучит не так уж просто.
Да и NRF модулей у меня уже полные карманы, придётся использовать. Не выбрасывать же :slight_smile:
А в чём преимущества HC12 перед NRF?

  1. HC-12 простой как 2 копейки удлинитель UART, сколько надо байт, столько и отправляешь как по обычным проводам, NRF передает блоками по 32 байта
  2. 433 Мгц распространяется через стены лучше чем 2.4 ГГц, потерь пакетов будет меньше.
  3. Чтоб заставить работать NRF надо сначала получить ученую степень доктора философии (чтоб понять что жизнь - авно)
  4. Никаких замудреных библиотек для НС-12 не нужно, обычный SoftSerial
  5. HC-12 работает на любых ногах, не только на SPI

На форуме Амперки есть тема про NRF, там больше сотни страниц, не всем удается ее победить (видимо, из-за того, что подделок много)

Решать тебе, я свои отнес в ведро и даже не плакал.

1 лайк

Нет, там просто пины tx rx называются, но туда можно пихать любой протокол.

Спасибо! Поизучают эту тему. У меня уже ведро этих NRF, правда :smiley: