Programowanie STM32 M0 z wykorzystaniem bibliotek LL - Cz. 2: Witaj świecie, tu dioda LED

Tu możesz pisać o swoich problemach z pisaniem programów w języku C/C++ dla STM.
Awatar użytkownika
ZbeeGin
User
User
Posty: 492
Rejestracja: sobota 08 lip 2017, 17:16
Lokalizacja: Śląsko-Zagłębiowska Metropolia
Kontaktowanie:

Programowanie STM32 M0 z wykorzystaniem bibliotek LL - Cz. 2: Witaj świecie, tu dioda LED

Postautor: ZbeeGin » poniedziałek 10 wrz 2018, 21:49

Witam w części drugiej... Ale czego części drugiej? Jak to ktoś świetnie ujął: "opisu aspirującego do miany kursu". Zatem na razie się tego trzymajmy. :) W tym odcinku rozprawimy się z podstawowymi funkcjami portów GPIO.


W poprzednim odcinku przeprowadziliśmy konfigurację środowiska i potrzebnych mu plików bibliotek, piekąc nawet dwie pieczenie na jednym ogniu, gdyż udało się również ujarzmić Atollic TrueStudio. Na koniec pokazałem Wam kawałek kodu, który w tym odcinku rozbierzemy na części pierwsze.

Struktura przykładowego kodu

Code: Select all

#include "stm32f0xx.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_cortex.h"
#include "stm32f0xx_ll_utils.h"
#include "stm32f0xx_ll_gpio.h"

int main(void)
{
/* konfigurujemy SysTick by można użyc funkcji opóźniającej */
/* domyślnie procesor w pełni pracuje na częstotliwosci 8MHz */
LL_Init1msTick(8000000);

/* włączamy zegar dla GPIOA */
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);

/* definiujemy strukturę konfiguracji portu GPIO */
LL_GPIO_InitTypeDef gpio;

/* i wypełniamy ją potrzebnymi danymi */
gpio.Pin = LL_GPIO_PIN_5;
gpio.Mode = LL_GPIO_MODE_OUTPUT;
gpio.OutputType = LL_GPIO_OUTPUT_PUSHPULL;

/* teraz przekazujemy gotową konfigurację */
LL_GPIO_Init(GPIOA, &gpio);

for(;;){
/* ustawiamy stan wysoki na PA5 i czekamy sekundę */
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(1000);

/* ustawiamy stan niski na PA5 i czekamy sekundę */
LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(1000);
}
}


Pominiemy nagłówek z komentarzem. Tekst programu rozpoczyna się serią dyrektyw #include. Pierwsza z nich to dyrektywa związana z wprowadzeniem do kodu pliku z definicjami danego procesora lub całej rodziny. Tu doklejamy plik nagłówkowy, w którym zawarto podstawowe definicje związane z procesorami STM32F0. Niezależnie czy będzie to najmniejszy przedstawiciel, czyli STM32F030 czy chyba największy STM32F092 to plik jest jeden. Ewentualne rozbieżności są traktowane kompilacjami warunkowymi.

Najważniejsze dla Nas są następne pliki, które należą już do bibliotek LL. I tak:

- stm32f0xx_ll_bus - zawiera podstawowe makra oraz funkcje związane z obsługą magistral łączeniowych AHB i APB. To one stanowią kręgosłup całego procesora. Magistrala AHB (Advanced Microcontroller Bus) stanowi podstawowe połączenie pomiedzy rdzeniem, pamięcią, układami bezpośredniego dostępu do pamięci, układami zarządzającymi procesorem, i o dziwo... portami wejścia-wyjścia. Dzięki temu dostęp do nich jest nieco szybszy. Magistrala APB (Advanced Peripherial Bus) zaś, to magistrala podrzędna połączona z główną magistralą AHB za pomocą specjalnego mostka. To do niej zostały podłączone pozostałe peryferia znajdujące się w procesorze.

- stm32f0xx_ll_cortex - to zbiór makr odwołujących się do poszczególnych elementów stanowiących rdzeń procesora. Dzięki niemu mamy dostęp do specjalnego licznika występującego w każdym procesorze ARM: SysTick. Jest on głównie wykorzystywany w systemach operacyjnych czasu rzeczywistego, np. FreeRTOS; ale może nam też posłużyć jako podstawa czasu w innych zastosowaniach. W naszym kodzie wykorzystamy go do generowania opóźnień. Oprócz tego z poziomu makr tego pliku możemy zarządzać stanami aktywności rdzenia.

- stm32f0xx_ll_utils - to zbiór makr i funkcji pomocniczych. To ona udostępnia nam specjalną funkcję opóżniającą korzystającą ze sprzętowego SysTick-a.

- stm32f0xx_ll_gpio - zawiera kompleksowy zestaw makr i dosłownie jedną funkcję pozwalającą na dostęp do portów wejścia-wyjścia (GPIO). To dzięki tym funkcjom możemy zmieniać konfigurację pinów portów oraz sprawdzać ją, zmieniać stan pinów, jak i całego portu, oraz doczytywać ich stany.


Naszą funkcję main() rozpoczniemy od skorzystania z dobrodziejstw licznika SysTick i skonfigurujemy go by co 1ms się przepełniał. Jest to podstawa czasu dla późniejszej funkcji z _ll_utils generującej opóźnienia. Dzięki funkcji LL_Init1msTick() zrobimy to bardzo prosto podając tylko szybkość taktowania taktowania procesora w Hz. Całą potrzebną resztę przeliczy sobie sama funkcja.

Lecz jaka jest ta szybkość taktowania? Okazuje się, że w przypadku bibliotek HAL-LL, domyślna szybkość pracy procesora to tylko 8MHz pochodząca z wewnętrznego generatora RC. Dlatego też jako parametr podajemy właśnie wartość 8000000. Szybkość tą można zmienić nawet do 48MHz i później poznamy całą strukturę służącą do tego celu oraz całą procedurę. Na chwilę obecną by "zamigać LED", 8MHz nam w pełni wystarczy.


