...

суббота, 28 февраля 2015 г.

Разбираемся с LCD экраном LPH9157-2 от Siemens C75/ME75



Внятной документации на этот экран я не нашел поэтому пришлось разбираться с тем что есть и экспериментировать. В качестве управляющего устройства я использовал Raspberry PI. Так-же была написана программа позволяющая превратить этот экран в мини-монитор.

Описание




Данный дисплей имеет разрешение 135 x 176 пикселей и даёт возможность работать с тремя цветовыми палитрами 16(6-5-6), 12(4-4-4) и 8(3-3-2) бит.



Распиновка и подключение




Тут всё просто, экран питается напряжением 2,9 вольт, подсветка (LED±) запитывается отдельно напряжением примерно 12 вольт(я использовал батарею аккумуляторов соединённую с подсветкой через резистор на 510 Ом).






























































Pin description
#Name Function
1RSLow=CMD, High=DATA
2~RSTReset input, active low
3~CSChip select, active low
4SYNCExternal frame synchorization input, unused by default
5CLKSPI Clock-in signal (High-to-Low)
6DATASPI Data-in signal (MSB first)
7VCCPower supply, normally 2.9V (I tested with 3.3V)
8GNDGround
9LED+Backlight voltage, approx. 12V (depends on required current)
10LED-Backlight common pin

Как можно заметить экран управляется через интерфейс SPI (контакты CS/CLK/DAT(MOSI)), предположительно это лишь половина интерфейса так как отсутствует контакт MISO, следовательно писать данные в экран мы можем, а вот читать — нет(здесь следует упомянуть что SPI может работать в двунаправленном режиме с использованием одного провода (MIMO) но так как отсутствует какие либо команды чтения данных из экрана будем считать что этот режим экраном не используется).

И перед тем как переходить непосредственно к управлению экраном надо бы этот экран к чему-нибудь подключить. В моём случае это будет Raspberry Pi. Контакты SPI экрана подключены к соответствующим им контактам SPI «малины», RS и RST к GPIO_17 и GPIO_27 соответственно. Данное подключение актуально для RPI Revision-2, если у вас иная модель то названия и номера контактов GPIO могут отличаться.












Заморачиваться с разъёмом подключения экрана я не стал и просто подпаялся к выводам проводом МГТФ. Экран в данном подключении питается от 3.3В, а не от 2.9 как в описании.

Вот так выглядит вся схема в сборе


Команды управления экраном




Экран управляется достаточно просто — путём посылки по SPI команд и данных. Отличить одни от других экрану помогает состояние пина RS где высокий уровень(лог. 1) означает передачу данных, а низкий(лог. 0) передачу команд. При передаче используется тупоконечный(big-ending) порядок байт.

Список команд:



  • CMD_RESET 0x01 — программный сброс

  • CMD_MEMORY_ACCESS_CONTROL 0x36 — установка направления заполнения области дисплея, имеет однобайтовый аргумент 0bVHRXXXXX, где

    V — заполнение по вертикали (0 — сверху-вниз, 1 — снизу-вверх),

    H — заполнение по горизонтали (0 — слева-направо, 1 — справа-налево),

    R — меняются местами строки и столбцы (при этом заполнение остается сверху-вниз, слева-направо))

  • CMD_WAKEUP 0x11 — выход из спящего режима

  • CMD_PALETTE 0x3A — установка цветовой палитры 8(0x02), 12(0x03) и 16(0x05) бит

  • CMD_ENABLE 0x29 — включение дисплея

  • CMD_SET_X 0x2A — задаем область рисования по X

  • CMD_SET_Y 0x2B — задаем область рисования по Y

  • CMD_START_WRITE 0x2C — начало записи в видеопамять




Код




Работа экрана была проверена во всех 3х цветовых режимах но, дабы не захламлять исходник, далее я буду рассматривать только 16-битный. Во всех остальных режимах работа экрана не отличается, за исключением, разве что 12-битного — в нём на 2 пикселя приходится 3 байта, а если нужно вывести лишь одну точку то посылается 2 байта(4 младших бита последнего экраном игнорируются).

Для доступа к GPIO «малины» была использована библиотека bcm2835.
Начальная инициализация GPIO


int init_gpio()
{
if (!bcm2835_init())
return 0;

bcm2835_spi_begin();
bcm2835_spi_setBitOrder(BCM2835_SPI_BIT_ORDER_MSBFIRST);

// CPOL = 0, CPHA = 0, Clock idle low, data is clocked in on rising edge, output data (change) on falling edge
bcm2835_spi_setDataMode(BCM2835_SPI_MODE0);

// в телефоне экран работает на частоте SPI в 13 МГц
// поэтому небольшое превышение по частоте ему не повредит
// хотя у меня он продолжал работать и при вдвое большей частоте (30 МГц)
bcm2835_spi_setClockDivider(BCM2835_SPI_CLOCK_DIVIDER_16); ///< 16 = 64ns = 15.625MHz
bcm2835_spi_chipSelect(BCM2835_SPI_CS0); /// Select Our Device
bcm2835_spi_setChipSelectPolarity(BCM2835_SPI_CS0, LOW);

// настроим порты на запись
bcm2835_gpio_fsel(LCD_RS, BCM2835_GPIO_FSEL_OUTP);
bcm2835_gpio_fsel(LCD_RESET, BCM2835_GPIO_FSEL_OUTP);

return 1;
}




Несколько вспомогательных функций


void send_cmd(char cmd)
{
bcm2835_gpio_write(LCD_RS, RS_CMD); // следующий байт - команда
bcm2835_spi_transfer(cmd);
}

void send_data(char data)
{
bcm2835_gpio_write(LCD_RS, RS_DATA); // следующий байт - данные
bcm2835_spi_transfer(data);
}




Задание области рисования


void set_draw_area(char x1, char y1, char x2, char y2)
{
send_cmd(CMD_SET_X);
send_data(x1);
send_data(x2);

send_cmd(CMD_SET_Y);
send_data(y1);
send_data(y2);
}




В результате экспериментов с экраном выяснилось что необязательно задавать область перед выводом каждого кадра, достаточно задать её лишь единожды и послав команду начала записи просто гнать по SPI последовательность кадров непрерывным потоком.
Подготовка к выводу и передача данных


void draw_start()
{
send_cmd(CMD_START_WRITE);
bcm2835_gpio_write(LCD_RS, RS_DATA);
}

void send_draw_data(char* data, int size)
{
bcm2835_spi_transfern(data, size);
}




В процессе инициализации экрана обнаружилась досадная вещь — для запуска необходимо передёрнуть RESET экрана, а вот последующие манипуляции данным выводом приводят экран в ступор и он перестаёт реагировать на внешние воздействия. Приходится сбрасывать его по питанию. Это необходимо учесть при разработке программы дабы процедура аппаратного сброса выполнялась лишь единожды.
Инициализация экрана


void reset_LCD()
{
// аппаратный сброс
bcm2835_gpio_write(LCD_RESET, LOW);
bcm2835_delay(50);
bcm2835_gpio_write(LCD_RESET, HIGH);
bcm2835_delay(50);

// программный сброс
send_cmd(CMD_RESET);
}

void init_LCD()
{
reset_LCD();

send_cmd(CMD_MEMORY_ACCESS_CONTROL);
send_data(0b00000000);

send_cmd(CMD_WAKEUP);

bcm2835_delay(20);

send_cmd(CMD_PALETTE);
send_data(_16_BIT_COLOR);

bcm2835_delay(20);

send_cmd(CMD_ENABLE);
}




Это была подготовка, а теперь самое вкусное — попробуем вывести на экран 16-битную картинку. Первый блин как известно — комом, после запуска программы я получил довольно странное изображение, оказалось — неверный порядок байт, после исправления всё заработало.
Код


int main(int argc, char **argv)
{
if (!init_gpio())
return 1;

init_LCD();

set_draw_area(0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1);
draw_start();

uint16_t screen[SCREEN_HEIGHT][SCREEN_WIDTH];

FILE* f_scr = fopen("test.bmp", "r");
fseek(f_scr, 0x42, SEEK_SET); // skip bmp header

fread(&screen, 1, SCREEN_HEIGHT * SCREEN_WIDTH * 2/*16bit*/, f_scr);
fclose(f_scr);

// change byte order
for(int x = 0; x < SCREEN_WIDTH; x++)
for(int y = 0; y < SCREEN_HEIGHT; y++)
screen[y][x] = (screen[y][x] >> 8) | (screen[y][x] << 8);

send_draw_data((char*)&screen[0][0], SCREEN_WIDTH*SCREEN_HEIGHT*2/*16 bit*/);

close_gpio();
return 0;
}




изображение до и после правок




LCD как монитор




С самого начала экспериментов меня не покидала мысль использовать экран как монитор для «малины», что я и поспешил реализовать.

Идея проста — изображение берётся из /dev/fb0, оно там как раз 16-битное, ресайзится и выдаётся на экран.

Так как результат сжатия картинки 1024x768 => 176x132 малоинформативен, для фреймбуфера было установлено разрешение 320x240, это можно сделать правкой config.txt на FAT разделе «малиновой» флешки.

framebuffer_width=320
framebuffer_height=240




После этого изображение всё равно жмётся с использованием примитивной интерполяции, но результат уже можно назвать приемлемым. Так-же был добавлен пропуск одинаковых кадров для экономии CPU.
Исходник LPH9157-2_RPI.c


#include <bcm2835.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>

#include <time.h>
#include <string.h>
#include <unistd.h>

// соответствия контактов GPIO и LCD
#define LCD_RS RPI_V2_GPIO_P1_11
#define LCD_RESET RPI_V2_GPIO_P1_13

#define RS_CMD 0
#define RS_DATA 1

#define CMD_RESET 0x01
#define CMD_MEMORY_ACCESS_CONTROL 0x36 // Memory Access Control
#define CMD_WAKEUP 0x11 // Выход из спящего режима
#define CMD_PALETTE 0x3A // Установка цветовой палитры
#define CMD_ENABLE 0x29 //Включение дисплея

#define CMD_SET_X 0x2A // задаем область по X
#define CMD_SET_Y 0x2B // задаем область по Y
#define CMD_START_WRITE 0x2C // начало записи в память

#define _8_BIT_COLOR 0x02
#define _12_BIT_COLOR 0x03
#define _16_BIT_COLOR 0x05

#define SCREEN_WIDTH 132
#define SCREEN_HEIGHT 176

int init_gpio()
{
if (!bcm2835_init())
return 0;

bcm2835_spi_begin();
bcm2835_spi_setBitOrder(BCM2835_SPI_BIT_ORDER_MSBFIRST);
bcm2835_spi_setDataMode(BCM2835_SPI_MODE0); /// CPOL = 0, CPHA = 0, Clock idle low,
/// data is clocked in on rising edge,
/// output data (change) on falling edge
bcm2835_spi_setClockDivider(BCM2835_SPI_CLOCK_DIVIDER_16); ///< 16 = 64ns = 15.625MHz
bcm2835_spi_chipSelect(BCM2835_SPI_CS0); /// Select Our Device
bcm2835_spi_setChipSelectPolarity(BCM2835_SPI_CS0, LOW);

// настроим порты на запись
bcm2835_gpio_fsel(LCD_RS, BCM2835_GPIO_FSEL_OUTP);
bcm2835_gpio_fsel(LCD_RESET, BCM2835_GPIO_FSEL_OUTP);

return 1;
}

int close_gpio()
{
bcm2835_spi_end();
bcm2835_close();
}

void send_cmd(char cmd)
{
bcm2835_gpio_write(LCD_RS, RS_CMD);
bcm2835_spi_transfer(cmd);
}

void send_data(char data)
{
bcm2835_gpio_write(LCD_RS, RS_DATA);
bcm2835_spi_transfer(data);
}

void send_data_array(char* data, int size)
{
bcm2835_gpio_write(LCD_RS, RS_DATA);
bcm2835_spi_transfern(data, size);
}

void set_draw_area(char x1, char y1, char x2, char y2)
{
send_cmd(CMD_SET_X);
send_data(x1);
send_data(x2);

send_cmd(CMD_SET_Y);
send_data(y1);
send_data(y2);
}

void draw_start()
{
send_cmd(CMD_START_WRITE);
bcm2835_gpio_write(LCD_RS, RS_DATA);
}

void send_draw_data(char* data, int size)
{
bcm2835_spi_transfern(data, size);
}

void reset_LCD()
{
bcm2835_gpio_write(LCD_RESET, LOW);
bcm2835_delay(50);
bcm2835_gpio_write(LCD_RESET, HIGH);
bcm2835_delay(50);

send_cmd(CMD_RESET);
}

void init_LCD()
{
reset_LCD();

send_cmd(CMD_MEMORY_ACCESS_CONTROL);
send_data(0b00000000);

send_cmd(CMD_WAKEUP);

bcm2835_delay(20);

send_cmd(CMD_PALETTE);
send_data(_16_BIT_COLOR);

bcm2835_delay(20);

send_cmd(CMD_ENABLE);
}

#define FB_WIDTH 320// 176
#define FB_HEIGHT 240// 144

int main(int argc, char **argv)
{
if (!init_gpio())
return 1;

int smooth = 0;
int dynamic_fps = 0;
int argn = 1;
while(argn < argc)
{
if(argv[argn][0] == '-')
switch(argv[argn][1])
{
case 'i':
init_LCD();
close_gpio();
printf("lcd initialized\n");
return 0;
break;

case 's':
smooth = 1;
break;
case 'd':
dynamic_fps = 1;
break;
default:
printf("Usage: lcd [options]\n");
printf("Options:\n");
printf(" -i initialize lcd (hardware reset)\n");
printf(" -d dynamic FPS (skip same frames)\n");
printf(" -s smooth image (enable basic intrpolation)\n");
return 0;
break;
}
argn++;
}

///------------------------------------------------
/// draw screen

set_draw_area(0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1);
draw_start();

uint16_t screen[SCREEN_HEIGHT][SCREEN_WIDTH];

uint16_t old_fb[FB_HEIGHT * FB_WIDTH];

int fd_scr = open("/dev/fb0", O_RDONLY);
int scr_sz = FB_HEIGHT * FB_WIDTH * 2/*16bit*/;
uint16_t* fb_screenshot = mmap(0, scr_sz, PROT_READ, MAP_PRIVATE, fd_scr, 0);

// scaling
float scale_X = FB_HEIGHT / (float)SCREEN_WIDTH;
float scale_Y = FB_WIDTH / (float)SCREEN_HEIGHT;

int frame_cnt = 0;
struct timespec ts1, ts2;
clock_gettime(CLOCK_MONOTONIC, &ts1);

for(;;) // forever
{
if(dynamic_fps)
if(memcmp(&old_fb, fb_screenshot, sizeof(old_fb)) == 0)
{
usleep(10000);
continue;
}
else
{
memcpy(&old_fb, fb_screenshot, sizeof(old_fb));
}

for(int x = 0; x < SCREEN_WIDTH; x++)
for(int y = 0; y < SCREEN_HEIGHT; y++)
{
int fb_x = y * scale_X;
int fb_y = x * scale_Y;

uint16_t px = fb_screenshot[fb_x + fb_y * FB_WIDTH];

if(smooth)
{
// look around
if((fb_x - 1 >= 0) && (fb_x + 1 < FB_WIDTH) && (fb_y - 1 >= 0) && (fb_y + 1 < FB_HEIGHT))
{
for(int dx = -1; dx <= 1; dx++)
for(int dy = -1; dy <= 1; dy++)
if((dx == 0) ^ (dy == 0))
{
uint16_t add_px = fb_screenshot[(fb_x + dx) + (fb_y + dy) * FB_WIDTH];
px = ((px & 0xf7de) >> 1) + ((add_px & 0xf7de) >> 1);
/// ^thank you habr => http://ift.tt/1vIjXyi
}
}
}
screen[y][SCREEN_WIDTH - 1 - x] = (px << 8) | (px >> 8);
}

send_draw_data((char*)&screen[0][0], sizeof(screen));

/// calc fps
frame_cnt++;
if(frame_cnt >= 100)
{
clock_gettime(CLOCK_MONOTONIC, &ts2);

float allsec = (ts2.tv_sec - ts1.tv_sec) + (ts2.tv_nsec - ts1.tv_nsec) / 1000000000.0;
float fps = frame_cnt / allsec;

printf("%f FPS\n", fps);

frame_cnt = 0;
clock_gettime(CLOCK_MONOTONIC, &ts1);
}

usleep(1000);
}

munmap(fb_screenshot, scr_sz);
close(fd_scr);
close_gpio();

printf("fin\n");
return 0;
}





Сборка:

pi@raspberrypi ~ $ gcc -o lcd LPH9157-2_RPI.c -lbcm2835 -lrt -std=gnu99



Запуск:

pi@raspberrypi ~ $ sudo ./lcd -i
pi@raspberrypi ~ $ sudo ./lcd -d -s




параметры:

-i — первичная инициализация (дёргаем хардварный reset)

-s — включить сглаживание

-d — динамический fps (одинаковые кадры пропускаются — экономит CPU)
Результат




This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий