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:
- Zwalniamy zatrzask
- Wysyłamy na wejście rejestru kolejno 3 bajty (adres wiersza jako pierwszy bajt)
- Zamykamy zatrzask
- Czekamy milisekundę
- Zwalniamy zatrzask
- Wysyłamy na wejście rejestru kolejno 3 bajty (adres kolejnego wiersza jako pierwszy bajt)
- Zamykamy zatrzask
- 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:
1 2 3 4 5 |
wiersz 1 = 11110111 = 247 wiersz 2 = 11101111 = 239 wiersz 3 = 11011111 = 223 wiersz 4 = 10111111 = 191 wiersz 5 = 01111111 = 127 |
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:
1 2 3 4 5 |
char adresy[5] = {247,239,223,191,127}; for (char wiersz=0;wiersz<5;wiersz++){ char adres = adresy[wiersz]; } |
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ę:
1 2 3 |
for (char wiersz=0;wiersz<5;wiersz++){ char adres = ~(1 << (3 + wiersz)); } |
Na początek przyglądnijmy się tylko temu, co mamy w nawiasie po prawej stronie:
1 |
1 << (3 + wiersz) |
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:
1 2 3 4 5 |
00000001 << 3 = 00001000 00000001 << 4 = 00010000 00000001 << 5 = 00100000 00000001 << 6 = 01000000 00000001 << 7 = 10000000 |
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:
1 2 3 4 5 |
~(00000001 << 3) = 11110111 ~(00000001 << 4) = 11101111 ~(00000001 << 5) = 11011111 ~(00000001 << 6) = 10111111 ~(00000001 << 7) = 01111111 |
Mamy wędrujące zero. Już prawie jesteśmy gotowi. Teraz zapakujemy sobie jeszcze kolejne bajty wyświetlacza do tablic i mamy gotowy kod.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
#define DANE 3 #define ZEGAR 2 #define ZATRZASK 4 #define ILOSC_WIERSZY 5 #define ILOSC_BAJTOW 2 char dane[ILOSC_WIERSZY][ILOSC_BAJTOW] = { // deklaracja tablicy dwuwymiarowej {234,202}, // przechowującej dane do przesłania na wyświetlacz {42,170}, {42,202}, {170,136}, {68,138}, }; void setup() { pinMode(DANE, OUTPUT); pinMode(ZEGAR, OUTPUT); pinMode(ZATRZASK, OUTPUT); } void loop() { for (char wiersz=0;wiersz<ILOSC_WIERSZY;wiersz++){ digitalWrite(ZATRZASK,LOW); shiftOut(DANE,ZEGAR,MSBFIRST,~(1<<3+wiersz)); shiftOut(DANE,ZEGAR,MSBFIRST,dane[wiersz][1]); shiftOut(DANE,ZEGAR,MSBFIRST,dane[wiersz][0]); digitalWrite(ZATRZASK,HIGH); delay(1); } } |
Prawda, że dużo ładniej??
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…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
// przypisanie pinów #define DANE 3 #define ZEGAR 2 #define ZATRZASK 4 // deklaracja rozmiaru matrycy #define ILOSC_WIERSZY 5 #define ILOSC_BAJTOW 2 #define ROZMIAR_EKRANU ILOSC_WIERSZY*ILOSC_BAJTOW // rozmiar pamięci ekranu (w bajtach) // definicja struktury danych pamięci obrazu matrycy struct t_Matryca { char dane[ILOSC_WIERSZY][ILOSC_BAJTOW]; }; t_Matryca m_Ekran; // deklaracja zmiennej m_Ekran przechowującej zawartość ekranu char m_Wiersz = 0; // numer aktualnie wyświetlanego wiersza (iterator) unsigned int cykl = 0; // licznik do pomiaru czasu pomiędzy zmianami obrazu char d_obrazek1[ROZMIAR_EKRANU] = {8,32,252,126,254,254,252,126,8,32}; // definicja obrazka1 char d_obrazek2[ROZMIAR_EKRANU] = {32,8,126,252,254,254,126,252,32,8}; // definicja obrazka2 // funkcja odświeżająca kolejny wiersz matrycy void m_RysujWiersz(){ digitalWrite(ZATRZASK,LOW); shiftOut(DANE,ZEGAR,MSBFIRST,~(1<<3+m_Wiersz)); shiftOut(DANE,ZEGAR,MSBFIRST,m_Ekran.dane[m_Wiersz][1]); shiftOut(DANE,ZEGAR,MSBFIRST,m_Ekran.dane[m_Wiersz][0]); digitalWrite(ZATRZASK,HIGH); m_Wiersz++; // przesunięcie do kolejnego wiersza if (m_Wiersz==ILOSC_WIERSZY) m_Wiersz=0; // sprawdzenie zakresu }; // ****************** funkcje obsługi ekranu // funkcja wyświetla na ekranie zawartość tablicy podanej jako parametr void mPoka(char zawartosc[ROZMIAR_EKRANU]){ memcpy(&m_Ekran,zawartosc,ROZMIAR_EKRANU); //kopiowanie zawartości zmiennej do pamięci ekranu } // funkcja odwraca wszystkie bity matrycy void mNegatyw(){ for(char wiersz=0;wiersz<ILOSC_WIERSZY;wiersz++){ for(char bajt=0;bajt<ILOSC_BAJTOW;bajt++) { m_Ekran.dane[wiersz][bajt]=~m_Ekran.dane[wiersz][bajt]; } } } // **************** kod główny void setup() { pinMode(DANE, OUTPUT); pinMode(ZEGAR, OUTPUT); pinMode(ZATRZASK, OUTPUT); }; void loop() { m_RysujWiersz(); // rysowanie kolejnego wiersza wywołujemy w każdym cyklu if (cykl==0) { mPoka(d_obrazek1); // w pierwszym cyklu wyświetlamy obrazek 1 } if (cykl==500) { mPoka(d_obrazek2); // w 500 cyklu (po około pół sekundy) wyświetlamy obrazek 2 } if (cykl==1000) { mPoka(d_obrazek1); // w 1000 cyklu (ok. sekundy) wyświetlamy obrazek 1 w negatywie mNegatyw(); } if (cykl==1500) { mPoka(d_obrazek2); // w 1500 cyklu (ok. 1,5 sekundy) wyświetlamy obrazek 2 w negatywie mNegatyw(); } cykl++; // inkrementujemy licznik if (cykl>2000) { // po około 2 sekundach zerujemy licznik i od nowa... cykl=0; } delay(1); } |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
#include <SPI.h> // przypisanie pinów #define ZATRZASK 4 // deklaracja rozmiaru matrycy #define ILOSC_WIERSZY 5 #define ILOSC_BAJTOW 2 #define ROZMIAR_EKRANU ILOSC_WIERSZY*ILOSC_BAJTOW // rozmiar pamięci ekranu (w bajtach) // definicja struktury danych pamięci obrazu matrycy struct t_Matryca { char dane[ILOSC_WIERSZY][ILOSC_BAJTOW]; }; t_Matryca m_Ekran; // deklaracja zmiennej m_Ekran przechowującej zawartość ekranu char m_Wiersz = 0; // numer aktualnie wyświetlanego wiersza (iterator) unsigned int cykl = 0; // licznik do pomiaru czasu pomiędzy zmianami obrazu char d_obrazek1[ROZMIAR_EKRANU] = {8,32,252,126,254,254,252,126,8,32}; // definicja obrazka1 char d_obrazek2[ROZMIAR_EKRANU] = {32,8,126,252,254,254,126,252,32,8}; // definicja obrazka2 // funkcja odświeżająca kolejny wiersz matrycy void m_RysujWiersz(){ digitalWrite(ZATRZASK,LOW); SPI.transfer(~(1<<3+m_Wiersz)); SPI.transfer(m_Ekran.dane[m_Wiersz][1]); SPI.transfer(m_Ekran.dane[m_Wiersz][0]); digitalWrite(ZATRZASK,HIGH); m_Wiersz++; // przesunięcie do kolejnego wiersza if (m_Wiersz==ILOSC_WIERSZY) m_Wiersz=0; // sprawdzenie zakresu }; // ****************** funkcje obsługi ekranu // funkcja wyświetla na ekranie zawartość tablicy podanej jako parametr void mPoka(char zawartosc[ROZMIAR_EKRANU]){ memcpy(&m_Ekran,zawartosc,ROZMIAR_EKRANU); //kopiowanie zawartości zmiennej do pamięci ekranu } // funkcja odwraca wszystkie bity matrycy void mNegatyw(){ for(char wiersz=0;wiersz<ILOSC_WIERSZY;wiersz++){ for(char bajt=0;bajt<ILOSC_BAJTOW;bajt++) { m_Ekran.dane[wiersz][bajt]=~m_Ekran.dane[wiersz][bajt]; } } } // **************** kod główny void setup() { SPI.begin(); pinMode(ZATRZASK, OUTPUT); }; void loop() { m_RysujWiersz(); // rysowanie kolejnego wiersza wywołujemy w każdym cyklu if (cykl==0) { mPoka(d_obrazek1); // w pierwszym cyklu wyświetlamy obrazek 1 } if (cykl==500) { mPoka(d_obrazek2); // w 500 cyklu (po około pół sekundy) wyświetlamy obrazek 2 } if (cykl==1000) { mPoka(d_obrazek1); // w 1000 cyklu (ok. sekundy) wyświetlamy obrazek 1 w negatywie mNegatyw(); } if (cykl==1500) { mPoka(d_obrazek2); // w 1500 cyklu (ok. 1,5 sekundy) wyświetlamy obrazek 2 w negatywie mNegatyw(); } cykl++; // inkrementujemy licznik if (cykl>2000) { // po około 2 sekundach zerujemy licznik i od nowa... cykl=0; } delay(1); } |
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ę.
Ej no… naprawdę ani jednego słowa komentarza???
A tak się starałem :(
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 :)
Super sprawa.Sam chciałbym zrobić sobie coś takiego.
Ale jestem bardziej zielony niż brokuły z biedronki.
Podziwiam.Efekt jest świetny.
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
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.