Панель оператора (HMI) с шиной I2C для Arduino

в 6:08, , рубрики: arduino, arduino nano, controllino, diy или сделай сам, FC-113, hd44780, I2C, lcd 1602, PCF8574, многоуровневое меню

В рамках работы с неким ардуино-совместимым оборудованием(о нем в конце) понадобился мне экран с кнопками для управления и отображения текущей информации. То есть, была нужна панель оператора, она же HMI.

Решено было сделать HMI самостоятельно, а в качестве интерфейса использовать «квадратную» шину i2c.
Панель оператора (HMI) с шиной I2C для Arduino - 1

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

Характеристики:

  • Дисплей 1602, монохромный 16х2 символов
  • 5 кнопок: вверх, вниз, отмена, ввод, редактирование(edit)
  • Интерфейс i2c
  • Разъем подключения DB9F
  • Размеры 155х90х44 мм

Тут возникнут очевидные вопросы:

Почему не купить готовый шилд?

Конечно, можно было у тех же китайцев купить готовый шилд c дисплеем и клавиатурой и типа такого:
Панель оператора (HMI) с шиной I2C для Arduino - 2
К этому шилду можно припаять 2 платки FC-113 и получится функционально то же самое, что и у меня: дисплей с клавиатурой, работающие по i2c. Цена набора составит от 4$.

Но на этой плате меня не устраивает размер кнопок, а мне хотелось большие, с возможностью установки разноцветных колпачков. Подключать Arduino к HMI мне хотелось не на соплях, а через нормальный разъем DB9F, а значит нужно было делать соединительную плату. А в этом случае какая разница, делать одну плату или две? Кроме того, у меня уже было в запасе несколько дисплеев 1602, а потому мне нужно было потратить всего 1.02$ для покупки на Алиэкспресс платы FC-113 (0.55$) и расширителя портов PCF8574P (0.47$).

Ну а самое главное- если имеешь дело с Ардуино, то самостоятельное изготовление шилдов для него это само собой разумеющееся дело, правда ведь?

Почему шина i2c, не проще ли кнопки подключить напрямую?
В сфере АСУ ТП, где я работаю, HMI для связи с устройствами используют интерфейсы цифровой передачи данных RS-232,RS-485, CAN и т.д. Поэтому для меня логично, что моя самодельная HMI будет вся работать по интерфейсу передачи данных, в данном случае по i2c.

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

Кроме того, различие между платой кнопок, которые идут напрямую ко входам Ардуино, и платой кнопок с интерфейсом i2c, заключается только в микросхеме PCF8574P(0.47$), конденсаторе и двух резисторах.

Почему кнопки расположены так, а не иначе?

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

Всего кнопок 5, хотя микросхема на плате клавиатуры позволяет подключить до 8 штук.
Достаточно было бы обойтись четырьмя кнопками и функционал бы не пострадал- «ввод» и «редактирование» можно совместить в одной кнопке. Но мне просто жалко стало, что из 8 ног микросхемы расширителя порта половина будет не задействована.
Еще отдельная кнопка «редактирование» может быть полезна, если я решу в одной строке выводить несколько параметров. Тогда этой кнопкой можно будет переключаться между параметрами, указывая, какой именно из них нужно изменить. Примерно так работает кнопка «SET» в популярных китайских HMI OP320.

Если первые две кнопки означают вверх и вниз, то почему бы их не разместить вертикально, как, например, сделано в указанном выше китайском шилде?

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

Железо

Панель оператора (HMI) с шиной I2C для Arduino - 3

Панель оператора (HMI) с шиной I2C для Arduino - 4

1. Самодельная соединительная плата с разъемом DB9F. Так, как питание +5V для расширителей портов и дисплея берем с Ардуино, на плате поставил предохранитель 0.1 А.

2. Всем нам хорошо известный дисплей 1602 с припаянной платой FC-113, которая подключает дисплей к шине i2c.

3. Самодельная клавиатурная плата с микросхемой PCF8574P, которая будет читать состояния кнопок и передавать их по шине i2c. Кстати, «дисплейная» плата FC-113 тоже основана на микросхеме PCF8574, только с индексом T, т.е. планарная, а не DIP, как PCF8574P.
Кнопки я поставил 12х12мм с квадратным толкателем- на них можно надеть большие разноцветные колпачки.

Фото и схемы самодельный плат

Панель оператора (HMI) с шиной I2C для Arduino - 5

Панель оператора (HMI) с шиной I2C для Arduino - 6

Панель оператора (HMI) с шиной I2C для Arduino - 7

Стоит сказать пару слов про микросхему PCF8574P, на основе которой я сделал клавиатурную плату.
PCF8574P это расширитель портов с интерфейсом i2c. Всего в нем 8 портов, каждый из которых можно сконфигурировать на работу в качестве входа или выхода. Для этой микросхемы и обвязки как таковой не требуется(вспомните, к примеру, max232), я только на всякий случай поставил конденсатор по питанию.
Адрес микросхемы PCF8574P задается с помощью адресных ног A0, A1, A2, которые подтягивают к земле или к питанию через резистор 10 кОм.
На клавиатурной плате я все адресные ноги PCF8574P поставил на землю, поэтому адрес жестко настроен как 0x20 и поменять его нельзя.

Как я уже писал, в качестве разъема для HMI я выбрал DB9F. На него от Ардуино поступают сигналы +5 V, GND, SDA, SCL.
Панель оператора (HMI) с шиной I2C для Arduino - 8

Провод для связи по i2c Ардуино и HMI сделал длинной 1.4 м, работает без глюков.

Платы нарисовал в Sprint Layout 6, методом ЛУТ перенес на текстолит и вытравил в растворе перекиси и лимонной кислоты.

Немного о травлении

В сети есть много рецептов травления лимонной кислотой плат на фольгированном стеклотекстолите.
Я делал такой раствор: 100 мл перекиси водорода 3%, 50 г лимонной кислоты, 3 чайные ложки соли. Баночку с перекисью подогрел в кастрюле с водой до температуры где-то 70 градусов.

Погружаем плату в раствор рисунком вниз, как рекомендуют при травлении перекисью.
Через пару десятков секунд начинается бурный процесс. Выделяется много пара, вдыхать который не рекомендуется. Наверное.
Панель оператора (HMI) с шиной I2C для Arduino - 9

Потом процесс стихает. Переворачиваем плату.
Панель оператора (HMI) с шиной I2C для Arduino - 10

Готово.
Панель оператора (HMI) с шиной I2C для Arduino - 11

Корпус сделал у друга из оргстекла 4 мм на станке лазерной резки.

Лирическое отступление по поводу корпуса

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

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

Пользуясь случаем, хочу сказать, спасибо тебе, Коля! Иначе мне пришлось бы еще целый день пилить и шлифовать фанеру, а результат едва бы был таким блистательным.

Программирование

С точки зрения Ардуино, данная HMI представляет из себя 2 устройства, которые работают по шине i2c: дисплей(LCD) с адресом 0x27 и клавиатура с адресом 0x20. Соответственно, работать Arduino будет отдельно с клавиатурой и отдельно с LCD.

Работа с LCD осуществляется через специальную библиотеку «LiquidCrystal_I2C.h», ее нужно установить в Aduino IDE.
Работа с клавиатурой осуществляется через стандартную библиотеку «Wire.h», которая изначально имеется в Aduino IDE.

Подключаем HMI к Ardiuno.
Панель оператора (HMI) с шиной I2C для Arduino - 12

1. Для начала проверим, видит ли Ардуино наш HMI. Для этого загружаем в нее программу, которая будет сканировать шину i2c на предмет нахождения на ней устройств.

Скетч 1, сканирование шины i2c

//i2c_scaner
#include <Wire.h>
String stringOne;
void setup()
{
  Wire.begin();
  Serial.begin(9600);
  while (!Serial);         
}
 
