Pull to refresh

Миникомпьютер из роутера с OpenWRT: пишем USB class-driver под Linux

Reading time 20 min
Views 78K

Добрый день, уважаемые хабровчане. В прошлой статье мы с вами разработали простую USB-видеокарту на базе STM32F103 и китайского дисплейного модуля на контроллере ILI9325.
Проверяли мы его из юзерспейса, при помощи LibUSB. Ну что ж, пришло время нам написать свой собственный драйвер, который позволит делать все то же самое, но из-под Linux и без дополнительных библиотек. Этот драйвер мы внесем в дерево исходников OpenWRT и он поселится там наравне со всеми остальными.
Начнем.

Введение


Так как железо с прошлой статьи у нас не поменялось, начнем с рассмотрения того, что нам нужно будет достичь. Но перед этим, как всегда, порекомендую ознакомиться со следующими материалами: прежде всего, это книга Linux Device Drivers, в которой изложено все необходимое для разработчика драйверов под линукс. Во-вторых, рекомендую еще раз ознакомиться с моей статьей про настройку и допиливание OpenWRT нашего роутера, т.к. нам придется очень много возиться с системой сборки и конфигами. Ну и наконец – очень, очень полезный ресурс, в свободном доступе выложены презентации с их обучающих семинаров.
Да, разработку мы будем вести, разумеется, из-под Linux, поэтому нужно заранее озаботиться наличием какого-нибудь дистрибутива. Я работаю под Linux Mint, но от дистрибутива мало что будет зависеть, единственные специфичные для него команды – это команды установки из репозитория дополнительных утилит.
Теперь рассмотрим подход к, собственно, драйверу. Любому, кто хоть немного знаком с драйверами устройств в Линукс на ум сразу приходят два общеизвестных типа этих самых устройств – символьные и блочные. Это, пожалуй, первое, что рассказывается в любой статье про драйверы. Обычно после этого еще идет фраза, что отдельно стоят драйверы сетевых интерфейсов.
Однако полная картина излагается далеко не везде. Многие драйверы не регистрируются напрямую как символьные или блочные, несмотря на то, что в системе отображаются таковыми. Вместо этого, они регистрируются через специализированный фреймворк соответствующего типа – например, framebuffer, input, tty и т.п. Фреймворк определяет общий интерфейс для драйверов и предоставляет стандартизованный интерфейс системе.


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

Подготавливаем рабочее пространство


Сам драйвер будет достаточно простой, но мы рассмотрим важные вопросы по добавлению его в дерево исходников. Начнем с хеллоуворлдного каркаса драйвера.

Хеллоуворлдный модуль ядра
#include <linux/module.h>	/* Needed by all modules */
#include <linux/kernel.h>	/* Needed for KERN_INFO */
#include <linux/init.h>		/* Needed for the macros */
#define DRIVER_AUTHOR "Amon-Ra"
#define DRIVER_DESC   "USB STM32-based LCD module driver"

static int __init lcddriver_init(void)
{
	printk(KERN_INFO "Hello, world!\n");
	return 0;
}

static void __exit lcddriver_exit(void)
{
	printk(KERN_INFO "Goodbye, world!\n");
}

module_init(lcddriver_init);
module_exit(lcddriver_exit);

MODULE_LICENSE("GPL");

MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);


