Kurs Raspberry Pi Pico – #7 – Poprawki w kodzie i własne funkcje

Czas czytania: 13 min.

W poprzednim artykule poznaliśmy kilka funkcjonalności związanych z mikrokontrolerem RP2040. Sterowaliśmy diodą LED za pomocą modulowanego sygnału PWM, odczytaliśmy sygnał analogowy z fotorezystora oraz nawiązaliśmy komunikację z komputerem. W tym artykule skorzystamy z poznanych już wcześniej funkcji i z ich pomocą stworzymy nieco bardziej rozbudowany program, tak aby później móc go uprościć. Zachowując jednocześnie jego funkcjonalność.

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.

Przygotowanie projektu

Obwód, znany z poprzednich projektów.

W dzisiejszym projekcie ponownie skorzystamy z obwodu przygotowanego we wcześniejszych poradnikach. Tym razem skorzystamy też ze wszystkich jego elementów, czyli diod LED, przycisku oraz fotorezystora. Zadaniem programu, który napiszemy w dalszej części, będzie sterowanie świecącymi elementami, zależnie od natężenia światła. Jeśli jego wartość znajdzie się w jednym z trzech zakresów, które stworzymy, uruchomiona zostanie konkretna dioda świecąca. Poza tym skorzystamy też z przycisku, po którego wciśnięciu mikrokontroler odczyta wartość z czujnika temperatury umieszczonego wewnątrz rdzenia procesora. Wartość tą przeliczymy na stopnie Celsjusza i prześlemy przez USB, tak aby móc odczytać ją z ekranu komputera.

W tym artykule korzystać będziemy z projektu nazwanego functions_test. Przy jego przygotowaniu należy pamiętać o aktywowaniu strumienia wyjściowego USB w pliku CMakeLists.txt.

Brzydki, ale działający kod

#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/adc.h"

#define RED_LED 2
#define YELLOW_LED 1
#define GREEN_LED 0
#define BUTTON 16

int main() {
stdio_init_all();

gpio_init(RED_LED);
gpio_init(YELLOW_LED);
gpio_init(GREEN_LED);
gpio_init(BUTTON);

gpio_set_dir(RED_LED, GPIO_OUT);
gpio_set_dir(YELLOW_LED, GPIO_OUT);
gpio_set_dir(GREEN_LED, GPIO_OUT);
gpio_set_dir(BUTTON, GPIO_IN);

gpio_pull_up(BUTTON);

adc_init();
adc_set_temp_sensor_enabled(true);
adc_gpio_init(26);

while(true) {

adc_select_input(0);
uint16_t photores_read = adc_read();
float voltage_photores = photores_read * (3.3/4095);
printf("Photoresistor ADC: value = %d, voltage = %f V\n", photores_read, voltage_photores);

if(photores_read <= 2500){
gpio_put(GREEN_LED, 1);
gpio_put(YELLOW_LED, 0);
gpio_put(RED_LED, 0);
}
else if(photores_read > 2500 && photores_read <= 3500){
gpio_put(GREEN_LED, 0);
gpio_put(YELLOW_LED, 1);
gpio_put(RED_LED, 0);
}
else if(photores_read > 3500){
gpio_put(GREEN_LED, 0);
gpio_put(YELLOW_LED, 0);
gpio_put(RED_LED, 1);
}
else
{
printf("Photoresistor error");
}


bool button_state = gpio_get(BUTTON);

if(button_state == false){
adc_select_input(4);
uint16_t adc_temp = adc_read();
float conversion_factor = 3.3 / (1<<12);
float result = adc_temp * conversion_factor;
float temp = 27 - (result -0.706)/0.001721;
printf("Temp = %f C\n", temp);

}
sleep_ms(500);
}
}

Na pierwszy ogień przyjrzyjmy się programowi, który działa, ale jak pewnie się domyślacie, w dalszej części artykułu będziemy go poprawiać. Kod złożony jest we większości ze znanych już poleceń, choć kilku z nich przyjrzymy się ponownie. Poza tym warto jest przeanalizować program jako całość, tak aby lepiej zrozumieć jego strukturę i późniejsze kroki, które wykonamy, aby go poprawić.

Główna funkcja main została jak zwykle poprzedzona bibliotekami oraz definicjami. W programie korzystamy z przetwornika analogowo-cyfrowego, tak więc musimy uzupełnić kod o hardware/adc.h. Poza tym, aby nie musieć korzystać z cyfrowych oznaczeń poszczególnych wyprowadzeń przypisane do nich zostały bardziej intuicyjne określenia.

Początek głównej części programu również może wyglądać znajomo. Znalazły się tutaj funkcje inicjalizujące piny mikrokontrolera, do których podłączone są diody LED oraz przycisk. Dodatkowo uruchomiony zostaje przetwornik ADC wraz z przypisanym do niego wyprowadzeniem 26, do którego podłączony jest fotorezystor. Nowością w tej części kodu jest funkcja adc_set_temp_sensor_enabled(true);, dzięki niej aktywowany zostaje umieszczony we wnętrzu procesora sensor temperatury. Warto zwrócić też uwagę, że jak dotąd nie pojawiło się jeszcze polecenie adc_select_input(), definiujące, który z sygnałów przekazany zostanie do przetwornika analogowo-cyfrowego. Jest to świadoma decyzja, ponieważ jak już wiecie RP2040 wyposażony jest tylko w jeden moduł ADC, do którego podłączonych jest kilka wejść. W typ projekcie korzystać będziemy z dwóch, dla fotorezystora i czujnika temperatury, dlatego w dalszej części kodu będziemy się między nimi przełączać.

Na początku nieskończonej pętli while pojawił się kod, który wykorzystywaliśmy już w poprzednim projekcie. Odczytujemy tutaj wartość generowaną przez fotorezystor, aby przekształcić ją później na napięcie i dzięki funkcji printf wysłać na ekran komputera. Zauważcie, że właśnie tutaj wybieramy też wejście, z którego sygnał przekazany zostanie do przetwornika ADC. Wyprowadzeniu o numerze 26 odpowiada zero, tak więc właśnie tą cyfrę należy umieścić wewnątrz funkcji.

W kolejnej części kodu znalazł się kaskadowy warunek zależny od wartości photores_read, czyli zmiennej, w której zapisana jest liczba odpowiadająca sygnałowi z fotorezystora. Jak już wiecie, w 12 bitowym przetworniku odczytane dane mogą przyjąć wartość z zakresu od 0 do 4095. Zakres ten postanowiłem podzielić na trzy części: od 0 do 2500, od 2501 do 3500 oraz od 3501 do 4095. Jeśli wartość photores_read znajdzie się w pierwszym z nich, uruchomiana zostanie zielona dioda LED, przy drugim zakresie będzie to żółty element świecący, a przy ostatnim czerwony. Poza tym w warunku umieszczone zostało zabezpieczenie na wypadek, gdyby zmienna przyjęła jakąś „inną” wartość. Wówczas na ekranie komputera pojawi się komunikat Photoresistor error.

Omówić musimy też samą konstrukcję warunków. Do tej pory przyrównywaliśmy zmienną do konkretnych wartości, dzięki dwóm znakom równości „==”. W tym przypadku jednak musimy porównać ją z wcześniej opisanymi zakresami. Innymi słowy, musimy sprawdzić, czy zmienna będzie większa lub mniejsza od konkretnej liczby. Podobnie jak w matematyce posłużyć możemy się tutaj znakami mniejszości „<”, większości „>”, mniejszy równy „<=” oraz większy równy „>=”. W pierwszym ifie sprawdzimy pierwszy zakres, tak więc porównujemy czy photores_read jest mniejszy bądź równy 2500. Trzeba jednak pamiętać, że taki zapis nie jest idealny, ponieważ warunek spełni się również przy okazji liczb ujemnych, których zakres nie obejmuje, ale tym zajmiemy się później. Bliźniaczą sytuację możemy zauważyć przy okazji trzeciego warunku, gdy sprawdzany jest ostatni zakres. Jeśli wartość z fotorezystora będzie większa niż 3501, aktywowana zostanie czerwona dioda LED. Zauważcie też, że sam warunek można zapisać na dwa sposoby photores_read > 3500 lub photores_read >= 3501, są one równoznaczne i aktywowane zostaną, gdy wartość zmiennej przyjmie co najmniej 3501. W drugim warunku, gdzie sprawdzamy środkowy zakres wartość dzieje się zdecydowanie najwięcej. W tym przypadku opisać trzeba konkretny zakres wartości i nie można przyjmować na wiarę wszystkich liczb poniżej lub powyżej jakiejś wartości. Aby żółta dioda LED uruchamiana była tylko, gdy wartość photores_read znajdzie się w zakresie od 2501 do 3500, stworzyć musimy niejako dwa warunki, połączone tajemniczym „&&”. Pierwszy z nich sprawdza, czy zmienna jest większa od 2500 –  photores_read > 2500, druga natomiast czy liczba nie przekroczyła 3500 – photores_read <= 3500. Konstrukcja „&&” umieszczona między dwoma porównaniami jest operatorem logicznym AND. Operatory logiczne kojarzyć możecie z matematyki lub bramek logicznych. W języku C poza AND dostępny jest też OR zapisywany jako „||”. Dzięki takiemu zapisowi funkcja warunkowa zostanie spełniona tylko, gdy oba porównania będą prawdziwe, innymi słowy wartość photores_read będzie mieścić się w opisanym zakresie. W przypadku, gdy przypisana do zmiennej liczba będzie poza zakresem, spełniony zostanie tylko jeden z warunków, przez co if jako całość nie będzie mógł być spełniony. Tak więc można powiedzieć, że dzięki operatorowi AND funkcja warunkowa zostanie wykonana tylko, gdy wszystkie warunki zostaną spełnione. Logiczny OR działa nieco inaczej, gdybyśmy umieścili go wewnątrz funkcji, dioda świeciłaby zawsze, ponieważ w takim przypadku wystarczy, że jeden z warunków zostanie spełniony i funkcja warunkowa zostanie wykonana.

W drugiej części programu znalazł się kod obsługujący przycisk oraz odczyt temperatury. Początkowo tworzymy zmienną button_state, do której zapisany zostanie logiczny stan przycisku. Jeśli ten zostanie wciśnięty, uruchomi się kod umieszczony wewnątrz funkcji warunkowej. W pierwszym etapie zmieniamy sygnał przekazywany do przetwornika ADC. Wcześniej wybrany był fotorezystor, tym razem chcemy odczytać wartość sensora temperatury, który przypisany jest do cyfry cztery. Następnie tworzymy zmienną adc_temp, do której przypisana zostaje odczytana wartość. Jednak, aby z czystej wartości ADC uzyskać temperaturę Celsjusza, musimy ją odpowiednio przekształcić. Do tego celu potrzebna będzie nam zmiennoprzecinkowa conversion_factor, w której zapisany zostanie wynik działania 3.3/(1<<12)-1. Ten dość tajemniczy zapis jest niczym innym, jak działaniem 3.3/4095, z którego korzystaliśmy już wcześniej przy fotorezystorze. (1<<12) to operacja przesunięcia bitowego w lewo. Oznacza to, że w zerowej zmiennej przesuniemy jedynkę na dwunaste miejsce, uzyskując w ten sposób liczbę 4096, od której na końcu odejmujemy jeden.

1 = 00000000 00000000 = 1

1<<1 = 00000000 00000010 = 2

1<<8 = 00000001 00000000 = 256

1<<12 = 0010000 00000000 = 4096

Przesunięcia bitowe dość łatwo jest zobrazować sobie na powyższym przykładzie. Przesuwając jedynkę o określoną ilość miejsc, można dość łatwo uzyskać liczby będące kolejnymi potęgami dwójki, które w elektronice i programowaniu są niezwykle przydatne. Bity przesuwać można też w lewo, dzięki operatorowi „>>”. Poza tym warto wiedzieć, że przesunięcia bitowe tyczyć mogą się zmiennej jako całości, ale takimi przykładami zajmiemy się innym razem.

Po uzyskaniu zmiennej result, która jest de facto wartością napięcia odczytaną z czujnika temperatury, musimy przekształcić ją na stopnie Celsjusza. Zadanie to realizuje polecenie float temp = 27 – (result -0.706)/0.001721;, dzięki któremu w zmiennej temp zapisana zostanie zmiennoprzecinkowa wartość temperatury. Za wykorzystanym działaniem nie kryje się żadna szczególna historia, jest to po prostu podstawiona do wzoru dostępnego w dokumentacji RP2040 wartość. Dzięki umieszczonemu na końcu poleceniu printf, na monitorze portu szeregowego będzie można zobaczyć odczytaną temperaturę.

Jak być może zauważyliście, w tym programie zmieniłem nieco umiejscowienie komentarzy. W poprzednich projektach umieszczałem je po prawej stronie funkcji, jednak w tym przypadku znalazły się one ponad nimi.

//conditional function controlling LEDs
if(photores_read <= 2500){
    gpio_put(GREEN_LED, 1);
    gpio_put(YELLOW_LED, 0);
    gpio_put(RED_LED, 0);
}

if(photores_read <= 2500){ //conditional function controlling LEDs
    gpio_put(GREEN_LED, 1);
    gpio_put(YELLOW_LED, 0);
    gpio_put(RED_LED, 0);
}

Tak naprawdę obie konwencje zapisu są poprawne i od was zależy, którą z nich będziecie stosować. Oczywiście sposoby zapisu można też mieszać, dodając komentarz raz obok, a raz ponad funkcją. Najważniejsze jest jednak samo dodawanie komentarzy, tak aby czytający mógł w prosty sposób zorientować się w strukturze kodu. 

Po kompilacji i wrzuceniu pliku .uf2 do pamięci RPI Pico W, sprawdzić możemy dwojaką funkcjonalność przygotowanego programu. Na filmie możecie zobaczyć jak stopniowo oświetlam fotorezystor. Im więcej światła otrzymuje ten element, tym bardziej zmniejsza się jego rezystancja, wpływając na wartość napięcia odczytywanego przez Raspberry Pi Pico. Tym sposobem zmianie ulega też odczytywana zmienna i zależnie od jej wartości uruchamiana jest inna dioda LED. Jeśli światła jest więcej, zmienna trafi do kolejnego zakresu opisanego w kaskadowej funkcji warunkowej.

Wartość temperatury wewnętrznej procesora.

Poza tym, jeśli uruchomimy monitor portu szeregowego, możemy zobaczyć wartość ADC oraz napięcie odczytane z wyprowadzenia fotorezystora. Tutaj też pojawi się wewnętrzna temperatura mikrokontrolera, wysyłana, gdy wciśniemy umieszczony na płytce stykowej przycisk. Trzymając palec na powierzchni mikrokontrolera, można obserwować rosnący powoli odczyt, ale pamiętać trzeba, że w porównaniu do zewnętrznych czujników nie jest on zbyt dokładny i w żadnym wypadku nie można traktować go jako czujnika temperatury zewnętrznej. Chip sam w sobie może się nagrzewać zależnie od obciążenia i czujnik ten służy do odczytywania właśnie temperatury samego układu. We większych projektach można powiązać ją z aktywnym chłodzeniem urządzenia.

Niewielkie poprawki

Program, który przygotowaliśmy działa poprawnie, fotorezystor reaguje na padające światło, a wciskając przycisk możemy zobaczyć temperaturę układu RP2040. Czy w takim razie jest w ogóle sens próbować poprawiać kod, skoro jest on funkcjonalny? Odpowiedź jest jak najbardziej pozytywna, w tym, jak i w innych przypadkach powinniśmy wracać do wcześniej napisanych programów z kilku istotnych powodów. Analizując wcześniejsze rozwiązania, ciągle doskonalimy swoje umiejętności, a poza tym nie raz wpaść możemy na lepszy sposób rozwiązania jakiegoś problemu. Poza tym powinniśmy starać się eliminować potencjalne błędy, jednocześnie usprawniając czytelność samego kodu, tak aby był on zrozumiały dla innych programistów. Ostatecznie, regularne przyglądanie się i poprawianie wcześniej napisanego kodu stanowi nieodłączny element procesu własnego samodoskonalenia.

Przy tej okazji chciałbym też zwrócić uwagę na jedno z zagadnień powiązanych z programowaniem, które wielu początkujących ignoruje, a znajomość tematu bywa w wielu przypadkach przydatna. Są to algorytmy, będące w rzeczywistości bardzo szerokim tematem, którym warto się zainteresować. Na pewnym forum spotkałem się kiedyś z określeniem, że programista nieznający algorytmów jest jak „code stitcher”, co można przetłumaczyć jako „klepacz kodu”. Cóż, ciężko się z tym nie zgodzić. Temat ten jest bardzo szeroki, ale w skrócie algorytmy możemy określić jako zorganizowane i optymalne sposoby rozwiązywania problemów. Z wieloma programistycznymi zagwozdkami twórcy programów spotykali się już lata temu, tak więc nie ma większego sensu wynajdywanie koła na nowo. Warto jest sięgnąć po książkę lub też przeszukać internet pod kątem stosowanych w programowaniu algorytmów, bo dzięki tej wiedzy pisane w przyszłości programy będą optymalniejsze. Poza tym algorytmy ułatwiają rozwiązywanie problemów, które są istotą programowania.

#include <stdio.h>
#include “pico/stdlib.h”
#include “hardware/adc.h”

#define RED_LED 2
#define YELLOW_LED 1
#define GREEN_LED 0
#define BUTTON 16

const float conversion_factor = 3.3/4095;

int main() {
    stdio_init_all();

    gpio_init(RED_LED);
    gpio_init(YELLOW_LED); 
    gpio_init(GREEN_LED);
    gpio_init(BUTTON);

    gpio_set_dir(RED_LED, GPIO_OUT);
    gpio_set_dir(YELLOW_LED, GPIO_OUT);
    gpio_set_dir(GREEN_LED, GPIO_OUT);
    gpio_set_dir(BUTTON, GPIO_IN);

    gpio_pull_up(BUTTON);

    adc_init();
    adc_set_temp_sensor_enabled(true);
    adc_gpio_init(26);

 

    while(true) {

    adc_select_input(0);
    uint16_t photores_read = adc_read();
    const float voltage_photores = photores_read * conversion_factor;
    printf(“Photoresistor ADC: value = %d, voltage = %f V\n”, photores_read, voltage_photores);

    if(photores_read >= 0 && photores_read <= 2500){
        gpio_put(GREEN_LED, 1);
        gpio_put(YELLOW_LED, 0);
        gpio_put(RED_LED, 0);
    }
    else if(photores_read > 2500 && photores_read <= 3500){
        gpio_put(GREEN_LED, 0);
        gpio_put(YELLOW_LED, 1);
        gpio_put(RED_LED, 0);
}
    else if(photores_read > 3500 && photores_read <= 4095){
        gpio_put(GREEN_LED, 0);
        gpio_put(YELLOW_LED, 0);
        gpio_put(RED_LED, 1);
    }
    else
   {
       printf(“Photoresistor error”);
   }

    bool button_state = gpio_get(BUTTON);

    if(button_state == false){
        adc_select_input(4);
        uint16_t adc_temp = adc_read();
        float temp = 27 – ((adc_temp * conversion_factor)-0.706)/0.001721;
        printf(“Temp = %f C, voltage = %f V\n”, temp, (adc_temp * conversion_factor));
    }
    sleep_ms(500);
    }
}

Skoro wiemy już, że warto wracać do napisanych wcześniej kodów, przyjrzyjmy się napisanemu chwilę wcześniej programowi i spróbujmy wprowadzić do niego kilka niewielkich zmian.

Na początek poprawmy kaskadową funkcję warunkową, która steruje diodami LED. Jak już wcześniej wspomniałem, jej problemem były niezbyt dokładnie opisane zakresy. Mogłoby się wydawać, że nie jest to jakiś duży problem, bo przecież wiemy, że odczytana z fotorezystora wartość nie będzie mniejsza niż zero i większa niż 4095. Nie mamy jednak stuprocentowej pewności czy we wnętrzu mikrokontrolera nie pojawi się jakiś problem i czy zbłąkany elektron nie zmieni wartości zapisanej w którymś z rejestrów. Oczywiście, nawet gdyby tak się stało, to w przypadku obwodu na płytce stykowej nie miałoby to większego znaczenia. Trzeba jednak pamiętać, że nie każdy program uruchamiany jest tylko na płytce stykowej – w systemach medycznych czy wojskowych błędy i nieprzewidywalne zachowania w programie są niedopuszczalne. Właśnie dlatego warto jest poprawić warunki, nawet w tak prostym kodzie. Po modyfikacji, każda z funkcji if odwołuje się do konkretnego zakresu, a warunki zbudowane zostały na omówionym już wcześniej operatorze AND.

Przyjrzeć możemy się też funkcjom związanym z odczytem wartości ADC z fotorezystora i sensora temperatury. W obu przypadkach korzystaliśmy ze zmiennej potrzebnej do konwersji, w której zapisany był wynik działania 3.3/4095. Wykonywanie dwukrotnie tego samego obliczenia jest marnotrawstwem czasu i zasobów. Dlatego zmienną conversion_factor możemy wyrzucić na początek programu i zrobić z niej stałą, dzięki czemu przyjmie ona odpowiednią wartość, która nie może być modyfikowana. Stałą definiujemy poprzez dodanie słowa kluczowego const przed określeniem typu. Stałe podobnie jak zmienne mogą być globalne lub lokalne, w tym przypadku deklarację conversion_factor umieściłem powyżej funkcji main, dzięki czemu będzie ona globalna, co przyda nam się jeszcze później.

Dzięki dodaniu stałej modyfikacji uległy nieco obliczenia związane z odczytywanymi sygnałami ADC. Przy fotorezystorze zmieniłem tylko zapis 3.3/4095 na deklarację stałej, ale przy obliczeniach powiązanych z czujnikiem temperatury całość skróciła się o dwie linijki. Pierwszym poleceniem, które zniknęło, jest oczywiście wyznaczenie conversion_factor, obecnie już w formie stałej. Poza tym zrezygnowałem z wyznaczania wartości result, która umieszczona była wewnątrz działania wyznaczającego temperaturę. Zamiast tego pod wzór podstawione zostały odpowiednie zmienne, w ten sposób uzyskaliśmy polecenie temp = 27 – ((adc_temp * conversion_factor)-0.706)/0.001721. Jak możecie zauważyć, operację mnożenia umieściłem w dodatkowym nawiasie, choć z perspektywy kolejności wykonywania działań operacja ta wykona się jako pierwsza. Jest on zbędny, ale osobiście lubię zapisywać działania właśnie w taki sposób, podkreślając niejako ciąg kolejno wykonywanych operacji.

Dodatkowo na samym końcu rozbudowane zostało polecenie printf. Od teraz poza wartością temperatury na ekranie komputera będzie można zobaczyć też wartość napięcia odczytaną z czujnika temperatury. Jak możecie zauważyć, zmienna podstawiana za drugi operator %f ma w rzeczywistości formę działania matematycznego. W języku C z powodzeniem możemy tworzyć tego typu konstrukcje, w których konkretne zmienne mają tak naprawdę formę innych działań czy nawet w niektórych przypadkach całych funkcji. Trzeba jednak uważać, aby przesadnie nie rozbudować jakieś instrukcji i nie tworzyć swego rodzaju „tasiemców”. Dłuższe instrukcje mogą z czasem stać się znacznie trudniejsze do zrozumienia.

Przygotowanie własnych funkcji

#include <stdio.h>
#include “pico/stdlib.h”
#include “hardware/adc.h”

#define RED_LED 2
#define YELLOW_LED 1
#define GREEN_LED 0
#define BUTTON 16

const float conversion_factor = 3.3/4095;

void PhotoresRun();
uint8_t TempSensorRun(bool button_state);

int main() {
    stdio_init_all();

    gpio_init(RED_LED);
    gpio_init(YELLOW_LED);
    gpio_init(GREEN_LED);
    gpio_init(BUTTON);

    gpio_set_dir(RED_LED, GPIO_OUT);
    gpio_set_dir(YELLOW_LED, GPIO_OUT);
    gpio_set_dir(GREEN_LED, GPIO_OUT);
    gpio_set_dir(BUTTON, GPIO_IN);

    gpio_pull_up(BUTTON);

    adc_init();
    adc_set_temp_sensor_enabled(true);
    adc_gpio_init(26);

 

    while(true) {

    PhotoresRun();

    bool button_state = gpio_get(BUTTON);
    uint8_t return_value = TempSensorRun(button_state);
    printf(“return_value = %d \n”, return_value);

    sleep_ms(500);
    }
}

void PhotoresRun(){
    adc_select_input(0);
    uint16_t photores_read = adc_read();
    const float voltage_photores = photores_read * conversion_factor;
    printf(“Photoresistor ADC: value = %d, voltage = %f V\n”, photores_read, voltage_photores);

    if(photores_read >= 0 && photores_read <= 2500){
        gpio_put(GREEN_LED, 1);
        gpio_put(YELLOW_LED, 0);
        gpio_put(RED_LED, 0);
    }
    else if(photores_read > 2500 && photores_read <= 3500){
        gpio_put(GREEN_LED, 0);
        gpio_put(YELLOW_LED, 1);
        gpio_put(RED_LED, 0);
    }
    else if(photores_read > 3500 && photores_read <= 4095){
        gpio_put(GREEN_LED, 0);
        gpio_put(YELLOW_LED, 0);
        gpio_put(RED_LED, 1);
    }
    else
    {
    printf(“Photoresistor error”);
    }
}

uint8_t TempSensorRun(bool button_state){

if(button_state == false){
    adc_select_input(4);
    uint16_t adc_temp = adc_read();
    float temp = 27 – ((adc_temp * conversion_factor)-0.706)/0.001721;
    printf(“Temp = %f C, voltage = %f V\n”, temp, (adc_temp * conversion_factor));
    return 0;
    }
return 1;
}

Do tej pory w naszych programach korzystaliśmy z pojedynczej, głównej funkcji main oraz obszaru ponad nią, gdzie umieszczaliśmy deklaracje zmiennych, stałych czy też dołączone biblioteki. Tym razem spróbujemy przygotować też własną funkcję, którą będziemy mogli umieścić wewnątrz głównego main, tak jak wiele spośród innych poleceń. Test przeprowadzimy na przygotowanym już programie, który omawiamy w tym artykule. W przykładzie spróbujemy wyodrębnić bloki kodu odpowiedzialne za obsługę fotorezystora i wewnętrznego czujnika temperatury i zamknąć je w ramy pojedynczej funkcji.

Przyjrzyjmy się przykładowi powyżej. Jak możecie zauważyć, główna funkcja main, a w szczególności kod umieszczony wewnątrz pętli while uległ dość znacznemu odchudzeniu. Obecnie składa się on już tylko z pięciu instrukcji, a kod, który jeszcze przed momentem się w niej znajdował, umieszczony jest teraz poniżej w dość tajemniczo opisanych blokach. Są to właśnie nasze własne funkcje nazwane PhotoresRun oraz TempSensorRun.

Podczas wykonywania kodu z pętli while mikrokontroler napotka polecenie PhotoresRun();, wówczas przeskoczy on do kodu, który opisany został w tej funkcji. Funkcje definiujemy jako – typ nazwa(opcjonalne operandy). W tym przypadku jest to void PhotoresRun(), czyli funkcja typu void o nazwie PhotoresRun, do której nie przekazujemy żadnych zmiennych, ponieważ nawias jest pusty. Typ funkcji określa też to, co dana funkcja nam zwraca. Gdy korzystamy z dla przykładu adc_read() funkcja zwraca nam cyfrową reprezentację sygnału odczytanego z jednego ze źródeł przetwornika ADC. Jeśli chcemy, aby funkcja nie zwracała nam żadnej wartości, stosujemy właśnie typ void. Kod, który wykonany zostanie w ramach PhotoresRun, umieszczony jest między nawiasami klamrowymi i jest to kopia programu, który we wcześniejszych przykładach obsługiwał fotorezystor.

Drugą funkcją jest TempSensorRun, którego deklaracja wygląda już nieco bardziej skomplikowanie – uint8_t TempSensorRun(bool button_state). Jest to funkcja zwracająca daną typu uint8_t o nazwie TempSensorRun, do której przekazać możemy zmienną typu bool nazywaną dalej button_state. We wnętrzu funkcji umieszczony został warunek obsługujący wciśnięcie przycisku. Jeśli przekazana do funkcji zmienna przyjmie wartość false, innymi słowy, przycisk zostanie wciśnięty, wówczas wykona się kod, który już wcześniej stosowaliśmy do obsługi wewnętrznego czujnika temperatury. Poza tym w tej funkcji możecie zobaczyć też słowa kluczowe return. To właśnie dzięki nim funkcja może „coś” zwrócić. Jeśli przeskoczymy na moment do głównego bloku main, zobaczymy, że utworzona została zmienna typu uint8_t o nazwie return_value, do której przypisana zostanie wartość zwracana przez TempSensorRun. Jeśli przycisk będzie wciśnięty, funkcja wykona obsługujący go kod, wysyłając poprzez USB dane o temperaturze i napięciu, ale poza tym zwróci też wartość zera, która przypisana zostanie do return_value. W przypadku, gdy przycisk nie będzie wciśnięty i na wyprowadzeniu RPI panować będzie stan wysoki, funkcja zwróci wartość jeden, która również przypisana zostanie do zmiennej.

W ten właśnie sposób możemy tworzyć własne funkcje, przy czym pamiętać trzeba też o ich prototypach, które należy umieścić powyżej głównego bloku main. Jest to nic innego jak krótkie powtórzenie deklaracji funkcji z jej typem, nazwą i opcjonalnymi operandami.

Dane przesyłane przez RPI.

Po uruchomieniu kodu na ekranie komputera można zobaczyć przesyłane przez Raspberry Pi Pico W dane z fotorezystora oraz wartość przypisaną do return_value. Gdy przycisk pozostaje swobodny, do zmiennej przypisana jest jedynka. Układ wykonuje funkcję TempSensorRun, ale ze względu na stan przycisku od razu zwraca liczbę jeden i wraca do głównej pętli. Sytuacja zmienia się, gdy wciśniemy przycisk. Wówczas poza danymi z wewnętrznego czujnika temperatury zobaczymy zmianę wartości return_value na zero.

Przygotowywanie własnych funkcji jest niezwykle przydatne, zwłaszcza w bardziej rozbudowanych kodach. Dzięki nim główny main ulega znacznemu odchudzeniu, przez co program jest czytelniejszy. Chcąc przyjrzeć się jakiejś konkretnej funkcji, wystarczy, że odszukamy jej deklarację, a następnie wrócimy do głównej pętli. Własne funkcje można też umieszczać w innych plikach, przez co kody stają się jeszcze bardziej optymalne, ale tym zagadnieniem zajmiemy się innym razem.

Kilka słów na koniec…

W dzisiejszym materiale poznaliśmy kilka teoretycznych zagadnień związanych z optymalizacją kodu. Poza tym wspomniałem też o dobrym nawyku wracania do wcześniej napisanych programów. Na koniec pokazałem wam, w jaki sposób budować można własne funkcje, a wiedza ta przyda nam się w przyszłości. Mimo bardziej teoretycznego charakteru tego materiału, poznaliście też sposób obsługi wewnętrznego czujnika temperatury w RP2040. W projektach wykorzystaliśmy też operatory logiczne oraz przesunięcia bitowe. W kolejnym materiale przyjrzymy się zagadnieniom związanym z przerwaniami w mikrokontrolerze.

Ź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

Jak oceniasz ten wpis blogowy?

Kliknij gwiazdkę, aby go ocenić!

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

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:

2 Responses

  1. Kurs jak dla mnie super.
    W przystępny sposób poznaję środowisko i funkcjonalności RP2040.
    VScode jest zainstalowany z git pytanie czy można go deaktywować jeżeli się nie używa a jeżeli nie to jak uaktualnić zmiany bo zaczyna ich przybywać w PICO-EXAMPLES.
    Może udało by się pokazać jak utworzyć nowy folder poza PICO-EXAMPLES i co trzeba skonfigurować żeby działało poprawnie.

    Pozdrawiam 😉

    1. Tworzenie własnych folderów pod Pico to nieco większy temat, bo są nawet specjalne programy, które zajmują się tylko tym, ale prawdopodobnie w ramach tej serii powstanie też bonusowy artykuł tylko o tym zagadnieniu.

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 i Warunkom użytkowania.