Прерывания по таймеру avr. Учебный курс AVR. Таймер - счетчик Т0. Регистры. Ч1. Несколько моментов относительно использования таймера

  • Bit 7 - OCIE2: Timer/Counter2 Output Compare Interrupt Enable - Разрешение прерывания по совпадению таймера/счетчика2
    При установленном бите OCIE2 и установленном бите I регистра статуса разрешается прерывание по совпадению содержимого регистра сравнения и состояния таймера/ счетчика2. Соответствующее прерывание (с вектором $0012) выполняется если произойдет совпадение при сравнении содержимого регистра сравнения и состояния таймера/счетчика2. В регистре флагов прерывания TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг совпадения таймера/счетчика2.
  • Bit 6 - TOIE2: Timer/Counter2 Overflow Interrupt Enable - Разрешение прерывания по переполнению таймера/счетчика2
    При установленном бите TOIE2 и установленном бите I регистра статуса разрешается прерывание по переполнению таймера/счетчика2. Соответствующее прерывание (с вектором $0014) выполняется если произойдет переполнение таймера/счетчика2. В регистре флагов TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг переполнения таймера/счетчика2.
  • Bit 5 - TICIE1: Timer/Counter1 Input Capture Interrupt Enable - Разрешение прерывания по захвату таймера/счетчика1
    При установленном бите TICIE1 и установленном бите I регистра статуса разрешается прерывание по захвату таймера/счетчика1. Соответствующее прерывание (с вектором $0016) выполняется если произойдет запуск захвата по выводу 29, PD4(IC1). В регистре флагов TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг захвата таймера/счетчика1.
  • Bit 4 - OCE1A: Timer/Counter1 Output CompareA Match Interrupt Enable - Разрешение прерывания по совпадению регистра A с таймером/счетчиком1
    При установленном бите OCIE1A и установленном бите I регистра статуса разрешается прерывание по совпадению регистра A с состоянием таймера/счетчика1. Соответствующее прерывание (с вектором $0018) выполняется если произойдет совпадение содержимого регистра A сравнения выхода с состоянием таймера/ счетчика1. В регистре флагов TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг совпадения регистра A с таймером/счетчиком1.
  • Bit 3 - OCIE1B: Timer/Counter1 Output CompareB Match Interrupt Enable - Разрешение прерывания по совпадению регистра B с таймером/счетчиком1
    При установленном бите OCIE1B и установленном бите I регистра статуса разрешается прерывание по совпадению регистра B с состоянием таймера/счетчика1. Соответствующее прерывание (с вектором $001A) выполняется если произойдет совпадение содержимого регистра B сравнения выхода с состоянием таймера/счетчика1. В регистре флагов TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг совпадения регистра B с таймером/счетчиком1.
  • Bit 2 - TOIE1: Timer/Counter1 Overflow Interrupt Enable - Разрешение прерывания по переполнению таймера/счетчика1
    При установленном бите OCIE1B и установленном бите I регистра статуса разрешается прерывание по переполнению таймера/счетчика1. Соответствующее прерывание (с вектором $001C) выполняется если произойдет переполнение таймера/счетчика1. В регистре флагов TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг переполнения таймера/счетчика1.При нахождении таймера/счетчика1 в PWM режиме флаг переполнения счетчика устанавливается когда счетчик изменит направление счета при $0000.
  • Bit 1 - OCIE0: Timer/Counter0 Output Compare Interrupt Enable - Разрешение прерывания по совпадению таймера/счетчика0
    При установленном бите OCIE0 и установленном бите I регистра статуса разрешается прерывание по совпадению содержимого регистра сравнения и состояния таймера/ счетчика0. Соответствующее прерывание (с вектором $001E) выполняется если произойдет совпадение при сравнении содержимого регистра сравнения и состояния таймера/счетчика0. В регистре флагов прерывания TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг совпадения таймера/счетчика0.
  • Bit 0 - TOIE0: Timer/Counter0 Overflow Interrupt Enable - Разрешение прерывания по переполнению таймера/счетчика0
    При установленном бите TOIE0 и установленном бите I регистра статуса разрешается прерывание по переполнению таймера/счетчика0. Соответствующее прерывание (с вектором $0020) выполняется если произойдет переполнение таймера/счетчика0. В регистре флагов TIFR (Timer/Counter Interrupt Flag Register) устанавливается флаг переполнения таймера/счетчика0.

В последнее время все больше и больше начинающих сталкиваются с проблемой освоения Таймеров/Счетчиков (далее Т/С) на этапе изучения микроконтроллеров. В данной статье я постараюсь развеять страхи перед данными модулями и доступно объяснить, как и с чем употребляют те самые Т/С.

За основу мы возьмем очень популярную среди разработчиков устройств на МК книгу, автором которой является А.В. Евстифеев. По ссылкам в конце статьи Вы сможете найти проект в и проект в . В этой статье мы разберем работу 8-ми битного Т/С Т2, который входит в состав Т/С МК Atmega8.

Итак, что же такое Таймер/Счетчик? Т/С - это один из модулей МК AVR с помощью которого можно отмерять определенные промежутки времени, организовать ШИМ и многие другие задачи. В зависимости от модели МК, количество Т/С может составлять 4 и более. Пример тому - МК Atmega640х, 1280х/1281х, 2560х/2561х, которые содержат на своем борту 6 Т/С: два 8-ми битных и четыре 16-ти битных. МК Atmega8 содержит в себе три Т/С: Т0 и Т2 с разрядностью 8 бит, Т1 с разрядностью 16 бит.

Давайте подробнее рассмотрим Т/С Т2 микроконтроллера Atmega8.

Этот таймер может работать в нескольких режимах: Normal, Phase correct PWM, CTC (сброс при совпадении), Fast PWM. Подробнее о каждом режиме Вы можете прочитать в книге.

Данный Т/С состоит из регистра управления, счетного регистра, регистра сравнения, регистра состояния асинхронного режима. Структурная схема Т2 приведена на рис.1

Рассмотрим в теории как же работает данный модуль. Чтобы для начала Вам было понятнее, мы не будем рассматривать все лишние примочки таймера и рассмотрим самый обычный его режим - NORMAL. Для себя определим что МК тактируется от внутреннего RC-генератора с частотой 1МГц и таймер настроен на работу в режиме NORMAL.

Тактовые импульсы поступают на вход clk i\o и попадают в предделитель таймера. Предделитель может быть настроен, по Вашим потребностям, на прямой проход тактовых импульсов или делить входящие импульсы, пропуская только их определенную часть. Поделить входящие импульсы можно на /8, /64, /256, /1024. Так как у нас Т\С может работать в асинхронном режиме, то при включении его в этот режим количество предделителей существенно вырастает, но мы их рассматривать пока не будем. С предделителя тактовые импульсы поступают в блок управления и уже с него попадают в счетный регистр. Счетный регистр в свою очередь совершает инкремент на каждый входящий импульс. Счетный регистр Т2 8-ми битный, поэтому он может считать только до 255. Когда наступает переполнение счетного регистра, он сбрасывается в 0 и в этом же такте начинает считать заново. Так же в момент переполнения счетного регистра устанавливается флаг TOV2 (флаг прерывания по переполнению) регистра TIFR.

Теперь, раз уж мы затронули такие слова, как РЕГИСТР, самое время с ними познакомится. Для начала мы затронем только те регистры, с которыми будем непосредственно работать, дабы не забивать мозг лишней информацией.

TCNT2 - счетный регистр, о его работе мы уже говорили.

TCCR2 - регистр управления таймером.

TIMSK - регистр маски прерываний(в Atmega8 этот регистр является единственным для всех таймеров).

TIFR - регистр флагов прерываний(в Atmega8 этот регистр является единственным для всех таймеров).

А теперь о каждом подробно:

Регистр управления TCCR2. Содержимое этого регистра вы можете посмотреть на рис.2.


рис.2

Биты 0-2 отвечают за тактирование таймера. Установка определенных комбинаций в этих битах настраивает предделитель данного таймера. Если все три бита сброшены - таймер выключен.

Биты 3,6 отвечают за режим работы таймера.

Биты 4,5 нужны для настройки поведения вывода ОСn (проще говоря, используются при настройке ШИМ)

И последний бит этого регистра - бит 7. С его помощью мы можем принудительно изменять состояние вывода ОСn.

Регистр маски прерываний - TIMSK. Его мы видим на рисунке №3

Из этого регистра нас интересуют только два последних бита, биты 6 и 7. Этими битами мы разрешаем работу прерываний.

Бит 6, если в него записать единицу, разрешает прерывание по событию "Переполнение Т\С Т2"

Бит 7, если в него записать еди ницу, разрешает прерывание по событию "Совпадение счетного регистра с регистром сравнения"

Регистр флагов прерываний TIFR. Его мы видим на рисунке №4

рис.4

В этом регистре нас так же интересуют два последних бита: биты 6 и 7.

Бит 6 - флаг, устанавливается по событию "Переполнение Т\С Т2"
Бит 7 - флаг, устанавливается по событию "Совпадение счетного регистра с регистром сравнения"

Эти биты сбрасываются автоматически при выходе из обработчика прерывания, но для надежности их можно сбрасывать самостоятельно, сбрасывая эти биты в "0".

Остальные биты регистров TIMSK и TIFR используются Т\С Т0 и Т1. Как вы уже заметили, у битов этих регистров даже названия совпадают, за исключением цифры в конце названия, которая и указывает к какому таймеру данный бит применИм.

Осталось рассмотреть две несложные таблички, а именно: таблица, в которой описано управление тактовым сигналом (рис. 6), и таблица, в которой описано, как в общем настроить таймер (рис.5).

О том, что находится в этих таблицах, я писал выше, однако привожу Вам их для наглядности.

Вот мы и закончили с теорией, и пора приступить к практической части. Сразу оговорюсь.

ЧАСЫ, КОТОРЫЕ ПОЛУЧАТСЯ В ХОДЕ ИЗУЧЕНИЯ ДАННОЙ СТАТЬИ, НЕ ОБЛАДАЮТ ВЫСОКОЙ ТОЧНОСТЬЮ. ДАННАЯ СТАТЬЯ ОРИЕНТИРОВАННА НА ОБЩИЕ ПРИНЦИПЫ РАБОТЫ С ТАЙМЕРАМИ.

Открываем Studio 6, создаем проект и выбираем Atmega8.

В самом начале указываем частоту тактирования и подключаем нужные нам для работы библиотеки

< avr/io.h > #include < avr/interrupt.h >

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

Во второй строчке кода подключается библиотека с общим описанием регистров нашего МК. Так же в ней всем регистрам присвоены читабельные имена.

В третьей строке подключается библиотека для работы с векторами прерываний.

TIMSK |= (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7);

На этом настройка нашего таймера закончена. Давайте подробнее рассмотрим последние три строки кода.

В первой строке мы разрешили прерывания по событию "Переполнение таймера\счетчика Т2"

И в третьей строкой мы глобально разрешили прерывания. Это можно было также написать следующим образом:

Asm("sei");

Остается добавить обработчик прерывания и код наших часов реального времени.

ISR (TIMER2_OVF_vect) { takt++; if (takt>=4){sek++; takt=0x00;} if (sek>=60) {min++; sek=0x00;} if (min>=60) {hour++; min=0x00;} if (hour>=24) {hour=0х00}; }

В коде, который находится в обработчике прерывания, нет ничего сложного и нового для Вас. Внимание обратим только на переменную takt и волшебную цифру "4". Откуда взялась эта цифра? Давайте рассмотрим подробно этот момент.

Мы знаем, что наш МК работает от внутреннего генератора с частотой 1МГц, таймер тактируется с предделителем \1024, считать наш таймер может до 255. Зная эти параметры мы можем посчитать сколько переполнений он совершит за 1 секунду

1 000 000 \ 1024 \ 256 = 3,814697.....

Ну, а так как мы учимся работать с таймерами и не ставили цель получить суперточный ход часов, мы округляем наш результат и получаем "4". Т.е. за 1 секунду таймер переполнится ~4 раза.

Почему мы делили на 256 если таймер считает только до 255? Потому что "0" это тоже число. Думаю, здесь все понятно.

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

Вот весь листинг программы которая у нас получилась.

#define F_CPU 1000000UL #include < avr/io.h > #include < avr/interrupt.h > unsigned char takt = 0; unsigned char sek = 0; unsigned char min=0; unsigned char hour=0; ISR (TIMER2_OVF_vect) { takt++; if (takt>=4){sek++; takt=0x00;} if (sek>=60) {min++; sek=0x00;} if (min>=60) {hour++; min=0x00;} if (hour>=24) {hour=0х00}; } int main(void) { TIMSK |= (1< < TOIE2); TCCR2 |= (1< < CS22)|(1< < CS20); SREG |= (1< < 7); while(1) { } }

А как же вывод информации пользователю? А тут кому как нравится. Можете использовать семисегментные индикаторы, графические или знакогенерирующие дисплеи и т.д.

В архиве Вы найдете проект с выводом информации на дисплей от nokia5110, проект в Proteus 7 и все нужные файлы и библиотеки для работы.

Обращаю внимание на то, что библиотека LCD_5110 для работы с дисплеем написана участником форума и предоставлена с его разрешения.




В МК 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

Прежде чем приступить к изучению таймера определимся с базовым понятием «частота». Простым языком, это количество повторений, в секунду. Это значит, что если вы за секунду хлопнете в ладошки 2 раза, то частота хлопков будет равна 2Гц. Если за 3 раза, значит 3Гц.

Каждый микроконтроллер работает на определенной частоте. Большинство инструкций выполняется за один такт, поэтому чем выше частота, тем быстрее работает микроконтроллер. Если нет источника тактирования, соответственно ничего работать не будет. На случай отсутствия внешнего источника тактирования, в большинстве микроконтроллеров имеется свой внутренний генератор. Обычно на него «с завода» настроены.

Частота внутреннего источника может изменяться («плавать») из за температуры и т.п., поэтому считается непригодным для серьезных проектов, а у нас ведь именно такие 🙂 Поэтому применяется стабильный источник внешней частоты — кварцевый резонатор (кварц). Один из вариантов исполнения кварцевого резонатора:

