Решено было сделать HMI самостоятельно, а в качестве интерфейса использовать «квадратную» шину i2c.
Если интересен процесс разработки и программирования подобных девайсов, добро пожаловать под кат.
Характеристики:
- Дисплей 1602, монохромный 16х2 символов
- 5 кнопок: вверх, вниз, отмена, ввод, редактирование(edit)
- Интерфейс i2c
- Разъем подключения DB9F
- Размеры 155х90х44 мм
Тут возникнут очевидные вопросы:
К этому шилду можно припаять 2 платки FC-113 и получится функционально то же самое, что и у меня: дисплей с клавиатурой, работающие по i2c. Цена набора составит от 4$.
Но на этой плате меня не устраивает размер кнопок, а мне хотелось большие, с возможностью установки разноцветных колпачков. Подключать Arduino к HMI мне хотелось не на соплях, а через нормальный разъем DB9F, а значит нужно было делать соединительную плату. А в этом случае какая разница, делать одну плату или две? Кроме того, у меня уже было в запасе несколько дисплеев 1602, а потому мне нужно было потратить всего 1.02$ для покупки на Алиэкспресс платы FC-113 (0.55$) и расширителя портов PCF8574P (0.47$).
Ну а самое главное- если имеешь дело с Ардуино, то самостоятельное изготовление шилдов для него это само собой разумеющееся дело, правда ведь?
Если бы я смастерил устройство, где дисплей работает по квадратной шине, а кнопки идут напрямую на входа Ардуино, это бы вызывало у меня чувство глубокого неудовлетворения. Как представлю эту картину: из панели торчит отдельно шнурок на интерфейс, отдельно провода на входа, брррр…
Кроме того, различие между платой кнопок, которые идут напрямую ко входам Ардуино, и платой кнопок с интерфейсом 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 мм на станке лазерной резки.
Изначально корпус хотел выпилить из фанеры. Но потом вспомнил, что у меня есть замечательный друг и, по большой для меня радости, директор фирмы по производству спортивных наград. У него имеются всякие там станки, в том числе и для лазерной резки.
Обратился за помощью и друг не отказал- за пару минут лазером нарезали деталей.
Пользуясь случаем, хочу сказать, спасибо тебе, Коля! Иначе мне пришлось бы еще целый день пилить и шлифовать фанеру, а результат едва бы был таким блистательным.
Программирование
С точки зрения Ардуино, данная HMI представляет из себя 2 устройства, которые работают по шине i2c: дисплей(LCD) с адресом 0x27 и клавиатура с адресом 0x20. Соответственно, работать Arduino будет отдельно с клавиатурой и отдельно с LCD.
Работа с LCD осуществляется через специальную библиотеку «LiquidCrystal_I2C.h», ее нужно установить в Aduino IDE.
Работа с клавиатурой осуществляется через стандартную библиотеку «Wire.h», которая изначально имеется в Aduino IDE.
Подключаем HMI к Ardiuno.
1. Для начала проверим, видит ли Ардуино наш HMI. Для этого загружаем в нее программу, которая будет сканировать шину 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.
/*
Вывод на 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». Причем, в теге каждого параметра указывается, доступен ли он для редактирования или только для чтения. Если текущий параметр только для чтения, в начале строки указатель будет '*', если редактирование параметра разрешено, указатель станет '+'.
/*
Древовидное меню, работа снопками и 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 русские слова английскими буквами. Вон, даже почтенная французская компания Shneider Electric(та самая, что еще до революции продавала гаубицы царю) за полтора десятилетия не сподобилась внедрить в свои знаменитые программируемые реле Zelio русский язык. Но это не мешает активно торговать ими на просторах всего СНГ. Причем, канальи, испанский и португальский языки ввели.
На многих наших заводах эти Zelio общаются с персоналом фразами типа «NASOS 1 VKL».
Когда непонятно, есть ли русские буквы в конкретном LCD, нужно вывести на экран все символы его знакогенератора. Если кириллица есть, она начинается со 160 позиции.
/*Последовательно выводит на 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
* а то Ардуино выводит их неправильно
*/
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)
Arcanum7
20.02.2017 11:51Есть ли «защита от дурака» на случай включения не в свой i2c порт, а в настоящий COM порт?
av0000
20.02.2017 12:42Если не жалко COM порт, то те же диоды/TVS спасут _пульт_, но могут пожечь COM… Иначе надо какую-то опто-развязку городить и т.п.
Проще уж, как делают в пром. контроллерах — клемма под винт или нажимнаяJmann
20.02.2017 20:42+1если поставить мультифьюзы или фьюзы в тандеме с TVS диодами, до будут гореть они, а не порт.
ExplodeMan
20.02.2017 14:04+1Оборудование, которое подключают напрямую в компьютерный ком-порт, сюда не воткнуть- на ПК разъем папа, а на панель поставил мама.
Если панель подключить к ПК через удлинитель для ком-порта, то тоже ничего страшного не будет:
сигналы расположены таким образом, что бы они не пересекались с комовскими выходами, за исключением общей земли на контакте 5.
http://www.denvo.ru/pub/hardware/rs-232.html
ragesteel
20.02.2017 11:58А почему кнопки, а не нажимаемый энкодер? Хотя-бы вместо вверх-вниз и подтверждения. По-моему так было-бы гораздо человечнее.
TimsTims
20.02.2017 12:33Достаточно было бы обойтись четырьмя кнопками и функционал бы не пострадал
Но ведь можно было обойтись и 2 кнопками — первая переключает свет 1, вторая свет 2.
av0000
Отдельный плюс за controllio — забавная игрушка. Если б ещё не цена…
А вот I2C без защиты это плохо. На полутора метрах вполне реально пожечь входа от включения соседнего холодильника или даже лампочки. TVS и/или диодная развязка плюс резистор, ом на 10-30 крайне желательны.
ExplodeMan
Спасибо за совет. Какую схему можете порекомендовать? Я бы переделал соединительную плату.
av0000
В своих поделках использую вариации на тему:
иногда вместо диодных сборок ставлю TVS везде.
Вместо сборок пойдут и «простые» диоды. 1N4148 или 1N5819 (SS14), но у вторых обратный ток сравнительно большой
ExplodeMan
Обязательно переделаю. На каком максимальном расстоянии подключали по i2c?
av0000
На ESP8266 датчик BME280 живет на примерно 4?5 метрах UTP кабеля под крышей уже пару лет. Грозы и «убитые» стартёры от древних ламп ЛБ-40 выдержал успешно. Тестовый китайский 1.8" OLED жил на метрах восьми от «ардуины», но подглючивал (возможно сам по себе)
… «1» — +3.3-5В, «2», «3» — шина данных, «4» — GND. TVS стоит на минимальные для этой модели 5В (срабатывание при 5.2 примерно). Если напряжение питания больше, выбрать другой на «сколько-в-питании или чуть выше»
custos
Думаю стоит упомянуть, что такие сборки есть в одном корпусе например USBLC6-2SC6. Конкретно эта предназначена для защиты USB линий, для I2C можно что-нибудь попроще выбрать.
superyarik
спасибо за сборку. а что есть для I2C серийного?
custos
Искать что-то специальное для I2C не нужно, я упомянул эту, поскольку её проще найти… с моей точки зрения, и она хорошо подходит по всем параметрам. Заточка под USB там только в способности работать на частотах USB2.0. Если сможете найти аналогичную, но дешевле, т.е. с большей ёмкостью диодов, для I2C она тоже подойдёт. Есть ещё сборки с четырьмя выводами, например, PRTR5V0U2X, которые разведены совсем как на картинке выше. Проще отфильтровать каталог ближайшего магазина, по ключевому ESD и типу корпуса (мне SOT нравится).
APLe
А «1» и «4» на J1 – это +5 и GND?
av0000
Угу (см выше)
Jmann
Ставя TVS — необходимо еще побеспокоится о том, куда пойдет ток КЗ после срабатывания TVS диода. Думаю минимум на резистор, а лучше на фьюз.
ebragim
Прддерживаю. 200 евро за ардуинку в красивом корпусе?..
av0000
Ну, там ещё куча светодиодиков и пафосные релюшки на 6А… %)
Если брать «фирменные» малогабаритные реле от того же weidmuller-а (а только что-то подобное и влезет в такой корпус ИМХО), то можно и дороже сделать… Но что-то мало верится…
ЗЫ: но корпуса-то забавные
ExplodeMan
Об этом и многом другом- в следующем выпуске.