Dva semafora na križanju

Dva Arduina razgovaraju preko žica: semafor A upravlja križanjem, semafor B ga sluša i javlja kada je stao, a uz millis() oba primijete kvar

Arduino Elektronika
Dva semafora na križanju

Prošli put svaki je polaznik izgradio semafor za automobile i pješake. Na pravom križanju semafori ne rade svaki za sebe: dok jedna cesta ima zeleno, druga mora imati crveno. Semafori se zato moraju dogovarati.

Danas ćemo dva Arduina spojiti žicom. Dva polaznika koja sjede jedan do drugoga zajedno grade jedno križanje. Semafor A upravlja križanjem i javlja semaforu B kada smije krenuti, a semafor B sluša te poruke.

Naučit ćemo i novu naredbu, millis(). S njom Arduino može mjeriti vrijeme, a da pritom ne prestane pratiti što se oko njega događa. Upravo to treba semaforu B: kada se semafor A pokvari, B to mora sam primijetiti.

Na kraju ćemo dodati i drugu žicu, u suprotnom smjeru. Kroz nju B javlja semaforu A da je stao, pa A više ne mora pogađati.

Naše križanje

Semafor A upravlja jednom cestom, a semafor B poprečnom

Semafor A upravlja automobilima i pješacima na jednoj cesti, a semafor B na poprečnoj cesti. Pješaci, kao i prošli put, hodaju uz svoje automobile i prelaze poprečnu ulicu. Kada A ima zeleno, njegovi pješaci prelaze cestu semafora B, pa B tada mora imati crveno. I obrnuto.

Najvažnija pravila križanja:

  • kada jedan semafor nema crveno, drugi mora imati crveno;
  • između njih uvijek postoji barem jedna sekunda u kojoj oba imaju crveno. Tada automobil koji je ušao u križanje na žuto stigne izaći iz njega.

Zašto ne bismo samo pokrenuli oba semafora istodobno?

Mogli bismo oba semafora pokrenuti u istoj sekundi, a semaforu B dati program pomaknut za pola ciklusa. Neko vrijeme to bi radilo, ali ne dugo:

  • Nitko ne može pritisnuti dvije tipke za reset u točno istoj milisekundi.
  • Svaki Arduino ima svoj sat, a dva sata nikada ne idu potpuno jednako. Razlika je malena, ali se zbraja: nakon sat vremena semafori bi mogli biti pomaknuti i za nekoliko sekundi.
  • Kada se jedan Arduino resetira, na primjer dok na njega učitavamo program, on kreće ispočetka. Drugi nastavlja kao da se ništa nije dogodilo.

Zato semafori moraju stalno razgovarati.

Tko je glavni?

U našem križanju semafor A je glavni. On mjeri sva vremena, a semaforu B preko žice šalje samo dvije poruke: kreni i stani. B sam ne odlučuje kada će krenuti. Odlučuje samo kako će poruku pokazati svojim svjetlima.

Zašto ne mogu oba semafora biti glavna?

Zamisli da oba semafora imaju isto pravilo: „Kada drugi ima crveno, ja smijem upaliti zeleno.” Nakon uključivanja oba imaju crveno. Oba pogledaju onog drugog, oba vide crveno i oba upale zeleno!

Programeri takvu grešku zovu utrka (engleski race condition): ishod ovisi o tome tko je brži. Najjednostavniji lijek je da je samo jedan glavni. Pravo križanje zato ima jedan upravljački uređaj koji upravlja svim semaforima.

Poruka kroz žicu

Žica može prenijeti samo dva stanja: HIGH (oko 5 V) i LOW (0 V). To je dovoljno za naše dvije poruke:

Stanje žicePoruka od AŠto radi B
HIGHkrenicrveno + žuto, pa zeleno
LOWstanizeleno trepće, žuto, pa crveno

Zašto baš LOW znači stani? Sjeti se što se sve može dogoditi: netko izvuče žicu, netko izvuče USB kabel semafora A ili se A resetira dok na njega učitavamo program. U svim tim slučajevima na žici nema napona, pa B čita LOW i staje. Svaki kvar veze znači stani, a nikada kreni. Inženjeri kažu da je takva veza sigurna pri kvaru (engleski fail-safe).

Kako oba semafora rade zajedno

VrijemeSemafor AŽicaSemafor B
0–4 scrvenoLOWzeleno trepće (2 s), žuto (1 s), crveno
4–5 scrveno + žutoLOWcrveno
5–9 szeleno, pješaci A hodajuLOWcrveno
9–11 szeleno trepćeLOWcrveno
11–12 sžutoLOWcrveno
12–13 scrvenoLOWcrveno
13–14 scrvenoHIGHcrveno + žuto
14–18 scrvenoHIGHzeleno, pješaci B hodaju
⇒povratak na početak, žica opet LOW

Cijeli ciklus traje 18 s. Svaka cesta ima ista stanja kao prošli put: crveno + žuto 1 s, zeleno 4 s, treptanje 2 s i žuto 1 s. Između 3. i 4. sekunde te između 12. i 13. sekunde oba semafora imaju crveno.

Istu tablicu možemo nacrtati kao vremenski dijagram. Vrijeme teče slijeva nadesno, a svaki redak pokazuje jedan semafor, jedne pješake ili jednu žicu. Što je crta u retku semafora viša, to je semafor dalje u svom ciklusu. Sivi stupci su sekunde u kojima svi stoje.

Vremenski dijagram: semafor A, semafor B i žica veze tijekom jednog ciklusa

Semafor A ne vidi svjetla semafora B. Zato nakon poruke stani čeka 4 s: 2 s za treptanje, 1 s za žuto i još 1 s u kojoj svi imaju crveno. Tek tada pali svoje crveno + žuto. U Vježbi 6 to ćemo popraviti: B će sam javiti kada je stao.

Materijal

Svaki polaznik dobiva:

  • sve iz prošle lekcije: Arduino Uno, USB kabel, veliki breadboard, 3D-printani semafor, LED-ice za pješake, zujalicu, otpornike i spojne žice
  • 2 × otpornik 470 Ω (žuto-ljubičasto-smeđe)
  • 2 × otpornik 10 kΩ (smeđe-crno-narančasto)

Svako križanje (dva polaznika) dobiva još:

  • tri dulje spojne žice (30–50 cm) u tri boje: ljubičastu za vezu, narančastu za potvrdu (Vježba 6) i crnu za GND

Na početku svaki polaznik spaja semafor točno kao na kraju prošle lekcije (Vježbe 1, 4 i 5) i učitava njezin cijeli program. Pravila za spajanje ista su kao prošli put. Kada semafor radi kao prošli put, spremni ste.

Vježba 1: Sat unutar Arduina

Dok traje delay(), Arduino ne radi ništa drugo: samo čeka. Semafor B ne smije tako čekati, jer mora cijelo vrijeme pratiti žicu. Treba nam drukčiji način mjerenja vremena.

Svaki Arduino ima ugrađenu štopericu. Ona se pokrene kada Arduino dobije napajanje ili se resetira i broji milisekunde. Naredba millis() kaže koliko je štoperica do sada izbrojila.

Koliko je vremena prošlo?

// C++
void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println(millis());
  delay(1000);
}

Otvori Serial Monitor (9600 baud). Svaki novi broj veći je od prethodnog za otprilike 1000. Pritisni tipku za reset na Arduinu: brojanje kreće ispočetka.

Što je novo u programu?

  • millis() – Vraća broj milisekundi od uključivanja ili reseta Arduina. 1000 ms je jedna sekunda. Štopericu ne možemo zaustaviti ni vratiti na nulu iz programa: ona uvijek broji dalje.
Koliko je vremena prošlo od uključivanja ako millis() vrati 90000?
90 000 ms = 90 s, odnosno minuta i pol.

Dva svjetla, dva ritma

Sada želimo da žuto za automobile trepće svakih 0,5 s, a zeleno za pješake svakih 0,3 s. Pokušaj zamisliti kako bi to napisao s delay(). Dok čekamo pola sekunde za žuto, ne možemo promijeniti zeleno!

S millis() radimo drukčije, kao kada povremeno pogledamo na sat. Ne čekamo, nego samo provjeravamo: je li već vrijeme za promjenu? Ako nije, idemo dalje.

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
ako je od zadnje promjene žutog prošlo 500 ms:
zapamti sadašnje vrijeme
promijeni žuto
ako je od zadnje promjene zelenog za pješake prošlo 300 ms:
zapamti sadašnje vrijeme
promijeni zeleno za pješake
// C++
const int zutoAuti = 9;
const int zelenoPjesaci = 6;

unsigned long zadnjeZuto = 0;      // kada smo zadnji put promijenili žuto
unsigned long zadnjePjesaci = 0;   // kada smo zadnji put promijenili zeleno za pješake
bool zutoUpaljeno = false;
bool pjesaciUpaljeno = false;

void setup() {
  pinMode(zutoAuti, OUTPUT);
  pinMode(zelenoPjesaci, OUTPUT);
}

void loop() {
  if (millis() - zadnjeZuto >= 500) {
    zadnjeZuto = millis();
    zutoUpaljeno = !zutoUpaljeno;
    digitalWrite(zutoAuti, zutoUpaljeno);
  }

  if (millis() - zadnjePjesaci >= 300) {
    zadnjePjesaci = millis();
    pjesaciUpaljeno = !pjesaciUpaljeno;
    digitalWrite(zelenoPjesaci, pjesaciUpaljeno);
  }
}

Svjetla trepću svako svojim ritmom, kao da ih vode dva različita programa.

Što je novo u programu?

  • unsigned long zadnjeZuto = 0; – long je cijeli broj koji može biti vrlo velik, a unsigned znači bez predznaka, dakle bez negativnih brojeva. Takva varijabla pamti brojeve do otprilike 4,29 milijardi. Vrijeme iz millis() uvijek spremamo u unsigned long. Na mnogim Arduinima int ide samo do 32 767, a to je u milisekundama tek 33 sekunde.
  • millis() - zadnjeZuto – Sadašnje vrijeme minus vrijeme zadnje promjene. To je broj milisekundi koje su prošle od zadnje promjene.
  • if (millis() - zadnjeZuto >= 500) – Ako je prošlo barem 500 ms, vrijeme je za promjenu. Ako nije, program ne čeka, nego odmah ide dalje.
  • zadnjeZuto = millis(); – Pamtimo trenutak promjene. Od njega brojimo sljedećih 500 ms.
  • zutoUpaljeno = !zutoUpaljeno; – Kao ledMode u lekciji 2.1: true postaje false, a false postaje true.
  • digitalWrite(zutoAuti, zutoUpaljeno); – Za Arduino je true isto što i HIGH (broj 1), a false isto što i LOW (broj 0).
  • U loop() nema nijednog delay(). Zato loop() prolazi tisuće puta u sekundi i svaki put samo pogleda na sat. Između toga može raditi i druge stvari: ovaj program pazi na dva svjetla, a semafor B će paziti i na žicu.

Što kada se štoperica napuni?

Nakon otprilike 49 dana i 17 sati millis() dođe do najvećeg broja koji unsigned long može zapamtiti i nastavi brojati od nule, kao kazaljka na satu koja nakon 12 dolazi na 1.

Zato uvijek pišemo millis() - zadnjeZuto >= 500, s oduzimanjem. Oduzimanje daje točan razmak i kada brojanje prijeđe preko nule, kao što od 11 sati do 1 sat prođu 2 sata, iako je 1 manje od 11. Da smo napisali millis() >= zadnjeZuto + 500, semafor bi u tom trenutku nakratko poludio: žuto bi nekoliko puta brzo bljesnulo, a semafor B mogao bi bez razloga početi treptati žuto.

Eksperiment: promijeni 300 u 1000, pa u 100. Svako svjetlo mijenja ritam, a drugo uopće ne primijeti promjenu.

Vježba 2: Dva Arduina, jedna žica

Dva polaznika koja sjede jedan do drugoga dogovore se koji je A, a koji B.

Veza između semafora A i semafora B

Shema prikazuje ploču A. Ploča B spojena je potpuno jednako. J1 i J2 su priključci: mjesta na kojima duge žice odlaze s ploče A. Oznake B: D2 i B: GND pokazuju na koji pin Arduina B ide koja duga žica.

Kako čitamo shemu: žice koje se križaju

Na shemi se crte ponekad moraju križati, jer ih drukčije ne možemo nacrtati. Zato pazimo na točke:

  • Spojeni vodiči Točka na mjestu gdje se crte sastaju znači da su žice spojene.
  • Vodiči bez spoja Crte koje se križaju bez točke nisu spojene: jedna žica samo prelazi preko druge.

Pogledaj našu shemu: žica prema B: D2 križa okomitu žicu GND, ali na tom mjestu nema točke. Veza i GND zato nisu spojene. Da jesu, na vezi bi uvijek bio LOW i B nikada ne bi krenuo.

Pravila za spajanje dva Arduina

  1. Dok spajamo, oba Arduina odspojena su s USB kabela.
  2. Između dviju ploča spajamo samo žice za poruke i GND. Pin 5 V nikada ne spajamo.
  3. Obje ploče spajamo jednako: za svaku žicu s porukama svaka ploča ima svoj otpornik od 470 Ω i svoj otpornik od 10 kΩ. Zato kasnije možemo zamijeniti uloge bez prespajanja.
  4. Prije uključivanja voditelj pregleda spoj.

Spajanje

Na obje ploče dodajemo vezu u redak 29. Kao i prošli put, žica s Arduina ulazi u lijevu polovicu retka, a otpornik prelazi preko središnjeg utora:

DioArduino pinŽica s ArduinaOtpornik 470 ΩDesna strana
VezaD2a29e29–f29otpornik 10 kΩ iz i29 u desnu – liniju; duga žica veze u h29

Zatim dvije duge žice spajaju ploče:

ŽicaPloča APloča B
Veza (ljubičasta)h29h29
GND (crna)desna – linijadesna – linija
Spajanje dvaju Arduina

Čemu služi svaki dio?

  • Zajednički GND. Napon je uvijek razlika između dviju točaka, kao visina: kada kažemo da je nešto visoko 5 m, moramo znati mjerimo li od poda ili od krova. Kada A pošalje 5 V, to znači „5 V više od mog GND-a”. B može razumjeti poruku samo ako napon mjeri od istog GND-a. Bez te žice poruka ne znači ništa.
  • Otpornik 10 kΩ: pull-down. U lekciji Kontrola osvjetljenja pull-up otpornik povlačio je bazu tranzistora prema +5 V. Ovaj otpornik povlači vezu prema GND, pa se zove pull-down. Kada nitko ne šalje HIGH, na primjer kada je žica izvučena, veza je pouzdano LOW. Bez njega bi pin bio u zraku: hvatao bi smetnje i čitao slučajno HIGH ili LOW. Upravo ovaj otpornik čini vezu sigurnom pri kvaru: kada poruke nema, B čita stani.
  • Otpornik 470 Ω: zaštita. Što ako greškom na oba Arduina učitamo program za A? Tada oba pina šalju napon, i kada jedan šalje HIGH, a drugi LOW, struja bi tekla izravno iz jednog pina u drugi. Takav kratki spoj može oštetiti pinove. Prema Ohmovu zakonu (I = U / R), s dva otpornika od 470 Ω struja je najviše 5 V / 940 Ω ≈ 5 mA, a to pinovima ne škodi. Kada sve radi kako treba, kroz otpornike teče vrlo mala struja i oni poruci ne smetaju.

Test veze

Prije semafora provjerimo radi li veza. Na pločici svakog Arduina nalazi se mala LED-ica s oznakom L. Spojena je na pin D13, a u programu se zove LED_BUILTIN.

Program za Arduino A šalje poruke: dvije sekunde HIGH, dvije sekunde LOW. Svoj L pali zajedno s porukom.

// C++
const int veza = 2;

