Powrót

Wokół Arduino. Programowanie obiektowe

W serii artykułów „Wokół Arduino” wyjaśniamy trudniejsze zagadnienia dotyczące sprzętu i programowania. Mówimy o języku C, ale Arduino wykorzystuje C++. Przyjrzyjmy się temu nieco dokładniej.

Programowanie obiektowe – lekkie, łatwe i przyjemne

Chcemy, wręcz musimy zapoznać się z programowaniem obiektowym. Moglibyśmy zacząć mówić o klasach, instancjach, metodach i polach, a także o hermetyzacji (enkapsulacji, kapsułkowaniu), o dziedziczeniu i polimorfizmie (wielopostaciowości) oraz o klasach bazowych i potomnych (pochodnych), jak to jest w większości kursów programowania. Jest to niepotrzebne elektronikowi, zaczynającemu programować „niewielkie” w sumie mikrokontrolery jednoukładowe. Dlatego my zrobimy inaczej: o klasach i związanych z nimi kwestiach wspomnimy krótko i na końcu artykułu, a podkreślimy praktyczne znaczenie obiektów dla elektronika programującego procesory.

Trudne początki

Trudność nauczenia się programowania można porównać do próby wskoczenia do pociągu, który już od około 70 lat jest w ruchu i wciąż przyspiesza. Wskoczyć raczej się nie da, ale można i trzeba ten pociąg dogonić. Przypomnijmy historię.

Pojawienie się na przełomie lat 40. i 50. XX wieku komputerów cyfrowych (wtedy używane były też komputery analogowe) spowodowało pojawienie się nowej grupy zawodowej zajmującej się programowaniem. Powstanie i gwałtowny rozwój zupełnie nowej dziedziny, informatyki, spowodowały ujawnienie się rozmaitych problemów, ale i rozwiązań, które były bardzo dalekie od wszystkiego, z czym ludzkość miała do czynienia wcześniej. Po pierwsze do programowania komputerów zastosowano specyficzne rozwiązania matematyczne (logiczne), które dla „zwykłych śmiertelników” są zupełnie obce. To jeszcze pół biedy, bo można stosunkowo łatwo nauczyć się „komputerowego, algorytmicznego podejścia”. Ale wraz z rozwojem informatyki pojawiły się zagadnienia, które nie mają żadnego związku z życiem codziennym, doświadczeniem i tak zwanym zdrowym rozsądkiem. W informatyce mamy mnóstwo abstrakcyjnych zagadnień, niemających odpowiedników czy analogii w życiu codziennym.

A jeśli chodzi o zamierzchłą historię, pierwsze komputery nie miały ani klawiatur, ani ekranów. Urządzeniem wyjściowym była drukarka, a raczej odmiana elektrycznej maszyny do pisania. Na samym początku programowanie było męczącym i żmudnym procesem, polegającym na dziurkowaniu specjalnych kart lub taśm. Najogólniej biorąc, programista musiał określić wszystkie szczegóły: jakie operacje ma wykonywać komputer i jak korzystać z dostępnej niewielkiej pamięci. Wykorzystywał przy tym zero-jedynkowy tzw. język maszynowy, bezpośrednio zrozumiały dla danego komputera. Program pisany w języku maszynowym nierozłącznie związany był ze szczegółami budowy danego komputera.

Dużą zmianą było wprowadzenie urządzenia wejściowego w postaci klawiatury z adaptowanego dalekopisu (teleksu) oraz urządzenia wyjściowego w postaci ekranu, wyświetlającego znaki – napisy oraz prostą grafikę.

Dla ułatwienia, zamiast pisać wprost w językach maszynowych rozkazy będące ciągami zer i jedynek, zaczęto się posługiwać asemblerami. Termin asembler oznacza zarówno prosty język programowania (niskiego poziomu), będący bezpośrednim odbiciem, a wręcz łatwiejszą do zapamiętania wersją kodu maszynowego, jak też pomocniczy program komputerowy, ułatwiający pisanie „programu właściwego” w języku asemblera.

Z czasem tworzone programy stawały się coraz obszerniejsze, a ścisłe powiązanie programu ze sprzętem stawało się coraz większą wadą. Problem w tym, że stopniowo pojawiło się kilka głównych rodzin komputerów i „przeniesienie” programu napisanego w asemblerze danego komputera na komputer innej rodziny równało się napisaniu go od nowa.

Aby polepszyć sytuację, stopniowo już od lat 50. XX wieku wprowadzano różne języki programowania wyższego poziomu. Jednym z celów było uniezależnienie programu od sprzętu.

Program napisany w języku wyższego poziomu mógł być uniwersalny, a stosownie do potrzeb można go było zamienić na kod maszynowy danego typu komputera za pomocą odpowiedniego programu (kompilatora lub interpretera).

Odnotowujemy, że wprowadzenie języków wyższego poziomu było ważnym krokiem w rozdzielaniu szczegółów dotyczących sprzętu od szczegółów dotyczących programowania. Oznaczało to, że programista nie musiał już znać szczegółów budowy i działania komputera. Mógł skupić się na programie, czyli na stworzeniu przepisu na realizację takiego czy innego zadania.

W latach 50., 60. i 70. powstawały języki stworzone specjalnie do rozwiązywania zadań z różnych dziedzin. Tym skądinąd interesującym wątkiem nie będziemy się zajmowali. Ważniejsze jest to, że wraz z rozwojem zmieniała się metodyka programowania.

Ściślej biorąc, na samym początku nie było żadnej metodyki. Mówiąc najprościej, programista „kombinował”, jak z pomocą maszyny o danych cechach i możliwościach zrealizować dane zadanie – jak napisać program, żeby mając określone „dane wejściowe”, uzyskać „na wyjściu” potrzebne wyniki. Ogólnie biorąc, program mógł być napisany jakkolwiek, byle tylko zrealizować postawione zadanie. Pierwsi programiści wypracowywali własne sposoby i style pracy. Niektóre stosowane sposoby i rozwiązania były gorsze, inne lepsze.

Podział „na kawałki”

Zdobywane doświadczenia wskazywały, że w związku z rozwojem oraz wzrostem stopnia złożoności i objętości programów, korzystne jest stosowanie pewnych standardowych rozwiązań: szablonów i „specjalizowanych kawałków kodu” – procedur realizujących poszczególne drobne zadania. Mówiąc najprościej i najogólniej, chodziło o nadanie programowi łatwiejszej do zrozumienia struktury i wykorzystanie standardowych kawałków kodu.

Kolejnym krokiem było jeszcze wyraźniejsze oddzielenie poszczególnych procedur i nadanie im standardowej formy. I tak pojawiły się funkcje, jako oddzielne, autonomiczne kawałki kodu. Doskonale widać to w powstałym na przełomie lat 60./70. języku C, gdzie każda funkcja musi być ściśle określona. Standardowo funkcja ma wejście (przyjmuje argumenty) i wyjście (zwraca wartość). Funkcje porównaliśmy do większych i mniejszych skrzynek z korbką, wejściem i wyjściem. Program może zawierać mnóstwo takich gotowych skrzynek. Funkcje można napisać samemu, ale można też skorzystać z mnóstwa gotowych funkcji „standardowych”. Przy takim podejściu „program właściwy” może być krótki, bo zawiera tylko odwołania (wywołania) wielu gotowych funkcji składowych. Pisanie programu jest zdecydowanie ułatwione, bo nie trzeba zajmować się wszystkimi szczegółami. Dzięki wykorzystaniu funkcji łatwiejsze jest pisanie złożonych programów. Łatwiejsze, ale przy coraz bardziej skomplikowanych zadaniach takie programowanie proceduralne i strukturalne z wykorzystaniem funkcji też okazuje się żmudne.

Kolejny ważny krok

Dla dalszego ułatwienia wprowadzono programowanie obiektowe (zorientowane obiektowo), oznaczane OOP (Object Oriented Programming). Początki programowania obiektowego sięgają lat 60. (Simula67). W latach 70. opracowano obiektowy język Smalltalk, a pod koniec lat 70. Bjarne Stroustrup zaproponował odmianę języka C z klasami, która na początku lat 80. upowszechniła się jako C++. Później koncepcję lub tylko elementy obiektowości wprowadzono do wielu języków programowania. Był to z jednej strony kolejny krok odcinania się od sprzętu. Ale, co ciekawe, z drugiej strony jakby krok do tyłu, wykorzystanie koncepcji mniej abstrakcyjnej, a bardziej zbliżonej do codziennego doświadczenia.

W języku C „standardową autonomiczną jednostką” jest funkcja, którą przyrównaliśmy do skrzynki z korbką oraz wejściem i wyjściem. Pokręcenie korbką oznacza zrealizowanie danej funkcji, co ogólnie oznacza pobranie danych z wejścia, ich przetworzenie i podanie na wyjście. Funkcja może być „mniejsza” lub „większa”, ale jej główną cechą jest to, że przeznaczona jest do realizacji jakiegoś jednego elementarnego zadania – stąd jedna korbka.

W programowaniu obiektowym standardową jednostką jest obiekt, który można porównać do skrzyni, dużo większej niż funkcja.

Do skrzyni, która ma więcej niż jedną korbkę, a zamiast jednego wejścia i wyjścia, może mieć więcej wejść/wyjść.

W (strukturalnym) języku C mamy różnego typu zmienne i rozmaite funkcje, które na tych zmiennych operują.

Zmienne i funkcje…

Można obrazowo powiedzieć, że obiekt jest dużym autonomicznym tworem, zamkniętą całością, w której zmienne i funkcje dotyczące tego obiektu są ze sobą ściśle powiązane, tworząc jednolitą całość. Tylko nazwy są inne…

Wchodzące w skład obiektu zmienne nazywa się polami. Natomiast wchodzące w skład obiektu funkcje nazywa się metodami. A program obiektowy jest zbiorem obiektów, które komunikują się ze sobą w celu realizacji wyznaczonego zadania.

W przypadku „pojedynczej” skrzynki – funkcji pokręcenie korbką powodowało zwrócenie wartości, czyli niejako automatyczne przekazanie tego wyniku do „głównego przebiegu programu”. Podkreślmy, że w programowaniu strukturalnym wywołanie funkcji w ogólnym przypadku powoduje właśnie to, że zwraca ona wartość i przekazuje ją do programu głównego. W programowaniu obiektowym jest podobnie. Zapamiętaj, że pola w obiektach są odpowiednikami występujących w funkcjach zmiennych. Do pól, czyli zmiennych, można wpisywać dane, a można też odczytywać ich zawartość.

Można powiedzieć, że pokręcenie jedną z wielu korb obiektu jest odpowiednikiem realizacji jakiejś „pojedynczej” funkcji – metody, dostępnej w danym obiekcie. Najogólniej biorąc, realizacja metody, czyli funkcji, polega na pobraniu danych z określonego pola lub pól obiektu, przetworzenie ich i zwrócenie wyniku (na zewnątrz lub na wyjście, którym też może być jedno z pól obiektu). Obiekty mogą być wykorzystywane przez „główny program”, który ma dostęp do pól (publicznych) i do metod wszystkich obiektów. I mniej więcej coś takiego mamy w przypadku Arduino i programowania mikrokontrolerów jednoukładowych.

Jeszcze jeden krok

Można sobie też wyobrazić sytuację, że nie ma „głównego programu”, tylko jest niezależna współpraca autonomicznych obiektów. Wtedy „obiekty nic nie robią i czekają”. Dopiero gdy nastąpi jakieś zdarzenie, jakieś działanie – zostanie wywołana określona metoda któregoś z obiektów, może to wywołać także kolejne metody innych obiektów. Można to nazwać sterowaniem lub programowaniem zdarzeniowym. I takie rozwiązania są dziś wykorzystywane bardzo często, na przykład we współczesnych aplikacjach na smartfony i komputery, gdzie programowanie obiektowe jest standardem. Co ciekawe, tworzenie takich aplikacji z wykorzystaniem obiektów (i w tak zwany wizualny sposób) okazuje się zaskakująco proste i łatwe! Ale to odrębna sprawa, do której jeszcze wrócimy, także w kontekście Arduino.

Znakomite ułatwienie

Czy już widzisz, jaki jest sens i jaka korzyść z podejścia obiektowego?

Najogólniej biorąc, programowanie obiektowe to znakomity sposób, pozwalający na łatwiejsze pisanie nawet bardzo skomplikowanych programów. Program jest zdecydowanie bardziej przejrzysty, ponieważ wykorzystuje autonomiczne, najczęściej już gotowe obiekty!

Wystarczy jednorazowo stworzyć obiekt o potrzebnych właściwościach, a potem wielokrotnie korzystać z zawartych w nim pól i metod. Zdecydowanie łatwiej zapanować nad całością, która jest przejrzyście podzielona na szereg oddzielnych, autonomicznych obiektów.

To jest dokładnie tak jak w naszej codziennej rzeczywistości: nasz świat składa się właśnie z obiektów. I obiekty te mają jakieś właściwości i cechy, które możemy zapisać w polach (zmiennych). Z obiektami związane są też specyficzne dla nich metody (funkcje, działania).

Stworzenie obiektów i korzystanie z nich w opisany sposób jest genialnym pomysłem z kilku względów.

Programista nie musi znać wszystkich szczegółów, ponieważ może wykorzystać gotowe, autonomiczne, w pewnym sensie zamknięte obiekty, w większości zrealizowane przez innych programistów. Wykorzystanie obiektu według cudzego przepisu nie wymaga znajomości szczegółów działania, a jedynie wiedzy: Jakie „publiczne” pola ma ten obiekt? I jakie są dostępne w związku z nim metody? Natomiast szczegóły „budowy i działania obiektu” można spokojnie pominąć.

Oczywiście programista, jeśli chce, może stworzyć obiekt (obiekty) według własnego szczegółowego przepisu – i Ty przekonasz się, że może to być zaskakująco łatwe.

My na razie jesteśmy na etapie programowania mikrokontrolerów jednoukładowych. Także tu można i warto wykorzystywać obiekty. I właśnie platforma Arduino bardzo często, tylko „po cichu” wykorzystuje dobrodziejstwo obiektowości dostępne w języku C++. Z punktu widzenia użytkownika Arduino sprawa obiektowości jest bardzo prosta i tak oczywista, że jej… praktycznie nie widać.

Wystarczy napisać „program główny”, który jak najczęściej będzie wykorzystywał gotowe obiekty, a mianowicie będzie zapisywał i odczytywał pola oraz wywoływał  metody poszczególnych obiektów.

 Obiekty w Arduino

Omawiane tu obiekty, które mogą się wydawać dziwnymi, abstrakcyjnymi „tworami programistycznymi”, w Arduino okazują się czymś naturalnym, ponieważ są związane z konkretnymi fizycznymi urządzeniami.

Tylko nie są nazywane obiektami!

Potwierdza się, że Arduino to zasłona, która przykrywa także obiektowość. Aby nie straszyć pojęciami takimi jak klasy i instancje, w Arduino mówimy po prostu o bibliotekach. Ale w programie jak najbardziej wykorzystujemy (często zupełnie nieświadomie, ale jak najbardziej zgodnie z intuicją) obiekty z ich polami i metodami!

Płytka Arduino z procesorem jest sercem systemu, który zawiera też inne elementy (moduły). I właśnie do bardzo wygodnej obsługi tych dodatkowych elementów – modułów warto w programie wykorzystać obiekty (i klasy). W Arduino zamiast o klasach, mówimy o bibliotekach, ale najczęściej są to klasy języka C++. Klasy możemy stworzyć sami, ale właśnie ogromną zaletą Arduino jest to, że inni już dawno takie klasy stworzyli, nazwali bibliotekami i udostępnili do bezpłatnego użytku.

Przykładowo chcemy zrealizować projekt termometru/higrometru, zawierający Arduino UNO, czujnik DHT22 i wyświetlacz znakowy LCD 16×2. W programie i czujnik DHT22, i moduł wyświetlacza są reprezentowane jako gotowe obiekty. Nie musimy w programie Arduino żmudnie rozpisywać szczegółowych procedur obsługi DHT22 ani rozkazów sterujących poszczególnymi końcówkami wyświetlacza LCD. Wszystkie szczegółowe procedury – metody są już gotowe w bibliotekach – klasach, a my w prosty sposób wykorzystujemy pola i metody dostępne w stworzonych w programie obiektach. Nie nazywamy ich polami i metodami, tylko albo piszemy krótki, prosty program, albo częściej modyfikujemy gotowy, przykładowy program – szkic podobnego rozwiązania.

Korzystamy z obiektów!

Podobnie postępujemy w przypadku mnóstwa innych czujników i innych modułów, takich jak serwomechanizmy, silniki krokowe, przetworniki ADC czy DAC, zegary czasu rzeczywistego RTC, czytniki kart FLASH i tak dalej. Szukamy potrzebnego modułu i odpowiedniej dla niego arduinowej biblioteki. Potem świadomie czy nie, w programie tworzymy obiekt i wykorzystujemy jego gotowe metody.

Proste, łatwe i przyjemne!

A wszystko dzięki obiektom i klasom.

Te straszne klasy

Podane tu wyjaśnienia są mocno uproszczone. Owszem, finalne znaczenie mają obiekty, ale pominęliśmy (celowo zresztą) bardzo ważny problem klas.

Jeśli poszukasz materiałów o programowaniu obiektowym, większość informacji dotyczyć będzie hermetyzacji (enkapsulacji, kapsułkowania), dziedziczenia i polimorfizmu (wielopostaciowości), czyli w sumie klas, w tym klas bazowych i potomnych (pochodnych) oraz kwestii pokrewnych.

Zagadnienia te są bardzo ważne dla profesjonalnych programistów. Elektronikowi programującemu mikrokontrolery jednoukładowe wystarczy elementarna wiedza na ten temat. A podstawy są w sumie proste.

Trzeba wrócić do wcześniej pominiętej kwestii: skąd biorą się obiekty, a raczej jak w programie tworzymy obiekty?

Nie, nie, nie, nie bój się! Nie będziemy szczegółowo omawiać konstruktorów (i destruktorów) a także zasad zarządzania pamięcią RAM.

Podstawy są bardzo proste: obiekty tworzymy, konstruujemy na podstawie szablonów, wzorców. Szablony te nazywa się fachowo klasami, więc tworzone obiekty stają się tzw. instancjami klasy.

Słowo instancja kojarzy się przede wszystkim z sądownictwem i szczeblami władzy. Ale u programistów instancja to pojedyncze wystąpienie niezależnego kodu zgodnego z danym wzorcem (według Wikipedii).

Tym wzorcem jest klasa (ang. class). Natomiast obiekt jest niezależnym bytem, wystąpieniem, instancją danej klasy. Klasa nie jest obiektem. Klasa jest wzorcem, szablonem i przepisem na obiekt.

Na początku programu na podstawie szablonu – klasy tworzymy, konstruujemy jeden obiekt lub więcej obiektów danej klasy. Każdy obiekt ma swoją własną nazwę. Potem program „działa na tych obiektach”.

Te podstawy naprawdę są jasne i proste. W rzeczywistości klasy – biblioteki mogą być i często są skomplikowanymi tworami, do których zastosowanie mają zaawansowane zagadnienia programistyczne.

I tak hermetyzacja, inaczej enkspsulacja czy kapsułkowanie, to najprościej mówiąc, udostępnienie programiście, czyli Tobie, tylko elementów najważniejszych: wspomnianych pól i metod, a ukrycie przed nim szczegółów „wewnętrznych”. Przypomnijmy, że obiekt zwykle składa się nie tylko z widocznych z zewnątrz pól publicznych, ale też prywatnych „pól pomocniczych”, które są konieczne do prawidłowego zrealizowania metod, ale nie są dostępne z zewnątrz. Ukrycie tych prywatnych pól zabezpiecza przed przypadkowymi błędami. I o to z grubsza chodzi w hermetyzacji.

Kolejny tajemniczy termin: dziedziczenie ma związek z faktem, że często w programie mamy wiele podobnych obiektów. Niektóre są praktycznie identyczne, stworzone jako instancje tej samej klasy i różnią się tylko wartościami zapisanymi w polach. Inne są podobne, ale na przykład mają mniej lub więcej dodatkowych pól lub metod. Dziedziczenie w sumie dotyczy tworzenia zmodyfikowanych, nowych klas pokrewnych (pochodnych, potomnych), które są w pewnych szczegółach różne od klasy macierzystej (rodzicielskiej) i które posłużą potem do stworzenia nowych obiektów, mniej lub więcej różniących się od obiektów stworzonych z klasy rodzicielskiej. Sprawą dziedziczenia nie musisz się przejmować – to zmartwienie zawodowych programistów (i twórców arduinowych bibliotek). My przy programowaniu mikrokontrolerów korzystamy zwykle z gotowych klas, nie zastanawiając się nad dziedziczeniem.

Podobnie nie musisz nic wiedzieć na temat polimorfizmu (wielopostaciowości), co z grubsza biorąc dotyczy obsługiwania różnego rodzajów (typów) danych w jednolity sposób. To są jednak zagadnienia dla zawodowych informatyków.

My chcemy programować mikrokontrolery, korzystając z platformy Arduino i wystarczą nam tylko drobne okruchy wiedzy o programowaniu obiektowym i o klasach.

Na początek powinieneś tylko zapamiętać, że klasa to swego rodzaju przepis i szablon. Biblioteki Arduino to klasy języka C++. Na podstawie klasy możemy w programie – szkicu stworzyć, skonstruować jeden lub więcej obiektów. Pamiętaj! W programie może być wiele obiektów danej klasy. Obiekt zawiera opisujące go pola, które są w istocie specyficznym typem zmiennych. Z danym obiektem związane są metody, czyli dostępne funkcje charakterystyczne dla danej klasy (i wszystkich powstałych z niej obiektów).

Wszystko to może na początku wydaje się dziwne, ale w praktyce okazuje się proste, a w każdym razie idea gotowych obiektów konstruowanych z szablonów – klas (bibliotek) jest oczywista i intuicyjna. Korzystamy z niej w każdym programie Arduino. Jak na razie hobbyści elektronicy zaczynają od Arduino i bardziej lub mniej wykorzystują język C. Zupełnie inna kwestia to błyskawiczne upowszechnianie się obiektowego języka Python („pajton”). Co prawda z kilku względów nie jest on najlepszy do programowania małych mikrokontrolerów, niemniej także elektronicy coraz częściej mają do czynienia z „pajtonem” czy jak kto woli, pytonem. Może obok cyklu o krokodylu, powinien też powstać cykl „pyton jest zupełnie inny”… Może w związku z automatyką domową (Smart Home) i platformą Home Assistant. Ale to zupełnie inna historia.

Piotr Górecki