Ponieważ nasz program będzie się odwoływał do portu GPIOA, a w szczególności do pinu PA5, który na płytkach Nucleo jest połączony z diodą LED to najpierw musimy wykonać jedno polecenie by ten port w ogóle działał: LL_AHB1_GRP1_EnableClock(), podając jako parametr nazwę bloku, któremu nasz zegar chcemy udostępnić.

Dlaczego takie podejście, że podstawowy element każdego mikrokontrolera nie działa zaraz po uruchomieniu? Projektanci wyszli z prostego założenia: jeśli jakiś element nie jest używany, należy mu odciąć sygnał zegarowy, a tym samym obniżyć poziom pobieranej energii. Nowoczesne procesory są wykonywane w technologii CMOS, zatem mają sporo tranzystorów typu MOSFET, których bramka stanowi pewną pojemność. Jej naładowanie powoduje chwilowy skok pobieranego prądu, by te pojemności naładować. Przekazywanie w takim wypadku przebiegu zegarowego dla nieużywanego peryferium byłoby marnotrawstwem energii, bo bramki bezproduktywnie przeładowywałyby się tylko w takt zegara. Wobec czego normalnie sygnał zegarowy jest odcięty dla wszystkich peryferii oprócz tych naprawdę niezbędnych by procesor wystartował.


Pora teraz na skonfigurowanie pracy wyjścia PA5, bo musimy ustawić go jako wyjście typu push-pull, bez podciągania.

INFO: Więcej o portach w procesorze (nie tylko ARM) w innym temacie: https://microgeek.eu/viewtopic.php?f=27&t=1795

W tym celu możemy użyć dwóch możliwości:
  1. Podejście strukturalne - gdzie tworzymy pewną strukturę z ustawionymi wszystkimi potrzebnymi parametrami i przekazanie jej do specjalnej funkcji inicjalizacyjnej. Takie podejście podobne do tego tego, jakie używane jest w bibliotekach SPL.
  2. Podejście niskopoziomowe - gdzie do ustawienia wszystkich parametrów używamy specjalnie przygotowanych makr. Każde z nich wykonuje jedną operację. Jest to właśnie kwintesencja całego ekosystemu bibliotek LL.

W tym przykładzie wykorzystaliśmy pierwszą możliwość. Najpierw tworzymy zmienną gpio, która jest strukturą ukrytą w typie LL_GPIO_InitTypeDef. Następnie wykorzystując dostępne pola struktury wpisujemy parametry.

- W pole "Pin" wpiszemy wartość zdefiniowaną w pliku _ll_gpio: LL_GPIO_PIN_5. Oznacza ona, że konfiguracji podlegać będzie tylko pin PA5. Nic nie stoi na przeszkodzie by wpisać tam sumę logiczną innych definicji odnoszących się do pozostałych pinów, np. (LL_GPIO_PIN_5 | LL_GPIO_PIN_6 | LL_GPIO_PIN_7) tworząc tzw. "maskę". Dzięki temu rozszerzymy zakres zmian na piny PA5, PA6 i PA7. Jest także specjalna definicja LL_GPIO_PIN_ALL, którą wykorzystamy, gdybyśmy chcieli przestawić opcje wszystkich pinów danego portu.

- W pole "Mode" wpiszemy inną definicję: LL_GPIO_MODE_OUTPUT. Oznacza ona, że dany pin lub grupa pinów ma pracować jako zwykłe wyjście. Do dyspozycji mamy jeszcze trzy inne: LL_GPIO_MODE_INPUT, LL_GPIO_MODE_ANALOG, LL_GPIO_MODE_ALTERNATE, odpowiednio: wejście, wejście analogowe, funkcja alternatywna pochodząca z peryferium.

- W pole "OutputType" wpiszemy jeszcze: LL_GPIO_OUTPUT_PUSHPULL co zapewni nam pracę końcówki portu jako wyjście typu push-pull. Gdybyśmy chcieli wykorzystać możlwość pracy portu jako "otwarty dren" to użylibyśmy LL_GPIO_OUTPUT_OPENDRAIN.

Dla tej konfiguracji to już wszystkie pola jakie powinniśmy wypełnić. Dalszą część zrobi już za nas funkcja LL_GPIO_Init(), której podajemy symboliczną nazwę portu na którym dokonujemy zmian oraz adres struktury zawierającej parametry. Procedura sama zadba by odpowiednie wartości uporządkować i przelać do właściwych rejestrów.

Przy podejściu niskopoziomowym całość możemy nieco skrócić wykorzystując odpowiednie makra. Kto chce może wykorzystać tą metodę zamieniając opisany wyżej proces na następujące wywołania:

Code: Select all

LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_5, LL_GPIO_OUTPUT_PUSHPULL);


Jedyną wadą będzie to, że parametry pracy będzie trzeba określać pojedynczo dla każdego pinu z osobna, gdyż funkcja LL_GPIO_SetPinMode() nie przyjmuje nazw pinów w formie maski. Coś za coś.


Skoro mamy już komplet konfiguracji, można skonstruować pętlę główną, która nie będzie miała nic innego do roboty jak tylko kręcić się w kółko zmeniając stan PA5. Do tego wykorzystamy dwa makra:

- LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5) - ustawiająca dany pin w stan wysoki,
- LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5) - resetująca dany pin w stan niski.

Oba działają na szybkich rejestrach o dostępie atomowym: BSRR, BRR. I co najważniejsze obsługują maski, więc jednym makrem możemy narzucić stan kilku pinom.

