Ардуино: параллельное выполнение задач по таймеру

Множество уроков на Ардуино сводятся к последовательному выполнению каких-то действий. Например, чтобы заставить контроллер мигать светодиодом или периодически пищать зуммером, достаточно будет такой простой программы:

const int ledPin = 2;

void setup() {
    pinMode(ledPin, OUTPUT);
}

void loop() {
    digitalWrite(ledPin, HIGH);
    delay(500);
    digitalWrite(ledPin, LOW);
    delay(500); 
}

Включаем светодиод, ждем пол секунды, затем выключаем его и снова ждем. Поскольку задача у нас одна, и кроме мигания мы ничего не собираемся делать, то в промежутках между включением и выключением используем функцию delay. Во время работы delay наш контроллер не делает ничего полезного. Он просто тратит время.

А что, если теперь мы захотим мигать одновременно двумя светодиодами с разным периодом? Допустим, первый светодиод должен включаться и выключаться каждую секунду, а второй каждые 300 миллисекунд.

Чтобы решить эту задачу, нам придется отказаться от функции delay. Это может быть тяжелым ударом для каждого новичка, но в серьезных программах delay никто не использует 🙂 Если мы научимся обходить эту функцию, мы автоматически научимся одновременному выполнению задач. Вперед!

Таймеры, секундомеры и таймаут

Представим, что у нас нет никаких контроллеров, а есть только механический секундомер. Проведем эксперимент у себя дома. Будем мигать лампочкой в комнате с периодом 20 секунд. Это значит, что каждые 10 секунд мы должны будем переключать выключатель из включенного положения, в выключенное, и наоборот. Получится такой естественный алгоритм:

TImer

Если вы внимательно посмотрите на этот алгоритм, то поймете, что он полностью отражает действия живого человека. Часто, вместо секундомера мы используем внутренний биологический таймер. У нас есть чувство времени, которое особенно развито у музыкантов.

Итак, алгоритм мы имеем. Как его переложить на программу для Ардуино? Нам потребуется электронный аналог секундомера — таймер. Благо он имеется во всех контроллерах, включая и Ардуино. Таймер — это устройство, которое умеет точно отсчитывать время. Запускается и обнуляется он каждый раз во время подачи питания на контроллер. А чтобы узнать, сколько времени «натикало» на таймере с момента запуска, мы будем использовать функцию millis:

int time = millis();

эта функция не имеет аргументов; она опрашивает таймер и возвращает количество миллисекунд, которые прошли с момента запуска Ардуино.

Теперь у нас есть всё, чтобы записать программу для Ардуино.

const int ledPin = 2;

unsigned long next_time; // время очередного переключения первого светодиода
int timeout = 500; // половина периода мигания
int led_state = 0; // начальное состояние светодиода - выключен

void setup() {
    pinMode(ledPin, OUTPUT);

    digitalWrite(ledPin, led_state); // гасим светодиод
    next_time = millis() + timeout; // вычисляем время следующего переключения
}

void loop() {
    int now_time = millis(); // текущее время
    if( now_time >= next_time ){ // если текущее время превысило намеченное время, то
        next_time = now_time + timeout; // вычисляем время следующего переключения
        led_state = !led_state; // меняем состояние на противоположное
        digitalWrite(ledPin, led_state); // зажигаем или гасим светодиод
    }
}

Загружаем программу на Ардуино. Светодиод будет мигать точь в точь, как в первой программе. Но теперь без delay! Переходим к следующем шагу.

Одновременное выполнение действий с разным периодом

У нас есть программа, которая совершает действие каждые 500 миллисекунд. Добавим в неё второе действие, но уже с другим периодом — 150 мс.

const int ledPin_1 = 2;
const int ledPin_2 = 3;

unsigned long next_time_1; // время очередного переключения первого светодиода
unsigned long next_time_2; // ... второго светодиода
int timeout_1 = 500; // половина периода мигания первого светодиода
int timeout_2 = 150; // ... второго светодиода
int led_state_1 = 0; // начальное состояние первого светодиода - выключен
int led_state_2 = 0; // ... второго светодиода

