Реклама ⓘ
Главная » Arduino
Призовой фонд
на апрель 2024 г.
1. 100 руб.
От пользователей

Реклама ⓘ

Текстовое меню на Arduino для дисплея 20х4

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

  •  Создание простых текстовых меню  с необходимым количеством экранов и строк на экране.
  •  Вывод на экран значения переменной отдельно для каждой строки.
  •  Создание экранов с различающимся количеством строк.
  •  Назначение до двух независимых функций для каждой строки экрана.

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

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

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

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

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

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

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

Теперь пройдемся по скетчу, разберем основные моменты. В самом начале подключаются необходимые библиотеки для работы с экраном, энкодером и шиной I2C:

В прикрепленном файле:
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

И в основном подключается библиотека энкодера и собственно файл с кодом меню:
 

#include "GyverEncoder.h"
#include "MatroskinMenu.h"

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

После можно указать требуемое количество экранов и строк на каждом экране:
 

//#define QUANTITY_SCREENS 5                               // количество экранов
//#define QUANTITY_LINES 7                                 // количество строк

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

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

Теперь непосредственно создание и заполнение меню. Для задания названия пункта меню используется функция void SetNames (uint8_t s, uint8_t l, String n, uint16_t ind) или void SetNames (uint8_t s, uint8_t l, String n). Она перегружена и принимает три или четыре параметра, номер экрана (uint8_t s), номер строки (uint8_t l), название строки (String n) и еще может принимать переменную для отображения в конце строки (uint16_t ind), но можно и без нее. Как не трудно догадаться, эта функция присваивает название соответствующей строке меню и передает указатель на переменную, которую мы хотим видеть на этой строке. Следует обратить внимание, что переменная для корректного отображения должна иметь тип uint16_t, на экране под нее отведено три знакоместа, что дает нам корректное отображение чисел от 1 до 999 (0 отображаться не будет) . Также важный момент: эта переменная хранится в массиве как указатель, поэтому в функцию мы ее передаем по ссылке (&) . Пример использования функции:

  menu_one.SetNames(0, 0, "First Screen");
  menu_one.SetNames(0, 1, "1_line Val", &pokazometr);
  menu_one.SetNames(0, 2, "2_line");
  menu_one.SetNames(0, 3, "3_line");

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

После задания названий пунктов меню нужно поставить в соответствие или, проще говоря, прикрепить какие-либо внешние функции. Для каждого пункта меню предусмотрено максимально две функции, они независимы друг от друга, для их назначения используются функции void SetFunc1(uint8_t s, uint8_t l, void *p) и void SetFunc1(uint8_t s, uint8_t l, void *p) соответственно. Синтаксис вызова функций похож на функцию присвоения названий пунктам меню, с той лишь разницей, что кроме координат (номера экрана и номера строки) в функцию передается указатель на прикрепляемую внешнюю функцию. Пример использования:
 

  menu_one.SetFunc1 (1, 2, LedOn);
  menu_one.SetFunc2 (1, 2, LedOff);

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

После всего этого остается только отобразить наше меню на экране, для этого используется функция void MakeMenu (uint8_t f, uint8_t s), у нее два принимаемых аргумента, это номер активной строки uint8_t f (фокуса меню) и номер экрана uint8_t s. Для каждого из этих параметров возможно три варианта развития событий. Первый - параметр равен единице, это означает, что мы хотим переключиться на следующую по порядку строку/экран. Второй, - когда  предаваемый параметр равен нулю, - предыдущая строка/экран. И во всех остальных случаях (в скетче для этого используется двойка) переключения не произойдет, текущий экран и активная строка будут просто перерисованы на дисплее без изменений (обновление содержимого экрана). Примеры вызова этих функций по событиям энкодера:

  enc1.tick();
  if (enc1.isRight()) {                // Событие перемещения по списку строк вниз (следующая строка)
    menu_one.MakeMenu( 1, 2);
  };
  if (enc1.isLeft()) {                 // Событие перемещения по списку строк вверх (предыдущая строка)
    menu_one.MakeMenu( 0, 2);
  };
  if (enc1.isClick()) {                // Событие переключения следующего экрана меню
    menu_one.MakeMenu( 2, 1);
  }

Событие для переключения к предыдущему экрану (menu_one.MakeMenu( 2, 0)) у меня не задано, т.к. количество экранов небольшое и они просто циклично переключаются по кругу, но при необходимости его не трудно добавить.

Осталась функция вызова прикрепленных функций, здесь все просто: void RunFunction1() и void RunFunction2() , для первой и второй прикрепленной функции соответственно. Эти функции не принимают ни каких параметров, они просто запускают внешние функции согласно текущему экрану и активной строке на нем. Их вызов по зажатому повороту энкодера:

if (enc1.isRightH()) {               //Событие для прикрепленной функции 1 (ПРАВЫЙ нажатый поворот энкодера)
    menu_one.RunFunction1();
  };

  if (enc1.isLeftH()) {                //Событие для прикрепленной функции 2 (ЛЕВЫЙ нажатый поворот энкодера)
    menu_one.RunFunction2();
  };

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

Прикрепленные файлы:

Теги:

Опубликована: 0 1
Я собрал 0 0
x

Оценить статью

  • Техническая грамотность
  • Актуальность материала
  • Изложение материала
  • Полезность устройства
  • Повторяемость устройства
  • Орфография
0

Средний балл статьи: 0 Проголосовало: 0 чел.

Комментарии (14) | Я собрал (0) | Подписаться

0
Публикатор #
На форуме автоматически создана тема для обсуждения статьи.
Ответить
+1
verbkinm #
Интересный проект, много куда можно внедрить! А сколько весит прошивка и каково потребление устройства?
Ответить
0

[Автор]
AMatroskin #
Для атмеги 328 компилируется так:

Скетч использует 8186 байт (26%) памяти устройства. Всего доступно 30720 байт.
Глобальные переменные используют 722 байт (35%) динамической памяти, оставляя 1326 байт для локальных переменных. Максимум: 2048 байт.

Потребление не измерял, думаю тут оно не критично, вряд ли кто то будет собирать на ардуино устройство, рассчитанное на длительную работу от АКБ. Для этого есть МК по серьезней.
Ответить
+2
andro #
Но не забываем, что пока это всего лишь пустое меню, графическая оболочка для программы, всё зависит от этой самой программы, для которой осталось ещё 100-26=74% памяти устройства.
Ответить
0
Petrovich #
Но Ардуино - платформа которая имеет на борту МК-ры от простых до сложных. Чтобы схема на Ардуино стала меньше потреблять энергии достаточно убрать пару радиодеталей. К тому же ни кто не запрещает стереть все данные с МК и использовать для компиляции кода другое ПО...
Ответить
0

[Автор]
AMatroskin #
Конечно можно, есть альтернативные IDE, вы чем пользуетесь?
Ответить
0

[Автор]
AMatroskin #
Да да, для программы место есть, имхо, сам скетч на потребление сейчас влияет крайне мало т.к. срабатывает только по внешним прерываниям
Ответить
+1
seawar #
Оба аппаратных прерывания заведены на "Clk" и "Data" энкодера? Зачем? Для нормальной работы достаточно одного..
Ответить
0

[Автор]
AMatroskin #
Не достаточно, для нормальной отработки энкодера необходимо два клика, если один не на прерывании то зачастую он пропускается и соответственно энкодер не срабатывает.
Ответить
+1
seawar #
Я все свои проекты с энкодером делал с одним прерыванием. Небольшой код для антидребезга - и все прекрасно, устойчиво работает.. Использовать два прерывания на одно внешнее устройство - расточительство.
Отредактирован 15.11.2020 09:04
Ответить
0

[Автор]
AMatroskin #
А чего экономить, можно себе позволить)))
Ответить
0
Aliaksandr #
А в чём расточительство то ?
Ведь практически любой пин ардуины может работать с прерыванием, я люблю А1, А2, А3 юзать под энкодер. С прерыванием не работает только портВ, хотя его тоже можно юзануть , но тогда SPI не будет работать
Ответить
0
Алексей Кузьмин #
Как сделать в вашем меню отображение даты и времени от датчика часов (например DS3231). Если это возможно, то приведите пример. Или нужно переделывать весь код меню?
Ответить
0
Чистов Михаил #
При проверке скейча, как вашего образца так и моего, выдает ошибку в коде меню, ambiguous overload for 'operator=' (operand types are 'String' and 'char') , подскажите что с этим можно сделать?
Ответить
Добавить комментарий
Имя:
E-mail:
не публикуется
Текст:
Защита от спама:
В чем измеряется напряжение?
Файлы:
 
Для выбора нескольких файлов использйте CTRL

Raspberry Pi 2
Raspberry Pi 2
USB осциллограф DSO-2090 Набор начинающего радиолюбителя
вверх