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


Мы с вами уже много раз рассматривали конструкцию таймера на 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 Пин МК
Timer0 8 бит 31 Гц – 1 МГц 32 258 – 1 мкс CHANNEL_A D6 PD6
CHANNEL_B D5 PD5
Timer1 16 бит 0.11 Гц – 1 МГц 9 000 000 – 1 мкс CHANNEL_A D9 PB1
CHANNEL_B D10 PB2
Timer2 8 бит 31 Гц – 1 МГц 32 258 – 1 мкс CHANNEL_A D11 PB3
CHANNEL_B D3 PD3

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


Таймер Разрядность Частоты Периоды Выходы Пин Arduino Пин МК
Timer0 8 бит 31 Гц – 1 МГц 32 258 – 1 мкс CHANNEL_A 13 PB7
CHANNEL_B 4 PG5
Timer1 16 бит 0.11 Гц – 1 МГц 9 000 000 – 1 мкс CHANNEL_A 11 PB5
CHANNEL_B 12 PB6
CHANNEL_C 13 PB7
Timer2 8 бит 31 Гц – 1 МГц 32 258 – 1 мкс CHANNEL_A 10 PB4
CHANNEL_B 9 PH6
Timer3 16 бит 0.11 Гц – 1 МГц 9 000 000 – 1 мкс CHANNEL_A 5 PE3
CHANNEL_B 2 PE4
CHANNEL_C 3 PE5
Timer4 16 бит 0.11 Гц – 1 МГц 9 000 000 – 1 мкс CHANNEL_A 6 PH3
CHANNEL_B 7 PH4
CHANNEL_C 8 PH5
Timer5 16 бит 0.11 Гц – 1 МГц 9 000 000 – 1 мкс CHANNEL_A 46 PL3
CHANNEL_B 45 PL4
CHANNEL_C 44 PL5

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


  • 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

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

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


При помощи метода 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 

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() {
}

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