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

от автора

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

image image


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

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

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 «тестилку i2c lcd». Вот прямая ссылка. Внутри архива 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’у, широко распространенному в интернет. Например вот.
Далее последовательно выдаются различные символы на экран.

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. Он позволяет получать необходимую информацию о компонентах ОС и располагать ее на индикаторе в нужном месте. Естественно, обновление в реальном времени.
Однако использование его с нашим индикатором на шине I2C вызвало некоторые затруднения.
Подключение дисплея на HD44780-I2C из комплекта LCD4Linux

в файле \etc\lcd4linux.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. Драйвер испытывался на 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.
Исходные файлы можно взять в каталоге 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 == '\0' || 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, согласно структуры каталогов (вроде все вспомнил) доступны тут.
ссылка на оригинал статьи https://habrahabr.ru/post/316100/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *