Энкодер управление меню на дисплее
Рассмотрим, как сделать меню с энкодером Ардуино на дисплее LCD 1602 I2C. Мы представим два примера: меню для включения светодиодов и меню на дисплее с управлением от энкодера яркостью светодиодов. Рекомендуем вам ознакомиться с подключением к Arduino дисплея LCD 1602 Arduino и модуля энкодер Ардуино. Если вы уже подключали данные модули, то можно приступать к этому мини проекту.
Меню на Ардуино LCD 1602 с энкодером
Примеры меню с модулем энкодера и жк дисплеем, размещенные на этой странице, вы сможете адаптировать под свои нужды. Во многих проектах на Ардуино требуется для пользователя создать возможность для настройки или управления устройством на микроконтроллере. Энкодер позволяет сделать простое и нативное меню на LCD дисплее для управления светодиодами, микро серво Ардуино или для других задач.
Как сделать меню с энкодером на дисплее
Для этого проекта нам потребуется:
Скетч. Управление энкодером меню дисплея LCD
В первом, более простом примере, в меню дисплея имеется три пункта которые можно менять с помощью вращения ручки энкодера. При нажатии на тактовую кнопку энкодера, включается выбранный цвет RGB светодиода. В зависимости от положения курсора на дисплее, при помощи условного оператора if включается нужный цвет. Соберите схему, как показано на картинке выше и загрузите следующую программу.
Пояснения к коду:
Скетч. Двухуровневое меню с энкодером Ардуино
В следующем примере меню имеет второй уровень для регулировки яркости каждого светодиода отдельно. Переход из одного меню в другое происходит при нажатии кнопки энкодера, при котором меняется значение глобальной переменной w в скетче. Схема подключения светодиодов и модулей к микроконтроллеру остается прежней, как на картинке выше, следует лишь загрузить в плату следующий код программы.
Пояснения к коду:
Заключение. Мы рассмотрели два примера создания меню с энкодером на дисплее 1602 IIC Arduino. Если вы уловили всю суть управления меню с помощью энкодера (датчик угла поворота), то легко сможете использовать представленные примеры в своих собственных проектах с дисплеем на Ардуино. Если у вас еще остались вопросы по данной теме, то вы можете их оставлять в комментариях к этой записи.
Простое меню на Arduino и ЖК дисплее
В разнообразных проектах на основе платы Arduino достаточно часто возникает задача создания меню, отображаемого на экране ЖК дисплея 16х2 и управляемого с помощью кнопок. В данной статье мы рассмотрим один из самых простых способов создания подобного меню (какой я только нашел в интернете). В этом проекте мы с помощью данного меню будем управлять светодиодом, но его легко можно адаптировать под любые другие электронные проекты на основе платы Arduino.
Необходимые компоненты
Внешний вид компонентов, необходимых для сборки проекта, показан на следующем рисунке.
Схема проекта
Схема проекта меню на основе платы Arduino и ЖК дисплея 16х2 представлена на следующем рисунке.
На следующем рисунке эта схема показана применительно к ее сборке на макетной плате.
Далее на нескольких рисунках последовательно представлен процесс сборки этой схемы на макетной плате.
Сначала подключаем ЖК дисплей – питание и общий провод.
Затем подключаем контакты для передачи данных ЖК дисплея. Здесь использован 8-битный способ подключения ЖК дисплея к плате Arduino, но можно упростить схему, использовав 4-битный режим.
Подключаем контакт 16 ЖК дисплея на землю, а к контакту 15 дисплея подключаем резистор сопротивлением 1 Ом. Автор проекта (ссылка на оригинал приведена в конце статьи) рассчитал сопротивление данного резистора исходя из даташита на используемую им модель ЖК дисплея (LMB 162ABC). Если фоновая подсветка дисплея (Backlight) вам не нужна, то эти контакты можно оставить неиспользованными.
Далее подключаем светодиод к контакту 9 платы Arduino через токоограничивающий резистор 220 Ом.
Далее подключаем кнопочную панель согласно ранее представленной схемы, либо же вместо нее вы можете использовать 3 обычные кнопки. В результате получаем следующий внешний вид собранной конструкции проекта.
Исходный код программы (скетча)
Автор проекта разработал его код таким образом, чтобы его можно было легко адаптировать (изменять) под другие проекты. Схема меню, реализуемая кодом программы, показана на следующем рисунке.
При первоначальном нажатии кнопки вы можете выбрать пункт Navigate (навигация) или Execute (исполнение).
Пункт Execute (исполнение) : в этом пункте мы будем управлять светодиодом – включать/выключать, плавно угасать или мигать.
Пункт Navigate (навигация) : этот пункт меню мы будем использовать для навигации с помощью кнопок.
На следующем рисунке видно как автор проекта с помощью ленты подписал названия кнопок для упрощения навигации по меню.
Далее представлен исходный код программы для реализации меню.
LiquidMenu: Arduino библиотека для создания меню на LCD дисплее
Библиотека LiquidMenu обертывает Arduino библиотеку LiquidCrystal с возможностью создания меню. Она упрощает процесс создания меню, абстрагируя элементы меню в иерархически организованные классы.
Пример использования библиотеки LiquidMenu
Содержание
Особенности
Требования
Загрузка
Скачать библиотеку можно по ссылке ниже:
Быстрый старт
Организация классов
Для представления различных элементов меню данная библиотека использует иерархически структурированные классы.
Базовая схема иерархии классов Полная схема иерархии классов
Класс LiquidLine представляет собой строку текста/чисел на дисплее. Чтобы создать новый объект LiquidLine используйте конструктор.
Класс LiquidScreen представляет собой набор строк, которые одновременно отображаются на дисплее (т.е. «текущий экран»).
Класс LiquidMenu объединяет экраны для формирования меню. Данный класс используется для управления меню (переключение экранов, выбор строк, вызов функций и т.д.).
Создание меню
Навигация по меню
Навигация по меню осуществляется из объекта LiquidMenu или, если имеется несколько меню, из объекта LiquidSystem. Экраны могут быть зациклены вперед и назад или конкретный экран может быть указан его объектом или номером:
Фокус и функции обратного вызова
Строки текста/чисел, показанные на дисплее, могут быть интерактивными. Каждая строка обладает прикрепленными к ней функциями обратного вызова (по умолчанию до 8 штук). Они прикрепляются с помощью числа, указанного пользователем:
Чтобы вызвать прикрепленную к строке функцию, необходимо, чтобы на строку был наведен фокус (строка была выбрана). Для циклического перемещения фокуса по строкам, показанным на экране, используйте метод:
Когда строка выбрана, может быть вызвана одна из прикрепленных функций с помощью:
number указывает, какая из прикрепленных функций дожна быть вызвана.
Простое меню для Arduino
И так, пока ждем посылки из Китая со всеми компонентами. Сделаем простенькое меню для Arduino и LCD Keyboard шилда.
Как выглядит готовый результат:
Программировать я больше привык в Visual Studio (сейчас у меня версия communit 2015) + Arduino для Visual Studio
Меню у меня просто плоское, без вложенных подменю, мне кажется так проще и понятнее, у каждого наименования параметра есть код, который поможет описать его в инструкции или документации. Для начала у меня только параметры настройки даты и времени, которые будут сохраняться в модуль реального времени, при нажатии “save & exit”. Код получился простой и я надеюсь понятный.
Итак, готовая прошивка для простого меню с комментариями:
Надеюсь, этот код кому-то сэкономит время, прошу не стесняться комментировать =)
20 thoughts on “ Простое меню для Arduino ”
Пытаюсь использовать ваш код но выдает что переменнные не задекларированы
MenuSaveRam’ was not declared in this scope
Это проблема компилятора, я когда-то сталкивался с такой, надо объявление функций и структур вынести в заголовочный файл (.h)
подскажите для чайников как это сделать или где про это почитать. Спасибо!
Мне даже сложно указать на источник, это нюансы самого компилятора и языка си. Я думаю лучше начать с изучения синтаксиса языка http://cppstudio.com/cat/274/. Потом просто учиться по примеру других программ, благо для ардуино много примеров и уроков, тут пользуясь гуглом думаю не промажешь =)
Мне не помогло. Печально
Приветствую, хорошее меню. А можно ли его реализовать на дисплее LCD 1602 I2C и подключеным отдельно на макете кнопкам, если да то что поменять?
Конечно можно, нужно поменять обработку клавиш в этом месте на вашу, читая значение пинов к которым присвоены кнопки в этом месте
if (ButtonPress == 0) < // Если кнопки не были нажаты ранее
int ButtonPinValue = analogRead(0); // Проверяем значение, не нажата ли кнопка
if (ButtonPinValue
А приведёте пример изменения такой записи на 4-5 кнопок, сравнительно с вашим подключением кнопок на шилд (какие команды будут это заменять )? Буду очень благодарен! ^_^
Не совсем понятно как с int ButtonPinValue = analogRead(0) сделать приём с 4-5 выходов Ардуинки.
Ну если это не аналоговый вход а цифровые, тогда надо пользоваться digitalRead()
Ну как-то так:
if (ButtonPress == 0) < // Если кнопки не были нажаты ранее
if (digitalRead(pinButton1)) ButtonPress = 4; // Нажата [+]
else if (digitalRead(pinButton2)) ButtonPress = 2; // Нажата [Prev]
else if (digitalRead(pinButton3)) ButtonPress = 3; // Нажата [Next]
else if (digitalRead(pinButton4)) ButtonPress = 5; // Нажата [-]
else if (digitalRead(pinButton5)) ButtonPress = 1; // Нажата [Menu]
> else …
где pinButton соответственно пин к которому подключена конкретная кнопка
1) У функции void MenuSaveRam(int Concat) < …>потерялась закрывающая фигурная скобка
2)Для того чтобы sprintf(str_format, …) могла отображать результат во всю ширину экранчика, длина массива str_format должна быть 17 (на 1 длиннее ширины экранчика – для хранения нуля, завершающего строку)
Не совсем так =)
1) Скобочка закрывающая есть, одна после if… а вторая после процедуры
2) Длинна и так в 17, так как размерность у массив начинается с 0. От 0 до 16 – 17 символов.
Не получается запустить Ваш скетч. Пишет кучу ошибок:
menu:46: error: ‘MenuSaveRam’ was not declared in this scope
menu:47: error: ‘SettingDateDay’ was not declared in this scope
menu:48: error: ‘SettingDateMonth’ was not declared in this scope
menu:49: error: ‘SettingDateYear’ was not declared in this scope
menu:50: error: ‘SettingTimeHour’ was not declared in this scope
menu:51: error: ‘SettingTimeMin’ was not declared in this scope
C:\Users\Р?ван\Documents\Arduino\menu\menu.ino: In function ‘void loop()’:
menu:80: error: ‘pinButton11’ was not declared in this scope
menu:81: error: ‘pinButton8’ was not declared in this scope
C:\Users\Р?ван\Documents\Arduino\menu\menu.ino: In function ‘void MenuSaveRam(int)’:
menu:127: error: a function-definition is not allowed here before ‘<‘ token
menu:131: error: a function-definition is not allowed here before ‘<‘ token
menu:135: error: a function-definition is not allowed here before ‘<‘ token
menu:139: error: a function-definition is not allowed here before ‘<‘ token
menu:143: error: a function-definition is not allowed here before ‘<‘ token
menu:145: error: expected ‘>’ at end of input
Помогите запустить.
мм круто, но лучше бы вы показали, и не потому что лень разбираться самим….
действительно одна фигурная скобка лишняя:
void MenuSaveRam(int Concat) < // SAVE & EXIT
if (Concat == 1) < //TODO Сохранение параметров в RAM >;
MenuEnter = false; // Выход из меню
>
А если всё, после “// Функции вызываемые для изменения значений переменных” перенести по тексту выше, до “// Главный цикл” то всё заработает.
точнее перенести выше “// Тип структуры данных описывающих пункт меню”, до описания пунктов меню
Спасибо! Скобка реально потерялась ))
Здравствуйте! Подскажите пожалуйста – после того как Вы скомпилировали код в VS, как и чем(как я понимаю что не через ардуино ИДЕ) его загружаете в контроллер? (Писать код в VS действительно удобней).
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() добавить строки: