-40% na bestsellery wydawnictwa HELION, min.: Java, C++, TensorFlow, PHP, MySQL. Sprawdź listę tytułów »

Raspberry Pi w robotyce amatorskiej – #3 – Programowanie

Raspberry Pi w robotyce amatorskiej – #3 – Programowanie

W pierwszych dwóch częściach poznaliśmy, czym właściwie jest komputerek i uruchomiliśmy go. Teraz, gdy mniej ważne sprawy nie zajmują nam czasu, możemy zająć się programowaniem.

Dowiemy się m.in. jak zmusić Raspberry Pi do rozmowy z innymi układami, wykorzystując do tego różne dostępne interfejsy komunikacyjne.

« Poprzedni artykuł z seriiNastępny artykuł z serii »

Hello World!

Pierwszy program, jaki napiszemy to, ku zdziwieniu większości, "Hello World"!

Najpierw sprawdzamy wersję kompilatora gcc:

Powinna pojawić się informacja podobna do tej poniżej:

Kiedy mamy kompilator, to mamy wszystko, co potrzebne. Tworzymy folder HelloWorld, a w nim plik hello.c:

A następnie w pliku:

Kolejnym krokiem jest zbudowanie naszego oszałamiającego programu:

Oraz instrukcja wykonania go:

W konsoli powinno pojawić się coś podobnego do:

 Gratulacje! Możemy się już nazywać programistami RPI! 

Wybór środowiska programistycznego

W poprzednim punkcie stworzyliśmy pierwszy, zaskakujący program za pomocą zwykłego edytora tekstu oraz bezpośredniego odpalenia kompilatora w terminalu. Efektywne, ale mało wygodne. Dlatego warto zastosować narzędzie do automatycznego budowania programu wraz ze wszystkimi dodatkowymi bibliotekami, kolorowaniem składni itd.

Mamy kilka opcji. Zaczynając od najprostszych edytorów wraz z pisaniem plików makefile, poprzez lekkie środowiska programistyczne takie jak Geany, po duże kombajny wraz z wieloma dodatkowymi bibliotekami jak Eclipse.

Gdy tworzę proste programy, korzystam z Geany, jednak te wykorzystujące bardziej zaawansowane mechanizmy piszę w połączeniu z biblioteką Qt 4.8 i środowiskiem QtCreator. Jest to co prawda koszmarnie wolny program, ale nie chciało mi się bawić w konfigurację Qt pod innym IDE. Instalujemy QtCreatora za pomocą komendy poniżej:

A następnie ustawiamy opcje kompilatora tak jak na screenie poniżej:

Ustawienia Toolchain w QtCreatorze na Raspberry Pi

Ustawienia Toolchain w QtCreatorze na Raspberry Pi

Jeśli chodzi o Geany to instalacja jest standardowa i nic nie trzeba konfigurować, można od razu kodzić:

Biblioteka wiringPi

Raspbian to prawie zwykły linux. Dlatego wszystkie operacje wykorzystujące niskopoziomowe interfejsy komunikacyjne można wykonać jako odpowiednie działania na odpowiednich plikach. Jednak, jak wszystko co niskopoziomowe, wymaga to od programisty dużego nakładu pracy. Oczywiście osiągnięty w ten sposób efekt jest zazwyczaj lepszy niż napisany za pomocą wyżej poziomowych funkcji – wszystko to kosztem naszego czasu.

Z uwagi na optymalizację czasu spędzonego nad określonym zadaniem, zdecydowałem się na wykorzystanie biblioteki wiringPi (strona domowa projektu). Aby zdobyć źródła biblioteki używamy gita:

Wchodzimy do ściągniętego katalogu i uruchamiamy narzędzie do budowania:

Sprawdzamy, czy instalacja na pewno się udała:

Gotowe! Biblioteki dynamiczne są w katalogu /usr/local/lib, zaś pliki nagłówkowe w katalogu /usr/local/include. Należy również poinformować kompilator, gdzie szukać  oraz jakich bibliotek wiringPi używać, co robimy poprzez dodanie tej informacji do linii budowania.

W różnych środowiskach może to wyglądać różnie: w Geany należy wejść w Build->SetBuildCommands, a następnie w okienku Build dopisać -lwiringPi, w Qt należy do pliku *.pro dopisać dwie linijki:

Do przestestowania możemy wykorzystać poniższy kod, który ma za zadanie zainicjować bibliotekę. Warto zaznaczyć, że w każdym programie, wykorzystującym jakąkolwiek funkcję wiringPi, należy na początku użyć funkcji wiringPiSetup().

Nazewnictwo pinów w wiringPi

W pierwszej części artykułu dowiedzieliśmy się, jak połączone są piny wejść/wyjść z układem SoC, co determinowało ich nazewnictwo. Biblioteka wiringPi korzysta jednak z innej nomenklatury, dlatego postanowiłem rozszerzyć wspomniane zestawienie o jeszcze jedną kolumnę.

Zestawienie pinów listwy GPIO wraz z nazwami według wiringPi

Zestawienie pinów listwy GPIO wraz z nazwami według wiringPi

General Purpose Input/Output

Mamy szereg możliwości manipulacji modułem GPIO. Aby jak najszybciej dobrać się do pinów malinki wystarczy nam... terminal. Zasada działania jest taka: wpisujemy sprecyzowane komendy do odpowiednich plików, co procesor później interpretuje i ustawia konkretne bity w rejestrach odpowiedzialnych za kontrolę wejść/wyjść.

Przechodząc do konkretów: aby aktywować dostęp do jakiegoś pinu, należy go „wyeksportować”, czyli wpisać jego numer (w nomenklaturze układu SoC, patrz Tab. 2. w pierwszej części artykułu). Dlatego jeżeli chcemy ustawić stan wysoki na GPIO_GEN0, to tak naprawdę będziemy manipulować pinem GPIO17. Najpierw jednak przechodzimy do trybu super user:

Następnie „eksportujemy” pin 17:

Teraz w /sys/class/gpio pojawił się katalog gpio17, a w nim kilka nowych plików, umożliwiających różne rodzaje działań: od ustawienia kierunku pinu (wejście/wyjście) po rodzaj zbocza, przy którym ma być zgłaszane przerwanie. My jednak ograniczymy się do zwykłej kontroli binarnej, ustawiając najpierw GPIO17 jako wyjście:

Ustawienie stanu wysokiego:

Teraz miernik w dłoń i podziwiamy naszą władzę nad Raspberry Pi. Aby zakończyć korzystanie z pinu należy go „odeksportować”:

Taki dostęp może jest prosty i szybki od strony użytkownika, jednak z perspektywy czasu wykonywania instrukcji to programistyczna porażka. Głównie z tego względu, że procesor musi się zajmować czymś, co może zrobić sprzętowo – czytając/pisząc w odpowiednich adresach pamięci układu Broadcom.

Owe adresy można znaleźć we wspomnianej w pierwszej części artykułu nocie katalogowej układu SoC. My jednak wykorzystamy do tego bibliotekę wiringPi. Poniżej przykład wykorzystania wszystkich funkcji dotyczących prostej manipulacji GPIO:

Pulse Width Modulation

Zegar PWM

Na początku omawiania tego peryferium należy przyjrzeć się nocie katalogowej układu Broadcom, gdzie można z nutką niepokoju stwierdzić, że na temat sprzętowego PWMu... prawie nic nie ma. Informacje są szczątkowe i niewystarczające (moim zdaniem) do zrozumienia istoty obsługi tego peryferium od strony programistycznej.

Na potrzeby właśnie takich sytuacji wymyślono internet. Okazuje się, że PWM jako jedno z peryferiów jest podpięty do szyny APB procesora, która jest taktowana zegarem o częstotliwości 250 MHz. Zegar PWM otrzymuje się poprzez dzielenie częstotliwości, w wyniku czego peryferium jest napędzane zegarem 19.2MHz.

Dzielnik stanowią dwa 12-bitowe rejestry w pamięci procesora. Pierwszy z nich zawiera informacje o części całkowitej, zaś drugi o jego części ułamkowej. Ze strony biblioteki wiringPi możemy ustawić tylko całkowitą część dzielnika za pomocą funkcji pwmSetClock(int div). Parametr ma wartość maksymalnie 4096 (czyli wypełniony jedynkami, wspomniany wcześniej 12-bitowy rejestr).

Warto również wspomnieć o tym, że omawiane tutaj sprzętowe PWMy w malinie są dwa, jednak jeden z nich jest wykorzystywany do złącza mini jack. Dlatego użytkownik ma dostęp tylko do jednego pinu z modulowaną szerokością impulsu - GPIO18 (według nomenklatury wiringPi - GPIO1).

Tryb Mark:Space

Należy zaznaczyć, że w Raspberry Pi dostępne są dwa tryby działania PWM. Pierwszy z nich to tak zwany Mark:Space, w którym priorytetem jest ustawienie kwantu czasu. Zarówno stan wysoki oraz cały przebieg to odpowiednie wielokrotności tej elementarnej długości. Ustawienia dokonujemy za pomocą funkcji pwmSetClock(int divisor), gdzie parametr divisor to wspomniany całkowity dzielnik częstotliwości zegara PWM (19.2MHz), otrzymywany według wzoru:

Elementy z indeksem q odnoszą się do omawianego kwantu czasu, odpowiednio częstotliwości oraz okresu (czasu trwania). W następnej kolejności należy wywołać dwie funkcje określające sam przebieg:

  • pwmSetRange(unsigned int range), gdzie parametr range określa okres (T) generowanego przebiegu PWM jako całkowitą wielokrotność kwantu czasu (Tq),
  • pwmWrite(int pin, int value), gdzie jako wartość pin podajemy liczbę 1 (odnosimy się do GPIO1), natomiast value będzie całkowitą wielokrotnością kwantu czasu (Tq), określającą długość stanu wysokiego (TH).

Rozważmy teraz przykład ustawienia przebiegu PWM o okresie równym 20ms oraz wypełnieniu na poziomie 1.5ms. Pierwszym krokiem jest ustawienie odpowiedniego kwantu czasu. W tym przypadku zdecydowałem się na 100us:

Parametr range funkcji pwmSetRange(unsigned int range) ustawiamy jako:

Parametr value funkcji pwmWrite(int pin, int value) ustawiamy jako:

Poniżej znajduje się listing programu ilustrującego ten przykład oraz screen otrzymanego przebiegu na oscyloskopie:

Przebieg sygnału PWM 1.5/20 ms wytworzonego przy pomocy metody MS

Przebieg sygnału PWM 1.5/20 ms wytworzonego przy pomocy metody MS

Tryb balanced

Drugim domyślnym, ale moim zdaniem mniej przydatnym trybem, jest balanced. Tutaj również priorytetem jest ustawienie odpowiedniego kwantu czasu, z tą różnicą, że długość trwania sygnału wysokiego jest równa temu kwantowi, a w locie obliczany jest jedynie okres całego przebiegu.

Konfigurując parametry przebiegu, zaczynamy od kwantu czasu według takiego samego wzoru, jak w poprzednim punkcie. Wiadomo, że stan wysoki będzie trwał dokładnie tyle samo, zaś okres całego przebiegu PWM będzie obliczany według wzoru:

Dlatego od razu widać, że wartości range oraz value same w sobie nie mają znaczenia, istotny jest ich stosunek. Przechodząc do przykładu, w tym trybie nie uzyskamy takiego przebiegu jak w poprzednim punkcie, ponieważ nie da się ustawić kwantu czasu równego 1.5ms. Winą oczywiście obarczamy fakt, że rejestr przechowujący dzielnik częstotliwości zegara PWM ma tylko 12 bitów:

Spróbujmy w takim razie ustawić przebieg o okresie 20us i wypełnieniu 1.5us. Wtedy:

Oraz:

Poniżej znajduje się listing programu ilustrującego ten przykład oraz screen otrzymanego przebiegu na oscyloskopie:

Przebieg sygnału PWM 1.5/20 us wytworzonego przy pomocy metody balanced

Przebieg sygnału PWM 1.5/20 us wytworzonego przy pomocy metody balanced

Soft PWM

A co jeśli mamy potrzebę stworzenia dwóch przebiegów PWM i nie chcemy kupować dodatkowych układów scalonych? Możemy oczywiście wykorzystać programowy PWM, który można postawić na dowolnym pinie, co ma jednak swoje ograniczenia.

Minimalna jednostkowa szerokość impulsu wynosi 100us, co w połączeniu z domyślnym okresem impulsu równym 100 jednostkowym szerokościom daje sygnał o częstotliwości 100Hz. Oczywiście można zmniejszyć zakres, co spowoduje zwiększenie częstotliwości przebiegu, ale odbije się na rozdzielczości. Podobnie można zwiększyć zakres, co spowoduje zwiększenie rozdzielczości, ale zmniejszenie częstotliwości. Dodatkowo, jak widać na poniższym przykładzie, sygnał jest wyraźnie mniej stabilny od sygnału generowanego przez sprzętowy PWM:

Przebieg sygnału PWM 1.5/20 ms wytworzonego przez programowy PWM

Przebieg sygnału PWM 1.5/20 ms wytworzonego przez programowy PWM

Kompilując powyższy program, należy pamiętać, by dodać bibliotekę -lpthread. Możemy teraz porównać przebiegi uzyskane przy użyciu sprzętowego i programowego PWMu.

Już na pierwszy rzut oka widać, że okres sygnału wygenerowanego przez sprzętowy PWM wynosi dokładnie 20ms. Analiza programowego PWMu pokazała, że przebieg przez niego wygenerowany ma pewne odstępstwo od żądanej wartości 20ms – sytuacja jest podobna w przypadku ustawianego wypełnienia.

Można również rzucić okiem na odchylenie standardowe sygnału, gdzie w przypadku sprzętowo generowanego przebiegu jest ono prawie dwa razy mniejsze niż w przypadku generacji programowej. Wszystko ma jednak swoje wady i zalety, dlatego warto znać oba sposoby, by móc odpowiednio dopasować  go do potrzebnego zastosowania.

Universal Asynchronous Receiver and Transmitter

Czas na próbę rozmowy Raspberry Pi z innymi urządzeniami poprzez najprostszy z interfejsów: UART. To peryferium jest w malince odrobinę niedopieszczone - ma pewne ograniczenia, o których można przeczytać w dokumentacji układu Broadcom (brak bitu parzystości, detekcji błędu ramki i inne).

Niemniej jednak nie najgorzej nadaje się do większości zastosowań – bez problemu można się porozumieć z mikroprocesorem, czy PCtem. Aby jednak zacząć zabawę, należy usunąć ustawienia trybu pinów UART z domyślnego debugowania. Realizujemy to poprzez zmianę zawartości plików konfiguracyjnych. Warto na wszelki wypadek zrobić kopię zapasową plików, które będziemy modyfikować:

Następnie usuwamy zapis console=ttyAMA0, 115200 oraz kgdboc=ttyAMA0,115200 z pliku /boot/cmdline.txt używając ulubionego edytora:

W drugim pliku usuwamy albo komentujemy ostatnią linijkę wstawiając '#' przed T0:23:respawn:/sbwin/getty -L ttyAMA0 115200 vt100, znów używając ulubionego edytora:

W ten sposób poprawnie przygotowaliśmy UART do działania. Po zwarciu pinów RXD (GPIO15) oraz TXD (GPIO14) możemy sprawdzić, czy UART działa jak należy, wykorzystując do tego poniższy program:

Przebieg sygnału na pinie TXD (GPIO14) przy wysyłaniu znaku 85 (binarnie 01010101)

Przebieg sygnału na pinie TXD (GPIO14) przy wysyłaniu znaku 85 (binarnie  01010101)

Sprawdźmy, czy wszystko się zgadza. W programie jest ustawiony baud rate jako 9600, czyli okres trwania jednego bitu powinien być równy:

Na załączonym screenie wyraźnie widać, że owe zależności są spełnione. Układ działa poprawnie i jest gotowy do dalszej pracy!

Serial Peripheral Interface

Nadszedł czas na zabawę kolejnym interfejsem komunikacyjnym w Raspberry Pi - SPI. Jest on jednak domyślnie wyłączony, dlatego przed jakimikolwiek czynnościami należy załadować moduł tego urządzenia. Można to zrealizować na różne sposoby, jednak najprostszym jest oferowana przez program gpio funkcja:

Aby sprawdzić, czy sterownik SPI został poprawnie zainstalowany w jądrze, wywołujemy:

I szukamy, czy pojawiły się wpisy: spi_bcm2708 oraz spidev. Jeżeli tak, to moduł jest zainstalowany i gotowy do użycia, wystarczy teraz zewrzeć pinyMOSI (GPIO10) oraz MISO (GPIO9), by móc za pomocą poniższego programu przetestować poprawność wysyłania oraz odbierania danych:

rzebieg sygnału na pinie MOSI (GPIO10) przy wysyłaniu znaku 129 (binarnie 10000001)

Przebieg sygnału na pinie MOSI (GPIO10) przy wysyłaniu znaku 129 (binarnie 10000001)

Warto zatrzymać się jeszcze przy jednej kwestii – częstotliwości taktowania zegara SPI. Autor biblioteki wiringPi podaje, że nie wszystkie częstotliwości są osiągalne przez SPI, jedynie ściśle określone:

  • 0.5 MHz,
  • 1 MHz,
  • 2 MHz,
  • 4 MHz,
  • 8 MHz,
  • 16 MHz,
  • 32 MHz.

Nawet jeśli w funkcji inicjującej SPI będzie podana inna wartość, to zostanie ona zaokrąglona do najbliższej ze wspomnianego szeregu. Sam autor biblioteki nie jest w stanie dokładnie wytłumaczyć zjawiska zaokrąglania. Wiadomym jest, że musi to mieć powiązanie z faktem, iż zegar SPI jest otrzymywany na podstawie odpowiedniego dzielenia częstotliwości zegara szyny APB (który, jak już wcześniej wspomnieliśmy, kręci się z częstotliwością 250MHz). Być może osiągnięcie częstotliwości 10MHz jest niemożliwe przy jakiejkolwiek konfiguracji dzielników częstotliwości zegara szyny APB.

Zakończenie

To już koniec naszej przygody z Raspberry Pi. Udało nam się omówić najważniejsze sprawy potrzebne do budowy własnych systemów (nie tylko) robotycznych, w których Raspberry Pi wcale nie musi grać pierwszych skrzypiec, ale może być na przykład jednym z ogniw łączących najbardziej wysokopoziomowe elementy systemu (interfejs użytkownika) z tymi niskopoziomowymi (czujniki, układy wykonawcze).

Możliwości wykorzystania komputerka w robotyce są naprawdę ogromne. Co prawda do większości zastosowań będzie zapewne potrzebny jeszcze jeden układ oparty o mikrokontroler, który będzie miał więcej portów IO, jednak jako jednostka obliczeniowa malinka sprawdza się świetnie. Mam nadzieję, że niniejszy cykl artykułów przyda się Wam i pozwoli na szybszy start z Raspberry dzięki zebraniu wszystkich najważniejszych informacji w jednym miejscu. Ciekaw jestem, co jesteście w stanie wymyślić z Raspebrry Pi na pokładzie?

« Poprzedni artykuł z seriiNastępny artykuł z serii »

konfiguracja, kursRPI, malinka, Raspbian Wheezy

Trwa ładowanie komentarzy...