- PVSM.RU - https://www.pvsm.ru -

Подключение символьного ЖКИ к плате от WD MyBook Live на AppliedMicro APM82181. Окончание

Добрый день!
Продожим работу с платой от NAS WesternDigital MyBook Live и подключенным к ней ЖК индикатором.
Итак, в предыдущей части мы нашли на плате место для подключения к шине I2C, подключили расширитель портов с индикатором, убедились что все работает. Сегодня выведем на индикатор состояние системы.

image image


Начало было тут: Подключение символьного ЖКИ к плате от WD MyBook Live на AppliedMicro APM82181 [1]

Содержание первой части:

1. Подключение консоли
2. Загрузка без диска
3. Компиляция в LEDE
4. Управление портами (через LuCI и консоль)
5. Подключение к шине I2C
6. Подключение расширителя портов PCF8574

Сегодня рассмотрим:

7. Инициализация HD44780 через i2cset
8. Символьное устройство для записи данных в шину I2C
9. Добавление драйвера HD44780 в ядро
10. Добавление обработки необходимых команд VT100 в драйвер HD44780
11. Добавление дисплея с некоторыми командами VT100 в LCD4Linux
12. Добавление команды программирования знакогенератора в драйвер HD44780
13. Оптимизация передачи данных по шине I2C

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

Итак, к этому моменту подключен в систему расширитель портов, которыми мы можем управлять. К расширителю присоединен символьный ЖК индикатор на клоне контроллера HD44780. Теоретически мы можем им управлять, включив все порты на вывод и зная их назначение. Подсветкой помигать уже удалось, дергая третий порт.

7. Инициализация HD44780 через i2cset

Соединение между контроллером HD44780 и расширителем портов организовано так:
RS — P0
R/W — P1
E — P2
BL — P3
D4 — P4
D5 — P5
D6 — P6
D7 — P7
Это один из встречающихся вариантов. Контроллер при этом переводится и работает в 4-битном режиме, а байт передается по частям.
Имея в распоряжении все порты расширителя, можно выдавать на него данные побитно и таким образом управлять дисплеем. Думаю согласитесь, что это не очень удобно.
Попробуем напрямую управлять через шину I2C. Простой вариант для проверки такой возможности — использовать набор утилит I2C-tools. В LEDE они в разделе Utilites. В набор входит i2cdetect, i2cdump, i2cget, i2cset. Нас интересует последняя и немного первая (для диагностики).
С помощью i2cdetect можно обнаружить подключенные на шину устройства и определить их адрес.

В нашем случае занят только адрес 0x27:

root@lede: i2cdetect 0
WARNING! This program can confuse your I2C bus, cause data loss and worse!
I will probe file /dev/i2c-0.
I will probe address range 0x03-0x77.
Continue? [Y/n] y
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- 27 -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

Утилита i2cset используется для вывода данных устройству с заданным адресом на шине I2C.
Зная последовательность инициализации для вашего ЖКИ, можно без проблем ее выполнить и вывести символы на экран.
Чтобы не изобретать велосипед, рекомендую скачать отсюда: I2C hd44780 модуль на расширителе PCF8574 [2] «тестилку i2c lcd». Вот прямая ссылка [3]. Внутри архива shell скрипт, который работает с индикатором через команду i2cset и выдает на экран символы поочередно. Единственно перед использованием надо закоментировать или удалить строки в начале файла:

insmod i2c-dev
insmod i2c-gpio-custom bus0=0,$sda_gpio,$scl_gpio

Они создают программный порт I2C на любых свободных портах ввода-вывода, а у нас уже есть аппаратный. Ну и кроме того она рассчитана на индикатор размерностью 4*40, но для проверки работоспособности и понимания использования утилиты i2cset это ни чуть не помешает.
Результат:
image
Немного пояснений по ее реализации.
Процедуры write_CMD и print_LCD выводят соответственно на индикатор команду или данные. Это зависит от сигнала RS, в нашем случае находящегося на нулевой бите.
Процедура init_LCD последовательно выдает команды для инициализации индикатора согласно его datasheet'у, широко распространенному в интернет. Например вот [4].
Далее последовательно выдаются различные символы на экран.

8. Символьное устройство для записи данных в шину I2C
Все замечательно, однако хотелось бы уйти от использования утилит, и иметь символьное устройство, выводя на который байты, они бы попадали прямо на шину I2C, конечно с заданным адресом.
К сожалению мне не удалось найти такой драйвер для шины I2C в LEDE. Поэтому с экспериментальными целями было решено переделать один из существующих. Понятно что при желании его использовать и далее, надо было не переделывать, а хотя бы создать на его основе новый.
Подходящим для опытов оказался драйвер EEPROM под шину I2C. В ядре LEDE был подключен драйвер kmod-eeprom-at24, после обновления системы сделана попытка добавления устройства:

root@lede: echo 24c00 0x27 > /sys/bus/i2c/devices/i2c-0/new_device
root@lede: echo 24c00 0x27 > 
[   33.335472] at24 0-0027: 16 byte 24c00 EEPROM, writable, 1 bytes/write
[   33.342102] i2c i2c-0: new_device: Instantiated device 24c00 at 0x27

Успешно подключилось. Теперь если вывести что-то в устройство,

root@lede: echo "1111" > /sys/bus/i2c/devices/i2c-0/0-0027/eeprom

то на шине мы увидим следующую последовательность байт:
0x4e-0x00-0x31 0x4e-0x01-0x31 0x4e-0x02-0x31 0x4e-0x03-0x31 0x4e-0x04-0x0a.
Первый байт в каждой тройке — это адрес устройства, умноженный на 2 (0x27 x 2). Второй — адрес ячейки в EEPROM, третий — данные. Драйвер вполне подходит для передачи данных на ЖКИ, за исключением выдачи адреса ячейки.
Чтобы убрать это исправим файл драйвера build_dir/target-powerpc_464fp_musl-1.1.15/linux-apm821xx_sata/linux-4.4.21/drivers/misc/eeprom/at24.c. Закомментируем несколько строк в процедуре at24_eeprom_write (335-337):

//if (at24->chip.flags & AT24_FLAG_ADDR16)
//  msg.buf[i++] = offset >> 8;
//msg.buf[i++] = offset;

Компилируем-обновляем, добавляем устройство, смотрим вывод

root@lede: echo 24c00 0x27 > /sys/bus/i2c/devices/i2c-0/new_device
[ 2708.782356] at24 0-0027: 16 byte 24c00 EEPROM, writable, 1 bytes/write
[ 2708.788891] i2c i2c-0: new_device: Instantiated device 24c00 at 0x27
root@lede: echo "1111" > /sys/bus/i2c/devices/i2c-0/0-0027/eeprom

Все правильно, вывод без адреса ячейки, только того что нам надо:
image

Теперь можно переделать тестовую программу, убрав оттуда вызов утилиты i2cset:

#!/bin/sh

i2c_adres=0x27
i2c_dev=/sys/bus/i2c/devices/i2c-0/0-0027/eeprom

led=8
ansi=0

to_octal () {
	hh3=$(($hh / 64))
	hh1=$(($hh - $hh3 * 64))
	hh2=$(($hh1 / 8))
	hh1=$(($hh1 - $hh2 * 8))
}

write_CMD () {
: $((hb = $c & 240))
: $((lb = ($c << 4)  & 240 ))

	hh=$((4 + $hb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
	hh=$((0 + $hb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
	hh=$((4 + $lb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
	hh=$((0 + $lb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
}

print_LCD () {
: $((hb = $c & 240))
: $((lb = ($c << 4)  & 240 ))

	hh=$((5 + $hb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
	hh=$((1 + $hb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
	hh=$((5 + $lb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
	hh=$((1 + $lb + $led))
	to_octal 
	echo -e -n \$hh3$hh2$hh1 >> $i2c_dev
}

##########  init LCD  #####################
init_LCD () {
if [[ ! -w $i2c_dev ]]
then 
	echo 24c00 0x27 > /sys/bus/i2c/devices/i2c-0/new_device
	sleep 0.5
fi
sleep 0.5
c=3
write_CMD
c=3
write_CMD
c=2
write_CMD
c=40 #28
write_CMD
c=44 #2C
write_CMD
c=44 #2C
write_CMD
c=12 #0C
write_CMD
c=1
write_CMD
sleep 0.2
c=6
write_CMD
c=2
write_CMD
}
###############################

init_LCD

	c=0x80 # stroka - 1
	write_CMD
	for i in `seq 32 63`; do
		if [ "$i" == 48 ]; then
			c=0xC0 # stroka - 2
			write_CMD
		fi
	c=$(($i + $ansi))
	print_LCD
	done
        sleep 3

	c=0x80 # stroka - 1
	write_CMD
	for i in `seq 64 95`; do
		if [ "$i" == 80 ]; then
			c=0xC0 # stroka - 2
			write_CMD
		fi
	c=$(($i + $ansi))
	print_LCD
	done
        sleep 3

	c=0x80 # stroka - 1
	write_CMD
	for i in `seq 96 127`; do
		if [ "$i" == 112 ]; then
			c=0xC0 # stroka - 2
			write_CMD
		fi
	c=$(($i + $ansi))
	print_LCD
	done

Заодно теперь программа рассчитана только на нашу геометрию экрана — 2х16 символов.
Понятно, что изменяя исходник в каталоге build_dir, следует ожидать что в ближайшем времени файл будет восстановлен из оригинальных пакетов при сборке. Для создания постоянных исправлений следует использовать возможность применения патчей на этапе сборки.

9. Добавление драйвера HD44780 в ядро
После изучения вопроса работоспособности данного варианта подключения ЖКИ было решено попробовать возложить на индикатор некоторый функционал. Например отображение какой-то части состояния операционной системы.
Такой пакет уже существует и даже включен в состав LEDE. Это LCD4Linux [5]. Он позволяет получать необходимую информацию о компонентах ОС и располагать ее на индикаторе в нужном месте. Естественно, обновление в реальном времени.
Однако использование его с нашим индикатором на шине I2C вызвало некоторые затруднения.
Подключение дисплея на HD44780-I2C из комплекта LCD4Linux

в файле etclcd4linux.conf

Display HD44780-I2C {
    Driver 'HD44780'
    Model 'generic'
    Bus 'i2c'
    Port '/dev/i2c-0'
    Device '0x27'
    Bits '4'
    Size '16x2'
    asc255bug 0
    Icons 1
    Wire {
        RW     'DB1'
        RS     'DB0'
        ENABLE 'DB2'
        GPO    'GND'
    }
}
вызывало ошибку:

root@lede: /usr/bin/lcd4linux -v -F

LCD4Linux 0.11.0-SVN-1193 starting
HD44780: $Rev: 1202 $
HD44780: using model 'generic'
HD44780: using I2C bus
HD44780: using 1 Controller(s)
HD44780: using 4 bit mode
udelay: using gettimeofday() delay loop
Segmentation fault

Было также опробованы почти все возможные варианты дисплеев из пакета, в том числе и для использования символьного устройства на базе драйвера EEPROM, сделанного в предыдущей главе. Не заработало.
Тогда было решено идти другим путем. Добавить в систему драйвер именно этого индикатора, принимающий для отображения символы и команды управления, а затем добавить новый дисплей, использующий этот драйвер, в LCD4Linux, благо в нем есть для этого руководство.
Итак, берем готовый драйвер для HD44780 на I2C отсюда: Linux driver for Hitachi HD44780 LCD attached to I2C bus via PCF8574 I/O expander [6]. Драйвер испытывался на Raspberry Pi, понимает пару команд управления терминала VT100, настраивается под разную геометрию индикатора, умеет отображать, гасить и мигать курсором. Осталось его интегрировать в LEDE и немного доработать.
Скачиваем, распаковываем в папку package/hd44780/src.

Дерево

ls -l
-rw-r--r-- 1 root root 18092 Feb 21  2016 LICENSE
-rw-r--r-- 1 root root    60 Nov  9 06:17 Makefile
-rw-r--r-- 1 root root  1945 Feb 21  2016 README.md
-rw-r--r-- 1 root root 10316 Nov 16 04:33 hd44780-dev.c
-rw-r--r-- 1 root root  7756 Feb 21  2016 hd44780-i2c.c
-rw-r--r-- 1 root root  1122 Nov 16 03:28 hd44780.h
-rw-r--r-- 1 root root   235 Feb 21  2016 make.sh

Оставляем в Makefile только это:

obj-m := hd44780.o
hd44780-y := hd44780-i2c.o hd44780-dev.o

И создаем новый Makefile, только папкой выше, в package/hd44780, по аналогии с файлами в других пакетах LEDE:

package/hd44780/Makefile

include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/kernel.mk

PKG_NAME:=hd44780
PKG_RELEASE:=1
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)

include $(INCLUDE_DIR)/package.mk

define KernelPackage/hd44780
SUBMENU:=Other modules
TITLE:=I2C HD44780 driver
FILES:=$(PKG_BUILD_DIR)/hd44780.ko
AUTOLOAD:=$(call AutoLoad,70,hd44780)
KCONFIG:=
endef

define Package/hd44780/description
Big comments....
...
endef

MAKE_OPTS:= 
	ARCH="$(LINUX_KARCH)" 
	CROSS_COMPILE="$(TARGET_CROSS)" 
	SUBDIRS="$(PKG_BUILD_DIR)" 
	EXTRA_CFLAGS="$(EXTRA_CFLAGS)"

define Build/Prepare
	mkdir -p $(PKG_BUILD_DIR)
	$(CP) ./src/* $(PKG_BUILD_DIR)/
endef

define Build/Compile
	$(MAKE) -C "$(LINUX_DIR)" 
		$(MAKE_OPTS) 
		modules
endef

$(eval $(call KernelPackage,hd44780))

Строку с автозагрузкой (AUTOLOAD:=$(call AutoLoad,70,hd44780)) можно добавить позже, когда драйвер будет протестирован.
Теперь при вызове конфигуратора LEDE

make menuconfig

Драйвер появится в модулях ядра (kmod-hd44780), и его можно добавить в конфигурацию:

LEDE Configuration

image

После компиляции, обновления и перезагрузки, если не включена автозагрузка модуля, то пробуем загрузить, смотрим результат:

root@lede: insmod hd44780
root@lede: lsmod |grep 44780
hd44780                 5450  0

Пробуем добавить устройство:

root@lede: echo hd44780 0x27 > /sys/bus/i2c/devices/i2c-0/new_device
[ 9463.913178] i2c i2c-0: new_device: Instantiated device hd44780 at 0x27

На индикаторе, как заложено в драйвере, при инициализации выдается адрес созданного устройства: "/dev/lcd0", с мигающим курсором в конце.
На это устройство можно отправлять символы, которые будут отображаться на индикаторе:

root@lede: echo -n 123 > /dev/lcd0

Также можно управлять режимами работы через sysfs (/sys/class/hd44780/lcd0). По этому пути есть следующие имена файлов: backlight, cursor_display, geometry, cursor_blink. Через них можно настраивать геометрию экрана, управлять режимами курсора и подсветкой. Например, для выключения мигания курсора достаточно дать команду:

root@lede: echo -n 0 > /sys/class/hd44780/lcd0/cursor_blink

Кроме того поддерживаются две команды терминала VT100, это очистка экрана и установка курсора в начальную позицию. Подать их можно так:

root@lede: echo -n -e 'x1b'[2J > /dev/lcd0
root@lede: echo -n -e 'x1b'[H > /dev/lcd0

Установку необходимых режимов также можно сделать при загрузке ОС. Для этого добавляем файл target/linux/apm821xx/base-files/etc/board.d/03_lcd в LEDE с содержимым:

#!/bin/sh
echo hd44780 0x27 > /sys/bus/i2c/devices/i2c-0/new_device
echo -n 16x2 > /sys/class/hd44780/lcd0/geometry
echo -n 0 > /sys/class/hd44780/lcd0/cursor_display
echo -n 0 > /sys/class/hd44780/lcd0/cursor_blink
echo -n -e 'x1b'[2JHello! > /dev/lcd0
exit 0

Теперь плата будет вас приветствовать каждый раз при загрузке системы.

10. Добавление обработки необходимых команд VT100 в драйвер HD44780

Итак, драйвер работает, но для использования в LCD4Linux он должен уметь размещать символы в любой позиции экрана. Согласно списка команд терминала выбираем нужную:
Esc[Line;ColumnH — Move cursor to screen location v,h
Находим файл package/hd44780/src/hd44780-dev.c и добавляем обнаружение и выполнение новой команды. Надо доработать процедуру обработки esc-последовательностей:

Оригинал:

static void hd44780_handle_esc_seq_char(struct hd44780 *lcd, char ch)
{
	int prev_row, prev_col;

	lcd->esc_seq_buf.buf[lcd->esc_seq_buf.length++] = ch;

	if (!strcmp(lcd->esc_seq_buf.buf, "[2J")) {
		prev_row = lcd->pos.row;
		prev_col = lcd->pos.col;

		hd44780_clear_display(lcd);
		hd44780_write_instruction(lcd, HD44780_DDRAM_ADDR | (lcd->geometry->start_addrs[prev_row] + prev_col));

		hd44780_leave_esc_seq(lcd);
	} else if (!strcmp(lcd->esc_seq_buf.buf, "[H")) {
		hd44780_write_instruction(lcd, HD44780_RETURN_HOME);
		lcd->pos.row = 0;
		lcd->pos.col = 0;

		hd44780_leave_esc_seq(lcd);
	} else if (lcd->esc_seq_buf.length == ESC_SEQ_BUF_SIZE) {
		hd44780_flush_esc_seq(lcd);
	}
}

Доработанный вариант:

static void hd44780_handle_esc_seq_char(struct hd44780 *lcd, char ch)
{
	int prev_row, prev_col;
	struct hd44780_geometry *geo = lcd->geometry;

	lcd->esc_seq_buf.buf[lcd->esc_seq_buf.length++] = ch;

	if (!strcmp(lcd->esc_seq_buf.buf, "[2J")) {
		prev_row = lcd->pos.row;
		prev_col = lcd->pos.col;

		hd44780_clear_display(lcd);
		hd44780_write_instruction(lcd, HD44780_DDRAM_ADDR | (lcd->geometry->start_addrs[prev_row] + prev_col));

		hd44780_leave_esc_seq(lcd);
	} else if (!strcmp(lcd->esc_seq_buf.buf, "[H")) {
		hd44780_write_instruction(lcd, HD44780_RETURN_HOME);
		lcd->pos.row = 0;
		lcd->pos.col = 0;

		hd44780_leave_esc_seq(lcd);
	} else if ((lcd->esc_seq_buf.buf[0]=='[') && (lcd->esc_seq_buf.buf[4]=='H') && // Esc[ Line ; Column H
		(lcd->esc_seq_buf.buf[2]==';' ) && (lcd->esc_seq_buf.length == 5)) {
		lcd->pos.row = lcd->esc_seq_buf.buf[1] % geo->rows;
		lcd->pos.col = lcd->esc_seq_buf.buf[3] % geo->cols;
		hd44780_write_instruction(lcd, HD44780_DDRAM_ADDR
		    | (geo->start_addrs[lcd->pos.row] + lcd->pos.col));
		hd44780_leave_esc_seq(lcd);
	} else if (lcd->esc_seq_buf.length == ESC_SEQ_BUF_SIZE) {
		hd44780_flush_esc_seq(lcd);
	}
}

И надо изменить длину буфера для накопления и анализа esc-последовательностей. Находим в файле hd44780.h строку

#define ESC_SEQ_BUF_SIZE   4

и исправляем значение на с 4 на 6. Можно компилировать и проверять. Можно компилировать только один пакет из LEDE.

root@debian:/apm82181-lede-master# make package/hd44780/compile
 make[1] package/hd44780/compile
 make[2] -C package/hd44780 compile

Если ошибок нет, то компилируем весь проект, обновляем, перезагружаемся.
Проверяем:

root@lede:/ echo -n -e 'x1b[1;6H' > /dev/lcd0

Курсор перемещается во вторую строку и 7-ю позицию (нумерация с нуля):

image

11. Добавление дисплея с некоторыми командами VT100 в LCD4Linux

Драйвер ЖКИ выполняет свой функционал. Теперь его можно задействовать в пакете LCD4Linux для отображения состояния системы. Однако я в нем не нашел дисплея, работающего с драйвером по протоколу терминала.
Значит пишем свой. Согласно инструкции How to write new display drivers [7].
Исходные файлы можно взять в каталоге build_dir/target-powerpc_464fp_musl-1.1.15/lcd4linux-custom/lcd4linux-r1203, либо из пакета dl/lcd4linux-r1203.tar.bz2.
Все как в руководстве:

  1. Из файла drv_Sample.c drv делаем копию drv_vt100.c
  2. Редактируем drv_vt100.c, удаляем все связанное с графическим режимом, с GPIO
  3. Добавляем новый драйвер в drv.c
  4. Добавляем в Makefile.am
  5. Добавляем в drivers.m4

    if test "$VT100" = "yes"; then
       TEXT="yes"
       I2C="yes"
       DRIVERS="$DRIVERS drv_vt100.o"
       AC_DEFINE(WITH_VT100,1,[vt100 driver])
    fi
  6. Добавляем в Makefile.am

Далее пишем свои процедуры в файл drv_vt100.c.

drv_vt100_open:

static int drv_vt100_open(const char *section)
{
    char *s;
    int f = -1;
    s = cfg_get(section, "Port", NULL);
    if (s == NULL || *s == '' || strlen(s) > 80) {
	error("%s: no '%s.Port' entry from %s", Name, section, cfg_source());
	return -1;
    }
    strcpy(Port, s);
    f = open(Port, O_WRONLY);
    if (f == -1) {
	error("open(%s) failed: %s", Port, strerror(errno));
	return -1;
    }
    close (f);
    return 0;
}

drv_vt100_send:

static void drv_vt100_send(const char *data, const unsigned int len)
{
    unsigned int i;
    int f;
    f = open(Port, O_WRONLY);
    write (f, data, len);
    close (f);
}

drv_vt100_clear:

static void drv_vt100_clear(void)
{
    char cmd[4];
    cmd[0] = 0x1B; // ESC
    cmd[1] = '[';  // [
    cmd[2] = '2';  // 2
    cmd[3] = 'J';  // J
    drv_vt100_send(cmd, 4);
    cmd[2] = 'H';  // H
    drv_vt100_send(cmd, 3);
}

drv_vt100_write:

static void drv_vt100_write(const int row, const int col, const char *data, int len)
{
    char cmd[6];
    cmd[0] = 0x1B; 		// ESC
    cmd[1] = '[';  		// [
    cmd[2] = row & 0xff;	// Line
    cmd[3] = ';';		// ;
    cmd[4] = col & 0xff;	// Column
    cmd[5] = 'H';		// H
    drv_vt100_send(cmd, 6);
}

drv_vt100_close оставляем пустой.
Редактируем и создаем файлы в отдельной от проекта LEDE папке. Затем, так как при компиляции проекта файлы LCD4linux обновляются из архива, то изменять их в папке build_dir/… бессмысленно. Необходимо пользоваться возможность применять патчи. Патчи для LCD4Linux располагаются в папке feeds/packages/utils/lcd4linux/patches. Свой, добавляющий новый драйвер дисплея VT100 нужно разместить тут же.
Для создания патча делаем рядом две папки. В одной (пусть 1/) размещаем оригинальные файлы, в другой (пусть 2/) те же, но измененные. Затем выполняем команду diff:

diff -Naur ./1 ./2 > 180-vt100.patch

В результате имеем файл 180-vt100.patch примерно с таким содержанием:

diff -Naur ./vt100/Makefile.am ./vt100-f/Makefile.am
--- ./vt100/Makefile.am	2016-11-28 11:01:56.000000000 +0000
+++ ./vt100-f/Makefile.am	2016-11-14 07:33:41.000000000 +0000
@@ -125,6 +125,7 @@
 drv_USBHUB.c                  
 drv_USBLCD.c                  
 drv_vnc.c                     
+drv_vt100.c                   
 drv_WincorNixdorf.c           
 drv_X11.c                     
                               
diff -Naur ./vt100/drivers.m4 ./vt100-f/drivers.m4
--- ./vt100/drivers.m4	2016-11-14 11:54:41.000000000 +0000
+++ ./vt100-f/drivers.m4	2016-11-14 07:37:00.000000000 +0000
@@ -39,7 +39,7 @@
   [                        Newhaven, Noritake, NULL, Pertelian, PHAnderson,]
   [                        PICGraphic, picoLCD, picoLCDGraphic, PNG, PPM, RouterBoard,]
   [                        Sample, SamsungSPF, serdisplib, ShuttleVFD, SimpleLCD, st2205, T6963,]
-  [                        TeakLCM, TEW673GRU, Trefon, ULA200, USBHUB, USBLCD, VNC, WincorNixdorf, X11],
+  [                        TeakLCM, TEW673GRU, Trefon, ULA200, USBHUB, USBLCD, VNC, vt100, WincorNixdorf, X11],
   drivers=$withval,
   drivers=all
 )
@@ -114,6 +114,7 @@
 	 USBHUB="yes"
          USBLCD="yes"
          VNC="yes"
+         VT100="yes"
 	 WINCORNIXDORF="yes"
          X11="yes"
          ;;
@@ -279,6 +280,9 @@
       VNC)
          VNC=$val
          ;;
+      vt100)
+         VT100=$val
+         ;;
       WincorNixdorf)
          WINCORNIXDORF=$val
          ;;
@@ -869,6 +873,13 @@
    fi
 fi
 
+if test "$VT100" = "yes"; then
+   TEXT="yes"
+   I2C="yes"
+   DRIVERS="$DRIVERS drv_vt100.o"
+   AC_DEFINE(WITH_VT100,1,[vt100 driver])
+fi
+
 if test "$WINCORNIXDORF" = "yes"; then
    TEXT="yes"
    SERIAL="yes"

Создается один патч для всех файлов. Если посмотреть в патчах, которые уже есть в папке feeds/packages/utils/lcd4linux/patches, внутри файлов отсутствуют строки, которые показывают выполняемую команду «diff -Naur...». Приводим наш патч в такое же состояние и копируем в папку.
Заходим в конфигуратор LEDE.

Видим появление нашего драйвера дисплея в LCD4Linuc-custom:

image

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

lcd4linux.conf

Variables {
   tick 500
   tack 100
   minute 60000
}
Display VT100 {
    Driver 'vt100'
    Size '16x2'
    Port '/dev/lcd0'
}
Widget Test {
    class 'Text'
    expression '1234567890123456'
    width 16
}
Layout Test {
    Row01.Col1 'Test'
    Row02.Col1 'Test'
}
Display 'VT100'
Layout 'Test'

Проверяем:

root@lede:/# /usr/bin/lcd4linux -v -F
LCD4Linux 0.11.0-SVN-1193 starting
vt100: $Rev: 001 $
initializing layout 'Test'
Creating new timer group (1000 ms)
 widget 'Test': Class 'text', Parent '<root>', Layer 1, Row 0, Col 0 (to 0,16)
 widget 'Test': Class 'text', Parent 'Test', Layer 1, Row 1, Col 0 (to 1,16)

На индикаторе мы видим, как и планировали в конфиге, цифры.
image

12. Добавление команд программирования знакогенератора в драйверы HD44780 и VT100

Индикатор на базе контроллера HD44780 имеет зашитый латинский алфавит с цифрами и знаками и свободно 8 программируемых символов (бывает есть и русские символы, но не в этом случае). Меняя отображение программируемых символов можно получить несложную анимацию. Ее варианты представлены в примере конфига LCD4Linux, но пока наши драйверы не поддерживают эту функцию, использовать их мы не можем.
Ничего сложного в этом нет, надо в одном драйвере принять 8 байт и отправить с командой в знакогенератор, в другом их отправить.
Снова исправляем

процедуру hd44780_handle_esc_seq_char в hd44780-dev.c

static void hd44780_handle_esc_seq_char(struct hd44780 *lcd, char ch)
{
	int prev_row, prev_col;
	struct hd44780_geometry *geo = lcd->geometry;

	if (lcd->is_in_set_char == 0) {
		lcd->esc_seq_buf.buf[lcd->esc_seq_buf.length++] = ch;
		if (!strcmp(lcd->esc_seq_buf.buf, "[2J")) {
			prev_row = lcd->pos.row;
			prev_col = lcd->pos.col;
			hd44780_clear_display(lcd);
			hd44780_write_instruction(lcd, HD44780_DDRAM_ADDR | (lcd->geometry->start_addrs[prev_row] + prev_col));
			hd44780_leave_esc_seq(lcd);
		} else if (!strcmp(lcd->esc_seq_buf.buf, "[H")) {
			hd44780_write_instruction(lcd, HD44780_RETURN_HOME);
			lcd->pos.row = 0;
			lcd->pos.col = 0;
			hd44780_leave_esc_seq(lcd);
		} else if ((lcd->esc_seq_buf.buf[0]=='[') && (lcd->esc_seq_buf.buf[4]=='H') && // Esc[ Line ; Column H
			(lcd->esc_seq_buf.buf[2]==';' ) && (lcd->esc_seq_buf.length == 5)) {
			lcd->pos.row = lcd->esc_seq_buf.buf[1] % geo->rows;
			lcd->pos.col = lcd->esc_seq_buf.buf[3] % geo->cols;
			hd44780_write_instruction(lcd, HD44780_DDRAM_ADDR
			    | (geo->start_addrs[lcd->pos.row] + lcd->pos.col));
			hd44780_leave_esc_seq(lcd);
		} else if (!strcmp(lcd->esc_seq_buf.buf, "(S")) { // Esc(S code matrix(8)
			lcd->is_in_set_char = 1;
		} else if (lcd->esc_seq_buf.length == ESC_SEQ_BUF_SIZE) {
			hd44780_flush_esc_seq(lcd);
		}

	} else if (lcd->is_in_set_char == 1) { // start set CGRAM code
		hd44780_write_instruction(lcd, HD44780_CGRAM_ADDR | 8 * (ch & 0x07));
		lcd->is_in_set_char++;
	} else {
		hd44780_write_data(lcd, ch & 0x1f); // set 8 bytes CGRAM code
		lcd->is_in_set_char++;
		if (lcd->is_in_set_char == 10){ // go to DDRAM mode
			hd44780_write_instruction(lcd, HD44780_DDRAM_ADDR
			    | (geo->start_addrs[lcd->pos.row] + lcd->pos.col));
			hd44780_leave_esc_seq(lcd);
	    }
	}
}

Добавляем в нее режим приема 8-и байтов знакогенератора и запись их в CGRAM (знакогенератор). Так как в VT100 я не нашел ESC-последовательность программирования символа, то пришлось что-то придумать. Пусть это будет Esc(S 8 байт, то есть код ESC, затем открывающая круглая скобка, латинская буква S и 8 байт матрицы. В нашем случае, при размере знакоместа 8*5 будет использоваться только 5 младших бит каждого байта.
В процедуре hd44780_write добавляем сброс режима приема знакогенератора (строка lcd->is_in_set_char = 0).

hd44780_write

 void hd44780_write(struct hd44780 *lcd, const char *buf, size_t count)
...
 			case 'e':
 				lcd->is_in_esc_seq = true;
				lcd->is_in_set_char = 0;
 				break;
 			default:
 				hd44780_write_char(lcd, ch);
...

И описываем это поле структуры (is_in_set_char) в файле заголовков hd44780.h.

struct hd44780

struct hd44780 {
	struct cdev cdev;
	struct device *device;
	struct i2c_client *i2c_client;
	struct hd44780_geometry *geometry;
	struct {
		int row;
		int col;
	} pos;
	char buf[BUF_SIZE];
	struct {
		char buf[ESC_SEQ_BUF_SIZE];
		int length;
	} esc_seq_buf;
	bool is_in_esc_seq;
	int is_in_set_char;
	bool backlight;
	bool cursor_blink;
	bool cursor_display;
	bool dirty;
	struct mutex lock;
	struct list_head list;
};

Теперь добавим этот функционал в драйвер дисплея LCD4Linux. Функция drv_vt100_defchar файла drv_vt100.c:

drv_vt100_defchar

static void drv_vt100_defchar(const int ascii, const unsigned char *matrix)
{
    char cmd[12];
    int i;

    /* call the 'define character' function */
    cmd[0] = 0x1B; 		// ESC
    cmd[1] = '(';  		// (
    cmd[2] = 'S';		// S
    cmd[3] = ascii & 0x07;	// code

    /* send bitmap to the display */
    for (i = 0; i < 8; i++) {
	cmd[i + 4] = (*matrix++) & 0x1f;
    }
    drv_vt100_send(cmd, 12);
}

Компилируем, обновляем, перезагружаем.
Меняем снова конфиг LCD4linux.

lcd4linux.conf

Variables {
   tick 500
   tack 100
   minute 60000
}
Display VT100 {
    Driver 'vt100'
    Size '16x2'
    Port '/dev/lcd0'
    Icons 1
}
Widget RAM {
    class  'Text'
    expression meminfo('MemFree')/1024
    postfix ' MB RAM'	
    width  11
    precision 0
    align  'R'
    update tick
}
Widget Busy {
    class 'Text'
    expression proc_stat::cpu('busy', 500)
    prefix 'Busy'	
    postfix '%'	
    width 9	
    precision 1
    align 'R'	
    update tick	
}	
Widget Uptime {
    class 'Text'
    expression uptime('%d days %H:%M:%S')
    width 20
    align 'R'
    prefix 'Up '
    update 1000
}
Widget Uptime {
    class 'Text'
    expression 'Up '.uptime('%d %H:%M:%S')
    width 16
    align 'L'
    update 1000
}
# Icons
Widget Timer {
    class 'Icon'
    speed 83
    Bitmap {
	Row1 '.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|'
	Row2 '.***.|.*+*.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.+++.|.+*+.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|'
	Row3 '*****|**+**|**++*|**+++|**++.|**++.|**+++|**+++|**+++|**+++|**+++|+++++|+++++|++*++|++**+|++***|++**.|++**.|++***|++***|++***|++***|++***|*****|'
	Row4 '*****|**+**|**+**|**+**|**+++|**+++|**+++|**+++|**+++|**+++|+++++|+++++|+++++|++*++|++*++|++*++|++***|++***|++***|++***|++***|++***|*****|*****|'
	Row5 '*****|*****|*****|*****|*****|***++|***++|**+++|*++++|+++++|+++++|+++++|+++++|+++++|+++++|+++++|+++++|+++**|+++**|++***|+****|*****|*****|*****|'
	Row6 '.***.|.***.|.***.|.***.|.***.|.***.|.**+.|.*++.|.+++.|.+++.|.+++.|.+++.|.+++.|.+++.|.+++.|.+++.|.+++.|.+++.|.++*.|.+**.|.***.|.***.|.***.|.***.|'
	Row7 '.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|'
	Row8 '.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|'
    }
}
Layout L16x2 {
    Row1 {
	Col1  'Uptime'
	col16 'Timer'
    }
    Row2 {
	Col1  'Busy'
	Col11 'RAM'
    }
}
Display 'VT100'
Layout 'L16x2'

Проверяем:

root@lede: /usr/bin/lcd4linux  -v -F
LCD4Linux 0.11.0-SVN-1193 starting
vt100: $Rev: 001 $
vt100: reserving 1 of 8 user-defined characters for icons
initializing layout 'L16x2'
Creating new timer group (1000 ms)
 widget 'Uptime': Class 'text', Parent '<root>', Layer 1, Row 0, Col 0 (to 0,16)
Creating new timer group (83 ms)
 widget 'Timer': Class 'icon', Parent '<root>', Layer 1, Row 0, Col 15 (to 1,16)
Creating new timer group (500 ms)
 widget 'Busy': Class 'text', Parent '<root>', Layer 1, Row 1, Col 0 (to 1,9)
 widget 'RAM': Class 'text', Parent '<root>', Layer 1, Row 1, Col 10 (to 1,21)

На экране видим время работы платы с последней загрузки, загрузку системы, свободной память и символ анимации в виде заполняющегося и очищающегося диска.
image
Добавив исправление конфига LCD4Linux в патч 180-vt100.patch, мы получим такой же вид индикатора сразу при загрузке:

180-vt100.patch

--- a/lcd4linux.conf.sample	2016-11-15 09:47:46.000000000 +0000
+++ a-f/lcd4linux.conf.sample	2016-11-18 03:18:22.000000000 +0000
@@ -567,7 +567,14 @@
     HttpPort	 '5800'
 }
 
-
+Display VT100 {
+    Driver 'vt100'
+    Size '16x2'
+    Port '/dev/lcd0'
+    Icons 1
+}
+            
+            
 Display FutabaVFD {
     Driver 'FutabaVFD'
     Port '/dev/parport0'	
@@ -674,7 +681,7 @@
 
 Widget RAM {
     class  'Text'
-    expression meminfo('MemTotal')/1024
+    expression meminfo('MemFree')/1024
     postfix ' MB RAM'	
     width  11
     precision 0
@@ -828,6 +835,14 @@
     update 1000
 }
 
+Widget Uptime {
+    class 'Text'
+    expression 'Up '.uptime('%d %H:%M:%S')
+    width 16
+    align 'L'
+    update 1000
+}
+
 Widget mpris_TrackPosition_bar {
     class 'Bar'
     expression  mpris_dbus::method_PositionGet('org.kde.amarok')
@@ -1015,7 +1030,7 @@
 
 Widget Timer {
     class 'Icon'
-    speed 50
+    speed 83
     Bitmap {
 	Row1 '.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|.....|'
 	Row2 '.***.|.*+*.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.*++.|.+++.|.+*+.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|.+**.|'
@@ -1225,6 +1240,17 @@
     }
 }
 
+Layout L16x2-2 {
+    Row1 {
+	Col1  'Uptime'
+	col16 'Timer'
+    }
+    Row2 {
+	Col1  'Busy'
+	Col11 'RAM'
+    }
+}
+
 Layout L20x2 {
     Row1 {
         Col1  'CPUinfo'
@@ -1323,7 +1349,7 @@
 
 
 
-Display 'ACool'
+#Display 'ACool'
 #Display 'SerDispLib'
 #Display 'LCD-Linux'
 #Display 'LCD2041'
@@ -1354,7 +1380,7 @@
 #Display 'IRLCD'
 #Display 'USBLCD'
 #Display 'BWCT'
-#Display 'Image'
+Display 'Image'
 #Display 'TeakLCD'
 #Display 'Trefon'
 #Display 'LCD2USB'
@@ -1363,15 +1389,17 @@
 #Display 'ctinclud'
 #Display 'picoLCD'
 #Display 'VNC'
+Display 'VT100'
 #Display 'FutabaVFD'
 #Display 'GLCD2USB'
 
-#Layout 'Default'
-Layout 'TestLayer'
+Layout 'Default'
+#Layout 'TestLayer'
 #Layout 'TestImage'
 #Layout 'L8x2'
 #Layout 'L16x1'
 #Layout 'L16x2'
+Layout 'L16x2-2'
 #Layout 'L20x2'
 #Layout 'L40x2'
 #Layout 'Test'

13. Оптимизация передачи данных по шине I2C

Теперь, когда все работает как планировалось, немного о скорости передачи данных.
Хочется обратить внимание на два момента.
Во-первых, данный по шине I2C передаются очень маленькими блоками, а конкретно по одному байту. А каждому блоку добавляется адрес ведомого устройства. Логично предположить, что передача адреса устройства с блоком бОльшего размера увеличит утилизацию шины и уменьшит время передачи.

Так выглядит передача отдельными байтами

image
Видно что каждый второй — байт адреса (0x4E).

Проведем частичную оптимизацию. Для этого вспомним как передается один байт данных на индикатор. ЖКИ работает в 4-битном режиме, т.е. получает за раз по полбайта. Эти полбайта должны подтверждаться выдачей сигнала «Enable». В итоге для передачи одного байте из процессора на индикатор по шине I2C идет 6 байт:

  1. Старшие полбайта без сигнала «Enable»
  2. Старшие полбайта c сигналом «Enable»
  3. Старшие полбайта без сигнала «Enable»
  4. Младшие полбайта без сигнала «Enable»
  5. Младшие полбайта с сигналом «Enable»
  6. Младшие полбайта без сигнала «Enable»

И так как каждый сопровождается адресом, то реально это составляет 12 байт, при скорости шины 100 КГц это 1.2 мс.
Предлагается передавать те же 6 байт, но одним блоком, с одним байтом адреса, т.е. 7 байт вместо 12.
Оригинальные процедуры передачи данных из драйвера HD44780.

hd44780_write_data, hd44780_write_nibble, pcf8574_raw_write из hd44780-dev.c

static void pcf8574_raw_write(struct hd44780 *lcd, u8 data)
{
	i2c_smbus_write_byte(lcd->i2c_client, data);
}

static void hd44780_write_nibble(struct hd44780 *lcd, dest_reg reg, u8 data)
{
	data = (data << 4) & 0xF0;
	if (reg == DR)
		data |= RS;
	data = data | (RW & 0x00);
	if (lcd->backlight)
		data |= BL;
	pcf8574_raw_write(lcd, data);
	pcf8574_raw_write(lcd, data | E);
	pcf8574_raw_write(lcd, data);
}

static void hd44780_write_data(struct hd44780 *lcd, u8 data)
{
	u8 h = (data >> 4) & 0x0F;
	u8 l = data & 0x0F;
	hd44780_write_nibble(lcd, DR, h);
	hd44780_write_nibble(lcd, DR, l);
	udelay(37 + 4);
}

Процедура из драйвера HD44780, исправленная для пакетной передачи данных.

hd44780_write_data из hd44780-dev.c

static void hd44780_write_data(struct hd44780 *lcd, u8 data)
{
	u8 h = (data >> 4) & 0x0F;
	u8 l = data & 0x0F;
	u8 buf[5];
	h = (h << 4) & 0xF0;
	l = (l << 4) & 0xF0;
	h |= RS;
	l |= RS;
	h = h | (RW & 0x00);
	l = l | (RW & 0x00);
	if (lcd->backlight){
		h |= BL;
		l |= BL;
	}
	buf[0] = h | E;
	buf[1] = h;
	buf[2] = l;
	buf[3] = l | E;
	buf[4] = l;
	i2c_smbus_write_i2c_block_data(lcd->i2c_client, h, 5, (const u8 *)(&buf[0]));
	udelay(37 + 4);
}

Вот так выглядит теперь передача байта 0x1F

image

И занимает 0,67 мс.
Во-вторых, скорость шины по умолчанию 100 КГц, а это не максимум. Конечно именно такая скорость рекомендуется для расширителя портов на ЖКИ. Но в тоже время многие разработчики говорят о бесперебойной работе и на 400 КГц. Конечно, использование нестандартного режима оправдано при необходимости и тщательном тестирование на отсутствие сбоев, а я всего лишь могу сказать как это сделать и что получится.
Информации в интернете о включении режима найти не удалось, пришлось пересматривать исходники LEDE. В итоге есть два варианта включения режима fast, т.е. 400 КГц.

Первый, это передать параметр модулю ядра. Модуль это i2c-ibm_iic. Параметр — iic_force_fast.
В итоге надо к параметрам ядра при запуске добавить i2c-ibm_iic.iic_force_fast=1.
Это можно сделать в загрузчике U-boot например так:

setenv addmisc 'setenv bootargs ${bootargs} i2c-ibm_iic.iic_force_fast=1'

После загрузки системы мы имеем:

 root@lede: dmesg | grep i2c
[    0.000000] Kernel command line: root=/dev_nfs rw nfsroot=192.168.1.10:/nfs/debian_ppc/rootfs ip=dhcp console=ttyS0,115200 i2c-ibm_iic.iic_force_fast=1
[    4.770923] i2c /dev entries driver
[    4.774742] ibm-iic 4ef600700.i2c: using fast (400 kHz) mode
[   10.456041] i2c i2c-0: new_device: Instantiated device hd44780 at 0x27

Второй — указать режим работы шины в дереве устройств (apollo3g.dtsi, параметр fast-mode):

IIC0: i2c@ef600700 {
	compatible = "ibm,iic";
	reg = <0xef600700 0x00000014>;
	interrupt-parent = <&UIC0>;
	interrupts = <0x2 0x4>;
	fast-mode;
	#address-cells = <1>;
	#size-cells = <0>;
};

После компиляции не забудьте обновить дерево устройств на TFTP сервере.
И результат:

root@lede: dmesg | grep i2c
[    4.774585] i2c /dev entries driver
[    4.778396] ibm-iic 4ef600700.i2c: using fast (400 kHz) mode
[   10.464396] i2c i2c-0: new_device: Instantiated device hd44780 at 0x27
root@lede: ls -al /proc/device-tree/plb/opb/i2c@ef600700
-r--r--r--    1 root     root             4 Nov 18 04:13 #address-cells
-r--r--r--    1 root     root             4 Nov 18 04:13 #size-cells
drwxr-xr-x    2 root     root             0 Nov 18 04:13 .
drwxr-xr-x   12 root     root             0 Nov 18 04:13 ..
-r--r--r--    1 root     root             8 Nov 18 04:13 compatible
-r--r--r--    1 root     root             0 Nov 18 04:13 fast-mode
-r--r--r--    1 root     root             4 Nov 18 04:13 interrupt-parent
-r--r--r--    1 root     root             8 Nov 18 04:13 interrupts
-r--r--r--    1 root     root             4 Nov 18 04:13 name
-r--r--r--    1 root     root             8 Nov 18 04:13 reg

И скорость передачи байта 0,19 мс:
image
Что почти на порядок лучше исходной.

В качестве вывода можно сказать, что в результате проделанной работы мы получили возможность использовать плату со слабодокументированным в открытых источниках процессором в проектах, где применим Linux (LEDE). Основной интерфейс Ethernet, хранение на SATA, управление через I2C и несколько портов дают широкие возможности для разработчиков.
Ну и напоследок, для дублирования всего вышесказанного, файлы из LEDE, согласно структуры каталогов (вроде все вспомнил) доступны тут [8].

Автор: sergey2ru

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/215272

Ссылки в тексте:

[1] Подключение символьного ЖКИ к плате от WD MyBook Live на AppliedMicro APM82181: https://habrahabr.ru/post/315912/

[2] I2C hd44780 модуль на расширителе PCF8574: http://cyber-place.ru/showthread.php?t=2164

[3] прямая ссылка: http://cyber-place.ru/showpost.php?p=30991&postcount=13

[4] вот: http://datasheet-pdf.com/datasheet-download.php?id=507176

[5] LCD4Linux: https://lcd4linux.bulix.org/wiki

[6] Linux driver for Hitachi HD44780 LCD attached to I2C bus via PCF8574 I/O expander: https://github.com/gorskima/hd44780-i2c

[7] How to write new display drivers: https://lcd4linux.bulix.org/wiki/driver_howto

[8] тут: https://yadi.sk/d/McvSlL8mzoFnA

[9] Источник: https://habrahabr.ru/post/316100/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best