Menu na wyświetlaczu 16×2

Bardzo często robiąc jakiś projekt zachodzi konieczność zaimplementowania interfejsu do komunikacji człowiek-urządzenie.  W prostych konstrukcjach z reguły wystarcza kilka pirzycisków i świecących diod, jednak gdy robimy bardziej zaawansowane urządzenie warto byłoby zastanowić się, czy nie wygodnie byłoy dodać wyświetlacz LCD i zrobić na nim menu w którym pomieścimy tyle ustawień na ile nam pamięć kontrolera pozwoli.

Potrzebne części

Tak to już wszystko! Aby zrobić takie menu poza „mózgiem” w postaci mikrokontrolera będziemy potrzebowali tylko wyświetlacza i czterech przycisków :)

Opis podłączenia

Do wszystkich czterech przycisków podłączamy z jednej strony masę, a z drugiej strony wybrany przez nas pin cyfrowy. Nie musimy się martwić o rezystor podciągający, gdyż ustawimy go programowo.

Aby podłączyć wyświetlacz z konwererem należy podpiąć do niego zasilanie oraz magistralę I2C (piny oznaczone jako SDA i SCL). Dokładniejszy opis podłączenia i sterowania takim wyświetlaczem możnap rzeczytać tutaj.

Założenia programu

Interfejs będzie składał się z linii nagłówka oraz linii opcji. Na pierwszym poziomie menu w nagłówku będzie wyświetlany napis „Menu glowne”, natomiat na drugim poziomie będzie tam widniała nazwa aktualnie ustawianej opcji.

4 przyciski będą nam służyły do nawigowania po menu oraz zatwierdzania opcji. Będą to przyciski W LEWO, W PRAWO, OK oraz COFNIJ. Przycisk OK będzie wybierał daną opcję i wchodził w drugi poziom menu oraz zapisywał ustawienia. Przycisk COFNIJ będzie tylko wracał do menu głównego (bez zapisywania).

Nasze menu powinno być uniwersalne, to jest powinniśmy móc swobodnie po nim nawigować, ustawiać wartości graniczne, wyświetlać listę możliwych ustawień dla danej opcji, ustawić daną opcję jako akcję (po wciśnięciu przycisku OK zamiast wchodzenia poziom niżej wykona się jakaś funkcja).

Piszemy kod

Gotowy kod do pobrania tutaj.

Jak widać po zadeklarowanych stałych

#define BTN_BACK  8
#define BTN_NEXT  3
#define BTN_PREV  7
#define BTN_OK    5

przycisk COFNIJ należy podłączyć do pinu 8, W PRAWO do 3, W LEWO do 7 oraz OK do 5. Możemy oczywiście zmienić te piny na takie, jakie są dla nas dogodne :)

W naszym kodzie deklarujemy też jedną strukturę oraz typ wyliczeniowy:

typedef struct {
  String label;
  int minVal;
  int maxVal;
  int currentVal;
  void (*handler)();
} STRUCT_MENUPOS;

typedef enum {
  BACK, NEXT, PREV, OK, NONE
} ENUM_BUTTON;

Każdy element STRUCT_MENUPOS będzie reprezentacją pojedynczej opcji w menu. Zastanawiać może tutaj ostatnie pole, czyli wskaźnik do funkcji nie przyjmującej oraz nie zwracającej żadnych parametrów. Będzie ona nam służyła na dwojaki sposób. Jeżeli minVal będzie większe bądź równe maxVal (czyli sytuacja zdawałoby się bezsensowna) funkcja ta będzie akcją, którą chcemy wykonać (bez wchodzenia na drugi poziom menu). Drugi sposób wykorzystania tej funkcji to nadpisanie domyślnego zachowania programu (czyli wyświetlania użytkownikowi bezpośrednio wartości currentVal – będziemy mogli wstawić tam własne stringi). Trzecia możliwość to wpisanie tam wartości NULL – wtedy program zachowa się domyślnie, czyli wyświetli currentVal bez żadnego formatowania.

LiquidCrystal_PCF8574 lcd(LCD_ADDR);
STRUCT_MENUPOS menu[5];

int currentMenuPos = 0;
int menuSize;
bool isInLowerLevel = false;
int tempVal;

Teraz musimy zadeklarować kilka przydacnych później zmiennych. Tablica menu będzie zawierała pozycje naszego menu. currentMenuPos będzie informowało porgram która opcja jest aktualnie wybrana. menuSize to rozmiar menu (jest obliczany automatycznie na podstawie deklaracji). isInLowerLevel informuje nas, czy jesteśmy w menu główny, czy poziom niżej, a tempVal to tymczasowa wartośc dla currentVal ze struktury STRUCT_MENUPOS.

