Привет, Хабр! Представляю вашему вниманию перевод статьи "Timer interrupts" автора E.
Предисловие
Плата 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 могут адаптировать код по своему усмотрению.
Сначала инициализируем таймер:
// avr-libc library includes
#include <avr/io.h>
#include <avr/interrupt.h>
#define LEDPIN 13
void setup()
{
pinMode(LEDPIN, OUTPUT);
// инициализация Timer1
cli(); // отключить глобальные прерывания
TCCR1A = 0; // установить TCCR1A регистр в 0
TCCR1B = 0;
// включить прерывание Timer1 overflow:
TIMSK1 = (1 << TOIE1);
// Установить CS10 бит так, чтобы таймер работал при тактовой частоте:
TCCR1B |= (1 << CS10);
sei(); // включить глобальные прерывания
}
Регистр TIMSK1 это регистр маски прерываний Таймера/Счетчика1. Он контролирует прерывания, которые таймер может вызвать. Установка бита TOIE1 приказывает таймеру вызвать прерывание когда таймер переполняется. Подробнее об этом позже.
Когда вы устанавливаете бит CS10, таймер начинает считать и, как только возникает прерывание по переполнению, вызывается ISR(TIMER1_OVF_vect). Это происходит всегда когда таймер переполняется.
Дальше определим функцию прерывания ISR:
ISR(TIMER1_OVF_vect)
{
digitalWrite(LEDPIN, !digitalRead(LEDPIN));
}
Сейчас мы можем определить цикл 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 используя:
TCCR1B |= (1 << CS10);
TCCR1B |= (1 << CS12);
то частота тактового источника поделится на 1024. Это дает разрешение таймера 1/(16 ? 10^6 / 1024) или 6.4e-5 с. Теперь таймер будет переполняться каждые (65535 ? 6.4e-5с) или за 4,194с. Это слишком долго.
Но есть и другой режим AVR таймера. Он называется сброс таймера по совпадению или CTC. Вместо счета до переполнения, таймер сравнивает свой счетчик с переменой которая ранее сохранена в регистре. Когда счет совпадет с этой переменной, таймер может либо установить флаг, либо вызвать прерывание, точно так же как и в случае переполнения.
Чтобы использовать режим CTC надо понять, сколько циклов вам нужно, чтобы получить интервал в одну секунду. Предположим, что коэффициент деления по-прежнему равен 1024.
Расчет будет следующий:
(target time) = (timer resolution) * (# timer counts + 1)
(# timer counts + 1) = (target time) / (timer resolution)
(# timer counts + 1) = (1 s) / (6.4e-5 s)
(# timer counts + 1) = 15625
(# timer counts) = 15625 - 1 = 15624
Вы должны добавить дополнительную единицу к числу отсчетов потому что в CTC режиме при совпадении счетчика с заданным значением он сбросит сам себя в ноль. Сброс занимает один тактовый период, который надо учесть в расчетах. Во многих случаях ошибка в один период не слишком значима, но в высокоточных задачах она может быть критичной.
Функция настройки setup() будет такая:
void setup()
{
pinMode(LEDPIN, OUTPUT);
// инициализация Timer1
cli(); // отключить глобальные прерывания
TCCR1A = 0; // установить регистры в 0
TCCR1B = 0;
OCR1A = 15624; // установка регистра совпадения
TCCR1B |= (1 << WGM12); // включение в CTC режим
// Установка битов CS10 и CS12 на коэффициент деления 1024
TCCR1B |= (1 << CS10);
TCCR1B |= (1 << CS12);
TIMSK1 |= (1 << OCIE1A); // включение прерываний по совпадению
sei(); // включить глобальные прерывания
}
Также нужно заменить прерывание по переполнению на прерывание по совпадению:
ISR(TIMER1_COMPA_vect)
{
digitalWrite(LEDPIN, !digitalRead(LEDPIN));
}
Сейчас светодиод будет зажигаться и гаснуть ровно на одну секунду. А вы можете делать все что угодно в цикле loop(). Пока вы не измените настройки таймера, программа никак не связана с прерываниями. У вас нет ограничений на использование таймера с разными режимами и настройками делителя.
Вот полный стартовый пример который вы можете использовать как основу для собственных проектов:
// Arduino таймер CTC прерывание
// avr-libc library includes
#include <avr/io.h>
#include <avr/interrupt.h>
#define LEDPIN 13
void setup()
{
pinMode(LEDPIN, OUTPUT);
// инициализация Timer1
cli(); // отключить глобальные прерывания
TCCR1A = 0; // установить регистры в 0
TCCR1B = 0;
OCR1A = 15624; // установка регистра совпадения
TCCR1B |= (1 << WGM12); // включить CTC режим
TCCR1B |= (1 << CS10); // Установить биты на коэффициент деления 1024
TCCR1B |= (1 << CS12);
TIMSK1 |= (1 << OCIE1A); // включить прерывание по совпадению таймера
sei(); // включить глобальные прерывания
}
void loop()
{
// основная программа
}
ISR(TIMER1_COMPA_vect)
{
digitalWrite(LEDPIN, !digitalRead(LEDPIN));
}
Помните, что вы можете использовать встроенные ISR функции для расширения функций таймера. Например вам требуется опрашивать датчик каждые 10 секунд. Но установок таймера, обеспечивающих такой долгий счет без переполнения нет. Однако можно использовать ISR чтобы инкрементировать счетную переменную раз в секунду и затем опрашивать датчик когда переменная достигнет 10. С использованием СТС режима из предыдущего примера прерывание могло бы выглядеть так:
ISR(TIMER1_COMPA_vect)
{
seconds++;
if(seconds == 10)
{
seconds = 0;
readSensor();
}
}
Поскольку переменная будет модифицироваться внутри ISR она должна быть декларирована как volatile. Поэтому, при описании переменных в начале программы вам надо написать:
volatile byte seconds;
Послесловие переводчика
В свое время эта статья сэкономила мне немало времени при разработке прототипа измерительного генератора. Надеюсь, что она окажется полезной и другим читателям.
Комментарии (19)
Polaris99
24.05.2019 13:41Казалось бы, при чем здесь Ардуино? Ну а вообще прекрасная иллюстрация принципов работы максимально простых средств разработки, где почти все спрятано от наивного пользователя под капот.
VT100
25.05.2019 19:46Как сказать…
Например, вместо того, что-бы получить увеличение периода до 2 сек (Сейчас светодиод будет зажигаться и гаснуть ровно на одну секунду) записью в TCTNT1 соответствующего значения (0x10000 — F_CPU(Гц)*1(сек)/1024 = 0xC2F7, что вызовет переполнение за меньшее время) зачем-то используется режим CTC.Polaris99
25.05.2019 20:15А Вы пробовали так уже? Ничего, что таким образом стабильность периода вообще ничем не гарантируется? Пока в прерывание попадете, пока новое значение в счетчик запишете, плавали — знаем.
VT100
26.05.2019 13:03Пока в прерывание попадете,
Вполне детерминированное время. ЕМНИП, 2..3 такта на переход к таблице и 2..3 такта на [R]JMP из таблицы.
Раз уж заходите с козырей — добавьте конкретики. Какой джиттер считаете неприемлемым для периода 2 сек.?
Считая вызов и пролог ISR примерно 30 тактов (называю по памяти для WinAVR) — ошибка получится 0,0001875 % для указанных в статье тактовой частоты 16 МГц и выходного периода 2 сек.
А если нужно ваще-точно — никто не мешает писать ISR на ассемблере и корректировать константу таймера на число тактов вызова и пролога (если, конечно, эта поправка не будет «съедена» предделителем). А можно скатиться до dithering и прочей паранойи.
iig
24.05.2019 14:58+1Статья должна называться «ATMEGA32 и прерывания таймера» ;) От ардуины тут только digitalWrite.
lamerok
24.05.2019 17:14Вызывать в прерывании readSensor() вообще не хорошая идея да и вообще не инлайн функции… Это может занять офигенно большое время плюс фиг его знает, что там в этой фукнции еще вызывается.
Yoooriii
24.05.2019 22:31+1Довелось и мне поиграться с таймерами когда собирал ретро часы на Ардуино, штука полезная.
На Ардуино я заменил штатный кварц на часовой (32768Гц), что позволило отказаться от внешнего модуля с часами. Получилось динамическая индикация + опрос клавиатуры 128Гц, управление внешним преобразователем (180В для запитки ИН12). Процессор работал на прерываниях, и уходил в спячку на остаток времени. Не уверен однако, что получившийся модуль можно продолжать называть Ардуино.iig
26.05.2019 08:39Месье понимает толк в прерываниях ;)
Не уверен, что sleep mode в часах на ИН12 имеет смысл.Yoooriii
26.05.2019 12:06У меня это был тестовый проект, и задачей было научиться работать с Atmega и выдавить из контроллера все что он умеет делать. В принципе получилось, не считая того, что играть мелодии по таймеру оказалось не очень удобно, и качество мелодий было далеко от идеала.
Собственно Ардуино это только проект для начинающих, с низким порогом вхождения, и в котором используется может 10% от того, что умеет Atmega. А дальше уже все зависит от фантазии.
stromen
25.05.2019 20:17Я как-то осваивал arduino и делал часы на самодельных 7-сегментниках. С регулировкой яркости. Яркость изменял как раз используя самодельный ШИМ на таймере2 (arduino nano). По совпадению включал разряд, по переполнению выключал.
Подсмотрел код в похожем проекте из интернета и «настроил» таймер вот так:
void setup() { TCCR2A |= (1 << WGM20)|(1 << WGM21); // Установка режима Fast PWM (WGM20 и WGM21) TCCR2B |= (1 << CS21); // Установка предделителя на 8 (CS21) TIMSK2 |= (1 << TOIE2)|(1 << OCIE2A); // Разрешение прерываний по переполнению (TOIE) и по совпадению (OCR2A) OCR2A = 1; // Начальное значение по совпадению sei(); }
Код, который я брал за основу был на С++ но не использовал фреймворк ардуино. То есть никаких setup'ов и loop'ов. Он был очень похож на тот, что в статье, но в нём отсутствовала первая часть, где обнуление регистров.
Измерил частоту осциллографом. Получил ~245 Гц. С предделителем 0 — ~490Гц. Методом «тыка» дошёл до того, что нужно обнулить регистры!
Ещё немного поэкспериментировал и получил такое:
TCCR2A = 0b00000011; // waveform generation mode (fast PWM) TCCR2B = 0b00000100; // prescaler (*100 => 64, ~970 Hz) TIMSK2 = 0b00000011; // interrupts (0 => compare B; 1 => compare A; 1 => overflow) OCR2A = 1; // compare
Мне так проще воспринимать. Я открыл даташит и смотрю в регистры, как они там написаны. Вижу, где стоят «единички» и что они означают. И места меньше занимает.
SeyranGV
28.05.2019 06:59а мне не понятна сама суть затеи, если человек понимает начинку и может писать на WinAVR, то зачем Arduino код, зачем смешивать, не проще написать какую то сложную вещь чисто на AtmelStudio, а простые вещи, ну или просто поиграться для экономии времени писать на Arduino?
biokin Автор
28.05.2019 14:02Например, если надо доработать готовый скетч. В моем случае это был экспромт. Требовалось срочно написать парсер целых чисел, который обрабатывал команды от компа по UART, а конспект по AVR куда-то потерялся. В ардуино это делается парой строк.
kzhyg
Как было бы хорошо, если бы volatile-переменные решали все проблемы..)
olartamonov
Особенно хорошо, что в данном примере volatile вообще никакую проблему не решает, а только жрёт ресурсы :)
Dima_Sharihin
volatile хорошо решает проблемы с производительностью