Każdy współczesny komputer osobisty czy smartfon ma procesor z więcej niż jednym rdzeniem. Prognozuję, że wkrótce podobnie będzie z autopilotami, więc już teraz zdecydowałem się na dwurdzeniowy mikrokontroler, pomimo że współczesne autopiloty radzą sobie z jednym. Dzisiaj sobie radzą, ale jutro mogą potrzebować więcej, więc teraz jest czas na zdobycie doświadczenia i wypracowanie narzędzi do obsługi wieloprocesorowości.

Mikrokontroler w APL3 ma dwa rdzenie: CM7, czyli Cortex-M7 – szybki i nowoczesny jak na kontrolery rdzeń, pracujący z częstotliwością 480 MHz, obsługujący dużą ilość pamięci, oraz CM4, czyli Cortex-M4 – tradycyjny rdzeń będący rozszerzeniem popularnego Cortex-M3 o jednostkę zmiennoprzecinkową, pracujący z częstotliwością 240 MHz. Jako punkt odniesienia weźmy System FPV w wersji 2.x, gdzie kontroler miał rdzeń Cortex-M3 pracujący na 72 MHz, albo APL2 również z Cortex-M3 pracującym na 120 MHz, gdzie ich wydajność spokojnie wystarczała, aby można było latać. 

Podstawowym wyznacznikiem wydajności autopilota jest prędkość aktualizacji jego pętli głównej, czyli cyklu obsługi wszystkich czujników, a w szczególności czujników jednostki inercyjnej IMU, pozwalającej na wyznaczenie orientacji w przestrzeni i aktualizacji swojej pozycji przez mechanizmy wykonawcze, czyli serwa lub silniki. W APL3 pętla główna obecnie jest aktualizowana z częstotliwością ok. 1900 Hz. Rdzeń CM4 obsługuje właściwy autopilot z wyłączeniem obsługi loggera na karcie SD i telemetrii. Rdzeń CM7 obsługuje multimedialny interfejs użytkownika, kamerę i obróbkę obrazu oraz czujniki zbliżeniowe: sonary i lidary. Docelowo CM7 ma również wyznaczać trasę lotu dla CM4 na podstawie danych ze swoich czujników, w szczególności obrazowego rozpoznania trasy lotu. Ze względu na różnorodność obsługiwanych funkcjonalności pracuje pod kontrolą systemu operacyjnego RTOS, obsługując wiele wątków, więc tutaj trudno mówić o częstotliwości obsługi, gdyż jest ona inna dla każdego wątku.

W sytuacji, gdy CM4 ma komplet informacji o stanie autopilota, a CM7 obsługuje interfejs użytkownika, w tym logger i telemetrię, pojawia się potrzeba wymiany informacji między nimi. Zostało to zrobione na bazie wspólnej pamięci SRAM4. W 2 kB zarezerwowanej w tym celu pamięci są zdefiniowane 2 tablice danych typu uint32_t: nBuforWymianyCM4[] i nBuforWymianyCM7[]. Każdy z rdzeni ma swoją kopię tych buforów w postaci unii uDaneCM7 i odpowiednio uDaneCM4, łączących powyższe tablice uint32_t oraz struktury danych wychodzących z każdego rdzenia. Unia jest po to, aby z jednej strony móc zapisać w niej dane o dowolnym typie i rozmiarze, a z drugiej strony móc efektywnie wykonać transfer danych do i ze wspólnej pamięci pełnymi 32-bitowymi słowami. Co kilka obiegów pętli oba rdzenie zapisują swoje dane do wspólnego bufora wymiany i odczytują dane drugiego rdzenia.

Ponieważ może zaistnieć sytuacja, że jeden rdzeń będzie czytał pamięć, a drugi jednocześnie ją zapisywał, mogłoby to spowodować konflikt elektryczny na magistrali oraz niespójność danych. Aby temu zapobiec dostęp do wspólnej pamięci jest w danej chwili przydzielany tylko jednemu rdzeniowi, a mechanizmem tym sterującym jest jeden z 32 sprzętowych semaforów HSEM. Jeżeli rdzeń próbuje uzyskać dostęp do bufora wymiany, a jest on zajęty przez inny rdzeń, to taki cykl dostępu jest pomijany. Wymiana danych następuje wielokrotnie szybciej niż ich logowanie lub wysyłanie w telemetrii, więc pominięcie jednej aktualizacji danych jest praktycznie niezauważalne.

Przy znanej częstotliwości pracy pętli głównej CM4 programista musi zdefiniować częstotliwość pracy wątku wymiany danych, aby pracował ze zbliżoną częstotliwością, aby było jak najmniej konfliktów między oboma asynchronicznymi procesami. Istotne jest też to aby dopasować tempo produkcji danych do możliwości ich konsumpcji po drugiej stronie. Gdyby w przyszłości trzeba było skalować obciążenie rdzenia, można podzielić dane na grupy o różnej częstotliwości wymiany. Dane z GPS, pomiary temperatur czy napięcie akumulatora mogą być aktualizowane wolniej niż dane jednostki inercyjnej.

Wraz z rozwojem oprogramowania pojawiło się zapotrzebowanie na świadczenie usług związanych z zasobami dostępnymi dla każdego z rdzeni. Podstawową usługą rdzenia CM4 jest dostęp do konfiguracji autopilota zapisanej w pamięci FRAM. Magistralą SPI obsługującą FRAM zarządza CM4, a aplikacja Naziemnej Stacji Kontroli komunikuje się z rdzeniem CM7, więc CM7 musi prosić CM4 o przysługę odczytania, zapisania lub przeładowania zapisanej konfiguracji. Po serii eksperymentów zdecydowałem się na wyodrębnienie kilku zmiennych przekazywanych razem z danymi:

  •  cWykonajPolecenie – identyfikuje numer polecenia obejmującego dane zadanie
  •  cPotwierdzenieWykonania – zmienna przesyła w drugą stronę identyfikator obsłużonego zadania. Po tym wiadomo, że zadanie zostało wykonane. Bez tego mechanizmu zadanie byłoby wykonywane przez cały czas, aż do zdefiniowania kolejnego. Pierwotnie przewidziałem zadanie puste, które miało być ustawione po wykonaniu ostatniego zadania, ale trudno było jednoznacznie stwierdzić, czy i kiedy wykonało się zlecone zadanie. Asynchronicznie pracujący rdzeń „serwera usług” ustawia identyfikator polecenia w zmiennej potwierdzenia tuż przed lub po wykonaniu zadania. Samo zadanie wykona się tylko wtedy, gdy potwierdzenie jest różne od polecenia. Wtedy jest pewność, że zadanie wykonało się dokładnie jeden raz tak szybko, jak to możliwe.

Do obsługi przesyłania konfiguracji zostały zdefiniowane jeszcze dodatkowe zmienne:

  •  sAdres – określa 16-bitowy adres komórki pamięci FRAM zawierającej konfigurację lub początek grupy danych.
  •  cRozmiar – określa rozmiar grupy danych. Przykładowo, konfigurując kanały odbiornika RC, prościej jest przesłać całą grupę 16 zmiennych, niż czekać 16 obiegów pętli wymiany na przesłanie kolejnych danych.
  • uRozne – jest to unia skupiająca tablice pól typów: uint8_t, uint16_t, uint32_t i float. Tablica dla uint32_t i float ma rozmiar 16 pól, dwukrotnie więcej dla uint16_t i czterokrotnie więcej dla uint8_t. Dzięki elastyczności unii można przesłać każdy rodzaj danych konfiguracyjnych. Unia nazywa się „uRozne”, gdyż jest uniwersalny kontener służący nie tylko do przesyłania konfiguracji ale różnych danych, w zależności od kontekstu danego polecenia.

Opisując zmienną cPotwierdzenieWykonania, wspomniałem, że polecenie wykona się tylko wtedy, gdy polecenie i potwierdzenie jego wykonania będą różne. Problemem okazał się odczyt większej ilości konfiguracji. Tam jest to samo polecenie dla różnych adresów. Rozwiązaniem okazało się rozbudowanie warunku wykonania polecenia o zmienność pola sAdres. Ponieważ każdy z rdzeni ma ten sam komplet danych, więc CM7, chcąc odczytać lub zapisać konfigurację, definiuje numer polecenia, adres i ilość danych, a CM4 po obsłudze tych danych ustawia adres w polu sAdres swojej struktury danych. Pole struktury danych rdzenia CM7 w unii danych nazywa się uDaneCM7. Analogicznie dla CM4 jest to uDaneCM4. Jeżeli więc uDaneCM7.dane.sAdres == uDaneCM4.dane.sAdres, to znaczy, że rdzeń zlecający obróbkę porcji danych zaczynających się od podanego adresu wie, że rdzeń serwujący tę usługę wykonał swoje zadanie.  Mechanizm użycia zmiennej sAdres jest uniwersalny i jest wykorzystywany do innych celów niż tylko konfiguracja.

Przykładem polecenia w drugą stronę jest emisja komunikatu głosowego, np. informującego o uzbrojeniu silników. Uzbrojeniem steruje CM4, a serwerem komunikatów audio jest CM7. Uzbrajając silniki, CM4 wysyła w swoich danych polecenie emisji komunikatu zdefiniowanego polem sAdres. Po wykonaniu zadania, czyli zainicjowaniu transferu DMA, CM7 pracujący jako serwer, ustawia w swoim polu sAdres ten sam identyfikator i CM4 wie, że zadanie zostało wykonane.

Inną specyficzną funkcjonalnością wymagającą dodatkowego wsparcia jest przechwycenie strumienia danych płynącego szybciej niż potok wymiany danych między rdzeniami. Można wymieniać dane w każdym obiegu pętli CM4, jednak CM7 nie ma co z nimi zrobić. Największymi konsumentami danych są telemetria i rejestrator na karcie SD. Maksymalna prędkość telemetrii to 100 Hz, a CM4 produkuje nowe dane z częstotliwością ok. 1800 Hz. Prędkość wymiany została zoptymalizowana do zasilenia tych dwóch konsumentów. Jednak niedawno pojawił się jeszcze jeden wymagający konsument – szybka transformata Fouriera (ang. FFT), którą realizuje CM7. Aby wizualizować pełny zakres częstotliwości pracy pętli głównej, potrzebuje być ona zasilana danymi z dwukrotnie większą częstotliwością niż częstotliwość pracy wizualizowanego procesu. Specjalnie dla niej CM4 musi odpytywać żyroskopy i akcelerometry 2 razy w każdym obiegu pętli głównej. Aby przesłać tak duży strumień danych, zastosowano bufor kołowy o pojemności nieznacznie większej niż ilość danych produkowana między kolejnymi wymianami między rdzeniami. CM4 napełnia bufor i przesuwa wskaźnik bieżącego zapisu, a CM7 odczytuje nową porcję danych między poprzednim stanem wskaźnika a bieżącym.