Przyjrzyjmy się teraz funkcji setup:

void setup() {
  Serial.begin(9600);
  lcd.begin(16, 2);
  lcd.setBacklight(255);

  pinMode(BTN_NEXT, INPUT_PULLUP);
  pinMode(BTN_PREV, INPUT_PULLUP);
  pinMode(BTN_BACK, INPUT_PULLUP);
  pinMode(BTN_OK, INPUT_PULLUP);

  menu[0] = {"Cyfry", 0, 9, 5, NULL};
  menu[1] = {"Liczby", 10, 1000, 15, NULL};
  menu[2] = {"Napisy", 0, 2, 0, formatNapisy};
  menu[3] = {"Ulamki", 0, 30, 15, formatUlamki};
  menu[4] = {"Port szer.", 0, 0, 0, actionPortSzeregowy};

  menuSize = sizeof(menu)/sizeof(STRUCT_MENUPOS);
}

Poza oczywistymi oczywistościami jak ustawianie trybu działania pinów cyfrowych i inicjalizacji wyświetlacza ustawiamy tutaj wszystkie pozycje naszego menu i tak np. menu z indeksem 2 będzie miało etykietkę „Napisy”, wartości w granicach od 0 do 2, a rysowanie tego podmenu będzie realizowała funkcja o sygnaturze void formatNapisy(). Menu z indeksem 4 wykona funkcję zadeklarowaną jako void actionPortSzeregowy() i nie wejdzie do niższego poziomu.

Czas zobaczyć co dzieje się w sercu programu, czyli w funkcji drawMenu:

Pozwolę sobie zacząć omawiać tę funkcję od końca, tak chyba będzie prościej zrozumieć w jaki sposób ona działa.

  lcd.home();
  lcd.clear();
  if(isInLowerLevel) {
    lcd.print(menu[currentMenuPos].label);
    lcd.setCursor(0, 1);
    lcd.print(F("> "));

    if(menu[currentMenuPos].handler != NULL) {
      (*(menu[currentMenuPos].handler))();
    } else {
      lcd.print(tempVal);
    }
  } else {
    lcd.print(F("Menu glowne"));
    lcd.setCursor(0, 1);
    lcd.print(F("> "));

    lcd.print(menu[currentMenuPos].label);
  }

Powyższy fragment odpowiada za wyświetlanie danych na ekranie. Na początku czyścimy wyświetlacz oraz ustawiamy kursor na pozycji początkowej (lcd.home()). Następnie sprawdzamy, czy jesteśmy na drugim poziomie. Jeżeli tak to w nagłówku wyświetlamy etykietkę opcji w któej jesteśmy, wypisujemy znak zachęty i jeżeli nie ma żadnej funkcji, która obsłuży nam wyświetlanie dostępnych możliwości to wypisujemy na ekran zmienną tymczasową. W przeciwnym wypadku wywołujemy funkcję z pola handler, która powinna zająć się wyświetleniem wartości na swój sposób.

Przyjrzyjmy się teraz obsłudze przycisków:

  ENUM_BUTTON pressedButton = getButton();

[…]

  switch(pressedButton) {
    case NEXT: handleNext(); break;
    case PREV: handlePrev(); break;
    case BACK: handleBack(); break;
    case OK: handleOk(); break;
  }

Odczytaniem przycisku zajmuje się funkcja getButton(). Nie będę się o niej rozpisywał, gdyż jedyne co robi to sprawdza stany na pinach za pomocą funkcji digitalRead i zwraca wciśnięty przycisk. Następnie wywołujemy odpowiednią funkcję w zależności od przycisku.

void handleNext() {
  if(isInLowerLevel) {
    tempVal++;
    if(tempVal > menu[currentMenuPos].maxVal) tempVal = menu[currentMenuPos].maxVal;
  } else {
    currentMenuPos = (currentMenuPos + 1) % menuSize;
  }
}

Obsługa przycisku W PRAWO jest prosta. Jeżeli jesteśmy na drugim poziome menu to zwiększamy wartość tymczasową i sprawdzamy, czy nie wyszła poza zakres. Jeżeli jesteśmy w menu głównym to zwiększamy currentMenuPos i w razie przekroczenia zakresu zaczynamy od 0 (operacja modulo). Podobnie działa przycisk W LEWO z tym, że w drugą stornę :)

void handleBack() {
  if(isInLowerLevel) {
    isInLowerLevel = false;
  }
}

Przycisk WSTECZ jedyne co robi to cofa się o poziom wyżej (do menu głównego).

