Вот уже несколько раз я ругался странным словом ШИМ. Пора бы внести ясность и разьяснить что же это такое. Вообще, я уже расписывал этот режим работы, но все же повторюсь в рамках своего курса.
Вкратце, Широтно Импульсная Модуляция (в буржуйской нотации этот режим зовется PWM — Pulse Width Modulation) это способ задания аналогового сигнала цифровым методом, то есть из цифрового выхода, дающего только нули и единицы, получить какие-то плавно меняющиеся величины. Звучит как бред, но тем не менее работает. А суть в чем:
Представь себе тяжеленный маховик, который ты можешь вращать двигателем. Причем двигатель ты можешь либо включить, либо выключить. Если включить его постоянно, то маховик раскрутится до максимального значения и так и будет крутиться. Если выключить, то остановится за счет сил трения.
А вот если двигатель включать на десять секунд каждую минуту, то маховик раскрутится, но далеко не на полную скорость — большая инерция сгладит рывки от включающегося двигателя, а сопротивление от трения не даст ему крутится бесконечно долго.
Чем больше продолжительность включения двигателя в минуту, тем быстрей будет крутится маховик.
При ШИМ мы гоним на выход сигнал, состоящий из высоких и низких уровней (применимо к нашей аналогии — включаем и выключаем двигатель), то есть нулей и единицы. А затем это все пропускается через интегрирующую цепочку (в аналогии — маховик). В результате интегрирования на выходе будет величина напряжения, равная площади под импульсами.
Меня скважность (отношение длительности периода к длительности импульса), можно плавно менять эту площадь, а значит и напряжение на выходе. Таким образом, если на выходе сплошные 1, то на выходе будет напряжение высокого уровня, если нули, то ноль. А если 50% времени будет высокий уровень, а 50% - низкий, то 6 вольт. Интегрирующей цепочкой тут будет служить масса якоря двигателя, обладающего довольно большой инерцией.
А что будет, если взять и гнать ШИМ-сигнал не от нуля до максимума, а от минуса до плюса. Скажем от +12 до -12. А можно задавать переменный сигнал! Когда на входе ноль, то на выходе -12V, когда один, то +12V. Если скважность 50%, то на выходе 0V. Если скважность менять по синусоидальному закону от максимума к минимуму, то получим... правильно! Переменное напряжение. А если взять три таких ШИМ-генератора и гнать через них синусоиды, сдвинутые на 120 градусов между собой, то получим самое обычное трехфазное напряжение, а значит привет бесколлекторные асинхронные и синхронные двигатели — фетиш всех авиамоделистов. На этом принципе построены все современные промышленные привода переменного тока. Всякие Unidrive и Omron Jxx
В качестве сглаживающей интегрирующей цепи в ШИМ может быть применена обычная RC-цепочка:
Так, принцип понятен, приступаем к реализации.
Аппаратный ШИМ
В случае ATMega16 проще всего сделать на его ШИМ-генераторе, который встроен в таймеры. Причем в первом таймере у нас целых два канала. Так что без особого напряга ATmega16 может реализовать одновременно четыре канала ШИМ.
Как это реализовано
Выходы сравнения выведены наружу, на выводы микроконтроллера
На демоплате Pinboard к этим выводам как раз подключены светодиоды. А если поставить джамперы вдоль, в сторону надписи RC, то к выводу ШИМ будет подключена интегрирующая цепочка.
Для Pinboard II разница в подключении невелика. Джамперы тут сгруппированы в один блок. А светодиоды и RC-цепочки сгруппированы в левом верхнем углу платы.
Предположим, что мы настроили наш ШИМ-генератор так, чтобы когда значение в счетном регистре больше чем в регистре сравнения, то на выходе у нас 1, а когда меньше, то 0.
Что при этом произойдет? Таймер будет считать как ему и положено, от нуля до 256, с частотой которую мы настроим битами предделителя таймера. После переполнения сбрасывается в 0 и продолжает заново.
Как видишь, на выходе появляются импульсы. А если мы попробуем увеличить значение в регистре сравнения, то ширина импульсов станет уже.
Так что, меняя значение в регистре сравнения, можно менять скважность ШИМ-сигнала. А если пропустить этот ШИМ-сигнал через сглаживающую RC-цепочку (интегратор), то получим аналоговый сигнал.
У таймера может быть сколько угодно регистров сравнения. Зависит от модели МК и типа таймера. Например, у Атмега16
Итого — четыре канала. В новых AVR бывает и по три регистра сравнения на таймер, что позволяет одним МК организовать просто прорву независимых ШИМ-каналов.
Самих режимов ШИМ существует несколько:
Fast PWMВ этом режиме счетчик считает от нуля до 255, после достижения переполнения сбрасывается в нуль и счет начинается снова. Когда значение в счетчике достигает значения регистра сравнения, то соответствующий ему вывод ОСхх сбрасыватся в ноль. При обнулении счетчика этот вывод устанавливается в 1. И все!
Частота получившегося ШИМ-сигнала определяется просто: Частота процесора 8 Мгц, таймер тикает до 256 с тактовой частотой. Значит один период ШИМ будет равен 8'000'000/256 = 31'250 Гц. Вполне недурно. Быстрей не получится — это максимальная скорость на внутреннем 8 Мгц тактовом генераторе. Но если переключить FUSE-биты на внешний кварц, то можно раскачать МК на 16Мгц.
Еще есть возможность повысить разрешение, сделав счет 8-, 9-, 10-разрядным (если разрядность таймера позволяет), но надо учитывать, что повышение разрядности, вместе с повышением дискретности выходного аналогового сигнала, резко снижает частоту ШИМ.
Phase Correct PWMВ результате, на выходе получаются импульсы всегда одинаковой скважности, но разной частоты. А чаще всего этот режим применяется, когда надо таймером отсчитывать периоды (и генерить прерывание) с заданной точностью.
Например, надо нам прерывание каждую миллисекунду. И чтобы вот точно. Как это реализовать проще? Через Режим СТС! Пусть у нас частота 8Мгц.
Прескалер будет равен 64, таким образом, частота тиков таймера составит 125'000 Гц. А нам надо прерывание с частотой 1000 Гц. Поэтому настраиваем прерывание по совпадению с числом 125.
Дотикал до 125 — дал прерывание, обнулился. Дотикал до 125 — дал прерывание, обнулился. И так бесконечно, пока не выключим.
Вот вам и точная тикалка.
Нет, конечно, можно и вручную. Через переполнение, т.е. дотикал до переполнения, загрузил в обработчике прерывания заново нужные значение TCNTх=255-125, сделал нужные полезные дела и снова тикать до переполнения. Но ведь через СТС красивей! :)
Аппаратура
Итак, тут правят бал регистры TCCR1A и TCCR1B.
Распишу их по битам.
COMxx1 | COMxx0 | Режим работы выхода |
0 | 0 | вывод отцеплен от регистра сравнения и не меняется никак. |
0 | 1 | Поведение вывода зависит от режима заданного в WGM, различается для разных режимов (FastPWM, FC PWM, Compar out) и разных МК, надо сверяться с даташитом. |
1 | 0 | прямой ШИМ (сброс при совпадении и установка при обнулении счета) |
1 | 1 | обратный ШИМ (сброс при обнулении и установка при совпадении) |
Регистр TCCR1A, биты WGM11 и WGM10 вместе с битами WGM12 и WGM13, находящимися в регистре TCCR1B задают режим работы генератора.
WGM13 | WGM12 | WGM11 | WGM10 | Режим работы |
0 | 1 | 0 | 1 | Fast PWM 8 бит |
0 | 1 | 1 | 0 | Fast PWM 9 бит |
0 | 1 | 1 | 1 | Fast PWM 10 бит |
Другие комбинации битов WGM задают режимы Phase Correct PWM и CTC (сброс OCxx при совпадении). Если интересно, то читай даташит, я для себя много интересного там не нашел, кроме Phase Correct PWM. И то мне сейчас важней скорость, а не точность фазы :)
После остается только запустить таймер, установив бит CS10 (подсчет тактовых импульсов с делителем 1:1)
Пример кода:
Попробуем поиграться яркостью светодиодов с помощью ШИМ-сигналов. Подключи джамперы, чтобы запитать светодиоды LED1 и LED2
Теперь все готово, можно писать код. Вначале в раздел инициализации устройств добавляю настройку таймера на запуск ШИМ и подготовку выводов.
;FastPWM Init SETB DDRD,4,R16 ; DDRD.4 = 1 Порты на выход SETB DDRD,5,R16 ; DDRD.5 = 1 ; Выставляем для обоих каналов ШИМ режим вывода ОС** сброс при совпадении. ; COM1A = 10 и COM1B = 10 ; Также ставим режим FAST PWM 8bit (таймер 16ти разрядный и допускает ; большую разрядность ШИМ-сигнала. Вплоть до 10 бит. WGM = 0101 ; Осталось только запустить таймер на частоте МК CS = 001 OUTI TCCR1A,2<<COM1A0|2<<COM1B0|0<<WGM11|1<<WGM10 OUTI TCCR1B,0<<WGM13|1<<WGM12|1<<CS10
Готово! Теперь ШИМ таймера1 генерит сигнал на выходаx OC1А и OC1B
Закинем в регистры сравнения первого и второго канала число 255/3=85 и 255/2 = 128
Так как ШИМ у нас 8-разрядный, то заброс идет только в младший разряд. Старший же остается нулем. Но регистры сравнения тут у нас 16-разрядные, поэтому грузить надо оба байта сразу. Не забыв запретить прерывания (это важно!!! ибо атомарный доступ)
CLI OUTI OCR1AH,0 OUTI OCR1AL,85 OUTI OCR1BH,0 OUTI OCR1BL,128 SEI
Поехали! :)
Прошиваем, тыкаемся в ноги микроконтроллера осциллографом — видим следующую картину по каналам:
Как мы и запланировали. С первого канала длительность импульса в 1/3 периода, а со второго в 1/2.
Ну и светодиоды горят с разной яркостью. Один ярче, другой тусклей. Меняя значение в регистрах OCR*** мы можем менять скважность.
Давай сделаем так, чтобы светодиод плавно менял свою яркость от нуля до максимума. Как помнишь, у нас там была программа, с мигающем по таймеру0 светодиодом. Немного ее подправим, сделаем так, чтобы по таймеру не светодиод мигал, а менялось значение в регистрах сравнения OCR1A и OCR1B. Причем меняться оно будет в разные стороны :)
; Main ========================================================= Main: LDS R16,TCNT ; Грузим числа в регистры LDS R17,TCNT+1 CPI R16,0x10 ; Сравниванем побайтно выдержку BRCS NoMatch CPI R17,0x01 ; Выдержку сделали поменьше = 0x0110 BRCS NoMatch ; Если совпало, то делаем экшн Match: CLI ; Запрет прерываний, т.к. атомарный доступ ; Меняем первый канал ; Особенность 16-разрядных регистров в том, что их надо правильно читать и записывать. ; Читают вначале младший, потом старший байты. Так надо, чтобы младший не успел измениться ; (он ведь может тикать по таймеру) пока читают первым старший. Укладывают их в обратном ; порядке. Сначала старший, потом младший. Правда, для регистров OCR это не имеет большой ; разницы -- они статичные, а вот для TCNT очень даже! IN R16,OCR1AL ; Достали первый байт сравнения IN R17,OCR1AH ; он 16-разрядный, но старший байт будет 0 INC R16 ; Увеличили OUT OCR1AH,R17 ; И сунули их обратно OUT OCR1AL,R16 ; Меняем второй канал IN R16,OCR1BL ; Достали второй байт сравнения IN R17,OCR1BH ; он 16-разрядный, но старший байт будет 0 DEC R16 ; Уменьшили OUT OCR1BH,R17 ; И сунули их обратно OUT OCR1BL,R16 SEI ; Конец атомарного доступа ; Теперь надо обнулить счетчик, иначе за эту же итерацию главного цикла ; Мы сюда попадем еще не один раз -- таймер-то не успеет натикать 255 значений, ; чтобы число в первых двух байтах счетчика изменилось. CLR R16 ; Нам нужен ноль. CLI ; Таймер меняется и в прерывании. Нужен ; атомарный доступ. Запрещаем прерывания OUT TCNT0,R16 ; Ноль в счетный регистр таймера STS TCNT,R16 ; Ноль в первый байт счетчика в RAM STS TCNT+1,R16 ; Ноль в второй байт счетчика в RAM STS TCNT+2,R16 ; Ноль в третий байт счетчика в RAM STS TCNT+3,R16 ; Ноль в первый байт счетчика в RAM SEI ; Разрешаем прерывания. ; Не совпало - не делаем :) NoMatch: NOP INCM CCNT ; Шарманка вращается дальше, вхолостую JMP Main
А теперь давайте включим режим с точной фазой (WGM = 0001) и посмотрим на то, как будет меняться скважность.
OUTI TCCR1A,2<<COM1A0|2<<COM1B0|0<<WGM11|1<<WGM10 OUTI TCCR1B,0<<WGM13|0<<WGM12|1<<CS10
ШИМ на прерываниях.
Но вот засада — плата уже разведена, захотелось ШИМ, а выводы OCxx уже задействованы под другие цели.
Ничего страшного, малой кровью можно это исправить. Также запускаем ШИМ, только:
Все просто :)
Пример:
;FastPWM Init на прерываниях ; ШИМ будет на выводах 3 и 6 порта D SETB DDRD,3,R16 ; DDRD.3 = 1 Порты на выход SETB DDRD,6,R16 ; DDRD.6 = 1 ; Выставляем для обоих каналов ШИМ режим вывода ОС** выключеным. ; COM1A = 00 и COM1B = 00 ; Также ставим режим FAST PWM 8bit (таймер 16-разрядный и допускает ; большую разрядность ШИМ-сигнала. Вплоть до 10 бит. WGM = 0101 ; Осталось только запустить таймер на частоте МК CS = 001 OUTI TCCR1A,0<<COM1A0|0<<COM1B0|0<<WGM11|1<<WGM10 OUTI TCCR1B,0<<WGM13|1<<WGM12|1<<CS10 SETB TIMSK,OCIE1A,R16 ; Включаем прерывание по сравнению А SETB TIMSK,OCIE1B,R16 ; Включаем прерывание по сравнению Б SETB TIMSK,TOIE1,R16 ; Включаем прерывание по переполнению Т1 ; Причем в режиме WGM=1010 переполнение ; будет на FF, т.е. таймер работает как ; 8-разрядный.
Осталось только прописать обработчики и вектора:
.CSEG .ORG $000 ; (RESET) RJMP Reset .ORG $002 RETI ; (INT0) External Interrupt Request 0 .ORG $004 RETI ; (INT1) External Interrupt Request 1 .ORG $006 RETI ; (TIMER2 COMP) Timer/Counter2 Compare Match .ORG $008 RETI ; (TIMER2 OVF) Timer/Counter2 Overflow .ORG $00A RETI ; (TIMER1 CAPT) Timer/Counter1 Capture Event .ORG $00C RJMP Timer1_OCA ; (TIMER1 COMPA) Timer/Counter1 Compare Match A .ORG $00E RJMP Timer1_OCB ; (TIMER1 COMPB) Timer/Counter1 Compare Match B .ORG $010 RJMP Timer1_OVF ; (TIMER1 OVF) Timer/Counter1 Overflow .ORG $012 RJMP Timer0_OV ; (TIMER0 OVF) Timer/Counter0 Overflow .ORG $014 RETI ; (SPI,STC) Serial Transfer Complete .ORG $016 RETI ; (USART,RXC) USART, Rx Complete .ORG $018 RETI ; (USART,UDRE) USART Data Register Empty .ORG $01A RETI ; (USART,TXC) USART, Tx Complete .ORG $01C RETI ; (ADC) ADC Conversion Complete .ORG $01E RETI ; (EE_RDY) EEPROM Ready .ORG $020 RETI ; (ANA_COMP) Analog Comparator .ORG $022 RETI ; (TWI) 2-wire Serial Interface .ORG $024 RETI ; (INT2) External Interrupt Request 2 .ORG $026 RETI ; (TIMER0 COMP) Timer/Counter0 Compare Match .ORG $028 RETI ; (SPM_RDY) Store Program Memory Ready .ORG INT_VECTORS_SIZE ; Конец таблицы прерываний ; Interrupts ============================================== Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI ; Вот наши обработчики на ШИМ Timer1_OCA: SBI PORTD,3 RETI Timer1_OCB: SBI PORTD,6 RETI Timer1_OVF: CBI PORTD,3 CBI PORTD,6 RETI ; End Interrupts ==========================================
Почему я в этих обработчиках не сохраняю регистры и SREG? А незачем! Команды SBI меняют только конкретные биты (а больше нам и не надо), не влияя на флаги и другие регистры.
Запустили...
Видео
И получили полную херню. Т.е. ШИМ как бы есть, но почему-то адово мерцает. А на осциллографе в этот момент полный треш. Кто виноват? Видимо, конфликт прерываний. Осталось только выяснить, где именно. Сейчас я вам дам практический пример реалтаймовой отладки :)
Итак, что мы имеем:
ШИМ, как таковой, работает. Скважность меняется. Значит, наш алгоритм верен.
Но длительности скачут. Почему? Видимо потому, что что-то мешает им встать вовремя. Когда у нас возникают фронты? Правильно — по прерываниям. А прерывания по таймерам. Т.е. врать не должны. Однако так получается. Давайте узнаем, в каком месте у нас конфликт.
Первым делом надо добавить в код обработчика отладочную инфу. Будем в обработчике прерываний инвертировать бит. Пусть это будет PD7 — зашли в обработчик, инверснули. Зашли — инверснули. В результате, у нас на выходе этого бита будет прямоугольный сигнал, где каждый фронт — сработка прерываний. Послужит нам как линейка, отмеряющая время.
; Interrupts ============================================== Timer0_OV: PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI ; Установка бита ШИМ канала А Timer1_OCA: SBI PORTD,3 RETI ; Установка бита ШИМ канала Б Timer1_OCB: SBI PORTD,6 RETI ;Сброс бита ШИМ канала А и Б Timer1_OVF: CBI PORTD,3 CBI PORTD,6 ;DEBUG PIN BEGIN --------------- PUSHF INVBM PORTD,7 POPF ;DEBUG PIN END ----------------- RETI
Инверсия бита невозможна без логических операций, поэтому надо сохранять флаги.
Видео
Из картинки стало понятно, что у нас накрывается прерывание по сравнению. Давайте попробуем посмотреть, с какими прерыванием происходит конфликт. Особых вариантов у нас нет — прерываний у нас тут четрые. А наиболее очевиден конфликт Timer0_OV vs Timer1_OCA vs Timer1_OCB.
OCA и OCB конфликтуют только тогда, когда счетные регистры у них сравниваются — вызов происходит почти одновременно, но сами обработчики короткие — всего несколько тактов, поэтому дребезг не столь сильный.
А вот Timer0_OV делает довольно мощный прогруз стека и еще вычитает четырехбайтную переменную. Т.е. тактов на 20 может задержать обработчик установки бита Timer1_OC*, от того и вылазят такие зверские дребезги.
Давайте проверим эту идею. Разрешим прерывания в обработчике Timer0_0V
; Interrupts ============================================== Timer0_OV: SEI PUSHF PUSH R17 PUSH R18 PUSH R19 INCM TCNT POP R19 POP R18 POP R17 POPF RETI ; Установка бита ШИМ канала А Timer1_OCA: SBI PORTD,3 RETI ; Установка бита ШИМ канала Б Timer1_OCB: SBI PORTD,6 RETI ;Сброс бита ШИМ канала А и Б Timer1_OVF: CBI PORTD,3 CBI PORTD,6 RETI
Картина сразу исправилась. Теперь более важное (для нас важное) прерывание задвигает обработчик от Таймера 0. Но тут надо просекать возможные риски:
ШИМ на таймерах
Когда совсем все плохо, то можно сделать на любом таймере. В обработчик прерывания по переполнению таймера заносим конечный автомат, который сначала загрузит в таймер длительность низкого уровня, а при следующем заходе — длительность высокого. Ну и, само собой, ноги процессора подергает как надо. Таким образом, на один таймер можно повесить дофига ШИМ-каналов, но задолбаешься все с кодовой реализацией всего этого. И процессорное время жрать будет некисло. Не говоря уже про дребезги, о которых только что было сказано. Это для эстетов извращенцев :)))))
Таймеры | Содержание | Передача данных через UART |