void loop()
{
  byte error, address;
  int nDevices;
  Serial.println("Scanning...");
  nDevices = 0;
  for(address = 1; address < 127; address++ ) 
  {
    Wire.beginTransmission(address);
    error = Wire.endTransmission();
 
if (error == 0)
  {
  String stringOne =  String(address, HEX);
  Serial.print("0x");     Serial.print(stringOne); Serial.print(" - ");
    if(stringOne=="0A") Serial.println("'Motor Driver'");
    if(stringOne=="0F") Serial.println("'Motor Driver'");
    if(stringOne=="1D") Serial.println("'ADXL345 Input 3-Axis Digital Accelerometer'");
    if(stringOne=="1E") Serial.println("'HMC5883 3-Axis Digital Compass'");
    if(stringOne=="5A") Serial.println("'Touch Sensor'");
    if(stringOne=="5B") Serial.println("'Touch Sensor'");
    if(stringOne=="5C") Serial.println("'BH1750FVI digital Light Sensor' OR 'Touch Sensor"  );
    if(stringOne=="5D") Serial.println("'Touch Sensor'");
    if(stringOne=="20") Serial.println("'PCF8574 8-Bit I/O Expander' OR 'LCM1602 LCD Adapter' ");   
    if(stringOne=="21") Serial.println("'PCF8574 8-Bit I/O Expander'");
    if(stringOne=="22") Serial.println("'PCF8574 8-Bit I/O Expander'");
    if(stringOne=="23") Serial.println("'PCF8574 8-Bit I/O Expander' OR 'BH1750FVI digital Light Sensor'");
    if(stringOne=="24") Serial.println("'PCF8574 8-Bit I/O Expander'");
    if(stringOne=="25") Serial.println("'PCF8574 8-Bit I/O Expander'");
    if(stringOne=="26") Serial.println("'PCF8574 8-Bit I/O Expander'");
    if(stringOne=="27") Serial.println("'PCF8574 8-Bit I/O Expander' OR 'LCM1602 LCD Adapter '");   
    if(stringOne=="39") Serial.println("'TSL2561 Ambient Light Sensor'");    
    if(stringOne=="40") Serial.println("'BMP180 barometric pressure sensor'"    ); 
    if(stringOne=="48") Serial.println("'ADS1115 Module 16-Bit'");
    if(stringOne=="49") Serial.println("'ADS1115 Module 16-Bit' OR 'SPI-to-UART'");
    if(stringOne=="4A") Serial.println("'ADS1115 Module 16-Bit'");
    if(stringOne=="4B") Serial.println("'ADS1115 Module 16-Bit'");
    if(stringOne=="50") Serial.println("'AT24C32 EEPROM'"); 
    if(stringOne=="53") Serial.println("'ADXL345 Input 3-Axis Digital Accelerometer'");
    if(stringOne=="68") Serial.println("'DS3231 real-time clock' OR 'MPU-9250 Nine axis sensor module'");
    if(stringOne=="7A") Serial.println("'LCD OLED 128x64'");
    if(stringOne=="76") Serial.println("'BMP280 barometric pressure sensor'");
    if(stringOne=="77") Serial.println("'BMP180 barometric pressure sensor' OR 'BMP280 barometric pressure sensor'");
    if(stringOne=="78") Serial.println("'LCD OLED 128x64'" );
   nDevices++;
  }
    else if (error==4) 
    {
      Serial.print("Unknow error at address 0x");
      if (address<16) 
        Serial.print("0");
      Serial.println(address,HEX);
    }    
  }
  if (nDevices == 0)
    Serial.println("No I2C devices foundn");
  else
    Serial.println("donen");
 
  delay(5000);          
}

Во время выполнения этой программы, Ардуино будет писать результаты сканирования шины i2c в последовательный порт. Для просмотра этих данных, в Arduino IDE заходим Инструменты-> Монитор порта.

Панель оператора (HMI) с шиной I2C для Arduino - 13

Видим, что Ардуино на шине i2c определило два устройства с адресами 0x20 и 0x27, это клавиатура и LCD соответственно.

2. Теперь посмотрим, как работает наша клавиатура. Создадим программу, которая будет опрашивать состояние кнопок и выводить его на LCD.

Скетч 2, вывод на экран состояния кнопок

/*
Вывод на LCD состояния кнопок по шине i2c
LCD подключен через плату FC-113, адрес 0x27 
Клавиатура подключена через расширитель портов PCF8574P, адрес 0x20
*/


#include <LiquidCrystal_I2C.h>
#include <Wire.h>

#define   led   13
#define   ADDR_KBRD  0x20
#define   ADDR_LCD   0x27

byte dio_in;
bool b;
bool key[5];

LiquidCrystal_I2C lcd(ADDR_LCD,16,2);  // Устанавливаем дисплей

void setup()
{
pinMode(led, OUTPUT);
//
  lcd.init();                     
  lcd.backlight();// Включаем подсветку дисплея
 
  //
  Wire.begin();

  Wire.beginTransmission(ADDR_KBRD);
  Wire.write(B11111111); //Конфигурация всех порты PCF8574P на клавиатуре как входа
  Wire.endTransmission();
     
}
 
void loop()
{
   
  Wire.requestFrom(ADDR_KBRD,1);
  while (!Wire.available());
  
  byte dio_in = Wire.read();  //читаем состояние портов PCF8574P(кнопок)

  //заполняем массив кнопок значениями их состояний
  byte mask=1;
  for(int i=0; i<5;i++)
  {
    key[i]=!(dio_in & mask);
    mask=mask<<1;
   }

  b=!b;
  digitalWrite(led, b); //Мигаем светодиодом на Ардуино

  //Вывод состояний кнопок на LCD
  lcd.setCursor(0, 0);
  lcd.print(String(key[0])+" "+
            String(key[1])+" "+
            String(key[2])+" "+
            String(key[3])+" "+
            String(key[4])+" ");
   
  
  delay(100);          
}

Панель оператора (HMI) с шиной I2C для Arduino - 14

Клавиатура работает.

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

Панель оператора (HMI) с шиной I2C для Arduino - 15

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

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

Параметры можно изменять нажатием кнопки «Edit». Причем, в теге каждого параметра указывается, доступен ли он для редактирования или только для чтения. Если текущий параметр только для чтения, в начале строки указатель будет '*', если редактирование параметра разрешено, указатель станет '+'.

Скетч 3, многоуровневое меню

/*
Древовидное меню, работа снопками и LCD по шине i2c
LCD подключен через плату FC-113, адрес 0x27 
Клавиатура подключена через расширитель портов PCF8574P, адрес 0x20
*/

#include <LiquidCrystal_I2C.h>
#include <Wire.h>

#define   led   13     //светодиод на плате Ардуно нано; будет мигать, показывая этим, что система не зависла 
#define   ADDR_KBRD  0x20   
#define   ADDR_LCD   0x27

#define   PORT_D2    2 
#define   PORT_D3    3
#define   PORT_D4    4

#define    POINT_ON_ROOT_MENU_ITEM   0   // 0/1= запретить/разрешить вывод указателя позиции(* или +) на главном экране меню

byte dio_in;
bool b;
byte i;

//bool переменные, которыми можно управлять из меню
bool  BoolVal[9]={0,0,0, 0,0,0, 0,0,0};   

#define  ValSvet1 BoolVal[0]
#define  ValSvet2 BoolVal[1]
#define  ValSvet3 BoolVal[2]

#define  ValRozetka1 BoolVal[3]
#define  ValRozetka2 BoolVal[4]
#define  ValRozetka3 BoolVal[5]

#define  ValClapan1 BoolVal[6]
#define  ValClapan2 BoolVal[7]
#define  ValClapan3 BoolVal[8]

//
struct STRUCT_KEY{
  bool StateCur;  //Текущее состояние кнопки  
  bool StateOld;  //Состояние кнопки при прошлом опросе
  bool Imp;       //Было нажатие кнопки (переход из 0 в 1)
  };

//кнопки
STRUCT_KEY Key[5]={0,0,0,
            0,0,0,
            0,0,0,
            0,0,0,
            0,0,0
           }; 
//---

/*Текстовые строки меню
 * Допустимы теги, например:
 * '#A1'  bool переменная, где 
 * '#'- тип переменной bool, 
 * 'A'- адрес(HEX) переменной в массиве BoolVal, 
 * '1'- редактирование переменной разрешено
 * при выводе текста, вместо тега автоматически подставляется значение переменной
 */
 
String StrNull=" ";      //пустая строка

String StrRoot1="COMP-MAN.INFO";    
String StrRoot2="PLC-BLOG.COM.UA";

String StrSvet= "СВЕТ";      //Свет
 String StrSvet1="СВЕТ 1   #01";    
 String StrSvet2="СВЕТ 2   #10";    
 String StrSvet3="СВЕТ 3   #21";    

String StrRozetka="РОЗЕТКИ";    //Розетки
 String StrRozetka1="РОЗЕТКА 1  #30";
 String StrRozetka2="РОЗЕТКА 2  #40";
 String StrRozetka3="РОЗЕТКА 3  #50";

String StrClapan="КЛАПАНЫ";       //Клапаны
 String StrClapan1="КЛАПАН 1  #60";    //
 String StrClapan2="КЛАПАН 2  #70";
 String StrClapan3="КЛАПАН 3  #80";

