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

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



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

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

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

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

Почему не купить готовый шилд?
Конечно, можно было у тех же китайцев купить готовый шилд c дисплеем и клавиатурой и типа такого:

К этому шилду можно припаять 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.

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

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

Железо






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

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

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

Кнопки я поставил 12х12мм с квадратным толкателем- на них можно надеть большие разноцветные колпачки.

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






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

Адрес микросхемы PCF8574P задается с помощью адресных ног A0, A1, A2, которые подтягивают к земле или к питанию через резистор 10 кОм.

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

Как я уже писал, в качестве разъема для HMI я выбрал DB9F. На него от Ардуино поступают сигналы +5 V, GND, SDA, SCL.



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

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

Немного о травлении
В сети есть много рецептов травления лимонной кислотой плат на фольгированном стеклотекстолите.

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

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



Потом процесс стихает. Переворачиваем плату.



Готово.



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

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

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

Обратился за помощью и друг не отказал- за пару минут лазером нарезали деталей.

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

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


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

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

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

Подключаем HMI к Ardiuno.



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 found\n");
  else
    Serial.println("done\n");
 
  delay(5000);          
}


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



Видим, что Ардуино на шине 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);          
}




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

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





В нете много информации по созданию многоуровневого меню на 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




В принципе, нет ничего страшного, если писать на 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 \343\270c\276\273e\271");

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

Чтобы корректно отображать русский текст на русифицированных 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 (и многих других).



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

Ссылки

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

Комментарии (23)


  1. av0000
    20.02.2017 10:29
    +2

    Отдельный плюс за controllio — забавная игрушка. Если б ещё не цена…

    А вот I2C без защиты это плохо. На полутора метрах вполне реально пожечь входа от включения соседнего холодильника или даже лампочки. TVS и/или диодная развязка плюс резистор, ом на 10-30 крайне желательны.


    1. ExplodeMan
      20.02.2017 10:35

      Спасибо за совет. Какую схему можете порекомендовать? Я бы переделал соединительную плату.


      1. av0000
        20.02.2017 10:56
        +1

        В своих поделках использую вариации на тему:



        иногда вместо диодных сборок ставлю TVS везде.
        Вместо сборок пойдут и «простые» диоды. 1N4148 или 1N5819 (SS14), но у вторых обратный ток сравнительно большой


        1. ExplodeMan
          20.02.2017 10:57
          +1

          Обязательно переделаю. На каком максимальном расстоянии подключали по i2c?


          1. av0000
            20.02.2017 11:18
            +1

            На ESP8266 датчик BME280 живет на примерно 4?5 метрах UTP кабеля под крышей уже пару лет. Грозы и «убитые» стартёры от древних ламп ЛБ-40 выдержал успешно. Тестовый китайский 1.8" OLED жил на метрах восьми от «ардуины», но подглючивал (возможно сам по себе)

            … «1» — +3.3-5В, «2», «3» — шина данных, «4» — GND. TVS стоит на минимальные для этой модели 5В (срабатывание при 5.2 примерно). Если напряжение питания больше, выбрать другой на «сколько-в-питании или чуть выше»


          1. custos
            20.02.2017 15:20
            +1

            Думаю стоит упомянуть, что такие сборки есть в одном корпусе например USBLC6-2SC6. Конкретно эта предназначена для защиты USB линий, для I2C можно что-нибудь попроще выбрать.


            1. superyarik
              21.02.2017 19:10

              спасибо за сборку. а что есть для I2C серийного?


              1. custos
                21.02.2017 22:09

                Искать что-то специальное для I2C не нужно, я упомянул эту, поскольку её проще найти… с моей точки зрения, и она хорошо подходит по всем параметрам. Заточка под USB там только в способности работать на частотах USB2.0. Если сможете найти аналогичную, но дешевле, т.е. с большей ёмкостью диодов, для I2C она тоже подойдёт. Есть ещё сборки с четырьмя выводами, например, PRTR5V0U2X, которые разведены совсем как на картинке выше. Проще отфильтровать каталог ближайшего магазина, по ключевому ESD и типу корпуса (мне SOT нравится).


        1. APLe
          20.02.2017 11:03
          +1

          А «1» и «4» на J1 – это +5 и GND?


          1. av0000
            20.02.2017 11:20
            +2

            Угу (см выше)


        1. Jmann
          20.02.2017 16:06

          Ставя TVS — необходимо еще побеспокоится о том, куда пойдет ток КЗ после срабатывания TVS диода. Думаю минимум на резистор, а лучше на фьюз.


    1. ebragim
      20.02.2017 12:23

      Прддерживаю. 200 евро за ардуинку в красивом корпусе?..


      1. av0000
        20.02.2017 12:40
        +1

        Ну, там ещё куча светодиодиков и пафосные релюшки на 6А… %)

        Если брать «фирменные» малогабаритные реле от того же weidmuller-а (а только что-то подобное и влезет в такой корпус ИМХО), то можно и дороже сделать… Но что-то мало верится…

        ЗЫ: но корпуса-то забавные


      1. ExplodeMan
        20.02.2017 13:18

        Об этом и многом другом- в следующем выпуске.


  1. Arcanum7
    20.02.2017 11:51

    Есть ли «защита от дурака» на случай включения не в свой i2c порт, а в настоящий COM порт?


    1. av0000
      20.02.2017 12:42

      Если не жалко COM порт, то те же диоды/TVS спасут _пульт_, но могут пожечь COM… Иначе надо какую-то опто-развязку городить и т.п.

      Проще уж, как делают в пром. контроллерах — клемма под винт или нажимная


      1. Jmann
        20.02.2017 20:42
        +1

        если поставить мультифьюзы или фьюзы в тандеме с TVS диодами, до будут гореть они, а не порт.


    1. ExplodeMan
      20.02.2017 14:04
      +1

      Оборудование, которое подключают напрямую в компьютерный ком-порт, сюда не воткнуть- на ПК разъем папа, а на панель поставил мама.
      Если панель подключить к ПК через удлинитель для ком-порта, то тоже ничего страшного не будет:
      сигналы расположены таким образом, что бы они не пересекались с комовскими выходами, за исключением общей земли на контакте 5.
      http://www.denvo.ru/pub/hardware/rs-232.html


  1. ragesteel
    20.02.2017 11:58

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


  1. TimsTims
    20.02.2017 12:33

    Достаточно было бы обойтись четырьмя кнопками и функционал бы не пострадал

    Но ведь можно было обойтись и 2 кнопками — первая переключает свет 1, вторая свет 2.


  1. Jmann
    20.02.2017 15:49
    +1

    Я обычно это делаю, что бы экономить порты микроконтроллера, например клонируя Adafruit. Для HMI лучше использовать хотя бы RS232. А в идеале rs485. Квадратная шина предназначена для обмена внутри прибора, между чипами, а не снаружи, между приборами и системами.


  1. Costic
    20.02.2017 19:42
    -1

    Дребезг контактов вас не беспокоит?
    Для кнопок как минимум необходим RC-фильтр на 100-1000Гц. И входы микросхемы надо посмотреть — есть ли там триггер Шмитта.


    1. Jmann
      20.02.2017 20:40
      +2

      Программная задержка вам, или debounce.