Popularny post Elvis Napisano Styczeń 28, 2020 Popularny post Udostępnij Napisano Styczeń 28, 2020 W poprzednich częściach zapoznaliśmy się z demonstracyjnym kodem dostarczanym przez producenta wyświetlacza. Wiemy jakie zalety i wady ma to rozwiązanie, nadszedł moment, żeby spróbować napisać własną wersję. Spis treści: Sterowanie wyświetlaczem TFT - część 1 - wstęp, podstawowe informacje Sterowanie wyświetlaczem TFT - część 2 - analiza problemu Sterowanie wyświetlaczem TFT - część 3 - testy prędkości na STM32 Sterowanie wyświetlaczem TFT - część 4 - własny program Sterowanie wyświetlaczem TFT - część 5 - optymalizacja programu Własna biblioteka graficzna Specjalnie wybrałem model mikrokontrolera, w którego pamięci zmieści się cały bufor ekranu. Jak pamiętamy konieczne jest 160 x 128 x 2 = 40960 bajtów pamięci, co nie jest jednak problemem dla układu STM32L476. Kolor każdego piksela jest przechowywany jako 16-bitowa wartość, możemy więc utworzyć bufor pisząc po prostu: uint16_t lcd_framebuffer[LCD_WIDTH * LCD_HEIGHT]; Wybrałem poziomą orientację ekranu, więc LCD_WIDTH ma wartość 160, a LCD_HEIGHT 128. Przykładowa procedura rysowania punktu może wyglądać następująco: void lcd_set_pixel(uint32_t x, uint32_t y, uint16_t color) { if (x < LCD_WIDTH && y < LCD_HEIGHT) lcd_framebuffer[x + y * LCD_WIDTH] = __REV16(color); } Jak widzimy jest to po prostu zapisanie do tablicy przechowywanej w pamięci RAM, działa więc błyskawicznie. Wywołanie __REV16 było mi potrzebne, aby zamienić kolejność bajtów - można byłoby odpowiednio przeliczyć kody kolorów, ale zamiana bajtów to raptem jedna instrukcja asemblera (oczywiście jak ktoś będzie chciał optymalizować kod może, a nawet powinien tego typu błędy wyeliminować). Ten wpis brał udział konkursie na najlepszy artykuł o elektronice lub programowaniu. Sprawdź wyniki oraz listę wszystkich prac » Partnerem tej edycji konkursu (marzec 2020) był popularny producent obwodów drukowanych, firma PCBWay. Kasowanie ekranu również odbywa się w pamięci, więc kod również jest prosty: void lcd_clear(uint16_t color) { uint16_t *p = lcd_framebuffer; uint16_t *end = lcd_framebuffer + LCD_WIDTH * LCD_HEIGHT; color = __REV16(color); while (p != end) *p++ = color; } Obecnie całe rysowanie odbywa się w lokalnej pamięci, dopiero gotowy obraz jest kopiowany na ekran. Przy pierwszym podejściu użyję HAL_SPI_Transmit, ale tym razem do przesłania wszystkich danych na raz (zamiast po jednym bajcie jak poprzednio): void lcd_copy(void) { lcd_cmd(ST7735S_RAMWR); HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi2, (uint8_t*)lcd_framebuffer, 2 * LCD_WIDTH * LCD_HEIGHT, 500); HAL_GPIO_WritePin(LCD_CS_GPIO_Port, LCD_CS_Pin, GPIO_PIN_SET); } Przed wysłaniem danych wykonywana jest komenda wyświetlacza RAMWR. Jej wykonanie powoduje zapis do zdefiniowanego okna zaczynając od pozycji (0, 0). Natomiast samo okno jest ustawiane raz podczas inicjalizacji sterownika - ma ono rozmiar całego wyświetlacza, czyli 160x128 pikseli. Zmieniłem trochę wyświetlany obraz bo w mojej wersji używam tylko poziomej orientacji wyświetlacza. Program działa następująco: Nie bardzo to widać, ale obraz jest rysowany w pętli - tym razem jednak nie widać kasowania, ani rysowania obrazu: Rysowanie w pamięci RAM zajmuje ok 6ms. Procedury nie są jakoś szczególnie zoptymalizowane, do rysowania linii i okręgów używany jest algorytm Bresenhama, czcionki są identyczne jak w kodzie WaveShare. Kopiowanie danych z bufora do wyświetlacza trwa 33ms, czyli tyle ile powinno. Biblioteka HAL jak widzieliśmy poprzednio nie jest demonem szybkości, gdy wysyłamy za jej pomocą pojedyncze bajty, jednak transmitując duży bufor, jej użycie jest już całkiem sensowne (a na pewno łatwe). Użycie DMA Kopiowanie za pomocą funkcji HAL_SPI_Transmit ma pewną wadę, przez 33ms mikrokontroler jest zajęty tylko kopiowaniem. Do tego celu można jednak wykorzystać mechanizm DMA, dzięki czemu procesor będzie mógł wykonywać inne zadania, a samo kopiowanie będzie odbywało się w pełni sprzętowo (a więc i z maksymalną szybkością). Użycie DMA w środowisku STM32CubeIDE jest dziecinnie proste - prawie wszystko robimy za pomocą graficznych kreatorów. Napisałem prawie, bo jednak trochę programowania jeszcze nam zostanie. Poza tym kod wygenerowany przez CubeMX nie zawsze działa... W przypadku L476 w wersji CubeIDE 1.1.0 kolejność inicjalizacji SPI oraz DMA jest niepoprawna, więc domyślnie tworzony kod po prostu nie działa. Niestety nie udało mi się zmusić CubeMX do generowania kodu włączającego DMA zanim zacznie konfigurację SPI. Obejściem było wyłączenie wywoływania inicjalizacji DMA zupełnie i ręczne dodanie odpowiedniego kodu. Może w kolejnej wersji narzędzia ten błąd zniknie, ale zobaczymy. Warto też pamiętać, że domyślne priorytety przerwań mogą zupełnie zawiesić kod biblioteki HAL. W każdym razie zmiany w kodzie programu związane z użyciem DMA są właściwie kosmetyczne. Pierwsza to nieco inne sterowanie pinem CS - poprzednio ustawialiśmy go w stan wysoki zaraz po powrocie z wywołania HAL_SPI_Transmit. Teraz musimy dodać obsługę przerwania, które będzie wywołane po zakończeniu transmisji. Dopisujemy więc: void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { lcd_copy_done(); } A w funkcji lcd_copy_done sterujemy odpowiednio pinem CS. Druga zmiana wynika z działania DMA w tle - nie chcemy zmieniać zawartości bufora ekranu, gdy DMA przesyła dane. Potrzebujemy więc flagi, która będzie informowała, że trwa transmisja (moglibyśmy oczywiście użyć podwójnego buforowania, ale 40KB to już i tak ogromny bufor jak na mikrokontroler). Oczywiście zamiast HAL_SPI_Transmit wywołujemy teraz HAL_SPI_Transmit_DMA. Sam program działa jak poprzednio, zyskaliśmy jednak 32ms na wykonywanie czegoś ciekawszego przez procesor. Program demonstracyjny Wiemy już jak sterować wyświetlaczem i uzyskać całkiem sensowne czasy działania. Odbyło się to za cenę dużego użycia pamięci, ale mamy możliwość rysowania prawie 25 klatek na sekundę. Jako przykład użycia kodu, wykorzystałem demo rysujące animowany tunel, które jest dokładnie opisane pod tym adresem. Pierwszy program oblicza kolory tekstury oraz bufory dla współrzędnych - wszystko jest opisane na blogu do którego podałem link, nie będę się więc rozpisywał o zasadzie działania programu. Użycie pamięci wzrosło do prawie 90KB Ale efekt jest chyba dość ładny: Program jest zaskakująco prosty (w pętli głównej wywoływana jest tylko funkcja draw_tunnel uint16_t texture[texHeight][texWidth]; uint8_t distanceTable[LCD_HEIGHT][LCD_WIDTH]; uint8_t angleTable[LCD_HEIGHT][LCD_WIDTH]; static void precalc(void) { for (int y = 0; y < texHeight; y++) { for (int x = 0; x < texWidth; x++) texture[y][x] = (x * BLUE / texWidth) ^ (y * BLUE / texHeight); } for(int y = 0; y < LCD_HEIGHT; y++) for(int x = 0; x < LCD_WIDTH; x++) { float ratio = 16.0; int dx = x - LCD_WIDTH / 2; int dy = y - LCD_HEIGHT / 2; float d = sqrt(dx * dx + dy * dy); int distance = (int)(ratio * texHeight / d) % texHeight; int angle = texWidth / 2 + (int)(texWidth / 2 * atan2(dy, dx) / M_PI); distanceTable[y][x] = distance; angleTable[y][x] = angle; } } static void draw_tunnel(void) { static uint32_t animation = 0; animation++; int shiftX = (int)(texWidth * 0.05 * animation); int shiftY = (int)(texHeight * 0.025 * animation); lcd_wait_ready(); for(int y = 0; y < LCD_HEIGHT; y++) for(int x = 0; x < LCD_WIDTH; x++) { uint16_t color = texture[(unsigned int)(distanceTable[y][x] + shiftX) % texWidth][(unsigned int)(angleTable[y][x] + shiftY) % texHeight]; lcd_set_pixel(x, y, color); } lcd_copy(); } Podobnie jak w opisywanym blogu, użyłem również użyć gotowej tekstury (zapisanej w pamięci Flash). Takie rozwiązanie jest nawet korzystniejsze jak chodzi o użycie pamięci RAM: Efekt działania programu: Możliwe jest również zaoszczędzenie pamięci przez wykorzystanie symetrii tablic wykorzystywanych podczas rysowania "tunelu". Takie program jest nieco bardziej skomplikowany, ale pozwala na uzyskanie efektu "rozglądania się" kamery: Podsumowanie Jak widzimy współczesne mikrokontrolery mają ogromną moc obliczeniową, odpowiednio optymalizując kod mogą całkiem sprawnie poradzić sobie ze sterowaniem niewielkiego wyświetlacza TFT. Odbywa się to za cenę użycia pamięci RAM, ale odpowiednio optymalizując kod można to zapotrzebowanie nieco zmniejszyć. W kolejnej części opiszę jak użyć trybu z mniejszą liczbą kolorów, dzięki czemu zapotrzebowanie na pamięć bardzo spadnie. Spis treści: Sterowanie wyświetlaczem TFT - część 1 - wstęp, podstawowe informacje Sterowanie wyświetlaczem TFT - część 2 - analiza problemu Sterowanie wyświetlaczem TFT - część 3 - testy prędkości na STM32 Sterowanie wyświetlaczem TFT - część 4 - własny program Sterowanie wyświetlaczem TFT - część 5 - optymalizacja programu 6
Treker (Damian Szymański) Styczeń 29, 2020 Udostępnij Styczeń 29, 2020 @Elvis bardzo fajna seria, różnica w efektach kolosalna - ostatni filmik robi świetne wrażenie 1
FilipK Luty 12, 2020 Udostępnij Luty 12, 2020 @Treker Zgadzam się! W porównaniu do takiego dzieła to mnie nie powinno tu być. -_-
Gość Luty 16, 2020 Udostępnij Luty 16, 2020 (edytowany) Dnia 29.01.2020 o 05:45, Treker napisał: ostatni filmik robi świetne wrażenie jak w gnieździe obcego ;D Nie mniej jednak nadal brakuje optymalnych i uniwersalnych bibliotek graficznych z którymi nie ma problemów. Bawiłem się GFXem ale jest on nieco uciążliwy a dodatkową uciążliwością są wprowadzane przez producenta zmiany bo trzeba się do nich adaptować od nowa. Może po ostatniej aktualizacji będzie lepiej. @Elvis jeśli masz doświadczenie z GFXem to 6 część mogłaby właśnie go dotyczyć Niezły art, pozdrawiam. Edytowano Luty 16, 2020 przez Gość literówki
Pomocna odpowiedź
Bądź aktywny - zaloguj się lub utwórz konto!
Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony
Utwórz konto w ~20 sekund!
Zarejestruj nowe konto, to proste!
Zarejestruj się »Zaloguj się
Posiadasz własne konto? Użyj go!
Zaloguj się »