ПОЛЕЗНЫЕ АЛГОРИТМЫ ДЛЯ ARDUINO

На этой странице буду публиковать некоторые полезные алгоритмы для ваших проектов, которые накопились у меня за пару лет разработки собственных. Статья обновляется по мере моей ленивости, так что иногда заходите, читайте =)

Но для начала рассмотрим несколько лайфхаков!

  • Автоформатирование — Arduino IDE умеет автоматически приводить ваш код в порядок (имеются в виду отступы, переносы строк и пробелы). Для автоматического форматирования используйте комбинацию CTRL+T на клавиатуре, либо Инструменты/АвтоФорматирование в окне IDE. Используйте чаще, чтобы сделать код красивым (каноничным, классическим) и более читаемым для других!

  • Скрытие частей кода — сворачивайте длинные функции и прочие куски кода для экономии места и времени на скроллинг. Включается здесь: Файл/Настройки/Включить сворачивание кода

  • Не используйте мышку! Чем выше становится ваш навык в программировании, тем меньше вы будете использовать мышку (да-да, как в фильмах про хакеров). Используйте обе руки для написания кода и перемещения по нему, вот вам несколько полезных комбинаций и хаков, которыми я пользуюсь ПОСТОЯННО:

    • Ctrl+← , Ctrl+→ — переместить курсор влево/вправо НА ОДНО СЛОВО
    • Home , End — переместить курсор в начало/конец строки
    • Shift+← , Shift+→ — выделить символ слева/справа от курсора
    • Shift+Ctrl+← , Shift+Ctrl+→ — выделить слово слева/справа от курсора
    • Shift+Home , Shift+End — выделить все символы от текущего положения курсора до начала/конца строки
    • Ctrl+Z — отменить последнее действие
    • Ctrl+Y — повторить отменённое действие
    • Ctrl+C — копировать выделенный текст
    • Ctrl+X — вырезать выделенный текст
    • Ctrl+V — вставить текст из буфера обмена

    Местные сочетания:

    • Ctrl+U — загрузить прошивку в Arduino
    • Ctrl+R — скомпилировать (проверить)
    • Ctrl+Shift+M — открыть монитор порта

    Также для отодвигания комментариев в правую часть кода используйте TAB, а не ПРОБЕЛ. Нажатие TAB перемещает курсор по некоторой таблице, из-за чего ваши комментарии будут установлены красиво на одном расстоянии за вдвое меньшее количество нажатий!

  • Питание от пинов — во время разработки прототипов без брэдборда всегда не хватает пинов для питания датчиков и модулей. Так вот, слабые (с потреблением тока менее 40 мА) 5 Вольтовые датчики можно питать от любых пинов! Достаточно сформировать пин как выход, и подать на него нужный сигнал (HIGH — 5 Вольт, LOW — GND).

    Пример: подключаем трёхпиновый датчик звука, не используя пины 5V и GND

    #define SENSOR_VCC 2    // пин VCC сенсора на D2
    #define SENSOR_GND 3    // пин GND сенсора на D3
    #define SENSOR_OUT 4    // сигнальный пин на D4
    
    void setup() {
      // настройка пинов
      pinMode(SENSOR_VCC, OUTPUT);
      pinMode(SENSOR_GND, OUTPUT);
    
      // подаём сигналы
      digitalWrite(SENSOR_VCC, HIGH);
      digitalWrite(SENSOR_GND, LOW);
    }
    
    void loop() {
      // в качестве примера считываем сигнал
      boolean sound_signal = digitalRead(SENSOR_OUT);
    }
  • Питание от штекера для программатора. Вы наверняка задавались вопросом, а зачем на Arduino NANO на краю платы расположены 6 пинов? Это порт для подключения ISP программатора. Что он делает в списке лайфхаков? Вот вам фото распиновки, используйте!

  • Использовать библиотеку энергосбережения Low Power. Примеры и описание внутри (видео урок пока не готов)

  • В паре с библиотекой сделать несколько модификаций: отключить светодиод питания и отрезать левую ногу регулятора напряжения. ВНИМАНИЕ! Резать ногу регулятору можно только в том случае, если плата питается от источника 3-5 Вольт в пины 5V и GND.

СТРУКТУРА ПРОГРАММЫ

/*
  пример "чистого" и удобного для работы цикла loop()
  работать так гораздо удобнее, и труднее запутаться
  Пример:
  1: получение показаний с датчика, фильтрация
  2: отработка нажатий кнопок
  3: отрисовка на дисплей
  4: отправка команд на управляющие устройства
  и так далее
*/

void setup() {

}

void loop() {
  task_1();
  task_2();
  task_3();
  task_4();
  // ...
}

void task_1() {
  // какие-то действия, ведущие к одной цели
}
void task_2() {
  // какие-то действия, ведущие к одной цели
}
void task_3() {
  // какие-то действия, ведущие к одной цели
}
void task_4() {
  // какие-то действия, ведущие к одной цели
}
/*
   Данный код демонстрирует переключение режимов работы при помощи кнопки
   Для удобства используется библиотека отработки нажатий кнопки
*/

#define PIN 3        // кнопка подключена сюда (PIN --- КНОПКА --- GND)
#define MODE_AM 5    // количество режимов (от 0 до указанного)

#include "GyverButton.h"
// моя библиотека для более удобной работы с кнопкой
// скачать мождно здесь https://github.com/AlexGyver/GyverLibs

GButton butt1(PIN);  // создаём нашу "кнопку"

byte mode = 0;       // переменная режима

void setup() {
  Serial.begin(9600);
}

void loop() {
  butt1.tick();             // обязательная функция отработки. Должна постоянно опрашиваться
  if (butt1.isPress()) {    // правильная отработка нажатия с защитой от дребезга

    // увеличиваем переменную номера режма. Если вышла за количество режимов - обнуляем
    if (++mode > MODE_AM - 1) mode = 0;
  }

  // всё переключение в итоге сводится к оператору switch
  switch (mode) {
    case 0: task_0();
      break;
    case 1: task_1();
      break;
    case 2: task_2();
      break;
    case 3: task_3();
      break;
    case 4: task_4();
      break;
  }
}

// наши задачи, внутри функций понятное дело может быть всё что угодно
void task_0() {
  Serial.println("Task 0");
}
void task_1() {
  Serial.println("Task 1");
}
void task_2() {
  Serial.println("Task 2");
}
void task_3() {
  Serial.println("Task 3");
}
void task_4() {
  Serial.println("Task 4");
}
/*
   Данный код демонстрирует переключение режимов работы при помощи кнопки
   Для удобства используется библиотека отработки нажатий кнопки
   В этом варианте примера функции "режимов" вызываются только один раз
*/

#define PIN 3        // кнопка подключена сюда (PIN --- КНОПКА --- GND)
#define MODE_AM 5    // количество режимов (от 0 до указанного)

#include "GyverButton.h"
// моя библиотека для более удобной работы с кнопкой
// скачать мождно здесь https://github.com/AlexGyver/GyverLibs

GButton butt1(PIN);  // создаём нашу "кнопку"

byte mode = 0;       // переменная режима

void setup() {
  Serial.begin(9600);
}

void loop() {
  butt1.tick();             // обязательная функция отработки. Должна постоянно опрашиваться
  if (butt1.isPress()) {    // правильная отработка нажатия с защитой от дребезга

    // увеличиваем переменную номера режма. Если вышла за количество режимов - обнуляем
    if (++mode > MODE_AM - 1) mode = 0;

    // всё переключение в итоге сводится к оператору switch
    // переключение и вызов происходит только при нажатии!!!
    switch (mode) {
      case 0: task_0();
        break;
      case 1: task_1();
        break;
      case 2: task_2();
        break;
      case 3: task_3();
        break;
      case 4: task_4();
        break;
    }
  }
}

// наши задачи, внутри функций понятное дело может быть всё что угодно
void task_0() {
  Serial.println("Task 0");
}
void task_1() {
  Serial.println("Task 1");
}
void task_2() {
  Serial.println("Task 2");
}
void task_3() {
  Serial.println("Task 3");
}
void task_4() {
  Serial.println("Task 4");
}

ВРЕМЯ, ТАЙМЕРЫ

// Данный код выполняет действия периодически за указанный период

// Нам нужно задать период таймера В МИЛЛИСЕКУНДАХ
// дней*(24 часов в сутках)*(60 минут в часе)*(60 секунд в минуте)*(1000 миллисекунд в секунде)
// (long) обязательно для больших чисел, иначе не посчитает
// можно посчитать на калькуляторе, но какбэ ардуино и есть калькулятор, пусть считает...
unsigned long period_time = (long)5*24*60*60*1000;

// переменная таймера, максимально большой целочисленный тип (он же uint32_t)
unsigned long my_timer;

void setup() {
  my_timer = millis();   // "сбросить" таймер
}
void loop() {
  if ((long)millis() - my_timer > period_time) {
    my_timer = millis();   // "сбросить" таймер
    // набор функций, который хотим выполнить один раз за период
    // бла бла бла
    // ...
  }
}
// Данный код выполняет действие с периодом PERIOD на время WORK_TIME, эдакий свернизкочастотный ШИМ
// Банально автополив

// Нам нужно задать период таймера В МИЛЛИСЕКУНДАХ
// дней*(24 часов в сутках)*(60 минут в часе)*(60 секунд в минуте)*(1000 миллисекунд в секунде)
// (long) обязательно для больших чисел, иначе не посчитает
// можно посчитать на калькуляторе, но какбэ ардуино и есть калькулятор, пусть считает...
unsigned long period_time = (long)5*24*60*60*1000;

unsigned long work_time = 10000;  // время, на которое ну скажем включится лампочка

#define TIMER_START 0    // 1 - отсчёт периода с момента ВЫКЛЮЧЕНИЯ лампочки, 0 - с ВКЛЮЧЕНИЯ

// переменная таймера, максимально большой целочисленный тип (он же uint32_t)
unsigned long period_timer, work_timer;
boolean work_flag;

void setup() {
  period_timer = millis();   // "сбросить" таймер
}
void loop() {
  if ((long)millis() - period_timer > period_time) {
    period_timer = millis();   // "сбросить" таймер периода
    work_timer = millis();     // сбросить таймер выполнения
    work_flag = true;          // начали выполнение
    // включить лампу, помпу, реле, что угодно
    // банально digitalWrite(пин, HIGH)
  }
  if ( ((long)millis() - work_timer > work_time) && work_flag) {
    work_flag = false;            // сброс флага на выполнение
    // можно сбросить таймер периода ПОСЛЕ выполнения задачи. Подумайте над этим!
    if (TIMER_START) period_timer = millis();
    // выключить лампу, помпу, реле, что угодно
    // банально digitalWrite(пин, LOW)
  }
  if (work_flag) {
    // а вот этот блок кода выполняется всегда, пока мы находимся по времени "внутри" WORK_TIME
  }
}
/*
   Делаем "параллельное" выполнение нескольких задач
   с разным периодом выполнения
*/

#define PERIOD_1 100    // период первой задачи
#define PERIOD_2 2000   // период второй задачи
#define PERIOD_3 666    // ...

unsigned long timer_1, timer_2, timer_3;

void setup() {

}

void loop() {
  if (millis() - timer_1 > PERIOD_1) {    // условие таймера
    timer_1 = millis();                   // сброс таймера
    
    // выполняем блок №1 каждые PERIOD_1 миллисекунд
  }
  if (millis() - timer_2 > PERIOD_2) {
    timer_2 = millis();
    
    // выполняем блок №2 каждые PERIOD_2 миллисекунд
  }
  if (millis() - timer_3 > PERIOD_3) {
    timer_3 = millis();
    
    // выполняем блок №3 каждые PERIOD_3 миллисекунд
  }
}
/*
   Пример параллельного выполнения нескольких задач
   по таймеру. Библиотеку GyverHacks можно скачать здесь
   https://github.com/AlexGyver/GyverLibs
*/

#include "GyverHacks.h"

// создать таймер, в скобках период в миллисекундах
GTimer myTimer1(500);
GTimer myTimer2(600);
GTimer myTimer3(1000);

void setup() {
  Serial.begin(9600);
}

void loop() {
  if (myTimer1.isReady())
    Serial.println("Timer 1!");

  if (myTimer2.isReady())
    Serial.println("Timer 2!");

  if (myTimer3.isReady())
    Serial.println("Timer 3!");
}

РАБОТА С SERIAL

/*
  Данный код позволяет принять данные, идущие из порта, в строку (String) без "обрывов"
*/

String strData = "";
boolean recievedFlag;

void setup() {
  Serial.begin(9600);
}

void loop() {
  while (Serial.available() > 0) {         // ПОКА есть что то на вход    
    strData += (char)Serial.read();        // забиваем строку принятыми данными
    recievedFlag = true;                   // поднять флаг что получили данные
    delay(2);                              // ЗАДЕРЖКА. Без неё работает некорректно!
  }

  if (recievedFlag) {                      // если данные получены
    Serial.println(strData);               // вывести
    strData = "";                          // очистить
    recievedFlag = false;                  // опустить флаг
  }
}
/*
   Данный алгоритм позволяет получить через Serial пачку значений, и раскидать
   их в целочисленный массив. Использовать можно банально для управления
   ЧЕМ УГОДНО через bluetooth, так как bluetooth модули есть UART интерфейс связи.
   Либо управлять через Serial с какой-то программы с ПК
   Как использовать:
   1) В PARSE_AMOUNT указывается, какое количество значений мы хотим принять.
   От этого значения напрямую зависит размер массива принятых данных, всё просто
   2) Есть массив inputData, его размер задаётся дефайном INPUT_AMOUNT
   Вот это значение должно быть больше или равно числу СИМВОЛОВ, которое будет в пакете
   То есть в с чёт идут пробелы, цифры и заголовок и хвост
   Пример: пакет $110 25 600 920; содержит 16 символов, таким образом INPUT_AMOUNT должен быть НЕ МЕНЬШЕ!
   3) Пакет данных на приём должен иметь вид:
   Начало - символ $
   Разделитель - пробел
   Завершающий символ - ;
   Пример пакета: $110 25 600 920;  будет раскидан в массив intData согласно порядку слева направо
   Что делает данный скетч:
   Принимает пакет данных указанного выше вида, раскидывает его в массив intData, затем выводит обратно в порт.
*/
#define PARSE_AMOUNT 5       // число значений в массиве, который хотим получить
#define INPUT_AMOUNT 80      // максимальное количество символов в пакете, который идёт в сериал
char inputData[INPUT_AMOUNT];  // массив входных значений (СИМВОЛЫ)
int intData[PARSE_AMOUNT];     // массив численных значений после парсинга
boolean recievedFlag;
boolean getStarted;
byte index;
String string_convert;

void parsing() {
  while (Serial.available() > 0) {
    char incomingByte = Serial.read();      // обязательно ЧИТАЕМ входящий символ
    if (incomingByte == '$') {              // если это $
      getStarted = true;                    // поднимаем флаг, что можно парсить
    } else if (incomingByte != ';' && getStarted) { // пока это не ;
      // в общем происходит всякая магия, парсинг осуществляется функцией strtok_r
      inputData[index] = incomingByte;
      index++;
      inputData[index] = '\0';
    } else {
      if (getStarted) {
        char *p = inputData;
        char *str;
        index = 0;
        String value = "";
        while ((str = strtok_r(p, " ", & p)) != NULL) {
          string_convert = str;
          intData[index] = string_convert.toInt();
          index++;
        }
        index = 0;
      }
    }
    if (incomingByte == ';') {        // если таки приняли ; - конец парсинга
      getStarted = false;
      recievedFlag = true;
    }
  }
}

void setup() {
  Serial.begin(9600);
}
void loop() {
  parsing();       // функция парсинга
  if (recievedFlag) {                           // если получены данные
    recievedFlag = false;
    for (byte i = 0; i < PARSE_AMOUNT; i++) { // выводим элементы массива
      Serial.print(intData[i]); Serial.print(" ");
    }
    Serial.println();
  }
}
/*
   Данный алгоритм позволяет получить через Serial пачку значений, и раскидать
   их в целочисленный массив. Использовать можно банально для управления
   ЧЕМ УГОДНО через bluetooth, так как bluetooth модули есть UART интерфейс связи.
   Либо управлять через Serial с какой-то программы с ПК
   Как использовать:
   1) В PARSE_AMOUNT указывается, какое количество значений мы хотим принять.
   От этого значения напрямую зависит размер массива принятых данных, всё просто
   2) Пакет данных на приём должен иметь вид:
   Начало - символ $
   Разделитель - пробел
   Завершающий символ - ;
   Пример пакета: $110 25 600 920;  будет раскидан в массив intData согласно порядку слева направо
   Что делает данный скетч:
   Принимает пакет данных указанного выше вида, раскидывает его в массив intData, затем выводит обратно в порт.
   Отличие от предыдущего примера: написан мной, не используя никаких хитрых функций. Предельно просто и понятно работает
*/
#define PARSE_AMOUNT 5         // число значений в массиве, который хотим получить
int intData[PARSE_AMOUNT];     // массив численных значений после парсинга
boolean recievedFlag;
boolean getStarted;
byte index;
String string_convert = "";
void parsing() {
  if (Serial.available() > 0) {
    char incomingByte = Serial.read();        // обязательно ЧИТАЕМ входящий символ
    if (getStarted) {                         // если приняли начальный символ (парсинг разрешён)
      if (incomingByte != ' ' && incomingByte != ';') {   // если это не пробел И не конец
        string_convert += incomingByte;       // складываем в строку
      } else {                                // если это пробел или ; конец пакета
        intData[index] = string_convert.toInt();  // преобразуем строку в int и кладём в массив
        string_convert = "";                  // очищаем строку
        index++;                              // переходим к парсингу следующего элемента массива
      }
    }
    if (incomingByte == '$') {                // если это $
      getStarted = true;                      // поднимаем флаг, что можно парсить
      index = 0;                              // сбрасываем индекс
      string_convert = "";                    // очищаем строку
    }
    if (incomingByte == ';') {                // если таки приняли ; - конец парсинга
      getStarted = false;                     // сброс
      recievedFlag = true;                    // флаг на принятие
    }
  }
}
void setup() {
  Serial.begin(9600);
}
void loop() {
  parsing();       // функция парсинга
  if (recievedFlag) {                           // если получены данные
    recievedFlag = false;
    for (byte i = 0; i < PARSE_AMOUNT; i++) { // выводим элементы массива
      Serial.print(intData[i]); Serial.print(" ");
    } Serial.println();
  }
}
/*
   Данный алгоритм позволяет получить через Serial пачку значений, и раскидать
   их в целочисленный массив. Использовать можно банально для управления
   ЧЕМ УГОДНО через bluetooth, так как bluetooth модули есть UART интерфейс связи.
   Либо управлять через Serial с какой-то программы с ПК.
   Парсинг не блокирующий, не содержит while и delay, не мешает работе основного скетча
   НО ОН ЧИТАЕТ ВЕСЬ ТРАФИК ИЗ ПОРТА!
   Как использовать:
   1) В PARSE_AMOUNT указывается, какое количество значений мы хотим принять.
   От этого значения напрямую зависит размер массива принятых данных, всё просто
   2) Пакет данных на приём должен иметь вид:
   Начало - символ $
   Разделитель - пробел
   Завершающий символ - ;
   Пример пакета: $110 25 600 920;  будет раскидан в массив intData согласно порядку слева направо
   Что делает данный скетч:
   Принимает пакет данных указанного выше вида, раскидывает его в массив intData, затем выводит обратно в порт.
   Отличие от предыдущего примера: алгоритм запрятан в GParsingStream.h, входит в пак GyverHacks
   Доступны и подсвечиваются функции parsingStream и dataReady
   Скачать можно здесь: https://github.com/AlexGyver/GyverLibs
*/

#define PARSE_AMOUNT 5         // число значений в массиве, который хотим получить
int intData[PARSE_AMOUNT];     // массив численных значений после парсинга

#include "GParsingStream.h"

void setup() {
  Serial.begin(9600);
}

void loop() {
  parsingStream((int*)&intData);               // функция парсинга, парсит в массив!

  if (dataReady()) {                           // если получены данные
    for (byte i = 0; i < PARSE_AMOUNT; i++) {  // выводим элементы массива
      Serial.print(intData[i]); Serial.print(" ");
    }
    Serial.println();
  }
}
/*
  Парсинг разных пакетов по ключевым "префиксам", построено на строках.
  Префикс состоит из двух символов (в этом примере m1, b2...)
  Таким образом пакет имеет вид: m1950 - переменной motor1 будет присвоено 950
  b21 - переменной button2 будет присвоено число 1
*/

char thisChar;
byte availableBytes;
String strData = "";
boolean recievedFlag;

int motor1, motor2;
int button1, button2;

void setup() {
  Serial.begin(9600);
}

void loop() {
  if (Serial.available() > 0) {                 // если есть что то на вход
    strData = "";                               // очистить строку
    while (Serial.available() > 0) {            // пока идут данные
      strData += (char)Serial.read();           // забиваем строку принятыми данными
      delay(2);                                 // обязательно задержка, иначе вылетим из цикла раньше времени
    }
    recievedFlag = true;                        // поднять флаг что получили данные
  }

  if (recievedFlag) {                           // если есть принятые данные
    int intVal = strData.substring(2).toInt();  // перевести в int всю строку кроме первых двух символов!
    String header = strData.substring(0, 2);    // создать мини строку, содержащую первые два символа

    if (strData.startsWith("m1"))               // если строка начинается с m1
      motor1 = intVal;
    if (strData.startsWith("m2"))               // если строка начинается с m2
      motor2 = intVal;
    if (strData.startsWith("b1"))               // если строка начинается с b1
      button1 = intVal;
    if (strData.startsWith("b2"))               // если строка начинается с b2
      button2 = intVal;

    recievedFlag = false;                       // данные приняты, мой капитан!

    // выводим в порт для отладки
    Serial.print(motor1); Serial.print(" ");
    Serial.print(motor2); Serial.print(" ");
    Serial.print(button1); Serial.print(" ");
    Serial.print(button2); Serial.println();
  }
}
/*
  Парсинг разных пакетов по ключевым "префиксам", чуть более оптимальный вариант без использования строк
  Префикс состоит из одного буквенного символа (в этом примере a, b, c...)
  Таким образом пакет имеет вид: a950 - переменной motor1 будет присвоено 950
  d1 - переменной button2 будет присвоено число 1
*/

int motor1, motor2;
int button1, button2;

void setup() {
  Serial.begin(9600);
}

void loop() {
  if (Serial.available() > 0) {                 // если есть что то на вход
    char header = Serial.read();                // мы ждём префикс
    if (isAlpha(header)) {                      // если префикс БУКВА
      int intValue = 0;                         // обнуляем
      delay(2);
      while (Serial.available() > 0) {          // пока идут данные
        char thisChar = Serial.read();          // читаем
        if (! isDigit(thisChar)) break;         // если приходит НЕ ЦИФРА, покидаем парсинг
        intValue = intValue * 10 + (thisChar - '0');  // с каждым принятым число растёт слева направо
        delay(2);                                 // обязательно задержка, иначе вылетим из цикла раньше времени
      }

      switch (header) {             // раскидываем по переменным
        case 'a':                   // если заголовок а
          motor1 = intValue;
          break;
        case 'b':                   // если заголовок b, и так далее
          motor2 = intValue;
          break;
        case 'c':
          button1 = intValue;
          break;
        case 'd':
          button2 = intValue;
          break;
      }

      // выводим в порт для отладки
      Serial.print(motor1); Serial.print(" ");
      Serial.print(motor2); Serial.print(" ");
      Serial.print(button1); Serial.print(" ");
      Serial.print(button2); Serial.println();
    }
  }
}
/*
  Парсинг разных пакетов по ключевым "префиксам", чуть более оптимальный вариант без использования строк
  Режим работы полностью "прозрачный" (not blocking), также сделан таймаут
  Префикс состоит из одного буквенного символа (в этом примере a, b, c...)
  Таким образом пакет имеет вид: a950 - переменной motor1 будет присвоено 950
  d1 - переменной button2 будет присвоено число 1
*/
#define TIMEOUT 100     // таймаут в миллисекундах на отработку неправильно посланных данных

int motor1, motor2;
int button1, button2;

int intValue;
char header;
boolean recievedFlag, startParse;
unsigned long parseTime;

void parsing() {
  if (Serial.available() > 0) {                 // если есть что то на вход
    char thisChar = Serial.read();              // принимаем байт
    if (startParse) {                           // если парсим
      if (! isDigit(thisChar)) {                // если приходит НЕ ЦИФРА
        switch (header) {                       // раскидываем по переменным
          case 'a':                             // если заголовок а
            motor1 = intValue;
            break;
          case 'b':                             // если заголовок b, и так далее
            motor2 = intValue;
            break;
          case 'c':
            button1 = intValue;
            break;
          case 'd':
            button2 = intValue;
            break;
        }
        recievedFlag = true;                  // данные приняты
        startParse = false;                   // парсинг завершён
      } else {                                // если принятый байт всё таки цифра
        intValue = intValue * 10 + (thisChar - '0');  // с каждым принятым число растёт слева направо
      }
    }
    if (isAlpha(thisChar) && !startParse) {     // если префикс БУКВА и парсинг не идёт
      header = thisChar;                        // запоминаем префикс
      intValue = 0;                             // обнуляем
      startParse = true;                        // флаг на парсинг
      parseTime = millis();                     // запоминаем таймер
    }
  }
  if (startParse && (millis() - parseTime > TIMEOUT)) {
    startParse = false;                   // парсинг завершён по причине таймаута
  }
}

void setup() {
  Serial.begin(9600);
}
void loop() {
  parsing();

  if (recievedFlag) {
    // выводим в порт для отладки
    Serial.print(motor1); Serial.print(" ");
    Serial.print(motor2); Serial.print(" ");
    Serial.print(button1); Serial.print(" ");
    Serial.print(button2); Serial.println();
    recievedFlag = false;
  }

}

ФИЛЬТРЫ ЗНАЧЕНИЙ

/*
  Простейший фильтр: запаздывающий, бегущее среднее, "цифровой фильтр", фильтр низких частот - это всё про него любимого
  Имеет две настройки: постоянную времени FILTER_STEP (миллисекунды), и коэффициент "плавности" FILTER_COEF
  Данный фильтр абсолютно универсален, подходит для сглаживания любого потока данных
  При маленьком значении FILTER_COEF фильтрованное значение будет меняться очень медленно вслед за реальным
  Чем больше FILTER_STEP, тем меньше частота опроса фильтра
  Сгладит любую "гребёнку", шум, ложные срабатывания, резкие вспышки и прочее говно. Пользуюсь им постоянно
*/

#define FILTER_STEP 5
#define FILTER_COEF 0.05

int val;
float val_f;
unsigned long filter_timer;

void setup() {
  Serial.begin(9600);  
}

void loop() {
  
  if (millis() - filter_timer > FILTER_STEP) {
    filter_timer = millis();    // просто таймер

    // читаем значение (не обязательно с аналога, это может быть ЛЮБОЙ датчик)
    val = analogRead(0);

    // основной алгоритм фильтрации. Внимательно прокрутите его в голове, чтобы понять, как он работает
    val_f = val * FILTER_COEF + val_f * (1 - FILTER_COEF);

    // для примера выведем в порт
    Serial.println(val_f);
  }

}
/*
   "Удобный" фильтр бегущее среднее (низких частот)
   Библиотеку GyverHacks можно скачать здесь
   https://github.com/AlexGyver/GyverLibs
*/

#include "GyverHacks.h"
GFilterRA analog0;    // фильтр назовём analog0

void setup() {
  Serial.begin(9600);

  // установка коэффициента фильтрации (0.0... 1.0). Чем меньше, тем плавнее фильтр
  analog0.setCoef(0.01);

  // установка шага фильтрации (мс). Чем меньше, тем резче фильтр
  analog0.setStep(10);
}

void loop() {
  Serial.println(analog0.filteredTime(analogRead(0)));
}
/*
  Элементарная реализация среднего арифметического. Сложили NUM_READINGS измерений,
  затем разделили сумму на NUM_READINGS и всё!
  Является "частным случаем" предыдущего фильтра
*/

#define NUM_READINGS 500
int average;

void setup() {
  Serial.begin(9600);
}

void loop() {
  long sum = 0;                                  // локальная переменная sum
  for (byte i = 0; i < NUM_READINGS; i++) {      // согласно количеству усреднений
    sum += analogRead(0);                        // суммируем значения с любого датчика в переменную sum
  }
  average /= NUM_READINGS;                       // находим среднее арифметическое, разделив сумму на число измерений
  Serial.println(average);                       // для примера выводим в порт
}
/*
  Медианный фильтр — довольно простая и интересная штука. Берёт значения и выбирает из них среднее.
  Не усредняет, а именно ВЫБИРАЕТ, отбрасывает все сильно отличющиеся.
  Простой пример, чем отличается медианный фильтр от среднего арифметического:
  Возьмём числа 3, 4, 50. Среднее арифметическое даст нам 19. Целью медианного фильтра является фильтрация
  резких скачков, и после фильтрации он даст нам 4, как среднее между 3 и 50, а 50 будет отброшено как скачок.
  В данном скетче реализована фильтрация по трём значениям. Если интересен вариант с фильтрацией более трёх значений,
  то добро пожаловать в исходную статью. Осторожно, жесть. http://tqfp.org/programming/mediannyy-filtr-na-sluzhbe-razrabotchika.html
*/
int val[3];
int val_filter;
byte index;

void setup() {
  Serial.begin(9600);
}

void loop() {
  if (++index > 2) index = 0; // переключаем индекс с 0 до 2 (0, 1, 2, 0, 1, 2…)
  val[index] = analogRead(0); // записываем значение с датчика в массив

  // фильтровать медианным фильтром из 3ёх ПОСЛЕДНИХ измерений
  val_filter = middle_of_3(val[0], val[1], val[2]);

  Serial.println(val_filter); // для примера выводим в порт
}

// медианный фильтр из 3ёх значений
float middle_of_3(float a, float b, float c) {
  int middle;
  if ((a <= b) && (a <= c)) {
    middle = (b <= c) ? b : c;
  }
  else {
    if ((b <= a) && (b <= c)) {
      middle = (a <= c) ? a : c;
    }
    else {
      middle = (a <= b) ? a : b;
    }
  }
  return middle;
}

СВЕТОДИОДЫ

Друзья, тут проблемка с отображением, вставляйте код в Arduino IDE и нажимайте Ctrl+T — автоформатирование!

/*
  Данный код позволяет получить 1023 оттенка цвета с RGB светодиода одним потенциомтером
  алгоритм цвета 1:
  синий максимум, плавно прибавляется зелёный
  зелёный максимум, плавно убавляется синий
  зелёный максимум, плавно прибавляется красный
  красный максимум, плавно убавляется зелёный
*/
// пины подключения. Обратите внимание, это ШИМ пины
#define R_PIN 3
#define G_PIN 5
#define B_PIN 6
byte bright = 100; // яркость, от 0 до 100 (можно повесить на второй потенциометр при желании)
byte R, G, B;
void setup() {
  // настраиваем как выходы
  pinMode(R_PIN, OUTPUT);
  pinMode(G_PIN, OUTPUT);
  pinMode(B_PIN, OUTPUT);
}
void loop() {
  int colorPot = analogRead(0); // получаем значение с потенциометра (0 - 1023)
  // разбиваем диапазон 0 - 1023 на 4 участка, и играемся с цветом согласно текущему значению
  if (colorPot <= 250) { byte k = map(colorPot, 0, 250, 0, 255); R = 0; G = k; B = 255; } else if (colorPot > 250 && colorPot <= 500) { byte k = map(colorPot, 250, 500, 0, 255); R = 0; G = 255; B = 255 - k; } else if (colorPot > 500 && colorPot <= 750) { byte k = map(colorPot, 500, 750, 0, 255); R = k; G = 255; B = 0; } else if (colorPot > 750 && colorPot <= 1023) {
    byte k = map(colorPot, 750, 1023, 0, 255);
    R = 255;
    G = 255 - k;
    B = 0;
  }
  // подаём ШИМ на светодиод, учитывая яркость
  analogWrite(R_PIN, (bright * R / 100));
  analogWrite(G_PIN, (bright * G / 100));
  analogWrite(B_PIN, (bright * B / 100));
}
/*
 Данный код позволяет получить 1023 оттенка цвета с RGB светодиода одним потенциомтером
 алгоритм цвета 2
 синий убавляется, зелёный прибавляется
 зелёный убавляется, красный прибавляется
*/
// пины подключения. Обратите внимание, это ШИМ пины
#define R_PIN 3
#define G_PIN 5
#define B_PIN 6
byte bright = 100; // яркость, от 0 до 100 (можно повесить на второй потенциометр при желании)
byte R, G, B;
void setup() {
 // настраиваем как выходы
 pinMode(R_PIN, OUTPUT);
 pinMode(G_PIN, OUTPUT);
 pinMode(B_PIN, OUTPUT);
}
void loop() {
 int colorPot = analogRead(0); // получаем значение с потенциометра (0 - 1023)
 // разбиваем диапазон 0 - 1023 на 2 участка, и играемся с цветом согласно текущему значению
 if (colorPot <= 500) { byte k = map(colorPot, 0, 500, 0, 255); R = 0; G = k; B = 255 - k; } else if (colorPot > 500) {
 byte k = map(colorPot, 500, 1000, 0, 255);
 R = k;
 G = 255 - k;
 B = 0;
 }
 // подаём ШИМ на светодиод, учитывая яркость
 analogWrite(R_PIN, (bright * R / 100));
 analogWrite(G_PIN, (bright * G / 100));
 analogWrite(B_PIN, (bright * B / 100));
}
/*
 Данный код позволяет получить 1023 оттенка цвета с RGB светодиода одним потенциомтером
 алгоритм цвета 3 - радужный
 красный в зелёный через жёлтый
 зелёный в синий через бирюзовый
 синий в краный через фиолетовый
*/
// пины подключения. Обратите внимание, это ШИМ пины
#define R_PIN 3
#define G_PIN 5
#define B_PIN 6
byte bright = 100; // яркость, от 0 до 100 (можно повесить на второй потенциометр при желании)
byte R, G, B;
void setup() {
 // настраиваем как выходы
 pinMode(R_PIN, OUTPUT);
 pinMode(G_PIN, OUTPUT);
 pinMode(B_PIN, OUTPUT);
}
void loop() {
 int colorPot = analogRead(0); // получаем значение с потенциометра (0 - 1023)
 // разбиваем диапазон 0 - 1023 на 2 участка, и играемся с цветом согласно текущему значению
 if (colorPot <= 340) { byte k = map(colorPot, 0, 340, 0, 255); R = 255 - k; G = k; B = 0; } else if (colorPot > 340 && colorPot <= 680) { byte k = map(colorPot, 340, 680, 0, 255); R = 0; G = 255 - k; B = k; } else if (colorPot > 680) {
 byte k = map(colorPot, 680, 1023, 0, 255);
 R = k;
 G = 0;
 B = 255 - k;
 }
 // подаём ШИМ на светодиод, учитывая яркость
 analogWrite(R_PIN, (bright * R / 100));
 analogWrite(G_PIN, (bright * G / 100));
 analogWrite(B_PIN, (bright * B / 100));
}

КНОПКИ

Все возможности кнопки реализованы в написанной мной библиотеке GyverButton, найти можно в паке моих библиотек на GitHub вот по этой ссылке. Там находится описание, здесь приведу пример использования

/*
   Пример использования библиотеки GyverButton, все возможности в одном скетче.
   - Опрос кнопки с программным антидребезгом контактов
   - Отработка нажатия, удерживания отпускания кнопки
   - Отработка одиночного, двойного и тройного нажатия (вынесено отдельно)
   - Отработка любого количества нажатий кнопки (функция возвращает число нажатий)
   - Отработка нажатия и удержания кнопки
   - Настраиваемый таймаут повторного нажатия/удержания
   - Функция изменения значения переменной с заданным шагом и заданным интервалом по времени
*/

#define PIN 3				// кнопка подключена сюда (PIN --- КНОПКА --- GND)

#include "GyverButton.h"
GButton butt1(PIN);
int value = 0;

void setup() {
  Serial.begin(9600);

  butt1.setDebounce(50);        // настройка антидребезга (по умолчанию 80 мс)
  butt1.setTimeout(300);        // настройка таймаута на удержание (по умолчанию 500 мс)
  butt1.setIncrStep(2);         // настройка инкремента, может быть отрицательным (по умолчанию 1)
  butt1.setIncrTimeout(500);    // настрйока интервала инкремента (по умолчанию 800 мс)
}

void loop() {
  butt1.tick();  // обязательная функция отработки. Должна постоянно опрашиваться

  if (butt1.isSingle()) Serial.println("Single");       // проверка на один клик
  if (butt1.isDouble()) Serial.println("Double");       // проверка на двойной клик
  if (butt1.isTriple()) Serial.println("Triple");       // проверка на тройной клик

  if (butt1.hasClicks())                                // проверка на наличие нажатий
    Serial.println(butt1.getClicks());                  // получить (и вывести) число нажатий

  if (butt1.isPress()) Serial.println("Press");         // нажатие на кнопку (+ дебаунс)
  if (butt1.isRelease()) Serial.println("Release");     // отпускание кнопки (+ дебаунс)
  if (butt1.isHolded()) Serial.println("Holded");       // проверка на удержание
  //if (butt1.isHold()) Serial.println("Hold");         // возвращает состояние кнопки

  if (butt1.isIncr()) {                                 // если кнопка была удержана (это для инкремента)
    value = butt1.getIncr(value);                       // увеличивать/уменьшать переменную value с шагом и интервалом
    Serial.println(value);      // для примера выведем в порт
  }
}
#define BTN 3   // кнопка подключена сюда (PIN --- КНОПКА --- GND)
boolean btnState, btnFlag;

void setup() {
  Serial.begin(9600);
  pinMode(BTN, INPUT_PULLUP);
}

void loop() {
  btnState = !digitalRead(BTN);  // читаем состояние кнопки с инверсией. 1 - нажата, 0 - нет
  
  if (btnState && !btnFlag) {    // если нажата и была отпущена (btnFlag 0)
    btnFlag = true;              // запомнили что нажата
    Serial.println("press");
  }
  if (!btnState && btnFlag) {    // если отпущена и была нажата (btnFlag 1)
    btnFlag = false;             // запомнили что отпущена
    Serial.println("release");
  }
}
#define BTN 3         // кнопка подключена сюда (PIN --- КНОПКА --- GND)
#define DEBOUNCE 100  // таймаут антидребезга, миллисекунды

boolean btnState, btnFlag;
unsigned long debounceTimer;

void setup() {
  Serial.begin(9600);
  pinMode(BTN, INPUT_PULLUP);
}

void loop() {
  btnState = !digitalRead(BTN);  // читаем состояние кнопки с инверсией. 1 - нажата, 0 - нет

  // если нажата и была отпущена (btnFlag 0) и прошло не менее DEBOUNCE времени
  if (btnState && !btnFlag && (millis() - debounceTimer > DEBOUNCE)) {
    btnFlag = true;              // запомнили что нажата
    debounceTimer = millis();    // запомнили время нажатия
    Serial.println("press");
  }
  if (!btnState && btnFlag) {    // если отпущена и была нажата (btnFlag 1)
    btnFlag = false;             // запомнили что отпущена
    debounceTimer = millis();    // запомнили время отпускания
    Serial.println("release");
  }
}

АНАЛОГОВЫЕ ПИНЫ, НАПРЯЖЕНИЕ

/*
   Скетч для калибровки точного вольтметра и его использование
   КАЛИБРОВКА:
   0) Ставим vol_calibration 1, прошиваем
   1) Запускаем, открываем монитор. Будет выведено реальное значение Vсс в милливольтах
   из расчёта по стандартной константе 1.1
   2) Измеряем вольтметром напряжение на пинах 5V и GND, полученное значение отправляем в порт
   В МИЛЛИВОЛЬТАХ (то есть если у нас 4.54В то отправляем 4540). Новая константа будет рассчитана
   автоматически и запишется во внутреннюю память
   3) Ставим vol_calibration 0, прошиваем
   4) Наслаждайтесь точными измерениями!
   ИСПОЛЬЗОВАНИЕ:
   0) Функция readVCC возвращает ТОЧНОЕ опорное напряжение В МИЛЛИВОЛЬТАХ. В расчётах используем
   не 5 Вольт, а readVсс!
   1) При использовании analogRead() для перевода в вольты пишем:
   float voltage = analogRead(pin) * (readVсс() / 1023.0); - это точный вольтаж В МИЛЛИВОЛЬТАХ
*/

#define vol_calibration 1    // калибровка вольтметра (если работа от АКБ) 1 - включить, 0 - выключить
float my_vcc_const = 1.1;    // начальное значение константы должно быть 1.1

#include "EEPROMex.h"        // библиотека для работы со внутренней памятью ардуино

void setup() {
  Serial.begin(9600);
  if (vol_calibration) calibration();     // калибровка, если разрешена
  my_vcc_const = EEPROM.readFloat(1000);  // считать константу из памяти
}

void loop() {
  /*
    // отображение заряда в процентах по ёмкости! Интерполировано
 вручную по графику разряда ЛИТИЕВОГО аккумулятора
    int volts = readVcc();
    int capacity;
    if (volts > 3870)
    capacity = map(volts, 4200, 3870, 100, 77);
    else if ((volts <= 3870) && (volts > 3750) )
    capacity = map(volts, 3870, 3750, 77, 54);
    else if ((volts <= 3750) && (volts > 3680) )
    capacity = map(volts, 3750, 3680, 54, 31);
    else if ((volts <= 3680) && (volts > 3400) )
    capacity = map(volts, 3680, 3400, 31, 8);
    else if (volts <= 3400)
    capacity = map(volts, 3400, 2600, 8, 0);

    Serial.println(capacity);
  */

}

void calibration() {
  my_vcc_const = 1.1;                                           // начальаня константа калибровки
  Serial.print("Real VCC is: "); Serial.println(readVcc());     // общаемся с пользователем
  Serial.println("Write your VCC (in millivolts)");
  while (Serial.available() == 0); int Vcc = Serial.parseInt(); // напряжение от пользователя
  float real_const = (float)1.1 * Vcc / readVcc();              // расчёт константы
  Serial.print("New voltage constant: "); Serial.println(real_const, 3);
  Serial.println("Set vol_calibration 0, flash and enjoy!");
  EEPROM.writeFloat(1000, real_const);                          // запись в EEPROM
  while (1);                                                    // уйти в бесконечный цикл
}

// функция чтения внутреннего опорного напряжения, универсальная (для всех ардуин)
long readVcc() {
#if defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
  ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
  ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
  ADMUX = _BV(MUX3) | _BV(MUX2);
#else
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#endif
  delay(2); // Wait for Vref to settle
  ADCSRA |= _BV(ADSC); // Start conversion
  while (bit_is_set(ADCSRA, ADSC)); // measuring
  uint8_t low  = ADCL; // must read ADCL first - it then locks ADCH
  uint8_t high = ADCH; // unlocks both
  long result = (high << 8) | low;

  result = my_vcc_const * 1023 * 1000 / result; // расчёт реального VCC
  return result; // возвращает VCC
}
// отображение заряда в процентах по ёмкости! Интерполировано
// вручную по графику разряда ЛИТИЕВОГО аккумулятора
int volts = analogRead(0) * 5 * (float)0.977;    // несовсем корректно, так как 5 вольт ровно не бывает. Смотри предыдущий пример
int capacity;
if (volts > 3870)
capacity = map(volts, 4200, 3870, 100, 77);
else if ((volts <= 3870) && (volts > 3750) )
capacity = map(volts, 3870, 3750, 77, 54);
else if ((volts <= 3750) && (volts > 3680) )
capacity = map(volts, 3750, 3680, 54, 31);
else if ((volts <= 3680) && (volts > 3400) )
capacity = map(volts, 3680, 3400, 31, 8);
else if (volts <= 3400)
capacity = map(volts, 3400, 2600, 8, 0);

Serial.println(capacity);

НЕСКОЛЬКО ПОЛЕЗНЫХ СТАТЕЙ


2018-04-25T21:11:49+00:00