Опубликовано

Прерывания и другие специальные функции

По материалам книги Джереми Блума “Изучаем Arduino: инструменты и методы технического волшебства. 2-е изд.”  (глава 13. Прерывания и другие специальные функции)

Изучаем Arduino: инструменты и методы технического волшебства. 2-е изд.

Исходный код и прочие электронные ресурсы:

Исходный код, видеоуроки и прочие электронные ресурсы для этой главы можно загрузить с веб-страницы https://www.exploringarduino.com/content2/ch13.

Исходный код для проектов этой главы можно также загрузить на вкладке Downloads веб-страницы издательства Wiley для этой книги: https://www.wiley.com/go/exploringarduino2e.

Что вы узнаете из этой главы

Все разрабатываемые нами до сих пор программы для Arduino были синхронного типа.

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

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

Аппаратные прерывания

Аппаратные прерывания активируются изменением состояния контакта ввода-вывода. Прерывания этого типа могут быть особенно полезными в тех случаях, когда нужно изменить значение переменной состояния, не прибегая к постоянному опросу состояния выхода кнопки. В некоторых предыдущих главах при каждой итерации главного цикла loop() мы выполняли программную защиту от дребезга и проверяли состояние кнопки. Такой подход работает удовлетворительно в тех случаях, когда исполнение кода в цикле loop() не занимает много времени.

Но, предположим, что нам нужно исполнить в цикле loop() довольно длительную процедуру. Например, нам нужно медленно постепенно повышать яркость светодиода или скорость вращения электродвигателя, используя для этого цикл for() и несколько операторов задержки delay(). Если управлять цветом или скоростью повышения яркости посредством кнопки, то мы пропустим нажатия кнопки, происходящие в течение исполнения функции delay(). Обычно, человеческая реакция достаточно инерционна, что позволяет выполнить большое количество функций в цикле loop() программы Arduino, включая опрос кнопки при каждой его итерации, не пропуская нажатий кнопки. Но когда цикл loop() содержит медленно исполняющиеся компоненты, существует вероятность пропустить события внешнего ввода.

Для решения подобных проблем и предназначены прерывания. Определенные контакты платы Arduino могут вызывать внешние аппаратные прерывания. Аппаратура микроконтроллера знает состояние этих контактов и может асинхронно предоставлять их значение коду прикладной программы. Таким образом, при обнаружении внешнего аппаратного прерывания исполнение главной программы можно приостановить, чтобы выполнить соответствующую процедуру обработки прерывания. Это прерывание может произойти в любой точке исполнения программы. На рис. 13.1 показан алгоритм процесса прерывания.

Рис. 13.1. Влияние внешнего прерывания на ход исполнения главной программы

Опрос состояния и прерывания: преимущества и недостатки каждого подхода

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

Программная реализация

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

Аппаратная реализация

С точки зрения аппаратной реализации для большинства цифровых контактов нет разницы между опросом контакта в цикле и прерыванием, т. к. в обоих случаях просто считывается состояние входа. Но в случае прерываний, активируемых перепадом уровней сигнала, аппаратную составляющую необходимо должным образом модифицировать. Как рассматривалось в главе 2, при нажатии многих кнопок (которые часто используются для активирования входа) их контакты испытывают дребезг. Это представляет серьезную проблему, поскольку дребезг будет много раз активировать процедуру обработки прерывания, тогда как мы хотим вызвать ее только один раз. Еще хуже то, что здесь не подойдет созданная нами ранее программная функция защиты от дребезга, поскольку в процедуре обработки прерывания недопустима функция delay(). Поэтому если для входного сигнала с дребезгом требуется аппаратное прерывание, то сначала нужно аппаратно устранить дребезг. Но в случае входного сигнала свободного от дребезга (например, с поворотного кодера или с другой микросхемы) аппаратное прерывание не будет отличаться от опроса.

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

Одна из основных причин для использования прерывания — возможность получения псевдо-многозадачности. Плата Arduino не способна поддерживать настоящую многозадачность, поскольку она оснащена только одним главным микроконтроллером, которым может исполнять только одну команду за раз. Но одну задачу можно кратковременно прерывать для перехода к другой, возвращаясь после нее к первой, и благодаря очень высокой скорости исполнения команд создается видимость одновременного выполнения этих задач, что и называется псевдо-многозадачностью. Например, используя прерывания, можно в процессе уменьшения яркости светодиода реагировать на нажатие кнопки, сигнал с которой управляет скоростью изменения яркости. При методе опроса получить значение состояния контакта внешнего входа можно только тогда, когда процесс исполнения кода в цикле loop() дойдет до функции digitalRead(). Это означает, что наличие в программе каких-либо медленных функций может создать трудности с эффективным мониторингом состояния входного сигнала.

Точность сбора данных

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

Но при этом нам необходимо не пропустить ни одного импульса от кодера. Эти импульсы довольно короткие (намного короче, чем импульсы, создаваемые нажатием кнопки), и если отслеживать их методом опроса в цикле loop(), то некоторые из них можно пропустить. Если поворотный кодер выдает только один импульс за полный поворот вала, то пропустив один такой импульс, программа будет считать, что двигатель вращается вдвое медленнее, чем в действительности. Таким образом, чтобы обеспечить захват важных событий, подобных рассмотренному, без аппаратных прерываний не обойтись. Но в случае сигналов с медленно изменяющимся состоянием (например, выходной сигнал кнопки) метод опроса может оказаться достаточным.

Возможности аппаратных прерываний Arduino

Для большинства плат Arduino сигналы прерываний можно подавать только на определенные контакты. Информацию, какие именно контакты вашей платы Arduino доступны для подачи на них сигналов прерываний, можно найти в документации по прерываниям на веб-сайте Arduino (blum.fyi/arduino-attach-interrupt).

Настройка контакта платы Arduino для работы в режиме источника аппаратных прерываний осуществляется с помощью функции attachInterrupt(). В первом аргументе функции указывается ID-номер прерывания, который не обязательно такой же самый, как и номер контакта. Определить, какой ID-номер прерывания можно присвоить конкретному контакту платы Arduino, можно с помощью функции digitalPinToInterrupt(). Для этого данной функции нужно просто передать номер цифрового контакта платы, и она сама определит соответствующий ID-номер прерывания в зависимости от версии платы Arduino, для которой компилируются скетч. Например, если мы хотим присвоить сигнал прерывания с кнопки контакту 2 платы Arduino Uno (или эквивалентной плате-клону с микроконтроллером ATmega328P), то в качестве первого аргумента функции attachInterrupt() нужно указать функцию digitalPinToInterrupt(2). Непосредственное указание одной функции в качестве аргумента другой функции вполне приемлемо[1], и, кроме того, делает код более аккуратным.

При возникновении аппаратного прерывания вызывается процедура его обработки. Данная функция указывается во втором аргументе функции attachInterrupt(). Например, менять значение булевой переменной на обратное при каждом прерывании можно с помощью следующей функции, которая передается функции attachInterrupt() во втором аргументе:

1
2
3
4
void toggleLed()
{
var = !var;
}

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

В последнем, третьем, аргументе функции attachInterrupt() указывается режим запуска прерывания. Прерывания Arduino могут запускаться низким уровнем сигнала (LOW) на контакте прерывания, изменением уровня сигнала (CHANGE), положительным перепадом уровня сигнала (RISING) или отрицательным перепадом уровня сигнала (FALLING). Некоторые платы Arduino также поддерживают запуск прерываний наличием сигнала высокого уровня (HIGH) на контакте прерывания. Наиболее часто используются режимы запуска CHANGE, RISING, and FALLING, поскольку они обеспечивают одноразовый запуск прерывания при изменении состояния внешнего входного сигнала, например, при смене уровня сигнала кнопки с низкого (LOW) на высокий (HIGH). Смена уровня сигнала с низкого (LOW) на высокий (HIGH) является положительным (RISING) перепадом, а с высокого (HIGH) на низкий (LOW) — отрицательным (FALLING). Режимы запуска LOW и HIGH менее распространены, поскольку в таких случаях прерывание будет запускаться постоянно, пока сохраняется данный уровень сигнала, блокируя исполнение остального кода программы.

Объединив все эти возможности, можно, например, при каждом нажатии кнопки, подключенной к контакту 2 платы Arduino Uno (т. е. изменении уровня ее выходного сигнала с низкого на высокий), вызывать рассмотренную ранее функцию toggleLED(). Для этого в секцию setup() нужно вставить следующую строку кода:

1
attachInterrupt(digitalPinToInterrupt(2), toggleLED, RISING);

Схема запуска прерывания кнопкой, оснащенной аппаратной защитой от дребезга

Чтобы закрепить наши новые знания, мы создадим схему с RGB-светодиодом и кнопкой с аппаратной защитой от дребезга. Яркость выбранного цветового элемента RGB-светодиода циклически постепенно увеличивается до максимальной, а затем уменьшается до минимальной, используя для этого функцию delay(). При нажатии кнопки элемент RGB-светодиода с регулируемой таким образом яркостью меняется на другой.

Схема аппаратной защиты от дребезга

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

Сначала рассмотрим обычный сигнал, получаемый при нажатии кнопки, оснащенной повышающим резистором. Функция повышающего резистора заключается в установлении на контакте ввода-вывода входного сигнала высокого уровня при разомкнутой кнопке. При нажатии кнопки происходит подключение контакта ввода-вывода на землю, т. е. установка на этом контакте сигнала низкого уровня. При необходимости этот сигнал низкого уровня можно инвертировать далее в схеме. На рис. 13.2 показана осциллограмма выходного сигнала кнопки в момент ее нажатия. Как можно видеть, уровень выходного сигнала кнопки не становится моментально низким, а скачет вверх и вниз, пока не установится в низком состоянии.

Рис. 13.2. Дребезг выходного сигнала нажатой кнопки

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

Такую цепочку можно создать, подключив выход кнопки к входному контакту микроконтроллера через резистор, а входной контакт микроконтроллера на землю — через конденсатор. При отпущенной кнопке конденсатор заряжается до напряжения питания (+5 В) через повышающий резистор и добавленный резистор RC-цепи. Конденсатор можно рассматривать, как накопитель электроэнергии, заряжающийся от шины положительного питания. Пока кнопка отпущена, этот накопитель заполняется. А при нажатии кнопки, т. е. при замыкании ее контактов, конденсатор начинает разряжаться на землю через резистор RC-цепи и замкнутые контакты кнопки.

Разряд конденсатора происходит не моментально, а занимает некоторый период времени, в течение которого на входном контакте микроконтроллера напряжение сохраняется на уровне близком к 5 В. Конденсатор перезаряжается при каждом отпускании кнопки, т. е. при размыкании ее контактов. Длительность времени разряда конденсатора превышает период дребезга контактов кнопки, в результате чего к моменту полного разряда конденсатора контакты кнопки успокоятся в стабильном замкнутом состоянии. В результате уровень сигнала на входном контакте микроконтроллера изменяется с высокого к низкому только один раз. Продолжительность периодов заряда и разряда конденсатора, и соответственно, длительность защиты от дребезга, определяется номиналами резистора и конденсатора Рассмотренная схема защиты от дребезга контактов показана на рис. 13.3.

Рис. 13.3. Схема защиты от дребезга с RC-цепочкой (Рисунок создан в программе EAGLE)

Подключенный последовательно кнопке резистор R2 в схеме на рис. 13.3 предотвращает почти моментальный разряд конденсатора. В результате выходной сигнал при разряде конденсатора выглядит так, как показано на осциллограмме на рис. 13.4.

Рис. 13.4. Устранение последствий дребезга контактов с помощью RC-цепочки

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

Большинство современных микроконтроллеров обладают встроенной способностью работать с медленными положительными или отрицательными перепадами цифровых сигналов. Например, в справочном листке для микроконтроллера ATmega приводятся разные пороговые значения для низкого и высокого уровней входного сигнала. При переходе входного сигнала с высокого уровня на низкий, его напряжение должно опуститься ниже 0,2U+, чтобы уровень определился, как низкий. А при переходе входного сигнала с низкого уровня на высокий, его напряжение должно подняться выше ниже 0,7U+, чтобы уровень определился, как высокий. Здесь U+ обозначает напряжение питания микроконтроллера (5 В в моем случае). Этот промежуток обеспечивает устойчивость сигнала в процессе перехода и называется гистерезисом.

Прерывания платформы Arduino могут запускаться таким медленным перепадом уровня входного сигнала. Но желательно разобраться, каким именно образом реализуется эта возможность. Для этого мы создадим схему на триггере Шмитта, чтобы она работала независимо от Arduino. Триггер Шмитта представляет собой схему, которая может создавать резкий перепад выходного сигнала, когда входной сигнал превышает определенное пороговое значение. Полученный таким образом сигнал можно подавать на входной контакт платы Arduino. Для нашего эксперимента мы возьмем микросхему 74AHCT14 триггера Шмитта. Хотя данная микросхема содержит шесть элементов триггера Шмитта, нам понадобится только один из них. Многие производители выпускают логические микросхемы, которые функционально идентичны серии 7400. На рис. 13.5 показана цоколевка шестиэлементной микросхемы инвертирующих триггеров Шмитта производства компании STMicroelectronics.

Рис. 13.5. Цоколевка шестиэлементной микросхемы инвертирующих триггеров Шмитта (Источник: © STMicroelectronics. Репродукция с разрешения)

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

Рис. 13.6. Схема аппаратной защиты от дребезга с обработкой выходного сигнала инвертирующим триггером Шмитта (Рисунок создан в программе EAGLE)

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

 

 

Рис. 13.7. Сигнал на выходе схемы подавления дребезга с инвертирующим триггером Шмитта

Такой сигнал вполне подходит для формирования аппаратного прерывания.

Монтаж схемы устранения дребезга

Теперь у нас есть достаточно знаний, чтобы собрать схему устранения дребезга кнопки. Для последующих экспериментов по тестированию аппаратной защиты от дребезга и кода обработки прерываний нам нужно будет собрать схему с RGB-светодиодом и кнопкой. Монтажная схема показана на рис. 13.8.

Рис. 13.8. Монтажная схема аппаратной защиты от дребезга (Рисунок создан в программе Fritzing)

Разработка программы

Следующая задача — создать простую программу для тестирования как нашей схемы защиты от дребезга, так и возможностей аппаратных прерываний платформы Arduino. Самым очевидным и полезным применением аппаратных прерываний с платформой Arduino будет получение возможности мониторинга состояния входных сигналов даже при выполнении операций заданной длительности, в которых используется функция delay(). Такая возможность может потребоваться во многих ситуациях, но мы рассмотрим одну из наиболее простых: постепенное изменение яркости светодиода методом широтно-импульсной модуляции посредством функции analogWrite(). В этом скетче яркость одного из элементов RGB-светодиода будет циклически повышаться до максимального значения 255, а затем понижаться до минимального значения 0. При нажатии кнопки будет меняться элемент RGB-светодиода с регулируемой таким образом яркостью. Решить эту задачу методом опроса невозможно, поскольку мы могли бы проверять состояние кнопки только по завершению цикла, в результате чего большинство нажатий кнопки были бы наверняка пропущены.

Но прежде чем приступать к рассмотрению программы для реализации этой задачи, нам нужно разобраться с понятием волатильных (временных) переменных. В частности, переменные, значения которых будут меняться при исполнении процедуры обработки прерывания, должны объявляться как волатильные: volatile. Это необходимо для правильной обработки этих переменных компилятором. Объявление переменной волатильной выполняется добавлением ключевого слова volatile в начале объявления:

volatile int selectedLED = 9;

Чтобы плата Arduino ожидала сигнала прерывания на соответствующем контакте, для этого контакта в разделе setup() нужно посредством функции attachInterrupt() задать режим отслеживания прерываний. В аргументах этой функции указывается ID-номер прерывания в виде функции digitalPinToInterrupt(), функция обработки прерывания, а также режим запуска прерывания (RISING, FALLING и т.п.). В нашей программе кнопка подключена к контакту 2 платы Arduino, которому функция digitalPinToInterrupt() присваивает ID-номер прерывания 0. При запросе прерывания вызывается функция swap(), а сам запуск осуществляется положительным перепадом сигнала прерывания. В результате, функция attachInterrupt() будет иметь такой вид:

attachInterrupt(digitalPinToInterrupt(2), swap, RISING);

Далее нужно создать функцию swap() и добавить ее в программу, полный код которой приведен в листинге 13.1. Это все, что нам необходимо сделать в отношении программной части. Подключив прерывание и создав функцию для его обработки, в остальной части основной программы можно выполнять любые требуемые операции. При запуске прерывания исполнение основной программы приостанавливается и исполняется функция обработки прерывания, по завершению которой возобновляется исполнение основной в точке, в которой оно было прервано. Поскольку прерывания останавливают ход основной программы, функции их обработки следует делать очень короткими и не использовать в них никаких пауз. В действительности, функция delay() даже не будет работать в функции обработки прерывания. Выяснив все это, теперь мы можем написать следующую программу, которая циклически повышает и понижает яркость одного из трех элементов RGB-светодиода и при нажатии кнопки переключается на другой, продолжая с ним делать то же самое.

Листинг 13.1. Программа hw_multitask.ino для демонстрации аппаратных прерываний

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// Управление прерыванием, используя кнопку с аппаратной защитой
// от дребезга
// Контакты Arduino для считывания сигнала с кнопок
const int BTN = 2; // Сигнал кнопки после устранения дребезга
// подаем на контакт 2
const int RED = 11; // Подключаем катод красного светодиода
// к контакту 11
const int GREEN = 10; // Подключаем катод зеленого светодиода
// к контакту 10
const int BLUE = 9; // Подключаем катод синего светодиода
// к контакту 9
// Волатильные переменные могут модифицироваться
// функцией обработки прерывания
volatile int selectedLED = RED;
 
void setup()
{
pinMode(RED, OUTPUT);
pinMode(GREEN, OUTPUT);
pinMode(BLUE, OUTPUT);
// Начинаем исполнение с выключенным RGB-светодиодом
// Сигнал управления инвертирован, поскольку мы управляем катодом
digitalWrite(RED, HIGH);
digitalWrite(BLUE, HIGH);
digitalWrite(GREEN, HIGH);
// Поскольку сигнал на контакте инвертирован,
// ожидаем положительный перепад
attachInterrupt(digitalPinToInterrupt(BTN), swap, RISING);
}
 
void swap()
{
// Выключаем текущий составляющий светодиод. Высокий уровень (HIGH)
// выключает светодиод, поскольку это светодиод с общим анодом
digitalWrite(selectedLED, HIGH);
// Затем включаем другой составляющий светодиод
if (selectedLED == GREEN)
selectedLED = RED;
else if (selectedLED == RED)
selectedLED = BLUE;
else if (selectedLED == BLUE)
selectedLED = GREEN;
}
 
void loop()
{
// Постепенно повышаем яркость светодиода
// Сигнал управления инвертирован, поскольку мы управляем катодом
for (int i=255; i>=0; i--)
{
analogWrite(selectedLED, i);
delay(10);
}
// Постепенно понижаем яркость светодиода
// Сигнал управления инвертирован, поскольку мы управляем катодом
for (int i=0; i<=255; i++)
{
analogWrite(selectedLED, i);
delay(10);
}
delay(1000);
}

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


Примечание

  1. На веб-странице для этой главы (https://www.exploringarduino.com/content2/ch13) можно просмотреть видеоклип, демонстрирующий эту схему и скетч для нее в действии.
  2. А в данном случае даже рекомендуется. См. документацию по функции attachInterrupt() на веб-сайте Arduino https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/.
Опубликовано

Создание собственных библиотек

По материалам книги “Arduino. Большая книга рецептов“, 3-е изд. (авторы Джепсон Брайан, Марголис Майкл, Уэлдин Николас Роберт) (глава 16. Использование, модифицирование и создание библиотек)

Arduino. Большая книга рецептов, 3-издание

ЗАДАЧА

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

РЕШЕНИЕ

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

Большинство библиотек Arduino создаются в виде класса. Те из вас, кто знаком с языком С++ или Java, должны знать, что такое класс. Однако библиотеки можно также создавать и без использования классов, и здесь мы увидим, как это сделать.

Итак, давайте модифицируем скетч решения из разд. 7.1, чтобы включить функцию BlinkLED() в библиотеку.

Схема подключения светодиодов приводится на рис. 7.2, а описание ее работы — в разд. 7.1. Создаваемая нами библиотека будет содержать функцию blinkLED() из этого решения. В листинге 16.5 приводится код скетча blinkLibTest, с помощью которого будет выполняться тестирование созданной библиотеки.

Листинг 16.5. Скетч для тестирования созданной библиотеки

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
* Скетч blinkLibTest
*/
 
#include "blinkLED.h"
const int firstLedPin = 3; // Контакты для подключения светодиодов
const int secondLedPin = 5;
const int thirdLedPin = 6;
 
void setup(){
pinMode(firstLedPin, OUTPUT); // Задаем выходной режим работы для
// контактов светодиодов
pinMode(secondLedPin, OUTPUT);
pinMode(thirdLedPin, OUTPUT);
}
 
void loop(){
// Мигаем каждым светодиодом один раз в 1,000 мс (1 секунду)
blinkLED(firstLedPin, 1000);
blinkLED(secondLedPin, 1000);
blinkLED(thirdLedPin, 1000);
}

Функцию blinkLED() нужно удалить из скетча решения, приведенного в разд. 7.1, и поместить в отдельный файл с названием blinkLED.cpp, как показано в листинге 16.6 (файлы *.cpp рассматриваются более подробно далее — в разд. «Обсуждение работы решения и возможных проблем» этого раздела).

Листинг 16.6. Код библиотеки blinkLED

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* blinkLED.cpp
* Простая библиотека для мигания светодиодом с периодом в миллисекундах
*/
#include "Arduino.h" // Для версий среды Arduino IDE более ранних,
// чем версия 1.0, используйте файл Wprogram.h
#include "blinkLED.h"
// Мигаем светодиодом на этом контакте в течение duration миллисекунд
void blinkLED(int pin, int duration)
{
digitalWrite(pin, HIGH); // Включаем светодиод
delay(duration);
digitalWrite(pin, LOW); // Выключаем светодиод
delay(duration);
}
Большинство библиотек разрабатывается программистами, которые используют для этого свою среду разработки, но их с таким же успехом можно создавать и в любом обычном текстовом редакторе типа Блокнот.

А теперь создайте заголовочный файл blinkLED.h и скопируйте в него следующий код:

1
2
3
4
5
6
/*
* blinkLED.h
* Заголовочный файл для библиотеки BlinkLED
*/
#include "Arduino.h"
void blinkLED(int pin, int duration); // Прототип функции

Обсуждение работы решения и возможных проблем

Наша библиотека будет называться blinkLED и храниться в папке libraries (см. разд. 16.2). Создайте в этой папке вложенную папку blinkLED и переместите в нее файлы blinkLED.h и blinkLED.cpp. Затем создайте в папке blinkLED вложенную папку examples, а в ней — вложенную папку blinkLibTest. Создайте из скетча для тестирования библиотеки (см. листинг 16.5) файл с именем blinkLibTest.ino и поместите его в папку blinkLibTest. Путь к этому файлу должен выглядеть так: examples/blinkLibTest/ blinkLibTest.ino.

В результате мы переместили функцию blinkLED() скетча решения из разд. 7.1 в файл библиотеки blinkLED.cpp (расширением cpp обозначаются файлы с исходным кодом, созданным на языке С++, — С plus plus).

Используемые в документации библиотек Arduino термины функция и метод обозначают блоки кода — такие как blinkLED(). Термин метод служит для обозначения функциональных блоков классов. Оба термина обозначают именованные функциональные блоки, доступ к которым предоставляется посредством библиотек.

Файл blinkLED.cpp содержит исходный код функции blinkLED(), который идентичен коду этой функции в скетче из листинга 7.1, за исключением следующих двух строк в начале кода:

1
2
#include "Arduino.h" // Подключение заголовочного файла Arduino.h
#include "blinkLED.h"

Оператор #include “Arduino.h” требуется для библиотек, использующих любые функции или константы языка Arduino. При отсутствии этого оператора компилятор будет выдавать сообщения об ошибке для всех функций, используемых в скетче.

 

Заголовочный файл Arduino.h был добавлен в версии 1.0 среды Arduino IDE, заменив заголовочный файл WProgram.h, используемый в более ранних версиях среды. При работе с такими более ранними версиями среды Arduino IDE можно использовать следующую конструкцию, чтобы подключать правильный заголовочный файл:

1
2
3
4
5
#if ARDUINO >= 100
#include "Arduino.h // для версий 1.0 и более поздних
#else
#include "WProgram.h" // для более ранних версий
#endif

Следующая строка кода: #include “blinkLED.h” — содержит определение функции (которое также называется прототипом функции) для нашей библиотеки. Компилятор Arduino автоматически создает прототипы для всех функций в скетче при его компилировании, но не создает прототипов для функций, содержащихся в библиотеках, поэтому при создании библиотеки необходимо самому создать заголовочный файл, содержащий эти прототипы. При подключении библиотеки к скетчу в него добавляется именно этот заголовочный файл (см. разд. 16.1).

Каждая библиотека должна иметь файл, в котором объявляются названия предо- ставляемых функций. Такой файл называется заголовочным (header file) или включаемым (include file), а его название имеет формат НазваниеБиблиотеки.h. В рассматриваемом примере заголовочный файл называется blinkLED.h и размещается в той же папке, что и файл blinkLED.cpp.

Содержимое заголовочного файла для нашей библиотеки очень простое — объявление одной функции:

1
void blinkLED(int pin, int duration); // Прототип функции

Это объявление почти такое же, как и объявление этой функции в файле blinkLED.cpp:

1
void blinkLED(int pin, int duration)

Однако разница между этими двумя объявлениями функции хотя и тонкая, но критическая. Объявление прототипа функции в заголовочном файле завершается точкой с запятой. Это сообщает компилятору, что это просто объявление формата для функции, но не ее кода. А отсутствие завершающей точки с запятой в объявлении функции в файле исходного кода blinkLED.cpp сообщает компилятору, что это собственно исходный код функции.

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

Более подробную информацию по использованию заголовочных файлов и файлов *.cpp для создания модулей кода можно получить, прочитав какую-либо хорошую книгу по языку C++ (в разд. «Дополнительная информация» этого раздела рекомендуется несколько таких весьма популярных книг).

Разместив файлы blinkLED.cpp, blinkLED.h и blinkLibTest.ino в соответствующие вложенные папки в папке libraries, закройте среду разработки Arduino IDE, а затем снова запустите ее. Структура задействованных папок и файлов должна выглядеть следующим образом:

libraries/

└── blinkLED/

├── blinkLED.cpp

├── blinkLED.h

└── examples/

└── blinkLibTest/

└── blinkLibTest.ino

Среда Arduino IDE обновляет список доступных библиотек только при ее запуске. Чтобы установленная вручную библиотека отобразилась в этом списке, необходимо закрыть, а затем снова запустить среду Arduino IDE. И хотя среду Arduino IDE нужно перезапустить при исходном добавлении библиотеки в папку libraries, впоследствии — после модифицирования библиотеки — перезапуск не требуется.

Выполнив команду меню Файл | Примеры (Примеры из пользовательских библиотек) | blinkLED | blinkLibTest, откройте в окне Arduino IDE тестовый скетч blinkLibTest. Загрузите этот скетч в свою плату Arduino, и подключенные к ней светодиоды должны начать мигать так же, как и при исполнении скетча решения из разд. 7.1.

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

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

1
2
3
4
5
// Константы для периода мигания светодиодов
const int BLINK_SHORT = 250;
const int BLINK_MEDIUM = 500;
const int BLINK_LONG = 1000;
void blinkLED(int pin, int duration); // Прототип функции

Затем надо модифицировать код в функции loop() скетча примера, как показано в листинге 16.7. Загрузив модифицированный скетч в плату, мы увидим, что каждый светодиод мигает с другой частотой.

Листинг 16.7. Модифицированный код функции loop() для мигания светодиодами с разной частотой

1
2
3
4
5
void loop(){
blinkLED(firstLedPin, <strong>BLINK_SHORT</strong>);
blinkLED(secondLedPin, <strong>BLINK_MEDIUM</strong>);
blinkLED(thirdLedPin, <strong>BLINK_LONG</strong>);
}

Также легко добавляются в библиотеку и новые функции. В листинге 16.8 приводится код главного цикла loop(), который мигает каждым светодиодом заданное для него количество раз.

Листинг 16.8. Код цикла loop() для мигания каждым светодиодом в течение заданного для него периода

1
2
3
4
5
void loop(){
blinkLED(firstLedPin, BLINK_SHORT, 5); // <strong>Мигает 5 раз</strong>
blinkLED(secondLedPin, BLINK_MEDIUM, 3); // <strong>Мигает 3 раза</strong>
blinkLED(thirdLedPin, BLINK_LONG); // <strong>Мигает один раз</strong>
}

Чтобы добавить эту функцию в библиотеку, в файл blinkLED.h нужно добавить ее прототип, как показано в листинге 16.9.

Листинг 16.9. Добавление прототипа новой функции в заголовочный файл

1
2
3
4
5
6
7
8
9
10
11
12
/*
* blinkLED.h
* Заголовочный файл для библиотеки blinkLED
*/
#include "Arduino.h"
// Константы для периода мигания светодиодов
const int BLINK_SHORT = 250;
const int BLINK_MEDIUM = 500;
const int BLINK_LONG = 1000;
void blinkLED(int pin, int duration);
<strong>// Определение новой функции для мигания заданного количества раз</strong>
<strong>void blinkLED(int pin, int duration, int repeats);</strong>

А в файл blinkLED.cpp добавляем исходный код функции, как показано в листинге 16.10.

Листинг 16.10. Исходный код функции мигания светодиодами заданное количество раз

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
* blinkLED.cpp
* Простая библиотека для мигания светодиодом с периодом в миллисекундах
*/
#include "Arduino.h"
#include "blinkLED.h"
// Мигаем светодиодом на этом контакте в течение duration миллисекунд
 
void blinkLED(int pin, int duration)
{
digitalWrite(pin, HIGH); // Включаем светодиод
delay(duration);
digitalWrite(pin, LOW); // Выключаем светодиод
delay(duration);
}
/* Функция для повторения мигания */
void blinkLED(int pin, int duration, int repeats)
{
while(repeats)
{
blinkLED(pin, duration);
repeats = repeats -1;
};
}

Для созданной библиотеки можно добавить возможность выделения цветом ключевых слов при просмотре исходного кода скетча в окне редактора скетчей среды IDE. Для этого нужно создать специальный файл keywords.txt. Это обычный текстовый файл, который содержит список ключевых слов и их типов: каждый тип ключевого слова выделяется другим цветом. Ключевые слова и типы разделяются табуляцией, а не просто пробелом. В листинге 16.11 приводится пример файла keywords.txt, в котором задается выделение цветом констант периода мигания светодиода.

Листинг 16.11. Файл keywords.txt для библиотеки blinkLED

1
2
3
4
5
6
7
8
9
10
11
#######################################
# Methods and Functions (KEYWORD2)
#######################################
blinkLED KEYWORD2
#######################################
# Constants (LITERAL1)
#######################################
 
BLINK_SHORT LITERAL1
BLINK_MEDIUM LITERAL1
BLINK_LONG LITERAL1

Сохраните его в папке blinkLED и перезапустите среду Arduino IDE. Если теперь открыть файл скетча blinkLibTest.ino в среде Arduino IDE, то константы BLINK_SHORT, BLINK_MEDIUM и BLINK_LONG будут выделяться цветом.

Дополнительная информация

Дополнительные примеры создания библиотек рассматриваются в разд. 16.5.

Дополнительная информация по созданию библиотек для Arduino приводится в справочном документе «Writing a Library for Arduino» (https://oreil.ly/vLNvx).

Рекомендуем также следующие книги по программированию на C++:

  • «Practical C++ Programming» («Практическое программирование на языке C++»), автор Steve Oualline, издательство O’Reilly;
  • «C++ Primer Plus» («Язык программирования С++»), автор Stephen Prata, издательство Sams;
  • «C++ Primer» («Учебник C++ для начинающих»), авторы Stanley B. Lippman, Josee Lajoie и Barbara E. Moo, издательство Addison-Wesley Professional.