Многозадачность и прерывания на Arduino

Человек-оркестр

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

Что такое прерывания?

Прерывание — это сигнал, который сообщает процессору, что нужно немедленно остановить то, что он в настоящий момент делает, и произвести некоторые операции, имеющие высокий приоритет. Эта обработка с высоким приоритетом называется обработчиком прерываний (interrupt handler).

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

Прерывания могут быть созданы несколькими источниками:

  • Одним из таймеров Arduino, формируя прерывания по таймеру;
  • Изменением состояния одного из входов внешних прерываний;
  • Изменение состояния одного из группы пинов.

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

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

 

Прерывания по таймеру Arduino

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

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

Arduino имеет три таймера: Timer0, Timer1 и Timer2. Timer0 уже настроен для генерации миллисекундных прерываний, обновляя счетчик миллисекунд, передаваемый в millis ().  Это именно то, что нам нужно!

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

Timer0 являющийся 8-битным, считает от 0 до 255 и генерирует прерывание, при переполнении (превышении значения в 255). Он использует тактовый делитель на 64 по умолчанию, чтобы дать нам частоту прерываний 976.5625 Гц (достаточно близко к 1 кГц для наших целей). Мы не будем изменять частоту в Timer0, потому что это точно нарушит работу millis ()!

 

Регистры сравнения

В регистрах сравнения хранятся данные, которые постоянно сравниваются с состоянием таймера/счетчика. Установим регистр сравнения (OCR0A) для генерации другого прерывания где-то в середине этого счета. Приведенный ниже код будет генерировать прерывание TIMER0_COMPA всякий раз, когда значение счетчика проходит 0xAF.

Затем определим обработчик прерывания для вектора прерывания по таймеру, называемому TIMER0_COMPA_vect. В этом обработчике прерывания будет делаться все то, что мы ранее делали в loop ().

Что оставляет нас с абсолютно пустым loop ()

Здесь мы теперь можем делать все что угодно. Можно даже вернуться к использованию delay ()! Это не затронет наши мигающие светодиоды и вращающиеся сервы. Они будут по-прежнему независимо вызываться один раз в миллисекунду.

Напишем новый код, независимо управляющий миганием светодиодов и вращением сервоприводов, но уже используя таймер.

Показать/скрыть код

 

Внешние прерывания

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

На используемой мной Arduino Mega 2560 имеется 6 пинов, обрабатывающих внешние прерывания (на Arduino UNO таких входов только 2). В этом примере я подключу кнопку к одному из них и использую ее для сброса сервоприводов.

Во-первых, давайте добавим функцию reset () в наш класс Sweeper.  Функция reset () устанавливает положение в ноль и немедленно перемещает туда серву.

Затем, добавим вызов функции attachInterrupt (), соединяющую внешнее прерывание с обработчиком. На Arduino Mega 2560, также как и на Arduino UNO, Inerrupt 0 ассоциировано с цифровым пином 2. Мы говорим, что ждем «спада» фронта сигнала на этом выводе. При нажатии кнопки сигнал «спадает» от высокого до низкого уровня и вызывается обработчик прерывания Reset.

И здесь есть обработчик прерывания. Он просто вызывает функции, сбрасывающие сервы.

Теперь, при нажатии на кнопку, серва останавливает вращение и немедленно возвращается в нулевую позицию.

Схема у нас та же, что и в прошлый раз.

multitasking_scheme04

Полный скетч с таймерами и внешними прерываниями:

Показать/скрыть код

И результат

Библиотеки для работы с прерываниями

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

Отличные библиотеки TimerOne и TimerThree от Paul Stoffregan дают множество низкоуровневых возможностей для конфигурирования прерываний по таймеру. Библиотека TimerThree не работает на Arduino UNO.  Ее можно использовать с Leonardo, Mega 2560 и некоторыми платами Teensy.

Arduino UNO имеет только два входа внешних прерываний. Но что делать если требуется более двух входов? К счастью Arduino UNO поддерживает прерывания «pin-change» (по изменению входа) для всех пинов.

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

Библиотека PinChangeInt реализует удобный интерфейс для прерываний по изменению входа.

 

Правила работы с прерываниями

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

Если все имеет высокий приоритет, значит высокого приоритета нет ни у чего

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

Одно прерывание в каждый момент времени

Когда программа находится в функции обработки прерывания, то другие прерывания отключены. Это имеет два важных следствия:

  • Работа, выполняемая в функции обработки прерывания должна быть короткой, чтобы не пропустить не одного прерывания.
  • Код, выполняемый в функции обработки прерывания не должен вызывать ничего такого, что требовало бы, чтобы прерывания были активны (например, delay () , или что-нибудь, использующее шину I2C). Это приведет к зависанию программы.

Отложите длительную обработку в loop ()

Если вам необходимо произвести обширную обработку в ответ на прерывание, используйте обработчик прерывания для того, чтобы сделать то, что необходимо, а затем установите переменную состояния volatile, показывающую, что дальнейшая обработка не требуется. При вызове функции Update () из loop () проверьте переменную состояния, на предмет того, не требуется ли какая-либо последующая обработка.

Проверка перед переконфигурированием таймера

Таймеры являются ограниченным ресурсом. На Arduino UNO их всего 3, и они используются для многих вещей. Если вы запутались с конфигурацией таймера, некоторые вещи могут перестать работать. Например, на Arduino UNO:

  • Timer0 - используется millis (), micros (), delay () и ШИМ на пинах 5 и 6
  • Timer1 - используется для Servo, библиотеки WaveHC и ШИМ на пинах 9 и 10
  • Timer2 - используется Tone и ШИМ на пинах 11 и 13

Безопасное совместное использование данных

Поскольку прерывание приостановит все, что процессор делает, мы должны побеспокоиться об обмене данными между обработчиками прерываний и кодом в loop ().

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

Даже пометка переменной как volatile будет не достаточным в случае, если переменная больше, чем целое (например, строка, массив, структура и т.п.). Большим переменным иребуется несколько циклов инструкций для обновления, и если прерывание происходит в середине этого обновления, данные могут быть повреждены. Если у вас есть большие переменные или структуры, которые совместно используются с обработчиком прерываний, вы должны отключить прерывания при обновлении их из loop (). Замечу, что прерывания отключены в обработчике прерывания уже по умолчанию.

 
Как вы оцениваете эту публикацию? 1 звезда2 звезды3 звезды4 звезды5 звезд (44 голосов, средняя оценка: 4.84 из 5)
Loading ... Loading ...

Вы можете пропустить чтение записи и оставить комментарий. Размещение ссылок запрещено.

15 комментариев к записи “Многозадачность и прерывания на Arduino”

  1. Валерий пишет:

    Очень познавательная статья.

    Есть вопрос: Как будет работать программа когда переполнится и обнулится millis ()?

    • Хороший вы задали вопрос, Валерий! Примерно через 50 дней программа работать перестанет: счетчик millis () обнулится и условия if ((currentMillis — lastUpdate) > updateInterval), (currentMillis — previousMillis >= OnTime) и (currentMillis — previousMillis >= OffTime) возможно никогда больше и не выполнятся. Чтобы этого избежать при условии, что не сильно критичен единичный сбой раз в 50 дней в длительности обновления состояний, то можно сравнивать не разность на превышение заданных интервалов, а модуль разностей. Например, для первого условия это будет выглядеть следующим образом:

      Если же даже сбой один раз в 50 дней в работе программы неприемлем, то необходимо отдельно обработать событие переполнения счетчика, учитывая, что переменная типа unsigned long может принимать значения от 0 до (2^32-1).

      • Валерий пишет:

        Спасибо за ответ.

      • Николай пишет:

        Добрый день всем.

        Я не волшебник я только учусь.

        (железным програмированием никогда не занимался — а обычное в школе институте проходили)

        вы написали ...

        как одно из решений

        if (abs (currentMillis — lastUpdate) > updateInterval)

        {

        ...

        }

        то есть, если я правильно понял — один раз в 50 дней доходим до максимального значения МИЛИСЕК-мах -потом сбрасывание в ноль будет «пропуск» одного интервала? ( если я правильно понял).

        а что если написать

        long interval = 1000;

        .

        .

        if (millis () % interval == 0 )

        {

        ...

        }

        все будет работать также ??

        • Николай, проблема тут вот в чем. Ход мыслей у вас правильный, но операция взятия отстатка от деления работает только с числами типа int (оба операнда должны быть типа int). Функция millis возвращает значение типа unsigned long. В комментарии к другой статейке, также посвященной многозадачности, я предложил одно из возможных вариантов отслеживания сброса счетчика.

          • Николай пишет:

            Спасибо

          • Виктор пишет:

            Странно. У меня отлично работает операция взятия остатка (%), в случае, когда первый операнд типа unsigned long, а второй константа.

            Например:

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

  2. droncs46 пишет:

    Есть такая функция

    Как её переделать для Arduino DUE? Там ведь таймеры 32-бит...

    • Для Arduino DUE работа с таймерами и прерываниями значительно отличается от описанного выше и выходит за рамки этой статьи. Могу порекомендовать вам ознакомится с осуждением работы с таймерами Arduino DUE на форуме arduino.cc. Пример кода взятый оттуда:

  3. Виталий пишет:

    Здравствуйте! Подскажите пожалуйста почему неадекватно работает функция analogRead () во внешнем прерывании int0?

  4. Andrews пишет:

    особенно полезно последние предупреждения :)

    я почти что решил перейти на прерывания вместо отслеживания через millis ()

  5. heX пишет:

    > Помните, что прерывания отключены, пока вы находитесь в обработчике прерывания. Если вы попытаетесь сделать слишком много на уровне прерывания, вы получите худший ответ на другие прерывания.

    Это ошибочное утверждение.

    Позволю процитировать уважаемого DI HALT:

    что будет если во время обработки одного прерывания придет другое? Оно потеряется?

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

    (easyelectronics.ru/avr-uc... -si-chast-3.html)

  6. RMAU пишет:

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

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

Оставить комментарий