Tryb PWM mikrokontrolerów AVR – dioda RGB

PWM czyli po polsku Modulacja szerokości impulsu służy głównie do sterowania mocą czy to silnika czy żarówki. Generalnie na pinie mikrokontrolera możemy ustawić stan wysoki (np 5V) albo niski czyli 0V, często istnieje potrzeba regulacji jasności czy szybkości obrotów silnika i normalnie nie dało by się tego rozwiązać ale jeśli doprowadzimy do tego że dioda bardzo szybko miga odniesiemy wrażenie że świeci słabiej, podobnie jest z silnikami, jeśli wywołamy szybkie przerwy w dostawie prądu, silnik będzie się kręcił wolniej.

Nasz cel możemy osiągnąć dzięki wbudowanym w mikrokontroler TIMERom, o których już wiemy z artykułu o multipleksowaniu: Timery, przerwania i multipleksowanie w AVR ATTINY84A z wyświetlaczem 7 segmetowym . Timery nie zawsze ale bardzo często mają tryb PWM. Dzięki temu trybowi możemy nie tylko wywołać przerwania co określony czas ale także zmiany stanu na konkretnym pinie.

W dzisiejszym artykule dzięki PWM nauczymy się otrzymywać dowolny kolor na diodzie RGB, czyli takiej z czterema nóżkami, mogącej świecić w trzech podstawowych kolorach. R – red czyli czerwony, G – green czyli zielony i B – blue czyli niebieski. Za pomocą połączenia tych trzech kolorów w odpowiednich proporcjach możemy otrzymać dowolny kolor. Zanim przejdziemy do zagadnienia głównego, spróbujemy sobie zaświecić zwykłą diodą z różną jasnością żeby opanować PWM.

O co chodzi w trybie PWM timera mikrokontrolera atmega88pu?

Jak już wiemy z artykułu o Timerach zgodnie z taktowaniem i ustawieniem dzielnika liczy od 0 do 255 (w przypadku timera 8bit) i resetuje się z powrotem do zera. W trybie CTC generuje przerwanie w określonym momencie i resetuje się wcześniej.

W trybie PWM idziemy o krok dalej i w jednym takim cyklu możemy wywołać zmianę stanu na pin w dowolnym momencie na stan niski i na początku na stan wysoki. W ten sposób generujemy sygnał określonej długości. Cały sens tego trybu tkwi w możliwości ustawienia dowolnej proporcji pomiędzy stanem niskim i wysokim.

W ten sposób na przykład przez 60% czasu możemy mieć stan wysoki i 40% stan niski. Możemy też zmieniać te proporcje w praktycznie dowolnym momencie. Dzięki temu możemy sprawić że dioda będzie świeciła mocniej lub słabiej.

Zmiana jasności diody

Mamy już pewne doświadczenia ze świeceniem diody, możesz zobaczyć jak ją podłączyć w artykule Jak zaprogramować mikrokontroler AVR ATTINY84A-PU przypomnę tylko że szukamy rezystora w okolicach 135, podpinamy pod pin PB1 (cóż za zbieg okoliczności), dalej podpinamy anodę naszej diody i katodę do GND.

W dalszym ciągu wykorzystuję Atmega88pu, posiada on dwa timery 8 bit i jeden timer 16 bit. Znajdziemy tutaj trzy piny, na których możemy ustawić tryb PWM. Są to PB1, PB2 i PB3, na schemacie znajdziemy dla tych wyjść opis OC1A, OC1B i OC2.

Bardzo możliwe że korzystasz z innego mikrokontrolera. To nie jest duży problem aby znaleźć odpowiednie wyjścia zajrzyj do noty katalogowej, to taki dokument, który ma ponad 100 stron i jest tam opisane każde wyjście mikrokontrolera jego funkcjonalności no i wszystko inne jak to w dokumentacji. Ja na przykład znalazłem informacje o wyjściach PWM w tym miejscu.

Jak skonfigurować timer?

Na razie skupmy się na PB1 i co za tym idzie OC1A, gdy uda nam się określić jasność świecenia diody przejdziemy do wykorzystania trzech wejść dla diody RGB kolorowej. Skoro mamy już podłączenie przejdźmy do naszej funkcji głównej programu w pliku main.c

int main(void)
{
    //ustawiam pin PB1 na wyjście
    DDRB |= (1 << PB1);

    //Ustawiam tryb PWM
    TCCR1A |= (1 << WGM11);
    TCCR1B |= (1 << WGM12) | (1 << WGM13);

    //ustawiam preskeler czyli dzielnik na 8
    TCCR1B |= (1 << CS11);
	
    //ustawiam wartość timera do porównania gdzie będzie generowane przerwanie 
    ICR1 = 255;
    //ustawiam długość sygnału
    OCR1A = 156;
    //ustawiam porównanie z OCR1A
    TCCR1A |= (1 << COM1A1);
	
    while (1) 
    {
        //teraz co 2,5s zmieniam jasność diody
	OCR1A = 200;
	_delay_ms(2500);
	OCR1A = 100;
	_delay_ms(2500);
    }
}

Na początku standardowo już ustawiam pin PB1 na wyjście. Będziemy emitować sygnał PWM. dalej przechodzimy do ustawień naszego timera. Wykorzystamy tutaj 16 bitowy TIMER1 A. Mogę znaleźć całkiem sporo informacji o tym Timerze także w dokumentacji w moim przypadku od strony 113. Stąd wiem chociażby że ten timer jest 16 bitowy oraz że obsługuje PWM.

Interesuje mnie w tym momencie tryb fast PWM, szukam tabelki w dokumentacji gdzie jest opis jak ustawić poszczególne tryby. Znajduję ją na stronie 136 i tam na dole widzę że dla trybów Fast PWM muszę ustawić WGM13, WGM12 i WGM11. Plus wartość graniczną będę chciał porównywać z ICR1.

Teraz jeszcze muszę poszukać do jakich rejestrów muszę się odnieść. Mam tę informację zaraz pod tabelką. Z tej tabelki wiem że WGM13 i WGM12 zapisuję do rejestru TCCR1B. Nie ma tutaj WGM11 więc zaraz poszukam. Warto zwrócić uwagę co jeszcze znajduje się w tym rejestrze przy okazji tej tabelki, zaraz to się nam przyda.

Ustawienie WGM11 znalazłem w rejestrze TCCR1A.

Skoro tryb timera mamy już ustawiony na fast PWM to teraz przydało by się odszukać ustawienie preskelera czyli np jak mamy 1Mhz czyli 1000 000 taktów na sekundę to licznik będzie się nam zwiększał o 8 taktów. Spróbujmy znaleźć odpowiednią tabelkę w dokumentacji jak by to należało zrobić.

Jak widać wystarczy ustawić bit CS11 na 1. Jak już widzieliśmy dwa obrazki wyżej, CS11 znajduje się w rejestrze TCCR1B.

Jakbyśmy chcieli ustawić dzielnik np na 1024 to wpisalibyśmy TCCR1B |= (1 << CS12) | (1 << CS10) . Dalej ustawiam wartość do porównania, przy której Timer1 się wyzeruje, na razie ustawiam na 255 (czy kojarzysz zapis RGB np z htmla?). Teraz przy takim ustawieniu co 8 taktów licznik będzie się zwiększał o 1 a co 255×8 = 2040 będzie się zerował. Czyli będziemy mieli 1000 000 / 2040 = 490 sygnałów na sekundę. Jako że oko nie jest w stanie uchwycić migania już przy ok 30 razach na sekundę to możemy być spokojni że nie będzie migać.

Jeszcze tylko ustawiam porównania z OCR1A żeby stan na pinie zmieniał się wg podanej tam wartości. To też można znaleźć w dokumentacji.

W pętli głównej programu co 2.5s zmieniam długość sygnału. Dzięki temu dioda powinna świecić raz jaśniej a raz ciemniej.

Możemy nawet wyliczyć % jasności diody ze stosunku OCR1A do ICR1. Czyli OCR1A/ICR1 = jasność diody. Na przykład jeśli ustawimy 156/255 = 0,611 czyli jasność diody będzie na ok 61% pełni mocy. Analogicznie będzie z silnikami i innymi elementami gdzie byśmy chcieli sterować mocą urządzenia. Teraz nadszedł czas na trochę praktyki i kolorową diodę.

Dioda RGB

Zasadniczą różnicą diody RGB od innych jest to że ma cztery nóżki. Najdłuższa jest anoda, którą podpinamy do plusa (VCC). Pojedyncza nóżka po jednej stronie anody to katoda od koloru czerwonego z drugiej strony od anody mamy zielony a dalej niebieski. Anodę podpinamy do VCC a katody pod poszczególne nóżki. Jako że na pinach będziemy mieli 5V a diodę zasilamy 3V to pamiętaj o dorzuceniu rezystora.

Red (czerwony) – PB1
Green (zielony) – PB2
Blue (niebieski) – PB3

Spróbujmy zrobić stosunkowo proste ćwiczenie gdzie dioda będzie po prostu zmieniała kolory. Wykorzystamy tutaj sobie dwa timery i jeden z nich będzie obsługiwał dwa piny. W pętli głównej co jakiś czas będziemy zmieniać wartość do porównania żeby kolory diody się zmieniały.

int main(void)
{
    //Ustawiam na wyjście piny PB 1,2 i 3 pod każdy z kolorów
    DDRB |= (1 << PB1);
    DDRB |= (1 << PB2);
    DDRB |= (1 << PB3);

    //Ustawiam timer na PWM tak jak wcześniej
    TCCR1A |= (1 << WGM11);
    TCCR1B |= (1 << WGM12) | (1 << WGM13);
    TCCR1B |= (1 << CS11);
    ICR1 = 255;
    OCR1A = 70;
    //Do porównani na drugim pinie czyli PB2 potrzebuję drugiego rejestru
    OCR1B = 140;
    //ustawiam tryb pwn na PB1 i PB2
    TCCR1A |= (1 << COM1A1) | (1 << COM1B1);
	
    //Do obsługi PB3 wykorzytam timer2 analogicznie jak TIMER1	
    TCCR2A |= (1 << WGM21) | (1 << WGM20);
    CCR2B |= (1 << CS21);
    OCR2A = 230;
    TCCR2A |= (1 << COM2A1);
	
    /* Replace with your application code */
    while (1) 
    {
	OCR1A++; //RED
	OCR1B++; // GREEN
	OCR2A++; //BLUE
	if(OCR1A > 250){
	    OCR1A = 0;
	}
	if(OCR1B > 250){
	    OCR1B = 0;
	}
	if(OCR2A > 250){
	    OCR2A = 0;
	}
		
         delay_ms(50);
    }
}

W powyższym kodzie ustawiłem różne wartości początkowe dla porównania i później w pętli głównej zwiększam wartości rejestrów do porównania o 1. Kiedy którać przekroczy 250 to wracam do zera. Robię to co 50 milisekund żeby zmiany były dość dobrze widoczne. Dioda powinna się mienić różnymi kolorami.

To nic że mamy tylko trzy piny do PWM. Można to w sumie zrobić po swojemu z wykorzystaniem TIMERa i analogicznie jak robiliśmy podczas multipleksowania wykorzystując przerwania można w dowolnym czasie zmieniać stan dowolnego pinu, wystarczy trochę kodu.

Na podsumowanie zrobimy sobie jakiś skomplikowany kolor na tej kolorowej diodzie. Idealnie mi nie wyszło na tej diodzie, która mam bo widzę głównie czerwony i niebieski ale z daleka trochę to się miesza 😛

Jak napisać własny tryb PWM i zaświecić diodę RGB na dowolny kolor?

Popatrzmy ja jakąś paletę kolorów RGB. Na przykład taką, którą mam w mojej wtyczce do przeglądarki collorzilla.

Teraz spróbujemy sobie to zaprogramować na takim podłączeniu jak mamy wyżej ale z wykorzystaniem jednego timera i przerwań. Na początek dokonamy pewnych zmian w funkcji głównej polegających na usunięciu większości wcześniejszego kodu i włączeniu przerwań. Pamiętaj tylko żeby dodać bibliotekę avr/interrupt.h w części nagłówkowej pliku.

int main(void)
{
    //włączam przerwania
    sei();
	
    //ustawiam piny na wyjście
    DDRB |= (1 << PB1);
    DDRB |= (1 << PB2);
    DDRB |= (1 << PB3);
	
    //ustawiam timer na CTC
    TCCR1B |= (1 << WGM12) | (1 << WGM13);
    //preskeler dalej jest na 8
    TCCR1B |= (1 << CS11);
    //przerwanie będzie przy 100 i licznik się zrestartuje
    ICR1 = 2;
    //włącznie przerwania przy
    TIMSK1 |= (1 << OCIE1A);	
	
    /* Replace with your application code */
    while (1) 
    {
	
    }
}

Wszystko opisałem w komentarzach. ICR1 ustawiłem na 2 więc przerwania będą generowane dość szybko bo co 16 taktów co daje nam 1000 000 / 16 = 62500 na sekundę. Teraz pozostaje obsługa przerwań, czyli nasza funkcja ISR. Pamiętaj o odpowiednim Wektorze. Można go znaleźć w tabelce w dokumentacji, zazwyczaj będzie to coś w stylu TIMER1_COMPA_vect, numer timera będzie się zmieniał i czasami literka.

ISR(TIMER1_COMPA_vect)
{
    static uint8_t counter;
    if(counter < 215) {
	PORTB &= ~(1 << PB1);
    } else {
	PORTB |= (1 << PB1);	
    }
    if(counter < 44) {
	PORTB &= ~(1 << PB2);
    } else {
	PORTB |= (1 << PB2);
    }
    if(counter > 224) {
	PORTB &= ~(1 << PB3);
    } else {
	PORTB |= (1 << PB3);
    }
	
    counter++;
    if(counter > (255)){
	counter = 0;
    }
}

I po uruchomieniu tego powinniśmy uzyckać oczekiwany kolor. Tutaj liczymy od 0 do 255 i czyklicznie zerujemy licznik. Oczywiście zakres ten ustaliłem na potrzeby RGB, można dać np od 0 do 100 i wtedy ustawiać procentową długość wypełnienia sygnału. Wartości do porównania można by także wyłączyć do zmiennych gdzieś wyżej.

W każdym razie w powyższym kodzie mam trzy warunki po jednym dla każdego koloru i porównuję czas jaki ma świecić dany kolor tak jak odczytałem to z tabelki. W sumie to można by taki efekt osiągnąć i z wykorzystaniem funkcji delay albo po prostu w pętli głównej programu. Najważniejsze żeby zapamiętać że PWM to długość wypełnienia sygnału. Jeszcze wrócimy do tematu w bardziej praktycznych ćwiczeniach.