Рубрики
Схемы на Arduino

Руководство по таймерам Arduino для начинающих

Платформа Arduino была первоначально спроектирована в 2005 году и первоначально предназначалась для того, чтобы люди, мало знакомые с электроникой и программированием, могли конструировать разнообразные электронные устройства. Но со временем она получила широкое распространение не только в кругах начинающих знакомиться с электроникой, но и среди профессионалов в сфере электроники.

В отличие от языков программирования для микроконтроллеров AVR, ARM, PIC, STM, в которых нужно хорошо представлять структуру этих микроконтроллеров, язык программирования для платформы Arduino исключительно простой и понятный. Достаточно легко понять, к примеру, как работают функции digitalWrite(), AnalogWrite(), Delay() и др. не вникая в суть машинного языка, который спрятан внутри них. Также не нужно вникать в суть различных регистров микроконтроллера, которые используются для управления этими процессами.

Но тем не менее, для лучшего понимания всех этих процессов, желательно все таки немного погрузиться внутрь этих процессов. К примеру, функция delay() используется для установки таймеров и битов регистров счета микроконтроллера AVR ATmega, являющегося основой платы Arduino.

В этой статье мы рассмотрим как без использования функции delay() управлять задержками в программе, непосредственно имея дело с регистрами микроконтроллера. Для этого мы будем использовать программную среду Arduino IDE. Мы будем устанавливать соответствующие биты регистра таймера и использовать прерывание переполнения таймера (Timer Overflow Interrupt) чтобы переключать (включать/выключать) состояние светодиода каждый раз когда происходит прерывание. Для контроля длительности задержки в схеме будут использоваться кнопки, с помощью которых можно будет изменять заранее загружаемое значение в биты таймера.

Что такое таймеры

Что же представляют собой таймеры в современной электронике? Фактически это определенный вид прерываний. Это простые часы, которые могут измерять длительность какого-нибудь события. Каждый микроконтроллер имеет встроенные часы (осциллятор), в плате Arduino Uno его роль выполняет кварцевый генератор, расположенный на плате, который работает на частоте 16 МГц. Частота влияет на скорость работы микроконтроллера. Чем выше частота, тем выше скорость работы. Таймер использует счетчик, который считает с определенной скоростью, зависящей от частоты осциллятора (кварцевого генератора). В плате Arduino Uno состояние счетчика увеличивается на 1 каждые 62 нано секунды (1/16000000 секунды). Фактически, это время за которое плата Arduino Uno переходит от одной инструкции к другой (то есть выполняет одну инструкцию).

Таймеры в Arduino Uno

В плате Arduino Uno используется три таймера:
Timer0: 8-битный таймер, используемый в таких функциях как delay(), millis().
Timer1: 16-битный таймер, используемый в библиотеке для управления серводвигателями.
Timer2: 8-битный таймер, используемый в функции tone().

Регистры таймеров в Arduino Uno

Для изменения конфигурации таймеров в плате Arduino Uno используются следующие регистры:

1. Timer/Counter Control Registers (TCCRnA/B) – управляющие регистры таймера/счетчика

Эти регистры содержат основные управляющие биты таймера и используются для управления предварительными делителями частоты (предделителями) таймера. Они также позволяют управлять режимом работы таймера с помощью битов WGM.

Формат этих регистров:

Предделитель (Prescaler)

Биты CS12, CS11, CS10 в регистре TCCR1B устанавливают коэффициент деления предделителя, то есть скорость часов таймера. В плате Arduino Uno можно установить коэффициент деления предделителя равный 1, 8, 64, 256, 1024.

2. Timer/Counter Register (TCNTn) – регистры таймера/счетчика

Эти регистры используются для управления счетчиками и для установки заранее загружаемого значения.

Формула для расчета заранее загружаемого значения (preloader value) для необходимого интервала времени (Time) в секундах выглядит следующим образом:

TCNTn = 65535 – (16×106xTime in sec / Prescaler Value)

Откуда берется величина 16х106? Здесь все просто — это переведенная в Герцы частота кварцевого генератора 16 МГц.

Чтобы для таймера 1 (timer1) задать время равное 2 секундам, при коэффициент деления предделителя (Prescaler Value) равном 1024, получим:

TCNT1 = 65535 – (16×106x2 / 1024) = 34285

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

Прерывания таймеров являются видом программных прерываний. В Arduino присутствуют следующие виды прерываний таймеров.

Прерывания переполнения таймера (Timer Overflow Interrupt)

Это прерывание происходит всегда, когда значение счетчика достигает его максимального значения, например, для 16-битного счетчика это 65535. Соответственно, процедура обработки (обслуживания) прерывания (ISR) вызывается когда бит прерывания переполнения таймера установлен (enabled) в TOIEx присутствующем в регистре масок прерываний TIMSKx.

ISR Format:

ISR(TIMERx_OVF_vect)
{
}

Output Compare Register (OCRnA/B) – регистр сравнения выхода

Процедура обработки прерывания сравнения выхода (Output Compare Match Interrupt) вызывается при вызове функции TIMERx_COMPy_vect если установлен бит/флаг OCFxy в регистре TIFRx. Эта процедура обработки прерывания (ISR) становится доступной при помощи установки бита OCIExy, присутствующем в регистре маски прерываний TIMSKx.

Захват входа таймера (Timer Input Capture)

Процедура обработки этого прерывания вызывается если установлен бит/флаг ICFx в регистре флагов прерываний таймера (TIFRx — Timer Interrupt Flag Register). Эта процедура обработки прерываний становится доступной при установке бита ICIEx в регистре маски прерываний TIMSKx.

Необходимые компоненты

  1. Плата Arduino Uno (купить на AliExpress).
  2. ЖК дисплей 16х2 (купить на AliExpress).
  3. Резисторы 10 кОм (2 шт.) и 2,2 кОм (купить на AliExpress).
  4. Светодиод (любого цвета) (купить на AliExpress).
  5. Кнопка (2 шт.).
  6. Источник питания с напряжением 5 В.

Работа схемы

Схема устройства представлена на следующем рисунке.

Необходимые соединения между платой Arduino Uno и ЖК дисплеем 16х2 представлены в следующей таблице:

ЖК дисплей 16х2 Arduino UNO
VSS GND
VDD +5V
V0 к среднему контакту потенциометра для контроля контрастности ЖК дисплея
RS 8
RW GND
E 9
D4 10
D5 11
D6 12
D7 13
A +5V
K GND

Две кнопки через подтягивающие резисторы 10 кОм подключены к контактам 2 и 4 платы Arduino Uno, а светодиод подключен к контакту 7 Arduino через резистор 2,2 кОм.

Собранная схема устройства будет выглядеть примерно следующим образом:

Программирование таймеров в плате Arduino UNO

В этом проекте мы будем использовать прерывание переполнения таймера (Timer Overflow Interrupt) и использовать его для включения и выключения светодиода на определенные интервалы времени при помощи установки заранее определяемого значения (preloader value) регистра TCNT1 с помощью кнопок. Полный код программы будет приведен в конце статьи, здесь же рассмотрим его основные части.

Для отображения заранее определяемого значения используется ЖК дисплей, поэтому необходимо подключить библиотеку для работы с ним.

#include<LiquidCrystal.h>

Анод светодиода подключен к контакту 7 платы Arduino, поэтому определим (инициализируем) его как ledPin.

#define ledPin 7  

Затем сообщим плате Arduino к каким ее контактам подключен ЖК дисплей.

LiquidCrystal lcd(8,9,10,11,12,13); 

Установим заранее определенное значение (preloader value) равное 3035 – это будет соответствовать интервалу времени в 4 секунды. Формула для расчета этого значения приведена выше в статье.

float value = 3035;

Затем в функции void setup() установим режим работы ЖК дисплея 16х2 и высветим приветственное сообщение на нем на несколько секунд.

lcd.begin(16,2);
lcd.setCursor(0,0);
lcd.print("ARDUINO TIMERS");
delay(2000);
lcd.clear();

Затем контакт, к которому подключен светодиод, установим в режим вывода данных, а контакты, к которым подключены кнопки – в режим ввода данных.

pinMode(ledPin, OUTPUT);
pinMode(2,INPUT);
pinMode(4,INPUT);

После этого отключим все прерывания.

noInterrupts();

Далее инициализируем Timer1.

TCCR1A = 0;
TCCR1B = 0;

Загрузим заранее определенное значение (3035) в TCNT1.

TCNT1 = value;

Затем установим коэффициент деления предделителя равный 1024 при помощи конфигурирования битов CS в регистре TCCR1B.

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

Разрешим вызов процедуры обработки прерывания переполнения счетчика с помощью установки соответствующего бита в регистре маски прерываний.

TIMSK1 |= (1 << TOIE1); 

Теперь разрешим все прерывания.

interrupts();

Теперь процедура обработки прерывания переполнения счетчика будет отвечать за включение и выключение светодиода с помощью функции digitalWrite. Состояние светодиода будет меняться каждый раз когда будет происходить прерывание переполнения счетчика.

ISR(TIMER1_OVF_vect)
{
TCNT1 = value;
digitalWrite(ledPin, digitalRead(ledPin) ^ 1);
}

В функции void loop() предварительно загружаемое значение увеличивается и уменьшается на 10 (инкрементируется и декрементируется) при помощи кнопок в схеме. Также это значение отображается на экране ЖК дисплея 16х2.

if(digitalRead(2) == HIGH)
{
value = value+10; //увеличиваем preload value
}
if(digitalRead(4)== HIGH)
{
value = value-10; //уменьшаем preload value
}
lcd.setCursor(0,0);
lcd.print(value);
}

Исходный код программы

Далее приведен полный текст программы. Работа нашего проекта продемонстрирована на видео, приведенном в конце статьи.

Видео, демонстрирующее работу схемы

22 ответа к “Руководство по таймерам Arduino для начинающих”

Не пинайте больно, только начал впитывать инфу. Вопрос: я так понял таймер ведет отсчет в обратном направлении, т.е. чем больше TCNT1 тем меньше время отсчета? И командой TIMSK1 |= (1 << TOIE1); я включаю отсчет, а командой TIMSK1 |= (0 << TOIE1); выключаю и он не работает пока не дадим первую команду?

Короче будет ли работать мой монстр? Пытаюсь имитировать датчик HC-SR04 датчиком GY-VL53L0XV2, так как нет доступа к исходному контроллеру.

#include «Adafruit_VL53L0X.h»

Adafruit_VL53L0X lox = Adafruit_VL53L0X();
unsigned long mm = 0;
unsigned long mks = 0;
bool D2_fall = false;

void setup() {
// настраиваю таймер на совпадение
cli();
TCCR1A = 0; // установить регистры в 0
TCCR1B = 0;
TCCR1B |= (1 << WGM12); // включить CTC режим
TCCR1B |= (1 << CS11); // Установить биты на коэффициент деления 8 каждый такт 0.5 микросекунды
sei(); // включить глобальные прерывания

Serial.begin(57600);

// wait until serial port opens for native USB devices
while (! Serial) {
delay(1);
}
pinMode(2, INPUT);
pinMode(4, OUTPUT);
digitalWrite(4, LOW);

Serial.println("Adafruit VL53L0X test");
if (!lox.begin()) {
Serial.println(F("Failed to boot VL53L0X"));
while(1);
}
lox.configSensor(Adafruit_VL53L0X::VL53L0X_SENSE_HIGH_ACCURACY);
attachInterrupt(0, D2_falling, FALLING);
}

void loop() {
VL53L0X_RangingMeasurementData_t measure;
lox.rangingTest(&measure, false); // pass in 'true' to get debug data printout!

if (measure.RangeStatus != 4) { // phase failures have incorrect data
mm = measure.RangeMilliMeter;
mks = mm*58/10;
Serial.print(D2_fall); Serial.print(" mm: "); Serial.print(mm); Serial.print(" mks: "); Serial.println(mks);
} else {
Serial.println(mks);
mks = 11600;
}
}
void D2_falling(){
OCR1A = 401; // 40кГц = 200 мксек / 0.5 время такта таймера
TCNT1 = 0; // reset counter value
TIMSK1 |= (1 << OCIE1A); // включить прерывание по совпадению таймера
}

ISR(TIMER1_COMPA_vect){
if (!D2_fall) {
OCR1A = mks * 2 + 1;
digitalWrite(4, HIGH);
TCNT1 = 0; // reset counter value
D2_fall = true;
} else {
digitalWrite(4, LOW);
TIMSK1 &= ~(1 << OCIE1A); // выключить прерывание по совпадению таймера
D2_fall = false;
}
}

Ну вы из меня супермена не делайте, как я могу сразу определить работает ли ваша программа или нет. К тому же я с датчиком GY-VL53L0XV2 никогда не сталкивался. Вы можете написать в каких строках вашей программы у вас возникает проблема, а я могу попытаться определить, из-за чего она у вас возникает

В коде программы данной статьи нет такой команды. Что вы хотите с ее помощью сделать?

Да, чем больше значение TCNT1, тем меньше время счета. Но он не в обратном направлении считает, а от значения TCNT1 до 65535. TIMSK1 |= (1

Спасибо за ответ, теперь в голове складывается картинка. Просто не мог взять в толк как прерывание переполнения может работать как прерывание совпадения, правильную вещь написали на каком-то форуме, «чем больше перечитываешь статью, тем понятнее становится написанное» :)) По поводу работы датчика GY-VL53L0XV2 вопросов не возникает, я впервые заморочился с таймерами а осциллографа под рукой не оказалось для проверки правильности работы таймера. Сомнения возникли по работе функций void D2_falling() и ISR(TIMER1_COMPA_vect), правильно ли описана работа таймера? Задача: при падении сигнала на D2 необходима задержка на 8 импульсов частотой 40кГц и затем поднимаю на D4 на время «полета ультразвука до объекта и обратно». Время mks расчетное и вопроса не вызывает. Привел программу и описание к тому чтобы быть уверенным в правильности своих действий и для будущих искателей истины, т.к. иногда возникает необходимость в использовании «каскадного» таймера а подобных примеров в просторах инета не нашел.

Включаем прерывание TIMSK1 |= (1 << TOIE1) а выключаем какой командой? TIMSK1 |= (0 << TOIE1) или TIMSK1 &= ~(1 << TOIE1) ?

Достал осциллограф и как результат все работает за исключением
void D2_falling(){
OCR1A = 401; // 40кГц = 200 мксек / 0.5 время такта таймера
TCNT1 = 0; // reset counter value
TIMSK1 |= (1 << OCIE1A); // включить прерывание по совпадению таймера
}
Само прерывание срабатывает но задержка в 200мксек отсутствует, вернее она равна примерно 14мксек и какое бы я значение туда не вносил не меняется. А вот в теле прерывания по таймеру те же команды устанавливают задержку и работает!

Я просто не вижу как вы записываете значение в таймер. Вы сбрасываете его значение командой TCNT1 = 0, а как потом новое значение записываете в него? И что конкретно делают команды TIMSK1 |= (1
Да, спасибо. После вашего ответа я теперь тоже понял, что то сразу невнимательно посмотрел. Надеюсь ваши комментарии окажут помощь начинающим в понимании работы таймеров в Arduino.

Команды разные и коменты разные, разница в одну букву «Ы»
Я использовал прерывание сравнения и значение сравнения вношу OCR1A = 401;
Много статей прочитал и не по разу 😉 TIMSK1 |= (1 << OCIE1A) выполняется логическая операция "или" и как мы знаем только "0 или 0 = 0" остальные дают "1", поэтому надо использовать логическую "и" если мы хотим обнулить регистр. В связи с этим используем TIMSK1 &= ~(1 << OCIE1A), да. еще эта тильда "~" означает инверсию, другими словами (если принять что номер бит OCIE1A, точно не помню какой именно, скажем пусть будет 3) имеем TIMSK1 &= ~(0100) после инверсии TIMSK1 &= 1011. и мы знаем что при умножении на 1 получим состояние какое и было, будь то 0 или 1, а при умножении на 0 получим в любом случае 0.
Понимаю что запутано, но чтобы понять это я прочитал статью раз 20. Убил на понимание работы прерывания по таймеру 2 дня и наконец то все понял (надеюсь на это). За исключением того почему не срабатывает таймер запрограммированный в прерыванию attachInterrupt(0, D2_falling, FALLING); т.е. в функции void D2_falling();. Пришлось обойти это путем введения третьего состояния в функции прерывания таймера по сравнению.

void D2_falling(){ // прерывание по падению на входе D2
TIMSK1 |= (1 << OCIE1A); // включить прерывание по совпадению таймера
}

ISR(TIMER1_COMPA_vect){ // прерывание по таймеру сравнения
if (D2_fall == 0) {
OCR1A = 370; //вносим значение для сравнения
TCNT1 = 0; // reset counter value
D2_fall = 1;
} else if (D2_fall == 1) {
OCR1A = mks * 2 + 1; //вносим значение для сравнения
digitalWrite(4, HIGH);
TCNT1 = 0; // reset counter value
D2_fall = 2;
} else if (D2_fall == 2){
digitalWrite(4, LOW);
TIMSK1 &= ~(1 << OCIE1A); // вЫключить прерывание по совпадению таймера
D2_fall = 0;
}
}
Значение OCR1A = 370; пришлось подбирать опытным путем по осциллографу, чтобы получить задержку в 200мксек

на самом деле оказывается что нет, если в исходном байте на месте этого бита буде 1 то она там и останется т.к. «1 или 0 равно 1» а нам надо внести туда нулик.

Да, точно. Это я ошибся. С кодами на AVR давненько не работал

Спасибо вам за вашу статью, ибо именно она стала отправной точкой в понимании всей этой галиматьи с таймерами. Остальные как копипастеры печатают одну и туже статью без внесения своих изменений, что никак не приближает «начинающих» к пониманию данной темы. И еще смеют писать в комментах «да что тут не понятно!? ведь в даташитах все английским по белому расписано…» Ваша статья, как раз отличается «заточкой под свои нужды». Именно это надо начинающим!

Спасибо за добрый отзыв и что оценили мою работу

Формула с ошибкой?
TCNT1 = 65535 – (16x1010x2 / 1024) = 34285
У меня получается 65503,4375

Спасибо за внимательность. Исправил ошибку в формуле (просто из копирования с Word’а в формуле потерялось возведение в степень), теперь все должно рассчитываться правильно. Также добавил дополнительное пояснение к формуле относительно частоты кварцевого генератора 16 МГц

Спасибо и вам что оценили мой труд. Но я думал что урок получится лучше, есть некоторые моменты, которые мне чуть чуть не нравятся, но не хватает времени переделать

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

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