struct MENU_ITEM      //Пункт меню(экран), состоит из 2 строк и координат перехода при нажатии кнопок
  {   
    byte KeyUp;       //№ пункта меню, куда переходить по кнопке "вверх"
    byte KeyDwn;      //№ пункта меню, куда переходить по кнопке "вниз"
    byte KeyCancel;   //№ пункта меню, куда переходить по кнопке "отмена"(cancel)
    byte KeyEnter;    //№ пункта меню, куда переходить по кнопке "ввод"(enter)
    byte KeyEdit;     //кнопка "edit", резерв
  
    String *pstr1;    //указатель на верхнюю строку меню(экрана)
    String *pstr2;    //указатель на нижнюю строку меню(экрана)
  };

//
MENU_ITEM  Menu[]={0,0,0,1,0,  &StrRoot1,&StrRoot2,               //0  Главный экран
                     1,8,0,2,0, &StrSvet,&StrRozetka,             //1  СВЕТ
                       2,3,1,2,0, &StrSvet1,&StrSvet2,              //2
                       2,4,1,3,0, &StrSvet2,&StrSvet3,              //3
                       3,4,1,4,0, &StrSvet3,&StrNull,               //4
                        0,0,0,0,0, &StrNull,&StrNull,                //5  РЕЗЕРВ
                        0,0,0,0,0, &StrNull,&StrNull,                //6
                        0,0,0,0,0, &StrNull,&StrNull,                //7
                     1,15,0,9,0, &StrRozetka,&StrClapan,          //8   РОЗЕТКИ
                       9,10,8,9,0,  &StrRozetka1, &StrRozetka2,     //9 
                       9,11,8,10,0, &StrRozetka2, &StrRozetka3,     //10
                       10,11,8,11,0, &StrRozetka3, &StrNull,        //11                        
                        0,0,0,0,0, &StrNull,&StrNull,                //12 РЕЗЕРВ
                        0,0,0,0,0, &StrNull,&StrNull,                //13
                        0,0,0,0,0, &StrNull,&StrNull,                //14
                     8,15,0,16,0,  &StrClapan, &StrNull,          //15   КЛАПАНЫ
                       16,17,15,0,0,  &StrClapan1,&StrClapan2,      //16
                       16,18,15,0,0,  &StrClapan2,&StrClapan3,      //17
                       17,18,15,0,0,  &StrClapan3,&StrNull,         //18
                        0,0,0,0,0, &StrNull,&StrNull,                //19  РЕЗЕРВ
                        0,0,0,0,0, &StrNull,&StrNull,                //20
                        0,0,0,0,0, &StrNull,&StrNull,                //21
                         
                    };

byte PosMenu=0;   //позиция меню
          
LiquidCrystal_I2C lcd(ADDR_LCD,16,2);  // Устанавливаем дисплей

//Чтение состояний кнопок
void ReadKey(byte dio_in)
{
  //заполняем массив кнопок значениями их состояний
  byte mask=1;
  for(i=0; i<5; i++)
  {
    Key[i].StateCur=!(dio_in & mask);
    mask=mask<<1;

    Key[i].Imp=!Key[i].StateOld & Key[i].StateCur;    //определяем нажатие кнопки (переход из 0 в 1)
     
    Key[i].StateOld=Key[i].StateCur; 
   }  
}

/*  
 *  Перекодировка UTF-8 русских букв (только заглавных) в коды LCD
 * а то Ардуино выводит их неправильно
 */
byte  MasRus[33][2]= {
                      144,  0x41,   //А
                      145,  0xa0,
                      146,  0x42,
                      147,  0xa1,
                      
                      148,  0xe0,
                      149,  0x45,
                      129,  0xa2,
                      150,  0xa3,
                      
                      151,  0xa4,
                      152,  0xa5,
                      153,  0xa6,
                      154,  0x4b,

                      155,  0xa7,
                      156,  0x4d,
                      157,  0x48,
                      158,  0x4f,
                      
                      159,  0xa8,
                      160,  0x50,
                      161,  0x43,
                      162,  0x54,
                      
                      163,  0xa9,
                      164,  0xaa,
                      165,  0x58,
                      166,  0xe1,
                      
                      167,  0xab,
                      168,  0xac,
                      169,  0xe2,
                      170,  0xad,
                      
                      171,  0xae,
                      172,  0xc4,
                      173,  0xaf,
                      174,  0xb0,
                      
                      175,  0xb1    //Я
  };

String RusStrLCD(String StrIn)
{
  String StrOut="";
  byte b1;
  byte y;

  byte l=StrIn.length();

  for(byte i=0; i<l; i++)
   {
    b1=StrIn.charAt(i);
    if (b1<128)
     StrOut=StrOut+char(b1);
    else 
    {
      if (b1==208)      //байт==208, это первый байт из 2-байтного кода рус. буквы
      {
        b1=StrIn.charAt(i+1);  
        for(y=0; y<33; y++)
         if(MasRus[y][0]==b1)
          {
            StrOut=StrOut+char(MasRus[y][1]);  
            break;
          } 
        }
       i++;          
     }  
   }
  return StrOut;
}
//--------------------------- 

//ASCII HEX ---> dec
byte  StrHexToByte(char val)
{
    byte dec=0;
    switch (val) {
    case '0':
      dec=0;
      break;
    case '1':
      dec=1;
      break;
    case '2':
      dec=2;
      break;    
    case '3':
      dec=3;
      break;
    case '4':
      dec=4;
      break;
    case '5':
      dec=5;
      break;
    case '6':
      dec=6;
      break;
    case '7':
      dec=7;
      break;
    case '8':
      dec=8;
      break;
    case '9':
      dec=9;
      break;
    case 'A':
      dec=10;
      break;
    case 'B':
      dec=11;
      break;
    case 'C':
      dec=12;
      break;
    case 'D':
      dec=13;
      break;
    case 'E':
      dec=14;
      break;
    case 'F':
      dec=15;
      break;
          
    default: 
    dec=0;
    break;
  }
return dec;
}

//Вывод на экран пункта меню
void WriteLCD(byte num)
{
   String str[]={"*"+*Menu[num].pstr1,*Menu[num].pstr2};
  if (num==0 && POINT_ON_ROOT_MENU_ITEM==0)  //на главном эркане нужно выводить указатель?
   str[0].setCharAt(0,' ');                  //стираем указатель, если нет 
   
  //Подставляем значения переменных вместо тегов
  byte NumVal;
  byte l;
  for(byte y=0; y<2; y++)
  {
  l=str[y].length();
  for(i=0; i<l; i++)
  {
   if (str[y].charAt(i)=='#')  //# bool, состояния off/ON     
    {
      if(StrHexToByte(str[y].charAt(i+2))==1 && y==0)   //редактирование параметра разрешено?
        str[y].setCharAt(0,'+'); 
  
      NumVal=StrHexToByte(str[y].charAt(i+1));
      str[y]=str[y].substring(0,i)+String(NumVal) ; 
      
      if(BoolVal[NumVal]==0)
        str[y]=str[y].substring(0,i)+"off" ; 
      if(BoolVal[NumVal]==1)
        str[y]=str[y].substring(0,i)+"ON" ; 
    }
    
   if (str[y].charAt(i)=='$') //$ int, делается по тому же принципу, но мне пока не надо
    {
      ;  
    }      
    
   if (str[y].charAt(i)=='~') //~ время, делается по тому же принципу, но мне пока не надо
    {
      ;  
    }      
  }  
  } 
  //---
  
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(str[0]);  
  lcd.setCursor(1, 1);
  lcd.print(str[1]);   
}

//Определяем, на какой пункт меню нужно перейти
byte GoMenu(byte key)
{

  byte PosMenuNew=PosMenu;
  
  switch (key) {
    case 0:
      PosMenuNew=Menu[PosMenu].KeyUp;
      break;
    case 1:
      PosMenuNew=Menu[PosMenu].KeyDwn;
      break;
    case 2:
      PosMenuNew=Menu[PosMenu].KeyCancel;
      break;
    case 3:
      PosMenuNew=Menu[PosMenu].KeyEnter;
      break;
    case 4:
      ;
      break;      
    default: 
    break;
  }
  return PosMenuNew; 
}

//действия по нажатию кнопки "Edit"
void  Edit(byte posmenu)
{
  byte NumVal;
  bool  *pval;
  String str=*Menu[posmenu].pstr1;
  
  byte l=str.length();

  for(i=0; i<l; i++)
   if (str.charAt(i)=='#')       //#- bool, состояние off/ON
    {
       if(StrHexToByte(str.charAt(i+2))==1)   //редактирование параметра разрешено?
       {   
          pval= &(BoolVal[StrHexToByte(str.charAt(i+1))]);    //находим параметр, который привязан к тек. пункту меню
          *pval=!(*pval);                     //меняем значение параметра на противоположное
       }   
    }
}

//Вывод данные в порты Ардуино
void ValToPort()
{
 digitalWrite(PORT_D2,ValSvet1);
 digitalWrite(PORT_D3,ValSvet2);
 digitalWrite(PORT_D4,ValSvet3);
}

