Временные задержки на прерываниях от таймера

Среди библиотечных функций Arduino есть удобные функции delay() и millis(), которые позволяют программировать в алгоритме периоды ожидания. Однако не всегда их возможностей бывает достаточно.

Например, я хочу, чтобы Arduino раз в секунду считывал значение потенциометра на входе Analog 0 и передавал его через USART по последовательному интерфейсу. Такой алгоритм выглядит несложно:

void setup()

{

Serial.begin(19200); // Инициализация USART

}

void loop()

{

short value = analogRead(0); // Считать показания АЦП

float voltage = ((float) value) / 1024 * 5; // Пересчитать в вольты (0..+5 В)

Serial.println(voltage); // Вывести напряжение на терминал

delay(1000); // Подождать 1 сек.

}

Однако моя задача усложняется, если я хочу добавить, например, опрос кнопки. Что, если я сделаю так:

#define BTNPIN 2 // Кнопка подключена к выводу Digital 2


void setup()

{

Serial.begin(19200);

pinMode(BTNPIN, INPUT); // Вывод 2 в режим ввода

}


void loop()

{

if (digitalRead(BTNPIN) == HIGH)

Serial.println("Button pressed.");

else

Serial.println("Button released.");

short value = analogRead(0);

float voltage = ((float) value) / 1024 * 5;

Serial.println(voltage);

delay(1000);

}

Сделав так, я получаю алгоритм, который раз в секунду проверяет, нажата ли кнопка, выводит сообщение на терминал, затем производится измерение напряжения на потенциометре, и после этого алгоритм снова 1 секунду ждёт.

Однако мне хотелось бы видеть следующее. Раз в секунду производится опрос потенциометра, а сообщение о том, что кнопка нажата или отпущена, появлялось бы тогда, когда пользователь нажимает или отпускает кнопку. А не раз в секунду сообщалось её состояние, как это сделано сейчас.

Вполне очевидно, что функцию delay() я использовать не могу, поскольку её использование в функции loop() тормозит работу алгоритма. Вместо delay() я могу использовать функцию millis(), возвращающую число миллисекунд, прошедших со времени последней перезагрузки Arduino:

#define BTNPIN 2


bool buttonPressed = false; // Текущее состояние кнопки

unsigned long time;


void setup()

{

Serial.begin(19200);

pinMode(BTNPIN, INPUT);

time = millis(); // Засекаем время запуска

}


void loop()

{

if (digitalRead(BTNPIN) == HIGH) {

if ( !buttonPressed) {

Serial.println("Button pressed.");

buttonPressed = true;

}

}

else {

if (buttonPressed) {

Serial.println("Button released.");

buttonPressed = false;

}

}

if (millis() - time >= 1000) { // Если прошла 1 секунда или более, то...

short value = analogRead(0);

float voltage = ((float) value) / 1024 * 5;

Serial.println(voltage);

time = millis(); // Засекаем время измерения напряжения

}

delay(25); // Небольшая задержка, чтобы подавить дребезг контакта кнопки

}

Такой алгоритм всячески прекрасен, за исключением того, что примерно раз в 50 дней происходит переполнение счётчика миллисекунд, и проверка, сколько прошло времени с последнего измерения, может давать ошибку, что времени прошло минус сколько-то там миллисекунд. А поскольку тип беззнаковый (unsigned), то ответом будет, что прошло не меньше 25 дней.

Однако есть способ повысить надёжность работы алгоритма, используя в качестве счётчика времени прерывание по переполнению встроенного таймера-счётчика микроконтроллера Arduino. Например, 8-разрядного таймера-счётчика номер 2.

Вот как это выглядит:

#include <ve_avr.h> // Используется библиотека VEduino


#define BTNPIN 2


volatile unsigned short counter = 0; // Переменная счётчик прерываний

volatile bool timeOut = true; // Флаг состояния - время истекло

bool buttonPressed = false;


/*

* Функция-обработчик прерывания по переполнению таймера-счётчика 2.

*/

ISR(TIMER2_OVF_vect)

{

--counter; // Уменьшить значение счётчика на 1.

if (counter == 0) { // Если счётчик равен 0...

timeOut = true; // Установить флаг

DEV_TICTRL2.overflowIntDisable(); // Запретить прерывание TIMER2_OVF_vect

}

}


void setup()

{

Serial.begin(19200);

pinMode(BTNPIN, INPUT);

DEV_TIMER2.setClockSelect(Prescaler2::Prescaler_64); // Предделитель /64. 16 МГц / 64 / 256 = 976 Гц

DEV_TIMER2.setWaveGenMode(Timer2::Normal);

}


void loop()

{

if (digitalRead(BTNPIN) == HIGH) {

if ( !buttonPressed) {

Serial.println("Button pressed.");

buttonPressed = true;

}

}

else {

if (buttonPressed) {

Serial.println("Button released.");

buttonPressed = false;

}

}

if (timeOut) { // Если время ожидания истекло

short value = analogRead(0);

float voltage = ((float) value) / 1024 * 5;

Serial.println(voltage);

timeOut = false; // Инициируем флаг ожидания

counter = 1000; // Устанавливаем значение задержки

DEV_TICTRL2.overflowIntEnable(); // Разрешаем прерывание по переполнению т/с 2.

interrupts(); // Разрешаем прерывания

}

delay(25);

}

Здесь нужно заметить, что в теле цикла алгоритм проверяет не 16-битное значение переменной counter, а булево значение флага timeOut. Это сделано неспроста.

Дело в том, что микроконтроллер ATmega 8-битный, и поэтому операцию проверки 16-разрядного значения он выполняет за несколько команд ассемблера. Но поскольку переменная counter изменяется в функции-обработчике прерывания, то очередное прерывание вполне может произойти посередине проверки. Таким образом, проверка даст непредсказуемый результат. Именно поэтому я проверяю булевый флаг.

Вот, как выглядит ассемблерный код этой проверки:

19c: 80 91 2f 01 lds r24, 0x012F ; Загрузить переменную timeOut в регистр r24

1a0: 88 23 and r24, r24 ; Если регистр r24 равен 0, то

1a2: 69 f1 breq .+90 ; перейти по адресу 0x1fe <loop+0x9c>

А вот как выглядел бы код проверки 16-битной переменной counter:

19c: 80 91 42 01 lds r24, 0x0142 ; Загрузить переменную counter в регистры r24 и r25

1a0: 90 91 43 01 lds r25, 0x0143

1a4: 89 2b or r24, r25 ; Если (r24 | r25) != 0, то

1a6: 69 f5 brne .+90 ; перейти по адресу 0x202 <loop+0xa0>

Как видно из кода, чтение значения переменной выполняется за две команды, между которыми вполне может произойти очередное прерывание. При этом первым читается младший байт, а вторым старший.

Допустим, что значение переменной было равно 256. То есть 0x0100. Младший байт равен 0, а старший единице. И вот, что произошло бы.

ЦПУ прочитал младший байт - r24 стал равен 0.

Произошло прерывание, и значение переменной counter уменьшилось на единицу, то есть стало равным 0x00FF.

ЦПУ прочитал старший байт - r25 стал равен 0.

Операция (r24 | r25) даёт в результате 0, и следовательно микроконтроллер считает, что время ожидания истекло. В то время как на самом деле ему ещё следует ждать 255 миллисекунд.

Код скетча можно скачать по ссылке.

Автор: Андрей Шаройко <vanyamboe@gmail.com>