Эффекты для WS2812B, автономная работа от SD карточки

Этот проект является продолжением темы “Создание эффектов для WS2812B”.
В его основе лежит использование программы для компьютера LedsImg.exe, которая, собственно, и создаёт файлы световых эффектов. В предыдущей версии я использовал вывод на ленту непосредственно из программы через Serial. Теперь же мы будем сначала записывать файлы на SD карточку, а уж с неё-то и выводить на ленту. Уже без компьютера.
Для начала о “железе”. Устройство управления лентой построено на двух Arduino Nano:

Одна из них, та, что обозначена, как Master, выполняет чтение из карточки, регулирует яркость, обеспечивает меню выбора нужного файла и обслуживает экран LCD1602.
Вторая, которая Slave, принимает данные от Arduino-Master и выводит их на ленту. Память у Nano маленькая, поэтому она у обеих Ардуин забита, что называется, под завязку.
Итак, что нам нужно:

  • Arduino Nano - 2шт.
  • SD-card Reader - 1шт. Вот такой:
  • Экран LCD1602 с преобразователем PCF8574-I2C:
  • Тактовые кнопки - 6 шт:
    Keyboard
  • Переменное сопротивление номиналом 2…5 Ком - 1 шт.
  • Постоянные сопротивления 1.2 Ком - 8 шт. Для клавиатуры можно использовать любые в пределах 1…2 Ком, для светодиодов индикации - 0.5…1.5 Ком.
  • Обычные светодиоды Ф3-5 мм любого цвета.
  • Сдвоенный DIP-переключатель или два одиночных (на схеме Sw1 и SW2). Они нужны для того, чтобы размыкать линии RX/Tx во время заливки скетчей.
    Плату я не делал, собирал на макетке, поэтому не привожу.
    Итак, кострукция собрана. Первое, что нужно сделать, это протестировать клавиатуру. Для этого в плату Master заливаем небольшой скетч:
#define pinKey  A1

word old = 0;

void setup() {
  pinMode(pinKey,INPUT_PULLUP);
  Serial.begin(115200);
}

void loop() {
  testButtons();
}

void testButtons() {
  word v = analogRead(pinKey);
  if (v!=old) {
    Serial.println(v);
    old = v;
  }
}

Затем открываем монитор порта и нажимаем кнопки. Значения, которые выводятся в монитор, записываем на бумажке (не забыв указать, какой именно кнопке это значение соответствует). Записываем также значение, которое выводится при отсутствии нажатий.
Затем открываем скетч “LEDS_SD_Master.ino” и корректируем значения в функции опроса клавиатуры. Как это сделать - подробно описано в самом скетче.
Теперь скетчи “LEDS_SD_Master.ino” и “LEDS_SD_Slave.ino” можно заливать в соответствующие платы. Не забудьте после этого вновь подключить линии RX/Tx.
Замечание: светодиодные индикаторы Led1 и Led2 вместе с резисторами R8 и R9 можно вообще исключить, т.к. все режимы работы отображаются на экране LCD. Я, например, их не ставил.
Теперь о программе LedsImg.exe. В архиве находится обновлённая версия программы. Она полностью сохранила прежнюю функциональность, но кое-что добавилось. Нажав на кнопку с глазиком:
ButtonShow
вы откроете окно создания светового шоу:


Теперь здесь два списка. В левом - все ранее созданные вами проекты, в правом - наборка из этих эффектов. В принципе, стандартный интерфейс, поэтому разберётесь. Переносите в список справа те проекты, которые хотите объединить в одно шоу. Причём в любом порядке и в любом количестве. Любой проект может повторяться сколько угодно раз. Для каждого проекта указываете желательное время воспроизведения. Желательное потому, что скорость воспроизведения будет зависеть от количества светодиодов и других факторов. Подробнее об этом смотрите файл “Структура файлов dpf.txt”, который вы найдёте в архиве.
Нажав на кнопку “Ок” вы начнёте воспроизведение вашего шоу по прежней схеме, т.е. через компьютер. Это если вы подключились старой платой из предыдущего проекта. А для того, чтобы создать файл для SD карточки, вам необходимо нажать на кнопку с дискетой и значком Ардуино на ней. По умолчанию файл записывается в папку "…\PROJECT\DPF", но если в USB вставлен адаптер с SD карточкой, то пишите прямо на неё. И только в корень тома - папки скетч не просматривает.
Замечание по именованию файлов. Формат имени должен быть 8.3, т.е. не более 8 символов на имя и 4 символа (включая точку) на расширение. Расширение должно быть только “.dpf”. Имя файла - только латиница, цифры и некоторые символы (исключая “*” и “?”).

Для тех, кто хочет и имеет возможность написать собственную десктопную программу создания эффектов (или адаптировать файлы, создаваемые другими программами) я даю полное описание структуры файлов “.dpf” - смотрите в архиве файл “Структура файлов dpf.txt”.
Скетчи содержат весьма подробные комментарии, поэтому здесь их я не описываю. Всё в архиве:
LedsImg2.zip

1 лайк

Какой смысл в использовании именно Arduino Nano, если проект не подразумевает обмена информации с компьютером?

Не понял вопроса - что вы имеете в виду?

Повторюсь: почему для проекта выбраны именно Arduino Nano?
И второй вопрос: зачем их две?

Следующий вопрос: почему такая странная схема клавиатуры?
IMHO она:

  • не обеспечивает какой либо вменяемый уровень при незамкнутых кнопках,
  • не обеспечивает один фиксированный уровень при нажатии на кнопку (может быть шум в пределах одной или нескольких единиц),
  • будет наверняка странно себя вести при одновременном нажатии на две или более кнопок.

И еще просьба: разместите, пожалуйста, как положено, скетч непосредственно в текст темы. Ссылки на скетчи на этом форуме mauvais ton.

Выбраны Нано, потому, что их у меня достаточно много в наличии. Две - потому, что одной памяти не хватит. Я же написал, что память у обеих ардуин загружена почти под завязку.

  • Вменяемый уровень она обеспечивает, потому, как пин определён как INPUT_PULLUP, т.е. подтянут к питанию.
  • Шум, безусловно, есть. Но мы от него очень успешно избавляемся правильной калибровкой значений и ступенчатым опросом - смотрите скетч.
  • Если вы расположите кнопки чуть-чуть с разносом, то исключите одновременное нажатие. Ну, разве что кулаком.

Ну, я не в курсе, что для вас лично является mauvais ton. Поэтому не реагирую.
А вот на ПРОСЬБУ вполне:

Скетч LEDS_SD_Master.ino

/*
Программа, обеспечивающая управление лентой
на светодиодах WS2812B. Считывание данных выполняется с SD карточки.
Количество светодиодов может быть от 2-х до 300.
Стандарт: 5 метров ленты с плотностью светодиодов 60 штук на метр.
Итого общее количество 300 штук на ленту.

Скетч загружается в плату, обозначенную на схеме как Arduino - Master.
Не забудьте перед загрузкой разомкнуть линии RX/TX, соединяющие плату
с Arduino - Slave.

Скетч работает в комплексе с компьютерной программой LedsImg.exe,
которая поставляется в дистрибутиве. Она, собственно, и создаёт
световые программы, которые затем записываются на SD карточку.

Скетч разработан Юрием Степановым в 2022 году. 

e-mail: `ruskuzmich1@gmail.com`
Viber: +380994743972
*/

// Подключаем библиотеки:
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <EasyTransfer.h>
#include <LiquidCrystal_I2C.h>

// Определение пинов платы:
#define pinKey    A1 // Пин, к которому подключена клавиатура
#define pinCS     10 // Пин, к которому подключается CS кардритера
#define pinBright A0 // Сюда подключаем движок потенциометра
#define pinPause  A2 // Пин, управляющий индикацией режима ПАУЗА
#define pinHide   A3 // Пин, управляющий индикацией режима ГАШЕНИЕ
// Константы для работы скетча:
#define _Start     1
#define _Prev      2
#define _Next      3
#define _Pause     4
#define _Stop      5
#define _Hide      6

// Описание специальных символов для LCD1602:
byte mPlay[8] = {B01000, B01100, B01110, B01111, B01110, B01100, B01000, B00000};   // 1 - символ "Воспроизведение"
byte mPause[8] = {B00000, B11011, B11011, B11011, B11011, B11011, B11011, B00000};  // 2 - символ "Пауза"
byte mHide[8] = {B00000, B11111, B11111, B11111, B11111, B11111, B11111, B00000};   // 3 - символ "Гашение"

// Переменная, определяющая режим обработки данных:
bool     readMode = false;

// Флаг блокировки работы системы
// (если установлен, то Arduino следует перегрузить):
bool _reboot = false;

// Файлы для работы с SD карточкой:
File     Prog;
File     root;
File     Menu;

// Различные переменные для работы скетча:
uint32_t filePos = 0;     // позиция в файле данных
uint32_t fileSize = 0;    // размер файла данных
String   fileName = "";   // имя файла данных
long     filesCount = 0;  // количество файлов на SD карточке
bool     hasSD = false;   // флаг готовности SD карточки
bool     doDraw = false;  // флаг режима вывода данных на ленту (вернее, на Arduino - Slave)
byte     bright = 255;    // максимальная яркость светодиодов
float    kBr = 1.0;       // коэффициент изменения яркости (определяется положением движка потециометра R1)
word     workFile = 0;    // позиция в файле меню
bool     isPause = false; // флаг режима ПАУЗА
bool     isHide = false;  // флаг режима ГАШЕНИЕ

// Буфер для сглаживания показаний потенциометра яркости:
word     bufBright[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

// Создаём объект EasyTransfer для передачи данных:
EasyTransfer ET;
// Определяем структуру данных:
struct SEND_DATA {
  uint8_t Data[3];
};
// Переменная в формате SEND_DATA для заполнения данными:
SEND_DATA ledData;

// Создаём объект для вывода данных на дисплей по протоколу I2C:
LiquidCrystal_I2C lcd(0x27, 16, 2);

// ============================ //
// Начальные установки программы.
// ============================ //
void setup() {
  // Назначаем пины для ввода/вывода:
  pinMode(pinKey, INPUT_PULLUP);
  pinMode(pinCS, OUTPUT);
  pinMode(pinPause, OUTPUT);
  pinMode(pinHide, OUTPUT);
  
  // Гасим светодиоды индикации:
  digitalWrite(pinPause, HIGH);
  digitalWrite(pinHide, HIGH);
  
  // Инициализируем библиотеку Wire:
  Wire.begin();

  // Инициализируем библиотеку LiquidCrystal_I2C:
  lcd.init();
  // Включаем подсветку экрана:
  lcd.backlight();
  // Очищаем экран:
  lcd.clear();
  // Записываем в память LCD1602 специальные символы:
  lcd.createChar(1, mPlay);
  lcd.createChar(2, mPause);
  lcd.createChar(3, mHide);

  // Инициализируем SD карточку:
  if (!SD.begin(pinCS)) {
    // если не удалось, выводим сообщение об ошибке:
    lcd.setCursor(0,0);
    lcd.print("SD not found!");
  } else { // если инициализация успешна, то:
    // устанавливаем флаг готовности SD карточки:
    hasSD = true;
    // выводим сообщение об удачной инициализации:
    lcd.setCursor(0,0);
    lcd.print("SD done.");
  }

  // Инициализируем Serial:
  Serial.begin(115200);
  // Инициализируем библиотеку EasyTransfer:
  ET.begin(details(ledData), &Serial);

  // Создаём меню программы:
  createMenu();
}

// ========================= //
// Основной цикл программы.
// ========================= //
void loop() {
  if ((!hasSD) && (!_reboot)) {
    // Если есть проблемы с SD карточкой, то выводим
    // соответствующее сообщение и блокируем работу:
    lcd.clear();
    lcd.setCursor(0,0);
    lcd.print("SD card error.");
    lcd.setCursor(0,1);
    lcd.print("Reboot system!");
    _reboot = true;
  }
  if (!_reboot) {
    if (doDraw) {
      // Если установлен флаг режима вывода данных,
      // то считываем данные из файла на SD карточке:
      readSD();
    } else {
      // Если флаг режима вывода данных сброшен,
      // то работаем в режиме меню:
      doMenu(workFile);
    }
  }
}

// ======================================= //
// Функция чтения данных из файла
// с последующей передачей на Adruino-Slave:
// ======================================= //
void readSD() {
  byte val;
  // "Бесконечный" цикл:
  while (true) {
    // считываем данные о яркости:
    setBright();
    // Если не включен режим ПАУЗА, то считываем данные
    // с SD карточки и передаём их на Arduuino-Slave:
    if (!isPause) {
      // Если достигнут конец файла данных, то возвращаемся
      // к его началу:
      if (filePos>=fileSize) {
        filePos = 0;
        Prog.seek(0);
      }
      // Считываем из файла бдок из трёх байт и
      // записываем в структуру ledData.
      // Если включен режим ПАУЗА, что чтение из
      // файла не выполняем и данные не передаём:
      for (byte j=0; j<3; j++) {
        val = Prog.read();
        ledData.Data[j] = val;
      }
      // Если первый байт в тройке не является командой:
      // 253 - команда переустановки данных о фактическом количестве светодиодов
      // 255 - команда начала блока данных фреймов (в формате HSV).
      // (см. информацию о формате файлов программы).
      if ((ledData.Data[0] != 253) && (ledData.Data[0] != 255)) {
        if (isHide) { // если включен режим ГАШЕНИЕ, то
          // переустанавливаем значение байта Value в 0:
          ledData.Data[2] = 0;
        } else { // в обычном режиме изменяем значение
          // байта Value в соответствии с положением регулятора яркости:
          ledData.Data[2] = round(ledData.Data[2]*kBr);
        }
      }
      // Отправляем данные на Arduuino-Slave:
      ET.sendData();
      // увеличиваем значение указателя положения в файле:
      filePos += 3;
    }
    // Проверяем состояние клавиатуры:
    byte key = checkButtons();
    // Реакция на нажатие клавиш:
    switch (key) {
      // Если нажата клавиша _Start или _Stop,
      // то завершаем чтение данных и выходим в меню:
      case _Start:
      case _Stop:
        doDraw = false;  // сбрасываем флаг режима вывода данных на ленту
        isPause = false; // сбрасываем флаг режима ПАУЗА
        isHide = false;  // сбрасываем флаг режима ГАШЕНИЕ
        // Формируем команду "Погасить светодиоды":
        ledData.Data[0] = 254;
        ledData.Data[1] = 0;
        ledData.Data[2] = 0;
        // Отправляем на Arduuino-Slave:
        ET.sendData();
        // Закрываем файл данных:
        Prog.close();
        // Очищаем экран:
        lcd.clear();
        // Позиционируем указатель в файле меню:
        Menu.seek(workFile*14);
        // Считываем из файла меню
        // название файла данных:
        loadFileName(workFile);
        // Выходим из функции readSD:
        return;
        break;
      case _Pause:
        // Эта клавиша срабатывает только
        // если ВЫКЛЮЧЕН режим ГАШЕНИЕ:
        if (!isHide) {
          // переключаем режим на противоположный:
          isPause = !isPause;
          // включаем/выключаем индикатор:
          digitalWrite(pinPause, !isPause);
          // отображаем режим на экране LCD:
          lcd.setCursor(15, 0);
          if (isPause) {
            lcd.print("\2"); // символ "Пауза"
          } else {
            lcd.print('\1'); // символ "Воспроизведение"
          } 
        }
        break;
      case _Hide:
        // Эта клавиша срабатывает только
        // если ВЫКЛЮЧЕН режим ПАУЗА:
        if (!isPause) {
          // переключаем режим на противоположный:
          isHide = !isHide;
          // включаем/выключаем индикатор:
          digitalWrite(pinHide, !isHide);
          // отображаем режим на экране LCD:
          lcd.setCursor(15, 1);
          if (isHide) {
            lcd.print("\3"); // символ "Гашение"
          } else {
            lcd.print(' '); // удаляем символ "Гашение" пробелом
          }
        }
        break;
    }
  }
}

// ========================= //
// Функция опроса клавиатуры.
// ========================= //
// Клавиатуру следует предварительно откалибровать.
// Для этого предварительно загрузите тестовый скетч
// LEDS_KEY_TEST, откройте монитор порта и выпишите
// значения, которые отобразятся при нажатии клавиш.
// У меня это были:
// 1010 - нет нажатий
// _Prev = 15, _Start = 210, _Next = 382, _Pause = 542, _Stop = 702, _Hide = 863
// Эти величины будем использовать для расчёта тестовых значений, как это показано ниже:
byte checkButtons() {
  byte n = 0;
  // считываем данные с пина клавиатуры:
  word v = analogRead(pinKey);
  // если это значение маньше тестового,
  // то есть нажатие:
  if (v<940) { //(1010 + 863)/2
    // защита от дребезга:
    // пауза и повторное считывание:
    delay(50);
    v = analogRead(pinKey);
    // если значение по прежнему маньше тестового,
    // то это действительно нажатие.
    // Сравниваем это значение с рассчитанными тестовыми
    // и делаем вывод, какая клавиша нажата.
    // Не нажимайте одновременно 2 или более клавиш,
    // результат непредсказуем.
    if (v<940) {
      if (v<112) { //(210 + 15)/2
        n = _Prev;
      } else {
        if (v<296) { //(382 + 210)/2
          n = _Start;
        } else {
          if (v<462) { //(542 + 382)/2
            n = _Next;
          } else {
            if (v<622) { //(702 + 542)/2
              n = _Pause;
            } else {
              if (v<783) { //(863 + 702)/2
                n = _Stop;
              } else {
                if (v<940) { //(1010 + 863)/2
                  n = _Hide;
                }
              }
            }
          }
        }
      }
    }
  }
  // Если было нажатие, то ждём отпускания клавиши:
  while (v<940) {
    v = analogRead(pinKey);
  }
  // Возвращаем код нажатой клавиши:
  return n;
}

// ======================================== //
// Функция чтения имени файла данных из файла
// меню. Параметр fp - позиция в файле меню.
// ======================================== //
String loadFileName(word fp) {
  String fn;
  String c;
  byte p;
  
  fn = "";
  // Считываем в строку fn имя файла:
  for (byte i=0; i<12; i++) {
    char c = Menu.read();
    fn += c;
  }
  // Обрезаем концевые пробелы:
  fn.trim();
  // Переводим в верхний регистр:
  fn.toUpperCase();
  // Увеличиваем номер позиции в файле меню.
  // Это для того, чтобы номера файлов на дисплее
  // начинались не с нуля, а с единицы:
  fp++;
  
  // Формируем сроку для вывода на дисплей:
  // присваиваем строковой переменной с значение
  // номера файла (нумерация файлов определяется
  // временем их создания):
  c = fp;
  // определяем длину имени файла без раширения:
  byte k = fn.indexOf(".");
  // окончательно формируем строку.
  // Должно получиться нечто такое:
  // "3. PROJ_5"
  // где "3. " - номер файла, а "PROJ_5" - его название
  c = c + ". " + fn.substring(0,k);

  // Центрируем строку и выводим на экран дисплея:
  lcd.setCursor(0,1);
  lcd.print("                "); // очистка всей строкм
  p = (16 - c.length())/2;       // центровка
  lcd.setCursor(p,1);
  lcd.print(c);
  return fn;
}

// =============================== //
// Функция меню выбора файла данных.
// =============================== //
void doMenu(word fpos) {
  String fn;
  String c;
  byte k, b1, b2;

  // Открываем файл меню:
  Menu = SD.open("Menu.txt", FILE_READ);
  // Если файл открылся, то:
  if (Menu) {
    // Выводим на дисплей приглашение к выбору файла данных:
    lcd.clear();
    lcd.setCursor(2,0);
    lcd.print("Select File:");
    // Позиционизуемся в файле меню на последнем выбранном
    // файле данных. Вначале это самый первый файл.
    Menu.seek(fpos*14);
    // Счтываем имя файла и выводим на дисплей:
    fn = loadFileName(fpos);
    // Циклическая работа меню:
    while (true) {
      // Опрашиваем клавиатуру:
      byte key = checkButtons();
      // Реакции на нажатые клавиши:
      switch (key) {
        case _Prev:
          // Клавиша выбора предыдущего файла в списке.
          // Если перед этим был выбран первый файл,
          // то указатель переместится на последний:
          if (fpos>0) {
            fpos--;
          } else {
            fpos = filesCount-1;
          }
          // Позиционируемся в файле меню:
          Menu.seek(fpos*14);
          // Считываем имя файла данных и выводим на дисплей:
          fn = loadFileName(fpos);
          break;
        case _Next:
          // Клавиша выбора следующего файла в списке.
          // Если перед этим был выбран последний файл,
          // то указатель переместится на первый:
          if (fpos<filesCount-1) {
            fpos++;
          } else {
            fpos = 0;
          }
          // Позиционируемся в файле меню:
          Menu.seek(fpos*14);
          // Считываем имя файла данных и выводим на дисплей:
          fn = loadFileName(fpos);
          break;
        case _Start:
          // Клавиша запуска выбранного файла данных на воспроизведение.
          // Запоминаем номер выбранного файла:
          workFile = fpos;
          // Если файл данных был открыт, закрываем его:
          if (Prog) Prog.close();
          // Открываем новый файл (на чтение):
          Prog = SD.open(fn, FILE_READ);
          // Очищаем дисплей:
          lcd.clear();
          lcd.setCursor(0,0);
          // Если файл успешно открыт, то:
          if (Prog) {
            // Выводим информацию о файле (имя без расширения):
            lcd.print("Out: ");
            k = fn.indexOf(".");
            c = fn.substring(0,k);
            lcd.print(c);
            // Выводим символ "Воспроизведение":
            lcd.setCursor(15, 0);
            lcd.print('\1');
            // Считываем размер файла данных:
            fileSize = Prog.size();
            // На всякий случай позиционируемся в начало:
            filePos = 0;
            Prog.seek(0);
            // Устанавливаем флаг режима вывода данных:
            doDraw = true;
            // Возврат из функции doMenu в функцию loop^
            return;
          } else {
            // Если файл открыть не удалось, то выводим сообщение об ошибке:
            lcd.clear();
            lcd.setCursor(0, 0);
            lcd.print("Error open file!");
            lcd.setCursor(0, 1);
            lcd.print("Press any key.");
            // Ждём нажатия на любую клавишу:
            key = checkButtons();
            while (key == 0) {
              key = checkButtons();
            }
            // Сбрасываем флаг готовности SD:
            hasSD = false;
            // Выходим из программы:
            return;
          }
          break;
      }
    }
  }
}

// ========================= //
// Функция сглаживания данных
// регулятора яркости.
// ========================= //
// Функция позволяет уменьшить шум,
// неизбежный в работе потенциометра.
word getBufBright(word val) {
  word sum = 0;
  // перемещаем данные в буфере по принципу стека:
  for (byte i=0; i<9; i++) {
    bufBright[i] = bufBright[i+1];
    // суммируем значения:
    sum += bufBright[i];
  }
  // присваиваем переданное функции значение
  // последнему элементу массива:
  bufBright[9] = val;
  // суммируем значения:
  sum += val;
  // возвращаем усреднённое значение данных в массиве:
  return (sum/10);
}

// ========================= //
// Функция контроля яркости:
// ========================= //
void setBright() {
  // Считываем значение потенциометра:
  word v = analogRead(pinBright);
  // Записываем в буфер и получаем сглаженное значение:
  v = getBufBright(v);
  // вычисляем долю этого значения от полного
  // (= 1023 при подсоединении движка потенциометра к +5v):
  float e = v/1023.0;
  // Переводим это значение в проценты от полной яркости:
  byte n = round(e*100);
  // Если значение отличается от прежнего, то:
  if (n!=bright) {
    // сохраняем новое значение
    bright = n;
    // вычисляем коэффициент изменения яркости:
    kBr = float(bright)/100.0;
    // выводим значение яркости (в %) на дисплей
    lcd.setCursor(0,1);
    lcd.print("Bright=");
    lcd.print(bright);
    lcd.print("%  ");
  }
}

// ======================================== //
// Функция создания меню выбора файла данных.
// ======================================== //
void createMenu() {
  String nm;
  // Устанавливаем счётчик файлов на SD карточке в 0.
  // Замечание 1: все файлы должны располагаться в
  // корне файловой системы, папки не просматриваются.
  // Замечание 2: имена файлов должны быть в формате
  // 8.3, т.е. длина имени не должна превышать 8 символов.
  // Имена должны быть только на латинице.
  // Расширение файлов предопределено, это ".dpf".
  filesCount = 0;
  // Открываем корневой каталог:
  root = SD.open("/");
  // Если успешно, то:
  if (root) {
    // Удаляем прежний файл меню, если таковой имеется:
    if (SD.exists("Menu.txt")) {
      SD.remove("Menu.txt");
    }
    // Создаём новый файл меню и открываем для записи:
    Menu = SD.open("Menu.txt", FILE_WRITE);
    // Открываем файл для просмотра каталога:
    File Next = root.openNextFile();
    // Если каталог не пуст, то:
    while (Next) {
      // извлекаем имя файла:
      nm = Next.name();
      // преобразуем имя в нижний регистр символов:
      nm.toLowerCase();
      // если расширение файла = ".dpf",
      // то это то, что нам нужно:
      if (nm.indexOf(".dpf")>0) {
        // Если длина имени вместе с расширением
        // меньше 12 символов, то дополняем его пробелами.
        // Такая операция нужна для того, чтобы стандартизировать
        // длину строк, записываемых в файл меню. Она будет
        // равна 14 символам - длина имени(12 символов) + cr/lf.
        while (nm.length()<12) {
          nm = ' ' + nm;
        }
        // Увеличиваем счётчик обнаруженных файлов:
        filesCount++;
        // Записываем имя файла данных в файл меню:
        Menu.println(nm);
      }
      // Закрываем файл просмотра каталога:
      Next.close();
      // Открываем следующий файл в каталоге:
      Next = root.openNextFile();
      // Повторяем цикл, пока не просмотрим все файлы.
    }
    // Закрываем файл меню:
    Menu.close();
  } else {
    // Если файл _root не открылся, значит ошибка файловой системы.
    // Сбрасываем флаг готовности SD карточки:
    hasSD = false;
  }
  // Если нужных файлов на карточке не обнаружено,
  // то сбрасываем флаг готовности:
  if (filesCount==0) hasSD = false;
  // Закрываем корневой каталог (если есть):
  if (root) root.close();
  // Если чтение было успешным:
  if (hasSD) {
    // Выводим сообщение о количестве
    // обнаруженных файлов:
    lcd.setCursor(0,1);
    lcd.print(filesCount);
    lcd.print(" Files found");
    // Торомозим на 2 секунды, чтобы успеть прочитать
    // это сообщение:
    delay(2000);
    // Считываем имя первого в списке файла данных:
    loadFileName(0);
  } else {
    // Если просмотр каталога был неуспешным,
    // то выводим соответствующее сообщение:
    lcd.setCursor(0,0);
    lcd.print("Files not found!");
  }
}

Скетч LEDS_SD_Slave:

/*
Программа, обеспечивающая управление лентой
на светодиодах WS2812B. Считывание данных выполняется с SD карточки.
Количество светодиодов может быть от 2-х до 300.
Стандарт: 5 метров ленты с плотностью светодиодов 60 штук на метр.
Итого общее количество 300 штук на ленту.

Скетч загружается в плату, обозначенную на схеме как Arduino - Slave.
Не забудьте перед загрузкой разомкнуть линии RX/TX, соединяющие плату
с Arduino - Master.

Скетч работает в комплексе с компьютерной программой LedsImg.exe,
которая поставляется в дистрибутиве. Она, собственно, и создаёт
световые программы, которые затем записываются на SD карточку.

Скетч разработан Юрием Степановым в 2022 году. 

e-mail: ruskuzmich1@gmail.com
Viber: +380994743972
*/

// Подключаем библиотеки:
#include <EasyTransfer.h>
#include <FastLED.h>

// Создаём объект EasyTransfer для приёма данных:
EasyTransfer ET;
// Определяем структуру данных:
struct RECEIVE_DATA {
  uint8_t Data[3];
};
// Переменная в формате RECEIVE_DATA для заполнения данными:
RECEIVE_DATA ledData;

/*  Пин, к которому подключена лента.
 *  Может быть любым цифровым (кроме 0 и 1) или аналоговым (кроме A6 и A7).
 *  Не забудьте, что между пином Arduino и входом данных ленты
 *  нужно поставить резистор 100Ом ... 1К! */
#define pinLeds  2

// Режимы работы скетча:
#define _Wait     0 // ожидание команды
#define _Read     1 // приём данных через Serial

int  numLeds = 300; // максимальное количество светодиодов
CRGB Leds[300];     // структура, в которую записывается фрейм "картинки" на ленте

byte readMode = _Wait; // текущий режим работы
int  _count; // счётчик фреймов
// Фрейм - это массив данных о цвете светодиодов в ленте.
// Размер фрейма равен numLeds * 3, где каждая тройка байт - это
// значение цвета в формате HSV (Hue, Saturation, Value).

// ============================ //
// Начальные установки программы.
// ============================ //
void setup() {
  // Подключаем ленту (для других типов лент смотрите 
  // документацию к библиотеке FastLed):
  FastLED.addLeds<WS2812B, pinLeds, GRB>(Leds, numLeds);
  /*  Устанавливаем максимальную яркость светодиодов.
   *  Зависит от конкретной ленты и условиях её эксплуатации.
   *  Если слишком ярко, можно уменьшить. Попутно уменьшится
   *  и максимальный ток чере ленту. */
  FastLED.setBrightness(255);

  /* Максимальный ток (при всех горящих белым цветом светодиодах) рассчитывается так:
  * На 1м ленты (60 светодиодов) мощность = 14.5Вт
  * На один светодиод это 14.5Вт/(5V*60) = 0.048(3)A = 48.3mA
  * Ток на выбранное число светодиодов = 43.8 * NumLeds = 24 * 48.3 = 1159.2mA
  * Для 300 светодиодов (полная лента) ток будет уже 300 * 48.3 = 14500mA = 14.5A
  * Устанавливаем ограничение тока с небольшим запасом: */
  set_max_power_in_volts_and_milliamps(5, 48*numLeds);
  // Гасим светодиоды:
  SetAll(0, 0, 0);
  FastLED.show();

  // Инициализируем Serial:
  Serial.begin(115200);
  // Инициализируем библиотеку EasyTransfer:
  ET.begin(details(ledData), &Serial);

  // Вызываем фунцию циклического опроса порта Serial:
  read_Data();
}

// ========================= //
// Основной цикл программы.
// ========================= //
void loop() {
  // Здесь ничего не нужно делать,
  // всё происходит в функции read_Data.
}

// =========================== //
// Установка одного цвета сразу
// для всей ленты:
// =========================== //
void SetAll(byte red, byte green, byte blue) {
  for (int i=0; i<numLeds; i++) {
    Leds[i].r = red;
    Leds[i].g = green;
    Leds[i].b = blue;
  }
}

// =========================== //
// Функция обработки данных,
// поступающих из порта Serial.
// =========================== //
void read_Data() {
  // "Вечный" цикл:
  while (true) {
    // Если в порт поступили данные, то:
    if (ET.receiveData()) {
      // Если первый байт = 253, то это команда
      // переустановки количества светодиодов в ленте:
      if (ledData.Data[0] == 253) {
        // Гасим все светодиоды:
        SetAll(0, 0, 0);
        FastLED.show();
        // Устанавливаем новое число светодиодов:
        numLeds = min(ledData.Data[2]*256 + ledData.Data[1], 300);
        // Очищаем данные:
        FastLED.clearData();
        // Подключаем ленту:
        FastLED.addLeds<WS2812B, pinLeds, GRB>(Leds, numLeds);
        // Устанавливаем максимальную яркость светодиодов:
        set_max_power_in_volts_and_milliamps(5, 48*numLeds);
        // Устанавливаем режим ожидания следующей команды:
        readMode = _Wait;
      }
      // Если первый байт = 254, то это команда
      // гашения всех светодиодов:
      if (ledData.Data[0] == 254) {
        SetAll(0, 0, 0);
        FastLED.show();
        readMode = _Wait;
      }

      // Если первый байт отличается от 253 или 254, то:
      switch (readMode) {
        case _Wait: // Если режим ожидания следующей команды:
          // Если первый байт = 255, то это команда
          // начала приёма данных фрейма:
          if (ledData.Data[0] == 255) {
            // Устанавливаем режим приёма данных:
            readMode = _Read;
            // Обнуляем счётчик пикселей:
            _count = 0;
          }
          break;
        case _Read: // Если режим приёма данных:
          if (_count < numLeds) { // если счётчик пикселей меньше, чем их количество, то
            // преобразуем триаду данных о цвете пикселя
            // из формата HSV в RGB и записываем в структуру Leds:
            Leds[_count] = CHSV(ledData.Data[0], ledData.Data[1], ledData.Data[2]);
            // увеличиваем счётчик пикселей:
            _count++;
          } else { // если счётчик пикселей равен их количеству,
            // т.е. все данные фрейма приняты, то отправляем их на ленту:
            FastLED.show();
            // обнуляем счётчик пикселей:
            _count = 0;
            // и устанавливаем режим ожидания команды:
            readMode = _Wait;
          }
          break;
      }
    }
  }
}

Но Вы ведь выкладываете на общий форум и в расчете на повторение конструкции? Или я что-то не так понимаю?
Почему с точки зрения того, кто будет повторять конструкцию, две нано лучше, чем один блупилл?

STM32 - это не ардуино.

Растопыренными пальцам я смогу нажать больше, чем одним кулаком.
В программировании есть принцип: все ошибки пользователя, которые возможно распознать и предотвратить, должны быть предотвращены. У Вас же, судя по всему, неявно предполагается, что пользователь не будет совершать ошибок.

Можете воспринимать мое замечание как багрепорт.

Пруф?

зато желающие могут клонировать хоть на блюпилл, хоть на ESP32, хоть на RP2040

1 лайк

Смотрите скетч - явно предполагается, и соответствующие указания даны. Кроме того, подобная конструкция клавиатуры - не моя выдумка, она используется много лет и в сотнях проектов. Её основное преимущество - предельная простота. А единственный недостаток - нежелательность нажатия нескольких кнопок одновременно.

на старом форуме я выкладывал клавиатуру на аналоговый пин в которой можно одновременно нажимать несколько кнопок

Могут, если памяти хватит. Но мне что, переделывать проект под эти платы? Я дал то, что уже сделано.

в rp2040 и esp32 должно хватить

Обнаружил ошибку в скетче LEDS_SD_Slave.ino:
строку
#define pinLeds A2
нужно заменить на
#define pinLeds 2
Просто у меня лента подключена именно к A2, а не D2. В скетче на странице я исправил, а тот, что в архиве - нет.

Возможно и так, я эту тему не видел. Но, тем не менее, эта у меня работает и на плоттере, и в такой конструкции, как я здесь описал, и на станке для термической рельефной обработки изделий из пенопласта и подобных материалов. Везде без проблем.

Юрий! Как и в первой теме ты отстаиваешь несколько странные представления о своих правах.
В той теме - не выкладывать примитивный код. Тебя защитили. Пусть. Сторонник копирайта сам страдает от своего мировоззрения.

В этой теме тебе указали на не лучший способ организации клавиатуры. Можно ответить, что ты принимаешь критику, но не имеешь желания переделывать, это же так? Настаивать на том, что “и так сойдет” - очень карикатурно по-русски. Выкладывание проекта - не демонстрация того, что у тебя “и так работает”, а ответственность перед теми, кто захочет повторить. С больным самолюбием вообще не стоит ничего выкладывать. Критика - это основная “фишка” клубного форума. If U Know What I Mean.

Если по делу - то исправь, плз в первом выложенном скетче тег “три апострофа” - их надо поставить отдельной строчкой, тогда синтаксис станет подсвеченным красиво, как и во втором файле, ОК?