Главная > AVR > Краткое пособие по микроконтроллерам AVR. Часть 2.

Краткое пособие по микроконтроллерам AVR. Часть 2.

Прерывания. Таймеры/счетчики.

Таймеры — еще один классический модуль, присутствующий практически во всех МК. Он позволяет решать множество задач, самая распространная из которых — задание стабильных временных интервалов. Второе по популярности применение — генерация ШИМ (о нем далее) на выводе МК. Несмотря на то, что, как уже было сказано, применение таймеров отнюдь не ограничивается этими задачами, здесь будут рассмотрены только эти две как наиболее распространенные.

Сам по себе таймер представляет собой двоичный счетчик, подключенный к системе тактирования микроконтроллера через дополнительный делитель. К нему, в свою очередь, подключены блоки сравнения (их может быть много), которые способны выполнять разные полезные функции и генерировать прерывания — в зависимости от настроек. Упрощенно устройство таймера можно представить следующим образом:

Настройка таймера, как и всей остальной периферии, производится через его регистры.

Генерация временных интервалов с помощью таймера.

Прерывания.

Из названия явствует, что главным назначением блоков сравнения является постоянное сравнение текущего значения таймера со значением, заданным в регистре OCRnX. Уже упоминалось, что имена регистров часто несут в себе глубокий сакральный смысл — и регистры сравнения не являются исключением. Так, n обозначает номер таймера, X — букву (тоже способ нумерации, блоков сравнения может быть много) регистра сравнения. Таким образом, OCR1A можно понять как Output Compare Register of 1st timer, unit A. К слову, искушенному эмбеддеру это даcт возможность предположить, что, возможно, существует таймер 0 и регистр сравнения B…

Итак, блоки сравнения могут генерировать прерывания при каждом совпадении значения таймера (к слову, оно находится в регистре TCNTnTimer/CouNTer #n) с заданым числом. Читателю уже должно быть знакомо понятие прерывания, однако на всякий случай освежим его в памяти, а заодно и поговорим о том, как его описать на С. Так вот, вышесказанное значит, что, как только случится описанное событие, процессор сохранит номер текущей команды в стеке и перейдет к выполению специально определенного кода, а после вернется обратно. Все происходит почти так же, как и при вызове обычной функции, только вызывается она на аппаратном уровне. Объявляются такие функции с помощью макроса, объявленного в avr/interrupt.h (ISR — «Interrupt Service Routine», «обработчик прерывания»):

ISR (<имя вектора прерывания>)
{
  /*код обработчка прерывания*/
}

Каждому прерыванию (естесственно, их много) соответствует т.н. вектор прерывания — константа, также объявленная в avr/interrupt. Например, обработчик прерывания по совпадению значения таймера со значением регистра OCR1A будет иметь следующий вид:

ISR (TIMER1_COMPA_vect)
{
  /*код обработчика*/
}

Несомненно, проницательный читатель уже догадался, каким образом формируются имена векторов. Тем не менее, полный список этих констант можно посмотреть в документации на avr-libc (библиотека стандартных функций для AVR-GCC).

Итак, для того, чтобы получить функцию, вызываемую через точные промежутки времени, нам осталось только сконфигурировать блок сравнения. Нужные настройки находятся в регистрах TCCR1B, TIMSK1 и, конечно, OCR1A. Здесь нам придется обратиться к даташиту на ATmega48.


Даташит (от англ. datasheet) — файл технической документации, описание конкретного прибора (микросхемы, транзистора и т.д.). Содержит всю информацию о характеристиках и применении компонента. Почти всегда имеет формат PDF. Обычно гуглится как «<название компонента> pdf».


Глядим:

Последние три бита управляют предделителем, упомянутым в самом начале (остальные же нас пока не интересуют):

Сконфигурируем таймер так, чтобы прерывания происходили два раза в секунду. Выберем предделитель 64; для этого установим биты CS11 и CS10:

TCCR1B=(1<<CS11) | (1<<CS10);

Тогда частота счета составит 8МГц/64=125КГц, т.е. каждые 8мкС к значению TCNT1 будет прибавляться единица. Мы хотим, чтобы прерывания происходили с периодом 500мС. Очевидно, что за это время таймер досчитает до значения 500мС/8мкС=62500, или 0xF424. Таймер 1 — шестнадцатибитный, так что все в порядке.

OCR1A=0xF424;

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

Осталось только разрешить прерывание по совпадению — за него отвечает бит в регистре TIMSK1:

Про него написано следующее:

Итак, устанавливаем нужное значение:

TIMSK1=(1<<OCIE1A);

Кроме того, следует помнить, что перед использованием прерываний необходимо их глобально разрешить вызовом функции sei(). Для глобального запрета прерываний служит функция cli(). Эти функции устанавливают/очищают бит I в регистре SREG, управляя самой возможностью использования такого механизма, как прерывания. Регистры же вроде TIMSKn — не более чем локальные настройки конкретного модуля.

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


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

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

ISR (TIMER1_COMPA_vect)
{
  TCNT1=0;

  if (PORTB & (1<<PB0))
    PORTB&=~(1<<PB0);
  else
    PORTB|=(1<<PB0);
}

void main(void)
{
  DDRB=0xFF;
  PORTB=0;

  OCR1A=0xF424;
  TIMSK1=(1<<OCIE1A);
  TCCR1B=(1<<CS11) | (1<<CS10);

  sei();

  while (1);
}

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

Генерация ШИМ с помощью таймера.

При определеных настройках блоки сравнения позволяют организовать аппаратную генерацию ШИМ-сигнала на ножках МК, обозначенных как OСnX:


ШИМ (PWM) — Широтно-Импульсная Модуляция (Pulse Width Modulation). ШИМ-сигнал представляет собой последовательность прямоугольных импульсов с изменяющейся длительностью:

Для ШИМ вводятся две родственные характеристики — коэффициент заполнения (duty cycle, D) и скважность — величина, обратная коэффицинту заполнения. Коэффициент заполнения представляет собой отношение времени импульса к длительности периода:

Коэффициент заполнения часто выражается в процентах, но так же распространена запись в десятичных дробях.

Значение ШИМ для народного хозяйства заключается в том, что действующее значение напряжения такого сигнала прямо пропоционально коэффициенту заполнения:

— с этим интегралом пособие смотрится солиднее; зависимость же выражается следующей формулой:

Uavg — среднее значение напряжения (тут — оно же действующее);
D — коэффициент заполнения;
Up-p — амплитуда импульса.

Таким образом, ШИМ является простым способом получить аналоговый сигнал с помощью микроконтроллера — для этого такую последовательность импульсов надо подать на фильтр низких частот (который, кстати, и является физическим воплощением интеграла, записанного выше).


Наиболее употребительным режимом ШИМ является т.н. Fast PWM (об остальных режимах можно прочесть непосредственно в документации), поэтому рассмотрим его. В этом случае блоки сравнения работают следующим образом: с обнулением таймера на выход OCnX подается высокий уровень; как только таймер досчитает до числа, записанного в OCRnX, OCnX переводится в состояние низкого уровня. Все это повторяется с периодом переполнения счетчика. Получается, что ширина выходного импульса зависит от значения OCRnX, а выходная частота равна тактовой частоте таймера, поделенной на его максимальное значение. Рисунок из даташита поясняет сказанное:

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

Настройка блока сравнения для генерации ШИМ.

Здесь нам опять поможет документация. Итак, сначала надо перевести блок сравнения в режим генерации ШИМ и выбрать интересующий выход из доступных. Эти настройки доступны в регистре TCCR0A:

Нас интересуют биты WGMxx и COMnXn. Про них сказано следующее:

Т.е., нас интересуют биты WGM00 и WGM01 — Fast PWM mode,

а также COM0A1 — non-inverting PWM на выводе OC0A. Настраиваем:

TCCR0A=(1<<COM0A1) | (1<<WGM01) | (1<<WGM00);

Естесственно, кроме этого выбранная ножка должна быть настроена на выход с помощью регистра DDR соответствующего порта.


Далее стоит инициализировать регистр OCR0A, например, значением 128 — 50% коэффициент заполнения:

OCR0A=128;

И, наконец, включить таймер, выбрав делитель. Тут все так же:


Обычно для ШИМ выбирается максимально возможная частота (для того, чтобы получить максимальное качество выходного сигнала). Т.е., целесообразно установить минимальное значение делителя:

TCCR0B=(1<<CS00);

На этом этапе настройка ШИМ завершается, и на выбранной ножке можно увидеть сигнал.

Как упомянуто выше, ШИМ — простой способ получения аналогового сигнала с помощью МК. Например, можно организовать плавное мигание светодиода (в этом случае роль интегратора-ФНЧ выполняет глаз наблюдателя, так что светодиод можно подключить к ножке МК через обычный резистор).


Некоторые моменты в предлагаемом примере требуют пояснения.

В списке включаемых файлов присутствует загадочный stdint.h — в этом файле объявлены типы с явно указанной разрядностью, например

uint8_tunsigned 8-bit integer type
uint16_tunsigned 16-bit integer type
uint32_tunsigned 32-bit integer type
int8_t — signed 8-bit integer type

и так далее. Такие типы способствуют единообразию и удобочитаемости программы. Кроме того, гарантируется, что при портировании кода разрядность данных останется указанной. И, кстати, uint8_t писать гораздо быстрее, чем unsigned char.

Модификатор volatile означает, что компилятору запрещается оптимизировать данную переменную. Например, если скомпилировать следующий пример:

void main(void)
{
  unsigned char i=0;

  while (1)
  {
    i++;
  } 
}

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


 

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

volatile uint8_t pwm_value=0,dn_count=0;

ISR (TIMER1_COMPA_vect)
{
  TCNT1=0;

  if (dn_count)   //плавно меняем яркость диода, по шагу за раз
    pwm_value--;
  else
    pwm_value++;

  if (pwm_value==0)   //проверка границ, переключение разгорание/затухание
    dn_count=0;
  if (pwm_value==0xFF)
    dn_count=1;

  OCR0A=pwm_value; //устанавливаем новый коэфф. заполнения
}

void main(void)
{
  DDRD=0xFF; //настройка порта на выход
  PORTD=0;

  OCR1A=0xF424; //константа, определяющая частоту прерываний
  TIMSK1=(1<<OCIE1A); //разрешаем прерывание по совпадению канала А
  TCCR1B=(1<<CS11) | (1<<CS10); //запускаем таймер 1

  TCCR0A=(1<<COM0A1) | (1<<WGM01) | (1<<WGM00); //таймер 0 будет генерировать ШИМ
  OCR0A=128; //начальное значение ШИМ
  TCCR0B=(1<<CS00); //запускаем таймер 0

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

  while (1); //все, дальше процесс идет на прерываниях и аппаратном ШИМе
}
Рубрики:AVR
  1. Дмитрий К.
    27/04/2017 в 13:34

    В первом примере можно упростить, если использовать ИСКЛЮЧАЮЩЕЕ-ИЛИ:
    ISR (TIMER1_COMPA_vect)
    {
    TCNT1=0;

    if (PORTB & (1<<PB0))
    PORTB&=~(1<<PB0);
    else
    PORTB|=(1<<PB0);
    }

    заменить на :
    ISR (TIMER1_COMPA_vect)
    {
    TCNT1=0;

    PORTB ^= 1;
    }

  2. Mick
    18/06/2013 в 13:21

    Я не хочу с вами спорит на тему того, что есть правильно, а что нет. Я говорю с позиции человека, который читает ваш материал, да и любой другой материал тоже. Тут дело ен в лени, а в том, что каждый раз перематывая туда-сюда уходиит куча времени и это не способствует лучшему усвоению. Здорово — это когда всё сразу етсь под рукой. Делаешь свой проект по аналогии не тратя время на перерывание чего-то ещё. И когда этот проект-аналог готов — уже сам лезешь дальше и получается лучше, тк основа есть! 🙂 Под комментариями в программе я не имею ввиду какие-то большие расшифровки, просто краткие напоминалочки, не более того 🙂

    • YS
      18/06/2013 в 14:33

      Ну я же уже сказал, что когда будет время, я поправлю статью. 🙂 Просто это надо заново переформатировать код. Тега code тут нет. 🙂

  3. Mick
    17/06/2013 в 11:45

    Я ещё не всё разобрал до конца, но первые впечатления такие: всё здорово, но НАДО бы сделать комментарии в тексте программы. Это хорошо и для понимания и для запоминания. Думаю, ещё комменты попишу, может быть что полезное скажу 😀

    • YS
      17/06/2013 в 20:05

      Добро пожаловать в мой блог! 🙂

      Хе-хе, комментарии — это домашнее задание читающим. 😉 Хотя я еще подумаю над этой идеей.

      • Mick
        18/06/2013 в 11:58

        Чтобы делать ДЗ — надо понимать. А если ты не понимаешь и пытаешься что-то сделать, уходит дофига времени и пропадает желание. Лучший вариант — это разжевать всё (если правда хочется кого-то научить),и потом уже делать по аналогии. После того, как выполнится аналогия, самому хочется поделать чего-то ещё, немного отклонённого от примера. Вот тут уже включаются мозги и творчество. И для этого есть база 🙂

        • YS
          18/06/2013 в 12:31

          Вообще да, это же метода к кружку. Там я разжевывал…

          Но вообще я учился именно так — по документации. У меня не было ни наставников, ни советчиков по микроконтроллерам. 😉

          А вообще, Вы не находите, что вся статья — это один большой комментарий к программе? 😉 Вот, посмотрите (большая картинка!). Просто читающему не надо лениться перематывать обратно (а то и в предыдущие статьи) и перечитывать. 😉 Если Вы читали введение, то знаете, что одна из целей пособия — заставить читателя работать и познавать самостоятельно.

          Но я подумаю над комментариями. Как будет время, может и добавлю.

          • Mick
            18/06/2013 в 13:22

            Я не хочу с вами спорит на тему того, что есть правильно, а что нет. Я говорю с позиции человека, который читает ваш материал, да и любой другой материал тоже. Тут дело ен в лени, а в том, что каждый раз перематывая туда-сюда уходиит куча времени и это не способствует лучшему усвоению. Здорово – это когда всё сразу етсь под рукой. Делаешь свой проект по аналогии не тратя время на перерывание чего-то ещё. И когда этот проект-аналог готов – уже сам лезешь дальше и получается лучше, тк основа есть! Под комментариями в программе я не имею ввиду какие-то большие расшифровки, просто краткие напоминалочки, не более того. (Сорри за дублирующийся коммент)

  1. No trackbacks yet.

Оставьте комментарий