Этот код в пояснениях не нуждается, он самый что ни есть хеллоуворлдный – мы описываем два колбэка и регистрируем их как функции, вызываемые при инициализации модуля и при его выгрузке. В них мы просто выводим сообщения.
Сохраняем этот код под именем usblcd.c.
Теперь давайте вспомним, как вообще работает система, генерирующая на выходе нашу прошивку. Это весьма сложный продукт, называющийся BuildRoot, который, по сути, берет на себя все заботы: он выкачивает и компилирует тулчейн для кросс-сборки кода, выкачивает исходники и патчи для ядра и сторонних утилит, генерирует конфигурационные файлы для всего этого в соответствии с более высокоуровневыми настройками, выполняет компиляцию, собирает скомпилированное в пакеджи, генерирует файловую систему, собирает все вместе и пакует в итоговый образ.
Из этого следует, что конфиги для сборки, допустим, ядра (а вообще-то – для всего что только можно), каждый раз генерируются динамически, и править генеренный конфиг или даже мейкфайл смысла нет, они будет перезаписаны.
В стандартном меню конфигурации образа, которое мы вызываем make menuconfig, мы также не найдем настроек ядра. Так как же быть?
Оказывается, конфиг для ядра генерируется из базового, который мы можем поменять. Для этого достаточно вызвать make kernel_menuconfig. Вот тут надо быть осторожным, это база для генерации таргет-специфик конфига, и откатиться через make clean не получится, только выкачав из репозитория исходники заново. Поэтому в нем действуем внимательно и запоминаем, что включили, а что выключили!
Правда, это не то, что нам сейчас нужно, ведь для того, чтобы скомпилировать наш драйвер, мы должны внести правки не только в конфиг, но и в файлы Makefile (определяющий, какие модули будут скомпилированы) и Kconfig (на основе которого генерируется меню из make kernel_menuconfig и который содержит опции для Makefile) – без этого в меню мы даже не найдем упоминания о нашем драйвере.
Кроме того, нам нужно, чтобы наш usblcd.c оказался в директории с исходниками линукс, в поддиректории /drivers/video.
И первое, что мы должны уяснить на данный момент – генерация таргет-специфик дерева исходников происходит при помощи файлов, лежащих в target/linux/<цель>/
Buildroot берет чистые исходники ядра и помещает их в build_dir/<целевая архитектура>/<целевая платформа>/linux-<версия ядра>
После этого, туда же копируются дополнительные файлы с исходным кодом из target/linux/<цель>/files – обычно, там лежат драйверы, специфичные для целевой платформы и другой платформенно-зависимый код.
После применяется набор патчей, находящийся в target/linux/<цель>/patches. В этих патчах обычно как раз и содержится дополнение для Makefile и Kconfig, добавляющее соответствующие драйверы в процесс сборки. Кроме платформ-специфик патчей также применяются патчи из target/linux/genegic, более общие, не заточенные под конкретный девайс.
После этого генерируется конфиг для сборки ядра и запускается, собственно, сборка того, что лежит в build_dir.
Теперь пойдем последовательно и по пунктам, как и в прошлой статье, и начнем с размещения usblcd.c.
  1. Заходим в target/linux/ar71xx/files/drivers и выполняем там mkdir video, создав директорию для нашего драйвера. Buildroot раскидывает содержимое files по соответствующим директориям дерева исходников. Помещаем файл usblcd.c в только что созданную папку.
  2. В target/linux/ar71xx/modules.mk добавляем запись для нового кернел-пакеджа. Тут следует помнить, что кернел-пакеджи — это не то же самое, что и кернел-модули. Кернел-пакеджи — это пакеты, предназначенные для утилиты opkg в OpenWRT, их можно вбилдить в образ, либо скомпиллить как отедльный пакет, но это никак не повлияет на то, как будет собран соответствующий модуль ядра.
    Говоря проще — если у нас есть некоторый бинарник, мы можем либо его запихнуть в пакедж больше ничего не делать (вариант M в make menuconfig), оставляя его лежать в директории со сбилденными пакеджами, либо вбилдить в файловую систему строящегося образа, то есть, предустановить его по умолчанию. При этом, если этот бинарник — кернел-модуль, то, при компиле ядра, само собой, он будет сбилден именно как отдельный модуль, чтобы его можно было упихать в пакедж. Встроить модуль ядра в само ядро можно только через make kernel_menuconfig (либо через патч для этих самых кернел конфигов).
    Это может по началу вызвать некоторую путаницу и выяснения, почему пакедж, который явно указан как встроенный, порождает отдельные кернел-модули. Однако если осмыслить этот механизм, все становится понятно и логично.
    Будем билдить драйвер как отдельный модуль, а вбилдивать его в ФС или нет можно будет каждый раз решать заново, переконфигурируя сборку через make menuconfig.

    Запись в modules.mk
    define KernelPackage/usb-lcd
      SUBMENU:=$(USB_MENU)
      TITLE:=USB STM32-based LCD module
      DEPENDS:=@TARGET_ar71xx
      KCONFIG:=CONFIG_STM32_USB_LCD
      FILES:=$(LINUX_DIR)/drivers/video/usblcd.ko
    endef
    
    define KernelPackage/usb-lcd/description
      Kernel module for USB STM32-based LCD module.
    endef
    
    $(eval $(call KernelPackage,usb-lcd))
    


    Здесь мы задаем правила для новой записи, вызываемой make menuconfig – говорим, что пакедж будет в подменю USB, с заданным нами заголовком, относящийся к платформе на базе AR71xx.
    Далее сообщаем, какую опцию ядра он активизирует – обзовем ее CONFIG_STM32_USB_LCD.
    Потом идет название файла, который система будет упихивать в пакедж – после компиляции наш модуль станет usblcd.ko, его и указываем. Потом задаем описание и не забываем последнюю строчку, дергающую скриптовую обработку всего этого.
  3. Из корня OpenWRT Выполняем make menuconfig ищем в Kernel modules/USB наш пакедж, и говорим что хотим его включить в получившийся образ.
  4. Этого пока мало, чтобы модуль был собран, выбор этого пакеджа всего лишь определит опцию CONFIG_STM32_USB_LCD при сборке ядра. Для того чтобы эта опция что-нибудь значила, нужно отредактировать файлы Kconfig и Makefile из <директория с исходниками ядра>/drivers/video. Однако, если мы полезем в build_dir за этим, ничего хорошего не получим, мы уже выяснили, что содержимое каждый раз генерируется заново. Правильный путь — создать боард-специфик патч для файлов ядра и поместить его в /target/linux/ar71xx/patches. Для этого нам пригодится quilt — система для работы с патчами. Подробнее про это можно и нужно почитать в статье с Wiki.OpenWRT.
    Устанавливаем quilt (sudo apt-get install quilt) и настраиваем его по статье из вики:

    cat > ~/.quiltrc <<EOF
    QUILT_DIFF_ARGS="--no-timestamps --no-index -pab --color=auto"
    QUILT_REFRESH_ARGS="--no-timestamps --no-index -pab"
    QUILT_PATCH_OPTS="--unified"
    QUILT_DIFF_OPTS="-p"
    EDITOR="nano"
    EOF
    

  5. Выполняем команду

    make target/linux/{clean,prepare} V=s QUILT=1
    

    Она подготовит нам дерево исходников в build_dir к дальнейшему использованию.
  6. Переходим в директорию с исходниками ядра в build_dir. Версия может отличаться, так что нужно будет скорректировать путь в соответствии с номером версии ядра.
    После этого применяем все патчи, которые предназначены для нашей сборки.

    cd build_dir/target-mips_r2_uClibc-0.9.33.2/linux-ar71xx_generic/linux-3.6.9/
    quilt push –a
    

  7. Далее нужно создать патч с именем, соответствующим определенному правилу — оно должно начинаться с цифры, которая больше, чем у любого из уже существующих патчей — это влияет на порядок применения патчей и позволяет избежать конфликтов. Чтобы узнать, какие патчи применены к этой версии ядра используем команду quilt serries и получаем большой-большой список, содержащий общие (generic) и специфичные для платформы (platform) патчи. Т.к. патч у нас будет специфичный для платформы, смотрим на Platform/xxx-<имя>.patch. Для той ревизии, что у меня, последним оказался platform/a06-rb750_nand-add-buffer-verification.patch поэтому номер возьмем b00.
    Исполняем

    quilt new platform/b00-usb-lcd.patch
    

  8. Сообщаем кильту какие файлы мы будем патчить:

    quilt add drivers/video/Kconfig 
    quilt add drivers/video/Makefile
    

  9. Редактируем файлы чем удобнее. Начнем с Kconfig, содержащего определения для меню настройки ядра. Наш драйвер пока не будет «честным» драйвером фреймбуфера, поэтому добавим его в корень, то есть до строк
    menuconfig FB
    tristate «Support for frame buffer devices»

    Пишем:

    config STM32_USB_LCD
    	tristate "USB STM32-based LCD module support"
    	help
    	  Simple USB STM32-based LCD module driver
    

    Это внесет в меню настройки ядра новый пункт, находящийся в меню Drivers – Graphics Support.
    Теперь редактируем Makefile, добавляя в него куда нибудь после # Hardware specific drivers go first строку

    obj-$(CONFIG_STM32_USB_LCD)             += usblcd.o
    

    Это добавит наш модуль в список компилируемых, если, конечно, он будет выбран в конфиге.
  10. Удостоверяемся, что все хорошо, отдав команду quilt diff, которая должна вывести получившийся файл патча.
    После говорим quilt refresh, сохраняя полученный патч.
  11. Возвращаемся в корень билдрута и говорим make target/linux/update V=s
    проверяем, что последними строками вывода команды будут примерно такие
    `/home/ra/openwrt/trunk/build_dir/target-mips_r2_uClibc-0.9.33.2/linux-ar71xx_generic/linux-3.6.9/patches/platform/b00-usb-lcd' -> `./patches-3.6/b00-usb-lcd'
  12. Отдаем команду make clean && make, генерируя образ для прошивки, заливаем его на роутер (в /tmp, например) и перепрошиваемся. Можно через mtd, можно через sysupgrade — во втором случае можно сохранить свои настройки /etc, что может оказаться полезно (ключ -с):

    scp bin/openwrt-ar71xx-generic-tl-mr3020-v1-squashfs-sy
    supgrade.bin root@192.168.0.48:/tmp
    sysupgrade -c openwrt-ar71xx-generic-tl-mr3020-v1-squashfs-sy
    supgrade.bin 
    

  13. Заходим на роутер по SSH, смотрим, что у нас лежит в /lib/modules — если все сделали правильно, среди прочих модулей ядра там будет наш usblcd.ko
    Выполним insmod usblcd && rmmod usblcd
    Ничего не должно упасть и вообще как-либо отреагировать.
    Пишем dmesg — и вот что мы должны увидеть в последних сообщениях:

    [ 291.630000] Hello, world!
    [ 291.640000] Goodbye, world!

