14. Многозадачность с единственным процессом

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


Переход из мира программирования больших систем

Плата Arduino привлекла множество энтузиастов (в том числе и меня), которые работают в индустрии программного обеспечения не один год, имеют опыт работы в составе коллективов из десятков человек, объединяющих усилия для создания больших программных продуктов, и привыкли решать все возникающие проблемы. Для нас возможность без продолжительного проектирования написать несколько строк кода и практически немедленно получить какое-нибудь интересное проявление в физическом мире является отличным противоядием от привычек, прививаемых в мире большого программного обеспечения.

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

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


Почему вам не нужны потоки выполнения

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

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

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


Функции setup и loop

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


Оценка, затем действие

Большинство проектов для Arduino предназначено для управления чем-то. Поэтому функция loop часто:

• проверяет нажатие кнопки или превышение данных с некоторого датчика порогового значения;

• выполняет соответствующее действие.

Простым примером может служить реализация мигания светодиода в результате нажатия кнопки.

Следующий пример иллюстрирует это. Однако, как будет показано далее, иногда необходимость ждать, пока выполняется код, управляющий миганием светодиода, бывает совершенно неприемлемой:

// sketch_14_01_flashing_1

const int ledPin = 13;

const int switchPin = 5;

const int period = 1000;

boolean flashing = false;

void setup()

{

pinMode(ledPin, OUTPUT);

pinMode(switchPin, INPUT_PULLUP);

}

void loop()

{

if (digitalRead(switchPin) == LOW)

{

flashing = ! flashing;

}

if (flashing)

{

digitalWrite(ledPin, HIGH);

delay(period);

digitalWrite(ledPin, LOW);

delay(period);

}

}

Проблема данной реализации в том, что она проверяет нажатие кнопки только после того, как завершится цикл включения/выключения светодиода. Если кнопка будет нажата во время этого цикла, факт нажатия зафиксирован не будет. Это может быть не важно для нормальной работы скетча, но если важно фиксировать каждое нажатие кнопки, следует полностью исключить любые задержки в функции loop. Фактически после перехода в режим мигания Arduino будет тратить основное время на задержки и только малую часть времени — на проверку состояния кнопки.

Пример в следующем разделе решает эту проблему.


Пауза без приостановки

Предыдущий скетч можно переписать без использования функции delay:

// sketch_14_02_flashing_2

const int ledPin = 13;

const int switchPin = 5;

const int period = 1000;

boolean flashing = false;

long lastChangeTime = 0;

int ledState = LOW;

void setup()

{

pinMode(ledPin, OUTPUT);

pinMode(switchPin, INPUT_PULLUP);

}

void loop()

{

if (digitalRead(switchPin) == LOW)

{

flashing = ! flashing;

// и выключить светодиод

if (! flashing)

{

digitalWrite(ledPin, LOW);

}

}

long now = millis();

if (flashing && now > lastChangeTime + period)

{

ledState = ! ledState;

digitalWrite(ledPin, ledState);

lastChangeTime = now;

}

}

В этом скетче я добавил две новые переменные, lastChangeTime и ledState. Переменная lastChangeTime хранит время последнего перехода светодиода между состояниями «включено» и «выключено», а переменная ledState хранит текущее состояние светодиода — «включено» или «выключено», чтобы знать, в каком состоянии он пребывает, когда потребуется переключить его.

Теперь функция loop не выполняет задержек. В первой части loop проверяется нажатие кнопки, и, если кнопка нажата, переключается режим мигания. Дополнительная инструкция if, следующая далее, просто выключает светодиод, если нажатие кнопки вызвало выключение режима мигания. В противном случае светодиод мог бы остаться включенным:

if (! flashing)

{

digitalWrite(ledPin, LOW);

}

Во второй части функция loop читает текущее значение счетчика миллисекунд вызовом millis() и сравнивает со значением lastChangeTime, увеличенным на значение period. То есть код внутри этой инструкции if выполняется, только если с момента последнего переключения светодиода прошло более period миллисекунд.

Затем значение переменной ledState изменяется на противоположное, и на цифровом выходе устанавливается соответствующий уровень напряжения. Потом значение now копируется в lastChangeTime, чтобы можно было определить, когда наступит момент следующего переключения.


Библиотека Timer

Решение, представленное в разделе «Пауза без приостановки», было обобщено и реализовано в виде библиотеки, позволяющей планировать выполнение повторяющихся операций с использованием функции millis. Несмотря на свое название, библиотека не использует аппаратные таймеры и потому прекрасно работает в большинстве моделей Arduino.

Получить библиотеку можно по адресу http://playground.arduino.cc//Code/Timer.

Применение библиотеки может существенно упростить код, как показано далее:

// sketch_14_03_flashing_3

#include

const int ledPin = 13;

const int switchPin = 5;

const int period = 1000;

boolean flashing = false;

int ledState = LOW;

Timer t;

void setup()

{

pinMode(ledPin, OUTPUT);

pinMode(switchPin, INPUT_PULLUP);

t.every(period, flashIfRequired);

}

void loop()

{

if (digitalRead(switchPin) == LOW)

{

flashing = ! flashing;

if (! flashing)

{

digitalWrite(ledPin, LOW);

}

}

t.update();

}

void flashIfRequired()

{

if (flashing)

{

ledState = ! ledState;

digitalWrite(ledPin, ledState);

}

}

Чтобы задействовать возможности библиотеки, необходимо определить объект таймера (в данном скетче он получил имя t) и в функции setup указать функцию для вызова через установленные периоды:

t.every(period, flashIfRequired);

Затем нужно добавить в функцию loop следующую строку:

t.update();

В каждом вызове функция update проверит значение millis, определит необходимость выполнения повторяющихся действий и, если пришло время для этого, вызовет указанную функцию (в данном случае flashIfRequired).

Библиотека Timer имеет также множество других вспомогательных функций. Более подробную информацию о ней можно найти, если пройти по ссылке, приведенной в начале раздела.


В заключение

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

В заключительной главе вы узнаете, как поделиться своими разработками с сообществом Arduino, создавая и публикуя библиотеки для Arduino.


Загрузка...