Прочитав монументальную серию статей о подключении LCD экрана к роутеру мне захотелось сделать то же самое. Однако многообразие используемого стека (openwrt, stm32, usb) в сочетании с отсутствием полных исходников кода но может плохо искал несколько затруднило задачу. Я решил начать с малого — написать свою реализацию framebuffer для raspberry и вывести графическую среду raspberry на LCD. Что из этого получилось, описываю далее.
Вообще-то имеются готовые драйверы для LCD (проект tbtft), но мы напишем свой, чтобы лучше понять как все устроено.
LCD 320×240 с контроллером ILI9341. Передача данных по 8 битной шине.
Запись данных в LCD осуществляется следующим образом (стр.28):
1 на RD и 1 на RESET после старта LCD держим все время. Перед передачей данных подаем 0 на CS, выставляем 8 бит данных на шине, устанавливаем 1 или 0 на RS (D/CX на графике) в зависимости от типа передачи — данные / команда, сбрасываем WR в 0, затем устанавливаем в 1. После окончания передачи данных выставляем CS в 1.
/* файл lcd.c */
void LCD_write(u8 VAL)
{
LCD_CS_CLR;
DATAOUT(VAL);
LCD_WR_CLR;
LCD_WR_SET;
LCD_CS_SET;
}
/* передача команды */
void LCD_WR_REG(u8 data)
{
LCD_RS_CLR;
LCD_write(data);
}
/* передача данных */
void LCD_WR_DATA(u8 data)
{
LCD_RS_SET;
LCD_write(data);
}
/* запись значения в регистр */
void LCD_WriteReg(u8 LCD_Reg, u8 LCD_RegValue)
{
LCD_WR_REG(LCD_Reg);
LCD_WR_DATA(LCD_RegValue);
}
/* передача 16 бит данных */
void Lcd_WriteData_16Bit(u16 Data)
{
LCD_RS_SET;
LCD_CS_CLR;
DATAOUT((u8)(Data>>8));
LCD_WR_CLR;
LCD_WR_SET;
DATAOUT((u8)Data);
LCD_WR_CLR;
LCD_WR_SET;
LCD_CS_SET;
}
Основной код управления LCD (для STM32), в основном взят отсюда и адаптирован для raspberry. Цвет каждого пикселя на LCD задается 16 битами в формате RGB565 (5 бит на красный цвет, 6 на зеленый, 5 на синий).
/* файл lcd.h */
#define LCD_W 320
#define LCD_H 240
/* файл lcd.c */
/* индикация того, что далее передаются данные для видеобуфера */
void LCD_WriteRAM_Prepare(void)
{
LCD_WR_REG(0x2C);
}
/* задаем прямоугольник на экране, который будем отрисовывать */
void LCD_SetWindows(u16 xStart, u16 yStart,u16 xEnd,u16 yEnd)
{
LCD_WR_REG(0x2A);
LCD_WR_DATA(xStart>>8);
LCD_WR_DATA(0x00FF&xStart);
LCD_WR_DATA(xEnd>>8);
LCD_WR_DATA(0x00FF&xEnd);
LCD_WR_REG(0x2B);
LCD_WR_DATA(yStart>>8);
LCD_WR_DATA(0x00FF&yStart);
LCD_WR_DATA(yEnd>>8);
LCD_WR_DATA(0x00FF&yEnd);
LCD_WriteRAM_Prepare();
}
/* ресет экрана */
void LCD_RESET(void)
{
LCD_RST_CLR;
delay(100);
LCD_RST_SET;
delay(50);
}
/* инициализация экрана */
void LCD_Init(void)
{
LCD_RESET();
LCD_WR_REG(0xCF);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0xC9);
LCD_WR_DATA(0X30);
LCD_WR_REG(0xED);
LCD_WR_DATA(0x64);
LCD_WR_DATA(0x03);
LCD_WR_DATA(0X12);
LCD_WR_DATA(0X81);
LCD_WR_REG(0xE8);
LCD_WR_DATA(0x85);
LCD_WR_DATA(0x10);
LCD_WR_DATA(0x7A);
LCD_WR_REG(0xCB);
LCD_WR_DATA(0x39);
LCD_WR_DATA(0x2C);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x34);
LCD_WR_DATA(0x02);
LCD_WR_REG(0xF7);
LCD_WR_DATA(0x20);
LCD_WR_REG(0xEA);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x00);
LCD_WR_REG(0xC0);
LCD_WR_DATA(0x1B);
LCD_WR_REG(0xC1);
LCD_WR_DATA(0x00);
LCD_WR_REG(0xC5);
LCD_WR_DATA(0x30);
LCD_WR_DATA(0x30);
LCD_WR_REG(0xC7);
LCD_WR_DATA(0XB7);
LCD_WR_REG(0x36);
LCD_WR_DATA(0x08);
LCD_WR_REG(0x3A);
LCD_WR_DATA(0x55);
LCD_WR_REG(0xB1);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x1A);
LCD_WR_REG(0xB6);
LCD_WR_DATA(0x0A);
LCD_WR_DATA(0xA2);
LCD_WR_REG(0xF2);
LCD_WR_DATA(0x00);
LCD_WR_REG(0x26);
LCD_WR_DATA(0x01);
LCD_WR_REG(0xE0);
LCD_WR_DATA(0x0F);
LCD_WR_DATA(0x2A);
LCD_WR_DATA(0x28);
LCD_WR_DATA(0x08);
LCD_WR_DATA(0x0E);
LCD_WR_DATA(0x08);
LCD_WR_DATA(0x54);
LCD_WR_DATA(0XA9);
LCD_WR_DATA(0x43);
LCD_WR_DATA(0x0A);
LCD_WR_DATA(0x0F);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x00);
LCD_WR_REG(0XE1);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x15);
LCD_WR_DATA(0x17);
LCD_WR_DATA(0x07);
LCD_WR_DATA(0x11);
LCD_WR_DATA(0x06);
LCD_WR_DATA(0x2B);
LCD_WR_DATA(0x56);
LCD_WR_DATA(0x3C);
LCD_WR_DATA(0x05);
LCD_WR_DATA(0x10);
LCD_WR_DATA(0x0F);
LCD_WR_DATA(0x3F);
LCD_WR_DATA(0x3F);
LCD_WR_DATA(0x0F);
LCD_WR_REG(0x2B);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x01);
LCD_WR_DATA(0x3f);
LCD_WR_REG(0x2A);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0x00);
LCD_WR_DATA(0xef);
LCD_WR_REG(0x11);
delay(120);
LCD_WR_REG(0x29);
LCD_WriteReg(0x36,(1<<3)|(1<<5)|(1<<6));
}
/* заполняем экран одним цветом */
void LCD_Clear(u16 Color)
{
unsigned int i;
LCD_SetWindows(0,0,LCD_W-1,LCD_H-1);
for(i=0;i<LCD_H*LCD_W;i++)
{
Lcd_WriteData_16Bit(Color);
}
}
/* рисуем картинку из raw файла (в нем подряд идут цвета пикселей в формате RGB565) */
void LCD_draw_image(char *file){
int fd = open(file, O_RDWR);
if(fd < 0){
perror("Open file");
exit(1);
}
u16 buffer[128];
LCD_SetWindows(0,0,LCD_W-1,LCD_H-1);
while(1){
int nread = read(fd, buffer, 256);
if(nread == 0 || nread < 0)
break;
/* buffer[i] - 2 байта, поэтому пишем nread/2 раз */
for(int i=0; i < nread/2; i++){
Lcd_WriteData_16Bit(buffer[i]);
}
}
close(fd);
}
Я использую raspberry pi 3 с установленным raspbian lite (версия ядра 4.14). GUI добавлено установкой пакетов lxde и xinit.
sudo apt-get install lxde xinit
В raspberry GPIO можно управлять через прямое обращение к памяти. Из мануала к BCM 2837 32 битные регистры GPFSEL0-5 используются для установки режима GPIO. На каждый GPIO пин отводится 3 бита. Пину 0 соответствуют биты 2-0 в GPFSEL0, пину 1 биты 5-3 и т.д. Каждый регистр управляет 10 GPIO. Биты 000 соответствуют режиму input, биты 001 режиму output. Установку режима можно описать следующим образом:
/* файл rpi_gpio.h */
/* установка input режима */
#define INP_GPIO(g) *(gpio+((g)/10)) &= ~(7<<(((g)%10)*3))
/* установка output режима */
#define OUT_GPIO(g) *(gpio+((g)/10)) |= (1<<(((g)%10)*3))
Для пинов 0 — 31 в режиме output установка 1 делается через регистр GPSET0. Чтобы установить GPIO n в 1, в регистр нужно записать число, n-ый бит в котором равен 1. Например, для установки 1 в GPIO 10 и 11 в регистр GPSET0 необходимо записать число 0b11 << 10.
Аналогично, установка 0 осуществляется через регистр GPCLR0.
/* устанавливаем 1 на GPIO, например, 1 на GPIO 10 - GPIO_SET = 1<<10 */
#define GPIO_SET *(gpio+7)
/* устанавливаем 0 на GPIO, например, 0 на GPIO 10 - GPIO_CLR = 1<<10 */
#define GPIO_CLR *(gpio+10)
gpio — содержит виртуальный адрес физического адреса 0x3F200000 (отображенного посредством mmap в виртуальную память процесса). *gpio позволяет обратиться к GPFSEL0. *(gpio+7) к GPSET0. *(gpio+10) к GPCLR0.
/* файл rpi_gpio.c */
int setup_rpi_gpio()
{
unsigned int gpio_base_addr = 0x3F200000;
/* open /dev/mem */
if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
printf("can't open /dev/mem n");
return -1;
}
/* mmap GPIO */
gpio_map = mmap(
NULL, //Any adddress in our space will do
BLOCK_SIZE, //Map length
PROT_READ|PROT_WRITE,// Enable reading & writting to mapped memory
MAP_SHARED, //Shared with other processes
mem_fd, //File to map
gpio_base_addr //Offset to GPIO peripheral
);
close(mem_fd); //No need to keep mem_fd open after mmap
if (gpio_map == MAP_FAILED) {
printf("mmap error %dn", (int)gpio_map);//errno also set!
return -1;
}
// Always use volatile pointer!
gpio = (volatile uint32_t *)gpio_map;
return 0;
}
/* файл lcd.h */
#define BIT_BASE 12
#define CS 20
#define RS 21
#define RST 22
#define WR 23
#define RD 24
#define LCD_CS_SET GPIO_SET=(1<<CS)
#define LCD_RS_SET GPIO_SET=(1<<RS)
#define LCD_RST_SET GPIO_SET=(1<<RST)
#define LCD_WR_SET GPIO_SET=(1<<WR)
#define LCD_RD_SET GPIO_SET=(1<<RD)
#define LCD_CS_CLR GPIO_CLR=(1<<CS)
#define LCD_RS_CLR GPIO_CLR=(1<<RS)
#define LCD_RST_CLR GPIO_CLR=(1<<RST)
#define LCD_WR_CLR GPIO_CLR=(1<<WR)
#define LCD_RD_CLR GPIO_CLR=(1<<RD)
#define DATAOUT(x) GPIO_SET=(x<<BIT_BASE);GPIO_CLR=(x<<BIT_BASE)^(0xFF<<BIT_BASE)
Перед тем как бросаться в пучину kernel, проверим работу с LCD в user space. Подготовим картинку image.jpg в формате raw 320×240. В output.raw содержатся подряд идущие 16 битные значения цвета каждого пикселя (RGB565):
mogrify -format bmp -resize 320 -crop 320x240 image.jpg
ffmpeg -vcodec bmp -i image.bmp -vcodec rawvideo -f rawvideo -pix_fmt rgb565 output.raw
Выведем output.raw на LCD:
/* файл main.c */
int main(int argc , char *argv[]){
if( setup_rpi_gpio() ) {
printf("Cannot map GPIO memory, probably use <sudo>n");
return -1;
}
for(int i = BIT_BASE; i <= RD; i++){
INP_GPIO(i);
OUT_GPIO(i);
}
//set BITS_BASE - RD to 1
GPIO_SET = 0xFFF<<12;
GPIO_SET = 1 << RD;
LCD_Init();
if(argc >= 2){
LCD_draw_image(argv[1]);
}
}
gcc main.c rpi_gpio.c lcd.c -o main
sudo ./main output.raw
Если все работает, самое время приступить к подготовке окружения для компиляции и запуска драйвера.
Заголовки ядра со скриптами сборки для текущей версии ядра в raspbian так просто не поставить, поэтому скачаем исходный код linux, скомпилируем и установим ядро, и будем использовать эти заголовки со скриптами для компиляции драйвера. Основной reference по этому процессу здесь. Версия сорцов ядра подобрана под мою версию raspbian.
git clone --depth=1 -b rpi-4.14.y https://github.com/raspberrypi/linux.git
cd linux
KERNEL=kernel7
make bcm2709_defconfig
make -j4 zImage modules dtbs
sudo make modules_install
sudo cp arch/arm/boot/dts/*.dtb /boot/
sudo cp arch/arm/boot/dts/overlays/*.dtb* /boot/overlays/
sudo cp arch/arm/boot/dts/overlays/README /boot/overlays/
sudo cp arch/arm/boot/zImage /boot/$KERNEL.img
Компиляцию драйвера в дальнейшем выполняем командой make, поместив в директорию с драйвером вот такой Makefile:
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
.PHONY: modules modules_install clean
else
# имя драйвера, если компилируем vfb.c, заменим на vfb.o
obj-m := lcd_drv_simple.o
endif
Теория фреймбуферов хорошо расписана здесь и здесь, поэтому повторяться не буду.
Начнем с виртуального фреймбуфера (vfb.c). Он выделяет область памяти, в которую пишет изображение, направленное в /dev/fbX (X — номер устройства). Это изображение потом можно легко прочитать через cat /dev/fbX. Этот драйвер удобен для тестирования (в нашем случае того, что компиляция и установка драйвера проходит успешно).
Код берем отсюда. Далее
make
sudo cp vfb.ko /lib/modules/$(uname -r)/extra/
# просим систему обновить зависимости
sudo depmod
# загружаем драйвер
sudo modprobe vfb_enable=1
# устанавливаем размер экрана и глубину цвета (16 бит, режим RGB565)
fbset -fb /dev/fb1 -g 320 240 320 240 16
Должно появиться новое framebuffer устройство (/dev/fb1). Запишем в него какое-нибудь изображение,
sudo apt-get install fbi
# fbi требует запуска из полноценной консоли, если запускаем под ssh используем sudo и -T 1 для указания первой консоли
sudo fbi -a -d /dev/fb1 -T 1 image.jpg
считаем его
cat /dev/fb1 > scrn.raw
и откроем в gimp как файл raw rgb565. Убедимся, что изображение есть.
Переходим к драйверу LCD. Велосипед не изобретаем, за основу берем код драйвера из той же статьи. Для начала упростим себе жизнь тем, что при обновлении экрана в LCD пишем весь видеобуфер, а не только измененные кусочки изображения.
Установку режима и уровня (1/0) пинов модифицируем следующим образом (просто прямой доступ к I/O памяти в ядре не работает):
/* файл lcd_drv_simple.c */
static void inp_gpio(u32 g){
u32 *addr = gpio+g/10;
u32 val = readl(addr);
u32 tmp = ~(7<<((g%10)*3));
val &= tmp;
writel(val,addr);
}
static void out_gpio(u32 g){
u32 *addr = gpio+g/10;
u32 val = readl(addr);
u32 tmp = (1<<(((g)%10)*3));
val |= tmp;
writel(val,addr);
}
static void GPIO_SET(u32 val){
writel(val,gpio+7);
}
static void GPIO_CLR(u32 val){
writel(val,gpio+10);
}
Адрес gpio получаем вызовом ioremap:
gpio = ioremap(PORT, RANGE);
u32 *gpio;
static unsigned PORT = 0x3F200000;
static unsigned RANGE = 0x40;
#define W 320
#define H 240
static struct fb_fix_screeninfo ili9341_fix = {
.type = FB_TYPE_PACKED_PIXELS,
.visual = FB_VISUAL_TRUECOLOR,
.accel = FB_ACCEL_NONE,
.line_length = W * 2,
};
static struct fb_var_screeninfo ili9341_var = {
.xres = W,
.yres = H,
.xres_virtual = W,
.yres_virtual = H,
.width = W,
.height = H,
.bits_per_pixel = 16,
.red = {11, 5, 0}, /* смещение 11 бит, 5 битов на красный цвет */
.green = {5, 6, 0}, /* смещение 5 бит, 6 битов на зеленый цвет */
.blue = {0, 5, 0}, /* смещение 0 бит, 5 битов на синий цвет */
.activate = FB_ACTIVATE_NOW,
.vmode = FB_VMODE_NONINTERLACED,
};
/* используем готовую реализацию операций с фреймбуфером */
static struct fb_ops ili9341_fbops = {
.owner = THIS_MODULE,
.fb_write = fb_sys_write,
.fb_fillrect = sys_fillrect,
.fb_copyarea = sys_copyarea,
.fb_imageblit = sys_imageblit,
.fb_setcolreg = ili9341_setcolreg,
};
/* ссылки на функции probe и remove */
struct platform_driver ili9341_driver = {
.probe = ili9341_probe,
.remove = ili9341_remove,
.driver = { .name = "my_fb_driver" }
};
/* задаем функцию ili9341_update, обновляющую экран (частота обновления задается в параметре delay) */
static struct fb_deferred_io ili9341_defio = {
.delay = HZ / 25,
.deferred_io = &ili9341_update,
};
static int ili9341_probe(struct platform_device *dev)
{
int ret = 0;
struct ili9341 *item;
struct fb_info *info;
unsigned char *videomemory;
printk("ili9341_proben");
/*выделяем память под вспомогательную структуру для хранения указателей */
item = kzalloc(sizeof(struct ili9341), GFP_KERNEL);
if (!item) {
printk(KERN_ALERT "unable to kzalloc for ili9341n");
ret = -ENOMEM;
goto out;
}
/* заполняем ее */
item->dev = &dev->dev;
dev_set_drvdata(&dev->dev, item);
/* получаем ссылку на минимально инициализированный fb_info */
info = framebuffer_alloc(0, &dev->dev);
if (!info) {
ret = -ENOMEM;
printk(KERN_ALERT "unable to framebuffer_allocn");
goto out_item;
}
item->info = info;
/* заполняем структуру fb_info нашими данными */
info->par = item;
info->dev = &dev->dev;
info->fbops = &ili9341_fbops;
info->flags = FBINFO_FLAG_DEFAULT;
info->fix = ili9341_fix;
info->var = ili9341_var;
info->fix.smem_len = VIDEOMEM_SIZE; // размер буфера видеопамяти
info->pseudo_palette = &pseudo_palette;
/* выделяем память под видеобуфер, в который пишут приложения, использующие /dev/fbX */
videomemory=vmalloc(info->fix.smem_len);
if (!videomemory)
{
printk(KERN_ALERT "Can not allocate memory for framebuffern");
ret = -ENOMEM;
goto out_info;
}
/* прописываем его в структуре fb_info и сохраняем в нашей структуре ili9341 для дальнейшего использования */
info->fix.smem_start =(unsigned long)(videomemory);
info->screen_base = (char __iomem *)info->fix.smem_start;
item->videomem = videomemory;
/* заполняем информацию об отложенном обновлении экрана */
info->fbdefio = &ili9341_defio;
fb_deferred_io_init(info);
/* передаем заполненную структуру fb_info ядру */
ret = register_framebuffer(info);
if (ret < 0) {
printk(KERN_ALERT "unable to register_frambuffern");
goto out_pages;
}
if (ili9341_setup(item)) goto out_pages;
return ret;
out_pages:
kfree(videomemory);
out_info:
framebuffer_release(info);
out_item:
kfree(item);
out:
return ret;
}
int ili9341_setup(struct ili9341 *item)
{
int i;
/* отображаем адрес для работы с портами GPIO в gpio */
gpio = ioremap(PORT, RANGE);
if(gpio == NULL){
printk(KERN_ALERT "ioremap errorn");
return 1;
}
/* инициализируем LCD */
for(i = BIT_BASE; i <= RD; i++){
inp_gpio(i);
out_gpio(i);
}
GPIO_SET(0xFFF<<12);
GPIO_SET(1 << RD);
LCD_Init();
printk("ili9341_setupn");
return 0;
}
static void ili9341_update(struct fb_info *info, struct list_head *pagelist)
{
/* получаем ссылку на нашу структуру с указателями */
struct ili9341 *item = (struct ili9341 *)info->par;
/* адрес видеопамяти */
u16 *videomemory = (u16 *)item->videomem;
int i, j, k;
/* заполняем весь экран */
LCD_SetWindows(0,0,LCD_W-1,LCD_H-1);
for(i = 0; i < LCD_W * LCD_H; i++){
/* читаем данные из видеопамяти попиксельно и записываем их в LCD */
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
}
Проверим работу драйвера. Скомпилируем, установим и загрузим его
make
sudo cp lcd_drv_simple.ko /lib/modules/$(uname -r)/extra/
sudo depmod
sudo modprobe lcd_drv_simple
Выведем случайное изображение:
cat /dev/urandom > /dev/fb1
Выведем на соответствующий /dev/fbX картинку или видео:
sudo fbi -a -d /dev/fb1 -T 1 image.jpg
mplayer -vo fbdev:/dev/fb1 video.mp4
Запустим графическую оболочку на LCD. Если Desktop environment (DE) еще не установлено (например, серверный вариант raspbian), его можно поставить:
sudo apt-get install lxde
Создадим файл /etc/X11/xorg.conf:
Section "Device"
Identifier "FBDEV"
Driver "fbdev"
Option "fbdev" "/dev/fb1"
EndSection
и добавим в /etc/rc.local:
/sbin/modprobe lcd_drv_simple
После перезагрузки на LCD должна появиться графическая оболочка.
Предыдущий вариант драйвера прост, но не очень быстр. Полная перерисовка экрана заметна. Deferred_io хорошо тем, что ядро передает в функцию ili9341_update список измененных страниц видеопамяти, которые и нужно перерисовать на экране. Т.е. необходимо понять, какая область экрана соответствует заданным 4096 байтам (размер страницы памяти).
Аналогично продолжаем рассуждения дальше, получается, что каждые 5 страниц ситуация повторяется. Поэтому достаточно прописать 5 вариантов отрисовки страницы памяти на экране. Отдельно прописываем работу с последней страницей номер 37, т.к. она занимает 2048 байтов:
/* файл lcd_drv_fast.c */
/* далее используем атомарные операции, которые по факту не очень нужны, т.к. метод ili9341_touch на raspberry ни разу не вызывался (т.е. нет ситуации нескольких потоков выполнения, изменяющих toUpdate одновременно */
static void ili9341_update(struct fb_info *info, struct list_head *pagelist)
{
struct ili9341 *item = (struct ili9341 *)info->par;
struct page *page;
int i;
/* для измененных страниц вычитаем 1 из toUpdate атомарно, toUpdate для этих страниц принимает значение -2 */
list_for_each_entry(page, pagelist, lru)
{
atomic_dec(&item->videopages[page->index].toUpdate);
}
for (i=0; i<FP_PAGE_COUNT; i++)
{
/* для всех страниц увеличиваем toUpdate на 1. Если страница не измененена, то вычтем 1 обратно и получим -1. Если изменена, то также получим -1 после инкремента, но в этом случае еще и выполним отрисовку измененной страницы */
if(atomic_inc_and_test(&item->videopages[i].toUpdate)){
atomic_dec(&item->videopages[i].toUpdate);
}
else
{
draw(item, i);
}
}
}
static void draw(struct ili9341 *item, int page){
int xs,ys,i;
/* рассчитываем адрес страницы в видеопамяти */
u16 *videomemory = (u16*)(item->videomem + PAGE_SIZE*page);
/* строка LCD, с которой начинается страница */
ys = (((unsigned long)(PAGE_SIZE*page)>>1)/W);
/* короткая страница памяти, обрабатываем отдельно */
if (page == 37){
// write PAGE_SIZE / 2;
//write 128 bytes
LCD_SetWindows(256, ys, LCD_W-1, ys);
for(i = 0; i < 128 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 3 lines
LCD_SetWindows(0, ys+1, LCD_W-1, ys+6);
for(i = 0; i < 640 * 3 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
}
else{
switch (page % 5){
//xs = 0. write full six lines and 256 bytes
//640 * 6 + 256
case 0:
//write 6 lines
LCD_SetWindows(0,ys,LCD_W-1,ys + 5);
for(i = 0; i < 640 * 6 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 256 bytes
LCD_SetWindows(0, ys+6, 256/2-1, ys + 6); //7th line from x = 0 to x = 256/2
for(i = 0; i < 256 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
break;
//xs = 128 (256 bytes). write 384 bytes, 5 full lines and 512 bytes
//384 + 640 * 5 + 512
case 1:
//write 384 bytes
LCD_SetWindows(256/2, ys, LCD_W-1, ys);
for(i = 0; i < 384 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 5 lines
LCD_SetWindows(0, ys+1, LCD_W-1, ys+5);
for(i = 0; i < 640 * 5 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 512 bytes
LCD_SetWindows(0, ys+6, 512/2-1, ys+6);
for(i = 0; i < 512 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
break;
//xs = 256 (512 bytes). write 128 bytes, then 6 full lines and 128 bytes
//128 + 640*6 + 128
case 2:
//write 128 bytes
LCD_SetWindows(256, ys, LCD_W-1, ys);
for(i = 0; i < 128 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 6 lines
LCD_SetWindows(0, ys+1, LCD_W-1, ys+6);
for(i = 0; i < 640 * 6 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 128 bytes
LCD_SetWindows(0, ys+7, 128/2-1, ys+7);
for(i = 0; i < 128 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
break;
//xs = 64 (128 /2). write 512 bytes, then 5 lines and 384 bytes
//512 + 640*5 + 384
case 3:
//write 512 bytes
LCD_SetWindows(64, ys, LCD_W-1, ys);
for(i = 0; i < 512 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 5 lines
LCD_SetWindows(0, ys+1, LCD_W-1, ys+5);
for(i = 0; i < 640 * 5 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
//write 384 bytes
LCD_SetWindows(0, ys+6, 384/2-1, ys+6);
for(i = 0; i < 384 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
break;
//xs = 384/2. write 256 bytes, then 6 full lines
//256 + 640*6
case 4:
//write 256 bytes
LCD_SetWindows(384/2, ys, LCD_W-1, ys);
for(i = 0; i < 256 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
LCD_SetWindows(0, ys+1, LCD_W-1, ys+6);
for(i = 0; i < 640 * 6 / 2; i++){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory++;
}
break;
default: break;
}
}
}
Также небольшие изменения в структуре ili9341 и функции ili9341_probe:
struct videopage
{
atomic_t toUpdate;
};
struct ili9341 {
struct device *dev;
struct fb_info *info;
unsigned char *videomem;
/* здесь отмечаем изменения в страницах памяти */
struct videopage videopages[FP_PAGE_COUNT];
};
static int ili9341_probe(struct platform_device *dev){
...
/* инициализируем массив для отслеживания изменений страниц памяти */
for(i=0;i<FP_PAGE_COUNT;i++)
{
atomic_set(&item->videopages[i].toUpdate, -1);
}
}
В структуре ili9341_fbops используем свои функции, которые работают как обертка над стандартными, при этом помечая измененные страницы с помощью функции ili9341_touch. Дело в том, что если ядро использует функции отрисовки, заданные структурой ili9341_fbops, измененные страницы памяти в ili9341_update не поступают и их нужно отдельно помечать. Фактически же, графическая система raspbian эти функции не использует.
static struct fb_ops ili9341_fbops = {
.owner = THIS_MODULE,
.fb_write = ili9341_write,
.fb_fillrect = ili9341_fillrect,
.fb_copyarea = ili9341_copyarea,
.fb_imageblit = ili9341_imageblit,
.fb_setcolreg = ili9341_setcolreg,
};
static ssize_t ili9341_write(struct fb_info *p, const char __user *buf, size_t count, loff_t *ppos){
ssize_t retval;
printk("ili9341_writen");
retval=fb_sys_write(p, buf, count, ppos);
ili9341_touch(p, 0, 0, p->var.xres, p->var.yres);
return retval;
}
static void ili9341_fillrect(struct fb_info *p, const struct fb_fillrect *rect)
{
printk("ili9341_fillrectn");
sys_fillrect(p, rect);
ili9341_touch(p, rect->dx, rect->dy, rect->width, rect->height);
}
static void ili9341_imageblit(struct fb_info *p, const struct fb_image *image)
{
printk("ili9341_imageblitn");
sys_imageblit(p, image);
ili9341_touch(p, image->dx, image->dy, image->width, image->height);
}
static void ili9341_copyarea(struct fb_info *p, const struct fb_copyarea *area)
{
printk("ili9341_copyarean");
sys_copyarea(p, area);
ili9341_touch(p, area->dx, area->dy, area->width, area->height);
}
static void ili9341_touch(struct fb_info *info, int x, int y, int w, int h)
{
struct ili9341 *item = (struct ili9341 *)info->par;
int firstPage;
int lastPage;
int i;
printk("touch x %d, y %d, w %d, h %d",x,y,w,h);
firstPage=((y*W)+x)*BYTE_DEPTH/PAGE_SIZE-1;
lastPage=(((y+h)*W)+x+w)*BYTE_DEPTH/PAGE_SIZE+1;
if(firstPage<0)
firstPage=0;
if(lastPage>FP_PAGE_COUNT)
lastPage=FP_PAGE_COUNT;
for(i=firstPage;i<lastPage;i++)
atomic_dec(&item->videopages[i].toUpdate);
schedule_delayed_work(&info->deferred_work, info->fbdefio->delay);
}
Немного поэксперементируем. Подключим к raspberry два экрана. В качестве основного экрана используем экран / телевизор, подключенный к HDMI. В качестве второго экрана используем LCD.
Чтобы перетаскивание окошек между экранами было лучше видно, я “увеличил” размер экрана LCD, которые видит linux до 640×480. В ядре я регистрирую экран 640×480, однако на сам LCD пишу каждый второй пиксель в строке и пропускаю каждую вторую строку. Измененный код ili9341_update:
/* файл lcd_drv_simple_640_480.c */
#define W 320*2
#define H 240*2
/* изменения в ili9341_update на примере простого драйвера */
for(j = 0; j < H; j++){
if (j % 2 == 1){ //skip
videomemory += W;
}
else{
for(i = 0; i < W; i += 2){
Lcd_WriteData_16Bit(readw(videomemory));
videomemory += 2;
}
}
}
Для работы с двумя экранами глубина цвета на них должна быть одинаковой. Для этого добавляем в /boot/config.txt:
[all]
framebuffer_depth=16
Ставим xinerama для перетаскивания окон между экранами:
sudo apt-get install libxinerama-dev
Заменяем конфигурационный файл /etc/X11/xorg.conf
Section "Device"
Identifier "LCD"
Driver "fbdev"
Option "fbdev" "/dev/fb1"
Option "ShadowFB" "off"
Option "SwapbuffersWait" "true"
EndSection
Section "Device"
Identifier "HDMI"
Driver "fbdev"
Option "fbdev" "/dev/fb0"
Option "ShadowFB" "off"
Option "SwapbuffersWait" "true"
EndSection
Section "Monitor"
Identifier "LCD-monitor"
Option "RightOf" "HDMI-monitor"
EndSection
Section "Monitor"
Identifier "HDMI-monitor"
Option "Primary" "true"
EndSection
Section "Screen"
Identifier "screen0"
Device "LCD"
Monitor "LCD-monitor"
EndSection
Section "Screen"
Identifier "screen1"
Device "HDMI"
Monitor "HDMI-monitor"
EndSection
Section "ServerLayout"
Identifier "default"
Option "Xinerama" "on"
Option "Clone" "off"
Screen 0 "screen0" RightOf "screen1"
Screen 1 "screen1"
EndSection
Результат:
Надеюсь было интересно. Код на github.
VK объявляет о приобретении 40% компании Intickets.ru (Интикетс). Это облачный сервис для контроля и управления продажей билетов на мероприятия. Сумма…
OpenAI готовится запустить собственную поисковую систему на базе ChatGPT. Информацию об этом публикуют западные издания. Ожидается, что новый поисковик может…
Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…
Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…
Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…
Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…