ПОЛЕЗНЫЕ АЛГОРИТМЫ ДЛЯ 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.

Arduino Pro Mini бывает двух типов: с кварцем на 16 МГц и 8 МГц. Китайцы обычно не подписывают плату, и есть риск перепутать разные платы, если у вас есть и те и те. На средних по цене Pro Mini стоит качественный полноразмерный кварц в овальном металлическом корпусе, на нём крупно написана цифра, обозначающая частоту в Мгц:

На недорогих платах стоит крошечный дешёвый кварц в SMD корпусе, вот он:

Берём лупу и смотрим: 16 МГц кварц маркируется примерно как “A1” or “A’N”, 8 МГц кварц маркируется “80’0” или что-то в этом стиле. Ну вот, теперь вы не перепутаете свои Pro Mini!

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

/*
  пример "чистого" и удобного для работы цикла 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) 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) 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 MODE_AMOUNT 5
byte mode = 0;

void nextMode() {
  mode++;  // увеличиваем переменную номера режима
  if (mode >= MODE_AMOUNT) mode = 0;  // закольцовываем
}
// Время выполнения 0.5 мкс

Есть ещё парочка интересных вариантов. Результат не отличается, но сам механизм знать будет полезно:

// второй вариант. Время выполнения 0.5 мкс
if (++mode >= MODE_AMOUNT) mode = 0;  // тут инкремент внесён в условие, получаем более короткую запись

// третий вариант. Время выполнения 5.5 мкс. НЕ ИСПОЛЬЗУЙТЕ ЕГО!
mode = ++mode % MODE_AMOUNT;  // очень интересный вариант, без использования условия! Работает остаток от деления

Очень часто в примерах используется delay(), и это несомненно очень плохо: построить на основе такого примера серьёзный проект практически невозможно. Одним из частых и критичных моментов являются циклы с delay(), это может касаться каких то эффектов со светодиодами, адресными светодиодными лентами, и прочими действиями со счётчиком:

for (int i = 0; i < 30; i++) {
  // например, зажигаем i-ый светодиод
  delay(100);
}

Как переписать такой цикл, чтобы он не блокировал выполнение кода? Очень просто: нужно избавиться и от цикла, и от delay. Введём таймер на millis(), и будем работать по нему:

int counter = 0;      // замена i
uint32_t timer = 0;   // переменная таймера
#define T_PERIOD 100  // период переключения

void loop() {
  
  if (millis() - timer >= T_PERIOD) { // таймер на millis()
    timer = millis(); // сброс
    // действие с counter - наш i-ый светодиод например
    counter++;  // прибавляем счётчик
    if (counter > 30) counter = 0;  // закольцовываем изменение
  }
  
}

Вот собственно и всё! Вместо переменной цикла i у нас теперь свой глобальный счётчик counter, который бегает от 0 до 30 (в этом примере) с периодом 100 мс.

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

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

// Нам нужно задать период таймера В МИЛЛИСЕКУНДАХ
// дней*(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 миллисекунд
  }
}
/*
   Пример параллельного выполнения нескольких задач
   по таймеру. Библиотеку GyverTimer можно скачать здесь
   https://github.com/AlexGyver/GyverLibs
*/

#include "GyverTimer.h"

// создать таймер, в скобках период в миллисекундах
GTimer_ms myTimer1(500);
GTimer_ms myTimer2(600);
GTimer_ms 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!");
}
uint32_t now = millis();
while (millis () - now < 5000) {
// тут в течение 5000 миллисекунд вертится код
// удобно использовать для всяких калибровок
}

В первом пункте мы разобрали “классический” таймер на millis(), давайте посмотрим ещё один, иногда встречающийся в скетчах:

#define PERIOD_1 2000

void loop() {
  if ( (millis() % PERIOD_1) == 0) {
    delay(1);
    // ваше действие
  }
}

Чем он хорош и чем плох? Хорош тем, что не нужна отдельная переменная типа uint32_t, а также данный таймер не сбивается и без проблем проходит через переполнение millis(). Минусы весьма существенные: операция % выполняется очень долго, также внутри таймера нужен delay(1), иначе таймер может сработать несколько раз в течение одной миллисекунды (пока миллис кратен периоду). Не используйте этот таймер, но знайте, что такой есть.

Недавно я задался вопросом: а можно ли сделать таймер на миллис, который будет корректно обходить переполнение millis() и не сбивать период? Можно, сделал:

#define PERIOD 500
uint32_t timer = 0;
void loop() {
  if (millis() - timer >= PERIOD) {
    // ваше действие
    do {
      timer += PERIOD;
      if (timer < PERIOD) break;  // переполнение uint32_t
    } while (timer < millis() - PERIOD); // защита от пропуска шага
  }
}

Данный таймер имеет механику классического таймера с хранением переменной таймера, а его период всегда кратен PERIOD и не сбивается. Эту конструкцию можно упростить до

#define PERIOD 500
uint32_t timer = 0;
void loop() {
  if (millis() - timer >= PERIOD) {
    // ваше действие
    timer += PERIOD;
  }
}

В этом случае алгоритм получается короче, кратность периодов сохраняется, но теряется защита от пропуска вызова и переполнения millis(). Мои библиотеки GyverTimer и timerMinim были обновлены до этого алгоритма, можете работать с ними.

РАБОТА С 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;                  // опустить флаг
  }
}
// приём двух float чисел через сериал
// десятичный разделитель - . (точка)
// разделитель - ; (семиколон)
// пример посылки: 5.326;-3.589

void setup() {
  Serial.begin(9600);
  Serial.setTimeout(50);    // таймаут шоб не ждать (по умолч. секунда)
}

void loop() {
  if (Serial.available() > 0) {
    String bufString = Serial.readString();       // читаем как строку
    byte dividerIndex = bufString.indexOf(';');   // ищем индекс разделителя

    String buf_1 = bufString.substring(0, dividerIndex);    // создаём строку с первым числом
    String buf_2 = bufString.substring(dividerIndex + 1);   // создаём строку со вторым числом

    float val_1 = buf_1.toFloat();    // преобразуем во флоат
    float val_2 = buf_2.toFloat();    // ...

    Serial.println(val_1);  // проверка
    Serial.println(val_2);  // ...

    Serial.println(val_1, 5);  // вывод с 5 знаками после запятой
    Serial.println(val_2, 7);  // вывод с 7 знаками
  }
}
/*
Данный алгоритм позволяет получить через 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;
}
}
/*
Пример текстового интерфейса через монитор порта. Парсинг осуществляется
через условия и встроенные методы работы со строками.
Принятие в порт блокирующее!!! readString()
Пример отправки: keyword 5 - ключевое слово keyword, значение 5
Между ключевым словом и значением стоит ПРОБЕЛ
Символа окончания не нужно, используется readString() со своим таймаутом
*/
// строки текстовых команд
String help_s = "help";
String sayHello_s = "say hello";
String getValue_s = "get value";
String setValue_s = "set value";
// переменная для теста
int value = 0;
void setup() {
Serial.begin(9600);
Serial.setTimeout(100);   // установка таймаута для readString (мс) (по умолчанию слишком длинный)
help();                   // вывод текстового меню
}
void loop() {
serialTick();     // обработка команд из порта
}
void help() {
// макрос F сохраняет текст во флеш память
Serial.println(F("***********************************************************************"));
Serial.println(F("You can use serial commands:"));
Serial.println(F("         - say hello"));
Serial.println(F("         - get value of value"));
Serial.println(F("   - set value to VALUE (example: set value 500)"));
Serial.println(F("              - print this instruction again"));
Serial.println(F("***********************************************************************"));
}
// опросчик и парсер сериал
void serialTick() {
if (Serial.available() > 0) {           // проверка данных на вход
String buf = Serial.readString();     // читаем как string
// механизм такой: командой startsWith сравниваем начало строки
// если совпадает - делаем какие то действия
// для приёма значений используется такой механизм:
// строка обрезается командой substring до длины команды .substring(команда.length())
// оставшееся число преобразуется в число командой .toInt()
if (buf.startsWith(help_s)) {
help();
}
else if (buf.startsWith(sayHello_s)) {
Serial.println("Hello!");
}
else if (buf.startsWith(getValue_s)) {
Serial.print("Value is ");
Serial.println(value);
}
else if (buf.startsWith(setValue_s)) {
value = buf.substring(setValue_s.length()).toInt();
Serial.print("Value set to ");
Serial.println(value);
}
}
}
/*
Пример текстового интерфейса через монитор порта. Парсинг осуществляется
через switch и встроенные методы работы со строками.
Принятие в порт блокируещее!!! Читаем методом readString()
Пример отправки: keyword 5 - ключевое слово keyword, значение 5
Между ключевым словом и значением стоит ПРОБЕЛ
Символа окончания не нужно, используется readString() со своим таймаутом
*/
// строки текстовых команд
const char *headers[]  = {
"mtr1",   // 0
"mtr2",   // 1
"mtr3",   // 2
"mtr4",   // 3
"set1",   // 4
"set2",   // 5
};
// соответствующие им названия для commandsTick
enum names {
MOTOR1,   // 0
MOTOR2,   // 1
MOTOR3,   // 2
MOTOR4,   // 3
SETT1,    // 4
SETT2,    // 5
};
int prsValue = 0;
boolean recievedFlag;
names thisName;
byte comm_amount = sizeof(headers) / 2;
void setup() {
Serial.begin(9600);
Serial.setTimeout(100);   // установка таймаута для readString (мс) (по умолчанию слишком длинный)
}
void loop() {
serialTick();     // обработка команд из порта
commandsTick();   // отработка полученных команд
}
void commandsTick() {
if (recievedFlag) {
recievedFlag = false;
switch (thisName) {
case MOTOR1:
Serial.print("motor1 ");
Serial.println(prsValue);
break;
case MOTOR2:
Serial.print("motor2 ");
Serial.println(prsValue);
break;
case MOTOR3:
Serial.print("motor3 ");
Serial.println(prsValue);
break;
case MOTOR4:
Serial.print("motor4 ");
Serial.println(prsValue);
break;
case SETT1:
Serial.print("sett1 ");
Serial.println(prsValue);
break;
case SETT2:
Serial.print("sett2 ");
Serial.println(prsValue);
break;
}
}
}
// опросчик и парсер сериал
void serialTick() {
if (Serial.available() > 0) {                 // проверка данных на вход
String buf = Serial.readString();           // читаем как string
for (byte i = 0; i < comm_amount; i++) {    // пробегаемся по всем именам
if (buf.startsWith(headers[i])) {         // если совпадаем по названию
String thisHeader = headers[i];         // костыль
prsValue = buf.substring(thisHeader.length()).toInt();  // перевод в int
recievedFlag = true;                    // флаг
thisName = i;                           // запоминаем номер команды
}
}
}
}
/*
Пример текстового интерфейса через монитор порта. Парсинг осуществляется
через switch и встроенные методы работы со строками.
Принятие в порт НЕ блокируещее!!! Читаем посимвольно
Пример отправки: keyword 5; - ключевое слово keyword, значение 5
Между ключевым словом и значением стоит divider (можно настроить)
Посылка завершается символом ending (можно настроить)
Парсинг работает со включенным и выключенным концом строки, т.е. универсально
*/
char divider = ' ';
char ending = ';';
const char *headers[]  = {
"asd1",   // 0
"asd2",   // 1
"asd3",   // 2
"asd4",   // 3
"qw1",    // 4
"qw2",    // 5
};
enum names {
MOTOR1,   // 0
MOTOR2,   // 1
MOTOR3,   // 2
MOTOR4,   // 3
SETT1,    // 4
SETT2,    // 5
};
names thisName;
byte headers_am = sizeof(headers) / 2;
uint32_t prsTimer;
String prsValue = "";
String prsHeader = "";
enum stages {WAIT, HEADER, GOT_HEADER, VALUE, SUCCESS};
stages parseStage = WAIT;
boolean recievedFlag;
void setup() {
Serial.begin(9600);
}
void loop() {
parsingSeparate();
if (recievedFlag) {
recievedFlag = false;
switch (thisName) {
case MOTOR1: Serial.print("motor1 "); Serial.println(prsValue);
break;
case MOTOR2: Serial.print("motor2 "); Serial.println(prsValue);
break;
case MOTOR3: Serial.print("motor3 "); Serial.println(prsValue);
break;
case MOTOR4: Serial.print("motor4 "); Serial.println(prsValue);
break;
case SETT1: Serial.print("sett1 "); Serial.println(prsValue);
break;
case SETT2: Serial.print("sett2 "); Serial.println(prsValue);
break;
}
}
}
void parsingSeparate() {
if (Serial.available() > 0) {
if (parseStage == WAIT) {
parseStage = HEADER;
prsHeader = "";
prsValue = "";
}
if (parseStage == GOT_HEADER)
parseStage = VALUE;
char incoming = (char)Serial.read();
if (incoming == divider) {
parseStage = GOT_HEADER;
}
else if (incoming == ending) {
parseStage = SUCCESS;
}
if (parseStage == HEADER)
prsHeader += incoming;
else if (parseStage == VALUE)
prsValue += incoming;
prsTimer = millis();
}
if (parseStage == SUCCESS) {
for (byte i = 0; i < headers_am; i++) { // >
if (prsHeader == headers[i]) {
thisName = i;
}
} recievedFlag = true; parseStage = WAIT;
} if ((millis() - prsTimer > 10) && (parseStage != WAIT)) {    // таймаут
parseStage = WAIT;
}
}

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

/*
Простейший фильтр: запаздывающий, бегущее среднее, "цифровой фильтр", фильтр низких частот - это всё про него любимого
Имеет две настройки: постоянную времени 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)));
}

Классический вариант бегущего среднего выглядит так:

// filtered_val - фильтрованное значение
// val - новое значение (с датчика)
// k - коэффициент фильтрации 0.. 1. Обычно около 0.01-0.1 (то бишь float)
filtered_val = filtered_val * (1 - k) + val * k;

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

filtered_val += (val - filtered_val) * k;
У фильтра “бегущее среднее” не один настраиваемый параметр, как может показаться на первый взгляд. Помимо коэффициента фильтрации k очень большую роль играет время итерации, то есть период вызова фильтра. В реальном коде фильтр вызывается с определённым промежутком времени, чтобы фильтровать шумы. Я обычно настраиваю фильтр вручную по графику, который строится средствами Arduino IDE или программой Serial Port Plotter. Задаюсь периодом итерации и подгоняю k, пока не станет “хорошо”. Но есть и аналитический способ расчёта коэффициента фильтрации (или времени итерации).
При выборе значения коэффициента k необходимо отталкиваться от того, какие изменения сигнала нам интересны, а какие мы будем считать за шум. Сделать это можно с помощью следующего выражения:
t = dt * (1 / k – 1)
где t — период времени, который отделяет слишком быстрые изменения от требуемых; dt — время итерации (период вызова фильтра).
Например, если в нашем случае с потенциометром, k = 0,1, а время между двумя измерениями dt = 20 мс, то время t = (1-0.1) * 0,02 / 0.1 = 0,18 сек. То есть все изменения сигнала, которые длятся меньше 0,18 секунд будут подавляться. Во втором случае (при k = 0,3), мы получим t = 0,047 сек. Вникнув в эту связь, можно настроить фильтр, всего лишь глянув на график сырого значения!
/*
Элементарная реализация среднего арифметического. Сложили NUM_READINGS измерений,
затем разделили сумму на NUM_READINGS и всё!
Является "частным случаем" предыдущего фильтра
Время выполнения примерно равно: 10 значений 50 мкс, 50 значений 92 мкс, 100 значений 146 мкс
*/
#define NUM_READINGS 500
int average;
void setup() {
Serial.begin(9600);
}
void loop() {
long sum = 0;                                  // локальная переменная sum
for (int i = 0; i < NUM_READINGS; i++) {      // согласно количеству усреднений
sum += analogRead(0);                        // суммируем значения с любого датчика в переменную sum
}
average = sum / NUM_READINGS;                  // находим среднее арифметическое, разделив сумму на число измерений
Serial.println(average);                       // для примера выводим в порт
}
/*
Готовая функция для вычисления среднего арифметического
Принимает новые значения, суммирует их в своём массиве
*/
#define NUM_AVER 10       // выборка (из скольки усредняем)
long average;             // перем. среднего
int valArray[NUM_AVER];   // массив
byte idx = 0;             // индекс
int middleArifm(int newVal) {       // принимает новое значение
valArray[idx] = newVal;           // пишем каждый раз в новую ячейку
if (++idx >= NUM_AVER) idx = 0;   // перезаписывая самое старое значение
average = 0;                      // обнуляем среднее
for (int i = 0; i < NUM_AVER; i++) {
average += valArray[i];         // суммируем
}
average /= NUM_AVER;              // делим
return 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;
}

ЧИСЛА, МАТЕМАТИКА

В этом примере покажу, как разбить число на цифры и поместить в массив:

void setup() {
Serial.begin(9600);
long data = 1234567;   // число, которое нужно разбить
int8_t bytes[10];      // буфер
byte amount;           // количество цифр в числе
for (byte i = 0; i < 10; i++) { //>
bytes[i] = data % 10; // записываем остаток в буфер
data /= 10;         // "сдвигаем" число
if (data == 0) {    // если число закончилось
amount = i;       // запомнили, сколько знаков
break;
}
} // массив bytes хранит цифры числа data в обратном порядке!
for (int8_t i = amount; i >= 0; i--) {  //>
Serial.println(bytes[i]);   // выводим
}
}
void loop() {}

Обратная задача: собрать число из цифр, например – из массива. Удобно при парсинге по одному символу

void setup() {
Serial.begin(9600);
byte digits[] = {1, 2, 3, 4, 5, 6, 7};
long number = 0;
for (byte i = 0; i < sizeof(digits) / sizeof(digits[0]); i++) {
number += digits[i];  // пишем следующую цифру
number *= 10;         // "сдвигаем" число
}
number /= 10; // убираем лишнее умножение на 10
Serial.println(number);
}

МАССИВЫ, БУФЕРЫ

void setup() {
Serial.begin(9600);
byte bytes[] = {0, 1, 2, 3, 4, 5, 6, 7};
// выводим в порт
for (byte i = 0; i < 8; i++) {
Serial.print(bytes[i]);
Serial.print(' ');
}
Serial.println();
// копируем массив в буфер
byte buf[8];
for (byte i = 0; i < 8; i++) {
buf[i] = bytes[i];
}
// переписываем наоборот
for (byte i = 0; i < 8; i++) {
bytes[i] = buf[7 - i];
}
// выводим для проверки
for (byte i = 0; i < 8; i++) {
Serial.print(bytes[i]);
Serial.print(' ');
}
}

Рассмотрим, как хранить в массиве например 5 последних значений с датчика для дальнейшего усреднения. Будем работать с линейным буфером, перед записью нового элемента все предыдущие сдвигаются влево, стирая самый первый элемент, и освобождая место для нового.

#define ARRAY_SIZE 5
byte bytes[ARRAY_SIZE];
void setup() {
Serial.begin(9600);
// 7 раз "задвинем" в массив случайное число
// и выведем в порт
for (byte i = 0; i < 7; i++) {
updateArray(random(0, 100));
printArray();
}
/*
Вывод:
0   0   0   0   7
0   0   0   7   49
0   0   7   49  73
0   7   49  73  58
7   49  73  58  30
49  73  58  30  72
73  58  30  72  44
*/
}
void updateArray(int newVal) {
for (byte i = 0; i < ARRAY_SIZE - 1; i++) {
// сдвигаем члены влево
bytes[i] = bytes[i + 1];
}
// пишем новое значение в последний элемент
bytes[ARRAY_SIZE - 1] = newVal;
}
void printArray() {
// выводим в порт
for (byte i = 0; i < ARRAY_SIZE; i++) {
Serial.print(bytes[i]);
Serial.print('\t');
}
Serial.println();
}
void loop() {}

Рассмотрим, как хранить в массиве например 5 последних значений с датчика для дальнейшего усреднения. Будем работать с циклическим буфером: придётся помнить номер последнего нового элемента. Данный алгоритм лучше, т.к. не приходится  перематывать массив.

#define ARRAY_SIZE 5
byte bytes[ARRAY_SIZE];
byte arrayCounter = 0;  // номер ячейки
void setup() {
Serial.begin(9600);
// 7 раз "задвинем" в массив случайное число
// и выведем в порт
for (byte i = 0; i < 7; i++) { //>
updateArray(random(0, 100));
printArray();
}
/*
Вывод:
7 0 0 0  
7 49 0 0 0
7 49 73 0 0
7 49 73 58 0
7 49 73 58 30
72 49 73 58 30
72 44 73 58 30
*/
}
void updateArray(int newVal) { // пишем новое значение в элемент номер arrayCounter
bytes[arrayCounter] = newVal;
arrayCounter++; // прибавляем
// и зацикливаем
if (arrayCounter > ARRAY_SIZE - 1) arrayCounter = 0;
}
void printArray() {
// выводим в порт
for (byte i = 0; i < ARRAY_SIZE; i++) {
Serial.print(bytes[i]);
Serial.print('\t');
}
Serial.println();
}
void loop() {}

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

// пример кольцевого буфера для хранения набора данных
#define buffer_SIZE 32    // размер буфера
int buffer[buffer_SIZE];  // сам буфер (массив)
uint8_t buffer_head;      // "голова" буфера
uint8_t buffer_tail;      // "хвост" буфера
void setup() {}
void loop() {}
// запись в буфер
void bufferWrite(int newVal) {
// положение нового значения в буфере
uint8_t i = (buffer_head + 1) % buffer_SIZE;
// если есть местечко
if (i != buffer_tail) {
buffer[buffer_head] = newVal; // пишем в буфер
buffer_head = i;              // двигаем голову
}
}
// чтение из буфера
int bufferRead() {
if (buffer_head == buffer_tail) return -1;  // буфер пуст
int thisVal = buffer[buffer_tail];          // берём с хвоста
buffer_tail = (buffer_tail + 1) % buffer_SIZE;  // хвост двигаем
return thisVal;   // возвращаем значение
}
// возвращает крайнее значение без удаления из буфера
// если буфер пуст, вернёт -1
int bufferPeek() {
return buffer_head != buffer_tail ? buffer[buffer_tail] : -1;
}
// вернёт количество непрочитанных элементов
// если буфер пуст, вернёт -1
int bufferAmount() {
return ((unsigned int)(buffer_SIZE + buffer_head - buffer_tail)) % buffer_SIZE;
}
// "очистка" буфера
void bufferClear() {
buffer_head = buffer_tail = 0;
}

Данный вариант отличается от предыдущего более быстрым выполнением (остаток от деления заменён условием)

// пример кольцевого буфера для хранения набора данных
#define buffer_SIZE 32    // размер буфера
int buffer[buffer_SIZE];  // сам буфер (массив)
uint8_t buffer_head;      // "голова" буфера
uint8_t buffer_tail;      // "хвост" буфера
void setup() {}
void loop() {}
// запись в буфер
void bufferWrite(int newVal) {
// положение нового значения в буфере
uint8_t i = (buffer_head + 1 >= buffer_SIZE) ? 0 : buffer_head + 1;
// если есть местечко
if (i != buffer_tail) {
buffer[buffer_head] = newVal; // пишем в буфер
buffer_head = i;              // двигаем голову
}
}
// чтение из буфера
int bufferRead() {
if (buffer_head == buffer_tail) return -1;  // буфер пуст
int thisVal = buffer[buffer_tail];          // берём с хвоста
if (++buffer_tail >= buffer_SIZE) buffer_tail = 0;  // хвост двигаем
return thisVal;   // возвращаем значение
}
// возвращает крайнее значение без удаления из буфера
// если буфер пуст, вернёт -1
int bufferPeek() {
return (buffer_head != buffer_tail) ? buffer[buffer_tail] : -1;
}
// вернёт количество непрочитанных элементов
// если буфер пуст, вернёт -1
int bufferAmount() {
return ((unsigned int)(buffer_SIZE + buffer_head - buffer_tail)) % buffer_SIZE;
}
// "очистка" буфера
void bufferClear() {
buffer_head = buffer_tail = 0;
}

ПЕРЕДАЧА ПАРАМЕТРОВ

int myArray[] = {100, 30, 890, 645, 251};
void setup() {
getSecond(myArray);   // результат 30
}
int getSecond(int *intArray) {
// возвращает второй элемент массива
return intArray[1];
}
void loop() {
}
В целом, то же самое что предыдущий вариант

int myArray[] = {100, 30, 890, 645, 251};
void setup() {
getSecond(myArray);   // результат 30
}
int getSecond(int intArray[]) {
// возвращает второй элемент массива
return intArray[1];
}
void loop() {
}
int myArray[] = {100, 30, 890, 645, 251};
void setup() {
uart.begin();
uart.println(getSecond(&myArray));   // результат 30
}
int getSecond(const void * intArray) {
const int * Array = (const int *) intArray;
// возвращает второй элемент массива
return Array[1];
}
void loop() {
}
struct myStruct {
byte lol = 210;
int kek = 2019;
float cheburek = 0.1;
} testStruct;
void setup() {
Serial.begin(9600);
Serial.println(getSecond(&testStruct));   // результат 2019
}
int getSecond(const void * tempStruct) {
const myStruct * thisStruct = (const myStruct *) tempStruct;
// возвращает второй элемент структуры 
return thisStruct->kek;
}
void loop() {
}
struct myStruct {
byte lol = 210;
int kek = 2019;
float cheburek = 0.1;
} testStruct;
void setup() {
Serial.begin(9600);
Serial.println(getSecond(testStruct));   // результат 2019
}
int getSecond(myStruct tempStruct) {
// возвращает второй элемент структуры 
return tempStruct.kek;
}
void loop() {
}
struct myStruct {
byte lol = 210;
int kek = 2019;
float cheburek = 0.1;
} testStruct;
void setup() {
Serial.begin(9600);
Serial.println(getSecond(testStruct));   // результат 2019
}
template‹class T› int getSecond(T& tempStruct) {
// возвращает второй элемент структуры 
return tempStruct.kek;
}
void loop() {
}
struct myStruct {
byte lol = 210;
int kek = 2019;
float cheburek = 0.1;
} testStruct;
void setup() {
Serial.begin(9600);
Serial.println(getSecond(&testStruct));   // результат 2019
}
template‹class T› int getSecond(T* tempStruct) {
// возвращает второй элемент структуры 
return tempStruct->kek;
}
void loop() {
}
int c;
int myArray[] = {100, 30, 890, 645, 251, 645, 821, 325};
int newArray[8];
struct kekstruct {
byte ass = 10;
int lol = 15000;
float tazz = 3.14;
} kek;
void setup() {
// передаём сам массив и его размер в БАЙТАХ
c = sumFunction(&myArray, sizeof(myArray));
rewriteFunction(&myArray, &newArray, sizeof(myArray));
uartBegin();
uartPrint(c);
for (byte i = 0; i ‹ sizeof(newArray) / sizeof(int); i++) {
uartPrintln(newArray[i]);
}
uartPrintln();
kekstruct kek2;
//rewriteFunction(&kek, &kek2, sizeof(kek));
rewrite(kek, kek2);
uartPrintln(kek2.ass);
uartPrintln(kek2.lol);
uartPrintln(kek2.tazz);
}
void loop() {
}
// суммирует массив
int sumFunction(void* intArray, int arrSize) {
uint8_t* current = reinterpret_cast‹uint8_t*›(intArray);
// переменная для суммирования
int sum = 0;
// находим размер массива, разделив его вес
// на вес одного элемента (тут у нас int)
/*arrSize = arrSize / sizeof(int);  */
for (byte i = 0; i ‹ arrSize; i++) {
sum += *current;
current++;
}
return sum;
}
// переписывает что угодно через reinterpret_cast
void rewriteFunction(const void* curArray, void* tarArray, int arrSize) {
uint8_t* target = reinterpret_cast‹uint8_t*›(tarArray);
const uint8_t* current = reinterpret_cast‹const uint8_t*›(curArray);
while (arrSize--) {
*target++ = *current++;
}
}

СВЕТОДИОДЫ

Друзья, тут проблемка с отображением, вставляйте код в 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);

РАЗНОЕ

Данный скетч сканирует адресное пространство шины I2C и выводит адреса обнаруженных устройств в последовательный порт

#include 
void setup() {
Wire.begin();
Serial.begin(9600);
while (!Serial);             // Leonardo: wait for serial monitor
Serial.println("\nI2C Scanner");
}
void loop() {
byte error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for (address = 1; address < 127; address++ )
{
// The i2c_scanner uses the return value of
// the Write.endTransmisstion to see if
// a device did acknowledge to the address.
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print("I2C device found at address 0x");
if (address < 16)
Serial.print("0");
Serial.print(address, HEX);
Serial.println("  !");
nDevices++;
}
else if (error == 4)
{
Serial.print("Unknown error at address 0x");
if (address < 16)
Serial.print("0");
Serial.println(address, HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
else
Serial.println("done\n");
delay(5000);           // wait 5 seconds for next scan
}

Пример вывода с термистора значения в градусах Цельсия, термистор подключен по схеме делителя напряжения с резистором. Я подключал 10к термистор с 10к резистором соответственно, по схеме GND — термистор — A0 — 10к — 5V. В коде предусмотрена настройка коэффициента термистора (берётся из даташита) и базовой температуры (тоже из даташита)

// GND --- термистор --- A0 --- 10к --- 5V
#define THERM A0            // к какому аналоговому пину мы подключены
#define RESIST_10K 10000    // точное сопротивление 10к резистора (Ом)
void setup() {
Serial.begin(9600);
//analogReference(EXTERNAL);
}
void loop() {
Serial.print("Temperature ");
Serial.print(getThermTemp(analogRead(THERM)));
Serial.println(" *C");
delay(1000);
}
// цифры взяты из даташита
#define RESIST_BASE 10000   // сопротивление при TEMP_BASE градусах по Цельсию (Ом)
#define TEMP_BASE 25        // температура, при которой измерено RESIST_BASE (градусов Цельсия)
#define B_COEF 3435         // бета коэффициент термистора (3000-4000)
float getThermTemp(int resistance) {
float thermistor;
thermistor = RESIST_10K / ((float)1023 / resistance - 1);
thermistor /= RESIST_BASE;                        // (R/Ro)
thermistor = log(thermistor) / B_COEF;            // 1/B * ln(R/Ro)
thermistor += (float)1.0 / (TEMP_BASE + 273.15);  // + (1/To)
thermistor = (float)1.0 / thermistor - 273.15;    // инвертируем и конвертируем в градусы по Цельсию
return thermistor;
}

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