Kolejna, piąta już, część cyklu o sterowaniu makietą małej stacji kolejowej to opis programu a zarazem sposobu działania pulpitu, który w tym przypadku jest „zarządcą” całego systemu.
Jako że sporo instrukcji zostało już opisanych w poprzednich artykułach to sprawy oczywiste będę traktował z pewną lekkością, aby się nie powtarzać a zarazem nie tworzyć z tego materiału „opasłej księgi”. Artykuł ten – jednak – i tak jest dosyć długi.
A zatem do rzeczy.
Dla przypomnienia – pulpit wygląda następująco :
Pod pulpitem znajduje się oczywiście płytka Arduino MEGA, która jest podłączona zgodnie
z przedstawionym wcześniejszym schematem (część 2).
Na początek deklaracja wejść/wyjść naszego mikrokontrolera:
// przyciski pulpitu //
#define key_a1 A0
#define key_a2 A1
#define key_b1 A2
#define key_c2 A3
#define key_d1 A4
#define key_e2 A5
#define key_f1 A6
#define key_f2 A7
#define key_rozw_przebiegu_1 A9
#define key_rozw_przebiegu_2 A8
#define key_Zw1 2
#define key_Zw2 3
#define key_Zw3_4 4
#define key_Zw5 5
#define key_Wk 6
#define key_SzA 7
#define key_SzB 8
#define key_SzC 9
#define key_SzD 10
#define key_SzE 11
#define key_SzF 12
// diody pulpitu //
#define led_A_czerwone 22
#define led_A_zielone 23
#define led_A_biale 24
#define led_B_czerwone 25
#define led_B_zielone 26
#define led_B_biale 27
#define led_C_czerwone 28
#define led_C_zielone 29
#define led_C_biale 30
#define led_D_czerwone 31
#define led_D_zielone 32
#define led_D_biale 33
#define led_E_czerwone 34
#define led_E_zielone 35
#define led_E_biale 36
#define led_F_czerwone 37
#define led_F_zielone 38
#define led_F_biale 39
#define led_Zw1_plus 40
#define led_Zw1_minus 41
#define led_Zw2_plus 42
#define led_Zw2_minus 43
#define led_Zw3_4_plus 44
#define led_Zw3_4_minus 45
#define led_Zw5_plus 46
#define led_Zw5_minus 47
#define led_a1 48
#define led_a2 49
#define led_b1 50
#define led_c2 51
#define led_d1 52
#define led_e2 53
#define led_f1 14
#define led_f2 15
#define led_Wk_otwarta 16
#define led_Wk_zamknieta 17
#define buzzer 18
Jak zatem widać wszystkie wyjścia i wejścia używane w pulpicie zostały pogrupowane i otrzymały swoje nazwy przypisane do konkretnych wyjść. Pora teraz zadeklarować zmienne, których użyjemy w tym programie. Większość zmiennych deklarujemy jako typ int (integer) – czyli liczby całkowite z określonego przedziału.
Na początek zmienna data która będzie przyjmowała wartość informacji przesyłanych przez makietę do pulpitu:
int data;
Pozostałe zmienne od razu otrzymują wartości początkowe.
Zmienne opisujące stan rozjazdów:
int stan_Zw1 = 1; // na wprost //
int stan_Zw2 = 1; // na wprost //
int stan_Zw3_4 = 1; // na wprost //
int stan_Zw5 = 1; // na wprost //
int stan_Wk = 1; // zamknięta //
Zmienne opisujące stan semaforów:
int stan_A = 1; // S1 czerwone //
int stan_B = 1; // S1 czerwone //
int stan_C = 1; // S1 czerwone //
int stan_D = 1; // S1 czerwone //
int stan_E = 1; // S1 czerwone //
int stan_F = 1; // S1 czerwone //
Zmienne opisujące czy dany rozjazd jest utwierdzony jakimś przebiegiem:
int utwierdzenie_Zw1 = 0;
int utwierdzenie_Zw2 = 0;
int utwierdzenie_Zw3_4 = 0;
int utwierdzenie_Zw5 = 0;
int utwierdzenie_Wk = 0;
Zmienne opisujące stan przebiegów pociągowych:
int stan_a1 = 0;
int stan_a2 = 0;
int stan_b1 = 0;
int stan_c2 = 0;
int stan_d1 = 0;
int stan_e2 = 0;
int stan_f1 = 0;
int stan_f2 = 0;
Zmienne związane z sygnałami zastępczymi:
int stan_SzA = 0;
int stan_SzB = 0;
int stan_SzC = 0;
int stan_SzD = 0;
int stan_SzE = 0;
int stan_SzF = 0;
Zmienne przyjmujące wartości czasu są natomiast typu unsigned long.
Licznik czasu jest wywołany funkcją millis(). Startuje on w momencie uruchomienia programu od wartości 0 i co każdą milisekundę zwiększa swą wartość o 1, dlatego jedna sekunda pracy systemu to wartość aż 1000 i trzeba się przygotować na spore liczby. Do tego właśnie nadaje się ten typ danych posiadający przedział „pracy” od zera do 4 294 967 295, co daje 1193 godziny pracy licznika czasu (potem się wyzeruje i zacznie się odliczanie od nowa) i powinno wystarczyć. Jednorazowe uruchomienie makiety trwa maksymalnie kilka godzin, a do dyspozycji mamy prawie 50 dni do ponownego wyzerowania licznika. Gdyby jednak ktoś potrzebował nieprzerwanej pracy na dłużej, to powinien dokonać pewnych modyfikacji.
Zatem nasze zmienne oparte o licznik czasu to:
unsigned long czas;
unsigned long czas_SzA = 0;
unsigned long czas_SzB = 0;
unsigned long czas_SzC = 0;
unsigned long czas_SzD = 0;
unsigned long czas_SzE = 0;
unsigned long czas_SzF = 0;
Pora teraz na konfigurację początkową naszego „komputera”
// —————————————————————————- S E T U P ——————
void setup()
{
Zaczynamy od uruchomienia komunikacji:
Serial.begin(9600);
Następnie czas na skonfigurowanie wszystkich przycisków pulpitu jako wejścia PULL_UP
// przyciski pulpitu //
pinMode(key_a1, INPUT_PULLUP);
pinMode(key_a2, INPUT_PULLUP);
pinMode(key_b1, INPUT_PULLUP);
pinMode(key_c2, INPUT_PULLUP);
pinMode(key_d1, INPUT_PULLUP);
pinMode(key_e2, INPUT_PULLUP);
pinMode(key_f1, INPUT_PULLUP);
pinMode(key_f2, INPUT_PULLUP);
pinMode(key_rozw_przebiegu_1, INPUT_PULLUP);
pinMode(key_rozw_przebiegu_2, INPUT_PULLUP);
pinMode(key_Zw1, INPUT_PULLUP);
pinMode(key_Zw2, INPUT_PULLUP);
pinMode(key_Zw3_4, INPUT_PULLUP);
pinMode(key_Zw5, INPUT_PULLUP);
pinMode(key_Wk, INPUT_PULLUP);
pinMode(key_SzA, INPUT_PULLUP);
pinMode(key_SzB, INPUT_PULLUP);
pinMode(key_SzC, INPUT_PULLUP);
pinMode(key_SzD, INPUT_PULLUP);
pinMode(key_SzE, INPUT_PULLUP);
pinMode(key_SzF, INPUT_PULLUP);
Teraz pora na skonfigurowanie wyjść i przypisanie im stanów początkowych:
// diody pulpitu //
pinMode(led_A_czerwone, OUTPUT);
pinMode(led_A_zielone, OUTPUT);
pinMode(led_A_biale, OUTPUT);
digitalWrite(led_A_czerwone, HIGH);
digitalWrite(led_A_zielone, LOW);
digitalWrite(led_A_biale, LOW);
pinMode(led_B_czerwone, OUTPUT);
pinMode(led_B_zielone, OUTPUT);
pinMode(led_B_biale, OUTPUT);
digitalWrite(led_B_czerwone, HIGH);
digitalWrite(led_B_zielone, LOW);
digitalWrite(led_B_biale, LOW);
pinMode(led_C_czerwone, OUTPUT);
pinMode(led_C_zielone, OUTPUT);
pinMode(led_C_biale, OUTPUT);
digitalWrite(led_C_czerwone, HIGH);
digitalWrite(led_C_zielone, LOW);
digitalWrite(led_C_biale, LOW);
pinMode(led_D_czerwone, OUTPUT);
pinMode(led_D_zielone, OUTPUT);
pinMode(led_D_biale, OUTPUT);
digitalWrite(led_D_czerwone, HIGH);
digitalWrite(led_D_zielone, LOW);
digitalWrite(led_D_biale, LOW);
pinMode(led_E_czerwone, OUTPUT);
pinMode(led_E_zielone, OUTPUT);
pinMode(led_E_biale, OUTPUT);
digitalWrite(led_F_czerwone, HIGH);
digitalWrite(led_F_zielone, LOW);
digitalWrite(led_F_biale, LOW);
pinMode(led_Zw1_plus, OUTPUT);
pinMode(led_Zw1_minus, OUTPUT);
pinMode(led_Zw2_plus, OUTPUT);
pinMode(led_Zw2_minus, OUTPUT);
pinMode(led_Zw3_4_plus, OUTPUT);
pinMode(led_Zw3_4_minus, OUTPUT);
pinMode(led_Zw5_plus, OUTPUT);
pinMode(led_Zw5_minus, OUTPUT);
digitalWrite(led_Zw1_plus, HIGH);
digitalWrite(led_Zw1_minus, LOW);
digitalWrite(led_Zw2_plus, HIGH);
digitalWrite(led_Zw2_minus, LOW);
digitalWrite(led_Zw3_4_plus, HIGH);
digitalWrite(led_Zw3_4_minus, LOW);
digitalWrite(led_Zw5_plus, HIGH);
digitalWrite(led_Zw5_minus, LOW);
pinMode(led_a1, OUTPUT);
pinMode(led_a2, OUTPUT);
pinMode(led_b1, OUTPUT);
pinMode(led_c2, OUTPUT);
pinMode(led_d1, OUTPUT);
pinMode(led_e2, OUTPUT);
pinMode(led_f1, OUTPUT);
pinMode(led_f2, OUTPUT);
digitalWrite(led_a1, LOW);
digitalWrite(led_a2, LOW);
digitalWrite(led_b1, LOW);
digitalWrite(led_c2, LOW);
digitalWrite(led_d1, LOW);
digitalWrite(led_e2, LOW);
digitalWrite(led_f1, LOW);
digitalWrite(led_f1, LOW);
pinMode(led_Wk_otwarta, OUTPUT);
pinMode(led_Wk_zamknieta, OUTPUT);
digitalWrite(led_Wk_otwarta, LOW);
digitalWrite(led_Wk_zamknieta, HIGH);
Na koniec funkcji SETUP opisujemy wyjście nazwane jako „buzzer” i przypisujemy mu w krótkich przerwach stan wysoki i niski, co spowoduje dwukrotne „piknięcie” generatora dźwiękowego (buzzera) po uruchomieniu się systemu.
pinMode(buzzer, OUTPUT);
digitalWrite(buzzer, HIGH);
delay(100);
digitalWrite(buzzer, LOW);
delay(100);
digitalWrite(buzzer, HIGH);
delay(100);
digitalWrite(buzzer, LOW);
Oczywiście stan początkowy wszystkich wyjść jest opisany i jasno zadeklarowany, ale aby od początku mieć pewność że wszystkie zmienne mają poprawne wartości i wszystko się zgadza wywołujemy funkcję:
wyswietl_stan_urzadzen();
} // koniec bloku setup //
Teraz – coś czego wcześniej nie było – czyli króciutka funkcja powodująca, że po jej wywołaniu nastąpi „piknięcie” generatora dźwiękowego. W tym projekcie użyłem standardowego buzzera z generatorem (oczywiście na napięcie 5V), podłączonego do wyjścia nr 18 naszego Arduino. (UWAGA! nie ma go na schemacie w części 2 artykułu!)
Używanie sygnalizacji dźwiękowej do np. potwierdzenia wciśnięcia przycisków pulpitu to oczywiście kwestia gustu i można z niej zrezygnować, warto jednak przynajmniej na początkowym etapie tworzenia jakiegoś projektu mieć potwierdzenie że system faktycznie wykrył wciśnięcie przycisku, bo nie zawsze jest to takie oczywiste. Dodatkową zaletą tej funkcji jest pewna zwłoka czasowa po wciśnięciu przycisku (dokładnie 0,5 sekundy) powodująca że wciśnięcie przycisku zostaje odczytane jako pojedyncze wywołanie a nie np. 64 razy – co mogło by mieć miejsce gdyby owej zwłoki nie było. Z całą pewnością powodowałoby to nieprzewidziane skutki w działaniu programu.
// —————————————————————————– B E E P ——————-
void beep()
{
digitalWrite(buzzer, HIGH);
delay(250);
digitalWrite(buzzer, LOW);
delay(250);
}
Pora teraz na funkcję odczyt_seriala czyli „rozmówki makieta – pulpit”.
Każdą informację jaką może wysłać którykolwiek z segmentów makiety trzeba przypisać do naszych zadeklarowanych zmiennych.
Na przykład kiedy nasz pulpit odczyta kod 4022 to przypisze zmiennej stan_Zw2 wartość 2
itd. zgodnie z poniższym schematem :
// —————————————————————- O D C Z Y T S E R I A L A ——
void odczyt_seriala()
{
data = Serial.parseInt();
// rozjazdy //
if (data == 4011)stan_Zw1 = 1; // na wprost (plus) //
if (data == 4012)stan_Zw1 = 2; // na bok (minus) //
if (data == 4021)stan_Zw2 = 1;
if (data == 4022)stan_Zw2 = 2;
if (data == 5011)stan_Zw3_4 = 1;
if (data == 5012)stan_Zw3_4 = 2;
if (data == 6011)stan_Zw5 = 1;
if (data == 6012)stan_Zw5 = 2;
// wykolejnica //
if (data == 4031)stan_Wk = 1; // zamknięta //
if (data == 4032)stan_Wk = 2; // otwarta //
// semafory //
if (data == 4041)stan_A = 1; // S1 czerwone //
if (data == 4042)stan_A = 2; // S2 zielone //
if (data == 4043)stan_A = 3; // Sz biale //
if (data == 4044)stan_A = 4; // S5 pomaranczowe //
if (data == 4045)stan_A = 5; // S13 2*pomaranczowe //
if (data == 4046)stan_A = 6; // S12 2*pom / miganie //
if (data == 4047)stan_A = 7; // S10 zielone + pomaranczowe //
if (data == 4051)stan_B = 1; // S1 //
if (data == 4052)stan_B = 2; // S2 //
if (data == 4053)stan_B = 3; // Sz //
if (data == 4061)stan_C = 1; // S1 //
if (data == 4062)stan_C = 2; // S2 //
if (data == 4063)stan_C = 3; // Sz //
if (data == 4064)stan_C = 4; // S10 //
if (data == 6021)stan_D = 1; // S1 //
if (data == 6022)stan_D = 2; // S2 //
if (data == 6023)stan_D = 3; // Sz //
if (data == 6031)stan_E = 1; // S1 //
if (data == 6032)stan_E = 2; // S2 //
if (data == 6033)stan_E = 3; // Sz //
if (data == 6034)stan_E = 4; // S10 //
if (data == 6041)stan_F = 1; // S1 czerwone //
if (data == 6042)stan_F = 2; // S2 zielone //
if (data == 6043)stan_F = 3; // Sz biale //
if (data == 6044)stan_F = 4; // S5 pomaranczowe //
if (data == 6045)stan_F = 5; // S13 2*pomaranczowe //
if (data == 6046)stan_F = 6; // S12 2*pom / miganie //
if (data == 6047)stan_F = 7; // S10 zielone + pomaranczowe //
Na koniec funkcji odczyt_seriala wywołujemy „odświeżenie” stanu pulpitu, aby pokazał on zmiany, które nastąpiły po odczytaniu danych z makiety:
wyswietl_stan_urzadzen();
}
Nadeszła kolej na następną funkcję, o której już tu kilkakrotnie wspominałem. Jest to funkcja wyświetlająca na pulpicie wszystkie informacje:
// ——————————————————- WYSWIETL STAN URZADZEN
void wyswietl_stan_urzadzen()
{
if (stan_Zw1 == 1) {
digitalWrite(led_Zw1_plus, HIGH);
digitalWrite(led_Zw1_minus, LOW);
}
if (stan_Zw1 == 2) {
digitalWrite(led_Zw1_plus, LOW);
digitalWrite(led_Zw1_minus, HIGH);
}
Przykładowo – widać zatem, że w zależności od wartości jaką będzie przyjmowała zmienna stan_Zw1 (1 lub 2) na pulpicie nastąpi zaświecenie odpowiedniej diody i jednocześnie gasnąć będzie druga – nieadekwatna do danego stanu – dioda zwrotnicy rozjazdu Zw1.
if (stan_Zw2 == 1) {
digitalWrite(led_Zw2_plus, HIGH);
digitalWrite(led_Zw2_minus, LOW);
}
if (stan_Zw2 == 2) {
digitalWrite(led_Zw2_plus, LOW);
digitalWrite(led_Zw2_minus, HIGH);
}
if (stan_Zw3_4 == 1) {
digitalWrite(led_Zw3_4_plus, HIGH);
digitalWrite(led_Zw3_4_minus, LOW);
}
if (stan_Zw3_4 == 2) {
digitalWrite(led_Zw3_4_plus, LOW);
digitalWrite(led_Zw3_4_minus, HIGH);
}
if (stan_Zw5 == 1) {
digitalWrite(led_Zw5_plus, HIGH);
digitalWrite(led_Zw5_minus, LOW);
}
if (stan_Zw5 == 2) {
digitalWrite(led_Zw5_plus, LOW);
digitalWrite(led_Zw5_minus, HIGH);
}
if (stan_Wk == 1) {
digitalWrite(led_Wk_zamknięta, HIGH);
digitalWrite(led_Wk_otwarta, LOW);
}
if (stan_Wk == 2) {
digitalWrite(led_Wk_zamknięta, LOW);
digitalWrite(led_Wk_otwarta, HIGH);
}
if (stan_A == 1) {
digitalWrite(led_A_czerwone, HIGH);
digitalWrite(led_A_zielone, LOW);
digitalWrite(led_A_biale, LOW);
}
if (stan_A == 2 || stan_A == 4 || stan_A == 5 || stan_A == 6 || stan_A == 7) {
digitalWrite(led_A_czerwone, LOW);
digitalWrite(led_A_zielone, HIGH);
digitalWrite(led_A_biale, LOW);
}
if (stan_A == 3) {
digitalWrite(led_A_czerwone, LOW);
digitalWrite(led_A_zielone, LOW);
digitalWrite(led_A_biale, HIGH);
}
if (stan_B == 1) {
digitalWrite(led_B_czerwone, HIGH);
digitalWrite(led_B_zielone, LOW);
digitalWrite(led_B_biale, LOW);
}
if (stan_B == 2) {
digitalWrite(led_B_czerwone, LOW);
digitalWrite(led_B_zielone, HIGH);
digitalWrite(led_B_biale, LOW);
}
if (stan_B == 3) {
digitalWrite(led_B_czerwone, LOW);
digitalWrite(led_B_zielone, LOW);
digitalWrite(led_B_biale, HIGH);
}
if (stan_C == 1) {
digitalWrite(led_C_czerwone, HIGH);
digitalWrite(led_C_zielone, LOW);
digitalWrite(led_C_biale, LOW);
}
if (stan_C == 2 || stan_C == 4) {
digitalWrite(led_C_czerwone, LOW);
digitalWrite(led_C_zielone, HIGH);
digitalWrite(led_C_biale, LOW);
}
if (stan_C == 3) {
digitalWrite(led_C_czerwone, LOW);
digitalWrite(led_C_zielone, LOW);
digitalWrite(led_C_biale, HIGH);
}
if (stan_D == 1) {
digitalWrite(led_D_czerwone, HIGH);
digitalWrite(led_D_zielone, LOW);
digitalWrite(led_D_biale, LOW);
}
if (stan_D == 2) {
digitalWrite(led_D_czerwone, LOW);
digitalWrite(led_D_zielone, HIGH);
digitalWrite(led_D_biale, LOW);
}
if (stan_D == 3) {
digitalWrite(led_D_czerwone, LOW);
digitalWrite(led_D_zielone, LOW);
digitalWrite(led_D_biale, HIGH);
}
if (stan_E == 1) {
digitalWrite(led_E_czerwone, HIGH);
digitalWrite(led_E_zielone, LOW);
digitalWrite(led_E_biale, LOW);
}
if (stan_E == 2 || stan_E == 4) {
digitalWrite(led_E_czerwone, LOW);
digitalWrite(led_E_zielone, HIGH);
digitalWrite(led_E_biale, LOW);
}
if (stan_E == 3) {
digitalWrite(led_E_czerwone, LOW);
digitalWrite(led_E_zielone, LOW);
digitalWrite(led_E_biale, HIGH);
}
if (stan_F == 1) {
digitalWrite(led_F_czerwone, HIGH);
digitalWrite(led_F_zielone, LOW);
digitalWrite(led_F_biale, LOW);
}
if (stan_F == 2 || stan_F == 4 || stan_F == 5 || stan_F == 6 || stan_F == 7) {
digitalWrite(led_F_czerwone, LOW);
digitalWrite(led_F_zielone, HIGH);
digitalWrite(led_F_biale, LOW);
}
if (stan_F == 3) {
digitalWrite(led_F_czerwone, LOW);
digitalWrite(led_F_zielone, LOW);
digitalWrite(led_F_biale, HIGH);
}
} // wyswietl stan urzadzen //
Wszystkie funkcje pomocnicze mamy już opisane, zatem pora na główną pętlę programu:
// —————————————————————————– L O O P ——————-
void loop()
{
Na początek uruchamiamy zmienną czas
czas = millis();
Kolejny krok to sprawdzenie czy do portu komunikacyjnego przyszły jakieś dane. Jeśli tak – to konieczne jest wywołanie funkcji odczyt_seriala, aby te dane przypisać do odpowiednich zmiennych.
if (Serial.available() > 0)odczyt_seriala();
Teraz sprawdzamy czy został naciśnięty przycisk key_Zw1. Jeśli tak – to przechodzimy do bloku opisanego w nawiasach klamrowych {}
if (digitalRead(key_Zw1) == LOW) {
A zatem – na początek sygnał dźwiękowy – jako potwierdzenie odczytania przez system wciśnięcia przycisku.
beep();
Teraz sprawdzamy czy rozjazd Zw1 nie jest przypadkiem utwierdzony jakimś przebiegiem pociągowym i czy możliwe jest jego przełożenie. Jeśli wartość utwierdzenie_Zw1==0 (czyli nie jest utwierdzony) to program może wysłać informację do makiety o zmianę położenia tegoż rozjazdu – w zależności od jego obecnego położenia.
if (utwierdzenie_Zw1 == 0) {
if (stan_Zw1 == 1)Serial.println(1012);
if (stan_Zw1 == 2)Serial.println(1011);
}
}
Jeśli natomiast będzie utwierdzony – to usłyszymy tylko sygnał dźwiękowy potwierdzający wciśnięcie przycisku i nic więcej się nie stanie.
Analogicznie postępujemy z wszystkimi przyciskami rozjazdów i wykolejnicy:
if (digitalRead(key_Zw2) == LOW) {
beep();
if (utwierdzenie_Zw2 == 0) {
if (stan_Zw2 == 1)Serial.println(1022);
if (stan_Zw2 == 2)Serial.println(1021);
}
}
if (digitalRead(key_Zw3_4) == LOW) {
beep();
if (utwierdzenie_Zw3_4 == 0) {
if (stan_Zw3_4 == 1)Serial.println(2012);
if (stan_Zw3_4 == 2)Serial.println(2011);
}
}
if (digitalRead(key_Zw5) == LOW) {
beep();
if (utwierdzenie_Zw5 == 0) {
if (stan_Zw5 == 1)Serial.println(3012);
if (stan_Zw5 == 2)Serial.println(3011);
}
}
if (digitalRead(key_Wk) == LOW) {
beep();
if (utwierdzenie_Wk == 0) {
if (stan_Wk == 1 && stan_Zw2==1)Serial.println(1032);
if (stan_Wk == 2 && stan_Zw2==1)Serial.println(1031);
}
}
Teraz będzie trochę trudniej.
Postaram się zatem rozbić ten blok „na drobne” i przejrzyście opisać:
// przebieg a1 //
Sprawdzamy zatem czy przycisk key_a1 został wciśnięty i jeśli tak się stało to przechodzimy do bloku w nawiasach klamrowych {}
if (digitalRead(key_a1) == LOW) {
Na początek potwierdzenie dźwiękowe:
beep();
a teraz dosyć skomplikowany warunek, który musi być spełniony, aby system uaktywnił przebieg a1.
Po kolei wygląda to tak:
Jeśli rozjazd Zw1 == 1 (czyli na wprost) i (&&) rozjazd Zw2 == 1 (czyli również na wprost) i wykolejnica zamknięta (czyli stan_Wk==1) i rozjazdy Zw3_4 również są w położeniu na wprost i rozjazd Zw_5==1 (czyli również na wprost) i stan_a1==0 (czyli nie ma obecnie takiego przebiegu) i stan_a2==0 i … stan_f2==0 (czyli nie ma obecnie aktywnego żadnego z tych przebiegów) – to znaczy, że wszystkie te warunki są spełnione równocześnie i będzie mógł być uruchomiony przebieg a1 :
if (stan_Zw1 == 1 && stan_Zw2 == 1 && stan_Wk == 1
&& stan_Zw3_4 == 1 && stan_Zw5 == 1 && stan_a1 == 0
&& stan_a2 == 0 && stan_b1 == 0 && stan_c2 == 0
&& stan_e2 == 0 && stan_f1 == 0 && stan_f2 == 0)
{
Jeśli warunek jest spełniony to przypisujemy zmiennej a1 wartość 1, co oznacza że przebieg a1 jest obecnie aktywny i utwierdzony.
stan_a1 = 1; // utwierdzenie przebiegu a1 //
Zapalamy na pulpicie grupy diód a1 sygnalizujących tenże fakt:
digitalWrite(led_a1, HIGH);
oraz wysyłamy informację do makiety o podanie odpowiedniego sygnału na semaforze:
if (stan_D == 1)Serial.println(1044); // A >> S5 //
if (stan_D == 2)Serial.println(1042); // A >> S2 //
Ale jako że mamy tutaj dwie możliwości czyli :
– semafor D wskazuje sygnał STÓJ (D==1) – to na semaforze A chcemy aby wyświetlił się sygnał S5 – więc wysyłamy informację 1044
– semafor D wskazuje sygnał S2 – czyli zezwala na dalszą jazdę (D==2) – to na semaforze A chcemy wyświetlić również sygnał S2 – więc wysyłamy informację 1042
}
} // a1 //
Widać zatem, że uaktywnienie jakiegoś przebiegu i zarazem podanie odpowiedniego sygnału na semaforze to dosyć rozbudowany warunek, który musi zostać spełniony.
Właściwie można uogólnić i stwierdzić, że wszystko co robi nasz system srk jest wyłącznie sprawdzaniem czy jakiś warunek jest spełniony czy też nie.
Zatem niezbędna jest znajomość zasad prowadzenia ruchu kolejowego i przepisów kolejowych. Warto więc (zanim zabierzemy się za pisanie oprogramowania) sprawdzić czy posiadamy poprawnie opisane sytuacje ruchowe dla naszej stacji, gdyż wyeliminowanie błędów z tych – jak by nie było – dość skomplikowanych warunków będzie niezwykle trudne, a na pewno czasochłonne.
Dla wszystkich, którzy chcą dokładniej przyjrzeć się tym zasadom polecam zaglądnięcie do artykułów Leszka, dotyczących przebiegów na tejże właśnie stacji i porównanie opisu słownego z zapisem kodu tego programu.\
Kolejne przebiegi to nic innego jak dalsze warunki dla każdego z tych „przypadków”:
// przebieg a2 //
if (digitalRead(key_a2) == LOW) {
beep();
if (stan_Zw1 == 2 && stan_Zw5 == 2
&& stan_a1 == 0 && stan_a2 == 0 && stan_b1 == 0 && stan_c2 == 0
&& stan_d1 == 0 && stan_f1 == 0 && stan_f2 == 0) {
stan_a2 = 1; // utwierdzenie przebiegu a2 //
digitalWrite(led_a2, HIGH);
if (stan_E == 1)Serial.println(1045); // A >> S 13 //
if (stan_E == 4)Serial.println(1046); // A >> S 12 //
}
} // a2 //
// przebieg b1 //
if (digitalRead(key_b1) == LOW) {
beep();
if (stan_Zw1 == 1 && stan_Zw2 == 1 && stan_Wk == 1
&& stan_a1 == 0 && stan_a2 == 0 && stan_b1 == 0 && stan_c2 == 0
&& stan_f2 == 0) {
stan_b1 = 1; // utwierdzenie przebiegu b1 //
digitalWrite(led_b1, HIGH);
Serial.println(1052); // B >> S2 zielone //
// jesli przebieg f1 jest juz utwierdzony to zmiana semafora F na zielony
if (stan_f1 == 1) Serial.println(3042); // F >> S2 zielone //
}
} // b1 //
// przebieg c2 //
if (digitalRead(key_c2) == LOW) {
beep();
if (stan_Zw1 == 2
&& stan_a1 == 0 && stan_a2 == 0 && stan_b1 == 0 && stan_f1 == 0 && stan_c2 == 0) {
stan_c2 = 1; // utwierdzenie przebiegu c2 //
digitalWrite(led_c2, HIGH);
Serial.println(1064); // C >> S 10 zielone+pomaranczowe //
// jesli przebieg f2 jest juz utwierdzony to zmiana semafora F na zielony
if (stan_f2 == 1) Serial.println(3046); // F >> S12 //
}
} // c2 //
// przebieg d1 //
if (digitalRead(key_d1) == LOW) {
beep();
if (stan_Zw3_4 == 1 && stan_Zw5 == 1
&& stan_a2 == 0 && stan_e2 == 0 && stan_f1 == 0 && stan_f2 == 0 && stan_d1 == 0) {
stan_d1 = 1; // utwierdzenie przebiegu d1 //
digitalWrite(led_d1, HIGH);
Serial.println(3022); // D >> S2 zielone //
// jesli przebieg a1 jest juz utwierdzony to zmiana semafora A na zielony
if (stan_a1 == 1) Serial.println(1042); // A >> S2 zielone
}
} // d1 //
// przebieg e2 //
if (digitalRead(key_e2) == LOW) {
beep();
if (stan_Zw5 == 2
&& stan_a2 == 0 && stan_d1 == 0 && stan_f1 == 0 && stan_f2 == 0 && stan_e2 == 0) {
stan_e2 = 1; // utwierdzenie przebiegu e2 //
digitalWrite(led_e2, HIGH);
Serial.println(3034); // E >> S10 //
// jesli przebieg a2 jest juz utwierdzony to zmiana semafora A na zielony
if (stan_a2 == 1) Serial.println(1046); // A >> S 12 //
}
} // e2 //
// przebieg f1 //
if (digitalRead(key_f1) == LOW) {
beep();
if (stan_Zw5 == 1 && stan_Zw3_4 == 1 && stan_Zw2 == 1 && stan_Zw1 == 1 && stan_Wk == 1
&& stan_a1 == 0 && stan_a2 == 0 && stan_c2 == 0 && stan_d1 == 0 && stan_e2 == 0 && stan_f2 == 0 && stan_f1 == 0) {
stan_f1 = 1; // utwierdzenie przebiegu f1 //
digitalWrite(led_f1, HIGH);
if (stan_B == 1)Serial.println(3044); // F >> S5 pomaranczowe //
if (stan_B == 2)Serial.println(3042); // F >> S2 zielone //
}
} // f1 //
// przebieg f2 //
if (digitalRead(key_f2) == LOW) {
beep();
if (stan_Zw5 == 2 && stan_Zw1 == 2
&& stan_a1 == 0 && stan_a2 == 0 && stan_b1 == 0 && stan_d1 == 0 && stan_e2 == 0 && stan_f1 == 0 && stan_f2 == 0) {
stan_f2 = 1; // utwierdzenie przebiegu f2 //
digitalWrite(led_f2, HIGH);
if (stan_C == 1)Serial.println(3045); // F >> S13 2*pomaranczowe //
if (stan_C == 4)Serial.println(3046); // F >> S12 //
}
} // f2 //
Oczywiście każdy przebieg – po stwierdzeniu „na gruncie”, że nasz modelowy pociąg zwolnił określone rozjazdy – musimy rozwiązać. Do tego służą przyciski rozwiązywania przebiegów.
Zatem sprawdźmy co zrobi nasz program po wciśnięciu przycisku rozwiązania_przebiegu_1:
// rozwiazywanie przebiegow //
if (digitalRead(key_rozw_przebiegu_1) == LOW) {
Najpierw oczywiście sygnał dźwiękowy:
beep();
Dalej zerujemy przebiegi dla tej głowicy i podane sygnały zastępcze (jeśli takowe wystąpią):
stan_a1 = 0;
stan_a2 = 0;
stan_b1 = 0;
stan_c2 = 0;
stan_SzA = 0;
stan_SzB = 0;
stan_SzC = 0;
Następnie „gasimy” diody wskazujące utwierdzony przebieg na pulpicie:
digitalWrite(led_a1, LOW);
digitalWrite(led_a2, LOW);
digitalWrite(led_b1, LOW);
digitalWrite(led_c2, LOW);
oraz wysyłamy sygnał stój na semafory A,B,C:
Serial.println(1041);
Serial.println(1051);
Serial.println(1061);
Następnie zmieniamy sygnał dla przebiegu powiązanego z tą głowicą – czyli f1 lub f2 – jeśli oczywiście jest aktywny:
// zmiana semafora F jesli podany jest wjazd
if (stan_f1 == 1)Serial.println(3044); // F >> S5 pomaranczowe //
if (stan_f2 == 1)Serial.println(3045); // F >> S13 2*pomaranczowe //
}
Nie musimy natomiast zmieniać sygnałów wyświetlanych na powtarzaczach semaforów pulpitu, gdyż po wysłaniu do makiety „żądania” zapalenia sygnałów STÓJ na semaforach A, B i C system odeśle do pulpitu potwierdzenie tejże operacji, a to zaowocuje automatycznym odwzorowaniem tej sytuacji na pulpicie (patrz funkcja wyswietl_stan_urzadzen()).
Jak widać przycisk kasowania przebiegu jest „uniwersalny” dla całej jednej głowicy stacji, ale pamiętajmy, że na jednej głowicy tej stacji nie może być utwierdzonych kilku przebiegów, zatem proces kasowania może się odbywać na zasadzie kasowania całej głowicy, co zdecydowanie upraszcza nasz program.
Analogicznie postępujemy z drugą głowicą rozjazdową naszej stacji:
if (digitalRead(key_rozw_przebiegu_2) == LOW) {
beep();
stan_d1 = 0;
stan_e2 = 0;
stan_f1 = 0;
stan_f2 = 0;
stan_SzD = 0;
stan_SzE = 0;
stan_SzF = 0;
digitalWrite(led_d1, LOW);
digitalWrite(led_e2, LOW);
digitalWrite(led_f1, LOW);
digitalWrite(led_f2, LOW);
Serial.println(3021);
Serial.println(3031);
Serial.println(3041);
// zmiana semafora A jesli podany jest wjazd
if (stan_a1 == 1)Serial.println(1044); // A >> S5 //
if (stan_a2 == 1)Serial.println(1045); // A >> S 13 //
}
Następny fragment dotyczy sygnałów zastępczych.
// Sygnaly zastepcze //
Sprawdzamy zatem czy został wciśnięty przycisk przebiegu zastępczego SzA:
if (digitalRead(key_SzA) == LOW) {
Jeśli tak, to oczywiście – sygnał dźwiękowy, a dalej sprawdzamy czy SzA nie jest już czasem podany i jeśli nie jest – to wysyłamy kod 1043:
beep();
if (stan_SzA == 0)Serial.println(1043);
Teraz zapisujemy, że SzA=1 czyli jest aktywny:
stan_SzA = 1;
oraz zapisujemy czas przez jaki ma być wyświetlany ten sygnał, czyli czas obecny + 90 sekund:
czas_SzA = czas + 90000;
}
Dla pozostałych sygnałów zastępczych postępujemy dokładnie tak samo:
if (digitalRead(key_SzB) == LOW) {
beep();
if (stan_SzB == 0)Serial.println(1053);
stan_SzB = 1;
czas_SzB = czas + 90000;
}
if (digitalRead(key_SzC) == LOW) {
beep();
if (stan_SzC == 0)Serial.println(1063);
stan_SzC = 1;
czas_SzC = czas + 90000;
}
if (digitalRead(key_SzD) == LOW) {
beep();
if (stan_SzD == 0)Serial.println(3023);
stan_SzD = 1;
czas_SzD = czas + 90000;
}
if (digitalRead(key_SzE) == LOW) {
beep();
if (stan_SzE == 0)Serial.println(3033);
stan_SzE = 1;
czas_SzE = czas + 90000;
}
if (digitalRead(key_SzF) == LOW) {
beep();
if (stan_SzF == 0)Serial.println(3043);
stan_SzF = 1;
czas_SzF = czas + 90000;
}
Teraz pora na automatyczne gaszenie sygnałów zastępczych.
Sprawdzamy zatem, czy nasz licznik czasu jest większy od czasu, który został zapamiętany przy wciśnięciu przycisku SzA i czy jest aktywny SzA.
Przykładowo:
Pamiętamy że zmienna czas ciągle zwiększa swoją wartość (+1) – dokładnie co 1 milisekundę, zatem jeśli np. w dokładnie 60 – tej sekundzie od startu systemu (czas = 60000) wciśniemy przycisk SzA to zmienna czas_SzA=60000+90000 czyli 150000.
Zatem jeśli licznik czasu przekroczy tę wartość czas>150000 to spełnimy pierwszą część poniższego warunku:
if (czas > czas_SzA && stan_SzA > 0) {
Jeśli tak jest to zerujemy zmienną SzA i wysyłamy kod 1041, który wyświetli sygnał STÓJ na semaforze:
stan_SzA = 0;
Serial.println(1041);
}
Analogicznie postępujemy dla pozostałych sygnałów zastępczych:
if (czas > czas_SzB && stan_SzB > 0) {
stan_SzB = 0;
Serial.println(1051);
}
if (czas > czas_SzC && stan_SzC > 0) {
stan_SzC = 0;
Serial.println(1061);
}
if (czas > czas_SzD && stan_SzD > 0) {
stan_SzD = 0;
Serial.println(3021);
}
if (czas > czas_SzE && stan_SzE > 0) {
stan_SzE = 0;
Serial.println(3031);
}
if (czas > czas_SzF && stan_SzF > 0) {
stan_SzF = 0;
Serial.println(3041);
}
Aby nieco ułatwić sobie programowanie wprowadziłem zmienną utwierdzenie, która dotyczy konkretnego rozjazdu np. utwierdzenie_Zw5. Jest ona zależna od wielu innych zmiennych, a konkretnie od utwierdzonych przebiegów. Aby nie było konieczności wpisywania tych wszystkich warunków podczas np. zmiany położenia rozjazdów, wyprowadziłem te warunki „na zewnątrz”, czyli właśnie poniżej. Do samego przekładania zwrotnic sprawdzamy tylko zmienną utwierdzenie_Zw… która przyjmuje wartość 0 jeśli rozjazd nie jest utwierdzony, lub wartość 1 jeśli jest utwierdzony i nie można go ruszyć:
// utwierdzenia rozjazdow i wykolejnicy //
if (stan_a1 == 1 || stan_a2 == 1 || stan_b1 == 1 || stan_c2 == 1 || stan_f1 == 1 || stan_f2 == 1) utwierdzenie_Zw1 = 1;
else utwierdzenie_Zw1 = 0;
Tutaj dla odmiany wystarczy, aby choć jeden z warunków był spełniony. Wówczas wartość zmiennej Zw1 przyjmie wartość 1 lub – jeśli tak się nie stanie (czyli wszystkie będą równe 0) – to zmienna Zw1 przyjmie wartość 0.
Podobnie w pozostałych przypadkach:
if (stan_a1 == 1 || stan_b1 == 1 || stan_f1 == 1 || stan_Wk == 1) utwierdzenie_Zw2 = 1;
else utwierdzenie_Zw2 = 0;
if (stan_a1 == 1 || stan_d1 == 1 || stan_f1 == 1 ) utwierdzenie_Zw3_4 = 1;
else utwierdzenie_Zw3_4 = 0;
if (stan_a1 == 1 || stan_a2 == 1 || stan_d1 == 1 || stan_e2 == 1 || stan_f1 == 1 || stan_f2 == 1) utwierdzenie_Zw5 = 1;
else utwierdzenie_Zw5 = 0;
if (stan_a1 == 1 || stan_b1 == 1 || stan_f1 == 1) utwierdzenie_Wk == 1;
else utwierdzenie_Wk = 0;
Nie trzeba oczywiście tworzyć takich dodatkowych zmiennych, ale w niektórych sytuacjach jest o wiele czytelniej i prościej, gdy pewne zależności „zgrupuje się” w jednym miejscu,
a następnie w programie skorzysta z gotowych wyników tych warunków, czyli w tym przypadku utwierdzenie_Zw… jest właśnie takim wynikiem kilku zmiennych.
Dobrnęliśmy zatem do końca pętli głównej programu:
} // loop //
Zdaję sobie sprawę z dużej ilości informacji zawartych w tej części artykułu, ale wbrew pozorom jest to tylko kilka prostych funkcji użytych wielokrotnie.
Istotą działania tego systemu są warunki, które muszą być spełnione celem osiągnięcia jakichś konkretnych celów – zupełnie jak na prawdziwej kolei.
Ważne zatem, aby najpierw zrozumieć owe warunki, które determinują tak naprawdę wszystko co się dzieje w tym systemie, czyli na owej makiecie stacji Wysoka – jak ją nazwaliśmy.
Niestety warunki te nie są uniwersalne. Dla każdej stacji, dla każdego układu torowego będą się one różniły, a co za tym idzie trzeba będzie inaczej je zapisać w programie. Dlatego tak ważne jest ich poznanie i zrozumienie. Bez tego nie będzie możliwe stworzenie odpowiedniego oprogramowania. W tym przypadku sprawę bardzo ułatwił Leszek opisując wszystkie sytuacje ruchowe na stacji Wysoka i niejako podając mi te warunki „na tacy”. To było impulsem, aby stworzyć tę serię artykułów oraz ogromną pomocą, bez której pewnie nawet bym się za to nie zabrał. Pragnę zatem podziękować Ci Leszku za tę pomoc.
Na koniec jeszcze poproszę wszystkich odbiorców moich artykułów o przesyłanie na naszą PMM-ową skrzynkę mailową sygnałów zwrotnych z informacjami dotyczącymi ewentualnych niejasności, problemów do rozwiązania lub też o… pochwalenie się swoimi wynikami w pracach nad urealnieniem systemów sterowania swoich makiet.
Bardzo dziękuję wszystkim czytelnikom za wytrwałość i życzę wielu wspaniałych sukcesów w dziedzinie sterowania ruchem na miniaturowej kolei. Pliki wszystkich elementów systemu pobierzesz klikając w wysoka_pulpit.