Arduino pisze wiersze

24 I 2022. Tutaj znajduje się źródło omawianego kodu.

Większość kursów na tym etapie przechodzi do prostych, lecz oderwanych od życia projektów. Jest to niewątpliwie jakaś droga poznania, jednak wydaje mi się, że możemy wybrać inną, ciekawszą. Zacznijmy zatem nie od rzeczy prostych, a powszechnych.

Co jest wspólną częścią większości projektów? Oczywiście oprócz samego mikroprocesora ze świtą. Interfejs użytkownika, a więc jakieś przyciski lub pokrętła oraz — w prostych urządzeniach — diody świecące, w bardziej złożonych — wyświetlacze alfanumeryczne.

Wyświetlacz, który może prezentować litery, cyfry i różne znaki, jest urządzeniem bardzo uniwersalnym, ponieważ potrafi zaprezentować informację w sposób wyczerpujący i ograniczony w zasadzie tylko wyobraźnią projektanta. Z pewnością każdy zauważył, że większość wyświetlaczy oferuje podobny, charakterystyczny krój liter. Muszą one zatem pochodzą z jednej rodziny, niezależnie od tego, czy były instalowane w faksie, kuchence czy tokarce.

Standard z jedną nóżką bardziej.

I tak właśnie jest: na skutek wielu nieplanowanych wydarzeń, powstał standard. Koślawy, niewygodny, zadziwiający w niektórych szczegółach, lecz standard, a z takimi się nie dyskutuje. Dawno, dawno temu, czyli na początku lat osiemdziesiątych, Hitachi opracował moduł wyświetlacza umożliwiającego prezentację do 80 liter, cyfr i znaków graficznych o skromnym rozmiarze 5x8 pikseli. 240 wzorów zaprogramowano na stałe, a użytkownik mógł sobie zaprojektować jeszcze 8 własnych.

Gdy przypatrzyć się szczegółom, widać wiele niedoskonałości: w zestawie znaków brakowało przydatnych semigrafik na rzecz nieużytecznych. Osiem własnych to niewiele, a adresy pozycji nie stanowiły obszaru ciągłego. Wyświetlacz podłączało się do mikroprocesora typową dla epoki magistralą ośmiobitową, która mogła także pracować w dziwnym trybie czterobitowym. Sterowanie było przedziwne i opanowanie protokołu, zwłaszcza w assemblerze, stanowiło wyzwanie. Na szczęście to już nas dziś nie dotyczy.

Tak wygląda tył wyświetlacza z epoki. Widzimy tutaj wspomniany mikrokontroler, a obok — dwa dodatkowe układy, niezbędne, gdy ilość znaków przekracza osiem.

Minęło 40 lat i HD44780, bo tak nazywa się ów standard, ma się dobrze, a nawet dużo lepiej niż w czasie, gdy powstawał. Nie będziemy się tutaj zajmować szczegółami dla nas nieistotnymi, lecz spróbujemy zrobić z tego standardu użytek konkretny. Ponieważ bohaterem odcinka jest wyświetlacz odzyskany ze starej kasy fiskalnej, postanowiłem najpierw rozkręcić go i wyczyścić.

Części jest niewiele. Kolejno: ekran lcd, w tym przypadku zawierający 32 pola znaków w dwóch rzędach, gumki przewodzące, element podświetlający — nie zawsze obecny i ramka spinającą całość. Blok podświetlacza zawiera wewnątrz pakiet ledów oraz rozpraszacz światła.
Z drugiej strony elementy wyglądają tak. Współczesne wyświetlacze mają zwykle te układy wyprodukowane już w technologii tańszej, w postaci struktur naniesionych bezpośrednio na płytkę. Jak widać, poza tymi układami nie ma tam już prawie nic.
Złóżmy całość z powrotem. Tak wygląda frontowa część płytki drukowanej.
Na nią należy przykleić — używając taśmy dwustronnej — blok podświetlacza.
Następnie układamy gumki przewodzące, które połączą złocone wyprowadzenia na płytce z tymi, które znajdują się na krawędzi ekranu.
W końcu układamy sam ekran, dbając by wszystko do siebie pasowało.
Na końcu osadzamy ramkę.
Ramka posiada wypusty, które powinny trafić w szczeliny. Gdy już wszystko będzie do siebie pasować, wypusty należy zagiąć w ten sposób, by ramka dociskała wyświetlacz do płytki w sposób pewny.

Na końcu trzeba jeszcze przylutować wyprowadzenia podświetlacza.

Pajęczynka przewodów.

W wyświetlaczach obowiązują dwa standardy wyprowadzeń: pionowy, dwurzędowy oraz poziomy. Zabawmy się tym drugim, bo będzie łatwiejszy w operowaniu.

Z zasady w te otworki wlutowuje się goldpiny, lecz w urządzeniach produkowanych masowo dla obniżenia kosztów lutowano tu przewody. I ja też tak proponuję zrobić.
Najpierw określmy sobie wszystkie wyprowadzenia.

Pierwsze (GND) to ujemny biegun zasilania, czyli tak zwana masa. Drugie (+5V albo VCC) — dodatni i jeśli nie określono inaczej, jest to pięć woltów.

Trzecie to dłuższa historia. Napięcie, które tutaj się dostarcza, określa kontrast liter. Zatem wstawiając potencjometr pomiędzy masę i napięcie zasilania, ze ślizgacza możemy czerpać dowolne napięcie, regulując tym wyrazistość obrazu. Nowe wyświetlacze pracują zwykle najlepiej, jeśli wystawimy tutaj zero woltów, czyli zewrzemy to wejście z wyprowadzeniem numer jeden, co też właśnie uczyniłem.

Czwarte, szóste i cztery ostatnie to wejścia sygnałowe, które podłączymy do mikrokontrolera. Trzeba jeszcze zewrzeć piąte wyprowadzenie do masy — bo tak, a pozostałe zostawić wolne.

Z lewej strony, nie po kolei, mamy jeszcze dwa wejścia. Nie ma standardu co do ich położenia i różnie z tym bywa. Tam dostarcza się napięcie dla diod podświetlających. Najczęściej łączy się je tak jak tutaj: 16 z masą, a 15 z pięcioma woltami, co widać po odwróceniu płytki.

Teraz już możemy przylutować przewody taśmy, przy czym polecam na ich końce założyć koszulki termokurczliwe. Przypominam, że nie ma żadnych przeciwwskazań, by wlutować szpilki goldpinów i użyć taśm dwustronnych albo płytek stykowych. W swoich rozwiązaniach po prostu promuję „wersję minimum”.

Ze względu na budowę Arduino Nano, co też za chwilę zobaczymy, drugi koniec przewodów, zaopatrzony już w gniazdka standardu goldpin, polecam wsunąć w tak przycięte obsadki.
Podsumujmy sobie raz jeszcze: z wyświetlacza wychodzi 8 przewodów: masa, zasilanie i 6 sygnałowych.

Masę podłączamy do nóżki opisanej jako GND, a zasilanie — do 5V. Pozostałe linie możemy podłączyć do dowolnych portów Arduino i w tym jest jego wielkość: każdy praktycznie port może pełnić cały szereg funkcji. Tutaj, dla porządku, linie te podłączyłem do kolejnych portów, począwszy od drugiego. Urządzenie nasze jest złożone, czas go ożywić.

Polecam jeszcze na czas testów przykręcić w ten sposób cztery śruby, co umożliwi wygodną obserwację ekranu.

Taśmę przewodów dobrze też przykleić dwustronną pianką do płytki wyświetlacza, dzięki czemu przypadkiem nie wyrwiemy jej.

Ala ma kota.

Odpalamy znane już nam środowisko Arduino IDE i podłączamy nasze Arduino Nano do portu USB. Nic się nie stanie, poza tym, że tło powinno się zaświecić, a na samym wyświetlaczu powinna się ujawnić struktura pikseli. Chyba już czas najwyższy zająć się programowaniem.

Na razie nasz projekt jest zupełnie pusty. Nadajmy mu nazwę: Pierwsze_urzadzenie i wpiszmy pierwszą linię.

Strasznie to wygląda, więc od razu wyjaśniam. Arduino może wykonywać pewne rozkazy, czy też użyjmy słowa: procedury. Będę je przedstawiał w kolejnych historiach, a niektóre poznamy za chwilę. Jednak ich ilość jest skończona i jeśli będziemy chcieli zająć się czymś bardziej złożonym, a tym jest tutaj wyświetlacz, będziemy musieli dołożyć dodatkowe, zwane bibliotekami.

#include to taki rozkaz, który każe programowi pobrać bibliotekę. Jej nazwa jest umieszczona w nawiasie trójkątnym. Bardzo ważną rzeczą jest przestrzeganie tych wszystkich reguł. Jeśli nawias jest trójkątny, to nie może być okrągły i tak dalej. Ktoś kiedyś wymyślił sobie akurat taki nawias i niestety tak już musi zostać.

<LiquidCrystal.h> to biblioteka, którą dostaliśmy od razu przy okazji ściągania całego środowiska, a służy do operowania wyświetlaczem. Czasem trzeba takie biblioteki ściągać osobno, ale nie tym razem. Czas na kolejną linię.

Program już będzie wiedział co znaczy LiquidCrystal, więc możemy teraz powiedzieć mu jak nasz wyświetlacz jest podłączony do Arduino. Bo wspominałem, że można go podłączyć w dowolny sposób. Rozkaz LiquidCrystal lcd(2, 3, 4, 5, 6, 7) umożliwia to.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

W nawiasie podaje się numery portów dla kolejnych wejść wyświetlacza, czyli RS, ENABLE i D4, D5, D6 oraz D7. Ponieważ podłączyliśmy je do portów od drugiego do siódmego, wpisujemy kolejno te liczby. Równie dobrze możemy odwrócić wtyczkę i wpisać liczby od tyłu, też będzie dobrze. Odejdźmy na chwilę od naszego małego problemu.

Każdy program na Arduino musi zawierać dwa bloki. Jeden zaczyna się od słów void setup(), a drugi — od void loop()

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup()
{
}

void loop()
{
}

O co w tym chodzi? Wszystko, co znajdzie się między nawiasami klamrowymi zaraz za void setup() wykona się po włączeniu zasilania. Jednorazowo, po czym program będzie wykonywał w kółko rozkazy, które będą się znajdować między nawiasami klamrowymi po słowach void loop() Prawdę powiedziawszy sensu to nie ma, ale tak musi być i koniec.

Tutaj, na samym początku, mamy jeszcze trzeci blok, który już wypełniliśmy dwoma wierszami. Ale to tak naprawdę nie są rozkazy dla procesora, a dla kompilatora, który nasze bazgroły przetłumaczy na język zrozumiały przez mikroprocesor. On pobierze stamtąd wiedzę na przykład jak podłączony jest wyświetlacz. Wpiszmy zatem coś, co już naprawdę będzie rozkazem.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup()
{
  lcd.begin(16, 2);
}

void loop()
{
}

Komenda lcd.begin(16, 2) jest krótka, ale robi dużo. Inicjuje wyświetlacz, przygotowując go do pracy. W rzeczywistości jest to sporo pracy, ale nie dla nas, bo my wpisujemy tylko tę linię. Przeanalizujmy ten wiersz dokładniej, bo jakby nie było, to nasz pierwszy rozkaz w życiu. lcd — znaczy, że rozkaz pochodzi z puli, którą dostarczyła biblioteka <LiquidCrystal.h> Po kropce jest dokładne określenie, o jaki rozkaz z tej puli chodzi. Wszystkie one są opisane w dokumentacji biblioteki. W nawiasach znajdują się tak zwane argumenty. Będą to liczby lub teksty, które mają jakieś znaczenie dla rozkazu. W tym wypadku rozkaz wymaga podania ilości kolumn i wierszy wyświetlacza. Zatem jeśli ktoś ma wyświetlacz o innych wymiarach, wpisuje sobie tu własne dane. Argumenty oddzielamy przecinkami, a rozkaz zawsze kończy się średnikiem. Odstępy, czyli spacje, mogą tu być, ale nie muszą. Czas na finał.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup()
{
  lcd.begin(16, 2);
  lcd.print("Towary Modne");
}

void loop()
{
}

Oto już ostatnia linia. Tym razem skorzystamy z rozkazu dotyczącego wyświetlacza o nazwie lcd.print("Towary Modne") Służy ona do pisania tekstów. Argumentem tym razem jest tekst, a teksty bierze się w cudzysłów. Reszta wygląda podobnie jak w rozkazie wyżej.

Koniec, program został napisany. Klikamy w drugą od lewej ikonkę i za chwilę program ląduje w Arduino.

A oto już nagroda.

Ala ma kota pro.

Podsumujmy nasze działania: Najpierw dołączyliśmy bibliotekę, ponieważ język Arduino nie posiada jej wśród rozkazów. Potem powiadomiliśmy kompilator w jaki sposób wyświetlacz został podłączony. Kolejno wywołaliśmy rozkaz inicjacji wyświetlacza, od razu przy okazji informując program, ile jest kolumn, a ile wierszy no i na końcu, gdy był już gotowy do współpracy, wysłaliśmy tekst do wyświetlenia.

Piękne, ale mało praktyczne. Takie niezmieniające się teksty to możemy sobie wydrukować na tekturce i wyjdzie na to samo. Zróbmy coś takiego, żeby komputer nie wstydził się, że go do tego zaangażowano. W tym celu dzisiaj będzie nam potrzebny jedynie drucik. Uczymy się dalej.

Oto trzy bliźniacze rozkazy zaczynające się od słów pinMode. Ważna uwaga: nie można zamienić sobie liter na duże albo małe, musi być tak.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup()
{
  lcd.begin(16, 2);
  lcd.print("Towary Modne");
  pinMode(8, INPUT_PULLUP);
  pinMode(9, INPUT_PULLUP);
  pinMode(10, INPUT_PULLUP);
}

void loop()
{
}

Jak wspominałem, linie danych, których w Arduino Nano są 22, mogą pełnić bardzo różne funkcje. Mogą być wyjściami, wejściami, mierzyć napięcia, wysyłać przebiegi, mogą też sterować różnymi magistralami. Rozkaz pinMode określa zadania tym liniom. W nawiasie podajemy numer portu, który mamy na myśli i czym on ma się zajmować. Porty mają numery od zera do 13 oraz od A0 do A7. Drugi argument tutaj znaczy, że port będzie wejściem podciągniętym do stanu wysokiego. Wejście oznacza, że ten port będzie nadsłuchiwany i czytany przez mikroprocesor, a wymuszony stan wysoki rozumieć należy w ten sposób, że jeśli nic tam nie podłączymy, odczyt zwróci informację, że jest tam stan wysoki, inaczej: 5 woltów. Żeby ta informacja była inna, należy do tego portu przyłączyć zero woltów, czyli zewrzeć go do „masy”. Kiedyś poznamy inne funkcje portów.

Zdefiniowaliśmy sobie zatem trzy porty, zaraz za naszą szóstką obsługującą wyświetlacz. Zróbmy z tego użytek, a także z obszaru, który stanowi część programu pracującą bez końca w pętli. Woda wiedzy będzie teraz trochę głębsza, lecz nie powinniśmy się utopić.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup()
{
  lcd.begin(16, 2);
  lcd.print("Towary Modne");
  pinMode(8, INPUT_PULLUP);
  pinMode(9, INPUT_PULLUP);
  pinMode(10, INPUT_PULLUP);
}

void loop()
{
  if (digitalRead(8) == LOW) {
    lcd.home();
    lcd.print("    Zajete!     ");
  }
}

if to jeden z podstawowych rozkazów, który zajmuje się podejmowaniem decyzji. W nawiasie okrągłym znajduje się warunek, a w klamrowym czynności, które należy spełnić, jeśli warunek zachodzi.

W tym wypadku będziemy sprawdzać, czy port ósmy ma niski stan, a sprawdzaniem portów zajmuje się funkcja digitalRead. Jak wspominałem, jeśli nic się tam nie podłączy, będzie tam stan wysoki, więc warunek nie zajdzie i nic się nie stanie. Gdy stan będzie niski, wykonają się rozkazy w nawiasie klamrowym.

Stan niski to LOW, stan wyskoki: HIGH. Sprawdzanie czy coś jest równe czemuś, realizuje się dwoma znakami równości.

W nawiasie klamrowym mamy dwa rozkazy. Drugi już znamy, to wyświetlanie napisów. Pierwszy, czyli lcd.home() rozkazuje ustawić kursor na początku ekranu. Dodajmy teraz bliźniaczy blok rozkazów, lecz zmieńmy warunek stanu niskiego na wysoki.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup()
{
  lcd.begin(16, 2);
  lcd.print("Towary Modne");
  pinMode(8, INPUT_PULLUP);
  pinMode(9, INPUT_PULLUP);
  pinMode(10, INPUT_PULLUP);
}

void loop()
{
  if (digitalRead(8) == LOW) {
    lcd.home();
    lcd.print("    Zajete!     ");
  }
  if (digitalRead(8) == HIGH) {
    lcd.home();
    lcd.print("    Wolne :)    ");
  }
}

Zatem poprzednio napisany blok if wykona się, gdy stan portu ósmego będzie niski, a blok napisany teraz — gdy wysoki. A co to znaczy w praktyce?

Wracamy do wspomnianego drucika. Jeden koniec założymy na szpicle opisanej jako GND, czyli na masie. W Arduino Nano są takie dwie i dobrze, bo jedna jest już zajęta przez wyświetlacz. Drugi koniec będziemy zakładać na szpilce portu ósmego. Wyślijmy nasz program do Arduino i dotykajmy teraz szpilki numer osiem przewodem podłączonym do masy.

No wreszcie coś się dzieje. Gdy wejście zwieramy do masy, zachodzi warunek pierwszego if. A gdy pozostawiamy wolnym — drugiego if. Na koniec rozbudujmy nasz program odrobinę.

Dodamy jeszcze dwa bloki, które będą analizować stany portów 9 i 10. Gdy zewrzemy je do masy, pojawią nam się kolejne napisy.

#include <LiquidCrystal.h>

LiquidCrystal lcd(2, 3, 4, 5, 6, 7);

void setup()
{
  lcd.begin(16, 2);
  lcd.print("Towary Modne");
  pinMode(8, INPUT_PULLUP);
  pinMode(9, INPUT_PULLUP);
  pinMode(10, INPUT_PULLUP);
}

void loop()
{
  if (digitalRead(8) == LOW) {
    lcd.home();
    lcd.print("    Zajete!     ");
  }
  if (digitalRead(9) == LOW) {
    lcd.home();
    lcd.print(" Jeszcze moment ");
  }
  if (digitalRead(10) == LOW) {
    lcd.home();
    lcd.print("Ciezka sprawa...");
  }
  if (digitalRead(8) == HIGH && digitalRead(9) == HIGH && digitalRead(10) == HIGH) {
    lcd.home();
    lcd.print("    Wolne :)    ");
  }
}

Skomplikował się ostatni blok. Musi on analizować już trzy porty naraz. Warunek jest spełniony, gdy wszystkie jednocześnie będą mieć poziom wysoki, czyli gdy zajdzie tu koniunkcja. Do tego celu używa się operatora złożonego z dwóch znaków & i łączy on warunki w ten sposób, że spełnione muszą być wszystkie naraz.

Zmieniłem także początek programu. Już nie wyświetla się tutaj napis „Towary Modne”, a „WC komputer”, bo tymże jest to urządzenie. Pojawiła się tutaj nowa instrukcja biblioteki obsługi wyświetlacza, jak łatwo się domyślić, ustawiająca kursor w drugim wierszu, dzięki czemu napisy będą się zmieniać tylko na górze. I na koniec: jak to działa?

Do wyprowadzeń 8, 9 i 10 należy podłączyć przełącznik.

Gdy nie będzie żadnego połączenia, pojawi się napis „Wolne”. Po zwarciu wyprowadzenia ósmego: „Zajęte”. Jeśli ktoś będzie nas popędzał, trzeba będzie przesunąć przełącznik w pozycję „Jeszcze moment”. A gdy rzecz się skomplikuje, dobrze będzie poinformować o tym zainteresowanych komunikatem „Ciężka sprawa...” ;)

Dodam tylko, że program ten, choć nie ma błędów, napisany jest bardzo niepoprawnie i niezgodnie ze sztuką optymalizacji. Ale o tym w następnych historiach.