W poprzednim artykule wspomniałem o DMA2D z funkcją mieszania obrazów (ang. blending). To jest podstawa do nałożenia nakładki z informacjami o parametrach lotu na rzeczywisty obraz. Nie jest mi to obecnie niezbędne, ale pokusa narysowania była zbyt silna, abym wytrwał w ascezie tworzenia jedynie potrzebnego na tym etapie kodu. Na obrazkach poniżej jest szkic ekranu OSD w rozdzielczości 480×320 naniesionego na szary obraz (30% bieli, 70% czerni) oraz na obraz z kamery.

Jest to szkic nie tylko dlatego, że parametry w górnej części ekranu są diagnostyczne zamiast rzeczywistych, ale przede wszystkim dlatego, że rysunek jest bardzo uproszczony. Zarówno horyzont, jak i skale prędkości i wysokości nie zawierają jeszcze podziałek. Najważniejsze jest jednak to, że zarówno czcionka, jak i linie rysowane są to pojedynczą linią bez obwiedni. Obiekty ekranowe porządnego OSD powinny mieć obwiednię, np. czarną a najlepiej czarną i białą, aby były dobrze widoczne na ekranie o zmieniającej się jasności. Powinny być widoczne zarówno w locie pod słońce, jak i w nocy. Narysowanie linii z obwiedniami jest rzeczą do zrobienia w kilka poranków i wieczorów (wtedy zwykle pracuję nad projektem). Natomiast zrobienie 4-kolorowej czcionki zawierającej czerń, biel, przezroczystość i kolor użytkownika to jest już grubsza sprawa, do zrobienia na późniejszym etapie.
Podwyższona widoczność
Na powyższym obrazku z kamery horyzont rysowany dosyć ciemną czerwoną linią jest w miarę widoczny na tle jasnej lodówki, ale na koszyku praktycznie go nie widać. Koniecznie potrzebna jest obwiednia widoczna na scenach o różnej jasności. A co, gdyby zamiast wspomnianej wyżej obwiedni w kolorze czarnym i białym użyć tego samego koloru tylko w innych intensywnościach? Pierwsze podejście do tego zagadnienia widać na przykładzie pomiaru parametrów baterii. Widoczne na ekranie napięcie baterii "Ub" jest w podstawowym kolorze czerwonym. Kolejny prąd "Ib" ma podniesioną jasność o 25% a następna energia "Ep" ('p' jak pozostająca, może być jeszcze indeks 'z', jak zużyta), ma kolor + 50% jasności. Kolory są dosyć blade, ponieważ wszystkie mają 50% przezroczystości po to, aby nie zakrywały obrazu pod nimi. Wartości kontroler nie są właściwe, ponieważ nie mierzę jeszcze parametrów pakietu i jako protezę wstawiłem tam temperaturę czujników IMU wyrażoną w K. Tak uzyskane kolory nadają się na jasną obwiednię. Żeby uzyskać ciemną, zdefiniowałem kolory o obniżonej jasności o 25%, 50% i 75%.
Format koloru ARB4444 bardzo ułatwia zarządzanie kolorami, ponieważ każdy znak odpowiada za jedną ze składowych RGB. Pierwotnie przyjąłem, że kolor z indeksem 0 to kolor podstawowy, a kolejne 3 (lub więcej, jeżeli trzeba) to indeksy o wyższych jasnościach. Do kolorów ciemniejszych, niż podstawowy przydałaby się skala ujemna. Żeby mocno nie komplikować z indeksami, przyjąłem format liczb ze znakiem jako dopełnienie do 2 dla liczb z zakresu 0..5, więc indeks -1 odpowiada liczbie 9, -2 to 8 itp.
Dla 3 stopni jasności (a maksymalnie może być 5 dla takiego sposobie indeksowania) definicje kolorów na przykładzie koloru czerwonego wygląda, tak jak w kodzie poniżej.
#define KOLOSD_CZER0 0x0F00 //pełen kolor
#define KOLOSD_CZER1 0x0F33 //pełen kolor + 1/4 jasności
#define KOLOSD_CZER2 0x0F77 //pełen kolor + 1/2 jasności
#define KOLOSD_CZER3 0x0FCC //pełen kolor + 3/4 jasności
#define KOLOSD_CZER9 0x0C00 //3/4 koloru
#define KOLOSD_CZER8 0x0700 //1/2 koloru
#define KOLOSD_CZER7 0x0300 //1/4 koloru
W formacie ARGB4444 składowa czerwona jest reprezentowana przez drugą cyfrę szesnastkową. W podstawowym kolorze ma wartość maksymalną, czyli 'F'. W wariantach o zwiększającej się jasności pozostałe składowe G i B równomiernie rosną, podnosząc jasność. W wariancie o zmniejszającej się jasności, wartość podstawowego koloru się zmniejsza.
Dla kolorów złożonych C, M, Y jest analogicznie, tylko tam kolor jest określony przez 2 składowe. Przykładowo dla koloru cyjan są to składowe niebieska i zielona. Wygląda to tak:
#define KOLOSD_CYJAN0 0x00FF //pełen kolor
#define KOLOSD_CYJAN1 0x03FF //pełen kolor + 1/4 jasności
#define KOLOSD_CYJAN2 0x07FF //pełen kolor + 1/2 jasności
#define KOLOSD_CYJAN3 0x0CFF //pełen kolor + 3/4 jasności
#define KOLOSD_CYJAN9 0x00CC //3/4 koloru
#define KOLOSD_CYJAN8 0x0077 //1/2 koloru
#define KOLOSD_CYJAN7 0x0033 //1/4 koloru
Mając kolory można spróbować narysować obiekty złożone z linii, czyli horyzont oraz skale prędkości i wysokości. Przyjąłem założenie że "słoneczko" świeci od strony PGR, więc linie od góry i z prawej strony są jasne a od dołu i z lewej ciemne. Wygląda to jak na obrazkach poniżej.

Czytelność jest wyraźnie większa. Na ciemnym obszarze widać głównie jasną składową, a na na jasnym widać ciemną. Poza tym linia jest grubsza i przez to dodatkowo wyraźniejsza.
Wydajność
Niestety nie ma nic za darmo i rysowanie 3 linii zamiast jednej trwa dłużej. Trzeba jeszcze wygumkować poprzedni obraz, zanim narysuje się nowy, a to wszystko zajmuje czas i moc obliczeniową procesora. Żeby mieć nad tym kontrolę, wstawiłem w górnej części ekranu kilka kontrolek przedstawiających czasy wykonywania wybranych operacji. Z lewej: "Twyp" to czas wypełniania ekranu tłem, "Trys" to czas rysowania wszystkich obiektów wewnątrz funkcji rysującej OSD, "Tble" to czas blendowania, czyli łączenia obrazu z kamery z obrazem OSD, "Tlcd" to czas wyświetlania na LCD po SPI. Po prawej "Thor" to czas rysowania horyzontu, "Tpiw" czas rysowania prędkości i wysokości, "Tdat" czas rysowania daty, która jest odrobinę bardziej skomplikowana niż czas przez to, że zawiera słowną nazwę miesiąca i "Tgnss" to czas rysowania jednej ze współrzędnych geograficznych.
Pierwsze pomiary trochę mnie zaskoczyły. Rysując najbardziej napracowałem się nad belkami sztucznego horyzontu, a pomiary pokazują, że jest to najszybciej rysująca się kontrolka. Z kolei banalne wyczyszczenie całego ekranu zajmowało najwięcej czasu, dlatego szybko z tego zrezygnowałem. Dla największej rozdzielczości (będącą rozdzielczością wyświetlacza) było to ok. 30 ms, czyli dwukrotnie więcej niż rysowanie wszystkiego innego.
Ponieważ długości napisów są w miarę stałe, więc nowy napis będzie zamazywał stary, trzeba tylko wygumkować sztuczny horyzont. On działał najszybciej, więc aplikuję go 2 razy. Pierwszy raz rysuję poprzedni stan horyzontu kolorem o 100% przezroczystości, czyszcząc go, a następnie właściwymi kolorami dla bieżących kątów pochylenia i przechylenia. Finalnie zapamiętuję oba kąty do następnego rysownia.
W obecnej wersji na wszelki wypadek zostawiłem czyszczenie ekranu po 1 linii. Gdyby na obrazie pojawiły się jakieś śmieci, to rysowana w kolejnych wierszach linia powoli zamazuje neutralnym kolorem ewentualne pozostałości. Tak więc nawet uszkodzona zawartość ekranu, naprawi się po upływie kilkunastu sekund.
Aby móc sprawdzić jak wielkość obrazu wpływa na czas rysowania OSD, zdefiniowałem 3 rozmiary ekranu i na nich rysuję ten sam układ graficzny. Oprócz prezentowanego wyżej układu o rozdzielczości 480×320, poniżej są pokazane 320×240 oraz 240×160 pikseli. Dla małych obrazków jest zdefiniowane zbyt dużo obiektów, które nachodzą na siebie, ale istotą eksperymentu było porównanie wydajności, więc w tym celu nawet nieczytelny obraz jest akceptowalny.

Zaokrąglone czasy rysowania wybranych elementów dla układu o podwyższonej widoczności, podane w mikrosekundach przedstawiam w tabeli:
| Operacja \ Rozdzielczość obrazu | 480×320 | 320×240 | 240×160 |
| Thor - rysowanie horyzontu (dwukrotne) | 3000 | 2000 | 1500 |
| Tpiw - rysowanie prędkości i wysokości | 1900 | 1750 | 1300 |
| Tdat - rysowanie daty | 1080 | 1080 | 1080 |
| Tgnss - rysowanie GNSS | 1200 | 1200 | 1200 |
| Twyp - czyszczenie 1 wiersza | 370 | 250 | 180 |
| Trys - suma rysowania wewnątrz funkcji | 22400 | 20900 | 19800 |
| Tble - czas blendowania obrazów | 26900 | 25300 | 24600 |
| Tlcd - czas transferu na LCD | 119800 | 55900 | 30000 |
Widać, że rysowanie napisu, którego rozmiar się nie zmienia zajmuje tyle samo czasu. Pokazanie horyzontu wymaga narysowania trochę dłuższych linii, natomiast czas transferu na ekran zmienia się proporcjonalnie do rozmiaru obrazu. Ciekawostką jest w miarę stały czas blendowania. Wygląda na to, że blendowanie jest szybkie, tylko jest sztucznie ograniczane przepustowością danych z kamery, bo długość ramki kamery jest stała bez względu na rozdzielczość, gdyż ma stałe pole widzenia. Szerokość ramki powinna się zmniejszać wraz z rozmiarem obrazu, ale to są szczegóły do dopracowania, jeszcze mało istotne na tym etapie.
Optymalizacja
Pewne rzeczy można zrobić na wiele sposobów. Przy okazji pomiarów wydajności pojawiła się potrzeba sprawdzenia, co będzie działało szybciej. Jakiś czas temu wypracowałem sobie metodę pomiaru polegającą na przechwytywaniu stanu licznika timera taktowanego 1 MHz. Jeżeli przed i za mierzonym fragmentem kodu przechwycę stany timera, to ich różnica jest czasem wykonywania tego kodu. Do pomiaru procesów trwających długo można użyć wolniejszego licznika np. SysTick-a, taktowanego zegarem 1 kHz.
Przy okazji zastanawiania się, czy efektywniej jest użyć funkcji printf("%f", float) do wyświetlenia wartości zmiennoprzecinkowej, czy tej samej funkcji z parametrem: "%d.%d" dla części całkowitej i ułamkowej tej liczby konwertowanej na liczby całkowite, znalazłem jeszcze jedną metodę pomiarową polegającą na pomiarze cykli maszynowych procesora. Tą metodą można mierzyć nawet bardzo krótkie procesy. Dodatkowo metoda pozwala mierzyć czas przerwań pojawiających się w trakcie pomiaru, mierzy czas pozostawania w stanie uśpienia i jeszcze kilka innych.
Napisałem taką funkcję inicjującą pomiar:
////////////////////////////////////////////////////////////////////////////////
// Rozpoczyna pomiar cykli maszynowych.
// Parametry: brak
// Zwraca: nic
////////////////////////////////////////////////////////////////////////////////
void StartPomiaruCykli(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // włącz DWT
DWT->CYCCNT = 0; //Processor clock cycle counter
DWT->CPICNT = 0; //Counts additional cycles required to execute multi-cycle instructions, except those recorded by DWT_LSUCNT, and counts any instruction fetch stalls.
DWT->EXCCNT = 0; //Counts the number of cycles spent in exception processing.
DWT->SLEEPCNT = 0; //Counts the number of cycles spent in sleep mode (WFI, WFE, sleep-on-exit).
DWT->LSUCNT = 0; //Counts additional cycles required to execute load and store instructions
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
Oraz funkcję drukującą wyniki na końcu pomiaru. Tutaj znając prędkość taktowania procesora, można od razu przeliczyć cykle maszynowe na czas.
////////////////////////////////////////////////////////////////////////////////
// Pisze na konsoli wynik pomiaru liczby cykli maszynowych.
// Parametry: brak
// Zwraca: nic
////////////////////////////////////////////////////////////////////////////////
void WyswietlCykle(void)
{
printf("Cykle: %ld (%ld us) multi: %d wyjatki: %d dodatk: %d sleep: %d\r\n", DWT->CYCCNT, DWT->CYCCNT / (HAL_RCC_GetSysClockFreq()/1000000), (uint8_t)DWT->CPICNT, (uint8_t)DWT->EXCCNT, (uint8_t)DWT->LSUCNT, (uint8_t)DWT->SLEEPCNT);
}
Finalny wynik obserwowany w konsoli wygląda tak:
Cykle: 7420065 (17176 us) multi: 0 wyjatki: 0 dodatk: 0 sleep: 0
Cykle: 7433435 (17207 us) multi: 0 wyjatki: 0 dodatk: 0 sleep: 0
Cykle: 7378579 (17080 us) multi: 0 wyjatki: 0 dodatk: 0 sleep: 0
Cykle: 7383963 (17092 us) multi: 0 wyjatki: 0 dodatk: 0 sleep: 0
Wyniki mają dosyć dużą zmienność, prawdopodobnie ze względu na pojawiające się w trakcie pomiaru przerwania, ale samo narzędzie wydaje się przydatne i nie wymaga dedykowanego licznika.
Układ ekranu
Rysując OSD, wykrystalizowała się koncepcja dynamicznego układu ekranu, czyli takiego, którego rozmiar można zmieniać w trakcie pracy. Na obecnym etapie jeszcze nie wiem, jaką przepustowość będzie miało wizyjne łącze radiowe. Jego przepustowość mogę regulować albo stopniem kompresji, albo wielkością obrazu. Wydaje mi się, że lepiej będzie mieć czysty obraz wielkości znaczka pocztowego, niż "mydło" wielkości mydelniczki. Dlatego też OSD powinno poprawnie wyświetlać swoje kontrolki dla każdej wielkości obrazu.
Podzieliłem ekran na pola, których współrzędnym (x, y) przypisałem wartości stałe. Dla współrzędnej X są to wartości: LEWA1, LEWA2, LEWA3, LEWA4, SRODEK, PRAWA4, PRAWA3, PRAWA2 i PRAWA1, rozumiane jako "pierwsze od lewej strony", "drugie od prawej" itd. Analogicznie dla współrzędnej Y są to GORA1..4, SRODEK i DOL4..1. Na powyższych obrazkach długość geograficzna ma współrzędne (LEWA1, DOL2), szerokość ma (LEWA1, DOL1), data (PRAWA2, DOL1), czas (PRAWA1, DOL1). Sztuczny horyzont zawsze ma współrzędne (SRODEK, SRODEK). Oprócz predefiniowanych stałych współrzędne mogą zawierać dowolne współrzędne ekranowe podane przez użytkownika np. x = 20, y = 100. Pobierając dane do konkretnego obiektu ekranowego, najpierw sprawdzam, czy 16-bitowa współrzędna ma wartość jednej ze stałych. W takim przypadku przeliczam to na współrzędne ekranowe, a jeżeli nie to przyjmuję, że podana liczba jest współrzędną użytkownika. Stałe mają wartości, począwszy od 0xFFFF w dół, więc nie ma ryzyka, że współrzędna nawet bardzo dużego ekranu będzie interpretowana jako stała.
Na razie stałe są bazowane do brzegów obrazu i zmieniają swoje współrzędne wraz ze zmianą jego wielkości. Życie może przynieść potrzebę dodania większej liczby stałych, gdy np. kontrolki będą mocno różniły się szerokością, ale ogólna koncepcja wydaje mi się dobra.