Прерывание по переполнению таймера в avr. AVR. Учебный курс. Таймеры. Настройка блока сравнения для генерации ШИМ


Рассмотрим, как сделать таймер своими руками на микроконтроллере ATmega8, хотя код довольно просто адаптировать и для МК AVR других серий. Электронный таймер нужное устройство во всех областях, где требуется выполнение определенных действий через конкретный промежуток времени.

Управление таймера состоит всего из четырех кнопок:

— увеличение значения числа;

— уменьшение значения числа;

— старт таймера;

— сброс таймера.

В качестве индикатора срабатывания таймера применяется генератор звуковой частоты с динамиком. Генератор будет запускаться с помощью транзисторного ключа Q5, который в свою очередь открывается положительным потенциалом, поступающим из порта PC2 микроконтроллера.

Упрощенно таймер работает следующим образом. Кнопками «+» и «-» устанавливается требуемое количество секунд; кнопкой «старт» запускается таймер. Когда таймер отсчитает до нуля, на выводе PC2 микроконтроллера ATmega8 появится высокий потенциал, который откроет Q5. Далее транзисторный ключ запустит генератор и раздастся звук в динамике. Сброс таймера осуществляется при нажатии кнопки «сброс». Генератор звуковой частоты собран на двух транзисторах Q6 и Q7 разный полупроводниковых структур. С принципом работы и описанием схемы подобных генераторов можно ознакомиться, перейдя по .

Алгоритм работы таймера на микроконтроллере

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

Для формирования интервала времени в одну секунду мы воспользуемся первым таймер-счетчиком микроконтроллера ATmega8. Все его настройки мы определим в функцию start . Сначала разделим рабочую частоту микроконтроллера 1000000 Гц на 64 и получим новую частоту 15625 Гц. За это отвечают бит CS10, CS11 и CS12 регистра TCCR1B. Далее разрешаем прерывание по совпадению и в регистр сравнения (старший и младший) записываем двоичное число равное десятичному 15625. Затем обнуляем счетный регистр TCNT1 и устанавливаем в единицу бит WGM12 регистра TCCR1B, что вызывает сброс счетного регистра при совпадении текущего его значения с числом, записанным в регистры сравнения.

void start (void)

