6 января 2013 в 13:18

Миникомпьютер из роутера с OpenWRT: пишем драйвер фреймбуфера

Добрый день, уважаемые хабровчане. Вот мы и подошли к самой интересной и важной части моего цикла статей про превращение небольшого роутера в миникомпьютер — сейчас мы с вами будем разрабатывать настоящий драйвер фреймбуфера, который позволит запустить на роутере разные графические приложения. Чтобы энтузиазм не угасал, вот видео одного из таких приложений — думаю, большинство узнают это великолепный старый квест:

На случай, если вы пропустили предыдущие части — вот ссылки:
1 — Миникомпьютер из роутера с OpenWRT: разрабатываем USB-видеокарту
2 — Миникомпьютер из роутера с OpenWRT: пишем USB class-driver под Linux
Итак, приступаем к работе.

Введение


Железо у нас опять не менялось (хотя кое-что в прошивке мы обязательно поменяем в следующей статье), поэтому начнем с обзора того, что нам предстоит сделать.
Несмотря на то, что наш драйвер из прошлой статьи уже умеет выводить графику, использовать его в полной мере (например, вывести консоль или запустить графическое приложение) весьма затруднительно. Дело в том, что подобные приложения требуют вполне определенного, стандартизованного интерфейса, так называемого фреймбуфера. При регистрации фреймбуфера в системе нам необходимо будет заполнить несколько специализированных структур-дескрипторов, на этот раз куда более специфических, чем просто абстрактные «файловые операции», которые мы заполняли в прошлый раз. Операции там тоже будут, но кроме них будут специальные колбэки, такие, как fb_imageblit (вызывается, когда кто-то хочет перенести блок бит с изображением в определенное место экрана), fb_copyarea (похожий перенос, но блок бит берется не из внешнего буфера а из видеопамяти) и т.п. Кроме того, будут структуры с описанием разрешения экрана, битности цвета и того, как в этой «битности» расположены цветовые компоненты.

Первое, что нужно осознать: у нас несколько нестандартная ситуация, если сравнивать с видеокартами PC (хотя для эмбеддед устройств вполне себе обычная) — наше устройство не имеет как таковой видеопамяти, к которой мы могли бы обращаться — точнее, память-то оно имеет, ту самую GRAM, запрятанную в недрах дисплея, но доступ у нас к ней только через «окошко» в 16 бит шириной. Памяти на борту тоже не настолько много, чтобы хранить там весь буфер кадра.
К счастью, в линуксе для этого предусмотрен специальный подход — для нас уже написаны функции с префиксом "sys_", например, "sys_imageblit", которые реализуют необходимую фреймбуферу функциональность, работая с областью оперативной памяти системы как с буфером кадра.
То есть, если буфер кадра у нас размещен в видеокарте и у нас есть аппаратная поддержка подобных операций, мы в колбэках просто пинаем нашу железку, отдавая команду «выполнить перенос блока бит» или «скопировать область видеопамяти».
Если же у нас ничего этого нет, мы выделяем в ядре память, размером равную нашему буферу кадра, и в колбэках вызываем эти самые функции с префиксом "sys_", которые выполняют необходимые операции в RAM.
Таким образом, можно получить полностью работающий фреймбуфер, который вообще не будет взаимодействовать с железом — такой драйвер уже есть и он называется vfb, virtual framebuffer. Его исходный код лежит в drivers/video/vfb.c.
Если к такому драйверу добавить периодическую посылку данных на реальное устройство мы получим уже настоящий драйвер фреймбуфера. Но перед тем, как мы займемся этим, давайте немного понастраиваем нашу систему и потренируемся на виртуальном драйвере, vfb.

Включаем поддержку графики в ядре


С этой частью я провозился довольно долго, в основном, по той причине, что я сначала написал свой драйвер, а потом пытался понять, почему у меня только черный экран — грешил на свои ошибки в коде. Потом догадался поставить вместо него драйвер VFB, вычитав содержимое памяти которого увидел тот же черный экран. Тогда я, наконец, понял, что дело не в драйвере, а в том, что ядро само по себе отказывается выводить на него информацию, после чего проблема решилась довольно быстро. Но обо всем по-порядку.
  1. Для того, чтобы мы увидели консольный вывод в памяти фреймбуфера (ну и на экране, если он настоящий, а не виртуальный) необходимы два драйвера — это, собственно, драйвер самого фреймбуфера, который создаст устройство /dev/fb[x] и драйвер консоли, работающий поверх него — это драйвер fbcon
  2. В ядре, соответственно, должна быть включена поддержка фреймбуферов, поддержка виртуальных терминалов (абстракция, объединяющая в себе устройство вывода+устройство ввода, дисплей и клавиатуру), поддержка отображения системной консоли на таких терминалах (да, это тоже можно отключить, тогда системная консоль будет выводиться только на физически существующие символьные устройства вроде ком-портов), сам драйвер fbcon, а также какой нибудь из доступных вбилденных в него шрифтов.
  3. Тот самый пункт, который я в начале упустил, когда не мог понять, почему ничего не выводится — нужно сообщить ядру, что необходимо выводить содержимое системной консоли на тот /dev/tty[x], который сцапал fbcon!
    Дело в том, что драйвер fbcon пытается захватить первый доступный /dev/tty[x], например, tty0. Но ядро ничего туда не выводит, это ни к чему не привязанная абстракция, т.к. на нем не запущено ни приложение, позволяющие логиниться в системе, ни вывод системной консоли.
    Для того чтобы решить эту проблему мы должны во-первых сказать ядру, что мы хотим видеть системную консоль на /dev/tty0 (впрочем, это опционально, если вдруг кто-то не хочет видеть процесс загрузки и системный вывод, то этот пункт можно опустить), а во-вторых сообщить иниту, что там нужно запустить софт для логина


Сейчас мы проделаем все три пункта относительно драйвера виртуального фреймбуфера, и, когда получим картинку в памяти, перейдем к написанию своего. fbcon и драйвер фреймбуфера могут быть сбилдены либо оба статически, либо оба в виде подключаемых модулей, либо кто-нибудь один статически, второй динамически — проблем это не вызовет, fbcon сцапает фреймбуфер сразу, как только его увидит.
Правда, при работе с vfb есть одна тонкость — чтобы его активировать, необходимо передать модулю параметр vfb_enable=1, либо запустить ядро с параметром "video=vfb". С модулем работать будет проще, поэтому ограничимся им. fbcon же вбилдим в ядро.

  1. Выполняем make kernel_menuconfig и заходим в пункт Device Drivers
  2. Включаем Graphics Support — Support for frame buffer devices, после чего нам становится доступен список самих драйверов, в котором выбираем Virtual framebuffer.
  3. Возвращаемся уровнем выше, идем в Character devices и включаем там

    Virtual terminal
    Enable character translations in console
    Support for console on virtual terminal
    Support for binding and unbinding console drivers


    Благодаря последним двум опциям мы сможем вывести на виртуальный терминал системную консоль, а потом, при желании, отбиндить ее от драйвера фреймбуфера, что позволит выгрузить его из памяти.
  4. Возвращаемся к Graphics Support, идем в ставшее доступным меню Console display driver support и включаем там Framebuffer Console support, после чего активируем пункт Select compiled-in fonts и выбираем там какой-нибудь шрифт — допустим, VGA 8x8 font
  5. Выходим в основное меню и обращаем внимание на пункт Kernel Hacking — если туда зайти, то можно обнаружить ближе к концу списка пункт, содержащий параметры загрузки ядра. Вообще, их передает ядру бут-лодер, но можно дополнить строку параметров при помощи этого пункта, либо вовсе переопределить ее. Переопределять не будем, а вот дополнить — дополним, т.к. бутлодер передает в нее параметр console=ttyATH0, что означает вывод системной консоли на последовательный порт. К сожалению, прямо тут мы это сделать не можем — данный параметр будет переопределен при применении платформ-специфик патчей, поэтому туда и отправимся. Не трогаем тут ничего, сохраняем конфиг и выходим.
  6. Идем туда, где, как мы помним, хранятся платформ-специфик файлы и патчи — target/linux/ar71xx/. Заходим в generic и открываем файл config-default. В нем мы видим единственную строку, тот самый параметр, который видели в настройке ядра:
    CONFIG_CMDLINE=«rootfstype=squashfs,jffs2 noinitrd»
    дописываем в конец console=tty0 и fbcon=font:<имя шрифта>, в качестве имени шрифта задав одно из тех, что выбрали в настройке ядра. Получаем что-то вроде
    CONFIG_CMDLINE=«rootfstype=squashfs,jffs2 noinitrd console=tty0 fbcon=font:ProFont6x11»


Последнее, что нам нужно сделать перед пересборкой — зайти в make menuconfig и включить в предоставляемые busybox возможности утилиту fbset, которая позволит задать параметры нашего фреймбуфера. Она находится в меню Base System — Busybox — Linux SyStem Utilities

Теперь можно пересобирать ядро. В build_dir/target-mips_r2_uClibc-0.9.33.2/linux-ar71xx_generic/linux-3.6.9/dri
vers/video/
забираем то, что с расширением .ko
Вопреки ожиданиям, он там будет не один, включение драйвера, использующего функции с префиксом "sys_" активирует сборку нескольких модулей, в которых эти самые функции лежат. Что интересно, в принципе, ничего не мешает вбилдить их статически в ядро, оставив драйвер подключаемым модулем, однако из меню мне этого сделать не удалось, пришлось писать соответствующий патч к Kconfig-файлу. Но это мы сделаем позже, а сейчас просто перешьем роутер новой прошивкой и перекинем на него все модули.

После заходим по SSH на роутер и идем /etc. Открываем файл inittab и видим там что-то вроде этого:

::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
ttyATH0::askfirst:/bin/ash --login

В последней строке как раз и сказано, что нужно запустить софт для логина (в данном случае, как и все системные бинарники — часть busybox'а) на ttyATH0, последовательном порту. При этом указано (askfirst) что для активации этой консоли нужно будет сначала нажать enter.
Добавляем еще одну строчку:

tty0::respawn:/bin/ash --login

Посмотрим, правильно ли указаны параметры ядра через

cat /proc/cmdline

и перезагрузим роутер.
Теперь по очереди инсмодим все, кроме драйвера vfb, а в самом конце пишем

insmod vfb.ko vfb_enable=1


После этого мы в dmesg должны увидеть слова наподобие этих: Console: switching to colour frame buffer device 53x21
Размеры консоли будут отличаться в зависимости от выбранного шрифта. Установим фреймбуферу параметры, более похожие на параметры нашего дисплея:

fbset -g 320 240 320 240 16

Это установит видимое и виртуальное разрешение в 320х240 (чаще всего они совпадают, но в принципе, можно задать виртуальное разрешение больше видимого, получив буфер кадра больше выводимого и использовать это для двойной буферизации), а глубину цвета — в 16 бит.
fbcon должен отреагировать на это сменой своего разрешения и сообщением в dmesg, но если этого вдруг не произошло, отключим консоль от фреймбуфера и подключим заново:

echo "0" > /sys/class/vtconsole/vtcon1/bind
echo "1" > /sys/class/vtconsole/vtcon1/bind

Это полезная пара команд, которая нам ни раз пригодится — без этого не выгрузить драйвер фреймбуфера, т.к. он будет занят консолью.
Очень не помешает подключить к роутеру клавиатуру — можно вслепую ввести команды clear и dmesg чтобы быть уверенным, что на виртуальном дисплее что-то есть.
После получаем «скриншот» командой

cat /dev/fb0 > scrn.raw

И скачиваем его на десктоп. Там открываем через GIMP или любой другой софт, который сможет загрузить сырые графические данные в формате RGB565 — задаем размеры изображения 320x240, не забываем о битности, и получаем картинку вроде этой (сообщения об открытии и закрытии /dev/fb0 выдает мой драйвер, т.к. скриншот я снимал не с виртуального фреймбуфера. Виртуальный о таких делах молчит):

Обратили внимание на красивый, «хакерский» зеленый цвет консоли? На самом деле это говорит нам об ошибке, точнее — об одной особенности, с которой нужно считаться. Но об этом мы поговорим позже. Перед тем, как перейти к главному действу — написанию своего драйвера фреймбуфера — давайте сравним доступные консольные шрифты. Для этого я подготовил шесть фото, по три на консоль и на Midnight Commander со шрифтами 4x4, 6x11 и 8x8. На мой взгляд, самый удобный — это 6х11:


4x4


6x11


8x8


4x4


6x11


8x8

Пишем драйвер фреймбуфера


Для начала — о подходе. Очевидным и не очень хорошим решением стало бы периодическое обновление всего экрана — можно было бы завести таймер, который бы кидал все содержимое фреймбуфера по USB уже знакомыми нам из прошлой статьи командами. Однако есть куда более правильное решение, так называемое deferred io.
Суть его проста: нам нужно только указать функцию-колбэк, задать интервал времени и зарегистрировать это самое deferred io для нашего фреймбуфера. При регистрации виртуальная память будет настроена так, что обращение к памяти буфера кадра вызовет исключение, которое поставит наш колбэк в очередь на обработку через заданный нами интервал. При этом операция записи в память не прервется! А когда вызовется колбэк, в него будет передан список страниц, которые были изменены. Не правда ли, очень удобно? Юзерспейс может спокойно писать в видеопамять не задумываясь ни о чем и не прерываясь, при этом периодически будет дергаться наш колбэк со списком страниц памяти, которые были изменены — нам нужно будет кидать на устройство только их, а не весь буфер.

Так как освобождение фреймбуфера — задача не такая простая, как кажется на первый взгляд (USB-устройство может уже отсутствовать, но сам фреймбуфер клиенты еще не отпустят и чистить память в этот момент никак нельзя), мы поступим не очень хорошо — напишем себе жирнейшим шрифтом TODO и клятвенно пообещаем реализовать правильную очистку и отключение устройства чуть-чуть позже, а пока напишем все остальное, чтобы наконец, увидеть плод своих действий. Нормальную очистку вместе с допиливанием прошивки видеокарты (что поднимет FPS минимум в два раза) мы обязательно рассмотрим в следующей статье.

Начнем с простого — так как нам будет приходить список страниц, в которые была произведена запись, проще всего заранее сохранить координаты на экране, соответствующие началу страницы а также указатель на соответствующую ей область памяти. Поэтому создадим структуру с этими полями, не забыв добавить атомарный флаг, показывающий, требуется ли данной странице апдейт. Это необходимо, т.к. внутренние операции фреймбуфера, те, что выполняются через функции с префиксом "sys_" не вызывают наш хендлер deferred io, поэтому нам нужно будет вручную посчитать, к каким страницам было обращение и пометить их как подлежащие апдейту.

Структура страницы памяти
struct videopage
{
        int                             x, y;
        unsigned char           *mem;
        unsigned long            length;
        atomic_t                     toUpdate;
};


Здесь все прозрачно, единственное что — храним длину, т.к. последняя страница может быть неполной — нам ни к чему слать дисплею лишние данные.
Объявим несколько дефайнов, связанных с размерами дисплея — количество пикселей в странице, количество страниц во фреймбуфере и т.п.
PAGE_SIZE задефайнена за нас, в исходниках ядра.

Вспомогательные дефайны
#define WIDTH			320
#define HEIGHT			240
#define BYTE_DEPTH		2
#define FB_SIZE			WIDTH*HEIGHT*BYTE_DEPTH
#define FP_PAGE_COUNT		PAGE_ALIGN(FB_SIZE)/PAGE_SIZE
#define PIXELS_IN_PAGE		PAGE_SIZE/BYTE_DEPTH


Объявим требуемые колбэки операций с фреймбуфером и колбэк-хендлер нашего deferred io.

Колбэки
static void display_fillrect(struct fb_info *p, const struct fb_fillrect *rect);
static void display_imageblit(struct fb_info *p, const struct fb_image *image);
static void display_copyarea(struct fb_info *p, const struct fb_copyarea *area);
static ssize_t display_write(struct fb_info *p, const char __user *buf, 
                                size_t count, loff_t *ppos); 
static int display_setcolreg(unsigned regno,
                               unsigned red, unsigned green, unsigned blue,
                               unsigned transp, struct fb_info *info);
//---------------
static void display_update(struct fb_info *info, struct list_head *pagelist);


Далее идет структура с неизменяемой информацией о дисплее.

Структура fb_fix_screeninfo
static struct fb_fix_screeninfo fixed_info =
{
        .id = "STM32LCD",
        .type        = FB_TYPE_PACKED_PIXELS,
        .visual      = FB_VISUAL_TRUECOLOR,
        .accel       = FB_ACCEL_NONE,
        .line_length = WIDTH * BYTE_DEPTH,
};


Тоже все просто — задаем строковый ID нашего дисплея, тип пикселей — другие нас не интересуют, у нас самый обычный битмэп, цвет у нас truecolor, во всяком случае близко к нему и уж точно не монохромный и не direct color.
Далее идет более интересная структура с (потенциально) переменной информацией. Но так как возможность смены разрешения мы реализовывать не будем, для нас она будет такой же постоянной как и в прошлой структуре.

Структура fb_var_screeninfo
static struct fb_var_screeninfo var_info =
{
        .xres                   =       WIDTH,
        .yres                   =       HEIGHT,
        .xres_virtual   =       WIDTH,
        .yres_virtual   =       HEIGHT,
        .width                  =       WIDTH,
        .height                 =       HEIGHT,
        .bits_per_pixel =       16,
        .red                    =       {11, 5, 0},
        .green                  =       {5, 6, 0},
        .blue                   =       {0, 5, 0},
        .activate               =       FB_ACTIVATE_NOW,
        .vmode                  =       FB_VMODE_NONINTERLACED,
};


Тут мы видим уже знакомые по fbset видимое и виртуальное разрешения, физические размеры дисплея в миллиметрах (можно задать, в принципе, любыми, я задал такими же, как и его разрешение), битность изображения, и — важные структурки — описатели того, как цветовые компоненты расположены в байтах. В них первым значением идет смещение, вторым длина в битах и третьим — флаг, «значащий бит справа».
Последней объявляем структуру отложенного ввода-вывода, deferred io.

Структура fb_deferred_io
static struct fb_deferred_io display_defio = {
        .delay          = HZ/6,
        .deferred_io    = &display_update,
};


Выбор значения периода, поля .delay, задача, большей частью, эмпирическая — если выбрать слишком маленькое значение, обновление будет происходить реже, чем позволяет аппаратура, если выбрать слишком большое — будет забиваться очередь отложенных работ. Наш дисплей в данный момент более чем тихоходный, причем это определяется полностью USB, а не выводом на экран. В реализации из первой статьи полная перерисовка экрана возможно с частотой не выше 3.6 FPS. Но не стоит отчаиваться — во-первых, мы не всегда будем перерисовывать весь экран, а во-вторых, уже в следующей статье, я покажу как выжать максимум из имеющегося у нас железа, так что FPS подскочит до ~8 — с учетом неполной перерисовки мы получим вполне юзабельное устройство, как на видео в начале статьи. Эти 9 FPS, кстати, являются физическим пределом, который мы можем получить на Full Speed USB при передаче сырых видеоданных (без сжатия). Это становится очевидным, если мы вспомним предел скорости передачи FS USB — 12 МБит/с. Наш кадр занимает 320*240*16 = 1 228 800 бит. Таким образом идеальная, сферическая в вакууме частота кадров будет не выше 9.8 FPS. Выбросим отсюда наши заголовки, потери в нашем драйвере, потери в драйвере контроллера хоста, потери в драйвере на STMке — и получим реальный предел в 8-9 ФПС, достигнуть который вполне неплохо. Но это мы сделаем в следующей статье, а сейчас помним, что наша частота около 3.5 FPS, и, по моим замерам, период примерно в два раза больший, то есть 6-7 герц, оказался оптимальным. Такой и задаем, пользуясь заранее задефайненным в исходниках ядра HZ. Кстати, стоит обратить внимание — несмотря на название, этот макрос определяет не частоту в 1 Гц, а соответствующий период (в квантах ядра), поэтому, для получения частоты в 6 Гц его следует не умножать, а делить на шесть.
Наконец заполняем структуру с колбэками операций над фреймбуфером.

Структура fb_ops
static struct fb_ops display_fbops = {
        .owner        = THIS_MODULE,
        .fb_read      = fb_sys_read,
        .fb_write     = display_write,
        .fb_fillrect  = display_fillrect,
        .fb_copyarea  = display_copyarea,
        .fb_imageblit = display_imageblit,
        .fb_setcolreg   = display_setcolreg,
};


На чтение сразу запишем колбэк из тех, что "sys_", нам там делать нечего. Во всех остальных нам нужно будет еще пометить соответствующие страницы на обновление, так что указываем свои.
Структура-дескриптор нашего девайса почти не изменится со времен предыдущей статьи, опишем ее.

Структура-дескриптор usblcd
struct usblcd
{
        struct usb_device                                       *udev;
        struct usb_interface                            *interface;

        struct device                                           *gdev;      
        struct fb_info                                          *info;

        struct usb_endpoint_descriptor          *bulk_out_ep;
        unsigned int                                             bulk_out_packet_size;
        struct videopage                                         videopages[FP_PAGE_COUNT];
       
        unsigned long pseudo_palette[17];
};


В нее добавился массив наших описателей страниц видеопамяти. Вообще-то, статически размещать большие объемы данных в ядре не рекомендуется, но сама структура у нас вышла небольшая, да и по количеству их всего 38, поэтому, чтобы не морочиться с лишними указателями оставим этот массив статическим, 760 байт или около того ядро как-нибудь осилит.
Последнее поле, pseudo_palette — место под псевдо-палитру, которую требует fbcon. Заполняется она в колбэке .fb_setcolreg, без которого fbcon отказывается работать. Во всех дровах, что я видел, этот колбэк выглядит копипастнутым из файла-примера фреймбуфера из исходников ядра, поэтому мы тоже не будем изобретать велосипед, тем более что, кроме fbcon этим, похоже, никто и не пользуется. С него и начнем.

Колбэк display_setcolreg
#define CNVT_TOHW(val,width) ((((val)<<(width))+0x7FFF-(val))>>16)

static int display_setcolreg(unsigned regno,
                               unsigned red, unsigned green, unsigned blue,
                               unsigned transp, struct fb_info *info)
{
        int ret = 1;
        if (info->var.grayscale)
                red = green = blue = (19595 * red + 38470 * green +
                                      7471 * blue) >> 16;
        switch (info->fix.visual) {
        case FB_VISUAL_TRUECOLOR:
                if (regno < 16) {
                        u32 *pal = info->pseudo_palette;
                        u32 value;

                        red = CNVT_TOHW(red, info->var.red.length);
                        green = CNVT_TOHW(green, info->var.green.length);
                        blue = CNVT_TOHW(blue, info->var.blue.length);
                        transp = CNVT_TOHW(transp, info->var.transp.length);

                        value = (red << info->var.red.offset) |
                                (green << info->var.green.offset) |
                                (blue << info->var.blue.offset) |
                                (transp << info->var.transp.offset);

                        pal[regno] = value;
                        ret = 0;
                }
                break;
        case FB_VISUAL_STATIC_PSEUDOCOLOR:
        case FB_VISUAL_PSEUDOCOLOR:
                break;
        }
        return ret;
}


Это, как я уже сказал, стандартный код для такого колбэка, включая макрос CNVT_TOHW, который используется для получения значений цветовых компонентов. Его также таскают из драйвера в драйвер — не совсем понятно, почему его в итоге не внесут в основной заголовочный файл fb.h.
Задача данного колбэка — заполнить 16-цветную псевдопалитру, к которой будет обращаться уже упомянутый драйвер консоли.
Теперь объявим небольшую функцию, которой мы будем передавать, по сути, прямоугольник, в области которого было произведено воздействие на видеопамять. Функция вычислит, какие страницы видеопамяти затронуты этим прямоугольником, поставит им флаг «требуется апдейт» и запланирует выполнение того же колбэка, который зовется в случае deferred io. После этого все колбэки операций сведутся к вызову функций "sys_" и написанной нами функции, которую мы обзовем touch, по аналогии с командой в Linux.

Функция display_touch
static void display_touch(struct fb_info *info, int x, int y, int w, int h) 
{
        int                              firstPage;
        int                              lastPage;
        int                              i;

        struct usblcd           *dev=info->par;

        firstPage=((y*WIDTH)+x)*BYTE_DEPTH/PAGE_SIZE-1;
        lastPage=(((y+h)*WIDTH)+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(&dev->videopages[i].toUpdate);

        schedule_delayed_work(&info->deferred_work, info->fbdefio->delay);
}


Код, я думаю, вполне понятен — просто вычисляем какие страницы мы затронули исходя из разрешения, округляем всегда в большую сторону, а точнее — просто берем с запасом, по одной странице с обоих концов.
Теперь опишем все остальные колбэки — они становятся очень простыми:

Все колбэки операций
static void display_fillrect(struct fb_info *p, const struct fb_fillrect *rect)
{
        sys_fillrect(p, rect);
        display_touch(p, rect->dx, rect->dy, rect->width, rect->height);
}

static void display_imageblit(struct fb_info *p, const struct fb_image *image)
{
        sys_imageblit(p, image);
        display_touch(p, image->dx, image->dy, image->width, image->height);
}

static void display_copyarea(struct fb_info *p, const struct fb_copyarea *area)
{
        sys_copyarea(p, area);
        display_touch(p, area->dx, area->dy, area->width, area->height);
}

static ssize_t display_write(struct fb_info *p, const char __user *buf, 
                                size_t count, loff_t *ppos)
{       
        int retval;
        retval=fb_sys_write(p, buf, count, ppos);
        display_touch(p, 0, 0, p->var.xres, p->var.yres);
        return retval;
}


Теперь опишем, наконец, наш колбэк от deferred io, который будет слать информацию на дисплей. Он во многом будет совпадать с колбэком .write из прошлой статьи. Будем писать в дисплей по страницам, не забывая приписывать, как и в прошлой статье, к ним требуемый заголовок. Благодаря нашей структуре videopage координаты x, y и длины уже посчитаны, так что все что нужно — просто запихнуть это в буфер и кинуть по USB.

Колбэк отложенного ввода-вывода
unsigned char videobuffer[PAGE_SIZE+8];
static void display_update(struct fb_info *info, struct list_head *pagelist)
{
        struct usblcd*                          dev = info->par;
        int retval;
        struct page *page;
        int i;
        int usbSent=0;

        list_for_each_entry(page, pagelist, lru) 
        {
                atomic_dec(&dev->videopages[page->index].toUpdate);
        }

        for (i=0; i<FP_PAGE_COUNT; i++)
         {
                if(atomic_inc_and_test(&dev->videopages[i].toUpdate))
                        atomic_dec(&dev->videopages[i].toUpdate);
                else
                {
                        *(unsigned short*)(videobuffer)=cpu_to_le16(dev->videopages[i].x);
                        *(unsigned short*)(videobuffer+2)=cpu_to_le16(dev->videopages[i].y);
                        *(unsigned long*)(videobuffer+4)=cpu_to_le32(dev->videopages[i].length>>1);
                        memcpy(videobuffer+8,dev->videopages[i].mem,dev->videopages[i].length);

                        retval = usb_bulk_msg(dev->udev,
                              usb_sndbulkpipe(dev->udev, 1),videobuffer,
                              dev->videopages[i].length+8,
                              &usbSent, HZ);

                        if (retval)
                                printk(KERN_INFO "usblcd: sending error!\n");
                }
        }       
}


Так как в этот колбэк мы можем попасть честным путем(через deferrd io), а можем — по собственному почину (запланировав его выполнение в одном из колбэков операций, вызвав display_touch), мы просто пробежимся по всем переданным нам страницам, если таковые имеются, и пометим их как подлежащие апдейту.
Если мы попали сюда не через отложенный ввод-вывод, то список просто будет пустым.
После этого мы просто проходимся по всем страницам атомарно проверяя необходимость апдейта и выполняя этот самый апдейт путем синхронной посылки по USB. В следующей статье, когда будем допиливать драйвер до нормального состояния, мы заменим синхронную посылку более правильным механизмом, именуемым USB Request Block, или URB. Он позволит кинуть USB-хосту запрос на отправку данных и сразу же вернуться к дальнейшей обработке. А о том, что URB долетел (или не долетел) до получателя нам сообщат в прерывании. Это позволит выжать еще чуть-чуть FPS из нашей системы (не превышая, однако, теоретического предела, о котором я говорил выше).
Нам осталось совсем чуть-чуть — раз уж мы решили поступить плохо и не чистить за собой, то осталось только инициализировать все в колбэке Probe.

Колбэк Probe
static int LCDProbe(struct usb_interface *interface, const struct usb_device_id *id)
{
        struct usblcd                                           *dev;
        struct usb_host_interface                       *iface_desc;
        struct usb_endpoint_descriptor          *endpoint;
        unsigned char                                           *videomemory;
        int retval = -ENODEV;
        int i;
        dev_info(&interface->dev, "USB STM32-based LCD module connected");
        dev = kzalloc(sizeof(*dev), GFP_KERNEL);
        if (!dev) 
        {
                dev_err(&interface->dev, "Can not allocate memory for device descriptor\n");
                retval = -ENOMEM;
                goto exit;
        }

        dev->udev=interface_to_usbdev(interface);

        dev->interface = interface;


        iface_desc = interface->cur_altsetting; 
        for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) 
        {
                endpoint = &iface_desc->endpoint[i].desc;
                if(usb_endpoint_is_bulk_out(endpoint))
                {
                        dev->bulk_out_ep=endpoint;
                        dev->bulk_out_packet_size = le16_to_cpu(endpoint->wMaxPacketSize);
                        break;
                }
        }

        if(!dev->bulk_out_ep)
        {
                dev_err(&interface->dev, "Can not find bulk-out endpoint!\n");
                retval = -EIO;
                goto error_dev;
        }

        dev->gdev = &dev->udev->dev;
        dev->info = framebuffer_alloc(0, dev->gdev);
        dev->info->par = dev;
        dev->info->dev = dev->gdev;

        if (!dev->info) 
        {
                dev_err(&interface->dev, "Can not allocate memory for fb_info structure\n");
                retval = -ENOMEM;
                goto error_dev;
        }

        dev->info->fix = fixed_info;
        dev->info->var = var_info;

        dev->info->fix.smem_len=FP_PAGE_COUNT*PAGE_SIZE;

        dev_info(&interface->dev, "Allocating framebuffer: %d bytes [%lu pages]\n",dev->info->fix.smem_len,FP_PAGE_COUNT);
        
        videomemory=vmalloc(dev->info->fix.smem_len);   
        if (!videomemory) 
        {
                dev_err(&interface->dev, "Can not allocate memory for framebuffer\n");
                retval = -ENOMEM;
                goto error_dev;
        }
        
        dev->info->fix.smem_start =(unsigned long)(videomemory);
        dev->info->fbops = &display_fbops;
        dev->info->flags = FBINFO_FLAG_DEFAULT|FBINFO_VIRTFB;
        dev->info->screen_base = videomemory;

        
        memset((void *)dev->info->fix.smem_start, 0, dev->info->fix.smem_len);

        for(i=0;i<FP_PAGE_COUNT;i++)
        {
                dev->videopages[i].mem=(void *)(dev->info->fix.smem_start+PAGE_SIZE*i);
                dev->videopages[i].length=PAGE_SIZE;
                atomic_set(&dev->videopages[i].toUpdate,-1);
                dev->videopages[i].y=(((unsigned long)(PAGE_SIZE*i)>>1)/WIDTH);
                dev->videopages[i].x=((unsigned long)(PAGE_SIZE*i)>>1)-dev->videopages[i].y*WIDTH;
        }
        dev->videopages[FP_PAGE_COUNT-1].length=FB_SIZE-(FP_PAGE_COUNT-1)*PAGE_SIZE;

        dev->info->pseudo_palette = &dev->pseudo_palette;
                
        dev->info->fbdefio=&display_defio;

        fb_deferred_io_init(dev->info);

        dev_info(&interface->dev, "info.fix.smem_start=%lu\ninfo.fix.smem_len=%d\ninfo.screen_size=%lu\n",dev->info->fix.smem_start,dev->info->fix.smem_len,dev->info->screen_size);

        usb_set_intfdata(interface, dev);

        retval = register_framebuffer(dev->info);
        if (retval < 0) {
                dev_err(dev->gdev,"Unable to register_frambuffer\n");
                goto error_buff;
        }
        
        return 0;

        error_buff:
        vfree(videomemory);

        error_dev:
        kfree(dev);     

        exit:
        return retval;
}


Здесь мы сначала выделяем память под наш дескриптор устройства, потом под структуру описателя фреймбуфера, и только потом — под сам фреймбуфер. Обратите внимание — выделяем через vmalloc. Отличие от kmalloc состоит в том, что vmalloc может перенастроить таблицы страниц виртуальной памяти, и «собрать по кусочками» запрошенный нами буфер. То есть для нас он будет выглядеть единым блоком памяти, но физически он может состоять из страниц даже близко не находящихся друг с другом. kmalloc же возвращает память, которая и физически является единым блоком. Так как мы запрашиваем достаточно большой кусок, хорошей практикой будет воспользоваться vmalloc.
Все, компиляем, инсмодим, и если все сделано правильно лицезреем консоль на дисплее!

Заключение


В этой статье мы сделали, наконец, этот важный шаг — от драйвера кастомного устройства, никак не используемого системой, перешли в объятия фреймворка, который позволил рассказать всем приложениям и драйверам о том, что у нас есть настоящий дисплей. Да, мы поступили немного нехорошо, не очищая за собой ресурсы и никак не хендля отключение устройства, поэтому выдергивать USB и перевтыкать его на работающем девайсе пока не рекомендуется.
Но это мы обязательно исправим в следующей статье. Что нам предстоит сделать?
  • Реализовать поддержку двойной буферизации на устройстве, обеспечив повышение FPS раза в два.
  • Исправить наш недочет, из-за которого консоль зеленая (если мы запустим что-нибудь кроме консоли, то выглядеть это будет ужасно. Может, кто-нибудь уже догадался в чем дело?)
  • Реализовать правильную очистку ресурсов, чтобы можно было спокойно отключать и подключать девайс
  • Перейти на использование асинхронного механизма URB'ов вместо синхронной посылки Bulk-мессаг, что снизит потери в драйвере
  • Скомпилировать разные веселые приложения, чтобы полностью насладиться нашим мини-компьютером. Так, например, мы посмотрим, что нужно пропатчить в коде движка Gobliins, чтобы они, наконец, запустились.

Ниже представлено еще одно видео со старым квестом (не менее любимым мной, чем Gobliins), несколько фото небольшой графической оболочки, которую можно без проблем запустить на устройстве и консольный браузер Elinks.


Первая Кирандия


Хабр в консольном браузере (шрифт VGA8x8)


Оболочка Gmenu2X


Встроенный в нее файловый браузер


Настройки

На этом у меня пока все. Удачной реализации и до следующей статьи!
+139
41249
356
Ariman 174,6

Комментарии (34)

+5
k1b0rg #
Жму руку
+1
SysCat #
Гениально, почти карманные Гоблины… Бежим раскупать железки. Жаль производитель не знает.
0
Ariman #
Для карманных могу порекомендовать mr3040, по железу такой же, но с аккумулятором внутри, идеален для кастомных мобильных девайсов.
+10
Nomad1 #
Уважаемый автор — псих. И это здорово!
0
RNZ #
respect и уважуха! ))
0
k1b0rg #
А че за оболочка Gmenu2X?
0
Ariman #
Не знаю, для какого-то мобильного девайса разрабатывалась, я нашел ее порт под OpenWRT (да там, вроде как, ничего особо и не портировали, она и так была под embedded linux). Поставил ради эксперимента, выглядит неплохо. Мышь, похоже, не поддерживает.
НЛО прилетело и опубликовало эту надпись здесь
0
wosk #
Вот это энтузиазма у аффтора. Хоть вагонами отгружай )
Спасибо большое за кадры из Кирандии. Это была игра моего детства )
+1
gibson_dev #
Великолепно ;)
0
vitmeat #
Во многих роутерах есть USB2.0, можно ли будет подключить несколько дисплеев, через usb хаб?
Просто ради спортивного интереса =)
0
Ariman #
Дык и в этом 2.0, можно, конечно. Он на видео через хаб и подключен, т.к. еще клавиатура и мышка.
Кстати, как fbcon хендлит несколько дисплеев я не знаю, возможно что и никак — надо поглядеть в исходниках.
А можно драйвер допилить, чтобы он их как один удвоенного разрешения юзал.
0
datacompboy #
подключил еще один экран — он автоматически пристроился снизу.
подключил еще два — пристроились сбоку.
воткнул 16 экранов — получил нормальное 1280*960
+1
Ariman #
Да, и 0.1 FPS)
0
datacompboy #
ну почему же? USB2.0 вполне позволит 16 устройств USB1.1 подключить и те же 8 FPS :)
0
Ariman #
Да, вообще вы правы)
Но, с другой стороны, тут уже сам 400 МГц процессор роутера может начать задыхаться. Хотя 800х600 из таких составных он, я полагаю, потянет).
Либо надо брать СТМку по жопастее, с USB 2.0, и более здоровую панель (китайцы продают на e-bay).
0
datacompboy #
такую панель ради интереса можно собрать и к десктопу… правда зачем — непонятно.
+1
datacompboy #
Тач! Я требую драйвер поддержки тача в обратную очередь!
+1
Ariman #
А с тачем занятно, ему драйвер в линухе-то не нужен будет (он там, кстати, есть).
Он ведь не по SPI подключен, а по USB. Таким образом нужен драйвер на STMке и добавление в дескриптор еще одного интерфейса, HID.
Может и сделаю, если время будет.
0
datacompboy #
причем стандартная библиотека STMки уже имеет HID устройство и драйвер тача. но я не видел их спаренных еще пока.
0
Ariman #
HID заводил, драйвер тача я там не видел, честно говоря. Но это можно и на базе линуксового сделать, если уж там что-то страшное — в линухе я видел исходники драйвера именно этого контроллера тача.
0
datacompboy #
м.м… у меня stm32 который пришел борд шел в комплекте с софтом, в том числе и с драйвером тача — то есть демо-прошивка на ура работала с тачем.
0
sev #
А scummvm с каким бекендом компилировался? Я так понмиаю, sdl?
0
Ariman #
Да, разумеется. Там все на SDL, эта оболчка тоже.
Единственное что — очень многие движки там напрочь игнорируют опцию mute, точнее они просто выставляют громкость в 0 и все. И падают на попытках работать с микшером.
Пришлось тех же гоблинов патчить, чтобы они при мьюте вообще не обращались к микшеру.
+1
sev #
Давайте патч, включим. Просто с этой проблемой никогда не сталкивались раньше, и выставления громкости в нуль хватало.
0
Ariman #
Видимо, хватало если устройство хоть как то поддерживает звук.
Я компилял ядро с поддержкой звука даже, все равно падало на микшере, в ::PlaySound, кажется, на assert(MixerReady).
Я особенно глубоко не вникал в архитектуру, поэтому просто в движке гоблинов добавил в конструкторе условие — если мьют, то не создавать объект для работы со звуком, а дальше в коде уже были проверки, что если он null, то не трогать звук.
Для гоблинов это проблему решило, но глобально — нет. Вторая кирандия, например, падает, когда пытается воспроизвести звук.
0
sev #
Ах, это. Тогда вам нужно сделать кастомный SDL бекенд, в котором выбрать null драйвер для звука. Могу рассказать, но это уже лучше в оффлайне.
0
Ariman #
Вообще это несколько странно, имхо логичнее если выбран мьют вообще не трогать микшер…
Причем я sdl даже задал через переменные среды чтоб выводил в файл и файл задал /dev/null, но все равно в микшер лезут…
–1
noonv #
супер! огромное спасибо за проделанную работу!
0
Plague #
А что делает на фотках «Всепожирающее пламя»? :-)
0
Ariman #
Выпало из колоды, которой подперт экран)
0
vitmeat #
А еще клево было бы замутить USB -> VGA переходничок на СТМке, и к старому маленькому ЭЛТ/LCD монитору подключить. =)
0
oisee #
по старой памяти для меня «мини-компьютер» — это что-то размером с холодильник =)
0
dlinyj #
Наконец нашёл время и достаточное количество внимания чтобы детально прочитать всю серию. Спасибо. Тут постоянно идут отсылки к четвёртой части, планируется ли она в серию?

За сам гайд написания драйвера — поясной поклон! Это очень полезная серия статей. Так же, хотелось бы узнать что за оболочки и как они собираются для OpenWRT. У меня на х86 платке бегает OpenWRT, хотел к ней прикрутить какие-нить гуи.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.