Kategoria: Programowanie

Jakich języków uczyć się do embedded? I w jakiej kolejności?

Wybór języka programowania to kolejny obok wyboru mikrokontrolera najczęściej poruszany temat. Pytanie o język zadają nie tylko zupełnie początkujący. Często na studiach mamy trochę do czynienia z C, C++, MATLABem, FPGA, Asemblerem. Uczymy się wszystkiego po trochu i musimy się na coś zdecydować. Jeżeli siedzimy trochę w branży – słyszymy o zaletach Rusta czy modern C++. Chcemy się dobrze ustawić do wiatru i uczyć się rzeczy jednocześnie popularnych i perspektywicznych. Wybór odpowiednich języków do nauki jest tutaj kluczowy.

Continue reading

Jaki mikrokontroler wybrać na początek?

To jeden z największych dylematów osób wchodzących do branży embedded. Często zadają je również osoby, które już zaczęły pisać pierwsze programy i jakiś procesor wybrały. Nic dziwnego – możecie się zastanawiać czy to była dobra decyzja? czy mogę ją jeszcze zmienić? czy lepiej uczyć się od razu kilku rodzin?

W tym artykule dam Ci jasną odpowiedź. Oraz pokażę całą argumentację dlaczego właśnie tak. Zaznaczam tutaj od razu, że skupiam się na „deeply embedded”. Czyli ścieżce skupiającej się na systemach bare metal albo opartych na RTOSach. Linux Embedded to zupełnie inna bajka.

Continue reading

Projekt embedded od zera – nowa seria na YT!

Widzieliście już nową serię filmów na moim kanale YT? To jest coś, czego do tej pory brakowało w świecie embedded. Pokazuję w niej prawdziwy projekt. Nie tylko z ostateczną wersją kodu, ale również z całą ewolucją, decyzjami i czynnikami wpływającymi na te decyzje. A wszystko to bez zbędnego lania wody – same konkrety. Aby utrzymać zwięzłość materiału zdecydowałem się nagrać to w formie filmów, a nie live.

W tym poście napiszę więcej o samym projekcie i kulisach jego powstawania. A jeżeli chcesz od razu przejść do oglądania – poniżej znajdziesz playlistę ze wszystkimi opublikowanymi do tej pory odcinkami.

Continue reading

Architektura Systemu – Między Ideałem a Koniecznością Biznesową

Grudniowy meetup (relacja tutaj) zapoczątkował ciekawą dyskusję na temat dziedziczenia w C++, interfejsów duplikacji kodu i czy czysty kod według Uncle Boba ma zastosowanie w środowisku embedded. Zainspirowany tą debatą postanowiłem podzielić się z Wami swoimi refleksjami. We wcześniejszym wpisie odniosłem się bardziej do kwestii związanych z C++, wydajnością i czystym kodem. Dzisiaj biorę na tapet decyzje architektoniczne i kompromisy projektowe.

Zacznijmy od ważnej uwagi – opinie w takim temacie są wypadkową poglądów filozoficznych i doświadczeń we własnych projektach. Każdy programista kieruje się własnymi preferencjami, a specyfika projektów różni się diametralnie. Toteż nie szukaj tu gotowych przepisów, bo jak zwykle, odpowiedź brzmi: „To zależy”.

Continue reading

Generowanie headerów ze stałymi w CMake

W prawie każdym projekcie potrzebujemy przechowywać jakieś wartości, które zmieniamy w zależności od wersji projektu. Najbardziej oczywistym przykładem jest właśnie numer wersji. Ale czasem chcemy wyświetlać również commit id z gita, datę kompilacji, czy użytą wersję kompilatora. Nie muszę chyba dodawać, że aktualizacja takich danych ręcznie jest niezwykle uciążliwa, a czasem wręcz niemożliwa (jak dodać commit id bezpośrednio w kodzie bez modyfikowania bez modyfikowania go?). W większych projektach będziemy w tym celu używać dodatkowych skryptów. A CMake ma wbudowane wsparcie do tego typu operacji.

Continue reading

Jak napisać skrypt cmake?

W tym artykule pokażę jak napisać prosty skrypt cmake. Zrealizujemy najważniejsze zadania, jakich wymagamy od skryptu budowania:

  • Dodawanie plików źródłowych.
  • Określenie ścieżek include.
  • Określenie globalnych define’ów.
  • Dodanie bibliotek statycznych.
  • Dodanie flag kompilacji.

Dzięki skryptowi CMake możemy łatwo uruchamiać kompilację na różnych systemach operacyjnych, systemach budowania i generować projekty na różne IDE.

Continue reading

Dlaczego w C const nie może być rozmiarem tablicy?

W C++ trwają starania, żeby praktycznie nigdy nie trzeba było korzystać z preprocesora. Jednym z typowych zastosowań define w C jest deklarowanie stałych będących rozmiarami tablic. W C++ od dawna możemy używać w tym celu const, a od jakiegoś czasu (dokładniej od C++11) również constexpr. Jednak kiedy zechcemy przenieść te praktyki do C czeka nas zawód.

Dlaczego nie chcemy używać preprocesora?

Zacznijmy od początku. Po co właściwie mielibyśmy zmieniać praktykę, która istnieje w C od dziesiątek lat? Dlaczego const miałby być lepszy? Otóż preprocesor wykonuje tylko prostą podmianę tekstu i w momencie kompilacji mamy już podmienioną wartość. Przez to z define gorzej radzą sobie sprzętowe debugery (dodatkowe linki na ten temat na końcu artykułu). Poza tym const zawiera informację o typie zmiennej, przez co możemy otrzymać dodatkowe warningi w przypadku podejrzanych operacji. No ale niestety w C nie działa to tak jak w C++.

Tablice globalne

Najczęściej tablice chcemy zadeklarować jako static, aby była zaalokowana na początku programu i widoczna w jednym pliku:

#include <stdint.h>
#include <stdlib.h>

static const size_t MAX_SIZE = 10;
static uint8_t array[MAX_SIZE] = {0};

Jednak podczas kompilacji spotka nas zawód:

<source>:5:16: error: variably modified 'array' at file scope

    5 | static uint8_t array[MAX_SIZE] = {0};

      |                ^~~~~

Błąd kompilacji występuje ponieważ w C const nie oznacza stałej, ale zmienną tylko do odczytu i deklaracja tablicy jest traktowana jak tablica o zmiennym rozmiarze (VLA – variable length array). Tak więc w przypadku tablic globalnych constów sobie nie użyjemy.

A co z tablicami wewnątrz funkcji?

Tutaj mamy dwie możliwości – tablice statyczne i alokowane na stosie. W pierwszym przypadku sytuacja jest identyczna jak z tablicą globalną:

#include <stdint.h>
#include <stdlib.h>

void fun(void)
{
    static const size_t MAX_SIZE = 10;
    static uint8_t array[MAX_SIZE];

    ...
}

I błąd kompilacji:

<source>: In function 'fun':

<source>:7:20: error: storage size of 'array' isn't constant

    7 |     static uint8_t array[MAX_SIZE];

      |                    ^~~~~

Nic dziwnego, w końcu zmienne statyczne wewnątrz funkcji są alokowane tak samo jak globalne, ale są widoczne tylko w jednej funkcji.

Przejdźmy teraz do tablic alokowanych na stosie:

#include <stdint.h>
#include <stdlib.h>

void fun(void)
{
    static const size_t MAX_SIZE = 10;
    uint8_t array[MAX_SIZE];
}

I tu niespodzianka – w końcu kompilacja się powiodła. Ale radość trwa krótko, bo okazuje się, że VLA to rozszerzenie z C99 i jeśli skompilujemy z flagami gcc -ansi -pedantic, znowu otrzymamy błąd:

<source>: In function 'fun':

<source>:7:5: warning: ISO C90 forbids variable length array 'array' [-Wvla]

    7 |     uint8_t array[MAX_SIZE];

      |     ^~~~~~~

Oznacza to, że nie mamy gwarancji, że nasz kod będzie portowalny. Poza tym samo deklarowanie tablic wewnątrz funkcji ma dosyć ograniczone zastosowanie ze względu na ograniczoną pojemność stosu.

Tak więc chyba już wystarczy tych prób – const nie nadaje się jako rozmiar tablicy w C.

Czy mamy jakąś alternatywę?

Jak widać const nie jest w stanie zastąpić define w C. Ale może jest jakaś inna alternatywa? Otóż możemy użyć enuma:

#include <stdint.h>
#include <stdlib.h>

enum
{
    MAX_SIZE = 10
};

static uint8_t array[MAX_SIZE];

Elementy enuma są stałymi liczbowymi, więc mogą być rozmiarami tablic. Dodatkowo są symbolami kompilatora, a nie preprocesora, więc debugery sobie z nimi lepiej poradzą. Natomiast użycie enuma w ten sposób jest dosyć zaskakujące, a zyski z tego niewielkie, więc nie ma sensu porzucać praktyki z define używanej od lat siedemdziesiątych.

Jeszcze parę słów o constach

Ogólnie const w C jest dosyć upośledzony względem C++. Tak jak mówiłem wcześniej const w C oznacza zmienną tylko do odczytu. Natomiast define i enum są traktowane jako stałe w czasie kompilacji. Po szczegóły odsyłam na cppreference:

W C++ constexpr i wartości liczbowe przypisane do const są również traktowane jako stałe w czasie kompilacji i dlatego możemy ich używać jako rozmiar tablicy.

Jest jeszcze jedna ważna właściwość const, która wyklucza używanie go do stałych. Może to wydać się dziwne, bo przecież sama nazwa sugeruje takie użycie. Otóż zmienne z modyfikatorem const są alokowane w pamięci, więc jeżeli w pliku zadeklarujemy długą listę stałych jako const możemy znacznie zwiększyć zużycie pamięci. Tutaj jeszcze wpływ ma poziom optymalizacji i czy deklarujemy zmienną z linkowaniem zewnętrznym (bez static), czy wewnętrznym (ze static). Ale nie zmienia to faktu, że define i enum nie mają tego problemu.

Skoro const nie spełnia swego podstawowego założenia – nasuwa się pytanie:

const w C? A komu to potrzebne?

Czasem chcemy zadeklarować stałą o typie złożonym – np. ciąg znaków, tablicę, strukturę. Robimy tak w przypadku lookup table, różnego rodzaju mapperów, maszyn stanów, configów i innych tego typu rzeczy. Wtedy define nam nie pomoże i musimy użyć const.

const jest również bardzo przydatny przy pracy z wskaźnikami. Możemy dzięki temu określić, że wartość pod wskaźnikiem się nie zmieni, albo adres wskaźnika się nie zmieni (opisywałem to kiedyś w tym artykule). Dzięki temu kompilator może dokonywać dodatkowych optymalizacji, a więcej na ten temat dowiesz się wyszukując hasło „const correctness”.

define i enum a debugowanie

Pisząc ten artykuł kierowałem się własnym doświadczeniem z debugowaniem enum i define. Wiele razy widziałem, że enumy są lepiej interpretowane. Nie powinno być w tym nic dziwnego – w końcu kompilator może zawszeć informacje o enumach w informacjach debugowych. Format DWARF przewiduje też sekcję debug_macro na informacje z preprocesora, ale widocznie nie jest to tak dobrze wspierane albo informacje nie są pełne.

Sam temat informacji debugowych jest ciekawy i znalazłem na ten temat kilka fajnych linków:

Jeżeli chcesz dowiedzieć się więcej o tym, jak pisać dobry kod w C – przygotowuję właśnie kurs online “C dla zaawansowanych”. Wejdź na https://cdlazaawansowanych.pl/ i zapisz się na mój newsletter. W ten sposób informacje o kursie na pewno Cię nie ominą.