[049] Wyświetlacz z Nokii cz. 2
Kontynuując temat sterowania wyświetlaczem zgodnym z PCD8544 zajmijmy się teraz własnymi znakami definiowanymi lokalnie, dostępnymi inaczej niż za pomocą importowanych czcionek. Skorzystamy tutaj ze stricte graficznych możliwości biblioteki, acz na początek – skromnie. Najpierw zdefiniujemy sobie tablicę 8x8 pikseli, którą dla wygody opiszemy w formacie binarnym. Dla wygody – dlatego, że możemy od razu projektować rysunek, zwłaszcza jeśli nieco zmrużymy oczy. Zera to piksel wygaszony, jedynka – zapalony. W ten sposób zaprojektowałem duszka.
const byte duszek[] PROGMEM = {
B00111100,
B01000010,
B10100101,
B10000001,
B10011001,
B01011010,
B01000010,
B00111100,
};
nokia.drawBitmap(0, 0, duszek, 8, 8, 1); // Ładuj obrazek pod współrzędne 0, 0, o wymiarach 8x8, w kolorze czarnym.
nokia.display();
Użycie duszka jest proste: należy wywołać go instrukcją drawBitmap, podając kolejno: położenie lewego górnego rogu, nazwę, wymiary w pikselach i kolor atramentu). Od tej pory możemy straszyć.
Duszka możemy animować – na początek prościutko, przesuwając go tylko w poziomie.
for (byte x = 0; x < 84; x++) { // Pętla, w której będziemy rysować przesuwającego się duszka.
nokia.drawBitmap(x, 0, duszek, 8, 8, 1); // Ładuj obrazek pod współrzędne x, 0, o wymiarach 8x8, w kolorze czarnym.
nokia.display(); // Wyświetl załadowaną grafikę.
delay(50); // Zaczekaj 50 ms
nokia.drawBitmap(x, 0, duszek, 8, 8, 0); // Ładuj obrazek pod współrzędne x, 0, o wymiarach 8x8, w kolorze tła.
nokia.display(); // Wyświetl załadowaną grafikę.
}
Jeśli utworzymy pętlę, w której będziemy rysować duszka kolejno obok siebie co 50 milisekund, grafika będzie się przyjemnie przesuwać. Musimy jednak pamiętać, że tuż przed narysowaniem kolejnej należy wyczyścić istniejącą, a najłatwiej zrobić to rysując ją tam w kolorze tła, czyli pikselami wygaszonymi.
Jak zapewne wielu się domyśliło, powierzchnia 8x8 pikseli nie jest niczym ograniczona i teraz pokażę jak wstawiać dowolne grafiki o dowolnych rozmiarach. Jest to bardzo proste, ponieważ powstały narzędzia konwertujące formaty i oferujące kod gotowy do wklejenia w szkic. Zacznijmy od znalezienia jakiejś grafiki. Użyję swojego portretu z awatara, jednakże ponieważ ma on postać pionową, tak właśnie będę go chciał wyświetlić. Zaczniemy od skadrowania zdjęcia do proporcji ekranu, czyli 84x48. Należy to zrobić tak, by zminimalizować marginesy, ponieważ przy tak marnej rozdzielczości obrazka całość stanie się jeszcze mniej czytelna.
Można teraz przeprowadzić konwersję do jednobitowej bitmapy w Photoshopie, lecz nie każdy posiada ten program. Powstał niezależny konwerter online, dostępny pod adresem javl.github.io/image2cpp którym możemy zamienić dowolny obraz w bajty. Nie ma on jednak krzywych ani interpretatora barw, więc efekty mogą być gorsze wobec tych z Photoshopa. Aplikacja nic nie kosztuje, więc rzućmy na nią okiem.
Po wskazaniu źródła ustawiamy rozdzielczość zgodną z posiadanym wyświetlaczem i sposób konwersji. Mamy cztery algorytmy, ten o nazwie Atkinson wydaje mi się najlepszym. Ważne są jeszcze dwie opcje: musimy obrócić obraz i ustawić jego negatyw, by u nas wyświetlił się w pozytywie. Po wygenerowaniu kodu wklejamy go w miejsce poprzedniego, który odpowiadał za wygląd duszka.
const byte autoportret[] PROGMEM = { // Tablica z obrazkiem
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x01, 0xbb, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x21, 0x0a, 0x6f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x34, 0x82, 0x23, 0x7f,
0xf0, 0x00, 0x00, 0x00, 0x00, 0x04, 0xec, 0xd8, 0x10, 0xa0, 0x4f, 0xf0, 0x00, 0x00, 0x00, 0x1c,
0xc9, 0x03, 0x42, 0x44, 0x04, 0x9f, 0xf0, 0x00, 0x80, 0x00, 0x42, 0x80, 0x11, 0x00, 0x81, 0x11,
0x3f, 0xf0, 0x03, 0x60, 0x1b, 0x33, 0x26, 0x44, 0x20, 0x80, 0x79, 0x4f, 0xf0, 0x05, 0x40, 0xce,
0xc9, 0x08, 0x00, 0x06, 0x3b, 0xfe, 0x3f, 0xf0, 0x14, 0x1b, 0x20, 0x84, 0xc1, 0xc0, 0x0f, 0xff,
0xff, 0xff, 0xf0, 0x18, 0xf3, 0xe1, 0x22, 0xff, 0xfd, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x63, 0x84,
0x90, 0x29, 0x7f, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xf0, 0x44, 0x29, 0x10, 0x87, 0xfd, 0x9a, 0x6f,
0xfa, 0x7f, 0xff, 0xf0, 0x14, 0x42, 0x42, 0x93, 0xf6, 0x6b, 0xb3, 0xfa, 0x7f, 0xff, 0xf0, 0x59,
0x10, 0x80, 0x47, 0xbe, 0x4e, 0xdc, 0x3f, 0x8f, 0xff, 0xf0, 0x40, 0x00, 0x24, 0x37, 0xe9, 0x12,
0x77, 0xc5, 0xf3, 0xff, 0xf0, 0x30, 0x86, 0x01, 0x2f, 0x7c, 0x91, 0x8f, 0x50, 0x18, 0xff, 0xf0,
0x07, 0x80, 0x08, 0x4d, 0x76, 0x84, 0x6a, 0x7e, 0xe2, 0x3f, 0xf0, 0x11, 0x89, 0x82, 0x1f, 0xdd,
0x22, 0x1b, 0xbf, 0xff, 0xe7, 0xf0, 0x10, 0x24, 0x40, 0xb4, 0xfd, 0x08, 0xc7, 0xeb, 0x6d, 0xf3,
0xf0, 0x23, 0x00, 0x86, 0x37, 0xfc, 0x41, 0x10, 0xc4, 0x4b, 0x32, 0x70, 0x22, 0x48, 0x20, 0x77,
0xf6, 0x4c, 0x33, 0x64, 0xfe, 0xf4, 0xc0, 0x44, 0x01, 0x09, 0x36, 0xf9, 0x00, 0xc4, 0x63, 0xfb,
0xb1, 0xa0, 0x48, 0x10, 0x44, 0x6e, 0x79, 0xb2, 0x44, 0xcf, 0xdb, 0xf2, 0xb0, 0x10, 0x84, 0x06,
0xf9, 0x9e, 0x16, 0x98, 0xcd, 0xff, 0xf6, 0xd0, 0x65, 0x81, 0x0a, 0xf6, 0x22, 0x67, 0x21, 0x2e,
0xcf, 0xf1, 0x70, 0x41, 0x80, 0x49, 0xec, 0x20, 0x83, 0x64, 0x63, 0x77, 0xf5, 0xf0, 0x03, 0x92,
0x07, 0xf9, 0x00, 0x03, 0xa8, 0xeb, 0xed, 0xf7, 0xf0, 0x33, 0xc0, 0x86, 0xd0, 0x4c, 0x25, 0xa2,
0x27, 0xff, 0xff, 0xf0, 0x20, 0x01, 0x02, 0xc4, 0x39, 0x93, 0xc8, 0xa7, 0xf3, 0xff, 0xf0, 0x08,
0x00, 0x02, 0x64, 0x32, 0x53, 0x24, 0xa7, 0xf7, 0xff, 0xf0, 0x10, 0x82, 0x46, 0x78, 0xd8, 0x44,
0xd3, 0x2b, 0xf9, 0xff, 0xf0, 0x04, 0x00, 0x00, 0x12, 0x7d, 0x84, 0x71, 0xaf, 0xef, 0xff, 0xf0,
0x32, 0x01, 0x0c, 0x9a, 0x7c, 0x21, 0x24, 0xe5, 0xf7, 0xff, 0xf0, 0x28, 0xca, 0x02, 0x17, 0xb9,
0x18, 0xc7, 0x63, 0x1f, 0xfd, 0xf0, 0x0c, 0x00, 0x00, 0x44, 0xfe, 0x43, 0x39, 0xed, 0xff, 0x7f,
0xf0, 0x01, 0x27, 0x44, 0x0f, 0x6e, 0x84, 0x4b, 0xe1, 0xf3, 0xdb, 0xf0, 0x13, 0x08, 0x12, 0x23,
0xfc, 0x8c, 0xdf, 0xe7, 0xff, 0xc7, 0xf0, 0x04, 0x88, 0x00, 0x09, 0xff, 0x3b, 0xe7, 0x2d, 0x78,
0x97, 0xf0, 0x04, 0x60, 0x24, 0x41, 0xfb, 0x62, 0x08, 0x2a, 0x28, 0x9f, 0xf0, 0x02, 0x22, 0x19,
0x12, 0x58, 0x40, 0x01, 0x32, 0x23, 0x0f, 0xf0, 0x01, 0x8c, 0xd0, 0x10, 0xc0, 0x01, 0x04, 0x48,
0x45, 0x37, 0xf0, 0x00, 0x69, 0x0c, 0x0c, 0x20, 0x00, 0x22, 0x28, 0x17, 0x1f, 0xf0, 0x00, 0x01,
0x63, 0x03, 0x44, 0x8c, 0x88, 0xb8, 0x1f, 0x2f, 0xf0, 0x00, 0x00, 0x01, 0x30, 0x90, 0x61, 0x78,
0x61, 0x07, 0x2f, 0xf0, 0x00, 0x00, 0x00, 0x0e, 0x03, 0x00, 0x01, 0x24, 0x3f, 0x17, 0x40, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x8f, 0x4d, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0xc2, 0x1f, 0x2f, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x7e, 0x04, 0xe0
};
nokia.drawBitmap(0, 0, autoportret, 84, 48, 1);
nokia.display();
Aktualizujemy jeszcze parametry wyświetlonej bitmapy i po przesłaniu szkicu do Arduino możemy cieszyć się mało czytelnym – co oczywiste w przypadku marnej rozdzielczości i jednobitowej głębii – obrazkiem.
Prezentowany sposób wyświetlania grafik jest miły, ale zjada duże ilości pamięci. W przypadku figur geometrycznych biblioteka oferuje bajtooszczędne rozwiązania. Zacznijmy od bardzo ograniczonej figury, jaką jest piksel. Wprost przydaje się rzadko, ale dla porządku podaję przepis: należy określić współrzędne i kolor atramentu.
nokia.drawPixel(0, 0, 1);
I tu powinniśmy przejść do czegoś bardziej zaawansowanego, jednak ten smutny piksel może się przydać – choćby do stworzenia wygaszacza ekranu. Tak będzie wyglądała wersja podstawowa.
void loop() {
nokia.drawPixel(random(84), random(48), random(2));
nokia.display();
}
Używamy tutaj trzech funkcji losowych: do określania miejsca i koloru atramentu. Przypomnę: kolory mamy dwa: jedynka to czarny, zero – biały, czyli piksel znika, więc funkcja przyjmie jedną z tych wartości (dwójka nie wystąpi nigdy – taka jest specyfika tej funkcji).
Taki szum jest fajny, ale chyba wolimy symulator nieba, taki z czasów DOS-a jeszcze. Wystarczy trzeci argument zamienić na wyrażenie logiczne. W tym momencie atrament biały będzie się pojawiał raz na 100 razy tylko. Stosunek tych liczb określa ilość gwiazd, a jeśli chcielibyśmy nieba w pozytywie – przysuńmy drugą wartość do pierwszej.
void loop() {
nokia.drawPixel(random(84), random(48), random(100) > 0);
nokia.display();
}
Stawiając piksele możemy rysować funkcje. Bez upiększeń, czyli układu współrzędnych itp. elementów będą one ubogie, ale czytelne. Argumentem poziomu będzie liczba z pętli, a pionu – tangens owej, przesunięty na środek i wzmocniony cztery razy dla lepszego zobrazowania. Kto w szkole nie spał, odnajdzie znajome kształty.
for (byte x = 0; x < 84; x++) {
nokia.drawPixel(x, 24 + 4 * tan(x), 1);
nokia.display();
}
Bawić można się w nieskończoność, lecz zostało nam jeszcze trochę innych funkcji. Spójrzmy na możliwość rysowania linii. Sprawa jest prosta: podajemy współrzędne początkowe, końcowe i kolor atramentu. Możemy zatem rysować linie poziome, pionowe, skośne, jak chcemy. Jednak do rysowania pionów i poziomów lepiej użyć szybszej instrukcji, w której nie podajemy adresu końca linii, a jej długość.
nokia.drawLine(0, 0, 83, 47, 1); // Linia.
nokia.drawFastHLine(0, 0, 84, 1); // Linia pozioma.
nokia.drawFastVLine(0, 0, 48, 1); // Linia pionowa.
I znowu zrobimy sobie małe demo z moimi ulubionymi funkcjami losowymi. Tym razem przed pętlą wylosujemy przesunięcia x oraz y, by za każdym razem uzyskać inny efekt. W linii rysującej wstawię złożone wyrażenie, ale korzystające z prostych operacji matematycznych rysujących serię 41 linii, kolejno je pochylając. W zależności od warunków startowych dostajemy charakterystyczny wygaszacz rysujący figury geometryczne. Zastosowałem tutaj skok co dwie pozycje (z+2), a nie pojedynczy, bo tak rysowane kształty wyglądają ciekawiej.
void loop() {
byte x = random(21);
byte y = random(48);
for (byte z = 0; z < 84; z = z + 2) {
nokia.drawLine(z, x + z / 3, 84 - z, y + z / 4, 1);
nokia.display();
}
nokia.clearDisplay();
delay(100);
}
Skoro mamy linie, czas na prostokąty. Składnia jest już chyba intuicyjna: współrzędne lewego górnego rogu, długość, szerokość i atrament. Mamy także alternatywną wersję, z wypełnieniem wnętrza. Bywa to przydatne do czyszczenia obszarów ekranu.
nokia.drawRect(0, 0, 84, 48, 1); // Prostokąt.
nokia.fillRect(0, 0, 84, 48, 1); // Prostokąt wypełniony.
Oczywiście nie mógłbym sobie napisać króciutkiego demo. Tym razem prostokąty są rysowane jeden w drugim – aż do zapełnienia całego ekranu, a następnie rysowane na nowo atramentem mażącym. Pulsowanie jest interesujące i sprawia wrażenie wciągania patrzącego w ekran.
void loop() {
for (byte x = 0; x < 2; x++) {
for (byte z = 0; z < 25; z = z + 2) {
nokia.drawRect(42 - z, 24 - z, z * 2, z * 2, x);
nokia.display();
}
}
}
Kółka również możemy rysować, a składnia jest oczywista: współrzędne środka, promień i kolor. Przy okazji widać, że proporcje ekranu są nieco zaburzone i kółka wychodzą kurze.
nokia.drawCircle(42, 24, 20, 1); // Koło.
nokia.fillCircle(42, 24, 20, 1); // Koło wypełnione.
Tym razem wygaszacz będzie pokazywał wnętrze kufla z… wodą mineralną. Będziemy rysować sto losowo rozmieszczonych okręgów, po czym wszystkie znikną i rysowanie zacznie się od początku.
void loop() {
for (byte m = 0; m < 100; m++) { // Wygaszacz z okręgów.
byte x = random(84);
byte y = random(48);
byte z = random(5);
nokia.drawCircle(x, y, z, 1);
delay(20);
nokia.display();
}
nokia.clearDisplay();
}
Przydatną funkcją jest rysowanie prostokątów z zaokrąglonymi rogami. Można w ten sposób rysować przyciski, choć akurat tutaj to nie będzie mieć tak częstego zastosowania. Oczywiście funkcja ma swój odpowiednik z wypełnieniem wnętrza.
nokia.drawRoundRect(0, 0, 84, 48, 10, 1); // Prostokąt zaokrąglony.
nokia.fillRoundRect(0, 0, 84, 48, 10, 1); // Prostokąt zaokrąglony, wypełniony.
W końcu mamy funkcję rysującą trójkąty, zarówno w wersji samych boków jak i wypełnionej. Tutaj mamy aż trzy pary współrzędnych, dla każdego z wierzchołków i oczywiście kolor atramentu.
nokia.drawTriangle(0, 0, 83, 16, 24, 47, 1); // Trójkąt.
nokia.fillTriangle(0, 0, 83, 16, 24, 47, 1); // Trójkąt wypełniony.
A skoro nowa figura, to nowy wygaszacz. Myślę, że znowu pojawią się tutaj skojarzenia ze starym DOS-em.
void loop() {
for (byte n = 0; n < 20; n++) { // Wygaszacz z trójkątów
byte x1 = random(84);
byte y1 = random(48);
byte x2 = random(84);
byte y2 = random(48);
byte x3 = random(84);
byte y3 = random(48);
nokia.drawTriangle(x1, y1, x2, y2, x3, y3, 1);
nokia.display();
delay(50);
}
nokia.clearDisplay();
}
W praktyce będziemy potrzebowali mieszać wszelkie dostępne elementy i jest to jak najbardziej możliwe. Przytoczę pełen szkic tym razem, by uporządkować sobie całą do tej pory poznaną wiedzę.
#include <Adafruit_GFX.h> // Biblioteka obsługująca rysowanie grafik na wyświetlaczach.
#include <Adafruit_PCD8544.h> // Biblioteka obsługująca ten konkretny wyświetlacz.
Adafruit_PCD8544 nokia = Adafruit_PCD8544(3, 4, 5, 6, 7); // nazwa wyświetlacza, piny: SCLK, MOSI, D/C, CE, RST
#include <Fonts/TomThumb.h> // Przykładowy zestaw czcionek.
const byte duszek[] PROGMEM = {
B00111100, // Tablica z duszkiem
B01000010,
B10100101,
B10000001,
B10011001,
B01011010,
B01000010,
B00111100,
};
void setup() {
nokia.begin(); // Inicjuj wyświetlacz.
nokia.setContrast(60); // Ustaw kontrast.
nokia.clearDisplay(); // Wyczyść wyświetlacz.
nokia.print(F("Mozemy mieszackroje pisma.")); // Tekst.
nokia.setCursor(12, 0); // Ustawianie kursora.
nokia.print(F("' `")); // Ogonki do tekstu.
nokia.drawFastHLine(0, 18, 72, 1); // Linia pozioma.
nokia.drawBitmap(76, 14, duszek, 8, 8, 1); // Grafika.
nokia.setFont(&TomThumb); // Zmiana źródła czcionek.
nokia.setCursor(0, 28); // Ustawianie kursora.
nokia.print(F("Czesc! Jestem wyswie- tlaczem z Nokii, ktory wiekszosc pamieta z gry w weza ;)")); // Tekst.
nokia.display(); // Wyświetl załadowaną grafikę.
}
A więc od góry mamy: napis krojem bazowym, następnie dobicie ogonków, poziomą linię, grafikę „duszka”, zmianę kroju na miniaturowy, przeniesienie kursora i napis nowym krojem. I dopiero po przesłaniu wszystkiego wysyłamy komendę odświeżenia treści, a wtedy wszystko ukaże się naraz. W praktyce takie spolszczanie znaków jest mało profesjonalne i gdy będziemy ich potrzebować w większej ilości, należy przeprojektować dostępny krój.
Dobry projekt daje dostęp do regulacji kontrastu. Możemy to zrobić jakkolwiek i zwykle na tę potrzebę dopisuje się stosowne menu. Ja użyję potencjometru dostępnego na płytce testowej, który jest połączony z portem A1.
const byte potencjometr = A1;
void loop() {
nokia.setContrast(analogRead(potencjometr) / 4);
}
Zadeklaruję więc jego istnienie i już w głównej pętli będę go odczytywał, konwertował do wielkości ośmiobitowej i wysyłał odczytaną wartość do rejestru kontrastu wyświetlacza. Tylko niewielki przedział daje pozytywne wrażenia, a w skrajnych pozycjach potencjometru nie widać niczego. Trzeba na to uważać, bo możemy się zasugerować brakiem obrazu i szukać przyczyny niepowodzenia zupełnie gdzie indziej.
Podobnie możemy postąpić z podświetleniem. Trzeba tylko uważać na maksymalny prąd, jaki może zaoferować pin Arduino, lecz w tym przypadku, gdy diody pobierają 8 mA w sumie, każda wersja da sobie radę.
const byte potencjometr = A1;
const byte podswietlenie = 9;
void loop() {
analogWrite(podswietlenie, 255 - analogRead(potencjometr) / 4);
}
Aby móc sterować podświetleniem, będziemy musieli wypiąć wejście sterujące ledami z masy i skierować je na któryś z wolnych portów PWM. Porzućmy sterowanie kontrastem na rzecz sterowania oświetleniem. W tym celu zadeklarujemy sobie port dziewiąty i określimy go jako wyjściowy. W linii wcześniej sterującej kontrastem będziemy teraz wysyłać na ten port wartość zmierzoną potencjometrem, zredukowaną do ośmiu bitów. A konkretniej, odwrotność tej wartości, ponieważ tak jest intuicyjnie: obracanie potencjometrem w prawo zwiększa jasność, w lewo – zmniejsza ją aż do zupełnego odcięcia. Nastawy takie także powinny się znaleźć w menu, jeśli projektujemy urządzenie profesjonalnie.
Skoro mamy już wyświetlacz graficzny i wiedzę, w kolejnym artykule spróbujemy napisać jakąś prostą grę.