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

Время на прочтение
7 мин

Количество просмотров 127K

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

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

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

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

Для начала определимся с требованиями, которые мы предъявляем к меню:

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

А еще, необходимо чтобы все это как можно меньше весило, было неприхотливо к ресурсам и запускалось на любой платформе(пока есть для AVR, работает с GLCD и текстовым LCD).
Теоретически, с соответствующими драйверами, данное менюОС можно просто взять и подключить к RTOS.

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

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

0 Корень/
   1 - Папка 1/ - папка с файлами
       3 -- Программа 1
       4 -- Программа 2
       5 -- Папка 3/  - папка с множеством копий программы. Положение курсора будет являться параметром запуска
           6 --- Программа 3.1
           6 --- Программа 3.2
           6 --- Программа 3.3
           6 --- хххххх
           6 --- Программа 3.64
   2 - Папка 2/ - папка  с конфигами
       7 -- Булев конфиг 1
       8 -- Числовой конфиг 2
       9 -- Числовой конфиг 3
      10 --  Программа Дата/время

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

struct filedata{
	uint8_t type;
	uint8_t parent;
	uint8_t mode1;//параметр 1
	uint8_t mode2;//параметр 2
	char name[20];
};

Для каждого файла определим 4 байта в массиве fileData:

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

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

mode1 = стартовый номер дочернего файла,
mode2 = количество файлов в ней.

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

T_FOLDER, 0, 1, 2,
type == T_DFOLDER

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

mode1 = номер дочернего файла, копии которого будем плодить
mode2 = количество копий файла.

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

type == T_APP

Приложение. Его задача — прописаться в диспетчере задач (встроенном или внешнем), перехватить управление кнопками и править.

mode1 = id запускаемого приложения.
type == T_CONF

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

mode1 = id конфига

У конфига есть свой массив configsLimit, где на каждый конфиг приходится три int16_t числа конфигурации:

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

Например, в ячейку 2 можно записать число от -100 до 150, тогда строка примет вид:

2, -100, 150, 
type == S_CONF

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

mode1 = id конфига
type == T_SFOLDER

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

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

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

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

Модуль настройки часов мне было лень переписывать под hwi. Все равно его нужно полностью переделывать. Он ужасен.
Как проходил рефакторинг, можно наглядно увидеть в репозитории.

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

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

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

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

//массив структуры
static const uint8_t fileStruct[FILENUMB*FILEREW] PROGMEM =
{
	T_FOLDER, 0, 1, 2,				//0
		T_FOLDER, 0, 3, 3,			//1
		T_FOLDER, 0, 7, 4,			//2
			T_APP,	1, 1, 0,		//3
			T_APP,	1, 2, 0,		//4
			T_DFOLDER, 1, 6, 66,		//5
				T_APP,	5, 2, 0,		//6			
			T_CONF,	2, 0, 0,		//7
			T_CONF,	2, 1, 0,		//8
			T_CONF,	2, 2, 0,		//9
			T_APP, 2, 3, 0			//10
		
	
};

//Массив названий
static PROGMEM const char file_0[] = "Root";
static PROGMEM const char file_1[] = "Folder 1";
static PROGMEM const char file_2[] = "Folder 2";
static PROGMEM const char file_3[] = "App 1";
static PROGMEM const char file_4[] = "App 2";
static PROGMEM const char file_5[] = "Dyn Folder";
static PROGMEM const char file_6[] = "App";
static PROGMEM const char file_7[] = "config 0";
static PROGMEM const char file_8[] = "config 1";
static PROGMEM const char file_9[] = "config 2";
static PROGMEM const char file_10[] = "Date and Time";

PROGMEM static const char *fileNames[]  = {
	file_0,  file_1,  file_2,  file_3,  file_4,  file_5,  file_6,  file_7,  file_8,
	file_9, file_10
};

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

//number of cell(step by 2), minimal value, maximum value
static const PROGMEM int16_t configsLimit[] = {
	0,0,0,// config  0:  0 + 0 дадут булев конфиг
	2,-8099,8096,//config 1
	4,1,48,//config	2
};
Настройка кнопок

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

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

 #define BUTTONSDDR DDRB
 #define BUTTONSPORT PORTB
 #define BUTTONSPIN PINB
 #define BUTTONSMASK 0x1F
 #define BSLOTS 5
 
 /**Button mask*/
 enum{
	BUTTONRETURN = 0x01,
	BUTTONLEFT = 0x02,
	BUTTONRIGHT = 0x10,
	BUTTONUP = 0x08,
	BUTTONDOWN = 0x04
 };
Настройка дисплея

Сейчас проект тащит за собой библиотеку GLCDv3, что не есть хорошо. Исторически так сложилось.
Ссылка на google-code — https://code.google.com/p/glcd-arduino

Создание приложения

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

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

#ifndef __SAMPLEAPP_H__
#define __SAMPLEAPP_H__

#include "hw/hwi.h"

#include "menuos/MTask.h"
#include "menuos/buttons.h"

class sampleapp
{
//variables
public:
	uint8_t  Setup(uint8_t argc, uint8_t *argv);//запуск приложения. В качестве параметров - текущий уровень и массив хлебных крошек
	uint8_t  ButtonsLogic(uint8_t button);//обработчик кнопок
	uint8_t TaskLogic(void);//обработчик таймера
protected:
private:
	uint8_t tick;
	void Return();//возврат в главное меню

//functions
public:
	sampleapp();
	~sampleapp();
protected:
private:

}; //sampleapp
extern sampleapp SampleApp;

//Сишные <s>костыли</s>обертки для обработчика кнопок и диспетчера
void SampleAppButtonsHandler(uint8_t button);
void SampleAppTaskHandler();

#endif //__SAMPLEAPP_H__

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

uint8_t sampleapp::Setup(uint8_t argc, uint8_t *argv)
{
	tick = 0;
        //пропишем себя в системных модулях
	Buttons.Add(SampleAppButtonsHandler);//add button handler
	Task.Add(1, SampleAppTaskHandler, 1000);//add task ha
	GLCD.ClearScreen();//очистим экран 
        //и на самом видном месте напишем 
	GLCD.CursorTo((HwDispGetStringsLength()-11)/2, HwDispGetStringsNumb()/2);
	GLCD.Puts("Hello Habr");	
	return 0;
}

Обертки:

void SampleAppButtonsHandler(uint8_t button){
	SampleApp.ButtonsLogic(button);
}

void SampleAppTaskHandler(){
	SampleApp.TaskLogic();
}

Обработчик кнопок:

uint8_t sampleapp::ButtonsLogic(uint8_t button){
	switch (button){
		case BUTTONLEFT:
		
		break;
		case BUTTONRIGHT:
	
		break;
		case BUTTONRETURN:
		Return();
		break;
		case BUTTONUP:
		
		break;
		case BUTTONDOWN:

		break;
		default:
		
		break;
		
	}
	return 0;
}

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

uint8_t sampleapp::TaskLogic(void){
	GLCD.CursorTo((HwDispGetStringsLength()-11)/2, HwDispGetStringsNumb()/2+1);
	GLCD.PrintNumber(tick++);	
}

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

void MMenu::AppStart(void){
	if (file.mode2 != BACKGROUND){
		Task.Add(MENUSLOT, MenuAppStop, 10);//100 ms update
		Task.ActiveApp = 1;//app should release AtiveApp to zero itself
	}
	switch (file.mode1){//AppNumber
		case 2:
			SampleApp.Setup(level, brCrumbs);
		break;
		case 3:
			Clock.Setup(level, brCrumbs);
		break;
		default:
			Task.ActiveApp = 0;		
		break;
	}
}

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

То же самое для визуалов

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

Ссылки и репозитории

Проект собран в среде программирования Atmel Studio, но настанет тот день и он будет форкнут и под Eclipse. Актуальная версия проекта доступна в любом репозитории(Резервирование).

  1. Репозиторий на GitHub: https://github.com/radiolok/menuosv1
  2. Репозиторий на Bitbucket: https://bitbucket.org/radiolok/menuosv1
  3. GLCDv3: https://code.google.com/p/glcd-arduino/
  4. openLCD:https://bitbucket.org/bperrybap/openglcd/

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

#include <LiquidCrystal.h>

LiquidCrystal lcd(12, 11, 10, 5, 4, 3, 2); // ARDUINO UNO

//LiquidCrystal lcd(3, 2, 1, 7, 8, 9, 10); // ATTINY84

byte buttons[] = {6,7,8}; //10 for additional pin

const byte nrButtons = 3; // change to 4 if additional button added (число используемых кнопок)

int menusize = 10;

String menu[] = {

  «Menu»,                   //0

  «Menu>LED»,               //1

  «Menu>LED>Off»,           //2

  «Menu>LED>On»,            //3

  «Menu>LED>Fade»,          //4

  «Menu>LED>Blink»,         //5

  «Menu>LCDlight»,          //6

  «Menu>Nothing1»,          //7  

  «Menu>Nothing2»,          //8

  «Menu>Nothing3»           //9

};

int t;

byte pressedButton, currentPos,currentPosParent, possiblePos[20], possiblePosCount, possiblePosScroll = 0;

String parent = «»;

int brightness;

int LCDtoggle = 0;

int fadeAmount = 5;

unsigned long timer = (millis() / 10);

int ledMode = 0;

void updateMenu () {

  possiblePosCount = 0;

  while (possiblePosCount == 0) {

    for (t = 1; t < menusize;t++) {

      if (mid(menu[t],1,inStrRev(menu[t],«>»)1).equals(menu[currentPos])) {

        possiblePos[possiblePosCount]  =  t;

        possiblePosCount = possiblePosCount + 1;

      }

    }

    //find the current parent for the current menu (находим родительское меню для текущего пункта меню

    parent = mid(menu[currentPos],1,inStrRev(menu[currentPos],«>»)1);

    currentPosParent = 0;

    for (t = 0; t < menusize; t++) {

       if (parent == menu[t]) {currentPosParent = t;}

    }

    // reached the end of the Menu line (достигли конца строки меню)

    if (possiblePosCount == 0) {

      //Menu Option Items

      switch (currentPos) {

        case 2:

        case 3:

        case 4:

        case 5://Choose between 2,3,4,5

          for (t = 2; t<6; t++) {

            if (mid(menu[t],len(menu[t]),1) == «*») {

              menu[t] = mid(menu[t],1,len(menu[t])1);

            }

          }

          menu[currentPos] = menu[currentPos] + «*»;

        break;

        case 6: //Toggle

          if (mid(menu[currentPos],len(menu[currentPos]),1) == «*») {

            menu[currentPos] = mid(menu[currentPos],1,len(menu[currentPos])1);

          } else {

            menu[currentPos] = menu[currentPos] + «*»;    

          }

        break;

      }

      //Set Variables (устанавливаем переменные)

      switch (currentPos) {

        case 2:

          ledMode = 0;

        break;

        case 3:

          ledMode = 1;

        break;

        case 4:

          brightness = 128;

          ledMode = 2;

        break;

        case 5:

          brightness = 0;

          ledMode = 3;

        break;

        case 6:

          LCDtoggle = abs(LCDtoggle 1);

          digitalWrite(13,LCDtoggle);

        break;

      }

      // go to the parent (переходим к родительскому пункту)

      currentPos = currentPosParent;

    }

  }

    lcd.clear();

    lcd.setCursor(0,0); lcd.print(mid(menu[currentPos],inStrRev(menu[currentPos],«>»)+1,len(menu[currentPos])inStrRev(menu[currentPos],«>»)));

    lcd.setCursor(0,1); lcd.print(mid(menu[possiblePos[possiblePosScroll]],inStrRev(menu[possiblePos[possiblePosScroll]],«>»)+1,len(menu[possiblePos[possiblePosScroll]])inStrRev(menu[possiblePos[possiblePosScroll]],«>»)));

}

// Look for a button press (смотрим какие кнопки нажаты)

byte checkButtonPress() {

  byte bP = 0;

  byte rBp = 0;

  for (t = 0; t<nrButtons;t++) {

    if (digitalRead(buttons[t]) == 0) {bP = (t + 1);}

  }

  rBp = bP;

  while (bP != 0) { // wait while the button is still down (ждем пока кнопка еще нажата)

    bP = 0;

    for (t = 0; t<nrButtons;t++) {

      if (digitalRead(buttons[t]) == 0) {bP = (t + 1);}

    }

  }

  return rBp;

}

void setup() {

  lcd.begin(16,2);

  lcd.clear();

  for (t=0;t<nrButtons;t++) {

    pinMode(buttons[t],INPUT_PULLUP);

  }

  pinMode(13,OUTPUT);

  pinMode(9,OUTPUT);

  lcd.setCursor(0,0); lcd.print(«HELLO»);

  delay(1000);

  updateMenu();

}

void loop() {

  switch (ledMode) {

    case 0:

      analogWrite(9, 0);

    break;

    case 1:

      analogWrite(9, 255);

    break;

    case 3:

      if ((millis() / 10) timer > 30) {

        timer = (millis() / 10);

        brightness = abs(brightness 255);

        timer = (millis() / 10);

        analogWrite(9, brightness);

      }

    break;

    case 2:

      if ((millis() / 10) timer > 3) {

        timer = (millis() / 10);

        analogWrite(9, brightness);

        brightness = brightness + fadeAmount;

        if (brightness <= 0 || brightness >= 255) {

          fadeAmount = fadeAmount;

        }

      }

    break;

  }

  pressedButton = checkButtonPress();

  if (pressedButton !=0) {

    switch (pressedButton) {

      case 1:

          possiblePosScroll = (possiblePosScroll + 1) % possiblePosCount; // Scroll

      break;

      // If I wanted a 4 button controll of the menu (если добавляем 4-ю кнопку для управления меню)

      //case 4:

        //  possiblePosScroll = (possiblePosScroll + possiblePosCount — 1) % possiblePosCount; // Scroll

      //break;

      case 2:

        currentPos = possiblePos[possiblePosScroll]; //Okay

      break;

      case 3:

        currentPos = currentPosParent; //Back

        possiblePosScroll = 0;

      break;

    }

    updateMenu();

  }

}

String mid(String str, int start, int len) {

   int t = 0;

   String u = «»;

   for (t = 0; t < len;t++) {

    u = u + str.charAt(t+start1);

   }

   return u;

}

int inStrRev(String str,String chr) {

  int t = str.length()1;

  int u = 0;

   while (t>1) {

    if (str.charAt(t)==chr.charAt(0)) {

      u = t+1;t = 1;

    }

    t = t 1;

   }

  return u;

}

int len(String str) {

  return str.length();

}

Здравствуйте!
Один из подписчиков попросил сделать пример меню, для Arduino, которое должно отображаться, на 2 рядном 16 символьном I2C LCD дисплее.

Чтобы при помощи кнопок, можно было перемещаться по меню, управлять например светодиодами и какой-то простенькой программой.

Menu I2C LCD 1602 for Arduino Nano

Основой проекта является плата Arduino Nano, и I2C LCD дисплей, также использую 4 кнопки, 5 маломощных светодиодов, 5 сопротивлений на 500 ом, макетную плату, и соединительные провода.

Menu I2C LCD 1602 for Arduino Nano

Загрузите этот скетч в плату Arduino.

Внимание! При загрузке скетча в плату Arduino, в скетче следует указать I2C адрес вашего дисплея. При загрузке скетча в приложение UnoArdusim в скетче указывается любой I2C адрес с 0x20 по 0x27, и соответственно такой же адрес необходимо указать на модуле LCD дисплея.

Если вы хотите редактировать этот скетч в симуляторе UnoArduSim, то используйте только библиотеку Adafruit_LiquidCrystal, так как другие библиотеки для LCD дисплеев не поддерживаются!.

//Начало скетча

#include "Adafruit_LiquidCrystal.h"  // Для использования скетча в UnoArdusim, и для загрузки в Arduino
//#include <LiquidCrystal_I2C.h>         // Для загрузки скетча в Arduino

Adafruit_LiquidCrystal lcd(0x20);    // Для использования скетча в UnoArdusim, и для загрузки в Arduino
//LiquidCrystal_I2C lcd(0x3f, 16, 2);    // Для загрузки скетча в Arduino

const int button_OK = 14;
const int button_EXIT = 15;
const int button_UP = 16;
const int button_DOWN = 17;

int buttonState_OK = 0;
int buttonState_EXIT = 0;
int buttonState_UP = 0;
int buttonState_DOWN = 0;

int i = 0, j = 0, k = 0;
String onoff_0, onoff_1, onoff_2, onoff_3, onoff_4, onoff_x;

const int Led0 = 0;
const int Led1 = 1;
const int Led2 = 2;
const int Led3 = 3;
const int Led4 = 4;

int StateLed0 = 0;
int StateLed1 = 0;
int StateLed2 = 0;
int StateLed3 = 0;
int StateLed4 = 0;
int ledState4 = 0;
int StateLed4cycle = 0;

unsigned long previousMillis = 0;
const long interval = 1000;

void setup() {
  lcd.begin(16, 2);            // Для использования скетча в UnoArdusim, и для загрузки в Arduino
  //lcd.init();                    // Для загрузки скетча в Arduino
  lcd.setBacklight(HIGH);
  pinMode(button_OK, INPUT_PULLUP);
  pinMode(button_EXIT, INPUT_PULLUP);
  pinMode(button_UP, INPUT_PULLUP);
  pinMode(button_DOWN, INPUT_PULLUP);
  pinMode(Led0, OUTPUT); digitalWrite(Led0, LOW);
  pinMode(Led1, OUTPUT); digitalWrite(Led1, LOW);
  pinMode(Led2, OUTPUT); digitalWrite(Led2, LOW);
  pinMode(Led3, OUTPUT); digitalWrite(Led3, LOW);
  pinMode(Led4, OUTPUT); digitalWrite(Led4, LOW);
}

void blink4();

