Zegar cyfrowy LED z bajerami – Część II: sterowanie matrycą przez SPI

Zegar cyfrowy LED z bajerami – Część II: sterowanie matrycą przez SPI

Uradowany faktem, że mój wpis wskoczył na główną czuję się zobowiązany zakasać rękawy i zabrać się do pisania biblioteki sterownika. W zasadzie mam ją już gotową i działającą, ale przecież nic nie stoi na przeszkodzie żebym napisał go jeszcze raz od nowa, może poprawimy to i owo?

W pierwszej części, sterowaliśmy wyświetlaczem dosyć topornie:

  1. Zwalniamy zatrzask
  2. Wysyłamy na wejście rejestru kolejno 3 bajty (adres wiersza jako pierwszy bajt)
  3. Zamykamy zatrzask
  4. Czekamy milisekundę
  5. Zwalniamy zatrzask
  6. Wysyłamy na wejście rejestru kolejno 3 bajty (adres kolejnego wiersza jako pierwszy bajt)
  7. Zamykamy zatrzask
  8. Czekamy milisekundę

I tak w sumie pięć razy aż zapalimy kolejno wszystkie wiersze, po czym powtarzamy całą procedurę od początku.

Na początek pozbędziemy się uciążliwego podawania adresu wiersza. Przypomnę jak wyglądają kolejne adresy wierszy które musimy wysłać do rejestru W:

Jest pewnie kilka,  jak nie kilkanaście sposobów, jak zamienić (zakodować) numer wiersza na wartość którą musimy wpisać do rejestru.  Podam dwa najbardziej dla mnie oczywiste. Pierwszy to tablica wartości:

Sposób szybki to i prosty zarazem :) Myślę, że kod nie wymaga żadnych wyjaśnień. Może prócz tego, że wiersze numeruję sobie od 0, a nie od 1. Radzę się do tego przyzwyczaić :) Tak adresowane są tablice w C i wielu innych językach. Pierwszym elementem jest zawsze zero i już. Trzeba przywyknąć – z czasem wchodzi w krew.

Dobra, jest super, w kolejnych iteracjach pętli wyciągamy sobie adresy z tablicy. Jest jednak pewne “ale”. W przypadku dużych matryc duża tablica przechowująca adresy powoduje nam zajęcie drogocennego – w naszym małym Arduino – RAMu. A mamy go tylko 2KB! Opcją jest przechowywanie tablicy w pamięci programu stosując dyrektywę PROGMEM, ale kod stałby się wtedy nieco mniej czytelny. Do tego też jeszcze dojdziemy jak będziemy robić tablicę czcionek, ale… dobra do celu. Chodzi o to, że możemy sobie sami zgrabnie wyliczyć wartość bajtu, nie korzystając z żadnych tablic. Zróbmy sobie wędrujące zero. Najpierw pokażę jak, a potem wytłumaczę:

Na początek przyglądnijmy się tylko temu, co mamy w nawiasie po prawej stronie:

Symbol  << to operator binarny “przesuń w lewo”. Analogicznie istnieje też “prawy” operator >>. Powoduje on (jak sama nazwa wskazuje) przesunięcie bajtu (z lewej strony operatora) o zadaną ilość bitów (z prawej strony operatora) w zadanym kierunku. W naszym przypadku w lewo,  czyli:

Jak widać na powyższych przykładach, wskakujące z prawej strony brakujące bity uzupełniane są zerami.

Dobra, wiemy już jak zrobić wędrującą jedynkę.  To teraz zanegujemy nasz bajt operatorem ~. Tylda to operator binarny NOT. Powoduje zmianę stanu wszystkich bitów na przeciwny:

Mamy wędrujące zero. Już prawie jesteśmy gotowi. Teraz zapakujemy sobie jeszcze kolejne bajty wyświetlacza do tablic i mamy gotowy kod.

Prawda, że dużo ładniej??

napis JUPI!

Dla swojej, a teraz również Waszej wygody, napisałem na szybko KALKULATOR, do sprytnego wyliczania wartości tablic.

Może i ładniej wygląda nasz kod, ale nadal wykonujemy wszystkie operacje odświeżania matrycy w głównej pętli loop(), gdzie docelowo chcielibyśmy wykonywać inne super fajne rzeczy. Przykładowo podmieniać obrazki, albo w ostateczności mierzyć czas. Zatem na dobry początek przeniesiemy sobie całą pętlę odświeżania do zewnętrznej funkcji, oraz zadeklarujemy zmienną, która będzie naszą “pamięcią obrazu”. Wszystkie zapisane do tej zmiennej dane, będą automatycznie wyświetlane na matrycy. Trzask prask…

No i zaczyna to powoli wyglądać.

Może kilka słów wyjaśnienia do powyższego kodu.  Pierwsza rzecz, to funkcja memcpy. Kopiuje ona zawartość obszaru pamięci.

memcpy(wskaźnik celu, wskaźnik źródła, ilość bajtów do skopiowania);

I tu pojawia się tajemnicze słowo wskaźnik. Coś mi mówi, że gdybym zaczął tłumaczyć czym są wskaźniki i jak można ich sprytnie używać, to skończyłbym pisać ten post w okolicach świąt wielkiej nocy, więc postaram się ograniczyć do niezbędnego minimum.

Wskaźnik to pojedyncza liczba, wskazująca położenie zmiennej/funkcji/struktury w fizycznej pamięci naszego Arduino. Pewnie zauważyliście przy pierwszym parametrze funkcji memcpy jakim jest m_Ekran pojawił się znaczek & (ampersand). Wskazuje on kompilatorowi, że pierwszym parametrem nie jest zawartość naszej struktury danych, tylko jej fizyczny adres (wskaźnik). Dlaczego zatem przy drugim parametrze nie wskazujemy kompilatorowi że nie chcemy zawartości tablicy tylko jej adres? No właśnie… dlatego, że zmienna t została przekazana jako parametr funkcji, a że jest tablicą, to została przekazana do funkcji właśnie jako wskaźnik. Więcej mogę wytłumaczyć w komentarzach, albo zapytajcie szwagra. W ostateczności googla. To naprawdę temat rzeka, a jest już dobrze po północy. Dla naszego przykładu wystarczy, że uwierzycie mi na słowo, że tablica przekazana do funkcji jest od razu wskaźnikiem i nie potrzebuje ampersanda. Trzeci parametr już chyba nie wymaga komentarza.

Drugą rzeczą która dla początkujących może wydać się niezrozumiała, może być definicja struktury na początku pliku. Skleciłem ją sobie dla wygody. Pozwala mi to odwoływać się do zmiennej m_Ekran.dane[wiersz][bajt], adresując sobie zgrabnie wiersze i bajty.

Kolejna rzecz – dlaczego w funkcji rysującej wiersz nie zrobiłem pętli do wyświetlania kolejnych bajtów, tylko podałem je “wprost”? Nie z wrodzonego lenistwa. Funkcja ta ma być wywoływana jak najczęściej, aby uniknąć efektu widocznego migotania matrycy.

Prosty eksperyment. Zmieniam wartość opóźnienia cyklu w ostatnim wierszu na 5 milisekund. Niby niewiele. Ale migotanie matrycy już widać i jest nawet całkiem nieznośne. Czyli jeżeli ładowanie rejestrów będzie trwało dłużej niż 5ms, to już lipa. A wyobraźmy sobie matrycę RGB gdzie dla każdego pojedynczego bitu matrycy podajemy 3 bajty danych? Dlatego właśnie nie użyłem pętli. W wypadku podania dwóch bajtów dużo szybciej będzie podać je wprost. Ale co w przypadku gdy nasza matryca byłaby faktycznie większa? Jak jeszcze możemy przyspieszyć nasz kod?

W Arduino jak i w wielu innych kontrolerach mamy interfejs SPI. To nic innego jak bardzo szybkie wyjście szeregowe, które może nam zastąpić dosyć powolną funkcję shiftOut(). W Arduino UNO jakie aktualnie katuję, działa ono domyślnie na pinach 10,11,12,13. Znających język Szekspira odsyłam pod adres: http://arduino.cc/en/Reference/SPI

My wykorzystamy tylko dwa piny, a to dlatego, że będziemy “gadać” tylko w jedną stronę i tylko z jednym urządzeniem. Przypinamy Arduno do matrycy w następujący sposób:

13 – do wejście zegarowego rejestru

11 – do wejścia danych rejestru

Zatrzask śmiało może pozostać na razie na  pinie 4. Do obsługi interfejsu SPI wykorzystamy dołączoną do Arduino IDE bibliotekę SPI.h

Jak widać, kod zmienił się minimalnie, ale wierzcie mi na słowo – różnica ogromna. Można to w prosty sposób sprawdzić, wysyłając w funkcji  m_RysujWiersz kilkanaście pustych bajtów zanim wyślemy nasze trzy istotne. Przy kilkudziesięciu “sztucznych  kolumnach” funkcja shiftOut zaczyna powodować widoczne migotanie matrycy, a SPI radzi sobie absolutnie bez mrugnięcia ;)

Na dzisiaj to tyle. Jutro może uda mi się napisać, jak całkowicie przenieść funkcję rysującą wiersz poza pętlę loop, aby wykonywała się samoczynnie w tle w zadanym odstępie czasu.

Tyle na dziś i zapraszam do komentowania. Nikt nie ma żadnych pytań? Nie wierzę.

Ocena: 4.6/5 (głosów: 43)

Podobne posty

5 komentarzy do “Zegar cyfrowy LED z bajerami – Część II: sterowanie matrycą przez SPI

  • Nie mam matrycy więc się nie odzywam ;)

    Dobrze że posłuchałeś rady Piotra i zacząłeś dodawać komentarze w programie. Tak ode mnie to brakuje mi tylko jakiegoś filmiku :)

    Odpowiedz
  • Super sprawa.Sam chciałbym zrobić sobie coś takiego.
    Ale jestem bardziej zielony niż brokuły z biedronki.

    Podziwiam.Efekt jest świetny.

    Odpowiedz
  • Bardzo fajny artykuł ;) sam ostatnio kupiłem dużo elektroniki i zamierzam zacząć coś w niej grzebać.
    Zdziwiła mnie jedna rzecz – obudowanie w strukturę pojedynczej tablicy. Aż się prosi o typedef :P

    Odpowiedz
    • Znów kolega ma rację. Zastosowałem strukturę trochę rozpędem z marszu, bo oryginalną bibliotekę mam osadzoną całą w jednej klasie i tam skaczę sobie po strukturze. A nie chciałem tutaj jeszcze wrzucać elementów programowania obiektowego, bo tak będzie łatwiej dla początkujących. Dzięki za czujność.

      UPDATE:
      Już chciałem poprawiać, ale doczytałem właśnie TUTAJ, że jest jeszcze jedna rzecz która przemawia za użyciem struktury dla typów tablicowych. Przy definicji typu tablicowego przez typedef, maskujemy to że dany_typ przechowuje tablicę. Po przekazaniu jej przez parametr do funkcji użytkownicy nie widzą, że mają do czynienia z tablicą, bo funkcja sizeof() zwraca rozmiar wskaźnika. Dodatkowo struktura umożliwia przekazanie pojedynczego elementu swojej tablicy do funkcji bezpośrednio poprzez wartość:
      funkcja(struktura.tablica[index]);
      a bez struktury funkcja(tablica[index]), przekaże nam tylko wskaźnik.
      Czyli w sumie wygodniej.

      Odpowiedz

Odpowiedz

anuluj

Masz uwagi?