Пишем драйвер фреймбуфера для Raspberry Pi с LCD

от автора

Прочитав монументальную серию статей о подключении LCD экрана к роутеру мне захотелось сделать то же самое. Однако многообразие используемого стека (openwrt, stm32, usb) в сочетании с отсутствием полных исходников кода но может плохо искал несколько затруднило задачу. Я решил начать с малого — написать свою реализацию framebuffer для raspberry и вывести графическую среду raspberry на LCD. Что из этого получилось, описываю далее.

Вообще-то имеются готовые драйверы для LCD (проект tbtft), но мы напишем свой, чтобы лучше понять как все устроено.

LCD

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

/* файл 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

Я использую raspberry pi 3 с установленным raspbian lite (версия ядра 4.14). GUI добавлено установкой пакетов lxde и xinit.

sudo apt-get install lxde xinit

Расположение GPIO

Подключение LCD к raspberry

  • LCD Data 0 -> GPIO 12
  • LCD Data 1 -> GPIO 13
  • LCD Data 7 -> GPIO 19
  • LCD CS -> GPIO 20
  • LCD RS -> GPIO 21
  • LCD RST -> GPIO 22
  • LCD WR -> GPIO 23
  • LCD RD -> GRPIO 24
  • LCD 5V -> 5V
  • LCD GND -> Ground

Управление GPIO

В 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.

Код установки gpio

/* файл 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 %d\n", (int)gpio_map);//errno also set!       return -1;    }     // Always use volatile pointer!    gpio = (volatile uint32_t *)gpio_map;    return 0; }

Управление LCD c raspberry

Пинами LCD управляем следующим образом:

/* файл 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)

Проверка работы с LCD в user space

Перед тем как бросаться в пучину 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:

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_probe\n");      /*выделяем память под вспомогательную структуру для хранения указателей */     item = kzalloc(sizeof(struct ili9341), GFP_KERNEL);     if (!item) {         printk(KERN_ALERT "unable to kzalloc for ili9341\n");         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_alloc\n");         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 framebuffer\n");         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_frambuffer\n");         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 error\n");         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_setup\n");     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++;     } }

Запускаем графическую оболочку на LCD

Проверим работу драйвера. Скомпилируем, установим и загрузим его

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 байтам (размер страницы памяти).

  • Первые 4096 байтов соответствуют полным 6 линиям и 128 пикселям 7ой линии, т.к. 4096 = 320*2*6 + 128*2 (2 байта на каждый пиксель)
  • Вторые 4096 байтов начинаются с 129 пикселя 7ой линии, требуют 384 байта для завершения линии (128*2 + 384 = 640), затем идут 5 полных линий и 256 пикселей в 6 линии (4096 = 384 + 640*5 + 512).

Аналогично продолжаем рассуждения дальше, получается, что каждые 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_write\n");     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_fillrect\n");     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_imageblit\n");     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_copyarea\n");     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

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.

ссылка на оригинал статьи https://habr.com/ru/post/536110/


Комментарии

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

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