Ale abyśmy mogli zauważyć pracę naszego programu jak przemiata pinem PA5 musimy wprowadzić pewne opóźnienie. Stąd pomiędzy te instrukcje wpleciemy jeszcze jedną funkcję, która nam te opóźnienie zapewni: LL_mDelay(). Korzysta ona z licznika SysTick i przy odpowiednim jego ustawieniu może wprowadzać opóźnienie w jednostkach milisekundowych. Minimum to ~1ms, maksimum zaś to... około 49 dni - jeśli dobrze liczę.
My nie będziemy tak długo czekać, więc ustawimy 1000ms co da nam zmianę stanu co około 1 sekundę. Kto chce może później poeksperymentować z różnymi wartościami.

I to już wszystko co dzieje się w tym naszym pierwszym programie. Teraz poszerzymy naszą wiedzę o kolejne cenne informacje po krótkiej przerwie na reklamę... Żartowałem. ;)


Jak myślicie, co stanowi największą przeszkodę w popularyzacji procesorów ARM wśród osób, które już co nieco wiedzą w dziedzinie programowania mikrokontrolerów? W zasadzie dwie rzeczy: odpowiednio skonstruowana procedura startowa, tzw. startup w języku maszynowym oraz konfiguracja zegara taktującego.
Obecnie jesteśmy w bardzo dobrej sytuacji, gdyż z pierwszą przeszkodą radzą sobie dobrze obecne środowiska programistyczne wspierające mikrokontrolery ARM, które same umieszczają odpowiedni plik przy tworzeniu nowego projektu, a sam producent dostarcza też domyślne pliki w bibliotekach.

Pozostaje kwestia zegara. Ci którzy używali wcześniej mikrokontrolerów, np. Atmel AVR wiedzą, że tam nie ma "prawie" żadnych problemów z zegarem taktującym. Ot, ustawiamy odpowiednie fuse-bity i nasz procesor już działa z taką prędkością jaką chcemy. W mikrokontrolerach ARM sprawa się jednak bardziej komplikuje. Właśnie przez to, że mamy więcej źródeł taktowania i z uwagi na pro-energooszczędność możemy dostosowywać to taktowanie.

Teraz być może przerażający rysunek, ale spokojnie, wszystko po kolei postaram się jak najprościej opisać. Nie posłużę się rysunkiem z noty katalogowej, gdyż tam nie widać dość jasno jakie są kolejne etapy przejścia sygnału zegarowego. Zamiast niego wykorzystam rysunek z STM32CubeMX. Będzie on przedstawiał domyślną konfigurację taktowania układu jakiej używaliśmy dotychczas.
rys1.png

Analizę należy rozpocząć od lewej strony gdzie znajdują się dostępne źródła przebiegów zegarowych. Mamy ich kilka:
  • LSE - Low Speed External - gdzie możemy podłączyć rezonator zwany "zegarkowym" o niskiej częstotliwości pracy. Służy on do taktowania wbudowanego układu RTC (Real Time Clock). Teraz ten moduł jest nieaktywny.
  • LSI RC - Low Speed Internal RC - który jest układem generatora RC, dostarczającym stałej częstotliwości 40kHz. Domyślnie przeznaczony jest on do taktowania układu nadzorującego pracę procesora IDWG (Independent Watchdog). Dzięki przełącznikowi w postaci multipleksera RTC Clock Mux, jego sygnał możemy też skierować do układu RTC.
  • HSI RC - High Speed Internal RC - który podobnie jak w przypadku poprzedniego generatora jest układem RC dostarczającym stałej częstotliwości 8MHz. W obecnej konfiguracji to on jest źródłem taktowania rdzenia i większości wbudowanych urządzeń peryferyjnych.
  • HSI48 RC - High Speed 48MHz Internal RC - stanowi on alternatywę dla układu HSI RC i dostarcza beż żadnych dodatkowych metod maksymalnej częstotliwości pracy układów STM32F0, czyli 48MHz. Występuje on jednak wyłącznie w układach posiadających interfejs USB.
  • HSE - High Speed External - który jest generatorem współpracującym z normalnym rezonatorem kwarcowym. Częstotliwość rezonatora może zawierać się w granicach 4-32MHz. Możemy go użyć jeśli zależy nam na większej stabilności taktowania procesora.
  • HSI14 RC - High Speed 14MHz Internal RC - który jest generatorem RC pracującym na stałej częstotliwości 14MHz i służącym do taktowania wbudowanego układu przetwornika ADC (Analog to Digital Converter).

Jak widać mamy dość sporo generatorów z czego kilka jest specjalnego przeznaczenia. Skupmy się zatem na tych, które mogą taktować rdzeń: HSI, HSI48 i HSE. Wszystkie sygnały wyjściowe z nich prowadzą do dwóch multiplekserów. Pierwszy z nich to System Clock Mux, drugi zaś to PLL Source Mux, którego wyjście też dołączono do pierwszego z nich. Po co takie komplikacje?

Musicie wiedzieć, że sygnały taktujące procesor nie tylko możemy dzielić - co wyniknie z dalszego opisu - by uzyskiwać mniejsze częstotliwości taktowania, ale także powielać by uzyskiwać większe. Np. korzystamy z HSE z klasycznym rezonatorem 8MHz, a procesor taktujemy aż 48MHz! Tak. To możliwe. Właśnie dzięki układowi PLL, który nam tą częstotliwość może bardzo prosto pomnożyć. Sposób z mnożeniem częstotliwości bazowej jest już znany od lat i obecnie stosowany prawie wszędzie. Nawet w Waszych urządzeniach, na których czytacie ten artykuł. Zatem niezależnie od tego, jaki generator zostanie wykorzystany to zawsze będzie można uzyskać częstotliwość zbliżoną do maksymalnie obsługiwanej przez mikrokontroler STM32F072.

INFO: Użycie układu PLL pozwala też na małe OC (Overclocking) naszego procesora. Ucierpią na tym jednak wbudowane peryferia. Mój egzemplarz jeszcze "wstał", mając taktowanie aż 80MHz. 8-)

Wróćmy zatem do naszego głównego przełącznika źródeł taktowania System Clock Mux. Dzięki niemu możemy wybrać czy główny przebieg zegarowy SYSCLK (System Clock) będzie pochodził bezpośrednio z generatora HSI, HSI48 czy HSE, albo z układu powielającego PLL. Sygnał ten jako niezmieniony może taktować niektóre układy transmisji szeregowej: I2C/I2S, USART.

Następnie układ trafia do dzielnika częstotliwości AHB Prescaler i po ewentualnym podziale staje się sygnałem zegarowym HCLK (Host Clock) dla rdzenia procesora, licznika SysTick - po ewentualnym dodatkowym podziale - i kilku innych układów jakie poznamy później. Następnie trafia do kolejnego dzielnika APB Precaler i "zasila" w przebieg zegarowy pozostałe układy peryferyjne.


Czas na jakiś konkretny przykład. Wykorzystamy "wolniejszy" generator HSI pracujący na częstotliwości 8MHz i przepuścimy go przez układ PLL by uzyskać taktowanie rdzenia oraz układów peryferyjnych na poziomie 48MHz. Oto jak nasz sygnał zegarowy będzie się przemieszczał w całej strukturze:
rys2.png

Kod jak dotychczas będzie migał zieloną diodą podłączoną do PA5, ale skrócimy nieco czasy opóźnień do 100ms. Dodatkowo na pin PA8, którego jedną z funkcji alternatywnych jest możliwość wyprowadzenia sygnału zegarowego z układu taktującego - sygnał MCO (Master Clock Output), skierujemy przebieg zegarowy SYSCLK podzielony przez dwa, co da nam 24MHz. Dzięki temu będziemy mieli wzorzec sygnału zegarowego procesora na zewnątrz i łatwo zmierzymy go oscyloskopem:
rys3.png

Nasz drugi przykładowy kod będzie wyglądał tak:

Code: Select all

#include "stm32f0xx.h"
#include "stm32f0xx_ll_rcc.h"
#include "stm32f0xx_ll_system.h"
#include "stm32f0xx_ll_bus.h"
#include "stm32f0xx_ll_cortex.h"
#include "stm32f0xx_ll_utils.h"
#include "stm32f0xx_ll_gpio.h"


void SystemClock_Config(void)
{
/* musimy dodac opóźnienia w dostępie do Flash, ponieważ
* prędkośc jaką chcemy uzyskac jest już zbyt duża dla niej
*/
LL_FLASH_SetLatency(LL_FLASH_LATENCY_1);

/* włączamy generator High Speed Internal 8MHz */
LL_RCC_HSI_Enable();
/* i czekamy aż się ustabilizuje */
while(LL_RCC_HSI_IsReady() != 1) {};
/* domyślne ustawienia dostrojenia częstotliwości generatora */
LL_RCC_HSI_SetCalibTrimming(16);

/* teraz możemy skonfigurowac i uruchomic układ powielania
* częstotliwości w pętli PLL
* powielimy częstotliośc żródłową sześc razy 8MHz x 6 = 48MHz
*/
LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_HSI, LL_RCC_PLL_MUL_6, LL_RCC_PREDIV_DIV_1);
LL_RCC_PLL_Enable();
/* i poczekamy aż się pętla ustabilizuje */
while(LL_RCC_PLL_IsReady() != 1) {};

/* teraz ustalimy dzielniki uzyskanej częstotliwości SYSCLK dla
* magistral AHB oraz APB1
*/
LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);

/* dopiero teraz możemy przełączyć źródło taktowania na wyjście z PLL
* i zaczekamy na pozytywne zakończenie tej operacji
*/
LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL) {};

/* ustawimy jeszcze specjalną zmienną SystemCoreClock, która
* przechowywac będzie bieżącą prędkośc taktowania
*/
LL_SetSystemCoreClock(48000000);

/* wyprowadzimy też ten syngnał na zewnątrz by sprawdzic
* czy tak ustawiony generator działa poprawnie
* podzielimy go przez dwa by uzyskac 24MHz
*/
LL_RCC_ConfigMCO(LL_RCC_MCO1SOURCE_SYSCLK, LL_RCC_MCO1_DIV_2);
}


int main(void)
{
/* zmieniamy konfigurację taktowania */
SystemClock_Config();

/* konfigurujemy SysTick by można użyc prostej funkcji
* opóźniającej
* szybkośc taktowania pobierzemy z specjalnej zmiennej
*/
LL_Init1msTick(SystemCoreClock);

/* włączamy zegar dla GPIOA */
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);

/* definiujemy strukturę konfiguracji portu GPIO */
LL_GPIO_InitTypeDef gpio;

/* i wypełniamy ją potrzebnymi danymi */
gpio.Pin = LL_GPIO_PIN_5;
gpio.Mode = LL_GPIO_MODE_OUTPUT;
gpio.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
gpio.Speed = LL_GPIO_SPEED_HIGH;

/* teraz przekazujemy gotową konfigurację PA5 */
LL_GPIO_Init(GPIOA, &gpio);


/* znów wypełniamy ją potrzebnymi danymi */
gpio.Pin = LL_GPIO_PIN_8;
gpio.Mode = LL_GPIO_MODE_ALTERNATE;
gpio.Alternate = LL_GPIO_AF_0;
gpio.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
gpio.Speed = LL_GPIO_SPEED_HIGH;

/* teraz przekazujemy gotową konfigurację PA8 */
LL_GPIO_Init(GPIOA, &gpio);


for(;;){
/* ustawiamy stan wysoki na PA5 i czekamy 100ms */
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(100);

/* ustawiamy stan niski na PA5 i czekamy 100ms */
LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(100);
}
}


Od razu pokażę Wam efekt jego działania:
PA5.png

Jak widać na pinie PA5 mamy zmiany stanu co ~100ms. Więc to już jest pierwszym pozytywnym symptomem poprawności działania naszego kodu. Sprawdźmy też czy na PA8 znajduje się przebieg 24MHz.
MCO.png

Cóż. Nie jest to przebieg idealnie prostokątny, ale częstotliwość się zgadza, zatem możemy stwierdzić, że poprawnie skonfigurowaliśmy cały łańcuch i mamy rdzeń taktowany 48MHz.

Teraz omówimy co, i najważniejsze: jak musieliśmy zmienić by uzyskać takie taktowanie.

Bieżący kod został nieco rozszerzony w stosunku do poprzednika. Na początek dodaliśmy dwie dyrektywy by wykorzystać makra znajdujące się w dwóch plikach bibliotek LL:
- stm32f0xx_ll_rcc.h - ten plik odnosi się do podsystemu RCC (Reset and Clock Control). W nim znajdują się poszczególne makra, pozwalające przestawiać odpowiednie bity odpowiedzialne za wybór jednostki taktującej i kierowanie przebiegiem ich sygnałów, ustawienia dzielników częstotliwości oraz układu powielania częstotliwości PLL.
- stm32f0xx_ll_system.h - tu znajdziemy odpowiednie makra by zarządzać pewnymi funkcjami wbudowanej pamięci Flash, portem debugowania i specyficznymi ustawieniami systemu.[/list]

Następnie zdefiniowaliśmy sobie funkcję SystemClock_Config() gdzie przeprowadzimy cały proces przełączenia na nowe taktowanie. Musimy go rozpocząć funkcją LL_FLASH_SetLatency(), ponieważ naszą docelową częstotliwością taktowania będzie 48MHz, a pamięć Flash nie będzie na tyle szybka by mogła przy takim taktowaniu pracować i trzeba będzie wstawiać puste cykle. Firma ST zaleca by stosować 1 cykl opóźniający gdy przekraczamy granicę 24MHz sygnału SYSCLK, wobec tego jako parametr funkcji podajemy LL_FLASH_LATENCY_1. Tą operację powinniśmy wykonać już teraz przed dalszymi zmianami, by uniknąć sytuacji, gdzie wprowadzając nowe taktowanie blokujemy możliwość wykonywania dalszych instrukcji z powodu niespełnionych zależności czasowych.

Następnie aktywujemy generator HSI poprzez LL_RCC_HSI_Enable(). Niestety każdy generator potrzebuje nieco czasu by się "rozpędzić" i ustabilizować, dlatego też wprowadziliśmy pustą pętlę oczekiwania while(). Sprawdzamy w niej czy status generatora zmienił się na aktywny, wykorzystując do tego makro LL_RCC_HSI_IsReady() zwracające nam 1 gdy generator jest już gotów do normalnej pracy.
Dzięki funkcji LL_RCC_HSI_SetCalibTrimming() możemy w pewnym stopniu dostrajać nasz generator by uzyskać częstotliwość zbliżoną do deklarowanej. Jak każdy generator zbudowany na obwodzie RC posiada pewien rozrzut częstotliwości. Producent układu deklaruje 1% dokładności tego generatora i dodatkowo sam go wstępnie kalibruje. Wartość 16 to domyślna wartość dostrojenia. Wykorzystując odpowiednie techniki - opisane m.in. w dokumentacji - możemy też w samym kodzie wyliczyć potrzebną wartość by zawsze uzyskać najbardziej dokładny przebieg.

Jeśli mamy już pracujący poprawnie generator podstawowy możemy skonfigurować układ PLL by powielił naszą częstotliwość. W związku z tym wywołujemy makro LL_RCC_PLL_ConfigDomain_SYS(). Jako pierwszy parametr musimy podać źródło jakim będziemy napędzać układ PLL. W naszym przypadku będzie to HSI stąd podajemy LL_RCC_PLLSOURCE_HSI. Gdybyśmy chcieli użyć generatora HSE użylibyśmy zaś LL_RCC_PLLSOURCE_HSE, oczywiście wcześniej go uruchamiając podobnie jak w przypadku HSI.
Jako drugi parametr musimy podać krotność powielenia. Chcąc uzyskać 48MHz, łatwo wyliczyć, że dzieląc 48MHz / 8MHz (częstotliwość docelowa / częstotliwość bazowa) wychodzi nam, że powinniśmy powielić częstotliwość 6 razy, stąd parametr LL_RCC_PLL_MUL_6. Jest ona prawdziwa, gdy spełnimy jeszcze jeden warunek: nasza częstotliwość bazowa przed trafieniem do układu PLL nie zostanie dodatkowo podzielona. Stopień podziału określamy w trzecim parametrze. Tutaj użyliśmy LL_RCC_PREDIV_DIV_1, zatem do wejścia doprowadzamy niepodzielony sygnał 8MHz. Gdybyśmy skorzystali np. z generatora HSE i podłączyli rezonator kwarcowy 16MHz, to dla stabilniejszej pracy PLL powinniśmy tą częstotliwość wstępnie podzielić przez dwa (lub nawet cztery) i zwiększyć krotność. Dlatego oba te parametry powinno się dobierać razem.
Jak już skonfigurujemy układ PLL możemy go włączyć makrem LL_RCC_PLL_Enable() i znów poczekać w pustej pętli sprawdzającej stan przez LL_RCC_PLL_IsReady() na jego ustabilizowanie.

Mając już wybrane źródło taktowania i skonfigurowany układ PLL możemy dodatkowo skonfigurować dwa pozostałe dzielniki służące do określania częstotliwości pracy magistral AHB oraz APB. Wykorzystujemy do tego makra:

Code: Select all

LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);


Teraz mamy już w pełni skonfigurowany "łańcuch" taktowania i możemy przełączyć się na nowe źródło taktowania. Wywołujemy w tym celu LL_RCC_SetSysClkSource(), podając jako parametr nowe źródło: LL_RCC_SYS_CLKSOURCE_PLL. Tu również powinniśmy sprawdzić czy nasze zmiany zostały przez układ RCC wykonane za pomocą LL_RCC_GetSysClkSource(), które zwraca nam bieżący status. Zaczekamy, aż pojawi się wartość LL_RCC_SYS_CLKSOURCE_STATUS_PLL, która będzie oznaczać, że nasze przełączenie w pełni się odbyło.

Pozostaje nam tylko drobna "kosmetyka". Za pomocą specjalnego makra LL_SetSystemCoreClock() ustawiamy wartość liczbową - w Hz - nowego taktowania. Wartość ta trafi do specjalnej zmiennej SystemCoreClock, którą możemy wykorzystać później. Warto to też zrobić ze względu na to, że niektóre funkcje lub inne biblioteki mogą wykorzystywać tą zmienną do określania częstotliwości pracy procesora z jaką przyszło im pracować.
Ostatnią linijką naszej funkcji SystemClock_Config będzie przełączenie multipleksera wybierającego źródło sygnału MCO (Master Clock Output) na przebieg SYSCLK i włączenie podziału tej częstotliwości przez 2: LL_RCC_ConfigMCO(LL_RCC_MCO1SOURCE_SYSCLK, LL_RCC_MCO1_DIV_2). Sygnał ten jeszcze się nie pojawi na podłączonym do MCO pinie procesora. Musimy jeszcze skonfigurować odpowiednio ten pin, ale zrobimy to już poza tą funkcją.


Nasza pętla główna również została nieco zmodyfikowana. Na początek dodajemy wywołanie nowej funkcji konfiguracji zegara jaką napisaliśmy wcześniej. Licznik SysTick skonfigurujemy już z pomocą zmiennej SystemCoreClock, dzięki temu jeśli poprawnie zmienimy treść funkcji SystemClock_Config - np. eksperymentując z innymi parametrami i przeliczając częstotliwość - to czas bazowy opóźnień 1ms będzie mimo tego zachowany.

Dodamy jeszcze część odpowiedzialną za konfigurację pinu PA8, którego jedną z funkcji alternatywnych jest wyprowadzenie na zewnątrz sygnału MCO. Zrobimy to tak samo strukturalnie jak poprzednio wykorzystując nowe pola:

- W pole "Alternate" wpiszemy: LL_GPIO_AF_0. Oznacza to, że wewnętrzny multiplekser wybierający jaką grupę funkcji alternatywnych przyłączyć do wybranego pinu GPIO będzie przełączony na grupę AF0. W niej właśnie PA8 staje się wyjściem MCO. Takich grup mamy 8; ich dokładna specyfikacja z rozpiską tych funkcji znajduje się w nocie katalogowej STM32F072 (str. 41).

- W pole "Speed" wpiszemy jeszcze: LL_GPIO_SPEED_HIGH. Dzięki temu nasze układy GPIO będą pracowały z najwyższą możliwą częstotliwością. Gdybyśmy wybrali niższą częstotliwość, np. LL_GPIO_SPEED_LOW wtedy nasz przebieg na wyjściu MCO przypominałby bardziej "tępą piłę" niż prostokąt, gdyż układy buforowe nie byłyby w stanie nadążać nad zmianami stanu przy częstotliwości 24MHz.

Kto chce może znów posłużyć się czystymi makrami oferowanymi przez biblioteki LL:

Code: Select all

LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_8, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetAFPin_8_15(GPIOA, LL_GPIO_PIN_8, LL_GPIO_AF_0);
LL_GPIO_SetPinOutputType(GPIOA, LL_GPIO_PIN_8, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed(GPIOA, LL_GPIO_PIN_8, LL_GPIO_SPEED_HIGH);


Przy czym należy uważać przy definiowaniu grup funkcji alternatywnych, gdyż mamy dwa makra LL_GPIO_SetAFPin. Pierwsze LL_GPIO_SetAFPin_0_7 służy do zdefiniowania grupy dla pinów z zakresu 0 do 7, a drugie LL_GPIO_SetAFPin_8_15 pozostałych. Gdybyśmy przykładowo chcieli przypisać pin PA3 do grupy AF1 to użylibyśmy:

Code: Select all

LL_GPIO_SetAFPin_0_7(GPIOA, LL_GPIO_PIN_3, LL_GPIO_AF_1);


Wersja "pure low-level" funkcji main():

Code: Select all

int main(void)
{
/* zmieniamy konfigurację taktowania */
SystemClock_Config();

/* konfigurujemy SysTick by można użyc prostej funkcji
* opóźniającej
* szybkośc taktowania pobierzemy z specjalnej zmiennej
*/
LL_Init1msTick(SystemCoreClock);

/* włączamy zegar dla GPIOA */
LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOA);

/* dokonamy niskopoziomowej konfiguracji PA5 */
LL_GPIO_SetPinMode( GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType( GPIOA, LL_GPIO_PIN_5, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed( GPIOA, LL_GPIO_PIN_5, LL_GPIO_SPEED_LOW);

/* oraz PA8 */
LL_GPIO_SetPinMode( GPIOA, LL_GPIO_PIN_8, LL_GPIO_MODE_ALTERNATE);
LL_GPIO_SetAFPin_8_15( GPIOA, LL_GPIO_PIN_8, LL_GPIO_AF_0);
LL_GPIO_SetPinOutputType( GPIOA, LL_GPIO_PIN_8, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed( GPIOA, LL_GPIO_PIN_8, LL_GPIO_SPEED_HIGH);

for(;;){
/* ustawiamy stan wysoki na PA5 i czekamy 100ms */
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(100);

/* ustawiamy stan niski na PA5 i czekamy 100ms */
LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(100);
}
}


I to tyle co dzieje się w naszym drugim przykładzie. By dopełnić opis zmodyfikujemy go jeszcze raz aby wykorzystać jeszcze jeden pin jako wejście. Dzięki temu wejdziemy w prostą interakcję z naszym kodem...

Rozpoczynamy od dodania parę linijek tuż przed definicją funkcji SystemClock_Config(). Wstawimy tam pewną tabelę z zadeklarowanymi wartościami opóźnień i zmienną, za pomocą której będziemy się po tej tabeli przemieszczać:

Code: Select all

/* maksymalna pozycja w tabeli */
#define DELAY_TABLE_MAX 4

/* deklaracja tabeli opóźnień
* będzie większa o jeden niż maksymalna pozycja
*/
const uint32_t DELAY_TABLE[DELAY_TABLE_MAX+1] = { 1, 100, 250, 500, 1000 };

/* bieżąca pozycja w tabeli
* skąd brane będzie opóźnienie */
uint32_t didx = DELAY_TABLE_MAX;


Teraz musimy skonfigurować jeden pin jako wejście. Na płytce Nucleo jest jeden przycisk użytkownika podłączony do PC13, który zwiera to wejście do masy. Stąd powinniśmy wykorzystać jeszcze opcję podciągania wejścia do VDD.

INFO: Wejście ma już podciągnięcie na płytce Nucleo, ale ktoś kto uruchamiałby ten kod gdzie indziej mógłby go nie mieć więc pull-up został mimo to włączony

Konfigurację przeprowadzimy trzema instrukcjami, które wstawimy do funkcji main() jeszcze przed pętlą for(). Pierwsza z nich włączy przebieg zegarowy dla układu GPIOC - gdyż jeszcze z niego nie korzystaliśmy. Następnie przełączymy tryb pracy na wejście oraz włączymy wewnętrzny pull-up.

Code: Select all

LL_AHB1_GRP1_EnableClock(LL_AHB1_GRP1_PERIPH_GPIOC);
/* a teraz skonfigurujemy PC13 gdzie mamy przycisk */
LL_GPIO_SetPinMode(GPIOC, LL_GPIO_PIN_13, LL_GPIO_MODE_INPUT);
LL_GPIO_SetPinPull(GPIOC, LL_GPIO_PIN_13, LL_GPIO_PULL_UP);


Teraz zmodyfikujemy naszą pętlę for() by w niej odpytywać o stan przycisku. Zrobimy prosty tzw. "polling". Na końcu pętli skorzystamy z dobrodziejstw funkcji LL_GPIO_IsInputPinSet(), która zwraca nam po prostu zero-jedynkowy stan pinu określonego w parametrach funkcji.

Code: Select all

for(;;){
/* ustawiamy stan wysoki na PA5 i czekamy sekundę */
LL_GPIO_SetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(DELAY_TABLE[didx]);

/* ustawiamy stan niski na PA5 i czekamy sekundę */
LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5);
LL_mDelay(DELAY_TABLE[didx]);

/* sprawdzamy czy klawisz jest naciśnięty
* wtedy funkcja nam zwróci zero
*/
if(LL_GPIO_IsInputPinSet(GPIOC, LL_GPIO_PIN_13) == 0){
/* przesuniemy indeks w tabeli ale
* zapętlimy ją by wracac
*/
if(--didx == 0) didx = DELAY_TABLE_MAX;
}
}


Jak widać nie korzystamy już ze sztywnych opóźnień. Bieżącą wartość opóźnienia pobieramy z tabeli na podstawie indeksu w zmiennej didx. Dopisany pierwszy warunek najpierw sprawdza czy stan pinu PC13 jest zerem, co oznaczałoby, że przycisk jest naciśnięty. Gdyby tak się stało zmniejszamy indeks w tabeli na niższy, co zmieni nam wartość opóźnienia. Oczywiście musimy się zabezpieczyć by nie wyjść poza tabelę, stąd drugi warunek który sprawdzi nam czy już doszliśmy do indeksu o numerze zero. Wtedy natychmiast zapętlimy tabelę zmieniając indeks na najwyższą wartość. Tu wartość w tabeli o indeksie zero nie ma żadnego znaczenia.

Kod taki oczywiście działa, ale jak się pewnie przekonaliście ma kilka wad - a nawet same wady. Zapewne zauważyliście, że przy najwyższym opóźnieniu klawisz trzeba przytrzymać aż przez 2 sekundy by opóźnienie przeskoczy do następnej wartości. Zaś przy najkrótszym opóźnieniu trzeba się starać by odpowiednio szybko puścić klawisz.
Można temu przeciwdziałać na kilka sposobów. Można np. tak skonstruować pętlę, by opóźnienie było niskie - na przykład 25ms - wtedy reakcja na klawisz będzie "natychmiastowa". Ba! Nawet nie będzie trzeba przeciwdziałać drganiom styków, bo kolejne obiegi pętli i tak będą dłuższe.
Aby taki mechanizm zrealizować z zachowaniem obecnych opóźnień w osobnej zmiennej należy zliczać przejścia pętli. Jeśli zliczymy odpowiednią ilość przejść zmienimy stan pinu. Zatem, aby uzyskać opóźnienie 250ms pomiędzy kolejnymi zmianami stanu diody LED będzie trzeba zliczyć ich 10: 10 × 25ms = 250ms; pół sekundy będzie ich wymagać 2 razy więcej, itd. Takie wartości możemy również stabelaryzować.
Kto chce może taki kod napisać, będzie on tylko nieco dłuższy niż obecny, zwłaszcza, że możemy wykorzystać jeszcze jedną funkcję związaną ze zmienianiem stanu pinów na przeciwny: LL_GPIO_TogglePin() - parametry są takie same jak w przypadku LL_GPIO_SetOutputPin().


My jednak nie będziemy tego robić tą metodą. W następnym odcinku poznamy mechanizm, który pozwoli nam reagować na zmianę stanu przycisku bez ciągłego jego odpytywania. To przycisk sam "powie nam" kiedy zareagować...
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.
Ostatnio zmieniony sobota 27 paź 2018, 14:15 przez ZbeeGin, łącznie zmieniany 2 razy.

Awatar użytkownika
ZbeeGin
User
User
Posty: 492
Rejestracja: sobota 08 lip 2017, 17:16
Lokalizacja: Śląsko-Zagłębiowska Metropolia
Kontaktowanie:

Re: Programowanie STM32 M0 z wykorzystaniem bibliotek LL - Cz. 2: Witaj świecie, tu dioda LED - Suplement

Postautor: ZbeeGin » wtorek 25 wrz 2018, 22:32

"A teraz coś z zupełnie innej beczki."

Jak jeszcze część 2 nie była nawet w połowie napisana to otrzymałem mniej więcej taki feedback (poza forum µG):

Zbychu, nie ucz ich pisania LL_GPIO_ResetOutputPin(GPIOA, LL_GPIO_PIN_5), bo potem potem przychodzą takie juniory i walą »GPIO_PIN_5«.


W zasadzie trudno się tu nie zgodzić z tym stwierdzeniem. Takiego podejścia broni jedynie to, że miały to być proste przykłady pokazujące konkretne makra/funkcje z biblioteki LL w konkretnych zastosowaniach. Nie czuję się tu żadnym ekspertem od poprawności kodowania. Jest parę osób na forum, które mają o wiele większe doświadczenie i kompetencje w tej dziedzinie. Czuję się jednak w obowiązku zaproponować kilka prostych rozwiązań pozostających w nurcie Low-Level.

Załóżmy, że dalej modyfikujemy ostatni kod, w wersji z makrami, a nie ze strukturą. Co możemy zrobić by był nieco bardziej czytelny i przenośny. Przede wszystkim możemy skorzystać z dobrodziejstw #define. Mamy diodę LED, którą sterujemy włącz-wyłącz. Wiadomo, że jest ona podłączona do pinu PA5. Spróbujmy to zatem to tak napisać, by nie używać w dalszej części programu takiego twardego przypisania.

Code: Select all

#define LED_GPIO GPIOA
#define LED_PIN LL_GPIO_PIN_5

#define LED_ON LL_GPIO_SetOutputPin(LED_GPIO, LED_PIN)
#define LED_OFF LL_GPIO_ResetOutputPin(LED_GPIO, LED_PIN)
#define LED_TOGGLE LL_GPIO_TogglePin(LED_GPIO, LED_PIN)


Taka konstrukcja pozwoli nam na łatwiejsze przenoszenie sterowania z miejsca na miejsce. Przykładowo chcąc by LED sterowany był przez PB0 po prostu zmienimy tylko wartości identyfikatorów LED_GPIO i LED_PIN. Reszta zostanie taka sama. Zdefiniowanie LED_ON, LED_OFF, LED_TOGGLE pozwoli nam też mniej przejmować się tym co na jakim porcie musimy zrobić. Piszemy LED_ON; i dioda się zapala. Piszemy LED_OFF; i dioda sobie gaśnie.

Code: Select all

/* zaświecamy diodę i czekamy 100ms */
LED_ON;
LL_mDelay(100);

/* gasimy diodę i czekamy 100ms */
LED_OFF;
LL_mDelay(100);


Jeśli byśmy potem zmienili sposób podłączenia diody by była sterowana od strony katody - czyli aktywnym stanem będzie stan niski - to po prostu zamieniamy wywołania funkcji z zachowaniem znaczenia LED_ON/LED_OFF:

Code: Select all

#define LED_ON LL_GPIO_ResetOutputPin(LED_GPIO, LED_PIN)
#define LED_OFF LL_GPIO_SetOutputPin(LED_GPIO, LED_PIN)


W samej konfiguracji też jeśli zastąpimy:

Code: Select all

LL_GPIO_SetPinMode( GPIOA, LL_GPIO_PIN_5, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType( GPIOA, LL_GPIO_PIN_5, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed( GPIOA, LL_GPIO_PIN_5, LL_GPIO_SPEED_LOW[/b]);


przez:

Code: Select all

LL_GPIO_SetPinMode( LED_GPIO, LED_PIN, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType( LED_GPIO, LED_PIN, LL_GPIO_OUTPUT_PUSHPULL);
LL_GPIO_SetPinSpeed( LED_GPIO, LED_PIN, LL_GPIO_SPEED_LOW);


Również zrobi się jakby czytelniej i bardziej elastycznie. Podobnie można zrobić z pinem dostarczającym MCO, który może mieć inne położenie w zależności od wybranego układu mikrokontrolera.


Także przycisk w ostatnim kodzie można potraktować w podobny sposób:

Code: Select all

#define BUTTON_GPIO GPIOC
#define BUTTON_PIN LL_GPIO_PIN_13

#define IS_BUTTON_PRESS (LL_GPIO_IsInputPinSet(BUTTON_GPIO, BUTTON_PIN) == 0)


i później przy testowaniu jego stanu napisać:

Code: Select all

if(IS_BUTTON_PRESS){
/* przesuniemy indeks w tabeli ale
* zapętlimy ją by wracać na "początek"
*/
if(--didx == 0) didx = DELAY_TABLE_MAX;
}


Znów ukrywając szczegóły gdzie i jak został on podłączony. W tym wypadku należy pamiętać by definicje zawierające wyrażenia obowiązkowo otoczyć nawiasami. Inaczej się to zemści w najmniej oczekiwanym przypadku.


Wróć do „Programowanie STM w C/C++”

Kto jest online

Użytkownicy przeglądający to forum: Obecnie na forum nie ma żadnego zarejestrowanego użytkownika i 6 gości