Поздравляю, модуль успешно собран и внедрен. Не обязательно каждый раз пересобирать весь образ и перешивать роутер, достаточно сказать make clean && make target/compile, что пересоберет только ядро с модулями, после чего нужные модули можно перекинуть на роутер по SCP. Теперь можно переходить к собственно написанию драйвера.

Драйвер


Здесь нам очень помогут исходные коды драйвера под названием usb-skeleton, которые можно просмотреть онлайн вот тут.
Кроме того, при гуглении обнаруживается неплохая статья в которой как раз описывается процесс разработки USB-драйвера для своего устройства.

  1. Прежде всего, мы должны объявить структуру, по которой ядро будет знать, какие именно устройства мы можем обслужить. Объявляем таблицу usb_device_id:

    static struct usb_device_id lcd_table[]={
    	{USB_DEVICE(DEVICE_VENDOR_ID, DEVICE_PRODUCT_ID)},
    	{ }
    };
    

    Здесь DEVICE_VENDOR_ID и DEVICE_PRODUCT_ID задефайненные VID и PID равные, соответственно, 0xDEAD и 0xF00D – такие же, как у нашей видеокарты.
  2. Сразу же после объявления вызываем макрос
    MODULE_DEVICE_TABLE(usb, lcd_table);
    сообщая юзерспейсу, что мы обрабатываем девайсы, указанные в таблице.
  3. Объявляем колбэки
    void LCDProbe(struct usb_interface *interface, const struct usb_device_id *id)
    и
    void LCDDisconnect(struct usb_interface *interface)
    Они будут вызваны при подключении и отключении устройства. Пока оставим их пустыми.
  4. Объявляем важную структуру, в которой объединяем все вышенаписанное:

    Структура usb_driver
    struct usb_driver usblcd_driver={
    	.owner = THIS_MODULE,
    	.name = "usblcd",
    	.probe = LCDProbe,
    	.disconnect = LCDDisconnect,
    	.id_table = lcd_table,
    };
    


  5. Обычно, регистрация нового USB драйвера выполняется в module_init, а дерегистрация — в module_exit, но вместо того, чтобы описывать эти два колбэка существует удобный макрос, который избавляет от необходимости вручную описывать функции инициализации и деинициализации модуля, они будут включены в код при использовании макроса и будут содержать вызовы usb_register(...) и usb_deregister(...), регистрируя/дерегистрируя наш драйвер, а больше от них ничего и не требуется:

    module_usb_driver(usblcd_driver);
    


Пришло время проверить драйвер — сохраняем написанное, отдаем команду make clean && make target/compile V=s, внимательно следя за тем, чтобы в процессе компилляции не возникло ошибок, после — перебрасываем получившийся usblcd.ko на устройство, заменяя старую версию.
Теперь на роутере делаем insmod usblcd.ko, подключаем дисплей, ждем секунду-другую, отключаем его и делаем rmmod usblcd. После чего вызываем dmesg.
Должно получиться что-то вроде этого:

[ 6002.060000] usbcore: registered new interface driver usblcd
[ 6010.850000] usb 1-1: new full-speed USB device number 2 using ehci-platform
[ 6011.010000] USB STM32-based LCD module connected
[ 6015.140000] usb 1-1: USB disconnect, device number 2
[ 6015.140000] USB STM32-based LCD module disconnected
[ 6024.240000] usbcore: deregistering interface driver usblcd


Помним, что printk не flush'ит, если в конце строки не стоит \n, поэтому, если хотим видеть информацию от драйвера в хронологическом порядке и без задержки, не забываем оканчивать наши отладочные сообщения символом перехода на новую строку.
Пришло время написать в драйвере что-нибудь полезное. Так как мы не будем пока писать настоящий драйвер фреймбуфера, зарегистрируем так называемый класс-драйвер USB-устройства. При его регистрации мы, точно также как и с символьным устройством, укажем колбэки для файловых операций через структуру-дескриптор file_operations для обычных действий с файлами — открытия, закрытия, чтения, записи и т.п. В этом случае системе будет плевать, что болтается на другом конце шнура — дисплей, звуковая крата или мышь. Использовать их в данном качестве получится только через свои юзерспейсные приложения, работающие с устройствами чтением-записью в файлы девайсов в нужном формате.
Стартануть иксы, допустим, на таком дисплее не получится — иксам нужен фреймбуфер, предоставляющий стандартизованный интерфейс.
Регистрируется такой драйвер при помощи структуры

