W tej części zajmiemy się delikatnym rozbudowaniem implementacji z poprzedniego artykułu. Zobaczysz jak za pomocą jednego przycisku lub enkodera zrealizować wiele funkcji w zależności od sytuacji, np. przechodzenie w górę menu i rozjaśnianie LCD. Ponadto, dodamy wyświetlanie nagłówków przy każdym poziomie menu tak jak było to pokazane na filmie demonstracyjnym we wprowadzeniu.
Struktura menu
Na początku zdefiniujmy wszystkie elementy menu. Niech wygląda ono następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// definition of menu's components: (*name, *next, *prev, *child, *parent, (*menu_function)) menu_t menu1 = { "DISPLAY", &menu2, &menu5, &sub_menu1_1, NULL, NULL }; menu_t sub_menu1_1 = { "Contrast", &sub_menu1_2, &sub_menu1_2, &sub_menu1_1_1, &menu1, NULL }; menu_t sub_menu1_1_1 = { "Test", NULL, &sub_menu1_1_1, NULL, &sub_menu1_1, NULL }; menu_t sub_menu1_2 = { "Brightness", NULL, &sub_menu1_1, NULL, &menu1, disp_brightness_callback }; menu_t menu2 = { "UART", &menu3, &menu1, &sub_menu2_1, NULL, NULL }; menu_t sub_menu2_1 = { "Baudrate : 19200", &sub_menu2_2, &sub_menu2_4, NULL, &menu2, NULL }; menu_t sub_menu2_2 = { "Data Bits : 8bit", &sub_menu2_3, &sub_menu2_1, NULL, &menu2, NULL }; menu_t sub_menu2_3 = { "Stop Bits : 1bit", &sub_menu2_4, &sub_menu2_2, NULL, &menu2, NULL }; menu_t sub_menu2_4 = { "Parity : none", NULL, &sub_menu2_3, NULL, &menu2, NULL }; menu_t menu3 = { "LED TOG", &menu4, &menu2, NULL, NULL, led_tog_callback }; menu_t menu4 = { "MOTORS", &menu5, &menu3, NULL, NULL, NULL }; menu_t menu5 = { "RESTORE DEFAULTS", NULL, &menu4, NULL, NULL, NULL }; |
Wyświetlanie nagłówków
Modyfikacje zacznijmy od implementacji wyświetlania nagłówków. Dla jasności, zamieszczam grafikę, która przedstawia, co będziemy starali się uzyskać:
Zmiany w kodzie z poprzedniej części nie będą duże, a z pewnością, nie będą skomplikowane. Zacznijmy od funkcji menu_refresh():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
void menu_refresh(void) { menu_t *temp; uint8_t i; uint8_t center; memset(lcd_buf, '-', LCD_COLS); if (currentPointer->parent) { temp = (currentPointer->parent)->child; center = (LCD_COLS>>1) - (strlen((temp->parent)->name)>>1); buf_locate(center-1,0); buf_char(' '); buf_str((temp->parent)->name); buf_char(' '); } else { temp = &menu1; buf_str_XY(0,0,"------- MENU -------"); } for (i = 0; i != menu_index - lcd_row_pos; i++) { temp = temp->next; } buf_clear_menu(); for (i = 1; i < LCD_ROWS; i++) { buf_locate(0, i); if (temp == currentPointer) buf_char(62); else buf_char(' '); buf_locate(2, i); buf_str(temp->name); temp = temp->next; if (!temp) break; } // lcd_refresh(); } |
Na poniższej grafice przedstawione są wszystkie zmiany (kliknij, aby powiększyć):
Na początku, za pomocą funkcji memset(), wypełniamy cały pierwszy wiersz bufora LCD znakiem „-„. Następnie, podobnie jak w implementacji z poprzedniej części, ustawiamy wskaźnik *temp na pierwszy element na danym poziome menu, ale w tym wypadku dodajemy jeszcze wyświetlanie nagłówka. Zmienna center służy do przechowywania współrzędnej kursora, od której ma zaczynać się tekst nagłówka (tak, aby był wyśrodkowany). Funkcja strlen() zwraca długość tekstu, który podamy jej jako argument. Przesunięcie bitowe całości w prawo o jeden bit jest równoważne z dzieleniem przez 2, ale wykonuje się szybciej 🙂
Funkcja buf_clear_menu() różni się od funkcji buf_clear() tylko tym, że nie czyści całego bufora wyświetlacza, a jedynie ostatnie wiersze, na których wyświetlane jest menu. No i została ostatnia w tej funkcji zmiana: wyświetlanie elementów menu zaczynamy od drugiego wiersza LCD, bo na pierwszym jest teraz nagłówek (i=1, bo indeksujemy od zera).
Niewielkiej zmiany wymagają jeszcze funkcje menu_next() oraz menu_prev():
Z punktu widzenia menu, nasz wyświetlacz ma teraz o jeden wiersz mniej, dlatego w zaznaczonych na powyższej grafice miejscach trzeba odjąć jeszcze jedną jedynkę.
Funkcje callback
Jak może na początku zauważyłeś, przy definicjach niektórych elementów menu dodałem adresy funkcji, które mają się wykonać po kliknięciu „enter”. Na pierwszy ogień weźmy prostszą:
1 |
menu_t menu3 = { "LED TOG", &menu4, &menu2, NULL, NULL, led_tog_callback }; |
Po takiej definicji, jeśli będziemy na pozycji menu o nazwie „LED TOG” i klikniemy „enter” zostanie wywołana funkcja led_tog_callback(). Dokładny opis tego jak jest to zrealizowane zawarłem już w poprzednim artykule, dlatego nie będę się tutaj powtarzał.
Jedynym zadaniem tej funkcji jest, jak sama nazwa wskazuje, zmiana stanu logicznego na przeciwny na pinie, do którego podłączona jest dioda LED:
1 2 3 4 |
void led_tog_callback(void){ GPIO_WriteBit(GPIOA, GPIO_Pin_5, !GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_5) ); } |
Drugą, bardziej złożoną funkcją, która również została przypisana podczas definicji elementów jest disp_brightness_callback().
1 |
menu_t sub_menu1_2 = { "Brightness", NULL, &sub_menu1_1, NULL, &menu1, disp_brightness_callback }; |
Jej zadaniem jest przeniesienie nas do „trybu sterującego podświetleniem LCD”, tzn. na ekranie zamiast menu ma być wyświetlony aktualnie ustawiony stopień jasności, a przyciski (lub np. enkoder), które służyły do przechodzenia na poprzedni/kolejny element menu mają umożliwiać zmianę jasności.
Jeden przycisk – wiele funkcji
Teraz przedstawię jak jednemu przyciskowi przypisać wiele funkcji. Oczywiście można to zrobić na instrukcjach warunkowych, ale jest lepszy sposób 🙂 I jest on bardzo prosty! Wiesz już co mam na myśli? Tak, w tym wypadku także można wykorzystać wskaźniki funkcyjne! 🙂 Funkcje do obsługi przycisków będą wyglądały następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void key_next_press(void){ static uint8_t key_next_lock=0; if( !key_next_lock && !(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10) ) ) { key_next_lock=1; if(key_next_func) (*key_next_func)(); } else if( key_next_lock && (GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_10)) ) key_next_lock++; } |
Jak widać zmianie uległa tylko jedna linia kodu 🙂
Należy jeszcze pamiętać, że przy definicji wskaźnika, należy zainicjalizować go adresem funkcji do obsługi menu:
1 |
void (*key_next_func)(void) = &menu_next; |
Funkcje dla pozostałych przycisków wyglądają analogicznie, więc pominę ich opis.
Ciało funkcji disp_brightness_callback()
Wróćmy teraz do funkcji disp_brightness_callback() i zajmijmy się jej implementacją:
1 2 3 4 5 6 7 8 9 10 |
void disp_brightness_callback(void){ key_next_func = disp_brightness_next; key_prev_func = disp_brightness_prev; key_enter_func = NULL; key_back_func = disp_brightness_back; disp_brightness_refresh(); } |
Na początku, do wskaźników funkcyjnych z obsługi przycisków, przypisujemy adresy funkcji z „trybu sterującego podświetleniem LCD”:
- disp_brightness_next() – zwiększenie jasności podświetlenia o 10%,
- disp_brightness_prev() – zmniejszenie jasności podświetlenia o 10%,
- disp_brightness_back() – powrót do menu.
Na końcu, za pomocą funkcji disp_brightness_refresh(), wyświetlamy na LCD aktualny poziom jasności podświetlenia.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void disp_brightness_refresh(void){ uint8_t percent = (TIM4->CCR1)/550; buf_str_XY(0,0, "---- BRIGHTNESS ----"); buf_clear_menu(); memset(&lcd_buf[2][8],0xff,percent/10); memset(&lcd_buf[2][8+percent/10],'-',10-percent/10); buf_locate(2,2); buf_int(percent); buf_locate(5,2); buf_char('%'); } |
Jak widać, w funkcji realizowana jest wyłącznie zmiana zawartości bufora LCD. Niezrozumiały może być jedynie zapis: TIM4->CCR1, dlatego delikatnie to rozwinę:
Regulacja podświetlenia od strony fizycznej może być zrealizowane przy pomocy tranzystora mosfet (np. 2N7000) i sterowania PWM. Generowanie przebiegu PWM jest możliwe do wykonania przy pomocy timerów, a rejestr CCR1 służy właśnie do regulacji wypełnienia impulsu. Nie będę szczegółowo rozwijał tego tematu i pominę konfigurację timera, bo w tej serii skupiam się na menu. W razie czego, cały kod, wraz z funkcjami konfigurującymi timer znajdziesz na moim githubie.
W funkcji disp_brightness_next() i disp_brightness_prev() zmieniamy tylko zawartość wspomnianego rejestru i odświeżamy LCD.
1 2 3 4 5 6 |
void disp_brightness_next(void){ if(TIM4->CCR1 < 55000) TIM4->CCR1 += 5500; disp_brightness_refresh(); } |
Funkcja disp_brightness_prev() jest analogiczna.
Pozostała już tylko funkcja umożliwiająca powrót do menu, czyli disp_brightness_back():
1 2 3 4 5 6 7 8 9 10 |
void disp_brightness_back(void){ key_next_func = menu_next; key_prev_func = menu_prev; key_enter_func = menu_enter; key_back_func = menu_back; menu_refresh(); } |
W tym miejscu do wskaźników funkcyjnych z obsługi przycisków przypisujemy adresy funkcji służących do przechodzenia po kolejnych elementach i poziomach menu. Na końcu wyświetlamy menu za pomocą funkcji znanej już z poprzedniej części – menu_refresh().
Podsumowanie
Na tym wpisie kończy się seria dotycząca menu na lcd. Wszystkie pliki źródłowe z tego przykładu znajdziesz na moim githubie. Jak wspominałem w poprzednim artykule, implementacja jest w bardzo dużym stopniu niezależna od rodzaju wyświetlacza czy wykorzystywanej platformy, więc bez problemu powinieneś przenieść ją na dowolny mikrokontroler czy wyświetlacz. Cieszę się i bardzo mi miło, że doszedłeś, aż tutaj. Mam nadzieję, że dowiedziałeś się czegoś nowego, a poznane tu mechanizmy przydadzą się w realizacji wielu ciekawych projektów.
Zostaw komentarz!
Podobała Ci się seria o menu? Masz jakieś pytania lub uwagi? Zostaw mi proszę komentarz, a jeśli uważasz artykuły za wartościowe – podaj je dalej 🙂 Fajnie by było gdyby trafiły do większej ilości osób. Pozdro i do następnego! 😉