Урок 10. Прерывание по таймеру в Ардуино. Библиотека MsTimer2. Параллельные процессы.

Arduino UNO R3

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

Предыдущий урок     Список уроков     Следующий урок

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

Операция

Время цикла
Опрашивает 3 кнопки, обрабатывает сигналы с них для устранения дребезга 2 мс
Регенерирует данные семисегментных светодиодных индикаторов 2 мс
Вырабатывает сигналы управления для 2 датчиков температуры DS18B20 и считывает данные с них. Датчики имеют последовательный интерфейс  1-wire. 100 мкс для каждого бита,
1 сек общий цикл чтения
Чтение аналоговых значений тока и напряжения на элементе Пельтье, напряжения питания 100 мкс
Цифровая фильтрация аналоговых значений тока и напряжения 10 мс
Вычисление мощности на элементе Пельтье 10 мс
ПИД (пропорционально интегрально дифференциальный) регулятор стабилизации тока и напряжения 100 мкс
Регулятор мощности 10 мс
Регулятор температуры 1 сек
Защитные функции, контроль целостности данных 1 сек
Управление, общая логика работы системы 10 мс

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

 

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

В предыдущих уроках мы создали класс для объекта кнопка. Мы сказали, что это класс для обработки сигнала в параллельном процессе. Что для его нормальной работы необходимо вызывать функцию (метод) обработки сигнала в цикле с регулярным периодом (мы выбрали время 2 мс). И тогда в любом месте программы доступны признаки, показывающие текущее  состояние кнопки или сигнала.

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

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

Аппаратное прерывание от таймера.

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

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

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

Установка режима и времени периода таймера Ардуино производится через аппаратные регистры микроконтроллера. При желании можете разобраться, как это делается. Но я  предлагаю более простой вариант – использование библиотеки MsTimer2. Тем более, что установка режима таймера происходит редко, а значит, использование библиотечных функций не приведет к замедлению работы программы.

 

Библиотека MsTimer2.

Библиотека предназначена для конфигурирования аппаратного прерывания от Таймера 2 микроконтроллера. Она имеет всего три функции:

  • MsTimer2::set(unsigned long ms, void (*f)())

Эта функция устанавливает время периода прерывания в мс. С таким периодом будет вызываться обработчик прерывания f. Он должен быть объявлен как void (не возвращает ничего) и не иметь аргументов. * f – это указатель на функцию. Вместо него надо написать имя функции.

  • MsTimer2::start()

Функция разрешает прерывания от таймера.

  • MsTimer2::stop()

Функция запрещает прерывания от таймера.

Перед именем функций надо писать MsTimer2::, т.к. библиотека написана с использованием директивы пространства имен namespace.

Для установки библиотеки скопируйте каталог MsTimer2 в папку libraries в рабочей папке Arduino IDE. За тем запустите программу Arduino IDE, откройте Скетч -> Подключить библиотеку и посмотрите, что в списке библиотек присутствует библиотека MsTimer2.

Загрузить библиотеку MsTimer2 в zip-архиве можно здесь. Для установки его надо распаковать.

Простая программа с параллельной обработкой сигнала кнопки.

Теперь напишем простую программу с одной кнопкой и светодиодом из урока 6. К плате Ардуино подключена одна кнопка по схеме:Схема подключения

Выглядит это так:

Подключение к плате

На каждое нажатие кнопки светодиод на плате Ардуино меняет свое состояние. Необходимо чтобы были установлены библиотеки Button и MsTimer2.

// sketch_10_1 урока 10
// Нажатие на кнопку меняет состояние светодиода

#include <MsTimer2.h>
#include <Button.h>

#define LED_1_PIN 13     // светодиод подключен к выводу 13
#define BUTTON_1_PIN 12  // кнопка подключена к выводу 12

Button button1(BUTTON_1_PIN, 15);  // создание объекта - кнопка

void setup() {
  pinMode(LED_1_PIN, OUTPUT);      // определяем вывод светодиода как выход
  MsTimer2::set(2, timerInterupt); // задаем период прерывания по таймеру 2 мс
  MsTimer2::start();              // разрешаем прерывание по таймеру
}

void loop() {

// управление светодиодом
  if ( button1.flagClick == true ) {
    // был клик кнопки
    button1.flagClick= false;         // сброс признака
    digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN));  // инверсия состояния светодиода
   
  }   
}

// обработчик прерывания
void  timerInterupt() {
  button1.scanState();  // вызов метода ожидания стабильного состояния для кнопки 
}

