В предыдущем примере был подробно рассмотрен принцип работы ШИМ в AVR микроконтроллерах, на примере ATtiny13. Теперь же рассмотрим пример реализации ШИМ регулятора.
Все опыты я провожу на своей отладочной плате, соответственно код привожу применительно к ней:
/*
* tiny13_board_adc_pwm
* Демо-прошивка отладочной платы на ATtiny13.
* Демонстрация работы ШИМ-регулятора (в режиме коррекции фазы) на двух каналах:
* неинверсный сигнал на выходе OC0A, инверсный - на выходе OC0B.
* ШИМ-сигнал регулируется переменным резистором:
* напряжение с него подаётся на вход АЦП
* и, в зависимости от величины измеренного напряжения,
* изменяется время отсчёта для таймера ШИМ.
*/
#define F_CPU 1200000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#define LED0 PB0 // OC0A
#define LED1 PB1 // OC0B
#define ANALOG_IN PB4 //ADC2
// Обработчик прерывания по завершению преобразования АЦП
ISR(ADC_vect)
{
// Записываем значение, полученное на выходе АЦП в регистры сравнения таймера.
// Здесь же, задаём минимальный порог свечения светодиода, после которого он будет гарантированно гаснуть
OCR0A=(ADCH < 10) ? 0 : ADCH;
OCR0B=(ADCH > 245) ? 255: ADCH;
}
int main(void)
{
// Светидиоды:
DDRB |= (1 << LED0)|(1 << LED1); // выходы = 1
PORTB &= ~((1 << LED0)|(1 << LED1)); // по умолчанию отключены = 0
// Перем. резистор:
DDRB &= ~(1 << ANALOG_IN); // вход = 0
// Таймер для ШИМ:
TCCR0A = 0xB1; // режим коррекции фазы ШИМ, неинверсный сигнал на выходе OC0A, инверсный - на выходе OC0B
TCCR0B = 0x02; // предделитель тактовой частоты CLK/8
TCNT0 = 0; // начальное значение счётчика
OCR0A = 0; // регистр сравнения A
OCR0B = 0; // регистр сравнения B
// Настройка АЦП:
ADMUX = 0x22; // опорное напряжение - VCC, левое ориентирование данных, выбран вход ADC2 (на нём висит перем. резистор)
ADCSRA = 0xEA; // АЦП включен, запуск преобразования, режим автоизмерения, прерывание по окончанию преобразования, частота CLK/4
ADCSRB = 0x00; // режим автоизмерения: постоянно запущено
DIDR0 |= (1 << ANALOG_IN); // запрещаем цифровой вход на ноге аналогового входа
sei(); //разрешаем глобально прерывания
while(1)
{
}
}
Видим, что код программы очень похож на код из предыдущего примера. Разница лишь в том, что в предыдущем примере, для демонстрации изменения ШИМ-сигналов на выходе, значение регистров сравнения плавно изменялось в бесконечном цикле, а здесь же — мы будем менять их вручную, с помощью переменного резистора, подключенного ко входу аналого-цифрового преобразователя (АЦП). На моей плате переменный резистор подключен ко входу ADC2 (PB4). АЦП будет отслеживать изменение входного напряжения и соответственно изменять значения регистров сравнения для таймера ШИМ.
Настройка АЦП
Рассмотрим подробнее, какие регистры отвечают за работу АЦП.
Регистр ADMUX (ADC Multiplexer Selection Register).
Для ATtiny13 его структура выглядит следующим образом:
Бит 6 — REFS0 (Reference Selection Bit): выбирает опорный сигнал напряжения для АЦП: *
- 0 — в качестве опорного используется напряжение Vсс;
- 1 — используется внутренний источник опорного напряжения 1.1В.
Бит 5 — ADLAR (ADC Left Adjust Result): Определяет порядок записи результатов преобразования в регистры ADCL и ADCH (см. ниже).
Биты 1-0 — MUX1-0 (Analog Channel Selection Bits): Отвечают за выбор входного канала: *
- 00 — ADC0 (PB5);
- 01 — ADC1 (PB2);
- 10 — ADC2 (PB4);
- 11 — ADC3 (PB3).
* Если какой-либо из битов REFS0, MUX0, MUX1 изменяется во время преобразования, изменение не будет действовать до тех пор, пока это преобразование не будет завершено (пока не установится бит ADIF в регистре ADCSRA).
Остальные биты не используются (зарезервированы) и доступны только для чтения (в них всегда нули).
Регистр ADCSRA (ADC Control and Status Register A):
Бит 7 — ADEN (ADC Enable): Установка этого бита активирует АЦП. Если сбросить этот бит во время преобразования, то преобразование остановится.
Бит 6 — ADSC (ADC Start Conversion): Запускает преобразование. В режиме одиночного преобразования, нужно устанавливать этот бит для каждого преобразования. В режиме автоматического преобразования — нужно установить этот бит для запуска первого преобразования.
Бит 5 — ADATE (ADC Auto Trigger Enable): Активирует автопреобразование. АЦП начнет преобразование на положительном фронте триггерного сигнала. Источник триггера выбирается установкой бита выбора триггера АЦП: ADTS регистра ADCSRB.
Бит 4 — ADIF (ADC Interrupt Flag): Флаг прерывания АЦП. Устанавливается по завершению преобразования, когда регистры данных обновились.
Бит 3 — ADIE (ADC Interrupt Enable): Разрешает прерывания АЦП.
Биты 2-0 — ADPS2-0 (ADC Prescaler Select Bits): Устанавливают предделитель тактовой частоты для АЦП:
- 000 — 2;
- 001 — 2;
- 010 — 4;
- 011 — 8;
- 100 — 16;
- 101 — 32;
- 110 — 64;
- 111 — 128.
Регистры данных ADCH и ADCL.
Результат аналого-цифрового преобразования может принимать значения от 0 до 2n-1 (где n — число разрядов АЦП). В ATtiny13 на борту десятиразрядный АЦП. Соответственно, результат АЦП может принимать 1024 значения (от 0 до 1023).
0 (0x00) — будет соответствовать входному напряжению 0В;
1023 (0x3FF) — входному напряжению, равному выбранному опорному (в зависимости от значения бита REFS0 регистра ADMUX).
По завершению преобразования, результат записывается в регистры данных ADCH и ADCL (соответственно, cтарший и младший регистры данных АЦП), в зависимости от значения бита ADLAR регистра ADMUX. Если бит ADLAR установлен — то ориентирование данных левое, если сброшен (по умолчанию) — то правое:
Когда ADCL считывается, регистр данных ADC не обновляется, пока не будет прочитан ADCH. Таким образом, если выбрано левое ориентирование данных (ADLAR=1) и нам достаточно 8-битной точности, то достаточно прочитать значение ADCH. В противном случае — сначала нужно считать значение ADCL, затем ADCH.
Регистр ADCSRB (ADC Control and Status Register B):
Биты 2-0 — ADTS2-0 (ADC Auto Trigger Source): Выбирают источник сигнала для старта преобразования в режиме автоизмерения. Преобразование будет запускаться каждый раз при установке выбранного флага прерывания:
- 000 — постоянно запущено;
- 001 — аналоговый компаратор;
- 010 — внешнее прерывание INT0;
- 011 — таймер/счётчик T0, по совпадению с регистром сравнения A;
- 100 — таймер/счётчик T0, по переполнению;
- 101 — таймер/счётчик T0, по совпадению с регистром сравнения B;
- 110 — внешнее прерывание PCINT0.
Бит 6 — ACME (Analog Comparator Multiplexer Enable): в этой статье я не буду затрагивать применение этого бита, т.к. аналоговый компаратор — это уже отдельная тема.
Остальные биты не используются (зарезервированы) и доступны только для чтения (в них всегда нули).
Регистр DIDR0 (Digital Input Disable Register 0).
Биты 5-2 — ADC3D-ADC0D (ADC3-0 Digital Input Disable): Если какой-либо из входов ADC3-0 используется как аналоговый, то в соответствующий бит ADC3D-ADC0D нужно устанавливать логическую единицу. При этом, буфер цифрового ввода на соответствующем выводе АЦП отключается для экономии энергии. Соответствующий бит PIN-регистра при этом всегда будет содержать ноль.
Возвращаемся к нашей программе
Итак, мы настроили таймер для ШИМ, и запустили АЦП: он будет постоянно отслеживать напряжение на входе ADC2, регулируемое переменным резистором, а каждый раз по окончанию преобразования будет срабатывать прерывание ISR(ADC_vect), в теле которого будут изменяться значения регистров сравнения для ШИМ-таймера, в зависимости от измеренного напряжения на входе АЦП. И не забываем, после настройки прерывания, разрешить прерывания глобально!
Полезные ссылки
ATtiny13 Datasheet (официальный)
ATtiny13 Datasheet (урезанный вариант)
AVR. Учебный курс. Использование АЦП
Модуль АЦП
Прокомментируйте, пожалуйста, еще один момент.
ADMUX = 0x22; // опорное напряжение — VCC, левое ориентирование данных, выбран вход ADC2 (на нём висит перем. резистор)
0х22 в десятичном формате это 0b100010. 6 цифр. Регистр- ADMUX- 8 бит. Используются только 4 бита. Как так получается?
Или ,может быть, заполнить 7,4,3 и 2 биты нулями?
я так понимаю.
0b100010 это 7 бит- пустой,его нет.
1- REFSO, используем VCC
0- ADLAR
4- нет
3- нет
2- нет
1 конфиг ноги, понятно
0 конфиг ноги, понятно.
можно ли писать вместо неиспользуемого бита- 0 для сохранения 8 битного регистра?
я просто не понимаю, откуда берется 6цифр?
если написать просто 0b0110 — по числу используемых регистров- компилируется, но не работает.
Объясните, пожалуйста, этот момент.
Заранее спасибо.
Значит так.
0x22=0b100010. Старшие два нулевых бита тут просто пропущены, а вообще, в нашем случае будет так: 0x22=0b00100010. Соответственно, Ваше 0b0110 тут не прокатит, потому что 0b0110=0b00000110. Как видите, это совсем не то же самое, что 0b00100010.
Чтобы было нагляднее, лучше открыть даташит на тини13 (стр.98, пункт 14.12.1) и мы увидим, что биты у нас нумеруются с 0 (младший, справа) до 7 (старший, слева). То есть у нас получается:
— бит 0 = 0 — MUX0
— бит 1 = 1 — MUX1
— бит 2 = 0 — не используется
— бит 3 = 0 — не используется
— бит 4 = 0 — не используется
— бит 5 = 1 — ADLAR
— бит 6 = 0 — REFS0
— бит 7 = 0 — не используется
Там же, ниже, читаем: биты 2-4,7 — доступны только для чтения и всегда = 0, так что при записи значения регистра, можете смело забивать их нулями (так будет нагляднее), хотя они в любом случае останутся равными 0, даже если попытаться записать в них единицы.
спасибо огромное. буду разбираться
спасибо огромное! Хотелось бы видеть еще больше статей по программированию AT13. в частности, интересует обработка сигналов с аналогового входа .
Здравствуйте. Огромное спасибо за интересный материал. очень помогает.
Объясните, пожалуйста, один момент. Например,
TCCR0A = 0xB1 В1 в двоичном формате -это 0b10110001
Из даташита я понял, как выставляются 1и 0 по необходимым задействованным битам регистров.
Мне кажется , проще и нагляднее сразу писать в двоичном виде, в шестнадцатеричном. Или это все конфигурируется заранее и на выходе получается 16-ричное число?
Объясните, пожалуйста.
Здравствуйте. Вы всё верно поняли. Действительно, в двоичном виде запись получается нагляднее. А по факту можно писать хоть в двоичном (0b10110001), хоть в шестнадцатиричном (0xB1), и даже в десятичном (177) — как кому удобнее в конкретном случае. Компилятор примет любой вариант, лишь бы синтаксис в коде был соблюдён.
спасибо. и все-таки- (0xB1) -это результат какого-то конфигуратора или мы сначала пишем конфигурацию в двоичном коде а потом переводим в шестнадцатиричный?
еще один вопрос.
OCR0A=(ADCH 245) ? 255: ADCH;
тут применен тернарный оператор. Можно как-то подробнее расписать эту строчку. все-таки тернарный оператор для новичка- сложновато.
В данном случае 0xB1 — это не результат какого-то конфигуратора, а просто запись значения регистра в 16-ричным исчеслении, Конфигурируем мы в уме, устанавливая нужные значения битов (в двоичном виде), а потом полученное значение пишем в коде в том виде, в каком нам удобно.
Что касается тернарного оператора — тут всё просто: в данной строчке мы указываем, что если ADCH больше 245 (значение в скобках истинно), то OCR0A = 255 (значение до двоеточия); в противном случае — OCR0A=ADCH (значение после двоеточия). То есть в общем виде синтаксис такой:
ПЕРЕМ=(условие) ? если_условие_истинно : если_условие_ложь;
По сути, такая запись эквивалентна следующей, просто более компактна:
if (ADCH > 245) OCR0A=255; else OCR0A=ADCH;