Zgodnie z założeniami, które nakreśliłem we wpisie o architekturze systemu, zabrałem się do projektowania prototypów funkcji poszczególnych bloków. Dzięki temu mogę zbudować szkielet aplikacji przechodzący przez wszystkie warstwy i stopniowo wypełniać go kodem. Główny nacisk położyłem na driverach powiązanych z warstwą sprzętową. Zależy mi na szybkim zaimplementowaniu driverów, żeby można było przetestować poprawność pracy poszczególnych podzespołów na docelowej płytce. Poza tym hardware jest najbardziej zależny od rzeczy narzuconych odgórnie i dobrze jest sprawdzić jak najszybciej, czy planowana koncepcja jest na pewno realizowalna. Zaktualizowany kod znajduje się na GitHubie projektu na gałęzi dev.

API driverów sprzętowych

Publiczne API driverów starałem się zaprojektować w taki sposób, żeby do wyższych warstw docierało jak najmniej szczegółów dotyczących sprzętu. Każdy z driverów ma swoją funkcję init, która nie przyjmuje żadnych parametrów. Posiada też oddzielne funkcje przyjmujące dane wejściowe dla drivera i zwracające wyniki.

Funkcja init konfiguruje do pracy peryferium oraz związane z nim porty GPIO, kanały DMA, timery itp. Funkcje init będą działały bezpośrednio na rejestrach procesora. Nie używam bibliotek ST do ustawiania peryferiów. Nie potrzebuję też bardziej elastycznych driverów z możliwością ustawiania tego samego peryferium w różne tryby. Plusami takiego rozwiązania są szybkość działania, lepsze zrozumienie działania peryferium i brak potrzeby uczenia się bibliotek od ST.

Nie we wszystkich driverach udało się zachować pełne odseparowanie od sprzętu. Driver ADC zwraca zmierzone wartości z 6 fototranzystorów i monitora baterii. Na zewnątrz musi być udostępniona informacja który kanał ADC mierzy którą wartość. Nie jest to duża przeszkoda i poradziłem sobie z nią dodając oddzielny moduł fototranzystorów, który woła funkcje ADC z odpowiednimi parametrami. Dzięki temu task zajmujący się wykrywaniem ścian nie będzie zaśmiecony niskopoziomowymi informacjami dotyczącymi ADC.

Innym problemem był driver I2C master. Aby dokonać odczytu z modułu IMU po I2C najpierw trzeba mu wysłać adres, z którego chcemy czytać, a dopiero potem rozpocząć odczyt. Przez to API drivera jest trochę nieintujcyjne, bo wywołując funkcję odczytu I2C musimy do bufora na odczytane dane najpierw wpisać rejestr. Docelowo ta zależność od sprzętu również zostanie prawdopodobnie przeniesiona do oddzielnego modułu.

Pozostałe moduły

Zrobiłem również prototypy modułów wykrywania ścian, obsługi silników, obsługi sensorów IMU oraz monitora baterii. Na razie są to puste funkcje, niektóre tylko wołają jakieś funkcje driverów. Modułów wyższego poziomu zajmujących się estymacją, mapowaniem labiryntu i wyznaczaniem ścieżek jeszcze nie zacząłem. Nie ma też jeszcze modułów związanych z interfejsem użytkownika i debugiem. Debug też będę chciał uruchomić szybko, żeby móc sczytywać dane z robota podczas jego pracy.

Podsumowanie

Zastosowane podejście do projektowania systemu, gdzie najpierw skupiam się na publicznych interfejsach w modułów i nie wchodzę od razu w szczegóły implementacyjne bardzo mi się spodobało. Dzięki temu połączenia między modułami są dobrze przemyślane, a nie narzucone przez istniejącą implementację. Muszę jednak przyznać, że trochę się wspomagam rozwiązaniami i doświadczeniami z poprzednich wersji robota. Nie wiem, czy był bym tak w stanie zrobić w zupełnie nieznanym systemie.