Arduino и переполнение счетчиков - мигаем светодиодом без delay

Новички, которые начинают изучать программирование микроконтроллеров Arduino (язык программирования Ардуино — Wiring, очень упрощенная версия C++), скорее всего, видели примеры, как помигать встроенным в микроконтроллер светодиодом. Обычно, в этих примерах используется функция delay(), которая как бы "замораживает" микроконтроллер на время своего выполнения. И если Arduino должен обрабатывать не только задержку при мигании светодиодом (например с задержкой в 5 сек.), а еще и нажатие на кнопку, то в момент выполнения функции delay() микроконтроллер может попросту "не заметить" нажатие на кнопку. Поскольку, обычно, в скетчах для Ардуино обрабатывается более 2 событий, использовать функцию delay() весьма не желательно. Чем и как правильно заменить delay, рассказано ниже.

Все примеры, опубликованные в этой статье протестированы на микроконтроллере (далее МК) Arduino Mega 2560.

Arduino - мигаем светодиодом
// Включение светодиода на 1 секунду и отключение на тот же интервал в цикле
 
void setup()
  {              
  // Инициализируем цифровой вход/выход в режиме выхода.
  // Выход 13 на большинстве плат Arduino подключен к светодиоду на плате.
  pinMode(13, OUTPUT);  
  }
 
void loop()
  {
  digitalWrite(13, HIGH);   // Подаем напряжение на светодиод
  delay(1000);              // Ожидаем 1 секунду
  digitalWrite(13, LOW);    // Отключаем напряжение со светодиода
  delay(1000);              // Ожидаем 1 секунду
  }
 

Этот пример служит только для быстрого старта, чтобы бы можно было сразу увидеть результат программирования микроконтроллера, и не сильно вникать в тонкости программирования. Проблемы начинаются тогда, когда от Arduino требуется выполнить несколько действий одновременно - функция delay() полностью "зависает" микроконтроллер на время своего выполнения, не позволяя параллельно обрабатывать, например, нажатия кнопок, показания датчиков и т.п.

Многозадачность Arduino

Для того, чтобы можно было выполнять несколько действий на Ардуино одновременно - следует воспользоваться таймерами. Пример вывода информации в Serial Monitor и параллельного мигания светодиодом без использования функции delay():

// http://petrenco.com/arduino.php

#define LED_MK_PIN 13      // Номер выхода, к которому подключен внутренний светодиод
#define LED_MK_INTERVAL 1000UL  // Интервал между включениея и выключения LED
#define SERIAL_SPEED 115200 // Скорость работы порта Serial - то же число должно быть выбрано и в мониторе порта
#define SERIAL_INTERVAL 3000UL  // Периодичность вывода времени в Serial (1 cсекунда)

void setup()
  {
  pinMode(LED_MK_PIN, OUTPUT);      
   
  Serial.begin(SERIAL_SPEED);
  }
 
void loop()
  {
  // Включение/отключение светодиода
  static unsigned long led_mk_timer = 0;
  if (millis() - led_mk_timer > LED_MK_INTERVAL)
    {
    led_mk_timer = millis();  //
    digitalWrite(LED_MK_PIN, !digitalRead(LED_MK_PIN));
    }
   
  // Вывод данных в Serial с заданным интервалом
  static unsigned long serial_timer = 0;
  static byte test_value_1 = 5;
  static byte test_value_2 = random(0, 255);
  static byte test_value_result;
  if (millis() - serial_timer > SERIAL_INTERVAL)
    {
    serial_timer = millis();
     
    test_value_2 = random(0, 255);
    test_value_result = test_value_1 - test_value_2;
     
    Serial.print("Current time:");
    Serial.println(millis());
    Serial.print("test_value_1: ");
    Serial.print(test_value_1);
    Serial.print("; test_value_2: ");
    Serial.print(test_value_2);
    Serial.print("; test_value_result (test_value_1 - test_value_2): ");
    Serial.print(test_value_result);
    Serial.println();
    Serial.println();
    }
  }

На самом деле, МК не имеет функций многозадачности, и не может параллельно/одновременно выполнять несколько различных действий. Но благодаря тому, что каждое последовательное действие в примере выше происходит очень быстро, за несколько микросекунд (1 секунда = 1 000 000 микросекунд), с использованием таймеров можно добиться того, что будет казаться, как будто бы Ардуино выполняет несколько различных операций одновременно.