void setup()
{
pinMode(led, OUTPUT); //светодиод на плате Ардуино нано

pinMode(PORT_D2, OUTPUT);  
pinMode(PORT_D3, OUTPUT);  
pinMode(PORT_D4, OUTPUT); 

//Перекодируем русские тексты для LCD
StrSvet=RusStrLCD(StrSvet);
 StrSvet1=RusStrLCD(StrSvet1);
 StrSvet2=RusStrLCD(StrSvet2);
 StrSvet3=RusStrLCD(StrSvet3);

StrRozetka=RusStrLCD(StrRozetka);
 StrRozetka1=RusStrLCD(StrRozetka1);
 StrRozetka2=RusStrLCD(StrRozetka2);
 StrRozetka3=RusStrLCD(StrRozetka3);
 
StrClapan=RusStrLCD(StrClapan);
 StrClapan1=RusStrLCD(StrClapan1);
 StrClapan2=RusStrLCD(StrClapan2);
 StrClapan3=RusStrLCD(StrClapan3);
 
//
  lcd.init();                     
  lcd.backlight();// Включаем подсветку дисплея
  WriteLCD(PosMenu);

  Wire.begin();

  Wire.beginTransmission(ADDR_KBRD);
  Wire.write(B11111111); //Конфигурация всех порты PCF8574P на клавиатуре как входа
  Wire.endTransmission();
}
 
void loop()
{
   
  Wire.requestFrom(ADDR_KBRD,1);
  while (!Wire.available());
  
  byte dio_in = Wire.read();  //читаем состояние портов PCF8574P(кнопок)

  ReadKey(dio_in);  //определяем состояния кнопок

  //проверяем, было ли нажатие кнопки; если да, ставим флаг у соответствующей кнопки
  int KeyImp=-1;
  for (i=0; i<5; i++)
   if(Key[i].Imp==1)
    {
      KeyImp=i;
      Key[i].Imp==0;
    }

  if (KeyImp>-1)  //так было нажатие?
  {
   if (KeyImp==4) //Кнопка "Edit"
    Edit(PosMenu);
    
   PosMenu=GoMenu((KeyImp));  
   WriteLCD(PosMenu);
  }

  b=!b;
  digitalWrite(led, b); //Мигаем светодиодом на Ардуино

  ValToPort();   //управление выходами
  
  delay(50);          
}

LCD 1602 и языковой вопрос

Отдельно нужно затронуть вопрос русификации.
В знакогенераторе некоторых LCD 1602 нет русских букв, а вместо них прошиты японские кракозябры. Перепрошить знакогенератор невозможно. Поэтому придется или писать на экране слова латинскими буквами, или в программе формировать русские буквы самому, т.к. в LCD 1602 есть возможность создавать и хранить в ОЗУ LCD собственные символы. Но, в последнем случае, можно выводить на экран не больше восьми «самодельных» символов за раз.

Таблицы символов LCD 1602

Панель оператора (HMI) с шиной I2C для Arduino - 16

Панель оператора (HMI) с шиной I2C для Arduino - 17

В принципе, нет ничего страшного, если писать на LCD русские слова английскими буквами. Вон, даже почтенная французская компания Shneider Electric(та самая, что еще до революции продавала гаубицы царю) за полтора десятилетия не сподобилась внедрить в свои знаменитые программируемые реле Zelio русский язык. Но это не мешает активно торговать ими на просторах всего СНГ. Причем, канальи, испанский и португальский языки ввели.
На многих наших заводах эти Zelio общаются с персоналом фразами типа «NASOS 1 VKL».

Когда непонятно, есть ли русские буквы в конкретном LCD, нужно вывести на экран все символы его знакогенератора. Если кириллица есть, она начинается со 160 позиции.

Скетч 4, вывод на экран всех символов из таблицы знакогенератора LCD 1602

/*Последовательно выводит на LCD все символы его знакогенератора
 * LCD подключен по шине i2c
 */

#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27,16,2);  // Устанавливаем дисплей

void setup() {
  // put your setup code here, to run once: 
  lcd.init();
  lcd.clear();
}
 
void loop() {
int i,y;

while(1)
{
  for (i=0; i < 16; i++) 
  {   
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print(String(i*16)+" - "+String(i*16+15));
      lcd.setCursor(0,1);      
      for(y=0;y<16;y++)
       lcd.print(char(i*16+y));
      delay(3000); 
   }
}
}

