Таймер на arduino

Многозадачная Ардуина: таймеры без боли

Не каждый ардуинщик знает о том, что помимо стартового кода в setup и бесконечного цикла в loop, в прошивку робота можно добавлять такие кусочки кода, которые будут останавливать ход основного цикла в строго определенное заранее запланированное время, выполнять свои дела, затем аккуратно передавать управление в основную программу так, что она вообще ничего не заметит. Такая возможность обеспечена механизмом прерываний по таймеру (обычное дело для любого микроконтроллера), с её помощью в прошивку можно вносить элементы реального времени и многозадачности.
Еще меньше используют такую возможность на практике, т.к. в стандартном не слишком богатом API Arduino она не предусмотрена. И, хотя, доступ ко всем богатствам внутренних возможностей микроконтроллера лежит на расстоянии вытянутой руки через подключение одного-двух системных заголовочных файлов, не каждый пожелает добавить в свой аккуратный маленький скетч пару-тройку экранов довольно специфического настроечного кода (попутно потеряв с ним остатки переносимости между разными платами). Совсем единицы (тем более, среди аудитории Ардуино) решатся и смогут в нем разобраться.
Сегодня я избавлю вас от страданий.
и расскажу, как получить настоящие многозадачность и реальное время в прошивке вашего ардуино-робота, добавив в неё ровно 3 строчки кода (включая #include в шапке). Обещаю, что у вас всё получится, даже если вы только что в первый раз запустили Blink.

Начнем сразу с кода

arduino-timer-api/examples/timer-api/timer-api.ino
Подключаем библиотеку timer-api.h (раз)
#include «timer-api.h»
Запускаем таймер с нужной частотой с timer_init_ISR_XYHz: здесь XYHz=1Hz — 1 Герц — один вызов прерывания в секунду (два)
void setup() { Serial.begin(9600); // частота=1Гц, период=1с timer_init_ISR_1Hz(TIMER_DEFAULT); pinMode(13, OUTPUT); }
(ISR — interrupt service routine, процедура-обработчик прерывания)
Добавляем в главный цикл loop любую блокирующую или неблокирующую ерунду: печатаем сообщение, ждём 5 секунд (здесь всё, как обычно, поэтому не считаем)
void loop() { Serial.println(«Hello from loop!»); delay(5000); // здесь любой код: блокирующий или неблокирующий }
Процедура, вызываемая прерыванием по событию таймера с заданным периодом, — реализация для функции с именем timer_handle_interrupts: печатаем сообщение, мигаем лампочкой (три)
void timer_handle_interrupts(int timer) { Serial.println(«goodbye from timer»); // мигаем лампочкой digitalWrite(13, !digitalRead(13)); }
То же самое, только добавим замер времени между двумя вызовами для наглядности и отладки:
void timer_handle_interrupts(int timer) { static unsigned long prev_time = 0; unsigned long _time = micros(); unsigned long _period = _time — prev_time; prev_time = _time; Serial.print(«goodbye from timer: «); Serial.println(_period, DEC); // мигаем лампочкой digitalWrite(13, !digitalRead(13)); }
Шьем плату, открываем Инструменты > Монитор порта, наблюдаем результат:

Как видим, обработчик timer_handle_interrupts печатает сообщение каждые 1000000 (1 миллион) микросекунд, т.е. ровно раз в секунду. И (о чудо!) постоянная блокирующая задержка на 5 секунд delay(5000) в главном цикле никаким образом ему в этом действии не мешает.

Вот вам реальное время и многозадачность в одном скетче в 3 строчки, я обещал.

Варианты частот для timer_init_ISR_XYHz

//timer_init_ISR_500KHz(TIMER_DEFAULT); //timer_init_ISR_200KHz(TIMER_DEFAULT); //timer_init_ISR_100KHz(TIMER_DEFAULT); //timer_init_ISR_50KHz(TIMER_DEFAULT); //timer_init_ISR_20KHz(TIMER_DEFAULT); //timer_init_ISR_10KHz(TIMER_DEFAULT); //timer_init_ISR_5KHz(TIMER_DEFAULT); //timer_init_ISR_2KHz(TIMER_DEFAULT); //timer_init_ISR_1KHz(TIMER_DEFAULT); //timer_init_ISR_500Hz(TIMER_DEFAULT); //timer_init_ISR_200Hz(TIMER_DEFAULT); //timer_init_ISR_100Hz(TIMER_DEFAULT); //timer_init_ISR_50Hz(TIMER_DEFAULT); //timer_init_ISR_20Hz(TIMER_DEFAULT); //timer_init_ISR_10Hz(TIMER_DEFAULT); //timer_init_ISR_5Hz(TIMER_DEFAULT); //timer_init_ISR_2Hz(TIMER_DEFAULT); //timer_init_ISR_1Hz(TIMER_DEFAULT);
(вызов timer_init_ISR_1MHz тоже есть, но он не даёт рабочий результат ни на одном из тестовых контроллеров)
Код прерывания, очевидно, должен выполняться достаточно быстро для того, чтобы успеть завершиться до следующего вызова прерывания и, желательно, еще оставить немного процессорного времени для выполнения главного цикла.
Полагаю, излишне пояснять, что чем выше частота таймера, тем меньше период вызова прерываний, тем быстрее должен выполняться код обработчика. Я бы не рекомендовал помещать в него вызовы блокирующих задержек delay, циклы с неизвестным заранее количеством итераций, любые другие вызовы с плохо предсказуемым временем выполнения (в том числе Serial.print).

Суммирование периодов (деление частоты)

В том случае, если стандартные частоты из предложенных на выбор вас не устраивают, можно ввести в код прерывания дополнительный счетчик, который будет выполнять полезный код только после определенного количества пропущенных вызовов. Целевой период будет равен сумме пропускаемых базовых периодов. Или можно сделать его вообще переменным.
arduino-timer-api/examples/timer-api-counter/timer-api-counter.ino
#include»timer-api.h» void setup() { Serial.begin(9600); while(!Serial); // частота=10Гц, период=100мс timer_init_ISR_10Hz(TIMER_DEFAULT); pinMode(13, OUTPUT); } void loop() { Serial.println(«Hello from loop!»); delay(6000); // здесь любой код: блокирующий или неблокирующий } void timer_handle_interrupts(int timer) { static unsigned long prev_time = 0; // дополнильный множитель периода static int count = 11; // Печатаем сообщение на каждый 12й вызов прерывания: // если базовая частота 10Гц и базовый период 100мс, // то сообщение будет печататься каждые 100мс*12=1200мс // (5 раз за 6 секунд) if(count == 0) { unsigned long _time = micros(); unsigned long _period = _time — prev_time; prev_time = _time; Serial.print(«goodbye from timer: «); Serial.println(_period, DEC); // мигаем лампочкой digitalWrite(13, !digitalRead(13)); // взводим счетчик count = 11; } else { count—; } }

Ну и, напоследок,

Вращение шаговым мотором через интерфейс step-dir:
— в фоне по таймеру генерируем постоянный прямоугольный сигнал для шага по фронту HIGH->LOW на ножке STEP
— в главном цикле принимаем от пользователя команды для выбора направления вращения (ножка DIR) или остановки мотора (ножка EN) через последовательный порт
arduino-timer-api/examples/timer-api-stepper/timer-api-stepper.ino
#include»timer-api.h» // Вращение шаговым моторов в фоновом режиме // Pinout for CNC-shield // http://blog.protoneer.co.nz/arduino-cnc-shield/ // X #define STEP_PIN 2 #define DIR_PIN 5 #define EN_PIN 8 // Y //#define STEP_PIN 3 //#define DIR_PIN 6 //#define EN_PIN 8 // Z //#define STEP_PIN 4 //#define DIR_PIN 7 //#define EN_PIN 8 void setup() { Serial.begin(9600); // step-dir motor driver pins // пины драйвера мотора step-dir pinMode(STEP_PIN, OUTPUT); pinMode(DIR_PIN, OUTPUT); pinMode(EN_PIN, OUTPUT); // Будем вращать мотор с максимальной скоростью, // для разных настроек делителя шага оптимальная // частота таймера будет разная. // Оптимальные варианты задержки между шагами // для разных делителей: // https://github.com/sadr0b0t/stepper_h // 1/1: 1500 мкс // 1/2: 650 мкс // 1/4: 330 мкс // 1/8: 180 мкс // 1/16: 80 мкс // 1/32: 40 мкс // Делилель шага 1/1 // частота=500Гц, период=2мс //timer_init_ISR_500Hz(TIMER_DEFAULT); // помедленнее timer_init_ISR_200Hz(TIMER_DEFAULT); // Делилель шага 1/2 // частота=1КГц, период=1мс //timer_init_ISR_1KHz(TIMER_DEFAULT); // помедленнее //timer_init_ISR_500Hz(TIMER_DEFAULT); // Делилель шага 1/4 // частота=2КГц, период=500мкс //timer_init_ISR_2KHz(TIMER_DEFAULT); // помедленнее //timer_init_ISR_1KHz(TIMER_DEFAULT); // Делилель шага 1/8 // частота=5КГц, период=200мкс //timer_init_ISR_5KHz(TIMER_DEFAULT); // помедленнее //timer_init_ISR_2KHz(TIMER_DEFAULT); // Делилель шага 1/16 // частота=10КГц, период=100мкс //timer_init_ISR_10KHz(TIMER_DEFAULT); // помедленнее //timer_init_ISR_5KHz(TIMER_DEFAULT); // Делилель шага 1/32 // частота=20КГц, период=50мкс //timer_init_ISR_20KHz(TIMER_DEFAULT); // помедленнее //timer_init_ISR_10KHz(TIMER_DEFAULT); ///////// // выключим мотор на старте // EN=HIGH to disable digitalWrite(EN_PIN, HIGH); // просим ввести направление с клавиатуры Serial.println(«Choose direction: ‘<‘ ‘>’, space or ‘s’ to stop»); } void loop() { if(Serial.available() > 0) { // читаем команду из последовательного порта: int inByte = Serial.read(); if(inByte == ‘<‘ || inByte == ‘,’) { Serial.println(«go back»); // назад digitalWrite(DIR_PIN, HIGH); // EN=LOW to enable digitalWrite(EN_PIN, LOW); } else if(inByte == ‘>’ || inByte == ‘.’) { Serial.println(«go forth»); // вперед digitalWrite(DIR_PIN, LOW); // EN=LOW to enable digitalWrite(EN_PIN, LOW); } else if(inByte == ‘ ‘ || inByte == ‘s’) { Serial.println(«stop»); // стоп // EN=HIGH to disable digitalWrite(EN_PIN, HIGH); } else { Serial.println(«press ‘<‘ or ‘>’ to choose direction, space or ‘s’ to stop,»); } } delay(100); } void timer_handle_interrupts(int timer) { // шаг на фронте HIGH->LOW digitalWrite(STEP_PIN, HIGH); delayMicroseconds(1); digitalWrite(STEP_PIN, LOW); }

Arduino.ru

Перевод, оригинальная статья Arduino Interrupts

Часто при работе с проектами на микроконтроллерах требуется запускать фоновую функцию через равные промежутки времени. Это часто реализуется установкой аппаратного таймера для выработки прерывания. Это прерывание запускает программу обработки прерываний (Interrupt Service Routine, ISR) для управления периодическим прерыванием. В настоящей статье я описываю установку 8-битного таймера 2 для выработки прерываний на микроконтроллере ATMega168 Arduino. Я пройдусь по этапам, требуемым для установки программы обработки прерываний и внутри нее самой.

Arduino подразумевает процессор ATMega168 (). Этот микроконтроллер имеет несколько систем ввода-вывода, которые доступны каждому пользователю Arduino, поскольку библиотека Arduino облегчает их использование. К примеру, цифровой ввод-вывод, ШИМ, аналого-цифровые входы и последовательный порт. ATMega168 также имеет три внутренних аппаратных таймера. Хотя библиотека Arduino позволяет использовать некоторые свойства таймеров, нельзя напрямую использовать таймер для выработки периодических прерываний.

Прерывания?

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

• В Википедии есть хороший обзор о прерываниях.

При поступлении нового символа UART генерирует прерывание. Микроконтроллер останавливает выполнение основной программы (вашего приложения) и перескакивает на программу обработки прерываний (ISR), предназначенную для данного прерывания. В данном случае это прерывание по полученному символу. Эта ISR захватывает новый символ из UART, помещает в буфер, затем очищает прерывание и выполняет возврат. Когда ISR выполняет возврат, микроконтроллер возвращается в основную программу и продолжает её с точки вызова. Все это происходит в фоновом режиме и не влияет напрямую на основной код вашего приложения.

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

Вы можете задуматься, почему бы не просто проверять новый символ время от времени, вместо использования такого сложного процесса прерывания. Давайте вычислим пример, чтобы увидеть, насколько важны процессы прерывания. Скажем, у вас есть последовательный порт со скоростью передачи данных 9600 бод. Это означает, что каждый бит символа посылается с частотой 9600 Гц или около 10 кГц. На каждый бит уходит 100 мкс. Около 10 бит требуется, чтобы послать один символ, так что мы получаем один полный символ каждую миллисекунду или около того. Если наш UART буферизован, мы должны извлечь последний символ до завершения приема следующего, это дает нам на всю работу 1 мс. Если наш UART не буферизован, мы должны избавиться от символа за 1 бит или 1 мкс. Рассмотрим для начала буферизованный пример.

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

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

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

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

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

Я опишу в ISR в общем только то, что касается прерываний таймера 2. Более подробно об об использовании прерываний в процессорах AVR можно прочитать в руководстве пользователя avr-libc(англ). На данном этапе не требуется полного понимания, но, в конечном счете, вы можете захотеть получить возможность ускорить использование прерываний, раз это важный инструмент для приложений на микроконтроллерах.

Продолжение часть 2

Прерывания – очень важный механизм Arduino, позволяющий внешним устройствам взаимодействовать с контроллером при возникновении разных событий. Установив обработчик аппаратных прерываний в скетче, мы сможем реагировать на включение или выключение кнопки, нажатие клавиатуры, мышки, тики таймера RTC, получение новых данных по UART, I2C или SPI. В этой статье мы узнаем, как работают прерывания на платах Ардуино Uno, Mega или Nano и приведем пример использования функции Arduino attachInterrupt().

Прерывания в Ардуино

Прерывание – это сигнал, который сообщает процессору о наступлении какого-либо события, которое требует незамедлительного внимания. Процессор должен отреагировать на этот сигнал, прервав выполнение текущих инструкций и передав управление обработчику прерывания (ISR, Interrupt Service Routine). Обработчик – это обычная функция, которую мы пишем сами и помещаем туда тот код, который должен отреагировать на событие.

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

Аппаратные и программные прерывания

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

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

Зачем нужны аппаратные прерывания

Аппаратные прерывания возникают в ответ на внешнее событие и исходят от внешнего аппаратного устройства. В Ардуино представлены 4 типа аппаратных прерываний. Все они различаются сигналом на контакте прерывания:

  • Контакт притянут к земле. Обработчик прерывания исполняется до тех пор, пока на пине прерывания будет сигнал LOW.
  • Изменение сигнала на контакте. В таком случае Ардуино выполняет обработчик прерывания, когда на пине прерывания происходит изменение сигнала.
  • Изменение сигнала от LOW к HIGH на контакте – при изменении с низкого сигнала на высокий будет исполняться обработчик прерывания.
  • Изменение сигнала от HIGH к LOW на контакте – при изменении с высокого сигнала на низкий будет исполняться обработчик прерывания.

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

Основными причинами, по которым необходимо вызвать прерывание, являются:

  • Определение изменения состояния вывода;
  • Прерывание по таймеру;
  • Прерывания данных по SPI, I2C, USART;
  • Аналогово-цифровое преобразование;
  • Готовность использовать EEPROM, флеш-память.

Как реализуются прерывания в Ардуино

При поступлении сигнала прерывания работа в цикле loop() приостанавливается. Начинается выполнение функции, которая объявляется на выполнение при прерывании. Объявленная функция не может принимать входные значения и возвращать значения при завершении работы. На сам код в основном цикле программы прерывание не влияет. Для работы с прерываниями в Ардуино используется стандартная функция attachInterrupt().

Отличие реализации прерываний в разных платах Ардуино

В зависимости от аппаратной реализации конкретной модели микроконтроллера есть несколько прерываний. Плата Arduino Uno имеет 2 прерывания на втором и третьем пине, но если требуется более двух выходов, плата поддерживает специальный режим «pin-change». Этот режим работает по изменению входа для всех пинов. Отличие режима прерывания по изменению входа заключается в том, что прерывания могут генерироваться на любом из восьми контактов. Обработка в таком случае будет сложнее и дольше, так как придется отслеживать последнее состояние на каждом из контактов.

На других платах число прерываний выше. Например, плата Ардуино Мега 2560 имеет 6 пинов, которые могут обрабатывать внешние прерывания. Для всех плат Ардуино при работе с функцией attachInterrupt (interrupt, function, mode) аргумент Inerrupt 0 связан с цифровым пином 2.

Прерывания в языке Arduino

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

Синтаксис attachInterrupt()

Функция attachInterrupt используется для работы с прерываниями. Она служит для соединения внешнего прерывания с обработчиком.

Синтаксис вызова: attachInterrupt(interrupt, function, mode)

Аргументы функции:

  • interrupt – номер вызываемого прерывания (стандартно 0 – для 2-го пина, для платы Ардуино Уно 1 – для 3-го пина),
  • function – название вызываемой функции при прерывании(важно – функция не должна ни принимать, ни возвращать какие-либо значения),
  • mode – условие срабатывания прерывания.

Возможна установка следующих вариантов условий срабатывания:

  • LOW – выполняется по низкому уровню сигнала, когда на контакте нулевое значение. Прерывание может циклично повторяться – например, при нажатой кнопке.
  • CHANGE – по фронту, прерывание происходит при изменении сигнала с высокого на низкий или наоборот. Выполняется один раз при любой смене сигнала.
  • RISING – выполнение прерывания один раз при изменении сигнала от LOW к HIGH.
  • FALLING – выполнение прерывания один раз при изменении сигнала от HIGH к LOW.4

Важные замечания

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

  • Функция – обработчик не должна выполняться слишком долго. Все дело в том, что Ардуино не может обрабатывать несколько прерываний одновременно. Пока выполняется ваша функция-обработчик, все остальные прерывания останутся без внимания и вы можете пропустить важные события. Если надо делать что-то большое – просто передавайте обработку событий в основном цикле loop(). В обработчике вы можете лишь устанавливать флаг события, а в loop – проверять флаг и обрабатывать его.
  • Нужно быть очень аккуратными с переменными. Интеллектуальный компилятор C++ может “пере оптимизировать” вашу программу – убрать не нужные, на его взгляд, переменные. Компилятор просто не увидит, что вы устанавливаете какие-то переменные в одной части, а используете – в другой. Для устранения такой вероятности в случае с базовыми типами данных можно использовать ключевое слово volatile, например так: volatile boolean state = 0. Но этот метод не сработает со сложными структурами данных. Так что надо быть всегда на чеку.
  • Не рекомендуется использовать большое количество прерываний (старайтесь не использовать более 6-8). Большое количество разнообразных событий требует серьезного усложнения кода, а, значит, ведет к ошибкам. К тому же надо понимать, что ни о какой временной точности исполнения в системах с большим количеством прерываний речи быть не может – вы никогда точно не поймете, каков промежуток между вызовами важных для вас команд.
  • В обработчиках категорически нельзя использовать delay(). Механизм определения интервала задержки использует таймеры, а они тоже работают на прерываниях, которые заблокирует ваш обработчик. В итоге все будут ждать всех и программа зависнет. По этой же причине нельзя использовать протоколы связи, основанные на прерываниях (например, i2c).

Примеры использования attachInterrupt

Давайте приступим к практике и рассмотрим простейший пример использования прерываний. В примере мы определяем функцию-обработчик, которая при изменении сигнала на 2 пине Arduino Uno переключит состояние пина 13, к которому мы традиционно подключим светодиод.

#define PIN_LED 13 volatile boolean actionState = LOW; void setup() { pinMode(PIN_LED, OUTPUT); // Устанавливаем прерывание // Функция myEventListener вызовется тогда, когда // на 2 пине (прерываниие 0 связано с пином 2) // изменится сигнал (не важно, в какую сторону) attachInterrupt(0, myEventListener, CHANGE); } void loop() { // В функции loop мы ничего не делаем, т.к. весь код обработки событий будет в функции myEventListener } void myEventListener() { actionState != actionState; // // Выполняем другие действия, например, включаем или выключаем светодиод digitalWrite(PIN_LED, actionState); }

Давайте рассмотрим несколько примеров более сложных прерываний и их обработчиков: для таймера и кнопок.

Прерывания по нажатию кнопки с антидребезгом

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

Избавиться от дребезга можно при помощи функции millis – она позволяет засечь время, прошедшее от первого срабатывания кнопки.

if(digitalRead(2)==HIGH) { //при нажатии кнопки //Если от предыдущего нажатия прошло больше 100 миллисекунд if (millis() — previousMillis >= 100) { //Запоминается время первого срабатывания previousMillis = millis(); if (led==oldled) { //происходит проверка того, что состояние кнопки не изменилось led=!led; }

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

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

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

Таймер и прерывание по таймеру позволяет выполнять прерывание один раз в миллисекунду. В Ардуино имеется 3 таймера – Timer0, Timer1 и Timer2. Timer0 используется для генерации прерываний один раз в миллисекунду, при этом происходит обновление счетчика, который передается в функцию millis (). Этот таймер является восьмибитным и считает от 0 до 255. Прерывание генерируется при достижении значения 255. По умолчанию используется тактовый делитель на 65, чтобы получить частоту, близкую к 1 кГц.

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

OCR0A = 0xAF;

TIMSK0 |= _BV(OCIE0A);

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

SIGNAL(TIMER0_COMPA_vect) { unsigned long currentMillis = millis(); sweeper1.Update(currentMillis); if(digitalRead(2) == HIGH) { sweeper2.Update(currentMillis); led1.Update(currentMillis); } led2.Update(currentMillis); led3.Update(currentMillis); } //Функция loop () останется пустой. void loop() { }

Подведение итогов

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

Всем привет! Собрав таймер на Ардуино из этой инструкции, вы сможете контролировать включение и выключение ваших устройств в нужное вам время. Это могут быть ночные фонари, система орошения сада и т.д. Мы воспользуемся Ардуино, дисплеем и RTC 1307, отображающим и управляющим временем. Вы можете задать часы «ВКЛ» и «ВЫКЛ» при помощи 4 кнопок, что позволит вам изменять настройки микросхемы таймера.

Также вы научитесь создавать часы, базирующиеся на Ардуино. Я приложил схему из fritzing и видео, так что вы без проблем соберёте данное устройство.

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

Arduino STM32 — таймеры


Это третья часть посвящённая плате Blue Pill (Arduino STM32).
Первая часть — прошивка
Вторая часть — внешние прерывания
Таймер — это адски крутая штуковина просто счетчик, который при достижении заданного значения может вызывать определённые события — прерывания, измерение времени между импульсами, генерировать ШИМ, отвечает за работу интерфейсов типа I2C, и ещё кучу всяких полезных делишек может делать.
Микроконтроллер STM32F103х имеет на борту четыре 16-ти битных таймера
• три таймера — TIM2, TIM3, TIM4, общего назначения (general-purpose timers).
• один продвинутый таймер — TIM1, с расширенными возможностями (advanced-control timers).
А так же…
• два WDT (WatchDog Timer).
• один SysTick Timer.
16-ти битный таймер умеет считать в диапазоне от 0 до 65535 (это значение называется «переполнение» см. ниже). То есть, говоря простым языком, в пямяти есть переменная, которая увеличивается на единицу с каждым следующим «тиком» таймера. При достижении заданного пользователем значения (если значение не задавать, то счёт идет до максимального значения — 65535), после чего счётчик сбрасывается в ноль, генерируется какое-либо событие, и отсчёт начинается заново.
Таймеры независимы друг от друга.
Таймеры тактируются (то есть «тикают») от системной частоты и соответственно не потребляют ресурсов.
Если не использовать дополнительных настроек, то таймер будет считать со скоростью 72Мгц (72 миллиона «тиков» в секунду). То есть до максимального значения (65535) он досчитает за ~0.9 миллисекунды. В общем шустрый парнишка.

Настройки таймера (режимы)

Так как настроек и режимов достаточно много, то я буду описывать их «от простого к сложному». Каким образом и при каких обстоятельствах применять те или иные режимы решать Вам.
На всякий случай:
мс — миллисекунда (1000мс = 1сек).
мкс — микросекунда (1000000мкс = 1сек).
нс — наносекунда (1000000000мкс = 1сек)
Предделитель
Как написано выше, если запустить таймер без настроек, то он будет считать очень быстро, что конечно же не всегда целесообразно. Для того чтобы понизить скорость отсчёта, нужно использовать предделитель.
Предделитель делит системную частоту на любое число от 1 до 65536. Например, если установить значение предделителя 3600, то таймер будет «тикать» со скоростью 20000 раз в секунду (72МГц / 3600 = 20КГц). То есть он будет доходить до максимального значения (65536) примерно за 3.22 секунды.
Для примера используем таймер №3, установим делитель 720 (72МГц / 720 = 100КГц) и активируем прерывание, которое будет происходить каждый раз когда таймер досчитает до мах. значения (примерно каждые 650мс). В обработчике будем менять состояние светодиода на противоположное…
volatile bool LEDOn13 = 0; void setup() { pinMode(LED_BUILTIN, OUTPUT); // PC13 Timer3.pause(); // останавливаем таймер перед настройкой Timer3.setPrescaleFactor(720); // устанавливаем делитель Timer3.attachInterrupt(TIMER_UPDATE_INTERRUPT, func_tim_3); // активируем прерывание Timer3.refresh(); // обнулить таймер Timer3.resume(); // запускаем таймер } void loop() {} void func_tim_3() // обработчик прерывания { digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); }
TIMER_UPDATE_INTERRUPT — режим таймера (автоматическое обновление и вызов прерывания).
Функции pause(), refresh() и resume() не обязательны, я их просто обозначил. Эти функции можно использовать в любом месте программы, например, если где-то в основном цикле нужно приостановить, запустить или обнулить таймер.
Переполнение
Переполнением называется то самое значение (65535), до которого считает таймер. Его можно менять по своему усмотрению.
Если переполнение не указано в коде, как в примере выше, то по умолчанию устанавливается мах. значение — 65535.
Если мы укажем в нашем коде переполнение равное 16000, тогда таймер будет обнуляться и начинать отсчёт заново достигнув этой цифры. Таким образом светодиод будет моргать в четыре раза чаще…
volatile bool LEDOn13 = 0; void setup() { pinMode(LED_BUILTIN, OUTPUT); // PC13 Timer3.pause(); // останавливаем таймер перед настройкой Timer3.setPrescaleFactor(720); // устанавливаем делитель Timer3.setOverflow(16000); // переполнение Timer3.attachInterrupt(TIMER_UPDATE_INTERRUPT, func_tim_3); // активируем прерывание Timer3.refresh(); // обнулить таймер Timer3.resume(); // запускаем таймер } void loop() {} void func_tim_3() // обработчик прерывания { digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); }
Оперируя предделителем и переполнением можно получать различные интервалы времени. Однако если нет желания заморачиваться с подсчётами, то можно функции setPrescaleFactor() и setOverflow() заменить одной функцией — setPeriod(), которая сделает всё сама, ей нужно только указать время в микросекундах…
volatile bool LEDOn13 = 0; void setup() { pinMode(LED_BUILTIN, OUTPUT); // PC13 Timer3.pause(); // останавливаем таймер перед настройкой Timer3.setPeriod(500000); // время в микросекундах (500мс) Timer3.attachInterrupt(TIMER_UPDATE_INTERRUPT, func_tim_3); // активируем прерывание Timer3.refresh(); // обнулить таймер Timer3.resume(); // запускаем таймер } void loop() {} void func_tim_3() // обработчик прерывания { digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); }
Таймер будет срабатывать каждые 500мс.
Эту функцию стоило бы описать самой первой, так как она самая простая, но тогда было бы непонятно, что она объединяет в себе настройки предделителя и переполнения.
Максимальная задержка ~59сек — setPeriod(59000000).
Вернёмся ко второму примеру и добавим функцию — setCount(). Эта функция в некотором роде противоположность setOverflow(), она указывает таймеру с какого места нужно начинать отсчёт.
Допустим что переполнение у нас равно 60000, то есть счётчик считает от 0 до 60000. Теперь если в обработчике прерывания дописать — Timer3.setCount(59000), то отсчёт будет вестись не от нуля, а от 59000. Соответственно таймер переполнится очень быстро.
Функция setCount() устанавливает новое значение только один раз, при следующей итерации таймер снова будет отсчитывать от нуля, поэтому её нужно вызывать каждый раз…
volatile int i = 0; volatile bool LEDOn13 = 0; void setup() { pinMode(LED_BUILTIN, OUTPUT); // PC13 Timer3.pause(); // останавливаем таймер перед настройкой Timer3.setPrescaleFactor(1720); // устанавливаем делитель Timer3.setOverflow(60000); // переполнение Timer3.attachInterrupt(TIMER_UPDATE_INTERRUPT, func_tim_3); // активируем прерывание Timer3.refresh(); // обнулить таймер Timer3.resume(); // запускаем таймер } void loop() {} void func_tim_3() // обработчик прерывания { digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); i++; if(i > 6 && i < 91) Timer3.setCount(59000); if(i > 90 && i < 101) Timer3.setCount(35000); if(i > 100) Timer3.setCount(0); }
Сначала светик мигнёт три раза с частотой заданой в setup(), потом заморгает очень быстро, потом медленней, и на конец вернётся к изначальной частоте.
Теперь пришло время разобраться с каналами таймеров…
У таймеров TIM1, TIM2, TIM3, TIM4 имеется в наличии по четыре канала ввода/вывода (TIMER_CH1…TIMER_CH4). Каналы могут работать в режимах — захват, сравнение, генерировать ШИМ и одиночные импульсы.
Сравнение
Работа каналов в режиме сравнения, это когда в специальную ячейку (для каждого канала своя ячейка) памяти помещается число от 0 до 65535, а таймер ведя отсчёт постоянно сравнивает своё значение со значением в ячейке. Как только значения совпадают, то тут же вызывается какое-либо событие, например прерывание. Таймер при этом продолжает считать пока не переполнится, после чего цикл повторяется.
Таким образом, один таймер может выполнить несколько действий с разными интервалами времени в рамках одного цикла переполнения.
В примере ниже происходит следующее: таймер №3 будет считать от 0 до 14000 (переполнение), и после каждого «тика» сравнивать своё значение со значениями сравнений каналов (в примере я задействовал все четыре канала). При каждом совпадении значений будут генерироваться прерывания…
пример volatile bool LEDOn10 = 0; volatile bool LEDOn11 = 0; volatile bool LEDOn12 = 0; volatile bool LEDOn13 = 0; void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); pinMode(PB10, OUTPUT); pinMode(PB11, OUTPUT); pinMode(PB12, OUTPUT); Timer3.setMode(TIMER_CH1, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH2, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH3, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH4, TIMER_OUTPUT_COMPARE); Timer3.pause(); Timer3.setPrescaleFactor(7200); Timer3.setOverflow(14000); // счетчик считает от 0 до 14000 Timer3.setCompare(TIMER_CH1, 3000); // сравнение Timer3.attachInterrupt(TIMER_CH1, func_1); Timer3.setCompare(TIMER_CH2, 6000); Timer3.attachInterrupt(TIMER_CH2, func_2); Timer3.setCompare(TIMER_CH3, 9000); Timer3.attachInterrupt(TIMER_CH3, func_3); Timer3.setCompare(TIMER_CH4, 11000); Timer3.attachInterrupt(TIMER_CH4, func_4); Timer3.resume(); } void loop() {} void func_1(void) { Serial.println(«Compare CH1»); digitalWrite(PB10, (LEDOn10 = !LEDOn10)); } void func_2(void) { Serial.println(«Compare CH2»); digitalWrite(PB11, (LEDOn11 = !LEDOn11)); } void func_3(void) { Serial.println(«Compare CH3»); digitalWrite(PB12, (LEDOn12 = !LEDOn12)); } void func_4(void) { Serial.println(«Compare CH4»); digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); }

… при этом ничто не мешает использовать ещё и прерывание по переполнению.
пример volatile bool LEDOn10 = 0; volatile bool LEDOn11 = 0; volatile bool LEDOn12 = 0; volatile bool LEDOn13 = 0; volatile bool LEDOn15 = 0; void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); pinMode(PB10, OUTPUT); pinMode(PB11, OUTPUT); pinMode(PB12, OUTPUT); pinMode(PB15, OUTPUT); Timer3.setMode(TIMER_CH1, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH2, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH3, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH4, TIMER_OUTPUT_COMPARE); Timer3.pause(); Timer3.setPrescaleFactor(7200); Timer3.setOverflow(14000); // счетчик считает от 0 до 14000 Timer3.setCompare(TIMER_CH1, 3000); // сравнение Timer3.attachInterrupt(TIMER_CH1, func_1); Timer3.setCompare(TIMER_CH2, 6000); Timer3.attachInterrupt(TIMER_CH2, func_2); Timer3.setCompare(TIMER_CH3, 9000); Timer3.attachInterrupt(TIMER_CH3, func_3); Timer3.setCompare(TIMER_CH4, 11000); Timer3.attachInterrupt(TIMER_CH4, func_4); Timer3.attachInterrupt(TIMER_UPDATE_INTERRUPT, func_5); // прерывание по переполнению Timer3.resume(); } void loop() {} void func_1(void) { Serial.println(«Compare CH1»); digitalWrite(PB10, (LEDOn10 = !LEDOn10)); } void func_2(void) { Serial.println(«Compare CH2»); digitalWrite(PB11, (LEDOn11 = !LEDOn11)); } void func_3(void) { Serial.println(«Compare CH3»); digitalWrite(PB12, (LEDOn12 = !LEDOn12)); } void func_4(void) { Serial.println(«Compare CH4»); digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); } void func_5(void) { Serial.println(«Overflow»); digitalWrite(PB15, (LEDOn15 = !LEDOn15)); }

Поскольку в рамках одного цикла интервал получается очень маленьким, то можно просто добавить переменную, которая будет увеличиваться при каждом прерывании и событие произойдёт по достижении нужного значения…
пример volatile bool LEDOn10 = 0; volatile bool LEDOn11 = 0; volatile bool LEDOn12 = 0; volatile bool LEDOn13 = 0; volatile bool LEDOn15 = 0; volatile int count = 0; void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); pinMode(PB10, OUTPUT); pinMode(PB11, OUTPUT); pinMode(PB12, OUTPUT); pinMode(PB15, OUTPUT); Timer3.setMode(TIMER_CH1, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH2, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH3, TIMER_OUTPUT_COMPARE); Timer3.setMode(TIMER_CH4, TIMER_OUTPUT_COMPARE); Timer3.pause(); Timer3.setPrescaleFactor(7200); Timer3.setOverflow(14000); // счетчик считает от 0 до 14000 Timer3.setCompare(TIMER_CH1, 3000); // сравнение Timer3.attachInterrupt(TIMER_CH1, func_1); Timer3.setCompare(TIMER_CH2, 6000); Timer3.attachInterrupt(TIMER_CH2, func_2); Timer3.setCompare(TIMER_CH3, 9000); Timer3.attachInterrupt(TIMER_CH3, func_3); Timer3.setCompare(TIMER_CH4, 11000); Timer3.attachInterrupt(TIMER_CH4, func_4); Timer3.attachInterrupt(TIMER_UPDATE_INTERRUPT, func_5); // прерывание по переполнению Timer3.resume(); } void loop() {} void func_1(void) { count++; if(count == 5) { Serial.println(«Канал_1 — моргаю при каждом пятом сравнении»); digitalWrite(PB10, (LEDOn10 = !LEDOn10)); count = 0; } } void func_2(void) { Serial.println(«Compare CH2»); digitalWrite(PB11, (LEDOn11 = !LEDOn11)); } void func_3(void) { Serial.println(«Compare CH3»); digitalWrite(PB12, (LEDOn12 = !LEDOn12)); } void func_4(void) { Serial.println(«Compare CH4»); digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); } void func_5(void) { Serial.println(«Overflow»); digitalWrite(PB15, (LEDOn15 = !LEDOn15)); }
TIMER_CH1…TIMER_CH4 — номера каналов.
TIMER_OUTPUT_COMPARE — режим канала.
ШИМ
К каждому каналу таймера привязана конкретная «ножка» МК, которую можно использовать в различных целях, например генерировать сигнал ШИМ…
каналыTIM1_CH1 — PA8
TIM1_CH2 — PA9
TIM1_CH3 — PA10
TIM1_CH4 — PA11 (это usb — не надо использовать)
TIM2_CH1 — PA0
TIM2_CH2 — PA1
TIM2_CH3 — PA2
TIM2_CH4 — PA3
TIM3_CH1 — PA6
TIM3_CH2 — PA7
TIM3_CH3 — PB0
TIM3_CH4 — PB1
TIM4_CH1 — PB6
TIM4_CH2 — PB7
TIM4_CH3 — PB8
TIM4_CH4 — PB9

Для примера воспользуемся первым и вторым каналом третьего таймера. Переполнением (setOverflow) зададим период, а сравнением (setCompare) будем регулировать длину импульса…

t — длина импульса.
T — период.
void setup() { pinMode(PA6, PWM); // выход ШИМ канал 1 pinMode(PA7, PWM); // выход ШИМ канал 2 Timer3.setPrescaleFactor(72); // 1мкс Timer3.setOverflow(5000); // период 5 мс Timer3.setCompare(TIMER_CH1, 4000); // импульс 4 мс Timer3.setCompare(TIMER_CH2, 100); // импульс 0.1 мс Timer3.refresh(); // обнулить счётчик Timer3.resume(); } void loop() { Timer3.setCompare(TIMER_CH1, 4000); Timer3.setCompare(TIMER_CH2, 100); Timer3.refresh(); delay(2000); Timer3.setCompare(TIMER_CH1, 2000); Timer3.setCompare(TIMER_CH2, 700); Timer3.refresh(); delay(2000); Timer3.setCompare(TIMER_CH1, 700); Timer3.setCompare(TIMER_CH2, 2000); Timer3.refresh(); delay(2000); Timer3.setCompare(TIMER_CH1, 100); Timer3.setCompare(TIMER_CH2, 4000); Timer3.refresh(); delay(2000); }
Добавив ещё один канал можно рулить RGB-лентой…
RGB int compare_R = 2000; int compare_G = 2000; int compare_B = 2000; void setup() { Serial.begin(115200); pinMode(PA6, PWM); // выход ШИМ канал 1 pinMode(PA7, PWM); // выход ШИМ канал 2 pinMode(PB0, PWM); // выход ШИМ канал 3 Timer3.setPrescaleFactor(72); // 1мкс Timer3.setOverflow(5000); // период 5 мс Timer3.setCompare(TIMER_CH1, compare_R); // R Timer3.setCompare(TIMER_CH2, compare_G); // G Timer3.setCompare(TIMER_CH3, compare_B); // B Timer3.refresh(); // обнулить счётчик Timer3.resume(); } void loop() { if(Serial.available() > 1) { char first_sim = Serial.read(); char second_sim = Serial.read(); for(int i=0; i < 3; i++) { Serial.read(); delay(1); } switch(first_sim) { case ‘R’: if(second_sim == ‘+’) { compare_R += 1000; if(compare_R > 5000) compare_R = 5000; Timer3.setCompare(TIMER_CH1, compare_R); Serial.print(«Compare_CH1 «); Serial.println(Timer3.getCompare(TIMER_CH1)); } else if(second_sim == ‘-‘) { compare_R -= 1000; if(compare_R < 0) compare_R = 0; Timer3.setCompare(TIMER_CH1, compare_R); Serial.print(«Compare_CH1 «); Serial.println(Timer3.getCompare(TIMER_CH1)); } break; case ‘G’: if(second_sim == ‘+’) { compare_G += 1000; if(compare_G > 5000) compare_G = 5000; Timer3.setCompare(TIMER_CH2, compare_G); Serial.print(«Compare_CH2 «); Serial.println(Timer3.getCompare(TIMER_CH2)); } else if(second_sim == ‘-‘) { compare_G -= 1000; if(compare_G < 0) compare_G = 0; Timer3.setCompare(TIMER_CH2, compare_G); Serial.print(«Compare_CH2 «); Serial.println(Timer3.getCompare(TIMER_CH2)); } break; case ‘B’: if(second_sim == ‘+’) { compare_B += 1000; if(compare_B > 5000) compare_B = 5000; Timer3.setCompare(TIMER_CH3, compare_B); Serial.print(«Compare_CH3 «); Serial.println(Timer3.getCompare(TIMER_CH3)); } else if(second_sim == ‘-‘) { compare_B -= 1000; if(compare_B < 0) compare_B = 0; Timer3.setCompare(TIMER_CH3, compare_B); Serial.print(«Compare_CH3 «); Serial.println(Timer3.getCompare(TIMER_CH3)); } break; default: break; } } }
Отправляя в «Монитор порта» R+ или R-, G+ или G-, B+ или B- можно управлять яркостью светиков.
Задействовав все четыре таймера можно подключить четыре ленты, и при этом выполнять ещё какие-либо действия без ущерба для всей этой иллюминации.
В примере под спойлером появилась новая функция — getCompare(). Если у некоторых функций заменить приставку — set на get, то можно посмотреть текущие значения…
volatile bool LEDOn13 = 0; void setup() { Serial.begin(115200); pinMode(LED_BUILTIN, OUTPUT); Timer3.setMode(TIMER_CH1, TIMER_OUTPUT_COMPARE); Timer3.pause(); Timer3.setPrescaleFactor(7200); Timer3.setOverflow(14000); Timer3.setCompare(TIMER_CH1, 3000); Timer3.attachInterrupt(TIMER_CH1, func_1); Timer3.resume(); } void loop() { Serial.println(Timer3.getPrescaleFactor()); Serial.println(Timer3.getOverflow()); Serial.println(Timer3.getCount()); // текущее значение счётчика Serial.println(Timer3.getCompare(TIMER_CH1)); Serial.println(«———«); delay(1000); } void func_1(void) { digitalWrite(LED_BUILTIN, (LEDOn13 = !LEDOn13)); }
Захват
Предположим что у вас есть некий источник сигналов, пускай это будет чередующиеся HIGH и LOW, генерируемые обычной Ардуиной. Нам нужно измерить длину импульса и период…

t — длина импульса.
T — период.
… вот как раз для решения этой задачи, лучше всего и подойдёт режим захвата.
Коротко говоря, выглядит это так: источник сигнала подключается к «ножке» соединённой (внутри МК) с двумя каналами таймера, таймер тикает с заданной частотой (допустим один «тик» за одну микросекунду, соответственно весь цикл будет длиться 65535 мкс = 65.5 мс) и при появлении сигнала HIGH (передний фронт) первый канал совершает захват, то есть в спец. ячейку памяти записывается кол-во «тиков» (ячейка та же самая, что используется для сравнения). После этого таймер продолжает считать и при появлении сигнала LOW (задний фронт) второй канал тоже совершает захват. Кол-во «тиков» между захватами и будет длиной импульса. Ну и наконец при появлении очередного сигнала HIGH, мы узнаем длину периода.
Важно понимать, что таймер должен быть настроен так, чтоб все три импульса (HIGH ⇨ LOW ⇨ HIGH) произошли за один цикл (от 0 до 65535), об этом ниже…
Итак, источником сигналов послужит обычная ардуина (конечно ничто не мешает задействовать для этого отдельный таймер нашей платы, таймеры ведь независимые, но для большей наглядности воспользуемся ардуиной), поэтому зальём в неё вот этот код…
void setup() { pinMode(5, OUTPUT); } void loop() { digitalWrite(5, HIGH); delay(20); // длина импульса 20 миллисекунд digitalWrite(5, LOW); delay(30); // длина периода получится 20 + 30 = 50 миллисекунд }
… и соединим пин 5 ардуины с пином PA6 (TIM3_CH1) платы. Согласуйте уровни напряжения.
В stm загрузим такую программу…
void setup() { Serial.begin(115200); pinMode(PA6, INPUT_PULLDOWN); Timer3.pause(); Timer3.setPrescaleFactor(72); // один «тик» равен одной микросекунде Timer3.setInputCaptureMode(TIMER_CH1, TIMER_IC_INPUT_DEFAULT); Timer3.setInputCaptureMode(TIMER_CH2, TIMER_IC_INPUT_SWITCH); Timer3.setPolarity(TIMER_CH2, 1); Timer3.setSlaveFlags(TIMER_SMCR_TS_TI1FP1 | TIMER_SMCR_SMS_RESET); Timer3.refresh(); Timer3.resume(); } void loop() { if(Timer3.getInputCaptureFlag(TIMER_CH2)) { Serial.print(«Длина импульса «); Serial.println(Timer3.getCompare(TIMER_CH2)); } if(Timer3.getInputCaptureFlag(TIMER_CH1)) { Serial.print(«Период «); Serial.println(Timer3.getCompare(TIMER_CH1)); } }
В этом режиме таймер работает следующим образом: один канал ловил фронты, а второй ловил спады. При этом оба канала могут обмениваться сигналами внутри микроконтроллера.
Этот код взят из предоставляемых примеров (A_STM32_Examples ⇨ Sensors ⇨ HardwareTimerPWMInput) и немного переделан.
Открываем

… и видим то, что и должны были увидеть. Длина импульса 20000 мкс = 20 мс и период 50000 мкс = 50 мс (небольшие хвостики не в счёт).
Теперь в коде для ардуины увеличим длительность LOW с 30-ти до 60-ти миллисекунд…
digitalWrite(5, LOW); delay(60); // длина периода 20 + 60 = 80 миллисекунд
В терминале вы увидите, что длина импульса будет верной, а период, вместо ожидаемых 80мс, будет какой-то странный. Это происходит потому, что наш счётчик обнуляется раньше чем ардуина поменяет состояние с LOW на HIGH. То есть, сейчас счётчик «тикает» с интервалом в одну микросекунду, соответственно он переполняется через 65535 микросекунд = 65.5 миллисекунд, и конечно же 80 мс сюда никак не влезают.
Для измерения сигналов различной длины, нужно изменять значение предделителя — setPrescaleFactor(). Если мы поменяем 72 на 720, то каждый «тик» таймера будет равен 10 микросекундам, и соответственно диапазон измерений вырастет с 65 мс до 655 мс (65535 «тиков» по 10 мкс = 655350 мкс = 655 мс).
Если вы пользовались осциллографом, то знаете, что там нужно вертеть разные крутилки чтоб подобрать желаемую частоту. В данном случае предделитель и есть та самая крутилка.
Измените значение и загрузите в плату…
Timer3.setPrescaleFactor(720); // один «тик» равен 10-ти микросекундам

Вот теперь всё в порядке, период равен 8000 (80мс = 80000мкс / 10 = 8000).
На этом наверно пора закругляться. В статье затронута лишь крохотная часть возможностей таймеров, однако на первое время этого должно хватить для использования в рамках IDE Arduino.
Полезные ресурсы…
Форум (рус.)
Таймеры stm32 (англ.)
Руководство (рус.) по stm32
Основной форум (англ.) — www.stm32duino.com/
Человека, который пилит ядро, зовут Roger Clark — github.com/rogerclarkmelbourne/Arduino_STM32

Оптимизируйте ваши программы для Arduino с помощью прерываний – простого способа для реагирования на события в режиме реального времени!

Мы прерываем нашу передачу…

Как выясняется, существует отличный (но недостаточно часто используемый) механизм, встроенный во все Arduino, который идеально подходит для отслеживания событий в режиме реального времени. Данный механизм называется прерыванием. Работа прерывания заключается в том, чтобы убедиться, что процессор быстро отреагирует на важные события. При обнаружении определенного сигнала прерывание (как и следует из названия) прерывает всё, что делал процессор, и выполняет некоторый код, предназначенный для реагирования на вызвавшую его внешнюю причину, воздействующую на Arduino. После того, как этот код будет выполнен, процессор возвращается к тому, что он изначально делал, как будто ничего не случилось!

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

Прерывания по кнопке

Начнем с простого примера: использования прерывания для отслеживания нажатия кнопки. Для начала, мы возьмем скетч, который вы, вероятно, уже видели: пример «Button», включенный в Arduino IDE (вы можете найти его в каталоге «Примеры», проверьте меню Файл → Примеры → 02. Digital → Button).

const int buttonPin = 2; // номер вывода с кнопкой const int ledPin = 13; // номер вывода со светодиодом int buttonState = 0; // переменная для чтения состояния кнопки void setup() { // настроить вывод светодиода на выход: pinMode(ledPin, OUTPUT); // настроить вывод кнопки на вход: pinMode(buttonPin, INPUT); } void loop() { // считать состояние кнопки: buttonState = digitalRead(buttonPin); // проверить нажата ли кнопка. // если нажата, то buttonState равно HIGH: if (buttonState == HIGH) { // включить светодиод: digitalWrite(ledPin, HIGH); } else { // погасить светодиод: digitalWrite(ledPin, LOW); } }

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

const int buttonPin = 2; // номер вывода с кнопкой const int ledPin = 13; // номер вывода со светодиодом volatile int buttonState = 0; // переменная для чтения состояния кнопки void setup() { // настроить вывод светодиода на выход: pinMode(ledPin, OUTPUT); // настроить вывод кнопки на вход: pinMode(buttonPin, INPUT); // прикрепить прерывание к вектору ISR attachInterrupt(0, pin_ISR, CHANGE); } void loop() { // Здесь ничего нет! } void pin_ISR() { buttonState = digitalRead(buttonPin); digitalWrite(ledPin, buttonState); }

Циклы и режимы прерываний

Здесь вы заметите несколько изменений. Первым и самым очевидным из них является то, что loop() теперь не содержит никаких инструкций! Мы можем обойтись без них, так как вся работа, которая ранее выполнялась в операторе if/else, теперь выполняется в новой функции pin_ISR(). Этот тип функций называется обработчиком прерывания: его работа состоит в том, чтобы быстро запуститься, обработать прерывание и позволить процессору вернуться обратно к основной программе (то есть к содержимому loop()). При написании обработчика прерывания следует учитывать несколько важных моментов, отражение которых вы можете увидеть в приведенном выше коде:

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

Вам, наверное, интересно: откуда мы знаем, когда запустится прерывание? Что его вызывает? Третья функция, вызываемая в функции setup(), устанавливает прерывание для всей системы. Данная функция, attachInterrupt(), принимает три аргумента:

  1. вектор прерывания, который определяет, какой вывод может генерировать прерывание. Это не сам номер вывода, а ссылка на место в памяти, за которым процессор Arduino должен наблюдать, чтобы увидеть, не произошло ли прерывание. Данное пространство в этом векторе соответствует конкретному внешнему выводу, и не все выводы могут генерировать прерывание! На Arduino Uno генерировать прерывания могут выводы 2 и 3 с векторами прерываний 0 и 1, соответственно. Для получения списка выводов, которые могут генерировать прерывания, смотрите документацию на функцию attachInterrupt для Arduino;
  2. имя функции обработчика прерывания: определяет код, который будет запущен при совпадении условия срабатывания прерывания;
  3. режим прерывания, который определяет, какое действие на выводе вызывает прерывание. Arduino Uno поддерживает четыре режима прерывания:
    • RISING – активирует прерывание по переднему фронту на выводе прерывания;
    • FALLING – активирует прерывание по спаду;
    • CHANGE – реагирует на любое изменение значения вывода прерывания;
    • LOW – вызывает всякий раз, когда на выводе низкий уровень.

И резюмируя, наша настройка attachInterrupt() соответствует отслеживанию вектора прерывания 0 (вывод 2), чтобы отреагировать на прерывание с помощью pin_ISR(), и вызвать pin_ISR() всякий раз, когда произойдет изменение состояния на выводе 2.

Volatile

Еще один момент, на который стоит указать: наш обработчик прерывания использует переменную buttonState для хранения состояния вывода. Проверьте определение buttonState: вместо типа int, мы определили его, как тип volatile int. В чем же здесь дело? volatile является ключевым словом языка C, которое применяется к переменным. Оно означает, что значение переменной находится не под полным контролем программы. То есть значение buttonState может измениться и измениться на что-то, что сама программа не может предсказать – в этом случае, пользовательский ввод.

Еще одна полезная вещь в ключевом слове volatile заключается в защите от любой случайной оптимизации. Компиляторы, как выясняется, выполняют еще несколько дополнительных задач при преобразовании исходного кода программы в машинный исполняемый код. Одной из этих задач является удаление неиспользуемых в исходном коде переменных из машинного кода. Так как переменная buttonState не используется или не вызывается напрямую в функциях loop() или setup(), существует риск того, что компилятор может удалить её, как неиспользуемую переменную. Очевидно, что это неправильно – нам необходима эта переменная! Ключевое слово volatile обладает побочным эффектом, сообщая компилятору, что эту переменную необходимо оставить в покое.

Удаление неиспользуемых переменных из кода – это функциональная особенность, а не баг компиляторов. Люди иногда оставляют в коде неиспользуемые переменные, которые занимают память. Это не такая большая проблема, если вы пишете программу на C для компьютера с гигабайтами оперативной памяти. Однако, на Arduino оперативная память ограничена, и вы не хотите тратить её впустую! Даже C компиляторы для компьютеров будут поступать точно так же, несмотря на массу доступной системной памяти. Зачем? По той же причине, по которой люди убирают за собой после пикника – это хорошая практика, не оставлять после себя мусор.