[Настройка Linux, Разработка на Raspberry Pi] Пишем драйвер фреймбуфера для Raspberry Pi с LCD
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Прочитав монументальную серию статей о подключении 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.
Код передачи данных / команд
SPL
/* файл 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
SPL
/* файл 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
SPL
/* файл 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 управляем следующим образом:
SPL
/* файл 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.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
SPL
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);
Параметры драйвера описываются в структурах:
SPL
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,
};
Основные функции:
SPL
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 байтов:
Код драйвера
SPL
/* файл 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 эти функции не использует.
Код
SPL
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
xorg.conf
SPL
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.
===========
Источник:
habr.com
===========
Похожие новости:
- [Разработка под Linux, Программирование микроконтроллеров, Схемотехника, Производство и разработка электроники] WSN-LTE шлюз на CC1310 и WP8548. Часть 1
- [Настройка Linux, Информационная безопасность, Open source, Виртуализация] Альтернативы VirtualBox для любителей приватности и свободы. Гипервизоры и менеджеры виртуальных машин. Часть I
- [Open source, *nix] FOSS News №49 – дайджест материалов о свободном и открытом ПО за 28 декабря 2020 года – 3 января 2021 года
- Оценка предпочтений пользователей Linux в выборе оборудования
- [Настройка Linux, Системное администрирование, *nix] Systemd для продолжающих. Part 1 — Запуск юнитов по временным событиям
- [Настройка Linux, Системное администрирование, *nix] Почему хабражители предпочитают велосипеды, вместо готовых решений? Или о systemd, part 0
- [Настройка Linux, Разработка под Arduino, Разработка на Raspberry Pi, Умный дом] Безумный дом
- [Промышленное программирование, SCADA] Веселые уроки WinCC OA. Установка WinCC OA под Debian и перенос прикладного проекта
- [Информационная безопасность, Системное программирование, Разработка под Linux] Карантин для динамической памяти ядра Linux
- [IT-инфраструктура, Сетевые технологии, Разработка систем связи, Разработка под Linux] Безопасные города без зоопарка
Теги для поиска: #_nastrojka_linux (Настройка Linux), #_razrabotka_na_raspberry_pi (Разработка на Raspberry Pi), #_framebuffer, #_linux_kernel, #_raspberry_pi, #_linux, #_nastrojka_linux (
Настройка Linux
), #_razrabotka_na_raspberry_pi (
Разработка на Raspberry Pi
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:01
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Прочитав монументальную серию статей о подключении 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. Код передачи данных / командSPL/* файл 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 на синий). Код управления LCDSPL/* файл 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
Управление 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. Код установки gpioSPL/* файл 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 управляем следующим образом:SPL/* файл 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.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: MakefileSPLifeq ($(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);
Параметры драйвера описываются в структурах:SPLu32 *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, }; Основные функции:SPLstatic 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 байтам (размер страницы памяти).
Аналогично продолжаем рассуждения дальше, получается, что каждые 5 страниц ситуация повторяется. Поэтому достаточно прописать 5 вариантов отрисовки страницы памяти на экране. Отдельно прописываем работу с последней страницей номер 37, т.к. она занимает 2048 байтов: Код драйвераSPL/* файл 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 эти функции не использует. КодSPLstatic 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 xorg.confSPLSection "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. =========== Источник: habr.com =========== Похожие новости:
Настройка Linux ), #_razrabotka_na_raspberry_pi ( Разработка на Raspberry Pi ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 06:01
Часовой пояс: UTC + 5