TCCR1B &= ~(1<

TCCR1B |= (1<

TIMSK |= (1<

OCR1AH = 0b00111101;

OCR1AL = 0b000001001; // регистр сравнения 15625

TCNT1 = 0;

TCCR1B |= (1<

Когда таймер отсчитает ровно одну секунду – вызовется прерывание. В теле функции прерывания мы будем снижать значение переменной на единицу. При достижении нуля на второй выход порта C микроконтроллера появится высокий потенциал, который откроет транзисторный ключ и запустит генератор, в результате чего мы услышим звук в динамике.

ISR (TIMER1_COMPA_vect)

Z—;




В МК ATMega16 есть три таймера/счетчика – два 8-битных (Timer/Counter0, Timer/Counter2) и один 16-битный (Timer/Counter1). Каждый из них содержит специальные регистры, одним из которых является счетный регистр TCNTn (n – это число 0, 1 или 2). Каждый раз, когда процессор выполняет одну команду, содержимое этого регистра увеличивается на единицу (либо каждые 8, 64, 256 или 1024 тактов). Потому он и называется счетным. Помимо него, есть еще и регистр сравнения OCRn (Output Compare Register), в который мы можем сами записать какое-либо число. У 8-битного счетчика эти регистры 8-битные. По мере выполнения программы содержимое TCNTn растет и в какой-то момент оно совпадет с содержимым OCRn. Тогда (если заданы специальные параметры) в регистре флагов прерываний TIFR (Timer/Counter Interrupt Flag Register) один из битов становится равен единице и процессор, видя запрос на прерывание, сразу же отрывается от выполнения бесконечного цикла и идет обслуживать прерывание таймера. После этого процесс повторяется.

Ниже представлена временная диаграмма режима CTC (Clear Timer on Compare). В этом режиме счетный регистр очищается в момент совпадения содержимого TCNTn и OCRn, соответственно меняется и период вызова прерывания.

Это далеко не единственных режим работы таймера/счетчика. Можно не очищать счетный регистр в момент совпадения, тогда это будет режим генерации широтно-импульсной модуляции, который мы рассмотрим в следующей статье. Можно менять направление счета, т. е. содержимое счетного регистра будет уменьшаться по мере выполнения программы. Также возможно производить счет не по количеству выполненных процессором команд, а по количеству изменений уровня напряжения на «ножке» T0 или T1 (режим счетчика), можно автоматически, без участия процессора, менять состояние ножек OCn в зависимости от состояния таймера. Таймер/Счетчик1 умеет производить сравнение сразу по двум каналам – А или В.

Для запуска таймера нужно выставить соответствующие биты в регистре управления таймером TCCRn (Timer/Counter Control Register), после чего он сразу же начинает свою работу.

Мы рассмотрим лишь некоторые режимы работы таймера. Если вам потребуется работа в другом режиме, то читайте Datasheet к ATMega16 – там все подробнейше по-английски написано, даны даже примеры программ на С и ассемблере (недаром же он занимает 357 страниц печатного текста!).

Теперь займемся кнопками.

Если мы собираемся использовать небольшое количество кнопок (до 9 штук), то подключать их следует между «землей» и выводами какого-либо порта микроконтроллера. При этом следует сделать эти выводы входами, для чего установить соответствующие биты в регистре DDRx и включить внутренний подтягивающий резистор установкой битов в регистре PORTx. При этом на данных «ножках» окажется напряжение 5 В. При нажатии кнопки вход МК замыкается на GND и напряжение на нем падает до нуля (а может быть и наоборот – вывод МК замкнут на землю в отжатом состоянии). При этом меняется регистр PINx, в котором хранится текущее состояние порта (в отличие от PORTx, в котором установлено состояние порта при отсутствии нагрузки, т. е. до нажатия каких-либо кнопок). Считывая периодически состояние PINx, можно определить, что нажата кнопка.

ВНИМАНИЕ! Если соответствующий бит в регистре DDRx будет установлен в 1 для вашей кнопки, то хорошее нажатие на кнопку может привести к небольшому пиротехническому эффекту – возникновению дыма вокруг МК. Естественно, МК после этого придется отправить в мусорное ведро…

Перейдем к практической части. Создайте в IAR новое рабочее пространство и новый проект с именем, например, TimerButton. Установите опции проекта так, как это описано в предыдущей статье. А теперь наберем следующий небольшой код.

#include "iom16.h" void init_timer0(void ) //Инициализация таймера/счетчика0 { OCR0 = 255; //Содержимое регистра сравнения //Задаем режим работы таймера TCCR0 = (1 void init_timer2(void ) //Инициализация таймера/счетчика2 { OCR2 = 255; TCCR2 = (1 //Устанавливаем для него прерывание совпадения } void main (void ) { DDRB = 255; init_timer0(); init_timer2(); while (1) { } } #pragma vector = TIMER2_COMP_vect //Прерывание по таймеру2 __interrupt void flashing() { if ((PORTB & 3) == 1) { PORTB &= (0xFF // Отключение выводов PB0, PB1 PORTB |= 2; // Включение PB1 } else { PORTB &= (0xFF // Отключение выводов PB0, PB1 PORTB |= 1; // Включение PB0 } }

Давайте посмотрим, как это работает. В функциях init_timern задаются биты в регистрах TCCRn, OCRn и TIMSK, причем такой способ может кому-нибудь показаться странным или незнакомым. Придется объяснить сначала, что означает запись «(1

где a – это то число, двоичное представление которого нужно сдвинуть, а b показывает, на сколько битов нужно его сдвинуть. При этом возможна потеря значения, хранящегося в a (т.е. не всегда возможно восстановить из С то, что было в а). Рассмотрим пример:

Что окажется в С после выполнения строки C = (22

2 в двоичном коде будет выглядеть как 00010110, а после сдвига влево на 3 бита получим С = 10110000.

Аналогично существует и сдвиг вправо. Еще пример:

char C; … C = ((0xFF > 2);

Сначала выполнится действие во внутренних скобках (0xFF – это 255 в шестнадцатеричном коде), из 11111111 получится 11111100, потом произойдет сдвиг вправо и получим С = 00111111. Как видим, здесь две взаимно обратные операции привели к другому числу, т. к. мы потеряли два бита. Этого не произошло бы, если бы переменная С была типа int, т. к. int занимает 16 бит.

Теперь рассмотрим еще два битовых оператора, широко применяющиеся при программировании МК. Это оператор «побитовое и» (&) и «побитовое или» (|). Как они действуют, думаю, будет понятно из примеров:

Действие: Результат (в двоичном коде): С = 0; // C = 00000000 C = (1 // C = 00100101 C |= (1 // C = 00101101 C &= (0xF0 >> 2); // C = 00101100 C = (C & 4) | 3; // C = 00000111

Чуть не забыл! Есть еще «побитовое исключающее или» (^). Оно сравнивает соответствующие биты в числе, и, если они одинаковые, возвращает 0, иначе единицу.

Вернемся к нашей программе. Там написано «(1

/* Timer/Counter 0 Control Register */ #define FOC0 7 #define WGM00 6 #define COM01 5 #define COM00 4 #define WGM01 3 #define CS02 2 #define CS01 1 #define CS00 0

При компиляции программы запись WGM01 просто заменяется на число 3, и в результате получается уже корректная запись. WGM01 называется макросом и он, в отличие от переменной, не занимает места в памяти (разве что в памяти программиста:-).

Если заглянуть теперь в Datasheet, но нетрудно будет увидеть, что WGM01 – это имя третьего бита в регистре TCCR0. То же самое касается и остальных битов этого регистра. Это совпадение не случайно и относится ко всем регистрам МК (или почти ко всем). Т. е., написав «(1

Итого, строчка

означает, что включен режим СТС, при срабатывании таймера0 меняется состояние «ножки» ОС0 (Она же PB3), содержимое счетчика увеличивается каждые 1024 такта.

Аналогично для таймера2: TCCR2 = (1

В регистре TIMSK (Timer/counter Interrupt MaSK register) задается режим прерываний. Мы написали

что означает прерывание таймера2 по совпадении TCNT2 и OCR2. Самая последняя функция – это собственно функция прерывания совпадения таймера2. Прерывания объявляются следующим образом:

#pragma vector = ВЕКТОР __interrupt ТИП ИМЯ()

где ВЕКТОР – это макрос вектора прерывания (по смыслу просто число, характеризующее это прерывание); эти макросы в порядке снижения приоритета перечислены в файле iom16.h. ТИП – тип возвращаемого функцией значения, в нашем случае void (ничего). ИМЯ – произвольное имя для этой функции. С прерываниями мы еще успеем наработаться в будущем.

При выполнении нашей функции должны по очереди моргать светодиоды, подключенные к PB0 и PB1. Судя по всему, частота равна 11059200/(256*1024) = 42 Гц. Это быстро, но будет заметно невооруженным глазом. Кстати, применение таймеров дает возможность отсчитывать точные временные интервалы, не зависящие от сложности вашей программы и порядка ее выполнения (но если у Вас не более одного прерывания).

Итак, сохраняем файл как «TimerDebug.c», добавляем его в проект, компилируем, прошиваем МК. Что же мы видим? Светодиод, подключенный к выводу PB3, будет активно моргать, а на PB0 и PB1 нет ни каких изменений. В чем же дело? Неужели что-то неверно?

Чтобы это выяснить, придется отладить нашу программу. Поскольку в IAR нет Debuggerа, придется использовать AVR Studio. Эту среду разработки можно скачать с сайта производителя http://atmel.com . Проблем с ее установкой, думаю, не должно быть. Перед запуском AVR Studio выберите в IAR режим Debug и создайте отладочный cof-файл (все опции проекта должны быть выставлены, как описано в предыдущей статье).

Открыв AVR Studio, мы увидим окно приветствия, в котором выберем «Open». Теперь лезем в папку с проектом, там в Debug\Exe, выбираем там «TimerDebug.cof», создаем проект там, где предложат, выбираем дивайс ATMega16 и режим отладки Simulator. После этого, если все сделали правильно, сразу же идет процесс отладки

Среда отладки здесь очень удобная, т.к. позволяет просматривать содержимое всех регистров МК, а также вручную устанавливать значения для них щелчками мыши. Например, если установить флаг прерывания в регистре TIFR в бите 7 (под черным квадратом в TIMSK), то следующим шагом программы (нажатие F10 или F11) должна быть обработка прерывания (флаг будет установлен автоматически и при совпадении регистров TCNT2 и OCR2). Но, к нашему удивлению, прерывания не будет!

Возникает вопрос: почему?

Откроем регистр CPU, SREG. Этот регистр определяет работу процессора, а конкретно седьмой его бит (I-бит, Interrupt bit) ответственен за обработку всех прерываний в МК. У нас он не установлен. Стоит его выставить, как сразу же пойдет выполняться прерывание (если одновременно установлен седьмой бит в TIFR).

Можно заметить одну интересную особенность: как только процессор уходит в обработку прерывания, этот бит (флаг разрешения обработки прерываний) снимается, а при выходе из функции прерывания вновь автоматически устанавливается. Это не позволяет процессору, не выполнив одного прерывания, схватиться за другое (ведь ориентируется он в программе именно таким образом – по флагам).

Значит, нужно добавить строчку кода для установки этого бита в единичное состояние. Добавим мы его в функцию init_timer2. Получится следующее:

void init_timer2(void ) { SREG |= (1 //Добавили эту строчку OCR2 = 255; TCCR2 = (1

Теперь, выбрав конфигурацию Release и прошив МК нажатием F7 и запуском AVReal32.exe, с радостью увидим, что все работает как надо.

Замечание: при отладке программы следует уменьшать интервалы таймеров, если они слишком длинные, т. к. в процессе отладки в AVR Studio программа выполняется в тысячи раз медленнее, чем внутри МК и вы не дождетесь срабатывания таймера. В целом отладка полностью аналогична таковой в других системах программирования, таких, как Visual C++.

Теперь, научившись отлаживать программы, создадим в IAR новый файл (а старый сохраним и удалим из проекта) и наберем следующий код:

#include "iom16.h" long unsigned int counter = 0; //Счетчик для формирования временных интервалов unsigned char B0Pressed = 0; //Здесь хранится состояние кнопки0 (0 - не нажата, 1 - нажата) unsigned char B1Pressed = 0; //Здесь хранится состояние кнопки1 (0 - не нажата, 1 - нажата) //Инициализация таймера2 //Нужно каждые 11059 такта (1 мс) увеличивать counter. У нас получается каждые 1,001175 мс void init_timer2() { OCR2 = 173; TCCR2 = (1 //Инициализация портов ввода/вывода init_io_ports() { DDRA =(1//формирование задержки в Pause_ms миллисекунд void delay(long unsigned int Pause_ms) { counter = 0; while (counter void main() { SREG |= (1 //Разрешаем прерывания init_timer2(); //Включаем таймер2 на каждые 64 такта, считать до 173 init_io_ports(); //Включаем порты ввода/вывода while (1) { //Обработка кнопки 0 if (B0Pressed == 1) { // уведичивает PORTB, ждет отпускания PORTB++; B0Pressed = 0; while ((PINC & (1 else { if ((PINC & (1 //Фиксирует нажатие { delay(50); if ((PINC & (1 //Проверяет нажатие { B0Pressed = 1; } } } //Обработка кнопки 1 if (B1Pressed == 1) //Если произошло нажатие на кнопку, { // уменьшает PORTB, ждет отпускания PORTB--; B1Pressed = 0; while ((PINC & (1 else { if ((PINC & (1 //Фиксирует нажатие { delay(200); //Устранение "дребезга клавиш" if ((PINC & (1 //Проверяет нажатие { B1Pressed = 1; //Устанавливает флаг "кнопка нажата" } } } } } //Прерывание по таймеру 2, прн этом увеличение счетчика counter #pragma vector = TIMER2_COMP_vect __interrupt void inc_delay_counter() { counter++; }

Сначала предлагаю взять уже готовый файл прошивки (файлы к статье, папка Release, файл TimerButton.hex или откомпилировать этот текст) и записать его в МК. После чего вынуть кабель прошивки, подключить к PC0 и PC1 кнопки и попробовать их понажимать. Увидим, что при нажатии на одну из кнопок увеличивается регистр PORTB (загораются светодиоды), а при нажатии на другую – уменьшается. Если не работает – попробуйте понажимать одну кнопку, удерживая другую – будет действовать. Дело в том, что я подключал кнопки следующим образом: при нажатии на кнопку вывод МК «болтается» в воздухе, а при отпускании замыкается на землю. Если вы подключили кнопки по-другому, то придется лишь чуть модернизировать программу.

Давайте разберемся с кодом. Здесь работа с таймером организована несколько иначе. Он срабатывает каждые 11072 такта (то есть каждые 1,001175 мс) и увеличивает содержимое переменной counter. Есть еще функция delay(long unsigned int Pause_ms), которая берет в качестве параметра количество миллисекунд Pause_ms, сбрасывает counter и ждет, когда counter достигнет значения Pause_ms, после чего продолжает работу МК. Таким образом, написав delay(1500), мы сформируем задержку в программе в 1,5 секунды. Это очень удобно для формирования временных интервалов.

С таймером вроде все понятно. Но для чего он используется? Рассмотрим бесконечный цикл while(1) в main(). В этом цикле проверяется состояние кнопок путем анализа содержимого регистра PINB. А зачем там стоит задержка на 50 мс? Это устранение т. н. «дребезга клавиш». Дело в том, что при нажатии на кнопку происходит удар одного контакта о другой, и, поскольку контакты металлические, удар этот упругий. Контакты, пружиня, замыкаются и размыкаются несколько раз, несмотря на то, что палец сделал лишь одно нажатие. Это приводит к тому, что МК фиксирует несколько нажатий. Давайте рассмотрим график зависимости напряжения на выходе PC0 от времени. Он может выглядеть так:

Точка А – момент нажатия кнопки. Он может быть зафиксирован МК. Затем идут несколько замыканий и размыканий (их может и не быть, а может быть и 12 штук – это явление можно считать случайным). В точке B контакт уже надежно зафиксирован. Между A и B в среднем около 10 мс. Наконец, в точке D происходит размыкание. Как же избавиться от этого неприятного явления? Оказывается, очень просто. Нужно зафиксировать момент нажатия кнопки (точка А), через какое-то время, например, 50 мс (точка С) проверить, что кнопка действительно нажата, сделать действие, соответствующее этой кнопке и ждать момент ее отпускания (точка D). То есть нужно сделать паузу от А до С, такую, чтобы весь «дребезг» оказался внутри этой паузы. А попробуйте теперь убрать строчку, формирующую задержку, откомпилировать программу и зашить ее в МК. Путем простых нажиманий на кнопки сможете легко убедиться, что все эти «мучения» не были напрасными.

А что же делать, если к МК нужно подключить, скажем, 40 кнопок? Ведь у него всего лишь 32 вывода. Казалось бы, никак. На самом деле это возможно. В таком случае используют алгоритм, называемый стробированием. Для этого нужно кнопки соединить в виде матрицы, как это показано на рисунке (рисунок взят из книги Мортона «МК AVR, вводный курс», где написано про программирование AVR на ассемблере).

При подаче на вывод PB0 лог. 1 (+5В), а на выводы PB1 и PB2 лог. 0 разрешается обработка кнопок 1, 4 и 7. После этого состояние каждой из них можно узнать, проверив напряжение на одном из выводов PB3..PB5. Таким образом, подавая последовательно на выводы PB0..PB2 лог. 1, можно определить состояние всех кнопок. Понятное дело, что выводы PB0..PB2 должны быть выходами, а PB0..PB2 входами. Чтобы определить, какое количество выводов потребуется для массива из Х кнопок, нужно найти пару сомножителей Х, сумма которых наименьшая (для нашего случая с 40 кнопками это будут числа 5 и 8). Это означает, что к одному МК можно подключить до 256 кнопок (а с применение дешифраторов и того больше, но о дешифраторах потом). Лучше сделать меньшее число выводов выходами, а большее – входами. В этом случае опрос всех строк матрицы займет меньше времени. Подобный способ подключения (стробирование) свойственен не только для кнопок. Там можно подключать самые разнообразные устройства, начиная от матриц светодиодов и заканчивая микросхемами flash-памяти.

© Киселев Роман
Июнь 2007

Здравствуйте, Артем! Спасибо за ответ!
Я свою проблему решил, прибор — это измеритель емкости от 1пФ+-0,25 пФ до 1,5 мкФ+-2,5нФ. Все работает, но интересно было бы сделать все не на Arduino IDE.
Вот мой код:

//#include // библиотека для работы с компаратором //#include // библиотека для ускореной работы с портами //#include // библиотека для LCD LiquidCrystal lcd(14 ,15 ,16 ,17 ,18 ,19 ) ; // инициализация LCD volatile unsigned int R = 1995 ; // резистор заряда конд. первого предела volatile unsigned int T = 0 ; // время заряда конд. плюс входная емкость плюс время виполнения команд volatile double C = 0.0 ; // вычесленное значение емкости void Timer1_Init( void ) { TCNT1 = 0 ; TCCR1A = 0 ; // Bits: COM1A1 COM1A0 COM1B1 COM1B0 - - WGM11 WGM10 TCCR1B = 0 ; // Bits: ICNC1 ICES1 - WGM13 WGM12 CS12 CS11 CS10 TCCR1C = 0 ; // Bits: FOC1A FOC1B - - - - - - TIMSK1 = 0 ; // Bits: - - ICIE1 - - OCIE1B OCIE1A TOIE1 TIFR1 = 0 ; // Bits: – – ICF1 – - OCF1B OCF1A TOV1 } volatile double calib = 122.0 ; // значение Т без конденсатора (калибрация) void setup() { lcd.begin (16 ,2 ) ; // вкл. LCD D8_Out; // назначаем D8 на выход D8_Low; // на D8 устанавливаем 0 (минус конденсатора) D12_Out; // назначаем D12 на выход D12_High; // на D12 устанавливаем 1 (заряд конд. резистором первого предела) D1_Out; // назначаем D1 на выход D1_High; // на D1 устанавливаем 1 (заряд конд. резистором второго предела) cli() ; // Global disable interrupts Timer1_Init() ; // Timer/Counter 1 initialization TIMSK0 & amp; = ~(1 & lt; & lt; TOIE0) ; // Disable Timer 0. TIMSK2 & amp; = ~(1 & lt; & lt; TOIE2) ; // Disable Timer 2. sei() ; // Global anable interrupts ADCSRA = 0 ; // Disable ADC PRR = B10010111; // Disable USART0, TWI, SPI } void loop() { D9_Out; // назначаем D9 на выход D9_Low; // на D9 устанавливаем 0 (разряд конд. через резистор 100 Ом) for (int i= 0 ; i& lt; 250 ; i++ ) // ждем пока разрядится конд. 1 cек { TCCR1B = (1 & lt; & lt; CS10) ; // запускаем таймер Fxtal/1 while ( TCNT1 & lt; 64000 ) { } TCCR1B = 0 ; //останавливаем таймер TCNT1 = 0 ; // сброс счетчика } D9_In; // делаем D9 входом, что б не мишать измерениям analogComparator.setOn (AIN0, AIN1) ; // компаратор вкл. TCCR1B = (1 & lt; & lt; CS11) ; // запускаем таймер Fxtal/8 while ((((ACSR & amp; & amp; (1 & lt; & lt; ACO) ) == 0 ) ) & amp; & amp; (TCNT1= 14001 ) // выбираем придел измерений { D1_Low; R = 20.0 ; calib = 6.0 ; lcd.print ("Range 700nF" ) ; lcd.setCursor (0 ,1 ) ; lcd.print (C/ 1000 ) ; lcd.print ("nF " ) ; // lcd.print(T); } if ( C & lt; = 14000 ) // выбираем придел измерений { D1_High; R = 1995.0 ; calib = 122.0 ; lcd.print (& Range 7nF& ) ; lcd.setCursor (0 ,1 ) ; lcd.print (C) ; lcd.print (& pF & ) ; // lcd.print(T); } }

//#include // библиотека для работы с компаратором //#include // библиотека для ускореной работы с портами //#include // библиотека для LCD LiquidCrystal lcd(14,15,16,17,18,19); // инициализация LCD volatile unsigned int R = 1995; // резистор заряда конд. первого предела volatile unsigned int T = 0; // время заряда конд. плюс входная емкость плюс время виполнения команд volatile double C = 0.0; // вычесленное значение емкости void Timer1_Init(void) // Timer/Counter 1 initialization { TCNT1 = 0; TCCR1A = 0; // Bits: COM1A1 COM1A0 COM1B1 COM1B0 - - WGM11 WGM10 TCCR1B = 0; // Bits: ICNC1 ICES1 - WGM13 WGM12 CS12 CS11 CS10 TCCR1C = 0; // Bits: FOC1A FOC1B - - - - - - TIMSK1 = 0; // Bits: - - ICIE1 - - OCIE1B OCIE1A TOIE1 TIFR1 = 0; // Bits: – – ICF1 – - OCF1B OCF1A TOV1 } volatile double calib = 122.0; // значение Т без конденсатора (калибрация) void setup() { lcd.begin(16,2); // вкл. LCD D8_Out; // назначаем D8 на выход D8_Low; // на D8 устанавливаем 0 (минус конденсатора) D12_Out; // назначаем D12 на выход D12_High; // на D12 устанавливаем 1 (заряд конд. резистором первого предела) D1_Out; // назначаем D1 на выход D1_High; // на D1 устанавливаем 1 (заряд конд. резистором второго предела) cli(); // Global disable interrupts Timer1_Init(); // Timer/Counter 1 initialization TIMSK0 &= ~(1 << TOIE0); // Disable Timer 0. TIMSK2 &= ~(1 << TOIE2); // Disable Timer 2. sei(); // Global anable interrupts ADCSRA = 0; // Disable ADC PRR = B10010111; // Disable USART0, TWI, SPI } void loop() { D9_Out; // назначаем D9 на выход D9_Low; // на D9 устанавливаем 0 (разряд конд. через резистор 100 Ом) for (int i=0; i<250; i++) // ждем пока разрядится конд. 1 cек { TCCR1B = (1<

Еще вопрос: как сделать что-бы значение таймера при переполнении продолжало увеличиваться, а не обнулялось и начиналось сначала? То есть, я хочу сделать таймер 32-х битным. Это для того, что-бы не делать отдельные пределы измерений, а просто увеличить время заряда конденсатора.
Заранее спасибо!

П.С. могу скинуть схемку, если интересно.

В этом уроке мы поговорим о таймерах.

Данная тема непосредственно связана с темой тактирования микроконтроллера. Поэтому рекомендую перед прочтением данного урока ознакомиться с предыдущим.

Итак, зачем нам таймер?

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

Решить поставленные задачи помогают именно таймеры. Но таймеры микроконтроллеров AVR не знают что такое секунда, минута, час. Однако они прекрасно знают, что такое такт! Работают они именно благодаря наличию тактирования контроллера. То есть, таймер считает количество тактов контроллера, отмеряя тем самым промежутки времени. Допустим, контроллер работает при тактовой частоте 8МГц, то есть когда таймер досчитает до 8 000 000, пройдет одна секунда, досчитав до 16 000 000, пройдет 2 секунды и так далее.

Однако, тут возникает первое препятствие. Регистры то у нас 8 битные, то есть досчитать мы можем максимум до 255, а взяв 16 битный таймер, мы, досчитаем максимум до 65535. То есть за одну секунду мы должны обнулить таймер огромное количество раз! Конечно, можно заняться этим, если больше заняться нечем. Но ведь просто измерять время, используя мощный микроконтроллер совсем не интересно, хочется сделать нечто большее. Тут нам на помощь приходит предделитель. В общем виде это промежуточное звено между таймером и тактовой частотой контроллера. Предделитель облегчает нашу задачу позволяя поделить тактовую частоту на определенное число, перед подачей её на таймер. То есть установив предделитель на 8, за 1 секунду наш таймер досчитает до 1 000 000, вместо 8 000 000 (Разумеется, при частоте тактирования контроллера 8МГц). Уже интереснее, не так ли? А поделить мы можем и не только на 8, но и на 64 и даже на 1024.

Теперь настало время собрать схему, настроить наш таймер, предделитель, и сделать уже хоть что-то полезное!

А делать мы сегодня будем “бегущие огни” из светодиодов. То есть поочередно будем зажигать 3 светодиода, с периодом 0.75 секунды (То есть время работы одного светодиода 0.25 секунды). Соберем следующую схему:

Номиналы резисторов R 1-R 3 рассчитайте самостоятельно.

Далее, рассмотрим регистры отвечающие за работу таймеров. Всего AtMega 8 имеет в своем составе 3 таймера.Два 8 битных(Timer 0,Timer 2) и один 16 битный(Timer 1).Рассматривать будем на примере 16 битного таймера 1.

Пара регистров 8 битных регистров TCNT 1H и TCNT 1L , вместе образуют 16 битный регистр TCNT 1. Данный регистр открыт как для записи, так и для чтения. При работе таймера 1, значение данного регистра при каждом счете изменяется на единицу. То есть в регистре TCNT 1 записано число тактов, которые сосчитал таймер. Так же мы можем записать сюда любое число в диапазоне от 0 до 2 в 16 степени. В таком случае отсчет тактов будет вестись не с 0, а с записанного нами числа.

Регистр TIMSK отвечает за прерывания, генерируемые при работе таймеров микроконтроллера. Прерывание – обработчик специального сигнала, поступающего при изменении чего либо . Любое прерывания микроконтроллера может быть разрешено или запрещено. При возникновении разрешенного прерывания, ход основной программы прерывается, и происходит обработка данного сигнала. При возникновении запрещенного прерывания, ход программы не прерывается, а прерывание игнорируется. За разрешение прерывания переполнения счетного регистра TCNT 1 таймера 1 отвечает бит TOIE 1(Timer 1 Overflow Interrupt Enable ).При записи 1 в данный бит прерывание разрешено, а при записи 0 – запрещено. Данное прерывание генерируется таймером 1 при достижении максимального значения регистра TCNT 1. Подробнее о прерываниях поговорим в следующем уроке.

Регистр TCCR 1B отвечает за конфигурацию таймера 1. В данном случае битами CS 10-CS 12 мы задаем значение предделителя согласно следующей таблицы.

Остальные биты пока нас не интересуют.

Так же существует регистр TCCR 1A , который позволяет настроить другие режимы работы таймера, например ШИМ, но о них в отдельной статье.

А теперь код на C :

#define F_CPU 16000000UL #include #include uint8_t num=0; ISR(TIMER1_OVF_vect) { PORTD=(1<2) { num=0; } TCNT1=61630;//Начальное значение таймера } int main(void) { DDRD|=(1<

#define F_CPU 16000000UL

#include

#include

uint8_t num = ;

ISR (TIMER1_OVF_vect )

PORTD = (1 << num ) ;

num ++ ;

if (num > 2 )

num = ;

TCNT1 = 61630 ; //Начальное значение таймера

int main (void )

DDRD |= (1 << PD0 ) | (1 << PD1 ) | (1 << PD2 ) ;

TCCR1B |= (1 << CS12 ) | (1 << CS10 ) ; //Предделитель = 1024

TIMSK |= (1 << TOIE1 ) ; //Разрешить прерывание по переполнению таймера 1

TCNT1 = 61630 ; //Начальное значение таймера

sei () ; //Разрешить прерывания

while (1 )

//Основной цикл программы, он пуст, так как вся работа в прерывании

Код на ASM :

Assembly (x86)

Include "m8def.inc" rjmp start .org OVF1addr rjmp TIM1_OVF start: ldi R16,LOW(RamEnd) out SPL,R16 ldi R16,HIGH(RamEnd) out SPH,R16 ldi R16,1 ldi R17,0b00000111 out DDRD,R17 ldi R17,0b00000101 out TCCR1B,R17 ldi R17,0b11110000 out TCNT1H,R17 ldi R17,0b10111110 out TCNT1l,R17 ldi R17,0b00000100 out TIMSK,R17 sei main_loop: nop rjmp main_loop TIM1_OVF: out PORTD,R16 lsl R16 cpi R16,8 brlo label_1 ldi R16,1 label_1: ldi R17,0b10111110 out TCNT1L,R17 ldi R17,0b11110000 out TCNT1H,R17 reti

Include "m8def.inc"

Rjmp start

Org OVF 1addr

Rjmp TIM 1_ OVF

start :

Ldi R 16, LOW (RamEnd )

Out SPL , R 16

Ldi R 16, HIGH (RamEnd )

Out SPH , R 16

Ldi R 16, 1

Ldi R 17, 0b00000111

Out DDRD , R 17

Ldi R 17, 0b00000101

Out TCCR 1B , R 17

Ldi R 17, 0b11110000

Out TCNT 1H , R 17

Ldi R 17, 0b10111110

По сути, таймер микроконтроллера - это цифровой счетчик, только "навороченый". На вход счетчика подается тактовый сигнал, по перепадам которого счетчик увеличивает свое значение. При возникновении событий - переполнение счетчика или совпадение его значения с заданным - генерируется запрос на прерывание.

Давайте разберем, как пользоваться таймером Т0 в режиме Normal. В этом режиме таймер считает от какого-то начального значения счетного регистра до максимально возможного (до 255 или 0xFF). Когда таймер Т0 досчитывает до максимума, то в следующий такт таймера возникает переполнение счетного регистра TCNT0 - он обнуляется и устанавливается флаг TOV0. Если в программе разрешены прерывания глобально (флаг I регистра SREG) и прерывание таймера Т0 по переполнению (флаг TOIE0 регистра TIMSK), то микроконтроллер вызовет соответствующий обработчик. Если значение счетного регистра совпадет с регистром сравнения OCR0, то установится флаг OCF0 и при разрешенном прерывании по событию совпадение, запустится его обработчик.

Таймер Т0 в режиме Normal

Рассмотрим практическую задачу - нам нужно каждые 20 мс опрашивать кнопку. Частота микроконтроллера 8 МГц, микроконтроллер ATmega16.

Первое, что нужно сделать - это определиться с выбором коэффициента предделителя таймера и рассчитать начальное значение для счетного регистра TCNT0.

Таймер Т0 может тактироваться от внутреннего тактового сигнала микроконтроллера или от внешнего, который подается на вывод Т0. При работе от внутреннего тактового сигнала пользователь может выбирать коэффициенты деления частоты этого сигнала. У таймера Т0 есть пять возможных вариантов коэффициента предделителя - 1, 8, 64, 256, 1024.

Для решения поставленной задачи, я рассуждаю следующим образом. Если бы один такт таймера Т0 имел период 1 мс, то мне бы это подошло. 20 тактов дают 20 мс. Какой коэффициент предделителя таймера позволит получить близкий к 1 мс период тактовой частоты? Можно посчитать.

Тактовая частота микроконтроллера Fcpu = 8000000 Гц
Период тактового сигнала микроконтроллера Tcpu = 1/Fcpu
Период тактового сигнала таймера Т0 равен Tt0 = (1/Fcpu)/k = k/Fcpu

При k = 1024 период тактовой частоты таймера Т0 будет равен Tt0 = 1024/8000000 = 0.128 мс

Это максимальный период тактового сигнала таймера, который мы можем получить при наших условиях (Fcpu = 8 МГц). При меньших коэффициентах - период получится еще меньше.

Ну хорошо, пусть один такт таймера это 0.128 мс, хватит ли разрядности счетного регистра, чтобы отсчитать этот временной интервал и сколько для этого понадобится тактов? Делим требуемый интервал времени (20 мс) на длительность одного такта таймера и получаем ответ.

n = t/Tto = 20 мс/ 0.128 мс = 156.25

Округлив до целого, получаем 156 тактов. Это меньше 255 (максимального значения счетного регистра), значит разрядности счетного регистра TCNT0 хватит.

Начальное значение для счетного регистра TCNT0 вычисляем как разницу между максимальным числом тактов таймера Т0 и требуемым, то есть 256 - 156 = 100. (256 - это максимальное количество временных интервалов, которые может отсчитать любой 8-и разрядный таймер.)

Думаю, теперь понятно, как рассчитывать начальное значение TCNT0 для режима Normal :

Вычисляем период одного такта таймера Tt0 = k/Fcpu,
- вычисляем требуемое количество тактов для заданного интервала n = t/Tto,
- вычисляем начальное значение для счетного регистра TCNT0 = 256 - n.

Можно автоматизировать эту процедуру с помощью макросов. Например, так:

#define F_CPU 8000000UL
#define TIME_MS(time, k) (256L - ((time)*(F_CPU))/(1000L*(k)))

Но с таким макросом нужно быть начеку, при определенных значениях time и k могут возникать ошибки.

Теперь переходим к коду. Чтобы использовать таймер Т0 (да и любой другой тоже), его нужно настроить (инициализировать) и описать обработчик прерывания (если они используются).

Инициализация таймера состоит из следующих шагов:

Остановка таймера,
- задание режима Normal в TCCR0 без старта,
- установка начального значения TCNT0,
- сброс флагов в регистре TIFR,
- разрешение прерывания по переполнению в TIMSK,
- установка предделителя в TCCR0, то есть старт таймера

В данной последовательности возможны вариации.

Для нашей задачи код инициализации будет выглядеть так:


/*значение для счетного регистра*/
#define T_POLL 100

TCCR0 = 0;
TCCR0 = (0< TCNT0 = T_POLL;
TIFR = (1< TIMSK |= (1< TCCR0 |= (1<

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

Сброс флагов прерываний в регистре TIFR выполняется записью 1 в соответствующий разряд. Эту операцию нужно выполнять именно перезаписью регистра, а не с помощью побитового ИЛИ. И вот почему.

Допустим, в регистре TIFR устанавлены два флага прерывания - TOV1 и TOV0. TOV0 нам нужно сбросить. При установке требуемого разряда с помощью ИЛИ происходит примерно следующая вещь.


//TIFR имеет значение 0b00000101
//установлены флаги TOV1 и TOV0
//выполняется код TIFR |= (1<
//TIFR копируется в R16
IN R16, 0x38

//в R16 устанавливается разряд TOV0
//хотя он и так уже установлен
ORI R16, 0x02

//R16, равный 0b00000101, записывается в регистр TIFR
OUT 0x38, R16

В результате сброшены оба флага, а мы хотели сбросить один.

Продолжаем.

Синтаксис описания обработчиков прерывания у разных компиляторов немного отличается. Для IAR`a обработчик прерывания таймера Т0 по событию переполнение будет выглядеть так:



{
TCNT0 = T_POLL;

/*здесь должен быть опрос кнопки*/

TIMER0_OVF_vect - это адрес вектора прерывания по событию переполнение. Он берется из заголовочных файлов на микроконтроллер. В данном случае я взял его из файла iom16.h.

Первая строка обработчика (TCNT0 = T_POLL;) выполняет перезапись счетного регистра, то устанавливает его начальное значение. Если этого не сделать, таймер продолжит счет с 0. Перезапись счетного регистра нужно выполнять в начале обработчика прерывания.

Весь код для нашей задачи будет выглядеть примерно так. (Код приведен для IAR`a. Для других компиляторов нужно изменить заголовочные файлы и обработчик прерывания.)

#include
#include
#include

#define T_POLL 100

int main(void)
{
/*инициализация таймера*/

TCCR0 = 0;
TCCR0 = (0< TCNT0 = T_POLL;
TIFR |= (1< TIMSK |= (1< TCCR0 |= (1<

/*инициализация остальной периферии*/
DDRB |= (1<

Enable_interrupt();
while(1);

/*обработчик прерывания T0
по событию переполнение*/
#pragma vector = TIMER0_OVF_vect
__interrupt void TimerT0Ovf(void)
{
/*перезапись счетного регистра*/
TCNT0 = T_POLL;

/*опрос кнопки*/

/*инверсия PB0 для отладки*/
PORTB ^= (1<

Управление выводом OC0

В режиме Normal таймер Т0 может изменять состояние вывода OC0 при совпадении счетного регистра и регистра сравнения. Причем даже без прерываний. Варианты управления определяются разрядами COM01 и COM00 регистра TCCR0.

Вот пример программы, генерирующей прямоугольный сигнала на выводе ОС0.

#include
#include

int main(void)
{
/*инициализация таймера Т0*/

TCCR0 = 0;
TCCR0 = (0< TCNT0 = 0;
OCR0 = 0;
TIMSK = 0;
TCCR0 |= (1<

/*инициализация OC0*/
DDRB |= (1<

While(1);
return 0;
}

Вывод ОС0 будет менять свое состояние на противоположное при нулевом значении счетного регистра.

Несколько моментов относительно использования таймера

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

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

И последнее. Может случится ситуация, что обработка прерывания таймера задержится (например, по вине другого обработчика) и регистр TCNT0 уже посчитает несколько тактов. Если просто перезаписать значение TCNT0, то следующее прерывание вызовется позже, чем нужно. Получится, что предыдущее (задержанное) и новое прерывания не выдержат требуемый интервал.

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

TCNT0 = TCNT0 + startValue;

Сложение текущего значения счетного регистра с инициализируемым, учтет эти лишние такты. Правда есть одно НО! При больших значения startValue операция сложения может вызвать переполнение счетного регистра.

Например, startValue = 250, а таймер успел досчитать до 10. Тогда операция сложения приведет к такому результату:

10 + 250 = 260

Берем 8 разрядов от 260 получаем 4. В TCNT0 запишется 4.







2024 © uzbek-seks.ru.