Прочитав монументальную серию статей о подключении LCD
экрана к роутеру мне захотелось сделать то же самое. Однако
многообразие используемого стека (openwrt, stm32, usb) в сочетании
с отсутствием полных исходников кода но может плохо
искал несколько затруднило задачу. Я решил начать с малого
написать свою реализацию framebuffer для raspberry и вывести
графическую среду raspberry на LCD. Что из этого получилось,
описываю далее.
Вообще-то имеются готовые драйверы для LCD (проект tbtft), но мы напишем свой, чтобы лучше понять как все устроено.
LCD
LCD 320x240 с контроллером 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
Я использую 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.
/* файл 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.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 320x240. В output.raw содержатся подряд идущие 16 битные значения цвета каждого пикселя (RGB565):
mogrify -format bmp -resize 320 -crop 320x240 image.jpgffmpeg -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 mainsudo ./main output.raw
Подготовка окружения
Если все работает, самое время приступить к подготовке окружения для компиляции и запуска драйвера.
Заголовки ядра со скриптами сборки для текущей версии ядра в raspbian так просто не поставить, поэтому скачаем исходный код linux, скомпилируем и установим ядро, и будем использовать эти заголовки со скриптами для компиляции драйвера. Основной reference по этому процессу здесь. Версия сорцов ядра подобрана под мою версию raspbian.
git clone --depth=1 -b rpi-4.14.y https://github.com/raspberrypi/linux.gitcd linuxKERNEL=kernel7make bcm2709_defconfigmake -j4 zImage modules dtbssudo make modules_installsudo 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) modulesmodules_install: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_installclean: rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions.PHONY: modules modules_install cleanelse # имя драйвера, если компилируем vfb.c, заменим на vfb.o obj-m := lcd_drv_simple.oendif
Драйвер фреймбуфера
Теория фреймбуферов хорошо расписана здесь и здесь, поэтому повторяться не буду.
Начнем с виртуального фреймбуфера (vfb.c). Он выделяет область памяти, в которую пишет изображение, направленное в /dev/fbX (X номер устройства). Это изображение потом можно легко прочитать через cat /dev/fbX. Этот драйвер удобен для тестирования (в нашем случае того, что компиляция и установка драйвера проходит успешно).
Код берем отсюда. Далее
makesudo 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 240static 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
Проверим работу драйвера. Скомпилируем, установим и загрузим его
makesudo cp lcd_drv_simple.ko /lib/modules/$(uname -r)/extra/sudo depmodsudo modprobe lcd_drv_simple
Выведем случайное изображение:
cat /dev/urandom > /dev/fb1
Выведем на соответствующий /dev/fbX картинку или видео:
sudo fbi -a -d /dev/fb1 -T 1 image.jpgmplayer -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 до 640x480. В ядре я регистрирую экран 640x480, однако на сам 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"EndSectionSection "Device" Identifier "HDMI" Driver "fbdev" Option "fbdev" "/dev/fb0" Option "ShadowFB" "off" Option "SwapbuffersWait" "true"EndSectionSection "Monitor" Identifier "LCD-monitor" Option "RightOf" "HDMI-monitor"EndSectionSection "Monitor" Identifier "HDMI-monitor" Option "Primary" "true" EndSectionSection "Screen" Identifier "screen0" Device "LCD" Monitor "LCD-monitor"EndSectionSection "Screen" Identifier "screen1" Device "HDMI" Monitor "HDMI-monitor"EndSectionSection "ServerLayout" Identifier "default" Option "Xinerama" "on" Option "Clone" "off" Screen 0 "screen0" RightOf "screen1" Screen 1 "screen1" EndSection
Результат:
Заключение
Надеюсь было интересно. Код на github.