static struct usb_class_driver usblcd_class = {
	.name =		"lcd%d",
	.fops =		&LCD_fops,
	.minor_base =	LCD_MINOR_BASE,
};

Здесь первое поле задает имя устройства (%d используется для подстановки номера устройства после имени), второе — указатель на структуру файловых операций, третье — номер, начиная с которого будут выдаваться значения миноров для устройства.

Порядок следующий:
  1. Объявляем колбэки (для начала можно их оставить без кода, только с return 0)

    Колбэки
    static int LCDOpen(struct inode *inode, struct file *filp);
    static int LCDrelease(struct inode *inode, struct file *file);
    static ssize_t LCDwrite(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos);
    


  2. Объявляем структуру file_operations:

    Структура file_operations
    static struct file_operations LCD_fops = {
    	.owner 		=	THIS_MODULE,
    	.write 		=	LCDwrite,
    	.open 		=	LCDOpen,
    	.release 		=	LCDrelease,
    };
    


    Отсутствие колбэка .read означает доступ только для записи (аналогично можно поступить не добавляя write. Отсутствие open или release не будет означать невозможность открыть или закрыть файл устройства, а, напротив, будет значить что эти операции всегда выполняются успешно).
  3. Почти всегда есть необходимость хранить какие-то глобальные данные, связанные с устройством и передавать их во все эти колбэки, поэтому обычно заводят свою, кастомную структуру, одним из полей которой будет указатель на usb_device, отданный нам системой при вызове колбэка probe (а точнее, полученный из переданного в probe интерфейса), остальные поля можно задать по своему усмотрению. Итак, описываем структуру-дескриптор девайса:

    Структура-дескриптор устройства usblcd
    struct usblcd
    {
    	struct usb_device 			*udev;
    	struct usb_interface 		*interface;
    	unsigned char			 minor;
    
    	struct usb_endpoint_descriptor	*bulk_out_ep;
    	unsigned int 			bulk_out_packet_size;
    	unsigned char			*videobuffer;
    };
    


  4. Опишем колбэк Probe. В нем инициализируем структуру девайса и выполняем все инициализации связанных с девайсом буферов. Если у нас есть буферы, которые индивидуальны для каждого открытого экземпляра файла устройства, то инитить их надо в LCD_fops.open

    Код колбэка 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;
    
    	int retval = -ENODEV;
    	int i;
    
    	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    	if (!dev) 
    	{
    		dev_err(&interface->dev, "Out of memory\n");
    		retval = -ENOMEM;
    		goto exit;
    	}
    
    	dev->udev=interface_to_usbdev(interface);
    	mutex_init(&dev->io_mutex);
    
    	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->videobuffer=kmalloc(TOTAL_BUFFER_SIZE,	GFP_KERNEL);
    
    	if (!dev->videobuffer) 
    	{
    		dev_err(&interface->dev, "Out of memory\n");
    		retval = -ENOMEM;
    		goto error_dev;
    	}
    
    	usb_set_intfdata(interface, dev);
    
    	retval = usb_register_dev(interface, &usblcd_class);
    	if (retval) {
    		dev_err(&interface->dev, "Not able to get a minor for this device.");
    		usb_set_intfdata(interface, NULL);
    		goto error_buff;
    	}
    
    	dev->minor = interface->minor;
    	dev_info(&interface->dev, "USB STM32-based LCD module connected as lcd%d\n",dev->minor-LCD_MINOR_BASE);
    	return 0;
    
    	error_buff:
    	kfree(dev->videobuffer);
    
    	error_dev:
    	kfree(dev);	
    
    	exit:
    	return retval;
    }
    


    Указатель на usb_device *udev получаем из переданного ядром в колбэк указателя *usb_interface через interface_to_usbdev(interface). Так как в пределах одного устройства может быть несколько несколько интерфейсов, получаем текущий описатель интерфейса (usb_host_interface) из поля той же структуры (interface->cur_altsetting(); )
    Далее проходимся по всем эндпоинтам интерфейса, проверяя, есть ли там нужные нам, cохраняя указатели на них и приемлемый размер пакетов для дальнейшего быстрого доступа в нашу структуру-дескриптор устройства. Для bulk-эндпоинтов размер буфера при посылке можно указать больше, чем это значение, ядро само разобьет ваши данные на пакеты подходящего размера, так что, в принципе, это значение дальше в коде нигде не используется, я сохранил его просто для собственного спокойствия, убедившись, что там именно те 0х40 байт, что я указал в дескрипторе. Проверка на принадлежность точки к тому или иному типу выполняется макросами вида usb_endpoint_is_bulk_out(endpoint). Не забываем, что для корректного получения значения числовых полей, нужно привести их порядок байт в соответствие с используемым процессором средствами функций вроде le16_to_cpu (little endian 16-бит в порядок cpu). Когда все инициализировано, указатель на дескриптор сохраняется в той самой структуре ядра, которую оно передает в колбэки, через вызов usb_set_intfdata(interface, dev); Последним шагом в функии Probe будет регистрация класс-драйвера USB. С этого момента в /dev появится файл /dev/lcd[n], а функции-колбэки из LCD_fops могут быть вызваны в любое время(при обращении к файлу устройства). Успешное завершение регистрации занесет в interface->minor присвоенный нам минор. Вычтя из него LCD_MINOR_BASE мы получим номер, под которым устройство будет видно в /dev/ — т.к. дисплей у нас один, то минор будет равен LCD_MINOR_BASE, соответственно в /dev появится /dev/lcd0.
  5. Сразу же симметрично пишем код колбэка Disconnect, получив указатель на наш дескриптор usblcd через вызов dev = usb_get_intfdata(interface);

    Код колбэка Disconnect
    static void LCDDisconnect(struct usb_interface *interface)
    {
    	struct 	usblcd 			*dev;
    	int 					minor;
    	
    	dev = usb_get_intfdata(interface);
    	minor=dev->minor;
    	usb_set_intfdata(interface, NULL);
    	usb_deregister_dev(interface, &usblcd_class);
    
    	dev->interface = NULL;
    
    	kfree(dev->videobuffer);
    	kfree(dev);
    	dev_info(&interface->dev,"USB STM32-based LCD module lcd%d disconnected\n",minor-LCD_MINOR_BASE);
    }
    


    Устанавливаем указатель на данные в интерфейсе в NULL и дерегестрируем класс-драйвер. С этого момента нас уже никто не дернет из юзер-спейса. После этого чистим все наши выделенные буферы и последним шагом очищаем память из-под самой структуры dev. Важно помнить, что в любой момент может быть вызван любой колбэк из LCD_fops, то есть если внезапно выдернуть устройство, любой другой просесс может в данный момент находиться посреди процедуры открытия, чтения или записи. Если код этих колбэков не написан правильно и не содержит синхронизации, это может привести к падению внутри ядра, что очень и очень плохо. Поэтому всегда стоит спрашивать себя «а что, если на этой строчке Disconnect вызовется Open? а если Write?»
    В этом случае поможет мютекс, но его мы допишем позже, когда будем писать write.
  6. Ограничим количество открытых файлов устройства одним, чтобы избежать проблем с параллельной записью видеоданных разными процессами.
    В случае с фреймбуффером такое решение не пройдет и придется придумывать обходные пути, но сейчас ограничимся этой реализацией.
    Для этого объявим атомарную глобальную переменную, показывающую, свободно наше устройство или нет. Можно, конечно, включить ее в структуру дескриптора девайса, но это будет означать необходимость получения ее из интерфейса, поэтому я предпочел оставить ее снаружи: static atomic_t DeviceFree=ATOMIC_INIT(1);
    А в колбэке Open напишем код, рекомендованный в Linux Device Drivers:

    if(!atomic_dec_and_test(&DeviceFree))		
    	{
    		atomic_inc(&DeviceFree);
    		return -EBUSY;
    	}
    	return 0;
    
    

    atomic_dec_and_test не может быть прервана, таким образом, если устройство было свободно (DeviceFree = 1), выполнится декремент (DeviceFree = 0), и функция вернет true, т.к. возвращает true только в случае если после выполнения операции переменная стала нулем.
    Если же устройство было занято (DeviceFree = 0), проверка сделает DeviceFree = -1 и вернет false. После этого нужно вернуть переменную в ее исходное значение (т.к. мы уже провели декремент) — выполнить атомарный инкремент и вернуть статус ошибки (EBUSY, устройство занято). Надо заметить, что после того, как код выполнил проверку и вошел в блок ифа, запросто может выполнится еще одно открытие девайса — но это уже ничем не помешает, т.к. еще один декремент сделает DeviceFree равным -2 и проверка опять вернет false.
  7. Сразу же добави в колбэке Release
    atomic_inc(&DeviceFree);
    сообщая, что устройство свободно.


Теперь уже можно проверить работоспособность кода, скомпилировав его и перенеся на роутер.
После выполнения insmod usblcd.ko и подключения устройства, dmesg должен сказать что-то вроде:

[ 9323.880000] usbcore: registered new interface driver usblcd
[ 9334.640000] usb 1-1: new full-speed USB device number 4 using ehci-platform
[ 9334.800000] usblcd 1-1:1.0: USB STM32-based LCD module connected as lcd0

В /dev/ должно появиться устройство /dev/lcd0, чтение из которого невозможно

root@OpenWrt:~# cat /dev/lcd0 
cat: read error: Invalid argument

А запись возможна только когда в устройство не пишет кто-то еще (выполняем одну и ту же команду в двух инстанциях ssh):

root@OpenWrt:~# cat /dev/urandom > /dev/lcd0 
-ash: cannot create /dev/lcd0: Device or resource busy

Продолжим реализацию.

  1. Подключаем заголовочный файл <linux/mutex.h>, добавляем в структуру-дескриптор поле
    struct mutex io_mutex;
    в колбэк Probe — его инициализацию вызовом mutex_init(&dev->io_mutex);
  2. Дополняем код колбэка Disconnect, добавляя лок после дерегистрации класс-драйвера:

    Дополненный колбэк Disconnect
    static void LCDDisconnect(struct usb_interface *interface)
    {
    	struct 	usblcd 			*dev;
    	int 					minor;
    	
    	dev = usb_get_intfdata(interface);
    	minor=dev->minor;
    	usb_set_intfdata(interface, NULL);
    	usb_deregister_dev(interface, &usblcd_class);
    
    	mutex_lock(&dev->io_mutex);
    	dev->interface = NULL;
    	mutex_unlock(&dev->io_mutex);
    
    	kfree(dev->videobuffer);
    	kfree(dev);
    	dev_info(&interface->dev,"USB STM32-based LCD module lcd%d disconnected\n",minor-LCD_MINOR_BASE);
    }
    


    Теперь, перед тем, как осуществлять какие-либо операции ввода вывода нам достаточно запросить лок мютекса и внутри проверить dev->interface на равенство нулю. То есть, выполняя в других колбэках код после лока io_mutex, можно быть уверенным, что девайс либо еще не выдернули (тогда dev->interface будет указывать на структуру и обращение к нему не приведет к падению), либо дисконнект уже выполнился (тогда там будет NULL).
  3. Дополняем код колбэка Open:
    после проверки на то, что мы открыли устройство впервые, получаем минор открытого устройства через
    subminor = iminor(inode);
    и его usb_interface через interface = usb_find_interface(&usblcd_driver, subminor);
    После получаем из него наш дескриптор устройства через dev = usb_get_intfdata(interface); не забывая на каждом шаге проверять правильность возвращенных данных. После вызываем наш лок, mutex_lock(&dev->io_mutex); чтобы удостовериться, что никто не произведет очистку, выдернув устройство в процессе открытия.
    Наконец, связываем дескриптор устройства со структурой открытого файла, чтобы иметь к нему доступ из колбэков LCD_fops:
    filp->private_data = dev;

    Дополненный колбэк Open
    static int LCDOpen(struct inode *inode, struct file *filp)
    {
    	struct usblcd 				*dev;
    	struct usb_interface 		*interface;
    	int retval		=			0;
    	int subminor;
    	filp->private_data=NULL;
    	if(!atomic_dec_and_test(&DeviceFree))		
    	{
    		atomic_inc(&DeviceFree);
    		retval = -EBUSY;
    		goto exit;
    	}
    
    	subminor = iminor(inode);
    	interface = usb_find_interface(&usblcd_driver, subminor);
    	if (!interface) {
    		printk(KERN_ERR "usblcd driver error, can't find device for minor %d\n", subminor);
    		retval = -ENODEV;
    		goto exit;
    	}
    	dev = usb_get_intfdata(interface);
    	if (!dev) 
    	{
    		retval = -ENODEV;
    		goto exit;
    	}
    	
    	mutex_lock(&dev->io_mutex);
    	
    	if(!dev->interface)
    	{
    		retval = -ENODEV;
    		goto unlock_exit;
    	}
    	
    	filp->private_data = dev;
    	dev_info(&interface->dev, "usblcd: opened successfuly");
    
    	unlock_exit:
    	mutex_unlock(&dev->io_mutex);
    	exit:
    	return retval;
    }
    


  4. Реализуем функцию записи, помня, что устройство ожидает от нас заголовок из координат (в пикселях) записи данных X (unsigned short), Y (unsigned short), и длины данных (unsgned long). При этом все они записаны в Little Endian.

    Колбэк Write
    static ssize_t LCDwrite(struct file *filp, const char __user *user_buf, size_t count, loff_t *ppos)
    {
    	struct usblcd 				*dev;
    	struct usb_interface 		*interface;
    	int retval = -ENODEV;
    
    	int usbSent;
    	int writtenCount=count;
    
    	int x,y;
    
    	if(*ppos>=VB_SIZE*2)
    	{
    		retval = -ENOSPC;
    		goto exit;
    	}
    	if(*ppos+count>VB_SIZE*2)
    		writtenCount=VB_SIZE*2-*ppos;
    
    	dev = filp->private_data;
    	if (!dev) 
    	{
    		printk(KERN_ERR "usblcd driver error, no device found\n");
    		retval = -ENODEV;
    		goto exit;
    	}
    	mutex_lock(&dev->io_mutex);
    	interface = dev->interface;
    	if (!interface) 
    	{
    		printk(KERN_ERR "usblcd driver error, no device found\n");
    		retval = -ENODEV;
    		goto exit;
    	}
    	if (copy_from_user(dev->videobuffer+8, user_buf, writtenCount))
    	{
    	        retval = -EFAULT;
    	        goto unlock_exit;
    	}
    	
    	y=((int)((*ppos)>>1)/(int)WIDTH);
    	x=((int)((*ppos)>>1))-y*WIDTH;
    
    	*(unsigned short*)(dev->videobuffer)=cpu_to_le16(x);
    	*(unsigned short*)(dev->videobuffer+2)=cpu_to_le16(y);
    	*(unsigned long*)(dev->videobuffer+4)=cpu_to_le32(writtenCount>>1);
    
    	retval = usb_bulk_msg(dev->udev,
                  usb_sndbulkpipe(dev->udev, 1),dev->videobuffer,
                  8+writtenCount,
                  &usbSent, HZ*5);
    
    	if (!retval) {
    	    retval = writtenCount;
    	    *ppos+=writtenCount;
    	}
    	else
    	{
    		retval=-EIO;
    		goto unlock_exit;
    	}
    
    	unlock_exit:
    	mutex_unlock(&dev->io_mutex);
    	exit:
    	return retval;
    }
    


    Здесь мы для начала вычисляем количество байт, которое можем передать устройству — исходим из представления нашего девайса файлом фиксированного размера в 320*240*2 байт. Если вылезаем за этот предел — кричим что нет места. В этом нам поможет указатель на текущую позицию *ppos, который мы должны будем в конце сдвинуть на количество записанных байт. Дальше не забываем удостовериться, что девайс еще на месте, локнув мютекс и проверив, что там лежит в dev->interface — не NULL ли. Потом забираем из юзерспейса переданные нам данные, оставляя в видеобуфере место под заголовок, заполняем этот самый заголовок (помня про Endian в котором девайс будет пытаться его прочесть) и отсылаем при помощи синхронной функции usb_bulk_msg.
    На самом деле, это не лучший вариант, и мы рассмотрим более красивую и эффективную реализацию в следующих статьях, но сейчас остановимся на нем, как на самом простом.
    Все, осталось только проверить, что все прошло нормально, сместить указатель на позицию внутри файла на количество записанных байт и занести его же в возвращаемое значение, т.к. по нему система поймет успешно или нет прошла запись. После разлочиваем мютекс и выходим.


