Powrót

Kurs Arduino – Fonty i glcdfont.c

W dwóch poprzednich odcinkach (UR022 i UR023 dość dokładnie zapoznaliśmy się z wyświetlaczami graficznymi OLED i nauczyliśmy się wyświetlać na nich obrazy – bitmapy. Otwiera to drogę do zrozumienia niełatwego problemu czcionek – fontów.Już wcześniej pojawiła się informacja, że w Arduino na wyświetlaczu graficznym litery, cyfry i inne symbole to w rzeczywistości wyświetlane na ekranie małe bitmapy. Generalnie tak właśnie jest, ale jak to mówią, diabeł tkwi w szczegółach.

Fonty

Wyświetlanie tekstu można zrealizować na najróżniejsze sposoby. My chcemy trzymać się biblioteki Adafruit  GFX, która we współpracy z „bibliotekami sprzętowymi” dla różnych sterowników, potrafi obsłużyć rozmaite wyświetlacze graficzne, w tym także kolorowe. Jeżeli zajrzymy do tej biblioteki (rysunek 1), to po pierwsze zauważymy katalog /Fonts, zawierający liczne fonty  (czcionki). Do tego jest tam katalog /fontconvert, który daje możliwość tworzenia własnych fontów, a do tego dwa pliki: glcdfont.c oraz gfxfont.h.

Rysunek 1

Kluczowa informacja: biblioteka Adafruit GFX oferuje dwie możliwości. Po pierwsze wykorzystanie jednego podstawowego fontu zawartego w pliku glcdfont.c. Po drugie możliwość wykorzystania rozmaitych fontów, których część jest zawarta w katalogu /Fonts, a mnóstwo innych można stworzyć samodzielnie lub ściągnąć z sieci.

W naszym przypadku dochodzi jeszcze problem polskichznaków diakrytycznych, ponieważ w oryginalnych bibliotekach polskich liter nie ma. Aby je w jakiś sposób dodać, trzeba zrozumieć, czym jest font i jak działa wyświetlanie znaków. Problem w tym, że zupełnie inaczej działa font podstawowy z pliku glcdfont.c, a zupełnie inaczej fonty z katalogu /Fonts.
Zaczniemy od wersji prostszej.

glcdfont.c

Arduino, oparte na języku C++, potrafi wykorzystywać tak zwane klasy, co znakomicie ułatwia pisanie programów. Jedną z bardzo przydatnych właściwości jest to, że niezależnie od sprzętu możemy wykorzystywać pożyteczną metodę .print(), która najogólniej biorąc, służy do pisania tekstu. Od dawna często posługujemy się tą metodą przy korzystaniu z wyświetlacza znakowego LCD oraz do wyświetlania tekstu na konsoli monitora za pomocą łącza Serial, a ostatnio w szkicu A2101.ino, który był zmodyfikowanym plikiem przykładowym, pokazującym możliwości biblioteki Adafruit GFX. Wróćmy jeszcze raz do tego szkicu, gdzie znajdziemy podstawowe polecenia związane z wyświetlaniem tekstu na wyświetlaczu graficznym: Uwa\GA KOLORY SĄ!!!!

wysw.setCursor(0, -3);

wysw.setTextColor(1, 0);

wysw.setTextSize(1);

wysw.setTextWrap(1);

wysw.print("wyswietlany napis"); wysw.display();

Interesują nas linie czerwone. Metoda

.setTextSize(1)

pozwoli ustawić wielkość tekstu: liczba 1 to wielkość podstawowa 5 × 7 pikseli, 2 to podwojenie rozmiaru, 3 – potrojenie, itd. Do właściwego wyświetlenia wykorzystujemy pożyteczną metodę

.print().

W podwójnych cudzysłowach podajemy tekst do wyświetlenia. Możemy też podać w pojedynczym cudzysłowie pojedynczy znak do wyświetlenia. Oto te dwie możliwości:

.print("tekst");
.print('a');

Pamiętamy, że tekst to w sumie też liczby – kody ASCII. Dlatego możemy w szkicu zamiast znaku wpisać liczbę – kod tego znaku. Możemy do tego wykorzystać prostszą metodę .write(), podając numer kodu:
.write(97); //kod ASCII litery a

Przypomnijmy, że kod ASCII jest siedmiobitowy, pierwotnie stworzony był do elektromechanicznych dalekopisów i dlatego pierwsze 32 kody (i kod 127) służyły do sterowania mechanizmem dalekopisu. Natomiast kody 32…126 służyły do wyświetlania liter, cyfr i innych zdefiniowanych symboli według rysunku 2.

Rysunek 2

Zielona podkładka pokazuje kody, które powodowały wyświetlanie znaków. Kody wyróżnione niebieską podkładką nie powodowały pisania, tylko realizowały inne funkcje. W szczególności dziesiąty kod (szesnastkowo 0xA) oznaczony LF (line feed) powodował niewielki obrót wałka z papierem, co oznaczało wysunięcie papieru i przygotowanie do pisania w następnej, niższej linii. Z reguły dziesiąty kod LF był wysyłany razem z kodem trzynastym (szesnastkowo 0xD) oznaczonym CR (carriage return), powodującym powrót karetki drukującej na początek linii. W dalekopisach, a potem w niektórych systemach komputerowych polecenie pisania od początku nowej linii wymagało wysłania tych dwóch znaków sterujących: kodów ASCII o numerach 10 i 13, czyli LF i CR. Jednak w systemach komputerowych nie są potrzebne oba te znaki i często poleceniem przejścia do początku nowej linii jest pojedynczy kod numer 10 (LF).

Prawie wszystkie kody sterujące były potrzebne w dalekopisach. Ale większość była niepotrzebna w systemach komputerowych. Tu zasadniczo potrzebny byłby tylko jeden kod sterujący: kod numer 10 – nowa linia (pomijając specyficzny kod 8 BS – backspace – cofnięcie o jeden znak oraz kod 0 NUL będący końcem napisu – łańcucha znaków). Kod 13  (CR) można pominąć, zignorować, bo jego pierwotną funkcję zrealizuje kod 10 LF.

Natomiast pozostałe kody sterujące z zakresu 1…31 można wykorzystać inaczej, niż pierwotnie było w dalekopisach: im też można przypisać jakieś znaki – symbole i wykorzystać do drukowania.

I tak właśnie robimy! Nieużywanym „najniższym” kodom sterującym ASCII przypisujemy jakieś znaki – symbole, o czym za chwilę.

Powszechnie wykorzystujemy bajty, czyli liczby 8-bitowe, dlatego można też wykorzystać kody „najwyższe”, których nie było w kodzie ASCII. Omawialiśmy już sprawę zamieszania z różnymi stronami kodowymi i rozwiązaniem w postaci Unikodu (z najpopularniejszym kodowaniem UTF-8). Teraz przy wykorzystaniu wyświetlaczy graficznych problemy te wracają.

W metodzie .print() jako argument podajemy tekst do wyświetlenia, czyli w zasadzie ciąg liczb z zakresu 1…255. Jeżeli są to ściśle zdefiniowane kody ASCII, czyli liczby w zakresie 33…126, to żadnego problemu nie ma – zostaną wyświetlone odpowiednie litery, cyfry i symbole według rysunku 2. My jednak docelowo chcielibyśmy wyświetlać polskie litery i dlatego musimy zbadać dokładniej, jak je można byłoby wyświetlić.

Najpierw zbadamy jednak, co biblioteka Adafruit wyświetli, gdy spróbujemy wyświetlić dostępne kody 0…255. Wszystkie znaki nie zmieszczą się na wyświetlaczu 128×64, dlatego zestaw podzielmy na dwie części. Z kodami 128…255 nie ma większego problemu. Zrealizuje to szkic 1  (dostępny w Elportalu jako A2301.ino w dwóch wersjach: dla SSD1306 oraz SH1106).

Szkic 1:

//wyświetlamy kody 128...255:

for (int i = 128, j = 0; i < 256; i++, j++) {

 if (j == 16) {  //po 16 w każdej linii

  wysw.print("\n"); // przejdź do nowej linii

//wysw.write(10); // to samo, co powyżej!

   j = 0;

  }

  wysw.write(i); //wyświetl znak o kodzie i

 }

wysw.display(); //teraz pokaż na ekranie

Mamy tu pętlę for z licznikiem i, w zakresie 128…255, która za pomocą polecenia wysw.write(i); wyświetli wszystkie „górne kody”.  Dla porządku w ośmiu liniach wyświetlmy po 16 znaków, w czym pomoże zmienna licznikowa j. Po wyświetleniu 16 znaków w jednej linii (j==16) do wyświetlacza wysyłamy polecenie przejścia do nowej linii:

wysw.print("\n");

przez co wyświetlacz będzie wyglądał jak na fotografii 3. Do omówienia mamy tu ważny szczegół: co tak naprawdę robi instrukcja: wysw.print(„\n”);?

Fotografia 3

Wiemy, że wyświetlanie to wysyłanie kodów do modułu wyświetlacza. W tym przypadku kodu nowej linii. Jak mówiliśmy przed chwilą, jest to kod ASCII numer 10 oznaczony na rysunku 2 skrótem LF. Dlatego w szkicu zamiast: wysw.print(„\n”); możemy śmiało wpisać ten sam rozkaz w inny sposób:  wysw.write(10);.

Sprawdź sam, czy tak jest naprawdę.

Jest!

I właśnie to jest powód, dla którego zaczęliśmy od „górnych kodów” 128…255. Problem w tym, że jeżeli analogicznie każemy wyświetlić kolejne symbole, kryjące się pod kodami 0…127 (samodzielnie zmodyfikuj szkic A2301.ino), to jednym z nich będzie kod numer 10, który zaburzy porządek wyświetlania!

Fotografia 4

Fotografia 4 pokazuje, że tak jest istotnie! Wyświetlanie zaczyna się od kodu 0 (NUL), który nie powinien reprezentować i nie reprezentuje żadnego symbolu. Potem kolejno wyświetlane są symbole przypisane do kodów 1…9. Kod 10, czyli LF, powoduje przejście do nowej linii. W tej nowej linii wyświetlane są tylko cztery kody, z których ostatni zapewne ma numer 15. Przed wyświetleniem kodu numer 16 następuje przejście do nowej linii dzięki zmiennej licznikowej j. Dalej kody wyświetlane są już w prawidłowej kolejności, a pierwszy drukowalny znak ASCII o numerze 32 to spacja, więc pole pozostaje puste.

Wyjaśniliśmy częściowo sprawę kodu numer 10, który u nas jest przejściem do początku nowej linii.

Zauważ jednak, że w dwóch pierwszych liniach liczba wyświetlonych symboli jest za mała! Jeżeli ostatni symbol w drugiej linii ma kod numer 15, to jaki numer ma pierwszy kod w tej linii? 12? A może 11?

Można to sprawdzić w różny sposób. Okaże się, że symbol ten jest wyświetlany przez kod 11 i że na ekranie brakuje symboli dla kodów 10, a także dla kodu ASCII numer 13, oznaczanym CR (powrót karetki), którego my  teraz nie wykorzystujemy, ale który jest stosowany we współczesnych standardach.

Mając taką wiedzę, możemy poprawić szkic A2301.ino dodając warunek:

//wyświetlamy kody 0...127:

for (int i=0, j=0; i<128; i++, j++)

 if (i==10 || i==13) wysw.print("x")

Gdy dochodzimy do znaków o kodach 10 i 13, wyświetlana jest literka x. Fotografia 5 przedstawia, co wyświetli tak poprawiony szkic A2302.ino (dostępny w Elportalu tylko w wersji SH1106), co pokazują czerwone strzałki. Zielonkawa podkładka wyróżnia klasyczne, drukowalne znaki ASCII o kodach 32…127.

Fotografia 5

Uporządkujmy wnioski: podczas korzystania z metody .print() i biblioteki Adafruit GFX możemy wyświetlić  ponad 250 liter, cyfr i różnych symboli. Wykorzystujemy rozszerzony kod ASCII, ale kody numer 10 i numer 13 nie wyświetlają znaków, tylko pozostają kodami sterującymi. Kod 10 to kod przejścia na początek nowej linii, a kod 13 jest po prostu ignorowany. Te wyjątki wydają się dziwne i dociekliwi zapytają: gdzie są ustalone te wyjątki?

Sprawa jest zawikłana, bo po pierwsze biblioteka Adafruit GFX oferuje nie tylko jeden prymitywny, podstawowy font o znakach 5×7 pikseli, o którym teraz mówimy. Dodatkowo oferuje najrozmaitsze „lepsze fonty GFX”. Ponadto Arduino wykorzystuje różne klasy i funkcje, których zupełnie nie widać w szkicu. Wiemy, że do wyświetlania i wysyłania tekstów może być wykorzystywana dość prymitywna metoda .write(), która najprościej biorąc, powoduje wysłanie/wydrukowanie/wyświetlenie jednego znaku o podanym kodzie (ASCII). Ale częściej korzystamy z lepszej, rozbudowanej metody .print(), która  potrafi inteligentnie wyświetlać liczby ze zmiennych różnego typu oraz napisy i pojedyncze znaki. Nie wchodząc w szczegóły: jeżeli starannie przeanalizujemy biblioteczny plik Adafruit_GFX.cpp, to znajdziemy tam fragment kodu pokazany w szkicu 2.

Szkic 2:

size_t Adafruit_GFX::write(uint8_t c) {

 if(!gfxFont) { // 'Classic' built-in font
if(c == '\n') {                        // Newline?
cursor_x  = 0;                    // Reset x to zero,
cursor_y += textsize_y * 8;      // advance y one line
} else if(c != '\r') {         // Ignore carriage returns
if(wrap &&((cursor_x+textsize_x*6)>_width)){// Off right?
cursor_x  = 0;                 // Reset x to zero,
cursor_y += textsize_y * 8;  // advance y one line
}
drawChar(cursor_x, cursor_y, c, textcolor, (...)

    cursor_x += textsize_x * 6;    // Advance x one char

Jak widać, jest to fragment definicji metody .write(), która ma wydrukować jeden znak o podanym kodzie ASCII (c). Widzimy, że program sprawdza najpierw, czy podany kod nie jest znakiem nowej linii ’\n’, czyli kodem numer 10 (LF). Jeżeli tak, to kursor jest przestawiany na początek następnej linii. Potem program sprawdza, czy podany kod nie jest znakiem ’\r’, czyli kodem 13 (CR). Jeśli nie jest ani kodem 10, ani 13, to po sprawdzeniu, czy znak zmieści się w bieżącej linii, jest on drukowany za pomocą wewnętrznej funkcji drawChar() i kursor przesuwany jest o jeden znak dalej.

Dlatego kody 10, 13 nie powodują wyświetlania znaków – symboli. Natomiast pozostałe kody z zakresu 0…255 znaki wyświetlają…

Ale jak wyświetlają?

Wcześniej mówiliśmy, że poszczególne litery, cyfry i inne symbole napisów wyświetlamy  w postaci „małych obrazków bitmapowych”. Generalnie tak właśnie jest, ale można to zrealizować na wiele sposobów. Na razie mówimy o prymitywnym podstawowym foncie 5×7, którego zawartość opisana jest w pliku glcdfont.c. Jeżeli zajrzymy do tego pliku, to po wstępnych dyrektywach kompilatora znajdziemy właściwą definicję standardowego fontu 5×7, co jest pokazane w szkicu 3.

Szkic 3:

// Standard ASCII 5x7 font
static const unsigned char font[] PROGMEM = {
   0x00, 0x00, 0x00, 0x00, 0x00, //kod nr 0
0x3E, 0x5B, 0x4F, 0x5B, 0x3E, //kod nr 1
0x3E, 0x6B, 0x4F, 0x6B, 0x3E, //kod nr 2
   0x1C, 0x3E, 0x7C, 0x3E, 0x1C, //kod nr 3
   0x18, 0x3C, 0x7E, 0x3C, 0x18, // itd.
0x1C, 0x57, 0x7D, 0x57, 0x1C,
0x1C, 0x5E, 0x7F, 0x5E, 0x1C,

   (...)   // i tak dalej...

Jak widać, dzięki obecności dyrektywy PROGMEM kompilator umieści w pamięci programu FLASH jednowymiarową tablicę o nazwie font, składającą się z bajtów (unsigned char, czyli uint8_t).

Wcześniej omawialiśmy zasadę definiowania i wyświetlania treści „zwyczajnych” obrazków bitmap, które są zapisywane w pamięci programu FLASH jako tablice bajtów. Poszczególne bity bajtów określają tam stan kolejnych pikseli w jednej poziomej linii. Teraz z w pliku Adafruit_GFX.cpp znajdziemy definicję wspomnianej metody drawChar(), która „drukuje” jeden znak – patrz szkic 4.

Szkic 4:

void Adafruit_GFX::drawChar(int16_t x, int16_t y, unsigned char c,
uint16_t color, uint16_t bg, uint8_t size_x, uint8_t size_y){

if(!gfxFont) { // 'Classic' built-in font
(...) // sprawdzanie kilku warunków, w tym:
if(!_cp437 && (c >=176)) c++; // Handle 'classic' charset
startWrite();
for(int8_t i=0; i<5; i++ ) { // Char bitmap = 5 columns
uint8_t line = pgm_read_byte(&font[c * 5 + i]);

  for(int8_t j=0; j<8; j++, line >>= 1) {

  if(line & 1) {

   if(size_x == 1 && size_y == 1) //normalna wielkość znaku

    writePixel(x+i, y+j, color);

   else // gdy rozmiar znaku ma być większy: 2, 3, 4.. razy

    writeFillRect(x+i*size_x, y+j*size_y, size_x, size_y, color);
  } else if(bg != color) { //jeżeli użyty kolor tła - bg

   if(size_x == 1 && size_y == 1) //gdy normalna wielkość

      writePixel(x+i, y+j, bg);
else //gdy rozmiar ma być większy

      writeFillRect(x+i*size_x, (...));   }  }  }
if(bg !=color){//If opaque, draw vertical line for last column
  if(size_x == 1 && size_y == 1) writeFastVLine(x+5, y, 8, bg);
else          writeFillRect(x+5*size_x, (...));  }
  endWrite();

 } else { // Custom font (...)

Jako argumenty do tej metody przekazujemy: gdzie na ekranie ma być wydrukowany znak (x, y), numer – kod ASCII tego znaku (c), kolor znaku (color) i ewentualnie kolor tła (bg), a także wielkość – mnożnik szerokości i wysokości znaku (size_x, size_y). Kolorem czerwonym wyróżniona jest podstawowa procedura tworzenia znaku. Jeżeli chcemy wyświetlić znak o kodzie ASCII równym c, to  do zmiennej line będziemy odczytywać pięć kolejnych bajtów z tablicy font z pamięci programu:

line = pgm_read_byte(&font[c * 5 + i]);

a potem wyświetlać ich kolejne bity. Potwierdza się informacja ze szkicu 3, że znak o kodzie c jest zdefiniowany w pięciu bajtach, z których pierwszy jest w tablicy font bajtem o numerze kolejnym c*5 (licząc oczywiście od zera).

Znów ze zdziwieniem stwierdzamy, że znak jest bardzo nieefektywnie tworzony przez zaświecanie pojedynczych pikseli poleceniem writePixel(). Ale bardziej zaskakujące jest to, że poszczególne bity bajtu nie określają stanu kolejnych pikseli linii poziomej, tylko linii pionowej. Zmienna licznikowa j, licząca 0…7 zwiększa bowiem wartość współrzędnej pionowej y:

for(int8_t j=0; j<8; j++, line >>= 1) {
  (...) writePixel(x+i, y+j, color);

Taki „pionowy” sposób rysowania  jest oszczędniejszy od „poziomego” rysowania klasycznych bitmap, bo nie „marnujemy bitów” i każdy znak opisuje tylko pięć bajtów. Bajt opisuje wygląd jednej kolumny. Przy rysowaniu „poziomym” każdy znak opisywałoby osiem bajtów.

Możemy to wszystko sprawdzić, rozpisując na przykład kody numer 1 i 3 z listingu 3 (kod 0 jest „pusty” – nieużywany). Wynik rozpisania pokazany jest na fotografii 6, gdzie wpisane i wyróżnione kolorem są tylko bity o wartości 1.

Fotografia 6

Tak kodowane są znaki fontu 5×7. Jeżeli przekręcimy obraz o 90 stopni, uzyskamy symbole wyświetlane przez kody (ASCII) o numerach 1 i 3, dokładnie tak, jak na fotografiach 4 i 5, co pokazuje rysunek 7, wykorzystujący mocno powiększony fragment fotografii 4.

Fotografia 7

Cieszymy się, że rozszyfrowaliśmy podstawy, ale wracamy do listingu – szkicu 4. Wiemy już, jak tworzony jest znak o standardowej, pojedynczej wielkości.  Niebieskim kolorem wyróżniona jest procedura rysowania znaków powiększonych 2, 3, 4… razy (powiększenie w osi poziomej i pionowej może być różne, o czym niezależnie decydują parametry size_x, size_y). W takim przypadku wykorzystujemy metodę writeFillRect(), która zamiast pojedynczego piksela, zaświeca malutki wypełniony prostokącik (lub częściej kwadracik) o rozmiarach size_x, size_y.

Możemy się o tym przekonać za pomocą programiku, który jest dostępny w Elportalu jako A2303.ino. W szkicu 5 pokazane są kluczowe fragmenty. Najpierw po prostu powiększamy wielkość znaków 2-, 3-, 4-, 5- i 6-krotnie. Na koniec wyświetlamy napis „chudy” oraz „płaski”, powiększając symbol tylko w jednej osi, najpierw pionowej (size_y = 2), potem poziomej (size_x = 2).

Szkic 5:

wysw.clearDisplay(); //czyść ekran

wysw.setCursor(0, 0); //zeruj kursor

wysw.setTextColor(1, 0);

wysw.setTextSize(1); // rozmiar standardowy

wysw.println("wielkosc normalna");

wysw.setTextSize(2);

wysw.println("podwojna");

wysw.setTextSize(3);

wysw.println("size3");

wysw.display();  //teraz wyświetl na ekranie

  (...)

wysw.clearDisplay(); //czyść ekran

wysw.setCursor(0, 0); //zeruj kursor

wysw.setTextSize(1, 1); //rozmiar standardowy

wysw.println("tekst normalny 1,1");

wysw.setTextSize(1, 2); //size_x = 1 size_y = 2

wysw.println("tekst \"chudy\" 1, 2");

wysw.setTextSize(2, 1); //size_x = 2 size_y = 1

wysw.println("\"plaski\" 2, 1");

wysw.display();  //teraz wyświetl na ekranie

Fotografia 8

Fotografia 8 przedstawia  dwa „zrzuty” ekranu.

I kolejny szczegół: omawiany font ma nazwę 5×7, ale każdy znak jest bitmapą zawierającą 5×8 pikseli (5 kolumn, 8 wierszy). Jednak na fotografiach 4, 5 kolejne znaki nie są wyświetlane bezpośrednio jeden za drugim, tylko rozdziela je jedna „pusta” pionowa linia. Jest ona niezbędna, by znaki „nie zlewały się”. Tworzenie tego odstępu realizuje fragment kodu ze szkicu 4, wyróżniony kolorem zielonym. Za znakiem rysowana jest pionowa linia: przy wielkości standardowej za pomocą metody writeFastVLine(), a przy większych za pomocą writeFillRect(). Tu nie wystarczy przesunięcie kursora – przy „odwróceniu kolorów”, czyli tekście negatywowym oraz na kolorowych wyświetlaczach musimy zaświecić piksele tej przerwy w kolorze tła.

Zrozumieliśmy podstawy. Ale nie kończymy tematu napisów i fontów. Do omówienia pozostają dwa bardzo ważne tematy: ulepszonych fontów GFX, a potem problem „polskich liter”☻
Piotr Górecki

Tytuł – maksymalnie 45 znaków wraz z spacjami

piotr-gorecki.pl/ZkasujE_MasterNowy

 

To jest ramka wstępna 3 linie – maksymalnie 300 znaków

druga linia

trzecia linia tekstu wstępnego w ramce

W dwóch poprzednich odcinkach dość dokładnie zapoznaliśmy się z wyświetlaczami graficznymi OLED i nauczyliśmy się wyświetlać na nich obrazy – bitmapy. Otwiera to drogę do zrozumienia niełatwego problemu czcionek – fontów. Już wcześniej pojawiła się informacja, że w Arduino na wyświetlaczu graficznym litery, cyfry i inne symbole to w rzeczywistości wyświetlane na ekranie małe bitmapy. Generalnie tak właśnie jest, ale jak to mówią, diabeł tkwi w szczegółach.

Fonty

Wyświetlanie tekstu można zrealizować na najróżniejsze sposoby. My chcemy trzymać się biblioteki Adafruit GFX, która we współpracy z „bibliotekami sprzętowymi” dla różnych sterowników, potrafi obsłużyć rozmaite wyświetlacze graficzne, w tym także kolorowe. Jeżeli zajrzymy do tej biblioteki (rysunek 1), to po pierwsze zauważymy katalog /Fonts, zawierający liczne fonty (czcionki). Do tego jest tam katalog /fontconvert, który daje możliwość tworzenia własnych fontów, a do tego dwa pliki: glcdfont.c oraz gfxfont.h.

Kluczowa informacja: biblioteka Adafruit GFX oferuje dwie możliwości. Po pierwsze wykorzystanie jednego podstawowego fontu zawartego w pliku glcdfont.c. Po drugie możliwość wykorzystania rozmaitych fontów, których część jest zawarta w katalogu /Fonts, a mnóstwo innych można stworzyć samodzielnie lub ściągnąć z sieci.

W naszym przypadku dochodzi jeszcze problem polskichznaków diakrytycznych, ponieważ w oryginalnych bibliotekach polskich liter nie ma. Aby je w jakiś sposób dodać, trzeba zrozumieć, czym jest font i jak działa wyświetlanie znaków. Problem w tym, że zupełnie inaczej działa font podstawowy z pliku glcdfont.c, a zupełnie inaczej fonty z katalogu /Fonts.

Zaczniemy od wersji prostszej.

glcdfont.c

Arduino, oparte na języku C++, potrafi wykorzystywać tak zwane klasy, co znakomicie ułatwia pisanie programów. Jedną z bardzo przydatnych właściwości jest to, że niezależnie od sprzętu możemy wykorzystywać pożyteczną metodę .print(), która najogólniej biorąc, służy do pisania tekstu. Od dawna często posługujemy się tą metodą przy korzystaniu z wyświetlacza znakowego LCD oraz do wyświetlania tekstu na konsoli monitora za pomocą łącza Serial, a ostatnio w szkicu A2101.ino, który był zmodyfikowanym plikiem przykładowym, pokazującym możliwości biblioteki Adafruit GFX. Wróćmy jeszcze raz do tego szkicu, gdzie znajdziemy podstawowe polecenia związane z wyświetlaniem tekstu na wyświetlaczu graficznym:

wysw.setCursor(0, -3);
wysw.setTextColor(1, 0);
wysw.setTextSize(1);
wysw.setTextWrap(1);
wysw.print(„wyswietlany napis”); wysw.display();

Interesują nas linie czerwone. Metoda .setTextSize(1) pozwoli ustawić wielkość tekstu: liczba 1 to wielkość podstawowa 5 × 7 pikseli, 2 to podwojenie rozmiaru, 3 – potrojenie, itd. Do właściwego wyświetlenia wykorzystujemy pożyteczną metodę .print(). W podwójnych cudzysłowach podajemy tekst do wyświetlenia. Możemy też podać w pojedynczym cudzysłowie pojedynczy znak do wyświetlenia. Oto te dwie możliwości:

.print(„tekst”);
.print(‚a’);

Pamiętamy, że tekst to w sumie też liczby – kody ASCII. Dlatego możemy w szkicu zamiast znaku wpisać liczbę – kod tego znaku. Możemy do tego wykorzystać prostszą metodę .write(), podając numer kodu:

.write(97); //kod ASCII litery a

Przypomnijmy, że kod ASCII jest siedmiobitowy, pierwotnie stworzony był do elektromechanicznych dalekopisów i dlatego pierwsze 32 kody (i kod 127) służyły do sterowania mechanizmem dalekopisu. Natomiast kody 32…126 służyły do wyświetlania liter, cyfr i innych zdefiniowanych symboli według rysunku 2. Zielona podkładka pokazuje kody, które powodowały wyświetlanie znaków. Kody wyróżnione niebieską podkładką nie powodowały pisania, tylko realizowały inne funkcje. W szczególności dziesiąty kod (szesnastkowo 0xA) oznaczony LF (line feed) powodował niewielki obrót wałka z papierem, co oznaczało wysunięcie papieru i przygotowanie do pisania w następnej, niższej linii. Z reguły dziesiąty kod LF był wysyłany razem z kodem trzynastym (szesnastkowo 0xD) oznaczonym CR (carriage return), powodującym powrót karetki drukującej na początek linii. W dalekopisach, a potem w niektórych systemach komputerowych polecenie pisania od początku nowej linii wymagało wysłania tych dwóch znaków sterujących: kodów ASCII o numerach 10 i 13, czyli LF i CR. Jednak w systemach komputerowych nie są potrzebne oba te znaki i często poleceniem przejścia do początku nowej linii jest pojedynczy kod numer 10 (LF).

Prawie wszystkie kody sterujące były potrzebne w dalekopisach. Ale większość była niepotrzebna w systemach komputerowych. Tu zasadniczo potrzebny byłby tylko jeden kod sterujący: kod numer 10 – nowa linia (pomijając specyficzny kod 8 BS – backspace – cofnięcie o jeden znak oraz kod 0 NUL będący końcem napisu – łańcucha znaków). Kod 13 (CR) można pominąć, zignorować, bo jego pierwotną funkcję zrealizuje kod 10 LF.

Natomiast pozostałe kody sterujące z zakresu 1…31 można wykorzystać inaczej, niż pierwotnie było w dalekopisach: im też można przypisać jakieś znaki – symbole i wykorzystać do drukowania.

I tak właśnie robimy! Nieużywanym „najniższym” kodom sterującym ASCII przypisujemy jakieś znaki – symbole, o czym za chwilę.

Powszechnie wykorzystujemy bajty, czyli liczby 8-bitowe, dlatego można też wykorzystać kody „najwyższe”, których nie było w kodzie ASCII. Omawialiśmy już sprawę zamieszania z różnymi stronami kodowymi i rozwiązaniem w postaci Unikodu (z najpopularniejszym kodowaniem UTF-8). Teraz przy wykorzystaniu wyświetlaczy graficznych problemy te wracają.

W metodzie .print() jako argument podajemy tekst do wyświetlenia, czyli w zasadzie ciąg liczb z zakresu 1…255. Jeżeli są to ściśle zdefiniowane kody ASCII, czyli liczby w zakresie 33…126, to żadnego problemu nie ma – zostaną wyświetlone odpowiednie litery, cyfry i symbole według rysunku 2. My jednak docelowo chcielibyśmy wyświetlać polskie litery i dlatego musimy zbadać dokładniej, jak je można byłoby wyświetlić.

Najpierw zbadamy jednak, co biblioteka Adafruit wyświetli, gdy spróbujemy wyświetlić dostępne kody 0…255. Wszystkie znaki nie zmieszczą się na wyświetlaczu 128×64, dlatego zestaw podzielmy na dwie części. Z kodami 128…255 nie ma większego problemu. Zrealizuje to szkic 1 (dostępny w Elportalu jako A2301.ino w dwóch wersjach: dla SSD1306 oraz SH1106). Mamy tu pętlę for z licznikiem i, w zakresie 128…255, która za pomocą polecenia wysw.write(i); wyświetli wszystkie „górne kody”. Dla porządku w ośmiu liniach wyświetlmy po 16 znaków, w czym pomoże zmienna licznikowa j. Po wyświetleniu 16 znaków w jednej linii (j==16) do wyświetlacza wysyłamy polecenie przejścia do nowej linii:

wysw.print(„\n”);

przez co wyświetlacz będzie wyglądał jak na fotografii 3. Do omówienia mamy tu ważny szczegół: co tak naprawdę robi instrukcja: wysw.print(„\n”);?

Wiemy, że wyświetlanie to wysyłanie kodów do modułu wyświetlacza. W tym przypadku kodu nowej linii. Jak mówiliśmy przed chwilą, jest to kod ASCII numer 10 oznaczony na rysunku 2 skrótem LF. Dlatego w szkicu zamiast: wysw.print(„\n”); możemy śmiało wpisać ten sam rozkaz w inny sposób: wysw.write(10);.

Sprawdź sam, czy tak jest naprawdę.

Jest!

I właśnie to jest powód, dla którego zaczęliśmy od „górnych kodów” 128…255. Problem w tym, że jeżeli analogicznie każemy wyświetlić kolejne symbole, kryjące się pod kodami 0…127 (samodzielnie zmodyfikuj szkic A2301.ino), to jednym z nich będzie kod numer 10, który zaburzy porządek wyświetlania!

Fotografia 4 pokazuje, że tak jest istotnie! Wyświetlanie zaczyna się od kodu 0 (NUL), który nie powinien reprezentować i nie reprezentuje żadnego symbolu. Potem kolejno wyświetlane są symbole przypisane do kodów 1…9. Kod 10, czyli LF, powoduje przejście do nowej linii. W tej nowej linii wyświetlane są tylko cztery kody, z których ostatni zapewne ma numer 15. Przed wyświetleniem kodu numer 16 następuje przejście do nowej linii dzięki zmiennej licznikowej j. Dalej kody wyświetlane są już w prawidłowej kolejności, a pierwszy drukowalny znak ASCII o numerze 32 to spacja, więc pole pozostaje puste.

Wyjaśniliśmy częściowo sprawę kodu numer 10, który u nas jest przejściem do początku nowej linii.

Zauważ jednak, że w dwóch pierwszych liniach liczba wyświetlonych symboli jest za mała! Jeżeli ostatni symbol w drugiej linii ma kod numer 15, to jaki numer ma pierwszy kod w tej linii? 12? A może 11?

Można to sprawdzić w różny sposób. Okaże się, że symbol ten jest wyświetlany przez kod 11 i że na ekranie brakuje symboli dla kodów 10, a także dla kodu ASCII numer 13, oznaczanym CR (powrót karetki), którego my teraz nie wykorzystujemy, ale który jest stosowany we współczesnych standardach.

Mając taką wiedzę, możemy poprawić szkic A2301.ino dodając warunek:

//wyświetlamy kody 0...127:
for (int i=0, j=0; i<128; i++, j++)
 if (i==10 || i==13) wysw.print(„x”)

Gdy dochodzimy do znaków o kodach 10 i 13, wyświetlana jest literka x. Fotografia 5 przedstawia, co wyświetli tak poprawiony szkic A2302.ino (dostępny w Elportalu tylko w wersji SH1106), co pokazują czerwone strzałki. Zielonkawa podkładka wyróżnia klasyczne, drukowalne znaki ASCII o kodach 32…127.

Uporządkujmy wnioski: podczas korzystania z metody .print() i biblioteki Adafruit GFX możemy wyświetlić ponad 250 liter, cyfr i różnych symboli. Wykorzystujemy rozszerzony kod ASCII, ale kody numer 10 i numer 13 nie wyświetlają znaków, tylko pozostają kodami sterującymi. Kod 10 to kod przejścia na początek nowej linii, a kod 13 jest po prostu ignorowany. Te wyjątki wydają się dziwne i dociekliwi zapytają: gdzie są ustalone te wyjątki?

Sprawa jest zawikłana, bo po pierwsze biblioteka Adafruit GFX oferuje nie tylko jeden prymitywny, podstawowy font o znakach 5×7 pikseli, o którym teraz mówimy. Dodatkowo oferuje najrozmaitsze „lepsze fonty GFX”. Ponadto Arduino wykorzystuje różne klasy i funkcje, których zupełnie nie widać w szkicu. Wiemy, że do wyświetlania i wysyłania tekstów może być wykorzystywana dość prymitywna metoda .write(), która najprościej biorąc, powoduje wysłanie/wydrukowanie/wyświetlenie jednego znaku o podanym kodzie (ASCII). Ale częściej korzystamy z lepszej, rozbudowanej metody .print(), która potrafi inteligentnie wyświetlać liczby ze zmiennych różnego typu oraz napisy i pojedyncze znaki. Nie wchodząc w szczegóły: jeżeli starannie przeanalizujemy biblioteczny plik Adafruit_GFX.cpp, to znajdziemy tam fragment kodu pokazany w szkicu 2.

Jak widać, jest to fragment definicji metody .write(), która ma wydrukować jeden znak o podanym kodzie ASCII (c). Widzimy, że program sprawdza najpierw, czy podany kod nie jest znakiem nowej linii ‚\n’, czyli kodem numer 10 (LF). Jeżeli tak, to kursor jest przestawiany na początek następnej linii. Potem program sprawdza, czy podany kod nie jest znakiem ‚\r’, czyli kodem 13 (CR). Jeśli nie jest ani kodem 10, ani 13, to po sprawdzeniu, czy znak zmieści się w bieżącej linii, jest on drukowany za pomocą wewnętrznej funkcji drawChar() i kursor przesuwany jest o jeden znak dalej.

Dlatego kody 10, 13 nie powodują wyświetlania znaków – symboli. Natomiast pozostałe kody z zakresu 0…255 znaki wyświetlają…

Ale jak wyświetlają?

Wcześniej mówiliśmy, że poszczególne litery, cyfry i inne symbole napisów wyświetlamy w postaci „małych obrazków bitmapowych”. Generalnie tak właśnie jest, ale można to zrealizować na wiele sposobów. Na razie mówimy o prymitywnym podstawowym foncie 5×7, którego zawartość opisana jest w pliku glcdfont.c. Jeżeli zajrzymy do tego pliku, to po wstępnych dyrektywach kompilatora znajdziemy właściwą definicję standardowego fontu 5×7, co jest pokazane w szkicu 3. Jak widać, dzięki obecności dyrektywy PROGMEM kompilator umieści w pamięci programu FLASH jednowymiarową tablicę o nazwie font, składającą się z bajtów (unsigned char, czyli uint8_t).

Wcześniej omawialiśmy zasadę definiowania i wyświetlania treści „zwyczajnych” obrazków bitmap, które są zapisywane w pamięci programu FLASH jako tablice bajtów. Poszczególne bity bajtów określają tam stan kolejnych pikseli w jednej poziomej linii. Teraz z w pliku Adafruit_GFX.cpp znajdziemy definicję wspomnianej metody drawChar(), która „drukuje” jeden znak – patrz szkic 4. Jako argumenty do tej metody przekazujemy: gdzie na ekranie ma być wydrukowany znak (x, y), numer – kod ASCII tego znaku (c), kolor znaku (color) i ewentualnie kolor tła
(bg), a także wielkość – mnożnik szerokości i wysokości znaku (size_x, size_y). Kolorem czerwonym wyróżniona jest podstawowa procedura tworzenia znaku. Jeżeli chcemy wyświetlić znak o kodzie ASCII równym c, to do zmiennej line będziemy odczytywać pięć kolejnych bajtów z tablicy font z pamięci programu:

line = pgm_read_byte(&font[c * 5 + i]);

a potem wyświetlać ich kolejne bity. Potwierdza się informacja ze szkicu 3, że znak o kodzie c jest zdefiniowany w pięciu bajtach, z których pierwszy jest w tablicy font bajtem o numerze kolejnym c*5 (licząc oczywiście od zera).

Znów ze zdziwieniem stwierdzamy, że znak jest bardzo nieefektywnie tworzony przez zaświecanie pojedynczych pikseli poleceniem writePixel(). Ale bardziej zaskakujące jest to, że poszczególne bity bajtu nie określają stanu kolejnych pikseli linii poziomej, tylko linii pionowej. Zmienna licznikowa j, licząca 0…7 zwiększa bowiem wartość współrzędnej pionowej y:

for(int8_t j=0; j<8; j++, line >>= 1) {
  (...) writePixel(x+i, y+j, color);

Taki „pionowy” sposób rysowania jest oszczędniejszy od „poziomego” rysowania klasycznych bitmap, bo nie „marnujemy bitów” i każdy znak opisuje tylko pięć bajtów. Bajt opisuje wygląd jednej kolumny. Przy rysowaniu „poziomym” każdy znak opisywałoby osiem bajtów.

Możemy to wszystko sprawdzić, rozpisując na przykład kody numer 1 i 3 z listingu 3 (kod 0 jest „pusty” – nieużywany). Wynik rozpisania pokazany jest na fotografii 6, gdzie wpisane i wyróżnione kolorem są tylko bity o wartości 1.

Tak kodowane są znaki fontu 5×7. Jeżeli przekręcimy obraz o 90 stopni, uzyskamy symbole wyświetlane przez kody (ASCII) o numerach 1 i 3, dokładnie tak, jak na fotografiach 4 i 5, co pokazuje rysunek 7, wykorzystujący mocno powiększony fragment fotografii 4.

 

Cieszymy się, że rozszyfrowaliśmy podstawy, ale wracamy do listingu – szkicu 4. Wiemy już, jak tworzony jest znak o standardowej, pojedynczej wielkości. Niebieskim kolorem wyróżniona jest procedura rysowania znaków powiększonych 2, 3, 4… razy (powiększenie w osi poziomej i pionowej może być różne, o czym niezależnie decydują parametry size_x, size_y). W takim przypadku wykorzystujemy metodę writeFillRect(), która zamiast pojedynczego piksela, zaświeca malutki wypełniony prostokącik (lub częściej kwadracik) o rozmiarach size_x, size_y.

Możemy się o tym przekonać za pomocą programiku, który jest dostępny w Elportalu jako A2303.ino. W szkicu 5 pokazane są kluczowe fragmenty. Najpierw po prostu powiększamy wielkość znaków 2-, 3-, 4-, 5- i 6-krotnie. Na koniec wyświetlamy napis „chudy” oraz „płaski”, powiększając symbol tylko w jednej osi, najpierw pionowej (size_y = 2), potem poziomej (size_x = 2). Fotografia 8 przedstawia dwa „zrzuty” ekranu.

I kolejny szczegół: omawiany font ma nazwę 5×7, ale każdy znak jest bitmapą zawierającą 5×8 pikseli (5 kolumn, 8 wierszy). Jednak na fotografiach 4, 5 kolejne znaki nie są wyświetlane bezpośrednio jeden za drugim, tylko rozdziela je jedna „pusta” pionowa linia. Jest ona niezbędna, by znaki „nie zlewały się”. Tworzenie tego odstępu realizuje fragment kodu ze szkicu 4, wyróżniony kolorem zielonym. Za znakiem rysowana jest pionowa linia: przy wielkości standardowej za pomocą metody writeFastVLine(), a przy większych za pomocą writeFillRect(). Tu nie wystarczy przesunięcie kursora – przy „odwróceniu kolorów”, czyli tekście negatywowym oraz na kolorowych wyświetlaczach musimy zaświecić piksele tej przerwy w kolorze tła.

Zrozumieliśmy podstawy. Ale nie kończymy tematu napisów i fontów. Do omówienia pozostają dwa bardzo ważne tematy: ulepszonych fontów GFX, a potem problem „polskich liter”. Zaczniemy to badać w następnym odcinku.

 

Piotr Górecki