Jeden z projektów wymagał podłączenia do Arduino zestawu 4 przycisków. Chodziło o zwykłe tact switche. Problem polegał jednak na tym, że do dyspozycji zostały mi dosłownie pojedyncze piny – i to te analogowe.
Po krótkim poszukiwaniu w sieci znalazłem sposób na podłączenie wielu przycisków do pojedynczego wejścia analogowego (A0…A7).
Na podobne rozwiązanie natknąłem się już przy okazji pracy z rozszerzeniem LCD opisanym tutaj: Moduł LCD z przyciskami dla Arduino. Odczytanie stanu przycisków wymagało wywołania funkcji „analogRead()” na pinie A0. Schemat działającego na tej samej zasadzie układu znalazłem tutaj: Tutorial: Analog input for multiple buttons (part two) – i to na tym właśnie tekście się oparłem (w sieci znajdziecie ich znacznie więcej). Postanowiłem jednak przeanalizować, jak to właściwie działa.
Rozwiązanie
Drobna nieścisłość: na rysunkach znajdziecie rezystory 20kΩ i 10kΩ. Takie były w oryginalnym tekście, na którym oparłem swój post. Ponieważ takich nie miałem – użyłem 22kΩ i 9.8kΩ, jak to jest w obliczeniach.
Rozwiązanie podane w Tutorial: Analog input for multiple buttons (part two) przedstawia się następująco:
Mamy tu 4 przełączniki S1 do S4. Podłączone są do nich 4 rezystory R1..R4 o wartościach 20kΩ. Między nimi dodano kolejne rezystory R5..R8 o wartościach 10kΩ i 20kΩ (R8). Całość podłączono do zasilania 5v. Wejście A0 Arduino podłączono za rezystorem R1 pierwszego z przełączników.
Zaletą tego układu jest to, że używa tylko 2 typów rezystorów: 10kΩ i 20kΩ.
Dzięki takiemu sposobowi połączenia, do sprawdzenia stanu czterech przycisków wystarczą mi 3 kabelki: 5v, GND i jeden do pinu analogowego (np. A0). „Normalnie” potrzebowałbym 5v, GND oraz 4 piny wejściowe – razem 6.
Rozwiązanie to możecie łatwo złożyć sobie na płytce prototypowej:
Zamiast przewidzianych w schemacie rezystorów 20kΩ użyłem najbliższych 22kΩ – a zamiast 10kΩ: 9.8kΩ.
Jak to działa?
Gdy nie wciśniecie żadnego przycisku, nie będzie połączenia między 5v a GND – żaden prąd nie popłynie, na A0 będzie wartość 0.
Załóżmy, że wciśnięto klawisz S1:
Prąd popłynie wtedy przez zamknięty przełącznik S1 oraz rezystor R1. Za rezystorem R1 Arduino zmierzy napięcie. Następnie prąd popłynie przez rezystory R5…R8 aż do masy.
Uprośćmy rysunek usuwając część układu, która nie jest w tym momencie aktywna:
Wygląda znajomo? Nie? to może bardziej klasycznie:
Dzielnik napięcia! Po wciśnięciu klawisza S1 (i nie tylko, ale o tym za chwilę), napięcie na pinie A0 będzie miało wartość jak dla dzielnika napięcia o rezystorach R1 i (R5+R6+R7+R8). Rezystory (R5+R6+R7+R8) zastąpię jednym o nazwie R5678:
Łatwo obliczę napięcie na A0 dla zamkniętego S1:
Rezystory R5+R6+R7+R8 połączone są szeregowo. Stąd rezystancja zastępcza R5678 będzie sumą rezystancji poszczególnych rezystorów (R5,6 i 7 to 9.8kΩ, R8: 22kΩ). Napięcie Uwe = 5v.
Teraz łatwo obliczę napięcie na A0:
Wartość, jaką oczekujemy na porcie A0 gdy wciśnięto klawisz S1, można obliczyć z proporcji:
Stąd:
Więcej o dzielniku napięcia: Arduino, miernik rezystancji w Scratchu.
A dla S3?
Sytuacja trochę zmieni się, jeżeli zamkniemy przełącznik S3:
Tutaj główną rolę będą grały R3 oraz suma R7 i R8. R5 i R6 ograniczą prąd wchodzący na pin A0 (chociaż samo Arduino radzi sobie z tym dobrze) i nie wpłyną na poziom napięcia.
Stąd:
Więc:
A wartość na A0:
Dla przycisku S2:
Podstawiając:
Na A0 dla zamkniętego S2 powinno pojawić się:
Dla S4:
Podstawiając wartości rezystorów i napięcia Uwe:
Na A0 powinno pojawić się:
Układ ten umożliwia również wykrywanie zdarzeń przyciśnięcia kilku klawiszy jednocześnie. Zasadę działania opiszę w osobnym tekście na ten temat.
Uruchomienie
Na potrzeby testów stworzyłem mały programik:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
void setup() { //Enable serial Serial.begin(9600); Serial.println("-------------------"); } void loop() { //Read the value from A0 int a0 = analogRead(0); //Output value of A0to serial Serial.print("Read: "); Serial.print(a0); Serial.print(", U: "); //Convert the value of A0 to //voltage (1023 is 5v) float u = (float)a0 * 5. / 1023.; Serial.print(u); Serial.println("[v]"); delay(100); } |
Ten program odczytuje wartość napięcia na pinie A0 i wyświetla na ekranie: odczytaną wartość (zmienna a0) oraz odpowiadającą jej wartość napięcia.
Funkcja „analogRead()” zwraca wartości od 0 do 1023 (10 bitową). Dla napięcia na pinie analogowym równego 0v – zwraca 0. Dla napięcia równego 5v – zwraca 1023. W praktyce wartości zwracane przez „analogRead()” odnoszą się do napięć na Arduino i mogą trochę różnić się od rzeczywistych.
Dla przykładu, wciśnięcie klawisza na płytce wygenerowało na pinie A0:
Przycisk | Teoretycznie | Zwrócone przez analogRead() |
S1 | 716 (ok. 3.5v) | 715 – 719 |
S2 | 669 (ok. 3.27v) | 667 – 674 |
S3 | 604 (ok. 3v) | 555 – 636 |
S4 | 511 (2.5v) | 510 – 515 |
Skąd się biorą rozbieżności między wartościami teoretycznymi a rzeczywistymi? Wpływają na nie głównie niedokładności rezystorów. Przykładowo: dla ścieżki R5678, teoretyczna wartość rezystancji to 51.4kΩ. W praktyce: 54.4kΩ. Dodatkowe niedokładności może również wprowadzać sam montaż na płytce stykowej.
Pewien rozrzut wartości wprowadzają same przełączniki, np. nie działają one natychmiastowo. Arduino może próbkować napięcie gdy przełącznik nie do końca „styka” – wtedy stawia dodatkowy opór, który wpływa na końcową wartość napięcia.
Różna jest też może być jakość tych przycisków – co dobrze widać po wartościach zwracanych przy próbkowaniu S3. W ostatecznym montażu zamierzam go pominąć.
Push vs pull
Znając zakresy A0 w odpowiedzi na wciśnięte przyciski, napisanie programu reagującego na klawisze nie powinno być trudne:
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 |
//KEY definitions #define KEY_S1 1 #define KEY_S2 2 #define KEY_S3 3 #define KEY_S4 4 #define KEY_NONE 0 //Handles key press //input: key code //output: true if success, false otherwise bool onKey(int key) { switch (key) { case KEY_S1: //Do something Serial.println(" Do something with KEY_S1"); break; case KEY_S2: //Do something Serial.println(" Do something with KEY_S2"); break; case KEY_S3: //Do something Serial.println(" Do something with KEY_S3"); break; case KEY_S4: //Do something Serial.println(" Do something with KEY_S4"); break; } return true; } //Checks which key was pressed //input: value read by analogRead() //output: key code or KEY_NONE if a0 out of known ranges int isKey(int a0) { if ( a0 >= 715 && a0 <= 719 ) { return KEY_S1; } if ( a0 >= 667 && a0 <= 674 ) { return KEY_S2; } if ( a0 >= 555 && a0 <= 636 ) { return KEY_S3; } if ( a0 >= 510 && a0 <= 515 ) { return KEY_S4; } return KEY_NONE; } void setup() { //Enable serial Serial.begin(9600); Serial.println("-------------------"); } void loop() { //Read the value from A0 int a0 = analogRead(0); int key = KEY_NONE; //Output value of A0to serial Serial.print("Read: "); Serial.println(a0); //Call onKey of key is known if ( (key = isKey(a0) ) != KEY_NONE) { onKey( key ); } delay(100); } |
Odpowiedzialność za rozpoznanie, który klawisz wciśnięto, przejęła tu funkcja „isKey()”. Jeżeli zwróci kod KEY_NONE – klawisz nie został rozpoznany. W przeciwnym wypadku pętla główna wywołuje kolejną funkcję „onKey()”, w której umieszczone będą procedury obsługi poszczególnych przycisków.
Oczywiście w powyższym kodzie brakuje kilku rzeczy. Przykładowo: Arduino próbkuje dość szybko – może się zdarzyć, że zanim puścicie któryś z przycisków, Arduino kilkakrotnie zgłosi wykrycie jego wciśnięcia.
Ten sposób nie jest jednak najwygodniejszy. Wymaga stałego odpytywania o stan klawiszy w pętli głównej loop. Możecie tutaj użyć mechanizmu przerwań. Wtedy wykonywanie pętli głównej zostanie przerwane w momencie wykrycia wciśnięcia klawisza.
Przerwanie…
Najniższe napięcie na A0 wystąpi w momencie wciśnięcia przycisku S4: ok. 2.5v. Wartość ta znajduje się powyżej poziomu, jaki Arduino uznaje za logiczną jedynkę (ok. 1.5v). Dzięki temu możemy zmodyfikować nasz układ tak, żeby wciśnięcie klawisza spowodowało wywołanie przerwania.
Połączcie pin analogowy (A0) z wybranym pinem D2 lub D3. Wystarczy do tego zwykły kabelek. Przed pinem A0 zawsze jest rezystor 22kΩ więc płynie przez niego bardzo niewielki prąd – nie zaszkodzi pinowi cyfrowemu.
Zmodyfikujemy teraz nasz program:
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 |
//KEY definitions #define KEY_S1 1 #define KEY_S2 2 #define KEY_S3 3 #define KEY_S4 4 #define KEY_NONE 0 //Handles key press //input: key code //output: true if success, false otherwise bool onKey(int key) { switch (key) { case KEY_S1: //Do something Serial.println(" Do something with KEY_S1"); break; case KEY_S2: //Do something Serial.println(" Do something with KEY_S2"); break; case KEY_S3: //Do something Serial.println(" Do something with KEY_S3"); break; case KEY_S4: //Do something Serial.println(" Do something with KEY_S4"); break; } return true; } //Checks which key was pressed //input: value read by analogRead() //output: key code or KEY_NONE if a0 out of known ranges int isKey(int a0) { if ( a0 >= 715 && a0 <= 719 ) { return KEY_S1; } if ( a0 >= 667 && a0 <= 674 ) { return KEY_S2; } if ( a0 >= 555 && a0 <= 636 ) { return KEY_S3; } if ( a0 >= 510 && a0 <= 515 ) { return KEY_S4; } return KEY_NONE; } //Interrupt handler: called when a key is pressed void onKeyPressed() { //Read the value from A0 int a0 = analogRead(0); int key = KEY_NONE; //Output value of A0to serial Serial.print("Read: "); Serial.println(a0); if ( (key = isKey(a0) ) != KEY_NONE) { onKey( key ); } } void setup() { //Enable serial Serial.begin(9600); Serial.println("-------------------"); //Attach an iterrupt to D3 attachInterrupt( digitalPinToInterrupt(3), onKeyPressed, RISING); } void loop() { Serial.print("."); delay(100); } |
Zauważcie: główna pętla „loop()” jest praktycznie… pusta. Za to w „setup()” dodałem nową instrukcję „attachInterrupt()”. W momencie pojawienia się zbocza narastającego na pinie D3, wywoływana jest funkcja „onKeyPressed()” – która teraz sprawdza stan A0.
W ten sposób stan klawiszy sprawdzany jest jedynie w momencie, gdy któryś z nich został faktycznie wciśnięty a nie, jak poprzednio – przy każdym przebiegu pętli.
Niestety w praktyce, takie rozwiązanie może nie być najlepsze…
Hybrydy push & pull
Powyższe rozwiązanie równie nie jest idealne. Procedury obsługi przerwań:
- Powinny być jak najkrótsze – podobnie jak funkcje, które wywołują,
- Mogą „gubić” wpisy na porcie szeregowym,
- Mogą wystąpić konflikty z zapisywaniem do zmiennych globalnych – trzeba deklarować je jako „volatile”,
- Mogą wystąpić różne dziwne konflikty – np. w przypadku funkcji „analogRead()”.
Jedno z rozwiązań, jakie stosuję jest model „hybrydowy”. Używam przerwania do ustawienia flagi, którą sprawdzam w pętli „loop()”, np:
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 |
//KEY definitions #define KEY_S1 1 #define KEY_S2 2 #define KEY_S3 3 #define KEY_S4 4 #define KEY_NONE 0 //Handles key press //input: key code //output: true if success, false otherwise bool onKey(int key) { //... return true; } //Checks which key was pressed //input: value read by analogRead() //output: key code or KEY_NONE if a0 out of known ranges int isKey(int a0) { //... return KEY_NONE; } //Interrupt handler: called when a key is pressed volatile bool aKeyHasBeenPressed = false; void onKeyPressed() { aKeyHasBeenPressed = true; } void setup() { //Enable serial Serial.begin(9600); Serial.println("-------------------"); //Attach an iterrupt to D3 attachInterrupt( digitalPinToInterrupt(3), onKeyPressed, RISING); } void loop() { //Read the value from A0 int a0 = analogRead(0); if( aKeyHasBeenPressed ){ cli(); int key = KEY_NONE; //Output value of A0 to serial Serial.print("Read: "); Serial.println(a0); if ( (key = isKey(a0) ) != KEY_NONE) { onKey( key ); } aKeyHasBeenPressed = false; sei(); } Serial.print("."); delay(100); } |
Zwróćcie uwagę, że w „loop()” używam funkcji „cli()”, która wyłącza przerwania, oraz „sei()” – które je z powrotem włącza. Robię to po to, żeby uniknąć wywołania przerwania w momencie kiedy obsługuję „aKeyHasBeenPressed == true” w pętli głównej „loop()”.
Podsumowanie
Rozwiązanie z przyciskami podłączanymi do wejścia analogowego może być całkiem atrakcyjną alternatywą dla tradycyjnych układów opierających się na wejściach cyfrowych. Wykorzystanie przetwornika analog-cyfra umożliwia znaczne ograniczenie liczby przewodów. Kod jest niewiele bardziej skomplikowany. Słowem: tylko lutować!
CZY W OBLICZENIACH SIE CZASAMI NIE POMYLILES ??
„Po wciśnięciu klawisza S1 (i nie tylko, ale o tym za chwilę), napięcie na pinie A0 będzie miało wartość jak dla dzielnika napięcia o rezystorach R1 i (R5+R6+R7+R8). Rezystory (R5+R6+R7+R8) zastąpię jednym o nazwie R5678:”
„Rezystory R5+R6+R7+R8 połączone są szeregowo. Stąd rezystancja zastępcza R5678 będzie sumą rezystancji poszczególnych rezystorów (R5,6 i 7 to 9.8kΩ, R8: 22kΩ). Napięcie Uwe = 5v.”
JAK TOBIE WYSZLO [( 51,4 )/(51,4)+22) ]
SKAD TE 51,4 ??
MOZESZ MI TO WYJASNIC ?
Witaj,
R5, R6 i R7 mają po 9.8k a R8=22k. Rezystancja zastępcza rezystorów łączonych szeregowo jest sumą rezystancji poszczególnych rezystorów, więc: 9.8k+9.8k+9.8k+22k = 51.4k. Teraz widzę, że na rysunkach są rezystory 10k i 20k. Nie miałem takich, więc wziąłem najbliższe im – poprawię rysunki przy najbliższej okazji – dzięki za uwagę:)
Pozdrawiam,
Arek
if ( a0 >= 715 && a0 <= 719 ) {
CO TO OZNACZA ?”&& a0 <= ” ?
…że a0 musi być 715,716, 717, 718 lub 719 – takie miejsce na niedokładności:)
CZY TO JEST POPRAWNIE ??
a0 >= 715 && a0 <= 719
CO TO OZNACZA ” && a0 <” ??
…zastosowanie nowego pluginu do WordPressa miało położyć kres podmienianiu na kody html… jak widać nie zawsze…
Dziękuję i pozdrawiam,
Arek
dokładnie. wyświetlały mi się jakieś krzaczki dlatego zadałem pytanie. Teraz rozumiem te przedziały. Pozdrawiam
Dziękuje za odpowiedź. Korzystam z Pana projektów. Pozdrawiam
Cieszę się, jeżeli coś się przydało:) A jak coś zbudujesz – pochwal się proszę:)