Теперь, кое что о таймере. Таймер работает на той же частоте, что и микроконтроллер. Иногда это может быть слишком быстро, поэтому используют предделитель который уменьшает количество тиков в 8/64/256/1024… раз. Включается это все программно.

Допустим, мы выбрали предделитель 1024, частота микроконтроллера 8 МГц, значит после предделителя частота таймера станет:
8 000 000 / 1024 = 7813 Гц — это частота, на которой работает наш таймер. По простому говоря, за одну секунду таймер тикнет 7813 раз.

К количеству тиков можно привязать выполнение кода. Эта фича есть не для всех таймеров, читайте документацию на свой камень. Допустим, нам нужно, чтобы раз в 0,5 секунды выполнялся наш код. За одну секунду 7813 тиков, за пол секунды в 2 раза меньше — 3906. Это значение вносится в регистр сравнения, и с каждым тиком проверяется достаточно ли оттикало или нет, как в будильнике, только очень быстро.

Но вот у нас совпали эти 2 значения и что дальше? Для этого существует такая полезная штука как прерывание по совпадению. Это значит, что при совпадении таймера и регистра сравнения, ваша текущая программа остановится. После этого выполнится участок кода, который абсолютно не связан с основной программой. Внутри этого участка вы можете писать что угодно и не беспокоиться о том, что он как то повлияет на программу, выполнится он только когда значение таймера совпадет с регистром сравнения.

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

Вот теперь мы готовы написать нашу программу. Поэтому создаем проект с помощью мастера проектов. Сразу прицепим LCD, мы же уже это умеем).

Переходим на вкладку Timers и тут остановимся поподробнее:

Выбираем частоту 7813 и устанавливаем галочку напротив пункта Interrupt on: Compare A Match. Таким образом мы указали, что при совпадении значения выполнять прерывание (то о чем было написано выше). Прерывание будем выполнять 1 раз в секунду, т.е. нам нужно тикнуть 7813 раз, поэтому переводим число 7813 в шестнадцатеричную систему и получим 1e85. Именно его и записываем в регистр сравнения Comp A. Регистр сравнения Comp A 16 битный, поэтому число больше 2^16=65536 мы записать не можем.

Генерим, сохраняем, вычищаем наш код. Появится новый непонятный кусок кода

// Timer 1 output compare A interrupt service routine
interrupt void timer1_compa_isr(void)
{

Это то самое прерывание. Именно внутри этих скобок мы можем писать тот код, который мы хотели бы выполнять через определенные промежутки времени. У нас это одна секунда. Итак логично создать переменную, которую мы будем увеличивать 1 раз в секунду, т.е. 1 раз за прерывание. Поэтому проинициализируем переменную int s =0; а в прерывании будем ее увеличивать от 0 до 59. Значение переменной выведем на жк дисплей. Никаких хитростей, все очень просто.
Получившийся код.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include #asm .equ __lcd_port= 0x18 ; PORTB #endasm #include int s = 0 ; // переменная для хранения секунд // Обработка прерывания по совпадению interrupt [ TIM1_COMPA] void timer1_compa_isr(void ) { s++; // увеличиваем переменную каждую секунду if (s> 59 ) // обнуляем секунды после 59 { s= 0 ; } TCNT1= 0 ; //обнуляем таймер } void main(void ) { TCCR1A= 0x00 ; //настройка таймера TCCR1B= 0x05 ; TCNT1= 0x00 ; //здесь увеличиваются тики OCR1A= 0x1E85 ; //записываем число в регистр сравнения TIMSK= 0x10 ; //запускаем таймер lcd_init(8 ) ; #asm("sei") while (1 ) { lcd_gotoxy(0 , 0 ) ; //вывод в 0 координате X и Y lcd_putchar(s/ 10 + 0x30 ) ; //вывод десятков секунд lcd_putchar(s% 10 + 0x30 ) ; //вывод секунд } ; }

#include #asm .equ __lcd_port=0x18 ;PORTB #endasm #include int s = 0; // переменная для хранения секунд // Обработка прерывания по совпадению interrupt void timer1_compa_isr(void) { s++; // увеличиваем переменную каждую секунду if(s>59) // обнуляем секунды после 59 { s=0; } TCNT1=0; //обнуляем таймер } void main(void) { TCCR1A=0x00; //настройка таймера TCCR1B=0x05; TCNT1=0x00; //здесь увеличиваются тики OCR1A=0x1E85; //записываем число в регистр сравнения TIMSK=0x10; //запускаем таймер lcd_init(8); #asm("sei") while (1) { lcd_gotoxy(0,0); //вывод в 0 координате X и Y lcd_putchar(s/10+0x30); //вывод десятков секунд lcd_putchar(s%10+0x30); //вывод секунд }; }

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

Давайте разберем, как пользоваться таймером Т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.