void setup() {
  pinMode(veza, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  digitalWrite(veza, HIGH);
  digitalWrite(LED_BUILTIN, HIGH);
  delay(2000);
  digitalWrite(veza, LOW);
  digitalWrite(LED_BUILTIN, LOW);
  delay(2000);
}

Program za Arduino B čita žicu i na svom L pokazuje što je pročitao.

// C++
const int veza = 2;

void setup() {
  pinMode(veza, INPUT);
  pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
  int poruka = digitalRead(veza);
  digitalWrite(LED_BUILTIN, poruka);
}

Učitajte oba programa. L na A i L na B moraju se paliti i gasiti zajedno.

Sada izvucite dugu žicu veze iz h29 na ploči B. L na B mora se ugasiti i ostati ugašen, iako L na A i dalje trepće. Tako znamo da pull-down otpornik radi. Vratite žicu.

Što je novo u programu?

  • pinMode(veza, INPUT); – Pin je ulaz: Arduino na njemu samo mjeri napon, a ne šalje ga.
  • digitalRead(veza) – Čita pin. Vraća HIGH ako je napon na pinu blizu 5 V, a LOW ako je blizu 0 V. To je par naredbi digitalWrite(): jedan Arduino piše, a drugi čita.
  • int poruka = digitalRead(veza); – Pročitano stanje pamtimo u varijabli poruka.
  • digitalWrite(LED_BUILTIN, poruka); – L pokazuje ono što je B pročitao: svijetli kada je poruka HIGH.
  • LED_BUILTIN – Ime pina na koji je spojena ugrađena LED-ica L. Ne moramo pamtiti da je to pin 13.

Vježba 3: Semafor A upravlja

Vježbe 3 i 4 rade se istodobno: polaznik A piše program za semafor A, a polaznik B program za semafor B.

Polaznik A kreće od cijelog programa iz prošle lekcije. Funkcija svjetlo() ostaje potpuno ista. U loop() dodajemo stanja iz tablice križanja i poruke za B.

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
crveno, čekaj 4 s (B završava zeleno i žuto)
crveno + žuto, čekaj 1 s
zeleno, čekaj 4 s
zeleno trepće, 2 s
žuto, čekaj 1 s
crveno, čekaj 1 s (svi stoje)
pošalji B: kreni
crveno, čekaj 5 s (B vozi)
pošalji B: stani

Ispod ostalih konstanti dodaj:

// C++
const int veza = 2;            // žica prema semaforu B

Na kraj setup() dodaj:

// C++
  pinMode(veza, OUTPUT);
  digitalWrite(veza, LOW);     // na početku B stoji

loop() zamijeni ovim:

// C++
void loop() {
  svjetlo(HIGH, LOW, LOW, 4000);     // crveno: B završava zeleno i žuto
  svjetlo(HIGH, HIGH, LOW, 1000);    // crveno + žuto
  svjetlo(LOW, LOW, HIGH, 4000);     // zeleno
  svjetlo(LOW, LOW, TREPCE, 2000);   // zeleno trepće
  svjetlo(LOW, HIGH, LOW, 1000);     // žuto
  svjetlo(HIGH, LOW, LOW, 1000);     // crveno: svi stoje

  digitalWrite(veza, HIGH);          // poruka za B: kreni
  svjetlo(HIGH, LOW, LOW, 5000);     // crveno: B ima crveno + žuto, pa zeleno
  digitalWrite(veza, LOW);           // poruka za B: stani
}

Što je novo u programu?

  • pinMode(veza, OUTPUT); – A šalje poruke, pa je njegov pin veze izlaz, kao pinovi za LED-ice.
  • digitalWrite(veza, LOW); – Odmah na početku šaljemo stani. Dok A ne odluči drukčije, B stoji.
  • digitalWrite(veza, HIGH); – Poruka kreni. Istom naredbom kojom palimo LED-icu sada šaljemo poruku drugom Arduinu.
  • svjetlo(HIGH, LOW, LOW, 5000); – Dok B vozi, A ima crveno. A odlučuje i koliko dugo B ima zeleno: 5 s poruke kreni, od toga 1 s crveno + žuto i 4 s zeleno.
  • Zašto loop() počinje s 4 s crvenog? Zamisli da se A resetira dok B ima zeleno. Pin veze u tom trenutku ostaje bez napona, pa B pročita stani i počne treptati. A kreće ispočetka: najprije setup(), a zatim loop(). Da loop() počinje s crveno + žuto, A bi već nakon 1 s imao zeleno, a B bi još treptao: obje bi ceste vozile! Ovako A uvijek najprije 4 s drži crveno i daje B dovoljno vremena da stane.
  • A smije koristiti delay(). Semafor A nikoga ne sluša, samo govori. Dok čeka, ne može propustiti nijednu poruku.

Ako na B još radi testni program iz Vježbe 2, L na B svijetli upravo onda kada bi B trebao imati crveno + žuto i zeleno. Usporedi to sa svjetlima semafora A.

Koliko dugo B ima zeleno ako u pozivu svjetlo(HIGH, LOW, LOW, 5000) promijenimo 5000 u 8000?
7 s. Poruka kreni tada traje 8 s, a od toga B prvu sekundu ima crveno + žuto. Program semafora B nismo morali mijenjati, jer o trajanju njegova zelenog odlučuje A.

Vježba 4: Semafor B sluša

I polaznik B kreće od cijelog programa iz prošle lekcije. Funkcija svjetlo() ostaje ista, ali B više ne mjeri koliko dugo traju crveno i zeleno. Čeka poruku.

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
crveno
čekaj dok je na žici stani
crveno + žuto, čekaj 1 s
ako je na žici još uvijek kreni:
zeleno
čekaj dok je na žici kreni
zeleno trepće, 2 s
žuto, čekaj 1 s

Ispod ostalih konstanti dodaj:

// C++
const int veza = 2;            // žica od semafora A

Na kraj setup() dodaj:

// C++
  pinMode(veza, INPUT);

loop() zamijeni ovim:

// C++
void loop() {
  svjetlo(HIGH, LOW, LOW, 0);           // crveno
  while (digitalRead(veza) == LOW) {    // čekamo dok A ne pošalje kreni
  }

  svjetlo(HIGH, HIGH, LOW, 1000);       // crveno + žuto

  if (digitalRead(veza) == HIGH) {      // je li A još uvijek za kreni?
    svjetlo(LOW, LOW, HIGH, 0);         // zeleno
    while (digitalRead(veza) == HIGH) { // vozimo dok A ne pošalje stani
    }
    svjetlo(LOW, LOW, TREPCE, 2000);    // zeleno trepće
    svjetlo(LOW, HIGH, LOW, 1000);      // žuto
  }
}

Što je novo u programu?

  • while (digitalRead(veza) == LOW) { } – Petlja while (engleski dok) ponavlja naredbe između { } dok je uvjet istinit. Ovdje su zagrade prazne, pa program samo iznova čita žicu, tisuće puta u sekundi. Čim A pošalje HIGH, uvjet više nije istinit i program ide dalje.
  • for ili while? Petlju for koristimo kada znamo koliko puta nešto ponavljamo. Petlju while koristimo kada čekamo da se nešto dogodi, a ne znamo koliko će to trajati.
  • svjetlo(HIGH, LOW, LOW, 0); – Trajanje 0: funkcija postavi svjetla i odmah se vrati, bez čekanja. Koliko dugo crveno traje, ne odlučuje B, nego A.
  • if (digitalRead(veza) == HIGH) – Za vrijeme crveno + žuto B jednu sekundu ne gleda žicu, jer funkcija svjetlo() čeka s delay(). Što ako se A baš u toj sekundi resetira? Tada B ne smije upaliti zeleno. Zato prije zelenog još jednom provjerimo žicu. Ako je na njoj LOW, B preskače cijeli dio s zelenim, loop() kreće ispočetka i B se vraća na crveno.
  • Zašto smijemo čekati za vrijeme treptanja i žutog? Nakon poruke stani A ionako 4 s drži crveno i ne šalje novu poruku, pa B ništa ne propušta.
  • Pješaci i zvuk rade sami. Funkcija svjetlo() je ista kao prošli put, pa pješaci B hodaju kada B ima mirno zeleno. Za njih nismo napisali nijedan novi redak.

Oba semafora zajedno

Učitajte program A na Arduino A i program B na Arduino B. Pratite svjetla oba semafora i usporedite ih s tablicom križanja. Provjerite oba pravila: kada jedan semafor nema crveno, drugi ima crveno, a između njih je uvijek sekunda u kojoj oba imaju crveno.

Zašto B nakon uključivanja ima crveno, iako mu to nitko nije javio?
loop() semafora B počinje crvenim. Zatim B čeka u petlji while sve dok A ne pošalje kreni. Ako A još ne radi, na žici je LOW zbog pull-down otpornika, pa B ostaje na crvenom.

Vježba 5: Kada se semafor A pokvari

Izvucite dugu žicu veze dok B ima crveno. Što se događa?

B ima crveno i čeka. I čeka. Takav kvar je siguran, jer nitko ne vozi kroz zeleno na pogrešnoj cesti. Ali vozači na cesti B čekali bi zauvijek pred semaforom koji izgleda sasvim ispravno. Vratite žicu.

Pravi semafori u kvaru trepću žuto. Tada vozači znaju da semafor ne radi i voze prema prometnim znakovima. Naš semafor B naučit ćemo isto:

  • U normalnom radu B na crvenom čeka 10 s (od 3. do 13. sekunde u tablici križanja).
  • Ako čeka dulje od 15 s, nešto ne valja: B počne treptati žuto.
  • Čim A pošalje kreni, B nastavlja normalno.

Ovdje ne možemo koristiti delay(): B mora istodobno mjeriti koliko dugo čeka, treptati žuto i paziti na žicu. Za to nam treba millis(), točno kao u Vježbi 1.

Planiraj prije nego što programiraš:
crveno
zapamti kada smo počeli čekati
dok je na žici stani:
ako čekamo dulje od 15 s:
trepći žuto
(ostatak kao prije)

Izmjena programa

Polaznik B ispod ostalih konstanti dodaje:

// C++
const unsigned long NAJDULJE_CEKANJE = 15000;   // nakon 15 s bez poruke: kvar

bool zutoUpaljeno = false;           // za treptanje žutog u kvaru
unsigned long zadnjeTreptanje = 0;   // kada se žuto zadnji put promijenilo

Ispod funkcije svjetlo() dodaj novu funkciju:

// C++
void trepceZuto() {
  if (millis() - zadnjeTreptanje >= 500) {   // prošlo je pola sekunde
    zadnjeTreptanje = millis();
    zutoUpaljeno = !zutoUpaljeno;
    svjetlo(LOW, zutoUpaljeno, LOW, 0);      // žuto upaljeno ili ugašeno
  }
}

U loop() promijeni samo početak, do prve petlje while, uključujući i nju:

// C++
void loop() {
  svjetlo(HIGH, LOW, LOW, 0);                  // crveno
  unsigned long pocetakCekanja = millis();     // NOVO: kada smo počeli čekati

  while (digitalRead(veza) == LOW) {           // čekamo dok A ne pošalje kreni
    if (millis() - pocetakCekanja >= NAJDULJE_CEKANJE) {   // NOVO
      trepceZuto();                            // A se predugo ne javlja
    }
  }

  // ostatak loop() ostaje isti

Što je novo u programu?

  • const unsigned long NAJDULJE_CEKANJE = 15000; – Koliko dugo B smije čekati na crvenom. Ime pišemo velikim slovima, kao TREPCE, jer se ne mijenja dok program radi. Tip je unsigned long jer ga uspoređujemo s vremenom iz millis().
  • unsigned long pocetakCekanja = millis(); – Pamtimo trenutak u kojem je B upalio crveno. Varijabla je napisana unutar loop(), pa pri svakom novom crvenom dobije novo vrijeme i B opet ima punih 15 s.
  • millis() - pocetakCekanja >= NAJDULJE_CEKANJE – Isti oblik kao u Vježbi 1: koliko je vremena prošlo otkad čekamo? Ako je prošlo 15 s ili više, A se predugo ne javlja.
  • void trepceZuto() – Ista ideja kao žuto u Vježbi 1. Važno je da ova funkcija nikada ne čeka. Pri svakom pozivu samo pogleda na sat i, ako je prošlo pola sekunde, promijeni žuto. Zato se petlja while i dalje vrti tisuće puta u sekundi i B primijeti poruku kreni čim stigne.
  • svjetlo(LOW, zutoUpaljeno, LOW, 0); – Kao u Vježbi 1, true znači HIGH, a false znači LOW. Jednim pozivom dobivamo ili upaljeno ili ugašeno žuto.
  • Pješaci u kvaru stoje. Funkcija svjetlo() pušta pješake samo kada automobili imaju mirno zeleno, a u kvaru ga nikada nemaju.
  • Što kada A ponovo proradi? A nakon uključivanja kreće svojim ciklusom, a B za to vrijeme još trepće žuto. Tek kada A pošalje kreni, B izlazi iz petlje while, pali crveno + žuto i vraća se u normalan rad.

Gdje pišemo varijable?

Zašto zadnjeTreptanje i zutoUpaljeno pišemo iznad setup(), a pocetakCekanja unutar loop()?

  • Varijabla iznad setup() nastaje jednom, kada se program pokrene, i pamti svoju vrijednost sve dok program radi. Može je koristiti svaki dio programa: loop() i sve naše funkcije.
  • Varijabla unutar loop(), ili unutar neke druge funkcije, nastaje iznova pri svakom prolazu i nestaje na kraju prolaza. Može je koristiti samo dio programa između vitičastih zagrada u kojima je napisana.

Funkcija trepceZuto() poziva se tisuće puta u sekundi, a pri svakom pozivu mora znati kada je žuto zadnji put promijenjeno i je li sada upaljeno. Te vrijednosti moraju ostati zapamćene između dva poziva, pa te varijable pišemo iznad setup(). Varijabla pocetakCekanja treba nam samo dok traje jedan prolaz kroz loop(), a pri svakom novom crvenom ionako dobiva novo vrijeme.

Programeri prvu vrstu zovu globalna, a drugu lokalna varijabla.

Mijenjaš trajanja u programu A?

Semafor B smije na crvenom čekati najviše NAJDULJE_CEKANJE. Ako u programu A produljiš ciklus toliko da B čeka dulje od 15 s, B će treptati žuto iako A radi. Tada povećaj i taj broj u programu B.

Eksperimenti

Svaki eksperiment počnite dok oba semafora rade normalno.

1. Prekinuta žica
Izvucite dugu žicu veze dok B ima crveno. Nakon koliko vremena B počne treptati? Vratite žicu i pratite kako se B vraća u normalan rad.

2. Reset
Pritisnite tipku za reset na Arduinu A dok B ima zeleno. Što odmah napravi B? Koliko dugo A nakon toga drži crveno?

3. Novi program
Dok B ima zeleno, ponovo učitajte program na Arduino A. Usporedite što se događa s eksperimentom 2.

4. Bez napajanja
Izvucite USB kabel Arduina A. Što radi B? Vratite kabel.

Zašto u eksperimentu 1 B počne treptati prije nego što prođe 15 s od izvlačenja žice?
B ne broji od trenutka kada je žica izvučena, nego od trenutka kada je upalio crveno. Kada smo izvukli žicu, B je već neko vrijeme čekao na crvenom, pa je do 15 s ostalo manje.
Zašto su eksperimenti 2, 3 i 4 jednaki za semafor B?
U sva tri slučaja Arduino A prestane slati napon na pin veze: pri resetu i učitavanju programa pin na trenutak postaje ulaz, a bez napajanja A ne šalje ništa. Pull-down otpornik tada drži vezu na LOW, pa B pročita stani. Kod svih kvarova B vidi istu poruku, a ona je sigurna.

Vježba 6: Povratna poruka

Semafor A još uvijek samo vjeruje da je B stao. Nakon poruke stani čeka 4 s, jer zna da B toliko treba za treptanje, žuto i sekundu u kojoj svi imaju crveno. Što ako netko u programu B produlji žuto?

Isprobajte: u programu B, u retku sa žutim, promijenite 1000 u 3000. Pratite križanje nakon što B dobije poruku stani. A pali crveno + žuto dok B još ima žuto, a odmah zatim i zeleno, bez sekunde u kojoj svi stoje. Na pravom križanju to bi bilo opasno. Ostavite 3000, trebat će nam za provjeru.

Popravit ćemo to drugom žicom, u suprotnom smjeru. Kroz nju B javlja semaforu A: stojim. A nakon poruke stani više ne čeka zadano vrijeme, nego čeka tu potvrdu.

Stanje povratne žicePoruka od BŠto radi A
HIGHstojim: B ima crveno i čeka poruku krenismije nastaviti: 1 s crveno, pa crveno + žuto
LOWne stojim, ili poruke nemačeka na crvenom

I ovdje HIGH znači ono što je sigurno samo kada poruka stvarno stigne. Ako je povratna žica izvučena ili B nema napajanje, pull-down otpornik drži žicu na LOW i A čeka. Kvar nikada ne pusti A na zeleno.

Usporedi novi vremenski dijagram s onim s početka lekcije. Kada sve radi normalno, svjetla se mijenjaju u istim trenucima. Razlika je u retku semafora A između 0. i 3. sekunde: A više ne broji 4 s, nego čeka potvrdu. Potvrda stigne čim B upali crveno, a zatim A odbroji još 1 s u kojoj svi stoje.

Vremenski dijagram: semafor A, semafor B, žica veze i žica potvrde tijekom jednog ciklusa

Što ako potvrda nikada ne stigne? B inače treba 3 s da stane. Ako A čeka dulje od 6 s, nešto ne valja i A počne treptati žuto, isto kao B u Vježbi 5. B tada ne dobiva poruku kreni, pa nakon 15 s i on trepće žuto. Cijelo križanje tada trepće žuto, kao pravo križanje u kvaru.

Spajanje

Na obje ploče dodajemo potvrdu u redak 30, na isti način kao vezu u retku 29:

DioArduino pinŽica s ArduinaOtpornik 470 ΩDesna strana
PotvrdaD4a30e30–f30otpornik 10 kΩ iz i30 u desnu – liniju; duga žica potvrde u g30

Treća duga žica spaja ploče:

ŽicaPloča APloča B
Potvrda (narančasta)g30g30
Veza i potvrda između semafora A i semafora B

I ova shema prikazuje ploču A, a ploča B spojena je jednako. Potvrda odlazi s ploče kroz novi priključak J3, s oznakom B: D4. Pazi i na križanja: otpornik R8 prelazi preko žice veze bez točke, pa s njom nije spojen.

Spajanje potvrde

Pinovi su sada u obrnutim ulogama: na povratnoj žici B piše, a A čita.

Izmjena programa B

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
crveno
javi A: stojim
čekaj dok je na žici stani (i trepći žuto ako čekaš predugo)
javi A: ne stojim
crveno + žuto, čekaj 1 s
(ostatak kao prije)

Polaznik B ispod ostalih konstanti dodaje:

// C++
const int potvrda = 4;         // žica prema semaforu A: HIGH znači "B stoji"

Na kraj setup() dodaj:

// C++
  pinMode(potvrda, OUTPUT);
  digitalWrite(potvrda, LOW);  // dok ne upalimo crveno, ne javljamo da stojimo

U loop() dodaj dva retka označena s NOVO:

// C++
void loop() {
  svjetlo(HIGH, LOW, LOW, 0);                  // crveno
  digitalWrite(potvrda, HIGH);                 // NOVO: javljamo A: stojim
  unsigned long pocetakCekanja = millis();     // kada smo počeli čekati

  while (digitalRead(veza) == LOW) {           // čekamo dok A ne pošalje kreni
    if (millis() - pocetakCekanja >= NAJDULJE_CEKANJE) {
      trepceZuto();                            // A se predugo ne javlja
    }
  }

  digitalWrite(potvrda, LOW);                  // NOVO: krećemo, više ne stojimo
  svjetlo(HIGH, HIGH, LOW, 1000);              // crveno + žuto

  // ostatak loop() ostaje isti

Što je novo u programu B?

  • pinMode(potvrda, OUTPUT); – Na povratnoj žici B šalje poruke, pa je pin D4 izlaz.
  • digitalWrite(potvrda, HIGH); iza paljenja crvenog – Najprije upalimo crveno, a tek onda javimo stojim. Nikada ne javljamo nešto što još nije istina.
  • digitalWrite(potvrda, LOW); prije crveno + žuto – Najprije povučemo potvrdu, a tek onda krenemo. Tako A ni u jednom trenutku ne misli da B stoji dok B već ima crveno + žuto.
  • Zašto B javlja stojim i dok u kvaru trepće žuto? U kvaru B i dalje čeka poruku kreni i bez nje nikada neće upaliti zeleno. Kada bi u kvaru javljao ne stojim, nakon kvara bi oba semafora čekala jedan drugoga: A bi čekao potvrdu, a B poruku kreni. Čekali bi zauvijek, iako bi žice opet bile ispravne. Programeri takvo stanje zovu zastoj (engleski deadlock).

Izmjena programa A

Planiraj prije nego što programiraš:
ponavljaj zauvijek:
crveno
čekaj dok B ne javi stojim (i trepći žuto ako čekaš predugo)
crveno, čekaj 1 s (svi stoje)
crveno + žuto, čekaj 1 s
(ostatak kao prije)

Polaznik A ispod ostalih konstanti dodaje:

// C++
const int potvrda = 4;         // žica od semafora B: HIGH znači "B stoji"
const unsigned long NAJDULJE_CEKANJE = 6000;    // nakon 6 s bez potvrde: kvar

bool zutoUpaljeno = false;           // za treptanje žutog u kvaru
unsigned long zadnjeTreptanje = 0;   // kada se žuto zadnji put promijenilo

Ispod funkcije svjetlo() dodaj funkciju trepceZuto(). Ista je kao u programu B:

// C++
void trepceZuto() {
  if (millis() - zadnjeTreptanje >= 500) {   // prošlo je pola sekunde
    zadnjeTreptanje = millis();
    zutoUpaljeno = !zutoUpaljeno;
    svjetlo(LOW, zutoUpaljeno, LOW, 0);      // žuto upaljeno ili ugašeno
  }
}

Na kraj setup() dodaj:

// C++
  pinMode(potvrda, INPUT);

U loop() prvi redak, svjetlo(HIGH, LOW, LOW, 4000);, zamijeni ovim:

// C++
  svjetlo(HIGH, LOW, LOW, 0);                  // crveno
  unsigned long pocetakCekanja = millis();     // kada smo počeli čekati

  while (digitalRead(potvrda) == LOW) {        // čekamo da B javi: stojim
    if (millis() - pocetakCekanja >= NAJDULJE_CEKANJE) {
      trepceZuto();                            // B se predugo ne javlja
    }
  }
  svjetlo(HIGH, LOW, LOW, 1000);               // crveno: svi stoje

Ostatak loop() ostaje isti.

Što je novo u programu A?

  • pinMode(potvrda, INPUT); i digitalRead(potvrda) – Isto kao kod semafora B u Vježbi 2, samo u suprotnom smjeru: sada A čita.
  • Prepoznaješ li petlju while? To je ista petlja kojom B u Vježbi 5 čeka poruku kreni: crveno, zapamti kada smo počeli čekati, čekaj poruku i trepći žuto ako čekaš predugo. Razlikuju se samo žica i najdulje čekanje.
  • NAJDULJE_CEKANJE = 6000 – B inače javi stojim 3 s nakon poruke stani. Dajemo mu dvostruko više vremena, a tek onda zaključimo da nešto ne valja.
  • svjetlo(HIGH, LOW, LOW, 1000); – B javlja stojim u trenutku kada upali crveno. Sekundu u kojoj svi stoje zato i dalje mora odbrojiti A.
  • Kada je semafor A sada siguran? Prije je A pogađao koliko B treba da stane. Sada to zna: koliko god dugo B imao žuto, A čeka njegovu potvrdu. Kada sve radi normalno, ciklus i dalje traje 18 s.
  • A sada i sluša, ali samo u petlji while, gdje ne koristi delay(). U ostatku ciklusa samo govori, pa tamo smije čekati s delay().

Oba semafora zajedno

Učitajte nove programe na oba Arduina. Ako na A radi novi program, a na B stari, B nikada ne javlja stojim, pa A nakon 6 s počne treptati žuto.

1. Sporiji B
Žuto u programu B još traje 3 s. Pratite križanje nakon poruke stani: A sada čeka da B stane i tek tada odbroji sekundu u kojoj svi stoje. Ciklus je dulji, ali sigurniji. Vratite žuto na 1000.

2. Prekinuta povratna žica
Izvucite narančastu žicu iz g30 na ploči A dok A ima zeleno. Pratite oba semafora barem 40 sekundi. Koji semafor prvi počne treptati žuto? Vratite žicu i pratite kako se križanje vraća u normalan rad.

Zašto u eksperimentu 2 najprije počne treptati A, a tek kasnije B?
A nakon poruke stani čeka potvrdu, ali potvrda ne stiže jer je žica izvučena. Nakon 6 s A počne treptati. B je u tom trenutku već stao i čeka poruku kreni, koju A u kvaru ne šalje. B počne treptati tek kada na crvenom čeka 15 s.
Zašto se nakon vraćanja žice A odmah vraća u normalan rad?
B i u kvaru javlja stojim, jer čeka poruku kreni i bez nje neće upaliti zeleno. Čim je žica vraćena, A pročita HIGH, izađe iz petlje while i nastavi ciklus. Kada A pošalje kreni, i B se vraća u normalan rad.

Zamjena uloga

Obje ploče spojene su jednako, pa uloge možete zamijeniti bez prespajanja. Polaznik A sada piše program za B (Vježbe 4, 5 i 6), a polaznik B program za A (Vježbe 3 i 6). Kada učitate nove programe, križanje mora raditi jednako kao prije.

Cijeli program: semafor A

// C++
const int crvenoAuti = 10;
const int zutoAuti = 9;
const int zelenoAuti = 8;
const int crvenoPjesaci = 7;
const int zelenoPjesaci = 6;
const int zujalica = 5;
const int veza = 2;            // žica prema semaforu B
const int potvrda = 4;         // žica od semafora B: HIGH znači "B stoji"

const int TREPCE = 2;          // treća vrijednost uz LOW (0) i HIGH (1)
const unsigned long NAJDULJE_CEKANJE = 6000;    // nakon 6 s bez potvrde: kvar

bool pjesaciHodaju = false;    // pamti jesu li pješaci imali zeleno
bool zutoUpaljeno = false;           // za treptanje žutog u kvaru
unsigned long zadnjeTreptanje = 0;   // kada se žuto zadnji put promijenilo

void svjetlo(int crveno, int zuto, int zeleno, int trajanje) {
  digitalWrite(crvenoAuti, crveno);
  digitalWrite(zutoAuti, zuto);

  // Pješaci hodaju samo kada automobili imaju mirno zeleno.
  if (zeleno == HIGH && crveno == LOW && zuto == LOW) {
    digitalWrite(crvenoPjesaci, LOW);
    digitalWrite(zelenoPjesaci, HIGH);
    if (!pjesaciHodaju) {          // upravo su dobili zeleno
      tone(zujalica, 800, 60);     // kratki viši zvuk: kreni
      pjesaciHodaju = true;
    }
  } else {
    digitalWrite(crvenoPjesaci, HIGH);
    digitalWrite(zelenoPjesaci, LOW);
    if (pjesaciHodaju) {           // upravo su dobili crveno
      tone(zujalica, 150, 150);    // kratki niži zvuk: stani
      pjesaciHodaju = false;
    }
  }

  if (zeleno == TREPCE) {
    for (int t = 0; t < trajanje; t = t + 500) {
      digitalWrite(zelenoAuti, LOW);
      delay(250);
      digitalWrite(zelenoAuti, HIGH);
      delay(250);
    }
  } else {
    digitalWrite(zelenoAuti, zeleno);
    delay(trajanje);
  }
}

void trepceZuto() {
  if (millis() - zadnjeTreptanje >= 500) {   // prošlo je pola sekunde
    zadnjeTreptanje = millis();
    zutoUpaljeno = !zutoUpaljeno;
    svjetlo(LOW, zutoUpaljeno, LOW, 0);      // žuto upaljeno ili ugašeno
  }
}

void setup() {
  pinMode(crvenoAuti, OUTPUT);
  pinMode(zutoAuti, OUTPUT);
  pinMode(zelenoAuti, OUTPUT);
  pinMode(crvenoPjesaci, OUTPUT);
  pinMode(zelenoPjesaci, OUTPUT);
  pinMode(zujalica, OUTPUT);
  pinMode(veza, OUTPUT);
  digitalWrite(veza, LOW);     // na početku B stoji
  pinMode(potvrda, INPUT);
}

void loop() {
  svjetlo(HIGH, LOW, LOW, 0);                  // crveno
  unsigned long pocetakCekanja = millis();     // kada smo počeli čekati

  while (digitalRead(potvrda) == LOW) {        // čekamo da B javi: stojim
    if (millis() - pocetakCekanja >= NAJDULJE_CEKANJE) {
      trepceZuto();                            // B se predugo ne javlja
    }
  }
  svjetlo(HIGH, LOW, LOW, 1000);               // crveno: svi stoje

  svjetlo(HIGH, HIGH, LOW, 1000);    // crveno + žuto
  svjetlo(LOW, LOW, HIGH, 4000);     // zeleno
  svjetlo(LOW, LOW, TREPCE, 2000);   // zeleno trepće
  svjetlo(LOW, HIGH, LOW, 1000);     // žuto
  svjetlo(HIGH, LOW, LOW, 1000);     // crveno: svi stoje

  digitalWrite(veza, HIGH);          // poruka za B: kreni
  svjetlo(HIGH, LOW, LOW, 5000);     // crveno: B ima crveno + žuto, pa zeleno
  digitalWrite(veza, LOW);           // poruka za B: stani
}

Cijeli program: semafor B

// C++
const int crvenoAuti = 10;
const int zutoAuti = 9;
const int zelenoAuti = 8;
const int crvenoPjesaci = 7;
const int zelenoPjesaci = 6;
const int zujalica = 5;
const int veza = 2;            // žica od semafora A
const int potvrda = 4;         // žica prema semaforu A: HIGH znači "B stoji"

const int TREPCE = 2;          // treća vrijednost uz LOW (0) i HIGH (1)
const unsigned long NAJDULJE_CEKANJE = 15000;   // nakon 15 s bez poruke: kvar

bool pjesaciHodaju = false;    // pamti jesu li pješaci imali zeleno
bool zutoUpaljeno = false;           // za treptanje žutog u kvaru
unsigned long zadnjeTreptanje = 0;   // kada se žuto zadnji put promijenilo

void svjetlo(int crveno, int zuto, int zeleno, int trajanje) {
  digitalWrite(crvenoAuti, crveno);
  digitalWrite(zutoAuti, zuto);

  // Pješaci hodaju samo kada automobili imaju mirno zeleno.
  if (zeleno == HIGH && crveno == LOW && zuto == LOW) {
    digitalWrite(crvenoPjesaci, LOW);
    digitalWrite(zelenoPjesaci, HIGH);
    if (!pjesaciHodaju) {          // upravo su dobili zeleno
      tone(zujalica, 800, 60);     // kratki viši zvuk: kreni
      pjesaciHodaju = true;
    }
  } else {
    digitalWrite(crvenoPjesaci, HIGH);
    digitalWrite(zelenoPjesaci, LOW);
    if (pjesaciHodaju) {           // upravo su dobili crveno
      tone(zujalica, 150, 150);    // kratki niži zvuk: stani
      pjesaciHodaju = false;
    }
  }

  if (zeleno == TREPCE) {
    for (int t = 0; t < trajanje; t = t + 500) {
      digitalWrite(zelenoAuti, LOW);
      delay(250);
      digitalWrite(zelenoAuti, HIGH);
      delay(250);
    }
  } else {
    digitalWrite(zelenoAuti, zeleno);
    delay(trajanje);
  }
}

void trepceZuto() {
  if (millis() - zadnjeTreptanje >= 500) {   // prošlo je pola sekunde
    zadnjeTreptanje = millis();
    zutoUpaljeno = !zutoUpaljeno;
    svjetlo(LOW, zutoUpaljeno, LOW, 0);      // žuto upaljeno ili ugašeno
  }
}

void setup() {
  pinMode(crvenoAuti, OUTPUT);
  pinMode(zutoAuti, OUTPUT);
  pinMode(zelenoAuti, OUTPUT);
  pinMode(crvenoPjesaci, OUTPUT);
  pinMode(zelenoPjesaci, OUTPUT);
  pinMode(zujalica, OUTPUT);
  pinMode(veza, INPUT);
  pinMode(potvrda, OUTPUT);
  digitalWrite(potvrda, LOW);  // dok ne upalimo crveno, ne javljamo da stojimo
}

void loop() {
  svjetlo(HIGH, LOW, LOW, 0);                  // crveno
  digitalWrite(potvrda, HIGH);                 // javljamo A: stojim
  unsigned long pocetakCekanja = millis();     // kada smo počeli čekati

  while (digitalRead(veza) == LOW) {           // čekamo dok A ne pošalje kreni
    if (millis() - pocetakCekanja >= NAJDULJE_CEKANJE) {
      trepceZuto();                            // A se predugo ne javlja
    }
  }

  digitalWrite(potvrda, LOW);                  // krećemo, više ne stojimo
  svjetlo(HIGH, HIGH, LOW, 1000);              // crveno + žuto

  if (digitalRead(veza) == HIGH) {      // je li A još uvijek za kreni?
    svjetlo(LOW, LOW, HIGH, 0);         // zeleno
    while (digitalRead(veza) == HIGH) { // vozimo dok A ne pošalje stani
    }
    svjetlo(LOW, LOW, TREPCE, 2000);    // zeleno trepće
    svjetlo(LOW, HIGH, LOW, 1000);      // žuto
  }
}

Ako nešto ne radi

  • U testu veze L na B ne prati L na A ⇒ Je li crna žica spojena između desnih – linija obiju ploča? Je li duga žica veze na obje ploče u h29? Jesu li žice s D2 u a29, a otpornici od 470 Ω u e29–f29?
  • L na B svijetli i kada je žica veze izvučena ⇒ Otpornik od 10 kΩ na ploči B nije spojen između i29 i desne – linije.
  • B stalno ima crveno, a nakon 15 s trepće žuto ⇒ Poruka ne stiže do B. Ponovite test veze iz Vježbe 2 i provjerite je li na Arduinu A učitan program A.
  • B trepće žuto iako A radi ⇒ Jeste li produljili trajanja u programu A? Povećajte NAJDULJE_CEKANJE u programu B.
  • A nakon poruke stani ne nastavlja, nego nakon 6 s trepće žuto ⇒ Potvrda ne stiže do A. Provjerite narančastu žicu (g30 na obje ploče), žice s D4 u a30, otpornike od 470 Ω u e30–f30 i otpornike od 10 kΩ iz i30. Je li na Arduino B učitan program iz Vježbe 6?
  • A trepće žuto iako potvrda stiže ⇒ Jeste li u programu B produljili treptanje ili žuto? Ako B treba više od 6 s da stane, povećajte NAJDULJE_CEKANJE u programu A.
  • Oba semafora rade svaki za sebe ili oba trepću žuto odmah nakon uključivanja ⇒ Na oba Arduina učitan je program A. Otpornici od 470 Ω štite pinove, ali na Arduino B treba učitati program B.
  • Oba semafora stalno imaju crveno, a zatim trepću žuto ⇒ Na oba Arduina učitan je program B, pa nitko ne šalje poruke.
  • Program se ne može učitati ⇒ Svaki Arduino spojen je na svoje računalo. Provjerite jesu li u Arduino IDE-u odabrani ploča Arduino Uno i pravi port, na računalu na koje je spojen Arduino koji programirate.

Dodatni izazovi

  1. Neka B ima dulje zeleno od A, na primjer 6 s. Koji program moraš promijeniti? Moraš li paziti i na NAJDULJE_CEKANJE?
  2. Neka A u Serial Monitoru ispiše koliko je milisekundi čekao potvrdu od B. Savjet: odmah iza petlje while izračunaj millis() - pocetakCekanja. Koliko A obično čeka? Što se dogodi s tim brojem kada u programu B produljiš žuto?

Što smo naučili?

  • millis() vraća broj milisekundi od uključivanja ili reseta Arduina.
  • Vrijeme iz millis() spremamo u unsigned long, jer int za njega može biti premalen.
  • Koliko je vremena prošlo, provjeravamo oduzimanjem: millis() - zadnje >= trajanje.
  • Program bez delay() ne čeka, nego samo gleda na sat, pa može istodobno pratiti više stvari.
  • pinMode(pin, INPUT) i digitalRead() čitaju je li na pinu HIGH ili LOW.
  • Petlja while ponavlja naredbe dok je uvjet istinit. Koristimo je kada čekamo da se nešto dogodi.
  • Dva Arduina mogu razgovarati preko žice: jedan piše s digitalWrite(), drugi čita s digitalRead(). Moraju imati zajednički GND.
  • Pull-down otpornik drži pin na LOW kada nitko ne šalje napon.
  • Veza je sigurna pri kvaru kada svaki kvar znači stani.
  • Kada dva uređaja moraju surađivati, najjednostavnije je da jedan bude glavni.
  • Povratna poruka (potvrda) zamjenjuje pogađanje: A ne računa koliko B treba da stane, nego čeka da B to javi.
  • Ako dva uređaja čekaju jedan drugoga, mogu čekati zauvijek. Takav zastoj sprječavamo tako da pažljivo odaberemo što svaki od njih javlja.

Mali kviz

1. Koja je razlika između delay(500) i provjere millis() - zadnje >= 500?
delay(500) zaustavi program na pola sekunde i Arduino za to vrijeme ne radi ništa drugo. Provjera s millis() ne čeka: samo pogleda je li prošlo pola sekunde i odmah ide dalje. Zato program s millis() može istodobno pratiti više stvari.
2. Zašto vrijeme iz millis() spremamo u unsigned long, a ne u int?
Na mnogim Arduinima int ide samo do 32 767, a millis() taj broj prijeđe već nakon 33 sekunde. unsigned long pamti brojeve do otprilike 4,29 milijardi, što je više od 49 dana u milisekundama.
3. Zašto na žici LOW znači stani, a ne kreni?
Zato što kod svakog kvara na žici ostaje LOW: kada je žica izvučena, kada A nema napajanje i kada se A resetira. Da LOW znači kreni, svaki kvar pustio bi automobile na cestu B. Ovako svaki kvar znači stani.
4. Čemu služi otpornik od 10 kΩ, a čemu otpornik od 470 Ω?
Otpornik od 10 kΩ je pull-down: drži vezu na LOW kada nitko ne šalje napon, pa pin nije u zraku. Otpornik od 470 Ω je zaštita: ako oba Arduina greškom šalju napon, jedan HIGH, a drugi LOW, struja kroz oba otpornika bude najviše oko 5 mA i pinovi se ne oštete.
5. Zašto dva Arduina moraju imati zajednički GND?
Napon je razlika između dviju točaka. Kada A pošalje 5 V, to je 5 V više od njegova GND-a. B može ispravno pročitati poruku samo ako mjeri od istog GND-a.
6. Zašto je A u Vježbi 3 nakon poruke stani čekao 4 s, a u Vježbi 6 čeka potvrdu?
U Vježbi 3 A nije vidio svjetla semafora B, pa je pogađao: B treba 2 s za treptanje i 1 s za žuto, a zatim 1 s u kojoj svi stoje. Kada bi B imao dulje žuto, A bi krenuo prerano. S potvrdom A zna kada je B stao i čeka koliko god treba. Isto vrijedi i kada se A resetira dok B ima zeleno: A na početku loop() čeka potvrdu.
7. Zašto funkcija trepceZuto() ne smije koristiti delay()?
Dok bi trajao delay(), B ne bi čitao žicu i ne bi mogao odmah primijetiti poruku kreni. Funkcija bez delay() samo pogleda na sat i odmah se vrati, pa petlja while čita žicu tisuće puta u sekundi.
8. Zašto B javlja stojim tek nakon što upali crveno, a potvrdu povlači prije nego što upali crveno + žuto?
Potvrda smije biti HIGH samo dok je istinita, odnosno dok B stvarno ima crveno i čeka. Zato B najprije upali crveno pa javi stojim, a prije polaska najprije povuče potvrdu pa tek onda upali crveno + žuto. Tako A ni u jednom trenutku ne dobije pogrešnu poruku.
9. Što bi se dogodilo da B u kvaru, dok trepće žuto, javlja ne stojim?
Nastao bi zastoj. A bi čekao potvrdu stojim, a B poruku kreni. Nijedan ne bi poslao ono što drugi čeka, pa bi oba semafora zauvijek treptala žuto, čak i kada bi sve žice bile ispravne. Zato B i u kvaru javlja stojim: i dalje čeka poruku kreni i bez nje neće upaliti zeleno.