void setup() {
    pinMode(ledPin_1, OUTPUT);
    pinMode(ledPin_2, OUTPUT);

    digitalWrite(ledPin_1, led_state_1); // гасим первый светодиод
    digitalWrite(ledPin_2, led_state_2); // гасим второй светодиод

    next_time_1 = millis() + timeout_1; // вычисляем время следующего переключения первого светодиода
    next_time_2 = millis() + timeout_2; // ... второго светодиода
}

void loop() {
    int now_time = millis(); // текущее время
    if( now_time >= next_time_1 ){ // если текущее время превысило намеченное время, то
        next_time_1 = now_time + timeout_1; // вычисляем время следующего переключения
        led_state_1 = !led_state_1; // меняем состояние на противоположное
        digitalWrite(ledPin_1, led_state_1); // зажигаем или гасим светодиод
    }

    now_time = millis();
    if( now_time >= next_time_2 ){
        next_time_2 = now_time + timeout_2;
        led_state_2 = !led_state_2;
        digitalWrite(ledPin_2, led_state_2);
    }
}

Загружаем программу, включаем питание. Теперь светодиоды мигают с разным периодом!

Заключение

При помощи таймера и таймаутов мы можем решить множество задач, которые решались сложно, либо вовсе не решались. Это те задачи, в которых требуется одновременное выполнение действий с разной частотой.

Это может быть вращение двух и более шаговых двигателей с разной скоростью, динамическая индикация вместе с опросом разных датчиков. Более сложный пример — бортовая программа квадрокоптера или балансирующего колесного робота, которая выполняет множества разных действий с разной частотой: опрос датчиков, работа системы стабилизации, обработка сигналов с пульта управление и др.


Изменено:

Ардуино: параллельное выполнение задач по таймеру: 11 комментариев

  1. Количество миллисекунд с момента начала выполнения программы — unsigned long
    иначе Вас ожидает ошибка компиляции

    • Ошибки компилятора не будет, так как с точки зрения синтаксиса всё правильно. Но хранение времени в переменной типа Int действительно может привести к беде при её переполнении (всего 32768 миллисекунды). Так что поправили типы переменных в уроке, благодарю за замечание!

  2. Доброе время суток уважаемые! Кто нибудь подскажет как подключить светодиодную матрицу к микросхеме max7219?
    А то в интернете столько ложной информации от которой мозг отказывается понимать.

  3. Спасибо, что помогли понять!!! Много статей, которые пишут профи, ходящие на пальцах и с простыми людьми не могут общаться на понятном языке! Очень помогли!

  4. Добрый день! Некоторое замечание в void setup: next_time_1 = millis() + timeout_1; — но ведь millis()==0 в это время…наверное достаточно next_time_1 = timeout_1;
    next_time_2 = timeout_2;
    типа с умничал…

  5. Подскажите насколько долго может работать такой алгоритм, возможно ли «перечисление» по времени событий (не вызовет ли это проблем со сбором данных с датчиков)?

    • функция millis() переполняется через 50 суток. честно не знаю что будет,сбросится ди она в ноль или в какую-нибудь абракадабру. не знаю ,что у вас за датчики,но микроконтроллеры , в том числе ардуино, могут вызвать функцию WatchDog,то есть перезапускать микроконтроллер.если конечно это программист предусмотрел. а вообще лучше самому изучить регистры TCNT и не пользоваться функцией millis().

  6. действительно! почему бы второму аргументу функции digitalWrite не присвоить тип boolean, а переменной хранящей время тип long? и не понятен момент в настройках void setup() {
    next_time = millis() + timeout; ———— это зачем тут????????????????
    }

  7. Здравствуйте! Меня заинтересовала фраза: «Вращение шаговых двигателей с разной скоростью». давно ищу исходники по управлению станками с ЧПУ в режиме интерполяций (круговой, линейной…), где как раз и нужна разная скорость. Не подскажете пример. как такую интерполяцию реализовать в режиме Step/Dir. На Arduinj ли, или просто на ПК? Спасибо.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

This site uses Akismet to reduce spam. Learn how your comment data is processed.