В функции setup() задаем время цикла прерывания по таймеру 2 мс и указываем имя обработчика прерывания timerInterrupt. Функция обработки сигнала кнопки button1.scanState() вызывается в обработчике прерывания таймера каждые 2 мс.

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

 

Квалификатор volatile.

Давайте изменим цикл loop() в предыдущей программе.

void loop() {

  while(true) {
    if ( button1.flagClick == true ) break;   
  }

    // был клик кнопки
    button1.flagClick= false;         // сброс признака
    digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN));  // инверсия светодиода       
}

Логически ничего не поменялось.

  • В первом варианте программа проходила цикл loop до конца и в нем анализировала флаг button1.flagClick.
  • Во втором варианте программа анализирует флаг  button1.flagClick в бесконечном цикле while. Когда флаг становится активным, то выходит из цикла while по break и инвертирует состояние светодиода.

Разница только в том, в каком цикле крутится программа в loop или в while.

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

#include <MsTimer2.h>
#define LED_1_PIN 13     // светодиод подключен к выводу 13
int count=0;

void setup() {
  pinMode(LED_1_PIN, OUTPUT);      // определяем вывод светодиода как выход
  MsTimer2::set(500, timerInterupt); // задаем период прерывания по таймеру 500 мс
  MsTimer2::start();              // разрешаем прерывание по таймеру
}

void loop() {

  while (true) {
    if ( count != 0 ) break;
  }

  count= 0; 
  digitalWrite(LED_1_PIN, ! digitalRead(LED_1_PIN));  // инверсия состояния светодиода       
}

// обработчик прерывания
void  timerInterupt() {
  count++; 
}

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

Дело в том, что компилятор языка C++ по мере своего интеллекта оптимизирует программу. Иногда это не идет на пользу. Компилятор видит, что в цикле while никакие операции с переменной count не производятся. Поэтому он считает, что достаточно проверить состояние count только один раз. Зачем в цикле проверять, то, что никогда не может измениться. Компилятор  корректирует код, оптимизируя его по времени исполнения. Проще говоря убирает из цикла код проверки переменной.  Понять, что переменная count меняет свое состояние в обработчике прерывания, компилятор не может. В результате мы зависаем в цикле while.

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

Если, например, добавить в цикл while вызов функции delay(), то программа заработает.

  while (true) {
    if ( count != 0 ) break;
    delay(1);
  }

Хороший стиль – разрабатывать программы, в которых цикл loop выполняется до конца и программа нигде не подвисает. В следующем уроке будет единственный код с анализом флагов в бесконечных циклах while. Дальше я планирую во всех программах выполнять loop до конца.

Иногда это сделать не просто или не так эффективно. Тогда надо использовать квалификатор volatile. Он указывается при объявлении переменной и сообщает компилятору, что не надо пытаться оптимизировать ее использование. Он запрещает компилятору делать предположения по поводу значения переменной, так как переменная может быть изменена в другом программном блоке, например, в параллельном процессе. Также компилятор размещает переменную в ОЗУ, а не в регистрах общего назначения.

Достаточно в программе при объявлении count написать

volatile int count=0;

и все варианты будут работать.

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

volatile Button button1(BUTTON_1_PIN, 15);  // создание объекта - кнопка

По моим наблюдениям применение квалификатора  volatile никак не увеличивает длину кода программы.

 

Сравнение метода обработки сигнала кнопки с библиотекой Bounce.

Существует готовая библиотека для устранения дребезга кнопок Bounce. Проверка состояния кнопки происходит при вызове функции update(). В этой функции:

  • считывается сигнал кнопки;
  • сравнивается с состоянием во время предыдущего вызова update();
  • проверяется, сколько прошло времени с предыдущего вызова с помощью функции millis();
  • принимается решение о том, изменилось ли состояние кнопки.

Далее надо еще считать состояние кнопки функцией read().

  • Но это не параллельная обработка сигнала. Функцию update() обычно вызывают в основном, асинхронном цикле программы. Если ее не вызывать дольше определенного времени, то информация о сигнале кнопки будет потеряна. Не регулярные вызовы приводят к не правильной работе алгоритма.
  • Сама функция имеет достаточно большой код и выполняется намного дольше функций библиотеки Button (уроки 7, 8, 9).
  • Цифровой фильтрации сигналов по среднему значению там вообще нет.

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

 

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

Предыдущий урок     Список уроков     Следующий урок

