[MBED] STM32 i SSD1306 – sterowanie dwukolorowym OLED [2]
Przedstawiamy drugą część kursu obsługi wyświetlacza OLED za pomocą STM32. Pierwsza część artykułu jest dostępna pod adresem.
Wyświetlacz graficzny doskonale się nadaje do wyświetlania tekstu. W klasycznych wyświetlaczach alfanumerycznych czcionki mają jednakową wielkość zależną od wielkości wyświetlacza i mogą być wyświetlane w ustalonych wierszach. W wyświetlaczu graficznym można definiować czcionki o różnych rozmiarach zależnie od potrzeb i umieszczać je w dowolnym miejscu wyświetlacza – o ile się tam zmieszczą.
Przy adresowaniu Page Addressing Mode jest bardzo łatwo zdefiniować znaki o wysokości 8 pikseli, lub wielokrotności tej wartości: 16, 24, 32 itd. Najprościej definiować znaki np. 8×6 pikseli umieszczając w pamięci generatora znaków 6 kolejnych bajtów – łatwo to sobie wyobrazić patrząc na rysunek 7. W programowych bibliotekach obsługujących wyświetlacze graficzne stosowałem z powodzeniem tę metodę. Mam zdefiniowaną tablice generatora znaków i proste procedury wyświetlania znaku na podstawie jego kodu ASCII. Niestety przy tym wyświetlaczu znaki o wysokości 8 pikseli są praktycznie nieczytelne. Wynika to po prostu z małych wymiarów (przekątnej) matrycy. W naszym przypadku wartością graniczną jest wysokość 12 pikseli, a najlepiej gdyby znaki miały 16 i więcej pikseli.
Do rysowania znaków o dowolnej wielkości najlepiej się nadaje funkcja, która potrafi zapalić lub zgasić jeden piksel o dowolnych współrzędnych x, y niezależnych od trybu adresowania. Taka funkcja została pokazana na listingu 7.
List. 7. Rysowanie punktu o ustalonych współrzędnych
//********************************************************** //"rysowanie” punktu w buforze RAM mikrokontrolera Hosta //********************************************************** void DrawPoint(unsigned char x,unsigned char y, unsigned char p) { uint8_t chPos, chBx, chTemp = 0; if (x > 127 || y > 63) { return; } chPos = 7 - y / 8; chBx = y % 8; chTemp = 1 << (7 - chBx); if (p) { DispBuff[x][chPos] |= chTemp; } else { DispBuff[x][chPos] &= ~chTemp; } }
Jak łatwo zauważyć funkcja void DrawPoint() z listingu 7 tylko modyfikuje zawartość dwuwymiarowego bufora DispBuff umieszczonego w pamięci RAM mikrokontrolera Hosta i nie zapisuje modyfikacji do sterownika SSD1306. Jak zobaczymy dalej wszystkie procedury wyświetlania modyfikują tylko ten bufor i aby zobaczyć efekt tych modyfikacji trzeba przepisać całą zawartość DispBuff do pamięci sterownika wywołując funkcję void RefreshRAM(void) pokazaną na listingu 5.
Mając do dyspozycji procedurę potrafiącą zapalać/gasić piksel o konkretnej współrzędnej możemy rysować proste, figury, okręgi, ale też rysować znaki alfanumeryczne o dowolnych wielkościach. Żeby to robić potrzebne są wzorce znaków umieszczone w tablicy zwanej tez generatorem znaków. Ręczne tworzenie wzorców znaków jest możliwe, ale to żmudne zajęcie. Istnieje szereg programów tworzących wzorce o zadanej wielkości znaku. W Internecie można też znaleźć gotowe tablice z wzorcami. Ja skorzystałem z gotowych tablic ze zdefiniowanymi znakami o wielkości 12x6pikseli i 16x8pikseli. Tablice są tak zbudowane, ze kod ASCII znaku adresuje grupę bajtów definiujących ten znak. To znacznie upraszcza procedury wyświetlające łańcuchy znaków (napisy). Na przykład tablica dla znaków o wysokości 12 i szerokości 6 pikseli jest dwuwymiarową tablicą const char c_chFont1206[95][12] i zawiera 95 wzorców znaków. Każdy element wzorca znaków ma 12bajtów i definiuje znak o szerokości 6 pikseli, ale wykorzystuje przestrzeń 12×8 pikseli – listing 8.
List. 8. Fragment tablicy generatora znaków 12×6 pikseli
const char c_chFont1206[95][12] = { {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*" ",0*/ {0x00,0x00,0x00,0x00,0x3F,0x40,0x00,0x00,0x00,0x00,0x00,0x00},/*"!",1*/ {0x00,0x00,0x30,0x00,0x40,0x00,0x30,0x00,0x40,0x00,0x00,0x00},/*""",2*/ {0x09,0x00,0x0B,0xC0,0x3D,0x00,0x0B,0xC0,0x3D,0x00,0x09,0x00},/*"#",3*/ {0x18,0xC0,0x24,0x40,0x7F,0xE0,0x22,0x40,0x31,0x80,0x00,0x00},/*"$",4*/ {0x18,0x00,0x24,0xC0,0x1B,0x00,0x0D,0x80,0x32,0x40,0x01,0x80},/*"%",5*/ {0x03,0x80,0x1C,0x40,0x27,0x40,0x1C,0x80,0x07,0x40,0x00,0x40},/*"&",6*/ {0x10,0x00,0x60,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00},/*"'",7*/ {0x00,0x00,0x00,0x00,0x00,0x00,0x1F,0x80,0x20,0x40,0x40,0x20},/*"(",8*/ {0x00,0x00,0x40,0x20,0x20,0x40,0x1F,0x80,0x00,0x00,0x00,0x00},/*")",9*/ {0x09,0x00,0x06,0x00,0x1F,0x80,0x06,0x00,0x09,0x00,0x00,0x00},/*"*",10*/ ……… }
Dla znaków 16×8 pikseli jest zdefiniowana druga tablica const uint8_t c_chFont1608[95][16].
Mając tablice generatora znaków i procedurę DrawPoint() potrafiącą „zapalać i gasić” bit w buforze pamięci mikrokontrolera odpowiadający zawartości pamięci RAM sterownika, a tym samym odpowiadający pikselowi na matrycy OLED można napisać procedurę „rysującą” znak w pamięci RAM mikrokontrolera. Pamiętamy, że aby ten znak się wyświetlił trzeba przepisać zawartość DispBuff do pamięci RAM wywołując funkcję void RefreshRAM().
Na listingu 9 pokazano procedurę void DisplayChar() z argumentami:
- x i y – współrzędne początku znaku na ekranie,
- Chr – kod ASCII wyświetlanego znaku,
- size – wielkość znaku 12, lub 16 pikseli,
- mode= 1 wyświetlanie normalne, mode=0 wyświetlanie w negatywie.
Zależnie od wartości argumentu size bajty wzorca są pobierane z tablicy c_chFont1206[95][12] lub z tablicy c_chFont1608[95][16]. Kiedy argument mode jest wyzerowany to dodatkowo wartość pobranego bajtu jest negowana. Potem jest analizowany każdy bit bajtu wzorca i zależnie od jego wartości DrawPoint() zapisuje do bufora DispBuff odpowiednią wartość.
List. 9. Wyświetlenie jednego znaku
//****************************************************************************** //wyświetlenie jednego znaku //argumenty: //x,y - współrzędne na ekranie //Chr - kod ASCII znaku //size - rozmiar 12, lub 16 //mode=1 znak wyświetlany normalnie, mode=0 znak wyświetlany w negatywie //******************************************************************************* void DisplayChar(uint8_t x, uint8_t y, uint8_t Chr, uint8_t size, uint8_t mode) { uint8_t i, j; uint8_t chTemp, chYpos0 = y; Chr =Chr - ' '; for (i = 0; i < size; i ++) { if (size == 12) { if (mode) { chTemp = c_chFont1206[Chr][i]; } else { chTemp = ~c_chFont1206[Chr][i]; } } else { if (mode) { chTemp = c_chFont1608[Chr][i]; } else { chTemp = ~c_chFont1608[Chr][i]; } } for (j = 0; j < 8; j ++) { if (chTemp & 0x80) { DrawPoint(x, y, 1); } else { DrawPoint(x, y, 0); } chTemp <<= 1; y ++; if ((y - chYpos0) == size) { y = chYpos0; x ++; break; } } } }
Mając procedurę wyświetlania jednego znaku o dowolnych współrzędnych można napisać procedurę wyświetlającą łańcuch znaków od określonej pozycji.
Na listingu 10 pokazano procedurę void DispTxt() z argumentami:
- x i y – współrzędne początku znaku na ekranie,
- *txt – wskaźnik na początek bufora z łańcuchem znaków,
- size – wielkość znaku 12 lub 16 pikseli,
- mode= 1 wyświetlanie normalne, mode=0 wyświetlanie w negatywie.
List. 10. Procedura wyświetlania ciągów znaków
//********************************************************************************* //wyświetlenie łańcucha znaków - napisu //argumenty //x,y - współrzędne na ekranie //*txt - wskaźnik na początek bufora zwierającego łańcuch znaków ASCII do wyświetlenia //size - wysokość znaków 12, lub 16 pikseli //mode=1 znaki wyświetlane normalnie, mode=0 znaki wyświetlane w negatywie //********************************************************************************* void DispTxt(uint8_t x, uint8_t y, const uint8_t *txt, uint8_t size, uint8_t mode) { while (*txt != '\0') { if (x > (SSD1306_WIDTH - size / 2)) { x = 0; y += size; if (y > (SSD1306_HEIGHT - size)) { y = x = 0; DisplayCls(0x00); } } DisplayChar(x, y, *txt, size, mode); x += size / 2; txt ++; } }