Если внимательно изучить код, то на первый взгляд может сложиться впечатление, что данные будут выводиться только в течении 4 294 967 295 миллисекунд, что приблизительно равно 50 дням. Ведь самый вместительный тип переменной unsigned long ограничен именно этим числом. При достижении в функции mills() значения 4 294 967 295 (в ней так же используется тип unsigned long), на следующем шаге её значение будет сброшено в 0, и далее снова начнет увеличиваться от нуля. Если применить всем известные математические правила к случаю, когда в serial_timer будет, например, через 50 дней записано значение 4 294 967 000, то после сброса mills() в ноль, строка if (millis() - serial_timer > SERIAL_INTERVAL) (например 5000 - 4 294 967 000 > 3000) может никогда не стать верной, соответственно, через 50 дней сообщения в Serial Monitor выводиться более не будут. Но то что кажется - не всегда верно, и вот почему.

Специально для того, чтобы объяснить, как будет работать таймер после сброса значения в ноль, в скрипте добавлены переменные: test_value_1, test_value_2, test_value_result. Все они объявлены, как тип byte, в котором могут храниться только положительные числа от 0 до 255 (byte является всегда только unsigned, поэтому писать unsigned byte будет ошибкой). Переменные объявлены как byte исключительно для облегчения восприятия, но то же самое будет происходить и с типом unsigned long. Теперь рассмотрим вывод данных и разберем, как так может получиться, что 5 минус 19 будет равно 242?

Current time:3001
test_value_1: 5; test_value_2: 19; test_value_result (test_value_1 - test_value_2): 242

Current time:6002
test_value_1: 5; test_value_2: 158; test_value_result (test_value_1 - test_value_2): 103

Current time:9003
test_value_1: 5; test_value_2: 38; test_value_result (test_value_1 - test_value_2): 223
 

Такая "странная", на первый взгляд математика происходит из за того, что в переменной не могут содержаться отрицательные значения из-за её типа. МК считает так (образно): поскольку 19 более 5, то от числа пять сначала отнимается пятерка, её же отнимаем от 19: 5 - 5 = 0, в остатке от 19 остается 14 (19 - 5 = 14). Далее, от 0 отнимается единица, что дает максимальное значение этого типа переменной (byte) 255, в остатке остается 13. Отняв 13 от 255 (255 - 13) и получается 242!

Теперь разберем следующий пример, вычтем из трех восемь (3 - 8). Цифру восемь раскладываем на 3 + 1 + 4. Теперь считаем: 3 - 3 = 0, 0 - 1 = 255, 255 - 4 = 251.

Работа с таймерами, приведена для примера, существуют более красивые, в плане читаемости кода, решения: классы, функции, сторонние библиотеки и др., но принципы их работы основываются на том, что все переменные, используемые в таймерах, объявляются как unsigned.

Если после прочтения статьи останется что-то не понятно - спрашивайте в комментариях и смотрите статьи по ссылкам ниже.

Опубликовано: 2015/06/15
HTML-код ссылки на эту страницу:
<a href="https://petrenco.com/arduino.php?txt=193" target="_blank">Arduino и переполнение счетчиков - мигаем светодиодом без delay</a>
18542
Комментарии
не учтен момент перезапуска mills

ведь если led_mk_timer = 4 294 967 000
а mills вернет 0

то ваше условие if (millis() - led_mk_timer > LED_MK_INTERVAL) будет ждать вечно

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

а как программировать таймеры тут и не пахнет
Алексей, ладно в статье да, сыр бор, и все очень размыто, но вы то точно шутник! Если mills вернет 0, то при вычитании из типа переменной unsigned, справа, хоть двести триллионов ставьте, все равно результат будет нуль! А в следующем цикле значение led_mk_timer переназначится на нормальную цифру. Так что будет работать так, как нужно!
Точнее в беззнаковых числах результат будет не ноль, число просто перевернется, но сути это не меняет, условие выполниться, как нужно, ибо подразумевается, что значение справа, это не цифра от балды, а предыдущее значение millis, поэтому результат вычисления не выдаст нам сюрпризов и оно не зашкалит. В вашем примере оно например равно 296 :) Никаких проблем нет при переполнении millis(), просто пример в статье не очень хорошо представлен, нет форматирования кода.
Добавить комментарий
Ваш e-mail: (не виден посетителям сайта)
Ваше имя:
Комментарий:
Символы с картинки:
Только выделенные поля формы добавления комментариев обязательны к заполнению.