Pracuję teraz nad jednym projektem na STM32F411, który będzie wykorzystywał stary monitor VGA do wyświetlania obrazu, a tak naprawdę jedynie tekstu. Z tego powodu nie potrzebuję pełnego framebuffera - mam zamiar trzymać w buforze jedynie informacje o znakach w poszczególnych lokalizacjach (wiersz/kolumna) i na ich podstawie w locie generować zawartość bufora z wartościami RGB do wyświetlenia w bieżącej linii.
Wartości napięć na liniach RGB będą generowane za pomocą drabinek rezystorowych - po dwa rezystory na kanał. W teorii daje mi to możliwośc użycia 64 kolorów, chociaż jak na razie zadowolę się zestawem kilku podstawowych barw.
Logikę wypełniania tego bufora "scanline" na podstawie zawartości bufora tekstowego i definicji czcionek jakoś sobie ogarnę. Teraz próbuję jednak zorganizować generowanie sygnałów i wysyłanie danych do portu sterującego rezystorami.
Interesuje mnie rozdzielczość 640x480 przy 60 Hz odświeżania. To daje nam pixel clock w okolicy 25 MHz - zdecydowanie za dużo, żeby ogarnąć software'owo, w przerwaniach. Z tego co widzę podobne projekty używają do tego DMA.
Teraz tylko chciałem dopytać:
W jaki sposób generować VSYNC/HSYNC?
W jaki sposób inicjować wysyłanie kolorów pikseli na port za pomocą DMA w taki sposób, żeby uwzględnić back porch (zaczynać w widocznej części linii)?
W jaki sposób skonfigurować DMA, żeby transfer każdej kolejnej zawartości z bufora scanline do portu był zsynchronizowany z pixel clock, ale bez używania przerwań?
Didn't find your answer? Ask the community — no account required.
M
Mirek
W dniu 14.03.2026 o 18:37, Atlantis pisze:
formatting link
W
Waldek Hebisch
Wersja monochromatyczna może używać SPI (jak w linku podanym przez Mirka), to daje ciąg bitów z jednakowymi odstępami między poszczególnymi bitami. No i jest tylko jeden transfer DMA na
8 pikseli.
W STM32 jest możliwość sterowania urządzeń przez liczniki. Jak chcesz mieć kolor to jeden licznik może generować żądania DMA, a drugi generować impulsy synchornizacji poziomej. Przy tym ten licznik od synchronizacji poziomej może też generować sygnał startu dla licznika pikseli.
Nie wiem czy Ci się uda: dokumentacja STM straszy że przełączanie dostępu do pamięci między procesorem a DMA zabiera ileś cykli co w domyśle oznacza że maksymalny użyteczny zegar dla DMA jest istotnie wolniejszy od zegara procesora. Maksymalny legalny zegar dla 411 to 100MHz, czyli transmitując bajty masz transfer co 4 takty. Możesz kombinować z FIFO w DMA, z dokumentacji nie jest jasne czy da się zrobić co potrzeba, ale teortycznie możesz zmniejszyć liczbę przesłań tranmitując 4 bajtowe słowa z pamięci a bajty do portu.
A
Atlantis
Prawdę mówiąc trochę się tego obawiałem. Przyjąłem założenie z użyciem GPIO, DMA i rezystorów, bo takie rozwiązanie podpatrzyłem w jakimś projekcie napisanym w Arduino pod ESP32. No cóż, ale ESP32 jednak może być taktowany szybciej (i być może nie posiada takich ograniczeń po stronie DMA). W teorii mógłbym spróbować zmniejszyć dwukrotnie pixel clock do 12,5 MHz, zostawiając te same parametry HSYNC i VSYNC i rysując jedną linię dwa razy. W ten sposób rzeczywista rozdzielczość spadłaby do 320x240. To jednak za mało, żeby uzyskać dobrze wyglądający tryb 80 kolumn. No cóż... Chyba będę musiał poeksperymentować z wersją monochromatyczną na SPI - kolory byłyby fajnym dodatkiem, ale priorytet stanowi dla mnie jednak wyraźny tekst w osiemdziesięciu kolumnach...
W ogóle eksperyment miał być tak naprawdę przymiarką do poważniejszego projektu z VGA na STM32. Widzę, że poważniejszych projektach tego rodzaju wykorzystuje się LTDC i pewnie właśnie tej metody spróbuję.
A
Atlantis
W sumie jeszcze raz przyjrzałem się i wygląda na to, że chyba mam szczęście. Jedna z linii sterujących zielonym kolorem może być wykorzystana jako wyjście MOSI SPI SPI1 oraz SPI3. Czyli jeśli usunę pozostałe rezystory, a ten na tej linii zamienię na 270 omów, będę mógł generować 0.7V impulsy zapalające zielone piksele, bez konieczności cięcia ścieżek i przerabiania płytki. Tekst będzie zielony, ale to nada mu charakterystycznego retro klimatu. ;)
W teorii mógłbym minimalnie przerobić projekt i podłączyć sygnał MOSI pod trzy rezystory, na każdym kanale RGB. Tyle tylko, że wtedy pobór prądu z pinu wyniesie prawie 30 mA - a to już oznacza jego przeciążenie. Trzeba by jednak dać jakiś bufor.
Dlatego właśnie zastanawia mnie rozwiązanie użyte projekcie spod linku przesłanego przez Mirka. Tam linie GREEN jest połączona bezpośrednio z wyjściem mikrokontrolera - nie dosyć, że monitor dostaje napięcie 3,3V (to jeszcze nie problem, bo większość nowoczesnych monitorów pewnie sobie z tym poradzi, interpretując to jako maksymalną jasność) to jeszcze jedynym ograniczeniem prądu jest wejściowa impedancja linii koloru (75 omów)...
A
Atlantis
Udało mi się osiągnąć częściowy sukces. Układ generuje już poprawne przebiegi HSYNC i VSYNC. Wyglądają ok na oscyloskopie, a mój monitor (stary CRT) wykrywa sygnał y wybudza się z trybu uśpienia. Natomiast za nic nie mogę wygenerować sygnału pikseli na kanale GREEN za pomocą SPI1. Skonfigurowałem SPI do pracy na 25 MHz i podpiąłem MOSI do właściwego pinu. Na razie próbuję po prostu w pętli głównej wysyłać cyklicznie 0xAA, ale na linii MISO cisza - znajduje się ona w stanie niskim. CLK również milczy. Co więcej, jeśli skonfiguruję tę linię do pracy w trybie wyjścia GPIO (PB5) to ni jestem w stanie zmieniać jej stanu w pętli głównej, ale już domyślna wartość ustawiona w CubeMX się zmienia (więc raczej można odrzucić hipotezę, że pin/port się upalił). Co więcej, spróbowałem wysłać na ten pin sygnał PWM z TIM3_CH2 i on jest widoczny. Ktoś spotkał się z takim problemem?
W
Waldek Hebisch
Nie używam CubeMX więc nie wiem co może zepsuć. "domyślna wartość ustawiona w CubeMX" to wygląda jak zmienna w GUI która dopiero pośrednio powinna wpływać na pin, ale samodzielnie nic nie znaczy. Ja bezpośrednio programuję porty, "typowy" problem to nie uaktywnienie zegara do jakiegoś potrzebnego urządzenia. Jak coś nie działa to przez debuger czytam rejestry urządzenia i sprawdzam czy mają oczekiwną wartość. STM często blokuje zmiany rejestrów czy specyficznych bitów jeśli urządzenie jest aktywne więc trzeba pilnować kolejności konfiguracji urządzeń.
Ja używałem SPI z niezbyt szybkim zegarem, najszybszy to było wysyłanie danych do wyświetlacza SPI. W zasadzie wszystko działało zgodnie z dokumentacją (uwzględniając erraty). Z innymi urządzeniami (i chyba z klonem a nie oryginalnym STM) miałem problem z stylu że pomiędzy dwoma zapisami do rejestu potrzebne było odczekanie przez 2 czy 3 takty, "optymalnie szybki" kod nie działał. W jednym chińskim klonie jeden kanał licznika zliczał OK, pin dało się wysterować przez GPIO, ale skierowanie wyjścia licznika do pinu nie działało.
A propo, do ustawienia SPI1 na PB3 i PB5 z podzielnikiem 64 oraz PB4 jako GPIO ma taki kod (który u mnie działa):
to używa kombinację moich prywatnych nagłówków mapujących rejestry i nagłówków libopencm3 które definiują maski bitowe (jak widać część stałych wyżej jest po prostu zaszyta). Mam nadzieję że konfiguracja jest w miarę oczywista. Ta konfiguracja daje następujący stan kluczowych rejestrów:
0x40023830 czyli AHB1ENR 0x00000087 (istone jest ustawienie bitu numer 1 uaktywniające GPIOB)
0x40023844 czyli APB2ENR 0x00005000 (istotne jest ustawienie bitu numer 12 uaktywniające SPI1)
0x40020400 czyli GPIOB_MODER 0x00000980
0x40020404 czyli GPIOB_OTYPER 0
0x40020408 czyli GPIOB_OSPEEDR 0x00000880
0x40020420 czyli GPIOB_AFRL 0x00505000
0x40013000 czyli SPI1_CR1 0x000003ec
0x40013004 czyli SPI1_CR2 0
Oczywiście poza błędną konfiguracją można mieć całą masę normalnych błędów. Z tą konfiguracją możesz "ręcznie" trasmitować: jak przerwiesz program to z debugera możesz wpisać wartość do rejestru danych SPI (pod adresem 0x4001300c), te dane powinny lecieć na PB5. Po takiej ręcznej transmisji odczyt z rejestru stanu SPI (pod adresem 0x40013008) powinien dać 3 tzn. TXE i RXNE ustawione. Normalnie jak program czyta odebrane dane (ja to robię żeby wiedzieć kiedy jest koniec transmisji na drucie) RXNE jest 0, niezerowa wartość pokazuje że SPI zadziałało.
A
Atlantis
Ok, okazało się, że problem miał banalną przyczynę. Przygotowując się do testów zakomentowałem większość kodu niezwiązanego z działaniem portu VGA. Niestety umknęło mi wywołanie jednej funkcji, która co prawda się wykonywała, ale bez inicjacji (która została zakomentowana) wpadała w nieskończoną pętlę. Operacje związane z uruchomieniem generowania HSYNC/VSYNC odbywały się wcześniej, ale transfer przez SPI czy operacje na porcie już nie miały okazji się wykonać.
W chwili obecnej "coś" już wyświetla mi się na ekranie, ale niestety nie jest to ten obraz, którego oczekuję.
Bufor scanline (80 bajtów uint8_t) wypełniam wartościami 0xF0. Zawartość bufora jest wysyłana z szybkością pixel clock (25 MHz) za pomocą DMA przez SPI, na kanał GREEN. Wysyłanie jest inicjowane dopiero wtedy, gdy OC na timerze HSYNC odmierzy koniec backporch. Do tego jeszcze sprawdzam wartość timera liczącego linie (w celu generowania VSYNC) i aktywuję transmisje SPI tylko wtedy, jeśli mieścimy się w widocznym zakresie.
Ne ekranie spodziewam się zobaczyć zestaw nieruchomych, pionowych linii. Widzę natomiast bardziej chaotyczny obraz, na którym w poszczególnych liniach faktycznie widać ten przerywany wzór, jednak linie są przesunięte względem siebie, tworząc coś przypominającego nieregularną kratę, która w dodatku lekko szumi/faluje.
Chyba mimo wszystko spróbuję dodać generator znaków i zobaczę w jaki sposób zniekształcona będzie oczekiwana zawartość bufora tekstowego.
A
Atlantis
Ok, na chwilę obecną udało mi się naprawić problemy związane z synchronizacją pionową i generowaniem obrazu tylko w widocznym okienku. Zniknął problem obrazu trzęsącego się w pionie. Przyczyna składała się z kilku czynników: po pierwsze musiałem zoptymalizować ustawienia priorytetów przerwań, po drugie w złym miejscu decydowałem czy linia znajduje się w widocznym oknie, przez co opóźnienia pojawiały się krytycznym punkcie. Wprowadzenie flagi ustawianej w mniej krytycznym obliczeniowo momencie rozwiązało ten problem.
Teraz już udało mi się wygenerować (względnie) stabilny obraz testowy, złożony z pionowych linii. Pozostał do rozwiązania ostatni problem, ale przynajmniej znam jego przyczynę.
W tej chwili (testowo) odpalam transmisje DMA/SPI za pomocą HAL_SPI_Transmit_DMA(). Te funkcja jest dość ciężka i zawiera sporo logiki. Efekt jest taki, że jeśli wołam ją na końcu HSYNC back porch, to właściwa transmisja startuje dopiero w chwilę później - na tyle późno, że linia zaczyna się dopiero w okolic 1/4 szerokości ekranu.
Właściwy schemat powinien wyglądać raczej tak:
- Po wysłaniu linii (HSYNC front porch) ustawiamy wszystkie parametry następnej transmisji DMA, ale jej nie startujemy.
- Właściwą transmisję odpalamy w HSYNC back porch.
Join the Discussion
Have something to add? Share your thoughts — no account required.
Didn't find your answer?
Ask the community — no account required
Report Content
You are reporting this content to the moderators. They will look at it
ASAP.