Kurs Raspberry Pi Pico – #11 – uruchomienie cyfrowego czujnika światła, czyli I2C

Czas czytania: 15 min.

W poprzednim materiale poruszyliśmy całkiem sporo tematów związanych z językiem C. Przyjrzeliśmy się tablicom, zbudowaliśmy strukturę oraz nauczyliśmy się przekazywać dane do funkcji poprzez wskaźnik. Poza tym uruchomiliśmy projekt sygnalizatora świetlnego opartego na maszynie stanów. W tym poradniku skorzystamy z dołączonego do zestawu elementów cyfrowego czujnika światła. Postaramy się go uruchomić i sprawdzimy, czy jest on dobrą konkurencją dla poznanego już wcześniej fotorezystora.

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.

Dzisiejszy bohater, czyli cyfrowy czujnik światła

Moduł DFRobot Gravity VEML7700.

W zestawie elementów dedykowanego dla tego kursu znaleźć możecie cyfrowy czujnik światła VEML7700 należący do serii Gravity brytyjskiego producenta DFRobot. Konstrukcja ta ma formę niewielkiego modułu, który nadaje się wręcz idealnie do wszelakiego prototypowania. 

Na płytce znajdziemy 4 pinowe złącze do zasilania napięciem od 3,3V do 5V oraz komunikacji za pomocą interfejsu I2C. Poza tym na płytce znalazło się też kilka elementów elektronicznych – rezystory, kondensatory, stabilizator napięcia oraz dwa tranzystory. Zdecydowanie najciekawszym elementem modułu jest sam sensor VEML7700 zamknięty w przezroczystej obudowie, tak aby światło mogło padać na rdzeń czujnika.

Mikroskopowe zdjęcie układu VEML7700.

Dzięki tak specyficznej obudowie można dość łatwo zajrzeć do wnętrza układu i przyjrzeć się bliżej jego konstrukcji. Centralną część krzemowego rdzenia zajmuje matryca szesnastu fotodiod pokrytych kolorowymi filtrami, które polaryzują światło. Zależnie od ilości padających na fotoelementy fotonów zmienia się wartość generowanego przez nie napięcia, które po przetworzeniu przez analogowo-cyfrowy przetwornik może zostać odczytane przez dowolny mikrokontroler. Cyfrowa wartość dostarczana przez czujnik jest już praktycznie gotową zmienną wartości natężenia światła w jednostce Lux. Dzięki temu obsługa VEML7700 jest stosunkowo prosta i nie powinna sprawić większych problemów. Jak już wspomniałem, z czujnikiem komunikujemy się za pomocą interfejsu I2C, którego specyfikę również wypadałoby krótko wyjaśnić. 

Koncepcja komunikacji I2C.

Interfejs I2C to dwu przewodowa metoda, która może łączyć ze sobą dwa lub większą ilość elektronicznych modułów lub urządzeń. Komunikacja tego typu bazuje na pojedynczym urządzeniu nadrzędnym, tak zwanym masterze, do którego podłączonych może być nawet kilkanaście modułów podrzędnych, określanych jako slave. Do wymiany danych wykorzystywane są przewody SCL – służy do synchronizacji między urządzeniami oraz SDA – linia danych, którą przesyłane są kolejne bity informacji. Interfejs I2C nie należy do najszybszych metod transmisji danych, ale dzięki swojej uniwersalności stosowany jest dość powszechnie. Dzięki niemu możemy podłączyć do mikrokontrolera nie tylko różnego rodzaju czujniki, ale i ekspandery wyprowadzeń, pozwalające zwiększyć fizyczną ilość pinów jednostki centralnej.

Zadaniem mastera w komunikacji I2C jest kontrola i zarządzanie przepływem informacji. Jako że każde z urządzeń podłączonych do magistrali ma swój własny i unikatowy adres, możliwa jest bezkolizyjna komunikacja, w której wydane przez nadrzędny układ polecenie trafia do konkretnego slava. W taki właśnie sposób spróbujemy skomunikować się z cyfrowym czujnikiem światła, wysyłając konkretne komendy pod adres czujnika.

Podłączenie czujnika do Raspberry Pi Pico

Obwód z podłączonym czujnikiem światła.

Jednak, aby zacząć korzystać z interfejsu I2C, musimy podłączyć czujnik światła do Raspberry Pi Pico. W zestawie z modułem znaleźć można przewód, z jednej strony zakończony specjalnym wtykiem PH4, z drugiej natomiast umieszczono klasyczne żeńskie gniazdo goldpin. Moglibyśmy skorzystać z tego przewodu, ale znacznie lepszym rozwiązaniem jest użycie łącznika, którego jedno z zakończeń wyposażono w męskie złącze goldpin, dzięki niemu znacznie łatwiej jest wpiąć się w płytkę stykową. Poza tym na końcówkach tego przewodu umieszczone są opisy konkretnych sygnałów. Czerwony przewód VCC podłączmy do dodatniej magistrali 3,3V, czarny – GND łączymy z masą. Końcówki sygnałowe SDA i SCL podłączamy kolejno pod piny GP6 i GP7, fizycznie są to wyprowadzenia numer 9 i 10. Jeśli spojrzycie na pinout RPI Pico, zobaczycie, że między innymi w tym miejscu wyprowadzone zostały sygnały magistrali I2C1. Co warto zauważyć, RP2040 dysponuje dwoma tego typu interfejsami I2C0 i I2C1, w naszym projekcie skorzystamy z drugiej magistrali danych.

Gdy czujnik jest już podłączony, możemy przejść do jego uruchamiania. Jednak zanim zaczniemy odczytywać cyfrowe wartości natężenia światła, musimy poznać adres czujnika. Jak już wspominałem interfejs I2C pozwala podłączyć do mikrokontrolera znacznie więcej niż jeden czujnik, a ich obsługa bazuje na unikalnym adresie każdego z urządzeń. Informacje o adresie elektronicznych modułów znaleźć można w wielu miejscach. Zdarza się, że już na samym laminacie znajdziemy nadrukowane oznaczenie ADDR:0x01, wiemy wówczas, że adres czujnika to właśnie 0x01. Poza tym wiele sensorów pozwala w niewielkim stopniu manipulować adresami, zmieniając ułożenie zworek (zazwyczaj rezystorów o wartości 0Ω) i tym sposobem zmieniać adres czujnika. Jednak najbezpieczniej jest zawsze zajrzeć do dokumentacji danego modułu.

Dokumentacja czujnika VEML7700. (https://www.vishay.com/docs/84286/veml7700.pdf)

Na szóstej stronie dostępnej w internecie nocie katalogowej sensora VEML7700 znaleźć możemy konkretne informacje o adresie urządzenia. Z sekcji Device Address dowiadujemy się, że czujnik w trybie slave ma stały, 7 bitowy adres o wartości 0x10 i tyle nam na ten moment wystarczy, wszakże w kolejnych programach korzystać będziemy właśnie z trybu slave, gdzie masterem będzie Raspberry Pi Pico.

W przypadku czujnika VEML7700, znalezienie informacji o adresie jest dość proste, nie zawsze jednak tak jest. Musimy zdawać sobie sprawę, że w przyszłości możemy natknąć się na moduły bądź czujniki, których adres będzie nieznany. W takim przypadku nie będziemy zgadywać wartości, pod którą zgłosiło się urządzenie, a skorzystamy z prostego programu, który pozwala zidentyfikować wszystkie podłączone do magistrali i2C sprzęty. Tego typu kody nazywamy „skanerami I2C”, ich działanie jest dość proste. Program odpytuje kolejne adresy na magistrali i zależnie od odpowiedzi stwierdza, czy pod tym adresem widnieje jakieś urządzenie. Dzięki temu nie musimy znać adresu urządzenia, bo poda nam je uruchomiony program. W kolejnym rozdziale uruchomimy tego typu kod i sprawdzimy, czy cyfrowy czujnik światła rzeczywiście ma adres 0x10. 

Skanowanie magistrali I2C

Tak jak wspomniałem – pierwszym programem, który dzisiaj uruchomimy, będzie skaner I2C, dzięki któremu sprawdzimy, czy adres sensora VEML7700 zgodny jest z wartością w dokumentacji. Kod ten pozwoli nam po raz pierwszy skorzystać z magistrali I2C. Tak więc, gdy czujnik jest już podłączony do RPI Pico, zgodnie z wcześniejszym opisem możemy przejść do projektu, który nazwałem I2C_scanner.

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

// I2C definitions
#define I2C_PORT i2c1
#define SDA_PIN 6
#define SCL_PIN 7

void i2c_scan(); // prototype of the scanning function

int main() {
stdio_init_all();

// I2C initialization
i2c_init(I2C_PORT, 100 * 1000);
gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(SDA_PIN);
gpio_pull_up(SCL_PIN);

while(true) {
i2c_scan();
sleep_ms(3000);
}

}

void i2c_scan() {
printf("Scanning I2C bus...\n");

// bus scanning loop
for (uint8_t addr = 1; addr = 0) {
printf("Found I2C device at address 0x%02X\n", addr);
}
}
printf("Scanning done.\n");
}

W pierwszej fazie programu, jak to zwykle bywa, znalazły się biblioteki oraz odpowiednie definicje. Jako że będziemy korzystać z magistrali I2C, musimy dołączyć bibliotekę „hardweare/i2c.h”, w pliku tym znalazły się wszystkie podstawowe polecenia pozwalające korzystać z tego rodzaju transmisji danych. Dzięki umieszczonym w dalszej części definicjom będziemy mogli korzystać z trzech makr: I2C_PORT, SDA_PIN oraz SCL_PIN. Jak łatwo się domyślić dwie ostatnie definicje odnoszą się do numerów wyprowadzeń, z których korzystać będzie cyfrowy czujnik światła, czyli GP6 i GP7. Natomiast I2C_PORT będzie od tej chwili równoznaczne z i2c1. Jak już wspominałem, Raspberry Pi Pico W dysponuje dwiema magistralami w standardzie I2C: I2C0 i I2C1. Czujnik, który w dalszej części artykułu uruchamiamy, podłączyliśmy do wyprowadzeń odpowiadających drugiej szynie danych, dlatego w kodzie umieszczamy i2c1.

Kolejnym krokiem jest dołączenie prototypu funkcji i2c_scan. To właśnie ona będzie główną częścią kodu, ale dla odmiany umieściłem ją na samym końcu programu, dlatego w takim przypadku, jak już kiedyś wspominałem dołączamy prototyp. Funkcja ta ma typ void, tak więc nie zwraca danych, dodatkowo tez nie przekazujemy do niej żadnych argumentów.

Wewnątrz głównej funkcji main inicjalizujemy standardowe polecenia biblioteki stdio.h, jak i sam interfejs I2C. Służy do tego instrukcja i2c_init, w której argumentach przekazujemy odpowiedni port, dzięki definicji I2C_PORT jest to i2c1, a także tak zwany boudrate, będący w tym przypadku wartością częstotliwości, z jaką przesyłane będą dane. I2C wspiera kilka szybkości transmisji danych, gdzie podstawowymi są 100kHz (standard), 400kHz (fast mode), 1MHz (fast mode plus), 3,4MHz (high speed mode) oraz 5MHz (ultra fast mode). W naszym projekcie skorzystamy ze standardowej wartości 100kHz i taką właśnie liczbę musimy podać w argumencie funkcji i2c_init. Jednak zamiast wpisać po prostu 100000 umieścimy tam działanie 100*1000, którego wynikiem jest właśnie wspomniana wartość. Taka konstrukcja nie ma większego uzasadnienia, chciałbym jedynie przypomnieć, że w języku C z łatwością możemy tworzyć tego typu konstrukcje, gdzie w argumencie funkcji podajemy działanie matematyczne, bądź też nawet odwołanie do jeszcze innego polecenia. W kolejnych krokach korzystamy ze znanych już poleceń inicjalizujących porty RPI Pico, dzięki GPIO_FUNC_I2C ustawiamy wyprowadzenia w tryb portów magistrali dwuprzewodowej, a następnie podciągamy je do zasilania funkcją gpio_pull_up. Jest to niezbędne, ponieważ transmisja I2C bazuje na sygnałach logicznego zera. Innymi słowy, pojawienie się na magistrali sygnału niskiego oznacza rozpoczęcie transmisji danych i domyślnie, gdy urządzenia nie wymieniają między sobą informacji, sygnał SDA jest w stanie wysokim.

W głównej pętli while znalazły się tylko dwa polecenia. Dzięki nim co trzy sekundy wywołujemy funkcje i2c_scan, która skanuje magistralę danych w poszukiwaniu podłączonych do niej urządzeń.

Funkcję tą zaczynamy od klasycznego już printf, który wyświetli w monitorze portu szeregowego informację o rozpoczętym skanowaniu. Następnie skorzystamy ze znanej już pętli for, tak aby sprawdzić każdy adres, pod którym zgłosić może się podłączone urządzenie. W jej argumentach tworzymy zmienną addr odpowiadającą właśnie wartości adresu o początkowym stanie 1, bo co wydaje mi się intuicyjne, skanowanie zacząć chcemy właśnie od pierwszego adresu. Dalej określamy, że pętla ma się wykonywać do czasu, gdy addr będzie mniejsze od wartości 0x7F, w systemie dziesiętnym jest to 127, bo właśnie tyle pojedynczych urządzeń możemy podłączyć do jednej magistrali I2C. Można by tutaj zastosować oczywiście konstrukcję addr<127, jednak jak pewnie zauważyliście, do tej pory wartość adresu zawsze zapisywałem jako hex i niech tak pozostanie, bo jest to ogólnie przyjęty standard w elektronice.  Ostatnim już argumentem jest polecenie, dzięki któremu przy każdym przebiegu pętli addr zwiększy swą wartość o jeden.

Wewnątrz pętli for tworzymy zmienną rxdata, do której zapisywać będziemy odebrane z podłączonego urządzenia dane. Z naszej perspektywy jednak zmienna ta nie jest ważna, a jedynie wymagana przez polecenie, które za chwilę opiszę. W programie nie oczekujemy odebrania żadnych konkretnych danych, a jedynie sprawdzenia co podłączone jest do wyprowadzeń Raspberry Pi Pico, dlatego aktualnie nie musimy poświęcać tej zmiennej większej uwagi. Ważna za to jest kolejna zmienna typu int8_t (liczba ze znakiem, będzie to ważne później), którą generujemy, czyli result. Od razu przypisujemy do niej wartość, którą zwróci nam polecenie i2c_read_blocking. Będzie to liczba odczytanych z potencjalnego urządzenia bajtów, dzięki temu dość łatwo będziemy mogli określić, czy pod danym adresem jest jakiś sprzęt. Jeśli odczytamy cokolwiek, również zero, oznaczać to będzie, że pod tym adresem zgłosił się konkretny moduł i analogiczne, gdy funkcja nie zwróci żadnej wartości oznaczać będzie, że dany adres jest pusty. Wyjaśnieniu wymaga też określenie funkcja blokująca. Oznacza to, że, gdy jest ona wykonywana, mikrokontroler niejako wstrzymuje swoje działanie i czeka na dopowiedz z drugiej strony.

Funkcja i2c_read_blocking, oczekuje od nas szeregu argumentów, które musimy oczywiście podać. Pierwszym z nich jest wybrany przez nas port, następnie adres, z którym chcemy nawiązać kontakt. Podajemy tutaj wartość zmiennej addr, który tak jak wspominałem, zwiększa się o jeden przy każdym przebiegu pętli. Następnie musimy podać miejsce, pod którym zapisane mają zostać potencjalnie odebrane dane, korzystamy tutaj ze wskaźnika, na utworzoną chwile wcześniej zmienną rxdata. „1” określa ilość bajtów, które chcemy odebrać, w tym przypadku nie jest to tak naprawdę ważne, dlatego zakładamy, że oczekujemy tylko jednego bajta. Ostatnim argumentem jest logiczny fałsz, dzięki niemu funkcja po każdorazowej wymianie danych przywróci magistralę I2C do domyślnego stanu. Jeśli umieścilibyśmy wewnątrz funkcji wartość true, to po wymianie danych magistrala pozostawałaby w stanie gotowym do transmisji. Innymi słowy, transmisja nie byłaby skończona, a mikrokontroler byłby nadal gotowy do komunikacji.

W dalszej części dzięki funkcji warunkowej if sprawdzamy wartość zmiennej result, dla przypomnienia przechowuje ona liczbę odpowiadającą ilości odebranych danych. Jeśli jest ona większa bądź równa zero oznacza to, że pod danym adresem podłączone jest jakieś urządzenie i można z nim wymieniać informacje. Dość nielogiczne może wydawać się, że odebranie zerowej ilości danych oznacza aktywne urządzenia, ale tak właśnie jest. Gdy pod danym adresem nie ma niczego, do result nie zostanie przypisane „nic”, przypisana zostanie wówczas specyficzna wartość będąca typem enumeracyjnym. Czyli specjalną wartością przewidzianą dla tego typu przypadków. W dalszej części sprawdzimy co dokładnie przypisywane jest do zmiennej result. Tak więc, gdy result ma zgodną z warunkiem wartość, wykonać możemy polecenie printf, które wyrzuci nam na ekran komputera aktualną wartość addr, czyli adresu, który chwile wcześniej sprawdzaliśmy za pomocą i2c_read_blocking.

Spojrzyjmy teraz na kod jako całość. Początkowo inicjalizujemy magistralę I2C oraz podciągamy do napięcia zasilania wykorzystywane fizycznie wyprowadzenia. Wewnątrz pętli while wywołujemy funkcje i2c_scan, której zadaniem jest sprawdzić, czy do dwuprzewodowej magistrali podłączone są jakieś urządzenia. Bazuje ona na wykonywanej 127 razy pętli for, która przy każdym przebiegu próbuje odczytać dane spod adresu zapisanego w addr. Jeśli odebrane zostanie cokolwiek lub zero, które też jest poprawną wartością, wysyłamy na ekran komputera aktualną wartość spod addr.

Biblioteki dodane w pliku CMakeLists.txt.

W tym programie korzystamy z biblioteki „hardweare/i2c.h”, dlatego pamiętajcie, aby dodać ją w pliku CMakeLists.txt.

Odczytany przez program adres czujnika.

Po uruchomieniu programu i otwarciu monitora portu szeregowego możemy zobaczyć pojawiającą się co trzy sekundy informację, że do magistrali I2C podłączone zostało urządzenie, którego adres wynosi 0x10. Tak więc jest to wartość zgodna z dokumentacją. Kod ten poza odczytaniem adrsu pozwala nam stwierdzić, że czujnik jest poprawnie podłączony i możemy spróbować nawiązać z nim nieco bardziej złożoną wymianę danych, jednak zanim do tego przejdziemy, przyjrzyjmy się dokładniej zmiennej result i przypisywanym jej wartością.

Co w rzeczywistości przechowuje zmienna result?

Rozwinięcie funkcji, po najechaniu na nią kursorem myszy.

Tak jak już wcześniej wspomniałem, w zmiennej result przechowywana jest liczba odebranych bajtów z potencjalnie podłączonego urządzenia, przy czym zerowa wartość również jest poprawnym odczytem. Może wydawać się to nieco dziwne i prowokować do pytania, co w takim razie przechowywane jest w zmiennej result, gdy pod konkretnym adresem nie zgłosi się żadne urządzenie?

Odpowiedzią to PICO_ERROR_GENERIC. Jeśli w Visual Studio Code najedziemy kursorem myszy na zapisaną funkcję, po chwili pojawi się niewielkie okno, w którym znajdziemy skrótowy opis tegoż polecenia. Tak też jest w tym przypadku, poza informacją o argumentach funkcji znajdziemy tutaj opis zwracanych wartości. Jest to wspominana liczba odczytanych bajtów danych oraz tajemnicze PICO_ERROR_GENERIC, gdy adres, z którym próbujemy się skomunikować jest nieosiągalny. W rzeczywistości jest to wartość typu enumeracyjnego o nazwie pico_error_codes.

Enum pico_error_codes.

Więcej informacji o tym enumie znaleźć możemy w pliku error.h, który jest częścią bazowych plików dołączanych przez środowisko automatycznie do każdego nowego projektu. Jak widać na załączonym obrazku, kodów błędów jest całkiem sporo i sądzić możemy, że w przypadku nieodczytania przez funkcję skojarzoną z interfejsem I2C do zmiennej result przypisana zostanie wartość -2. Zmodyfikujmy nieco wcześniej napisaną funkcję i2c_scan i sprawdźmy to.

void i2c_scan() {
printf("Scanning I2C bus...\n");

for (uint8_t addr = 1; addr < 0x7F; addr++) {
uint8_t rxdata;
int8_t result = i2c_read_blocking(I2C_PORT, addr, &rxdata, 1, false);

printf("address = 0x%02X, result = %d\n", addr, result);
}
 printf("Scanning done.\n");
}

W nowej formie funkcja i2c_scan będzie wysyłać na monitor portu szeregowego wartości spod zmiennej result przy każdym ze 127 adresów. Tym samym sprawdzimy, czy w przypadku braku urządzenia przypisane zostanie do niej -2, a także ile bajtów danych jest odczytywane z podłączonego do Pico czujnika pod adresem 0x10.

Widoczne w monitorze portu szeregowego adresy.

Po uruchomieniu kodu otrzymujemy listę wszystkich adresów, które skanuje mikrokontroler, wraz z wartościami spod result. Rzeczywiście do zmiennej przypisywana jest wartość odpowiadająca PICO_ERROR_GENERIC, a gdy procesor sprawdza adres, pod którym podłączony jest nasz czujnik światła, zwracaną wartością jest jeden.

Uruchomienie cyfrowego czujnika światła

Pierwsze kroki z I2C za nami, dlatego możemy przejść do głównego tematu dzisiejszego materiału, czyli uruchamiania cyfrowego czujnika światła VEML7700. Do tego celu przygotowałem nowy projekt o nazwie veml7700_test. W kodzie, który już za moment poznacie, spróbujemy nawiązać komunikację z modułem i odczytać wartość natężenia światła.

Tabela rejestrów czujnika VEML7700. (https://www.vishay.com/docs/84286/veml7700.pdf)

Jednak, aby uzyskać wartość natężenia światła, musimy zajrzeć na moment do dokumentacji. Znajdziemy w niej tabele, w której opisane zostały wszystkie dostępne rejestry, z którymi możemy się skomunikować. To właśnie na rejestrach bazuje komunikacja I2C, z poziomu mikrokontrolera będziemy wybierać konkretny z nich i zapisywać lub odczytywać dane w nim zawarte. Jak możecie zauważyć, dostępnych rejestrów jest osiem, ale na początek skupimy się tylko na dwóch. Pierwszym z nich będzie ALS_CONF_0, który służy do konfiguracji czujnika, już za moment przyjrzymy się mu dokładnie, ponieważ pierwszym krokiem w kodzie będzie właśnie wpisanie do niego odpowiednich bitów, tak aby zainicjalizować czujnik. Drugim rejestrem, który nas interesuje jest ALS, bo to właśnie w nim przechowywana jest cyfrowa wartość odpowiadająca natężeniu światła. Gdy wykonamy już inicjalizacje, przejdziemy właśnie do tego rejestru i odczytamy zapisane w nim dane.

Struktura rejestru ALS_CONF_0. (https://www.vishay.com/docs/84286/veml7700.pdf)

Tak jak wspomniałem – pierwszym krokiem, który musimy wykonać, aby uruchomić czujnik, będzie odpowiednie skonfigurowanie rejestru ALS_CONF_0. Składa się on z 16 bitów i analizę rozpocznijmy od najmłodszego z nich, czyli od dołu tabeli. Pierwszy bit, zależnie od stanu uruchamia (0) lub wyłącza (1) pomiar światła. Pod drugim bitem ukryte zostało ustawienie wewnętrznych przerwań czujnika, tych nie musimy uruchamiać, dlatego bit ten ustawimy na zero. Kolejne dwa bity są zarezerwowane i powinny być ustawione na zero. Następnymi danymi są ustawienia dostępu, te ustawiamy na 1, zerując oba bity, a kolejnymi czterema bitami wybieramy czas trwania pojedynczego pomiaru. Wybierzemy tutaj wartość mniej więcej ze środka stawki, czyli 100ms. Dalej trafiamy na kolejne zarezerwowane dane oraz decyzję co chcemy zrobić z wartością pomiaru. Możemy pomnożyć ją przez odpowiednio 1, 2, 1/8 i 1/4. W naszym przypadku nie chcemy, aby dane były w jakikolwiek sposób modyfikowane, dlatego oba bity ustawimy na zero. Ostatnie trzy bity tego rejestru również są zarezerwowane i nie powinniśmy ich ustawiać.

Tak więc konfiguracja rejestru w pierwszym programie będzie składać się z samych zer i taką wartość właśnie ustawimy:

  • ALS_SD – 0,
  • ALS_INT_EN – 0,
  • ALS_PERS – 00,
  • ALS_IT – 0000,
  • ALS_GAIN – 00.

Gdy wiemy już pokrótce co musimy zrobić, możemy przejść do kodu, który to zrealizuje. Program obsługujący czujnik podzielimy niejako na dwie osobne funkcje, pierwszą z nich będzie inicjalizacja, w której wywalmy dane do rejestru ALS_CONF_0. Drugą funkcją odczytamy rejestr ALS, w którym ukryte są dane odpowiadające natężeniu światłą i wartość tą wyślemy na ekran komputera.

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

#define I2C_PORT i2c1
#define SDA_PIN 6
#define SCL_PIN 7
#define VEML7700_ADDR 0x10

void veml7700_init() {
uint8_t init_cmd[] = {0x00, 0x00};
i2c_write_blocking(I2C_PORT, VEML7700_ADDR, init_cmd, 2, false);
}

uint16_t veml7700_read_light() {
uint8_t reg = 0x04; // adres rejestru pomiaru
uint8_t buffer[2];
i2c_write_blocking(I2C_PORT, VEML7700_ADDR, &reg, 1, true);
i2c_read_blocking(I2C_PORT, VEML7700_ADDR, buffer, 2, false);
return (buffer[1] << 8) | buffer[0];
}

int main() {
stdio_init_all();
i2c_init(I2C_PORT, 100 * 1000); // prędkość I2C 100kHz

gpio_set_function(SDA_PIN, GPIO_FUNC_I2C);
gpio_set_function(SCL_PIN, GPIO_FUNC_I2C);
gpio_pull_up(SDA_PIN);
gpio_pull_up(SCL_PIN);

veml7700_init();

while (true) {
uint16_t light = veml7700_read_light();
printf("Light value: %u\n", light);
sleep_ms(1000);
}

}

Tak jak przy pierwszych próbach uruchomienia magistrali I2C, tak i tym razem program zaczynamy od dołączenia odpowiednich bibliotek oraz definicji. Analogicznie znalazły się tutaj opisy portów, ale też definicja adresu naszego czujnika, tak jak odczytać możemy z dokumentacji, ale też sprawdzić empirycznie jest to wartość 0x01, która od tej pory przypisana jest do VEML7700_ADDR.

W dalszej części znalazły się dwie funkcje obsługujące czujnik światła – inicjalizująca oraz odczytująca wartość natężenia światła. Zajmiemy się nimi za chwilę w momencie, gdy polecenia te będą wywoływane. 

Początek funkcji main, powinien być już nam znany. Tak jak w poprzednim projekcie uruchamiamy interfejs I2C oraz ustawiamy odpowiednio wyprowadzenia mikrokontrolera. Po tym zabiegu wywołujemy funkcję inicjalizującą moduł VEML7700. Jej konstrukcja jest dość prosta i składa się tylko z dwóch poleceń. Poza tym inicjalizacja ma typ void, tak więc nie zwraca żadnych danych. Jak pamiętacie, w czasie inicjalizacji musimy wpisać odpowiednie dane do rejestru ALS_CONF_0. W tym celu tworzymy dwuelementową tablicę uint8_t o nazwie init_cmd. W jej wnętrzu zapisujemy od razu dwa zerowe bajty. Być może zastanawiacie się, dlaczego tworzymy tutaj tablicę z dwoma elementami, zamiast pojedynczą zmienną uint16_t? Odpowiedź na to pytanie jest dość prosta i wynika ze specyfiki interfejsu I2C, który wysyła dane w jedno bajtowych paczkach. Dlatego informacje, które będziemy przesyłać, również należy podzielić na jedno bajtowe elementy. Drugą funkcją inicjalizacji jest wysłanie danych, charakterystyka polecenia i2c_write_blocking jest niezwykle podobna do wcześniej omawianej funkcji odczytującej dane. W argumentach podajemy odpowiedni port, docelowy adres, miejsce, gdzie zapisane są dane, które będziemy wysyłać, ilość bajtów, które należy przesłać oraz logicznie zdefiniowany koniec transmisji po wykonaniu tej instrukcji. Tym sposobem właśnie pod adres 0x10 wyślemy dwa zerowe bajty, w ten sposób inicjalizując czujnik.

Wróćmy jednak do miejsca, z którego wywołana została funkcja inicjalizująca. Po jej wykonaniu przechodzimy do nieskończonej pętli while, w której łatwo zauważyć będziemy co sekundę odczytywać wartość natężenia światła i przypisywać ją do zmiennej uint16_t o nazwie light. Tutaj też skaczemy do odpowiedniej funkcji, której działanie opisze poniżej.

Wewnątrz bloku kodu, który odczytuje wartość zapisaną w rejestrze czujnika o nazwie ALS, znalazło się kilka funkcji. Początkowo tworzymy dwie zmienne, które potrzebne będą w trakcie komunikacji – reg, która przechowuje adres rejestru ASL, czyli 0x04. Drugą zmienną jest w rzeczywistości dwuelementowa tablica buffer, w której umieścimy odebraną z czujnika wartość. Podobnie jak wcześniej rejestr ASL czujnika przechowuje dwa bajty danych, jednak ze względu na specyfikę I2C informację musimy przesyłać w jedno bajtowych paczkach, dlatego też dane zapiszemy w tablicy. Gdy zmienne są gotowe, możemy przejść do samej komunikacji. W pierwszym kroku wysyłamy wskaźnik na zmienną reg, tym samym informując czujnik, że będziemy odczytywać dane z rejestru ASL. Tutaj też warto zauważyć, że ostatnim argumentem funkcji jest true, ponieważ nie jest to koniec transmisji. Zanim przejdziemy do kolejnego polecenia, chciałbym zwrócić uwagę na pewien szczegół, który być może zauważyliście. W funkcji inicjalizującej czujnik nie skorzystaliśmy ze wskaźnika, mimo że polecenie i2c_write_blocking tego właśnie oczekuje. Różnicą między init_cmd a reg jest fakt, że pierwsza z konstrukcji jest tablicą, a druga tylko zmienną. Wykorzystujemy tutaj pewną unikalną cechę tablic, które same w sobie są też wskaźnikami i pokazują na pierwszy zapisany w nich element. Właśnie dlatego przy funkcji inicjalizującej mogliśmy w argumencie funkcji podać tablicę w sposób bezpośredni, a w przypadku zmiennej reg musimy dodatkowo skorzystać z operatora wyłuskania adresu.

Gdy czujnik otrzymał już adres rejestru, z którego odczytywać będziemy dane, możemy przejść do polecenia i2c_read_blocking. Argumenty tej funkcji są już nam znane, dlatego nie wymagają większego wyjaśnienia. Warto jedynie zauważyć, że korzystamy tutaj z tablicy buffer, w której zapisane zostaną odczytane z sensora dane.

Przekształcenie tablicy na dane typu uint16_t.

Jak widzicie funkcja veml7700_read_light zwraca dane typu uint16_t, które wewnątrz funkcji while przypisywane są do zmiennej light. Wartość natężenia światła zwracamy dzięki poleceniu return, ale nie tak po prostu. Jak widać wartość odczytana, przechowywana w buffer ma postać dwóch zmiennych uint8_t zapisanych w tablicy i żeby odesłać je w formie jednej zmiennej musimy dokonać pewnej modyfikacji. Korzystamy tutaj ze sprytnej konstrukcji, która przesuwa o osiem miejsc w lewo drugi element tablicy i dzięki logicznemu OR (|) łączy ją z pierwszym elementem. W ten sposób właśnie uzyskujemy pojedynczą dwu bajtową wartość przypisywaną do zmiennej light.

Zanim jeszcze przejdziemy do samego uruchomienia przygotowanego kodu, chciałbym zwrócić waszą uwagę na pewien szczegół. Zauważcie, że w funkcji inicjalizującej czujnik nie podajemy adresu rejestru ALS_CONF_0 i niejako wysyłamy dwa zerowe bajty w nicość, podając tylko adres samego czujnika, czyli 0x10. Może to wyglądać dość dziwacznie, zwłaszcza że w funkcji, która odczytuje wartość światła, podajemy wcześniej w argumencie funkcji wartość 0x04, która jest adresem rejestru ASL. W wielu urządzeniach wyposażonych w interfejs I2C istnieje pewna funkcjonalność określana jako wysyłanie danych bez specyfikacji rejestru. Oznacza to, że wysłane w ten sposób dane są zawsze wartościami konfiguracyjnymi i urządzenie odbiorcze o tym wie. Tak też jest w naszym przypadku, gdy czujnik VEML7700 odbierze dane bez wskazanego adresu, zapisze je właśnie w rejestrze ALS_CONF_0.

Wartości natężenia światła odczytywane przez czujnik.

Po uruchomieniu programu na ekranie komputera zobaczyć możemy odczyty z cyfrowego czujnika światła. Gdy go zasłonimy, wartości będą niewielkie, bliskie niemal zeru i analogicznie im więcej światła będzie padać na ukryty w przezroczystej obudowie rdzeń, tym wyższy będzie odczyt.

Czujnik cyfrowy vs fotorezystor

Porównanie czujnika światła z fotorezystorem.

Do tej pory mieliśmy już okazję dokonywać pomiaru natężenia światła, przy okazji testów fotorezystora, którego można powiedzieć zadanie jest identyczne, jak cyfrowego czujnika światła. Oba elementy mogą dostarczyć nam informacji o jego natężeniu, jednak powstaje pytanie, który z nich jest doskonalszy, i który lepiej stosować. Odpowiedź nie jest do końca jednoznaczna i zależy głównie od tego, co chcemy osiągnąć. Przy fotorezystorze korzystamy ze wbudowanego w mikrokontroler przetwornika ADC, którego rozdzielczość wynosi 12 bitów. Dzięki temu uzyskujemy pomiary z zakresu od 0 do 4095. W tym porównaniu cyfrowy czujnik światła wypada znacznie lepiej. Jest to 16 bitowa jednostka, przez co odczyty są znacznie bardziej dokładne i mieszczą się w zakresie od 0 do 65535. Z drugiej jednak strony obsługa takiego modułu jest nieco trudniejsza i bardziej wymagająca. Dlatego wybór między czujnikiem a fotorezystorem zależy głównie od tego, jakiego rodzaju pomiar potrzebujemy. Jeśli wymagana jest tylko orientacyjna wartość natężenia światła wystarczy zastosować fotorezystor. Jednak, gdy potrzebujemy jak najbardziej dokładnej wartości, wówczas lepszym wyborem będzie cyfrowy moduł.

Kilka słów na koniec…

W dzisiejszym materiale przyjrzeliśmy się bliżej funkcjonalności mikrokontrolera, jaką jest interfejs I2C. Na początku skorzystaliśmy z niego, aby sprawdzić, czy producent cyfrowego czujnika światła nie kłamie w dokumentacji i adres tam zawarty jest zgodny z rzeczywistością. Natomiast w drugiej części artykułu uruchamialiśmy moduł VEML7700 i dzięki niewielkiemu programowi poznaliśmy odczytywaną przez niego wartość natężenia światła. W kolejnym poradniku zostaniemy w temacie cyfrowego modułu z interfejsem I2C, jednak spróbujemy zwiększyć jego możliwości oraz pomyśleć nad napisaniem własnej biblioteki.

Ź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://www.vishay.com/docs/84286/veml7700.pdf

Jak oceniasz ten wpis blogowy?

Kliknij gwiazdkę, aby go ocenić!

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

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:

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.