Piszemy bootloader dla STM32, cz. 3

Napisano dnia 7.02.2021 r. o godzinie 8:00
Autor: Piotr Sperka

Wstęp

Witaj w trzeciej, i póki co ostatniej, części serii artykułów o bootloaderze dla STM32. W poprzednich dwóch częściach udało nam się stworzyć kompletny szkielet bootloadera oraz aplikacji użytkownika. Pomimo, że w przykładzie użyłem płytki z mikrokontrolerem STM32F207, to po nieznacznych modyfikacjach cały szkielet nada się również dla innych rodzin STM32. W ramach demonstracji stworzymy dzisiaj bardzo prosty bootloader wykorzystujący UART do zaprogramowania i pobrania kopii zapasowej programu. Oczywiście – można kwestionować sens tego programu, gdyż mikrokontrolery STM32 są wyposażone we wbudowany bootloader działający z UARTem i uruchamiany odpowiednią konfiguracją pinów BOOT. Uznałem jednak, że UART będzie najlepszy  do celów czysto dydaktycznych i demonstracyjnych ze względu na swoją prostotę. Nadmieniam już na początku, że kod ten napisałem tylko po to, żeby do końca pokazać ideę działania bootloadera i nie jest on odporny na celowe próby zepsucia go.

Komunikacja

Jak już wspomniałem we wstępie, do komunikacji z użytkownikiem wykorzystamy UART. Oczywiście, w wersji produkcyjnej warto byłoby dodać jakieś sprawdzanie błędów transmisji, jak choćby sumę kontrolną. Nie chcemy przecież pozwolić wgrać niepoprawnie przesłanych danych. Wykorzystamy transmisję z prędkością 115200 baud/s. Bootloader będzie obsługiwał komendy:

  • reset – resetuje mikrokontroler,
  • write – rozpoczyna zapis programu,
  • read – zwraca zawartość pamięci programu użytkownika,
  • erase – czyści obszar pamięci programu użytkownika.

Każda komenda kończy się znakiem „\n”, czyli Line Feed. Co istotne, w programie nie ma echa, czyli w przypadku ręcznego wprowadzania komend z terminala (na przykład PuTTY), nie widzimy wprowadzanych znaków. Myślę, że jedyną komendą wymagającą szerszego omówienia jest komenda zapisu danych, czyli write. Zapis programu odbywa się w „paczkach” mających maksymalnie 1024 bajty. Dodatkowo, ich rozmiar powinien być podzielny bez reszty przez 4. Wysłanie paczki o zerowym rozmiarze zakańcza procedurę. Warto zauważyć, że warunek podzielności przez 4 jest zapewniony niejako automatycznie – STM32 jest mikrokontrolerem 32-bitowym, a więc każde słowo programu ma 4 bajty (lub wielokrotność). Na poniższym rysunku możesz zobaczyć, jak wygląda procedura zapisu programu.

Schemat blokowy przebiegu komendy write

Przebieg komendy write

Nie będę tutaj przedstawiał kodu obsługującego komunikację (możesz oczywiście zajrzeć do kodu źródłowego – wszystko dzieje się w funkcji main()). Nie jest on skomplikowany, ale za to dosyć rozwlekły. W skrócie – jeżeli przy starcie bootloadera pin B13 jest zwarty do masy, program wchodzi w nieskończoną pętlę nasłuchując komend. Wyjście z tej pętli możliwe jest jedynie poprzez reset – czy to przyciskiem, czy to komendą reset.

Interfejs użytkownika

Do wgrywania lub pobierania pliku binarnego napisałem prosty skrypt w Pythonie 3, który również jest dostępny na repozytorium. Jego zadaniem jest odpowiednie parsowanie oraz wysyłanie i odbieranie danych. Zasadniczo można go uruchomić w trybie pobierania programu z mikrokontrolera, lub wgrywania nowego programu. Na początku musimy pamiętać o zresetowaniu mikrokontrolera w trybie bootloadera (zwierając PB13 do masy), a następnie możemy wywołać:

python main.py --port=COM3 --baud=115200 --ofile=out.bin, aby zczytać program użytkownika do pliku out.bin, korzystając z portu COM3,

python main.py --port=COM3 --baud=115200 --ifile=blink.bin, aby wgrać nowy program użytkownika umieszczony w pliku blink.bin.

Podsumowanie

W ten sposób wspólnie dotarliśmy do końca tej krótkiej serii poświęconej prostemu bootloaderowi dla STM32. Mam nadzieję, że w ten sposób udało mi się nieco przybliżyć Ci zasadę działania i budowania bootloaderów. Przedstawiony przykład ma tak naprawdę jedynie pokazać, że nasz szkielet bootloadera działa i zachęcić Cię do dalszych samodzielnych eksperymentów. Do rzeczywistych zastosowań moim zdaniem warto wykorzystać szybszy interfejs komunikacyjny, na przykład SPI, a przede wszystkim zaimplementować mechanizm sprawdzania poprawności transmisji. Jednak swoją prawdziwą siłę bootloader pokaże, gdy wyposażymy go w obsługę bardziej przyjaznych użytkownikowi końcowemu interfejsów. Mogą to być na przykład karta SD, USB, czy też Ethernet. Cały kod dostępny jest na moim GitHubie, i jak zawsze w przypadku jakichkolwiek uwag czy wątpliwości – zapraszam do kontaktu.

Do następnego razu!