Использование прерываний в Arduino (продолжение)

Продолжение - начало тут

Таймеры на Arduino

Я связался с Дэвидом Меллисом (David Mellis) из команды разработчиков Arduino и узнал, что Arduino пользуется всеми тремя таймерами ATMega168.

  • Tаймер 0 (Системное время, ШИМ 5 and 6)
    Используется для хранения счетчика времени работы программы. Функция millis() возвращает число миллисекунд с момента запуска программы, используя ISR глобального приращения таймера 0. Таймер 0 также используется для реализации ШИМ на выводах 5 и 6.
  • Tаймер 1 (ШИМ 9 и 10)
    Используется для реализации ШИМ для цифровых выводах 9 и 10.
  • Tаймер 2 (ШИМ 3 и 11)
    Используется для управления выходами ШИМ для цифровых выводов 3 и 11.

Хотя все таймеры используются, только Tаймер 0 имеет назначенную таймеру ISR. Это означает, что мы можем захватить Таймер 1 и/или Таймер2 под свои нужды. Однако в результате вы не сможете использовать ШИМ на некоторых портах ввода-вывода. Если вы планируете использовать ШИМ, имейте это ввиду. Я выбрал использование Таймера 2, что окажет влияние на выводы 3 и 11.

Моя тестовая программа полностью отключила вывод ШИМ на цифровые выводы, управляемые с таймера 2. Я подозреваю, что библиотека функций ШИМ рассчитана на работу счетчика в заданном диапазоне, который я превысил. Функция захвата сравнивает загруженные значения, и значения моего таймера просто не будут понятны для ШИМ.

Дэвид Меллис дал мне эту ссылку для изучения того, как библиотечные программы используют таймеры. Она хорошо документирована и я уверен, что она сослужит мне хорошую службу в будущем. В будущих экспериментах я планирую управлять ШИМ-модуляцией напрямую, чтобы достичь намного большей частоты, чем нормальная. Высокочастотный ШИМ может здорово пригодиться для функции аудио-ЦАП.
Ссылки, относящиеся к прерываниям в Arduino.

Здесь приведены несколько ссылок (англ.), относящихся к прерываниям на Arduino. Я просмотрел некоторые из них, пока забавлялся с прерываниями. Я должен признать, что мои познания – заслуга этих авторов.

Тексты программ

Программы, приведенные в этой статье, доступны в zip-архиве.

Установка Таймера 2

Приведенная ниже программа показывает созданную мной функцию установки Таймера 2. Эта функция подключает прерывание по переполнению Таймера 2, устанавливает предварительно заданный масштаб для таймера подсчитывает загружаемое значение таймера, дающее желаемую частоту прерываний по времени. Эта программа основана на программе, найденной мной по вышеуказанным ссылкам. Я приобрел опыт, читая спецификации AVR, однако гораздо проще повторно использовать программу, если вы можете её найти. Рекомендую вам искать информацию по таймерам в спецификациях, только если по-другому никак не представить, как это трудно для понимания.

#define TIMER_CLOCK_FREQ 2000000.0 //2MHz for /8 prescale from 16MHz

//Установка Таймера2.
//Конфигурирует 8-битный Таймер2 ATMega168 для выработки прерывания
//с заданной частотой.
//Возвращает начальное значение таймера, которое должно быть загружено в TCNT2
//внутри вашей процедуры ISR.
//Смотри пример использования ниже.
unsigned char SetupTimer2(float timeoutFrequency){
unsigned char result; //Начальное значение таймера.

//Подсчет начального значения таймера
result=(int)((257.0-(TIMER_CLOCK_FREQ/timeoutFrequency))+0.5);
//257 на самом деле должно быть 256, но я получил лучшие результаты с 257.

//Установки Таймер2: Делитель частоты /8, режим 0
//Частота = 16MHz/8 = 2Mhz или 0.5 мкс
//Делитель /8 дает нам хороший рабочий диапазон
//так что сейчас мы просто жестко запрограммируем это.
TCCR2A = 0;
TCCR2B = 0<<CS22 | 1<<CS21 | 0<<CS20;

//Подключение прерывания по переполнению Timer2
TIMSK2 = 1<<TOIE2;

//загружает таймер для первого цикла
TCNT2=result;

return(result);
}

Сначала определяется тактовая частота таймера. Показано, что тактовая частота установлена 2 МГц, так как мы используем деление на 8 (делитель частоты) опорной частоты 16 МГц. Это жестко запрограммировано в функции. Это определение улучшает внешний вид программы и может быть полезно в некоторых приложениях. Наконец, оно напоминает мне, как я настроил таймер.

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

Далее подсчитывается значение, перезагружаемое в таймер. Это очень простой подсчет, но требует операций с плавающей точкой. К счастью, нам нужно сделать это только один раз, поскольку операции с плавающей точкой очень дорого обходятся в пересчете на машинное время. Примем, что таймер будет установлен на 2 МГц при каждом счете. Загружаемое значение – это число отсчетов, которое мы хотим произвести при 2 МГц между прерываниями. Вы можете заметить, что я использовал 257 вместо 256 для числа отсчетов в этом выражении. Я знаю, что верное значение – 256, но я получил лучшие результаты с 257. Далее в этой в этой статье я разъясню, почему.

Следующий участок секретного кода устанавливает таймер в режим 0 и выбирает делитель частоты /8. Режим 0 – это базовый режим таймера, а делитель /8 показывает, как мы получаем счетчик, считающий с частотой 2 МГц или 0,5 мкс на отсчет.

Далее подключается прерывание по переполнению. После выполнения этого кода микроконтроллер будет вызывать ISR каждый раз, когда счетчик прокрутится от 0xFF до 0×00. Это случится, когда счетчик просчитает от нашего загруженного значения через FF и назад до 00.

Наконец, мы загружаем значение счетчика в таймер и возвращаем это загруженное значение, чтобы ISR могла использовать его позже.

Я запускал таймер максимум при 50 кГц. Это очень быстро и любые действия, выполняемые в ISR, значительно тормозят выполнение основной программы. Я не рекомендую использовать частоты свыше 50 кГц, разве что вы почти ничего не делаете в ISR.

Загрузка микроконтроллера прерываниями

Чтобы дать вам представление об эффекте, предположим, что таймер ISR запускался бы каждые 20 мкс. Процессор, работающий на 16 МГц, может выполнить около 1 машинной команды каждые 63 нс или около 320 машинных команд для каждого цикла прерывания (20 мкс). Предположим также, что исполнение каждой строки программы на С может занять много машинных команд. Каждая инструкция, используемая в ISR, отнимает время, доступное для исполнения любой другой программы. Если бы наша ISR использовала около 150 машинных циклов, мы сожрали бы половину доступного процессорного времени. При активных прерываниях главная программа откладывалась бы около ½ времени, занимаемого ей в других случаях. 150 машинных команд – не очень большая программа на С, поэтому вы должны быть внимательны.

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

Измерение загрузки прерываниями

Поскольку мне хотелось иметь очень быстрый таймер ISR, я должен был измерить, насколько я загрузил доступные ресурсы. Я придумал трюк, выполняющий оценку загрузки и позволяющий мне вывести измерение на последовательный порт. Так как я работал с таймером ISR, я мог сохранить следы загрузки прерываниями.
Таймер не был установлен в режим, когда он перезагружается автоматически. Это значит, что ISR должна перезагрузить таймер для следующего интервала счета. Было бы точнее иметь автоматически перезагружаемый таймер, но, используя этот режим, мы можем измерить время, проводимое в ISR, и соответственно исправить время, загружаемое в таймер. Ключ в том, что при помощи этой коррекции мы при разумной точности, мы также получаем и число, показывающее, сколько времени мы проводим в ISR.

Трюк заключается в том, что таймер хранит время, даже если он переполнен и прерван. В конце нашей ISR мы можем захватить текущее значение счетчика таймера. Это значение представляет то время, которое он отнял у нас до следующей точки программы. Это суммарное время, затраченное на переход в процедуру прерывания и выполнение программы в ISR. Небольшая ошибка будет оттого, что не подсчитывается время, затраченное на команду перезагрузки таймера, но мы можем исправить её эмпирически. Фактически именно поэтому я использовал в формуле подсчета загружаемого значения 257 вместо 256. Я обнаружил опытным путем, что это дает лучший результат. Лишний такт компенсирует команду перезагрузки таймера.

Продолжение