Ніжки куди під'єднані кнопки налаштуйте на вхід GPIO_InPut, та назвіть за принципом:
Скопіюйте до згенерованого проекту файли бібліотек для роботи з "WH1602" і файли "MicroMenu-2". Заголовні файли до теки "inc" проекту, а сирцеві файли бібліотек до теки "src" згенерованого CubeMX проекту.
Меню без дисплею і кнопок керування (клавіатури) втрачає сенс, то ж приділимо дисплею і кнопкам деяку увагу в прикладі, щоб було максимально зрозуміло і для початківців, як вбудовувати micromenu до своїх проектів.
Підключаємо дисплей
До головного файлу main.c в розділі користувацьких #include додаємо такий рядок:
/* USER CODE BEGIN Private defines */
#define BUTTONn 5 // Кількість кнопок
typedef enum
{
BUTTON_LEFT = 0,
BUTTON_UP = 1,
BUTTON_DOWN = 2,
BUTTON_RIGHT = 3,
BUTTON_SELECT = 4,
BUTTON_NOTHING = 255
} Button_TypeDef;
/* USER CODE END Private defines */
Назначили кількість кнопок для опитування та нумерований тип Button_TypeDef:
В розділ
USER CODE PFP у файлі main.c додамо прототип функції отримання коду натиснутої кнопки:
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
static uint8_t getPressKey(void); // Функція отримання коду натиснутої кнопки
/* USER CODE END PFP */
А в розділі
USER CODE 4 файлу main.c додамо саму функцію отримання коду натиснутої кнопки:
/* USER CODE BEGIN 4 */
static uint8_t getPressKey(void)
{
for(uint8_t i = 0; i < BUTTONn; i++)
{
if(HAL_GPIO_ReadPin(BUTTON_PORT[i],BUTTON_PIN[i]) && !flagButton[i])
{
flagButton[i] = true;
key_pressed = i;
}
if(!HAL_GPIO_ReadPin(BUTTON_PORT[i],BUTTON_PIN[i]) && flagButton[i])
{
flagButton[i] = false;
key_pressed = BUTTON_NOTHING;
}
}
return key_pressed;
}
/* USER CODE END 4 */
Щоб функція опитування кнопок працювала правильно потрібно ще додати деякі змінні на початку головного файлу main.c в користувацький блок приватних змінних. Додамо рядки зі змінними в блок
USER CODE PV:
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
bool flagButton[BUTTONn] = {};
Button_TypeDef key_pressed = BUTTON_NOTHING;
GPIO_TypeDef* BUTTON_PORT[BUTTONn] = {BUTTON_LEFT_GPIO_Port,
BUTTON_UP_GPIO_Port,
BUTTON_DOWN_GPIO_Port,
BUTTON_RIGHT_GPIO_Port,
BUTTON_SELECT_GPIO_Port};
const uint16_t BUTTON_PIN[BUTTONn] = {BUTTON_LEFT_Pin,
BUTTON_UP_Pin,
BUTTON_DOWN_Pin,
BUTTON_RIGHT_Pin,
BUTTON_SELECT_Pin};
/* USER CODE END PV */
А щоб можна було оперувати змінними типу
bool потрібно додати бібліотеку "stdbool.h". Блок користувацьких вкладень у файлі main.c буде тепер виглядати так:
/* USER CODE BEGIN Includes */
#include "hd44780.h"
#include <stdbool.h>
/* USER CODE END Includes */
Все готово для вбудовування micromenu до проекту. На дисплей вже можна виводити пункти меню і всіляку інформацію, а кнопками "бігати" по меню і міняти якість параметри/значення. Для перевірки можна запустити компілятор і пересвідчитись що проект збереться без помилок, а значить ми на вірному шляху.
MicroMenu-2
Для прикладу, щоб не переобтяжувати статтю, створимо нескладне меню. Але достатнє щоб зрозуміти принцип. Меню буде такої структури:
 |
Структура меню |
Перший рівень буде мати три пункти меню: Menu-1, Menu-2 і Menu-3. З Menu-1 можна буде потрапити до другого рівня меню з двох пунктів: Menu-1.1, Menu-1.2. А з Menu-1.1 можна буде потрапити до єдиного пункту - меню третього рівня: Menu-1.1.1. Розпочнімо!
Перш за все треба вкласти до нашого проекту-прикладу бібліотеку "micromenu-2", файли вже заздалегідь було додано, залишилось додати рядок до
USER CODE Includes в файлі main.c. Тепер блок з вкладеннями буде виглядати так:
/* USER CODE BEGIN Includes */
#include "hd44780.h" // Бібліотека для дисплею WH1602
#include <stdbool.h> // Бібліотека визначення змінних типу bool
#include "MicroMenu.h" // Бібліотека мікроменю
/* USER CODE END Includes */
Тепер потрібно створити функцію яка буде виводити на екран назви пунктів меню за замовчуванням. Прототип функції додамо до блоку
USER CODE PFP в файлі main.c .Тепер цей блок виглядатиме так:
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
static uint8_t getPressKey(void); // Функція отримання коду натиснутої кнопки
static void Generic_Write(const char* Text); // Функція виводу тексту на дисплей за замовчуванням
/* USER CODE END PFP */
А саму функцію додамо до блоку
USER CODE 4 в файлі main.c:
static void Generic_Write(const char* Text)
{
if (Text) // Якщо є текст то...
{
lcdClrScr(); // Очистимо дисплей
lcdPuts(Text); // Надрукуємо текст
}
}
Можна розпочинати створювати безпосередньо самі пункти меню, їх назви, взаємодію з іншими меню і реакцію на виклик пунктів меню. До блоку
USER CODE 0 в файлі main.c додамо спочатку меню першого рівня:
/* USER CODE BEGIN 0 */
// Menus Name | Next | Prev | Parent | Child | SelectFunction | EnterFunction | Text
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1");
MENU_ITEM(Menu_2, Menu_3, Menu_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-2");
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-3");
/* USER CODE END 0 */
Де в дужках по порядку: поточне ім'я меню, яке буде наступне меню, яке буде попереднє меню, меню предок, меню нащадок, виклик функції коли обрано меню, виклик функції коли натиснуто "ENTER" та текст самого меню. Як бачимо наступне і попереднє меню прописано так щоб при натисканні кнопок вниз чи догори обиралось наступне чи попереднє меню та створено коло з меню (з першого можна потрапити до останнього та навпаки). Поки що не прописані меню нащадки і ніякі функції не викликаються.
Додамо ще одну важливу деталь. Виклик функції показу на дисплей тексту меню за замовчуванням, коли викликається те чи інше меню, та яке меню буде початковим при увімкнені. Ось ці рядки додайте до блоку
USER CODE 2 де вже знаходиться функція ініціалізації дисплею:
/* Set up the default menu text write callback, and navigate to an absolute menu item entry. */
Menu_SetGenericWriteCallback(Generic_Write);
Menu_Navigate(&Menu_1);
І щоб вже спробувати "побігати" по меню і побачити хоч якийсь результат, потрібно до безкінечного циклу while(1) в головній функції main() файлу main.c написати взаємодію меню з кнопками (клавіатурою). Додамо такі рядки до нашого прикладу в блок
USER CODE WHILE:
/* USER CODE BEGIN WHILE */
while (1)
{
if (getPressKey() != BUTTON_NOTHING && !flagPressed)
{
flagPressed = true;
switch(getPressKey())
{
case BUTTON_LEFT:
Menu_Navigate(MENU_PARENT);
break;
case BUTTON_UP:
Menu_Navigate(MENU_PREVIOUS);
break;
case BUTTON_DOWN:
Menu_Navigate(MENU_NEXT);
break;
case BUTTON_RIGHT:
Menu_Navigate(MENU_CHILD);
break;
case BUTTON_SELECT:
Menu_EnterCurrentItem();
break;
default:
break;
}
} else if (getPressKey() == BUTTON_NOTHING && flagPressed) {
flagPressed = false;
}
/* USER CODE END WHILE */
Та оголосимо в розділі
USER CODE 1 змінну типу bool - прапорець натиснутої кнопки:
/* USER CODE BEGIN 1 */
bool flagPressed = false;
/* USER CODE END 1 */
При натисканні кнопки "ліворуч" буде викликатись меню-предок. При натисканні кнопки "догори" буде викликатись попереднє меню. При натисканні кнопки "вниз" буде викликатись наступне меню. При натисканні кнопки "праворуч" буде викликатись меню-нащадок. При натисканні кнопки "вибір" буде викликатись функція прописана в розділі пункту меню -"EnterFunction".
Можна вже компілювати проект та заливати прошивку до макету. Натискайте кнопки "догори" чи "вниз" і побачите на дисплеї як міняється текст з назвами пунктів меню. На інші кнопки - ніякої реакції. Так і має бути. Йдемо далі.
Додамо пункти меню другого рівня. Розташуємо ці рядки до рядків, з меню першого рівня, що ми вже створили:
MENU_ITEM(Menu_1_1, Menu_1_2, Menu_1_2, Menu_1, NULL_MENU, NULL, NULL, "\tMenu-1.1");
MENU_ITEM(Menu_1_2, Menu_1_1, Menu_1_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1.2");
В першому меню другого рівня - Menu_1_1 є вихід до меню першого рівня Menu_1. Але ж так само треба з меню першого рівня зробити вхід до меню другого рівня. До рядка опису першого пункту меню, першого рівня внесемо такі зміни:
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, Menu_1_1, NULL, NULL, "\tMenu-1");
І загалом тепер опис нашого меню буде виглядати так:
/* USER CODE BEGIN 0 */
// Menus Name | Next | Prev | Parent | Child | SelectFunction | EnterFunction | Text
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, Menu_1_1, NULL, NULL, "\tMenu-1");
MENU_ITEM(Menu_2, Menu_3, Menu_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-2");
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-3");
MENU_ITEM(Menu_1_1, Menu_1_2, Menu_1_2, Menu_1, NULL_MENU, NULL, NULL, "\tMenu-1.1");
MENU_ITEM(Menu_1_2, Menu_1_1, Menu_1_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1.2");
/* USER CODE END 0 */
Компілюйте, заливайте до MCU і пробуйте потрапити з Menu-1 до Menu-1.1 натиснувши кнопку "праворуч". З Menu-1.1 кнопкою "ліворуч" можна повернутись до Menu-1.
По такому ж принципу створимо пункт меню третього рівня Menu-1.1.1. Не забуваємо прописати точку входу і виходу. Тепер опис нашого меню буде виглядати так:
/* USER CODE BEGIN 0 */
// Menus Name | Next | Prev | Parent | Child | SelectFunction | EnterFunction | Text
MENU_ITEM(Menu_1, Menu_2, Menu_3, NULL_MENU, Menu_1_1, NULL, NULL, "\tMenu-1");
MENU_ITEM(Menu_2, Menu_3, Menu_1, NULL_MENU, NULL_MENU, NULL, NULL,"\tMenu-2");
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-3");
MENU_ITEM(Menu_1_1, Menu_1_2, Menu_1_2, Menu_1, Menu_1_1_1, NULL, NULL, "\tMenu-1.1");
MENU_ITEM(Menu_1_2, Menu_1_1, Menu_1_1, NULL_MENU, NULL_MENU, NULL, NULL, "\tMenu-1.2");
MENU_ITEM(Menu_1_1_1, NULL_MENU, NULL_MENU, Menu_1_1, NULL_MENU, NULL, NULL, "\tMenu-1.1.1");
/* USER CODE END 0 */
Компілюйте, заливайте, пробуйте.
Залишилось додати функцій до нашого меню, щоб можна було щось міняти на нашому макеті, бо просто "бігати" по пунктах меню - який в тому сенс?
Якщо підсвічування дисплею під'єднати до живлення через транзисторний ключ, то можна програмно керувати підсвічуванням дисплею. Вмикати чи вимикати через наше меню.
Схема підключення підсвічування дисплею має бути такою.
 |
Схема підключення підсвічування дисплею через транзисторний ключ |
В мене підсвічування дисплею під'єднано до ніжки PB3 мікроконтролера, яка налаштована на вихід і названий цей PIN в CubeMX як LCD_BLACKLIGHT (дивись малюнок "Налаштування ніжок для прикладу"). Тепер до меню можна додати функцію, яка буде вмикати чи вимикати підсвічування дисплею. Або можна, замість підсвічування дисплею, "поблимати" набортним світлодіодом на PC13.
Наприклад, за увімкнення чи вимкнення підсвічування дисплею буде відповідати "Menu_3". Тоді робимо таку послідовність дій:
- В блоці "USER CODE PFP" оголошуємо прототип функції для цього меню;
// Menus function
static void Level1Item3_Enter(void);
- Пишемо саму функцію для цього меню, де: очистимо дисплей, надрукуємо інформацію, зчитаємо стан підсвічування і надрукуємо його, та увімкнемо чи вимкнемо підсвічування дисплею в залежності від натиснутої кнопки. Після завершення очищаємо дисплей і повертаємось до поточного пункту меню:
static void Level1Item3_Enter(void)
{
bool flagPressed = false; // Прапорець натиснутої кнопки
lcdClrScr(); // Очищаємо екран дисплею
lcdGoto(LCD_1st_LINE,0); // Друкуємо в першому рядку
lcdPuts(" BackLight is"); // Назву параметру
// Поки не натиснути кнопка "ліворуч" тут виконує функцію як "вихід"
while(getPressKey() != BUTTON_LEFT)
{
// Перевіримо в якому стані ніжка мікроконтролера куди під'єднано підсвічування дисплею
if(HAL_GPIO_ReadPin(LCD_BACKLIGHT_GPIO_Port,LCD_BACKLIGHT_Pin))
{
lcdGoto(LCD_2nd_LINE,0); // В другому рядку друкуємо стан параметру
lcdPuts("\tON "); // Підсвічування увімкнено
} else {
lcdGoto(LCD_2nd_LINE,0); // В інакшому разі
lcdPuts("\tOFF"); // Підсвічування вимкнено
}
// Скануємо тільки кнопки "вгору" і "вниз"
if(getPressKey() != BUTTON_NOTHING && !flagPressed)
{
flagPressed = true; // Коли якусь кнопку натиснули
switch(getPressKey()) // Перевіряємо що натиснуто
{
case BUTTON_UP: // Якщо кнопку "вгору", то увімкнемо підсвічування дисплею
HAL_GPIO_WritePin(LCD_BACKLIGHT_GPIO_Port,LCD_BACKLIGHT_Pin,ENABLE);
break;
case BUTTON_DOWN: // Якщо кнопку "вниз", то вимкнемо підсвічування дисплею
HAL_GPIO_WritePin(LCD_BACKLIGHT_GPIO_Port,LCD_BACKLIGHT_Pin,DISABLE);
break;
default: // В будь якому іншому випадку просто вихід з switch
break;
}
} else if(getPressKey() == BUTTON_NOTHING && flagPressed) {
flagPressed = false; // Коли кнопку відпустили
}
}
lcdClrScr(); // Очищення дисплею
Menu_Navigate(&Menu_3); // Повертаємося до того ж меню де були
}
- Та на останок потрібно ще змінити сам пункт меню таким чином, щоб було посилання на функцію і назву меню більш конкретизувати, щоб було зрозуміло що до чого:
MENU_ITEM(Menu_3, Menu_1, Menu_2, NULL_MENU, NULL_MENU, NULL, Level1Item3_Enter, " Press Enter\nON/OFF BACKLIGHT");
Тепер можна компілювати, заливати і пробувати як все це працює.
Післямова
Це лише маленький приклад реалізації меню. "Micromenu-2" є дуже гнучкою і зручною реалізацією і далі вже залежить від ваших навичок і фантазії. Наприклад, в кожне меню, в розділ
"SelectFunction", можна додати виклик функції, яка відповідає за короткий високий звук "біп". Тоді "бігаючи" по меню ваш пристрій буде весело "біпкати". Також реалізація функцій розділу
"EnterFunction" може відрізнятись. Це лише приклад. Можна скоротити кількість кнопок. Наприклад, якщо меню і значення параметрів "закільцьовані", то можна позбутись кнопки "вгору" чи "вниз". Або не використовувати кнопку "Select", яка виконувала, в прикладі, функцію кнопки "Enter". Замість цього, створити додаткові меню, при входженні до якого вже викликати функцію зміни якихось параметрів, налаштувань, тощо. Або зробити відстеження одиночного чи подвійного натискання кнопки і відповідно реагувати на це. Також можна опитування клавіатури (кнопок) оформити як окрему бібліотеку. Дерзайте!
Файли