Kursy • Poradniki • Inspirujące DIY • Forum
Słowem wstępu dodam, że artykuł ten "przemyca" jeszcze jedną nowość. Podczas programowania centralki alarmowej pokażę w jaki sposób poradzić sobie z zaprogramowaniem urządzenia, które musi wykonywać całkiem sporo operacji. Dzięki użyciu maszyny stanów znacznie uprościmy kod i unikniemy zagnieżdżania warunków. Co więcej, okaże się, że odpowiednie podejście do problemu pozwoli stworzyć dobrze działający program nawet bez korzystania z przerwań.
Klawiatura matrycowa do Arduino
Do tej pory podłączenie każdego przycisku do Arduino "zabierało" nam jedno wejście. Przy jednym, dwóch lub trzech przyciskach nie stanowiło to problemu. Czasami jednak chcemy mieć możliwość wprowadzenia do układu większej ilości danych. Na przykład wpisanie kodu pin do alarmu wymaga minimum 10 klawiszy (cyfry od 0 do 9), a najlepiej gdybyśmy do dyspozycji mieli jeszcze kilka przycisków (np. do anulowania lub zatwierdzania operacji).
Po prostu - przydałaby się nam klawiatura numeryczna:
Oczywiście podłączenie każdego przycisku do osobnego pinu byłoby uciążliwe, a czasami wręcz niemożliwe (szczególnie przy większych klawiaturach). Na szczęście istnieje sprytne rozwiązanie, które pozwala znacznie zredukować ilość potrzebnych pinów.
Budowa klawiatury matrycowej
W klawiaturach matrycowych producenci łączą przyciski w kolumny oraz wiersze. Dzięki temu powstaje matryca połączeń. Może to przykładowo wyglądać tak:
Dzięki temu możemy sprawdzać
16 przycisków za pomocą 8 linii danych!
W chwili, gdy wciskamy przycisk zwieramy jedną kolumnę z jednym wierszem. Przykładowo na powyższym rysunku wciśnięcie przycisku "1" zwiera wiersz P1 z kolumną P5. Natomiast wciśnięcie "4" zwiera wiersz P2 z kolumną P5. Sprawdzając połączenia między wierszami P1-P4 oraz kolumnami P5-P8 możemy wykryć, czy i jaki przycisk został wciśnięty.
Klawiatura matrycowa w praktyce
Podczas ćwiczeń wykorzystamy prostą wersję klawiatury (bez panelu przedniego), w której widać wewnętrzne połączenia. Dzięki temu osoby, dla których powyższa zasada działania odczytów nie jest jasna będą mogły sprawdzić "z bliska", jak to wszystko wygląda:
Pora uruchomić klawiaturę w praktyce!
Gotowe zestawy do kursów Forbota
Komplet elementów Gwarancja pomocy Wysyłka w 24h
Części do wykonania ćwiczeć z kursu Arduino (poziom 2) dostępne są w formie gotowych zestawów! W komplecie m.in. diody programowalne, termometry analogowe i cyfrowe, wyświetlacze 7-segmentowe oraz czujnik ruchu (PIR).
Zamów w Botland.com.pl »Biblioteka KeyPad
Oczywiście w przypadku Arduino znajdziemy gotową bibliotekę, która jeszcze bardziej ułatwi nam wykorzystywanie takich klawiatur. W tym wypadku będzie to biblioteka nazwana Keypad. Można ją znaleźć bezpośrednio w managerze bibliotek lub na GitHubie: https://github.com/Chris--A/Keypad
Informacje na temat sposobu instalacji bibliotek podane zostały w artykule:
Kurs Arduino II – #2 – programowalne diody RGB (WS2812)
Pierwsze użycie klawiatury numerycznej
Na początek warto napisać program testowy, który będzie sprawdzał, czy jakiś przycisk jest wciśnięty. Jeśli tak będzie, to odpowiedni znak zostanie wysłany do komputera przez UART.
Na początek podłączamy klawiaturę do Arduino za pomocą pinów od 2 do 9 zgodnie z poniższym rysunkiem. Kolejność pinów jest ważna, abyśmy mogli poprawnie odczytywać wciśnięte przyciski.
Klawiaturę można wpiąć w płytkę stykową (przyciski będą wtedy skierowane jednak w dół). Warto więc wykorzystać przewody i podłączyć ją bezpośrednio do Arduino. W tym celu trzeba "stworzyć" 8 przewodów męsko-żeńskich (tak jak w poprzednim artykule):
U mnie, całość po spięciu i ułożeniu przewodów wyglądała tak:
Teraz można przejść do programowania. Na początku dołączamy bibliotekę i podajemy wszystkie niezbędne informacje na temat naszej klawiatury i jej podłączenia:
1 2 3 4 5 6 7 |
#include <Keypad.h> //biblioteka od klawiatury const byte ROWS = 4; // ile wierszy const byte COLS = 4; //ile kolumn byte rowPins[ROWS] = {5, 4, 3, 2}; //piny wierszy byte colPins[COLS] = {6, 7, 8, 9}; //piny kolumn |
Fragment ten jest na tyle przejrzysty, że chyba nie ma potrzeby rozpisywania się na jego temat. Jedynie podane przeze mnie numery pinów (a właściwie ich kolejność) mogą budzić zastanowienie. Akurat to wyjaśni się kawałek dalej.
Kolejnym etapem jest mapowanie klawiatury, czyli przypisanie konkretnych znaków pod przyciski. najwygodniej będzie jeśli przycisk opiszemy w standardowy sposób. Ja przyjąłem taką kolejność:
Informacje te podajemy w następujący sposób:
1 2 3 4 5 6 |
char keys[ROWS][COLS] = { //mapowanie klawiatury {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; |
Zapis ten może wydawać się mało intuicyjny, jednak pozwala uporządkować program. Osoby, które mają wcześniejsze doświadczenia z programowaniem poznają oczywiście, że jest to zwykła tablica dwuwymiarowa.
W kursie Arduino, do tablic jeszcze wrócimy - więc, jeśli ktoś ich nie zna,
to nie musi się teraz niczego obawiać.
Ostatni krok podczas konfiguracji, to utworzenie nowego obiektu typu Keypad, u nas będzie nosił on nazwę klawiatura:
1 |
Keypad klawiatura = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); //inicjalizacja klawiatury |
Analogiczną operację musieliśmy wykonywać podczas deklaracji nowej linijki świetlnej podczas ćwiczeń z artykułu na temat diod programowalnych WS2812. Gdy klawiatura będzie zadeklarowana można przejść dalej - czyli do odczytywania znaków.
Nie ma potrzeby ustawiania pinów,
do których podłączona jest klawiatura jako wejścia.
Cały kod wygląda następująco:
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 |
#include <Keypad.h> //biblioteka od klawiatury const byte ROWS = 4; // ile wierszy const byte COLS = 4; //ile kolumn byte rowPins[ROWS] = {5, 4, 3, 2}; //piny wierszy byte colPins[COLS] = {6, 7, 8, 9}; //piny kolum char keys[ROWS][COLS] = { //mapowanie klawiatury {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; Keypad klawiatura = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); //inicjalizacja klawiatury void setup(){ Serial.begin(9600); } void loop(){ char klawisz = klawiatura.getKey(); if (klawisz){ Serial.println(klawisz); } } |
W pętli głównej programu tworzymy zmienną do przechowywania znaku (typu char), a następnie przypisujemy jej wartość odczytaną z klawiatury. Znak ten otrzymamy po wywołaniu polecenia klawiatura.getKey(). Dalej sprawdzamy, czy faktycznie odebrano znak i jeśli tak, to odsyłamy go do komputera przez UART.
Warunek if (klawisz) jest spełniony, jeśli pod zmienną klawisz będzie dowolny znak.
Każda wartość większa od 0 w warunku jest traktowana jako PRAWDA.
Po wgraniu programu w monitorze portu szeregowego wyświetlać będą się znaki przypisane do aktualnie wciskanych klawiszy:
Jeśli w komputerze wyświetlają się inne znaki, niż te na wcześniejszym rysunku z mapowaniem, to warto sprawdzić jeszcze raz połączenia. Zamiana kolejności przewodów sprawi, że funkcja będzie błędnie interpretować wciskane klawisze.
Innym sposobem naprawy takiego błędu byłaby zmiana wartości w tablicy mapowania. Np. jeśli chcielibyśmy odwrócić klawiaturę, to należałoby umieścić tam poniższy kod:
1 2 3 4 5 6 |
char keys[ROWS][COLS] = { //mapowanie klawiatury {'*','0','#','D'}, {'7','8','9','C'}, {'4','5','6','B'}, {'1','2','3','A'} }; |
Oczywiście możliwe są również znacznie bardziej "udziwnione" kombinacje. Jednak wtedy kod staje się mniej czytelny. To właśnie dlatego nalegałem, aby na początku wykorzystać konkretne numery pinów i kolejność połączeń.
Dosłownie bliźniaczy program do powyższego, można znaleźć w przykładach dostarczonych wraz z biblioteką. Dodatkowo jest tam kilka innych ciekawych programów - zachęcam do samodzielnych testów. W dalszej części artykułu zajmiemy się już zaprogramowaniem prostej centralki alarmowej.
Centralka alarmowa na Arduino
Nasz system alarmowy będzie składał się z klawiatury numerycznej, sygnalizatora dźwiękowego, sygnalizatora świetlnego, czujnika ruchu (PIR) oraz czujnika otwarcia drzwi (kontaktron).
Użyte czujniki alarmowe zostały opisane w poprzedniej części kursu Arduino.
Oczywiście całość może działać na wiele różnych sposób. Ja postanowiłem, że mój alarm będzie działał następująco: po włączeniu zasilania wchodzimy w tryb gotowości. Alarm nic nie robi tylko czeka na jego uzbrojenie. Po wciśnięciu klawisza A (jak Alarm) następuje procedura uzbrajania.
Po kilku sekundach (czas na wyjście z pomieszczenia) alarm zaczyna działać - od tej pory nasze pomieszczenie jest pilnowane. W momencie, gdy wykryjemy ruch w pomieszczeniu alarm zaczyna natychmiast sygnalizować zagrożenie.
Jeśli wykryjemy otwarcie drzwi, to dajemy użytkownikowi kilka sekund na rozbrojenie alarmu, które polega na wpisaniu 4 cyfrowego kodu. Jeśli kod pin będzie błędny lub nie zostanie wprowadzony, to alarm również zostanie uruchomiony.
Dodatkowo całość zostanie zaprogramowana w taki sposób, aby bez używania przerwań alarm działał sprawnie i bez opóźnień. Wszystko dzięki zbudowaniu prostej maszyny stanów.
Czym jest maszyna stanów?
W kontekście Arduino maszyną stanów (lub automatem skończonym) nazwiemy pewną metodykę pisania programów, która pozwala na łatwe i czytelne zaimplementowanie w urządzeniu różnych funkcji, które następują w ustalonej kolejności.
Dzięki temu unikamy zagnieżdżonych, długich instrukcji warunkowych. Temat od strony teorii jest znacznie bardziej złożony - zainteresowanych odsyłam do Wikipedii. Nam wystarczy teraz luźne nawiązanie do tej metody.
Tutaj skupimy się na praktycznym wykorzystaniu maszyny stanów,
teoria nie będzie potrzebna. Całość jest bardzo intuicyjna.
Biorąc za przykład powyższy opis działania alarmu możemy wyróżnić w nim 4 stany:
- Czuwanie - alarm czeka na uzbrojenie.
- Monitorowanie - system pilnuje naszego pomieszczenia.
- Rozbrajanie - alarm czeka na wprowadzenie poprawnego pinu.
- Sygnalizacja alarmu - alarm wydaje dźwięki i sygnały świetlne.
Oczywiście funkcje te mogą następować po sobie tylko w odpowiedniej kolejności. Nie zdarzy się sytuacja, że będziemy rozbrajać alarm, gdy jest w czasie czuwania itd. Najlepiej widoczne jest to na poniższym schemacie (nie jest to graf stanów, traktujmy to jako zwykłą, graficzną reprezentację powyższych opisów):
Jak widać program spokojnie mógłby składać się z 4 niezależnych funkcji, które kierują do siebie nawzajem - oczywiście w ustalonej kolejności. Zacznijmy więc pisać kod.
Kod prostej centralki alarmowej na Arduino
Mam nadzieję, że to oczywiste, jednak dla pewności podkreślę: poniższe ćwiczenie jest jedynie przykładem, zabawą i hobbystycznym projektem. Na pewno nie jest to rozwiązanie profesjonalne i przemysłowe. Chodzi tutaj wyłącznie o naukę!
Główny szkielet programu widoczny jest poniżej. Na początku wpisałem już informacje na temat czujników oraz buzzera, które będą później używane. Zadeklarowałem też klawiaturę - połączenia zostały takie same jak podczas jej pierwszego wykorzystania.
Bardzo ważna jest zmienna stanAlarmu, która będzie mówiła o aktualnym stanie, w którym jest nasze urządzenie. To jej wartość będzie decydowała o aktualnie wykonywanych operacjach. Dalej, w pętli głównej korzystamy z konstrukcji switch-case opisane w kursie Arduino, poziom I.
Za pomocą tego switcha będziemy przemieszczać się między kodem,
który ma być wykonywany zależnie od aktualnego stanu układu.
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 |
#define BUZZER 11 #define KONTAKTRON 10 #define PIR 1 #include <Keypad.h> //biblioteka od klawiatury const byte ROWS = 4; // ile wierszy const byte COLS = 4; //ile kolumn byte rowPins[ROWS] = {5, 4, 3, 2}; //piny wierszy byte colPins[COLS] = {6, 7, 8, 9}; //piny kolumn char keys[ROWS][COLS] = { //mapowanie klawiatury {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; Keypad klawiatura = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); //inicjalizacja klawiatury volatile int stanAlarmu = 1; void setup() { pinMode(BUZZER, OUTPUT); pinMode(KONTAKTRON, INPUT_PULLUP); pinMode(PIR, INPUT_PULLUP); } void loop() { switch(stanAlarmu) { //Wykonywania akcji odpowiedniej dla danego stanu case 1: //Czuwanie break; case 2: //Monitorowanie break; case 3: //Rozbrajanie break; case 4: //Sygnalizacja alarmu break; } } |
Stan 1: czuwanie alarmu
Podczas tego stanu urządzenie powinno sygnalizować swoją gotowość np. świeceniem diody, a po wciśnięciu klawisza "A" alarm powinien przejść do stanu 2 - po wcześniejszym odczekaniu czasu na wyjście z pomieszczenia. W roli diody użyjemy linijkę diod RGB opisaną w 1 artykule z tej serii. Podłączamy ją do pinu A0.
Oczywiście program również wymaga dodania nowej biblioteki oraz inicjalizacji linijki. Co więcej podczas, gdy urządzenie jest w stanie pierwszym, to jedna z diod będzie świeciła na zielono.
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 |
#define BUZZER 11 #define KONTAKTRON 10 #define PIR 1 #include <Keypad.h> //biblioteka od klawiatury #include <Adafruit_NeoPixel.h> //biblioteka od linijki LED const byte ROWS = 4; // ile wierszy const byte COLS = 4; //ile kolumn byte rowPins[ROWS] = {5, 4, 3, 2}; //piny wierszy byte colPins[COLS] = {6, 7, 8, 9}; //piny kolumn char keys[ROWS][COLS] = { //mapowanie klawiatury {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; Keypad klawiatura = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); //inicjalizacja klawiatury Adafruit_NeoPixel linijka = Adafruit_NeoPixel(8, A0, NEO_GRB + NEO_KHZ800); //konfiguracja linijki LED volatile int stanAlarmu = 1; void setup() { pinMode(BUZZER, OUTPUT); pinMode(KONTAKTRON, INPUT_PULLUP); pinMode(PIR, INPUT_PULLUP); linijka.begin(); //inicjalizacja linijki linijka.show(); } void loop() { switch(stanAlarmu) { //Wykonywania akcji odpowiedniej dla danego stanu case 1: //Czuwanie linijka.setPixelColor(0, linijka.Color(0, 15, 0)); //Dioda nr 1 świeci na zielono linijka.show(); break; case 2: //Monitorowanie break; case 3: //Rozbrajanie break; case 4: //Sygnalizacja alarmu break; } } |
Pora dodać możliwość uzbrajania. Przypomnę, że po wciśnięciu przycisku mapowanego jako "A" (jak alarm) mamy przejść do stanu drugiego. W tym celu wewnątrz stanu 1 dopisujemy odpowiedni warunek, fragment ten wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
char klawisz = 0; switch(stanAlarmu) { //Wykonywania akcji odpowiedniej dla danego stanu case 1: //Czuwanie linijka.setPixelColor(0, linijka.Color(0, 15, 0)); //Dioda nr 1 świeci na zielono linijka.show(); klawisz = klawiatura.getKey(); if (klawisz == 'A') { //Czy uzbroic alarm? stanAlarmu = 2; } break; case 2: //Monitorowanie linijka.setPixelColor(0, linijka.Color(15, 0, 0)); //Dioda nr 1 świeci na czerwono linijka.show(); break; |
To właśnie przypisanie zmiennej stanAlarmu = 2; sprawia, że program w następnym obiegu pętli przejdzie do stanu uzbrojonego. Oczywiście zgodnie z założeniami warto dopisać tutaj wspomniane kilka sekund na wyjście z pomieszczenia. Dla lepszego efektu postanowiłem, że w tym miejscu będę wykonywał długi efekt świetlny, który potrwa około 10 sekund.
Tutaj wstrzymanie działania programu za pomocą funkcji delay
nie wpłynie negatywnie na działanie programu.
Dodatkowo dodałem do programu funkcję gaszącą wszystkie diody. Wewnątrz stanu drugiego (czuwanie) pojawił się też bardzo mały delay. Dzięki niemu mamy pewność, że urządzenie działa (miganie diody), a z drugiej strony jest ono na tyle małe, że nie zaszkodzi reszcie programu.
W chwili obecnej kod powinien wyglądać następująco:
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 |
#define BUZZER 11 #define KONTAKTRON 10 #define PIR 1 #include <Keypad.h> //biblioteka od klawiatury #include <Adafruit_NeoPixel.h> //biblioteka od linijki LED const byte ROWS = 4; // ile wierszy const byte COLS = 4; //ile kolumn byte rowPins[ROWS] = {5, 4, 3, 2}; //piny wierszy byte colPins[COLS] = {6, 7, 8, 9}; //piny kolumn char keys[ROWS][COLS] = { //mapowanie klawiatury {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; Keypad klawiatura = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); //inicjalizacja klawiatury Adafruit_NeoPixel linijka = Adafruit_NeoPixel(8, A0, NEO_GRB + NEO_KHZ800); //konfiguracja linijki LED volatile int stanAlarmu = 1; void setup() { pinMode(BUZZER, OUTPUT); pinMode(KONTAKTRON, INPUT_PULLUP); pinMode(PIR, INPUT_PULLUP); linijka.begin(); //inicjalizacja linijki linijka.show(); } void loop() { char klawisz = 0; //zmienna do przetrzymywania znakow z klawiatury int i = 0; //zmienna pomocnicza do pętli switch(stanAlarmu) { //Wykonywania akcji odpowiedniej dla danego stanu case 1: //Czuwanie linijka.setPixelColor(0, linijka.Color(0, 15, 0)); //Dioda nr 1 świeci na zielono linijka.show(); klawisz = klawiatura.getKey(); if (klawisz == 'A') { //Czy zubroic alarm? for (i = 1; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(0, 0, 15)); //Dioda nr i świeci na niebiesko linijka.show(); delay(710); } // wykonanie tej petli zajmie okolo 5 sekund for (i = 1; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(15, 0, 0)); //Dioda nr i świeci na czerwono linijka.show(); delay(710); } // wykonanie tej petli zajmie okolo 5 sekund wylaczDiody(); stanAlarmu = 2; } break; case 2: //Monitorowanie linijka.setPixelColor(7, linijka.Color(15, 0, 0)); //Dioda nr 8 świeci na czerwono linijka.show(); delay(50); linijka.setPixelColor(7, linijka.Color(0, 0, 0)); //Dioda nr 8 wylaczona linijka.show(); delay(50); break; case 3: //Rozbrajanie break; case 4: //Sygnalizacja alarmu break; } } void wylaczDiody() { int i = 0; for (i = 0; i < 8; i++){ linijka.setPixelColor(i, linijka.Color(0, 0, 0)); //Dioda nr 1 wylaczona } linijka.show(); } |
Test uzbrajania alarmu w praktyce (czerwono dioda w rzeczywistości miga wyraźniej):
Stan 2: monitorowanie pomieszczenia
Gdy urządzenie przebywa w drugim stanie, to oprócz migania diodą powinno ciągle sprawdzać stany czujników. Pomijamy kwestię przerwań w tym miejscu - tutaj obejdziemy się bez nich.
Przed edycją programu trzeba oczywiście podłączyć wymagane czujniki. Piny podane zostały już w programie z pomocą dyrektywy #define. Na potrzeby dalszych eksperymentów wystarczy oba czujniki (kontaktron i PIR) przymocować do naszej podstawki:
W kolejnym kroku dodajemy dwa warunki. Wykrycie ruchu ma natychmiast uruchamiać alarm, natomiast otwarcie drzwi (kontaktron) powinno dać nam szanse na wyłączenie układu. Korzystając ze zmiennej stanAlarmu można to zrobić bardzo prosto
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
case 2: //Monitorowanie linijka.setPixelColor(7, linijka.Color(15, 0, 0)); //Dioda nr 8 świeci na czerwono linijka.show(); delay(50); linijka.setPixelColor(7, linijka.Color(0, 0, 0)); //Dioda nr 8 wylaczona linijka.show(); delay(50); if (digitalRead(PIR) == HIGH) { stanAlarmu = 4; //Natychmiast uruchamiamy alarm } else if (digitalRead(KONTAKTRON) == HIGH) { stanAlarmu = 3; //Szansa na rozbrojenie } break; |
Oczywiście po uruchomieniu programu nie zobaczymy nowych efektów, ponieważ stany 3 oraz 4 jeszcze nic nie robią. Dlatego na razie ominiemy stan 3 z rozbrajaniem układu. Najpierw dodajmy kilka linijek do stanu 4, które wywołają alarm. Na razie tylko świetlny.
Stan 4: sygnalizacja alarmu
Na chwile obecną program nie przewiduje, że można wyjść z trybu alarmu. W tej sekcji kodu można więc "zaszaleć", właściwe żadne opóźnienia nam nie zaszkodzą. Ja dodam na początek miganie w dwóch kolorach czerwonym i niebieskim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
case 4: //Sygnalizacja alarmu for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(255, 0, 0)); //Dioda nr i świeci na czerwono } linijka.show(); delay(100); for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(0, 0, 255)); //Dioda nr i świeci na niebiesko } linijka.show(); delay(100); break; } |
Kod jest na tyle prosty, że nie ma co w nim tłumaczyć. Jeśli ktoś miałby wątpliwości, co się tutaj dzieje, to polecam wrócić do początku kursu Arduino (poziom II). Od tej pory, po aktywacji alarmu i wykryciu ruchu przez czujnik PIR, urządzenie powinno sygnalizować alarm:
Stan 3: Wpisywanie pinu
Pora na rozbudowanie programu o jedną z ważniejszych funkcji, czyli deaktywację alarmu. Problem ten rozwiążemy w dwóch stopniach. Na początku zajmiemy się tylko wpisywaniem kodu pin.
Później dodamy licznik, który będzie sprawdzał,
czy czas na wpisanie kodu już minął.
Na początku w sekcji ze zmiennymi globalnymi dodajemy następujące wartości:
1 2 3 4 5 |
int pinAlarmuPozycja = 1; char pinCyfra1 = '1'; char pinCyfra2 = '2'; char pinCyfra3 = '3'; char pinCyfra4 = '4'; |
Tak jak wspomniałem wcześniej, na tym etapie kursu jeszcze nie korzystamy z tablic, więc kod zapisałem w 4 osobnych zmiennych. Gdy w jednym z kolejnych artykułów przećwiczymy tablice, to będzie można wrócić do tego miejsca i napisać kod bardziej elegancko.
Na ten moment chce jednak jak najprościej pokazać,
jak działa ten mechanizm sprawdzania kodu.
Gdy mamy już zadeklarowany kod (w tym przypadku 1234), to wewnątrz stanu 3 możemy dokonać jego sprawdzenia. Mechanizm działa następująco - komentarze kodu powinny wiele rozjaśnić:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
case 3: //Rozbrajanie klawisz = klawiatura.getKey(); if (klawisz) { //Czy kolejna podana cyfra jest poprawna? if (pinAlarmuPozycja == 1 && klawisz == pinCyfra1) { //Jesli sprawdzamy 1 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 2 && klawisz == pinCyfra2) { //Jesli sprawdzamy 2 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 3 && klawisz == pinCyfra3) { //Jesli sprawdzamy 3 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 4 && klawisz == pinCyfra4) { //Jesli sprawdzamy 4 pozycje PINu stanAlarmu = 1; //Wszystkie 4 cyfry kodu sa poprawne pinAlarmuPozycja = 1; //Resetujemy informacje o wpisywanym pinie } else { stanAlarmu = 4; //Blad w kodzie PIN - wlacz alarm pinAlarmuPozycja = 1; //Resetujemy informacje o wpisywanym pinie } } break; |
Podsumowując mechanizm ten pamięta w zmiennej pinAlarmuPozycja ile podano już dobrych znaków z kodu. Dzięki temu wiemy, z którą zmienną aktualnie trzeba porównać wpisywany znak.
Gdy czwarty podany znak jest poprawny, to znaczy, że cały kod był poprawny - wtedy możemy wrócić do stanu 1 (czuwanie). Jeśli w którymś miejscu podczas wpisywania kodu się pomylimy, to przechodzimy do stanu 4 i uruchamiamy alarm.
Na ten moment cały kod powinien wyglądać następująco:
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 |
#define BUZZER 11 #define KONTAKTRON 10 #define PIR 1 #include <Keypad.h> //biblioteka od klawiatury #include <Adafruit_NeoPixel.h> //biblioteka od linijki LED const byte ROWS = 4; // ile wierszy const byte COLS = 4; //ile kolumn byte rowPins[ROWS] = {5, 4, 3, 2}; //piny wierszy byte colPins[COLS] = {6, 7, 8, 9}; //piny kolumn char keys[ROWS][COLS] = { //mapowanie klawiatury {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; Keypad klawiatura = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); //inicjalizacja klawiatury Adafruit_NeoPixel linijka = Adafruit_NeoPixel(8, A0, NEO_GRB + NEO_KHZ800); //konfiguracja linijki LED volatile int stanAlarmu = 1; int pinAlarmuPozycja = 1; char pinCyfra1 = '1'; char pinCyfra2 = '2'; char pinCyfra3 = '3'; char pinCyfra4 = '4'; void setup() { pinMode(BUZZER, OUTPUT); pinMode(KONTAKTRON, INPUT_PULLUP); pinMode(PIR, INPUT_PULLUP); linijka.begin(); //inicjalizacja linijki linijka.show(); } void loop() { char klawisz = 0; //zmienna do przetrzymywania znakow z klawiatury int i = 0; //zmienna pomocnicza do pętli switch(stanAlarmu) { //Wykonywania akcji odpowiedniej dla danego stanu case 1: //Czuwanie linijka.setPixelColor(0, linijka.Color(0, 15, 0)); //Dioda nr 1 świeci na zielono linijka.show(); klawisz = klawiatura.getKey(); if (klawisz == 'A') { //Czy zubroic alarm? for (i = 1; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(0, 0, 15)); //Dioda nr i świeci na niebiesko linijka.show(); delay(710); } // wykonanie tej petli zajmie okolo 5 sekund for (i = 1; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(15, 0, 0)); //Dioda nr i świeci na czerwono linijka.show(); delay(710); } // wykonanie tej petli zajmie okolo 5 sekund wylaczDiody(); stanAlarmu = 2; } break; case 2: //Monitorowanie linijka.setPixelColor(7, linijka.Color(15, 0, 0)); //Dioda nr 8 świeci na czerwono linijka.show(); delay(50); linijka.setPixelColor(7, linijka.Color(0, 0, 0)); //Dioda nr 8 wylaczona linijka.show(); delay(50); if (digitalRead(PIR) == HIGH) { stanAlarmu = 4; //Natychmiast uruchamiamy alarm } else if (digitalRead(KONTAKTRON) == HIGH) { stanAlarmu = 3; //Szansa na rozbrojenie } break; case 3: //Rozbrajanie klawisz = klawiatura.getKey(); if (klawisz) { //Czy kolejna podana cyfra jest poprawna? if (pinAlarmuPozycja == 1 && klawisz == pinCyfra1) { //Jesli sprawdzamy 1 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 2 && klawisz == pinCyfra2) { //Jesli sprawdzamy 2 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 3 && klawisz == pinCyfra3) { //Jesli sprawdzamy 3 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 4 && klawisz == pinCyfra4) { //Jesli sprawdzamy 4 pozycje PINu stanAlarmu = 1; //Wszystkie 4 cyfry kodu sa poprawne pinAlarmuPozycja = 1; //Resetujemy informacje o wpisywanym pinie } else { stanAlarmu = 4; //Blad w kodzie PIN - wlacz alarm pinAlarmuPozycja = 1; //Resetujemy informacje o wpisywanym pinie } } break; case 4: //Sygnalizacja alarmu for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(255, 0, 0)); //Dioda nr i świeci na czerwono } linijka.show(); delay(100); for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(0, 0, 255)); //Dioda nr i świeci na niebiesko } linijka.show(); delay(100); break; } } void wylaczDiody() { int i = 0; for (i = 0; i < 8; i++){ linijka.setPixelColor(i, linijka.Color(0, 0, 0)); //Dioda nr 1 wylaczona } linijka.show(); } |
Działanie tego fragmentu kodu w praktyce:
Drugi przypadek dla kodu poprawnego:
Odliczanie czasu na wpisanie pinu
Pozostaje dopisać fragment kodu, który będzie odpowiadał za uruchomienie alarmu jeśli wpisanie kodu zajmowałoby nam z byt dużo czasu. Oczywiście nie można tutaj wpisać np. delay(10000), bo taka instrukcja zamroziłaby cały program i nie moglibyśmy sprawdzać kodu.
Ale... moglibyśmy wprowadzić małe opóźnienie np. 50 ms. Wartość ta nie zakłóci działania kodu, cały czas będziemy mogli wpisywać pin. Wystarczy więc dodać warunek, który sprawdzi, czy w stanie 3 (rozbrajanie) byliśmy więcej niż 100 razy.
Każde wejście do tego bloku funkcji zajmie 50 ms, więc 100 wejść da 5 sekund.
Jeśli przez ten czas nie wpiszemy kodu, to trzeba włączyć alarm.
Przełożenie tego do programu jest bardzo proste. Wystarczy dodać zmienną globalną:
1 |
int ileCzasuMinelo = 0; |
Za jej pomocą zliczymy ile razy byliśmy wewnątrz bloku sprawdzającego poprawność kodu pin. Następnie wewnątrz bloku funkcji (wykonywanych podczas stanu 3) dodajemy:
1 2 3 4 5 6 |
delay(100); ileCzasuMinelo++; if (ileCzasuMinelo >= 50) { stanAlarmu = 4; } |
Każde 100 ms odczekiwania zwiększy wartość zmiennej ileCzasuMinelo, gdy naliczymy 50 lub więcej, to w kolejnym obiegu pętli głównej przejdziemy do stanu 4 i włączymy alarm. Warto jeszcze zadbać, aby zmienna ileCzasuMinelo liczyła od zera, gdy alarm da nam szansę na podanie pinu. Najlepiej kod ten dodać wewnątrz stanu 2, tuż przed przejściem do 3.
1 2 3 4 5 6 |
if (digitalRead(PIR) == HIGH) { stanAlarmu = 4; //Natychmiast uruchamiamy alarm } else if (digitalRead(KONTAKTRON) == HIGH) { ileCzasuMinelo= 0; //Zerowanie zmiennej stanAlarmu = 3; //Szansa na rozbrojenie } |
Ostateczna wersja programu wygląda następująco:
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 143 144 |
#define BUZZER 11 #define KONTAKTRON 10 #define PIR 1 #include <Keypad.h> //biblioteka od klawiatury #include <Adafruit_NeoPixel.h> //biblioteka od linijki LED const byte ROWS = 4; // ile wierszy const byte COLS = 4; //ile kolumn byte rowPins[ROWS] = {5, 4, 3, 2}; //piny wierszy byte colPins[COLS] = {6, 7, 8, 9}; //piny kolumn char keys[ROWS][COLS] = { //mapowanie klawiatury {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; Keypad klawiatura = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); //inicjalizacja klawiatury Adafruit_NeoPixel linijka = Adafruit_NeoPixel(8, A0, NEO_GRB + NEO_KHZ800); //konfiguracja linijki LED volatile int stanAlarmu = 1; int pinAlarmuPozycja = 1; char pinCyfra1 = '1'; char pinCyfra2 = '2'; char pinCyfra3 = '3'; char pinCyfra4 = '4'; int ileCzasuMinelo = 0; void setup() { pinMode(BUZZER, OUTPUT); pinMode(KONTAKTRON, INPUT_PULLUP); pinMode(PIR, INPUT_PULLUP); linijka.begin(); //inicjalizacja linijki linijka.show(); } void loop() { char klawisz = 0; //zmienna do przetrzymywania znakow z klawiatury int i = 0; //zmienna pomocnicza do pętli switch(stanAlarmu) { //Wykonywania akcji odpowiedniej dla danego stanu case 1: //Czuwanie linijka.setPixelColor(0, linijka.Color(0, 15, 0)); //Dioda nr 1 świeci na zielono linijka.show(); klawisz = klawiatura.getKey(); if (klawisz == 'A') { //Czy zubroic alarm? for (i = 1; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(0, 0, 15)); //Dioda nr i świeci na niebiesko linijka.show(); delay(710); } // wykonanie tej petli zajmie okolo 5 sekund for (i = 1; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(15, 0, 0)); //Dioda nr i świeci na czerwono linijka.show(); delay(710); } // wykonanie tej petli zajmie okolo 5 sekund wylaczDiody(); stanAlarmu = 2; } break; case 2: //Monitorowanie linijka.setPixelColor(7, linijka.Color(15, 0, 0)); //Dioda nr 8 świeci na czerwono linijka.show(); delay(50); linijka.setPixelColor(7, linijka.Color(0, 0, 0)); //Dioda nr 8 wylaczona linijka.show(); delay(50); if (digitalRead(PIR) == HIGH) { stanAlarmu = 4; //Natychmiast uruchamiamy alarm } else if (digitalRead(KONTAKTRON) == HIGH) { ileCzasuMinelo= 0; //Zerowanie zmiennej stanAlarmu = 3; //Szansa na rozbrojenie } break; case 3: //Rozbrajanie klawisz = klawiatura.getKey(); if (klawisz) { //Czy kolejna podana cyfra jest poprawna? if (pinAlarmuPozycja == 1 && klawisz == pinCyfra1) { //Jesli sprawdzamy 1 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 2 && klawisz == pinCyfra2) { //Jesli sprawdzamy 2 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 3 && klawisz == pinCyfra3) { //Jesli sprawdzamy 3 pozycje PINu pinAlarmuPozycja++; //Cyfra poprawna, mozna sprawdzic na kolejna } else if (pinAlarmuPozycja == 4 && klawisz == pinCyfra4) { //Jesli sprawdzamy 4 pozycje PINu stanAlarmu = 1; //Wszystkie 4 cyfry kodu sa poprawne pinAlarmuPozycja = 1; //Resetujemy informacje o wpisywanym pinie } else { stanAlarmu = 4; //Blad w kodzie PIN - wlacz alarm pinAlarmuPozycja = 1; //Resetujemy informacje o wpisywanym pinie } } delay(100); ileCzasuMinelo++; if (ileCzasuMinelo >= 50) { stanAlarmu = 4; } break; case 4: //Sygnalizacja alarmu for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(255, 0, 0)); //Dioda nr i świeci na czerwono } linijka.show(); delay(100); for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(0, 0, 255)); //Dioda nr i świeci na niebiesko } linijka.show(); delay(100); break; } } void wylaczDiody() { int i = 0; for (i = 0; i < 8; i++){ linijka.setPixelColor(i, linijka.Color(0, 0, 0)); //Dioda nr 1 wylaczona } linijka.show(); } |
Działanie tego fragmentu programu w praktyce:
Dodanie efektów dźwiękowych
Aby alarm spełniał swoją rolę trzeba podłączyć buzzer - może być wersja z generatorem lub bez. Ja zdecydowałem się na wersję bez generatora i podłączyłem ją do pinu nr 11, tak jak było to wcześniej zadeklarowane:
Następnie dopisałem dwie linijki do 4 stanu układu, który odpowiada za sygnalizację alarmu. Więcej informacji na temat funkcji tone() znaleźć można w 3 części kursu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
case 4: //Sygnalizacja alarmu for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(255, 0, 0)); //Dioda nr i świeci na czerwono } linijka.show(); tone(BUZZER, 4300); delay(100); for (i = 0; i < 8; i++) { linijka.setPixelColor(i, linijka.Color(0, 0, 255)); //Dioda nr i świeci na niebiesko } linijka.show(); tone(BUZZER, 3500); delay(100); break; |
Ostateczne działanie całego alarmu widać na poniższym filmie:
Zadania dodatkowe - dla chętnych
Program wyszedł dość rozbudowany, więc nie będę go teraz jeszcze bardziej "rozciągać". Chętni mogą jednak rozbudować program o nowe funkcje. Podaję listę rzeczy, które warto dodać:
- sygnał dźwiękowy podczas uzbrajania alarmu,
- sygnał dźwiękowy podczas podawania pinu (wciśnięcie klawisza - "bip"),
- osobny przycisk podłączony pod przerwanie, który resetuje alarm,
- bardziej rozbudowane efekty dźwiękowe.
Zachęcam do umieszczania w komentarzach zdjęć i filmów,
jeśli uda Wam się dodać chociaż jedną z tych funkcji!
Podsumowanie
Podczas tego artykułu starałem się pierwszy raz zbudować razem z Wami jeden, długi kod. Mam nadzieję, że taka forma artykułu również jest przydatna - dajcie znać w komentarzach! Poprzednio "straszyłem" wszystkich przerwaniami, a tutaj ich nie użyłem. Dlaczego? Bo nie zawsze trzeba!
Ten program udało się okroić z opóźnień. Jego działanie składa się praktycznie z ciągłego obiegania pętli głównej i wykonywania jedynie potrzebnych (w danej chwili) operacji. Dlatego nie trzeba obawiać, że Arduino nie zauważy sygnału z czujnika.
Warto znać różne rozwiązania i dobierać je pod konkretne projekty. Gdyby nasze urządzenie wykonywało dużo więcej czasochłonnych operacji, to przerwania niewątpliwie znalazły by tutaj swoje zastosowanie.
Nawigacja kursu
Autor kursu: Damian (Treker) Szymański
Powiązane wpisy
alarm, arduino, czujniki, kurs, kursArduino2
Trwa ładowanie komentarzy...