Sygnaturka wpisu to wycinek z “komiksu” odnośnie reużywalności kodu ze strony monkeyuser.

Spotkałem się nie raz ze stwierdzeniem, że ~”webówka jest łatwa, bo wszystko już jest dostępne i tylko trzeba połączyć gotowe biblioteki, bo community ich bardzo dużo naprodukowało”. Klasykiem jest strona z licznikiem ile dni minęło od ostatniego nowego frameworka w javascripcie -> licznik.

Dla mikrokontrolerów zazwyczaj trzeba dużo napisać samemu, ciężko znaleźć gotowe biblioteki akurat na naszą platformę itp. + wydaje mi się, że bardzo dużo ludzi lubi “odkrywać koło na nowo” - ale ile razy w życiu można pisać podobną bibliotekę do jakiegoś typu sensorów?

Przez to sami sobie utrudniamy pracę - dzięki reużywalnym bibliotekom możemy znacznie skrócić przyszłe projekty.

Moim zdaniem typowe błędy/wady w bibliotekach dla mikrokontrolerów, które są udostępniane w internecie - w poradnikach na blogach itp + propozycje rozwiązań.

  1. Brak warstwy abstrakcji nad peryferiami/bibliotekami producenta.
  2. Zależność od bibliotek producenta.
  3. Umieszczenie w repozytorium pełnego przykładowego projektu z biblioteką w środku.
  4. Konfiguracyjny plik nagłówkowy jako część projektu/biblioteki.
  5. Brak komentarzy/pliku readme z opisem biblioteki.
  6. Niespójny coding standard.
  7. Brak ustandaryzowanego układu katalogów/plików biblioteki.

Te błędy można dość łatwo wyeliminować - zastanawiałem sie, czy dodać tutaj też testy - jednak jest to temat na kilkanaście wpisów - może w przyszłości.

1. Brak warstwy abstrakcji nad peryferiami/bibliotekami producenta.

Przez to biblioteki można przenieść łatwo tylko na taki sam typ mikrokontrolera - a nawet idąc dalej czasem ograniczania powodują użycie tylko z konkretną rodziną układów.

Jeśli fragment biblioteki musi być wywoływany np. po przerwaniu zewnętrznym EXTI - nie wrzucajmy do biblioteki konfiguracji tego przerwania razem z pełnym handlerem - wydzielmy to co musi być tam wywołane do osobnej funkcji i przekażmy odpowiedzialność za poprawne wywołanie tego na użytkownika.

Funkcje służące np. do przesłania danych po SPI biblioteka może przyjmować jako element inicjalizacji.

2. Zbędna zależność od bibliotek producenta.

Jeśli plik źródłowy zaczyna się od czegoś typu:

#include <avrio.h>

ucinamy z automatu “grupę docelową” która może w łatwy sposób użyć naszą bibliotekę.

Oczywiście często jest potrzebne uzależnienie od bibliotek producenta - np. wrapper na I2C z STHAL - warto to zaznaczyć w readme modułu.

3. Umieszczenie w repozytorium pełnego przykładowego projektu z biblioteką w środku.

Zdarza się, że poradnik kończy się zdaniem “przykład znajduje się w repozytorium” - jednak jeśli znajdzie się tam cały projekt trzeba się trochę nagimnastykować aby wyciąć/pokopiować tylko to co nas interesuje, tracimy wtedy powiązanie z repozytorium.

W niektórych przypadkach biblioteka nie jest nawet wydzielona do osobnego katalogu, a rozbita tak, aby dostosować ją do “domyślnego” układu plików jakiego wymaga IDE - np. CubeIDE domyślnie tworzy katalogi “Core/Inc” i “Core/Src”, ale nic nie stoi na przeszkodzie, żeby bibliotekę wrzucić w osobnym katalogu i odpowiednio w projekcie skonfigurować ścieżki do includów i sourców.

Rozwiązanie jest proste - rozdzielmy kod biblioteki i exampla, a chyba najlepszym rozwiązaniem jest repozytorium zawierający TYLKO bibliotekę i używać jej potem jako “submoduł” gita.

4. Konfiguracyjny plik nagłówkowy jako część projektu/biblioteki.

Jeśli mamy moduł składający się z plików “button.c” i “button.h” to nie umieszczajmy w pliku nagłówkowym “definów” które powinien zmienić użytkownik. W ten sposób każdy projekt z użyciem tej biblioteki będzie miał inną zawartość pliku, który mógłby być wspólny. Rozwiązanie - konfigurację można przenieść na poziom budowania i w CDT/Make/CMake lub wymusić, alby takie rzeczy znalazły się w osobnym pliku. W przypadku, kiedy nie muszą to być definicje preprocesora znane przy kompilacji - opcją do wykorzystania jest zgrupowanie takich parametrów w strukturze inicjalizacyjnej modułu.

5. Brak komentarzy/pliku readme z opisem biblioteki.

Tu chyba nie jest potrzebny dodatkowy komentarz.

6. Niespójny coding standard.

Wystarczy uruchomić jakiś formatter przed przesłaniem biblioteki na repozytorium - choćby autoformatter eclipsowy. Dodatkowo można przemyśleć kwestie nazewnictwa funkcji itp.

7. Brak ustandaryzowanego układu katalogów/plików biblioteki.

Ułatwi to “odnalezienie” się w biblitekach, ułatwia podpięcie bibliotek do build systemu.

Moje zasady

Zasady odnośnie moich bibliotek powiązane z powyższymi punktami (co robię, żeby nie popełniać tego błędu):

  1. Biblioteki np do sensorów po I2C/wyświetlaczy przyjmują funkcje oferujące dostęp do hardware jako callbacki przy inicjalizacji
  2. Jeśli biblioteka nie jest dedykowana pod daną platformę nie korzysta z żadnych nagłówków zależnych od producenta, jeśli jest to np. wrapper na I2C korzystający z bibliotek ST jest to wyraźnie napisane w readme
  3. Biblioteki to zawsze osobne repozytoria - nie są obudowane w żaden projekt, ewentualnie projekt mają wrzucowny w sekcję “examples”
  4. Biblioteki wymagają pliku “board.h” w projekcie gdzie należy umieścić konfigurację biblioteki per projekt, czyli przykładowo biblioteka I2C oczekuje, że “#define” wartością “I2C_MAX_TIMEOUT” znajdzie się w tym pliku (lub jakimś innym zaincludowanym w “board.h”)
  5. Staram się zawsze zamieścić plik readme.md z “instrukcją obsługi” w repozytorium
  6. Pisząc kod używam autoformattera eclipse - nie ma więc niespójnej składni w plikach
  7. Układ plików i katalogów to trochę dłuższy temat - opiszę jaki ja stosuję - sprawdza się on u mnie i w małych projektach, a także ta sama koncepcja została przetestowana w firmowym projekcie i dobrze się sprawdziła.

Jak już wcześniej wspomniałem stosuję submoduły gita do przechowywania pojedynczych bibliotek (jeszcze nie musiałem ich jakoś grupować w kilka).

Struktura katalogów w każdym module wygląda następująco:

<nazwa modulu>
|   readme.md     -> opis, który wyświetli się na głównej stronie repo
|   [CMakeList.txt] -> dobre miejsce na ten plik - aktualnie nie używam CMAKE
└───<nazwa modulu>
|   └───inc -> publiczne interfejsy modułu - ten katalog należy dodać do ścieżek projektu
|   └───src -> źródła, a także wewnętrzne interfejsy
|
└───docs    -> jakaś dodatkowa dokumentacja jeśli potrzebna
└───board   -> przyklady konfiguracji biblioteki - można użyć w przypadku projektów na tej samej płytce
|       <nazwa_modulu>_<nazwa_projektu_lub_plytki>.h
└───tests    -> testy jeśli są napisane - aktualnie używany framework to GTEST, wcześniej był doctest
└───examples -> przykłady użycia biblioteki

Integracja z Atolliciem/CubeIDE polega na tym, że submoduły pobieram do katalogu drivers, który jest oznaczony jako źródłowy (i co za tym idzie wszystkie jego podfoldery). Następnie należy dodać ścieżkę do katalogu “inc” w “Includes” z zakładce “Path and symbols”. Może pojawić się problem jeśli w katalogach “tests” lub “examples” znajdują się jakieś pliki źródłowe - należy wtedy te katalogi wykluczyć z buildu.

To jest wyżej wspomniana zakładka z “Includes”:

obr

Swoje biblioteki będę pomału udostępniał to podlinkuję tutaj przykłady.

W taki sposób można zdecydowanie ułatwić sobie pracę, a przy opensourcowych bibliotekach także innym, którzy chcieliby skorzystać z naszego kodu.

Projekty, które mają dobrze zorganizowane zarządzanie bibliotekami:

  • arduino IDE - można się nabijać z arduino itp, ale zarządzanie bibliotekami jest tam zrobione bardzo przejrzyście - reużywalność bibliotek stoi na wysokim poziomie
  • platformIO - jako nakładka na arduino ma fajnego managera bibliotek - opcję używania różnych wersji bibliotek per projekt itp
  • espressif ESP-IDF - całość oparta o CMAKE + fajna koncepcja “komponentów” jako totalnie niezależnych bibliotek, które są dołączane do projektu

Jako bibliografię do tego tematu mogę polecić książkę Jacob Beningo: “Reusable Firmware Development” .

Przy okazji natknęło mnie na małego mema odnośnie udostępniania innym swoich bibliotek i ułatwiania przez to pracy innym:

obr

Jeszcze ważna uwaga odnośnie standaryzacji - np. coding standardu - coding standard ma być spójny i przestrzegany w firmie - “gównoburze” czy po klamrach należy dawać entery, co jest lepsze spacje, czy taby itp. to marnowanie czasu i energii. Ustalamy to na początku projektu/bierzemy ustalenia firmowe, ustawiamy IDE/skrypty formattera żeby nam w tym pomogło i zapominamy o sprawie. O tego typu kwestie można zrobić jakieś głosowanie itp - a jeśli w firmie już jest wybrany CS - należy go przestrzegać, ewentualnie uzupełniać luki w nim - ale nie samodzielnie tylko razem w teamie.