[083] Układamy wiedzę - cz. 9
Tablica – rzecz niezwykle przydatna, a z doświadczenia wiem, że tu zaczynają się schody. Spróbujmy tak: tablica jest zbiorem elementów, których jest za dużo, żeby wybierać je serią instrukcji if, czy nawet switch case. Jak pisałem w poprzednim artykule, ta ostatnia funkcja nadaje się do wyliczanek kilkunastu, góra kilkudziesięciu warunków. Z reguły jednak tak duże wyliczanki dotyczą sytuacji, gdy warunki tworzą działania niepodobne do siebie. Na przykład gdy jedno, to „zapal światło”, a drugie – „zakręć silniczkiem”.
W świecie bajtów częściej zdarzają się całe serie warunków, które robią takie same rzeczy, tylko o różnych wagach, wartościach czy istocie mogącej być opisaną liczbą. Zróbmy sobie teraz taki przykład, będący modułem całkiem użytecznego dla odmiany podzespołu – wyświetlacza siedmiosegmentowego.
To klasyka wśród wyświetlaczy, dająca wszystkie kształty cyfr i kilka liter. Problem polega na tym, że takie gołe, czyli pozbawione elektroniki wyświetlacze, należy zasilać segment po segmencie i już w naszej kwestii leży to, które segmenty mają się świecić, a które nie. Innymi słowy, każda cyfra czy znak wymaga wysłania na port wyświetlacza bajtu z poustawianymi bitami dla świecących segmentów i wyzerowanymi dla zgaszonych (albo na odwrót – w zależności czy mamy do czynienia ze wspólną anodą, czy katodą). Bity trzeba ustawić, analizując wcześniej połączenia, co jest trochę mozolne. I teraz mamy do wyboru: albo stworzyć zestaw dziesięciu case, które będą wysyłać konkretny układ bitów, albo skorzystać z tablicy.
Przyznaję: nie chciało mi się pisać przykładu z kolejnym analizowaniem zmiennej licznik i wysyłaniem odpowiedniej paczki, ale po ostatniej dawce wiedzy chyba każdy już wie, jak by to wyglądało (źle). Więc nie zobaczymy takiej postaci programu i dobrze, bo lepiej do tego oczu nie przyzwyczajać. W takich przypadkach aż się prosi o użycie tablicy.
#include // Dołącz bibliotekę sterującą układem MCP23008
Adafruit_MCP23008 ekspander; // Nadaj układowi nazwę "ekspander"
const byte tablica[10] = {
B00111111, // 0, Tablica zawierająca wzorce znaków wyświetlacza siedmiosegmentowego.
B00000110, // 1, połączenia kolejno: P-G-F-E-D-C-B-A
B01011011, // 2
B01001111, // 3
B01100110, // 4
B01101101, // 5
B01111101, // 6
B00000111, // 7
B01111111, // 8
B01101111, // 9
};
byte licznik; // Licznik do wyświetlenia na wyświetlaczu siedmiosegmentowym.
void setup() {
ekspander.begin(36); // Inicjuj bibliotekę sterującą układem MCP23008 pod adresem 0x4
for (byte x = 0; x < 8; x++) { // Zadeklaruj wszystkie porty jako wyjścia.
ekspander.pinMode(x, OUTPUT);
}
}
void loop() {
for (licznik = 0; licznik < 10; licznik++) { // Wyświetlaj kolejno cyfry od 0 do 9
for (byte x = 0; x < 8; x++) // Ustawiaj każdy z ośmiu bitów - segmentów wyświetlacza.
ekspander.digitalWrite(x, bitRead(tablica[licznik], x)); // Pobierz stan owych segmentów z tablicy.
delay(250); // Zaczekaj ćwierć sekundy.
}
}
A wygląda ona tak: najpierw określamy naturę elementów w niej zawartych, a więc: będą tam liczby typu byte, do tego umieszczone w pamięci stałej (const), której zawsze jest więcej niż RAM-u. Potem mamy nazwę tablicy, którą nazwałem tablica i nieobowiązkowy jej rozmiar [10] (ale nawiasy muszą być zawsze). Potem już można sobie poukładać elementy rozdzielone przecinkiem. Zwykło układać się je tak, by czytelność służyła szybkiej modyfikacji treści, jeśli zaszłaby potrzeba. Dlatego użyłem trybu binarnego, zaczynającego się od litery B, bo tak od razu widać segmenty wyświetlacza i jeśli pomylimy się, szybko odnajdziemy konkretny element. Dla programu nie ma to znaczenia, można użyć dowolnego formatu jak i układu.
Program będzie wyświetlał w kółko kolejne cyfry od zera do dziewiątki. Nie pytajcie po co, to przecież program szkoleniowy. Toteż deklarujemy zmienną licznik, która będzie się zwiększać w podanym zakresie co ćwierć sekundy. Wewnątrz tej pętli znajdziemy kolejną x, która będzie się tym razem wykonywać osiem razy, bo wraz z kropką mamy tutaj osiem segmentów i wobec każdego należy podjąć działania: zaświecić go albo zgasić.
Sedno tablicowego myślenia tkwi w tej zawiłej nieco linii.
ekspander.digitalWrite(x, bitRead(tablica[licznik], x));
Jak zwykle ćwiczymy z pomocą Płytki Edukacyjnej TME, więc odwołujemy się do kolejnych segmentów, tutaj konkretnie podłączonych do kolejnych linii ekspandera, czyli układu portu równoległego, wyłuskując kolejny bit z bajtu zawierającego kształt znaku. x to numer bitu – do tego za chwilę wrócę, a licznik – bajt, z którego będziemy wybierać bity. W jaki sposób pobieramy dane z tablicy? Każdy element ma swój niepowtarzalny adres, począwszy od zera. Więc jeśli licznik wyniesie na przykład pięć, sięgniemy do piątej pozycji – za pomocą konstrukcji: nazwa tablicy, nawias kwadratowy, adres, a tam jest zapisany kształt piątki. I to cała tajemnica.
Tablica może zawierać różne elementy, nie tylko cyfry, ale też znaki. Cyfry mogą oznaczać wszystko co chcemy: adresy plików na dysku, frazy do wypowiedzenia przez syntezator mowy, adresy przekaźników itd. Oprócz upraszczania i skracania programów tablice pozwalają na szybką modyfikację danych. Spójrzmy na jeszcze jeden przykład.
#include // Biblioteka obsługująca magistralę I2C
#include // Biblioteka obsługująca wyświetlacze 44780
#include // Dodatek obsługujący wyświetlacze podłączone do ekspandera I2C
hd44780_I2Cexp lcd(0x20, I2Cexp_MCP23008, 7, 6, 5, 4, 3, 2, 1, HIGH); // Konfiguracja połączeń wyświetlacza LCD
const byte woltomierz = A1; // Port, który będzie mierzyć napięcie.
byte znak[80] = {
0b01110, 0b10001, 0b10001, 0b00000, 0b10001, 0b10001, 0b01110, 0b00000, // 0, Tablica kształtów cyfr.
0b00000, 0b00001, 0b00001, 0b00000, 0b00001, 0b00001, 0b00000, 0b00000, // 1
0b01110, 0b00001, 0b00001, 0b01110, 0b10000, 0b10000, 0b01110, 0b00000, // 2
0b01110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b01110, 0b00000, // 3
0b00000, 0b10001, 0b10001, 0b01110, 0b00001, 0b00001, 0b00000, 0b00000, // 4
0b01110, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b01110, 0b00000, // 5
0b01110, 0b10000, 0b10000, 0b01110, 0b10001, 0b10001, 0b01110, 0b00000, // 6
0b01110, 0b00001, 0b00001, 0b00000, 0b00001, 0b00001, 0b00000, 0b00000, // 7
0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110, 0b00000, // 8
0b01110, 0b10001, 0b10001, 0b01110, 0b00001, 0b00001, 0b01110, 0b00000, // 9
};
void setup() {
lcd.begin(16, 2); // Inicjalizacja wyświetlacza LCD
}
void loop() {
lcd.createChar(0, znak + 8 * (analogRead(woltomierz) / 103)); // Pobierz z tablicy osiem bajtów kształtu znaku zerowego, przesuniętych o pozycję potencjometru.
lcd.setCursor(0, 0); // Ustaw kursor na początku wyświetlacza.
lcd.write(0); // Wyświetl grafikę spod adresu zerowego.
}
Jak niegdyś pisałem, najpopularniejszy na świecie wyświetlacz alfanumeryczny standardu 44780 zawiera osiem komórek na własne kształty znaków. Niewiele tego, a samo pole dla projektu nowych ikonek jest malutkie: 5x8 pikseli. Osiem komórek nie pozwala nawet zaprogramować pełnego zestawu cyfr. Jednak można to obejść, jeśli zgodzimy się na wykorzystanie dla takich celów tylko maksymalnie ośmiu pól wyświetlacza. Wówczas będziemy mogli na bieżąco programować grafiki, a nawet je łączyć – otrzymując także całkiem spore cyfry i obrazki, byle tylko w sumie siedziały na ośmiu pozycjach. Mogą też na większej ilości, ale wówczas muszą się powtarzać.
Ten przykład pochodzi z bardziej rozbudowanej wersji dywagacji na temat takich wyświetlaczy, ale skrócę go, byśmy mogli się skupić na tablicach. Zaprogramowałem sobie zbiór dziecięciu kształtów cyfr, udających siedmiosegmentowe, gdyż oryginalnie wyświetlacze pokazują cyfry kształtu bardziej ludzkiego. Traktujmy to jako ciekawostkę albo po prostu ćwiczenie. Jak teraz zarządzać… policzmy: 10 cyfr, a każda potrzebuje osiem bajtów – czyli osiemdziesięcioma bajtów w sumie? Można oczywiście stworzyć dziesięć if-ów, a w każdym osiem wysyłek, bajt po bajcie do wyświetlacza, ale chyba każdy sobie wyobraża, że byłoby to nieczytelne i rozwlekłe.
#include // Biblioteka obsługująca magistralę I2C
#include // Biblioteka obsługująca wyświetlacze 44780
#include // Dodatek obsługujący wyświetlacze podłączone do ekspandera I2C
hd44780_I2Cexp lcd(0x20, I2Cexp_MCP23008, 7, 6, 5, 4, 3, 2, 1, HIGH); // Konfiguracja połączeń wyświetlacza LCD
const byte woltomierz = A1; // Port, który będzie mierzyć napięcie.
byte znak[80] = {
0b01110, 0b10001, 0b10001, 0b00000, 0b10001, 0b10001, 0b01110, 0b00000, // 0, Tablica kształtów cyfr.
0b00000, 0b00001, 0b00001, 0b00000, 0b00001, 0b00001, 0b00000, 0b00000, // 1
0b01110, 0b00001, 0b00001, 0b01110, 0b10000, 0b10000, 0b01110, 0b00000, // 2
0b01110, 0b00001, 0b00001, 0b01110, 0b00001, 0b00001, 0b01110, 0b00000, // 3
0b00000, 0b10001, 0b10001, 0b01110, 0b00001, 0b00001, 0b00000, 0b00000, // 4
0b01110, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b01110, 0b00000, // 5
0b01110, 0b10000, 0b10000, 0b01110, 0b10001, 0b10001, 0b01110, 0b00000, // 6
0b01110, 0b00001, 0b00001, 0b00000, 0b00001, 0b00001, 0b00000, 0b00000, // 7
0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110, 0b00000, // 8
0b01110, 0b10001, 0b10001, 0b01110, 0b00001, 0b00001, 0b01110, 0b00000, // 9
};
void setup() {
lcd.begin(16, 2); // Inicjalizacja wyświetlacza LCD
}
void loop() {
lcd.createChar(0, znak + 8 * (analogRead(woltomierz) / 103)); // Pobierz z tablicy osiem bajtów kształtu znaku zerowego, przesuniętych o pozycję potencjometru.
lcd.setCursor(0, 0); // Ustaw kursor na początku wyświetlacza.
lcd.write(0); // Wyświetl grafikę spod adresu zerowego.
}
Istotą tablicy jest także to, że o warunkach myśli się później. W tablicy wszystkie odpowiedzi dla warunków układa się jeden za drugim, a potem dopiero układa się algorytm na czerpanie konkretnych danych. Jak widać, znowu dla własnej czytelności dane ułożyłem równo w wiersze – to kolejne cyfry i kolumny, po osiem – to kolejne linie pikseli każdej cyferki. Jak poprzednio, zapisałem je w postaci binarnej, by było od razu widać kształty znaków. Jedynka to piksel zapalony, zero – wygaszony. Cyfra jeden, składająca się z dwóch kreseczek tylko, ma tu najmniej jedynek, a ósemka – najwięcej. Pytanie: dlaczego mamy tylko pięć bitów, a nie osiem, skoro bajt to przecież osiem bitów? Bo znak ma pięć pikseli szerokości. Pozostałe trzy najstarsze bity nie mają znaczenia, więc nie trzeba ich umieszczać.
I podobnie jak poprzednio, będziemy wydobywać kształt konkretnego znaku i przesyłać go do wyświetlacza. Różnica jest taka, że każdy znak składa się z ośmiu bajtów, więc licznik trzeba za każdym razem zwiększać o osiem. No i nie mamy tutaj nawiasu kwadratowego, bo tego wymaga konstrukcja tej konkretnej procedury związanej z biblioteką obsługi wyświetlacza. I na tym etapie zakończmy tablicowe rozważania, ale coś nam po nich pozostało i o czym napiszę w kolejnym artykule.