ARDUINO Скорость работы , энкодеры и RGB ленты, как же это все за.. связано ?

Очень часто встречаю подобные проблемы: "я напихал в ардуино по самое небалуйся, а оно перестало работать, что делать?" Будем размышлять и разбираться.
Теории будет мало. будут примеры.
И для начала берем простой пример написанный тут же на коленке. Тут к ардуино нано 168 по 3-му пину подключена LED лента 2м, на 120 светодиодов. Далее огонек по загорается случайным цветом от начала ленту к концу и тухнет обратно. И так до потери пульса в бесконечном цикле loop. И все работает без проблем!
C++:
#include <FastLED.h>
#define LED_PIN     3
#define NUM_LEDS    120
#define CHIPSET     WS2812
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
#define BRIGHTNESS  128


void setup() {
  LEDS.addLeds<WS2812, LED_PIN, RGB>(leds, NUM_LEDS);
  LEDS.setBrightness(84);
}

void loop() {
  for (int i = 0; i < NUM_LEDS; i++)
  {
    leds[i] = CRGB(random(256), random(256), random(256)); 
    FastLED.show();
    delay(50);
  }
  for (int i = NUM_LEDS; i > 0; i--)
  {
    leds[i-1] = CRGB(random(0), random(0), random(0)); 
    FastLED.show();
    delay(50);
  }
}

Что хотЮ дальше ? ХотЮ, что бы у меня был энкодер и регулирование скорости пробега огонька.
Для этого я его подключу без к 6-му и 7-му пинам, используя библиотек Гайвера. Прерывания я не использую, в теле будет tick
И он тест тоже работает без проблем, код приводить не буду он есть в примерах (см. encoder_demo.);

Казалось бы осталось завести глобальную переменную задержки и все будет работать. Сделаем это:
C++:
#define CLK 6
#define DT 7
#include "GyverEncoder.h"
Encoder enc1(CLK, DT);

int16_t led_delay=50;

void setup() {
  Serial.begin(115200);
  enc1.setType(TYPE2);
}

void loop() {
  enc1.tick();
  if (enc1.isRight())
    {
    led_delay=max(0,--led_delay);
    Serial.println(led_delay);
    }
  if (enc1.isLeft())
    {
    led_delay=min(200,++led_delay);
    Serial.println(led_delay);
    }
}
Регулируется задержка в диапазоне от 0 до 200, эти константы жестко записаны в коде, хотите сделайте определение для них сами.

Совмещаем эти два кода в один и ... не прокатило...
Вот он совмещенный код

C++:
#include <FastLED.h>
#define LED_PIN     3
#define NUM_LEDS    120
#define CHIPSET     WS2812
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
#define BRIGHTNESS  128


#define CLK 6
#define DT 7
#include "GyverEncoder.h"
Encoder enc1(CLK, DT);

int16_t led_delay = 50;

void setup() {
  Serial.begin(115200);
  enc1.setType(TYPE2);

  LEDS.addLeds<WS2812, LED_PIN, RGB>(leds, NUM_LEDS);
  LEDS.setBrightness(84);
}

void loop() {
  enc1.tick();
  if (enc1.isRight())
  {
    led_delay = max(0, --led_delay);
    Serial.println(led_delay);
  }
  if (enc1.isLeft())
  {
    led_delay = min(200, ++led_delay);
    Serial.println(led_delay);
  }

  for (int i = 0; i < NUM_LEDS; i++)
  {
    leds[i] = CRGB(random(256), random(256), random(256));
    FastLED.show();
    delay(led_delay);
  }
  for (int i = NUM_LEDS; i > 0; i--)
  {
    leds[i - 1] = CRGB(random(0), random(0), random(0));
    FastLED.show();
    delay(led_delay);
  }
}

Кошмар, паника, жуть...
Моя любимая часть , рассчеты:
В коде у меня стоит задержка 50 мс. Считаем, 120 светодиодов, умножить на 50, да два раза, получаем 12 000 мс, 12 секунд. И энкодер считывает своё состояние раз в 12 секунд, он просто отказывается работать с такой частотой. По моим прикидкам он работает хорошо, когда считывает свои показания.. более 12 тысяч раз в секунду. Можно попробовать настроить ENC_DEBOUNCE_TURN на 12-13 секунд.. но это жопа!

Практическая часть проверки расчетов.
Я уже несколько раз приводил простой код для проверки кол-ва циклов в секунду, помогает понять что не так. Приведу его еще раз. И встрою его в неработающий код. Просто добавлю в самый низ, а вызов функции ShowFPS() сделаю самой первой строкой в цикле loop
C++:
void ShowFPS()
{
  static uint32_t tm_m = 0;
  static uint32_t cnt_m = 0;
  cnt_m++;
  if ((millis() - tm_m) > 1000)
  {
    Serial.print("loop per sec: "); Serial.println(cnt_m);
    cnt_m = 0;
    tm_m = millis();
  }
}

Результат предсказуемый, хоть и не правильный, 1 цикл, так как это миниму, что она показывает. Приехали. Если закомментировать отработку эфекта, то получим 56 тысяч циклов в секунду. Отлично. Значит функция с задержками не подходит.
А если сделать начальную задержку не 50мс, 1 ? Все тот же 1 цикл в секунду, и пару раз я даже сумел поменять значение задержки, но это не спасло, т.к. нормальной работой это назвать нельзя.

Это была присказка, теперь сказка. Будем городить чудо.
Как выгодать время, которое занимает ожидание между изменением огоньков ?
1) Возьмем под это все сделаем отдельную функцию. void my_effect();
2) В начале заведем переменную, которая будет контролировать время my_timer
Вот кусок кода в начале функции эффекта, который проверяет настало ли время выполнять все или нет.
C++:
  static uint32_t my_timer=0;
  if (millis()-my_timer)<led_delay return;
  my_timer=millis();
static говорит, что значение переменной не будет утеряно после выхода из функции, хоть переменная и локальная.
если текущее время минус время последнего выполнения меньше задержки , то выходим из функции.
запоминаем последнее время выполнения функции. эту строку можно поставить в конец, тогда интервал будет чистым, в начале точным.
Но этого мало! теперь надо капитально переделывать код... я бы даже сказал, что переписать его легче , чем изменить.
1) Заводим логическу статическую переменную , которая будет говорить нам зажигаем мы огни или гасим . Пусть будет goUP и изначально она будет static bool goUp=true;

2) Заводим статический номер текущего огня, static int16_t Nomer=0;

Зажигать или гасить будем по одному номеру, после чего номер увеличиваем или уменьшаем на 1, если он будет равен числу диодов или меньше 0, то это значит мы уже зажгли последний или первый диод и пора идти в другую сторону.


C++:
#include <FastLED.h>
#define LED_PIN     3
#define NUM_LEDS    120
#define CHIPSET     WS2812
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
#define BRIGHTNESS  128


#define CLK 6
#define DT 7
#include "GyverEncoder.h"
Encoder enc1(CLK, DT);

int16_t led_delay = 10;

void setup() {
  Serial.begin(115200);
  enc1.setType(TYPE2);

  LEDS.addLeds<WS2812, LED_PIN, RGB>(leds, NUM_LEDS);
  LEDS.setBrightness(84);
}

void loop() {
  ShowFPS();
  enc1.tick();

  if (enc1.isRight())
  {
    led_delay = max(0, --led_delay);
    Serial.println(led_delay);
  }
  if (enc1.isLeft())
  {
    led_delay = min(200, ++led_delay);
    Serial.println(led_delay);
  }

  my_effect();
}

void ShowFPS()
{
  static uint32_t tm_m = 0;
  static uint32_t cnt_m = 0;
  cnt_m++;
  if ((millis() - tm_m) > 1000)
  {
    Serial.print("loop per sec: "); Serial.println(cnt_m);
    cnt_m = 0;
    tm_m = millis();
  }
}


void my_effect()
{
  static uint32_t my_timer=0;
  if ((millis()-my_timer)<led_delay) return;
  my_timer=millis();


  static bool goUp=true
  static int16_t Nomer=0;
  if (goUp) leds[Nomer] = CRGB(random(256), random(256), random(256));
  else leds[Nomer] = CRGB(random(0), random(0), random(0));
  FastLED.show();

  if (goUp) Nomer++; else Nomer--;

  if (Nomer==NUM_LEDS)
    {
      Nomer--;
      goUp=false;
    }
  if (Nomer<0)
    {
      Nomer++;
      goUp=true;
    }
}

Наиболее прошареные скажут, а как же прерывания ? А так:
FastLED запрещает прерывания на время вывода данных в ленту
Дополнение: вариант ли перейти на ESP32 ? Отчасти да, во-первых, там во время delay процессор обрабатывает WiFi, во-второых есть многопоточность, это спасет, но опять же отчасти, т.к. у процессора 2 ядра, то выигрыш будет с 2-мя потоками, к примеру один рисует эффкт, а второй занимается обслуживанием кнопок. Добавление 3-го и более потоков будет уже тормозить общее выполнение программы.
Вывод такой: правильное планирование и понимание проблемы нужно всегда.

Дополнение 2. По поводу STM. Замечательный быстрый процессор, многое позволяет, но и его можно "задолбать" прерываниями, на выполнения кода не останется времени. При этом будет работать так, что даже прошивать придется с нажатием ресета.
 
Изменено:

Комментарии

kostyamat

★★★★★★✩
29 Окт 2019
1,097
630
Очень часто встречаю подобные проблемы: "я напихал в ардуино по самое небалуйся, а оно перестало работать, что делать?" Будем размышлять и разбираться.
Теории будет мало. будут примеры.
И для начала берем простой пример написанный тут же на коленке. Тут к ардуино нано 168 по 3-му пину подключена LED лента 2м, на 120 светодиодов. Далее огонек по загорается случайным цветом от начала ленту к концу и тухнет обратно. И так до потери пульса в бесконечном цикле loop. И все работает без проблем!
C++:
#include <FastLED.h>
#define LED_PIN     3
#define NUM_LEDS    120
#define CHIPSET     WS2812
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
#define BRIGHTNESS  128


void setup() {
  LEDS.addLeds<WS2812, LED_PIN, RGB>(leds, NUM_LEDS);
  LEDS.setBrightness(84);
}

void loop() {
  for (int i = 0; i < NUM_LEDS; i++)
  {
    leds[i] = CRGB(random(256), random(256), random(256));  
    FastLED.show();
    delay(50);
  }
  for (int i = NUM_LEDS; i > 0; i--)
  {
    leds[i-1] = CRGB(random(0), random(0), random(0));  
    FastLED.show();
    delay(50);
  }
}

Что хотЮ дальше ? ХотЮ, что бы у меня был энкодер и регулирование скорости пробега огонька.
Для этого я его подключу без к 6-му и 7-му пинам, используя библиотек Гайвера. Прерывания я не использую, в теле будет tick
И он тест тоже работает без проблем, код приводить не буду он есть в примерах (см. encoder_demo.);

Казалось бы осталось завести глобальную переменную задержки и все будет работать. Сделаем это:
C++:
#define CLK 6
#define DT 7
#include "GyverEncoder.h"
Encoder enc1(CLK, DT);

int16_t led_delay=50;

void setup() {
  Serial.begin(115200);
  enc1.setType(TYPE2);
}

void loop() {
  enc1.tick();
  if (enc1.isRight())
    {
    led_delay=max(0,--led_delay);
    Serial.println(led_delay);
    }
  if (enc1.isLeft())
    {
    led_delay=min(200,++led_delay);
    Serial.println(led_delay);
    }
}
Регулируется задержка в диапазоне от 0 до 200, эти константы жестко записаны в коде, хотите сделайте определение для них сами.

Совмещаем эти два кода в один и ... не прокатило...
Вот он совмещенный код

C++:
#include <FastLED.h>
#define LED_PIN     3
#define NUM_LEDS    120
#define CHIPSET     WS2812
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
#define BRIGHTNESS  128


#define CLK 6
#define DT 7
#include "GyverEncoder.h"
Encoder enc1(CLK, DT);

int16_t led_delay = 50;

void setup() {
  Serial.begin(115200);
  enc1.setType(TYPE2);

  LEDS.addLeds<WS2812, LED_PIN, RGB>(leds, NUM_LEDS);
  LEDS.setBrightness(84);
}

void loop() {
  enc1.tick();
  if (enc1.isRight())
  {
    led_delay = max(0, --led_delay);
    Serial.println(led_delay);
  }
  if (enc1.isLeft())
  {
    led_delay = min(200, ++led_delay);
    Serial.println(led_delay);
  }

  for (int i = 0; i < NUM_LEDS; i++)
  {
    leds[i] = CRGB(random(256), random(256), random(256));
    FastLED.show();
    delay(led_delay);
  }
  for (int i = NUM_LEDS; i > 0; i--)
  {
    leds[i - 1] = CRGB(random(0), random(0), random(0));
    FastLED.show();
    delay(led_delay);
  }
}

Кошмар, паника, жуть...
Моя любимая часть , рассчеты:
В коде у меня стоит задержка 50 мс. Считаем, 120 светодиодов, умножить на 50, да два раза, получаем 12 000 мс, 12 секунд. И энкодер считывает своё состояние раз в 12 секунд, он просто отказывается работать с такой частотой. По моим прикидкам он работает хорошо, когда считывает свои показания.. более 12 тысяч раз в секунду. Можно попробовать настроить ENC_DEBOUNCE_TURN на 12-13 секунд.. но это жопа!

Практическая часть проверки расчетов.
Я уже несколько раз приводил простой код для проверки кол-ва циклов в секунду, помогает понять что не так. Приведу его еще раз. И встрою его в неработающий код. Просто добавлю в самый низ, а вызов функции ShowFPS() сделаю самой первой строкой в цикле loop
C++:
void ShowFPS()
{
  static uint32_t tm_m = 0;
  static uint32_t cnt_m = 0;
  cnt_m++;
  if ((millis() - tm_m) > 1000)
  {
    Serial.print("loop per sec: "); Serial.println(cnt_m);
    cnt_m = 0;
    tm_m = millis();
  }
}

Результат предсказуемый, хоть и не правильный, 1 цикл, так как это миниму, что она показывает. Приехали. Если закомментировать отработку эфекта, то получим 56 тысяч циклов в секунду. Отлично. Значит функция с задержками не подходит.
А если сделать начальную задержку не 50мс, 1 ? Все тот же 1 цикл в секунду, и пару раз я даже сумел поменять значение задержки, но это не спасло, т.к. нормальной работой это назвать нельзя.

Это была присказка, теперь сказка. Будем городить чудо.
Как выгодать время, которое занимает ожидание между изменением огоньков ?
1) Возьмем под это все сделаем отдельную функцию. void my_effect();
2) В начале заведем переменную, которая будет контролировать время my_timer
Вот кусок кода в начале функции эффекта, который проверяет настало ли время выполнять все или нет.
C++:
  static uint32_t my_timer=0;
  if (millis()-my_timer)<led_delay return;
  my_timer=millis();
static говорит, что значение переменной не будет утеряно после выхода из функции, хоть переменная и локальная.
если текущее время минус время последнего выполнения меньше задержки , то выходим из функции.
запоминаем последнее время выполнения функции. эту строку можно поставить в конец, тогда интервал будет чистым, в начале точным.
Но этого мало! теперь надо капитально переделывать код... я бы даже сказал, что переписать его легче , чем изменить.
1) Заводим логическу статическую переменную , которая будет говорить нам зажигаем мы огни или гасим . Пусть будет goUP и изначально она будет static bool goUp=true;

2) Заводим статический номер текущего огня, static int16_t Nomer=0;

Зажигать или гасить будем по одному номеру, после чего номер увеличиваем или уменьшаем на 1, если он будет равен числу диодов или меньше 0, то это значит мы уже зажгли последний или первый диод и пора идти в другую сторону.


C++:
#include <FastLED.h>
#define LED_PIN     3
#define NUM_LEDS    120
#define CHIPSET     WS2812
#define COLOR_ORDER GRB
CRGB leds[NUM_LEDS];
#define BRIGHTNESS  128


#define CLK 6
#define DT 7
#include "GyverEncoder.h"
Encoder enc1(CLK, DT);

int16_t led_delay = 10;

void setup() {
  Serial.begin(115200);
  enc1.setType(TYPE2);

  LEDS.addLeds<WS2812, LED_PIN, RGB>(leds, NUM_LEDS);
  LEDS.setBrightness(84);
}

void loop() {
  ShowFPS();
  enc1.tick();

  if (enc1.isRight())
  {
    led_delay = max(0, --led_delay);
    Serial.println(led_delay);
  }
  if (enc1.isLeft())
  {
    led_delay = min(200, ++led_delay);
    Serial.println(led_delay);
  }

  my_effect();
}

void ShowFPS()
{
  static uint32_t tm_m = 0;
  static uint32_t cnt_m = 0;
  cnt_m++;
  if ((millis() - tm_m) > 1000)
  {
    Serial.print("loop per sec: "); Serial.println(cnt_m);
    cnt_m = 0;
    tm_m = millis();
  }
}


void my_effect()
{
  static uint32_t my_timer=0;
  if ((millis()-my_timer)<led_delay) return;
  my_timer=millis();


  static bool goUp=true
  static int16_t Nomer=0;
  if (goUp) leds[Nomer] = CRGB(random(256), random(256), random(256));
  else leds[Nomer] = CRGB(random(0), random(0), random(0));
  FastLED.show();

  if (goUp) Nomer++; else Nomer--;

  if (Nomer==NUM_LEDS)
    {
      Nomer--;
      goUp=false;
    }
  if (Nomer<0)
    {
      Nomer++;
      goUp=true;
    }
}

Наиболее прошареные скажут, а как же прерывания ? А так:
Вообще-то, FastLED-у можно запретить запрещать прерывания. Но к данной теме это не относится.
 

AlexGyver

★★★★★★✩
Команда форума
30 Июл 2018
359
567
Можно обновлять ленту не чаще, чем 30-60 раз в секунду, куча времени останется на энкодер. Также у фл в файле конфиг.н можно включить разрешение прерываний и будет вообще пофиг)
 

Старик Похабыч

★★★★★★★
14 Авг 2019
4,190
1,280
Москва
Обновление надо настраивать исходя из желаемого результата. Но если обновление будет занимать какое то продолжительное время, то жди неудобств и неприятностей.
 

poty

★★★★★★✩
19 Фев 2020
2,990
895
Интересно, а почему бы не включить обработку энкодера внутрь циклов управления светодиодами? Не нужно связываться ни с какими таймерами.
Выключение прерываний в FastLED в конструкциях, в которых используются прерывания (тот же таймер) весьма часто даёт проблемы на длинных цепочках светодиодов. FastLED, конечно, пытается это компенсировать повтором передачи, но это заметно.
 
Изменено:

Старик Похабыч

★★★★★★★
14 Авг 2019
4,190
1,280
Москва
Это один из вариантов решения проблемы. Не самый изящный, но работающий.
Но я считаю, что обработчик должен быть только в одном месте. А если не только энкодер, а еще кнопки, а еще датчики ? а эффектов 50 ?
 

poty

★★★★★★✩
19 Фев 2020
2,990
895
Есть такое, но, насколько я понимаю, ветка и была создана для того, чтобы уточнить: факт успешной загрузки правильного кода в МП не гарантирует того, что это будет работать, если имеются несколько конкурирующих процессов. Отсутствие многопоточности и аппаратной поддержки основных интерфейсов (не зависящей от кода и не блокирующей прерывания) не позволят использовать все "ноги" Ардуино для интерактивных вещей.
В случае, когда "становится жарко", есть отличная возможность использования совсем простых МП для обработки атомарных операций. А уж соединить это потом в шину с "основным процессором" - дело ОДНОГО обработчика, а не многих.
А вообще, поднятый вопрос - отличная основа для статьи, на которую можно ссылаться для начинающих. Причесать, сделать best-practice, порекомендовать библиотеки.
 
Изменено: