- PVSM.RU - https://www.pvsm.ru -
Сейчас появилось достаточно много различных дешевых одноплатников с очень достойными характеристиками, которые вполне можно назвать экономичными и портативными. Однако очень часто встает вопрос вывода изображения на дисплей: к сожалению, в подобные устройства обычно ставят урезанные версии чипсетов без видеовыхода на обычные матрицы. Конечно в них практически всегда есть HDMI, но это совершенно не выход для портативного устройства: прожорливый чип скалера будет очень негативно влиять на время работы от АКБ. Да и сами подобные дисплеи очень дорогие: почти 2.000 рублей за матрицу со скалером — это действительно бьет по карману. Сегодня я расскажу Вам о существующих протоколах для дисплеев, подскажу, как применить экранчики от старых навигаторов/мобильников и мы подключим с вами SPI-дисплей к одноплатнику без видеовыхода. Причем мы реализуем как просто библиотеку, которая позволяет выводить произвольную графику из ваших программ, так
и службу, которая будет напрямую копировать данные из фреймбуфера и преобразовывать в формат для нашего дисплея. Интересно? Тогда жду вас в статье!
На самом деле, существует достаточно много различных физических протоколов для общения с дисплеями. На программном уровне, общение с ними относительно стандартизированно, однако на аппаратном уровне различий довольно много. Самые распространенные из них:
Я не стал упоминать «большие» протоколы типа HDMI или eDP — они так или иначе, в физическом плане близки к MIPI DSI. Как видите — протоколов много и самых разных, соответственно и дисплеи нужно искать в разных местах. Дешевые DIY-дисплеи можно найти за довольно разумные деньги на алике — 1.8" матрицы на момент написания статьи стоили ~200 рублей, 2.4 — ~400 рублей, 3.5 и выше — от 700 рублей и выше. Пичем Вы вольны выбирать интерфейс — кому-то удобнее SPI, кому-то удобнее 8080. Я лично выбрал SPI — поскольку он есть в «хардварном» виде на большинстве одноплатников и доступен для программирования как из обычного пользовательского режима (т.е можно пользоваться шиной из обычной программы), так и из драйверов.
Однако есть способ найти дисплеи «бесплатно» — из старых и нерабочих устройств. Например, из автомобильных навигаторов. Недавно читатель с DTF предложил заслать с 10-ок подобных девайсов, я конечно же согласился! Что самое приятное в них — так это то, что дисплеи там обычно стандартизированы — как по размерам, так и по шлейфу. Суть вот в чем: китайские компании довольно долго производили 4" дисплеи с разрешением 480x232 и резистивным тачскрином.

Поэтому Вы практически на 100% можете быть уверены, что один дисплей подойдет к другому навигатору и покажет картинку (а если нет — то открываем даташит на дисплей и корректируем тайминги). Эти дисплеи используют TTL/RGB протокол, поэтому для того, чтобы с ними работать, вам понадобится либо много свободных пинов, либо превратить микроконтроллер в видеоконтроллер (Raspberry Pi Pico/ESP32 должен с этим справиться без проблем). Большинство из этих дисплеев работает в 16-битном режиме, т.е до 65536 цветов. Ниже прилагаю распиновку к ним:

Для более удобно подключения, можно использовать такие [1] breakout-платы для 40-пин шлейфов. Я себе заказал несколько, в том числе и для паябельных шлейфов от старых мобилок. Стоят на алике копейки — в среднем, 100 рублей за 5 плат (берите 40 пин/0.5мм).

На некоторых одноплатниках уже есть готовый 40-пин коннектор для подключения ваших дисплеев. Большинство из них базируется на базе чипсетов AllWinner F1C100s/F1C200s/V3s и экран работает там «из коробки», за исключением тачскрина (с ним надо повозиться), известные мне — Lctech Pi, MangoPi (извиняюсь за плохое качество фото, это с моего сайд-проекта):

Если Вам нужен маленький дисплей, то можно взять оный от старого нерабочего кнопочного телефона. Из самых простых — Siemens C65, S65, M65, A55, A65. Эти дисплеи работают по протоколу SPI и к ним легко подпаяться. Как еще один из вариантов — дисплей от «народного» Motorola C350, который работает через интерфейс SPI, но требует 12-битного формата на цвет:

Обратите внимание, что для этих дисплеев нужно самому мастерить бустер подсветки: от 3.7в они не заведутся. Сименсовским дисплеям нужно 12в — связано это с тем, что светодиоды в подсветке подключены последовательно, дабы уменьшить потребление. Если есть желание — можно разобрать модуль и перепаять светодиоды параллельно, но «кушать» такая сборка будет ощутимо, проще взять step-up преобразователь до 12В с алика за пару соток.
MIPI дисплеи можно достать из копеечных старых смартфонов ZTE/Lenovo/МТС/Билайн и.т.п. Предпочтительнее здесь именно именитые бренды, поскольку и ZTE и Lenovo делятся исходниками прошивки — так что можно будет найти команды инициализации и самому запустить дисплей. Кроме инициализации дисплея, там же можно будет найти и драйвер тачскрина — обычно они общаются по протоколу I2C и при очень большом желании, можно будет заставит работать и его.

Для работы с ними, я также рекомендую Breakout-платы, а схему на коннектор дисплея можно найти в сервисмануале или схеме устройства (если таковой имеется для вашего смартфона). Для Lenovo подобные ищутся без проблем, но для топовых Samsung S2/S3/S4 с крутыми OLED-дисплеями за MIPI-дисплеи придётся забыть, т.к схем в открытом доступе нет.

8080 дисплеи можно достать из старых китайских «кнопочников». Ищите те модели, на которые есть сервис-мануал (Fly DS124 и другие модели, некоторые Explay), тогда Вы сможете прочесть ID дисплея из регистра 0x0 (вида 0x9325/0x7739 и.т.п), найти даташит на интересующий вас контроллер и использовать его в своем проекте. В этих дисплеях самое приятное — паябельный шлейф и подсветка 5в, которая будет работать и на 3.7в, но немного тусклее.

Если же Вам хотелось бы экранчик побольше, с разрешением 480x320, то смотрите в сторону очень дешевых мобильников из начала 2010х — Explay N1, Fly Jazz, Fly Wizard. Вполне может быть так, что у Вас лежит подобный девайс будучи разбитым или утопленным, а дисплей остался. Кстати, если вдруг у вас лежит один из подобных ультрадешевых китайчиков, но вам они не нужны — пишите в ЛС, есть идеи для проектов с ними.

Обратите внимание, что эти дисплеи используют 18-битный физический интерфейс, но для программного доступа должно хватать 16-бит. Кроме того, на этом шлейфе есть пин IM0 — он отвечает за установку режима работы контроллера дисплея. Если бы у нас был еще IM1 и IM2, то мы могли бы хоть режим SPI установить, но в данном случае, мы можем установить либо 8-битный режим, либо 16-битный. Можете отследить пин IM0 на шлейфе и если он идет к обвязке, где предположительно разрывается/соединяется IM1/IM2, то можете попробовать разорвать/кинуть на них высокий уровень. Насчет подсветки на таких дисплеях пока что не знаю. Если распиновки на телефон нет, то поищите диагностические пятачки под коннектором, с осциллографом или даже просто тестером можно попытаться найти распиновку.

На этом предлагаю перейти к практической реализации нашего драйвера дисплея. Как я уже говорил, реализовать его можно двумя способами: в виде user-space библиотеки для вывода картинки из обычных программ, так и kernel-mode драйвер, который будет реализовать framebuffer, что позволит выводить туда и X Window System, и SDL — что душе угодно.
У каждого подхода есть плюсы и минусы. Перечисляю их:
Работать мы будем с простеньким 1.8" дисплеем, который имеет разрешение 128x160, работает на контроллере ST7739.
В качестве одноплатника я взял Orange Pi One. Брал я его на вторичке за 1.000 рублей, однако продавец меня порадовал и положил не один, а два девайса — в благодарность за статьи о Orange Pi 3G IoT :) Сейчас старые модели RPi и Orange Pi (но не их Mini и Zero версии) стоят копейки.

Накатываем систему на флэшку (я выбрал Debian с ядром 3.4 — то которое еще не имело поддержки DeviceTree) и идем изучать гребенку:

Видим SPI? Он нам и нужен! Подключаем питание дисплея (3.3В на VCC, 5В на LED и не забываем землю), подключаем сигнальные линии (SCK — CLK, SDA — MOSI, A0 и RESET — цепляем на произвольный GPIO, на котором «ничего нет», я выбрал PA10 и PA20 пины). Если SPI Вам нужен только для дисплея, то можно просто поставить перемычку между CS и землей. Оставлять его «в воздухе» нельзя — иначе дисплей не будет работать.

Если подключили все верно, то при включении одноплатника, Вы увидите подсветку.
Теперь для того, чтобы им управлять, нам нужно получить доступ к шине SPI и проинициализировать контроллер. Для этого убеждаемся в том, что у нас есть spidev в каталоге /dev/, где spidev0.0 — первый контроллер SPI с первой линией CS, spidev0.1 — первый контроллер SPI с второй линией CS. У OrangePi One в стоке он только один — а для CS предлагается использовать sysfs. Кроме этого, нам нужно «экспортировать» из задать направлением пинам, которые мы будем использовать для сигналов RESET и DC. Для этого пишем номера пинов на гребенке прямо в устройство /sys/class/gpio/export, например так:
echo 10 > /sys/class/gpio/export
echo 20 > /sys/class/gpio/export
echo out > /sys/class/gpio/gpio20/direction
echo out > /sys/class/gpio/gpio10/direction
Обратите внимание, что в свежих версиях ядра появилось нормальное API для доступа к GPIO из userspace, управлять пинами через sysfs — в какой-то степени считается плохим тоном.
Открываем устройство как обычный файл:
fd = open("/dev/spidev0.0", O_RDWR | O_NONBLOCK);
dcFd = open("/sys/class/gpio/gpio10/value", O_RDWR);
resetFd = open("/sys/class/gpio/gpio20/value", O_RDWR);
И отправляем контроллер дисплея в RESET:
gpHelperSetState(resetFd, 0);
usleep(250000); // 250ms
gpHelperSetState(resetFd, 1);
После этого, реализовываем методы для передачи данных через SPI. В Linux, общение через эту шину идёт посредством транзакции, причем размер одной транзакции ограничен конкретным SPI-контроллером. В случае AllWinner, тут от 64, до 128 байт. Для каждой транзакции можно установить тактовую частоту — AllWinner поддерживает до ~100мгц.
void CLCM::Command(unsigned char cmd)
{
spi_ioc_transfer tf;
memset(&tf, 0, sizeof(tf));
tf.bits_per_word = 8;
tf.len = 1;
tf.speed_hz = 64000000;
tf.tx_buf = (unsigned long)&cmd;
gpHelperSetState(dcFd, 0);
if(ioctl(fd, SPI_IOC_MESSAGE(1), &tf) < 0)
LOG("SPI transfer failedn");
}
void CLCM::Data(unsigned char data)
{
spi_ioc_transfer tf;
memset(&tf, 0, sizeof(tf));
tf.bits_per_word = 8;
tf.len = 1;
tf.speed_hz = 64000000;
tf.tx_buf = (unsigned long)&data;
gpHelperSetState(dcFd, 1);
if(ioctl(fd, SPI_IOC_MESSAGE(1), &tf) < 0)
LOG("SPI transfer failedn");
}
Теперь нам нужно инициализировать дисплей. Для этого, нужно передать ему несколько команд, которые задают настройки развертки, поворота, внутренние настройки цветности и.т.п:
void CLCM::SoftwareReset()
{
Command(0x11);//Sleep out
usleep(120000);
//ST7735R Frame Rate
Command(0xB1);
Data(0x01);
Data(0x2C);
Data(0x2D);
Command(0xB2);
Data(0x01);
Data(0x2C);
Data(0x2D);
Command(0xB3);
Data(0x01);
Data(0x2C);
Data(0x2D);
Data(0x01);
Data(0x2C);
Data(0x2D);
//------------------------------------End ST7735R Frame Rate-----------------------------------------//
Command(0xB4);//Column inversion
Data(0x07);
//------------------------------------ST7735R Power Sequence-----------------------------------------//
Command(0xC0);
Data(0xA2);
Data(0x02);
Data(0x84);
Command(0xC1);
Data(0xC5);
Command(0xC2);
Data(0x0A);
Data(0x00);
Command(0xC3);
Data(0x8A);
Data(0x2A);
Command(0xC4);
Data(0x8A);
Data(0xEE);
//---------------------------------End ST7735R Power Sequence-------------------------------------//
Command(0xC5);//VCOM
Data(0x0E);
Command(0x36);//MX, MY, RGB mode
Data(0xC8);
//------------------------------------ST7735R Gamma Sequence-----------------------------------------//
Command(0xe0);
Data(0x02);
Data(0x1c);
Data(0x07);
Data(0x12);
Data(0x37);
Data(0x32);
Data(0x29);
Data(0x2d);
Data(0x29);
Data(0x25);
Data(0x2b);
Data(0x39);
Data(0x00);
Data(0x01);
Data(0x03);
Data(0x10);
Command(0xe1);
Data(0x03);
Data(0x1d);
Data(0x07);
Data(0x06);
Data(0x2e);
Data(0x2c);
Data(0x29);
Data(0x2d);
Data(0x2e);
Data(0x2e);
Data(0x37);
Data(0x3f);
Data(0x00);
Data(0x00);
Data(0x02);
Data(0x10);
Command(0x2A);
Data(0x00);
Data(0x02);
Data(0x00);
Data(0x81);
Command(0x2B);
Data(0x00);
Data(0x01);
Data(0x00);
Data(0xA0);
//------------------------------------End ST7735R Gamma Sequence-----------------------------------------//
//Command(0x3A);
//Data(0x05);
Command(0x3A);//65k mode
Data(0x05);
Command(0x2C);//Display on
Command(0x29);//Display on
// Set viewport
int x1 = 0;
int x2 = 128;
int y1 = 0;
int y2 = 160;
Command(0x2A);
Data(x1>>8);
Data(x1);
Data(x2>>8);
Data(x2);
Command(0x2B);
Data(y1>>8);
Data(y1);
Data(y2);
Data(y2);
Command(0x2C); // Начинает запись фреймбуфера в память
}
Для передачи фреймбуфера, мы реализовываем отдельный метод, который разобьёт его на транзакции. В нашем случае, фреймбуфер занимает 128 * 160 * 2 = 40960 байт, делим на 64, получаем 640 транзакций на передачу одного кадра.
void CLCM::Bitmap(void* data, int len)
{
gpHelperSetState(dcFd, 1);
for(int i = 0; i < len / 64; i++)
{
spi_ioc_transfer tf;
memset(&tf, 0, sizeof(tf));
tf.bits_per_word = 8;
tf.len = 64;
tf.speed_hz = 32000000;
tf.tx_buf = (unsigned long)data;
data += 64;
if(ioctl(fd, SPI_IOC_MESSAGE(1), &tf) < 0)
LOG("SPI transfer failedn");
}
}
Компилируем нашу программу, запускаем и видим: на дисплее появился мусор, а это значит, что он успешно проинициализирован. Если у Вас всё равно белый дисплей — смотрите подключение и убедитесь, что подключили сигнальные линии RESET/DC куда надо. После инициализации, на DC должен быть логический 0 (0В), на RESET — логический 1 (3.3В).
Пишем простенький загрузчик TGA и выводим картинку на экран:
CImage* img = CImage::FromFile("test.tga");
if(img)
Bitmap(img->RGB, img->Width * img->Height * 2);
Всё работает и у нас есть картинка на дисплее! Производительность системы, скажем так, оптимальная, но учтите: чем выше разрешение, тем выше нагрузка на ядро!
Это всё конечно замечательно, однако зачастую есть необходимость отображать картинку, которые рисуют другие программы — X Window System, или, например, порт эмулятора денди на SDL1.2. Для этого, нам нужен способ выводить на наш дисплейчик то, что рисуется в главный фреймбуфер — /dev/fb0. И для этого, у нас есть целых два способа:
Именно второй способ мы и выберем в силу его некоторой диковинности. Фреймбуфер Linux имеет одну очень приятную особенность: он способен сам выполнять преобразования формата пикселей и динамически менять размер рабочего пространства. Мы можем просто попросить драйвер установить комфортный для нашего дисплея режим (128x160), цветность (RGB565) и читать уже готовые битмапы, по необходимости пересылая их на дисплей.
Давайте напишем простенькую службу для этого. Наша служба должна быть универсальной, дабы уметь выводить картинку на несколько разных дисплеев, в зависимости от статически слинкованных с ней драйверов. Для этого мы сразу определяем структуру, описывающую процедуру инициализации и вывода уже готовой картинки на экран:
struct CLCM
{
char* name;
int width, height;
void(*init)();
void(*presentBuffer)(void* buf);
};
CLCM lcm7735
{
.name = "ST7735",
.width = 128,
.height = 160,
.init = &st7735Init,
.presentBuffer = &st7735Bitmap
};
CLCM* lcmList[] = {
&lcm7735
};
Теперь у нашей службы есть некоторая гибкость. Захотели — поставили дисплей на базе ILI9341, захотели — на базе ILI9325, достаточно лишь портировать код инициализации.
Открываем всем необходимые устройства и назначаем нашему фреймбуферу желаемое разрешение. Обратите внимание, что мы можем весь буфер кадра отобразить в наш процесс с помощью mmap: это гораздо быстрее и экономичнее к памяти, чем выделять отдельный буфер под read/write.
bool setupFrameBuffer()
{
LOG("Open framebuffer device");
fbDevice = open("/dev/fb0", O_RDWR);
if(!fbDevice)
{
LOG("Failed to open primary framebuffer");
return false;
}
ioctl(fbDevice, FBIOGET_VSCREENINFO, &fbVar);
fbVar.xres = lcm->width;
fbVar.yres = lcm->height;
if(ioctl(fbDevice, FBIOPUT_VSCREENINFO, &fbVar) < 0)
{
LOG("Unable to set framebuffer size :c");
return false;
}
ioctl(fbDevice, FBIOGET_VSCREENINFO, &fbVar); // Get yet another time for test
LOGF("Parent FB: %ix%i %i-bits", fbVar.xres, fbVar.yres, fbVar.bits_per_pixel);
ioctl(fbDevice, FBIOGET_FSCREENINFO, &fbFix);
fbMem = (char*)mmap(0, fbFix.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fbDevice, 0);
buf = (unsigned short*)malloc(lcm->width * lcm->height * 2);
if(!fbMem)
{
LOG("mmap failed");
return false;
}
return true;
}
К сожалению, в случае с OrangePi, мне не удалось запросить драйвер обрабатывать картинку в формате RGB565, поэтому для вывода пришлось выделять внешний буфер, где мы на лету конвертируем картинку из 32х-битного RGB в 16-битный.
__inline unsigned short lcmTo565(unsigned int r, unsigned int g, unsigned int b)
{
short ret = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3);
return bswap_16(ret);
}
Ну и переходим, собственно, к копированию фреймбуфера на наш дисплей:
void lcmCopyFramebuffer()
{
int bpp = fbVar.bits_per_pixel / 8;
for(int i = 0; i < lcm->width; i++)
{
for(int j = 0; j < lcm->height; j++)
{
unsigned char* rgbData = (unsigned char*)&fbMem[(j * fbFix.line_length) + (i * bpp)];
buf[j * lcm->width + i] = lcmTo565(rgbData[0], rgbData[1], rgbData[2]);
}
}
lcm->presentBuffer(buf);
}
Да, это вся программа. Тестируем наш результат:

Работает! Теперь если мы захотим запустить, например, эмуляторы, или вывести иксы на внешний экранчик — то мы смоежм сделать это без каких либо проблем.
Как видите, даже из казалось бы, из неактуальных и нерабочих гаджетов можно вытащить дисплеи для собственных проектов. Документация по протоколам доступна в свободном доступе в сети, да и со схемами уже не так сложно, как в нулевых.
Даже с практический точки зрения нет никаких проблем в том, чтобы подключить дисплейчик даже к устройствам, где подобный видеовыход и не предусмотрен. Надеюсь этот подробный материал окажется полезным моим читателям. Само собой, я создал репозиторий на гитхабе [2] и запушил туда все наработки из сегодняшней статьи.
Автор: Богдан
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/diy/386625
Ссылки в тексте:
[1] такие: https://aliexpress.ru/item/1005005333447959.html
[2] репозиторий на гитхабе: https://github.com/monobogdan/st7735_lcm/tree/main
[3] Источник: https://habr.com/ru/companies/timeweb/articles/753062/?utm_source=habrahabr&utm_medium=rss&utm_campaign=753062
Нажмите здесь для печати.