Aby ruszyć dalej z pracami nad micromousem, potrzebuję funkcji logujących dane z działania programu na konsolę w czasie rzeczywistym. Są mi one potrzebne do kalibracji czujników ścian i doboru nastaw dla regulatorów silników. Idealnym rozwiązaniem było by wykorzystanie standardowej funkcji printf. Na mikrokontrolerze jednak nie jest to takie proste, ponieważ trzeba dopisać warstwę obsługi drivera USARTa. Poza tym należy rozwiązać pewne problemy implementacyjne. To wszystko opiszę w tym artykule. Poza tym kod data loggera oraz drivera USART jest dostępny na moim GitHubie.

Printf na mikrokontrolerach

Funkcja printf z biblioteki standardowej rzadko jest wykorzystywana na mikrokontrolerach. Najczęściej zamiast niej implementuje się własną funkcję printf zawierającą wsparcie tylko dla kilku podstawowych stringów formatujących. Takie rozwiązanie pozwala na lepszą kontrolę zużycia pamięci zarówno RAM jak i FLASH.

Niektóre kompilatory, jak na przykład xc32 od Microchipa dla procesorów PIC32 zawierają lekką implementację biblioteki standardowej, w tym funkcję printf. Potrzebne wtedy jest jedynie dopisanie funkcji łączącej wypisywanie znaków z konkretnym peryferium UART. W PIC32 służy do tego funkcja _mon_putc.

Niestety printf z arm-none-eabi-gcc jest robiony raczej z myślą o bardziej złożonych systemach wykorzystujących Linuxa. Dlatego printf jest tutaj dosyć spory, przynajmniej jak na realia mikrokontrolerów (szczegółowe dane o rozmiarze w dalszej części wpisu). Nie przeszkadza mi to jednak użyć go w moim projekcie. Najwyżej później, kiedy będę potrzebował miejsca, przerzucę się na lżejsze rozwiązanie.

Data logger

Implementując funkcję printf musimy się uporać z jednym głównym problemem. Otóż drukowanie znaków za pomocą sprzętu trwa dosyć długo. W tym czasie program może chcieć zapisywać na konsolę kolejne dane. Możliwe następstwa to gubienie wiadomości, czy wywłaszczanie w środku pisania. To drugie objawia się przez rozpoczęcie drukowania jednej wiadomości i wydrukowanie drugiej w połowie pierwszej, albo drukowanie na zmianę litery z jednej i drugiej wiadomości. Aby wyeliminować te problemy należy zaimplementować kontrolę dostępu i buforowanie.

Zdecydowałem się zaimplementować data logger wykorzystujący driver USART Tx w trybie DMA i swój własny task działający na niskim priorytecie. Logger zawiera podwójny bufor. Do jednego bufora logowane są przychodzące wiadomości, a drugi jest używany przez DMA do wysyłania. W tasku loggera cyklicznie bufory są zamieniane, kiedy DMA skończy wysyłać i są nowe dane z aplikacji.

Rozmiar bufora jest dobrany na podstawie baudrate i okresu tasku. Baudrate to 115200 bps, a okres tasku to 50 ms. Driver jest w stanie wysłać 115200 bitów. Zakładając, że każdy bajt zawiera 8 bitów danych + 1 bit startu + 1 bit stopu, wysyłamy 11520 bajtów na sekundę. Czyli w 50 ms możemy wysłać  576 bajtów. Aby zapewnić dodatkowo trochę zapasu, rozmiar bufora został ustalony na 1024 bajty.

Zapisywanie do bufora zostało zrealizowane przez kopiowanie wiadomości przekazanej jako argument. Jeżeli cała wiadomość nie mieści się do bufora, zapisywana jest maksymalna możliwa ilość znaków, która jest zwracana przez funkcję zapisu. W takim wypadku wyższa warstwa decyduje, czy próbuje wysłać pozostałe dane ponownie, czy dropuje ramkę.

Aby zabezpieczyć się przed jednoczesnym dostępem do bufora przez task loggera i funkcję zapisu wywoływaną przez inne wątki wykorzystuję mutex. W driverze USART zaimplementowany jest podobny mechanizm. Semafor binarny synchronizuje funkcję zapisu drivera z przerwaniem od końca transmisji DMA. Ma on na celu uniemożliwienie zlecenia kolejnej transmisji, kiedy trwa poprzednia.

Kod loggera

Poniżej przedstawiam kod tasku loggera.

Syscalls – wywołania systemowe

Warstwą pośrednią między biblioteką standardową, a konkretną implementacją sprzętową jest zbiór wywołań systemowych. Aby połączyć funkcję printf z naszą implementacją drivera UART musimy zaimplementować syscall _write_r. Inne wywołania systemowe odpowiadają między innymi za otwieranie/zamykanie plików, kończenie programu, operacje na procesach takie jak kill, getpid, czy fork, a także sbrk odpowiedzialny za obsługę heapa wykorzystywanego do dynamicznej alokacji pamięci.

Literka r w nazwie funkcji oznacza, że jest to wersja reentrant, czyli umożliwiająca wywoływanie w tym samym czasie przez wiele wątków. Jako argumenty funkcja przyjmuje:

  • struct _reent *r – struktura zawierająca stan dla bierzącego kontekstu, nieużywana przeze mnie.
  • int file – identyfikator pliku, do którego mają być zapisane dane. Moja funkcja obsługuje stdin (STDOUT_FILENO).
  • const void *buf – bufor do wydrukowania na konsoli.
  • size_t nbyte – rozmiar bufora

Moja implementacja próbuje wysłać otrzymany bufor do loggera, a gdy to się nie uda – ponawia próbę wysłania reszty bufora aż do wysłania całej wiadomości. Poniżej kod funkcji write_r:

Zużycie pamięci

Dzięki opisanym wyżej krokom zintegrowałem systemową funkcję printf z własnym taskiem loggera wysyłającym dane na USART. Po uruchomieniu programu testowego na docelowym procesorze dane drukują się na konsolę. Tak więc zrealizowałem założenia i mam działającą konsolę operującą na systemowym printfie, czyli obsługującą stringi formatujące. Zobaczmy teraz ile taka fanaberia kosztowała mnie pamięci FLASH oraz RAM.

Program testowy z wykomentowaną funkcją printf ma rozmiar:

Natomiast program z włączonymi printfami:

Podane wyżej liczby są dla kompilacji z flagą -O0, czyli bez żadnej optymalizacji. Jak widać, printf potrzebuje 20kB FLASH i 1.5kB RAM – dosyć sporo. Lekkie implementacje printfa wymagają kilka razy mniej pamięci.

Podsumowanie

W artykule przedstawiłem sposób implementacji systemowego printfa wykorzystującego USART na STM32. Na pewno nie jest to najbardziej optymalna metoda. Jeżeli potrzebujemy lekkiej implementacji – lepiej poszukać gotowych funkcji w internecie. Jeżeli jednak zużycie pamięci nie jest dla nas problemem i chcemy szybko dodać printfa mającego wszystkie „ficzery” – przedstawiona metoda będzie idealna.

Przy okazji, kiedy spushowałem zmiany w loggerze okazało się, że build nie przechodzi na Travisie. Przyczyną jest za długa kompilacja nowej wersji toolchaina. Będę musiał do tego usiąść, albo dać za wygraną i przenieść się na oficjalny toolchain do ARMów.