Kurs Raspberry Pi Pico – #12 – przygotowujemy bibliotekę dla cyfrowego czujnika światła 1/2

Czas czytania: 18 min.

Ostatnim razem po raz pierwszy uruchomiliśmy cyfrowy czujnik światła VEML7700, poznając tym samym podstawowe zagadnienia związane z magistralą I2C. W dzisiejszym materiale pozostaniemy w tym temacie i spróbujemy nieco rozbudować program obsługujący ten sensor, tak aby mogli korzystać z niego również inni użytkownicy.

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.

Ile możemy wyciągnąć z czujnika VEML7700?

Tak jak wspomniałem, w poprzednim artykule przygotowaliśmy podstawowy kod obsługujący cyfrowy czujnik światła. Dzięki niemu mogliśmy obserwować zmieniające się liczbowe wartości reprezentujące poziom natężenia światła. Można by pomyśleć, że jest to już pełna funkcjonalność VEML7700, wszakże cóż więcej może robić czujnik światła, jak nie tylko mierzyć jego wartość. Jednak w rzeczywistości możliwości sensora są nieco większe, sam pomiar jest konfigurowalny, poza tym, czujnik wspiera system przerwań oraz pomiar natężenia światła białego. Tak więc dostępna funkcjonalność jest nieco większa i w dalszej części spróbujemy skorzystać z tych funkcji. Najpierw jednak przejdźmy do dokumentacji i przeanalizujmy dokładnie możliwości VEML7700.

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

Spis rejestrów dostępnych w sensorze światła poznaliśmy już wcześniej, jednak poprzednim razem korzystaliśmy tylko z dwóch – ALS_CONF_0, w którym umieszczone są parametry konfiguracyjne oraz ALS, przechowującego odczytaną wartość natężenia światła. Poza nimi dostępnych jest jeszcze sześć rejestrów, których znaczenie warto poznać.

ALS_WH i ALS_WL są dwoma szesnastobitowymi konstrukcjami związanymi z systemem przerwań. Dokładniej przechowują one wartości niskiego i wysokiego progu, na których podstawie generowane są sygnały przerwań. Dane o natężeniu światła możemy monitorować w sposób ciągły, cały czas odczytując rejestr ALS, tak jak to miało miejsce w poprzednim projekcie lub też skorzystać z systemu przerwań. Dzięki wartościom progowym możemy ustawić odpowiedni zakres, a gdy natężenie światła przekroczy jedną ze skrajnych liczb, czujnik wygeneruje przerwanie. W ten sposób możemy niejako przenieść na czujnik analizowanie poziomu natężenia światła. Jeśli w projekcie potrzebna jest podstawowa analiza, bazująca na zerojedynkowym – czujnik jest oświetlony/czujnik nie jest oświetlony, wystarczy odpowiednio ustawić wartości progowe, a gdy te zostaną przekroczone, pojawi się przerwanie. Tym samym mikrokontroler nie będzie musiał non stop analizować konkretnej wartości, a tylko zareaguje na pojawiające się przerwanie.

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

Jak łatwo się domyślić rejestr Power Saving odpowiedzialny jest za zarządzanie energią w VEML7700. Innymi słowy zmieniając ustawienie bitów PSM, możemy wymusić działanie w jednym z czterech trybów oszczędzania energii. Poza tym możliwe jest natywne wyłącznie tej funkcjonalności poprzez zmianę na zero bitu PSM_EN.

