Интернет-магазин

Просмотр корзины
В корзине:

товаров - 0 шт.



§ 37. Таймеры, кнопки и прерывания

Киселев Роман, Июнь 2007
Статья обновлена 26 Мая 2014

Файлы к статье скачать

В этой статье будет рассмотрено использование таймеров в МК и способ подсоединения кнопок к нему.Сначала немного теории...


В МК 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 умеет производить сравнение сразу по двум каналам – А или В.

Далее представлена функциональная схема таймера/счетчика0:

Для запуска таймера нужно выставить соответствующие биты в регистре управления таймером 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 << WGM01) | (1 << COM00) | (1 << CS02) | (1 << CS00); 
}

void init_timer2(void)  //Инициализация таймера/счетчика2
{
  OCR2 = 255;
  TCCR2 = (1 << WGM21) | (1 << CS22) | (1 << CS21) | (1 << CS20);
  TIMSK |= (1 << OCIE2);  //Устанавливаем для него прерывание совпадения
}

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 << 2);       // Отключение выводов PB0, PB1
    PORTB |= 2;	                // Включение PB1
  }
  else
  {
    PORTB &= (0xFF << 2);	// Отключение выводов PB0, PB1
    PORTB |= 1; 		// Включение PB0
  }
}

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

C = (a << b), вернее a << b

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

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

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

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

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

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

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

Действие:				Результат (в двоичном коде):

С = 0;					// C = 00000000
C = (1 << 5) | (1 << 2) | (1 << 0);	// C = 00100101
C |= (1 << 3);				// C = 00101101
C &= (0xF0 >> 2);			// C = 00101100
C = (C & 4) | 3;			// C = 00000111

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

Вернемся к нашей программе. Там написано «(1 << WGM01)». WGM01 – это число? – спросите вы. Да, если заглянуть в файл iom16.h, то там можно будет найти следующее:

/* 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 << WGM01)», мы установили этот бит в единицу и включили режим СТС, что видно из таблицы из Datasheetа:

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

TCCR0 = (1 << WGM01) | (1 << COM00) | (1 << CS02) | (1 << CS00);

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

Аналогично для таймера2: TCCR2 = (1 << WGM21) | (1 << CS22) | (1 << CS21) | (1 << CS20); Режим СТС, каждые 1024 такта.

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

TIMSK |= (1 << OCIE2);

что означает прерывание таймера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 << 7);  //Добавили эту строчку
  OCR2 = 255;
  TCCR2 = (1 << WGM21) | (1 << CS22) | (1 << CS21) | (1 << CS20);
  TIMSK |= (1 << OCIE2);
}

Теперь, выбрав конфигурацию 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 << WGM21) | (1 << CS22);
  TIMSK = (1 << 7);
}

//Инициализация портов ввода/вывода
init_io_ports()
{
  DDRA =(1<< DDA0)|(1<< DDA1)|(1<< DDA2)|(1<< DDA3)|(1<< DDA4)|(1<< DDA5)|(1<< DDA6)|(1<< DDA7);
  DDRB =(1<< DDB0)|(1<< DDB1)|(1<< DDB2)|(1<< DDB3)|(1<< DDB4)|(1<< DDB5)|(1<< DDB6)|(1<< DDB7);
  DDRC =(0<< DDC0)|(0<< DDC1)|(1<< DDC2)|(1<< DDC3)|(1<< DDC4)|(1<< DDC5)|(1<< DDC6)|(1<< DDC7);
  DDRD =(1<< DDD0)|(1<< DDD1)|(1<< DDD2)|(1<< DDD3)|(1<< DDD4)|(1<< DDD5)|(1<< DDD6)|(1<< DDD7);
  
  PORTA =(0<< PA0)|(0<< PA1)|(0<< PA2)|(0<< PA3)|(0<< PA4)|(0<< PA5)|(0<< PA6)|(0<< PA7);
  PORTB =(0<< PB0)|(0<< PB1)|(0<< PB2)|(0<< PB3)|(0<< PB4)|(0<< PB5)|(0<< PB6)|(0<< PB7);
  PORTC =(1<< PC0)|(1<< PC1)|(0<< PC2)|(0<< PC3)|(0<< PC4)|(0<< PC5)|(0<< PC6)|(0<< PC7);
  PORTD =(0<< PD0)|(0<< PD1)|(0<< PD2)|(0<< PD3)|(0<< PD4)|(0<< PD5)|(0<< PD6)|(0<< PD7);
}

//формирование задержки в Pause_ms миллисекунд
void delay(long unsigned int Pause_ms)
{
  counter = 0;
  while (counter < Pause_ms)
  {}
}

void main()
{
  SREG |= (1 << 7); //Разрешаем прерывания
  init_timer2();  //Включаем таймер2 на каждые 64 такта, считать до 173
  init_io_ports();  //Включаем порты ввода/вывода
  while(1)
  {
    //Обработка кнопки 0  
    if (B0Pressed == 1) //Если произошло нажатие на кнопку, 
    {                   // уведичивает PORTB, ждет отпускания
      PORTB++;
      B0Pressed = 0;
      while ((PINC & (1 << PC0)) == 1)
      {}
    }
    else
    {
      if ((PINC & (1 << PC0)) == 1)      //Фиксирует нажатие
      {
        delay(50);        //Устранение "дребезга клавиш"
        if ((PINC & (1 << PC0)) == 1)    //Проверяет нажатие
        {
          B0Pressed = 1;  //Устанавливает флаг "кнопка нажата"
        }
      }    
    }
    //Обработка кнопки 1
    if (B1Pressed == 1)  //Если произошло нажатие на кнопку, 
    {                     // уменьшает PORTB, ждет отпускания
      PORTB--;
      B1Pressed = 0;
      while ((PINC & (1 << PC1)) == 2)
      {}
    }
    else
    {
      if ((PINC & (1 << PC1)) == 2)      //Фиксирует нажатие
      {
        delay(200);        //Устранение "дребезга клавиш"
        if ((PINC & (1 << PC1)) == 2)    //Проверяет нажатие
        {
          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
http://www.kernelchip.ru



© KERNELCHIP 2006 - 2023