Modulacja szerokości impulsów.
Jeśli czytasz tę stronę, pewnie wiesz, co to takiego, więc tylko tak w największym skrócie. Mamy prostokątny sygnał okresowy, czyli włączony/wyłączony, z okresem na przykład 20 ms (50Hz). Jeśli czas trwania stanu "włączony" wyniesie 2 ms, energia przekazywana do odbiornika wyniesie 10% energii, jaką przekazalibyśmy gdyby sygnał był w stanie "włączony" cały czas, czyli 20 ms (no, wtedy nie byłoby sygnału prostokątnego...). Jeśli 10ms, to 50%, 18ms - 80% itp.
Wykorzystać to można do zasilania silników, regulacji jasności oświetlenia LED, regulacji prądu w spawarkach albo do sterowania trochę "mądrzejszych" urządzeń.
Raspberry umie generować sygnał PWM. W sieci pełno mądrych rad i programików na ten temat. Niestety nie udało mi się znaleźć niczego rzetelnego, postanowiłem więc sam trochę na ten temat napisać, skorygować parę krążących mitów.
Sygnał pwm generowany jest w Raspberry przez układ BCM2835 (w starszych modelach inny). Potrafi on generować tylko dwa sygnały pwm (kanały PWM0 i PWM1), mogą one być zmodulowane w różny sposób, ale oba posiadają tę samą częstotliwość zegara.
Kanał PWM0 wyprowadzony jest na GPIO12, 18, 40 i 52, PWM1 na GPIO13, 19, 41, 45 i 53 (numeracja GPIO według Broadcom, alternatywną numerację wiringPi i fizyczne numery pinów najprościej zobaczyć komendą gpio readall). BCM2835 posiada 54 piny GPIO. W Raspberry mamy łączówkę 40 pinową, więc nie wszystkie są na nią wyprowadzone. Możemy skorzystać z GPIO 12 i 18 (PWM0) i GPIO 13 i 19 (PWM1). Możliwe jest więc sterowanie dwoma urządzeniami, a przy pewnych ograniczeniach, o czym później, czterema.
Starsze modele Raspberry miały jednak łączówkę GPIO 26 pinową, wyprowadzony na nią był tylko jeden sygnał PWM, GPIO18. Stąd wiele nieporozumień, można wyczytać że Raspberry ma tylko jeden pin z sygnałem PWM, dwa (pewnie skrót myślowy, bo dwa są kanały PWM), cztery (w nowszych modelach), albo 9, bo w istocie tyle ich jest, choć nie wszystkie są dostępne.
Jeśli ograniczymy rozważania do nowszych Raspberry, z łączówką 40 pinową, mamy:
- Dwa sygnały (kanały) PWM
- Cztery piny na łączówce na których te sygnały mogą się pojawić
- Na pinach GPIO12 i GPIO18 pojawi się identyczny sygnał, z kanału 0
- Na pinach GPIO13 i GPIO19 pojawi się identyczny sygnał, z kanału 1
No to jak to się robi. Potrzebujemy na przykład taki sygnał. Jedynka logiczna przez 2 milisekundy, co 10 milisekund:
Użyjemy, podobnie jak w innych przykładach biblioteki wiringPi. Na początek programowanie generatora PWM.
Funkcja
pwmSetMode (int mode) ;
programuje go w trybie mark:space lub balanced, jako parametr mode użyjemy odpowiednio PWM_MODE_MS lub PWM_MODE_BAL.Tryb mark:space to tradycyjny sposób, sygnał/brak_sygnału. Balanced próbowałem zrozumieć czytając dokumentację Broadcom, ale się poddałem. Chyba nie jako jedyny. Jeśli kiedyś znajdę czas, douczę się o co w tym trybie balanced chodzi, wtedy napiszę.
Na razie użyjemy trybu mark:space.
Teraz sygnał zegara. Broadcom pisze, że kanały PWM taktowane są zegarem 100MHz. W Raspberry, jak wynika z moich pomiarów jest to 19,2MHz. Też jeszcze nie wiem, dlaczego. Częstotliwość tę zmienić możemy funkcją
pwmSetClock (int divisor) ;
Parametr divisor to wartość, przez jaką zostanie podzielona bazowa częstotliwość zegara.
Jeśli użyjemy wartości 192 otrzymamy zegar 100kHz, jeśli 1920, to 10kHz itd.
10kHz to impuls co 0,1ms, jeśli tego dzielnika użyjemy, z taką dokładnością będziemy mogli programować nasz generator.
Teraz programujemy okres naszego sygnału:
pwmSetRange (unsigned int range) ;
Parametr range to właśnie okres naszego sygnału, czyli czas jaki upływa pomiędzy kolejnymi impulsami. Nie jest to po prostu czas, to liczba taktów zegara przypadająca na jeden okres. Jeśli zaprogramujemy zegar na 10 kHz, jeden takt potrwa 0,1 ms, więc aby uzyskać okres 10 ms jako range podać musimy 100.
Teraz jeszcze musimy ustawić pin GPIO na którym chcemy mieć sygnał pwm w odpowiednim trybie:
void pinMode (int pin, int mode) ;
Jako mode podajemy PWM_OUTPUT. Inne tryby to INPUT i OUTPUT, jeśli ich użyjemy, to na pinie nie pojawi się sygnał pwm. Pozostanie jednak zaprogramowany, możemy to wykorzystać do okresowego wyłączania pwm, co zostanie zilustrowane przykładem w dalszej części opisu.
Teraz ostatnia funkcja, pozwoli nam ustawić czas trwania jedynki logicznej i wystawi sygnał pwm na wybrany pin:
void pwmWrite (int pin, int value) ; Na pinie pin pojawi się sygnał pwm o czasie trwania jedynki logicznej value.
Pamiętajmy, ze tylko na czterech pinach łączówki GPIO Raspberry możemy ustawić wyjście sygnału pwm.
Podobnie jak dla funkcji pwmSetRange, wartość parametru value podajemy jako liczbę taktów zegara pwm, wiec jeśli chcemy uzyskać impuls 2 ms, a zegar zaprogramowaliśmy na 10 kHz, podajemy wartość 20.
Od tej chwili na naszym pinie GPIO mamy sygnał. W każdej chwili możemy zmienić jego wypełnienie poprzez kolejne pwmWrite z inną wartością value.
A jak go wyłączyć? Po prostu ustawić pinMode na OUTPUT lub INPUT.
No to czas na testowe programy.
Nie sądzę, że każdy z Was ma pod ręką oscyloskop, żeby sobie obejrzeć i pomierzyć wygenerowany sygnał. Ja też nie mam. Dlatego napisałem prosty programik markspace.c który na pinie 22 (według wiringPi) mierzy czas trwania jedynki logicznej, czas zera logicznego i okres sygnału. Nie jest super precyzyjny, pomiar zakłóca mu wielozadaniowość Raspbiana, ale do naszych celów wystarczy.
Kompilujemy go gotowym skryptem cms lub ręcznie: cc markspace.c -o markspace -lwiringPi
łączymy pin 22 z pinem na którym chcemy mierzyć sygnał i uruchamiamy:./markspace
Jeśli na pinie 22 będzie jakiś okresowy sygnał, na ekranie coś takiego się wyświetli:
Mark: 4.00 ms Space: 6.00 ms Period: 10.00 ms
Zamiast programu markspace do pinu pwm podłączyć możemy diodę LED, wtedy (przynajmniej w niektórych przykładach) "na własne oczy" zobaczymy ze pwm działa, jasność świecenia będzie się zmieniała:
Pierwszy test.
Program pwm1.c i skrypt do jego kompilacji cpwm1
Wygenerujemy sygnały o okresie 10 ms i wypełnieniu 10%, 20% ... 90%, każdy z nich będzie trwał 3 sekundy. Sygnał możemy obserwować programem markspace, albo podłączyć diodę LED pomiędzy piny 1 (według wiringPI, czyli 12 fizyczny pin łączówki GPIO) i masę, na przykład 6 fizyczny pin łączówki GPIO.
Dioda po każdych 3 sekundach będzie świeciła jaśniej.
Na początek programujemy kanał PWM0.
Pin 1 (według wiringPi, fizycznie będzie to pin 12) ustawiamy w tryb pwm:
pinMode(1, PWM_OUTPUT);Programujemy tryb mark:space generatora pwm:
pwmSetMode(PWM_MODE_MS);Ustawiamy dzielnik zegara 1920, jeden takt będzie trwał 0,1 ms:
pwmSetClock(1920);Programujemy okres sygnału, 100 * 0,1 = 10ms:
pwmSetRange(100);Uruchamiamy generator na pinie 1, 10% wypełnienia, czyli jedynka przez 1 ms. Dla kontroli wyświetlamy informację o czasie trwania jedynki logicznej. Pamiętamy, że jeden takt naszego zegara to 0,1 ms, więc potrzebujemy 10 taktów, żeby uzyskać 10 ms:
printf("1 ms\r");fflush(0); pwmWrite(1,10); sleep(3);Po 3 sekundach zmieniamy wypełnienie na 20%, impuls 2 ms:
printf("2 ms\r");fflush(0); pwmWrite(1,20); sleep(3);I tak kolejno co 1 ms aż do 9 ms.
Warto zwrócić uwagę, że po skończeniu pracy naszego programu sygnał pwm nie zniknie, pozostanie na pinie 1, 10 milisekund, z wypełnieniem 90%, takim, jak go ostatnio zaprogramowaliśmy.
Teraz program sterujący trzema urządzeniami pwm. Jak to zrobić mając tylko dwa kanały pwm?
Da się, choć sterowanie nie będzie całkowicie niezależne. Dwa urządzenia muszą działać naprzemiennie, nigdy oba w tym samym czasie, na przykład sygnalizator normalnej pracy i sygnalizator awarii. Inny przypadek to dwa urządzenia które wymagają sterowania takim samym sygnałem, na przykład sygnalizator wewnątrz budynku i sygnalizator na zewnątrz.
W prosty sposób można rozbudować ten przykład do czterech urządzeń, pamiętając o wspomnianych wyżej ograniczeniach.
Program, jak poprzednio, pwm2.c i skrypt do jego kompilacji cpwm2
Programujemy pwm, sygnał będziemy pobierać z pinów 1, 23 i 24 (numeracja wiringPi, nie BCM).
Na pinach 23 i 24 będzie taki sam sygnał, to jeden kanał, PWM1:
pinMode(1, PWM_OUTPUT); pinMode(23, PWM_OUTPUT); pinMode(24, PWM_OUTPUT); pwmSetMode(PWM_MODE_MS); pwmSetClock(1920); pwmSetRange(200);Teraz generujemy na pinach 1, 23 i 24 sygnały, z dwu różnych kanałów:
printf("Aktywne piny 1, 23 i 24\n");fflush(0); for(i=0;i<2;i++) { pwmWrite(1,5); pwmWrite(23,15); sleep(1); pwmWrite(1,25); pwmWrite(23,20); sleep(1); }Wyłączamy pwm na pinie 24, aktywne będą tylko urządzenia podpięte do pinów 1 i 23:
printf("Aktywne piny 1 i 23\n");fflush(0); for(i=0;i<2;i++) { pinMode(24, OUTPUT); pwmWrite(1,5); pwmWrite(23,15); sleep(1); pwmWrite(1,25); pwmWrite(23,20); sleep(1); }Włączamy pwm na pinie 24. Niestety musimy ponownie zaprogramować generator:
printf("Aktywne piny 1, 23 i 24\n");fflush(0); pinMode(24, PWM_OUTPUT); pwmSetMode(PWM_MODE_MS); pwmSetClock(1920); pwmSetRange(200); for(i=0;i<2;i++) { pwmWrite(1,5); pwmWrite(23,15); sleep(1); pwmWrite(1,25); pwmWrite(23,20); sleep(1); }Urządzenia podpięte do alternatywnych pinów tego samego kanału mogą działać naprzemiennie, z różnymi sygnałami, równocześnie, sterowane tym samym sygnałem, albo być selektywnie włączane lub wyłączane, wystarczy przeprogramowanie trybu pracy pinu.