Współczesny rynek elektroniki użytkowej wymaga od projektantów i konstruktorów, by projektowane przez nich urządzenia były nie tylko funkcjonalne i poprawnie wykonane, ale również, by czas od fazy wczesnego prototypu do wdrożenia gotowego produktu na rynek był możliwie krótki, a pierwszy prototyp – tym ostatnim. Dlatego też, wielu producentów sprzętu w swoich rozwiązaniach decyduje się na wykorzystanie bardziej zasobnych sprzętowo mikrokontrolerów i mikroprocesorów, umożliwiających uruchomienie pełnego systemu operacyjnego, takiego jak np. Linux czy Android.
Zwiększony nakład finansowy na warstwę sprzętową jest rekompensowany poprzez skrócenie czasu pracy nad oprogramowaniem i jego testami. Dzięki wykorzystaniu systemu operacyjnego, programista urządzenia uzyskuje dostęp do ogromnych zasobów gotowych komponentów programowych (np. w postaci bibliotek), przetestowanych przez miliony użytkowników. Co więcej, system operacyjny pełni rolę wydajnego menadżera zasobów oraz zapewnia abstrakcję warstwy sprzętowej, dzięki czemu programista nie musi znać niskopoziomowej specyfiki podłączonych urządzeń peryferyjnych.
Interfejs I2C jest popularnym i powszechnie wykorzystywanym w systemach wbudowanych, synchronicznym interfejsem szeregowym. Mając jednak na uwadze fakt, że część Czytelników niniejszego artykułu może „wywodzić się” z grupy programistów systemu Android i urządzeń mobilnych, a dzięki Android Things dopiero rozpoczyna swoje przygody z elektroniką, poniżej przedstawiono zwięzły opis teoretycznych podstaw działania magistrali I2C.
Magistrala I2C jest dwukierunkowym i dwuprzewodowym interfejsem szeregowym, przeznaczonym do komunikacji pomiędzy układem nadrzędnym (mikrokontrolerem), a innymi układami peryferyjnymi umieszczonymi w obrębie jednego systemu, na przykład: czujnikami temperatury, zegarami czasu rzeczywistego, przetwornikami C/A oraz A/C. Transmisja na magistrali realizowana jest w sposób synchroniczny z wykorzystaniem dwóch linii: SDA (linia danych) oraz SCL (linia zegarowa). Za poprawną pracę magistrali I2C odpowiada układ nadrzędny Master, do którego zadań należy generowanie sygnału zegarowego na linii SCL i synchronizacja wymiany danych. Linie interfejsu I2C są liniami typu otwarty dren, co oznacza, że magistrala do poprawnej pracy wymaga zewnętrznych rezystorów „podciągających”. Rezystory te są odpowiedzialne za ustawienie stanu wysokiego na magistrali w przypadku braku transmisji. Standard magistrali I2C umożliwia podłączenie wielu układów podrzędnych (układów typu Slave), co narzuca konieczność wprowadzenia odpowiedniego mechanizmu adresowania. Układy Slave są adresowane poprzez 7-bitowy adres – ostatni 8. bit określa wówczas kierunek przepływu kolejnej transakcji danych (odczyt/zapis). Sam standard magistrali udostępnia również możliwość wykorzystania 10-bitowego adresowania układów, jednak wariant ten nie będzie omawiany w ramach niniejszego artykułu. Schematyczna budowa magistrali I2C została przedstawiona na rysunku 1.
Dane przesyłane za pomocą interfejsu I2C grupowane są w 8-bitowe bloki. Rozpoczęcie transmisji odbywa się po wystawieniu przez układ Master sekwencji START, która identyfikuje początek ramki oraz rezerwuje magistralę, aż do wystąpienia sekwencji STOP. Przy sekwencji startowej, układ Master ściąga linię SDA do poziomu niskiego, przy wysokim stanie linii zegarowej. Koniec transmisji ramki jest operacją odwrotną – zbocze narastające na linii SDA przy wysokim poziomie zegara. Sygnał startu przełącza wszystkie podpięte do magistrali układy w tryb odbioru danych. Od tego momentu dane przesyłane za pomocą linii SDA są ważne i mogą zmieniać się wyłącznie w stanie niskim linii zegarowej. Po ośmiu taktach zegara (nadaniu bajtu danych) zaadresowany układ Slave, musi potwierdzić prawidłowość odbioru przesyłanych informacji. Czynność ta realizowana jest poprzez bit ACK (ang. ACKnowledge – potwierdzać), czyli wymuszenie przez układ Slave stanu niskiego na linii SDA. Układy podrzędne mają również możliwość „ściągnięcia do masy” sygnału zegarowego w wypadku, gdy nie nadążają z odbiorem danych lub realizują inne zadania (np. realizują na żądanie układu Master proces pomiaru temperatury lub wilgotności). Typowy przebieg transmisji na poziomie bitowym przedstawiono na rysunku 2.
Na rysunku 3 oraz rysunku 4 przedstawiono dwa typowe scenariusze przebiegu transmisji na poziomie bajtowym (typowe dla układów Slave posiadających organizację rejestrową, tzn. gdzie komunikacja z układem odbywa się poprzez zapis/odczyt określonych rejestrów urządzenia pełniących przypisane przez producenta funkcje).
W pierwszym z przypadków (rysunek 3) układ Master generuje adres układu podrzędnego z ósmym bitem o wartości 0 – zapis do układu Slave. Po wygenerowaniu adresu, wybrany układ podrzędny odpowiada bitem potwierdzenia ACK (o ile istnieje urządzenie Slave o zadanym adresie). Kolejny bajt zapisywany do urządzenia Slave jest numerem rejestru, do którego Master będzie zapisywał dane. Zapis danych, będzie realizowany do momentu wygenerowania sekwencji STOP. Procedura odczytu zorganizowana jest w analogiczny sposób – po zaadresowaniu układu podrzędnego i wyborze rejestru z którego będziemy dokonywać odczytu, kolejne bajty danych generowane są przez układ Slave – układ Master dokonuje odczytu i potwierdza ich odbiór bitem ACK.
Warstwa sprzętowa projektu AndroidThings-I2C
Obsługa magistrali I2C w systemie Android Things zostanie przedstawiona na przykładzie dwóch układów peryferyjnych:
- Modułu modOLED096_I2C wyposażonego w miniaturowy wyświetlacz OLED z wbudowanym sterownikiem SSD1306 (rysunek 5).
- Modułu modHTU21 z czujnikiem wilgotności i temperatury HTU21 (rysunek 6).
W oparciu o wyżej wymieniony moduły, przygotowana zostanie prosta aplikacja AndroidThings-I2C, realizująca pomiar temperatury i ciśnienia w 1-sekundowych odstępach czasu. Uzyskane wyniki pomiarów będą prezentowane w postaci wykresów na ekranie podłączonym do wyjścia HDMI zestawu Raspberry Pi. Aby zmaksymalizować walor dydaktyczny, w realizowanym projekcie przyjęto następujące założenia:
- Obsługa modułu wyświetlacza SSD1306 zostanie zrealizowana z wykorzystaniem gotowych sterowników udostępnianych przez system Android Things i rozwijaną wraz z systemem bibliotekę Peripheral Driver Library [1].
- Do obsługi modułu modHTU21 zostaną wykorzystane funkcje bezpośredniej obsługi interfejsu I2C z biblioteki Things Support Library.
- Interfejs graficzny aplikacji zostanie zbudowany w oparciu o bibliotekę MPAndroidChart, tak aby zaprezentować łatwość wykorzystania otwartoźródłowych, zewnętrznych bibliotek rozwijanych aktywnie przez ogromną społeczność programistów systemu Android.
W typowych projektach embedded budowanych „od podstaw” i bez wykorzystania systemu operacyjnego, niezbędnym krokiem jest dokładne zapoznanie się z dokumentacją i programową obsługą wszystkich podłączonych komponentów (konfiguracją urządzenia, układem rejestrów, formatem wymiany danych, itp.). W zależności od złożoności układu (a tym samym obszerności jego dokumentacji), etap ten może być bardzo czasochłonny. Wykorzystanie rozbudowanych systemów operacyjnych oraz sterowników sprzętu udostępnianych przez te systemy, pozwala maksymalnie skrócić czas przygotowania warstwy programowej. Jak zostanie to przedstawione na przykładzie wyświetlacza z kontrolerem SSD1306, dzięki wykorzystaniu gotowego sterownika i czytelnego zestawu udostępnianych przez niego funkcji, programista może uniknąć żmudnego etapu studiowania dokumentacji i przygotowywania niskopoziomowej obsługi wyświetlacza. Aby jednak nie pominąć istotnych zagadnień związanych z obsługą magistrali I2C w systemie Android Things, obsługa czujnika HTU21 zostanie zrealizowana z wykorzystaniem „niskopoziomowych” funkcji zarządzających pracą magistrali (niskopoziomowych w sensie funkcji realizujących odczyt i zapis danych na magistrali, a nie obsługi sprzętowego kontrolera I2C poprzez zapis/odczyt rejestrów mikroprocesora).
Komplet połączeń sprzętowych pomiędzy płytką deweloperską Raspberry Pi a modułami modOLED096_I2C oraz modHTU21, przedstawiono na rysunku 7.
Warstwa programowa projektu AndroidThings-I2C
Korzystając z pakietu Android Studio, jak i informacji przedstawionych w poprzedniej części cyklu [LINK], utwórzmy nowy projekt – AndroidThings-I2C. Jeżeli nie korzystamy z przedpremierowej wersji Android Studio (oznaczonej numerem 3.0), należy pamiętać, aby dokonać odpowiednich modyfikacji plików build.gradle oraz AndroidManifest.xml, na potrzeby systemu Android Things.
Do budowy graficznego interfejsu użytkownika zostanie wykorzystana otwartoźródłowa biblioteka MPAndoridChart, udostępniona na licencji Apache 2.0, pod adresem https://goo.gl/v5BcKT.
Aplikacje tworzone w środowisku Android Studio korzystają z wydajnego i przystępnego dla użytkownika narzędzie Gradle [2] (zarządzającego procesem budowy projektu), dzięki czemu włączenie w projekt zewnętrznych bibliotek ogranicza się do edycji kilku linii skryptów budujących. Konfigurację zależności dla biblioteki MPAndroidChart, rozpoczniemy od edycji piku build.gradle, gdzie w sekcji repositories, dodamy repozytorium https://jitpack.io, w którym to skrypt budujący będzie poszukiwał bibliotek wymaganych dla etapu kompilacji:
allprojects {
repositories {
jcenter()
maven { url „https://jitpack.io” }
}
}
Po określeniu repozytorium, możemy przystąpić do edycji pliku app/build.gradle, gdzie w sekcji dependencies, zdefiniujemy właściwe zależności:
dependencies {
/…/
compile ‚com.github.PhilJay:MPAndroidChart:v3.0.2’
/…/
}
W projekcie AndroiThings-I2C, oprócz biblioteki MPAndroidChart, wykorzystana zostanie również biblioteka do obsługi wyświetlacza SSD1306. Aby usprawnić proces tworzenia aplikacji, deweloperzy z firmy Google, wraz z systemem Android Things rozwijają projekt biblioteki Peripheral Driver Library, która implementuje niskopoziomową obsługę najbardziej popularnych układów peryferyjnych. Kod źródłowy biblioteki wraz z gotowymi przykładami jej obsługi, został udostępniony pod adresami:
https://goo.gl/9fUPsM
https://goo.gl/CAvD2P
Włączenie sterownika SSD1306 z biblioteki Peripheral Driver Library wymaga ponownej edycji pliku app/build.gradle i dodania w sekcji dependencies nowej zależności, jak niżej:
dependencies {
/…/
compile ‚com.github.PhilJay:MPAndroidChart:v3.0.2’
/…/
}
Przed implementacją głównej funkcjonalności projektu, przystąpmy do edycji pliku layout/activity_main.xml i zdefiniowania prostego graficznego interfejsu użytkownika. Do budowy interfejsu wykorzystamy układ LinearLayout [3] z atrybutem Vertical, co oznacza, że wszystkie zdefiniowane elementy interfejsu, będą umieszczone kolejno od góry do dołu ekranu. Interfejs aplikacji będzie złożony z czterech komponentów: napisu wyświetlającego aktualną wartość temperatury (jako komponent TextView z bibliotek systemu Android), wykresu przedstawiającego wyniki ostatnich 60. pomiarów temperatury (wykres typu LineChart z biblioteki MPAndroidChart), napisu wyświetlającego aktualną wartość wilgotności względnej oraz wykresu jak dla pomiarów temperatury. Schematyczny rozkład komponentów został przedstawiony na rysunku 8.
Wszystkim komponentom interfejsu przypisujemy unikalną wartość pola ID (android:id), tak aby mieć możliwość do bezpośredniego odwołania i programowej zmiany ich parametrów z poziomu klasy MainActivity. Dla komponentów typu TextView ustawiamy pożądane wartości koloru (android:textColor), stylu (android:textStyle)
i rozmiaru czcionki (android:textSize), koloru tła (android:background), a także odległości dzielących je od pozostałych komponentów interfejsu (android:padding). Wizualne aspekty komponentów LineChart zostaną zdefiniowane bezpośrednio z poziomu klasy MainActivity. Kompletną postać pliku layout/activity_main.xml, przedstawiono na listingu 1.
Zatem przystąpmy do finalnej implementacji klasy MainActivity. Tworzenie aplikacji rozpoczniemy od obsługi czujnika HTU21. Do tego celu wykorzystamy niskopoziomowe funkcje obsługi interfejsu I2C oraz znany już z poprzedniego odcinka PeripheralManagerService. Poprzez metodę openI2cdevice() i określenie 7-bitowego adresu urządzenia Slave (w przypadku modułu HTU21, adres ma stałą, niekonfigurowalną wartość 0x40), uzyskajmy dostęp do magistrali I2C, jak na listingu 2.
Klasa I2cDevice [4] udostępnia szereg niskopoziomowych metod, umożliwiających wygodną obsługę magistrali. Wybrane metody klasy I2cDevice, zostały przedstawione w tabeli 1.
Metody z grupy writeReg*() oraz readReg*() pozwalają na wygodną obsługą urządzeń peryferyjnych posiadających organizację rejestrową (w której odczyt/zapis danych jest poprzedzony wskazaniem adresu urządzenia). Za pomocą tych pojedynczych metod realizujemy cały cykl działań na magistrali I2C (przesłanie adresu rejestru, adresu urządzenia Slave z odpowiednio ustawionym bitem kierunku przesyłu informacji, itd.), jak został to przedstawione na rysunku 3 oraz rysunku 4. Ponieważ moduł HTU21 nie posiada organizacji rejestrowej, do wymiany danych z modułem wykorzystamy funkcje write() oraz read(), które pozwalają nam uzyskać pełną kontrolę na danymi przesyłanymi na magistrali I2C.
System Android Things nie zawiera dedykowanego sterownika dla układu HTU21, więc JEST niezbędne zapoznanie się z dokumentacją układu i jego programową obsługą [5]. Lista komend obsługiwanych przez czujnik HTU21 została przedstawiona w tabeli 2. Na podstawie funkcji z tab. 1 oraz listy komend z tab. 2 przygotujmy generyczną funkcję, której zadaniem będzie pomiar zadanej wartości mierzonej w trybie „Hold Master”, a także sprawdzenie sumy CRC z wielomianem generującym X8+X5+X4+1 (CRC-8-Dallas). Kod funkcji htu21_readData() został przedstawiony na listingu 3.
Pomiar temperatury i wilgotności będzie realizowany w 1-sekundowych odstępach czasu. Do zapewnienia cykliczności pomiarów wykorzystamy mechanizm Runnable, który wykorzystany został również w implementacji „migającej diody” w pierwszej części cyklu poświęconej systemowi Android Things. Aby upewnić się, że moduł HTU21, do którego uzyskaliśmy dostęp we fragmencie kodu z listingu 2, odpowiada na przesyłane zapytania, przed uruchomieniem mechanizmu Runnable, prześlijmy komendę zerującą układ – SoftReset (0xFE). W tym celu rozbudujemy metodę onCreate() o fragment kodu pokazany na listingu 4. Dla pełnej czytelności kodu, warto przedstawić również implementację funkcji htu21_softReset() – pokazano ją na listingu 5.
Zgodnie z przyjętymi założeniami, wyzwolenie pomiarów temperatury i ciśnienia oraz aktualizacja graficznego interfejsu użytkownika będzie realizowana w 1-sekundowych odstępach czasu. Do zadań funkcji wyzwalających pomiar, będzie należało również odpowiednie przeliczenie wartości pomiarowych, przesłanych przez moduł HTU21. Dokumentacja modułu [5] definiuje dwa wzory, pozwalające na uzyskanie rzeczywistych wartości wielkości mierzonej, wyrażonej w stopniach Celsjusza (dla temperatury) oraz w procentach (dla wilgotności względnej):
Temp=–46,85+175.72×(STemp/216)
RH=–6+125×(SRH/216)
gdzie:
- STemp – 16-bitowa wartość odczytana z modułu po przesłaniu komendy 0xE3 lub 0xF3.
- SRH – 16-bitowa wartość odczytana z modułu po przesłaniu komendy 0xE5 lub 0xF5.
Implementacja funkcji realizującej wyzwolenie pomiarów oraz przeliczenie mierzonych wartości, została przedstawiona na listingu 6.
Jak można zauważyć, mimo braku dedykowanego sterownika, podstawowa obsługa układu HTU21, to tylko kilkanaście linii kodu. Dzięki funkcjom udostępnianym przez Things Support Library, programista nie musi zapewniać pełnej obsługi kontrolera interfejsu I2C (poprzez bezpośrednią pracę na rejestrach, kontrolę bitów ACK/NACK, itd.). Jeszcze lepiej sytuacja prezentuje się w momencie, gdy w systemie mamy dostępny gotowy sterownik urządzenia. Dodajmy więc zatem do naszego projektu prostą obsługę wyświetlacza z kontrolerem SSD1306, z wykorzystaniem sterownika udostępnionego przez bibliotekę Peripheral Driver Library.
Dzięki wykorzystaniu gotowego sterownika, programista nie musi znać żadnych aspektów niskopoziomowej obsługi wyświetlacza (adresu na magistrali, definicji rejestrów, itp.) – cała jego programowa obsługa została zawarta w kilku dobrze zdefiniowanych metodach obiektu Ssd1306 [6] – tabela 3.
Ponieważ nasz projekt posiada już zdefiniowany graficzny interfejs użytkownika (wyświetlany z pomocą ekranu dołączanego do portu HDMI), niech obsługa kontrolera SSD1306 ma charakter wyłącznie dydaktyczny – zadaniem miniaturowego wyświetlacza OLED będzie wyłącznie zaprezentowanie prostego wzoru graficznego (korzystając
z informacji zawartych w ramce poniżej, można nadać bardziej praktyczny charakter zadań przypisanych do wyświetlacza SSD1306, poprzez wyświetlenie „logo producenta” lub napisów informujących o błędach w odczycie czujnika HTU21). Kod implementujący wyświetlenie prostego wzoru szachownicy został przedstawiony na listingu 7.
Ostatnim elementem do pełnego skompletowania projektu jest konfiguracja wykresów – generowanych z wykorzystaniem biblioteki MPAndroidChart – oraz programowa obsługa interfejsu graficznego. W metodzie onCreate(), wywołanie setContentView() pobiera dane z pliku XML opisującego układ (w omawianym przypadku jest to plik layout/activity_main.xml), a następnie tworzy obiekty dla odpowiednich komponentów. Za pomocą szeregu wywołań findViewById(), pobieramy referencję do tych obiektów, co pozwoli następnie na zmianę wybranych ich wartości np. poprzez metodę setText() – listing 8. Po serii wywołań findViewById(), program wykonuje utworzoną na potrzeby projektu funkcję setupChart(), która konfiguruje pola wykresów dla pomiarów temperatury i wilgotności – listing 9.
Biblioteka MPAndroidChart, nie jest integralną częścią systemu Android Things, tak więc niniejszy artykuł nie będzie zawierał opisu jej konfiguracji i obsługi. Szczegółowy opis interfejsu programowego (API) biblioteki został umieszczony na stronie projektu pod adresem https://goo.gl/UAoif8. Do głównych zadań funkcji setupChart() należy konfiguracja osi wykresu (ustawienie wartości maksymalnej i minimalnej na osi Y, wielkości czcionki dla wartości opisujących oś, itp.), a także koloru tła i ustawień siatki. Wykresy inicjalizowane są pustym zbiorem wartości (poprzez metodę setData()), który wraz z realizacją kolejnych pomiarów, jest uzupełniany poprzez wywołanie addEntry(), jak na listingu 10.
Finalny efekt projektu AndroidThings-I2C zaprezentowano na poniższym krótkim filmie https://goo.gl/HpvKCB. Uporządkowany i skompletowany (również o nadpisanie metody onDestroy()) kod programu został przedstawiony na listingu 11.
Łukasz Skalski
Linki zewnętrzne: