No to zaczynamy kolejny odcinek cyklu “nie dla lalusiów”, gdzie spróbujemy dodać kilka nowych funkcji do naszej biblioteki, i pobawić się już trochę w wyświetlanie i pomiar czasu. Przypominam tym, którzy przez ostatnie dwa odcinki zapomnieli, że końcowym efektem ma być zegar :) Wybaczcie, że tak się szeroko rozpisałem o programowaniu, a mało o majsterkowaniu, ale pomyślałem sobie, że wielu z Was – podobnie jak ja – zagląda tutaj też po to, żeby się czegoś nauczyć, a nie tylko pooglądać zdjęcia fajnych gadżetów. A w programowaniu na razie czuję się mocniej, niż w majsterkowaniu, więc dzielę się tym co potrafię najlepiej. Może komuś przyda się to w jego projekcie i to nie tylko zegara ;)
Śmiało, podsumujmy co już potrafi nasza biblioteka:
- Wyświetla obraz automatycznie z zadanego obszaru pamięci (elegancko w przerwaniu i baaaardzo szybko, bo przez SPI)
- mamy funkcję która ładuje nam obrazek z tablicy do pamięci obrazu
- mamy funkcję która robi nam negatyw obrazka
To trochę mało, przecież chcielibyśmy wyświetlać 4 różne cyfry:
No i oczywiście kropeczka nad matrycą ma mrugać co sekunde ;)
Cała reprezentacja zawartości naszej matrycy w pamięci to 5 bajtów na lewy(L) wyświetlacz oraz 5 bajtów na prawy(P), zapisane kolejno:
1L , 1P , 2L ,2P , 3L , 3P , 4L , 4P , 5L ,5P
I jak być może pamiętacie, w ten właśnie sposób do tej pory zapisywaliśmy wyświetlane obrazki zajmujące całą matrycę:
1 |
char d_obrazek1[ROZMIAR_EKRANU] = {8,32,252,126,254,254,252,126,8,32}; |
Do wyświetlania 4 różnych cyfr raczej słabo się to sprawdzi. Na szybko policzyłem mi, że gdyby zapisać w ten sposób wszystkie możliwe pozycje dla zegara 24 godzinnego mielibyśmy 24*60 = 1440 obrazków do zaprojektowania :) W dodatku zajęlibyśmy 14KB pamięci. Na taką rozrzutność to sobie nie możemy pozwolić :) Musimy to zrobić inaczej.
Zdefiniujemy sobie każdą cyfrę z osobna i będziemy ją wrzucać do odpowiednich kolumn matrycy, przecież już wiemy jak przesuwać bity w lewo i w prawo. Mam także śliczny odręczny projekt cyferek mojego autorstwa:
No to kodujemy je pojedynczo jako 3 najmłodsze bity i pakujemy w strukturę tablicy:
1 2 3 4 5 6 7 8 9 10 11 12 |
char znaki[10][5] = { {7,5,5,5,7}, // cyfra 0 {1,1,1,1,1}, // 1 {7,1,7,4,7}, // 2 {7,1,7,1,7}, // 3 {5,5,7,1,1}, // 4 {7,4,7,1,7}, // 5 {7,4,7,5,7}, // 6 {7,1,1,1,1}, // 7 {7,5,7,5,7}, // 8 {7,5,7,1,7}, // 9 }; |
No i mamy tablicę znaków. A dla niezorientowanych w zawiłych meandrach zapisu binarnego, wrzucam obrazek poglądowy z konwersji cyfry 9. Ponoć czasem jeden obraz więcej wart więcej niż tysiąc słów:
Przypomniał mi się taki “cyfrowy” suchar z brodą:
Ile jest typów ludzi?
10 – tacy którzy rozumieją zapis binarny i tacy którzy nie.
Jeżeli nie zrozumiałeś żartu, to należysz do tych drugich :D
Cyfry w tablicy znaki[] celowo ułożyłem zaczynając od zera, bo przecież wszyscy pamiętamy, że tablice w języku C indeksowane są od zera, prawda? I korzystając właśnie z tej cechy, indeks pozycji naszej tablicy jednocześnie wskazuje nam jaka cyfra jest zawarta w tej pozycji. No i cudownie. Ale zanim przejdziemy dalej, znów pojawi się chwila nudnej teorii, która jednak może się Wam kiedyś przydać.
Mnie już dopadł problem o którym za chwilę opowiem, a i to w zasadzie w pierwszym poważnym projekcie na Arduino. A chodzi dokładnie o pamięć RAM, której niestety mamy w procesorze atmega328P tylko 2KB. A w atmega168, na którym właśnie robiłem ów pechowy projekt, mamy jej tylko 1KB. Ale może od początku.
Zrobiłem prototyp kontrolera MIDI na Arduno Uno(Atmega328P). Wszystko pięknie działało na płytce stykowej, dumny jak paw postanowiłem z prototypu przejść w fazę upojnego lutowania i przeniesienia oprogramowania właśnie na kość atmega168. Po kompilacji kod wynikowy miał niecałe 10Kb, więc nie spodziewałem się żadnych problemów, w końcu 168 ma jak sama nazwa wskazuje 16KB pamięci programu. A tu lipa panie. Soft kontrolera po przeniesieniu na inny procek działa kilka, czasem kilkanaście sekund i przestaje reagować. Uparcie. I to w przeróżnych odstępach czasu. Bez sensu. Pierwsze podejrzenie zimny lut. Przemierzyłem wszystko wszerz i wzdłuż, wymieniałem kości na inne przy czym, w przypływach rozpaczy coraz częściej czyniłem swój język prawdziwie plugawym, złorzecząc haniebnie na swe próżne starania. Ciągle nic. Ciągle ruletka. Raz działa nawet prawie minute. A potem znów ledwo rusza.
W końcu zacząłem wywalać fragmenty kodu i też bez zmian. Wczytałem szkic w wersji z przedwczoraj – HULA! Porównuję co się zmieniło, a tutaj niespodzianka. Praktycznie nic. Zmieniłem tylko w definicjach na początku kodu ilość programowalnych presetów z 2 na 4. Wynikiem tego, i tak już dosyć spora struktura danych przechowująca ustawienia dla presetów całego urządzenia, puchła na tyle, że gdy tylko procesor próbował zrobić cokolwiek mądrzejszego, to 1KB ramu w Atmega168 przestawało wystarczać. I pac. Totalna zwiecha. Myślałem, że będę musiał zrezygnować z presetów całkiem, ale poszukałem po internetach i okazało się, że jednak jest jeszcze nadzieja.
Domyślnie wartości wszystkich zmiennych globalnych, które zadeklarujemy w naszym projekcie, przechowywane są właśnie w pamięci RAM, a ta ze względu na rozmiar kiepsko nadaje się do przechowywania większych struktur. A szczególnie jeżeli nasz procesor ma jeszcze dodatkowo robić cokolwiek innego. Ale mamy jeszcze pamięć flash przeznaczoną na kod programu, której zwykle nie wykorzystujemy do końca. A przecież jest jej kilka a nawet kilkanaście razy więcej niż RAMu!
Aby przechowywać zawartość zmiennej w pamięci programu, musimy zadeklarować ją, używając dyrektywy PROGMEM.
1 |
typ_danych nazwa_zmiennej[] PROGMEM = {}; |
lub alternatywnie:
1 |
PROGMEM typ_danych nazwa_zmiennej[] = {}; |
Oczywiście jak zwykle, są pewne obostrzenia i limity.
- w pamięci PROGMEM możemy przechowywać tylko określone typy danych o specjalnych nazwach (o tym za moment)
- nie możemy w pamięci PROGMEM przechowywać liczb zmiennoprzecinkowych (pośrednio się da, ale to osobna bajka)
- do dostępu do danych w pamięci PROGMEM musimy używać specjalnych funkcji (mamy je w bibliotece “avr/pgmspace.h”)
Niewielki to koszt, za dodatkowe kilka KB pamięci na nasze drogocenne dane. A te “specjalne” typy danych, to w zasadzie nic innego jak 3 podstawowe typy liczb całkowitych znane nam już z C, tylko trochę inaczej się nazywają:
1 2 3 4 5 6 |
prog_char - odpowiednik char (1 bajt) -127 do 128 prog_uchar - odpowiednik unsigned char (1 bajt) 0 do 255 prog_int16_t - odpowiednik signed int (2 bajty) -32767 do 32768 prog_uint16_t - odpowiednik unsigned int (2 bajty) 0 to 65535 prog_int32_t - odpowiednik signed long (4 bajty) -2147483648 to * 2147483647. prog_uint32_t - odpowiendik unsigned long (4 bajty) 0 to 4294967295 |
Spokojnie wystarcza. A o tych specjalnych funkcjach dostępu można więcej poczytać na stronie: http://www.arduino.cc/en/Reference/PROGMEM.
Nic specjalnie trudnego ;) zaraz zobaczycie na przykładzie. Postaram się tak skomentować kod, aby wszystko było jasne ;).
To wrzućmy nasze struktury danych do pamięci programu, dodajmy kilka pożytecznych funkcji, które zbliżą nasz projekt o krok od bycia pełnoprawnym zegarem i zobaczmy co z tego wyjdzie:
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
#include <SPI.h> #include "avr/pgmspace.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) // definicje znaków prog_uchar znaki[10][ILOSC_WIERSZY] PROGMEM = { {7,5,5,5,7}, // 0 {1,1,1,1,1}, // 1 {7,1,7,4,7}, // 2 {7,1,7,1,7}, // 3 {5,5,7,1,1}, // 4 {7,4,7,1,7}, // 5 {7,4,7,5,7}, // 6 {7,1,1,1,1}, // 7 {7,5,7,5,7}, // 8 {7,5,7,1,7}, // 9 }; // i definicje obrazków prog_uchar o_test[ROZMIAR_EKRANU] PROGMEM = {238,110,72,132,76,68,72,36,78,196}; // napis TEST // ****************** funkcje obsługi ekranu // funkcja wyświetla na ekranie zawartość tablicy podanej jako parametr void mPoka(prog_uchar *zawartosc){ memcpy_P(&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]; } } } // funkcja czyści matrycę (wypełnia zerami) void mCzysc(){ for(char wiersz=0;wiersz<ILOSC_WIERSZY;wiersz++){ for(char bajt=0;bajt<ILOSC_BAJTOW;bajt++) { m_Ekran.dane[wiersz][bajt]=0; } } } //wyświetla znak z tablicy znaków na zadanej pozycji wyświetlacza (0-3) void mZnak(char znak, char pozycja){ // tablica do której zaczytamy pojedynczy znak z pamięci PROGMEM char t_znak[ILOSC_WIERSZY]; // obliczamy który bajt matrycy musimy modyfikowac dla zadanej pozycji znaku char bajt = pozycja / 2; // w zależności od tego czy pozycja jest parzysta czy nie // liczymy o ile przesuwamy znak w lewo (1 lub 5 bitów) char przesuw = (pozycja % 2)?1:5; // kopiujemy znak z pamieci PROGMEM do naszej zmiennej memcpy_P(t_znak,znaki[znak],ILOSC_WIERSZY); // i dla kolejnych wierszy for(char wiersz=0;wiersz<ILOSC_WIERSZY;wiersz++){ // najpier czyścimy odpowiednie 3 bity m_Ekran.dane[wiersz][bajt] &= ~(7<<przesuw); // i wpisujemy odpowiednio przesuniety wiersz znaku m_Ekran.dane[wiersz][bajt] |= t_znak[wiersz]<<przesuw; } } // funkcja w zależności od parametru (true/false) zapala lub gasi kropkę sekunową void mKropka(bool swieci){ if (swieci) { m_Ekran.dane[4][0]|=1; // wiersz 4, kolumna 0 - tam jest przylutowana } else { m_Ekran.dane[4][0]&=~1; } } // **************** kod główny void setup() { pinMode(ZATRZASK, OUTPUT); SPI.begin(); // initjalizacja biblioteki SPI // definicja przerwania zegarowego cli(); // zatrzymaj wykonywanie jakichkolwiek przerwań TCCR2A = 0;// zerujemy rejestr kontrolny A TCCR2B = 0;// zerujemy rejestr kontrolny B TCNT2 = 0;// zerujemy licznik OCR2A = 124;// = (16000000) / (500*256) - 1 TCCR2A |= (1 << WGM21); // ustawiamy timer2 w tryb obsługi przerwań (CTC) TCCR2B |= (1 << CS11); // ustawiam dzielnik na 256 TIMSK2 |= (1 << OCIE2A); // wzkazujemy że przerwanie ma być wywołane dla rejestro OCR2A sei();//zezwalamy na wykonywanie przerwań mPoka(o_test); // wyświetlamy napis test for (int i=0;i<24;i++) { // dla bajery na starcie przez chwilę migotamy napisem delay(i*i); // dla porażającego efektu - ze zmienną szybkością :) mNegatyw(); }; delay(1000); // czekamy sekunde mCzysc(); // czyścimy ekran przed przejściem do funkcji loop(); }; // funkcja obsługi przerwania timera 2 // w której odświeżamy wiersz matrycy ISR(TIMER2_COMPA_vect){ 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 } void loop() { mZnak(1,0); // wyświetl 1 na pozycji 0 mZnak(3,1); // wyświetl 3 na pozycji 1 mZnak(5,2); // wyświetl 5 na pozycji 2 mZnak(8,3); // wyświetl 8 na pozycji 3 mKropka(true); // zapal kropke delay(1000); // czekaj sekunde mKropka(false); // gaś kropke delay(1000); // czekaj sekunde } |
I co nam z tego wyszło? (ktoś narzekał na brak filmów)
Jak widać w kodzie pozwoliłem sobie umieścić czcionki i napis test w pamięci PROGMEM. A sam odczyt niewiele się zmienił. Używamy niemal tej samej funkcji memcpy, tyle, że z przyrostkiem _P, oznaczającym że odczytujemy dane z pamięci programu. Jedyne co się zmienia, to to, że drugi parametr, czyli źródło musi wskazywać na zmienną w pamięci PROGMEM. Reszta bez zmian.
Jeszcze jedna rzecz wymaga chyba wyjaśnienia początkującym:
1 |
char przesuw = (pozycja % 2) ? 1 : 5; |
taka konstrukcja nazywana jest “operatorem warunkowym“:
zmienna = warunek_0 ? wyrażenie_1 : wyrażenie_2;
i działa tak:
Jeżeli spełniony jest warunek_0, zmiennej przypisywana jest wartość wyrażenia_1, jeżeli nie jest – wyrażenie 2.
Czyli zapis:
1 |
char przesuw = (pozycja % 2) ? 1 : 5; |
jest równoważny zapisowi:
1 2 3 4 5 6 |
if (pozycja % 2) { przesuw = 1; } else { przesuw = 5; } |
ale zajmuje dużo mniej miejsca.
W naszym przypadku wyrażenie jest spełnione gdy reszta z dzielenia (% – operator modulo) przez dwa wynosi 1, czyli wartość w nawiasie jest prawdą dla liczb nieparzystych, a fałszem dla parzystych. Trochę to może dziwne na pierwszy rzut oka, ale działa to dlatego, że w języku C wartość zero interpretowana jest jako fałsz, każda inna jako prawda. Czyli zapis (pozycja % 2) jest tożsamy z ((pozycja % 2)== 0). Jak jeszcze coś jest niejasne – śmiało pytajcie w komentarzach.
Teraz jeszcze chwila zabawy z wyświetlaniem cyferek. Podmieniamy funkcję loop() i zrobimy pseudo-stoperek liczący do 100 sekund z dokładnością setnej części sekundy:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void loop() { for (int i=0;i<10000;i++){ // wyświetlacz lewy mZnak(i/1000,0); // wyświetl cyfrę rzędu tysięcy na pozycji 0 \___ niby sekundy mZnak((i%1000)/100,1); // wyświetl cyfrę rzędu setek na pozycji 1 / mKropka(((i%1000)/100)%2); // mrugamy ktopką co niby sekundę (świeci dla nieparzystych wartości na poz. 1) // wyświetlacz prawy mZnak((i%100)/10,2); // wyświetl cyfrę rzędu dziesiątek na pozycji 2 \_____ niby setne części sekund mZnak(i%10,3); // wyświetl cyfrę rzędu jedności na pozycji 3 / delay(10); // czekamy setną część sekundy } } |
No i efekt:
Teoretycznie dwie lewe cyfry wyświetlacza powinny wskazywać ilość upływających sekund. Ale niestety – tylko teoretycznie. Porównując pomiar z “lepszym” stoperem, już w przeciągu dwóch minut nasz stoper spóźnia się około sekundy. Specjalnie nagrałem całe 100 sekund, żeby niedowiarki sami mogli sobie zmierzyć. A dlaczego tak?
Pomiędzy wywołaniami funkcji delay(10) wykonujemy także operacje wyświetlania znaków. Dodatkowo każdy delay(10) mający podobno trwać 10 milisekund jest przynajmniej 5 razy przerywany naszym ukrytym przerwaniem zegarowym (przypominam – 500Hz), w którym odświeżana jest zawartość matrycy. Niby przez SPI – więc migiem, ale też nie dzieje się to zupełnie natychmiast.
Zatem – już wiadomo: używając funkcji delay() dokładnego zegara nie zrobimy. Pewnie gdyby zmniejszyć częstotliwość odświeżania i aktualizować cyfry tylko raz na sekundę, precyzja by nieco wzrosła, ale góra do poziomu wystarczającego na zgrubny pomiar w krótkich odstępach czasu. Minutnik do jajek dałoby radę :) Ale przecież nie o to nam chodzi.
Inną (kiepską) alternatywą może być użycie funkcji millis(), która zwraca nam (nawet dosyć dokładnie) ilość milisekund która upłynęła od startu naszego programu. Spróbujmy:
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 |
unsigned long ssek=0; // ile czasu minęło od startu pętli loop (w setnych częsciach sekund) unsigned long czas_start=0; //zapamiętuje czas startu pierwszej pętli loop (w tysiecznych czesciach sek.) void loop() { if (czas_start==0) { // w pierwszym cyklu zapamiętaj tylko kiedy wystartowała petla loop czas_start = millis(); } else // w kolejnych licz ile czasu upłynęło od staru { ssek = (millis() - czas_start)/10; // dzielimy przez 10 zeby miec setne czesci a nie tysieczne } ssek = ssek % 10000; // w prosty sposób ograniczamy zakres wyświetlanych liczb do 10000 // wyświetlając tylko resztę z dzielenia przez 10000 mZnak(ssek/1000,0); // wyświetl cyfrę rzędu tysięcy na pozycji 0 \___ sekundy mZnak((ssek%1000)/100,1); // wyświetl cyfrę rzędu setek na pozycji 1 / mKropka(((ssek%1000)/100)%2); // mrugamy ktopką co sekundę (świeci dla nieparzystych wartości na poz. 1) mZnak((ssek%100)/10,2); // wyświetl cyfrę rzędu dziesiątek na pozycji 2 \_____ setne części sekund mZnak(ssek%10,3); // wyświetl cyfrę rzędu jedności na pozycji 3 / delay(10); // czekamy setną część sekundy } |
Nie będę już wrzucał kolejnego, prawie identycznego filmiku, ale jest teraz duuuużo dokładniej. Prawie idealnie. Więc dlaczego napisałem, że jest to kiepska metoda do zrobienia zegara? Po pierwsze dlatego, że wartość zwracana przez funkcję millis() po jakimś czasie (kilku dniach, się nam przepełni) i wyzeruje. Można też próbować nie zliczać milisekund tylko same sekundy i na upartego wydłużylibyśmy ten czas do paru tygodni. Niby tak. Ale co jak odłączymy zasilanie? Albo wciśniemy reset?
Za każdym razem musielibyśmy ustawiać na nowo bieżący czas. Kompletnie bez sensu.
I tutaj czas na peryferia ;) Podłączymy do Arduino moduł zegara czasu rzeczywistego oparty na układzie DS1307. Sam moduł jest bardzo prosty w konstrukcji, i co ważne – posiada opcje potrzymania zegara baterią. Tutaj poradnik jak poskładać takowy samemu: http://tronixstuff.wordpress.com/2010/05/28/lets-make-an-arduino-real-time-clock-shield/
Pewnie sprawi to niejednemu frajde. Ja jednak – jako urodzony – leń poszedłem na łatwiznę i kupiłem za mniej niż 10 pln – gotowy moduł. W dodatku od razu z baterią.
Sam moduł komunikuje się z mikrokontrolerem za pomocą magistrali I2C, która w Arduino Uno znajduje się na pinach ANALOG4 i ANALOG5. Do jego obsługi są też gotowe biblioteki, co czyni jego wykorzystanie banalnie prostym.
Podłączymy DS1307, oraz parę innych rzeczy, już w następnym odcinku. Oj, będzie dużo kabelków. Przeniesiemy też projekt na docelową płytkę i będzie też trochę lutowania.
A tymczasem kończę część czwartą i życzę udanego weekendu.
Drugi film jest prywatny, może dlatego nie da się dodać.
Ciekawy poradnik :)
Kawał dobrej roboty :) Ja bym tą jedną diodę zamienił na dwie połączone pomiędzy tymi wyświetlaczami bo tak z jedną mrugającą nad to trochę głupio wygląda :D
O ile dobrze widzę dokumentację Arduino, można korzystać z binarnej formy zapisu, np: B1111011 – to ułatwia zapis (i późniejszą poprawę) tablic fontów lub obrazków. Dodatkowo nie trzeba przeliczać nic na dziesiętny, co jest sporą oszczędnością czasu.
Tak, też to odkryłem parę dni temu. A dokładniej ten zapis wygląda tak: 0b00001010. Całe życie się człowiek uczy ;)
Jest inny sposób na wykorzystanie millis() , wystarczy dać operator reszty z dzielenia % i jeśli jest on równy 0 to niech progrm doda sekundę, po czym wyzeruje tą wartosć :>