Хабрахабр

[Из песочницы] 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) с

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

Типы таймеров

У чипов Atmega168 и Atmega328 есть три таймера Timer0, Timer1 и Timer2. В стандартных платах Arduino на 8 битном AVR чипе имеется сразу несколько таймеров. Вот некоторые особенности каждого таймера. Они также имеют сторожевой таймер, который можно использовать для защиты от сбоев или как механизм программного сброса.

е. Timer0:
Timer0 является 8 битным таймером, это означает, что его счетный регистр может хранить числа вплоть до 255 (т. Timer0 используется стандартными временными функциями Arduino такими как delay() и millis(), так что лучше не запутывать его если вас заботят последствия. байт без знака).

Этот таймер использует библиотека Arduino Servo, учитывайте это если применяете его в своих проектах. Timer1:
Timer1 это 16 битный таймер с максимальным значением счета 65535 (целое без знака).

Он используется в Arduino функции tone(). Timer2:
Timer2 — 8 битный и очень похож на Timer0.

Все они 16 битные и работают аналогично Timer1. Timer3, Timer4, Timer5:
Чипы ATmega1280 и ATmega2560 (установлены в вариантах Arduino Mega) имеют три добавочных таймера.

Конфигурация регистров

Таймеры содержат множество таких регистров. Для того чтобы использовать эти таймеры в AVR есть регистры настроек. п.). Два из них — регистры управления таймера/счетчика содержат установочные переменные и называются TCCRxA и TCCRxB, где x — номер таймера (TCCR1A и TCCR1B, и т. Вот сведения из даташита Atmega328: Каждый регистр содержит 8 бит и каждый бит хранит конфигурационную переменную.

Они определяют тактовую частоту таймера. Наиболее важными являются три последние бита в TCCR1B: CS12, CS11 и CS10. Вот таблица из даташита, описывающая действие битов выбора: Выбирая их в разных комбинациях вы можете приказать таймеру действовать на различных скоростях.

По умолчанию все эти биты установлены на ноль.

Когда он переполнится, вы хотите вызвать подпрограмму прерывания, которая переключает светодиод, подсоединенный к ножке 13, в состояние включено или выключено. Допустим вы хотите, чтобы Timer1 работал на тактовой частоте с одним отсчетом на период. Сторонники чистого AVR могут адаптировать код по своему усмотрению. Для этого примера запишем Arduino код, но будем использовать процедуры и функции библиотеки avr-libc всегда, когда это не делает вещи слишком сложными.

Сначала инициализируем таймер:

// 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));
}

Чтобы выключить таймер, установите TCCR1B=0 в любое время. Сейчас мы можем определить цикл loop() и переключать светодиод независимо от того, что происходит в главной программе.

Как часто будет мигать светодиод?

Поскольку таймер 16-битный, он может считать до максимального значения (2^16 – 1), или 65535. Timer1 установлен на прерывание по переполнению и давайте предположим, что вы используете Atmega328 с тактовой частотой 16 МГц. 25e-8 с. При 16 МГц цикл выполняется 1/(16 ∗ 10^6) секунды или 6. 25e-8 с) и ISR будет вызываться примерно через 0,0041 с. Это означает что 65535 отсчетов произойдут за (65535 ∗ 6. Это слишком быстро, чтобы увидеть мерцание. И так раз за разом, каждую четырехтысячную секунды.

Подобный эксперимент показывает удивительную мощь микроконтроллеров — даже недорогой 8-битный чип может обрабатывать информацию намного быстрей чем мы способны обнаружить. Если мы подадим на светодиод очень быстрый ШИМ сигнал с 50% заполнением, то свечение будет казаться непрерывным, но менее ярким чем обычно.

Делитель таймера и режим CTC

Например, вы бы хотели мигания светодиода с интервалом одна секунда. Чтобы управлять периодом, вы можете использовать делитель, который позволяет поделить тактовый сигнал на различные степени двойки и увеличить период таймера. Если установить биты CS10 и CS12 используя: В регистре TCCR1B есть три бита CS устанавливающие наиболее подходящее разрешение.

TCCR1B |= (1 << CS10);
TCCR1B |= (1 << CS12);

Это дает разрешение таймера 1/(16 ∗ 10^6 / 1024) или 6. то частота тактового источника поделится на 1024. Теперь таймер будет переполняться каждые (65535 ∗ 6. 4e-5 с. Это слишком долго. 4e-5с) или за 4,194с.

Он называется сброс таймера по совпадению или CTC. Но есть и другой режим AVR таймера. Когда счет совпадет с этой переменной, таймер может либо установить флаг, либо вызвать прерывание, точно так же как и в случае переполнения. Вместо счета до переполнения, таймер сравнивает свой счетчик с переменой которая ранее сохранена в регистре.

Предположим, что коэффициент деления по-прежнему равен 1024. Чтобы использовать режим CTC надо понять, сколько циклов вам нужно, чтобы получить интервал в одну секунду.

Расчет будет следующий:

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

Например вам требуется опрашивать датчик каждые 10 секунд. Помните, что вы можете использовать встроенные ISR функции для расширения функций таймера. Однако можно использовать ISR чтобы инкрементировать счетную переменную раз в секунду и затем опрашивать датчик когда переменная достигнет 10. Но установок таймера, обеспечивающих такой долгий счет без переполнения нет. С использованием СТС режима из предыдущего примера прерывание могло бы выглядеть так:

ISR(TIMER1_COMPA_vect)

}

Поэтому, при описании переменных в начале программы вам надо написать: Поскольку переменная будет модифицироваться внутри ISR она должна быть декларирована как volatile.

volatile byte seconds;

Послесловие переводчика

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть