организация меню на микроконтроллере
Электроника для всех
Блог о электронике
Организация древовидного меню
Почти для всех проектов на микроконтроллере с экранчиком требуется система меню. Для каких-то проектов одноуровневое, для других — многоуровневое древовидное. Памяти, как обычно, мало, поэтому хочется запихнуть все во флэш.
Попутно, из проекта в проект, развивалась своя псевдоОС — таймеры, события, диспетчеры.
Я ее полностью писал на си, оптимизацией и вылизыванием еще не занимался.
Перебирая разные системы, наткнулся на MicroMenu:
Попробуем разобрать ее на части и прикрутить к системе.
Структура данных:
Меню организовано в виде четырехсвязного списка. Каждый элемент меню (пункт меню) ссылается на предыдущего и последующего элемента, также ссылается на своего родителя (пункт меню предыдущего уровня) и потомка (пункт подменю). Если это первый пункт, то предыдущего элемента у него нет, соответствующая ссылка пустая.
Изобразим это на рисунке:
Для чего такая избыточность? По сути, с текущим пунктом меню можно сделать четыре вещи:
При наличии джойстика (или четырех кнопок «крестом») эти действия как раз вешаются на свою кнопку.
Соответственно, все эти действия отражают четыре указателя. В оригинальной системе указатель на потомка обозван SIBLING, но я считаю это идеологически неверным. Sibling – это родственник того же уровня. Брат или сестра. Но никак не потомок. Поэтому мы будем использовать идеологически выверенное CHILD.
Итак, описание структуры пункта меню:
Добавлен байт Select – это код команды, привязанный к текущему пункту. Если у данного пункта есть подменю, код нулевой. Также есть поле Text. Капитан Очевидность подсказывает, что это, собственно, текст пункта меню. По расходам памяти — на каждый пункт меню расходуется 9 байт плюс длина текстовой части. И это все — кладется во флеш.
Самое полезное, почерпнутое у MicroMenu – набор дефайнов для быстрого и удобного определения меню.
#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. Это означает, что такой идентификатор будет где-то описан, не обязательно в этом же файле. В качестве бонуса это позволит растащить меню по нескольким файлам, если вдруг возникнет такое неудовлетворенное желание.
Теперь самое интересное: описание структуры меню, как на рисунке.
// для начала — пустой элемент. Который 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, «Процесс»);
Естественно, пункты меню можно описывать и вперемешку, в порядке обхода дерева. Типа такого:
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, «Настройка»);
Можно даже пойти дальше — строить меню в какой-нибудь визуальной среде, а потом автоматически генерировать такой список. Но это на потом.
Плюсы и минусы такой организации. Минус — явная избыточность. Плюс — возможность быстро редактировать меню — вставить новый пункт, поменять местами, удалить. Изменяются только соседние элементы меню, без тотальной перенумерации. Мне этот плюс перевесил все остальные минусы.
Опять же бонус — можно организовать несколько не связанных друг с другом деревьев меню. Главное не потерять точку входа.
Дальше. Как ходить по меню. Автор предлагает несколько дефайнов. Я их сохранил, хотя можно и без них обойтись.
Вроде должно быть понятно. Выполняется проверка, если есть куда переходить, то переходим. Иначе — не переходим. Вызывается эта процедура таким образом:
Далее, процедура реакции на нажатие клавиш (в качестве параметра передается код нажатой клавиши):
Процедура отрисовки меню. Зависит от выбранного экранчика, а также от используемой анимации при выборе. Например у меня экранчик 128х64 точки, текущий пункт меню всегда по середине экрана, сверху и снизу выводятся два предыдущих и два последующих элемента (если есть). Отрисовка вызывается после каждого нажатися на кнопку и по таймеру, два раза в секунду. Мало ли, может изменится что.
В шапке можно выводить текст родителя, чтобы знать, где находимся. Можно двигать курсор по пунктам, а не пункты прокручивать. На вкус и цвет все фломастеры разные.
И последний штрих — инициализация меню:
Для начала хватит. В продолжении — сделать несколько меню, сделать процедуру работы с меню реентерабельной, забабахать модель в протеусе.
Краткое описание того, что делает процедура setHandler — она привязывает обработчик к событию. В данном случае, при возникновении события MSG_KEY_PRESS вызовется функция keyMenu для обработки этого события.
Для демонстрации системы меню, описанной в предыдущем посте, собрал модель в протеусе. На базе двухстрочного LCD-индикатора, контроллераatmega32 и пяти кнопок (влево-вправо-вверх-вниз и выбор). В своих схемах использую джойстики от мобилок, они тоже пятипозиционные. Также воткнул три светодиода, чтобы хоть как-то реагировать на выбор пунктов меню.
Поскольку на экране у нас всего две строчки, решил отображение меню сделать горизонтальным. В верхней строчке отображается родительский пункт меню (или просто «Меню:», если мы на верхнем уровне), во второй строчке — текущий пункт меню. Клавишами влево-вправо выбираем предыдущий-следующий. Клавишей вверх — возвращаемся в родительское меню, клавиша вниз (или ОК) — заходим в выбранный пункт.
Обработка меню:
Некоторые модификации, связанные с моделированием:
Ну и, надеюсь, мне простят подключение светодиодов без балластного резистора? 😉
Спасибо. Вы потрясающие! Всего за месяц мы собрали нужную сумму в 500000 на хоккейную коробку для детского дома Аистенок. Из которых 125000+ было от вас, читателей EasyElectronics. Были даже переводы на 25000+ и просто поток платежей на 251 рубль. Это невероятно круто. Сейчас идет заключение договора и подготовка к строительству!
А я встрял на три года, как минимум, ежемесячной пахоты над статьями :)))))))))))) Спасибо вам за такой мощный пинок.
121 thoughts on “Организация древовидного меню”
Прикольно.
А как организовать такое меню на 7-сегментных индикаторах?
А в чем проблема то? Поменяй тип отображения только и всего. А в поле текст загони какой нибудь 7сег код спецсимвола, который у тебя будет отвечать за имя пункта меню.
Все то же самое, только вместотекста — цифры )
Проектик, не помешал бы.
Берем любой код, работающий с динамической индикацией, например вот этот:
http://radiokot.ru/start/mcu_fpga/avr/15/
В конец знакогенератора (здесь это DcMatrix) дописываем новых символов (например А Б В Г Е и т.п.). Дальше просто выводим символы как и обычные цифры, только что код >9.
P.S. Надеюсь не сильно туплю, время позднее …
На холодильных установках стоят веселые менюшки 🙂 Как они бедные извращались, чтобы названия параметров вместить в 3 семисегментника …
Так что сделать реально, правда русские буквы плохо выходят из палок, да и английские далеко не все.
Просто пронумеровать пункты, а расшифровку в инструкцию. И пусть читают.
А я вообще сюда попал, набросал в C# такой код, и он работает,как ни странно.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using AvrUsbDevice;
using System.Timers;
namespace LedTest
<
public partial class Form1 : Form
<
bool ledOn = false; // Флаг включен (true) или нет (false) светодиодик на плате
ushort vid = 0x16C0, pid = 0x05DC; // Тут комментарии излишни — это VID и PID
ATMega8 dev; // Объявляем объект типа ATMega16(так в оригинале я заменил на М8)
public Form1()
<
InitializeComponent();
>
private void Form1_Load(object sender, EventArgs e)
<
dev = new ATMega8(vid, pid); // Создаем объект dev класса ATMega16.
if (!dev.IsOpen()) // Если есть проблемы с USB — сообщим и выйдем
<
MessageBox.Show(String.Format(«Невозможно найти устройство vid = 0x<0:X>, pid = 0x<1:X>», vid, pid),
«Ошибка USB», MessageBoxButtons.OK, MessageBoxIcon.Error);
<
>
Close();
>
else // Если все хорошо, настроим микроконтроллер по USB
<
dev.DDRB |= 0x3f; // Пины bx00111111 порта B — на вывод
dev.PORTB &= 0x00; // Выключим светодиодик на плате
dev.DDRC |= 0x3f; // Пины bx00111111 порта C — на вывод
dev.PORTC &= 0x00; // Выключим светодиодик на плате
dev.DDRD |= 0xeb; // Пины bx11101011 порта D — на вывод
dev.PORTD &= 0x00; // Выключим светодиодик на плате
//dev.DDRA |= 0x55; // для меги 16-32
//dev.PORTA &= 0x00; // Выключим светодиодик на плате
>
>
// работа с портом B ++++++++++++++++++++++++++++++++++++++++++++++++++++++
private void button1_Click(object sender, EventArgs e)
<
<
dev.PORTB |= 0x01; // Включим светодиодик на плате
//System.Threading.Thread.Sleep(5000); // это таймер
dev.PORTB &= 0x00; // Выключим светодиодик на плате
dev.PORTC &= 0x00; // Гасим все в порту С это на пробу!
dev.PORTD &= 0x00; // Гасим все в порту D это на пробу!
panel1.BackColor = Color.LightYellow;
panel2.BackColor = Color.LightYellow;
panel3.BackColor = Color.LightYellow;
panel4.BackColor = Color.LightYellow;
panel5.BackColor = Color.LightYellow;
panel6.BackColor = Color.LightYellow;
panel7.BackColor = Color.LightYellow;
panel8.BackColor = Color.LightYellow;
panel9.BackColor = Color.LightYellow;
panel10.BackColor = Color.LightYellow;
panel11.BackColor = Color.LightYellow;
panel12.BackColor = Color.LightYellow;
panel13.BackColor = Color.LightYellow;
panel14.BackColor = Color.LightYellow;
panel15.BackColor = Color.LightYellow;
panel16.BackColor = Color.LightYellow;
panel17.BackColor = Color.LightYellow;
panel18.BackColor = Color.LightYellow;
> это часть кода
Смысл в том что я удаленно перегружаю зависшие устройства, там реле стоят. Только надо б чтоб если комп завис а реле само вернулось в исходное состояние-5 сек.
Памагите!
Есть небольшое замечание:
Более удобная реализация делается на многомерных массивах, не нужно указывать детей, соседей и родителей.
Плюс вместо кода операции лучше сделать ссылку на колбек функцию.
Компактно произвольное дерево в многомерный массив не упихать. Будут дырки, причем не маленькие. Также все равно нужны связи «назад», чтобы вернуться, например, в меню верхнего уровня. Либо запоминать всю иерархию.
По поводу колбэк функции. Да, в оригинале было сделано именно так. В моей парадигме — возникает событие
sendMessage(MSG_MENU_SELECT, sel);
а кто его будет обрабатывать — это не головная боль меню.
не нужно запоминать иерархию, нужно запомнить только текущий путь меню.
делается это массивом размером в максимальную глубину дерева.
Да? А второй размер массива какой предложите? Я даже не говорю о скорости работы с таким вот «деревом».
Почитайте книжку Кормена «Алгоритмы: построение и анализ», там описаны «стандартные» и очень эффективные реализации большинства часто встречающихся структур данных.
Здесь речь не идет о супер-скоростной обработке информации, типа сотни тысяч раз в секунду. Даже если работа с этим деревом будет затянется на десятки миллисекунд (что очень и очень врядли), то все равно это будет быстрее, чем скорость обновления картинки на недорогом ЖК индикаторе. Более приоритетные прерывания во время выполнения этой задачи — тоже не проблема, пусть себе выполняются.
Здесь мне кажется удобство разработки и объем занимаемой памяти заметно более важно, чем быстродействие.
А вообще, наваять свою удобную и себе понятную архитектуру поддержки меню является весьма хорошим упражнением по программизму.
А если меню неоднородное, а имеет разную глубину, то получим на массивах большую избыточность по занимаемым данным.
Да, зато нету необходимости ручной работы. Забудешь ссылку поменять (изменилось имя переменной) и кушай гемморой с битой ссылкой. А тут можно компактно обработать рекурсивной функцией. Да займет больше ресурсов, зато красивое и масштабируемое решение. Конечно если пишешь для Тини прийдётся немного попрыгать на углях :3
А я делал подобную систему на нескольких массивах структур — получилось довольно компактно (почти без дыр) и удобно. А главное — почти всё в статике, кроме полей ввода/вывода. Правда доделать до конца пока ещё не успел (появился срочный проект), как руки дойдут, может опишу поподробнее.
Еще минус по поводу колбэк-функции: данную систему я использую не только, как Главное Меню Приложения, а также как дополнительное («выпадающее» или «контекстное» в терминах большого интерфейса). Например сохранить-отменить, просмотреть-удалить-редактировать-записать и т.д. Естественно, вызываться оно может из разных мест и выполнять при выборе должно совершенно разные вещи.
В случае фиксированных колбэк-функций надо будет городить несколько идентичных меню, различающихся только колбэками. Или сами функции делать навороченными.
Что в данном случае контекст? Какие-то внешние переменные?
Я наоборот хочу все упихать в один модуль, чтобы процедура стала реентерабельной.
Ето меню для WinAVR или еще для Codevision подойдет?
WinAVR. Диалекта Codevision не знаю, скорее всего будут различия в работе с флэшем. А так — связный список он и в Африке связный список.
А он, собственно, и висит на кнопке вправо. Просто, поскольку джойстик пятипозиционный, то нажатие на него такжа приравнивается к движению вправо.
А, точно, это же С… Привык к паскалевскому case.
режет глаз использование указателей вида «void *Next;» Скажем так, небезопасно, тем более, что тут одна структура определяет элемент меню.
может я чего не догоняю, но не лучше ли использовать «menuItem* Next;» сразу становится понятнее, что находится по указателю.
Вот тоже интересная организация меню
Модули для текстового интерфейса пользователя (TUI)
http://arv.radioliga.com/content/view/180/38/
Проходил через такое описание, когда текст отдельно, а пункты — отдельно.
// массив меню изменения режима меню
PROGMEM t_menu m_mode[] = <
item_sim(mmm1,menu_one),
item_sim(mmm2,menu_multi)
>;
При любых изменениях — править в трех местах. Очень неудобно. MicroMenu подкупило компактным описанием пункта — все в одной строке.
Мозг можно поломать увидев m_s1i1 — что это.
Только скачав оригинал, понял что имелось ввиду что то типа Level1Item1.
А названия в принципе-то и не важны. Если принять второй формат описания, когда подпункты меню идут сразу вслед за родительским пунктом, то структура меню будет видна и так, невооруженным глазом.
У меня тоже как-то встала проблема постоения меню. Честно говоря смотреть что-то готовое даже в голову не приходило, да и тогда наверное этого не так много было. Писал я на Си, знаю я правда его достаточно посредственно( всякие тайпдефы, указатели и прочее я практически не знаю и очень редко применяю, из-за чего иногда получается громоздко).
Задача была аналогичная — сделать меню, на экранчике 128х64, древовидное. Причем напротив некоторых пунктов нужно было сделать чекбокс, а в некоторых возможность ввода чисел, не выходя из интерфейса меню. Впрочем это больше проблемы отображения чем построения самого меню.
Структуру меню я решил хранить ввиде строки для минимального использования памяти
^ — означает начало новой страницы меню,
| — разделяет пункты меню. причем первая строчка после ^ является заголовком страницы, а не пунктом
$ — означает то место куда должно вписываться число, которое можно редактировать с клавиатуры.
При запуске программы меню индексируется и находятся начальные позиции каждой страницы, в результате потом все работает очень быстро.
Кроме всего этого необходимы таблицы переходов, которые выглядели так(специально укоротил чтобы суть ясна была):
Тут следует заметить, что при нажатии на пункт меню может происходить две разные в корне вещи
вариант 1 — мы переходим на следущий уровень меню
вариант 2 — выполняется какое либо нестандартное действие — открытие специального окна, или какие-то действия и возврат в меню.
В первом случае в третьем массиве указывается номер страницы меню. Во втором — пишется отрицательное число. его модуль означает номер специального действия, которые все описаны в программе.
ТАБЛИЦА СО ЗНАЧЕНИЯМИ
// Всякие значения в менюшках
// ПЕРВУЮ КОЛОНКУ ОСТАВИТЬ ПУСТОЙ
flash unsigned char page[]= < 0, 0, 5, 5, 5, 5, 5, 5>; // страница меню
flash unsigned char line[]= < 0, 5, 0, 1, 2, 3, 4, 5>; // пункт меню
unsigned int value[]= < 0, 100, 0, 0, 0,59048, 1, 0>; // значение (хранится в оперативной памяти)
flash unsigned int max_value[]= < 0, 1000, 1000, 255,59048,59048, 255, 255>; // максимально допустимое число
flash unsigned char digits[]= < 0, 4, 4, 3, 5, 5, 3, 3>; // сколько позиций отводить на написание числа(минимум)
ТАБЛИЦА С ЧЕКБОКСАМИ
тут все аналогично. в моей программе они пока не использовались, поэтому и пусто везде.
// чекбоксы в меню
// ПЕРВУЮ КОЛОНКУ ОСТАВИТЬ ПУСТОЙ
flash unsigned char box_page[]= < 0, 0, 0, 0, 0, 0, 0, 0>;
flash unsigned char box_line[]= < 0, 0, 0, 0, 0, 0, 0, 0>;
unsigned char box_value[]= < 2, 2, 2, 2, 2, 2, 2, 2>;
Прочитав как другие люди по-нормальному делают меню, понимаю что можно было наверное намного ровнее сделать 🙂 Особенно всякие таблицы переходов можно было сделать на тайпдефах. Главный недостаток этой схемы — нельзя добавить пункт меню между другими без перенумерации в пределах одной страницы. Впрочем, если часто все это использовать — можно написать редактор, который будет генерить весь код, и тогда все проблемы такого плана отпадут.
Конечно к этому всему нужны функции вывода, но они весьма специфичны ибо писались под один конкретный проект и выкладывать их нет смысла.
>Главный недостаток этой схемы — нельзя добавить пункт меню между другими без перенумерации в пределах одной страницы.
Вот это меня всегда убивало и сподвигло на поиск новой системы.
я не писал для электроники, но когда-то кодил для телефонов j2me игрушки. меню делал на автоматах. описывал автомат отдельно от кода игры: структуру меню, действия, доп действия и т.д. и по этому описанию генерил java код, который представлял из себя конечный автомат. в итоге описание легко править, ошибок практически не было, и работал очень быстро. не было динамических структур, все данные есть до компиляции. получался минимальный размер кода и т.д.
я сколько ни пробовал прикрутить конечные автоматы, они какие-то нежизнеспособные. Либо количество состояний зашкаливает, либо параллельность надо.
извините за оффтоп, вопрос по асму для к580)))
msk equ 25h
mvi h, 14h
mvi l, 0h
lxi b ff11h
ldax b
xri msk
jnz m1
m1: inr l
dcx b
dcr h
jnz
ассемблер выдает «неопределенный символ» на строке 0004
в чем дело.
запятую пропустил. Да и дальше — строки 7 и 8 вместе не имеют смысла. Строка 11 — нет метки куда переходить.
Эх, давно я не видел этого асма. Где ж такое до сих пор преподают? И главное, зачем? Преподавали бы что-нибудь, что хоть как-то можно было бы использовать на практике. Если так хочется 8-битный то уже AVR, или PIC или 8051 на худой конец.
с запятой та же ситуация..
а в остальном все в порядке…
понимаете в чем тут дело, прога выполняет подсчет совпадений с маской, счетчик в регистре L
если вводить в десятичном виде все нормально…
H Реализация многоуровневого меню для Arduino с дисплеем в черновиках Tutorial
Здравствуйте хабравчане! Мне всегда были интересны всякие устройства, и еще больше была интересна возможность создавать их самому. И вот, однажды после очередной мысли блуждающей в голове была приобретена Arduino Mega 2560 и начались эксперименты. Как и большинство из тех кто становился обладателем Arduino, я, вдоволь помигав светодиодами, покрутив шаговые двигатели, решил двигаться дальше и создать что-нибудь более полезное. Когда идея о будущем устройстве сформировалась в голове я приступил к разработке. Успешная реализация идеи подразумевает решение нескольких комплексных задач. Одной из таких задач является создание удобного интерфейса для настройки.
Устройство имеет довольно большое количество параметров (несколько десятков) и в дальнейшем их количество скорей всего будет увеличиваться. Управлять таким количеством параметров отдельными кнопками и потенциометрами, как и контролировать точные значения параметров — невозможно, поэтому устройство оснащено дисплеем и клавиатурой, а управление параметрами осуществляется через меню. Для удобства настройки меню многоуровневое. Значения параметров имеют различный тип, это может быть как вкл/выкл, числовое значение, время и т. д. Так как у параметра может быть большое количество значений то имеется также возможность задания параметра путем удержания кнопки. Теперь подробней как это реализуется.
Клавиатура
Обеспечение работы клавиатуры немного более сложная задача чем кажется на первый взгляд человеку никогда не имеющему дело с электроникой. Так как мы имеем меню то для управления нам понадобятся следующие кнопки:
«Вверх» (передвижение вверх по меню, выбор следующего значения для параметра)
«Вниз» (передвижение вниз по меню, выбор предыдущего значения для параметра)
«Вправо» (вход в подменю, начало редактирования параметра)
«Влево» (выход из подменю, выход из редактирования параметра)
дополнительно нам пригодится еще несколько кнопок, например:
«Старт/Стоп»
«Быстрое действие 1»
«Быстрое действие 2»
«Быстрое действие 3»
…
«Быстрое действие 100»
Я намеренно указал большое количество кнопок, так как устройство будет развиваться и, с большой вероятностью, по мере использования, настройка некоторых параметров будет вынесена на главную панель. Также на панель будут выноситься кнопки для выполнения быстрых действий. Из этого следует что использовать для каждой кнопки отдельный вход микроконтроллера будет неправильно так как нам может либо не хватить входов, либо мы будем тратить много времени на обработку этих входов. Но существует очень распространенное решение этой задачи, при этом понадобится лишь один аналоговый вход. Суть решения заключается в следующем:
Клавиатура собирается из множества последовательно соединенных резисторов которые представляют из себя делитель напряжения. Один конец цепи подключается к земле, другой конец цепи подключается к +5 В. Далее все кнопки одним контактом подключаются к входу микроконтроллера а другим контактом к местам соединения резисторов. Схема такой клавиатуры выглядит следующим образом:
Таким образом в момент нажатия кнопки на вход микроконтроллера подается определенное напряжение которое зависит от нажатой кнопки. Микроконтроллер измеряя это напряжение с помощью АЦП понимает какая кнопка нажата.
Дребезг контактов
Следующая задача заключается в решении проблемы дребезга контактов. Используя выключатели для света, различные кнопки, клавиатуры и т. д. мы часто не задумываемся о том, что происходит внутри этой кнопки. Складывается ошибочное впечатление что кнопка имеет два фиксированных состояния — включено/выключено. На самом же деле когда мы нажимаем на кнопку то замыкание цепи происходит не сразу. В течении короткого промежутка времени(короткого только для человека, но не для микроконтроллера осуществляющего миллионы операций в секунду) контакты вибрируют, искрят, шумят и т. д. и кнопка за этот промежуток времени может имитировать большое количество срабатываний (нажатий и отжатий). Таким образом если эта кнопка используется, например, для счетчика нажатий то без защиты от дребезга контактов при однократном нажатии наш микроконтроллер может подумать что кнопку нажали 3 раза, или 5, или 20 раз и т. д. Все зависит от качества кнопки, частоты опроса кнопки, от погоды на Марсе и т. д. В большинстве случаев проблема решается считыванием значения кнопки не сразу после нажатия а через некоторое время.
Программно защиту от дребезга контактов реализуют несколькими способами, и, к сожалению довольно часто, для этого используют функцию Delay(). Я бы не рекомендовал ее использовать так как в момент ее выполнения микроконтроллер гоняет балду вместо того чтобы заниматься делами которыми он должен заниматься, таким образом мы теряем часть производительности. Вместо использования Delay() я запоминаю время нажатия кнопки и сам факт того что было нажатие. В рабочем цикле loop проверяется было ли нажатие и если нажатие было то сравнивается время нажатия с текущим временем. Если разница выше определенного значения (определенной задержки за которое дребезг успевает устаканиться) то производится считывание значения нажатой кнопки.
Такая реализация заодно позволяет реализовать функционал «зажатой кнопки» т. е. если нам необходимо увеличить значение параметра, например, с 1 до 100, то нам не придется 100 раз нажимать кнопку. Мы просто нажмем и будет удерживать ее. В момент нажатия значение однократно изменится с 1 на 2 и через несколько секунд удержания кнопки значение начнет изменяться с большой скоростью. Запоминание времени позволяет также легко задать несколько зависимостей, например если мы держим кнопку 1 секунду то значение начинает изменяться каждые 500 миллисекунд. Если мы держим кнопку 5 секунд то значение начинает изменяться каждые 25 миллисекунд и т. д.
Дополнительно такая реализация также защищает от быстрого повторного срабатывания кнопки сразу после нажатия. Т. е. если кнопка, например, отвечает за съемку кадра фотоаппаратом, то при нажатии кнопки фотоаппарат не начнет строчить как из пулемета. Будет сделан первый кадр и повторные кадры начнут сниматься только через некоторое время.
Работает следующим образом:
В цикле loop постоянно запоминаем текущее время в переменную KeyBoardTime2 и сравниваем ее с переменной KeyBoardTime1. В случае если разница между данными переменными будет больше чем переменная KeyBoardTimeInterval то мы сохраняем в переменную KeyBoardTime1 переменную KeyBoardTime2 и запускаем процедуру для обработки нажатий KeyBoardCalculate(). Дальнейшая обработка нажатий проверяется в процедуре KeyBoardCalculate(). Такой механизм позволяет нам сократить количество проверок нажатия кнопки, и более менее фиксировать частоту проверок без использования Delay.
В процедуре KeyBoardCalculate() первым делом считывается уровень напряжения с делителя напряжения клавиатуры
Функция analogRead в случае использования Arduino Mega 2560 кодирует аналоговое значения напряжения входа в цифровое 10-битное значение, т. е. если на входе мы будем иметь 5 В то значение будет равно 1023, если на входе 0 В то значение равно 0, если на входе 2.5 В то значение равно 512 и т. д. Таким образом в переменной KeyButton1Value хранится напряжение получаемое с клавиатуры. В моей клавиатуре в качестве рабочего диапазона я принял диапазон от 50 до 1000, т. е. в случае если на входе напряжение из этого диапазона то микроконтроллер думает что нажата одна из кнопок. Если значение не попадает в этот диапазон то микроконтроллер думает что нет ни одной нажатой кнопки.
После того как считано значение с клавиатуры программа определяет нажата ли хоть одна кнопка
Если ни одна кнопка не нажата это условие выполнится и программа сохранит последнее время когда не была нажата ни одна кнопка в переменную KeyButton1TimePress, после чего обнулит переменную KeyButton1WasChecked, говорящую о том что есть необработанные нажатия и переменную KeyButton1RepeatTimePress, использующуюся при обработке повторных нажатий.
Далее в переменную KeyButton1TimeFromPress заносится разница между текущим временем и последним временем когда не была нажата ни одна кнопка.
Затем выполняется проверка нажатия кнопки:
Если кнопка нажата то данное условие выполняется и мы проверяем факт первого нажатия кнопки
Если время которое прошло с момента последнего нажатия кнопки больше чем время в течении которого устаканивается дребезг контактов (переменная KeyButton1Latency) и это нажатие мы еще не обработали (KeyButton1WasChecked==0) то запоминаем значение клавиатуры:
запускаем процедуру ButtonPress() (которая определяет какое действие надо сделать для этой кнопки), запоминаем что обработали первое нажатие (KeyButton1WasChecked=1) и обнуляем переменную для обработки повторных нажатий (KeyButton1RepeatTimePress=0).
Если время которое прошло с момента последнего нажатия кнопки больше чем время после которого мы считаем что нажатие повторное+время следующей обработки повторного нажатия ((KeyButton1TimeFromPress)>(KeyButton1RepeatLatency+KeyButton1RepeatTimePress)) и первая обработка нажатия уже произошла (KeyButton1WasChecked==1) то сохраняем значение клавиатуры:
запускаем процедуру ButtonPress() (которая определяет какое действие надо сделать для этой кнопки), увеличиваем время через которое будет снова обработано удерживание кнопки
Процедура выполнения соответствующего для кнопки действия выглядит так:
т.е. проверяется какое напряжение выдает клавиатура и выполняется соответствующее действие, например, если измеренное напряжение имеет значение от 125 до 135 то нажата кнопка Вверх, если от 255 до 262 то кнопка Вниз и т.д. Количество кнопок и процедур их обрабатывающих ограничивается практически только вашей фантазией, уровнем шумов вашей клавиатуры и битностью АЦП.
Собственно вот и все что относится к обработке клавиатуры. Еще раз кратко. Если нажата кнопка — проверяем, если кнопка нажата первый раз то проверяем как давно, если недавно то работаем дальше пропустив обработку клавиатуры, если кнопка нажата уже достаточно долго и дребезга нет то запоминаем значение клавиатуры, обрабатываем действие для кнопки и запоминаем что первый раз нажатие уже обработано, если же первое нажатие кнопки уже обработано то проверяем наступило ли время повторной обработки кнопки, если наступило то обрабатываем нажатие и вычисляем время следующей повторной обработки.
Немного слов о том почему я использовал такую конструкцию в цикле loop, а не использовал прерывания. Дело в том что я не силен в работе прерываний микроконтроллеров и в моем устройстве уже используется одно прерывание которое должно работать с очень хорошей периодичностью, причем частота выполнения этого прерывания меняется со временем. Поэтому для избежания накладывания прерываний обработки клавиатуры на «важные» прерывания я решил отказаться от них, да и в моем понимании при неумении правильно «готовить» прерывания, они могут оказать негативный эффект, схожий с тем что возникает при использовании переходов GoTo.
Теперь мы дошли до описания работы меню. Для реализации структуры меню я вспомнил курс информатики, динамические списки и графы. Если вкратце то динамические списки бывают разные. Самые простые из них, например однонаправленные и двунаправленные.
Однонаправленные выглядят так:
Изначально мы имеем адрес элемента 1. Зная его адрес мы можем перейти на сам элемент 1, получить его значение и узнать адрес следующего элемента. Таким образом мы можем последовательно проходить по списку, узнавать значения и адреса следующих элементов. Беда такого списка в том что он однонаправленный. Т. е. перейдя в элемент 5 мы не можем вернуться в элемент 4. Для этого нам придется пройти весь список заново. Эта проблема решена в двунаправленных списках:
В таком списке находясь в элементе 5 нам известен адрес элемента 4 и мы можем без проблем перейти к этому элементу.
Для наглядности мы можем представить этот список в виде трех массивов, где массив Value[] хранит значения элементов. Массив Parent[] хранит индексы родителей либо 0 если родителей нет, и массив Child[] хранит индексы дочерних элементов и 0 если дочерних элементов нет. Таким образом двунаправленный список который был описан выше будет выглядеть так:
N | Value | Parent | Child |
1 | 1 | 0 | 2 |
2 | 2 | 1 | 3 |
3 | 3 | 2 | 4 |
4 | 4 | 3 | 5 |
5 | 5 | 4 | 0 |
Если мы например хотим сделать этот список кольцом то он будет выглядеть так:
N | Value | Parent | Child |
1 | 1 | 5 | 2 |
2 | 2 | 1 | 3 |
3 | 3 | 2 | 4 |
4 | 4 | 3 | 5 |
5 | 5 | 4 | 1 |
С этим думаю все понятно.
Давайте теперь представим что наша структура выглядит не как цепочка элементов а как дерево элементов. В таком случае элемент по прежнему может иметь одного родителя, но количество дочерних элементов теперь может быть любым. Такую структуру наши три массива уже описать не способны так как они хранят только два адреса. Эту проблему можно решить следующим образом — введем дополнительное условие. Пусть все дочерние элементы одного родительского элемента будут стоять строго по порядку. В дополнение к этому я изменю массив Child[] на массив ChildFirst[] и добавлю еще один массив ChildEnd[]. Пусть эти два массива хранят в себе индексы первого и последнего дочернего элемента соответственно и ноль если у элемента нет дочерних элементов.
Таким образом, например, если у нас есть один родительский элемент и три дочерних то структура будет выглядеть следующим образом:
N | Value | Parent | ChildFirst | ChildEnd |
1 | 1 | 2 | 2 | 5 |
2 | 2 | 0 | 3 | 0 |
3 | 3 | 0 | 4 | 0 |
4 | 4 | 0 | 5 | 0 |
Или если изобразить графически то примерно так:
Теперь у нас есть возможность описывать деревья. Давайте также введем некий элемент номер ноль. Пусть он будет родителем всех элементов для которых значение Parent равно нулю. А теперь взглянем на небольшой пример:
Используя тот же принцип можно описать и другую структуру, например:
N | Value | Parent | ChildFirst | ChildEnd |
0 | Main Menu | 0 | 1 | 3 |
1 | Menu1 | 0 | 4 | 7 |
2 | Menu2 | 0 | 10 | 14 |
3 | Menu3 | 0 | 8 | 9 |
4 | Param1_1 | 1 | 0 | 0 |
5 | Param1_2 | 1 | 0 | 0 |
6 | Param1_3 | 1 | 0 | 0 |
7 | Param1_4 | 1 | 0 | 0 |
8 | Param3_1 | 3 | 0 | 0 |
9 | Param3_2 | 3 | 0 | 0 |
10 | Param2_1 | 2 | 0 | 0 |
11 | Param2_2 | 2 | 0 | 0 |
12 | Param2_3 | 2 | 0 | 0 |
13 | Param2_4 | 2 | 0 | 0 |
14 | Param2_5 | 2 | 0 | 0 |
Графически это будет выглядеть примерно так:
Если мы нарисуем структуру которую описывают эти данные то получим основное меню Main Menu, состоящее из трех пунктов — Menu1,Menu2,Menu3. Каждый из этих пунктов включает в себя нескольких параметров ParamX_Y. Собственно это все. Мы получили меню с некоторым количеством параметров. Для возможности изменять параметры я добавил еще несколько массивов:
MenuTypeCode[]-массив содержит цифру которая показывает тип пункта меню. Например, если это пункт меню, а не параметр то значение равно 0, если это редактируемый параметр то для целого числа это будет цифра 1, для времени это цифра 2, для значений On|Off это цифра 3 и т. д. сколько душе угодно.
MenuValue[]-массив содержит значение конкретного параметра который мы настраиваем.
Таким образом добавив эти массивы к предыдущей структуре и заполнив их вымышленными данным получим структуру которая будет описываться так:
N | Value | Parent | ChildFirst | ChildEnd | MenuTypeCode | MenuValue |
0 | Main Menu | 0 | 1 | 3 | 0 | 0 |
1 | Menu1 | 0 | 4 | 7 | 0 | 0 |
2 | Menu2 | 0 | 10 | 14 | 0 | 0 |
3 | Menu3 | 0 | 8 | 9 | 0 | 0 |
4 | Param1_1 | 1 | 0 | 0 | 1 | 50 |
5 | Param1_2 | 1 | 0 | 0 | 2 | 3600 |
6 | Param1_3 | 1 | 0 | 0 | 3 | 0 |
7 | Param1_4 | 1 | 0 | 0 | 1 | 120 |
8 | Param3_1 | 3 | 0 | 0 | 2 | 7200 |
9 | Param3_2 | 3 | 0 | 0 | 3 | 1 |
10 | Param2_1 | 2 | 0 | 0 | 1 | 8 |
11 | Param2_2 | 2 | 0 | 0 | 2 | 0 |
12 | Param2_3 | 2 | 0 | 0 | 3 | 1 |
13 | Param2_4 | 2 | 0 | 0 | 1 | 5 |
14 | Param2_5 | 2 | 0 | 0 | 2 | 60 |
В итоге получилось меню MainMenu где например в пункте Menu1 есть параметр Param1_2 имеющий значение MenuValue=3600. Так как мы знаем что значение MenuTypeCode для этого элемента равно 2 то мы трактуем это значение как время, т.е. например поделим на 3600 секунд и получим 1 час. Для элемента Param1_3 значение MenuTypeCode равно 3 и мы будем трактовать MenuValue=0 не как ноль, а как Off. Забегая вперед скажу что в итоге это будет выглядеть так:
Осталось лишь реализовать программно правильный вывод на дисплей и правильное редактирование параметров. Перед описанием кода еще пару слов о железе. В качестве дисплея я использовал дисплей JN12864J с контроллером Sitronix ST7920 (разрешение дисплея 128х64 как раз подходит для отображения меню). Для работы с дисплеем использовал библиотеку U8glib. Клавиатура подключена к входу A0.
Я постарался максимально подробно прокомментировать код. Часть функционала уже описана выше. Расскажу подробней как осуществляется вывод меню на экран, редактирование пунктов, передвижение по меню:
Ранее описано что после обработки нажатия кнопки запускается процедура ButtonPress(), которая в свою очередь определяет какая кнопка была нажата и в зависимости от нажатой кнопки запускает соответствующие процедуры:
Для кнопки «Вверх» — процедура UpPress(), используется для передвижения по меню вверх, в режиме редактирования увеличивает значение параметра.
Для кнопки «Вниз» — процедура DownPress(), используется для передвижения по меню вниз, в режиме редактирования уменьшает значение параметра.
Для кнопки «Вправо» — процедура RightPress(), используется для входа в меню либо для входа в режим редактирования параметра.
Для кнопки «Влево» — процедура LeftPress(), используется для выхода из дочернего меню либо для выхода из режима редактирования.
В самом начале работы программы мы формируем меню запуском процедуры MenuSetup() В этой процедуре полностью описывается вся структура меню. Также задается начальное положение в меню:
Данная переменная хранит в себе индекс выделенного пункта меню.
Во время нажатия кнопок проверяется не находимся ли мы в режиме редактирования. Для этого используется переменная MenuEdit. Если данная переменная равна единице это означает что в данный момент времени мы редактируем значение параметра имеющего индекс MenuNowPos.
Таким образом если мы нажали кнопку Вверх или кнопку Вниз то производится проверка включен или нет режим редактирования. Если режим редактирования включен то проверяем тип элемента с индексом MenuNowPos и в соответствии с его типом меняем его значение. Для кнопки Вниз например:
Если режим редактирования выключен то проверяем есть ли соседний дочерний элемент у выделенного, и, если такой элемент есть то передвигаемся на него вверх или вниз, в зависимости от того какая кнопка нажата. Например для кнопки Вверх:
Аналогичная операция производится при нажатии кнопок Влево и Вправо. Если нажимаем кнопку Влево то проверяем находимся ли в режиме редактирования, если да выходим из него:
есди нет то проверяем есть ли для текущего элемента родительский, и, если есть — переходим на него:
Для кнопки Вправо алгоритм практически такой же. Проверяем есть ли дочерний элемент для текущего, и, если есть — переходим на него, если же нет и мы стоим на параметре то включаем режим редактирования:
Остается лишь вывести все на экран. Для этого используется процедуры Draw() и DrawMenu()
Процедура Draw() имеет стандартный вид для библиотеки U8glib. Запускает обновление экрана и завершает свое выполнение когда обновление завершено. Ее вызов происходит в цикле loop с некоторой периодичностью.
Процедура DrawMenu() отвечает непосредственно за вывод меню. Принцип прост — у нас есть несколько переменных:
MenuNowPos — индекс текущего выделенного элемента
MenuDrawPos — номер строки на экране в которой находится текущий выделенный элемент MenuNowPos
MenuDrawCount — максимальное количество отображаемых строк на экране.
и, если являются то рядом с ними отобразить их текущее значение в зависимости от их типа, например, для бинарного типа:
Немного видео демонстрирующего работу меню. В видео есть и меню не помещающееся на экран, и разные типы параметров (цифры, время, On|Off) и многоуровневое меню (меню в меню в основном меню), и повторное срабатывание кнопки через некоторое время:
Полный код рабочей программы привожу ниже:
P.S.: В дополнение к тому что уже описано добавлю что довольно легко ввести новые типы данных, сделать индивидуальный шаг для каждого параметра меню, индивидуально задать максимальное и минимальное значение для каждого параметра, ввести возможность блокировки пунктов при установке определенных значений для других параметров, настроить различную скорость изменения параметров и многое многое другое.
P.S.2: А чтобы настройки параметров не пропадали после перезагрузки устройства достаточно выполнить несколько
1. Подключить в начале программы библиотеку EEPROM.h строкой
2. В конец процедуры MenuSetup() добавить строки:
3. В процедуру UpdateSettings() добавить строки: