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


Мы с вами уже много раз рассматривали конструкцию таймера на millis(), который позволяет наладить логику работы кода по таймерам. Минусом этого способа является необходимость постоянно опрашивать конструкцию таймера, чтобы проверять, не сработал ли он. Соответственно код в главном цикле должен быть “прозрачным”, то есть не содержать задержек, долгих замкнутых циклов и просто блокирующих кусков. Если для таймеров с длинным периодом (минута, 5 секунд) это не так критично, то для выполнения действий с высокой строго заданной частотой любая маленькая задержка в главном цикле может стать большой проблемой! Выходом из ситуации может стать прерывание по таймеру. Вспомните урок про аппаратные прерывания: прерывание позволяет “выйти” из любого выполняемого на данный момент участка кода в основном цикле, выполнить нужный блок кода, который находится внутри прерывания, и вернуться туда, откуда вышли, и продолжить выполнение. Таким образом это практически параллельное выполнение задач. В этом уроке мы научимся делать это по аппаратному таймеру.

Для чего использовать прерывания по таймеру?

  • Генерация сигналов
  • Измерение времени
  • Параллельное выполнение задач
  • Выполнение задачи через строго заданный период времени
  • И многое другое

Таймеры


Прерывания генерируются отдельным аппаратным таймером, который находится в микроконтроллере где-то рядом с вычислительным ядром. Аппаратный таймер, он же счётчик, занимается очень простой задачей: считает “тики” тактового генератора (который задаёт частоту работы всей системы) и, в зависимости от режима работы, может дёргать ногами или давать сигнал на микроконтроллер при определённых значениях счётчика. Таким образом “разрешение” работы таймера – один тик (такт) задающего генератора, при 16 МГц это 0.0625 микросекунды. Второй важный момент для понимания: таймер-счётчик работает и считает импульсы параллельно вычислительному ядру. Именно поэтому генерация ШИМ сигнала даже на высокой частоте абсолютно не влияет на выполнение кода – оно всё происходит параллельно.

В ардуино нано (atmega328) у нас три таких таймера, и каждый может активировать независимое прерывание по своему периоду. Что касается счета времени: функции millis() и micros() как раз таки работают на прерывании таймера 0. Если перенастроить таймер 0 – у нас пропадёт корректный счёт времени (и, возможно, ШИМ на пинах 5 и 6). Некоторые библиотеки также используют прерывания таймеров, например Servo использует первый, а встроенная функция tone() – второй. Также мы обсуждали в уроках что таймеры занимаются генерацией ШИМ сигнала на своих пинах, и при перенастройке таймера ШИМ может отключиться, начать работать в другом режиме или изменить частоту.

В отличие от генерации ШИМ сигнала и аппаратных прерываний, управление прерываниями по таймерам не реализовано разработчиками Ардуино в ядре и стандартных библиотеках, поэтому работать с прерываниями будем при помощи сторонних библиотек. Можно работать с таймером напрямую, как описано в даташите, но это не входит в данный курс уроков. Для первого и второго таймеров можно найти старые библиотеки, называются timerOne и timerTwo. У меня есть своя библиотека – GyverTimers, которая позволяет гибко настроить все таймеры на atmega328 (Arduino UNO/Nano) и atmega2560 (Arduino Mega). Скачать библиотеку можно по прямой ссылке, также про нее есть отдельная страница у меня на сайте, с описанием, документацией и примерами. Рассмотрим основные инструменты библиотеки.

Библиотека GyverTimers


Библиотека GyverTimers позволяет генерировать прерывания по таймеру с заданной частотой на выбранном канале таймера или на нескольких каналах сразу со сдвигом по фазе: прерывания будут происходить с одинаковой частотой, но “сдвинуты” друг относительно друга. Также можно задать действие для вывода таймера: включить/выключить/переключить: таймер будет управлять выбранным пином независимо от вычислительного ядра МК, таким образом можно генерировать меандр (квадратный сигнал), причём как однотактный, так и двух- и трёхтактный с настраиваемым сдвигом по фазе.

Для Arduino Nano/UNO/Pro Mini доступно три таймера: Timer0, Timer1, Timer2. Для Arduino MEGA – пять: Timer0, Timer1, Timer2, Timer3, Timer4, Timer5. В библиотеке таймеры описаны как объекты, обращение происходит как обычно через точку. Например Timer1.stop();

Таблица таймеров ATmega328p


ТаймерРазрядностьЧастотыПериодыВыходыПин ArduinoПин МК
Timer08 бит61 Гц.. 1 МГц16 384.. 1 мксCHANNEL_AD6PD6
CHANNEL_BD5PD5
Timer116 бит0.24 Гц.. 1 МГц4 200 000.. 1 мксCHANNEL_AD9PB1
CHANNEL_BD10PB2
Timer28 бит61 Гц.. 1 МГц16 384.. 1 мксCHANNEL_AD11PB3
CHANNEL_BD3PD3

Таблица таймеров ATmega2560


ТаймерРазрядностьЧастотыПериодыВыходыПин ArduinoПин МК
Timer08 бит61 Гц.. 1 МГц16 384.. 1 мксCHANNEL_A13PB7
CHANNEL_B4PG5
Timer116 бит0.24 Гц.. 1 МГц4 200 000.. 1 мксCHANNEL_A11PB5
CHANNEL_B12PB6
CHANNEL_C13PB7
Timer28 бит61 Гц.. 1 МГц16 384.. 1 мксCHANNEL_A10PB4
CHANNEL_B9PH6
Timer316 бит0.24 Гц.. 1 МГц4 200 000.. 1 мксCHANNEL_A5PE3
CHANNEL_B2PE4
CHANNEL_C3PE5
Timer416 бит0.24 Гц.. 1 МГц4 200 000.. 1 мксCHANNEL_A6PH3
CHANNEL_B7PH4
CHANNEL_C8PH5
Timer516 бит0.24 Гц.. 1 МГц4 200 000.. 1 мксCHANNEL_A46PL3
CHANNEL_B45PL4
CHANNEL_C44PL5

Максимальный период


В таблице выше приведены диапазоны для 16 МГц тактирования. Для другого системного клока максимальный период считается по формуле, где F_CPU – системная частота в Гц:

  • 8 бит таймеры: (1000000UL / F_CPU) * (1024 * 256)
  • 16 бит таймеры: (1000000UL / F_CPU) * (1024 * 65536)

Настройка частоты/периода


  • setPeriod(период); – установка периода в микросекундах и запуск таймера. Возвращает реальный период в мкс (точность ограничена разрешением таймера).
  • setFrequency(частота); – установка частоты в Герцах и запуск таймера. Возвращает реальную частоту в Гц (точность ограничена разрешением таймера).
  • setFrequencyFloat(частота float); – установка частоты в Герцах и запуск таймера, разрешены десятичные дроби. Возвращает реальную частоту (точность ограничена разрешением таймера).

Контроль работы таймера


  • pause(); – приостановить счёт таймера, не сбрасывая счётчик
  • resume(); – продолжить счёт после паузы
  • stop(); – остановить счёт и сбросить счётчик
  • restart(); – перезапустить таймер (сбросить счётчик)

Прерывания


  • enableISR(канал, фаза); – запустить прерывания на выбранном канале с выбранным сдвигом фазы. Если ничего не указывать, будет выбран канал A и фаза 0
    • Канал – CHANNEL_A, CHANNEL_B или CHANNEL_С (см. таблицу выше!)
    • Фаза – численное значение 0-359
  • disableISR(канал); – отключить прерывания на выбранном канале. Если ничего не указывать, будет выбран канал A

Библиотека даёт прямой доступ к прерыванию без “Ардуиновских” attachInterrupt, что позволяет сократить время вызова функции-обработчика прерывания. Прерывание с настроенной частотой будет обрабатываться в блоке вида ISR(канал) {}, пример:

ISR(TIMER1_A) {
// ваш код
}

ISR(TIMER1_B) {
// ваш код
}

ISR(TIMER2_B) {
// ваш код
}

ISR(TIMER0_A) {
// ваш код
}

Аппаратные выходы


  • outputEnable(канал, режим); – включить управление аппаратным выходом таймера
    • Канал: CHANNEL_A или CHANNEL_B (+ CHANNEL_C у ATmega2560, см. таблицу таймеров).
    • Режим: TOGGLE_PIN, CLEAR_PIN, SET_PIN (переключить/выключить/включить пин по срабатыванию таймера)
  • outputDisable(канал); – отключить выход таймера
    • Канал: CHANNEL_A или CHANNEL_B (+ CHANNEL_C у Mega2560, см. таблицу таймеров)
  • outputState(канал, состояние); – вручную сменить состояние канала. Например для установки каналов в разное состояние для запуска генерации двухтактного меандра.
    • Канал: CHANNEL_A или CHANNEL_B (+ CHANNEL_C у ATmega2560, см. таблицу таймеров).
    • Состояние: HIGH или LOW

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

Сдвиг фазы (с 1.6)


При помощи phaseShift(source, angle) можно сдвинуть прерывания или переключения пинов на выбранном канале source по фазе angle – угол сдвига в градусах от 0 до 360.

  • У 8-битных таймеров можно задать сдвиг только у второго канала (CHANNEL_B)
  • У 16-битных можно двигать все три канала

Настройка по умолчанию


При помощи метода setDefault() можно сбросить настройки таймера на “Ардуиновские” умолчания: частоту и режим работы.

Примеры


// Демонстрация всех функций библиотеки

#include "GyverTimers.h"

void setup() {
  // Перенастроить таймер и задать ему период или частоту
  // Все функции возвращают реальный период / частоту, которые могут отличаться от введенных
  Timer2.setPeriod(1000);           // Задать конкретный период 1000 мкс (~ 1000 гц), вернет реальный период в мкс
  Timer0.setFrequency(250);         // Задать частоту прерываний таймера в Гц, вернет реальную частоту в герцах
  Timer1.setFrequencyFloat(50.20);  // Задать частоту более точно, в дробных числах, актуально для низких частот и таймера 1
  // С этого момента таймер уже перенастроен и гоняет с выьранной частотой / периодом

  // Подключить прерывание таймера, с этого момента прерывания начнут вызываться
  Timer0.enableISR();               // Подключить стандартное прерывание, канал А, без сдига фаз
  Timer2.enableISR(CHANNEL_B, 180); // Подключить прерывание таймера 2, канал B, начальная фаза - 180 градусов
  Timer1.enableISR(CHANNEL_A, 60);  // Подключить прерывание канала А, задать фазу для канала А доступно только для таймера 1!
  Timer1.enableISR(CHANNEL_B, 120); // Подключить второе прерывание таймера 1, и задать фазовый сдвиг для этого потока
  // Прерывание уже начнет вызываться

  // Если вдруг прерывание нужно отключить, не останавливая таймер
  Timer1.disableISR(CHANNEL_B);
  // С этого момента прерывание B больше не будет вызываться

  // Если нужно приостановить таймер ПОЛНОСТЬЮ, аппаратно
  Timer2.pause();
  // С этого момента таймер стоит на месте, содержимое счетчика остается нетронутым

  // Теперь таймер можно вернуть в строй
  Timer2.resume();
  // Таймер продолжил считать с того же места

  // Если нужно полностью остановить таймер и сбросить содержимое счетчика
  Timer1.stop();
  // Таймер стоит, счетчик сброшен

  // Возвращаем таймер в строй
  Timer1.restart();
  // Таймер перезапущен, начал считать с начала

  // Если нужно вернуть стандартные Arduino - настройки таймера
  Timer0.setDefault();
  // Теперь таймер работает в станлартном режиме
}

// векторы прерываний
ISR(TIMER1_A) {

}

ISR(TIMER1_B) {

}

ISR(TIMER2_B) {

}

ISR(TIMER0_A) {

}

void loop() {

}

// Пример простой генерации прерываний аппаратным таймером
#include "GyverTimers.h"

void setup() {
  Serial.begin(9600);
  Timer1.setFrequency(3);               // Высокоточный таймер 1 для первого прерывания, частота - 3 Герца
  //Timer1.setPeriod(333333);           // то же самое! Частота 3 Гц это период 333 333 микросекунд
  //Timer1.setFrequencyFloat(4.22);     // Если нужна дробная частота в Гц  
  Timer1.enableISR();                   // Запускаем прерывание (по умолч. канал А)

  // запустим второй таймер
  Timer2.setPeriod(1000000);     // Устанавливаем период таймера 1000000 мкс -> 1 гц
  Timer2.enableISR(CHANNEL_A);   // Или просто .enableISR(), запускаем прерывание на канале А таймера 2
  pinMode(13, OUTPUT);           // будем мигать
}

void loop() {}

// Прерывание А таймера 1
ISR(TIMER1_A) {  // пишем  в сериал
  Serial.println("timer1");
}

// Прерывание А таймера 2
ISR(TIMER2_A) {   // генерируем меандр 1 кгц, мигаем
  digitalWrite(13, !digitalRead(13));
  //Serial.println("timer2");
}

// Пример генерации двухканальных прерываний на таймере с РАВНЫМ периодом, но сдвинутых по фазе
// два потока прерываний с сдвигом 180 градусов (полная инверсия)

#include "GyverTimers.h"

void setup() {
  Serial.begin(9600);

  Serial.print("Real timer frequency is : ");        // Выведем реальную частоту, реальная может отличаться от заданой (ограничено разрешением таймера)
  Serial.println(Timer1.setFrequencyFloat(2.50));    // Частота прерываний - 2.5 гц , используй .setFrequency(...) для целых чисел
  delay(1000);
  Timer1.enableISR(CHANNEL_A, 0);     // Первый канал - А, начальная фаза - 0 градусов
  Timer1.enableISR(CHANNEL_B, 180);   // Второй канал - B, начальная фаза - 180 градусов
}

void loop() {}

// два прерывания на одном таймере
ISR(TIMER1_A) {
  Serial.println(" Channel A interrupt !");   // Прерывание А
}

ISR(TIMER1_B) {
  Serial.println(" Channel B interrupt !");   // Прерывание B
}

//Пример генерации меандра на таймере 2 , канале B (D3 на Arduino UNO) 
#include "GyverTimers.h"

void setup() {
  pinMode(3, OUTPUT);                           // настроить пин как выход

  // из-за особенности генерации меандра таймером
  // частоту нужно указывать в два раза больше нужной!
  Timer2.setFrequency(500 * 2);                 // настроить частоту таймера в Гц
  Timer2.outputEnable(CHANNEL_B, TOGGLE_PIN);   // в момент срабатывания таймера пин будет переключаться
}

void loop() {
}

// Пример генерации двухтактного меандра на таймере 2 (пины D3 и D11)
#include "GyverTimers.h"

void setup() {
  pinMode(3, OUTPUT);                           // настроить пин как выход
  pinMode(11, OUTPUT);                          // настроить пин как выход

  // из-за особенности генерации меандра таймером
  // частоту нужно указывать в два раза больше нужной!
  Timer2.setFrequency(15000 * 2);               // настроить частоту в Гц и запустить таймер. Меандр на 15 кГц
  Timer2.outputEnable(CHANNEL_A, TOGGLE_PIN);   // в момент срабатывания таймера пин будет переключаться
  Timer2.outputEnable(CHANNEL_B, TOGGLE_PIN);   // в момент срабатывания таймера пин будет переключаться
  Timer2.outputState(CHANNEL_A, HIGH);          // задаём начальное состояние пина 11
  Timer2.outputState(CHANNEL_B, LOW);           // задаём начальное состояние пина 3
}

void loop() {
}

Видео


Важные страницы