Budowanie szkieletu aplikacji

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.

2 Comments

  1. Bartłomiej Świderek

    16 czerwca 2019 at 10:15

    Od jakiegoś czasu czytam Twoje artykuły i przyznam, że są bardzo inspirujące. Bardzo spodobało mi się podejście pokazane w tym artykule, odnośnie wydzielania modułów i inicjalizacji konkretnych portów w danym module, który tych portów używa. Jest to odmienne podejście do tego, z którym na ogół się stykam, a mianowicie, że piny konfigurujemy poza modułami w oddzielnym makrze bądź funkcji.
    Mam pytanie, gdzie w takim modułowym systemie zainicjalizować porty, których nie używamy? Stworzyć dodatkowy moduł?
    Szukam dobrego, spójnego wzorca aby poprawić jakość swojego kodu.

    • GAndaLF

      20 czerwca 2019 at 13:26

      Taka inicjalizacja z założenia będzie psuła modułowość. Kodu do nieużywanych portów nigdzie nie upchniemy. A chcemy uniknąć modułu GPIO, bo tak naprawdę każda grupa GPIO jest powiązana z konkretnym peryferium które w tym podejściu obsługujemy oddzielnie.

      Ja w większości przypadków bazowałem na domyślnym ustawieniu nieużywanych portów jako wejścia o wysokiej impedancji. Jak musisz ustawić nieużywane porty inaczej to najlepszym miejscem będzie jakaś procedura startowa na samym początku aplikacji. Przed inicjalizacją konkretnych modułów ustawiasz wszystkie GPIO na domyślne stany. Z kolei jeżeli interesują Cię stany jedynie jakiś konkretnych pinów – możesz do tego zrobić dedykowany moduł ustawiający je tylko na starcie. Przy okazji może to się przydać później kiedy się okaże, że coś więcej musisz z nimi robić.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *