gotowiec 5 - jeszcze jedno MENU

Zbiór tutoriali związanych z Arduino.
ODPOWIEDZ
Awatar użytkownika
wojtekizk
Starszy majsterkowicz
Posty: 311
Rejestracja: 19 lis 2013, 10:54
Lokalizacja: Bydgoszcz

gotowiec 5 - jeszcze jedno MENU

Post autor: wojtekizk » 1 wrz 2019, 21:31

Temat do mnie wraca jak bumerang, więc postanowiłem raz jeszcze ogarnąć to na pozór niebanalne zadanie :
Jak zbudować przyjazne MENU (np. dla 5-ciu przycisków) bez spowalniania czegokolwiek w głównej pętli programu i przy tym jak mieć możliwość sterowania tymże Menu w dowolnej chwili i niezależnie od tego co aktualnie robi nasz mikroprocesor?
Odpowiedź jest prosta - wystarczy zbudować dowolnie skomplikowane Menu i sterować nim w funkcji obsługi przerwań.
Prawda, że banalne? Niestety nie dla wszystkich, stąd ten tutorial właśnie :-)
Na potrzeby tego posta popełniłem bardzo prostą i intuicyjną bibliotekę MyMenu (plik do pobrania i dołączenia do IDE Arduino jest w załączniku MyMenu.zip).
Natomiast w katalogu example_MyMenu jest przykładowy pliczek do uruchomienia na naszej płytce Arduino UNO.
Do tejże prezentacji użyłem także standardowego shielda LCD Keypad Shied, czyli wyświetlacza LCD z wbudowaną klawiaturką 5-cio przyciskową.
Przykładowy program jest "obficie przyodziany" w szczegółowe komentarze, które chyba pomogą zrozumieć co autor miał na myśli :-)
Na początek musimy dołączyć bibliotekę do naszego IDE, czyli Szkic->Dołącz bibliotekę ->Dodaj bibliotekę.ZIP ... i wskazać na wcześniej pobrany plik z załącznika.
Jeśli masz właśnie pod ręką Arduino UNO, Leonardo, DFRobot czy co tam jeszcze oraz shilda LCD z wbudowanym Keypadem to masz już wszystko gotowe do wgrania przykładowego pliku.
Opis działania programu:
Na początku umieściłem przykładowe Menu oraz poglądowy wygląd klawiaturki. Obsługa programu sprowadza się do wybrania odpowiedniej opcji i zatwierdzeniu wyboru klawiszem OK.
Poruszamy się po menu za pomocą 4 klawiszy nawigacyjnych zgodnie z tym co przedstawia wygląd przykładowego menu.
Każda zmiana aktualnej opcji jest wyświetlana w pierwszej linii wyświetlacza LCD, natomiast po zatwierdzeniu wybranej opcji klawiszem OK (Select), w drugiej linii jest wyświetlany komunikat o podjętej akcji, zaś program wykonuje przypisaną do danej opcji funkcję / działanie.
Jeśli nasze przykładowe Menu ma w sumie 15 opcji to dla każdej z nich możesz przypisać określone działanie.
Niby trywialne... ale ... możesz sterować tym Menu NIEZALEŻNIE od tego co aktualnie robi program, bo to menu oparte jest na obsłudze przerwania, w tym konkretnym przypadku wykorzystujemy przerwanie Timera.Zatem program średnio 8 razy na sekundę kontroluje stan klawiatury i zależnie od wybranej opcji sięga po wcześniej zdefiniowaną funkcję i po prostu ją wykonuje.
Zauważ, że w pętli loop niczego nie robimy... nie ma tam żadnych opóźnień zaś całe sterowanie klawiatury załatwia nam sprzętowa obsługa przerwania Timera, jakby to wszystko odbywało się w tle... bez ani jednej linijki kodu w głównej pętli loop :-)
Z drugiej strony w tej pętli możesz sobie teraz pisać/wykonywać dowolny program, dowolne inne działanie i mieć nadal pełną obsługę Menu bez zakłóceń działania programu.
Z tym właśnie problemem boryka się duża grupa kolegów z tego forum.
Co więcej, każda wybrana opcja w Menu może wykonywać działanie zdefiniowane w funkcji obsługi tej opcji.
Jeśli menu ma np. 15 opcji to masz 15 dodatkowych funkcji, które program wykona niezależnie od tego, co wyrabia loop :-)
Dla przykładu uruchamiasz silnik na np. 30 sekund (delay w funkcji loop) , potem wybierasz inna opcję z Menu i za jej przyczyną zapalasz np. światło.
Normalnie najpierw delay musiałaby wykonać swoje zadania i po 30 sek. dopiero mógłbyś włączyć światło :-)
Możliwości jest wiele, tak jak jest wiele pomysłów na to co ma robić każda z funkcji obsługi Menu.
Ten przykładowy programik pomoże chociaż zrozumieć samą zasadę wykorzystania takiego podejścia do Menu.
Dla chętnych polecam przyjrzenie się plikom biblioteki MyMenu.
Jest mojego autorstwa, ale każdy może z niej korzystać.
Poniżej przykładowy programik:

Kod: Zaznacz cały

/* wymagana biblioteka MyMenu , do pobrania z majsterkowo.pl (Forum -> Arduino-> Tutoriale -> klasa Menu ....) 
 * Przykładowe Menu: ... by [email protected] (opublikowane na majsterkowo.pl)
 *                  PLIK          Edycja          SZKIC          POMOC
 *                    Nowy          Kopiuj          Wgraj          O programie...  
 *                    Galeria       Wklej           Kompiluj       O mnie...
 *                    Exit                          Dodaj plik 
 *                    
 * w sumie 14 pozycji: (4 menu głowne i po kilka submenu dla każdej pozycji)                    
 * układ klawiszy: klawiatura 5-cio przyciskowa (standard np. dla shielda LCD Keypad Shield)
 *                               UP
 *                SELECT   LEFT      RIGTH         
 *                              DOWN 
 * sterowanie - odczyt stanów na wejsciu A0 (standardowa drabinka rezystorowa)                             
 */    
#include <MyMenu.h>              // dołączamy wcześniej pobraną i zainstalowaną bibliotekę MyMenu
#include <LiquidCrystal.h>       // dołączamy bibliotekę obsługi wyświetlacza LCD 
LiquidCrystal lcd(8,9,4,5,6,7);  // konfiguracja wyswietlacza - tutaj standardowy dla LCD Keypad Shield
/* --- zmienne globalne, specyfikator volatile wymusza zaniechanie optymalizacji kodu dla tych zmiennych 
   innymi słowy te zmienne są aktualizowane natychmiast, bez uproszczeń i szachrajstw kompilatora :-)    */
volatile int poz=1;    // aktualna pozycja menu
volatile int x=-1;     // zmienna pomocnicza, do odczytu stanu klawiatury
volatile int pop=-1;   // zmienna pomocnicza, poprzedni stan klawiatury
Menu My(15);           // tworzymy obiekt klasy Menu, o nazwie My. To menu My ma tutaj np. 15 opcji.
/* --- funkcja odczytu klawiatury (standardowa klawiatura dla LCD Keypad Shield, podpięta do A0)         */
int odczyt_klawiatury()          
{
 int  stan_klawiatury = analogRead(0);   
 if (stan_klawiatury> 1000) return -1; 
 // te wartości musisz dobrać doswiadczalnie w zależnosci od wersji shielda (czy producenta)
 // de facto to zależy od wartosci użytych rezystorów w drabince i wartości napięcia odniesienia
 if (stan_klawiatury < 50)   {return 6;}      
 if (stan_klawiatury < 180)  {return 2;} 
 if (stan_klawiatury < 400)  {return 8;}  
 if (stan_klawiatury < 550)  {return 4;}  
 if (stan_klawiatury < 800)  {return 5;}  
} /* funkcja zwraca jedną z wartości: 2-UP, 6-RIGTH, 8-DOWN, 4-LEFT, 5-OK (SELECT), -1 -nic nie wciśnieto   */
/* --- funkcja obsługi przerwania Timer 1, nazwa tej funkcji jest zdefiniowana w bibliotece Arduino ---     */
ISR(TIMER1_COMPA_vect)  // TIMER1_COMPA_vect jest tutaj do obsługi wektora przerwań
{
pop=x;                  // stan poprzedni
x=odczyt_klawiatury();  // sprawdzamy czy wciśnięto jakiś klawisz
if(x!=pop && x!=-1)     // jeśli zmiana stanu (x != pop),.. i wciśnieto coś, (czyli nie -1), to...
  {
  pop=x;                // aby niepotrzebnie nie sprawdzać tego samego warunku przy kolejnym wywołaniu przerwania TIMER1
  lcd.clear();          // kosmetyka po zmianie opcji   
  switch (x)            // sprawdzamy co wciśnięto: 
    {
    case 5: My.items[poz].ItemAkcja();       // wciśnieto klawisz OK - wywołujemy funkcję przypisaną do akt. pozycji w menu.
            lcd.setCursor(0,1);lcd.print("                "); // czyścimy drugą linię, bez migotania
            lcd.setCursor(0,1);lcd.print(My.items[poz].lineTo);   // komunikat w drugiej linni lcd
            Serial.println(My.items[poz].lineTo);                 // opcjonalnie, dla sprawdzenia - taki mini debugger :-)
            break;
    case 2: if(My.items[poz].navi[0]>0){poz=My.items[poz].navi[0];} break;  // wciśnięto klawisz UP
    case 8: if(My.items[poz].navi[2]>0){poz=My.items[poz].navi[2];} break;  // wciśnięto klawisz DOWN
    case 6: if(My.items[poz].navi[1]>0){poz=My.items[poz].navi[1];} break;  // wciśnieto klawisz RIGTH
    case 4: if(My.items[poz].navi[3]>0){poz=My.items[poz].navi[3];} break;  // wciśnięto klawisz LEFT
    }
  lcd.setCursor(0,0);lcd.print("                ");  // podobna technika, zapisujemy 16 spacji 
  lcd.setCursor(0,0);lcd.print(My.items[poz].nazwa); // a następnie wypełniamy komunikatem 
  }
}
// ======================================================================================================
void setup() {
Serial.begin(9600);
lcd.begin(16,2);
/* --- konfiguracja Timera 1 --- */
noInterrupts(); 
TCCR1B = 0;       // ustawiamy niezbedne rejestry Timera 1
OCR1A = 31250/16; // ustawiamy czas pomiędzy kolejnymi przerwaniami 
TCCR1B |= (1 << WGM12); // CTC mode
TCCR1B |= (1 << CS12); // 256 prescaler
TIMSK1 |= (1 << OCIE1A); // uruchamiamy przerwanie Timer 1
interrupts(); 
/* --- poniżej przykładowa konfiguracja wszystkich opcji Menu ---                                        */
My.items[1].create("Plik",0,2,5,0,item1,"akcja dla PLIK");            // menu Plik         index=1
My.items[2].create("Edycja",0,3,8,1,item2,"akcja dla EDIT");             // menu Edit         index=2
My.items[3].create("Szkic",0,4,10,2,item3,"run dla SZKIC");                // menu Szkic        index=3
My.items[4].create("Pomoc",0,0,13,3,item4,"robo dla POMOC");                // menu Pomoc        index=4
My.items[5].create("Nowy",1,0,6,0,item5,"akcja dla Nowy");                  // submenu dla PLIK  index=5
My.items[6].create("Galeria",5,0,7,0,item6,"akcja galeria");                                     // submenu dla Plik  index=6
My.items[7].create("Exit",6,0,0,0,item7,"zamykamy EXIT");                  // submenu dla Plik  index=7
My.items[8].create("Kopiuj",2,0,9,0,item8,"tu kopiujemy");                // submenu dla Edit  index=8 
My.items[9].create("Wklej",8,0,0,0,item9,"a tu wklejemy");                 // submenu dla Edit  index=9
My.items[10].create("Wgraj",3,0,11,0,item10,"taki zarcik");       // submenu dla Szkic index=10
My.items[11].create("Kompiluj",10,0,12,0,item11,"akcja Kompiluj");         // submenu dla Szkic index=11
My.items[12].create("Dodaj plik",11,0,0,0,item12,"dodawanko");        // submenu dla Szkic index=12
My.items[13].create("...o programie",4,0,14,0,item13,"about program");       // submenu dla Pomoc index=13
My.items[14].create("...o mnie",13,0,0,0,item14,"autobiografia");         // submenu dla Pomoc index=14
/* --- na koniec w funkcji setup wyświetlam pierwszą pozycję menu ---                                   */
lcd.setCursor(0,0);lcd.print("                ");
lcd.setCursor(0,0);lcd.print(My.items[poz].nazwa);
}
/* --- poniżej przykładowe definicje wszystkich funkcji obsługujacych poszczególne pozycje Menu ---     */
void item1(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item2(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item3(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item4(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item5(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item6(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item7(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item8(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item9(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item10(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item11(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item12(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item13(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
void item14(){Serial.print("Wywolano funkcję obsługi dla opcji: ");Serial.println(My.items[poz].nazwa);}
// =====================================================================================================
void loop() {
  /* 
   *  Tutaj nie robię niczego, bo wszystko robi za mnie obsługa przerwań Timera1
   *  Dzięki tej technice możesz teraz zapuścić silnik, uruchomić ramię robota,
   *  prowadzić ciagły pomiar jakieś wartości,... i jednocześnie korzystać z Menu 
   *  ... to chyba najlepsze dobrodziejstwo przerwań sprzętowych, programowych i Timera(ów)
   *  Jesteś chłopie (kobito) teraz zupełnie niezależnym człowiekiem: FREE HOMO DEBILUS :-) 
 */
 
 } /* ==== K O N I E C =================================================================== */
Wyjaśnienia wymaga kilka kwestii:

Kod: Zaznacz cały

My.items[10].create("Wgraj",3,0,11,0,item10,"taki zarcik");       // submenu dla Szkic index=10
Tutaj buduję pozycję nr 10.
* W pierwszej linii LCD wyświetla się napis "Wgraj ".
* Następne 4 liczby to nawigator dla tej pozycji menu:
- liczba 3 to numer pozycji menu UP (czyli nad obecną, w tym przypadku nad "Wgraj" jest "SZKIC", a SZKIC to 3 opcja w naszym Menu.
- liczba 0 to numer pozycji menu RIGTH (czyli na prawo, w tym przypadku nie ma nic, więc 0 (bo "Wgraj" to submenu dla "SZKIC")
- liczba 11 to numer pozycji menu DOWN (czyli pod obecną, w tym przypadku to "Kompiluj", czyli 11 pozycja w naszym menu.
- liczba 0 to analogicznie LEFT (czyli na lewo) - tu znowu nie mamy niczego, więc 0 (bo "Wgraj" to submenu dla "SZKIC")
* Następnie item10 - to nazwa zdefiniowanej funkcji obsługi dla tej opcji - patrz w kodzie co robi funkcja item10
*Ostatni parametr to tekst jaki ma być wyświetlany dla tej opcji menu.
... analogicznie pozostałe opcje Menu (każda ma nazwę, nawigator: UP, RIGTH,DOWN i LEFT, nazwę funkcji obsługi i tekst w drugiej linii LCD :-)

Najważniejsza część programu jest niepozorna:

Kod: Zaznacz cały

Menu My(15);           // tworzymy obiekt klasy Menu, o nazwie My. To menu My ma tutaj np. 15 opcji.
... to właśnie tutaj tworzy się owo 15-to elementowe menu (wszystko jest zdefiniowane w naszej bibliotece MyMenu więc nie musisz się zastanawiać jak to jest zrobione, chociaż powinieneś :-)
Natomiast tutaj dzieje się wszystko, co związane jest z obsługą Menu - to funkcja obsługi przerwania:

Kod: Zaznacz cały

ISR(TIMER1_COMPA_vect)  // TIMER1_COMPA_vect jest tutaj do obsługi wektora przerwań
{
pop=x;                  // stan poprzedni
x=odczyt_klawiatury();  // sprawdzamy czy wciśnięto jakiś klawisz
if(x!=pop && x!=-1)     // jeśli zmiana stanu (x != pop),.. i wciśnieto coś, (czyli nie -1), to...
  {
  pop=x;                // aby niepotrzebnie nie sprawdzać tego samego warunku przy kolejnym wywołaniu przerwania TIMER1
  lcd.clear();          // kosmetyka po zmianie opcji   
  switch (x)            // sprawdzamy co wciśnięto: 
    {
    case 5: My.items[poz].ItemAkcja();       // wciśnieto klawisz OK - wywołujemy funkcję przypisaną do akt. pozycji w menu.
            lcd.setCursor(0,1);lcd.print("                "); // czyścimy drugą linię, bez migotania
            lcd.setCursor(0,1);lcd.print(My.items[poz].lineTo);   // komunikat w drugiej linni lcd
            Serial.println(My.items[poz].lineTo);                 // opcjonalnie, dla sprawdzenia - taki mini debugger :-)
            break;
    case 2: if(My.items[poz].navi[0]>0){poz=My.items[poz].navi[0];} break;  // wciśnięto klawisz UP
    case 8: if(My.items[poz].navi[2]>0){poz=My.items[poz].navi[2];} break;  // wciśnięto klawisz DOWN
    case 6: if(My.items[poz].navi[1]>0){poz=My.items[poz].navi[1];} break;  // wciśnieto klawisz RIGTH
    case 4: if(My.items[poz].navi[3]>0){poz=My.items[poz].navi[3];} break;  // wciśnięto klawisz LEFT
    }
  lcd.setCursor(0,0);lcd.print("                ");  // podobna technika, zapisujemy 16 spacji 
  lcd.setCursor(0,0);lcd.print(My.items[poz].nazwa); // a następnie wypełniamy komunikatem 
  }
}
... tu komentarz jest dość szczegółowy więc nie przeszkadzam w lekturze...
pozdrawiam
Nie masz wymaganych uprawnień, aby zobaczyć pliki załączone do tego posta.

Co miesiąc do wygrania nagrody o wartości ponad 1600 zł!


ODPOWIEDZ

Strony partnerskie: