Драйвера для устройств под linux

Пишем свой драйвер под Linux

Хочу признаться сразу, что я вас отчасти обманул, ибо драйвер, если верить википедии — это компьютерная программа, с помощью которой другая программа (обычно операционная система) получает доступ к аппаратному обеспечению некоторого устройства. А сегодня мы создадим некую заготовку для драйвера, т.к. на самом деле ни с каким железом мы работать не будем. Эту полезную функциональность вы сможете добавить сами, если пожелаете.

То, что мы сегодня создадим, корректнее будет назвать LKM (Linux Kernel Module или загрузочный модуль ядра). Стоит сказать, что драйвер – это одна из разновидностей LKM.

Писать модуль мы будем под ядра линейки 2.6. LKM для 2.6 отличается от 2.4. Я не буду останавливаться на различиях, ибо это не входит в рамки поста.

Мы создадим символьное устройство /dev/test, которое будет обрабатываться нашим модулем. Хочу сразу оговориться, что размещать символьное устройство не обязательно в каталоге /dev, просто это является частью «древнего магического ритуала».

Немного теории

Если кратко, то LKM – это объект, который содержит код для расширения возможностей уже запущенного ядра Linux. Т.е. работает он в пространстве ядра, а не пользователя. Так что не стоит экспериментировать на рабочем сервере. В случае ошибки, закравшейся в модуль, получите kernel panic. Будем считать, что я вас предупредил.

Модуль ядра должен иметь как минимум 2 функции: функцию инициализации и функцию выхода. Первая вызывается во время загрузки модуля в пространство ядра, а вторая, соответственно, при выгрузке его. Эти функции задаются с помощью макроопределений: module_init и module_exit.

Стоит сказать несколько слов о функции printk(). Основное назначение этой функции — реализация механизма регистрации событий и предупреждений. Иными словами эта функция для записи в лог ядра некой информации.

Т.к. драйвер работает в пространстве ядра, то он отграничен от адресного пространства пользователя. А нам хотелось бы иметь возможность вернуть некий результат. Для этого используется функция put_user(). Она как раз и занимается тем, что перекидывает данные из пространства ядра в пользовательское.

Хочу ещё сказать пару слов о символьных устройствах.

Выполните команду ls -l /dev/sda* . Вы увидите что-то вроде:
brw-rw—- 1 root disk 8, 0 2010-10-11 10:23 /dev/sda
brw-rw—- 1 root disk 8, 1 2010-10-11 10:23 /dev/sda1
brw-rw—- 1 root disk 8, 2 2010-10-11 10:23 /dev/sda2
brw-rw—- 1 root disk 8, 5 2010-10-11 10:23 /dev/sda5

Между словом «disk» и датой есть два числа разделённых запятой. Первое число называют старшим номером устройства. Старший номер указывает на то, какой драйвер используется для обслуживания данного устройства. Каждый драйвер имеет свой уникальный старший номер.

Файлы устройства создаются с помощью команты mknod, например: mknod /dev/test c 12 . Этой командой мы создадим устройство /dev/test и укажем для него старший номер (12).

Я не буду сильно углубляться в теорию, т.к. кому интересно – тот сможет сам почитать про это подробнее. Я дам ссылку в конце.

Прежде чем начать

Нужно знать несколько «волшебных» команд:

  • insmod – добавить модуль в ядро
  • rmmod – соответственно, удалить
  • lsmod – вывести список текущих модулей
  • modinfo – вывести информацию о модуле

Для компиляции модуля нам потребуются заголовки текущего ядра.

В debian/ubutnu их можно легко поставить так (к примеру для 2.6.26-2-686):
apt-get install linux-headers-2.6.26-2-686
Либо собрать пакет для вашего текущего ядра самим: fakeroot make-kpkg kernel_headers

Исходник

#include

  • /* Для printk() и т.д. */
    #include
  • /* Эта частичка древней магии, которая оживляет модули */
    #include
  • /* Определения макросов */
    #include
  • #include /* put_user */

  • // Ниже мы задаём информацию о модуле, которую можно будет увидеть с помощью Modinfo
    MODULE_LICENSE( «GPL» );
    MODULE_AUTHOR( «Alex Petrov

    » );
    MODULE_DESCRIPTION( «My nice module» );
    MODULE_SUPPORTED_DEVICE( «test» ); /* /dev/testdevice */

    #define SUCCESS 0
    #define DEVICE_NAME «test» /* Имя нашего устройства */

    // Поддерживаемые нашим устройством операции
    static int device_open( struct inode *, struct file * );
    static int device_release( struct inode *, struct file * );
    static ssize_t device_read( struct file *, char *, size_t, loff_t * );
    static ssize_t device_write( struct file *, const char *, size_t, loff_t * );

    // Глобальные переменные, объявлены как static, воизбежание конфликтов имен.
    static int major_number; /* Старший номер устройства нашего драйвера */
    static int is_device_open = 0; /* Используется ли девайс ? */
    static char text[ 5 ] = «test\n» ; /* Текст, который мы будет отдавать при обращении к нашему устройству */
    static char * text_ptr = text; /* Указатель на текущую позицию в тексте */

    // Прописываем обработчики операций на устройством
    static struct file_operations fops =
    <
    .read = device_read,
    .write = device_write,
    .open = device_open,
    .release = device_release
    >;

    // Функция загрузки модуля. Входная точка. Можем считать что это наш main()
    static int __init test_init( void )
    <
    printk( KERN_ALERT «TEST driver loaded!\n» );

    // Регистрируем устройсво и получаем старший номер устройства
    major_number = register_chrdev( 0, DEVICE_NAME, &fops );

    if ( major_number «Registering the character device failed with %d\n» , major_number );
    return major_number;
    >

    // Сообщаем присвоенный нам старший номер устройства
    printk( «Test module is loaded!\n» );

    printk( «Please, create a dev file with ‘mknod /dev/test c %d 0’.\n» , major_number );

    // Функция выгрузки модуля
    static void __exit test_exit( void )
    <
    // Освобождаем устройство
    unregister_chrdev( major_number, DEVICE_NAME );

    printk( KERN_ALERT «Test module is unloaded!\n» );
    >

    // Указываем наши функции загрузки и выгрузки
    module_init( test_init );
    module_exit( test_exit );

    static int device_open( struct inode *inode, struct file *file )
    <
    text_ptr = text;

    if ( is_device_open )
    return -EBUSY;

    static int device_release( struct inode *inode, struct file *file )
    <
    is_device_open—;
    return SUCCESS;
    >

    device_write( struct file *filp, const char *buff, size_t len, loff_t * off )
    <
    printk( «Sorry, this operation isn’t supported.\n» );
    return -EINVAL;
    >

    static ssize_t device_read( struct file *filp, /* include/linux/fs.h */
    char *buffer, /* buffer */
    size_t length, /* buffer length */
    loff_t * offset )
    <
    int byte_read = 0;

    if ( *text_ptr == 0 )
    return 0;

    return byte_read;
    >

    * This source code was highlighted with Source Code Highlighter .

    Сборка модуля

    Ну а теперь можем написать небольшой Makefile:

    obj-m += test.o
    all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
    clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

    И проверить его работоспособность:

    root@joker:/tmp/test# make
    make -C /lib/modules/2.6.26-2-openvz-amd64/build M=/tmp/test modules
    make[1]: Entering directory `/usr/src/linux-headers-2.6.26-2-openvz-amd64′
    CC [M] /tmp/1/test.o
    Building modules, stage 2.
    MODPOST 1 modules
    CC /tmp/test/test.mod.o
    LD [M] /tmp/test/test.ko
    make[1]: Leaving directory `/usr/src/linux-headers-2.6.26-2-openvz-amd64′

    Посмотрим что у нас получилось:

    root@joker:/tmp/test# ls -la
    drwxr-xr-x 3 root root 4096 Окт 21 12:32 .
    drwxrwxrwt 12 root root 4096 Окт 21 12:33 ..
    -rw-r—r— 1 root root 219 Окт 21 12:30 demo.sh
    -rw-r—r— 1 root root 161 Окт 21 12:30 Makefile
    -rw-r—r— 1 root root 22 Окт 21 12:32 modules.order
    -rw-r—r— 1 root root 0 Окт 21 12:32 Module.symvers
    -rw-r—r— 1 root root 2940 Окт 21 12:30 test.c
    -rw-r—r— 1 root root 10364 Окт 21 12:32 test.ko
    -rw-r—r— 1 root root 104 Окт 21 12:32 .test.ko.cmd
    -rw-r—r— 1 root root 717 Окт 21 12:32 test.mod.c
    -rw-r—r— 1 root root 6832 Окт 21 12:32 test.mod.o
    -rw-r—r— 1 root root 12867 Окт 21 12:32 .test.mod.o.cmd
    -rw-r—r— 1 root root 4424 Окт 21 12:32 test.o
    -rw-r—r— 1 root root 14361 Окт 21 12:32 .test.o.cmd
    drwxr-xr-x 2 root root 4096 Окт 21 12:32 .tmp_versions

    Теперь посмотрим информацию о только что скомпилированном модуле:

    root@joker:/tmp/test# modinfo test.ko
    filename: test.ko
    description: My nice module
    author: Alex Petrov
    license: GPL
    depends:
    vermagic: 2.6.26-2-openvz-amd64 SMP mod_unload modversions

    Ну и наконец установим модуль в ядро:

    root@joker:/tmp/test# insmod test.ko

    Посмотрим есть ли наш модуль с списке:

    root@joker:/tmp/test# lsmod | grep test

    И что попало в логи:

    root@joker:/tmp/test# dmesg | tail

    [829528.598922] Test module is loaded!
    [829528.598926] Please, create a dev file with ‘mknod /dev/test c 249 0’.

    Наш модуль подсказываем нам что нужно сделать.

    Последуем его совету:

    root@joker:/tmp/test# mknod /dev/test c 249 0

    Ну и наконец проверим работает ли наш модуль:

    root@joker:/tmp/test# cat /dev/test

    Наш модуль не поддерживает приём данных со стороны пользователя:

    root@joker:/tmp/test# echo 1 > /dev/test

    bash: echo: ошибка записи: Недопустимый аргумент

    Посмотрим что что скажет модуль на наши действия:

    root@joker:/tmp/test# dmesg | tail

    [829528.598922] Test module is loaded!
    [829528.598926] Please, create a dev file with ‘mknod /dev/test c 249 0’.
    [829747.462715] Sorry, this operation isn’t supported.

    root@joker:/tmp/test# rmmod test

    И посмотрим что он нам скажет на прощание:

    root@joker:/tmp/test# dmesg | tail

    [829528.598922] Test module is loaded!
    [829528.598926] Please, create a dev file with ‘mknod /dev/test c 249 0’.
    [829747.462715] Sorry, this operation isn’t supported.
    [829893.681197] Test module is unloaded!

    Удалим файл устройства, что бы он нас не смущал:

    root@joker:/tmp/test# rm /dev/test

    Заключение

    Дальнейшее развитие этой «заготовки» зависит только от вас. Можно превратить её в настоящий драйвер, который будет предоставлять интерфейс к вашему девайсу, либо использовать для дальнейшего изучения ядра Linux.

    Только что в голову пришла совершенно безумная идея сделать sudo через файл устройства. Т.е. посылаем в /dev/test команду и она выполняется от имени root.

    Литература

    И под конец дам ссылку на книгу заклинаний LKMPG (Linux Kernel Module Programming Guide)

    UPD:
    У некоторых может не собраться модуль через Makefile, описанный выше.
    Решение:
    Создаём Makefile только с одной строкой: obj-m += test.o
    И запускаем сборку так:
    make -C /usr/src/linux-headers-`uname -r` SUBDIRS=$PWD modules

    UPD2:
    Поправил ошибки в исходнике.
    Парсер глючит и сохраняет ‘MODULE_DEscriptION( «My nice module» );’. Естественно в module_description все буквы заглавные.

    UPD3:
    segoon прислал несколько поправок к посту:

    1) В функции device_open() находится race condition:

    static int device_open( struct inode *inode, struct file *file )
    <
    text_ptr = text;

    Источник

    Разработка драйвера PCI устройства под Linux


    В данной статье я рассматриваю процесс написания простого драйвера PCI устройства под OC Linux. Будет кратко изучено устройство программной модели PCI, написание собственно драйвера, тестовой пользовательской программы и запуск всей этой системы.

    В качестве подопытного выступит интерфейс датчиков перемещения ЛИР940/941. Это устройство, отечественного производства, обеспечивает подключение до 4 энкодеров с помощью последовательного протокола SSI поверх физического интерфейса RS-422.

    На сегодняшний день шина PCI (и её более новый вариант — PCI Express) является стандартным интерфейсом для подключения широкого спектра дополнительного оборудования к современным компьютерам и в особом представлении эта шина не нуждается.

    Не редко именно в виде PCI адаптера реализуются различные специализированные интерфейсы ввода-вывода для подключения не менее специализированного внешнего оборудования.
    Так же до сих пор не редки ситуации когда производитель оборудования предоставляет драйвер лишь под OC Windows.

    Плата ЛИР941 была приобретена организацией, в которой я работаю, для получения данных с высокоточных абсолютных датчиков перемещения. Когда встал вопрос о работе под Linux оказалось, что производитель не предоставляет ничего под эту ОС. В сети так же ничего не нашлось, что впрочем нормально для такого редкого и специализированного устройства.

    На самой плате находится FPGA фирмы Altera, в которой реализуется вся логика взаимодействия, а также несколько (от 2 до 4) интерфейсов RS-422 с гальванической развязкой.

    Обычно в такой ситуации разработчики идут по пути обратного инженеринга, пытаясь разобраться как работает Windows драйвер.

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

    Я спросил не могут ли они предоставить какую-нибудь документацию или спецификацию на их устройство, дабы я мог разработать открытый драйвер под Linux. К моему удивлению производитель пошел на встречу, мне ответили очень быстро и прислали всю необходимую документацию!

    Шина PCI

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

    Шина PCI позволяет подключать одновременно большое количество устройств и нередко состоит из нескольких физических шин, соединяющихся между собой посредством специальных «мостов» — PCI Bridge. Каждая шина имеет свой номер, устройствам на шине так же присваивается свой уникальный номер. Так же каждое устройство может быть многофункциональным, как бы разделенным на отдельные устройства, реализующие какие-то отдельные функции, каждой такой функции аналогично присваивается свой номер.
    Таким образом системный «путь» к конкретному функционалу устройства выглядит так:
    .

    Что бы посмотреть какие устройства подключены к шине PCI в Linux достаточно выполнить команду lspci.

    Вывод может оказаться неожиданно длинным, т. к. кроме устройств непосредственно физически подключенных через pci/pci express слоты (например видеоадаптера) есть множество системных устройств, распаянных (или же входящих в устройство микросхем чипсета) на материнской плате.
    Первая колонка этого вывода, состоящая из чисел, как раз и представляет собой набор рассмотренных выше идентификаторов.

    Этот вывод означает, что видеоадаптер NVIDIA GT 240 находится на PCI шине 01, номер устройства — 00 и номер его единственной функции так же 0.

    Следует еще добавить, что каждое PCI устройство имеет набор из двух уникальных идентификаторов — Vendor ID и Product ID, это позволяет драйверам однозначно идентифицировать устройства и правильно работать с ними.

    Выдачей уникальных Vendor ID для производителей аппаратного обеспечения занимается специальный консорциум – PCI-SIG.

    Что бы увидеть эти идентификаторы достаточно запустить lspci с ключами -nn:

    Где 10de — идентификатор производителя, NVIDIA Corporation, а 0ca3 — идентификатор конкретного оборудования.

    Узнать кто есть кто можно с помощью специальных сайтов, например The PCI ID Repository

    Чтение служебной информации и конфигурация PCI устройства осуществляется посредством набора конфигурационных регистров. Каждое устройство обязано предоставлять стандартный набор таких регистров, которые будут рассмотрены далее.

    Регистры отображаются в оперативную память компьютера во время загрузки и ядро операционной системы связывает с устройством особую структуру данных — pci_dev, а так же предоставляет набор функций для чтения и записи.

    Помимо конфигурационных регистров PCI устройства могут иметь до 6 каналов ввода-вывода данных. Каждый канал так же отображается в оперативную память по некоему адресу, назначаемому ядром ОС.

    Операции чтения-записи этой области памяти, с определенными параметрами размера блока и смещения, приводят непосредственно к записи-чтению в устройство.

    Получается, что для написания драйвера PCI необходимо знать какие конфигурационные регистры использует устройство, а так же по каким смещениям (и что именно) нужно записывать/читать. В моем случае производитель предоставил всю необходимую информацию.

    Конфигурационное пространство PCI

    Первые 64 байта являются стандартизированными и должны предоставляться всеми устройствами, независимо от того требуются они или нет.

    На картинке отмечены регистры являющиеся обязательными, они всегда должны содержать какие-либо осмысленные значения, остальные же могут содержать нули, если это не требуется в данном случае.

    Порядок байт во всех регистрах PCI — little-endian, это следует учитывать, если разработка драйвера ведется для архитектуры с иным порядком.

    Давайте посмотрим что из себя представляют некоторые регистры.

    VendorID и ProductID — уже известные нам регистры, в которых хранятся идентификаторы производителя и оборудования. Каждый из регистров занимает 2 байта.

    Command — этот регистр определяет некоторые возможности PCI устройства, например разрешает или запрещает доступ к памяти.


    Инициализацией этих битов занимается операционная система.

    Status — биты этого регистра хранят информацию о различных событиях PCI шины.


    Эти значения выставляются оборудованием, в моём случае были сконфигурированы только биты 9, 10, определяющие время реакции платы.

    Revision ID — число, ревизия конкретной платы. Полезно в тех случаях, когда есть несколько ревизий устройства и различия необходимо учитывать в коде драйвера.

    Class Code — «волшебное» число, отображающее класс устройства, например: Network Controller, Display Controller и т. п. Список существующих кодов можно посмотреть тут.

    Base Address Registers — эти регистры, в количестве 6 штук, служат для определения того как и сколько памяти выделется устройству для процедур ввода/вывода. Этот регистр используется pci подсистемой ядра и обычно не интересен разработчикам драйверов.

    Теперь можно перейти к программирование и попробовать прочесть эти регистры и получить доступ к памяти ввода/вывода.

    Разработка модуля ядра

    Как наверное многие знают — точками входа и выхода в модуль ядра Linux являются специальные __init и __exit функции.

    Определим эти функции и выполним процедуру регистрации нашего драйвера с помощью вызова специальной функции — pci_register_driver(struct pci_driver *drv), а так же процедуру выгрузки с помощью pci_unregister_driver(struct pci_driver *drv).

    Аргументом функций register и unregister является структура pci_driver, которую необходимо предварительно инициализировать, сделаем это в самом начале, объявив структуру статической.

    Поля структуры, которые мы инициализируем:
    name — уникальное имя драйвера, которое будет использовано ядром в /sys/bus/pci/drivers
    id_table — таблица пар Vendor ID и Product ID, с которым может работать драйвер.
    probe — функция вызываемая ядром после загрузки драйвера, служит для инициализации оборудования
    remove — функция вызываемая ядром при выгрузке драйвера, служит для освобождения каких-либо ранее занятых ресурсов

    Так же в структуре pci_driver предусмотрены дополнительные функции, которые мы не будем использовать в данном примере:
    suspend — эта функция вызывается при засыпании устройства
    resume — эта функция вызывается при пробуждении устройства

    Рассмотрим как определяется таблица пар Vendor ID и Product ID.
    Это простая структура со списком идентификаторов.

    Где 0x0F0F — Vendor ID, а 0x0F0E и 0x0F0D — пара Product ID этого вендора.
    Пар идентификаторов может быть как одна, так и несколько.
    Обязательно завершать список с помощью пустого идентификатора

    После объявления заполненной структуры необходимо передать её макросу

    В функции my_driver_probe() мы можем делать, собственно, все что нам хочется.

    Например можно попробовать прочитать конфигурационные регистры, описанные выше, с целью проверки корректности идентификаторов или выяснения ревизии платы.

    В случае каких-либо проблем или несоответствий можно вернуть отрицательное значение кода ошибки и ядро прервет загрузку модуля. О чтении регистров конфигурации будет рассказано ниже.

    Также обычно в этом месте выполняют инициализацию памяти ввода/вывода устройства для последующей работы с ней.

    Полезным будет в этом месте определить некоторую «приватную» структуру драйвера в которой будут храниться данные, полезные во всех функциях драйвера. Например это может быть указатель на ту же память ввода/вывода устройства.

    После инициализации приватной структуры необходимо выполнить её регистрацию

    В функции my_driver_remove() удобно выполнять освобождение занятых ресурсов, например можно освободить память ввода/вывода.

    Так же тут необходимо освобождать саму структуру struct pci_dev

    Работа с регистрами конфигурации

    Чтение 8, 16 и 32 бит регистров соответственно:

    Запись 8, 16 и 32 бит регистров соответственно:

    Первый аргумент всех этих функций — структура pci_dev, которая непосредственно связана с конкретным устройством PCI. Инициализация этой структуры будет рассмотрена далее.

    Например мы хотим прочитать значения регистров Vendor ID, Product ID и Revision ID:

    Как видно — все предельно просто, подставляя необходимое значение аргумента whrere мы можем получить доступ к любому конфигурационному регистру конкретного pci_dev.

    С чтением/записью памяти устройства все несколько сложнее.

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

    Дальше мы можем свободно работать с памятью, на которую указывает hwmem.
    Правильнее всего использовать для этой цели специальные функции ядра.

    Запись 8, 16 и 32 бит в память устройства:

    Чтение 8, 16 и 32 бит из памяти устройства:

    Убедитесь, что у вас установлены заголовочные файлы ядра. Для Debian/Ubuntu установка необходимого пакета выполняется так:

    Компиляция модуля выполняется простой командой make, попробовать загрузить модуль можно командой

    Скорее всего просто тихо ничего не произойдет, разве что у вас действительно окажется устройство с Vendor и Product ID из нашего примера.

    Теперь я хотел бы вернуться к конкретному устройству, для которого разрабатывался драйвер.
    Вот какую информацию про IO мне прислали разработчики платы ЛИР-941:

    RgStatus:
    b7 — Флаг паузы между транзакциями SSI (1 — пауза) (см. протокол SSI)
    b6 — Флаг текущей трансакции (1- происходит передача данных) (см. протокол SSI)
    b5 — Ext4 (Произошла защелка данных по сигналу Ext4)
    b4 — Ext3 (Произошла защелка данных по сигналу Ext3)
    b3 — Ext2 (Произошла защелка данных по сигналу Ext2)
    b2 — Ext1 (Произошла защелка данных по сигналу Ext1)
    b1 — Режим непрерывного опроса (По окончании передачи кода, аппаратно вырабатывается новый запрос)
    b0 — По запросу от компьютера (Однократный запрос текущего положения)

    Это значит, что если я хочу, например, прочитать данные от энкодера, подключенного к каналу 3 мне необходимо проверить седьмой бит блока RgStatus3, дождаться там еденички (пауза между транзакциями — значит уже ранее получили информацию от датчика и записали её в память платы, идет подготовка к следующему запросу) и прочитать число, хранящееся в третьем куске памяти длиной 32 бита.

    Всё сводится к вычислению необходимо сдвига от начала куска памяти и чтения необходимого количества байт.

    Из таблицы ясно, что данные каналов хранятся в виде 32 битных значений, а данные RgStatus — в виде значений длиной 8 бит.

    Значит для чтения RgStatus3 необходимо сдвинуться 4 раза 32 бита и два раза по 8 бит и затем прочесть 8 бит из этой позиции.

    А для чтения данных третьего канала необходимо сдвинуться 2 раза по 32 бита и прочесть значение длиной 32 бита.

    Для выполнения всех этих операций можно написать удобные макросы:

    Где chnum — номер требуемого канала, начиная с нуля.

    Также не лишним в данном деле будет и такой простой макрос, определяющий «включен» ли бит на определенной позиции.

    Получается такой код для чтения третьего канала данных:

    Все, мы получили от платы данные датчика, подключенного к третьему каналу и записали их в переменную enc_data.

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

    DATA WIDTH — Определяет максимальное количество бит в одной трансакции SSI. (разрядность приемного регистра). Допустимые значения – от 1 до 32

    CLOCK RATE – Порт, определяющий коэффициент деления системного Clk (33 МГц) для формирования Сlock SSI.
    Kдел = (CLOCK RATE)*2+2
    PAUSE RATE Порт, определяющий величину паузы после транзакции, в периодах Clk (30 нс)
    CONTROL 1:
    b7 — Режим SSI (0 – обычный режим, 1 – режим 16 разрядного абс. Датчика, с ожиданием стартового бита (устаревший вариант выдачи данных, нужен только для совместимости)).
    b6 — Зарезервировано
    b5 — Разрешение внешнего сигнала Ext4
    b4 — Разрешение внешнего сигнала Ext3
    b3 — Разрешение внешнего сигнала Ext2
    b2 — Разрешение внешнего сигнала Ext1
    b1 — Разрешение непрерывного опроса датчика
    b0 — Выработать однократный опрос

    Тут все аналогично — считаем сдвиг для необходимой области и записываем значение соответствующей функцией iowriteX

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

    Существует несколько путей общения вышестоящего ПО с нашим драйвером. Одним из самых старых, простых и популярных способов является символьное устройство, character device.
    Character device — это виртуальное устройство, которое может быть добавлено в каталог /dev, его можно будет открывать, что-то записывать, читать, выполнять вызовы ioctl для задания каких-либо параметров.

    Хороший пример подобного устройства — драйвер последовательного порта с его /dev/ttySX

    Регистрацию character device удобно вынести в отдельную функцию

    Указатель на нашу приватную структуру необходим для последующей инициализации файлового объекта, так что при каждом пользовательском вызове open/read/write/ioctl/close мы будем иметь доступ к нашей приватной структуре и сможем выполнять операции чтения/записи в PCI устройство.

    Вызывать create_char_devs() удобно в функции my_driver_probe(), после всех инициализаций и проверок.

    В моём случае эта функция называется именно create_char_devs(), во множественном числе. Дело в том, что драйвер создает несколько одноименных (но с разными цифровыми индексами в конце имени) character device, по одному на канал платы ЛИР941, это позволяет удобно, независимо и одноврменно работать сразу с несколькими подключенными датчиками.

    Создать символьное устройство довольно просто.

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

    Внутри ядра все /dev устройства идентифицируются с помощью пары идентификаторов

    Некоторые идентификаторы major являются зарезервированными и всегда назначаются определенным устройствам, остальные идентификаторы являются динамическими.
    Значение major разделяют все устройства конкретного драйвера, отличаются они лишь идентификаторами minor.

    При инициализации своего устройства можно задать значение major руками, ну лучше этого не делать, т. к. можно устроить конфликт. Самый лучший вариант — использовать макрос MAJOR().

    Его применение будет показано в коде ниже.

    В случае с minor значение обычно совпадает с порядковым номером устройства, при создании, начиная с нуля. Это позволяет узнать к какому именно устройству /dev/device-X обращаются из пространстрва ядра — достаточно посмотреть на minor доступный в обработчике файловых операций.

    Идентификтаоры : отображаются утилитой ls с ключем -l
    например если выполнить:

    Число 89 — это major идентификатор драйвера контроллера i2c шины, оно общее для всех каналов i2c, а 0,1,2,3,4 — minor идентификаторы.

    Пример создания набора устройств.

    Функция mydev_open() будет вызываться, если кто-то попробует открыть наше устройство в пространстве пользователя.

    Очень удобно в этой функции инициализировать приватную структуру для открытого файла устройства. В ней можно сохранить значение minor для текущего открытого устройства
    Также туда можно поместить указатель на какие-то более глобальные структуры, помогающие взаимодействовать с остальным драйвером, например, мы можем в этом месте сохранить указатель на my_driver_priv, с которым мы работали ранее. Указатель на эту структуру можно использовать в операциях ioctl/read/write для выполнения запросов к аппаратуре.

    Мы можем определить такую структуру:

    Операции чтения и записи являются довольно простыми, единственный «нюанс» — небезопаность (а то и невозможность) прямого доступа пользовательского приложения к памяти ядра и наоборот.
    В связи с этим для получения данных, записиваемых с помощью функции write() необходимо использовать функцию ядра copy_from_user().

    А при выполнении read() необходимо пользоваться copy_to_user().

    Обе функции оснащены различными проверками и обеспечивают безопасное копирование данных между ядром и пользовательским пространством

    Обработчик вызова ioctl() принимает в качестве аргументов собственно номер ioctl операции и какие-то переданные данные в качестве аргументов (если они необходимы).
    Номера операций ioctl() определяются разработчиком драйвера. Это просто некие «волшебные» числа, скрывающиеся за читабельными define.

    Об этих номерах должна знать пользовательская программа, поэтому удобно выносить их куда-то в виде отдельного заголовочного файла.

    Пример обработчика ioctl

    Функция mydev_release() вызывается при закрыти файла-устройства.

    В нашем случае достаточно лишь освободить память нашей приватной файловой структуры

    В функции уничтожения символьного устройства необходимо удалить все созданные устройства, уничтожить sysfs class и освободить память.

    Эту функцию следует вызывать в __exit методе модуля ядра, так что бы символьное устройство уничтожалось при выгрузке.

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

    Полный исходный код драйвера платы ЛИР941 можно посмотреть на Github.

    А тут лежит простая тестовая утилита, работающая с этим драйвером.

    Тестирование драйвера на настоящем железе 🙂

    Спасибо за внимание!
    Надеюсь этот материал будет полезен тем, кто решить написать свой драйвер для чего-нибудь.

    Источник

    Читайте также:  Залипает панель задач windows 10
    Оцените статью