Arduino и прерывания таймера
Привет, Хабр! Представляю вашему вниманию перевод статьи «Timer interrupts» автора
Предисловие
Плата Arduino позволяет быстро и минимальными средствами решить самые разные задачи. Но там где нужны произвольные интервалы времени (периодический опрос датчиков, высокоточные ШИМ сигналы, импульсы большой длительности) стандартные библиотечные функции задержки не удобны. На время их действия скетч приостанавливается и управлять им становится невозможно.
В подобной ситуации лучше использовать встроенные AVR таймеры. Как это сделать и не заблудиться в технических дебрях даташитов, рассказывает удачная статья, перевод которой и предлагается вашему вниманию.
В этой статье обсуждаются таймеры AVR и Arduino и то, как их использовать в Arduino проектах и схемах пользователя.
Что такое таймер?
Как и в повседневной жизни в микроконтроллерах таймер это некоторая вещь, которая может подать сигнал в будущем, в тот момент который вы установите. Когда этот момент наступает, вызывается прерывание микроконтроллера, напоминая ему что-нибудь сделать, например выполнить определенный фрагмент кода.
Таймеры, как и внешние прерывания, работают независимо от основной программы. Вместо выполнения циклов или повторяющегося вызова задержки millis() вы можете назначить таймеру делать свою работу, в то время как ваш код делает другие вещи.
Итак, предположим, что имеется устройство, которое должно что-то делать, например мигать светодиодом каждые 5 секунд. Если не использовать таймеры, а писать обычный код, то надо установить переменную в момент зажигания светодиода и постоянно проверять не наступил ли момент ее переключения. С прерыванием по таймеру вам достаточно настроить прерывание, и затем запустить таймер. Светодиод будет мигать точно вовремя, независимо от действий основной программы.
Как работает таймер?
Он действует путем увеличения переменной, называемой счетным регистром. Счетный регистр может считать до определенной величины, зависящей от его размера. Таймер увеличивает свой счетчик раз за разом пока не достигнет максимальной величины, в этой точке счетчик переполнится и сбросится обратно в ноль. Таймер обычно устанавливает бит флага, чтобы дать вам знать, что переполнение произошло.
Вы можете проверять этот флаг вручную или можете сделать таймерный переключатель — вызывать прерывание автоматически в момент установки флага. Подобно всяким другим прерываниям вы можете назначить служебную подпрограмму прерывания (Interrupt Service Routine или ISR), чтобы выполнить заданный код, когда таймер переполнится. ISR сама сбросит флаг переполнения, поэтому использование прерываний обычно лучший выбор из-за простоты и скорости.
Чтобы увеличивать значения счетчика через точные интервалы времени, таймер надо подключить к тактовому источнику. Тактовый источник генерирует постоянно повторяющийся сигнал. Каждый раз, когда таймер обнаруживает этот сигнал, он увеличивает значение счетчика на единицу. Поскольку таймер работает от тактового источника, наименьшей измеряемой единицей времени является период такта. Если вы подключите тактовый сигнал частотой 1 МГц, то разрешение таймера (или период таймера) будет:
T = 1 / f (f это тактовая частота)
T = 1 / 1 МГц = 1 / 10^6 Гц
T = (1 ∗ 10^-6) с
Таким образом разрешение таймера одна миллионная доля секунды. Хотя вы можете применить для таймеров внешний тактовый источник, в большинстве случаев используется внутренний источник самого чипа.
Типы таймеров
В стандартных платах Arduino на 8 битном AVR чипе имеется сразу несколько таймеров. У чипов Atmega168 и Atmega328 есть три таймера Timer0, Timer1 и Timer2. Они также имеют сторожевой таймер, который можно использовать для защиты от сбоев или как механизм программного сброса. Вот некоторые особенности каждого таймера.
Timer0:
Timer0 является 8 битным таймером, это означает, что его счетный регистр может хранить числа вплоть до 255 (т. е. байт без знака). Timer0 используется стандартными временными функциями Arduino такими как delay() и millis(), так что лучше не запутывать его если вас заботят последствия.
Timer1:
Timer1 это 16 битный таймер с максимальным значением счета 65535 (целое без знака). Этот таймер использует библиотека Arduino Servo, учитывайте это если применяете его в своих проектах.
Timer2:
Timer2 — 8 битный и очень похож на Timer0. Он используется в Arduino функции tone().
Timer3, Timer4, Timer5:
Чипы ATmega1280 и ATmega2560 (установлены в вариантах Arduino Mega) имеют три добавочных таймера. Все они 16 битные и работают аналогично Timer1.
Конфигурация регистров
Для того чтобы использовать эти таймеры в AVR есть регистры настроек. Таймеры содержат множество таких регистров. Два из них — регистры управления таймера/счетчика содержат установочные переменные и называются TCCRxA и TCCRxB, где x — номер таймера (TCCR1A и TCCR1B, и т. п.). Каждый регистр содержит 8 бит и каждый бит хранит конфигурационную переменную. Вот сведения из даташита Atmega328:
TCCR1A | ||||||||
---|---|---|---|---|---|---|---|---|
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0x80 | COM1A1 | COM1A0 | COM1B1 | COM1B0 | — | — | WGM11 | WGM10 |
ReadWrite | RW | RW | RW | RW | R | R | RW | RW |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
TCCR1B | ||||||||
---|---|---|---|---|---|---|---|---|
Бит | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
0x81 | ICNC1 | ICES1 | — | WGM13 | WGM12 | CS12 | CS11 | CS10 |
ReadWrite | RW | RW | R | RW | RW | RW | RW | RW |
Начальное значение | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Наиболее важными являются три последние бита в TCCR1B: CS12, CS11 и CS10. Они определяют тактовую частоту таймера. Выбирая их в разных комбинациях вы можете приказать таймеру действовать на различных скоростях. Вот таблица из даташита, описывающая действие битов выбора:
CS12 | CS11 | CS10 | Действие |
---|---|---|---|
0 | 0 | 0 | Нет тактового источника (Timer/Counter остановлен) |
0 | 0 | 1 | clk_io/1 (нет деления) |
0 | 1 | 0 | clk_io/8 (делитель частоты) |
0 | 1 | 1 | clk_io/64 (делитель частоты) |
1 | 0 | 0 | clk_io/256 (делитель частоты) |
1 | 0 | 1 | clk_io/1024 (делитель частоты) |
1 | 1 | 0 | Внешний тактовый источник на выводе T1. Тактирование по спаду |
1 | 1 | 1 | Внешний тактовый источник на выводе T1. Тактирование по фронту |
По умолчанию все эти биты установлены на ноль.
Допустим вы хотите, чтобы Timer1 работал на тактовой частоте с одним отсчетом на период. Когда он переполнится, вы хотите вызвать подпрограмму прерывания, которая переключает светодиод, подсоединенный к ножке 13, в состояние включено или выключено. Для этого примера запишем Arduino код, но будем использовать процедуры и функции библиотеки avr-libc всегда, когда это не делает вещи слишком сложными. Сторонники чистого AVR могут адаптировать код по своему усмотрению.
Сначала инициализируем таймер:
Регистр TIMSK1 это регистр маски прерываний Таймера/Счетчика1. Он контролирует прерывания, которые таймер может вызвать. Установка бита TOIE1 приказывает таймеру вызвать прерывание когда таймер переполняется. Подробнее об этом позже.
Когда вы устанавливаете бит CS10, таймер начинает считать и, как только возникает прерывание по переполнению, вызывается ISR(TIMER1_OVF_vect). Это происходит всегда когда таймер переполняется.
Дальше определим функцию прерывания ISR:
Сейчас мы можем определить цикл loop() и переключать светодиод независимо от того, что происходит в главной программе. Чтобы выключить таймер, установите TCCR1B=0 в любое время.
Как часто будет мигать светодиод?
Timer1 установлен на прерывание по переполнению и давайте предположим, что вы используете Atmega328 с тактовой частотой 16 МГц. Поскольку таймер 16-битный, он может считать до максимального значения (2^16 – 1), или 65535. При 16 МГц цикл выполняется 1/(16 ∗ 10^6) секунды или 6.25e-8 с. Это означает что 65535 отсчетов произойдут за (65535 ∗ 6.25e-8 с) и ISR будет вызываться примерно через 0,0041 с. И так раз за разом, каждую четырехтысячную секунды. Это слишком быстро, чтобы увидеть мерцание.
Если мы подадим на светодиод очень быстрый ШИМ сигнал с 50% заполнением, то свечение будет казаться непрерывным, но менее ярким чем обычно. Подобный эксперимент показывает удивительную мощь микроконтроллеров — даже недорогой 8-битный чип может обрабатывать информацию намного быстрей чем мы способны обнаружить.
Делитель таймера и режим CTC
Чтобы управлять периодом, вы можете использовать делитель, который позволяет поделить тактовый сигнал на различные степени двойки и увеличить период таймера. Например, вы бы хотели мигания светодиода с интервалом одна секунда. В регистре TCCR1B есть три бита CS устанавливающие наиболее подходящее разрешение. Если установить биты CS10 и CS12 используя:
то частота тактового источника поделится на 1024. Это дает разрешение таймера 1/(16 ∗ 10^6 / 1024) или 6.4e-5 с. Теперь таймер будет переполняться каждые (65535 ∗ 6.4e-5с) или за 4,194с. Это слишком долго.
Но есть и другой режим AVR таймера. Он называется сброс таймера по совпадению или CTC. Вместо счета до переполнения, таймер сравнивает свой счетчик с переменой которая ранее сохранена в регистре. Когда счет совпадет с этой переменной, таймер может либо установить флаг, либо вызвать прерывание, точно так же как и в случае переполнения.
Чтобы использовать режим CTC надо понять, сколько циклов вам нужно, чтобы получить интервал в одну секунду. Предположим, что коэффициент деления по-прежнему равен 1024.
Расчет будет следующий:
Вы должны добавить дополнительную единицу к числу отсчетов потому что в CTC режиме при совпадении счетчика с заданным значением он сбросит сам себя в ноль. Сброс занимает один тактовый период, который надо учесть в расчетах. Во многих случаях ошибка в один период не слишком значима, но в высокоточных задачах она может быть критичной.
Функция настройки setup() будет такая:
Также нужно заменить прерывание по переполнению на прерывание по совпадению:
Сейчас светодиод будет зажигаться и гаснуть ровно на одну секунду. А вы можете делать все что угодно в цикле loop(). Пока вы не измените настройки таймера, программа никак не связана с прерываниями. У вас нет ограничений на использование таймера с разными режимами и настройками делителя.
Вот полный стартовый пример который вы можете использовать как основу для собственных проектов:
Помните, что вы можете использовать встроенные ISR функции для расширения функций таймера. Например вам требуется опрашивать датчик каждые 10 секунд. Но установок таймера, обеспечивающих такой долгий счет без переполнения нет. Однако можно использовать ISR чтобы инкрементировать счетную переменную раз в секунду и затем опрашивать датчик когда переменная достигнет 10. С использованием СТС режима из предыдущего примера прерывание могло бы выглядеть так:
Поскольку переменная будет модифицироваться внутри ISR она должна быть декларирована как volatile. Поэтому, при описании переменных в начале программы вам надо написать:
Послесловие переводчика
В свое время эта статья сэкономила мне немало времени при разработке прототипа измерительного генератора. Надеюсь, что она окажется полезной и другим читателям.
Прерывания по таймерам Arduino. GyverTimers v1.9
ОБНОВЛЕНИЯ
- v1.7 – поправлена документация
- v1.8 – исправлен баг с макс периодом
- v1.9 – исправлен баг с возвращаемым 2х периодом
ТЕОРИЯ
БИБЛИОТЕКА
GyverTimers v1.9
Настройка и контроль прерываний по аппаратным таймерам AVR:
- Поддерживаются все три таймера на ATmega328 и шесть таймеров на ATmega2560;
- Настройка периода (мкс) и частоты (Гц) прерываний:
- 8 бит таймеры: 61 Гц – 1 МГц (16 384 мкс – 1 мкс);
- 16 бит таймеры: 0.24 Гц – 1 МГц (4 200 000 мкс – 1 мкс);
- Автоматическая корректировка настройки периода от частоты тактирования (F_CPU);
- Функция возвращает точный установившийся период/частоту для отладки (частота ограничена разрешением таймера);
- Поддержка многоканального режима работы: один таймер вызывает 2 (ATmega328) или 3 (ATmega2560, таймеры 1, 3, 4, 5) прерывания с настраиваемым сдвигом по фазе 0-360 градусов;
- Настраиваемое действие аппаратного вывода таймера по прерыванию: высокий сигнал, низкий сигнал, переключение. Позволяет генерировать меандр (одно- и двухтактный);
- Контроль работы таймера: старт/стоп/пауза/продолжить/инициализация;
- Разработано by Egor ‘Nich1con’ Zaharov специально для AlexGyver
Поддерживаемые платформы: платы на ATmega328p (Arduino Nano/UNO/Pro Mini), ATmega2560 (Arduino Mega).
Прерывания по таймеру
Мы с вами уже много раз рассматривали конструкцию таймера на 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 бит | 61 Гц.. 1 МГц | 16 384.. 1 мкс | CHANNEL_A | D6 | PD6 |
CHANNEL_B | D5 | PD5 | ||||
Timer1 | 16 бит | 0.24 Гц.. 1 МГц | 4 200 000.. 1 мкс | CHANNEL_A | D9 | PB1 |
CHANNEL_B | D10 | PB2 | ||||
Timer2 | 8 бит | 61 Гц.. 1 МГц | 16 384.. 1 мкс | CHANNEL_A | D11 | PB3 |
CHANNEL_B | D3 | PD3 |
Таблица таймеров ATmega2560
Таймер | Разрядность | Частоты | Периоды | Выходы | Пин Arduino | Пин МК |
Timer0 | 8 бит | 61 Гц.. 1 МГц | 16 384.. 1 мкс | CHANNEL_A | 13 | PB7 |
CHANNEL_B | 4 | PG5 | ||||
Timer1 | 16 бит | 0.24 Гц.. 1 МГц | 4 200 000.. 1 мкс | CHANNEL_A | 11 | PB5 |
CHANNEL_B | 12 | PB6 | ||||
CHANNEL_C | 13 | PB7 | ||||
Timer2 | 8 бит | 61 Гц.. 1 МГц | 16 384.. 1 мкс | CHANNEL_A | 10 | PB4 |
CHANNEL_B | 9 | PH6 | ||||
Timer3 | 16 бит | 0.24 Гц.. 1 МГц | 4 200 000.. 1 мкс | CHANNEL_A | 5 | PE3 |
CHANNEL_B | 2 | PE4 | ||||
CHANNEL_C | 3 | PE5 | ||||
Timer4 | 16 бит | 0.24 Гц.. 1 МГц | 4 200 000.. 1 мкс | CHANNEL_A | 6 | PH3 |
CHANNEL_B | 7 | PH4 | ||||
CHANNEL_C | 8 | PH5 | ||||
Timer5 | 16 бит | 0.24 Гц.. 1 МГц | 4 200 000.. 1 мкс | CHANNEL_A | 46 | PL3 |
CHANNEL_B | 45 | PL4 | ||||
CHANNEL_C | 44 | PL5 |
Максимальный период
В таблице выше приведены диапазоны для 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(канал) <> , пример:
Аппаратные выходы
- 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() можно сбросить настройки таймера на “Ардуиновские” умолчания: частоту и режим работы.