CUSTOMELECTRONICS.RU
Информационно-учебный блог о разработке электроники
Эл. почта: info@customelectronics.ru

AVR. Аппаратный ШИМ микроконтроллера

Широтно-импульсная модуляция (pulse width modulation, PWM) часто используется в цифровой технике. AVR-микроконтроллеры имеют встроенную аппаратную возможность генерировать ШИМ-сигнал.
Особенность этого способа модуляции заключается в том, что он позволяет регулировать постоянную составляющую выходного сигнала средствами логических элементов. Другими словами можно изменять яркость светодиодов, скорость вращения коллекторных двигателей, температуру нагревателей и т.д.
Временная диаграмма ШИМ-сигнала:

ШИМ-сигнал

ШИМ-сигнал

Подготовка к работе

На нашей плате TutorShield есть трехцветный светодиод. Отдельные цвета подключены к выводам PB1, PB2 и PB3 на которые можно вывести ШИМ-сигнал. Для возможности управления им, установите перемычку "color" так, как показано на рисунке. Всех остальных перемычек быть не должно.
ColorLed
Голубой светодиод подключен к выводу PB1, оранжевый к PB2, зеленый к PB3.

Первый пример

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

#define F_CPU 16000000UL //16MHz
#include <avr/io.h>
#include <util/delay.h>

#define BLUE_PB     1
#define BLUE_PWM    OCR1A
#define ORANGE_PB   2
#define ORANGE_PWM  OCR1B
#define GREEN_PB    3
#define GREEN_PWM   OCR2

void pin_init(void) {
    DDRB |= (1<<BLUE_PB) | (1<<ORANGE_PB) | (1<<GREEN_PB);
    PORTB &= ~((1<<BLUE_PB) | (1<<ORANGE_PB) | (1<<GREEN_PB));
}

void timer1_init(void) {
    TCCR1A |= (1 << COM1A1) | (1 << WGM11);
    TCCR1B |= (1 << WGM13) | (1 << WGM12) | (1 << CS10);
    TCNT1 = 0x00;
    ICR1 = 0xFF;
    OCR1A = 0x00;
}

int main(void) {
    pin_init();
    timer1_init();
    while(1) {
        BLUE_PWM = 250;
        _delay_ms(1000);
        BLUE_PWM = 50;
        _delay_ms(1000);
    }
}

То что программа делает хорошо видно в функции main(). С периодичностью в 1000мс она изменяет яркость светодиода:

Теперь давайте подробно разберемся с настройкой таймера.

Настройка таймера

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

  1. Timer/Counter 1 Control Register A – TCCR1A. Бит COM1A1 это регистра отвечает за сброс таймера при совпадении. Бит WGM11 связан с битами WGM12 и WGM13 следующего регистра.
  2. Timer/Counter 1 Control Register B – TCCR1B. Биты WGM11, WGM12, WGM13, выставленные одновременно, переводят таймер в режим генерирования ШИМ-сигнала. Бит CS10 задает скорость счета таймера одновременно запускает таймер и задает делитель частоты тактового сигнала. Если не задать никакой делитель — таймер будет остановлен. С битом CS10 делитель будет равен единице, то есть таймер будет тактироваться от кварца.
  3. Timer/Counter 1 – TCNT1H и TCNT1L. В этих двух регистрах хранится состояние таймера. Именно в них с каждым "тиком" кварца увеличивается хранимое значение. Обнулим его, на всякий случай
  4. Input Capture Register 1 – ICR1H и ICR1L. Это период следования импульсов. То есть, когда содержимое регистра TCNT1 достигнет значения 0xFF начнется следующий период сигнала
  5. Output Compare Register 1A — OCR1AH и OCR1AL. Задает скважность. То есть пока в регистре TCNT1 значение меньше OCR1A, то на выводе "1", а после этого "0"

Все это гораздо проще понять по следующей картинке:

Диаграмма работы таймера

Диаграмма работы таймера

Мы предположим, в регистр OCR1A записано значение "100".
Тактовый сигнал у нас задан без делителя. Период следования тактовых импульсов 1/16МГц = 62,5нс. С каждым новым тактом, состояние регистра TCNT1 увеличивается на единицу. При это постоянно происходит его сравнение с регистрами OCR1A и ICR1. Как только он досчитывает до OCR1A происходит переключение состояния вывода из "1" в "0". При достижении значения ICR1 состояние TCNT1 сбрасывается в ноль, на выводе снова выставляется "1" и начинается следующий период сигнала.
Несложно посчитать период следования импульсов в нашем проекте: 255*62,5 = 15937,5нс. Примерно 16мкс. Частота при этом 62,5кГц. Это очень большая частота и снизить ее можно увеличив делитель или/и значение ICR1.
В итоге, чтобы изменить скважность сигнала остается только записать новое значение в регистр OCR1. Мы переопределили его директивой

#define BLUE_PWM    OCR1A

В бесконечном цикле программа именно это и происходит: устанавливаем относительную скважность 200, ждем 1с, скважность 50, ждем 1с. Осталось только самое простое — понять на каком выводе будет этот сигнал. Изменить это нельзя и формироваться он будет на выводе OC1A, который в микроконтроллере Atmega8 подключен к 13му пину PB1 (или D9 выводу Arduino).

Второй пример

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

#define F_CPU 16000000UL //16MHz
#include <avr/io.h>
#include <util/delay.h>

#define BLUE_PB     1
#define BLUE_PWM    OCR1A
#define ORANGE_PB   2
#define ORANGE_PWM  OCR1B
#define GREEN_PB    3
#define GREEN_PWM   OCR2

void pin_init(void) {
    DDRB |= (1<<BLUE_PB) | (1<<ORANGE_PB) | (1<<GREEN_PB);
    PORTB &= ~((1<<BLUE_PB) | (1<<ORANGE_PB) | (1<<GREEN_PB));
}

void timer1_init(void) {
    TCCR1A |= (1 << COM1A1) | (1 << WGM11);
    TCCR1B |= (1 << WGM13) | (1 << WGM12) | (1 << CS10);
    TCNT1 = 0x00;
    ICR1 = 0xFF;
    OCR1A = 0x00;
}

int main(void) {
    pin_init();
    timer1_init();
    while(1) {
        for (int i=0; i<256; i++) {
            BLUE_PWM = i;
            _delay_ms(10);
        }
    }
}

Если все сделано правильно — то работа будет выглядеть вот так:

Третий пример

Всего в микроконтроллере Atmega8 два аппаратных таймера — 16ти-битный и 8ми-битный. Причем в первом есть сразу два модуля сравнения и он может формировать одновременно два разных ШИМ-сигнала. Третий ШИМ-сигнал может быть сформирован при помощи второго таймера. То есть всего на трех выводах может быть сгенерирован аппаратный ШИМ. Это выводы МК PB1, PB2 и PB3. Именно этим обусловлены ограничения функции Arduino analogWrite().
В этом разделе настроим еще два канала и будем управлять остальными светодиодами.
Для этого потребуется только дополнительно выставить бит COM1B1 и начать управлять регистром OCR1B. На эти действия будет реагировать оранжевый светодиод, подключенный к выводу PB2.
Для использования третьего канала придется вставить дополнительную функцию timer2_init() для запуска второго таймера.

#define F_CPU 16000000UL //16MHz
#include <avr/io.h>
#include <util/delay.h>

#define BLUE_PB     1
#define BLUE_PWM    OCR1A
#define ORANGE_PB   2
#define ORANGE_PWM  OCR1B
#define GREEN_PB    3
#define GREEN_PWM   OCR2

void pin_init(void) {
    DDRB |= (1<<BLUE_PB) | (1<<ORANGE_PB) | (1<<GREEN_PB);
    PORTB &= ~((1<<BLUE_PB) | (1<<ORANGE_PB) | (1<<GREEN_PB));
}

void timer1_init(void) {
    TCCR1A |= (1 << COM1A1) | (1 << COM1B1) | (1 << WGM11);
    TCCR1B |= (1 << WGM13) | (1 << WGM12) | (1 << CS10);
    TCNT1 = 0x00;
    ICR1 = 0xFF;
    OCR1A = 0x00;
    OCR1B = 0x00;
}

void timer2_init(void) {
    TCCR2 |= (1 << COM21) | (1 << WGM21) | (1 << WGM20) | (1 << CS20);
    TCNT2 = 0x00;
    OCR2 = 0x00;
}

int main(void) {
    pin_init();
    timer1_init();
    timer2_init();
    while(1) {
        for (int i=0; i<256; i++) {
            BLUE_PWM = i;
            _delay_ms(10);
        }
        BLUE_PWM = 0;
        for (int i=0; i<256; i++) {
            ORANGE_PWM = i;
            _delay_ms(10);
        }
        ORANGE_PWM = 0;
        for (int i=0; i<256; i++) {
            GREEN_PWM = i;
            _delay_ms(10);
        }
        GREEN_PWM = 0;
    }
}

Второй таймер имеет гораздо меньше функций и настраивать его проще. Бит COM21 выводит сигнал на ногу. Биты WGM21 и WGM20 переводят таймер в режим формирования ШИМ. CS20 задает делатель тактового сигнала, равный нулю. Как видите, тут изменять частоту получится только при помощи делителя.
Вот видео работы этого примера:

Индивидуальные задания

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

Остальные статьи цикла можно найти здесь.

Мы будем очень рады, если вы поддержите наш ресурс и посетите магазин наших товаров shop.customelectronics.ru.

Метки: , , , , , Просмотров: 24954