Tabela zależności trybów oszczędzania energii i poboru prądu. (https://www.vishay.com/docs/84286/veml7700.pdf)

W dokumentacji czujnika możemy znaleźć też tabelę opisującą nieco dokładniej zależności między poszczególnymi trybami oszczędzania energii, a wartością natężenia prądu pobieraną przez czujnik. Moduł sam w sobie nie jest przesadnie prądożerny, ale w wymagających tego aplikacjach wartość można zredukować nawet do poziomu 2µA. Poza tym warto zwrócić uwagę na czas pomiaru ALS_IT oraz czas, co który odczytywane będą dane, bo od tych wartości również zależny jest pobierany przez sensor prąd.

Kolejnym rejestrem jest WHITE, podobnie jak ALS przechowuje on szesnastobitową wartość odpowiadającą natężeniu światła. Tym razem jednak jest to liczba wskazująca tylko na światło białe, tym samym można powiedzieć, że VEML7700 może mierzyć nie tylko ogólną wartość natężenia światła, ale też wartość światła białego.

Rejestr statusu przerwań. (https://www.vishay.com/docs/84286/veml7700.pdf)

ALS_INT to kolejne miejsce związane z obsługą przerwań. Rejestr ten służy tylko do odczytu i nie możemy modyfikować zawartych w nim danych, przechowuje informacje o wystąpieniu przerwania. Jeśli wartość natężenia światła przekroczy któryś z ustawionych progów, w rejestrze tym ustawiony zostanie odpowiedni bit.

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

Ostatnim rejestrem jest ID, który przechowuje unikalny identyfikator czujnika. Może się wydawać, że identyfikator sensora nie jest szczególnie przydatną informacją, ale pozory mylą i można go wykorzystać dla przykładu do nieco bardziej rozbudowanej procedury inicjalizacji. Znając identyfikator, możemy sprawdzić, czy jest on poprawny i tym samym zweryfikować poprawność połączenia z czujnikiem i potwierdzić, że komunikacja działa prawidłowo.

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

Przypomnijmy sobie też strukturę konfiguracyjnego rejestru ALS_CONF_0. Z jego poziomu możemy wprowadzić czujnik w stan uśpienia – ALS_SD, włączyć lub wyłączyć funkcję przerwań – ALS_INT_EN, zmienić liczbę pomiarów, która przekroczyć musi wartość progową, aby wygenerowane zostało przerwanie – ALS_PERS, zmodyfikować czas pojedynczego pomiaru – ALS_IT oraz zmienić format wartości odczytanej przez sensor – ALS_GAIN.

Jak widać, VEML7700 oferuje całkiem sporo funkcji, jak na tak prosty moduł, którego głównym zadaniem jest tylko mierzyć poziom światła. Jak widać też, poprzednim razem wykorzystaliśmy moduł w naprawdę podstawowej formie, dlatego teraz zajmijmy się nieco bardziej rozbudowaną wersją programu, który pozwoli nam w pełni skorzystać z opcji, jakie oferuje cyfrowy czujnik światła.  

Rozbudowana obsługa cyfrowego czujnika światła

Zastanówmy się przez chwilę, co powinno znaleźć się w rozbudowanym kodzie obsługującym VEML7700. Potrzebować będziemy tak jak ostatnio funkcji inicjalizującej czujnik, która uruchomi go w podstawowym trybie. Poza tym oczywiście odczytamy wartość natężenia światła oraz natężenia światła białego. Dodatkowo przygotujemy funkcje obsługujące przerwania, odczyt ustawianych przez sensor bitów, zapis wartości progowych oraz włączenie lub wyłączenie samych przerwań. Przydatne mogą być też funkcje uruchamiające tryb oszczędzania energii oraz odczyt numeru identyfikacyjnego czujnika. Poza tym, aby za każdym razem nie wywoływać funkcji inicjalizującej w przypadku zmiany pojedynczych bitów ASL_CONF_0 zbudujemy funkcje modyfikujące czas pomiaru, ilość pomiarów wywołujących przerwanie oraz format odczytywanych z sensora wartości natężenia światła.

Każda z tych funkcji bazować będzie na wysyłaniu, bądź odbieraniu danych poprzez interfejs I2C, dlatego warto będzie napisać też uniwersalne funkcje realizujące te procesy. 

Tak więc gdy wiemy już mniej więcej, co musi znaleźć się w naszym programie, przejść możemy do jego napisania. W tym celu przygotowałem kolejny projekt nazwany veml7700 i to na nim będę bazował w tym artykule, również was zachęcam do przygotowania osobnego projektu, ponieważ w dalszej części tego poradnika, będzie się w nim sporo dziać. Skorzystamy tutaj z przygotowanego już wcześniej obwodu, także nie ma potrzeby dokonywania w nim żadnych zmian. Poza tym sam kod, również przedstawię w nieco inny sposób, ponieważ będzie on nieco dłuższy. Omówimy go krok po kroku, w częściach, które trzeba będzie złożyć w całość.

#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

//VEML7700 register
#define VEML7700_ALS_CONF_0   0x00
#define VEML7700_ALS_WH       0x01
#define VEML7700_ALS_WL       0x02
#define VEML7700_POWER_SAVING 0x03
#define VEML7700_ALS          0x04
#define VEML7700_WHITE        0x05
#define VEML7700_ALS_INT      0x06
#define VEML7700_ID_REG       0x07

 

Na początek obowiązkowo musimy dołączyć niezbędne do działania programu biblioteki, tak jak w poprzednim projekcie, będzie to między innymi hardware/i2c.h, ponieważ to właśnie za pomocą tego interfejsu komunikujemy się z cyfrowym czujnikiem światła.

Poza tym znalazły się tu deklaracje związane z interfejsem I2C, oraz adresem samego sensora, które już znamy, ale umieściłem tutaj też spis rejestrów VEML7700 zgodnie z dostępnym w dokumentacji opisem. Dzięki temu w dalszej części nie będziemy musieli stosować cyfrowych oznaczeń poszczególnych rejestrów i dla przykładu, zamiast używać 0x05, użyjemy VEML7700_WHITE, co jest znacznie bardziej przejrzyste.

void veml7700_write_register(uint8_t reg, uint16_t value) {
   uint8_t data[3] = {reg, value & 0xFF, value >> 8};
   i2c_write_blocking(I2C_PORT, VEML7700_ADDR, data, 3, false);
}
uint16_t veml7700_read_register(uint8_t reg) {
   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];
}

W tym miejscu możemy przejść do napisania funkcji, które obsługiwać będą nasz czujnik, ale tak jak wspomniałem wcześniej, każda z nich bazować będzie na komunikacji I2C, a dokładniej na zapisie lub odczycie danych. Dlatego właśnie warto przygotować uniwersalne funkcje realizujące to zadanie, dzięki czemu w kolejnych konstrukcjach skojarzonych stricte z czujnikiem, nie będzie potrzeby dokładnego opisu komunikacji, a wystarczy tylko wywołać odpowiedni fragment kodu.

Na początek zajmijmy się funkcją wysyłającą dane, ponieważ jest ona nieco prostsza. Nazwiemy ją veml7700_write_register i nie będzie ona zwracać żadnych danych, stąd właśnie typ void. Chcąc wysłać dane, potrzebować będziemy dwóch informacji – docelowego rejestru oraz samych danych do wysłania. Dlatego w argumentach funkcji umieściłem dwie zmienne – uint8_t reg, który będzie określać rejestr czujnika oraz uint16_t value, w którym przechowywane będą informacje do wysłania. Zauważcie, że dane te mają format 16 bitów, ponieważ właśnie takiej liczby bitów może oczekiwać sensor.

Jak pamiętacie, magistrala I2C bazuje na 8 bitowych paczkach danych, dlatego wewnątrz funkcji musimy utworzyć specjalną tablicę o takiej właśnie wielkości, w której zapiszemy przekazaną do funkcji informację o docelowym rejestrze oraz samych danych. Z pierwszym elementem tablicy nie ma większego problemu, ponieważ reg ma format 8 bitów i możemy przekazać go bezpośrednio, ale z value jest pewna niedogodność.

Operacje przeprowadzane na zmiennej value.

Wartość ta jest typu uint16_t, przez co tak naprawdę musimy podzielić ją na dwie 8 bitowe części. Do tego celu wykorzystamy sprytną konstrukcję, którą możecie zobaczyć na grafice powyżej. Chcąc wydzielić osiem młodszych bitów, musimy wykonać logiczną operację AND, czyli innymi słowy przemnożyć logicznie value, przez 0xFF, czyli 256. Taki proces nazywamy maskowaniem bitów i dość dobrze obrazuje go grafika, którą przygotowałem. Struktura 0xFF jest dość specyficzna, ponieważ osiem starszych bitów jest w tym przypadku zerami, pozwala to wyzerować również osiem starszych bitów zmiennej value, dzięki czemu w zmiennej wynikowej zawsze otrzymamy w tym miejscu zera. Z drugiej strony młodsza połowa 0xFF ma wartość jeden, przez co mamy pewność, że wartości value pozostaną w tym miejscu bez zmian. Maskowanie bitów jest prostym sposobem, w którym możemy zerować wybrane przez siebie bity. Dla przykładu moglibyśmy wykonać instrukcję value & 0x0000, w takim przypadku wszystkie bity drugiej zmiennej są zerami i tym sposobem, całość value zmieniłaby się w zero.

Być może zastanawiacie się, czy maskowanie bitów jest na pewno potrzebne i co by się stało, gdybyśmy do elementu tablicy uint8_t przypisali wartość spod uint16_t. W przypadku procesora RP2040 nie stałoby się nic, a lepszym określeniem jest, że stanie się to samo co w przypadku maskowania bitów. Niezależnie od sytuacji mikrokontroler przypisze do elementu tablicy osiem młodszych bitów zmiennej value. W takim razie, po co stosować maskowanie bitów? Musimy pamiętać, że programy w języku C uruchamiane mogą być na naprawdę sporej liczbie platform, jedną z nich są też procesory sygnałowe DSP, których działanie może być dość specyficzne. Sam standard języka C zasadniczo nie przewiduje, aby do mniejszej zmiennej przypisane zostało coś innego niż określona ilość młodszych bitów większej zmiennej, ale pewności czy tak będzie nigdy nie mamy, przez różnorodność sprzętowych rozwiązań we współczesnych jednostkach centralnych, dlatego warto się zabezpieczać i stosować maskowanie bitów.

Ostatni element tablicy powinien przechowywać osiem starszych bitów zmiennej value, dlatego dzięki operatorowi „>>” przesuwamy ją w prawo o osiem miejsc. Tym samym starsza część zmiennej zastępuje młodszy fragment, a puste miejsca wypełnione zostają zerami i nie ma potrzeby dodawania maskowania nowych bitów.

Tym sposobem przygotowaliśmy odpowiednią tablicę, w której umieszczone będą kolejno informacje o docelowym rejestrze oraz dane do wysłania rozdzielone na dwa mniejsze elementy. Tak przygotowane informacje możemy wysłać, stosując poznaną już wcześniej funkcję, realizującą to zadanie.

Drugą uniwersalną funkcją, którą trzeba przygotować, jest odczyt danych z czujnika. Nazwałem ją veml7700_read_register i jest to polecenie zwracające 16 bitów odczytanych z rejestru VEML7700. Poza tym w argumencie funkcji podawać będziemy też miejsce, z którego będziemy chcieli wyciągnąć informację.

Wewnątrz funkcji tworzymy dwuelementowy bufor, który przechowywać będzie odebrane informacje. Składa się on z dwóch elementów, ponieważ jak już wiecie, I2C operuje na 8 bitowych paczkach, a odebranych danych może być więcej. W kolejnym kroku wysyłamy do sensora informację o rejestrze, z którego za moment odczytamy dane, jak widać korzystamy tutaj ze wskaźnika, o czym wspominałem już w poprzednim artykule, ale dla przypomnienia jest tak, ponieważ przekazujemy tutaj pojedynczą zmienną, a funkcja i2c_write_blocking zawsze wymaga wskaźnika. W przypadku przekazywania do funkcji tablicy nie musimy stosować zapisu „&”, ponieważ tablica sama w sobie jest wskaźnikiem, pokazującym na swój pierwszy element.

Gdy czujnik otrzyma już informację o rejestrze, z którego chcemy odczytać dane, przechodzimy do samego odczytu. Stosujemy tutaj poznaną już wcześniej funkcję i2c_read_blocking, która zapisze w tablicy buffer. Warto wspomnieć, że instrukcja ta również wymaga wskaźnika, do docelowego miejsca dla danych, ale korzystając z tablicy, nie musimy się tym przejmować. Funkcja odczyta pierwsze osiem bitów i zapisze je w pierwszym elemencie tablicy, a następnie wyciągnie z czujnika kolejne dane i wrzuci je do kolejnego elementu tablicy.

Gdy dane są już odebrane, możemy je zwrócić dzięki poleceniu return. Jako że są one zapisane w dwuelementowej tablicy, a nasza funkcja zwraca pojedynczą zmienną w formacie uint16_t stosujemy poznaną już wcześniej konstrukcję, która przesuwa o osiem miejsc w lewo drugi element tablicy i dzięki logicznemu OR „|” łączy ją z pierwszym elementem.

				
					void veml7700_init() {
    veml7700_write_register(VEML7700_ALS_CONF_0, 0x0000);
}

uint16_t veml7700_read_light() {
    return veml7700_read_register(VEML7700_ALS);
}

uint16_t veml7700_read_white() {
    return veml7700_read_register(VEML7700_WHITE);
}

uint16_t veml7700_read_interrupt() {
    return veml7700_read_register(VEML7700_ALS_INT);
}

uint16_t veml7700_read_id() {
    return veml7700_read_register(VEML7700_ID_REG);
}

				
			

Po przygotowaniu bazowych funkcji, dzięki którym możliwa będzie komunikacja poprzez I2C, możemy przejść do przygotowania konkretnych poleceń obsługujący tan moduł. Na pierwszy ogień weźmy kilka prostszych instrukcji, czyli inicjalizację, odczyt wartości natężenia światła białego oraz ogólnego, sprawdzenie flag przerwań oraz pobranie unikalnego numeru ID czujnika.

Inicjalizację czujnika przeprowadzaliśmy już w poprzednim materiale, dlatego tym razem nie musimy wyjaśniać jej sobie aż tak dokładnie. Wewnątrz funkcji veml7700_init znalazło się tylko jedno polecenie zapisujące do rejestru ALS_CONF_0 wartość zerową, tym samym ustawiając domyślnie wszystkie parametry.

Konstrukcja pozostałych funkcji jest bliźniacza, każda z nich zwraca wartość typu uint16_t, a w jej wnętrzu znajdziemy polecenie odczytujące wartość spod rejestru, do którego adres podajemy w argumencie. Dzięki wcześniejszym deklaracją nie musimy pamiętać konkretnych adresów i wystarczy odwołać się do konkretnej nazwy.

void veml7700_set_integration_time(uint16_t time) {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(0x0F << 6); // Clear previous integration time settings
   config |= (time << 6);  // Set a new integration time
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

void veml7700_set_gain(uint16_t gain) {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(0x03 << 4); // Clear previous gain settings
   config |= (gain << 4);  // Set new gain
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

void veml7700_set_persistence(uint8_t persistence) {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(0x03 << 4); // Clear previous ALS_PERS settings
   config |= (persistence << 4); // Set new ALS_PERS bits
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

W kolejnym kroku zajmijmy się nieco bardziej złożonymi funkcjami, będzie to ustawienie czasu pomiaru natężenia światła, wybór formatu odczytywanych danych oraz ustawienie ilości pomiarów, po których przekroczeniu aktywowane zostanie przerwanie. Funkcje te są do siebie dość podobne i różnią się tylko pojedynczymi szczegółami, dlatego możemy przeanalizować je zbiorczo, na przykładzie ustawiania czasu pojedynczego pomiaru.

Funkcja ta nie musi zwracać żadnych danych, dlatego jej typem jest void. W argumencie oczekiwać będziemy konkretnej wartości, zgodnej z dokumentacją, która określać będzie czas trwania próbki. Dla przykładu, jeśli będziemy chcieli, aby pomiar trwał 200ms, w argumencie podamy 0x0001. Pierwszym krokiem wewnątrz funkcji, jest odczytanie aktualnej wartości spod rejestru ALS_CONF_0 i zapisanie jej w zmiennej config. Jest to niezbędny zabieg, ponieważ nie znamy aktualniej zawartości rejestru konfiguracyjnego, a nie ma innego sposobu na modyfikację zawartych w nim danych, jak podmiana ich wszystkich. Innymi słowy, nie ma możliwości zmiany tylko kilku konkretnych bitów, a nie chcemy też zmienić wartości pozostałych.

Maskowanie i zmiana wartości zmiennej config.

Po odczytaniu wartości rejestru konfiguracyjnego i zapisaniu go w config, możemy wyczyścić bity od 6 do 9, czyli ALS_IT, wartości odpowiedzialne za czas pomiaru. Do tego celu wykorzystamy wartość 0x0F, której cztery najmłodsze bity mają wartość jeden i przesuniemy je o sześć miejsc w lewo. W ten sposób otrzymujemy bitowe 1111000000, które następnie negujemy (operator „~”), czyli odwracamy wartość wszystkich bitów na przeciwny otrzymując 0000111111. Tak przygotowana wartość może dzięki logicznemu poleceniu AND wyzerować cztery bity, od 6 do 9 w zmiennej config. Proces ten jest niczym innym jak poznanym już wcześniej maskowaniem bitów. Gdy zmienna jest już gotowa, możemy umieścić w niej wartość, która przekazywana jest w argumencie funkcji. W tym celu, korzystamy z logicznego OR i przesuwamy wartość spod time o sześć miejsc w lewo, tak aby znalazła się ona w przeznaczonej ALS_IT destynacji.

Tym sposobem zmodyfikowaliśmy wartość config, tak aby znalazła się tam nowa wartość, odpowiadająca czasowi pomiaru, jednocześnie nie zmieniając pozostałych bitów. Dzięki temu możemy być pewni, że po wysłaniu danych do czujnika, co realizuje kolejne z poleceń, zmodyfikujemy tylko i wyłącznie porządną wartość, a pozostałe bity konfiguracyjne pozostaną bez zmian.

Pozostałe dwie funkcje z tego bloku kodu są bardzo podobne, dlatego nie ma sensu ich dokładniej omawiać. Zauważcie tylko, że różnią się one wartością, którą używamy do maskowania config. 0x0F pozwalało wyzerować cztery bity, 0x03 umożliwia zrobienie tego samego na dwóch bitach. Poza tym funkcje różnią się też wartością przesunięć, tak aby przekazana w argumencie zmienna umieszczona była w odpowiednim miejscu.

				
					void veml7700_power_save(uint16_t mode) {
    veml7700_write_register(VEML7700_POWER_SAVING, mode);
}

void veml7700_set_high_threshold(uint16_t threshold) {
    veml7700_write_register(VEML7700_ALS_WH, threshold);
}

void veml7700_set_low_threshold(uint16_t threshold) {
    veml7700_write_register(VEML7700_ALS_WL, threshold);
}

				
			

Kolejne obsługujące moduł VEML7700 funkcje bazują na poznanym już wcześniej schemacie, w którym korzystamy z uniwersalnej instrukcji zapisującej dane pod konkretny adres I2C. Są to polecenia pozwalające uruchamiać tryb oszczędzania energii oraz wprowadzić odpowiednie dane do rejestrów ALS_WH i ALS_WL, które to definiują progowe wartości, przy których wyzwolone zostanie przerwanie. Podobnie jak wcześniej, dane wysyłane do czujnika podawane są za pomocą argumentu funkcji.

void veml7700_enable_interrupt() {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config |= (1 << 1); // Set bit responsible for interrupts
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

void veml7700_disable_interrupt() {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(1 << 1); // Clearing the bit responsible for interrupts
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

Ostatnimi funkcjami, które musimy przygotować, jest włączenie i wyłącznie przerwań. Nie zwracają one żadnych danych i bazują na procesie, który poznaliśmy już wcześniej. Bity odpowiedzialne za uruchamianie lub dezaktywację przerwań znajdują się w rejestrze ALS_CONF_0 dlatego w pierwszym kroku pobieramy jego zawartość i umieszczamy w zmiennej config. W kolejnym kroku, musimy ustawić lub wyzerować drugi bit rejestru ALS_CONF_0, zależnie od tego, czy chcemy włączyć, czy wyłączyć przerwania. Jako że operujemy tylko na jednym bicie, proces ten jest nieco prostszy niż w przypadku wcześniejszych manipulacji zawartością rejestru konfiguracyjnego. W tym przypadku nie musimy przejmować się zawartością ALS_CONF_0 w całości, ponieważ bity inne niż ALS_INT_EN nas nie interesują, a zmiany, których za moment dokonamy, nie wpłyną na odczytaną zawartość zapisaną w config. Innymi słowy, na koniec wyślemy identyczną zmienną, która różnić może się tylko bitem odpowiedzialnym za konfigurację przerwań.

Gdy chcemy włączyć system przerwań, musimy przeprowadzić na zmiennej config logiczną operację OR, która na drugim bicie tej zmiennej ustawi wartość jeden, dlatego właśnie czystą wartość 1 przesuwamy o jeden krok w lewo. Analogicznie, gdy chcemy wyzerować bit ALS_INT_EN stosujemy logiczny AND, a zmienną maskującą dodatkowo negujemy, tak aby mieć pewność, że drugi bit zostanie wyzerowany.

W ten sposób wszystkie funkcje pozwalające zarządzać cyfrowym czujnikiem temperatury są gotowe. Od teraz będziemy mogli w prosty sposób inicjalizować moduł, odczytywać wartości natężenia światła, flagę przerwań oraz indywidualny numer chipa. Poza tym możemy dowolnie manipulować parametrami konfiguracyjnymi oraz ustawiać wartości progowe przerwań. Tak więc, gdy ten fragment programu jest już gotowy, możemy przejść do głównej funkcji main i wykorzystać przygotowane polecenia w rzeczywistości.

				
					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();
    veml7700_set_integration_time(0x0001); 
    veml7700_set_gain(0x0000); 
    veml7700_set_persistence(0x0001); 
    veml7700_set_high_threshold(0x9C40); 
    veml7700_set_low_threshold(0x0FA0); 
    veml7700_enable_interrupt(); 
    veml7700_power_save(0x01); 

				
			

Wewnątrz głównej funkcji aktywujemy w poznany już wcześniej sposób magistralę I2C oraz ustawiamy odpowiednio piny SDA i SCL. Po tej operacji skorzystać możemy z przygotowanych wcześniej funkcji. Pierwszym krokiem jest inicjalizacja czujnika, która uruchamia go w podstawowej konfiguracji, a następnie za pomocą konkretnych instrukcji możemy zdefiniować poszczególne parametry i funkcjonalności modułu. W tym programie skorzystamy z wszystkich dostępnych możliwości, tak aby sprawdzić działanie napisanych funkcji. Tak więc za pomocą kolejnych poleceń zmieniamy czas pojedynczego pomiaru na 200ms, format danych definiujemy ponownie w bazowej konfiguracji, ustawiamy ALS_PERS, tak aby przerwanie aktywowane było po dwóch pomiarach, zmieniamy wartości progowe na 0x9C40 (40000) i 0x0FA0 (4000), włączamy system przerwań oraz aktywujemy przykładowy tryb oszczędzania energii. Dane przekazujemy poprzez argumenty funkcji, które jak już wiemy,  podzielone są na dwie części i zapisywane w odpowiednich rejestrach czujnika.

    while (true) {
       uint16_t device_id = veml7700_read_id();
       printf("Device ID: 0x%04X\n", device_id);

       uint16_t light = veml7700_read_light();
        printf("Ambient Light: %u\n", light);

       uint16_t white = veml7700_read_white();
        printf("White Light: %u\n", white);

        uint16_t als_int = veml7700_read_interrupt();

       if (als_int & 0x4000) {
           printf("High threshold interrupt! Light: %u\n", light);
        }

       if (als_int & 0x8000) {
           printf("Low threshold interrupt! Light: %u\n", light);
        }

       sleep_ms(1000);
    }

W nieskończonej pętli while skorzystamy z kolejnych funkcji czujnika. Odczytamy jego uniwersalny numer identyfikacyjny, wartość ogólnego natężenia światła oraz światła białego, dodatkowo sprawdzimy też, czy pojawiło się przerwanie wysokiego lub niskiego progu. W tym celu musimy sprawdzić, czy bit 14 lub 15 w ALS_INT jest jedynką. Moglibyśmy sprawdzać go niejako pojedynczo, ale prostszym sposobem jest skorzystanie z logicznego AND. Poddając zmienną takiej operacji, możemy sprawić, że funkcja if wykona się, gdy w konkretnym jej miejscu pojawi się logiczna prawda. O tym, które będzie to miejsce decyduje drugi wykorzystywany w działaniu argument. 0x4000 to nic innego jak bitowe 100000000000000, czyli jedynka na czternastym miejscu. Analogicznie 0x8000 odpowiada jedynce na piętnastym miejscu. Tak więc, gdy 14 bit w ALS_INT będzie prawdą, wykona się pierwsza funkcja warunkowa, a gdy jedynka znajdzie się na piętnastym miejscu, wykonany zostanie drugi if. 

Monitor portu szeregowego po uruchomieniu programu.

Po wrzuceniu programu do pamięci Raspberry Pi Pico, na ekranie monitora portu szeregowego obserwować możemy odczytywane kolejno wartości natężenia światła wraz z towarzyszącym im numerem ID czujnika. Dodatkowo, gdy zasłonimy czujnik i dwa odczyty z rzędu będą miały wartość mniejszą niż 4000, aktywowane zostanie przerwanie, które odczytamy. Podobnie zdarzy się, gdy oświetlimy lico czujnika ostrym światłe, tak aby jego wartość przekroczyła 40000, wówczas również pojawi się przerwanie, jednak jego źródłem, będzie rejestr wysokiego progu.

W ten oto sposób udało się nam uruchamiać program, który w pełnik korzysta z dostępnych w VEML7700 funkcji. Kod może wydawać się nieco skomplikowany, ale gdy spojrzymy na niego z szerszej perspektywy, wszystko się rozjaśnia. Przygotowaliśmy szereg funkcji, które mogą odczytywać lub zapisywać dane, a do samej komunikacji używają dwóch uniwersalnych poleceń. Główna funkcja programu jest tutaj tylko formalnością, która pozwala sprawdzić, czy moduł działa poprawnie.

Możemy tak naprawdę powiedzieć, że wspólnymi siłami przygotowaliśmy namiastkę biblioteki, obsługującej cyfrowy czujnik światła VEML7700. Jednak, aby nasz program można było nazwać w pełni funkcjonalną biblioteką, musimy go nieco zmodyfikować. Tak, aby w przypadku przygotowania większego projektu móc w prosty sposób zaimplementować obsługę modułu bez większych ingerencji w główny kod. Jednak, aby było to możliwe, musimy podzielić nasz program na osobne pliki.

Podział projektu na pliki

Do tej pory projekty, które budowaliśmy, składały się tylko z jednego pliku z rozszerzeniem .c, w którym umieszczaliśmy wykonywany przez RPI kod. Musicie jednak wiedzieć, że w przypadku większych programów powszechną, jak i wskazaną praktyką jest dzielenie kodu na mniejsze części. Tym sposobem rozbijamy projekt na elementy składowe, skojarzone zazwyczaj z konkretną funkcjonalnością lub modułem zewnętrznym. Pozwala to uprościć program i zmniejszyć objętość głównego pliku, ponieważ funkcje w nim zawarte odwoływać będą się do kodu, który umieszczony będzie w innym miejscu.

Podział na pliki możemy przećwiczyć właśnie na kodzie obsługującym cyfrowy czujnik światła. Spróbujemy wyrzucić przygotowane wcześniej polecenia do osobnego pliku, tak aby główny program był jak najprostszy. Pozwoli to też w przyszłości dołączać obsługę VEML7700 do innych projektów.

Pliki w folderze veml7700.

W dalszym ciągu korzystać będziemy z projektu veml7700, jednak teraz zmodyfikujemy nieco jego strukturę. Klikając prawym przyciskiem myszy na nazwę projektu, należy dodać do niego dwa pliki: main.c, który od teraz będzie naszym głównym plikiem z kodem oraz veml7700.h, który będzie pełnić funkcję pliku nagłówkowego.

Tego typu podział jest dość powszechnie stosowany we większych projektach przygotowywanych w języku C. Poprzednio dysponowaliśmy tylko jednym plikiem z rozszerzeniem .c i budując projekt, środowisko uznawało go za główny plik programu, choć co warto zauważyć za pomocą #include, dołączaliśmy już inne biblioteki, niewidoczne tutaj, ale obecne w pico-examples. Od teraz naszym głównym plikiem będzie main.c, który swoją nazwę zawdzięcza bazowej funkcji programu w C. Do tego pliku dołączymy veml7700.h, czyli plik nagłówkowy, w którym znajdą się wszystkie prototypy funkcji obsługujących cyfrowy czujnik światła. Ciało funkcji, czyli jej dokładne działalnie rozbite na pojedyncze polecenia umieścimy w veml7700.c. Tym sposobem przeniesiemy całkowicie obsługę sensora, do dwóch osobnych plików i aby korzystać z jego funkcjonalności wystarczy, że dołączymy do głównego pliku z kodem, plik nagłówkowy skojarzony z VEML7700.

Wydzielenie danej funkcjonalności do osobnych plików ma jeszcze jedną zaletę. Dzięki temu w przyszłości, gdy będziemy chcieli skorzystać z czujnika wystarczy, że do gotowego już projektu wrzucimy dwa dodatkowe pliki. Innymi słowy, dodamy do projektu własną, dodatkową bibliotekę.

Zanim jednak przejdziemy do omawiania poszczególnych plików, zastanówmy się przez moment, jak ten proces będzie wyglądać. Może wydawać się to dość proste, ot przeniesiemy napisane funkcje do osobnego pliku, a w głównym main.c dołączymy go jako bibliotekę i rzeczywiście tak to wygląda. Zatrzymać musimy się jednak nad pewnym aspektem, który można powiedzieć niejako łączy bibliotekę, którą przygotujemy i główny program, a jest to I2C. Czujnik korzysta z interfejsu I2C, który inicjujemy aktualnie na początku głównej funkcji main i bez tej deklaracji nie będzie on działał poprawnie. Dlatego właśnie musimy zdecydować czy pozostawić to w ten sposób, aby to na barkach potencjalnego użytkownika spoczywał obowiązek zadbania o interfejs I2C, czy też umieścić ten kod w naszej bibliotece.

Oba rozwiązania mają swoje wady i zalety, użytkownik niekoniecznie musi chcieć zaglądać do biblioteki, aby zmieniać zawarte w niej dane, w sytuacji, gdy na przykład czujnik podłączony jest do innych wyprowadzeń RPI Pico. Z drugiej jednak strony można powiedzieć, że taka biblioteka nie jest do końca funkcjonalna, ponieważ nie została stworzona według zasady „dołączam do projektu i działa, nie interesuje mnie co jest w środku”.

Osobiście skłaniam się bardziej do drugiej opcji, wolę, jeśli przygotowany przeze mnie kod jest nieco bardziej „zamknięty” i nie wymaga od potencjalnego użytkownika dodawania dodatkowej funkcjonalności, ponieważ zakładam, że nie musi on znać języka C na tym poziomie, aby pamiętać o inicjalizacji interfejsu.

Dlatego właśnie do pliku z opisem działania VEML7700 dołączę dodatkową funkcję o nazwie veml7700_i2c_init, w której to znajdzie się kod inicjalizujący magistralę I2C. Będzie to na swój sposób pośrednie rozwiązanie, ponieważ inicjalizacja interfejsu nie będzie umieszczona wewnątrz veml7700_init, a w osobnej funkcji. Domyślnym będzie jej użycie, ale jeśli ktoś chciałby zająć się przygotowaniem I2C w głównym programie, wystarczy, że nie skorzysta z tej funkcji.

Tak więc przejdźmy do omówienia poszczególnych plików z kodem.

veml7700.h
				
					#define I2C_PORT i2c1
#define SDA_PIN 6
#define SCL_PIN 7
#define VEML7700_ADDR 0x10

// VEML7700 register
#define VEML7700_ALS_CONF_0   0x00
#define VEML7700_ALS_WH       0x01
#define VEML7700_ALS_WL       0x02
#define VEML7700_POWER_SAVING 0x03
#define VEML7700_ALS          0x04
#define VEML7700_WHITE        0x05
#define VEML7700_ALS_INT      0x06
#define VEML7700_ID_REG       0x07

void veml7700_write_register(uint8_t reg, uint16_t value);
uint16_t veml7700_read_register(uint8_t reg);

void veml7700_i2c_init();

void veml7700_init();
uint16_t veml7700_read_light();
uint16_t veml7700_read_white();
uint16_t veml7700_read_interrupt();
uint16_t veml7700_read_id();
void veml7700_set_integration_time(uint16_t time);
void veml7700_set_gain(uint16_t gain);
void veml7700_set_persistence(uint8_t persistence);
void veml7700_power_save(uint16_t mode);
void veml7700_set_high_threshold(uint16_t threshold);
void veml7700_set_low_threshold(uint16_t threshold);
void veml7700_enable_interrupt();
void veml7700_disable_interrupt();

				
			

W pliku nagłówkowym należy umieścić wszystkie definicje i prototypy funkcji. W ten sposób znalazły się tutaj informacje związane z interfejsem I2C, adresy rejestrów VEML7700 oraz prototypy wszystkich funkcji, które opisane będą w veml7700.c. Dołączając plik nagłówkowy do głównego main.c, umożliwiamy mu dostęp do tych właśnie funkcji, które dokładniej opisane są w pliku z rozszerzeniem .c.

Jak możecie zauważyć, struktura pliku .h jest nieco inna niż wcześniej przygotowywanych kodów. Nie ma tutaj dodatkowych bibliotek lub standardowego podziału na główną funkcję main i nieskończoną pętlę while. Jest to jednak naturalne, bo jak sama nazwa wskazuje plik nagłówkowy, ma zawierać tylko nagłówki, wskazania innych funkcji, które to umieszczone są w innym pliku z kodem C.

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

void veml7700_write_register(uint8_t reg, uint16_t value) {
   uint8_t data[3] = {reg, value & 0xFF, value >> 8};
   i2c_write_blocking(I2C_PORT, VEML7700_ADDR, data, 3, false);
}

uint16_t veml7700_read_register(uint8_t reg) {
   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];
}

void veml7700_i2c_init(){
   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);
}

void veml7700_init() {
   veml7700_write_register(VEML7700_ALS_CONF_0, 0x0000);
}

uint16_t veml7700_read_light() {
   return veml7700_read_register(VEML7700_ALS);
}

uint16_t veml7700_read_white() {
   return veml7700_read_register(VEML7700_WHITE);
}

uint16_t veml7700_read_interrupt() {
   return veml7700_read_register(VEML7700_ALS_INT);
}

uint16_t veml7700_read_id() {
   return veml7700_read_register(VEML7700_ID_REG);
}

void veml7700_set_integration_time(uint16_t time) {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(0x0F << 6); // Clear previous integration time settings
   config |= (time << 6);  // Set a new integration time
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

void veml7700_set_gain(uint16_t gain) {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(0x03 << 4); // Clear previous gain settings
   config |= (gain << 4);  // Set new gain
  veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

void veml7700_set_persistence(uint8_t persistence) {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(0x03 << 4); // Clear previous ALS_PERS settings
   config |= (persistence << 4); // Set new ALS_PERS bits
  veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

void veml7700_power_save(uint16_t mode) {
   veml7700_write_register(VEML7700_POWER_SAVING, mode);
}

void veml7700_set_high_threshold(uint16_t threshold) {
   veml7700_write_register(VEML7700_ALS_WH, threshold);
}

void veml7700_set_low_threshold(uint16_t threshold) {
   veml7700_write_register(VEML7700_ALS_WL, threshold);
}

void veml7700_enable_interrupt() {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config |= (1 << 1); // Setting the bit responsible for interrupts
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

void veml7700_disable_interrupt() {
   uint16_t config = veml7700_read_register(VEML7700_ALS_CONF_0);
   config &= ~(1 << 1); // Clearing the bit responsible for interrupts
   veml7700_write_register(VEML7700_ALS_CONF_0, config);
}

We wnętrzu veml.c, tak jak wspominałem, znalazły się wszystkie funkcje, które przygotowaliśmy już wcześniej. Jedyną nowością jest tutaj veml7700_i2c_init, czyli opcjonalne polecenie inicjalizujące magistralę I2C, którego rozwinięciem są instrukcje umieszczone wcześniej w głównej funkcji main.

Kod ten może już nieco bardziej przypominać standardowe programy w języku C, które poznaliśmy do tej pory. Jak widać, znalazło się tu kilka dodatkowych bibliotek oraz dołączony plik z rozszerzeniem .h, który przygotowaliśmy wyżej. Zabrakło jednak tutaj funkcji main i pętli while, ponieważ zadaniem tego pliku jest tylko rozwinąć dostępne z poziomu veml7700.h funkcje, innymi słowy opisać ich działanie.

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

int main() {
    stdio_init_all();

   veml7700_i2c_init();
    veml7700_init();

   veml7700_set_integration_time(0x0001);
   veml7700_set_gain(0x0000);
   veml7700_set_persistence(0x0001);
   veml7700_set_high_threshold(0x9C40);
   veml7700_set_low_threshold(0x0FA0);
   veml7700_enable_interrupt();
    veml7700_power_save(0x01);

   while (true) {
       uint16_t device_id = veml7700_read_id();
        printf("Device ID: 0x%04X\n", device_id);

       uint16_t light = veml7700_read_light();
        printf("Ambient Light: %u\n", light);

       uint16_t white = veml7700_read_white();
        printf("White Light: %u\n", white);

       uint16_t als_int = veml7700_read_interrupt();
       if (als_int & 0x4000) {
           printf("High threshold interrupt! Light: %u\n", light);
        }

       if (als_int & 0x8000) {
           printf("Low threshold interrupt! Light: %u\n", light);
        }

       sleep_ms(3000);
   }
}

Struktura pliku main.c, który od tej pory będzie naszym głównym plikiem z kodem C, jest znacznie bardziej odchudzona względem wcześniejszego programu, w którym wszystko umieszczone było w jednym miejscu. Dzięki dołączeniu veml.h, zyskujemy z jego poziomu dostęp do funkcji opisanych w veml.c. Tym sposobem, po dołączeniach możemy od razu przejść do głównej funkcji main, w której umieszczona funkcja inicjalizująca magistralę I2C oraz polecenia związane stricte z czujnikiem światła, które poznaliśmy już wcześniej. Nieskończona pętla while, również nie różni się znacząco względem poprzedniego programu. Jedyną różnicą jest tutaj trzy sekundowe odczekanie, przed kolejnym przebiegiem pętli. Poza tym, tak jak wcześniej, odczytujemy dane z sensora i wysyłamy je na ekran komputera.

Tym sposobem podzieliliśmy projekt obsługujący cyfrowy czujnik światła na trzy osobne pliki, jednak, aby można było go skompilować, należy dokonać niewielkiej zmiany z CMakeLists.txt.

Struktura CMakeLists.txt dla projektu veml7700.

Jako że aktualnie w strukturze projektu umieszczone są dwa pliki z rozszerzeniem .c, taką informację trzeba umieścić w CMakeLists.txt. Oba pliki dodajemy wewnątrz add_executable, tak jak widać na załączonym wyżej obrazku.

Tak przygotowany projekt można skompilować, a wygenerowany plik .uf2 wrzucić do pamięci RPI Pico. Po uruchomieniu efekt powinien być identyczny jak poprzednim razem. Na ekranie komputera zobaczymy zwracane przez sensor wartości natężenia światła wraz z informacją o przerwaniu, jeśli takowe wystąpi.

Kilka słów na koniec…

Był to pierwszy materiał poświęcony przygotowaniu biblioteki dla cyfrowego czujnika światła. Kod, który przygotowaliśmy, jest już w pełni funkcjonalny, a ponadto podzielony na pliki, dzięki czemu można go w prosty sposób dołączyć do istniejącego już projektu. Nie jest to jednak koniec, dlatego w kolejnym artykule spróbujemy przerobić program, tak aby czujnik stał się elementem obiektowym, a poza tym przyjrzymy się jednemu z modułów RP2040 o nazwie DMA.

Ź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: 5 / 5. Liczba głosów: 8

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.