Tipkala kako treba

Tipkalo s INPUT_PULLUP, brojanje pritisaka, poskakivanje kontakata, smirivanje s millis() i s bibliotekom Bounce2 te razlika između pritiska i držanja

Arduino Elektronika
Tipkala kako treba

Ove godine gradimo pinball. Igrač njime upravlja samo tipkama: dvije tipke sa strane pokreću palice koje odbijaju kuglicu (engleski flippers), a tipka Start pokreće igru. Igrač od tipki očekuje da uvijek rade točno onako kako ih pritisne. Palica koja zakasni ili tipka Start koja jedan pritisak broji dvaput pokvarit će svaku igru.

Tipkalo izgleda kao najjednostavniji dio sklopa: pritisneš ga i spoji dvije žice. Ipak, s njim su povezane tri zamke, a danas ćemo upoznati sve tri:

  • pin koji nije nigdje spojen čita nasumično;
  • program koji samo pita je li tipkalo pritisnuto? jedan pritisak broji tisućama puta;
  • metalni kontakti u tipkalu pri svakom pritisku nekoliko puta poskoče.

Prvu i treću zamku pokazuju ova dva dijagrama: kako se mijenja napon na pinu D2 dok tipkalo dvaput zaredom pritisnemo i otpustimo. U žutim prugama tipkalo je pritisnuto.

Pin u zraku. Dok tipkalo nije pritisnuto, pin nije spojen ni s čim, pa njegov napon luta (gornji redak). Iznad 3 V Arduino sigurno čita 1, a ispod 1,5 V sigurno čita 0. Između toga može pročitati bilo što. Zato digitalRead() (donji redak) nasumično daje 1 ili 0. Samo dok je tipkalo pritisnuto, pin je sigurno na 0 V.

Napon na pinu u zraku nasumično luta između 0 i 5 V, a pročitana vrijednost skače između 1 i 0. Dok je tipkalo pritisnuto, napon je 0 V

Poskakivanje. S pull-up otpornikom, koji ćemo upoznati u Vježbi 1, otpušteno tipkalo drži pin na 5 V, a pritisnuto na 0 V. Pri svakom pritisku i otpuštanju napon nekoliko puta skoči između 5 V i 0 V prije nego što se smiri. Skokovi su nacrtani mnogo širi nego što stvarno jesu: cijelo poskakivanje traje nekoliko milisekundi.

Napon na pinu s pull-up otpornikom: pri svakom pritisku i otpuštanju nekoliko puta skoči između 5 V i 0 V prije nego što se smiri

Tri načina čitanja tipkala

Isto tipkalo program može čitati na tri načina. Pinball treba sva tri:

Što radi pinballŠto program pitaPrimjer
Palica je gore dok držiš tipku.Je li tipkalo sada pritisnuto?lijeva i desna palica
Jedan pritisak dodaje jednog igrača.Je li tipkalo upravo pritisnuto i otpušteno?jedan kratki pritisak na Start
Dugo držanje pokreće novu igru.Koliko dugo je tipkalo pritisnuto?Start držiš 1 s

Na mnogim pravim pinballima kratki pritisak na tipku Start dodaje igrača, a dulje držanje iste tipke ponovno pokreće igru. Do kraja ove lekcije naš Arduino znat će to isto.

Materijal

Svaki polaznik dobiva:

Za dodatni izazov na kraju trebaju još jedno tipkalo i još jedan otpornik od 470 Ω.

Što je program pročitao, pokazat će LED-ica na pinu D8, Serial Monitor i Serial Plotter.

Pravila za spajanje

  1. Dok spajamo, Arduino je odspojen s USB kabela.
  2. Na breadboard ne spajamo 5 V. Kao i kod semafora, s Arduina nam treba samo GND.
  3. GND Arduina spajamo na desnu (–) liniju.
  4. Sve spajamo u gornjoj polovici ploče, u retke 1 do 30.
  5. Prije uključivanja voditelj pregleda spoj.

Kako tipkalo stoji na ploči i koje su mu nožice uvijek spojene, piše na stranici Breadboard.

Vježba 1: Tipkalo bez vanjskog otpornika

U lekciji Dva semafora na križanju pin koji čita poruku imao je pull-down otpornik od 10 kΩ. Bez njega bi pin bio u zraku i čitao nasumično HIGH ili LOW. Tipkalu treba isto: dok nije pritisnuto, nešto mora držati pin u poznatom stanju.

Ovaj put taj otpornik nećemo staviti na ploču. Arduino ga već ima unutra, u svom čipu, samo ga treba uključiti. Taj ugrađeni otpornik spaja pin s 5 V, pa je to pull-up otpornik. Zato tipkalo spajamo s pinom na jednoj strani i s GND na drugoj:

TipkaloŠto pin vididigitalRead()
otpuštenougrađeni otpornik povlači pin prema 5 VHIGH
pritisnutotipkalo spaja pin s GNDLOW
Tipkalo spojeno između pina D2 i GND preko otpornika od 470 oma, a crvena LED-ica na pinu D8 preko otpornika od 220 oma

Spajanje

LED-icu spajamo kao pješački semafor u lekciji Semafor za automobile i pješake: žica s D8 ulazi u lijevu polovicu retka 3, otpornik od 220 Ω stoji preko središnjeg utora, a LED-ica u desnoj polovici.

Ispod nje je tipkalo. Žica s D2 ulazi u lijevu polovicu retka 8. Otpornik od 470 Ω leži uz lijevu polovicu ploče, od retka 8 do retka 12. Tipkalo stoji preko središnjeg utora, s nožicama u retcima 12 i 14, kao u primjeru na stranici Breadboard.

DioArduino pinŽica s ArduinaOtpornikDesna strana
LED-icaD8a3220 Ω: e3–f3crvena LED-ica: dulja nožica u j3, kraća u desnu – liniju
TipkaloD2a8470 Ω: b8–b12tipkalo u e12, e14, f12, f14; žica iz j14 u desnu – liniju
Zajednički GNDGNDdesna – linija––
Spajanje: D8 u a3, otpornik od e3 do f3, LED-ica iz j3 u desnu minus liniju; D2 u a8, otpornik od b8 do b12, tipkalo preko središnjeg utora u retcima 12 i 14, žica iz j14 u desnu minus liniju

Tipkalo koristi dvije dijagonalne nožice: e12 i f14. Pritisak spaja redak 12 s retkom 14, pa put ide ovako: D2 ⇒ redak 8 ⇒ otpornik ⇒ redak 12 ⇒ tipkalo ⇒ redak 14 ⇒ GND.

Čemu služi svaki dio?

  • LED-ica s otpornikom od 220 Ω pokazuje što je program pročitao. Dulja nožica (anoda) ide prema otporniku i pinu, a kraća (katoda) prema GND. Kao na semaforu, struja je oko 14 mA.
  • Tipkalo prema GND, a ne prema 5 V. Na ploči nema 5 V, pa ni pritisnuto tipkalo ne može spojiti 5 V s GND.
  • Otpornik 470 Ω: zaštita. Arduino pamti zadnji program koji smo na njega učitali, i nakon što ga isključimo. Ako je na njemu još program semafora A iz prošle lekcije, pin D2 je izlaz i šalje HIGH. Pritisak na tipkalo tada bi taj pin spojio izravno s GND. Takav kratki spoj može oštetiti pin. S otpornikom struja je najviše 5 V / 470 Ω ≈ 11 mA, a to pinu ne škodi. Ugrađeni pull-up otpornik ima nekoliko desetaka kΩ, pa je 470 Ω prema njemu malen i pritisnuto tipkalo i dalje daje LOW.

Prvi program

// C++
const int tipkalo = 2;
const int ledPin = 8;

