Организация древовидного меню |
|
| Рубрика: Электроника для всех | Раздел: AVR. Учебный курс |
| дата:16-05-2010 | |
|
Почти для всех проектов на микроконтроллере с экранчиком требуется система меню. Для каких-то проектов одноуровневое, для других — многоуровневое древовидное. Памяти, как обычно, мало, поэтому хочется запихнуть все во флэш.
Попутно, из проекта в проект, развивалась своя псевдоОС — таймеры, события, диспетчеры. Перебирая разные системы, наткнулся на MicroMenu: Попробуем разобрать ее на части и прикрутить к системе. Изобразим это на рисунке:
Для чего такая избыточность? По сути, с текущим пунктом меню можно сделать четыре вещи:
При наличии джойстика (или четырех кнопок «крестом») эти действия как раз вешаются на свою кнопку. Соответственно, все эти действия отражают четыре указателя. В оригинальной системе указатель на потомка обозван SIBLING, но я считаю это идеологически неверным. Sibling – это родственник того же уровня. Брат или сестра. Но никак не потомок. Поэтому мы будем использовать идеологически выверенное CHILD. Итак, описание структуры пункта меню: 1 2 3 4 5 6 7 8 typedef struct PROGMEM{ void *Next; void *Previous; void *Parent; void *Child; uint8_t Select; const char Text[]; } menuItem;Добавлен байт Select – это код команды, привязанный к текущему пункту. Если у данного пункта есть подменю, код нулевой. Также есть поле Text. Капитан Очевидность подсказывает, что это, собственно, текст пункта меню. По расходам памяти — на каждый пункт меню расходуется 9 байт плюс длина текстовой части. И это все — кладется во флеш. Самое полезное, почерпнутое у MicroMenu – набор дефайнов для быстрого и удобного определения меню. 1 2 3 4 5 6 #define MAKE_MENU(Name, Next, Previous, Parent, Child, Select, Text) \ extern menuItem Next; \ extern menuItem Previous; \ extern menuItem Parent; \ extern menuItem Child; \ menuItem Name = {(void*)&Next, (void*)&Previous, (void*)&Parent, (void*)&Child, (uint8_t)Select, { Text }}В чем пафос такой конструкции? Для того, чтобы определить текущий элемент, нам надо указать ссылку на следующий, еще не известный компилятору. Поэтому этот дефайн создает заведомо избыточное количество описаний extern. Это означает, что такой идентификатор будет где-то описан, не обязательно в этом же файле. В качестве бонуса это позволит растащить меню по нескольким файлам, если вдруг возникнет такое неудовлетворенное желание. Теперь самое интересное: описание структуры меню, как на рисунке. 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 // для начала — пустой элемент. Который NULL на рисунке #define NULL_ENTRY Null_Menu menuItem Null_Menu = {(void*)0, (void*)0, (void*)0, (void*)0, 0, {0x00}}; enum { MENU_CANCEL=1, MENU_RESET, MENU_MODE1, MENU_MODE2, MENU_MODE3, MENU_SENS1, MENU_SENS2, }; // NEXT, PREVIOUS PARENT, CHILD MAKE_MENU(m_s1i1, m_s1i2, NULL_ENTRY, NULL_ENTRY, m_s2i1, 0, "Запуск"); MAKE_MENU(m_s1i2, m_s1i3, m_s1i1, NULL_ENTRY, m_s3i1, 0, "Настройка"); MAKE_MENU(m_s1i3, NULL_ENTRY,m_s1i2, NULL_ENTRY, NULL_ENTRY, MENU_RESET, "Сброс"); // подменю Запуск MAKE_MENU(m_s2i1, m_s2i2, NULL_ENTRY, m_s1i1, NULL_ENTRY, MENU_MODE1, "Режим 1"); MAKE_MENU(m_s2i2, m_s2i3, m_s2i1, m_s1i1, NULL_ENTRY, MENU_MODE2, "Режим 2"); MAKE_MENU(m_s2i3, NULL_ENTRY,m_s2i2, m_s1i1, NULL_ENTRY, MENU_MODE3, "Режим 3"); // подменю Настройка MAKE_MENU(m_s3i1, m_s3i2, NULL_ENTRY, m_s1i2, m_s4i1, 0, "Давление"); MAKE_MENU(m_s3i2, NULL_ENTRY,m_s3i1, m_s1i2, m_s5i1, 0, "Время"); // подменю Давление MAKE_MENU(m_s4i1, m_s4i2, NULL_ENTRY, m_s3i1, NULL_ENTRY, MENU_SENS1, "Датчик 1"); MAKE_MENU(m_s4i2, NULL_ENTRY,m_s4i1, m_s3i1, NULL_ENTRY, MENU_SENS2, "Датчик 2"); // подменю Время MAKE_MENU(m_s5i1, m_s5i2, NULL_ENTRY, m_s3i2, NULL_ENTRY, MENU_WARM, "Разогрев"); MAKE_MENU(m_s5i2, NULL_ENTRY,m_s5i1, m_s3i2, NULL_ENTRY, MENU_PROCESS, "Процесс");Готово! Естественно, пункты меню можно описывать и вперемешку, в порядке обхода дерева. Типа такого: 1 2 3 4 5 6 MAKE_MENU(m_s1i1, m_s1i2, NULL_ENTRY, NULL_ENTRY, m_s2i1, 0, "Запуск"); // подменю Запуск MAKE_MENU(m_s2i1, m_s2i2, NULL_ENTRY, m_s1i1, NULL_ENTRY, MENU_MODE1, "Режим 1"); MAKE_MENU(m_s2i2, m_s2i3, m_s2i1, m_s1i1, NULL_ENTRY, MENU_MODE2, "Режим 2"); MAKE_MENU(m_s2i3, NULL_ENTRY,m_s2i2, m_s1i1, NULL_ENTRY, MENU_MODE3, "Режим 3"); MAKE_MENU(m_s1i2, m_s1i3, m_s1i1, NULL_ENTRY, m_s3i1, 0, "Настройка");Можно даже пойти дальше — строить меню в какой-нибудь визуальной среде, а потом автоматически генерировать такой список. Но это на потом. Плюсы и минусы такой организации. Минус — явная избыточность. Плюс — возможность быстро редактировать меню — вставить новый пункт, поменять местами, удалить. Изменяются только соседние элементы меню, без тотальной перенумерации. Мне этот плюс перевесил все остальные минусы. Опять же бонус — можно организовать несколько не связанных друг с другом деревьев меню. Главное не потерять точку входа. Дальше. Как ходить по меню. Автор предлагает несколько дефайнов. Я их сохранил, хотя можно и без них обойтись. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define PREVIOUS ((menuItem*)pgm_read_word(&selectedMenuItem->Previous)) #define NEXT ((menuItem*)pgm_read_word(&selectedMenuItem->Next)) #define PARENT ((menuItem*)pgm_read_word(&selectedMenuItem->Parent)) #define CHILD ((menuItem*)pgm_read_word(&selectedMenuItem->Child)) #define SELECT (pgm_read_byte(&selectedMenuItem->Select)) menuItem* selectedMenuItem; // текущий пункт меню void menuChange(menuItem* NewMenu) { if ((void*)NewMenu == (void*)&NULL_ENTRY) return; selectedMenuItem = NewMenu; }Вроде должно быть понятно. Выполняется проверка, если есть куда переходить, то переходим. Иначе - не переходим. Вызывается эта процедура таким образом: 1 menuChange(PREVIOUS);Далее, процедура реакции на нажатие клавиш (в качестве параметра передается код нажатой клавиши): 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 uint8_t keyMenu(msg_par par) { switch (par) { case 0: { return 1; } case KEY_UP: { menuChange(PREVIOUS); break; } case KEY_DOWN: { menuChange(NEXT); break; } case KEY_RIGHT: ; case KEY_OK: { // выбор пункта uint8_t sel; sel = SELECT; if (sel != 0) { sendMessage(MSG_MENU_SELECT, sel); killHandler(MSG_KEY_PRESS, &keyMenu); killHandler(MSG_DISP_REFRESH, &dispMenu); return (1); } else { menuChange(CHILD); } break; } case KEY_LEFT: { // отмена выбора (возврат) menuChange(PARENT); } } dispMenu(0); return (1); }Процедура отрисовки меню. Зависит от выбранного экранчика, а также от используемой анимации при выборе. Например у меня экранчик 128х64 точки, текущий пункт меню всегда по середине экрана, сверху и снизу выводятся два предыдущих и два последующих элемента (если есть). Отрисовка вызывается после каждого нажатися на кнопку и по таймеру, два раза в секунду. Мало ли, может изменится что. И последний штрих — инициализация меню: 1 2 3 4 5 6 7 void startMenu(void) { selectedMenuItem = (menuItem*)&m_l1i1; dispMenu(0); setHandler(MSG_KEY_PRESS, &keyMenu); setHandler(MSG_DISP_REFRESH, &dispMenu); }Для начала хватит. В продолжении — сделать несколько меню, сделать процедуру работы с меню реентерабельной, забабахать модель в протеусе. Краткое описание того, что делает процедура setHandler - она привязывает обработчик к событию. В данном случае, при возникновении события MSG_KEY_PRESS вызовется функция keyMenu для обработки этого события. Для демонстрации системы меню, описанной в предыдущем посте, собрал модель в протеусе. На базе двухстрочного LCD-индикатора, контроллераatmega32 и пяти кнопок (влево-вправо-вверх-вниз и выбор). В своих схемах использую джойстики от мобилок, они тоже пятипозиционные. Также воткнул три светодиода, чтобы хоть как-то реагировать на выбор пунктов меню. Поскольку на экране у нас всего две строчки, решил отображение меню сделать горизонтальным. В верхней строчке отображается родительский пункт меню (или просто “Меню:”, если мы на верхнем уровне), во второй строчке - текущий пункт меню. Клавишами влево-вправо выбираем предыдущий-следующий. Клавишей вверх - возвращаемся в родительское меню, клавиша вниз (или ОК) - заходим в выбранный пункт. Обработка меню:
Некоторые модификации, связанные с моделированием:
Ну и, надеюсь, мне простят подключение светодиодов без балластного резистора? ;) Файлы к статье |
|
| <<< Предыдущая статья | Следующая статья >>> |