niedziela, 22 września 2019

Jak napisać w języku C wygodny dostęp do warstwy sprzętowej . Praktyczny przykład na bazie modułu radiowego SI4463.

W artykule pokażę jedną z metod poruszania się po warstwie sprzętowej  w języku C . Swoje rozważanie oprę na module radiowym SI4463. Ale tak naprawdę sprzęt jest tylko tłem dla pokazania samej metodologi i podejścia do zagadnienia. W wielu przypadkach bazujemy na bibliotekach sprzętowych napisanych przez innych adeptów języka C ,no bo w sumie po co odkrywać koło na nowo. Jeśli mamy szczęście to z takich bibliotek można się czegoś nauczyć bo metodologia jest zrozumiała i w sposób przejrzysty zakodowana. Ale to rzadkość. W większości przypadków do bibliotek czujemy awersję bo styl nam nie leży i ostatecznie trudno zrozumieć co poeta miał na myśli etc. Działa bo działa ale analiza kodu przysparza nas o ból głowy, więc odpuszczamy. Ekspertem od języka C nie jestem i wymądrzać się tutaj nie mam zamiaru. Ale ja wiele nauczyłem się na podstawie plików nagłówkowych napisanych dla MCU 32 bitowych. Gdzie królują Struktury , Unie, Wskaźniki . Moim zdaniem to olbrzymia kopalnia wiedzy o programowaniu warstwy sprzętowej w języku C, ponieważ te pliki piszą zawodowi informatycy, dlatego mamy pewność, że zastosowane rozwiązania programistyczne są optymalne.
Przyjrzymy się zatem na proces napisania wygodnego dostępu dla wybranej funkcjonalności jaką chcemy ustawić w module radiowym RF4463 . Do rozważań przyjmuję GLOBAL_CLK_CONFIG Zobaczmy jak ta funkcjonalność wygląda od strony dokumentacji modułu :


Widzimy , że nasza wybrana funkcjonalność jest reprezentowana przez jeden bajt konfiguracyjny, którego poszczególne pola są opisane jako :

CLK_32_SEL - bit 0,1

0 - bit 2

DIVIDED_CLK_SEL - bit 3,4,5

DIVIDED_CLK_EN - bit 6

0 - bit 7

Aby skonfigurować moduł w zakresie powyższej funkcjonalności należy do  bajtu konfiguracyjnego lub tylko wybranych pól bitowych tego bajtu dokonać wpisu i wysłać to do modułu. Oczywiście jeśli wpisu dokonujemy na całym bajcie to tutaj problemu nie ma ale schody zaczynają się kiedy chcemy dokonać wpisu w konkretne pole bitowe tak aby nie zmienić pozostałych bitów. Wtedy rozglądamy się za przesunięciami bitowymi, maskami i operacjami logicznymi. Ale ja pokażę inny sposób o wiele sympatyczniejszy.

Kolejnym etapem jest rozpoznanie jak są opisane dostępne wartości dla poszczególnych pól bitowych. Rozwijamy zatem opisy tych pól.




W tym momencie możemy sobie poczynić już pierwsze linijki kodu np w pliku si4463.h :

/*GLOBAL_CLK_CFG Property*/
typedef enum {DISABLE,ENABLE} divided_clk_en_Type;
typedef enum {DIV_1,DIV_2,DIV_3,DIV_7_5,DIV_10,DIV_15,DIV_30} divided_clk_sel_Type;
typedef enum {OFF,RC,CRYSTAL} clk_32k_sel_Type;

Powołałem do życia trzy typy enum dające wygodny dostęp z opisem dla poszczególnych wartości pól bitowych. Od tej chwili możemy się już posługiwać tymi typami i wprost operować opisami wartości dla pól bitowych.
Ale jak tych typów użyć i gdzie ? Najpierw jednak trochę teorii . Proponuję zapoznać się z poniższą teorią i ją postarać się przyswoić. To esencja i klucz wygodnego dostępu do pól bitowych za pomocą połączenia Unii , Struktury i pól bitowych.

Połączenie Unii, Struktury i pól bitowych dające wygodny dostęp m.in do pojedyńczych bitów dowolnej zmiennej.
Możemy zmieniać wartość całej zmiennej lub pojedyncze bity tej zmiennej bez wpływu na pozostałe pola lub bity:

typedef union {
uint8_t byte;
struct {
uint8_t bit0 :1;
uint8_t bit1 :1;
uint8_t bit2 :1;
uint8_t bit3 :1;
uint8_t bit4 :1;
uint8_t bit5 :1;
uint8_t bit6 :1;
uint8_t bit7 :1;
};
} byte_t;

Jak tego używać przykłady poniżej :

byte_t foo;                  /*powołujemy nową unię*/
uint8_t test1, test2, test3 ;/*zmienne pomocnicze*/
test1 = foo.byte ;           /*test1 = 0b00000000, odczyt całej zmiennej*/
foo.bit0 = 1 ;               /*zmiana na bicie nr 0*/
foo.bit1 = 1 ;               /*zmiana na bicie nr 1*/
test2 = foo.byte ;           /*test2 = 0b00000011 ,odczyt całej zmiennej*/
foo.byte = 0b00000000 ;      /*zerujemy zmienną po całości*/
test3 = foo.byte ;           /*test3 CLK_32K_SEL= 0b00000000 ,odczyt całej zmiennej*/

Do czego takie połączenie może się przydać ?? ano np wyobraźmy sobie, że ubieramy w ten kod jakiś rejestr mikrokontrolera i nagle robi się przyjazny dostęp do rejestru i poszczególnych jego bitów/pól bitowych. Nie trzeba robić żadnych przesunięć bitowych , masek i operacji logicznych .etc aby zrobić wpis w konkretne pole bitowe rejestru.

Powyższy tekst pochodzi z mojego starego, artykułu. Przyłużmy tę teorię do naszego konkretnego przypadku. Przedstawmy za pomocą mezaliansu Unii , Struktury i pól bitowych nasz bajt konfiguracyjny dla GLOBAL_CLK_CONFIG i umieśćmy to w pliku si4463.h


typedef union {
uint8_t byte_config;
struct{
clk_32k_sel_Type CLK_32K_SEL :2;
uint8_t  :1;
divided_clk_sel_Type DIVIDED_CLK_SEL :3;
divided_clk_en_Type DIVIDED_CLK_EN :1;
uint8_t  :1;
};
} global_clk_cfg_Type

Zauważmy , że w powyższej anonimowej strukturze (wewnątrz Unii) pojawiły się nasze trzy nowe typy zbudowane na bazie enumeratora ,zadeklarowane i zdefiniowane wyżej. Wiemy zatem już gdzie ich użyć.
Zmienna byte_config reprezentuje w powyższej konstrukcji wartość całego bajtu w skład , którego wchodzą poszczególne pola bitowe opisane strukturą. To co jest zawarte w strukturze musi mieć dokładnie wymiar jaki ma zmienna byte_config. Zliczając pola bitowe w strukturze otrzymujemy wynik 8 bitów.
W tym momencie warto sobie uzupełnić wiedzę co to jest Unia, Struktura i pole bitowe. Odsyłam do literatury traktującej o języku C.

W sumie powyższa konstrukcja to jest nasz Core na którym opieramy wszelkie działania i operacje na bajcie i bitach funkcjonalności dotyczącej bajtu konfiguracyjnego dla GLOBAL_CLK_CONFIG . No dobrze co nam po takiej wymyślnej konstrukcji i jak to ugryźć dalej. Najpierw garść definicji , które nam się przydadzą w toku dalszych rozważań (umieszczamy w pliku si4463.h):

Poniżej kod komendy wysyłającej strumień danych do modułu SI4463 (patrz dokumentacja API modułu)
 /*Command*/
#define SI4463_CMD_SET_PROPERTY            0x11
  
Poniżej Group i Number , które są znakiem rozpoznawczym w strumieniu dancyh przesyłanych do modułu radiowego dla GLOBAL_CLK_CFG. Wartości te pozyskamy z dokumentacji API modułu , widoczne są na pierwszym zdjęciu tego artykułu.
/*Property*/
#define SI4463_PRT_GLOBAL_CLK_CFG_Group         0x00
#define SI4463_PRT_GLOBAL_CLK_CFG_Number        0x01


Dodatkowo w pliku si4463.c umieszczamy zmienną pomocniczą:

uint8_t start = 0;

Teraz napiszemy funkcję wysyłającą nasz bajt konfiguracyjny do modułu SI4463 dla GLOBAL_CLK_CONFIG :


Powyższą defincję funkcji umieszczamy w pliku si4463.c a jej deklarację w pliku si4463.h. Co robi ta funkcja ?? Funkcja formuje fizyczną konstrukcję strumienia wysyłanych bajtów do modułu radiowego,  tak aby moduł zrozumiał , że rzecz dotyczy właściwości GLOBAL_CLK_CONFIG i wysyła ten strumień bajtów do modułu radiowego. Nie będę tutaj tłumaczył szczegółów konstrukcji i kolejności wysyłanych bajtów bo nie jest to istotą rozważań ,opisałem to w tym artykule o SI4463. 
Dla nas ważne tylko w jaki sposób podajemy do funkcji informację o obrazie naszego bajtu konfiguracyjnego i jak on jest reprezentowany wewnątrz funkcji. Widzimy, że jedynym argumentem dla funkcji jest wskaźnik , którego typem jest global_clk_cfg_Type czyli nasza konstrukcja Unii i Struktury. Nie możemy w inny sposób przekazać w argumencie funkcji naszej konstrukcji jak poprzez wskaźnilk. Przekazujemy zatem funkcji informację o położeniu w pamięci MCU adresu początku obiektu o typie global_clk_cfg_Type. Ale ten obiekt należy jeszcze powołać do życia. Robimy to w pliku main.c w ten prosty sposób:

global_clk_cfg_Type GLOBAL_CLK_CFG

Utworzyliśmy tym samym nowy obiekt (innobyt :) ) o konstrukcji naszej Unii i Struktury o nazwie GLOBAL_CLK_CFG.

Nas interesuje jak zmieniać wybiórczo poszczególne bity bajtu konfiguracyjnego dla powołanego powyżej obiektu GLOBAL_CLK_CONFIG. Weźmy na przykład , że chcemy ustawić konkretnie wartość pola CLK_32K_SEL na 1 czyli RC. Robimy to w ten sposób mając na uwadze wcześniejsze rozważania :

GLOBAL_CLK_CFG.CLK_32K_SEL = RC;

Zauważmy , że jest to znacznie czytelniejsza forma wpisu wartości dla pola bitowego w bajt ,niż ekwilibrystyka za pomocą przesunięć i maskowania.

Mamy zmienione pole bitowe CLK_32K_SEL teraz czas wysłać do modułu nasz zmieniony obraz bajtu konfiguracyjnego dla GLOBAL_CLK_CONFIG, robimy to wywołując funkcję wysyłającą SI4463_GLOBAL_CLK_CFG_Update() gdzie argumentem jest nasz nowy obiekt GLOBAL_CLK_CFG z operatorem wyłuskania adresu &:

SI4463_GLOBAL_CLK_CFG_Update( &GLOBAL_CLK_CFG );

Wartość naszego bajtu jest reprezentowana przez zapis w funkcji wysyłającej cfg->byte_config (gdzie fizycznie w funkcji za cfg jest podstawiane GLOBAL_CLK_CFG) co jest odwołaniem za pomocą wskaźnika do elementu unii (->byte_config . A co gdy chcemy podać namiar na wartość konkretnego pola bitowego struktury a robimy to tak cfg->CLK_32K_SEL (gdzie fizycznie w funkcji za cfg jest podstawiane GLOBAL_CLK_CFG)

Korzystamy z tego dobrodziejstwa tak, że najpierw dokonujemy wpisu/ustawienia pól bitowych w naszym bajcie konfiguracyjnym , które nas interesują a potem wysyłamy cały bajt konfiguracyjny w strumieniu bajtów za pomocą funkcji wysyłającej , w naszym przypadku SI4463_GLOBAL_CLK_CFG_Update( )
 
I to już wszystko jeśli chodzi o metodologię pisania dostępu do warstwy sprzętowej. Zdaję sobie sprawę, że dla początkujących jest to mimo wszystko trudny orzech do zgryzienia. Ale radzę 30 razy przeczytać aż zaskoczy klapka w mózgu bo to naprawdę ułatwia życie. 



Pozdrawiam
picmajster.blog@gmail.com



2 komentarze:

  1. problemem w wykorzystaniu tak fajnych unii i enum w rozbiciu na pliki mogą być problemy z zasięgiem zmiennych

    OdpowiedzUsuń
  2. Tak jest w istocie ale równie dobrze można stwierdzić, że problemem w wykorzystaniu języka C w rozbiciu na pliki mogą być problemy z zasięgiem zmiennych :) Miejmy też na uwadze , że struktury i unie dają nam namiastkę obiektowości w języku C.

    OdpowiedzUsuń