Прочитав монументальную серию статей о подключении 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 %dn", (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_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++;
    }
}

Запускаем графическую оболочку на 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_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

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.

Let’s block ads! (Why?)

Read More

Recent Posts

VK купила 40% билетной платформы Intickets.ru

VK объявляет о приобретении 40% компании Intickets.ru (Интикетс). Это облачный сервис для контроля и управления продажей билетов на мероприятия. Сумма…

3 часа ago

OpenAI готовится запустить поисковую систему на базе ChatGPT

OpenAI готовится запустить собственную поисковую систему на базе ChatGPT. Информацию об этом публикуют западные издания. Ожидается, что новый поисковик может…

12 часов ago

Роскомнадзор рекомендовал хостинг-провайдерам ограничить сбор данных с сайтов для иностранных ботов

Центр управления связью общего пользования (ЦМУ ССОП) Роскомнадзора рекомендовал компаниям из реестра провайдеров ограничить доступ поисковых ботов к информации на российских сайтах.…

1 день ago

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

6 дней ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

7 дней ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

7 дней ago