void setup() {
  pinMode(tipkalo, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
}

void loop() {
  int stanje = digitalRead(tipkalo);
  digitalWrite(ledPin, stanje);
}

Učitaj program i pritisni tipkalo nekoliko puta. Kada svijetli LED-ica?

Zašto LED-ica svijetli kada tipkalo NIJE pritisnuto?
Otpušteno tipkalo daje HIGH, jer ugrađeni pull-up otpornik povlači pin prema 5 V. Program to stanje šalje ravno na LED-icu, pa ona svijetli. Pritisnuto tipkalo spaja pin s GND i daje LOW, pa se LED-ica ugasi.

Što je novo u programu?

  • pinMode(tipkalo, INPUT_PULLUP); – Pin je ulaz, kao INPUT u prošloj lekciji, ali s uključenim ugrađenim pull-up otpornikom. Na ploči zato ne treba vanjski otpornik prema 5 V.
  • Pritisnuto je LOW. S pull-up otpornikom logika je obrnuta: LOW znači pritisnuto, a HIGH otpušteno. Na to ćemo paziti u svakom programu s tipkalima.

Neka LED-ica svijetli dok držimo tipkalo

loop() zamijeni ovim:

// C++
void loop() {
  if (digitalRead(tipkalo) == LOW) {   // LOW znači: pritisnuto
    digitalWrite(ledPin, HIGH);
  } else {
    digitalWrite(ledPin, LOW);
  }
}

Sada LED-ica svijetli dok držiš tipkalo. Točno tako radi palica pinballa: gore je dok držiš tipku. Ovo je prvi način čitanja tipkala: program pita je li tipkalo sada pritisnuto?

Eksperiment: pin u zraku

U setup() promijeni INPUT_PULLUP u INPUT i učitaj program. Sada ugrađeni otpornik nije uključen, pa dok tipkalo nije pritisnuto, pin nije spojen ni s čim. Pogledaj LED-icu: može svijetliti, biti ugašena, slabije svijetliti ili se mijenjati kada prstom dotakneš otpornik ili žicu. Pin hvata smetnje iz okoline. To je zabluda 10 na stranici Zablude: nespojeni ulaz ne čita 0, nego nasumično.

Vrati INPUT_PULLUP i ponovo učitaj program.

Zašto pull-up, a ne pull-down?

U prošloj lekciji koristili smo vanjski pull-down otpornik, pa je HIGH značio poruku. Zašto tipkalo sada spajamo obrnuto?

Čip Arduina Uno ima ugrađene samo pull-up otpornike. Kada tipkalo spojimo prema GND, ne treba nam nijedan dodatni dio, a na ploču ne moramo dovesti 5 V. Zato gotovo svi uređaji s tipkalima, od daljinskog upravljača do pinballa, tipkala spajaju prema GND. Obrnutu logiku program lako popravi: == LOW umjesto == HIGH.

Vježba 2: Brojimo pritiske

Sada želimo brojati pritiske, kao tipka Start koja broji igrače. Prvi pokušaj izgleda jednostavno: kada je tipkalo pritisnuto, brojač povećamo za jedan. Da vidimo što se događa, program svakih 100 ms ispiše dvije vrijednosti: što pin D2 upravo čita i koliko je brojač.

// C++
const int tipkalo = 2;
const unsigned long ISPIS = 100;       // ispisujemo svakih 100 ms
unsigned long brojac = 0;              // unsigned long, jer će brojač rasti jako brzo
unsigned long zadnjiIspis = 0;         // kada smo zadnji put ispisali

void setup() {
  pinMode(tipkalo, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  if (digitalRead(tipkalo) == LOW) {
    brojac = brojac + 1;
  }

  if (millis() - zadnjiIspis >= ISPIS) {   // je li vrijeme za ispis?
    zadnjiIspis = millis();
    Serial.print("Tipkalo:");
    Serial.print(digitalRead(tipkalo));
    Serial.print(", Brojac:");
    Serial.println(brojac);
  }
}

Otvori Serial Monitor (9600 baud) i pritisni tipkalo jednom, kratko.

Zašto brojač nakon jednog pritiska pokazuje desetke tisuća?
loop() se vrti desetke tisuća puta u sekundi. Dok držiš tipkalo, svaki prolaz vidi LOW i povećava brojač. Program je brojao koliko je puta pogledao tipkalo dok je bilo pritisnuto, a ne koliko si ga puta pritisnuo.

Što je novo u programu?

  • Serial.print("Tipkalo:"); – U jednom retku ispisujemo dvije vrijednosti: što pin čita (1 je HIGH, 0 je LOW) i brojač. Oznake s dvotočkom daju linijama imena u Serial Plotteru, kao u lekciji Djelilo napona, Arduino i PWM.
  • if (millis() - zadnjiIspis >= ISPIS) – Ispisujemo svakih 100 ms, a ne u svakom prolazu kroz loop(). Ispis je spor: pri 9600 baud jedan ovakav redak putuje do računala više od 20 ms. Da ispisujemo u svakom prolazu, loop() bi čekao ispis i tipkalo bi čitao manje od 50 puta u sekundi. Ovako ga čita u svakom prolazu, a ispisuje 10 puta u sekundi. Isti oblik s millis() upoznali smo na semaforima.
  • unsigned long brojac – Brojač će narasti veći nego što stane u int (najviše 32767). U unsigned long stanu mnogo veći brojevi, kao vrijeme iz millis().

Pritisak je promjena

Pritisak nije tipkalo je pritisnuto, nego trenutak u kojem se tipkalo promijeni iz otpuštenog u pritisnuto. Da bi ga primijetio, program mora pamtiti kakvo je tipkalo bilo u prošlom prolazu.

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
pročitaj tipkalo
ako je drukčije nego zadnji put:
zapamti novo stanje
ako je sada pritisnuto:
povećaj brojač
svakih 100 ms ispiši tipkalo i brojač

Iznad setup() sada je i varijabla zadnjeStanje, a dio s ispisom ostaje isti:

// C++
const int tipkalo = 2;
const unsigned long ISPIS = 100;     // ispisujemo svakih 100 ms
int brojac = 0;
int zadnjeStanje = HIGH;             // kakvo je tipkalo bilo zadnji put: na početku otpušteno
unsigned long zadnjiIspis = 0;       // kada smo zadnji put ispisali

void setup() {
  pinMode(tipkalo, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  int procitano = digitalRead(tipkalo);

  if (procitano != zadnjeStanje) {   // tipkalo se promijenilo
    zadnjeStanje = procitano;
    if (procitano == LOW) {          // promjena u pritisnuto: to je pritisak
      brojac = brojac + 1;
    }
  }

  if (millis() - zadnjiIspis >= ISPIS) {   // je li vrijeme za ispis?
    zadnjiIspis = millis();
    Serial.print("Tipkalo:");
    Serial.print(digitalRead(tipkalo));
    Serial.print(", Brojac:");
    Serial.println(brojac);
  }
}

Učitaj program i otvori Serial Plotter (izbornik Tools, 9600 baud). Pritisni tipkalo nekoliko puta, polako. Linija Tipkalo je na 1 dok je tipkalo otpušteno i padne na 0 dok ga držiš. Linija Brojac pri svakom pritisku naraste za jednu stepenicu.

Što je novo u programu?

  • int zadnjeStanje = HIGH; – Pamti stanje tipkala iz prošlog prolaza. Napisana je iznad setup(), pa vrijednost ostaje zapamćena između dva prolaza kroz loop() (globalna varijabla iz prošle lekcije).
  • != – Nije jednako. Suprotno od ==. Uvjet je istinit kada se pročitano stanje razlikuje od zapamćenog, dakle kada se tipkalo upravo promijenilo.
  • if (procitano == LOW) – Tipkalo se može promijeniti na dva načina: kada ga pritisnemo i kada ga otpustimo. Brojimo samo prvu promjenu.
  • int brojac = 0; – Brojač je opet int. Sada broji samo pritiske, pa neće narasti velik.

Poskakivanje kontakata

Pritisni tipkalo 20 puta, polako, i prati brojač. Je li stigao točno do 20? Kod većine tipkala brojač ponekad skoči za 2 ili 3, iako si pritisnuo samo jednom. U Serial Plotteru to se vidi kao jedan pad linije Tipkalo uz koji Brojac naraste za dvije ili tri stepenice odjednom. Ako se to tvom tipkalu ne događa, pokušaj pritiskati lagano ili ga brzo otpuštati.

Program nije pogriješio. Tipkalo stvarno nekoliko puta spoji i odspoji. Kada se metalni kontakti u tipkalu udare jedan o drugi, odskoče, kao lopta koja padne na pod, i tek se nakon nekoliko milisekundi smire. Isto se događa i kada tipkalo otpustimo. Čovjek to ne može primijetiti, ali Arduino tipkalo pročita mnogo puta u svakoj milisekundi i vidi svaki skok. Ta pojava zove se poskakivanje kontakata (engleski contact bounce).

Same skokove Plotter ne pokazuje. Program ispisuje svakih 100 ms, a kontakti se smire za nekoliko milisekundi, pa ispis gotovo uvijek uhvati tipkalo koje je već mirno. Vidimo samo posljedicu: brojač koji poskoči.

Vremenski dijagram: tipkalo pri pritisku tri puta poskoči, a pri otpuštanju jednom. Brojač bez smirivanja broji četiri pritiska, a brojač sa smirivanjem jedan

Na dijagramu je poskakivanje nacrtano sporije nego što stvarno traje, da bi se vidio svaki skok. Gornji redak pokazuje što Arduino čita na pinu D2. Program iz ove vježbe vidi tri pritiska dok se tipkalo smiruje, a pri otpuštanju još jedan. Brojač je zato nakon jednog pritiska na 4. Donji redak pokazuje brojač iz sljedeće vježbe.

Vježba 3: Smirivanje

Kontakti se smire za nekoliko milisekundi, a čovjek ne može pritisnuti tipkalo dvaput u 50 ms. Zato uvodimo jednostavno pravilo: nakon svake promjene 50 ms ne gledamo tipkalo. Dok se kontakti smiruju, program ih ne čita, pa skokove ne vidi. To se zove smirivanje tipkala (engleski debounce).

Žute pruge na dijagramu su tih 50 ms. Prva promjena odmah se broji, a skokovi nakon nje padnu u žutu prugu i program ih ne vidi.

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
ako je od zadnje promjene prošlo barem 50 ms:
pročitaj tipkalo
ako je drukčije nego zadnji put:
zapamti sadašnje vrijeme i novo stanje
ako je sada pritisnuto:
povećaj brojač
svakih 100 ms ispiši tipkalo i brojač

Ovo je isti oblik s millis() koji već koristimo za ispis: je li već prošlo dovoljno vremena? Sada ga program koristi dvaput, za smirivanje i za ispis.

// C++
const int tipkalo = 2;
const unsigned long SMIRIVANJE = 50;   // nakon promjene 50 ms ne gledamo tipkalo
const unsigned long ISPIS = 100;       // ispisujemo svakih 100 ms

int brojac = 0;
int zadnjeStanje = HIGH;               // kakvo je tipkalo bilo zadnji put
unsigned long zadnjaPromjena = 0;      // kada se tipkalo zadnji put promijenilo
unsigned long zadnjiIspis = 0;         // kada smo zadnji put ispisali

void setup() {
  pinMode(tipkalo, INPUT_PULLUP);
  Serial.begin(9600);
}

void loop() {
  if (millis() - zadnjaPromjena >= SMIRIVANJE) {   // jesu li se kontakti smirili?
    int procitano = digitalRead(tipkalo);

    if (procitano != zadnjeStanje) {               // tipkalo se promijenilo
      zadnjaPromjena = millis();
      zadnjeStanje = procitano;
      if (procitano == LOW) {                      // promjena u pritisnuto: to je pritisak
        brojac = brojac + 1;
      }
    }
  }

  if (millis() - zadnjiIspis >= ISPIS) {           // je li vrijeme za ispis?
    zadnjiIspis = millis();
    Serial.print("Tipkalo:");
    Serial.print(digitalRead(tipkalo));
    Serial.print(", Brojac:");
    Serial.println(brojac);
  }
}

Ponovo pritisni tipkalo 20 puta. Sada brojač mora stići točno do 20. U Serial Plotteru Brojac pri svakom padu linije Tipkalo naraste za točno jednu stepenicu.

Što je novo u programu?

  • const unsigned long SMIRIVANJE = 50; – Koliko dugo nakon promjene ne gledamo tipkalo. Tip je unsigned long jer ga uspoređujemo s vremenom iz millis(), kao NAJDULJE_CEKANJE na semaforu.
  • if (millis() - zadnjaPromjena >= SMIRIVANJE) – Tipkalo čitamo tek kada je od zadnje promjene prošlo barem 50 ms. Dok to vrijeme ne prođe, loop() samo pogleda na sat i odmah ide dalje.
  • zadnjaPromjena = millis(); – Pamtimo trenutak promjene, kod pritiska i kod otpuštanja. Od njega brojimo sljedećih 50 ms.
  • Serial.print(digitalRead(tipkalo)); – Varijabla procitano postoji samo unutar vitičastih zagrada { } u kojima je napisana, pa za ispis tipkalo čitamo ponovno.
  • Prva promjena broji se odmah. Program ne čeka da se kontakti smire, nego reagira na prvi dodir, a smirivanje vrijedi tek nakon njega. Zato bi i palica pinballa s ovakvim programom krenula istog trenutka kada igrač pritisne tipku.

Zašto ne delay(50)?

Smirivanje bismo mogli napisati i s delay(50) iza svakog pritiska. Brojač bi radio, ali Arduino bi za vrijeme tih 50 ms bio gluh i slijep: ne bi čitao drugo tipkalo, ne bi pomaknuo palicu i ne bi osvježio zaslon. Pinball ima dvije palice, tipku Start i mnoge prekidače na stolu. S millis() program ne čeka, nego samo pogleda na sat, pa može istodobno paziti na sve njih.

Eksperimenti

1. Bez smirivanja
Promijeni SMIRIVANJE u 0 i učitaj program. Brojač opet ponekad skoči za 2 ili 3, kao u Vježbi 2.

2. Predugo smirivanje
Promijeni SMIRIVANJE u 500 i pritiskaj tipkalo što brže možeš, 10 puta. Koliko je pritisaka program izbrojio? Vrati SMIRIVANJE na 50.

Zašto program u eksperimentu 2 izgubi neke pritiske?
Nakon pritiska program pola sekunde ne gleda tipkalo, a nakon otpuštanja još pola sekunde. Ako pritisneš i otpustiš tipkalo brže od toga, program jednu promjenu uopće ne vidi. Smirivanje mora biti dulje od poskakivanja kontakata, ali kraće od najbržeg pritiska koji čovjek može napraviti. 50 ms je dobra sredina.

Vježba 4: Pritisak ili držanje?

Sada ćemo napraviti tipku Start pravog pinballa:

  • kratki pritisak dodaje jednog igrača, najviše do 4;
  • držanje dulje od 1 s pokreće novu igru s jednim igračem;
  • LED-ica svijetli dok držimo tipkalo, kao palica u Vježbi 1.

Kada program zna je li pritisak bio kratak? Dok je tipkalo pritisnuto, to još ne zna: možda ga igrač upravo drži za novu igru. Zato kratki pritisak prepoznajemo tek kada se tipkalo otpusti. Držanje prepoznajemo dok je tipkalo još pritisnuto, čim prođe 1 s. Tada moramo zapamtiti da smo držanje već javili, inače bi otpuštanje nakon držanja dodalo još jednog igrača.

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
ako se tipkalo promijenilo (sa smirivanjem kao u Vježbi 3):
ako je upravo pritisnuto:
zapamti kada je pritisnuto
držanje još nije javljeno
ako je upravo otpušteno, a držanje nije javljeno:
to je bio kratki pritisak: dodaj igrača (najviše 4)
ako je tipkalo pritisnuto barem 1 s, a držanje još nije javljeno:
javi držanje: nova igra s jednim igračem
LED-ica svijetli dok je tipkalo pritisnuto

Cijeli program

// C++
const int tipkalo = 2;
const int ledPin = 8;
const unsigned long SMIRIVANJE = 50;   // nakon promjene 50 ms ne gledamo tipkalo
const unsigned long DRZANJE = 1000;    // toliko dugo držimo tipkalo za novu igru

int zadnjeStanje = HIGH;               // kakvo je tipkalo bilo zadnji put
unsigned long zadnjaPromjena = 0;      // kada se tipkalo zadnji put promijenilo
unsigned long pocetakPritiska = 0;     // kada je tipkalo pritisnuto
bool drzanjeJavljeno = false;          // jesmo li već javili držanje
int igraci = 0;                        // broj igrača, od 0 do 4

void setup() {
  pinMode(tipkalo, INPUT_PULLUP);
  pinMode(ledPin, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  if (millis() - zadnjaPromjena >= SMIRIVANJE) {   // jesu li se kontakti smirili?
    int procitano = digitalRead(tipkalo);

    if (procitano != zadnjeStanje) {               // tipkalo se promijenilo
      zadnjaPromjena = millis();
      zadnjeStanje = procitano;

      if (procitano == LOW) {                      // upravo pritisnuto
        pocetakPritiska = millis();
        drzanjeJavljeno = false;
      } else if (!drzanjeJavljeno) {               // upravo otpušteno nakon kratkog pritiska
        if (igraci < 4) {
          igraci = igraci + 1;
        }
        Serial.print("Igrača: ");
        Serial.println(igraci);
      }
    }
  }

  // Držimo li tipkalo već dovoljno dugo?
  if (zadnjeStanje == LOW && !drzanjeJavljeno && millis() - pocetakPritiska >= DRZANJE) {
    drzanjeJavljeno = true;
    igraci = 1;
    Serial.println("Nova igra! Igrača: 1");
  }

  digitalWrite(ledPin, zadnjeStanje == LOW);  // LED-ica svijetli dok je tipkalo pritisnuto
}

Otvori Serial Monitor i isprobaj:

  1. Pritisni tipkalo kratko tri puta. Program ispisuje 1, 2 i 3 igrača, svaki put tek kada otpustiš tipkalo.
  2. Pritisni ga još tri puta. Igrača ima najviše 4.
  3. Drži tipkalo. Nakon jedne sekunde program ispiše Nova igra!, iako tipkalo još držiš. Kada ga otpustiš, ne dodaje se nijedan igrač.

Što je novo u programu?

  • const unsigned long DRZANJE = 1000; – Koliko dugo treba držati tipkalo za novu igru: 1000 ms je jedna sekunda.
  • pocetakPritiska = millis(); – Pamtimo trenutak pritiska. Iz njega kasnije računamo koliko dugo tipkalo držimo: millis() - pocetakPritiska.
  • else if (!drzanjeJavljeno) – else if znači inače, ako. Prvi uvjet bio je upravo pritisnuto, pa ovaj dio radi kada je tipkalo upravo otpušteno, ali samo ako držanje nije javljeno. Tada je pritisak bio kratak.
  • if (igraci < 4) – Igrača može biti najviše četiri. Peti kratki pritisak ne mijenja ništa.
  • bool drzanjeJavljeno – Bez nje program bi novu igru javljao u svakom prolazu kroz loop() dok držimo tipkalo, a nakon otpuštanja još bi i dodao igrača. Ova varijabla pamti da je držanje već obavljeno, pa se ono javi samo jednom.
  • Provjera držanja stoji izvan smirivanja. Dok tipkalo držimo, ono se ne mijenja, pa unutarnji if ništa ne radi. Koliko dugo ga držimo, program mora provjeravati u svakom prolazu.
  • digitalWrite(ledPin, zadnjeStanje == LOW); – Usporedba zadnjeStanje == LOW daje true ili false, a za Arduino je to isto što i HIGH i LOW. Jednim retkom LED-ica prati tipkalo.
  • U loop() nema nijednog delay(). Program istodobno smiruje tipkalo, mjeri koliko ga dugo držimo i pali LED-icu.
Igrač drži tipku Start pola sekunde i otpusti je. Što ispiše program?
Pritisak je bio kraći od 1 s, pa držanje nije javljeno. Kada igrač otpusti tipkalo, program vidi kratki pritisak i doda jednog igrača.

Vježba 5: Tipka Start s bibliotekom Bounce2

Smirivanje treba gotovo svaki program s tipkalima, pa su ga drugi programeri već napisali i objavili kao biblioteku: gotov kod koji uključimo u svoj program. Jedna od najpoznatijih biblioteka za tipkala zove se Bounce2. Napravit ćemo istu tipku Start kao u Vježbi 4, ali smirivanje i mjerenje vremena prepustit ćemo biblioteci. Spoj ostaje isti.

Instaliranje biblioteke

  1. U Arduino IDE-u otvori Library Manager: ikona s knjigama na lijevoj strani ili izbornik Sketch ⇒ Include Library ⇒ Manage Libraries…
  2. U tražilicu upiši Bounce2.
  3. Instaliraj biblioteku Bounce2 autora Thomas O Fredericks.

Za instaliranje treba internet. Ako ga u radionici nema, voditelj biblioteku unaprijed instalira na svako računalo.

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
neka biblioteka pročita i smiri tipkalo
ako je upravo pritisnuto:
držanje još nije javljeno
ako je upravo otpušteno, a držanje nije javljeno:
to je bio kratki pritisak: dodaj igrača (najviše 4)
ako je tipkalo pritisnuto barem 1 s, a držanje još nije javljeno:
javi držanje: nova igra s jednim igračem
LED-ica svijetli dok je tipkalo pritisnuto

Plan je gotovo isti kao u Vježbi 4. Nema samo dijela zapamti kada je pritisnuto, jer to pamti biblioteka.

Program s bibliotekom

// C++
#include <Bounce2.h>                   // biblioteka za smirivanje tipkala

const int tipkalo = 2;
const int ledPin = 8;
const unsigned long DRZANJE = 1000;    // toliko dugo držimo tipkalo za novu igru

Bounce2::Button start;                 // tipka Start
bool drzanjeJavljeno = false;          // jesmo li već javili držanje
int igraci = 0;                        // broj igrača, od 0 do 4

void setup() {
  start.attach(tipkalo, INPUT_PULLUP); // tipkalo je na D2, s ugrađenim pull-up otpornikom
  start.interval(10);                  // promjena vrijedi kada je tipkalo mirno 10 ms
  start.setPressedState(LOW);          // pritisnuto je LOW
  pinMode(ledPin, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  start.update();                      // pročitaj i smiri tipkalo

  if (start.pressed()) {               // upravo pritisnuto
    drzanjeJavljeno = false;
  }

  if (start.released() && !drzanjeJavljeno) {   // upravo otpušteno nakon kratkog pritiska
    if (igraci < 4) {
      igraci = igraci + 1;
    }
    Serial.print("Igrača: ");
    Serial.println(igraci);
  }

  // Držimo li tipkalo već dovoljno dugo?
  if (start.isPressed() && !drzanjeJavljeno && start.currentDuration() >= DRZANJE) {
    drzanjeJavljeno = true;
    igraci = 1;
    Serial.println("Nova igra! Igrača: 1");
  }

  digitalWrite(ledPin, start.isPressed());  // LED-ica svijetli dok je tipkalo pritisnuto
}

Učitaj program i ponovi sva tri pokusa iz Vježbe 4. Program mora raditi isto.

Što je novo u programu?

  • #include <Bounce2.h> – Uključuje biblioteku u program. Ako biblioteka nije instalirana, prevođenje javi grešku Bounce2.h: No such file or directory.
  • Bounce2::Button start; – start je objekt: varijabla koja uz svoje podatke ima i svoje funkcije. Funkcije objekta pozivamo s točkom, na primjer start.update(), kao što već pozivamo Serial.begin() i Serial.print(). Bounce2:: znači da tip Button dolazi iz biblioteke Bounce2. Objekt sam pamti ono što smo u Vježbi 4 pamtili u varijablama zadnjeStanje, zadnjaPromjena i pocetakPritiska.
  • start.attach(tipkalo, INPUT_PULLUP); – Veže objekt uz pin D2 i postavlja pin kao ulaz s pull-up otpornikom, pa pinMode() za tipkalo ne pišemo.
  • start.interval(10); – Koliko dugo tipkalo mora biti mirno da bi biblioteka prihvatila promjenu: 10 ms. Zašto je to manje od 50 ms u Vježbi 3, piše niže.
  • start.setPressedState(LOW); – Biblioteci jednom kažemo da pritisnuto znači LOW. Dalje u programu obrnutu logiku više ne pišemo: start.isPressed() daje true kada je tipkalo pritisnuto.
  • start.update(); – Biblioteka pročita pin i smiri ga. Zato mora biti u svakom prolazu kroz loop(), jednom, prije svih pitanja o tipkalu.
  • start.pressed() i start.released() – Daju true samo u prolazu u kojem je tipkalo upravo pritisnuto ili otpušteno.
  • start.isPressed() – Daje true dok je tipkalo pritisnuto.
  • start.currentDuration() – Koliko je milisekundi tipkalo u sadašnjem stanju. Dok ga držimo, to je koliko ga dugo držimo, pa nam ne treba pocetakPritiska.

Tri načina čitanja tipkala s početka lekcije u biblioteci imaju svoje funkcije: sada je isPressed(), upravo su pressed() i released(), a koliko dugo je currentDuration().

Što je drukčije?

Obje vježbe smiruju tipkalo, ali na različite načine:

  • Vježba 3 i 4: program reagira na prvu promjenu, a zatim 50 ms ne gleda tipkalo.
  • Bounce2: promjena vrijedi tek kada je tipkalo mirno 10 ms. Svaki skok kontakata ponovno pokrene tih 10 ms, pa biblioteka pričeka da se kontakti smire i tek tada javi pritisak.

Zato je biblioteci dovoljno 10 ms: ne mora čekati dulje od cijelog poskakivanja, nego samo malo dulje od razmaka između dva skoka.

Vježba 4: naš kodVježba 5: Bounce2
Kada program vidi pritisak?odmah, pri prvom dodiru kontakatakada se kontakti smire i ostanu mirni 10 ms
Kratka smetnja, kraća od 10 msbroji se kao pritisakne broji se
Što program pamti za tipkalotri varijablejedan objekt
Obrnuta logika (LOW je pritisnuto)== LOW na više mjestajednom, u setPressedState(LOW)
Drugo tipkalojoš tri varijable i sav kod još jednomjoš jedan objekt
Što treba instaliratiništabiblioteku Bounce2

Prednosti biblioteke:

  • Program je kraći i lakše se čita. Funkcije imaju imena koja kažu što pitaju: pressed(), released(), isPressed().
  • Manje je mjesta za greške. Ne može se dogoditi da zaboravimo zapamtiti vrijeme promjene ili da negdje napišemo == HIGH umjesto == LOW.
  • Više tipkala je lako: svako tipkalo dobije svoj objekt, na primjer lijevo i desno za palice. Dodatni izazov 1 s bibliotekom postaje mnogo kraći.
  • Biblioteku koriste mnogi ljudi u mnogim projektima, pa su njezine greške većinom već pronađene i popravljene.
  • Kratka smetnja na žici, na primjer kada prstom dotakneš žicu, ne broji se kao pritisak.

Mane biblioteke:

  • Treba je instalirati na svako računalo, a za to treba internet. Program koji nekome pošalješ ne prevodi se dok on ne instalira istu biblioteku.
  • Ne vidimo kako radi. Da nismo prvo sami napisali smirivanje, ne bismo znali što znači interval(10) ni zašto update() mora biti u svakom prolazu.
  • Pritisak stiže malo kasnije: tek kada se kontakti smire i prođe još 10 ms. Za tipku Start to nitko neće primijetiti. Kod palica pinballa važna je svaka milisekunda, pa je ondje bolje smirivanje koje reagira odmah, kao naše.
  • Biblioteka se s vremenom mijenja. Funkcija currentDuration() u starijim se verzijama zvala duration(), pa primjeri s interneta ne odgovaraju uvijek verziji koju imaš.
  • Zauzima malo više memorije: ovaj program je oko 270 bajtova veći od programa iz Vježbe 4. Arduino Uno ima 32 KB za programe, pa nam to danas ne smeta.

Kada razumiješ kako nešto radi, biblioteka štedi vrijeme i smanjuje broj grešaka. Zato smo smirivanje prvo napisali sami: sada znamo što biblioteka radi umjesto nas.

Zašto Bounce2 ne broji smetnju kraću od 10 ms, a naš program iz Vježbe 3 je broji?

Naš program reagira na prvu promjenu, pa mu je i kratka smetnja pritisak. Tek nakon nje 50 ms ne gleda tipkalo. Bounce2 promjenu prihvati tek kada tipkalo ostane mirno 10 ms. Smetnja nestane prije toga, pa je biblioteka nikada ne javi.

Bounce2 zna smirivati i kao naš program: reagirati odmah i zatim neko vrijeme ne gledati tipkalo (engleski lock-out). Taj se način ne uključuje u našem programu, nego u datoteci same biblioteke, Bounce2.h, pa ga ovdje ne koristimo.

Ako nešto ne radi

  • LED-ica nikada ne svijetli ⇒ Je li dulja nožica LED-ice u j3, a kraća u desnoj – liniji? Okrenuta LED-ica ne svijetli. Je li žica s D8 u a3, a otpornik od e3 do f3?
  • LED-ica ne reagira na tipkalo ili brojač stoji ⇒ Je li tipkalo preko središnjeg utora, s nožicama u retcima 12 i 14? Je li žica iz j14 u desnoj – liniji, a GND Arduina u istoj liniji? Je li žica s D2 u a8, a otpornik od b8 do b12?
  • LED-ica svijetli i kada tipkalo ne diramo, a gasi se kada ga pritisnemo ⇒ Tako radi prvi program u Vježbi 1. Popravak je drugi loop(), s == LOW.
  • LED-ica ili brojač mijenja se sam od sebe ⇒ U setup() piše INPUT umjesto INPUT_PULLUP.
  • Brojač stalno raste i kada tipkalo ne diramo ⇒ Tipkalo je okrenuto za 90 stupnjeva, pa je pin stalno spojen s GND. Okreni ga i pogledaj nožice na stranici Breadboard.
  • Serial Monitor ili Serial Plotter ne pokazuje ništa ili pokazuje čudne znakove ⇒ Odaberi 9600 baud.
  • Brojač i sa smirivanjem ponekad skoči za 2 ⇒ Neka tipkala poskakuju dulje. Povećaj SMIRIVANJE na 100.
  • Greška Bounce2.h: No such file or directory ⇒ Biblioteka Bounce2 nije instalirana. Instaliraj je u Library Manageru, kako piše u Vježbi 5.
  • U Vježbi 5 program ne reagira na tipkalo ⇒ Je li start.update(); na početku loop()? Bez njega biblioteka tipkalo nikada ne pročita.
  • Program se ne može učitati ⇒ Provjeri jesu li u Arduino IDE-u odabrani ploča Arduino Uno i pravi port.

Dodatni izazovi

  1. Dvije palice. Dodaj drugo tipkalo na D4, spojeno na isti način kao prvo: žica s D4 u a18, otpornik od b18 do b22, tipkalo u e22, e24, f22 i f24, žica iz j24 u desnu – liniju. Neka program u Serial Monitoru ispiše lijevo ili desno pri svakom pritisku. Program možeš napisati na dva načina. Bez biblioteke, kao u Vježbi 3, svako tipkalo treba svoje varijable zadnjeStanje i zadnjaPromjena. S bibliotekom Bounce2, kao u Vježbi 5, svako tipkalo dobije svoj objekt, na primjer lijevo i desno. Napiši oba programa: koji je kraći? Radi li smirivanje kada pritisneš oba tipkala istodobno?
  2. Koliko dugo? Neka program pri svakom otpuštanju ispiše koliko je milisekundi tipkalo bilo pritisnuto. Koliko traje tvoj najkraći pritisak?
  3. Servisni način. Na pravim pinballima voditelj ima tipke kojima ulazi u servisni način i provjerava dijelove stola. Neka držanje dulje od 3 s ispiše Servis, a držanje od 1 do 3 s i dalje pokreće novu igru. Savjet: o tome koje je držanje bilo, program može odlučiti tek kada se tipkalo otpusti.

Što smo naučili?

  • pinMode(pin, INPUT_PULLUP) uključuje ugrađeni pull-up otpornik, pa tipkalo spajamo samo između pina i GND, bez vanjskog otpornika.
  • S pull-up otpornikom pritisnuto tipkalo daje LOW, a otpušteno HIGH.
  • Pin koji nije nigdje spojen čita nasumično. Tipkalu uvijek treba pull-up ili pull-down otpornik.
  • Otpornik od 470 Ω prema tipkalu štiti pin ako je greškom izlaz.
  • Tipkalo čitamo na tri načina: je li sada pritisnuto, je li upravo pritisnuto i otpušteno te koliko dugo je pritisnuto.
  • Pritisak je promjena: program mora pamtiti prijašnje stanje i usporediti ga s novim. != znači nije jednako.
  • Ispis je spor. Zato ispisujemo svakih 100 ms s millis(), a ne u svakom prolazu, pa program tipkalo i dalje čita brzo. Serial Plotter od ispisanih vrijednosti crta grafikon.
  • Metalni kontakti pri pritisku i otpuštanju nekoliko puta poskoče. Zato tipkalo smirujemo: nakon svake promjene neko vrijeme ga ne gledamo.
  • Smirivanje s millis() ne zaustavlja program, pa Arduino može istodobno paziti na više tipkala.
  • Kratki pritisak prepoznajemo tek kada se tipkalo otpusti, a držanje dok je još pritisnuto. Varijabla tipa bool pamti je li nešto već javljeno.
  • Biblioteka je gotov kod koji uključimo s #include. Bounce2 smiruje tipkalo i mjeri koliko ga dugo držimo, a prije prvog korištenja treba je instalirati.
  • Bounce2 promjenu prihvati tek kada je tipkalo mirno, pa ne broji kratke smetnje, ali pritisak javi malo kasnije od našeg programa.

Mali kviz

1. Zašto s INPUT_PULLUP pritisnuto tipkalo daje LOW?
Ugrađeni pull-up otpornik povlači pin prema 5 V, pa otpušteno tipkalo daje HIGH. Pritisnuto tipkalo spaja pin izravno s GND, pa pin čita LOW.
2. Zašto tipkalo spajamo prema GND, a ne prema 5 V?
Čip Arduina Uno ima ugrađene samo pull-up otpornike, pa uz tipkalo prema GND ne treba nijedan dodatni dio. Osim toga, na ploču tada ne moramo dovesti 5 V, pa ni pritisnuto tipkalo ne može spojiti 5 V s GND.
3. Čemu služi otpornik od 470 Ω uz tipkalo?
Štiti pin. Ako je na Arduinu još stari program u kojem je pin izlaz i šalje HIGH, pritisak na tipkalo spojio bi ga izravno s GND. Otpornik tada ograničava struju na oko 11 mA, što pinu ne škodi.
4. Zašto je prvi brojač u Vježbi 2 brojao desetke tisuća?
Brojao je svaki prolaz kroz loop() u kojem je tipkalo bilo pritisnuto, a loop() se vrti desetke tisuća puta u sekundi. Pritisak je promjena iz otpuštenog u pritisnuto, a ne samo stanje pritisnuto.
5. Što je poskakivanje kontakata?
Kada se metalni kontakti u tipkalu udare, odskoče i nekoliko se puta spoje i odspoje prije nego što se smire. To traje nekoliko milisekundi. Čovjek to ne primijeti, ali Arduino vidi svaki skok kao novi pritisak.
6. Zašto smirivanje pišemo s millis(), a ne s delay(50)?
Za vrijeme delay(50) Arduino ne radi ništa drugo: ne čita druga tipkala i ne upravlja ničim drugim. S millis() program samo pogleda je li prošlo 50 ms i odmah ide dalje, pa istodobno može paziti na više tipkala.
7. Zašto smirivanje od 500 ms nije dobro?
Program bi nakon svake promjene pola sekunde bio slijep za tipkalo, pa bi izgubio brze pritiske. Smirivanje mora biti dulje od poskakivanja (nekoliko ms), a kraće od najbržeg pritiska.
8. Zašto kratki pritisak na tipku Start prepoznajemo tek kada je otpustimo?
Dok je tipkalo pritisnuto, program ne zna hoće li ga igrač otpustiti ili držati dulje od 1 s za novu igru. Tek kada ga otpusti prije isteka 1 s, sigurno znamo da je pritisak bio kratak.
9. Čemu služi varijabla drzanjeJavljeno?
Pamti da je program već javio držanje. Bez nje program bi Nova igra! ispisivao u svakom prolazu kroz loop() dok držimo tipkalo, a nakon otpuštanja još bi i dodao igrača.
10. Navedi jednu prednost i jednu manu biblioteke Bounce2.
Prednosti: program je kraći i lakše se čita, svako tipkalo je jedan objekt, a kratke smetnje se ne broje. Mane: biblioteku treba instalirati na svako računalo, ne vidimo kako radi, a pritisak javi malo kasnije od našeg programa.