Временные задержки на прерываниях от таймера
Среди библиотечных функций 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>