Kurs Raspberry Pi Pico – #6 – PWM, ADC i komunikacja z komputerem

Czas czytania: 16 min.

W poprzedniej części poznaliśmy podstawowe zmienne, instrukcje warunkowe oraz pętle. Poza tym opowiedziałem wam nieco o błędach i prostym sposobie ich rozwiązywania. W dzisiejszym materiale przyjrzymy się bliżej kilku funkcjonalnościom mikrokontrolera RP2040. Będą to PWM, USB oraz ADC. Oczywiście poza teoretycznym opisem poznacie też praktyczne zastosowanie tych funkcji, tak więc zaczynajmy.

Kup zestaw do nauki programowania z Raspberry Pi Pico W i skorzystaj z kursu dostępnego na Blog Botland!

W zestawie: moduł Raspberry Pi Pico W, płytka stykowa, przewody, diody LED, rezystory, przyciski, fotorezystory, cyfrowe czujniki światła, temperatury, wilgotności i ciśnienia, wyświetlacz OLED i przewód USB-microUSB.

Przed wyruszeniem w drogę należy zebrać drużynę

Zestaw elementów do kursu Raspberry Pi Pico.

Chcąc uczyć się programowania, bazując na rzeczywistych projektach, potrzebny będzie oczywiście odpowiedni sprzęt, ale bez obaw – nie musisz teraz skakać między kolejnymi artykułami i przygotowywać listę niezbędnych elektronicznych elementów. W sklepie Botland dostępny jest gotowy zestaw, zawierający wszystkie komponenty niezbędne do wykonania projektów opisanych w serii poradników o Raspberry Pi Pico. 

gotowym zestawie elementów znajdziecie:

  • Raspberry Pi Pico W,
  • Przewód microUSB,
  • Płytkę stykową,
  • Zestaw przewodów połączeniowych w trzech rodzajach,
  • Zestaw diod LED w trzech kolorach,
  • Zestaw najczciej stosowanych w elektronice rezystorów,
  • Przyciski Tact Switch,
  • Fotorezystory,
  • Cyfrowy czujnik światła,
  • Cyfrowy czujnik wilgotności, temperatury i ciśnienia,
  • Wyświetlacz OLED.

Modulacja szerokości impulsów – PWM

Do tej pory sterowane przez Raspberry Pi Pico W diody LED świeciły lub nie, proces ten był zero jedynkowy i nie było możliwości sterownia tym elementem w nieco bardziej „płynny” sposób. Uzyskanie sygnału analogowego ze stanów cyfrowych nie jest wcale takim prostym zadaniem, tego typu funkcjonalność można uzyskać tylko dzięki przetwornikom cyfrowo-analogowym (C/A lub DAC). Istnieje jednak pewien sposób, dzięki któremu będziemy mogli udawać, że generujemy sygnał analogowy i sterować dzięki niemu jasnością świecenia diody LED.

Przykładowe przebiegi sygnału PWM o wypełnieniu 50%, 75% i 25%. (https://en.wikipedia.org/wiki/Pulse-width_modulation)

Modulacja szerokości impulsów (PWM) to termin odnoszący się do funkcji implementowanej we większości współczesnych mikrokontrolerów. Jak sama nazwa wskazuje, dzięki niej będziemy mogli manipulować szerokością pojedynczego impulsu generowanego przez RP2040. W przykładach, które przerabialiśmy, zdarzało się, że dioda LED uruchamiana była synchronicznie co pół sekundy. W takim przypadku można powiedzieć, że cały impuls trwał sekundę, a jego wypełnienie wynosiło 50%, ponieważ dioda działała przez połowę dostępnego czasu. Jednak dla mikrokontrolera sekunda to niemal wieczność i może on z powodzeniem generować impulsy, których czas jest znacznie krótszy. Do tego celu wykorzystywane są konkretne polecenia wspierane przez wewnętrzne moduły procesora, dzięki czemu nie musimy co chwilę wstrzymywać działania układu. 

Sterować będziemy wypełnieniem takiego impulsu, czyli właśnie czasem, w jakim na wyprowadzeniu utrzymywany jest stan wysoki. Zmieniając wypełnienie, uzyskujemy swego rodzaju imitację sygnału analogowego. Imitacja jest tutaj jak najbardziej poprawnym stwierdzeniem, ponieważ nadal jest to sygnał cyfrowy, tylko dzięki bezwładności ludzkiego oka, wydawać będzie się, że sterujemy jasnością świecenia diody, jak gdyby zmieniać jej napięcie zasilania. Poza tym sygnały PWM są z powodzeniem wykorzystywane w innych układach i obwodach. Znaleźć możemy czujniki, w których odczytana wartość, na przykład tlenu w powietrzu reprezentowana jest właśnie przez sygnał PWM, pojawiający się na wyjściu modułu. Poza tym sygnały, których szerokość impulsu jest modyfikowana, wykorzystywane są do sterowania serwomechanizmami stosowanymi w robotyce.

				
					#include "pico/stdlib.h"
#include "hardware/pwm.h"

#define GREEN_LED 0 //assigning names of sspecyfic values

uint8_t fill = 0;
uint8_t changes = 1;

int main() {
    gpio_init(GREEN_LED); //initialization and setting of pin mode
    gpio_set_function(GREEN_LED, GPIO_FUNC_PWM);

    uint8_t slice_num = pwm_gpio_to_slice_num(GREEN_LED); //assignment of values to variables slice_num and channel
    uint8_t channel = pwm_gpio_to_channel(GREEN_LED);
    
    pwm_set_wrap(slice_num, 255); //set the maximum PWM range (255=100%)

    pwm_set_enabled(slice_num, true); //PWM signal activation
    
    while (true)
    {
        pwm_set_chan_level(slice_num, channel, fill); //setting the signal filling to the value stored in fill

        if (fill < 255) //if fill <255 assign value + change
        {
            fill = fill + changes;
        }
        Else //incompatible condition set fill to 0
        {
            fill = 0;
        }

        sleep_ms(10);

    }
    
}

				
			

Bez zbędnego przedłużania, przejdźmy od razu do kodu, który pozwoli w praktyce wykorzystać sygnał PWM do sterowania diodą LED. Na potrzeby testów tej funkcjonalności stworzyłem, nowy projekt, który nazwałem pwm_led. Natomiast sam obwód zbudowany na płytce stykowej pozostaje bez zmian, choć w tym przykładzie skorzystamy tylko z jednej diody LED.

Przygotowany program pozwoli sterować diodą LED za pomocą sygnału PWM, innymi słowy będziemy stopniowo rozjaśniać diodę, zwiększając wypełnienie impulsu. Czas, w którym na wyprowadzeniu utrzymywany będzie stan wysoki, określimy za pomocą liczby, przyjmując, że 0 oznacza 0% (dioda LED nie świeci) natomiast 255 oznacza 100% (dioda LED świeci przez cały czas trwania impulsu).

Początek programu wygląda znajomo, choć znalazły się tam pewne nowości. Jak można zauważyć, po raz pierwszy dołączyłem dodatkową bibliotekę hardware/pwm.h. Jest ona potrzebna, ponieważ w kreowaniu sygnału PWM korzystać będziemy ze sprzętowego wsparcia mikrokontrolera, a funkcje opisujące ten proces opisane zostały właśnie w tej bibliotece. Dodatkową paczkę instrukcji będziemy musieli dodać też w pliku CMakeLists.txt, ale tym zajmiemy się później. Przed główną funkcją main znalazła się znana z wcześniejszych programów definicja oraz deklaracja dwóch zmiennych typu uint8_t – fill oraz changes.

Przy zmiennych tych musimy zatrzymać się na moment. W poprzednich przykładach, zmienne deklarowane były wewnątrz funkcji main, zazwyczaj w okolicy poleceń, które z nich korzystały. Były to zmienne lokalne, z których korzystać możemy tylko wewnątrz funkcji, w których zostały one wygenerowane. Zmienne, których deklaracje umieszczone są ponad funkcją główną, określić możemy mianem zmiennych globalnych, oznacza to, że korzystać możemy z nich w obrębie całego programu. Oczywiście zmienne fill oraz changes, określające kolejno wypełnienie oraz zmianę wypełnienia przy każdym cyklu programu, mogłyby być stworzone w dalszej części kodu, ale na potrzeby wyjaśnienia konceptu zmiennej lokalnej i globalnej umieściłem je właśnie w tym miejscu. Wyjaśnienia wymagają również cyfry 0 i 1 umieszczone przy nazwach tych zmiennych. Dzięki nim możemy określić domyślną wartość, które zmienne przyjmą od razu po uruchomianiu programu.

Główną funkcję main zaczynamy znanymi już poleceniami, choć tym razem dzięki GPIO_FUNC_PWM wyprowadzenie przypisane do GREEN_LED pełnić będzie rolę pinu wspierającego funkcjonalność PWM. Kolejno deklarujemy dwie zmienne slice_num oraz channel, do których od razu przypisujemy wartości odczytane przez polecenia pwm_gpio_to_slice_num i pwm_gpio_to_channel, których argumentem jest konkretny numer wyprowadzenia RP2040. Nad tym, co zostało zapisane w tych zmiennych, nie musimy się zastanawiać, są one potrzebne, ponieważ sygnał PWM, mimo że przypisany do konkretnego wyprowadzenia określany jest tak naprawdę przez slice, channel oraz fill, które zadeklarowaliśmy już wcześniej.

Dzięki kolejnemu poleceniu, czyli pwm_set_wrap określamy liczbowy zakres wypełnienia sygnału PWM. W argumentach funkcji umieszczamy odczytany wcześniej slice oraz wartość, która opisywać będzie 100% wypełnienia pojedynczego impulsu. W przykładzie jest to 255, ale można tam z powodzeniem wstawić dowolną wartość, choć pamiętać trzeba, że odnosi się ona do wypełnienia, czyli zmiennej fill, której typem jest uint8_t o ograniczonym zakresie. Przy większych wartościach należy rozważyć typy zmiennych o większych zakresach, takie jak uint16_t czy uint32_t. Dzięki kolejnemu poleceniu pwm_set_enable, aktywujemy sygnał PWM na wyprowadzeniu, którego wartość slice przypisana jest do slice_num.

W głównej części programu znalazła się nieskończona pętla while, w której wnętrzu w pierwszym kroku ustawiamy wartość wypełniania sygnału PWM na pinie opisanym przez slice_num oraz channel. Sama wartość wypełnienia zapisana jest w fill, której domyślna wartość to zero, tak więc przy pierwszym uruchamianiu dioda będzie wygaszona. Następnie umieszczona została funkcja warunkowa, sprawdzająca wartość wypełnienia. Jeśli będzie ono mniejsze od 255 to wartość fill, zmieniona zostanie na samą siebie z dodaną wartością spod zmiennej changes. Innymi słowy, przy każdym wykonaniu funkcji warunkowej wypełnienie zwiększy się o jeden, aż do momentu, gdy warunek fill < 255 nie będzie spełniony. Wówczas wypełnienie zostanie z powrotem zmniejszone do zera. Na końcu programu znalazło się niewielkie opóźnienie, wstrzymujące program na 10ms. Tak więc w krótkim podsumowaniu pętli, która wykonywana będzie przez Raspberry Pi Pico W, aż do wyłączenia zasilania, można powiedzieć, że wypełnienie sygnału PWM będzie zwiększane o jeden co 10ms, aż do momentu, gdy przekroczy ono wartość 255 i wówczas dioda zostanie wygaszona.

CMakeLists.txt dla projektu pwm_led.

Tak jak wspomniałem na początku, chcąc korzystać ze sprzętowego wsparcia dla sygnałów PWM, musimy dodać bibliotekę hardware/pwm.h w pliku CMakeLists.txt. Jej deklaracje umieszczamy wewnątrz polecenia target_link_libraries, tak jak na grafice powyżej. W przyszłości kolejne biblioteki będziemy zawsze oddzielać spacjami. 

Uruchomiony program pwm_led powinien zachowywać się identycznie jak na filmie. Dioda LED rozjaśnia się stopniowo i gdy osiągnie już swoją pełną moc, jest wygaszana i cały cykl rozpoczyna się od nowa. Jednak tak jak wspominałem wcześniej, sterowanie diodą nie jest płynne, w rzeczywistości mruga ona z bardzo dużą częstotliwością, tak że jest to niezauważalne dla ludzkiego oka. Przy każdej pętli programu zwiększane jest też czasowe wypełnienie pojedynczego impulsu stanem wysokim, dzięki czemu wydaje się nam, że LED świeci coraz jaśniej. Zachęcam do eksperymentów i zmian wartości zmiennej changes oraz czasu, na jaki wstrzymywany jest program. Poza tym możecie spróbować zmienić liczbowy zakres wypełnienia, pamiętając jednocześnie o typie zmiennej fill.

Komunikacja z komputerem, czyli USB

Do tej pory korzystaliśmy wyłącznie z samego mikrokontrolera i podłączonych do niego elektronicznych elementów. Czas to zmienić i po raz pierwszy nawiązać komunikację z większym urządzeniem, jakim jest komputer.

Zazwyczaj w poradnikach bazujących na niewielkich platformach embedded, komunikacja z komputerem powiązana jest ściśle z omówieniem interfejsu UART. Wydaje mi się, że wzięło się to przede wszystkim z artykułów poświęconych Arduino, gdzie interfejs ten jest sprzętowo połączony przez pewien układ scalony z portem USB płytki. Jednak projektanci Raspberry Pi Pico, jak i samego mikrokontrolera RP2040 zdecydowali się na nieco bardziej wyrafinowane rozwiązanie. Oczywiście procesor dysponuje sprzętowym wsparciem UARTa, ale z komputerem komunikację można nawiązać prościej, poprzez USB. Gniazdo microUSB widoczne na laminacie RPI nie służy tylko do zasilania i programowania, ale można również dzięki niemu wymieniać dane z komputerem. Do wspomnianego tutaj UARTa, który jest jednym z najprostszych interfejsów szeregowych, wrócimy w jednym z końcowych artykułów z tej serii.

Złącze microUSB na płytce RPI Pico W.

Jednak zanim przejdziemy do programu, chciałbym opowiedzieć wam nieco o samym USB. Pierwszym skojarzeniem z USB jest przede wszystkim rodzaj złącza, które możemy spotkać we większości obecnie produkowanych urządzeń. Złącza te występują w kilku standardach i rodzajach różniących się między sobą fizycznymi wymiarami, ilością połączeń oraz parametrami elektrycznymi. Dla nas jednak najważniejsze będzie, że USB (ang. Universal serial bus) jest uniwersalną magistralą szeregową, która pozwala przesyłać dane między dwoma urządzeniami. Do tego celu w podstawowej konfiguracji wykorzystywana jest para sygnałów D+ i D-, które oprócz przewodów zasilających można znaleźć w standardowym przewodzie USB. Na płytce, z której korzystamy, wyprowadzenia złącza USB połączone są bezpośrednio z mikrokontrolerem, tak więc podłączając przewód do komputera, łączymy się z procesorem bez jakichkolwiek pośredników. RP2040 wspiera sprzętowo ten protokół, dzięki czemu już za moment pokaże wam kilka przykładów, w których dzięki prostej instrukcji będziemy mogli przesyłać komunikaty, które pojawią się na ekranie komputera. Takie rozwiązanie jest niezwykle przydatne w procesie programowania i debugowania. Do tej pory nasze programy były proste i nie było potrzeby tworzenie w nich tak zwanych „flag”, ale już za moment przejdziemy do bardziej rozbudowanych konstrukcji, w których umieszczać będziemy odpowiednie komunikaty. Dzięki nim będziemy na bieżąco wiedzieć, który fragment kodu jest wykonywany przez mikrokontroler.

Obwód, który zbudowaliśmy wcześniej, może pozostać bez zmian, początkowo korzystać będziemy tylko z płytki i przewodu USB, ale w kolejnych kodach spróbujemy sterować również diodą LED.

Wysyłanie komunikatów do komputera

				
					#include "pico/stdlib.h"

int main() {
    stdio_init_all(); //initialization of the stdio.h library
    
    while(true) {
        printf("RPI Pico W wysyla komunikat\n");
        sleep_ms(1000);
    }
}

				
			

Na potrzebę kolejnych przykładów, przygotowałem projekt pod nazwą usb_communication i to z niego będę korzystać w tym i kolejnych programach.

Na pierwszy ogień weźmy niezwykle prosty kod, którego zadaniem będzie przesyłać do komputera co sekundę tekst „RPI Pico W wysyla komunikat”. Jak widać, zadanie to realizuje funkcja printf, która jest jednym z podstawowych poleceń języka C. Dzięki niej możemy budować komunikaty złożone z samego tekstu lub tekstu wraz ze zmiennymi, o czym później. Komunikat, który chcemy wysłać do komputera, umieszczany jest w podwójnym cudzysłowie, poza tym na jego końcu zauważyć możecie tajemnicze ‘\n’. Jest to znak końca linii, nie będzie on widoczny, ale dzięki niemu komputer będzie wiedział, że kolejny otrzymany tekst należy wyświetlić w nowej linii.

Aby polecenie printf zostało poprawnie skompilowane, musimy dołączyć w programie bibliotekę <stdio.h>, w której zawarto całkiem sporo bazowych funkcji języka C. Poza tym bibliotek ta musi zostać zainicjalizowana, służy do tego konstrukcja stdio_init_all().

CMakeLists.txt dla usb_communication.

Jeśli chcemy, aby projekt skompilował się poprawnie, musimy dodać też trzy polecenia do pliku CMakeLists.txt, z których jedno jest komentarzem.

				
					# enable usb output, disable uart output
pico_enable_stdio_usb(usb_communication 1) 
pico_enable_stdio_uart(usb_communication 0)

				
			

Dzięki nim aktywujemy tak zwany strumień wyjściowy na porcie USB, dezaktywując jednocześnie wyjście UARTa.

Uruchomiony serial monitor.

Po zbudowaniu projektu i wrzuceniu pliku .uf2 do pamięci RPI, płytka zgłosi się jako urządzenie szeregowe USB pod jednym z portów COM. Szczegóły można podejrzeć w menedżerze urządzeń. Chcąc sprawdzić wysyłane przez mikrokontroler komunikaty, można skorzystać z dowolnego programu pozwalającego zarządzać portami szeregowymi lub wykorzystać do tego celu funkcjonalność wbudowaną w Visual Studio Code. W dolnej części znajdziemy SERIAL MONITOR, po którego otwarciu zobaczymy kilka opcji. Najważniejszą z nich jest Port, to właśnie tam pojawi się RPI pod nazwą Urządzenie szeregowe USB, w moim przypadku przypisane do portu COM3. Jako że jest to w rzeczywistości wirtualny port szeregowy, to nie musimy zwracać uwagi na pozostałe opcje. Po kliknięciu przycisku Start Monitoring port zostanie otwarty, a na ekranie komputera powinien pojawić się komunikat wysyłany przez procesor „RPI Pico W wysyla komunikat”. Dzięki zastosowaniu znaku końca linii (\n), każdy z nich zaczynać będzie się od nowej linii.

Komunikaty liczbowe

				
					#include "pico/stdlib.h"

uint8_t counter = 0;
float pi = 3.1415926;

int main() {
    stdio_init_all(); //initialization of the stdio.h library
    
    while(true) {
        printf("RPI Pico W wysyla komunikat\n");
        printf("counter value = %d\n", counter); //%d = counter variable
        printf("pi = %f\n", pi); //%f = pi variable
        counter++;
        sleep_ms(1000);
    }
}

				
			

Poza samymi komunikatami w postaci czystego tekstu istnieje możliwość przesyłania zmiennych, a dokładniej zapisanych w nich wartości. Do tego celu również skorzystać możemy z polecenia printf, jednak w nieco zmienionej strukturze. Powyżej zobaczyć możecie rozbudowaną wersję kodu z poprzedniego przykładu. Dodałem do niego dwie zmienne: całkowitą – counter, której wartość startowa to zero oraz zmiennoprzecinkową – pi, która przechowuje przybliżoną wartość liczby pi. W główniej części programu umieściłem dwie dodatkowe funkcje printf, w których wnętrzu zauważyć możecie dość tajemniczy zapis %d oraz %f. Dzięki niemu do generowanego komunikatu wstawione zostaną zmienne, zapisane dalej po przecinku. Innymi słowy, %d zostanie podmieniony na wartość counter, a w miejscu %f umieszczone zostanie pi. Na sam koniec umieściłem też zwiększenie wartości counter o jeden, tak aby przy każdym przebiegu pętli wyświetlany był inny stan.

Komunikaty widoczne w monitorze portu szeregowego.

Po uruchomieniu kodu i otwarciu monitora portu szeregowego zobaczyć możemy kolejne komunikaty wysyłane przez Raspberry Pi Pico W. Poza znanym już ze wcześniejszego programu tekstem, zobaczyć możemy kolejne stany zmiennej counter, zwiększającej się o jeden przy każdym przebiegu programu oraz wartość zapisaną pod zmiennoprzecinkowym pi.

Konstrukcje takie jak %d i %f są niezwykle przydatne w programowaniu, ponieważ pozawalają w prosty sposób ujawnić zapisane pod konkretną zmienną dane. Poza nimi skorzystać możemy też z %x (dla liczb całkowitych wyświetlanych w formacie hex), %c (znaki ASCII, dla zmiennych typu char) lub %s (tzw. String, czyli ciągi znaków ASCII). Być może zastanawiacie się w jaki sposób w monitorze portu szeregowego wyświetlić znak procenta lub backslash. Odpowiedź jest prosta, należy użyć go dwukrotnie, czyli tworząc polecenie printf(”%% \\”); w oknie VSC zobaczymy ‘% \’.

Ciekaw jestem, czy zwróciliście uwagę, że w dwóch ostatnich przykładach skorzystałem z języka polskiego. Mam tutaj na myśli „RPI Pico wysyla komunikat”, jeśli wyłapaliście ten szczegół to bardzo dobrze. Komunikat ten napisałem celowo, aby móc teraz przypomnieć o dobrej zasadzie, jaką jest stosowanie języka angielskiego. Polski jest dopuszczalny, ale dobrym zwyczajem jest zapisywać wszystkie komunikaty i komentarze po angielsku. 

Sterowanie diodą LED z komputera

				
					#include "pico/stdlib.h"

#define RED_LED 2
uint8_t led_state = 0;
char user_input;

int main() {
    stdio_init_all(); //initialization of the stdio.h library

    gpio_init(RED_LED);
    gpio_set_dir(RED_LED, GPIO_OUT);
    
    while(true) {
        
        printf("Enter 0 or 1 to control the LED\n");
        user_input = getchar(); //writing the first received character to a variable

        if(user_input == '1'){ //condition checking the received sign
            gpio_put(RED_LED, 1);
            printf("LED activated\n");
        }
        else if(user_input == '0'){
            gpio_put(RED_LED, 0);
            printf("LED off\n");
        }
        else
        {
            printf("Incorrect character entered\n");
        }
        
    }
}

				
			

Ostatnim programem, który przeanalizujemy, aby lepiej poznać interfejs USB będzie komunikacja dwustronna. Dzięki widocznemu wyżej programowi będziemy mogli sterować świeceniem diody LED z poziomu Serial Monitora. RPI Pico W będzie przechwytywać pierwszy wysłany przez komputer znak i na jego podstawie decydować o uruchomieniu lub dezaktywacji diody.

Program korzysta z dwóch zmiennych led_state, która przechowuje stan diody LED, czyli zero – dioda nie świeci oraz jeden – półprzewodnik emituje fotony. Drugą zmienną jest user_input, w której zapisywać będziemy znak wprowadzony z klawiatury komputera i przesłany do RP2040.

Główną część programu możemy podzielić na dwie części, w pierwszej wyświetlimy komunikat z prośbą o wpisanie zera lub jedynki i dalej zareagujemy na dane przesłane z komputera. Dzięki funkcji getchar() w zmiennej user_input zapiszemy pierwszy znak odebrany przez mikrokontroler. Podkreślenie, że jest to pierwszy znak, jest tutaj niezwykle ważne, ponieważ komputer przesłać może ciąg znaków, ale taką sytuację opiszę na końcu tego podrozdziału. W drugiej części programu znalazła się kaskadowa instrukcja warunkowa. Do tej pory korzystaliśmy z pojedynczego ifa skojarzonego z poleceniem else. Tym razem wykorzystamy również coś takiego jak else if, jest to zabieg niezbędny, gdy sprawdzać będziemy dwa warunki, odnoszące się do tej samej zmiennej. Program w funkcji warunkowej sprawdza wartość user_input. Jeśli przechowuje ona wartość 1, innymi słowy, komputer przesłał do mikrokontrolera znak jedynki, wówczas dioda LED zostanie uruchomiona. Analogicznie, jeśli w user_input zapisane będzie zero, dioda zostanie wyłączona. W ostatnim punkcie rozpatrywana jest sytuacja, w której żaden z warunków nie zostanie spełniony. W takim przypadku założyć możemy, że z klawiatury komputera wprowadzony został niepoprawny znak i taki też komunikat zostanie wyświetlony.

Wyjaśnieniu wymaga też samo zastosowanie funkcji kaskadowej. Czy polecenie else if, można zostać zastąpione przez zwykłego ifa? Pewnie domyślacie się, że nie i jest to poprawna odpowiedź. Wyobraźmy sobie jednak, że zostało tam umieszczone polecenie if. Wysyłamy do mikrokontrolera znak jeden, sprawdzany jest pierwszy warunek, który jest spełniony i dioda zaczyna świecić. Sprawdzamy kolejny warunek, jest on niepoprawny, a dalej procesor napotyka polecenie else, które musi wykonać, ponieważ poprzedni warunek nie został spełniony. W takie sytuacji dioda zostaje aktywowana i jednocześnie zobaczymy komunikat o niepoprawnym znaku. Właśnie takim sytuacjom zapobiega kaskadowa instrukcja warunkowa. Sprawdzamy w niej niezależnie dwa warunki i wystarczy, że tylko jeden z nich będzie spełniony, a kod zapisany w bloku else nie zostanie wykonany. Komunikat o niepoprawnym znaku zobaczymy tylko, gdy oba warunki nie zostaną spełnione.

Sterowanie diodą LED z serial monitora.

Po wrzuceniu pliku .uf2 do pamięci RPI uruchomić możemy serial monitor i spróbować uruchomić diodę LED. Co ważnie przy pierwszym uruchomieniu, prawdopodobnie nie zobaczycie komunikatu z prośbą o wprowadzenie zera lub jedynki. Dzieje się tak, ponieważ wysyłany jest on niemal od razu po uruchomieniu kodu. W tego typu sytuacjach, gdy w czasie prototypowania chcielibyśmy, aby program nie startował od razu, można skorzystać z prymitywnego rozwiązania, jakim jest funkcja sleep_ms. Umieszczając ją dla przykładu przed startem nieskończonej pętli while, a w jej argumencie umieszczając 10s, będziemy mieć wystarczająco czasu, aby uruchomić monitor portu szeregowego przed startem głównej części kodu. Możecie spróbować wprowadzić taką modyfikację.

Pojedyncze znaki lub większy tekst możemy wysyłać dzięki dolnej belce widocznej w środowisku programistycznym. Po wpisaniu 1 i kliknięciu klawisza enter dioda zacznie świecić, natomiast gdy wyślemy 0, LED zostanie wyłączony. Poza tym na ekranie komputera zobaczymy też odpowiednie komunikaty „LED activated” lub „LED off”. W sytuacji, gdy wyślemy dowolny inny znak, mikrokontroler poinformuje nas, że jest on niepoprawny.

Wcześniej wspomniałem, że procesor do zmiennej user_input zapisywać będzie zawsze pierwszy otrzymany znak. Co się jednak stanie, gdy spróbujemy wysłać mu ciąg znaków? Dla przykładu wyślijcie teraz coś takiego jak: „501”. Po wysłaniu dioda będzie świecić, a na ekranie komputera zobaczycie coś dziwnego, czyli komunikat o niepoprawnym znaku, wyłączeniu diody i włączeniu diody. Przeanalizujmy jednak ten przypadek dokładniej. RP2040 otrzymuje ciąg znaków „501”, który zapisywany jest w odpowiednim buforze. Wykonujący się program odczytuje piątkę, która z założenia jest niepoprawnym znakiem i taki komunikat zostaje wyświetlony. Pętla wraca na początek, ale w buforze znajdują się kolejne dane, piątka została już wcześniej usunięta, ale kolejne jest zero, dlatego program wyłącza diodę LED. Kolejne wykonanie pętli, w buforze umieszczone są jakieś dane, dlatego mikrokontroler myśli, że użytkownik wysłał kolejne znaki. Jest to jedynka, w efekcie czego dioda zostanie uruchomiona. Dopiero przy kolejnym wykonaniu pętli bufor będzie pusty i program będzie czekać na dane wprowadzane za pomocą klawiatury. Tak więc w przypadku, gdy wyślemy do mikrokontrolera większy tekst, tam zostanie obsłużony w całości i układ zareaguje na każdy pojedynczy znak.

Przetwornik ADC

Przetwornik ADC w RP2040. (https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf)

Za pomocą sygnału PWM staraliśmy się na swój sposób imitować sygnał analogowy, tak aby wydawało nam się, że dioda LED sterowana jest w płynny sposób. Była to jednak tylko imitacja, bo jak już wiecie, w rzeczywistości nadal był to sygnał w stu procentach cyfrowy. W tym podrozdziale zajmiemy się prawdziwym sygnałem analogowym, jednak nie będziemy go generować, a jedynie odczytywać.

We wnętrzu współczesnych mikrokontrolerów znaleźć możemy coś takiego jak ADC. Dokładniej jest to przetwornik ADC (ang. Analog-digital converter) umożliwiający konwersję sygnału analogowego do postaci cyfrowej. W rzeczywistości działa to w ten sposób, że układ przypisuje konkretną wartość liczbową do odczytanego z zewnętrznego wyprowadzenia napięcia. O tym, jaka to będzie wartość, decyduje przede wszystkim rozdzielczość przetwornika. Procesor umieszczony na płytce RPI Pico W wyposażony został w 12 bitowy przetwornik, przez co sygnał analogowy może przyjąć wartość z zakresu od 0 do 4095. Spotkać można się też z 8 lub 16 bitowymi przetwornikami i zasadą jest, że im wyższa rozdzielczość, tym dokładniejszy będzie pomiar. Dla przykładu w 8 bitowych konstrukcjach zakres odczytu mieści się między 0 a 255, przez co jest on mniejszy i pomiar jest mniej dokładny. Pamiętać trzeba też, że mierzony sygnał musi być napięciowo zgodny z mikrokontrolerem. RP2040 operuje na logice 3,3V, dlatego mierzony sygnał nigdy nie powinien przekraczać tej wartości, w przeciwnym razie możemy uszkodzić układ.

W Raspberry Pi Pico W dostępnych jest pięć wejść, do których podłączyć możemy sygnał analogowy, z czego jeden zarezerwowany jest dla wewnętrznego czujnika temperatury, umieszczonego bezpośrednio w krzemie. Pozostałe wejścia połączone są z zewnętrznymi wyprowadzeniami o numerach 26, 27, 28 i 29. Warto też wspomnieć, że tak naprawdę RP2040 wyposażono tylko w jeden przetwornik ADC, który obsługuje wszystkie możliwe źródła sygnału analogowego. Są one przełączane przez specjalny multiplekser i dopiero wówczas sygnał poddawany jest konwersji. Takie rozwiązanie jest stosowane dość powszechnie, przede wszystkim przez fakt, że przetworniki są dość skomplikowanymi i drogimi w produkcji konstrukcjami.

Obwód do testów fotorezystora.

Działanie przetwornika ADC przetestujemy w rzeczywistych warunkach, dzięki podłączonemu do RPI fotorezystorowi, który zmienia wartość swojej rezystancji pod wpływem padającego na jego lico światła. Jednak nie będzie on podłączony tak po prostu – aby stworzyć sygnał analogowy, zbudować musimy dzielnik napięcia złożony ze wspomnianego elementu oraz rezystora o wartości 1,2kΩ. Jedno z wyprowadzeń fotorezystora podłączamy do napięcia 3,3V dostępnego na wyprowadzeniu numer 36. Osobiście napięcie to wyprowadziłem do oznaczonej czerwonym kolorem magistrali na płytce stykowej. Drugie wyprowadzenie musimy połączyć z pierwszą nóżką rezystora, natomiast drugą końcówkę rezystora podłączamy do masy. Sygnał odczytywać będziemy w miejscu połączenia rezystora i fotorezystora, podłączamy je przewodowo do pinu GP26, oznaczanego na pinoucie płytki jako ADC0.

Rozwiązanie z dzielnikiem napięcia jest niezbędne, ponieważ gdybyśmy podłączyli fotorezystor do RPI tak po prostu, uzyskiwalibyśmy napięcie zbliżone do 3,3V, niezależnie od padającej na element wiązki światła. Zamianie ulegałaby rezystancja oraz płynący prąd, ale napięcie byłoby zawsze takie samo. Jednak dzięki dodatkowemu rezystorowi, możemy podzielić odkładające się na elementach napięcie na dwie części, które zależne będą właśnie od rezystancji. Więcej światła padającego na fotorezystor oznacza zmniejszenie jego rezystancji, wzrost prądu i obniżenie odkładającego się na nim napięcia. Większą część napięcia przechwytuje wówczas rezystor, na którym to w rzeczywistości wykonujemy pomiar. Należy pamiętać, że napięcie odczytujemy względem masy, dlatego, tak naprawdę mierzymy rezystor, a nie fotorezystor. Analogicznie, gdy światła jest mniej, opór fotorezystora rośnie i odkłada się na nim coraz większe napięcie, jednocześnie obniżając różnicę potencjałów na rezystorze. Ktoś mógłby zapytać, dlaczego zbudowałem właśnie taki obwód i czy można zamienić miejscami rezystor z fotorezystorem? Oczywiście elementy te można zamienić miejscami, ale wówczas odwrócimy działanie układu. W moim rozwiązaniu odczytywane napięcie będzie rosło wraz ze wzrostem ilości fotonów padających na fotorezystor, po zamianie elementów działałoby to odwrotnie, co wydaje mi się mniej intuicyjne.

Po tym nieco dłuższym wstępie możemy przejść do programowania. Na potrzeby testów przygotowałem projekt nazwany photoresistor_adc. Jego zadaniem będzie odczytanie sygnału z pinu GP26 i przesłanie liczbowej wartości poprzez USB do komputera, dzięki czemu będziemy mogli podejrzeć ją w serial monitorze.

				
					#include "pico/stdlib.h"
#include "hardware/adc.h"

int main() {
    stdio_init_all();

    adc_init(); //ADC initialization

    adc_gpio_init(26); //assignment to ADC pin 26 (ADC0)
    adc_select_input(0);

    while(true) {
        
        uint16_t result = adc_read(); //ADC value reading
        float voltage_value = result * (3.3/4095); //ADC to voltage conversion
        printf("Read ADC: value = %d, voltage = %f V\n", result, voltage_value);
        
        sleep_ms(500);
    }
}

				
			

Program, który wspólnie omówimy, jest dość krótki, ale znalazło się w nim kilka nowości, które za chwilę poznacie. Jako że korzystać będziemy z kolejnej wbudowanej w mikrokontroler funkcjonalności, dołączyć musimy odpowiednią bibliotekę, a będzie to hardware/adc.h. Pamiętajcie o dodaniu jej w pliku CMakeLists.txt, poza tym w projekcie korzystamy też ze strumienia wyjściowego USB i go również należy aktywować w tym pliku, podobnie jak w poprzednich przykładach.

W pierwszej części programu inicjalizujemy przetwornik ADC, dzięki poleceniu adc_init(), a także wybieramy wykorzystywany port. W pierwszej funkcji wybieramy odpowiedni numer GPxx, natomiast adc_select_input odpowiada za wybór sygnału ADC, który przekazany zostanie do przetwornika, przez wcześniej wspomniany multiplekser.

W głównej części programu tworzymy zmienną result, która dzięki użyciu funkcji adc_read() przechowywać będzie liczbową wartość sygnału ADC, odczytaną z pinu GP26. W kolejnym kroku generujemy zmiennoprzecinkową voltage_value, ponieważ poza liczbową wartością ADC warto byłoby też znać konkretne napięcie, jakie pojawia się na wyprowadzeniu RPI Pico W. Różnicę potencjałów możemy obliczyć, korzystając z prostej zależności:

4095 = 3,3V

1 = xV

4095x = 3,3V

x = 3,3V/4095 = 0,000805V

Dzięki niej wiemy, że każde zwiększenie wartości ADC o jeden odpowiada w rzeczywistości wzrostowi napięcia o około 0,000805V. W języku C z powodzeniem można tworzyć różnego rodzaju matematyczne działania, dlatego, aby RP2040 mógł przesłać nam odpowiednią wartość, która zostanie dalej wyświetlona, przygotujemy proste obliczenie matematyczne. Chcąc uzyskać wartość napięcia odpowiadającą wartości ADC, wystarczy, że przemnożymy ją przez obliczoną chwilę wcześniej wartość. Umożliwia nam to operator gwiazdki (*), mnożymy wartość zmiennej result przez (3.3/4095). Oczywiście można by tu wstawić po prostu 0,000805, ale nic nie stoi na przeszkodzie, aby mikrokontroler sam wyznaczył odpowiednią wartość. Dlatego dzięki operatorowi (/) dzielimy 3,3 przez 4095. Zauważcie, że działanie to umieszczone jest w nawiasie, gdyby go zabrakło, układ najpierw wykonałby operację mnożenia a dopiero później dzielenie, uzyskując wówczas niepoprawny wynik. Warto wiedzieć, że C wspiera znaną z matematyki i realizowaną dzięki nawiasom kolejność wykonywania działań.

W ostatnim punkcie, dzięki znanej już funkcji printf wysyłamy do komputera odpowiedni komunikat zawierający zmienną result oraz voltage_value.

Wartości ADC i napięcia przesyłane przez RPI.

Po uruchomieniu programu i otwarciu serial monitora obserwować możemy zmieniającą się liczbową reprezentację ADC i napięcia na ekranie naszych komputerów. Wartości te rosną wraz z ilością padającego na fotorezystor światła.

Uszkodzony fotorezystor RPP130 z lat 80. (http://www.cemi.cba.pl/fotorezystor.html)
Współczesny fotorezystor.

Na koniec chciałbym pokazać wam dwie ciekawe fotografie związane z fotorezystorami. Te optoelektroniczne elementy zbudowane są z cienkich, półprzewodnikowych ścieżek, nanoszonych na podłoże z dielektryka. Wykorzystany materiał półprzewodnikowy zależy od konkretnego typu fotorezystora i jego charakterystyki widmowej, ale powszechnie spotykanymi materiałami są: siarczek kadmu (CdS), siarczek ołowiu (PbS), selenek ołowiu (PbSe) lub antymonek indu (InSb). Każdy fotorezystor, podobnie jak inne elementy elektroniczne produkowany musi być w sterylnych warunkach, ale błędy mogą zdarzyć się zawsze. Na zdjęciu po prawej stronie zobaczyć możecie zwyczajny, poprawnie wykonany fotorezystor. Natomiast na lewej fotografii znalazł się uszkodzony w czasie produkcji element o oznaczeniu RPP130 pochodzący z nieistniejących już Toruńskich zakładów Unitra TOMI. Przyczyną awarii, była prawdopodobnie para wodna, która osiadła na strukturze fotorezystora przed umieszczeniem go w hermetycznej obudowie.

Uszkodzony fotorezystor.

Jednak tego typu uszkodzenie to domena nie tylko wiekowych fotorezystorów z lat 80. Na zdjęciu powyżej zobaczyć możecie ciekawy przypadek współczesnego fotorezystora o podobnym uszkodzeniu. Dzięki swojej nietypowej strukturze charakteryzuje się on znacznie większą opornością niż katalogowe 15kΩ.

Kilka słów na koniec…

Kolejny materiał o Raspberry Pi Pico W za nami. Tym razem poznaliśmy modulację szerokości impulsów PWM i wykorzystaliśmy tą funkcjonalność do sterowania diodą LED. Poza tym opowiedziałem wam, w jaki sposób nawiązać komunikację z komputerem, dzięki czemu w kolejnych programach, w prosty sposób będziemy mogli wyświetlać informację przesyłane przez RP2040 na jego ekranie. Poza tym uruchomiliśmy fotorezystor, korzystając z przetwornika ADC. W kolejnym artykule wykorzystamy zdobytą do tej pory wiedzę i przygotujemy wspólnie nieco większy program.

Źródła:

  • https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf
  • https://datasheets.raspberrypi.com/picow/pico-w-datasheet.pdf
  • https://www.raspberrypi.com/products/rp2040/
  • https://www.raspberrypi.com/documentation/microcontrollers/raspberry-pi-pico.html
  • https://en.wikipedia.org/wiki/Pulse-width_modulation
  • http://www.cemi.cba.pl/fotorezystor.html

Jak oceniasz ten wpis blogowy?

Kliknij gwiazdkę, aby go ocenić!

Średnia ocena: 5 / 5. Liczba głosów: 12

Jak dotąd brak głosów! Bądź pierwszą osobą, która oceni ten wpis.

Podziel się:

Picture of Rafał Bartoszak

Rafał Bartoszak

Współpracujący z Botlandem elektronik, który dzieli się swoją wiedzą w  internecie. Entuzjasta systemów cyfrowych, układów programowalnych i mikroelektroniki. Pasjonat historii, ze szczególnym naciskiem na wiek XX.

Zobacz więcej:

Jedna odpowiedź

  1. Na jutro (niedziela, 09.03.2024) zaplanowałem przerobienie dotychczas ostatniej, 6 części cyklu. Kiedy pojawią się następne części? Długo już nie było aktualizacji.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Ze względów bezpieczeństwa wymagane jest korzystanie z usługi Google reCAPTCHA, która podlega Polityce Prywatności oraz Warunkom użytkowania.