7.8 LED semafor
Tri LED-ice, tri otpornika, i program koji prolazi kroz fiksni niz koraka a nikada ne staje čekati. Ovaj projekt gradi minijaturni europski semafor (zeleno, blinkajuće zeleno, narančasto, crveno, pa crveno i narančasto zajedno) i uvodi konačni automat, uzorak na kojem je izgrađen gotovo svaki pravi ugradbeni program.
U ovoj dokumentaciji naučit ćeš:
- Što je konačni automat, i kako su tri varijable dovoljne da ga izgradiš
- Kako pokrenuti vremenski niz pomoću
millis()umjestodelay() - Kako natjerati jednu LED-icu da blinka unutar stanja dok vlastiti timer tog stanja nastavlja raditi
- Kako pratiti napredak programa u Serial Monitoru
Potreban hardver
- 1x Soldered NULA MINI pločica
- 1x Breadboard
- 1x Zelena LED-ica
- 1x Narančasta ili žuta LED-ica
- 1x Crvena LED-ica
- 3x Otpornik od 330 Ω
- 7x Jumper žice
- 1x USB-C kabel
Spajanje komponenti
Sve u ovom sklopu sjedi na f–j strani breadboarda. Ništa ne prelazi središnji kanal, a isti lanac od četiri dijela ponavlja se tri puta, svaki put šest redova dalje.
1. Umetni pločicu
Utisni NULA MINI u jedan kraj breadboarda tako da zauzme redove 25 do 30. Tijelo pločice prekriva stupce f do i, pa je na ovoj strani dostupan još samo stupac j.

2. Sagradi vodilicu mase
Kratka plava jumper žica vodi j30 (pin GND pločice) dolje do plave − vodilice. Sve u ovom krugu vraćat će se do mase preko te vodilice.

3. Povuci zelenu signalnu žicu
Zelena jumper žica ide od j27 (pin IO4) do j21, izvan pločice.

4. Dodaj otpornik zelene LED-ice
Otpornik od 330 Ω premošćuje h21 i h19, od reda u koji dolazi signalna žica do reda u kojem će sjediti LED-ica.
h21 i j21 su isti red, pa su otpornik i jumper žica spojeni iako sjede u različitim stupcima rupica. Ovdje je važan samo broj reda; koristi one rupice do kojih nožice udobno dosežu.
5. Utisni zelenu LED-icu
Zelena LED-ica ulazi s dugom nožicom u i19, redu u koji dolazi otpornik, i kratkom nožicom u i17.

6. Spoji zelenu LED-icu na masu
Plava jumper žica vodi j17 dolje do plave − vodilice, čime je prvi lanac dovršen.

7. Povuci narančastu signalnu žicu
Sada isti lanac ponovno, šest redova dalje. Narančasta jumper žica ide od j26 (pin IO3) do j15.

8. Dodaj otpornik narančaste LED-ice
Drugi otpornik od 330 Ω premošćuje h15 i h13.

9. Utisni narančastu LED-icu
Duga nožica u i13, kratka nožica u i11.

10. Spoji narančastu LED-icu na masu
Plava jumper žica vodi j11 dolje do plave − vodilice.

11. Povuci crvenu signalnu žicu
Crvena jumper žica ide od j25 (pin IO2) do j9. Duga je žica i ide zaobilaznim putem s pločice i natrag; ispod nje nije ništa spojeno, pa je ta petlja bezopasna.

12. Dodaj otpornik crvene LED-ice
Treći otpornik od 330 Ω premošćuje h9 i h7.

13. Utisni crvenu LED-icu
Duga nožica u i7, kratka nožica u i5.

14. Spoji crvenu LED-icu na masu
Posljednja plava jumper žica vodi j5 dolje do plave − vodilice. Krug je sada dovršen.

15. Spoji USB kabel
Utisni USB-C kabel u pločicu i u svoje računalo.

Cijeli krug na jedan pogled
| # | Dio | Iz | U | Što radi |
|---|---|---|---|---|
| 1 | Jumper žica | j30 (GND) | plava − vodilica | Čini vodilicu dijeljenom masom |
| 2 | Jumper žica | j27 (IO4) | j21 | Zeleni signal s pločice |
| 3 | 330 Ω | h21 | h19 | Ograničava struju, zeleni kanal |
| 4 | Zelena LED-ica | i19 (duga nožica) | i17 | Zeleno svjetlo |
| 5 | Jumper žica | j17 | plava − vodilica | Zelena LED-ica natrag na masu |
| 6 | Jumper žica | j26 (IO3) | j15 | Narančasti signal s pločice |
| 7 | 330 Ω | h15 | h13 | Ograničava struju, narančasti kanal |
| 8 | Narančasta LED-ica | i13 (duga nožica) | i11 | Narančasto svjetlo |
| 9 | Jumper žica | j11 | plava − vodilica | Narančasta LED-ica natrag na masu |
| 10 | Jumper žica | j25 (IO2) | j9 | Crveni signal s pločice |
| 11 | 330 Ω | h9 | h7 | Ograničava struju, crveni kanal |
| 12 | Crvena LED-ica | i7 (duga nožica) | i5 | Crveno svjetlo |
| 13 | Jumper žica | j5 | plava − vodilica | Crvena LED-ica natrag na masu |
Čitaj pločicu od dalekog kraja natrag i LED-ice izlaze crveno, narančasto, zeleno, pravi semafor koji leži na boku.
Kako radi konačni automat
Konačni automat je program koji je uvijek u točno jednoj od malog, fiksnog skupa situacija, i kreće se između njih na neki povod. Ovdje su situacije pet faza svjetla, a povod je sat.
Cijelo pamćenje automata su tri varijable:
| Varijabla | Značenje |
|---|---|
state | U kojoj smo fazi upravo sada |
lastChange | Trenutak u kojem smo ušli u tu fazu |
stateDuration | Koliko dugo namjeravamo ostati u njoj |
Svaka faza zatim radi iste dvije stvari, i samo te dvije: postavi LED-ice onako kako ta faza želi, i provjeri je li joj vrijeme prošlo. Ako je, imenuj sljedeću fazu, zapamti trenutni trenutak kao novu početnu točku, i postavi sljedeće trajanje. Taj uzorak, ponovljen pet puta, je cijeli program.
| Faza | Upaljene LED-ice | Trajanje |
|---|---|---|
GREEN | zelena | 5 s |
GREEN_BLINK | zelena, blinka svakih 0.4 s | 3 s |
ORANGE | narančasta | 2 s |
RED | crvena | 5 s |
RED_ORANGE | crvena i narančasta | 2 s |
Jedan cijeli krug je 17 sekundi, i onda počinje ponovno. Faza crveno-i-narančasto je upozorenje koje se koristi na mnogim europskim cestama da zeleno upravo dolazi.
enum na vrhu je ono što program čini čitljivim. Bez njega bi pet faza bili brojevi 0 do 4, i greška bi bila vrlo lako napraviti i vrlo teško opaziti. S njim pišeš GREEN i RED, i kod se čita gotovo kao opis prave stvari.
Zašto millis() a ne delay()
delay() zaustavlja program u mjestu. To je u redu kada program ima jednu stvar za raditi, ali pogledaj fazu blinkajućeg zelenog: zelena LED-ica mora blinkati svakih 0.4 sekunde dok trosekundni timer same faze nastavlja raditi. Dva sata, u isto vrijeme, u istom bloku koda. delay() to ne može, jer na koji god čekaš, drugi staje.
millis() vraća broj milisekundi od kada je pločica počela izvršavati trenutni program. Umjesto da čeka, program samo pogleda sat i pita je li prošlo dovoljno vremena?:
if (now - lastChange >= stateDuration) {
Petlja se nastavlja vrtjeti, tisućama puta u sekundi, i odgovor je gotovo svaki put false. Kada napokon postane true, faza se mijenja. Ništa nikada nije blokirano, pa bilo koliko timera može raditi jedan uz drugoga.
Dvije linije koje blinkanje drže poštenim
Kada program napušta GREEN za GREEN_BLINK, radi i ovo:
greenOn = true;
lastBlink = now;
Bez njih bi greenOn i lastBlink još držali ono na čemu su ostavljeni u prethodnom krugu. Zelena LED-ica mogla bi u fazu blinkanja ući već ugašena, i prvo bljeskanje sletjelo bi u pogrešnom trenutku. Dvije linije da faza počne iz poznatog stanja, mala stvar koju je lako izostaviti i naporno naknadno pronaći.
Primijeti također da blok GREEN_BLINK nikada ne piše u LED_GREEN na vrhu kako to rade druge faze. Samo gasi druge dvije. To je namjerno: dok ta faza radi, kod blinkanja posjeduje zelenu LED-icu, a postavljanje na HIGH u svakom prolazu poništilo bi blinkanje.
Kod
/**
**************************************************
*
* @file 7.8_LED_Traffic_Light.ino
* @brief Projekt koji simulira semafor s tri LED-ice, prolazeći kroz zeleno, blinkajuće zeleno, narančasto,
* crveno i crveno zajedno s narančastim, baš kao svjetla na mnogim europskim cestama.
* Uvodi konačni automat, ili FSM, a to je vrlo čest način pisanja programa koji se kreću
* kroz fiksni niz koraka a nikada ne koriste delay() za čekanje.
* Za detalje, shemu spajanja i više, pogledaj dokumentaciju primjera na: <link placeholder>
* @author Soldered
***************************************************
*/
/*
Ovo su varijable kojima predajemo brojeve pinova na koje smo spojili tri LED-ice.
NULA pločica ima logiku imenovanja pinova ovako: IO4, gdje je 4 broj koji dajemo varijabli.
Ako želiš koristiti drugi pin, pobrini se da koristiš pin označen s IO__.
Zapamti da svakoj od tih LED-ica treba vlastiti otpornik od 330 Ohma u seriji s njom. Taj otpornik ograničava koliko
struje teče, a bez njega LED-ica vuče više nego što je građena izdržati i može se oštetiti.
*/
const int LED_GREEN = 4;
const int LED_ORANGE = 3;
const int LED_RED = 2;
/*
Enum nam dopušta da damo imena fiksnom skupu vrijednosti. Bez njega bismo morali pamtiti da stanje 0 znači zeleno a
stanje 3 znači crveno, i tu bi grešku bilo vrlo lako napraviti a vrlo teško opaziti. S njim možemo pisati GREEN i
RED, i program se čita gotovo kao opis pravog semafora.
Svako od tih imena je jedno stanje u kojem naš semafor može biti, i uvijek može biti samo u jednom od njih odjednom.
*/
enum TrafficState {
GREEN,
GREEN_BLINK,
ORANGE,
RED,
RED_ORANGE
};
/*
Ova varijabla drži stanje u kojem smo upravo sada. Zajedno s dvjema ispod nje, ona je cijelo pamćenje našeg
automata: "lastChange" pamti trenutak u kojem smo ušli u trenutno stanje, a "stateDuration" drži koliko dugo namjeravamo
u njemu ostati. To je sve što konačnom automatu treba: gdje sam, od kada, i koliko dugo.
*/
TrafficState state = GREEN;
unsigned long lastChange = 0;
unsigned long stateDuration = 0;
/*
Te tri varijable koristi samo stanje blinkajućeg zelenog. "greenOn" pamti je li zelena LED-ica trenutno
upaljena, "blinkInterval" je koliko dugo tako ostaje prije nego se prebaci, a "lastBlink" pamti kada se posljednji put prebacila.
Slobodno eksperimentiraj s intervalom da blinkanje ubrzaš ili usporiš.
*/
bool greenOn = true;
const unsigned long blinkInterval = 400;
unsigned long lastBlink = 0;
void setup() {
/*
Serial.begin() uspostavlja serijsku komunikaciju između tvoje pločice i tvog računala preko USB kabela. Ovdje ga koristimo
da najavimo svaku promjenu stanja, što mnogo olakšava praćenje onoga što automat radi.
*/
Serial.begin(115200);
/*
pinMode() je funkcija koja konfigurira zadani pin da se ponaša ili kao ulaz ili u ovom slučaju kao izlaz.
Sve tri LED-ice su stvari u koje pišemo, pa sva tri pina idu u OUTPUT mod.
*/
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_ORANGE, OUTPUT);
pinMode(LED_RED, OUTPUT);
/*
Ovdje postavljamo početno stanje našeg automata. millis() vraća broj milisekundi koje su prošle od kada je pločica
počela izvršavati trenutni program, pa njegovo spremanje sada znači "zeleno stanje je počelo u ovom trenutku". Trajanje kaže
da namjeravamo ostati zeleni 5000 milisekundi, a to je pet sekundi.
*/
state = GREEN;
lastChange = millis();
stateDuration = 5000;
Serial.println("Traffic Light Example started!");
Serial.println("State: GREEN");
}
void loop() {
/*
Sat čitamo jednom na vrhu petlje i tu jednu vrijednost koristimo svugdje ispod, čime sve usporedbe
u ovom prolazu ostaju međusobno usklađene.
*/
unsigned long now = millis();
/*
Switch je urednije napisan dugi niz if naredbi kada jednu varijablu uspoređujemo s
nekoliko fiksnih vrijednosti. Ovdje nam dopušta da svakom stanju damo vlastiti blok koda, i izvršava se samo blok koji pripada
trenutnom stanju.
Primijeti da svaki blok radi iste dvije stvari: postavi LED-ice onako kako to stanje želi, i onda provjeri
je li mu vrijeme prošlo. Ako je, imenuje sljedeće stanje, pamti trenutni trenutak kao novu početnu točku, i
postavlja koliko dugo sljedeće stanje treba trajati. Taj uzorak ponovljen pet puta je cijeli automat.
Break na kraju svakog bloka govori switch naredbi da tu stane umjesto da propadne u sljedeći
blok.
*/
switch (state) {
case GREEN:
//Zeleno upaljeno, sve ostalo ugašeno.
digitalWrite(LED_GREEN, HIGH);
digitalWrite(LED_ORANGE, LOW);
digitalWrite(LED_RED, LOW);
//Nakon pet sekundi prelazimo na stanje blinkajućeg zelenog i dajemo mu tri sekunde.
if (now - lastChange >= stateDuration) {
state = GREEN_BLINK;
lastChange = now;
stateDuration = 3000;
/*
Ovdje postavljamo blinkanje prije nego mu predamo posao. Bez tih dviju linija blinkanje bi se nastavilo
odande gdje je stalo prethodni put kroz ciklus, pa bi zelena LED-ica mogla u ovo stanje ući ugašena
i prvo bljeskanje došlo bi u pogrešnom trenutku.
*/
greenOn = true;
lastBlink = now;
Serial.println("State: GREEN_BLINK");
}
break;
case GREEN_BLINK:
/*
Ovdje zelenu LED-icu ostavljamo na miru, jer blinkanje ispod je ono što odlučuje je li upaljena ili ugašena. Druge
dvije ostaju ugašene.
*/
digitalWrite(LED_ORANGE, LOW);
digitalWrite(LED_RED, LOW);
/*
Ovo je samo blinkanje. Svaki put kada je interval prošao, prebacimo greenOn na njegovu suprotnu vrijednost operatorom
"!" i novu vrijednost upišemo na pin. Budući da to radi po satu a ne na delay(), automat
iznad nastavlja raditi cijelo vrijeme dok LED-ica blinka.
*/
if (now - lastBlink >= blinkInterval) {
greenOn = !greenOn;
digitalWrite(LED_GREEN, greenOn);
lastBlink = now;
}
//Nakon tri sekunde blinkanja prelazimo na narančasto i dajemo mu dvije sekunde.
if (now - lastChange >= stateDuration) {
state = ORANGE;
lastChange = now;
stateDuration = 2000;
Serial.println("State: ORANGE");
}
break;
case ORANGE:
//Narančasto upaljeno, sve ostalo ugašeno.
digitalWrite(LED_GREEN, LOW);
digitalWrite(LED_ORANGE, HIGH);
digitalWrite(LED_RED, LOW);
//Nakon dvije sekunde prelazimo na crveno i dajemo mu pet sekundi.
if (now - lastChange >= stateDuration) {
state = RED;
lastChange = now;
stateDuration = 5000;
Serial.println("State: RED");
}
break;
case RED:
//Crveno upaljeno, sve ostalo ugašeno.
digitalWrite(LED_GREEN, LOW);
digitalWrite(LED_ORANGE, LOW);
digitalWrite(LED_RED, HIGH);
//Nakon pet sekundi prelazimo na crveno zajedno s narančastim i dajemo mu dvije sekunde.
if (now - lastChange >= stateDuration) {
state = RED_ORANGE;
lastChange = now;
stateDuration = 2000;
Serial.println("State: RED_ORANGE");
}
break;
case RED_ORANGE:
/*
Crveno i narančasto upaljeni u isto vrijeme, a to je na mnogim europskim semaforima upozorenje da zeleno dolazi.
*/
digitalWrite(LED_GREEN, LOW);
digitalWrite(LED_ORANGE, HIGH);
digitalWrite(LED_RED, HIGH);
//Nakon dvije sekunde smo natrag na početku, i cijeli ciklus počinje ponovno.
if (now - lastChange >= stateDuration) {
state = GREEN;
lastChange = now;
stateDuration = 5000;
Serial.println("State: GREEN");
}
break;
}
}
Što bi trebao vidjeti
Tri LED-ice se neprekidno vrte u ciklusu, a cijeli krug traje 17 sekundi.
Četiri od pet faza su mirne, a ona u sredini nije: tijekom blinkajućeg zelenog LED-ica bljesne četiri puta, upaljena 0.4 s i ugašena 0.4 s. Pola tih triju sekundi svaka LED-ica na pločici je ugašena, a to vrijedi znati prije nego odlučiš da je nešto prestalo raditi. Jedina faza s dvjema upaljenim LED-icama odjednom je ona crveno-narančasta na kraju.
U Serial Monitoru
Za razliku od većine projekata u ovom kitu, ova skica govori. Otvori Serial Monitor na 115200 bauda i svaka promjena faze najavljuje se dok se događa, pa možeš pratiti automat i kada ne gledaš LED-ice. Neposredno nakon reseta vidjet ćeš:
Traffic Light Example started!
State: GREEN
State: GREEN_BLINK
State: ORANGE
State: RED
State: RED_ORANGE
State: GREEN
a onda tih istih pet linija iznova i iznova.
Praznine između linija su sama trajanja: pet sekundi nakon što se pojavi State: RED, slijedi ga State: RED_ORANGE. To Monitor čini korisnim testom sam po sebi: ako te linije nastavljaju teći pravim tempom a LED-ice ne rade ništa, program je u redu a greška je u spajanju.
Ako jedna boja nikada ne zasvijetli, kriv je lanac te LED-ice, a druge dvije dokazuju da su pločica i kod u redu. Provjeri prvo njezinu polarnost, pa onda premošćuje li njezin otpornik stvarno dva različita reda. Ako ništa uopće ne zasvijetli, provjeri žicu mase u j30: bez nje nijedan od triju lanaca nema put natrag.
Cijeli primjer
Pogledaj cijeli kod primjera na linku ispod:
7.8_LED_Traffic_Light.ino
Simulacija semafora pomoću NULA MINI pločice i tri LED-ice. Prikazuje konačne automate, neblokirajuće mjerenje vremena pomoću millis(), i pobuđivanje više izlaza iz jednog vremenskog niza.