czwartek, 25 marca 2021

STM32G0 - zapis do rejestrów, wybrane zagadnienia.


W MCU 8-bitowych wszystko było proste, rejestry były proste, zapisy do nich były proste , życie było proste. W rejestrach nie było praktycznie pól bitowych o rozmiarze większym niż jeden bit, zapis sprowadzał się do operowania na jednym bicie .Człowiekowi było mało , więc  stworzył 32-bitowe monstra. Rozmowa z nimi drastycznie się skomplikowała, wydłużyły się rejestry pojawiły się pola bitowe . Skoro rozmowa z MCU się skomplikowała to zaczęto wprowadzać dodatkową abstrakcje w postaci opasłych bibliotek typu HAL. Biblioteki tego typu uprościły komunikację użytkownika z rejestrami i migrację ale wprowadziły jednocześnie niepożądane zjawiska takie jak błędy związane z rozwojem biblioteki , dodatkowy narzut zasobów MCU jakie trzeba poświecić do obsługi biblioteki oraz wyraźne spowolnienie wykonywania zadań przez MCU w tym spowalnianie działania interfejsów komunikacyjnych . Dlatego w krytycznych czasowo aplikacjach tam gdzie nie można sobie pozwolić na luksus związany z wygodą stosowania biblioteki HAL, tam  pożądana jest umiejętność rozmowy z MCU w technice określanej jako "Bare Metal" czyli to co w MCU 8-btowych było chlebem powszednim.

Nie będę nikogo przekonywał, że będąc totalnie początkującym w zakresie MCU, nie zabierał się za konstrukcje 32-bitowe ale zaczął sobie spokojnie na 8-bitach . Ja w każdym bądź razie tak bym postąpił.

Odnośnie programowania "Bare Metal" na STM32, nie będę powielał  poradników dostępnych w necie ale skupię się na aspektach, które w mojej ocenie są ważne a często mogą być pomijane lub nawet nie dostrzegane.

Podstawą podstaw jaką musimy sobie przyswoić jest metodyka zmiany poszczególnych bitów w rejestrze. I tym zagadnieniem zajmę się w tym wpisie. Dokumentem jakim będę się posługiwał w swoich rozważaniach dotyczących rejestrów jest Manual Reference dla STM32G071KBT6

Weźmy rozważmy sobie zmianę ustawień rejestru na przykładzie rejestru MODER w module GPIO. Jest to rejestr w którym ustawiamy na wybranym pinie kierunek przepływu informacji czyli czy ma to być wejście, wyjście etc. Dokładnie mamy cztery dostępne tryby ustawień i możemy jeden z nich przyporządkować dowolnemu pinowi w naszym MCU.



Jeśli rejestr odniesiemy do portu A to zapis w np. MODE2 będzie odnosił się do pinu PA2 w naszym MCU.
Zmiany ustawień dokonujemy poprzez zapis do pola bitowego złożonego z dwóch bitów. 
Załóżmy , że chcemy dokonać wpisu polegającego na ustawieniu pinu PA2 jako wyjście i nie chcemy dodatkowo wpływać na zmianę wartości w pozostałych polach rejestru. Zatem  pole bitowe MODE2 musimy ustawić na wartość 0b01. Musimy tu jednak zwrócić uwagę, że wartość początkowa rejestru MODER dla portu A to : 0xEBFF FFFF czyli pole bitowe dla MODE2 ma wartość po resecie MCU 0b11 co odpowiada trybowi wejścia analogowego. W takim układzie należy jeszcze zwrócić uwagę czy w toku kolejnych operacji bitowych mających na celu ustawienie PA2 jako wyjścia nie zmienimy przejściowo np. pinu PA2 na wejście. A może tak się zdarzyć jeśli np. będziemy chcieli na początku wyzerować pole bitowe czyli ustawić je na wartość 0b00 zapisem np. jak poniżej :

GPIOA->MODER &= ~(GPIO_MODER_MODE2_Msk);  // zerujemy pole bitowe w MODE2 używając maski, pozostałe bity rejestru zostaw bez zmian

gdzie posłużyliśmy się definicjami z pliku nagłówkowego naszego MCU.

#define GPIO_MODER_MODE2_Pos           (4U)
#define GPIO_MODER_MODE2_Msk           (0x3UL << GPIO_MODER_MODE2_Pos)          /*!< 0x00000030 */
#define GPIO_MODER_MODE2               GPIO_MODER_MODE2_Msk
#define GPIO_MODER_MODE2_0             (0x1UL << GPIO_MODER_MODE2_Pos)          /*!< 0x00000010 */
#define GPIO_MODER_MODE2_1             (0x2UL << GPIO_MODER_MODE2_Pos)          /*!< 0x00000020 */

GPIOA->MODER |= GPIO_MODER_MODE2_0 // po wyzerowaniu ustawiamy młodszy bit pola bitowego na 1 i mamy PA2 ustawione jako wyjście.

W powyższym przykładzie zapisu do rejestru ze stanu początkowego 0b11 (analog input) przechodzimy do stanu 0b00 (Input) i na końcu otrzymujemy zamierzony efekt czyli 0b01 (Output)
W tym konkretnym przypadku taki stan pośredni ustawienia pinu PA2 jako wejścia (Input) a docelowo jako wyjścia (Output), nie wpłynie pewnie na działanie urządzenia ale może zdarzyć się sytuacja kiedy będzie to miało znaczenie. I właśnie na ten aspekt chciałem zwrócić uwagę.

Jak zatem ustawić pole bitowe MODE2 aby nie wprowadzić PA2 przejściowo podczas zmiany w stan "input" i nie zmienić pozostałych bitów rejestru ??. Rozwiązanie poniżej :


GPIOA->MODER |= GPIO_MODER_MODE2_0 // ustaw młodszy bit pola bitowego MODE2 na 1

GPIOA->MODER &= ~GPIO_MODER_MODE2_1 // ustaw starszy bit pola bitowego MODE2 na 0

 
W powyższym zapisie unikamy stanu pośredniego 0b00 czyli trybu wejścia i problem rozwiązany .Można zadać pytanie po co ustawiać młodszy bit na 1 kiedy taki stan jest na wejściu po resecie MCU. Może się zdarzyć , że jednak stracimy kontrolę nad tym jaki jest aktualny stan pola bitowego w rejestrze i wtedy taki nadmiarowy zapis uchroni nas przed popełnieniem błędu konfiguracyjnego. Jest jeszcze jeden sposób poradzenia sobie z tym zagadnieniem, mianowicie wprowadzamy dodatkową zmienną do której przepisujemy zawartość rejestru , zmian dokonujemy na tej zmiennej a potem końcowy efekt przekształeceń przypisujemy do rejestru. Takim sposobem posługuje się kod wynikowy z STM32CUBE IDE.

Wprowadzenie dodatkowej zmiennej wydaje się być trochę przewymiarowanym działaniem ale w efekcie otrzymujemy wydajnieszy kod w asemblerze i unikamy wszystkich stanów pośrednich.

Myślę sobie , że już sam fakt istnienia takich niuansów przy programowaniu w oparciu o czyste rejestry w STM32, przysporzyć może sporo niezapomnianych wrażeń przez co wolniej wpadniemy w rutynę a to diametralnie wpłynie na nasz rozwój .

W STM32G0 producent dostarcza jeden plik nagłówkowy o nazwie stm32g071xx.h, który służy nam do rozmowy z rejestrami. Przykładowa zawartość w części dotyczącej rejestru MODER wygląda jak poniżej :


STM bardzo ubogo wręcz ascetycznie przygotował ten plik. Sam nie wiem czy uznać to za wadę czy zaletę :). W porównaniu do ATSAM-ów  gdzie każde peryferium ma swój oddzielny plik nagłówkowy i mamy możliwość łatwego wpisu do wybranego pola bitowego to STM32 wygląda tutaj naprawdę ascetycznie.

W pierwszym odruchu chciałem ten plik STM-a poprawić tak aby ułatwić dostęp i zapis do pól bitowych w rejestrach ale doszedłem do wniosku , że jednak zostawię to tak jak jest. Bo modyfikacja pliku nagłówkowego potem przenoszenie tego do innych projektów wprowadzi trochę zamieszania.
Nie mniej pokażę jak takiego usprawnienia dokonać. Wróćmy do tego jak pierwotnie ustawiamy pole bitowe w rejestrze MODER czyli przykładowe ustawienie dwóch bitów w tym polu :

GPIOA->MODER |= GPIO_MODER_MODE2_0     //ustaw młodszy bit pola bitowego MODE2 na 1
GPIOA->MODER &= ~GPIO_MODER_MODE2_1     //ustaw starszy bit pola bitowego MODE2 na 0
 
Chciałbym powyższy zapis sprowadzić do tego aby mieć dostęp w jednym zapisie do całego pola bitowego i aby formuła takiego zapisu była maksymalnie zrozumiała dla użytkownika.
W tym celu tworzę w pliku nagłówkowym STM-a nad deklaracją typu GPIO_TypeDef nowy typ _MODER_TypeDef_t o strukturze unii. Wewnątrz unii tworzę strukturę zbudowana z pól bitowych MODE0-MODE15, każde pole bitowe jest złożone z dwóch bitów. I ten mój nowy typ _MODER_TypeDef_t wykorzystuję w strukturze STM-a w części dotyczącej MODER. Aby rozjaśnić sytuację obrazki poniżej :


Powyżej na obrazku zaznaczyłem fragment kodu , który dodałem nad typem strukturalnym STM-a
A poniżej miejsce modyfikacji w typie strukturalnym STM-a :


co taka modyfikacja mi dała ?. Ano spójrzmy jak teraz mógłbym zmienić zawartość pola bitowego w rejestrze MODER, pole bitowe MODE2 dla portu A.

GPIOA->MODER.MODE2 = GPIO_Mode_OUT ;

Widzimy , że takim zapisem rozmowa z rejestrem byłaby samą przyjemnością i mniej przestrzeni do popełnienia pomyłki w ustawieniu. W powyższy sposób programujemy ATSAM-y Microchipa czy PIC-e. , STM32 wyglądają na tym polu bardzo słabo. Z drugiej strony jednak biorąc pod uwagę, że ATSAM-y czy PIC32CM są bardziej złożonymi co do peryferii MCU niż STM32-y to Microchip przynajmniej ułatwił dostęp do ich rejestrów.

Jest jednak metoda pośrednia aby sobie ułatwić rozmowę z rejestrami w STM32 i nie modyfikować przy tej okazji pliku nagłówkowego. Tworzymy sobie własny plik z funkcjami wspomagającymi konfigurację. Tak jest to dodanie abstrakcji i odejście trochę od poruszania się po rejestrach ale jest to zdecydowanie inna bajka jeśli piszemy to sami dla własnych potrzeb niż używamy gotowych rozwiązań z zewnątrz. Mam tu na myśli takie zapisy jak poniżej :

GPIO_SetPinAsOutput(GPIOA, GPIO_Pin_2);

to jest przykładowy zapis jaki stosują ludzie w STM32 czyli coś ala Arduino. Ja jeśli bym w coś takiego wszedł dla własnych potrzeb to by u mnie wyglądało by to tak :

GPIO_SetPinAsOutput(PA2);

Abstrakcja taka ma jednak jedną poważną wadę. Gdy robimy projekty w zespole i każdy w tym zespole by wprowadzał swoje modyfikacje ułatwiające zapis do rejestrów to projekt wyglądałby jak jakiś składak , którego nie da się zarejestrować :). Dlatego surowy zapis do rejestrów w oparciu o plik nagłówkowy dostarczany przez producenta dla STM32 jest językiem uniwersalnym i ten uniwersalizm jest dużą zaletą programowania w oparciu o rejestry. Szkoda tylko , że STM odwalił taką kichę i przygotował dla użytkowników bardzo słabe moim zdaniem pliki nagłówkowe. Biblioteki będą ewoluować , zmieniać się , wprowadzać zamieszanie w postaci nie działających programów po aktualizacji biblioteki i skompilowaniu na nowo programu a język uniwersalny w postaci zapisu bezpośredniego do rejestru będzie ponad tymi problemami w STM32. Howgh !!.

Poniżej na szybko napisana moja propozycja funkcji upraszczającej konfigurację pinu, nie testowana jeszcze na żywym organizmie. Jak przyjdzie płytka i ją złożę to przetestuję poniższą funkcję.



 
Pozdrawiam

PICmajster
picmajster.blog@gmail.com

1 komentarz: