Pokażę tu jak uruchomić z Arduino czterocyfrowy wyświetlacz 7-segmentowy sterowany poprzez rejestr przesuwny 74HCT164 i cztery tranzystory BC546. Płytka z wyświetlaczem znów będzie odzyskana z elektrośmieci. Będzie to już kolejny temat, w którym uruchamiam wyświetlacz, gdyż omawiałem już wyświetlacz oparty o SM1628B i LCD oparty o BL55066, ale mimo wszystko uznałem, że bez klasycznego sposobu z rejestrem przesuwnym seria byłaby niekompletna.
Tym razem temat pociągnę troszkę dalej, aż do uruchomienia prymitywnego timera i/lub zegarka 24-godzinnego z możliwością też ustawienia mu godziny poprzez przyciski znajdujące się na płytce.
Oczywiście samą analizę płytki też umieszczę - odzyskana będzie ona z jakiegoś starego tunera sat.
No i standardowo - wszystko spróbuję pokazać krok po kroku, dodatkowo w dość uproszczony sposób, stroniąc od bardziej zaawansowanych konceptów, które Arduino sprytnie przed początkującymi ukrywa.
Pokrewne tematy z serii
W tym stylu napisałem już co najmniej kilka tematów, wszystkie przeznaczone raczej dla początkujących i czasem korzystające z Arduino:
Stary DVD Wiwa HD-128U - wnętrze, obsługa wyświetlacza i klawiatury SM1628B
Teardown drukarki HP Deskjet D1360 i przykład użycia jej części z Arduino
Wnętrze odtwarzacza DVD United 7071, wykorzystanie części z Arduino
Wnętrze HP DeskJet 990Cxi C6455A oraz wykorzystanie zasilacza i przetwornicy
Stary tuner sat Kathrein - wnętrze, wykorzystanie części, zabawy z PAL
Stary modem ADSL Acer Surf USB - wnętrze, wykorzystanie przetwornic ze środka
Drugie życie zasilaczy impulsowych ze starych telewizorów CRT
LCD ze złomu - BL55066 i Arduino, I2C, UART sterowanie z PC + Konkurs
GPS Mio N177 z elektrośmieci, ogniwo Li-ion i moduł ładowania za darmo
Wszystkie tematy z serii zawierają moją "radosną twórczość" i operują na elektronice odzyskanej ze złomu, czyli na tzw. "przydasiach".
Moduł odzyskany ze złomu, rozpoznanie wyprowadzeń
Tak prezentuje się bohater tematu jeszcze z obudową, którą zaraz zdejmiemy:
Po zdjęciu trzeba będzie przeanalizować płytkę. Czy czarny przewód to masa?
Na płytce jest jeden układ scalony - rejestr przesuwny 74HCT164.
Rejestr przesuwny pozwala nam za pomocą tylko dwóch pinów (zegar CP i dane DSA/DSB połączone razem) uzyskać całe 8 różnych wyjść (Q1-Q8). MR to tylko pin RESET, może być na stale na stanie wysokim. Podział na DSA i DSB tutaj nie ma dla nas znaczenia, nawet na tym PCB te sygnały są razem, ale ogólnie ten HCT robi na nich AND przy pobieraniu kolejnego bitu.
Wyświetlacz:
Ogólnie są dwa rodzaje takich wyświetlaczy - ze wspólną katodą lub anodą. Jak sama nazwa wskazuje, diody są połączone anodami (lub katodami), jak również segmentami. Pozwala to nam wydajnie nimi sterować. Przy 7 segmentach i 4 cyfrach potrzeba powiedzmy 7 + 4 sygnałów (4 sygnały kontrolują która cyfra się zapala, a 7 określa które jej fragmenty). Przez te 4 wspólne katody (bądź anody - zależy jaki wyświetlacz mamy) płynie nieco większy prąd, więc na płytce są też 4 tranzystory BC546.
Odbiornik IR:
Oprócz tego są wspomniane tranzystory BC546 (NPN). 4 po jednym na cyfrę wyświetlacza, oraz jeden pewnie od sygnału IR. Prześledzenie ścieżek dużo może powiedzieć.
Sprawdziłem połączenia multimetrem i naniosłem moje oznaczenia na zdjęcie:
- BTN_PWR - do tego sygnału podłączone są wszystkie trzy pzyciski. Dodatkowo posiada on rezystor pull up do VDD. Z drugiej strony przyciski są podłączone do rejestru przesuwnego, więc skanowanie przycisków odbywa się tak, że ustawiamy jeden bit rejestru przesuwnego (ten od przycisku np. PWR) na 0, a potem sprawdzamy czy na BTN_PWR jest 0. Jeśli tak, to PWR jest wciśnięty. Potem ustawiamy bit od przycisku CH+ na 0, znów sprawdzamy BTN_PWR, i tak dalej.
- MR - sygnał od rejestru przesuwnego
- CP - sygnał od rejestru przesuwnego
- DS - sygnał od rejestru przesuwnego
- GND - masa (występuje dwa razy)
- IR - odbiornik od pilota (on ma jeden BC546 na drodze)
- TR1 - tranzystor włączający pierwszą cyfrę wyświetlacza
- TR2 - tranzystor włączający drugą cyfrę wyświetlacza
- TR3 - tranzystor włączający trzecią cyfrę wyświetlacza
- TR4 - tranzystor włączający czwartą cyfrę wyświetlacza
- VDD - zasilanie (5V)
Do podłączenia do Arduino można użyć zwykłych kabelków od płytki stykowej:
Czyli czarny przewód to było VDD...
Uruchomienie rejestru przesuwnego
W środowisku Arduino mamy gotową funkcję do obsługi takiego rejestru. Nazywa się ona shiftOut.
https://www.arduino.cc/reference/en/language/functions/advanced-io/shiftout/
shiftOut(dataPin, clockPin, bitOrder, value)
Argumenty kolejno to:
- pin danych
- pin zegara
- kolejność bitów
- wartość ośmiobitowa do wysłania
W ten sposób możemy ustawić wszystkie piny naszego 74HCT164. Tak też zrobimy, ale najpierw ustawimy role pinów jako wyjścia oraz dodatkowo też włączymy 4 tranzystory, tak by wszystkie segmenty się świeciły. Oto pierwsze demko:
Kod: C / C++
Rezultat:
Chyba działa, świecą się losowe fragmenty.
Ktoś mógłby powiedzieć, że to przypadek - sprawdźmy zatem jak reaguje wyświetlacz na zmianę wartości wysyłanych przez rejestr.
Kod: C / C++
Co 0.25s zmieniamy segment. Jaki da to nam rezultat?
Wygląda na to, że działa.
Mapowanie bitów na segmenty
Teraz musimy określić, który bit to który segment. Przypominam budowę i oznaczenia segmentów wyświetlacza 7-segmentowego:
Źródło grafiki: wikipedia, licencja: CC0
Z tych oznaczeń będę korzystać w tym temacie.
Więc, na próbę, ustawmy jeden bit i zobaczmy co się wyświetli:
Kod: C / C++
Niespodzianka:
Z tego co widzę jednak to bit zgaszony = jeden zapalony segment.
To samo potwierdza zresztą podłączenie wyświetlacza.
Więc zrobimy na odwrót:
Kod: C / C++
to daje nam segment B:
Zapiszmy to. Użyjemy do tego preprocesora, dyrektywa #define. Tak zdefiniujemy bity dla każdego segmentu.
Kod: C / C++
Kolejno przesuwamy zero bitowe, kompilujemy, wgrywamy i sprawdzamy rezultat.
Po chwili mamy:
Kod: C / C++
Czyli wiemy już który bit mapuje się na który segment.
Łączenie segmentów w cyfry
Teraz pora połączyć te segmenty w cyfry. Patrzymy na obrazek, co tworzy cyfrę, powiedzmy, 1? Segmenty B i C...
Możemy to zapisać w kodzie:
Kod: C / C++
Użyty tu jest operator AND, iloczyn logiczny, bo chcemy by jedynki zostały tylko tam gdzie są w przypadku wszystkich segmentów. Gdyby było na odwrót (a segmenty to były zapalone pojedyncze bity) to byśmy użyli OR - |.
W ten sposób tworzymy wszystkie cyfry:
Kod: C / C++
Mapowanie wartości zmiennej na flagę bitową cyfry
Teraz trzeba zrobić coś, by można było łatwo dostawać się do kodów cyfr - najlepiej poprzez tablicę. Indeks tablicy będzie odpowiadać danej cyfrze, tj. na miejscu np. czwartym w tablicy będzie kod czwórki.
Kod: C / C++
Jeśli chcemy wspierać znaki typu A, B, itp. to można je też dopisać do tej tablicy.
Można przetestować to animacją:
Kod: C / C++
Rezultat:
Tak, wszystkie pozycje wyświetlają tę samą cyfrę - to dlatego, że jeszcze nie operujemy wcale tranzystorami. Nie ma multipleksingu. Zaraz to naprawimy.
Multipleksing poprzez tranzystory - w głównej pętli
(nie powinno się tego robić w głównej pętli, ale mam wrażenie, że lepiej małymi kroczkami pokazywać jak to działa, w następnych akapitach przeniosę to do przerwania/timera).
Teraz pora dostać się do konkretnych miejsc na wyświetlaczu. Są 4 miejsca, na 4 cyfry. To która cyfra jest zapalona określają tranzystory. Jest jednak pewien haczyk. Mamy tylko 7 wyjść do segmentów, więc... jak zmienimy je dla kolejnej cyfry to poprzednia się zgubi.
Tutaj wchodzi w grę multipleksing.
Po prostu tak szybko przełączamy tranzystory i cyfry, że każda wyświetla się tak krótko, że daje nam to złudzenie wyświetlania się wszystkich na raz.
Będzie się nam wydawać, że wyświetlacz wyświetla napis 1 2 3 4 - ale to nie będzie prawda. On wyświetlać będzie najpierw bardzo krótko 1, potem 2, potem 3, potem 4 i od nowa 1, itd.
Kod: C / C++
Można by to zrobić znacznie lepiej, tym bardziej jak pozwolimy pisać sobie bezpośrednio do portu - wtedy można ustawić tam kilka bitów na raz, bez digitalWrite, ale nie chcę aż tak komplikować.
Pora sprawdzić nasz kod:
(na żywo aż tak źle nie jest)
Niestety jest pewien problem... wygląda na to, że w cyfrach palą się niepożądane segmenty.
Bierze się to stąd, że zanim wykona się shiftOut to zdążymy zmienić tranzystor, ale segmenty zostają wcześniejsze. To tzw. "ghosting", prześwitujące segmenty jednej cyfry na drugą cyfrę.
Aby to wyeliminować, na czas operacji na rejestrze przesuwnym, całkiem wyłączamy tranzystory.
Kod: C / C++
O niebo lepiej! W kodzie mogłoby być też lepiej, ale to zaraz....
(jeszcze kolejność tranzystorów trzeba zmienić, ale to już mniej istotne)
Multipleksing i biblioteka TimerInterrupt
Zastanawiałem się czy to wprowadzać czy nie, ale myślę, że nie ma sensu się bać bardziej zaawansowanych konceptów. Wprowadzimy teraz timer - to nam zwolni funkcję loop.
Nie chciałem jednak też pisać bezpośrednio po rejestrach, więc zdecydowałem się na bibliotekę, która ułatwia korzystanie z Timerów i przerwań - TimerInterrupt.
https://github.com/khoih-prog/TimerInterrupt
Biblioteka ta sprowadza użycie przerwania/timera do:
Kod: C / C++
W ten sposób przeniosłem obsługę multipleksingu do przerwania. W jednym przerwaniu obsługuję jedną cyfrę i tylko zwiększam licznik cyfr, który po 4 się zapętla. Między przerwaniami po prostu świecą wybrane segmenty jednej cyfry.
Oto mój przerobiony kod:
Kod: C / C++
Częstotliwość w Hz jest do dobrania.
Dodatkowo, tylko w celu wizualizacji, ustawiłem niską częstotliwość i nagrałem film:
Obsługa przycisków
Pora obsłużyć przyciski. Są one pomiędzy wyjściami rejestru przesuwnego (Q0, Q1 i Q2) a sygnałem BTN_PWR.
Musimy na czas testu zgasić całkiem wyświetlacz, a potem kolejno gasić bity Q0, Q1 i Q2 i testować, czy przycisk jest włączony, tj. czy na pinie BTN_PWR jest stan niski.
Skąd wiem, że akurat stan niski, a nie np. wysoki?
Chociażby stąd, że linia BTN_PWR ma rezystor podciągający do masy - tzw. pull up. Ustawia on "domyślny" stan na pinie wejścia, czyli 1. Z kolei 0 oznacza wciśnięty przycisk.
Zacznijmy od małego programu testowego - bez przerwań, tylko demonstracja jak to działa:
Kod: C / C++
Wciskam POWER - czy działa?
Działa, to jeszcze dopiszemy Q1 i Q2...
Kod: C / C++
Jak widać dla każdego przycisku musimy zrobić osobno shiftOut. Inaczej byśmy nie wiedzieli, który przycisk jest wciskany, gdyż do odczytu stanów przycisków jest jeden wspólny pin.
Działa. Można jeszcze dodać negację - by 1 oznaczało wciśnięcie.
Potem można to przerzucić do przerwania.
Obsługa przycisków - przeniesiona do przerwania
Nie robimy kolejnego przerwania - dodajemy tylko "magiczną" piątą cyfrę, w której przerwanie po prostu zbada wszystkie klawisze. Oto zmodyfikowane przerwanie:
Kod: C / C++
Przyznam, że bardzo mnie kusi by zamiast osobnych zmiennych typu integer zrobić jedną zmienną int g_buttons i tam ustawiać poszczególne bity oznaczające stan przycisku, ale staram się pisać jasno i zrozumiale, więc aż tak nie chcę komplikować.
Oczywiście tu będzie problem drgania styków. Drganie styków wynika z niedoskonałości elementów. Jak wciskamy przycisk (bądź go puszczamy) to nie przechodzi on płynnie z 1 na 0, tylko tworzy często kilka pozornych przejść, więc jedno go wciśnięcie z naszej strony może wielokrotnie wykonywać daną akcję. Przydałby jakiś debouncing. Nie będę się w to zagłębiać, ale może jakiś prosty się wymyśli....
Wyłuskanie cyfr z liczb
Przydałby się jakiś dostęp do ustawienia wyświetlanych cyfr, bo na razie wyświetlamy zawsze "0,1,2,3".
Służyć ku temu będzie druga tablica. Za pomocą wartości tablicy zaindeksujemy tablicę mask bitowych znaków.
Kod: C / C++
Oraz modyfikacja odświeżania wyświetlacza:
Kod: C / C++
Teraz jeszcze funkcja zapisująca do tej tablicy:
Kod: C / C++
Funkcja po prostu bierze dwa razy liczbę i wyciąga z niej osobno ilość jedności i dziesiątek za pomocą dzielenia i operacji modulo (reszta z dzielenia). Funkcja zakłada, że argumenty będą nie większe niż 99, w przeciwnym razie do np. g_digits[0] wpisana zostanie wartość większa niż 9 na skutek czego potem wyjedziemy poza zakres tablicy g_digitCodes.
I wywołanie w głównej pętli:
Kod: C / C++
Rezultat:
Odliczanie sekund
Kolejną rzeczą którą można zrobić jest odliczanie sekund. Zastanawiałem się, czy nie użyć jakiegoś gotowego rozwiązania, ale w sumie można by to oprzeć o nasz timer. Jak mamy daną częstotliwość timera, to ile jego wywołań przekłada się na jedną sekundę? No, właśnie tyle co częstotliwość...
Kod: C / C++
Na Arduino mamy jeszcze funkcję milis(), ale najlepszym sposobem byłoby zastosowanie modułu RTC, chociażby taki z interfejsem I2C i bateryjką CR3032 do podtrzymania czasu po odcięciu zasilania.
Minuty, godziny..
Analogicznie można dodać minuty i godziny. Po prostu czekamy na przepełnienie się licznika sekund (zakładamy, że on zawiera wartość od 0 do 60) i wtedy zwiększamy licznik minut, itd.
Kod: C / C++
Rezultat:
Ustawianie czasu
Zasadniczo mamy już minutnik bądź zegar, w zależności od tego czy wyświetlamy minuty i sekundy, czy godziny i minuty. Problem polega na tym, że nie mamy jak ustawić czasu, więc zasadniczo trzeba by podłączyć zasilanie o północy, by wyświetlany czas miał sens.
Pora to naprawić.
Modyfikacja czasu będzie odbywać się poprzez przyciski. W ten czas migać będzie ekran.
Role klawiszy będą następujące:
- power - włącza/wyłącza tryb edycji, przechodzi do następnego pola
- up/down - przewija wartość edytowanej godziny/minuty.
Zaczniemy jednak od animacji migania. Ta zmienna określi, czy jest ona włączona:
Kod: C / C++
Gdzie najprościej jest się teraz wpiąć, by pomigać całością?
Kod: C / C++
Wyświetlamy cyfrę albo jeśli albo tryb migania jest wyłączony, albo licznik wywołań jest mniejszy niż jego połowa - czyli migamy co pół sekundy.
Tryb edycji
Jak na razie obsługa samego przycisku power. Tu trzeba wprowadzić jakieś prymitywne obsłużenie drgania styków - chociażby poprzez delay. W moim przypadku to starcza. Jest to prymitywne programowe rozwiązanie, są też rozwiązania sprzętowe - np. kondensator na linii z przyciskiem.
Kod: C / C++
Od teraz przycisk POWER przełącza tryb migania.
Kolejnym krokiem jest przełączanie trybów i zmiana cyfr. Tym razem wszystko na raz:
Kod: C / C++
Kod można by znacznie rozbudować - chociażby wykrywać wciśnięcie dłuższe niż bodajże pół sekundy i wtedy szybciej zwiększać liczniki (np. co 10, by nie trzeba było pompować 30 razy by przejść z 20:10 do 20:40), można by również wydzielić obsługę przycisku do funkcji a nawet struktury czy tam klasy. Mimo to, ustawianie czasu działa.
Tryb edycji też należałoby wydzielić do enum.
No i w zależności od tego czy chcecie timer czy zegar należałoby zmienić wyświetlanie na godziny/minuty bądź minuty/sekundy.
Odbiornik IR, pilot i biblioteka IRRemote
Arduino może też odbierać sygnały z pilota - ale to opisywałem już w poprzednim temacie z serii:
Wnętrze odtwarzacza DVD United 7071, wykorzystanie części z Arduino
Podobny panel, ale w SMD
Jeszcze mała ciekawostka - bardzo podobny układ, ale w dużej mierze zbudowany z elementów montowanych powierzchniowo. Warto sobie porównać rozmiary i zobaczyć, jak postępuje miniaturyzacja.
Tutaj dokładnie to jest 74HC164D, też oczywiście są cztery tranzystory (po jednym na cyfrę).
Sterowanie wyświetlaczem bez mikrokontrolera
Na koniec chciałbym jeszcze umieścić małą ciekawostkę, która jest luźno związana z tematem. Młodsi użytkownicy mogą wcale tego nie kojarzyć, ale wyświetlacze 7-segmentowe mogą też być sterowane w ogóle bez MCU. Oto przykład takiego urządzenia (to jest jakiś miernik impulsów czy tam prędkości):
Układy scalone na pokładzie: NE555, HCF4093, HCF4017, MM74C925N.
Podsumowanie
Kolejny wyświetlacz uruchomiony - tym razem na rejestrze przesuwnym.
Pierwotny plan na ten temat był nieco inny, ale myślę, że i tak wyszło dość ciekawie. Starałem się nie komplikować zbyt mocno kodu, chociaż w kilku miejscach kusiło mnie by nieco inaczej to zorganizować. Tak samo z przyzwyczajenia wolałbym pisać po rejestrach, a nie ciągle digitalWrite, ale nie chcę odstraszać początkujących.
Podobnie kusiło mnie by nie powtarzać tego kodu obsługi każdej z czterej cyfr, albo chociaż zrobić jakieś #define...
Wielu rzeczy tu nie omówiłem, chociażby nie poruszyłem tematu tego z jaką częstotliwością powinien się odświeżać wyświetlacz by ludzkie oko miało złudzenie "stałości" zapalenia segmentów, jak również udało mi się "schować" timery/przerwania za biblioteką.
Po szczegóły odnośnie działania biblioteki TimerInterrupt zapraszam do dokumentacji i na jej repozytorium:
https://github.com/khoih-prog/TimerInterrupt
https://www.arduino.cc/reference/en/libraries/timerinterrupt/
O użytym shiftOut również możecie poczytać tutaj:
https://www.arduino.cc/reference/en/language/functions/advanced-io/shiftout
Fajne? Ranking DIY Pomogłem? Kup mi kawę.