Нам осталось совсем чуть-чуть: компилируем драйвер (это мы уже умеем, натренировавшись на хеллоуворлдных), заливаем на роутер, выполняем insmod usblcd.ko.
Удостоверяемся в том, что система не падает, устройство обнаруживается при подключении и создается /dev/lcd0.

Давайте проверим, как все прошло!

Выводим рандом на экран:

cat /dev/urandom > /dev/lcd0



Теперь посмотрим, как выглядит главный бинарник системы — busybox:

cat /bin/busybox > /dev/lcd0



Не правда ли, видно разницу между исполняемым файлом и чистым рандомом?
Ну и наконец, выведем какую-нибудь картинку:

cat hobgoblin.raw > /dev/lcd0



Заключение


Итак, мы с вами наконец-то перешли от юзерспейса к кернелу, написали настоящий драйвер устройства под linux и вбилдили его в наш дистрибутив OpenWRT.
В следующей статье мы займемся более серьезным делом — попробуем объяснить ядру, что у нас не просто какое-то кастомное устройство, а самый настоящий дисплей, и что его можно использовать для вывода консоли или еще чего поинтереснее. Делать мы это будем при помощи написания драйвера фреймбуфера.
До встречи в следующей статье!

Hail to the speaker,
Hail to the knower,
Joy to him who has understood,
Delight to those who have listened.
Tags:
Hubs:
+118
Comments 32
Comments Comments 32

Articles