15 комментариев на «Урок 10. Прерывание по таймеру в Ардуино. Библиотека MsTimer2. Параллельные процессы.»

    • Создать 3 объекта Button, для каждого вызывать метод scanState() в прерывании и сделать в цикле loop() 3 проверки для каждой кнопки и светодиода.

  1. Правильно ли прерывание по таймеру называть параллельным процессом?
    Ведь loop() во время выполнения timerInterupt() прерывается?

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

  2. А как насчет использования функции millis(), что-то типа:
    TS=millis();
    if ((TS-TSo) >= 1000)
    {
    TSo=TS;

    }
    пробовал…, вроде бы работает…

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

  3. Такая ситуация…
    В Ардуину передаю строку (например дату-время) периодически (каждую секунду)…
    В случае аппаратного прерывания строка «порвется»?
    Если да, то как с этим бороться?

    • Что значит строка порвется?
      Если имеется в виду передача по последовательному порту, то это происходит так. Строка функцией класса Serial загружается в буфер. От туда по прерыванию аппаратного последовательного порта после передачи каждого байта загружается новый байт. На загрузку нового байта есть время передачи предыдущего байта. Даже если произойдет задержка, то ничего страшного не будет. Передача или прием это тоже параллельные процессы, реализованные встроенным классом Serial.

  4. Когда-то давно изучал бейсик и 386 комп был чудом техники, Теперь жизнь заставляет учить все с нуля. Спасибо очень помогают Ваши уроки, но никак не могу понять для чего нужны классы? Объем кода меньше не становиться.

  5. А если мне надо одновременно контролировать несколько кнопок, причем контролировать длительность из нажатия.
    К примеру, короткое нажатие- считываю соответствующее кнопке значение ПЗУ и выполняю какое-то действие, длинное- считываю усредненное значение АЦП, и очень длинное- какое-то другое значение, пусть будет какая-нибудь калибровка. Тут как быть? случай без dalay, т.е. с прерываниями. В прерывании опрашивать кнопки или считать тики? я тут не зря упоминаю усредненное значение АЦП. )))) Старый стал))) Не соображу))

    • Много вариантов решения. Например, в уроке 42 в программе контроллера элемента Пельтье при удержании кнопок «+» или «-» значение параметра меняется автоматически. Посмотрите как там реализовано.

  6. а если так:
    кнопки на пин1, пин2, пин3, пин4
    входы подтянуты к плюсу
    примем А=!пин1*8, В=!пин2*4, С=!пин3*2, D=!пин4,
    ПОРТ=A+B+C+D
    Это, типа порт с маской
    Запускаем «тикалку»

    Введем некую переменную ПортХ

    если ПортХ=ПОРТ && ПортХ !=0, то считаем тики ТИК++
    если ПортХ !=ПОРТ && ПортХ !=0, тогда

    смотрим, сколько у нас натикало
    если ТИК<10 считаем, что дребезжит и сбрасываем счетчик

    если ТИК <20, сбрасываем счетчик тикалки, считаем, что на кнопку наступили и делаем, что-то первое

    если ТИК<40, сбрасываем счетчик тикалки, считаем, что на кнопку наступили и делаем, что-то второе

    если ТИК<80, сбрасываем счетчик тикалки, считаем, что на кнопку конкретно наступили и делаем, что-то третье

    если натикало больше 80, считаем, что кот спит на клаве, сбрасываем счетчик тикалки и ничего не делаем, вернее, всё остальное по порядку

    В прерывании по тикалке читаем состояние ПОРТ

    Так пойдет? Тут получается, что мы можем отслеживать несколько длительностей (тут отслеживаем 5 состояний нажатий кнопок
    1- кнопки никому не нужны и давить их никто не собирался, ну, может, импульсы вокруг летают, или кот проскочил, наступив на клаву, до 10 тиков

    2- нормально наступили, ненадолго, от 10 до 20 тиков,

    3- конкретно надавили, чуток подержали и отпустили от 20 до 40 тиков

    4- давили от души, но вовремя одумались от 40 до 80 тиков

    5- больше 80- либо тарелку с салатом и клаву перепутали, либо кот-подлец нашел себе новую лёжку
    Вроде, должно работать
    Число тиков, конечно, условно

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

  8. Не совсем понимаю… А можно ли сделать так (кнопки не нужны мне), чтобы три управляющих выхода на плате мигали светодиодами с разной задержкой, но одновременно? То есть пин1 = 100 мс, пин2 = 500 мс, пин3 = 1000мс. А то они работают друг за другом…

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

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