void loop() {
  StateLed0 = digitalRead(Led0);
  StateLed1 = digitalRead(Led1);
  StateLed2 = digitalRead(Led2);
  StateLed3 = digitalRead(Led3);
  StateLed4 = digitalRead(Led4);
  lcd.setCursor(1, 0);
  lcd.print("Led controller");
  lcd.setCursor(1, 1);
  lcd.print("Prog");
  lcd.setCursor(6, 1);
  lcd.print(StateLed0);
  lcd.setCursor(8, 1);
  lcd.print(StateLed1);
  lcd.setCursor(10, 1);
  lcd.print(StateLed2);
  lcd.setCursor(12, 1);
  lcd.print(StateLed3);
  lcd.setCursor(14, 1);
  lcd.print(StateLed4);
  buttonState_OK = digitalRead(button_OK);
  if (buttonState_OK == LOW) {
    delay(300); i = 1; lcd.clear();
    while (i > 0) {
      if (StateLed0 == 0) {
        onoff_0 = "OFF";
      } else {
        onoff_0 = "ON";
      }
      if (StateLed1 == 0) {
        onoff_1 = "OFF";
      } else {
        onoff_1 = "ON";
      }
      if (StateLed2 == 0) {
        onoff_2 = "OFF";
      } else {
        onoff_2 = "ON";
      }
      if (StateLed3 == 0) {
        onoff_3 = "OFF";
      } else {
        onoff_3 = "ON";
      }
      if (StateLed4cycle == 0) {
        onoff_4 = "OFF";
      } else {
        onoff_4 = "ON";
      }
      lcd.setCursor(6, 0);
      lcd.print("Menu:");
      lcd.setCursor(0, 1);
      lcd.print("Program");
      lcd.setCursor(9, 1);
      lcd.print(j);
      lcd.setCursor(11, 1);
      lcd.print(onoff_x);
      buttonState_OK = digitalRead(button_OK);
      if (buttonState_OK == LOW) {
        delay(300);
        k++;
        if (k > 1) {
          k = 0;
        } lcd.clear();
      }
      buttonState_EXIT = digitalRead(button_EXIT);
      if (buttonState_EXIT == LOW) {
        i = 0;
        lcd.clear();
      }
      buttonState_UP = digitalRead(button_UP);
      if (buttonState_UP == LOW) {
        j--;
        delay(300);
        if (j < 0) {
          j = 4;
        } lcd.clear();
      }
      buttonState_DOWN = digitalRead(button_DOWN);
      if (buttonState_DOWN == LOW) {
        j++;
        delay(300);
        if (j > 4) {
          j = 0;
        } lcd.clear();
      }
      if (j == 0) {
        onoff_x = onoff_0;
        while (k == 1) {
          onoff_x = onoff_0;
          lcd.setCursor(6, 0);
          lcd.print("Edit:");
          lcd.setCursor(1, 1);
          lcd.print("Led Pin");
          lcd.setCursor(9, 1);
          lcd.print(j);
          lcd.setCursor(11, 1);
          lcd.print(onoff_x);
          if (StateLed0 == 0) {
            onoff_0 = "OFF";
          } else {
            onoff_0 = "ON ";
          }
          buttonState_OK = digitalRead(button_OK);
          if (buttonState_OK == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_EXIT = digitalRead(button_EXIT);
          if (buttonState_EXIT == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_UP = digitalRead(button_UP);
          if (buttonState_UP == LOW) {
            delay(300);
            StateLed0++;
            if (StateLed0 > 1) {
              StateLed0 = 0;
            }
          }
          buttonState_DOWN = digitalRead(button_DOWN);
          if (buttonState_DOWN == LOW) {
            delay(300);
            StateLed0++;
            if (StateLed0 > 1) {
              StateLed0 = 0;
            }
          }
          digitalWrite(Led0, StateLed0);
          blink4();
        }
      }
      if (j == 1) {
        onoff_x = onoff_1;
        while (k == 1) {
          onoff_x = onoff_1;
          lcd.setCursor(6, 0);
          lcd.print("Edit:");
          lcd.setCursor(1, 1);
          lcd.print("Led Pin");
          lcd.setCursor(9, 1);
          lcd.print(j);
          lcd.setCursor(11, 1);
          lcd.print(onoff_x);
          if (StateLed1 == 0) {
            onoff_1 = "OFF";
          } else {
            onoff_1 = "ON ";
          }
          buttonState_OK = digitalRead(button_OK);
          if (buttonState_OK == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_EXIT = digitalRead(button_EXIT);
          if (buttonState_EXIT == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_UP = digitalRead(button_UP);
          if (buttonState_UP == LOW) {
            delay(300);
            StateLed1++;
            if (StateLed1 > 1) {
              StateLed1 = 0;
            }
          }
          buttonState_DOWN = digitalRead(button_DOWN);
          if (buttonState_DOWN == LOW) {
            delay(300);
            StateLed1++;
            if (StateLed1 > 1) {
              StateLed1 = 0;
            }
          }
          digitalWrite(Led1, StateLed1);
          blink4();
        }
      }
      if (j == 2) {
        onoff_x = onoff_2;
        while (k == 1) {
          onoff_x = onoff_2;
          lcd.setCursor(6, 0);
          lcd.print("Edit:");
          lcd.setCursor(1, 1);
          lcd.print("Led Pin");
          lcd.setCursor(9, 1);
          lcd.print(j);
          lcd.setCursor(11, 1);
          lcd.print(onoff_x);
          if (StateLed2 == 0) {
            onoff_2 = "OFF";
          } else {
            onoff_2 = "ON ";
          }
          buttonState_OK = digitalRead(button_OK);
          if (buttonState_OK == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_EXIT = digitalRead(button_EXIT);
          if (buttonState_EXIT == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_UP = digitalRead(button_UP);
          if (buttonState_UP == LOW) {
            delay(300);
            StateLed2++;
            if (StateLed2 > 1) {
              StateLed2 = 0;
            }
          }
          buttonState_DOWN = digitalRead(button_DOWN);
          if (buttonState_DOWN == LOW) {
            delay(300);
            StateLed2++;
            if (StateLed2 > 1) {
              StateLed2 = 0;
            }
          }
          digitalWrite(Led2, StateLed2);
          blink4();
        }
      }
      if (j == 3) {
        onoff_x = onoff_3;
        while (k == 1) {
          onoff_x = onoff_3;
          lcd.setCursor(6, 0);
          lcd.print("Edit:");
          lcd.setCursor(1, 1);
          lcd.print("Led Pin");
          lcd.setCursor(9, 1);
          lcd.print(j);
          lcd.setCursor(11, 1);
          lcd.print(onoff_x);
          if (StateLed3 == 0) {
            onoff_3 = "OFF";
          } else {
            onoff_3 = "ON ";
          }
          buttonState_OK = digitalRead(button_OK);
          if (buttonState_OK == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_EXIT = digitalRead(button_EXIT);
          if (buttonState_EXIT == LOW) {
            k = 0;
            delay(300);
          }
          buttonState_UP = digitalRead(button_UP);
          if (buttonState_UP == LOW) {
            delay(300);
            StateLed3++;
            if (StateLed3 > 1) {
              StateLed3 = 0;
            }
          }
          buttonState_DOWN = digitalRead(button_DOWN);
          if (buttonState_DOWN == LOW) {
            delay(300);
            StateLed3++;
            if (StateLed3 > 1) {
              StateLed3 = 0;
            }
          }
          digitalWrite(Led3, StateLed3);
          blink4();
        }
      }
      if (j == 4) {
        onoff_x = onoff_4;
        while (k == 1) {
          onoff_x = onoff_4;
          lcd.setCursor(6, 0);
          lcd.print("Edit:");
          lcd.setCursor(0, 1);
          lcd.print("Blink Pin");
          lcd.setCursor(10, 1);
          lcd.print(j);
          lcd.setCursor(12, 1);
          lcd.print(onoff_x);
          if (StateLed4 == 0) {
            onoff_4 = "OFF";
          } else {
            onoff_4 = "ON ";
          }
          buttonState_OK = digitalRead(button_OK);
          if (buttonState_OK == LOW) {
            k = 0;
            delay(300);
            lcd.clear();
          }
          buttonState_EXIT = digitalRead(button_EXIT);
          if (buttonState_EXIT == LOW) {
            k = 0;
            delay(300);
            lcd.clear();
          }
          buttonState_UP = digitalRead(button_UP);
          if (buttonState_UP == LOW) {
            delay(300); StateLed4++; StateLed4cycle++;
            if (StateLed4 > 1) {
              StateLed4 = 0;
            } if (StateLed4cycle > 1) {
              StateLed4cycle = 0;
            }
          }
          buttonState_DOWN = digitalRead(button_DOWN);
          if (buttonState_DOWN == LOW) {
            delay(300); StateLed4++; StateLed4cycle++;
            if (StateLed4 > 1) {
              StateLed4 = 0;
            } if (StateLed4cycle > 1) {
              StateLed4cycle = 0;
            }
          }
          blink4();
        }
      }
      blink4();
    }
    blink4();
  }
  blink4();
}

void blink4() {
  if (StateLed4cycle == 1) {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= interval) {
      previousMillis = currentMillis;
      if (ledState4 == LOW) {
        ledState4 = HIGH;
      } else {
        ledState4 = LOW;
      }
      digitalWrite(Led4, ledState4);
    }
  } else {
    digitalWrite(Led4, LOW);
  }
}


//Конец скетча

И соберите все, как показано на этой схеме.

Схема Menu I2C LCD 1602 for Arduino Nano

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

Удачных экспериментов!.

Видео «Меню для дисплея LCD 1602 на базе Arduino Nano. Контроль и управление нагрузками.»

Menu I2C LCD 1602 for Arduino Nano

Добавлено 14 апреля 2018 в 17:13

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

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

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

Содержание

Особенности

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

Требования

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

Загрузка

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

Или на github: ссылка.

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

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

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

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

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

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

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

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

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

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

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

// Принимает столбец и строку в качестве позиции и от 1 до 4 ссылок на переменные.
// Эти ссылки на переменные – это то, что необходимо напечатать на дисплее. Они могут быть
// целыми числами, используемыми в программе, строковыми литералами, переданными напрямую,
// или char* для изменяемого текста.
LiquidLine(byte column, byte row, A &variableA...);

// Принимает от 0 до 4 объектов LiquidLine.
LiquidScreen(LiquidLine &liquidLine1...);

// Принимает ссылку на объект LiquidCrystal, от 0 до 4 объектов LiquidScreen и
// номер экрана, который будет показан первым.
LiquidMenu(LiquidCrystal &liquidCrystal, LiquidScreen &liquidScreen1..., byte startingScreen = 1);

// Принимает от 0 до 4 объектов LiquidMenu и номер меню, которое будет показано первым.
LiquidSystem(LiquidMenu &liquidMenu1..., byte startingMenu = 1);

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

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

void LiquidMenu::next_screen();
void LiquidMenu::previous_screen();
bool LiquidMenu::change_screen(LiquidScreen &liquidScreen);

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

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

bool LiquidLine::attach_function(byte number, void (*function)(void));

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

void LiquidMenu::switch_focus(bool forward = true);

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

void LiquidMenu::call_function(byte number);

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

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

Базовый пример

// Сначала нам необходимо создать объект LiquidCrystal.
LiquidCrystal lcd(LCD_RS, LCD_E, LCD_D4, LCD_D5, LCD_D6, LCD_D7);

// ----- Экран приветствия -----
/// Создание строки с одним строковым литералом.
LiquidLine welcome_line1(1, 0, "Hello Menu");

/// Создание строки с целочисленной переменной.
byte oneTwoThree = 123;
LiquidLine welcome_line2(2, 1, oneTwoThree);

/// Формирование экрана из приведенных выше строк.
LiquidScreen welcome_screen(welcome_line1, welcome_line2);
// --------------------------

// ----- Экран 2 -----
LiquidLine some_line(0, 0, "Some line");
LiquidScreen some_screen(some_line);
// --------------------

// Теперь скомпонуем экраны в меню.
LiquidMenu my_menu(lcd, welcome_screen, some_screen);

void setup() {
    lcd.begin(16, 2);
    ...
}

void loop() {
    if (rightButton) {
        my_menu.next_screen();
    }
    if (leftButton) {
        my_menu.previous_screen();
    }
    if (somethingElse) {
        oneTwoThree++;
        my_menu.update;
    }
    ...
}

Описание API

  • Класс LiquidLine – представляет собой отдельные строки, напечатанные на дисплее.
  • Класс LiquidScreen – представляет собой экран, показанный на дисплее.
  • Класс LiquidMenu – представляет собой коллекцию экранов, формирующих меню.
  • Класс LiquidSystem – представляет собой коллекцию меню, формирующих систему меню.

Примеры

  1. hello_menu: как на Arduino с библиотекой LiquidMenu создавать меню из экранов с динамически изменяющейся информацией
  2. serial_menu: как на Arduino с библиотекой LiquidMenu использовать последовательную связь для выполнения команд
  3. functions_menu: как на Arduino с библиотекой LiquidMenu прикреплять функции, срабатывающие по событиям в меню
  4. buttons_menu: как на Arduino с библиотекой LiquidMenu использовать кнопки, функции обратного вызова и переменные, меняющие текст
  5. progmem_menu: как на Arduino с библиотекой LiquidMenu отображать строки, сохраненные во флеш-памяти
  6. focus_menu: как на Arduino с библиотекой LiquidMenu настроить индикатор фокуса
  7. glyph_menu: как на Arduino с библиотекой LiquidMenu создать пользовательский символ и использовать его в меню
  8. system_menu: как на Arduino с библиотекой LiquidMenu построить систему меню
  9. I2C_menu: как на Arduino с библиотекой LiquidMenu построить меню на LCD дисплее, подключенном через шину I2C

Теги

ArduinoLCD дисплейМенюПрограммирование

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

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

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

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

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

const menuStruct PROGMEM points[MENU_points] = {
  {"MENU v 1.2", 0, 0},     //0
  {"point1", 0, 12},        //1
  {"point2", 0, 12},        //2
  {"point3", 1, 2},         //3
  {"point4", 0, 10},        //4
  {"point5", 0, 7},         //5
  {"point6", 0, 10},        //6
};

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

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

Yfpx7IZSx3k.jpg

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

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

menu.mainDisplay("abba", "is a great", "r",
                 'g', 'r', 'o', 'u', 'p', 1234);

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

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

Код прилагаю (тут лежит еще моя библиотека myButton)

  • Возможность удобного описания меню;
  • Возможность построения многоуровнего меню — само собой;
  • Прокрутка длинных пунктов меню;
  • Фишки вроде индикаторов, подсказывающих, что не все пункты уместились на экране или автоотключение подсветки тоже будут кстати.

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

Обратите внимание! После написания данной статьи я создал библиотеку LiquidCrystal_I2C_Menu. В ней вы найдёте функции для построения меню, ввода и выбора значений и многое другое. 

#include <LiquidCrystal_I2C.h> 
#define cols 20
#define rows 4
LiquidCrystal_I2C lcd(0x27, cols, rows);
char *Blank;


#define ShowScrollBar 1     
#define ScrollLongCaptions 1
#define ScrollDelay 800     
#define BacklightDelay 20000
#define ReturnFromMenu 0    

enum eMenuKey {mkNull, mkBack, mkRoot, mkQuad, mkQuadSetA, mkQuadSetB, mkQuadSetC, mkQuadCalc, mkMulti, mkSettings, mkSetMotors,
               mkMotorsAuto, mkMotorsManual, mkSetSensors, mkSetUltrasonic, mkSetLightSensors, mkSetDefaults
              };


#define pin_CLK 2 
#define pin_DT  4 
#define pin_Btn 3 

unsigned long CurrentTime, PrevEncoderTime;
enum eEncoderState {eNone, eLeft, eRight, eButton};
eEncoderState EncoderState;
int EncoderA, EncoderB, EncoderAPrev, counter;
bool ButtonPrev;


eEncoderState GetEncoderState();
void LCDBacklight(byte v = 2);
eMenuKey DrawMenu(eMenuKey Key);


int InputValue(char* Title, int DefaultValue, int MinValue, int MaxValue) {
  
  lcd.clear();
  lcd.print(Title);
  lcd.setCursor(0, 1);
  lcd.print(DefaultValue);
  delay(100);
  while (1)
  {
    EncoderState = GetEncoderState();
    switch (EncoderState) {
      case eNone: {
          LCDBacklight();
          continue;
        }
      case eButton: {
          LCDBacklight(1);
          return DefaultValue;
        }
      case eLeft: {
          LCDBacklight(1);
          if (DefaultValue > MinValue) DefaultValue--;
          break;
        }
      case eRight: {
          LCDBacklight(1);
          if (DefaultValue < MaxValue) DefaultValue++;
          break;
        }
    }
    lcd.setCursor(0, 1);
    lcd.print(Blank);
    lcd.setCursor(0, 1);
    lcd.print(DefaultValue);
  }
};

int A = 2, B = 5, C = -3;
void Demo() {
  lcd.clear();
  lcd.print("It's just a demo");
  while (GetEncoderState() == eNone) LCDBacklight();
};

void InputA() {
  A = InputValue("Input A", A, -10, 10);
  while (A == 0) {
    lcd.clear();
    lcd.print("Shouldn't be 0!");
    lcd.setCursor(0, 1);
    lcd.print("Input another value");
    while (GetEncoderState() == eNone) LCDBacklight();
    A = InputValue("Input A", A, -10, 10);
  }
};

void InputB() {
  B = InputValue("Input B", B, -10, 10);
};

void InputC() {
  C = InputValue("Input C", C, -10, 10);
};

void Solve() {
  int D;
  float X1, X2;
  lcd.clear();
  lcd.print(A);
  lcd.print("X^2");
  if (B >= 0) lcd.print("+");
  lcd.print(B);
  lcd.print("X");
  if (C >= 0) lcd.print("+");
  lcd.print(C);
  lcd.print("=0");
  D = B * B - 4 * A * C;
  lcd.setCursor(0, 1);
  if (rows > 2) {
    lcd.print("D=");
    lcd.print(D);
    lcd.setCursor(0, 2);
  }
  if (D == 0) {
    X1 = -B / 2 * A;
    lcd.print("X1=X2="); lcd.print(X1);
  }
  else if (D > 0) {
    X1 = (-B - sqrt(B * B - 4 * A * C)) / (2 * A);
    X2 = (-B + sqrt(B * B - 4 * A * C)) / (2 * A);
    lcd.print("X1=");  lcd.print(X1);
    lcd.print(";X2="); lcd.print(X2);
  }
  else
    lcd.print("Roots are complex");
  while (GetEncoderState() == eNone) LCDBacklight();
};


byte ScrollUp[8]  = {0x4, 0xa, 0x11, 0x1f};
byte ScrollDown[8]  = {0x0, 0x0, 0x0, 0x0, 0x1f, 0x11, 0xa, 0x4};

byte ItemsOnPage = rows;    
unsigned long BacklightOffTime = 0;
unsigned long ScrollTime = 0;
byte ScrollPos;
byte CaptionMaxLength;

struct sMenuItem {
  eMenuKey  Parent;       
  eMenuKey  Key;          
  char      *Caption;     
  void      (*Handler)(); 
};

sMenuItem Menu[] = {
  {mkNull, mkRoot, "Menu", NULL},
    {mkRoot, mkQuad, "Quadratic Equation Calculator", NULL},
      {mkQuad, mkQuadSetA, "Enter value A", InputA},
      {mkQuad, mkQuadSetB, "Enter value B", InputB},
      {mkQuad, mkQuadSetC, "Enter value C", InputC},
      {mkQuad, mkQuadCalc, "Solve", Solve},
      {mkQuad, mkBack, "Back", NULL},
    {mkRoot, mkMulti, "Multi-level menu example", NULL},
      {mkMulti, mkSettings, "Settings", NULL},
        {mkSettings, mkSetMotors, "Motors", NULL},
          {mkSetMotors, mkMotorsAuto, "Auto calibration", Demo},
          {mkSetMotors, mkMotorsManual, "Manual calibration", Demo},
          {mkSetMotors, mkBack, "Back", NULL},
        {mkSettings, mkSetSensors, "Sensors", NULL},
          {mkSetSensors, mkSetUltrasonic, "Ultrasonic", Demo},
          {mkSetSensors, mkSetLightSensors, "Light sensors", Demo},
          {mkSetSensors, mkBack, "Back", NULL},
        {mkSettings, mkSetDefaults, "Restore defaults", Demo},
        {mkSettings, mkBack, "Back", NULL},
      {mkMulti, mkBack, "Back", NULL}
};

const int MenuLength = sizeof(Menu) / sizeof(Menu[0]);

void LCDBacklight(byte v) { 
  if (v == 0) { 
    BacklightOffTime = millis();
    lcd.noBacklight();
  }
  else if (v == 1) { 
    BacklightOffTime = millis() + BacklightDelay;
    lcd.backlight();
  }
  else { 
    if (BacklightOffTime < millis())
      lcd.noBacklight();
    else
      lcd.backlight();
  }
}

eMenuKey DrawMenu(eMenuKey Key) { 
  eMenuKey Result;
  int k, l, Offset, CursorPos, y;
  sMenuItem **SubMenu = NULL;
  bool NeedRepaint;
  String S;
  l = 0;
  LCDBacklight(1);
  
  for (byte i = 0; i < MenuLength; i++) {
    if (Menu[i].Key == Key) {
      k = i;
    }
    else if (Menu[i].Parent == Key) {
      l++;
      SubMenu = (sMenuItem**) realloc (SubMenu, l * sizeof(void*));
      SubMenu[l - 1] = &Menu[i];
    }
  }

  if (l == 0) { 
    if ((ReturnFromMenu == 0) and (Menu[k].Handler != NULL)) (*Menu[k].Handler)(); 
    LCDBacklight(1);
    return Key; 
  }

  
  CursorPos = 0;
  Offset = 0;
  ScrollPos = 0;
  NeedRepaint = 1;
  do {
    if (NeedRepaint) {
      NeedRepaint = 0;
      lcd.clear();
      y = 0;
      for (int i = Offset; i < min(l, Offset + ItemsOnPage); i++) {
        lcd.setCursor(1, y++);
        lcd.print(String(SubMenu[i]->Caption).substring(0, CaptionMaxLength));
      }
      lcd.setCursor(0, CursorPos);
      lcd.print(">");
      if (ShowScrollBar) {
        if (Offset > 0) {
          lcd.setCursor(cols - 1, 0);
          lcd.write(0);
        }
        if (Offset + ItemsOnPage < l) {
          lcd.setCursor(cols - 1, ItemsOnPage - 1);
          lcd.write(1);
        }
      }
    }
    EncoderState = GetEncoderState();
    switch (EncoderState) {
      case eLeft: {
          
          LCDBacklight(1);
          ScrollTime = millis() + ScrollDelay * 5;
          if (CursorPos > 0) {  
            if ((ScrollLongCaptions) and (ScrollPos)) {
              
              lcd.setCursor(1, CursorPos);
              lcd.print(Blank);
              lcd.setCursor(1, CursorPos);
              lcd.print(String(SubMenu[Offset + CursorPos]->Caption).substring(0, CaptionMaxLength));
              ScrollPos = 0;
            }
            
            lcd.setCursor(0, CursorPos--);
            lcd.print(" ");
            lcd.setCursor(0, CursorPos);
            lcd.print(">");
          }
          else if (Offset > 0) {
            
            Offset--;
            NeedRepaint = 1;
          }
          break;
        }
      case eRight: {
          
          LCDBacklight(1);
          ScrollTime = millis() + ScrollDelay * 5;
          if (CursorPos < min(l, ItemsOnPage) - 1) {
            if ((ScrollLongCaptions) and (ScrollPos)) {
              
              lcd.setCursor(1, CursorPos);
              lcd.print(Blank);
              lcd.setCursor(1, CursorPos);
              lcd.print(String(SubMenu[Offset + CursorPos]->Caption).substring(0, CaptionMaxLength));
              ScrollPos = 0;
            }
            
            lcd.setCursor(0, CursorPos++);
            lcd.print(" ");
            lcd.setCursor(0, CursorPos);
            lcd.print(">");
          }
          else {
            
            if (Offset + CursorPos + 1 < l) {
              Offset++;
              NeedRepaint = 1;
            }
          }
          break;
        }
      case eButton: {
          
          LCDBacklight(1);
          ScrollTime = millis() + ScrollDelay * 5;
          if (SubMenu[CursorPos + Offset]->Key == mkBack) {
            free(SubMenu);
            return mkBack;
          }
          Result = DrawMenu(SubMenu[CursorPos + Offset]->Key);
          if ((Result != mkBack) and (ReturnFromMenu)) {
            free(SubMenu);
            return Result;
          }
          NeedRepaint = 1;
          break;
        }
      case eNone: {
          if (ScrollLongCaptions) {
            
            S = SubMenu[CursorPos + Offset]->Caption;
            if (S.length() > CaptionMaxLength)
            {
              if (ScrollTime < millis())
              {
                ScrollPos++;
                if (ScrollPos == S.length() - CaptionMaxLength)
                  ScrollTime = millis() + ScrollDelay * 2; 
                else if (ScrollPos > S.length() - CaptionMaxLength)
                {
                  ScrollPos = 0;
                  ScrollTime = millis() + ScrollDelay * 5; 
                }
                else
                  ScrollTime = millis() + ScrollDelay;
                lcd.setCursor(1, CursorPos);
                lcd.print(Blank);
                lcd.setCursor(1, CursorPos);
                lcd.print(S.substring(ScrollPos, ScrollPos + CaptionMaxLength));
              }
            }
          }
          LCDBacklight();
        }
    }
  } while (1);
}


void setup() {
  pinMode(pin_CLK, INPUT);
  pinMode(pin_DT,  INPUT);
  pinMode(pin_Btn, INPUT_PULLUP);
  lcd.begin();
  lcd.backlight();
  CaptionMaxLength = cols - 1;
  Blank = (char*) malloc(cols * sizeof(char));
  for (byte i = 0; i < CaptionMaxLength; i++)
    Blank[i] = ' ';
  if (ShowScrollBar) {
    CaptionMaxLength--;
    lcd.createChar(0, ScrollUp);
    lcd.createChar(1, ScrollDown);
  }
  Blank[CaptionMaxLength] = 0;
}

void loop() {
  DrawMenu(mkRoot);
}


eEncoderState GetEncoderState() {
  
  eEncoderState Result = eNone;
  CurrentTime = millis();
  if (CurrentTime >= (PrevEncoderTime + 5)) {
    PrevEncoderTime = CurrentTime;
    if (digitalRead(pin_Btn) == LOW ) {
      if (ButtonPrev) {
        Result = eButton; 
        ButtonPrev = 0;
      }
    }
    else {
      ButtonPrev = 1;
      EncoderA = digitalRead(pin_DT);
      EncoderB = digitalRead(pin_CLK);
      if ((!EncoderA) && (EncoderAPrev)) { 
        if (EncoderB) Result = eRight;     
        else          Result = eLeft;      
      }
      EncoderAPrev = EncoderA; 
    }
  }
  return Result;
}

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

Реакция на выбор определенного пункта меню может быть построена двумя способами:

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

В данном примере я использую обработчики. Кроме упомянутого параметра ReturnFromMenu в скетче есть и другие: ShowScrollBar, ScrollLongCaptions, ScrollDelay, BacklightDelay. Их назначение понятно из названия.

СОДЕРЖАНИЕ ►

  • Меню с энкодером на Arduino LCD 1602
  • Как сделать меню с энкодером на LCD
    • Код управления энкодером меню на дисплее
    • Код переключения меню/подменю энкодером

Рассмотрим, как сделать меню с энкодером Ардуино на дисплее LCD 1602 I2C. Мы представим два примера: меню для включения светодиодов и меню на дисплее с управлением от энкодера яркостью светодиодов. Рекомендуем вам ознакомиться с подключением к Arduino дисплея LCD 1602 Arduino и модуля энкодер Ардуино. Если вы уже подключали данные модули, то можно приступать к этому мини проекту.

Меню на Ардуино LCD 1602 с энкодером

Видео. Меню с энкодером на Ардуино LCD 1602

Примеры меню с модулем энкодера и жк дисплеем, размещенные на этой странице, вы сможете адаптировать под свои нужды. Во многих проектах на Ардуино требуется для пользователя создать возможность для настройки или управления устройством на микроконтроллере. Энкодер позволяет сделать простое и нативное меню на LCD дисплее для управления светодиодами, микро серво Ардуино или для других задач.

Как сделать меню с энкодером на дисплее

Для этого проекта потребуется:

  • Arduino Uno / Arduino Nano / Arduino Mega;
  • модуль энкодера;
  • беспаечная макетная плата;
  • светодиоды и резисторы;
  • дисплей LCD 1602 Arduino;
  • провода «папа-папа», «папа-мама».

Управление энкодером меню дисплея LCD

Управление энкодером меню дисплея LCD

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

Скетч. Управление энкодером меню дисплея LCD

#include "Wire.h"                                 // библиотека для протокола I2C
#include "LiquidCrystal_I2C.h"           // библиотека для дисплея
LiquidCrystal_I2C LCD(0x27, 16, 2);   // присваиваем имя дисплею

#include "RotaryEncoder.h"                // библиотека для энкодера
RotaryEncoder encoder(4, 5);       // пины подключение энкодера (DT, CLK)

// задаем шаг энкодера и макс./мин. значение
#define STEPS  6
#define POSMIN 0
#define POSMAX 12

int lastPos, newPos;
boolean buttonWasUp = true;

void setup() {
   LCD.init();            // инициализация LCD дисплея и подсветки
   LCD.backlight();

   pinMode(9, OUTPUT);              // пины для светодиодов
   pinMode(10, OUTPUT);
   pinMode(11, OUTPUT);
   pinMode(2, INPUT_PULLUP);  // пин для кнопки энкодера

   Serial.begin(9600);
   encoder.setPosition(10 / STEPS);
}

void loop() {
   LCD.setCursor(0, 0);
   LCD.print("LED1  LED2  LED3");

   // проверяем положение ручки энкодера
   encoder.tick();
   newPos = encoder.getPosition() * STEPS;
   if (newPos < POSMIN) { encoder.setPosition(POSMIN / STEPS); newPos = POSMIN; }
   else if (newPos > POSMAX) { encoder.setPosition(POSMAX / STEPS); newPos = POSMAX; }

   // если положение изменилось - выводим на монитор и дисплей
   if (lastPos != newPos) {
      Serial.println(newPos);
      LCD.setCursor(lastPos, 1);
      LCD.print("    ");
      LCD.setCursor(newPos, 1);
      LCD.print("====");
      lastPos = newPos;
   }

   // узнаем состояние кнопки модуля энкодера
   boolean buttonIsUp = digitalRead(2);
   if (buttonWasUp && !buttonIsUp) {
      // исключаем дребезг контактов тактовой кнопки
      delay(10);
      // снова считываем сигнал с кнопки энкодера
      buttonIsUp = digitalRead(2);

      // если кнопка нажата, то включаем соответствующий светодиод
      if (!buttonIsUp  && newPos == 0) {
          digitalWrite(9, HIGH); digitalWrite(10, LOW); digitalWrite(11, LOW);
      }
      if (!buttonIsUp  && newPos == 6) {
          digitalWrite(9, LOW); digitalWrite(10, HIGH); digitalWrite(11, LOW);
      }
      if (!buttonIsUp  && newPos == 12) {
          digitalWrite(9, LOW); digitalWrite(10, LOW); digitalWrite(11, HIGH);
      }
   }

}

Пояснения к коду:

  1. положение курсора на LCD может принимать три значения — 0, 6 или 12;
  2. кнопка энкодера подключена к цифровому порту при помощи конфигурации пина параметром INPUT PULLUP, что позволяет считывать нажатие кнопки.

Скетч. Двухуровневое меню с энкодером Ардуино

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

#include "Wire.h"                                 // библиотека для протокола I2C
#include "LiquidCrystal_I2C.h"           // библиотека для дисплея
LiquidCrystal_I2C LCD(0x27, 16, 2);   // присваиваем имя дисплею

#include "RotaryEncoder.h"                // библиотека для энкодера
RotaryEncoder encoder(4, 5);       // пины подключение энкодера (DT, CLK)

// задаем шаг энкодера и макс./мин. значение в главном меню
#define STEPS  6
#define POSMIN 0
#define POSMAX 12

// задаем шаг энкодера и макс./мин. значение в подменю
#define STEPS_2  10
#define POSMIN_2 0
#define POSMAX_2 250

int lastPos, newPos;
boolean buttonWasUp = true;
byte w = 2;

void setup() {
  LCD.init();            // инициализация LCD дисплея и подсветки
  LCD.backlight();

  pinMode(9, OUTPUT);              // пины для светодиодов
  pinMode(10, OUTPUT);
  pinMode(11, OUTPUT);
  pinMode(2, INPUT_PULLUP);  // пин для кнопки энкодера

  Serial.begin(9600);
  encoder.setPosition(10 / STEPS);
}

void loop() {

   // ЦИКЛ С ГЛАВНЫМ МЕНЮ НА ДИСПЛЕЕ
  while (w == 0) {
    LCD.setCursor(0, 0);
    LCD.print("LED1  LED2  LED3");
    // проверяем положение ручки энкодера
    encoder.tick();
    newPos = encoder.getPosition() * STEPS;
    if (newPos < POSMIN) {
      encoder.setPosition(POSMIN / STEPS);
      newPos = POSMIN;
    }
    else if (newPos > POSMAX) {
      encoder.setPosition(POSMAX / STEPS);
      newPos = POSMAX;
    }
    // если положение изменилось - выводим на монитор и дисплей
    if (lastPos != newPos) {
      Serial.println(newPos);
      LCD.setCursor(lastPos, 1);
      LCD.print("    ");
      LCD.setCursor(newPos, 1);
      LCD.print("====");
      lastPos = newPos;
    }
      // если кнопка нажата, то переходим в цикл с вложенным меню
      // перед входом в подменю очищаем дисплей и делаем задержку
    boolean buttonIsUp = digitalRead(2);
    if (buttonWasUp && !buttonIsUp) {
      delay(10);
      buttonIsUp = digitalRead(2);
      if (!buttonIsUp  && newPos == 0)  { LCD.clear(); delay(500); w = 1; }
      if (!buttonIsUp  && newPos == 6)  { LCD.clear(); delay(500); w = 2; }
      if (!buttonIsUp  && newPos == 12) { LCD.clear(); delay(500); w = 3; }
    }
  }

   // ВЛОЖЕННОЕ МЕНЮ-1 С ПЕРВЫМ СВЕТОДИОДОМ
  while (w == 1) {
    LCD.setCursor(0, 0);
    LCD.print("LED1  - ");
    // проверяем положение ручки энкодера
    encoder.tick();
    newPos = encoder.getPosition() * STEPS_2;
    if (newPos < POSMIN_2) {
      encoder.setPosition(POSMIN_2 / STEPS_2);
      newPos = POSMIN_2;
    }
    else if (newPos > POSMAX_2) {
      encoder.setPosition(POSMAX_2 / STEPS_2);
      newPos = POSMAX_2;
    }
    // если положение изменилось - меняем яркость светодиода
    if (lastPos != newPos) {
      LCD.setCursor(7, 0);
      LCD.print(newPos);
      analogWrite(9, newPos);
    }
    // если кнопка энкодера была нажата, то выходим в главное меню
    // перед входом очищаем дисплей и выключаем светодиод
    boolean buttonIsUp = digitalRead(2);
    if (buttonWasUp && !buttonIsUp) {
      delay(10);
      buttonIsUp = digitalRead(2);
      if (!buttonIsUp)  { LCD.clear(); digitalWrite(9, LOW); delay(500); w = 0; }
  } 
}

   // ВЛОЖЕННОЕ МЕНЮ-2 СО ВТОРЫМ СВЕТОДИОДОМ
  while (w == 2) {
    LCD.setCursor(0, 0);
    LCD.print("LED2 - ");
    // проверяем положение ручки энкодера
    encoder.tick();
    newPos = encoder.getPosition() * STEPS_2;
    if (newPos < POSMIN_2) {
      encoder.setPosition(POSMIN_2 / STEPS_2);
      newPos = POSMIN_2;
    }
    else if (newPos > POSMAX_2) {
      encoder.setPosition(POSMAX_2 / STEPS_2);
      newPos = POSMAX_2;
    }
    // если положение изменилось - меняем яркость светодиода
    if (lastPos != newPos) {
      LCD.setCursor(7, 0);
      LCD.print(newPos);
      analogWrite(10, newPos);
    }
    // если кнопка энкодера была нажата, то выходим в главное меню
    // перед входом очищаем дисплей и выключаем светодиод
    boolean buttonIsUp = digitalRead(2);
    if (buttonWasUp && !buttonIsUp) {
      delay(10);
      buttonIsUp = digitalRead(2);
      if (!buttonIsUp)  { LCD.clear(); digitalWrite(10, LOW); delay(500); w = 0; }
  }
}

   // ВЛОЖЕННОЕ МЕНЮ-3 С ТРЕТЬИМ СВЕТОДИОДОМ
  while (w == 3) {
    LCD.setCursor(0, 0);
    LCD.print("LED3 - ");
    // проверяем положение ручки энкодера
    encoder.tick();
    newPos = encoder.getPosition() * STEPS_2;
    if (newPos < POSMIN_2) {
      encoder.setPosition(POSMIN_2 / STEPS_2);
      newPos = POSMIN_2;
    }
    else if (newPos > POSMAX_2) {
      encoder.setPosition(POSMAX_2 / STEPS_2);
      newPos = POSMAX_2;
    }
    // если положение изменилось - меняем яркость светодиода
    if (lastPos != newPos) {
      LCD.setCursor(7, 0);
      LCD.print(newPos);
      analogWrite(11, newPos);
    }
    // если кнопка энкодера была нажата, то выходим в главное меню
    // перед входом очищаем дисплей и выключаем светодиод
    boolean buttonIsUp = digitalRead(2);
    if (buttonWasUp && !buttonIsUp) {
      delay(10);
      buttonIsUp = digitalRead(2);
      if (!buttonIsUp)  { LCD.clear(); digitalWrite(11, LOW); delay(500); w = 0; }
  } 
}

}

Пояснения к коду:

  1. для каждого раздела меню используется цикл while, так как изначально глобальная переменная w=0;, то выполняется цикл while (w == 0);
  2. если вы не хотите, чтобы светодиоды выключались при выходе в главное меню — удалите команду digitalWrite() во всех циклах вложенных меню.

Заключение. Мы рассмотрели два примера создания меню с энкодером на дисплее 1602 IIC Arduino. Если вы уловили всю суть управления меню с помощью энкодера (датчик угла поворота), то легко сможете использовать представленные примеры в своих собственных проектах с дисплеем на Ардуино. Если у вас еще остались вопросы по данной теме, то вы можете их оставлять в комментариях к этой записи.

Понравилась статья? Поделить с друзьями:
  • Как написать менеджеру каспи
  • Как написать мемуары план
  • Как написать листочек или листочик
  • Как написать куплю продажу автомобиля от руки образец 2022 года
  • Как написать кундигунг на