Но даже если ваш LCD 1602 русифицирован, вывести на экран русские слова не так просто. По крайней мере, используя библиотеку «LiquidCrystal_I2C.h» при работе с LCD по шине i2c.
Если просто выводить русский текст, например инструкцией lcd.print(«Привет!!!»), то вместо «Привет!!!» на экране появится какая-то белиберда.
Это потому, что русские буквы Arduino IDE переводит в двухбайтный код UTF-8, а в LCD все символы однобайтные.

Та же проблема, кстати, наблюдается при передаче русских текстов из Ардуино в монитор порта Arduino IDE. Ардуино передает в последовательный порт русские буквы в двухбайтной кодировке UTF-8, а монитор порта Arduino IDE пытается их читать в однобайтной кодировке Windows-1251 (cp1251). Хотя cp1251 тоже 8-битная, как и кодировка LCD 1602, но с ней не совпадает.

Можно формировать русские тексты через коды символов. К примеру, строку 'ЖК дисплей' на русифицированный LCD получится вывести так:

lcd.print("243K 343270c276273e271");

Но мне такой подход не нравится.

Что бы корректно отображать русский текст на русифицированных LCD 1602, для Ардуино придумали несколько библиотек. Но почитав отзывы я увидел, что многие жалуются на глюки при их использовании.
Поэтому я в своей программе многоуровневого меню сам написал простую функцию преобразования UTF-8 в коды LCD. Правда, сделал это только для заглавных русских букв, что меня вполне устраивает.

Функция конвертирования заглавных русских букв UTF-8 в однобайтный код LCD 1602

/*  
 *  Перекодировка UTF-8 русских букв (только заглавных) в коды LCD
 * а то Ардуино выводит их неправильно
 */
byte  MasRus[33][2]= {
                      144,  0x41,   //А
                      145,  0xa0,
                      146,  0x42,
                      147,  0xa1,
                      
                      148,  0xe0,
                      149,  0x45,
                      129,  0xa2,
                      150,  0xa3,
                      
                      151,  0xa4,
                      152,  0xa5,
                      153,  0xa6,
                      154,  0x4b,

                      155,  0xa7,
                      156,  0x4d,
                      157,  0x48,
                      158,  0x4f,
                      
                      159,  0xa8,
                      160,  0x50,
                      161,  0x43,
                      162,  0x54,
                      
                      163,  0xa9,
                      164,  0xaa,
                      165,  0x58,
                      166,  0xe1,
                      
                      167,  0xab,
                      168,  0xac,
                      169,  0xe2,
                      170,  0xad,
                      
                      171,  0xae,
                      172,  0xc4,
                      173,  0xaf,
                      174,  0xb0,
                      
                      175,  0xb1    //Я
  };

String RusStrLCD(String StrIn)
{
  String StrOut="";
  byte b1;
  byte y;

  byte l=StrIn.length();

  for(byte i=0; i<l; i++)
   {
    b1=StrIn.charAt(i);
    if (b1<128)
     StrOut=StrOut+char(b1);
    else 
    {
      if (b1==208)      //байт==208, это первый байт из 2-байтного кода рус. буквы
      {
        b1=StrIn.charAt(i+1);  
        for(y=0; y<33; y++)
         if(MasRus[y][0]==b1)
          {
            StrOut=StrOut+char(MasRus[y][1]);  
            break;
          } 
        }
       i++;          
     }  
   }
  return StrOut;
}

На этом про самодельную HMI с шиной i2c у меня все.

Ах да, в начале статьи я писал, что делаю HMI не совсем для Ардуино, а для ардуино-совместимого оборудования. Это я про ПЛК CONTROLLINO MAXI, который программируется из среды Arduino IDE(и многих других).

Панель оператора (HMI) с шиной I2C для Arduino - 18

CONTROLLINO MAXI это фактически Arduino + куча шилдов и все оформлено как промышленный ПЛК.
Но про него в следующий раз.

Ссылки

Архив со схемами, скетчами и печатной платой в формате lay6
Ардуино-совместимый ПЛК СONTROLLINO, работа с которым вдохновила на создание HMI i2c

Расширитель портов PCF8574 и подключение его к Arduino
Плата FC-113 для работы LCD 1602 по шине i2c и подключение ее к Arduino
Многоуровневое древовидное меню, общие принципы создания на Си
Кодировка UTF-8
Кодировка Windows-1251

Автор: ExplodeMan

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js