Использование прерываний в Arduino (окончание)
Продолжение - начало часть 1, часть 2
ISR Таймера2
ISR для прерывания по переполнению Таймера2 показана ниже.
#define TOGGLE_IO 9 //вывод Arduino для переключения по таймеру ISR //Timer2 указатель вектора прерывания по переполнению ISR(TIMER2_OVF_vect) { //Переключение IO-вывода в другое состояние. digitalWrite(TOGGLE_IO,!digitalRead(TOGGLE_IO)); //Захват текущего значения таймера. Это величина ошибки //из-за задержки обработки прерывания и работы этой функции latency=TCNT2; //Перезагрузка таймера и коррекция по задержке TCNT2=latency+timerLoadValue; }
Эта функция короткая и её основная задача – переключать порт ввода-вывода. После переключения она захватывает текущее значение таймера и использует его для коррекции задержки на перезагрузку таймера. Значение задержки глобальное, его может отслеживать главная программа для вышеупомянутых измерений загрузки. Это число тактов на частоте 2 МГЦ, которое занимает эта ISR для выполнения своих функций.
Запомните что ISR должна быть короткой, так как она вызывается каждые 20 мкс при таймере, настроенном на 50 кГц. Вы можете делать больше в ISR, но вам нужно найти баланс между интервалом прерываний и количеством операций, выполняемых в ISR. Значение задержки поможет в этом, как описано ниже.
Мои тесты показали среднюю задержку около 20 тактов, что составляет до 45 % загрузки процессора. Это означает, что из-за ISR главная программа будет выполняться в среднем примерно на 45 % медленнее. Не так уж страшно, но заметьте, что вся работа, выполняемая в ISR, состоит в переключении порта ввода-вывода. Это стоило нам почти половину всего процессорного времени! Эта величина складывается из времени на обслуживание прерывания, выполнение процедур цифровой записи/цифрового чтения и полного процесса перезагрузки таймера.
ISR была бы гораздо быстрее, если бы мы напрямую обращались к выводу с помощью регистров порта. Но было проще использовать общие библиотечные функции, и если бы мне действительно нужно было только переключать состояние порта ввода-вывода, меня бы устроила потеря 45 %.
Главная программа. Функция Setup()
Функция Setup() вызывается с помощью системной программы Arduino однократно при запуске программы. Она инициализирует порты ввода-вывода и таймер. Она также выполняет вывод в последовательный порт, показывающий, что программа запущена.
void setup(void) { //Устанавливает порт, который нам нужно переключать в ISR, выходным. pinMode(TOGGLE_IO,OUTPUT); //Запускает последовательный порт Serial.begin(9600); //Сообщение о запуске программы Serial.println("Timer2 Test"); //Запускает таймер и получает загружаемое значение таймера. timerLoadValue=SetupTimer2(44100); //Выводит загружаемое значение таймера Serial.print("Timer2 Load:"); Serial.println(timerLoadValue,HEX); }
Setup() начинается с установки переключаемого порта в выходной порт, так что мы можем переключать его в ISR. Затем она активирует последовательный порт и выводит текстовое сообщение, чтобы показать, что программа действует.
Далее вызывается функция SetupTimer2 с частотой, установленной 44100 Гц, общей частотой дискретизации звука. Возвращаемое значение сохраняется в глобальной переменной timerLoadValue для последующего использования в ISR.
Наконец, Setup() выводит timerLoadValue, так что мы можем убедиться, что оно в пределах разумного.
На этой стадии таймер запущен и наша процедура ISR вызывается с заданной частотой. Если подключить осциллограф, вы увидите переключение вывода, генерирующее частоту, равную ½ интервала таймера. ½ получается потому, что мы устанавливаем порт в низкий уровень в одной ISR, и записываем в него высокий уровень в другой.
Главная программа, функция Loop()
Функция цикла вызывается снова и снова, пока программа запущена. Каждый раз при возврате из цикла он вызывается снова. Текст программы выглядит сложным, но на самом деле все, что мы делаем – усредняем величину задержки ISR Таймера2 и выводим результаты измерений после получения 100 замеров.
Отметим, что функция цикла ничего не должна делать относительно переключения линии ввода-вывода. Все это управляется ISR, позволяющей функции цикла заняться другими вещами, не обращая внимания на процессы, происходящие в ISR. В случае с последовательным портом, вы не должны беспокоиться о загрузке следующего символа в UART, когда он готов. Вы занимаетесь своими делами, а последовательный порт управляется в фоновом режиме. Это одно из достоинств программ, управляемых прерываниями. Функции вызываются при появлении события, и отделены от вашей прикладной программы.
void loop(void) { //Собирает задержку ISR каждые 10 мс. delay(10); //Собирает текущее значение задержки из ISR и увеличивает счетчик на 1 //the sample counter latencySum+=latency; sampleCount++; //Как только наберется 10 замеров, вычисляет и выводит результат измерений if(sampleCount>99) { float latencyAverage; float loadPercent; //Вычисляет среднюю задержку latencyAverage=latencySum/100.0; //обнуляет значения сумм sampleCount=0; latencySum=0; //Вычисляет ожидаемый процент загрузки процессора loadPercent=latencyAverage/(float)timerLoadValue; loadPercent*=100; //Переводит доли в проценты; //Выводит среднюю задержку Serial.print("Latency Average:"); Serial.print((int)latencyAverage); Serial.print("."); latencyAverage-=(int)latencyAverage; Serial.print((int)(latencyAverage*100)); //Выводит ожидаемый процент загрузки Serial.print(" Load:"); Serial.print((int)loadPercent); Serial.println("%"); } }
Функция цикла начинается с задержки 10 мс. (Заметьте, что мы не можем использовать подобные задержки, если нужно переключать порт ввода-вывода с высокой частотой без прерываний). Задержка 10 мс регулирует то, как часто мы делаем замеры задержки и вывод измерений. После того как мы получим 100 измерений, каждое из которых занимает 10 мс, результат выводится каждую секунду.
Далее значение задержки из ISR накапливается в глобальной переменной latencySum. Мы просто захватываем текущее значение задержки и прибавляем его к тому, что уже накоплено. Кроме того, мы увеличиваем на единицу счетчик накопленного количества замеров.
Теперь мы проверяем, было ли уже накоплено 100 замеров. Если нет, то пропускаем остальной код и возвращаемся. Если накопилось 100 замеров, то получаем среднее значение делением накопленной задержки на число замеров и сохраняем результат в latencyAverage. После этого очищаем аккумулятор и счетчик замеров, чтобы можно было начать все заново.
Теперь, имея хорошее измерение задержки, мы можем подсчитать ожидаемую загрузку процессора. Нам известно, что таймер переполняется каждый раз, когда он досчитывает от нашего перезагружаемого значения до 0xFF и потом назад до 0×00, это показатель переполнения. Процент загрузки будет определяться как задержка/число тактов в ISR. Это значение подсчитывается и выводится как результат измерения, так что мы можем оценить влияние нашей программы ISR.
Результаты выведены, и функция цикла возвращается, чтобы быть вызванной вновь и ждать 10 мс. Я снова обращаю ваше внимание на то, что главная программа не выполняет никакой работы по переключению порта ввода-вывода. Она не ограничена в использовании задержек, вызывается с собственными интервалами времени и на нее влияет только то, как быстро она будет выполняться. Время, которое она занимает – это остаток времени от выполнения всех фоновых ISR, ISR этого таймера и других, активных постоянно, например, Таймера0 и последовательного порта.
В примере программы таймер загружен шестнадцатеричным значением D4, или десятичным 212. Это означает, что он будет прерван каждый раз, как только отсчитает 44 такта. Мы знаем, что пока процессор выполняет код ISR, таймер отсчитывает около 20 тактов, так что остается всего около 24 до того, как он снова вернется в ISR. Эти 24 такта – все время, которое наша главная программа получает на выполнение. Так что из общего времени 44 такта между прерываниями мы тратим 20 тактов на ISR, оставляя около 24 тактов прикладной программе. Это составляет около 45 %, потраченных процессором на ISR.
Также нам известно, что на весь исполнимый код ISR около 1 такта тратится и не измеряется в значении задержки. Именно поэтому мне пришлось добавить единицу к временному интервалу, чтобы это исправить в программе, приведенной выше. Реально приложению достается только 23 такта из 44. Когда я запустил ту же самую программу, закомментировав переключение вывода, задержка в среднем составила около 3 тактов, или 6% загрузки. Сама ISR, без обслуживания каких-либо полезных задач, использует около 6 % доступных ресурсов. Оставшееся время съедают функции цифрового чтения и записи.
Резюме
Если вы дочитали до этого места, то имеете начальное представление о прерываниях, почему они очень полезны и важны, и как они влияют на главную программу. Также у вас есть несколько примеров кода для создания собственных проектов. Будьте внимательны при работе с быстрым таймером ISR. Если вы запускаете таймер с меньшей скоростью, ваши проблемы, конечно, уменьшаются. Я здесь рассмотрел крайние случаи, чтобы дать вам понять, чего можно ожидать.
Перевод, оригинальная статья Arduino Interrupts