Как написать меню для дисплея на arduino

Обновлено: 07.07.2024

Тема посвящена нарождающемуся проекту легкого, простого в обращении и не слишком простого по функционалу меню, использующему для вывода LCD экран типа 1602.

Несколько дней назад я создавал тему "Меню на LCD". Тогда проект уже наполовину работал и с каждым днем прогрессировал, потому я его не бросил и не пошел рассматривать готовый код. Тогда у меня возникла проблема с передачей меню названий пунктов, и быстро решилась при помощи массива структур. На данный момент код имеет версию 1.2 (все, что было до 1.0 - писалось без класса, подряд, взахлеб и зачастую необдуманно, и потому криво).

Меню можно представить как дерево, в самом корне которого - функция control. Она всегда крутится в цикле loop, к ней так или иначе привязаны главные переменные и в ней вызываются функции, определяющие работу меню. Ей передается один параметр - значение кнопки. Это может быть на самом деле что угодно, что может передавать значения от 1 до 4. Функций, в ней вызываемых, несколько: menuDisplay - функция, в которой реализуется вывод названия вроде "меню такой-то версии", вывод названий пунктов и их листание; pointDisplay - в ней содержится код, отвечающий за вывод и функционирование пунктов меню; а также displaySleep и displayClean - они, как понятно из их названий, отвечают за сон (отключение подсветки) и обновление экрана.

Кроме функций, доступных только внутри класса и напрямую связанных с control, есть публичные функции, дающие возможность управлять меню пользователю. Это: mainDisp, которой через запятую передаются данные для вывода на главном экране, secondValue, которая позволяет запретить или разрешить вывод второго значения в пункте, и begin, которая существует в качестве костыля.

Названия пунктов меню задаются при помощи массива структур, определяемого пользователем. В этом массиве нулевой элемент хранит название меню. Кроме названий, в структуре хранятся два значения, ограничивающие диапазон переменных, задаваемых в пункте. Сам массив кладется в FLASH память.

Переменные, связанные с пунктами, хранятся в трех массивах в EEPROM, что позволяет сохранять их значения. Почему массива три? В первом массиве значения определяют, можно ли выводить второе значение пункта и менять его. Во втором и третьем - первое и второе значения пункта.

Что за второе значение пункта, спросите вы? Это лучше показать фотографией:

Yfpx7IZSx3k.jpg

Да, получается, что даже если в пункте не надо задавать два значения, он все равно будет занимать три ячейки. Неэкономично, но, думаю, простительно для столь сырого проекта.

Наверное, стоит пару слов сказать про передачу параметров главному экрану - это такая небольшая крутая фича меню. Функция реализована на вариативном шаблоне, поэтому ей можно передать различные значения через запятую (не знаю, насколько различные на самом деле, но int и char - точно). Переход на следующую строку осуществляется передачей ей аргумента “\r”.

Передача строк и чисел для вывода на главном экране:

Теперь об управлении. Как я уже сказал выше, главной функции передается значение кнопки или чего угодно, от 1 до 4. Этим значениям соответствуют ESC, DOWN, UP и ENTER. Если экран спит, то нажатием на любую клавишу он выводится из сна. Нажатие на ENTER, сделанное в главном экране, переключает его на экран меню. Здесь можно листать пункты клавишами UP и DOWN. Переход в пункт осуществляется ENTER’ом. Но в пункте ENTER выполняет только одно действие - переключает текущее изменяемое значение, что индицируется курсором. Как менять значения, думаю, понятно. Выход из пункта и из меню - это ESC. При выходе из пункта значение обоих переменных обновляется (если было изменено) в EEPROM.

Несмотря на то, что уже получился ничегосебе уже лонгридище, стоит написать еще кое-что. Да, это «еще одно меню», но конкретно это меню можно выделить из остальных проектов. Во-первых тем, что оно в теории (и такая была изначальная идея) проще в использовании, хоть и не умеет разных крутых штук. Во-вторых, оно создавалось (и создается) с целью оформить его в библиотеку и избавить конечного пользователя от необходимости созерцать в своем коде кучи вещей, которые было бы удобно спрятать «под капот». В-третьих, идея этого меню - быть как можно более легким по использованию памяти.


Несколько месяцев назад на хабре появилась статья «Реализация многоуровневого меню для Arduino с дисплеем». «Но, погодите, — подумал я. — Я написал такое меню еще шесть лет назад»!

В далеком 2009 году, я написал первый проект на базе микроконтроллера и дисплея под названием «Автомат управления освещением», для которого потребовалось создать такую оболочку меню, в которую влезет тысяча конфигов, а то и более. Проект был успешно рожден, компилируется и способен работать до сих пор, а оболочка менюОС пошла кочевать из проекта в проект, используя лучшие практики Ущербно-Ориентированного программирования. «Хватит это терпеть» сказал я, и переписал код.

Подкатом вы найдете legacy-код отборного качества, сказ о том, как я его переписал, а также инструкции для тех, кто захочет это использовать.

Требования и возможности менюОС

  1. простота использования, кнопки влево-вправо, вверх-вниз, назад-вперед.;
  2. древовидная структура любой адекватной глубины (до 256);
  3. общее количество пунктов меню, которого хватит всем (10^616);
  4. редактирование настроек;
  5. запуск программ.
  6. простенький встроенный диспетчер задач.

Файловая структура

В качестве примера, будем разбирать следующую структуру меню(слева номер пункта):

Главным догматом менюОС является «Все есть файл». Да будет так.
У каждого файла есть тип, название, родительская папка, прочие параметры
Опишем структурой:

  1. type,
  2. parent, он не очень нужен, так как вся информация есть в хлебных крошках, но остался как legacy
  3. mode1, два параметра, специфичных для каждого типа файла
  4. mode2
type == T_FOLDER

Основным файлом является папка. Она и позволяет создать древовидную структуру всего меню.
Самая главная здесь — корневая папка под номером нуль. Что бы не произошло, в итоге мы вернемся в нее.
Параметрами папки являются

В корневой папке 0 лежат файлы 1 и 2, всего 2 штуки.
Опишем ее так:

type == T_DFOLDER

В Папке 3 лежит несколько копий одной и той же программы, однако с разными ключами запуска.
Например, в автомате управления освещением имеется возможность установить до 64 суточных программ, с 16 интервалами в каждой. Если описывать каждый пункт, потребуется 1024 файла. На практике достаточно двух. А хлебные крошки скормим программе в виде параметров.

Нехитрая математика подсказывает нам, что если все 256 файлов будут динамическими папками с максимальным числом копий, общее число пунктов меню в системе составит 256^256 = 3.2 x 10^616. Этого ТОЧНО хватит на любой адекватный и не очень случай.

type == T_APP
type == T_CONF

Конфиг-файл, ради которого и затеян весь сыр-бор. Позволяет устанавливать булево или числовое значение какого-либо параметра. Работает с int16_t.

  1. Cell ID — Стартовый номер ячейки памяти для хранения данных. Все данные занимают два байта.
  2. Minimum — минимальное значение данных
  3. Maximum — максимальное значение данных.
type == S_CONF

Интересный(но оставшийся пока только в старом коде) конфиг, работает в связке с T_SFOLDER

type == T_SFOLDER

Особый вид папки вынесен ближе к конфигу, так как является одной из его разновидностей.
Представьте себе, у вас в системе зашита возможность работы по RS-485 по протоколам A,B или C. Помещаем в папку кучку файлов вида S_CONF и выбираем из них необходимый. Более того, когда мы зайдем в папку вновь, курсор подсветит активный вариант.
mode1, mode2 аналогичны для T_FOLDER. Дочерними файлами являются только T_SCONF

Результаты рефакторинга

  1. Выделил работу с аппаратной частью как минимум в отдельные функции в отдельном файле. В HWI вошли:
  2. Переписаны модули под классы. Спрятано в private все что только можно, унифицирован внешний вид, Фишка с классами и более-менее унифицированным интерфейсом потом пригодится.
  3. «Добавлен» интерфейс для работы с RTOS. Вернее, штатный диспетчер задач довольно просто заменить на любой другой.
  4. Банально прибрался в коде, сделал его более понятнее, убрал магические числа, улучшил интерфейс. Теперь его не стыдно показать.

Создание своего проекта

Настройка проекта включает в себя следующие пункты:

Создание файлов

Создадим массивы по ранее рассмотренной структуре

Создадим массив для конфигов:

Настройка кнопок


Я предпочитаю подключать кнопки с замыканием на землю и подтягивающим резистором к питанию, который всегда в наличии в МК.

В файле hw/hwdef.h укажем названия регистров и расположение кнопок:

Настройка дисплея
Создание приложения

Рассмотрим пример приложения, использующий базовые функции меню.
menuos/app/sampleapp.cpp

Создадим класс со следующей структурой:

И набросаем основные функции:

И функция, которая будет вызываться каждую секунду:

Теперь в menu.cpp пропишем, что по номеру 2 будет вызываться наша программа:


Соберем проект и посмотрим, что у нас получилось:

В разнообразных проектах на основе платы Arduino достаточно часто возникает задача создания меню, отображаемого на экране ЖК дисплея 16х2 и управляемого с помощью кнопок. В данной статье мы рассмотрим один из самых простых способов создания подобного меню (какой я только нашел в интернете). В этом проекте мы с помощью данного меню будем управлять светодиодом, но его легко можно адаптировать под любые другие электронные проекты на основе платы Arduino.

Внешний вид проекта меню на Arduino и ЖК дисплее
Необходимые компоненты

  1. Плата Arduino Uno (купить на AliExpress).
  2. ЖК дисплей 16х2 (купить на AliExpress).
  3. Светодиод (купить на AliExpress).
  4. Резисторы 220 Ом (2 шт.) и 1 Ом (купить на AliExpress).
  5. Кнопочная панель с 4-мя кнопками или 3 обычных кнопки (купить на AliExpress).
  6. Макетная плата.
  7. Соединительные провода.

Внешний вид компонентов, необходимых для сборки проекта, показан на следующем рисунке.

Внешний вид компонентов, необходимых для сборки проекта
Схема проекта

Схема проекта меню на основе платы Arduino и ЖК дисплея 16х2 представлена на следующем рисунке.

Схема проекта меню на основе платы Arduino и ЖК дисплея 16х2

На следующем рисунке эта схема показана применительно к ее сборке на макетной плате.

Схема сборки проекта на макетной плате

Далее на нескольких рисунках последовательно представлен процесс сборки этой схемы на макетной плате.

Сначала подключаем ЖК дисплей – питание и общий провод.

Подключение контактов управления ЖК дисплеем

Подключение контактов передачи данных ЖК дисплея

Подключаем контакт 16 ЖК дисплея на землю, а к контакту 15 дисплея подключаем резистор сопротивлением 1 Ом. Автор проекта (ссылка на оригинал приведена в конце статьи) рассчитал сопротивление данного резистора исходя из даташита на используемую им модель ЖК дисплея (LMB 162ABC). Если фоновая подсветка дисплея (Backlight) вам не нужна, то эти контакты можно оставить неиспользованными.

Подключение контактов управления фоновой подсветкой ЖК дисплея

Далее подключаем светодиод к контакту 9 платы Arduino через токоограничивающий резистор 220 Ом.

Подключение светодиода к контакту 9 платы Arduino

Далее подключаем кнопочную панель согласно ранее представленной схемы, либо же вместо нее вы можете использовать 3 обычные кнопки. В результате получаем следующий внешний вид собранной конструкции проекта.

Исходный код программы (скетча)

Автор проекта разработал его код таким образом, чтобы его можно было легко адаптировать (изменять) под другие проекты. Схема меню, реализуемая кодом программы, показана на следующем рисунке.

Схема меню для нашего проекта

При первоначальном нажатии кнопки вы можете выбрать пункт Navigate (навигация) или Execute (исполнение).

Пункт Execute (исполнение) : в этом пункте мы будем управлять светодиодом – включать/выключать, плавно угасать или мигать.

Пункт Navigate (навигация) : этот пункт меню мы будем использовать для навигации с помощью кнопок.

На следующем рисунке видно как автор проекта с помощью ленты подписал названия кнопок для упрощения навигации по меню.

Внешний вид собранной конструкции проекта с подписанными названиями кнопок

Далее представлен исходный код программы для реализации меню.

Текстовое меню для Ардуино на ЖК дисплее и энкодере вращения

Меню - это массив элементов sMenuItem, каждый из которых имеет свой уникальный ключ и ключ родителя для создания иерархии, а также название и ссылку на функцию-обработчик. В качестве ключа я использую перечисления, т.к. они удобнее чем просто числовые значения. Ключ mkBack имеет особое назначение: он нужен для пунктов меню "Back" - возврат на вышестоящий уровень меню.

  1. Задание функции обработчика. В этом случае параметр ReturnFromMenu должен быть установлен в 0. Если выбранный элемент не содержит дочерних элементов (т.е. это не подменю) и если для него задана функция-обработчик, то она будет вызвана. После выполнения функции управление будет передано обратно в меню.
  2. Анализ значения, возвращаемого функцией DrawMenu. Для этого параметр ReturnFromMenu должен быть установлен в 1. Анализ возвращаемого значения (ключа выбранного элемента меню) легко осуществить при помощи оператора switch.
В данном примере я использую обработчики. Кроме упомянутого параметра ReturnFromMenu в скетче есть и другие: ShowScrollBar, ScrollLongCaptions, ScrollDelay, BacklightDelay. Их назначение понятно из названия.
Функция DrawMenu принимает ключ подменю, которое требуется отрисовать. Затем в цикле анализируется состояние энкодера. При его вращении осуществляется перемещение курсора и перерисовка меню. При нажатии вызывается функция DrawMenu для выбранного элемента. В остальное время выполняется прокрутка длинных названий.

Меню верхнего уровня в данном скетче содержит 2 элемента: первый для решения квадратных уравнений, второй просто как пример построения многоуровнего меню. Параметр ReturnFromMenu установлен в 0 и при выборе пунктов меню вызываются соответствующие обработчики. Так осуществляется задание коэффициентов квадратного уравнения и нахождение его корней (функции InputA, InputB, InputC, Solve). В обработчиках следует помнить об управлении подсветкой дисплея, для этого регулярно вызывается функция LCDBacklight. Функция loop содержит единственный вызов DrawMenu. Вся дальнейшая работа будет заключаться в отрисовке меню и выполнении функций-обработчиков.

Вот в общем-то и всё, остальное должно быть понятно из примера. Ну и в заключение дочитавшим до конца предлагаю видео работы этого меню:


Возможно, Вам будет интересно:

90 комментариев:

Здравствуйте тёзка. Я "полный новичок" в ардуино, самостоятельно накидать менюшку с управлением энкодером, для меня, как-то сложновато. Поиск дал кучу вариантов.
Но Ваша работа наиболее качественная, и фишки в виде бегущего текста, вообще супер.
Но, сходу использовать скетч, не прокатило.
На версии IDE 1.6.7 (Uno R3) заругалась на строку "eMenuKey DrawMenu(eMenuKey Key)" словами "does not name a type". Подскажите, что можно сделать? И ой, чувствую, вопросов будет куча.

Здравствуйте!
Когда готовил публикацию у меня не было такой ошибки. А сейчас тоже поймал ее. Заметил, что если путь к файлу скетча содержит русские буквы, то ошибка не возникает. Пока не понял с чем это связано, попробую разобраться.
Вопросу будут - задавайте.

Попробовал оба варианта путей к скетчу, и с "C:\Новая папка\Arduino_KY040_Menu" и просто из корневухи С:\Arduino_KY040_Menu. Не выходит. Выдает целый список "недовольств":

Я использую версию 1.6.12. Она таких ошибок не выдавала, как и web редактор create.arduino.cc.
Скачал 1.6.7, немного изменил скетч, скомпилировалось без ошибок. Пробуйте.

По-быстрому спаял прототип KY-040, заменил lcd.begin на lcd.init, и всё заработало!
Слов нет, одни ассемблер-еденицы в адрес Вашей кармы! Шикарно! Да ещё и с большинством комментариев, да на русском . мне, как чайнику, просто сказка!
Но, когда через 20 сек., погас экран, у меня что-то внутри ёкнуло!
Да Вы, батенька, шутник!
Такой "спящий режим", для работы от батареек, нужная функция. Она отключаема?
И ещё момент: подскажите, как можно вставить первой строкой - просто текст (заголовок меню/подменю), и чтобы курсор заголовка не касался? По-простому lcd.print"Заголовок", не прокатило. :)
Ещё раз - не выражаемая Вам благодарность за труды!

Для управления подсветкой по скетчу разбросаны вызовы LCDBacklight: без параметров функция гасит подсветку через заданный интервал времени (BacklightDelay); параметры 0 и 1 - для выключения и включения подсветки соответственно. Если подсветка не должна отключаться автоматически, то можно просто выкинуть из программы все вызовы LCDBacklight без параметра и явно включать/выключать ее где нужно.

Но это всё так, навскидку.
Разумеется, заголовок прокручиваться не будет, это надо учитывать. Добавить прокрутку можно, но, мне кажется, это перегрузит меню.

Библиотека LiquidMenu обертывает Arduino библиотеку LiquidCrystal с возможностью создания меню. Она упрощает процесс создания меню, абстрагируя элементы меню в иерархически организованные классы.

Библиотека LiquidMenu

Пример использования библиотеки LiquidMenu

Содержание

Особенности

  • Быстрое и простое создание меню.
  • Выбираемые пункты меню.
  • Функции обратного вызова.
  • Связь I2C.

Требования

  • Arduino библиотека LiquidCrystal или аналог. , поддерживающий LiquidCrystal (на чипсете Hitachi HD44780 или совместимом). или совместимый микроконтроллер.
  • Устройство ввода рекомендуется (кнопки, поворотный энкодер и т.п.). Например, плата расширения с дисплеем и кнопками.

Загрузка

Быстрый старт

Организация классов

Для представления различных элементов меню данная библиотека использует иерархически структурированные классы.

Базовая схема иерархии классов Полная схема иерархии классов

Класс LiquidLine представляет собой строку текста/чисел на дисплее. Чтобы создать новый объект LiquidLine используйте конструктор.

Класс LiquidScreen представляет собой набор строк, которые одновременно отображаются на дисплее (т.е. "текущий экран").

Класс LiquidMenu объединяет экраны для формирования меню. Данный класс используется для управления меню (переключение экранов, выбор строк, вызов функций и т.д.).

LiquidSystem – это необязательный класс, который объединяет меню для формирования системы меню (например, главное меню, настройки и т.д.). У него такой же публичный интерфейс, что и у LiquidMenu .

Создание меню

Создание меню – это всё, что касается структуры. Сначала у нас есть переменные/константы, которые входят в состав объектов LiquidLine . Затем объекты LiquidLine входят в состав объектов LiquidScreen . Затем объекты LiquidScreen входят в состав объекта(ов) LiquidMenu . И, необязательно, объекты LiquidMenu входят в состав объекта LiquidSystem . Данная структура может быть реализована при создании объекта или позже с помощью публичных методов классов.

Навигация по меню

Навигация по меню осуществляется из объекта LiquidMenu или, если имеется несколько меню, из объекта LiquidSystem. Экраны могут быть зациклены вперед и назад или конкретный экран может быть указан его объектом или номером:

Фокус и функции обратного вызова

Строки текста/чисел, показанные на дисплее, могут быть интерактивными. Каждая строка обладает прикрепленными к ней функциями обратного вызова (по умолчанию до 8 штук). Они прикрепляются с помощью числа, указанного пользователем:

Чтобы вызвать прикрепленную к строке функцию, необходимо, чтобы на строку был наведен фокус (строка была выбрана). Для циклического перемещения фокуса по строкам, показанным на экране, используйте метод:

Когда строка выбрана, может быть вызвана одна из прикрепленных функций с помощью:

number указывает, какая из прикрепленных функций дожна быть вызвана.

Схожие функции могут быть присоединены под одним и тем же номером к разным строкам, а затем вызваны по похожим событиям. Например, если мы печатаем на дисплее состояние четырех светодиодов. Четыре светодиода показываются в четырех объектах LiquidLine с помощью имени и состояния. Функции, используемые для их включения, можно прикрепить под номером 1, а функции для выключения – под номером 2. Затем, если у нас 3 кнопки, первая может использоваться для переключения фокуса , вторая кнопка (например, кнопка "ВВЕРХ") может использоваться для вызова функции 1, а третья кнопка (например, кнопка "ВНИЗ") может использоваться для вызова функции 2.

Читайте также: