24 października 2018

GPIO raz jeszcze

Obsługa linii wejścia-wyjścia, czyli GPIO to pozornie prosty temat. Niestety jak zwykle pozory mylą.
Tym razem zajmę się prędkością działania wyjść. Za ustawianie prędkości portu odpowiedzialny jestr rejestr GPIO_OSPEEDR:

Jak łatwo zauważyć, każdy pin jest konfigurowany za pomocą dwóch bitów. Mamy możliwość ustawienia trzech prędkości przełączania wyprowadzenia: niskiej, średniej i wysokiej.
Co znaczą te określenia odnajdziemy w nocie katalogowej naszego mikrokontrolera:
Postanowiłem sprawdzić jak działanie wygląda w praktyce. Napisałem więc prosty program, który jednocześnie przełącza trzy piny - z których każdy ma inne ustawienia prędkości:

#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER9_0 | GPIO_MODER_MODER5_0;

 GPIOC->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR9_0;
 GPIOC->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR5_0|GPIO_OSPEEDER_OSPEEDR5_1;

 while (1)
 {
  GPIOC->BSRR = GPIO_BSRR_BS_8 | GPIO_BSRR_BS_9 | GPIO_BSRR_BS_5;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->BSRR = GPIO_BSRR_BR_8 | GPIO_BSRR_BR_9 | GPIO_BSRR_BR_5;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
 }
}

W ramach testów podłączyłem oscyloskop do wspomnianych wyprowadzeń. Wolnozmienny sygnał nie pokazuje różnic między ustawieniami pinów:
Jednak po zmianie podstawy czasu widać różnice w czasie narastania sygnału. Kanał 1 to wysoka prędkość działania wyjścia, drugi - średnia i trzeci - niska:


Podobnie sytuacja wygląda dla zbocza opadającego:


Właściwie na tym mógłbym zakończyć testowanie prędkości wyprowadzeń GPIO, ale nie byłbym sobą gdybym nie spróbował napisać programu, który wygeneruje możliwie wysoką częstotliwość na wyjściu.
Usunięcie opóźnień z prezentowanego wcześniej programu niewiele pomogło. Kolejnym krokiem było więc ustawienie maksymalnej optymalizacji kodu. Niestety nadal uzyskana częstotliwość niewiele przekraczała 2 MHz.
Postanowiłem więc zmienić C na stary dobry asembler i przygotowałem taką funkcję testową:

 .syntax unified
   .thumb

 .global test_gpio

 .equ GPIOC, 0x48000800
 .equ BSRR, 0x18
 .equ BRR, 0x28

 .section .text
 .type test_gpio, %function
test_gpio:
 ldr  r0, =0x0100 | 0x0200 | 0x0020
 ldr  r1, =GPIOC
 ldr  r2, =GPIOC
main_loop:
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]
 str  r0, [r1, BSRR]
 str  r0, [r1, BRR]

 b main_loop

Powtarzanie instrukcji str zamiast prostej pętli miało na celu wyeliminowanie opóźnienia instrukcji skoku.
Efekt był dużo lepszy niż w przypadku języka C:

Nie dawało mi jednak spokoju skąd się bierze asymetria sygnału. Postanowiłem więc przenieść program testowy z pamięci Flash do SRAM. Efekt był znacznie lepszy:

Wykonanie instrukcji LDR oraz STR zajmuje 2 cykle maszynowe. Przy częstotliwości taktowania 48 MHz utrzymujemy więc 12 MHz na wyjściu.

Wniosek jest taki, że STM32F030 nie jest demonem prędkości. No i warto pamiętać, że opóźnienia dostępów do pamięci flash mogą mieć wpływ na działanie programu.

23 października 2018

Wydajność procesora i wpływ pamięci flash

Tym razem kilka słów o wydajności układu - wiadomo producent podaje niesamowite parametry, ale jak to wygląda faktycznie? Niech testem będzie zwykłe zwiększanie wartości zmiennej. Pomiar będzie wykonywany co 1ms, test polega na ustaleniu do ilu program jest w stanie doliczyć.
Testując program szybko odkryłem, że dodawanie nie jest operacją atomową, stąd wyłączanie przerwań. To oczywiście spowalnia działanie programu, ale chociaż daje powtarzalne wyniki. Pierwsza wersja programu używa wbudowanego generatora 8MHz i nie ma dodatkowych instrukcji NOP. Testy będą polegały na dokładaniu instrukcji pustych i pomiarze ich wpływu.
Kod programu testowego:

#include "stm32f0xx.h"

volatile uint32_t counter;
volatile uint32_t counter_shadow;

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

void SysTick_Handler(void)
{
 counter_shadow = counter;
 counter = 0;
}

int main(void)
{
 //clock_init();

 SysTick_Config(8000);

 while (1)
 {
  __disable_irq();
  counter++;
  //asm volatile ("nop");
  //asm volatile ("nop");
  //asm volatile ("nop");
  __enable_irq();
 }
}

A teraz wyniki:
  • bez nop: 568
  • 1 nop: 530
  • 2 nop-y: 497
  • 3 nop-y: 468
W ciągu 1ms wykonywanych jest 8000 cykli zegara - jak widać maksymalny wynik licznika to 568, to nieco mniej niż można byłoby oczekiwać.

Dużo ciekawiej jest natomiast, gdy użyjemy taktowania 48MHz.
  • bez nop: 2283
  • 1 nop: 2085
  • 2 nop-y: 1998
  • 3 nop-y: 1844
Tutaj mamy dwa wnioski, pierwszy dla mnie niespodziewany to raptem 4 krotny wzrost wydajności przy 6 krotnym wzroście częstotliwości taktowania.
Drugi wniosek, tym razem spodziewany, to wpływ opóźnień dostępu do pamięci flash na działanie programu. Widać to na małej różnicy w wydajności programu z jedną i dwoma instrukcjami nop. Po prostu dodatkowy dostęp do pamięci zajmuje i tak cykl zegara, stąd podobna wydajność.



GPIO ciąg dalszy

Obiecałem wrócić do linii wejścia-wyjścia, więc wracam. Na początek wyjaśnienie o co chodziło z rejestrem ODR. Najlepiej będzie wyjaśnić problem na przykładzie - tylko zamiast migania diodami użyję analizatora stanów logicznych. Z diodami byłoby dokładnie tak samo, ale trudniej zauważyć efekt. A diagram powinien być wart tysiąc słów.
Na początek to co było, czyli program generujący przebieg prostokątny. Używam już taktowania 48MHz, inicjalizację zegara przeniosłem do nowej funkcji clock_init() - zmieniłem też pin używany do testu. Wybrałem PC8, bo jest łatwo dostępny. Później będę jeszcze używał PC6, który jest wyprowadzony zaraz obok. To co ważne to użycie dwóch pinów z tego samego portu. No i oczywiście wygoda łączenia kabelków.
Pierwsza wersja programu:

#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER6_0;

 while (1)
 {
  GPIOC->ODR |= GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->ODR &= ~GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
 }
} 

Program oczywiście testuję, przebieg sygnału wygląda zgodnie z oczekiwaniami:


Teraz potrzebne będzie przerwanie. Procesory z rdzeniem Cortex-M mają wbudowany licznik SysTick najczęściej używany przez systemy operacyjne. Jest to bardzo prosty w użyciu moduł, możemy go więc wykorzystać do sterowania pinu PC6.
Najpierw piszemy funkcję obsługi przerwania - jest to zwykła funkcja o nazwie SysTick_Handler. W niej będziemy zmieniać stan pinu PC6 na przeciwny. Kod ładniej wyglądałby z użyciem operatora alternatywy wykluczającej (XOR), ale chciałem zostać przy znanym kodzie.
Cała konfiguracja timera sprowadza się do wywołania funkcji SysTick_Config. Jako parametr podajemy liczbę taktów zegara systemowego między przerwaniami. Ponieważ częstotliwość taktowania to 48MHz, podaję 48000 aby uzyskać przerwania co 1ms.
Na początek komentuję treść pętli głównej i sprawdzam, czy program działa:

#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

void SysTick_Handler(void)
{
 static int output_on = 0;

 output_on = !output_on;

 if (output_on)
  GPIOC->ODR |= GPIO_ODR_6;
 else
  GPIOC->ODR &= ~GPIO_ODR_6;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER6_0;

 SysTick_Config(48000);

 while (1)
 {
  /*
  GPIOC->ODR |= GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->ODR &= ~GPIO_ODR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
  */
 }
}

Na analizatorze używam już dwóch kanałów, pierwszy jest podłączony do pinu PC8, drugi do właśnie testowanego PC6.

Wszystko wygląda pięknie, czas coś popsuć. Usuwam więc komentarz z pętli głównej programu i uruchamiam analizator logiczny. Okazało się, że wcale nie tak łatwo błąd wytropić, ale w końcu dobrałem wyzwalanie i wyszło coś takiego:

W środkowej części widzimy zakłócenie na linii PC6. Przerwanie od SysTick pojawiło się dokładnie w momencie, gdy główny program był między pobraniem zawartości rejestru ODR, a zapisaniem zmienionej wersji. Kod przerwania zmienił zawartość rejestru ODR, ale chwilę później program główny nadpisał tą zmianę.
Takie działanie może wyglądać na mało prawdopodobne i nieszkodliwe. ale to czasem bardzo poważny błąd. Po pierwsze trudny do wykrycia - występuje tylko w pechowych chwilach, gdy przerwanie pojawi się w nieodpowiednim momencie. Przykładowy program i tak co 1ms zmieniał stan PC6, więc błąd został po chwili naprawiony - ale co jeśli to przerwanie ustawiałoby tylko raz i to ważny moduł...

Na szczęście używanie rejestru ODR to nie jedyna możliwość. Są jeszcze dwa rejestry BSRR pozwala na ustawianie oraz zerowanie stanu pinów w sposób atomowy, a BRR tylko na zerowanie.

Jak widzimy dolne 16 bitów rejestru BSRR nazywane jest BSx, gdzie x to numer pinu - służą one do ustawiania stanu wysokiego na odpowiednim wyjściu. Analogicznie wyższe bity zostały nazwane BRx i służą do ustawiania stanu niskiego.
Rejestr BRR ma tylko bity BRx:
Jego funkcjonalność duplikuje się z BSRR, ale wygodne jest to że te same bity w BSRR ustawiają stan wysoki, co stan niski w BRR. Niektóre modele STM32 nie mają rejestru BRR, ale BSRR pojawia się właściwie zawsze.
Najważniejsza różnica to ignorowanie zapisu zer do rejestrów BSRR oraz BRR. Zapis jedynki ustawia lub zeruje odpowiednie wyprowadzenie, natomiast zero nic nie zmienia. Dzięki temu wystarczy zwykły zapis (=) do rejestru, zamiast operacji alternatywy (|=), czy koniunkcji (&=). Ponieważ zapisy są atomowe, więc w trakcie ich wykonywania nie może pojawić się przerwanie.
Zmienię teraz program, tak aby używał rejestrów BSRR oraz BRR zamiast ODR:


#include "stm32f0xx.h"

void clock_init(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;
}

void SysTick_Handler(void)
{
 static int output_on = 0;

 output_on = !output_on;

 if (output_on)
  GPIOC->BSRR = GPIO_BSRR_BS_6;
 else
  GPIOC->BSRR = GPIO_BSRR_BR_6;
}

int main(void)
{
 clock_init();

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER8_0 | GPIO_MODER_MODER6_0;

 SysTick_Config(48000);

 while (1)
 {
  GPIOC->BSRR = GPIO_BSRR_BS_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);

  GPIOC->BSRR = GPIO_BSRR_BR_8;
  for (volatile uint32_t dly = 0; dly < 1000; dly++);
 }
}

Na koniec test dla upewnienia się, że wszystko działa jak powinno:



Od tej chwili będę się starał unikać używania ODR - łatwo w jego przypadku o trudne do wychwycenia błędy w programie.



22 października 2018

Taktowanie mikrokontrolera

W kolejnych wpisach planuję wrócić do GPIO, ale najpierw kilka słów o taktowaniu układu. To bardzo ważny temat, więc warto od niego zacząć.
Jak zwykle wszystko opisane jest w dokumentacji układu, niestety zamieszczony diagram może w pierwszej chwili wyglądać nieco odstraszająco:
Okazuje się, że większość tego schematu można (na razie) pominąć. W dolnej części widać MCO, czyli wyjście generatora - można je wykorzystać do taktowania innego układu podłączonego z naszym mikrokontrolerem. MCO przyda się również do przetestowania konfiguracji.
Na górze oraz po prawej stronie widzimy taktowanie peryferiów - w przypadku STM32F030 zarówno rdzeń, jak i peryferia mogą być taktowane z tą samą częstotliwością, więc również ten fragment można pominąć.
Nieco powyżej MCO widzimy również taktowanie dla RTC, czyli zegara czasu rzeczywistego - ten moduł też na razie pominiemy.
Można więc nieco ograniczyć wspomniany diagram:
Pominięte moduły są teraz zaznaczone odpowiednio kolorami:
  • fioletowy - MCO
  • czerwony - RTC
  • zielony - peryferia
To co zostaje jest znacznie mniejsze, chociaż nadal ciekawe:


Mamy do dyspozycji dwa generatory sygnału zegarowego. Zaznaczony zieloną elipsą wbudowany generator RC o częstotliwości 8MHz. Na czerwono zaznaczyłem moduł zewnętrznego generatora - może on pracować z rezonatorem kwarcowym, którego niestety nie ma na płytce Nucleo. Możliwa jest również konfiguracja z zewnętrznym źródłem sygnału taktującego - ta wersja jest dostępna przy wykorzystaniu sygnału 8 MHz z programatora na płytce Nucleo.

Na żółto zaznaczona jest pętla PLL, która pozwala na podniesienie częstotliwości jednego z sygnałów opisanych wcześniej.
Na niebiesko wreszcie zaznaczyłem multiplekser, który pozawala na wybór źródła taktowania samego układu.
Po resecie źródłem taktowania jest wewnętrzny generator RC o częstotliwości 8 MHz. Można się o tym bardzo łatwo przekonać - wystarczy ustawić pin PA8 tryb funkcji alternatywnej, którą domyślnie jest właśnie MCO. Więc po dodaniu kodu:

    GPIOA->MODER |= GPIO_MODER_MODER8_1;
    RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;


Na wspomnianym pinie pojawi się taktowanie mikrokontrolera (SYSCLK - zegar systemowy).
Aby się upewnić że obserwujemy sygnał z generatora RC, czyli HSI najlepiej nieco zmodyfikować częstotliwość. W rejestrze RCC_CR znajdziemy bity HSITRIM, które pozwalają na korektę częstotliwości tego generatora. Domyślna wartość jak widzimy była bardzo bliska 8MHz, zmienimy jej wartość zerujący wszystkie bity HSITRIM:

    RCC->CR = (RCC->CR & ~RCC_CR_HSITRIM_Msk);

Jak widać, częstotliwość jest teraz trochę niższa:
Skoro generator RC, czyli HSI już znamy można przetestować generator zewnętrzny, czyli HSE. Brak rezonatora kwarcowego niestety nie jest typową konfiguracją, pozostaje więc tylko użycie sygnału z programatora.
Ponieważ korzystamy z zewnętrznego generatora, konieczne będzie ustawienie bitu HSEBYP w rejestrze CR. Następnie wystarczy uruchomić generator ustawiając bit HSEON oraz poczekać na jego zadziałanie, co będzie sygnalizowane zapaleniem bitu HSERDY. Kod wygląda więc następująco:

    RCC->CR |= RCC_CR_HSEBYP | RCC_CR_HSEON;
    while ((RCC->CR & RCC_CR_HSERDY) == 0);


Po jego wykonaniu sygnał HSE jest dostępny, ale mikrokontroler nadal używa HSI. Trzeba więc przełączyć multiplekser - wykonamy to zmieniając odpowiednie bity w rejestrze RCC_CFGR:

    RCC->CFGR |= RCC_CFGR_SW_HSE;

W tym momencie mikrokontroler pracuje w oparciu o sygnał z programatora. Jako sprawdzenie mierzymy jeszcze raz częstotliwość na wyjściu MCO:
Jak widać mamy znowu częstotliwość 8MHz, a jak pamiętamy HSI generuje teraz nieco mniej - więc możemy być pewni, że układ działa jak planowaliśmy.
Teraz czas przejść do pętli fazowej PLL - która jest chyba najczęściej używanym źródłem taktowania.
Sama pętla musi mieć na wejściu sygnał z generatora - możemy wybrać HSI podzielony przez 2 (czyli o częstotliwości 4 MHz), albo HSE który przed chwilą przetestowaliśmy.
Użyję HSI/2, bo ten sygnał jest dostępny nawet przy braku programatora w Nucleo.
Do konfiguracji PLL wykorzystamy rejestr RCC_CFGR. Najpierw ustawimy źródło sygnału na HSI - jest to domyślna wartość, ale dla lepszej czytelności użyjemy stałej RCC_CFGR_PLLSRC_HSI_DIV. Musimy również ustawić mnożnik częstotliwości. Przykładowo niech będzie 4 - powinniśmy uzyskać wówczas częstotliwość 4MHz * 4, czyli 16 MHz.

    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL4;

W kolejnym kroku uruchomimy PLL ustawiając bit PLLON w rejestrze RCC_CR oraz czekamy na zadziałanie pętli - zostanie wówczas ustawiony bit PLLRDY.

    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL4;
    RCC->CR |= RCC_CR_PLLON;

    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

Na koniec wystarczy zmienić źródło taktowania mikrokontrolera na PLL:

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;


Od tej chwili system powinien pracować z częstotliwością 16 MHz co możemy łatwo sprawdzić:

Pętla PLL daje nam ogromne możliwości dopasowania częstotliwości taktowania do oczekiwań projektu. Jednak pewnie najpopularniejsze będzie użycie częstotliwości maksymalnej, czyli 48 MHz. Jak łatwo obliczyć, będziemy do tego potrzebowali mnożnika 12.
Jednak zanim zmienimy program jeszcze jedna ważna informacja. Pamięć Flash może działać z częstotliwością do 24 MHz. Więc jeśli będziemy chcieli zwiększyć taktowanie powyżej tej wartości, musimy dodać opóźnienia dostępu do pamięci (latency).
Uzyskamy to pisząc:

    FLASH->ACR |= FLASH_ACR_LATENCY;

Teraz gdy wszystko gotowe, czas przetestować cały program:

#include "stm32f0xx.h"

int main(void)
{
    RCC->CFGR = RCC_CFGR_PLLSRC_HSI_DIV2 | RCC_CFGR_PLLMUL12;
 RCC->CR |= RCC_CR_PLLON;
    while ((RCC->CR & RCC_CR_PLLRDY) == 0) ;

 FLASH->ACR |= FLASH_ACR_LATENCY;

    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while ((RCC->CFGR & RCC_CFGR_SWS_PLL) != RCC_CFGR_SWS_PLL) ;

 RCC->AHBENR |= RCC_AHBENR_GPIOAEN | RCC_AHBENR_GPIOCEN;

 GPIOC->MODER |= GPIO_MODER_MODER5_0;
 GPIOC->MODER |= GPIO_MODER_MODER6_0;
 GPIOC->MODER |= GPIO_MODER_MODER8_0;

 // clock test
 GPIOA->MODER |= GPIO_MODER_MODER8_1;
 GPIOA->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR8_0 | GPIO_OSPEEDER_OSPEEDR8_1;
 RCC->CFGR |= RCC_CFGR_MCO_SYSCLK;

 while (1)
 {
  GPIOC->BSRR = GPIO_BSRR_BS_5;
  for (volatile uint32_t dly = 0; dly < 600000; dly++)
   ;
  GPIOC->BSRR = GPIO_BSRR_BR_5;

  GPIOC->BSRR = GPIO_BSRR_BS_6;
  for (volatile uint32_t dly = 0; dly < 600000; dly++)
   ;
  GPIOC->BSRR = GPIO_BSRR_BR_6;

  GPIOC->BSRR = GPIO_BSRR_BS_8;
  for (volatile uint32_t dly = 0; dly < 600000; dly++)
   ;
  GPIOC->BSRR = GPIO_BSRR_BR_8;
 }
}

Wynik spełnia oczekiwania (z dokładnością to możliwości analizatora oczywiście):

Jeden szczegół w kodzie został pominięty - mianowicie odwołanie do rejestru GPIOA_OSPEEDR. Przy częstotliwości 48MHz konieczne jest ustawienie maksymalnej prędkości pracy pinu. Wrócę jeszcze do tego tematu.


Małe wyjaśnienie pierwszego programu

W poprzednim wpisie wykorzystałem prosty program do przetestowania działania płytki. Wypadałoby jednak napisać kilka słów wyjaśnienia - co i jak działa.
Dioda LD2 jest podłączona na płytce Nucleo do pinu PA5. Ta informacja jest głęboko ukryta w dokumentacji płytki ewaluacyjnej, jednak skoro działa to pewnie wszystko się zgadza.
Pierwsza linijka program to uruchomienie zegara dla portu A:

RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

Moduł RCC jest odpowiedzialny za konfigurację zegarów, po resecie wszystkie moduły poza pamięcią Flash oraz SRAM są wyłączone. Ponieważ chcemy sterować diodą LD2, uruchamiamy taktowanie zegara dla portu A. Więcej o rejestrze AHBENR znajdziemy w dokumentacji:

 
Jak łatwo się domyślić znajdziemy tam piny odpowiadające za taktowanie pozostałych portów (B,C,D,E,F), moduł obliczania sumy kontrolnej (CRC), kontroler pamięci Flash, SRAM oraz DMA.

Po resecie wartość tego rejestru to 0x14, co odpowiada wspomnianemu włączeniu taktowania pamięci Flash oraz SRAM.

Kolejne dwie linijki to ustawienie trybu dla pinu PA5 jako wyjścia:

    GPIOA->MODER |= GPIO_MODER_MODER5_0;
    GPIOA->MODER &= ~GPIO_MODER_MODER5_1;


Za konfigurację każdego pinu odpowiadają dwa bity - co daje 4 możliwe tryby:

  • 0 - wejście
  • 1 - wyjście
  • 2 - moduł peryferyjny
  • 3 - wejście analogowe
Jak zwykle pełny opis tego rejestru znajdziemy w dokumentacji:


 W przykładzie ustawiłem niższy bit, a wyzerowałem wyższy - w ten sposób wybrana została funkcja "1", czyli pin w trybie wyjściowym. Po resecie układu większość pinów jest w trybie wejścia, czyli rejestr MODER ma wartość zero - stąd instrukcję zerowania bitu można byłoby pominąć, ale tym razem ją zostawiłem dla porządku.

Pętla główna to opóźnienia w pętlach for oraz zapalenie i wygaszenie diody instrukcjami:

        GPIOA->ODR |= GPIO_ODR_5;
        GPIOA->ODR &= ~GPIO_ODR_5;

 
Rejestr ODR przechowuje wartości dla pinów w trybie wyjścia - czyli dokładnie to o co nam chodziło:


Ustawienie odpowiedniego bitu sprawia, że na pinie PA5 pojawia się napięcie 3.3V, a dioda LD5 zaczyna świecić. Natomiast wyzerowanie bitu zmienia napięcie na 0V i gasi diodę LD5. W dokumentacji znajdziemy ważną uwagę o braku atomowości przy dostępie do rejestru ODR. Wrócę do tego później, w największym skrócie: lepiej nie pisać programów jak pokazuje ten przykład.
W przypadku naszego mikorkontrolera operator |= oraz &= jest tłumaczony na kilka instrukcji asemblera, możemy się o tym przekonać zatrzymując program za pomocą debuggera i oglądając jego kod:



Jak widzimy, kompilator wygenerował następujący kod:

   ldr     r2, [r2, #20]
   movs    r1, #32
   orrs    r2, r1
   str     r2, [r3, #20]


Instrukcja LDR wczytuje wartość rejestru ODR, następne dwie instrukcje to zapalenie bitu, a końcowy STR to zapis nowej wartości. Niestety pomiędzy LDR, a STR istnieje ryzyko na pojawienie się przerwania. W obecnym programie to nie problem, ale gdyby w przerwaniu zmienić zawartość rejestru ODR, program mógłby zachowywać się nieprawidłowo. Później wrócę do tego przykładu.
Podsumowując: ten program jest poprawny, chociaż zaprezentowanego sposobu dostępu do pinów należy unikać. Później zobaczymy dlaczego oraz jak zrobić to lepiej.

21 października 2018

Brudnopis do kursu programowania stm32

Wcześniej napisałem jak bardzo nie spodobało mi się TrueSTUDIO oraz używanie domyślnych narzędzi. Okazuje się jednak, że czasem trzeba polubić nawet rzeczy za którymi nie przepadamy. Większość osób jednak lubi system Windows oraz środowiska oparte o Eclipse - a wiersz poleceń systemu Linux oraz starego dobrego make-a jakoś mniej. Nie mam więc wyjścia tylko chociaż na chwilę poddać się ogólnej modzie i opisać jak programować stm32 za pomocą tych narzędzi.
Ten i najbliższe wpisy będą niejako brudnopisem, który może kiedyś zmieni się w kolejny kurs na stronie Forbot-a.
Na początek jak zwykle wybór narzędzi. System i środowisko już wspomniałem, teraz mikrokontroler oraz płytka. Próby z F4 pokazały, że programowanie na poziomie rejestrów jest świetne, ale znacznie lepiej pasuje do prostych peryferiów niż np. pamięci SDRAM, czy wyświetaczy. Stąd pomysł użycia układów z rodziny F0 - w nich chociaż peryferia są łatwiejsze do opanowania.
Wybrałem więc STM32F030 jako jeden z najprostszych i najtańszych układów. Będę używał płytki Nucleo dostępnej np. tutaj: https://kamami.pl/stm32-nucleo/212019-nucleo-f030r8-zestaw-startowy-z-mikrokontrolerem-z-rodziny-stm32-stm32f030.html
Na początek szybki start, czyli instalacja TrueSTUDIO ze strony: https://atollic.com/truestudio/ oraz programu STM32 Cube Programmer: https://www.st.com/en/development-tools/stm32cubeprog.html
Ten drugi program warto najpierw uruchomić oraz podłączyć płytkę Nucleo. W moim przypadku okazało się, że Nucleo ma starszą wersję firmware-u i nie będzie działało z TrueSTUDIO. Za pomocą STM32CubeProgrammer-a udało mi się wykryć przyczynę problemów oraz uaktualnić oprogramowanie.
Kolejny krok to utworzenie projektu w TrueStudio. Opiszę to krok po kroku na potrzeby przyszłego kursu - i trochę dla siebie, żeby nie zapomnieć. Nadal uważam, że make to genialne narzędzie, chociaż nie trzeba tylu zrzutów ekranu żeby zapamiętać jak utworzyć projekt.
Na początek z menu File opcja New oraz Project... Następnie można wybrać jeden z miliona typów projektów - "C Project" wydaje się dobrym wyborem:


Teraz należy podać nazwę projektu oraz doprecyzować jego typ - tym razem wybieram "Embedded C Project" i przechodzę dalej:


Wybierania ciąg dalszy: trzeba się zdecydować jaki ma być typ mikrokontrolera albo wręcz model płytki ewaluacyjnej. Wybieram mikrokontroler STM32F030R8:


Nie jest łatwo, teraz wybór "konfiguracji oprogramowania". Domyślnie zaproponowana biblioteka Newlib-nano brzmi dobrze, rezygnuję za to z "Use tiny printf/sprintf/fprintf" - na początek nie będę z nich korzystać, kiedyś pewnie do tematu wrócę. Opcje optymalizacji zostawiam domyślne:


Następne okno i następne pytania. Tym razem typ programatora - Nucleo ma ST-Link na pokładzie, więc taki wybieram:


Jeszcze pytanie to wybór konfiguracji. Domyślne Debug i Release brzmią rozsądnie, chociaż sam nie wiem po co mi dwie konfiguracje do tak prostego projektu, ale może kiedyś się przydadzą.



Niesamowite, udało się utworzyć projekt! Pytań było mnóstwo, ale warto było - coś powstało:


Domyślnie wygenerowany kod składa się praktycznie z samych komentarzy, chyba ST płaci programistom za liczbę okienek w kreatorach oraz liczbę linijek w plikach, niekoniecznie kodu - im więcej i bardziej bez sensu tym lepiej. Kasuję więc kod i wstawiam własny:

#include "stm32f0xx.h"

int main(void)
{
    RCC->AHBENR |= RCC_AHBENR_GPIOAEN;

    GPIOA->MODER |= GPIO_MODER_MODER5_0;
    GPIOA->MODER &= ~GPIO_MODER_MODER5_1;

    while (1)
    {
        GPIOA->ODR |= GPIO_ODR_5;

        for (volatile uint32_t dly = 0; dly < 100000; dly++)
            ;

        GPIOA->ODR &= ~GPIO_ODR_5;

        for (volatile uint32_t dly = 0; dly < 100000; dly++)
            ;
    }
}

Nie jest może ładniejszy, ale może chociaż działa. Program kompiluje się (o przepraszam buduje) poprawnie, czas wgrać go na płytkę. Po drodze kolejne okienko, tym razem ustawienie debuggera:


Naciskam OK, czekam niecierpliwie a tutaj znowu coś się pojawia:



No tak, na pewno bardzo chciałem mieć breakpoint-a w funkcji main, wybieram kontynuowanie i... mogę się cieszyć migającą diodą.


20 lipca 2018

Pierwsze próby z wyświetlaczem

Jak wspominałem wcześniej moim celem było opanowanie wyświetlacza na płytce stm32f429-discovery. Wbrew pozorom to dość skomplikowane wyzwanie, ale teraz wszystkie niezbędne elementy: PLL, SPI oraz SDRAM są uruchomione, można zacząć przygodę z wyświetlaczem.
Na początek drobna poprawka modułu spi. Poprzednio testowałem go z żyroskopem, ale teraz chciałbym mieć możliwość używania zarówno do obsługi tego czujnika, jak i wyświetlacza. Zmieniam więc niec plik nagłówkowy spi.h:

#ifndef __SPI__
#define __SPI__

#include <stdint.h>

#define SPI_L3GD20              0
#define SPI_LCD                 1

void spi_init(void);

void spi_start(int spi);

void spi_stop(int spi);

uint8_t spi_sendrecv(uint8_t tx);

#endif // __SPI__

Oraz sam kod w spi.c:

#include "spi.h"
#include "stm32f429xx.h"

static void config_pin(GPIO_TypeDef* port, uint8_t pin, uint32_t func)
{
        if (pin < 8) {
                port->AFR[0] |=  func << (pin * 4);
                port->AFR[0] |=  func << (pin * 4);
        } else {
                port->AFR[1] |=  func << ((pin - 8) * 4);
                port->AFR[1] |=  func << ((pin - 8) * 4);
        }
        port->MODER |= 2 << (pin * 2);
        port->OSPEEDR |= 2 << (pin * 2);
}

void spi_init(void)
{
        RCC->APB2ENR |= RCC_APB2ENR_SPI5EN;

        config_pin(GPIOF, 7, 5);
        config_pin(GPIOF, 8, 5);
        config_pin(GPIOF, 9, 5);

        GPIOC->MODER |= GPIO_MODER_MODE1_0 | GPIO_MODER_MODE2_0;
        GPIOC->BSRR = GPIO_BSRR_BS1 | GPIO_BSRR_BS2;

        SPI5->CR1 = SPI_CR1_BR_2 | SPI_CR1_BR_0 | SPI_CR1_MSTR |
                    SPI_CR1_SSM | SPI_CR1_SSI;
        SPI5->CR1 |= SPI_CR1_SPE;
}

uint8_t spi_sendrecv(uint8_t tx)
{
        while ((SPI5->SR & SPI_SR_TXE) == 0);
        SPI5->DR = tx;
        while ((SPI5->SR & SPI_SR_RXNE) == 0);

        return SPI5->DR;
}

void spi_start(int spi)
{
        if (spi == SPI_L3GD20)
                GPIOC->BSRR = GPIO_BSRR_BR1;
        else
                GPIOC->BSRR = GPIO_BSRR_BR2;
}

void spi_stop(int spi)
{
        while ((SPI5->SR & SPI_SR_TXE) == 0);
        while (SPI5->SR & SPI_SR_BSY);

        if (spi == SPI_L3GD20)
                GPIOC->BSRR = GPIO_BSRR_BS1;
        else
                GPIOC->BSRR = GPIO_BSRR_BS2;
}

Zmiany są kosmetyczne - teraz można wybrać układ z którym chcemy się komunikować.
Czas uruchomić wyświetlacz. Pamięć SDRAM posłuży za bufor obrazu - w kolejnym wpisie, na początek niech wyświetli się samo tło. Interfejs SPI służy do konfiguracji kontrolera obrazu, same dane są przesyłane przez kontroler LCD.
Taka transmisja wymaga dość sporo pinów, używam więc funkcji config_pin do ich ustawienia:

        config_pin(GPIOA, 3, 14);
        config_pin(GPIOA, 4, 14);
        config_pin(GPIOA, 6, 14);
        config_pin(GPIOA, 11, 14);
        config_pin(GPIOA, 12, 14);

        config_pin(GPIOB, 0, 9);
        config_pin(GPIOB, 1, 9);
        config_pin(GPIOB, 8, 14);
        config_pin(GPIOB, 9, 14);
        config_pin(GPIOB, 10, 14);
        config_pin(GPIOB, 11, 14);

        config_pin(GPIOC, 6, 14);
        config_pin(GPIOC, 7, 14);
        config_pin(GPIOC, 10, 14);

        config_pin(GPIOD, 3, 14);
        config_pin(GPIOD, 6, 14);

        config_pin(GPIOF, 10, 14);

        config_pin(GPIOG, 6, 14);
        config_pin(GPIOG, 7, 14);
        config_pin(GPIOG, 10, 9);
        config_pin(GPIOG, 11, 14);
        config_pin(GPIOG, 12, 9);

To które piny są używane i jakie funkcje należy im przypisać opisane zostało w dokumentacji. Wystarczy poszukać i przepisać.
Jak zwykle moduł wyświetlacza jest domyślnie wyłączony, trzeba więc go uruchomić:

        RCC->APB2ENR |= RCC_APB2ENR_LTDCEN;

Wyświetlacz używa własnego sygnału zegarowego z dedykowanego PLL. Konfiguracja wygląda następująco:

        RCC->PLLSAICFGR = (192 << 6) | (7 << 24) | (4 << 28);
        RCC->DCKCFGR |= 0x00020000;
        RCC->CR |= RCC_CR_PLLSAION;
        while ((RCC->CR & RCC_CR_PLLSAIRDY) == 0);


Następnie należy ustawić timingi wyświetlacza. Jest z tym trochę liczenia, ale końcowy kod jest prosty:

        LTDC->SSCR = (9 << 16) | 1;
        LTDC->BPCR = (29 << 16) | 3;
        LTDC->AWCR |= (269 << 16) | 323;
        LTDC->TWCR |= (279 << 16) | 327;


W pierwszym przykładzie nie wyświetlam danych, jedynie tło. Jego kolor ustawiany jest w rejestrze BCCR:

        LTDC->BCCR |= 0xf0ff00;

Ostatni krok to uruchomienie wyświetlacza:

        LTDC->GCR |= LTDC_GCR_LTDCEN;

Okazuje się, że uruchomienie wyświetlacza nie jest aż takie trudne jak się wydaje. Co prawda jest jeszcze jeden ważny element - konfiguracja kontrolera w samym wyświetlaczu. Jak wspomniałem używane jest do tego SPI, a sam kod czyli funkcje LCD_PowerOn przeniosłem z przykładów - wymaga konfiguracji ogromnej liczby rejestrów, ale na szczęscie wykonuje się go tylko raz. Wypadałoby wszystko sprawdzić w dokumentacji, ale tym razem byłem trochę leniwy.
Poniżej efekt działania programu:


Program główny w main.c:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "stm32f429xx.h"
#include "pll.h"
#include "sdram.h"
#include "uart.h"
#include "spi.h"
#include "lcd.h"

int _write(int fd, char *str, int len)
{
        int i;
        for (i = 0; i < len; i++)
                usart_putc(str[i]);
        return len;
}

int main(int argc, char *argv[])
{
        RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOCEN |
                        RCC_AHB1ENR_GPIODEN | RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_GPIOFEN |
                        RCC_AHB1ENR_GPIOGEN;

        pll_init();
        uart_init();
        sdram_init();
        spi_init();
        lcd_init();

        printf("Hello world!\r\n");

        while (1) {
                printf("Starting SPI communication...\r\n");

                spi_start(SPI_L3GD20);

                uint8_t r = spi_sendrecv(0x8f);
                printf("Received: %02x\r\n", r);

                r = spi_sendrecv(0x00);
                printf("Received: %02x\r\n", r);

                spi_stop(SPI_L3GD20);

                printf("done.\r\n");
                for (volatile uint32_t dly = 0; dly < 10000000; dly++);
        }

        return 0;
}

Oraz to co najważniejsze, czyli moduł lcd.c:

#include "lcd.h"
#include "spi.h"
#include "stm32f429xx.h"

#define LCD_SLEEP_OUT            0x11   /* Sleep out register */
#define LCD_GAMMA                0x26   /* Gamma register */
#define LCD_DISPLAY_OFF          0x28   /* Display off register */
#define LCD_DISPLAY_ON           0x29   /* Display on register */
#define LCD_COLUMN_ADDR          0x2A   /* Colomn address register */
#define LCD_PAGE_ADDR            0x2B   /* Page address register */
#define LCD_GRAM                 0x2C   /* GRAM register */
#define LCD_MAC                  0x36   /* Memory Access Control register*/
#define LCD_PIXEL_FORMAT         0x3A   /* Pixel Format register */
#define LCD_WDB                  0x51   /* Write Brightness Display register */
#define LCD_WCD                  0x53   /* Write Control Display register*/
#define LCD_RGB_INTERFACE        0xB0   /* RGB Interface Signal Control */
#define LCD_FRC                  0xB1   /* Frame Rate Control register */
#define LCD_BPC                  0xB5   /* Blanking Porch Control register*/
#define LCD_DFC                  0xB6   /* Display Function Control register*/
#define LCD_POWER1               0xC0   /* Power Control 1 register */
#define LCD_POWER2               0xC1   /* Power Control 2 register */
#define LCD_VCOM1                0xC5   /* VCOM Control 1 register */
#define LCD_VCOM2                0xC7   /* VCOM Control 2 register */
#define LCD_POWERA               0xCB   /* Power control A register */
#define LCD_POWERB               0xCF   /* Power control B register */
#define LCD_PGAMMA               0xE0   /* Positive Gamma Correction register*/
#define LCD_NGAMMA               0xE1   /* Negative Gamma Correction register*/
#define LCD_DTCA                 0xE8   /* Driver timing control A */
#define LCD_DTCB                 0xEA   /* Driver timing control B */
#define LCD_POWER_SEQ            0xED   /* Power on sequence register */
#define LCD_3GAMMA_EN            0xF2   /* 3 Gamma enable register */
#define LCD_INTERFACE            0xF6   /* Interface control register */
#define LCD_PRC                  0xF7   /* Pump ratio control register */

static void LCD_WriteCommand(uint8_t LCD_Reg)
{
        GPIOD->BSRR = GPIO_BSRR_BR13;

        spi_start(SPI_LCD);
        spi_sendrecv(LCD_Reg);
        spi_stop(SPI_LCD);
}

static void LCD_WriteData(uint8_t value)
{
        GPIOD->BSRR = GPIO_BSRR_BS13;

        spi_start(SPI_LCD);
        spi_sendrecv(value);
        spi_stop(SPI_LCD);
}

static void LCD_PowerOn(void)
{
        LCD_WriteCommand(0xCA);
        LCD_WriteData(0xC3);
        LCD_WriteData(0x08);
        LCD_WriteData(0x50);
        LCD_WriteCommand(LCD_POWERB);
        LCD_WriteData(0x00);
        LCD_WriteData(0xC1);
        LCD_WriteData(0x30);
        LCD_WriteCommand(LCD_POWER_SEQ);
        LCD_WriteData(0x64);
        LCD_WriteData(0x03);
        LCD_WriteData(0x12);
        LCD_WriteData(0x81);
        LCD_WriteCommand(LCD_DTCA);
        LCD_WriteData(0x85);
        LCD_WriteData(0x00);
        LCD_WriteData(0x78);
        LCD_WriteCommand(LCD_POWERA);
        LCD_WriteData(0x39);
        LCD_WriteData(0x2C);
        LCD_WriteData(0x00);
        LCD_WriteData(0x34);
        LCD_WriteData(0x02);
        LCD_WriteCommand(LCD_PRC);
        LCD_WriteData(0x20);
        LCD_WriteCommand(LCD_DTCB);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteCommand(LCD_FRC);
        LCD_WriteData(0x00);
        LCD_WriteData(0x1B);
        LCD_WriteCommand(LCD_DFC);
        LCD_WriteData(0x0A);
        LCD_WriteData(0xA2);
        LCD_WriteCommand(LCD_POWER1);
        LCD_WriteData(0x10);
        LCD_WriteCommand(LCD_POWER2);
        LCD_WriteData(0x10);
        LCD_WriteCommand(LCD_VCOM1);
        LCD_WriteData(0x45);
        LCD_WriteData(0x15);
        LCD_WriteCommand(LCD_VCOM2);
        LCD_WriteData(0x90);
        LCD_WriteCommand(LCD_MAC);
        LCD_WriteData(0xC8);
        LCD_WriteCommand(LCD_3GAMMA_EN);
        LCD_WriteData(0x00);
        LCD_WriteCommand(LCD_RGB_INTERFACE);
        LCD_WriteData(0xC2);
        LCD_WriteCommand(LCD_DFC);
        LCD_WriteData(0x0A);
        LCD_WriteData(0xA7);
        LCD_WriteData(0x27);
        LCD_WriteData(0x04);

        /* colomn address set */
        LCD_WriteCommand(LCD_COLUMN_ADDR);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteData(0xEF);
        /* Page Address Set */
        LCD_WriteCommand(LCD_PAGE_ADDR);
        LCD_WriteData(0x00);
        LCD_WriteData(0x00);
        LCD_WriteData(0x01);
        LCD_WriteData(0x3F);
        LCD_WriteCommand(LCD_INTERFACE);
        LCD_WriteData(0x01);
        LCD_WriteData(0x00);
        LCD_WriteData(0x06);

        LCD_WriteCommand(LCD_GRAM);

        LCD_WriteCommand(LCD_GAMMA);
        LCD_WriteData(0x01);

        LCD_WriteCommand(LCD_PGAMMA);
        LCD_WriteData(0x0F);
        LCD_WriteData(0x29);
        LCD_WriteData(0x24);
        LCD_WriteData(0x0C);
        LCD_WriteData(0x0E);
        LCD_WriteData(0x09);
        LCD_WriteData(0x4E);
        LCD_WriteData(0x78);
        LCD_WriteData(0x3C);
        LCD_WriteData(0x09);
        LCD_WriteData(0x13);
        LCD_WriteData(0x05);
        LCD_WriteData(0x17);
        LCD_WriteData(0x11);
        LCD_WriteData(0x00);
        LCD_WriteCommand(LCD_NGAMMA);
        LCD_WriteData(0x00);
        LCD_WriteData(0x16);
        LCD_WriteData(0x1B);
        LCD_WriteData(0x04);
        LCD_WriteData(0x11);
        LCD_WriteData(0x07);
        LCD_WriteData(0x31);
        LCD_WriteData(0x33);
        LCD_WriteData(0x42);
        LCD_WriteData(0x05);
        LCD_WriteData(0x0C);
        LCD_WriteData(0x0A);
        LCD_WriteData(0x28);
        LCD_WriteData(0x2F);
        LCD_WriteData(0x0F);

        LCD_WriteCommand(LCD_SLEEP_OUT);
        LCD_WriteCommand(LCD_DISPLAY_ON);
        /* GRAM start writing */
        LCD_WriteCommand(LCD_GRAM);
}

void lcd_init(void)
{
        GPIOD->MODER |= GPIO_MODER_MODE13_0;

        LCD_PowerOn();

        RCC->APB2ENR |= RCC_APB2ENR_LTDCEN;

        config_pin(GPIOA, 3, 14);
        config_pin(GPIOA, 4, 14);
        config_pin(GPIOA, 6, 14);
        config_pin(GPIOA, 11, 14);
        config_pin(GPIOA, 12, 14);

        config_pin(GPIOB, 0, 9);
        config_pin(GPIOB, 1, 9);
        config_pin(GPIOB, 8, 14);
        config_pin(GPIOB, 9, 14);
        config_pin(GPIOB, 10, 14);
        config_pin(GPIOB, 11, 14);

        config_pin(GPIOC, 6, 14);
        config_pin(GPIOC, 7, 14);
        config_pin(GPIOC, 10, 14);

        config_pin(GPIOD, 3, 14);
        config_pin(GPIOD, 6, 14);

        config_pin(GPIOF, 10, 14);

        config_pin(GPIOG, 6, 14);
        config_pin(GPIOG, 7, 14);
        config_pin(GPIOG, 10, 9);
        config_pin(GPIOG, 11, 14);
        config_pin(GPIOG, 12, 9);

        RCC->PLLSAICFGR = (192 << 6) | (7 << 24) | (4 << 28);
        RCC->DCKCFGR |= 0x00020000;
        RCC->CR |= RCC_CR_PLLSAION;
        while ((RCC->CR & RCC_CR_PLLSAIRDY) == 0);

        LTDC->SSCR = (9 << 16) | 1;
        LTDC->BPCR = (29 << 16) | 3;
        LTDC->AWCR |= (269 << 16) | 323;
        LTDC->TWCR |= (279 << 16) | 327;
        LTDC->BCCR |= 0xf0ff00;

        LTDC->GCR |= LTDC_GCR_LTDCEN;
}

static void config_pin(GPIO_TypeDef* port, uint8_t pin, uint8_t func)
{
        if (pin < 8) {
                port->AFR[0] |=  func << (pin * 4);
        } else {
                port->AFR[1] |=  func << ((pin - 8) * 4);
        }
        port->MODER |= 2 << (pin * 2);
        port->OSPEEDR |= 2 << (pin * 2);
}

W kolejnych wpisach dodam drobne modyfikacje do modułu lcd. Wbrew pozorom teraz będzie już tylko łatwiej (i ładniej).