void handleOk() {
  if(menu[currentMenuPos].handler != NULL && menu[currentMenuPos].maxVal <= menu[currentMenuPos].minVal) {
    (*(menu[currentMenuPos].handler))();
    return;
  }
  if(isInLowerLevel) {
    menu[currentMenuPos].currentVal = tempVal;
    isInLowerLevel = false;
  } else {
    tempVal = menu[currentMenuPos].currentVal;
    isInLowerLevel = true;
  }
}

Zobaczmy co kryje pod sobą funkcja obsługująca przycisk OK. Jeżeli pole handler nie jest NULLem oraz maxVal jest mniejszy bądź równy minVal to wykonujemy funkcję zdefiniowaną jako handler i wychodzimy z handleOk(). W przeciwnym wypadku jeżeli jesteśmy na niższym poziomie to zapisujemy tempVal do pola currentVal i przechodzimy poziom wyżej. Jeżeli jesteśmy w menu głównym to robimy dokładnie odwrotnie: zapisujemy currentVal do tempVal oraz wchodizmy na niższy poziom menu.

Teraz ostatnia (a właściwie pierwsza) część funkcji drawMenu:

  static unsigned long lastRead = 0;
  static ENUM_BUTTON lastPressedButton = OK;
  static unsigned int isPressedSince = 0;
  int autoSwitchTime = 500;

  ENUM_BUTTON pressedButton = getButton();

  if(pressedButton == NONE && lastRead != 0) {
    isPressedSince = 0;
    return;
  }
  if(pressedButton != lastPressedButton) {
    isPressedSince = 0;
  }

  if(isPressedSince > 3) autoSwitchTime = 70;
  if(lastRead != 0 && millis() - lastRead < autoSwitchTime && pressedButton == lastPressedButton) return;

  isPressedSince++;
  lastRead = millis();
  lastPressedButton = pressedButton;

W tym fragmencie kodu dbamy o to, aby menu nie odświeżało się za każdym razem a tylko wtedy, keidy wciśniemy jakiś przycisk. Jest też funkcjonalność, która przy przytrzymaniu przycisku przez 3 zmiany pozycji menu zacznie robić to szybciej (widać na filmiku na końcu artykułu). Są to głównie zależności czasowe na podstawie funkcji millis().

Zerknijmy jeszcze tylko szybko na funkcje-uchwyty:

/* Funkcje-uchwyty użytkownika */
void actionPortSzeregowy() {
  Serial.println("Wywolano akcje: Port szeregowy");
}

void formatNapisy() {
  String dictonary[] = {"Napis 1", "Napis 2", "Napis 3 :)"};
  lcd.print(dictonary[tempVal]);
}

void formatUlamki() {
  lcd.print(tempVal / 10.0);
}

Tak przygotowany program możemy wgrać na Arduino i sprawdzic go w praktyce.

28 myśli nt. „Menu na wyświetlaczu 16×2

  1. siutek

    Buduję właśnie slider do aparatu, chciałem wykorzystać powyższy kod do oprogramowania całości.
    Jak „łapać” ustawione parametry w celu przekazania ich do środka funkcji, np do actionPortSzeregowy().
    W dalszej części ta funkcja będzie miała inną zawartość i inną nazwę. Ale to nie istone ;)

    Odpowiedz
    1. Kamil Autor wpisu

      Aktualną wartość ustawianego parametru masz w tempVal (tymczasowa, która jest wyświetlana aktualnie na ekranie), a jak robisz akcję, czyli np. port szeregowy i chciałbyś wypisać na nim to co jest aktualnie ustawione w ustawieniu „Cyfry” to robisz w handlerze Serial.println(menu[0].currentVal);

      Odpowiedz
  2. Piotrek

    Menu.ino:14:1: error: ‚ENUM_BUTTON’ does not name a type
    Dlaczego nie chce mi się skompilować? :(

    Odpowiedz
      1. Jurek

        Czy można jaśniej przepraszam ale nie wszyscy wiedzą jak zadeklarować ENUM_BUTTON PRZEZ typedef

        Odpowiedz
        1. Kamil Autor wpisu

          Jasne, chodzi dokładnie o ten krótki fragment kodu podany w artykule:

          typedef enum {
          BACK, NEXT, PREV, OK, NONE
          } ENUM_BUTTON;

          Odpowiedz
  3. Piotr

    Mam głupie pytanie:
    jak zmienić aby pod napisem menu kryła się na przykład zmienna odpowiadająca za temperaturę.
    z góry dzięki za odpowiedź.

    Odpowiedz
    1. Kamil Autor wpisu

      W funkcji drawMenu() masz linijkę „lcd.print(F(„Menu glowne”));”. Wypisz tam po prostu to co chcesz zamiast Menu glowne

      Odpowiedz
      1. Piotr

        Dzięki,
        a wie pan może jak dodać kolejne okno (pulpit) w menu??
        chodzi o to:
        menu[0] = {„Cyfry”, 0, 9, 5, NULL};
        menu[1] = {„Liczby”, 10, 1000, 15, NULL};
        menu[2] = {„Napisy”, 0, 2, 0, formatNapisy};
        menu[3] = {„Ulamki”, 0, 30, 15, formatUlamki};
        menu[4] = {„Port szer.”, 0, 0, 0, actionPortSzeregowy};

        Odpowiedz
        1. Kamil Autor wpisu

          Zależy co chcesz wyświetlić, jeżeli będzie to menu z wartościami liczbowymi to
          menu[x] = {„Nazwa menu”, start_zakresu, koniec_zakresu, wartosc_domyslna, NULL};
          W przypadku jakbyś chciał wyświetlić opcje tego menu we własnym formacie musisz napisać funkcję formatującą i jej adres przezazać jako ostatni parametr (zamiast NULL).

          Odpowiedz
          1. Piotr

            chodziło mi o pomnożenie np. menu[1] = {„Liczby”, 10, 1000, 15, NULL};

          2. Kamil Autor wpisu

            Nie bardzo rozumiem co masz na myśli przez pomnożenie :D

          3. Piotr

            Nie bardzo rozumiem co masz na myśli przez pomnożenie :D
            chodzi o dwie pozycje w menu. są od numerowane od 0 do 4 a chciał bym aby było od o do 6.

          4. Kamil Autor wpisu

            No to dopisz menu[5] = {…} i menu[6] = {…} tylko pamiętaj żeby wcześniej odpowiednio zwiększyć rozmiar tablicy menu

          5. Kamil Autor wpisu

            Gdzieś w kodzie masz pewnie fragment „STRUCT_MENUPOS menu[5];”. Tam zmieniasz 5 na inną liczbę np. jak masz menu od 0 do 7 to jest razem 8 elementów więc wpisujesz tam 8.

  4. Patryk

    Witam,
    mam pewien problem odnośnie czujnika temperatury. Gdy dodaję „void” odpowiedzialny za temperaturę i chcę umieścić to w menu to po wybraniu danej opcji na wyświetlaczu wyświetla się „-127 stopni” (czyli jakby czujnik nie był podpięty) lecz gdy wgram szkic odpowiedzialny tylko za wyświetlenie temperatury to już jest dobrze.
    nie wiem z czego to wynika…
    z góry dziękuje za pomoc

    Odpowiedz
          1. Kamil Autor wpisu

            Używanie funkcji delay() to bardzo zły pomysł w przypadku takich aplikacji. Druga sprawa w funkcji temperatura() masz sensors.getTempCByIndex(0), pdoczas gdy wcześniej przypisałeś pobraną temperaturę do zmiennej temp (w funkcji loop). Zrób sobie globalną zmienną na temperaturę, zrób pomiar w oddzielnym „wątku” i w funkcji temperatura() użyj tej globalnej zmiennej ;)

          2. Patryk

            a mógł byś (jeśli się da) zmienić ten program tak aby działał

          3. Kamil Autor wpisu

            Jeżeli chcesz mogę to zrobić odpłatnie. Napisz na kontakt (at) krupson.eu

          4. Patryk

            nie trzeba,
            bawię sie dla przyjemność.
            a co oznacza słowo „wątku w tym przypadku”
            i jeczcze chciałem dodać że w programie który polega tylko na tym aby pokazać temperaturę na wyświetlaczu oled wszystko działa

          5. Kamil Autor wpisu

            Pod pojęciem „wątek” mam na myśli, abyś użył funkcji takich jak millis() i co określony czas robił odczyt temperatury , zapisywał go do jakiejśc zmiennej globalnej. Drugi wątek odpowiedzialny za wyświetlanie obrazu na ekranie pobierałby z tej zmiennej dane i je prezentował odpowiednio.

            Tutaj pokazałem sposób jak możesz coś takiego zaimplementować: https://pastebin.com/DYLvQbfW

            Oczywiście to tylko udawana wielowątkowość. Jakbyś chciał prawdziwą to możesz użyć FreeRTOS lub FemtoOS, ale ostrzegam że to już trochę wyższa szkoła jazdy :)

          6. Patryk

            ok dzięki spróbuję to zrobić
            i wieczcorem napisze jak mi wyszło

          7. Patryk

            gdzie w szkicu mam umieścić
            ” float temp;
            sensors.requestTemperatures();
            temp = sensors.getTempCByIndex(0);

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *