Как написать драйвер для linux

Обновлено: 06.07.2024

Недавно мне довелось написать драйверы для специализированного
USB-сканера. Этот сканер работает в режиме непрерывной развертки с
частотой 10 кадров в секунду и обеспечивает скорость потока на чтение
3,2 МБайт/сек.

Драйверы сначала были разработаны под ядро Linux версии 2.6.15, а
затем, по требованию заказчика, адаптированы под ядро версии 2.4.26. К
моему удивлению, в обоих упомянутых версиях ядра написать драйвер не
составило особого труда. Для этого лишь необходимо четко представлять,
как работает USB и какими функциями обеспечивается взаимодействие
драйвера с USB-устройством. Все остальное - уже давно реализовано в
ядре linux.

При написании этой статьи я вижу перед собой следующие задачи:
1. рассказать об основных свойствах обмена устройств по USB
2. описать программный интерфейс взаимодействия драйвера с
USB-устройством
3. привести примеры использования функций ядра

Я надеюсь, что данная информация будет интересна программистам
системного уровня.


Введение в USB

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

Поэтому сразу перейдем к менее очевидным вещам - к тому, как USB
устроен внутри.


Кто главный?

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

Есть только одно исключение: после того, как хост перевел устройство в
suspend-режим, устройство может посылать запрос remote wakeup. Во всех
остальных случаях хост формирует запросы, а устройства отвечают на
них.


Направление

Хост всегда является мастером, а обмен данными должен осуществляться в
обоих направлениях:

* OUT - отсылая пакет с флагом OUT, хост отсылает данные устройству


* IN - отсылая пакет с флагом IN, хост отправляет запрос на прием
данных из устройства.


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

Пример: отсылка данных устройству

Чтобы отослать данные устройству, хост посылает пакет Token "OUT",
затем пакет Data. Если устройство готово обработать принятые данные,
оно отсылает пакет Handshake "ACK", подтверждающий транзакцию. Если
оно занято, оно отсылает отказ - Handshake "NACK". Если произошла
какая-то ошибка, то устройство может не отсылать Handshake.

Пример: отсылка данных хосту

Как уже говорилось, устройство самостоятельно никогда не отсылает
данные. Только по запросу. Чтобы принять данные, хост посылает пакет
Handshake "IN". Устройство по запросу может отослать пакет Data, а
затем Handshake "ACK". Либо может отослать Handshake "NACK", не
посылая Data.


Типы передачи данных

Спецификация USB определяет 4 типа потоков данных:
1. bulk transfer - предназначен для пакетной передачи данных с
размером пакетов 8, 16, 32, 64 для USB 1.1 и 512 для USB 2.0.
Используется алгоритм перепосылки (в случае возникновения ошибок),
а управление потоком осуществляется с использованием handshake
пакетов, поэтому данный тип является достоверным. Поддерживаются
оба направления - IN и OUT.
2. control transfer - предназначен для конфигурирования и управления
устройством. Также, как и в bulk, используются алгоритмы
подтверждения и перепосылки, поэтому этот тип обеспечивает
гарантированный обмен данными. Направления - IN (status) и
OUT(setup, control).
3. interrupt transfer - похож на bulk. Размер пакета - от 1 до 64
байт для USB 1.1 и до 1024 байт для USB 2.0. Этот тип гарантирует,
что устройство будет опрашиваться (то есть хост будет отсылать ему
token "IN") хостом с заданным интервалом. Направление - IN.
4. isochronous transfer - предназначен для передачи данных без
управления потоком (без подтверждений). Область применения -
аудио-потоки, видео-потоки. Размер пакета - до 1023 байт для USB
1.1 и до 1024 байт для USB 2.0. Предусмотрен контроль ошибок (на
приемной стороне) по CRC16. Направления - IN и OUT.


Endpoint No.0

EP0 имеет особое значение для USB. Это Control EP. Он должен быть в
каждом USB-устройстве. Этот EP использует token "setup", чтобы
сигнализировать, что данные, отправляемые после него, предназначены
для управления устройством.

Используя этот EP0, хост может передавать setup-пакет длиной 8 байт и
данные, которые следуют за этим пакетом. Во многих случаях может
хватать передачи только setup-пакета. Однако устройство может
использовать и передачу данных по EP0, например для смены прошивок
компонентов устройства, или получения расширенной информации об
устройстве.

Рассмотрим немного подробнее setup-пакет.


EP0: setup-пакет

Содержимое setup-пакета представлено в таблице

Байт (No.) Имя Назначение

0 bmRequestType Поле для указания типа запроса, направления,
получателя
1 bRequest идентификатор запроса
2 wValueL 16-битное значение wValue, зависит от запроса.
3 wValueH
4 wIndexL 16-битное значение wIndex, зависит от запроса.
5 wIndexH
6 wLengthL количество байт, отсылаемых после setup-пакета.
7 wLengthH

Как видно из таблицы, setup-пакет содержит 5 полей. bmRequestType и
bRequest определяют запрос, а wValue, wIndex и wLength - его свойства.

Спецификация USB резервирует диапазон значений bRequest под
стандартные запросы. Каждое устройство обязано отвечать на все
стандартные запросы. В следующей таблице приведены только несколько
стандартных запросов, с которыми мы будем сталкиваться дальше.

bRequest Имя Описание

0x05 Set Address установка уникального адреса устройства в системе
0x06 Get Descriptor получение информации об устройстве. Тип информации
зависит от поля wValue.


Распознавание устройства

Как происходит распознавание устройства, только что подключившегося к
системе? Уже упоминалось, что каждое устройство обязано обеспечить
доступ к EP0. Но кроме этого, оно еще должно отвечать на запросы,
указанные в спецификации USB для EP0. Пользуясь этими запросами и
происходит распознавание устройства в системе.

Алгоритм детектирования нового устройства следующий:
1. хост отсылает setup-пакет "Get Descriptor" (wValue = "device").
2. хост получает идентифицирующую информацию об устройстве
3. хост отсылает setup-пакет "Set address", после чего устройство
получает уникальный адрес в системе
4. хост отсылает остальные setup-пакеты "Get Descriptor" и получает
дополнительную информацию об устройстве: количество EP, требования
к питанию, и т.п.


Поддержка USB в ядре Linux

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

В ядре файлы программ располагаются в drivers/usb/, а заголовочные
файлы - в include/linux/. Информации, представленной в этих
директориях, достаточно, чтобы самостоятельно написать драйвер для
любого USB-устройства.

Драйвер, взаимодействующий с USB-устройством(-ами), как правило,
выполняет следующие действия:
1. регистрация/выгрузка драйвера
2. регистрация/удаление устройства
3. обмен данными: управляющий и информационный.

Структура usb_driver описана в include/linux/usb.h Рассмотрим наиболее
важные поля этой структуры.


Очевидно, что name - это имя драйвера. id_table - это массив структур
usb_device_id. Этот список предназначен для определения cоответствия
подключаемого устройства определенным параметрам. Только те
устройства, которые соответствуют перечисленным параметрам, могут быть
подключены к драйверу. Если массив пуст, система будет пытаться
подключить каждое устройство к драйверу.

Поле driver говорит о том, что usb_driver унаследован от device_driver.

* идентификатор производителя (Vendor ID)
* идентификатор устройства (Device ID).


Определение структуры usb_device_id можно видеть в
include/linux/mod_devicetable.h

probe и disconnect - это callback-функции, вызываемые системой при
подключении и отключении USB-устройства. probe будет вызыван для
каждого устройства, если список id_table пуст, или только для тех
устройств, которые соответствуют параметрам, перечисленным в списке.


В этом примере регистрируется драйвер USB-устройства, которое имеет
значения полей PRODUCT_ID = 0x1, VENDOR_ID = 0x1234. Только для
устройства с такими параметрами будет вызвана функция my_probe.

Вызов my_probe фактически означает регистрацию устройства в драйвере
my_driver, а вызов my_disconnect - удаление устройства. Поэтому
перейдем к следующему этапу - регистрация/удаление устройства.

Для отключения устройства от драйвера система вызывает функцию
disconnect, которой передается один параметр - интерфейс:


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

В качестве примера регистрации/удаления устройства лучше обратиться к
примеру из ядра linux, который находится в drivers/usb/usb-skeleton.c.

В ядре версии 2.6 вышеописанная процедура сильно упрощена благодаря
появлению udev и sysfs. В системе работает программа-daemon udevd,
которая отслеживает появление файлов в sysfs (/sys/class/). На
основании информации, читаемой из этих файлов, она автоматически,
пользуясь правилами udev для данного устройства, создает необходимые
файлы в dev.

В программном интерфейсе USB для этих целей есть функция
usb_register_dev, которая выполняет все необходимое, чтобы udev
выполнил вышеописанные процедуры:

extern int usb_register_dev(struct usb_interface *intf,
struct usb_class_driver *class_driver);
extern void usb_deregister_dev(struct usb_interface *intf,
struct usb_class_driver *class_driver);

usb_register_dev принимает на вход interface и class_driver. Структура
usb_class_driver выглядит следующим образом:

name - это имя устройства. Директория с этим именем появится в sysfs.
fops - файловые операции символьного устройства. minor_base - базовый
minor номер.


Функция usb_register_dev выполняет следующие действия:

* регистрирует символьное устройство с major номером 180 (см
include/linux/major.h) и резервирует диапазон из 16 minor номеров.
Поэтому minor_base должен иметь младший полубайт = 0.


* в зарезервированном диапазоне minor номеров выделяет один номер
для данного устройства. Этот номер записывает в interface->minor.


* создает все необходимые файлы в sysfs: после этого udev создает
файлы в /dev/


Вызов usb_deregister_dev выполняет обратные процедуры, поэтому должен
вызываться в функции disconnect.


Обмен данными с устройством

Рассмотрим обмен данными для наиболее часто используемых типов:
control и bulk. Соответствующие функции определены в
drivers/usb/core/message.c.

Для отсылки/приема данных в 0й EP используется функция
usb_control_msg:

extern int usb_control_msg(struct usb_device *dev, unsigned int pipe,
__u8 request, __u8 requesttype, __u16 value, __u16 index,
void *data, __u16 size, int timeout);


dev - указатель на usb_device. Этот указатель может быть получен с
помощью вызова функции interface_to_usbdev(interface). pipe - EP pipe.
Этот параметр хранит в себе: тип передачи данных (bulk, control, . ),
направление, номер EP. Для задания pipe в include/linux/usb.h
определены макросы. Приведем только некоторые:

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

Пример. Отсылка setup-пакета с заданными bRequest и wValue может
выглядеть следующим образом:

// .
int ret;
struct usb_device* udev = interface_to_usbdev(interface);

ret = usb_control_msg(udev, usb_sndctrlpipe(udev, 0),
bRequest, USB_TYPE_VENDOR,
wValue, 0, udev, 0, HZ);
// .


В этом примере отсылается только setup-пакет. Поэтому data = udev,
size = 0.

Для использования bulk transfer используется функция usb_bulk_msg:

extern int usb_bulk_msg(struct usb_device *dev, unsigned int pipe,
void *data, int size, int *actual_size,
int timeout);


Параметры dev, pipe, data, size, timeout имеют тот же смысл, что и в
usb_control_msg. actual_size - количество переданных байт в
действительности. actual_size <= size.

usb_bulk_msg имеет то же свойство, что и usb_control_msg - ее нельзя
вызывать в контексте прерываний (см выше).

Пример. Прием 100 байт из 5го EP может выглядеть следующим образом:

Часть 2: Пишем в классе наш первый драйвер для Linux

Оригинал: "Device Drivers, Part 2: Writing Your First Linux Driver in the Classroom"
Автор: Anil Kumar Pugalia
Дата публикации: December 1, 2010
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.

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

Пагс поспешно ответил, что они обсуждали именно ту тему, которую сегодня изучают в классе — драйверы устройств в Linux. Пагс был более, чем счастлив, когда профессор сказал: "Хорошо! Тогда что-нибудь скажите о динамической загрузке в Linux. Если вы справитесь, то я прощу вас обоих!". Пагс знал, что один из способов сделать профессора счастливым, это — покритиковать Windows.

Он объяснил: "Как известно, при обычной установке драйверов в Windows для того, чтобы их активировать, необходимо перезагрузить систему. А если это, предположим, действительно неприемлемо в случае, если это нужно делать на сервере? Вот где выигрывает Linux. В Linux можно загружать и выгружать драйверы на лету, и это активно используется сразу после загрузки системы. Кроме того, драйвер мгновенно отключается после его выгрузки. Это называется динамической загрузкой и выгрузкой драйверов в Linux ".

Это впечатлило профессора. "Хорошо! Идите на свои места, но больше не опаздывайте". Профессор продолжил лекцию: "Теперь, когда вы уже знаете, что такое динамическая загрузка и выгрузка драйверов, я, прежде, чем мы перейдем к написанию нашего первого драйверов, покажу вам, как загружать и выгружать драйвера".

Динамическая загрузка драйверов

Эти динамически загружаемые драйвера чаще всего называют модулями, которые собираются в виде отдельных модулей с расширением .ko (объект ядра). В каждой системе Linux в корне файловой системы (/) есть стандартное место для всех предварительно собранных модулей. Они организованы аналогично древовидной структуре исходных кодов ядра и находятся в директории /lib/modules/<kernel_version>/kernel , где <kernel_version> результат вывода системной команды uname -r (см.рис.1).


Рис.1: Предварительно собранные модули Linux

Чтобы динамически загружать и выгружать драйверы, воспользуйтесь следующими командами, которые находятся в директории /sbin и должны выполняться с привилегиями пользователя root:

  • lsmod — список модулей, загруженных в текущий момент
  • insmod <module_file> — добавление / загрузка указанного файла модуля
  • modprobe <module> — добавление / загрузка модуля вместе со всеми его зависимостями
  • rmmod <module> — удаление / выгрузка модуля

Давайте в качестве примера рассмотрим соответствующие драйвера файловой системы FAT. На рис.2 показан весь процесс нашего эксперимента. Файлы с модулями будут fat.ko , vfat.ko и т.д., находящиеся в директории fat (в vfat для старых версий ядра) в /lib/modules/`uname -r`/kernel/fs . Если они представлены в сжатом формате .gz , вам нужно будет распаковать их с помощью команды gunzip , прежде чем вы сможете выполнить операцию insmod .


Рис.2: Операции с модулями Linux

Модуль vfat зависит от модуля fat , так что первым должен быть загружен модуль fat.ko . Чтобы автоматически выполнить распаковку и загрузку зависимостей, воспользуйтесь командой modprobe . Обратите внимание, что когда вы пользуетесь командой modprobe , вы не должны в имени модуля указывать расширение .ko . Команда rmmod используется для выгрузки модулей.

Наш первый драйвер для Linux

Перед тем, как написать наш первый драйвер, давайте рассмотрим некоторые понятия. Драйвер никогда не работает сам по себе. Он похож на библиотеку, загружаемую из-за функций, которые будут вызваны из работающего приложения. Он написан на языке C, но в нем отсутствует функция main() . Кроме того, он будет загружаться / компоноваться с ядром, поэтому он должен компилироваться аналогично тому, как было откомпилировано ядро, и вы можете в качестве заголовочных файлов использовать только те, что есть в исходном коде ядра, а не из стандартного директория /usr/include .

Интересный факт, касающийся ядра, это то, что оно, как мы видим даже на примере нашего первого драйвера, представляет собой объектно-ориентированную реализацию на языке C. В любом драйвере есть конструктор и деструктор. Когда модуль успешно загружается в ядро, то вызывается конструктор модуля, а дескруктор модуля вызывается, когда команде rmmod удается успешно выгрузить модуль. Это в драйвере две обычные функции, разве что они называются init и exit, соответственно, и вызываются с помощью макросов module_init() и module_exit() , которые определены в заголовков ядра module.h .

С учетом вышесказанного это полный код нашего первого драйвера; назовем его ofd.c. Обратите внимание, что отсутствует заголовок stdio.h (заголовок пользовательского пространства), вместо него мы используем аналог kernel.h (заголовок пространства ядра). Функция printk() эквивалентна функции printf() . Кроме того, для обеспечения совместимости версии модуля с ядром, в которое будет загружен модуль, добавлен заголовок version.h . С помощью макроса MODULE_* заполняется информация, относящаяся к модулю, которая будет использована как "подпись" модуля.

Сборка нашего первого драйвера

Т.к. у нас есть код на языке C, настало время его скомпилировать и создать файл модуля ofd.ko . Для этого мы используем систему сборки ядра. В приведенном ниже файле Makefile происходит обращение к системе сборки ядра из исходных кодов, а файл Makefile ядра, в свою очередь, обращается к файлу Makefile нашего нового драйвера с тем, чтобы собрать драйвер.

Чтобы собрать драйвер для Linux, у вас в системе должен быть исходный код ядра (или, по крайней мере, заголовки ядра). Предполагается, что исходный код ядра будет находиться в директории /usr/src/linux . Если в вашей системе он находится в каком-нибудь другом месте, то укажите это место в переменной KERNEL_SOURCE в файле Makefile .

Когда есть код на языке C ( ofd.c ) и готов файл Makefile , то все, что нам нужно сделать для сборки нашего первого драйвера ( ofd.ko ), это вызвать команду make .

Подведем итог

Как только у нас будет файл ofd.ko , мы в роли пользователя root или с помощью команды sudo выполним обычные действия.

Команда lsmod должна вам сообщить о том, что драйвер ofd загружен.

Пока студенты экспериментировали со своим первым модулем, прозвенел звонок, сообщивший об окончании урока. Профессор Гопи подвел итог: "В настоящий момент мы не увидели ничего, кроме того, что модуль lsmod сообщил о загрузке драйвера. Куда выводит информацию команда printk ? Найдите это самостоятельно на лабораторных занятиях и познакомьте меня с своими выводами. Также учтите, что наш первый драйвер будет шаблоном для любого драйвера, который можно написать для Linux. Написание специализированных драйверов это всего лишь вопрос о том, чем будет заполнен конструктор и деструктор драйвера. Поэтому дальнейшее изучение будет представлять собой расширение данного драйвера с целью получить драйвер с конкретными функциональными возможностями".

Подскажите какой-нибудь простейший пример PCI драйвера (самый маленький в ядре), или может есть руководство или книжка на эту тему.

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

Для начала (самый простой вариант) - на устройстве допустим на всем диапазоне запрошенного и выделенного ему адресного пространства лежит повторяющийся одинаковый текст (из пары слов), просто нужно в программе через драйвер этот текст считать с максимальной скоростью (DMA) и вернуться к исполнению программы.



Ух ты! Люто спасибирую анонимуса!

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


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

Так что ответы еще принимаются, всё же надо что-то более полноценное.

А что, разве в LDD не описано, как писать драйвер для PCI-устройств? Для доступа из user space можно оформить все это хозяйство как char-устройство, к примеру.

А вариант сделать мост на usb3 (для большой скорости) через ftdi или cypress рассматривался? Ведь подключить эти не очень дорогие чипы, наверняка, довольно просто. С программной точки зрения, по-моему, это простые FIFO. Да ещё и гибко в плане подключения ПЛИС к ПК.


Суть не просто связать устройство с ПК (и не только ПК!) максимально простым способом (а таких куча, в том числе и через USB3 тех же FTDI и др.), а разработать именно полноценное PCI устройство.


а разработать именно полноценное PCI устройство

занимались мы такой работой в одной компании. причём много лет. в среднем, на разработку, отладку и выкатку в продакшен одной серьёзной PCIe платы уходило не меньше года. это если специалисты опытные, заранее знают всё о шине и уже имеют опыт разработки, разводки и т.д. и если все схемы с заказом печати и пайки отлажены.

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

с готовыми мостами тоже не намного меньше возни. тысячи страниц документации, иногда не очень понятные (или даже неправильные!) схемы обвязки (если производитель вообще этим заморачивается). так что там куда ни плюнь - везде какая-нить засада.

Iron_Bug ★★★★★ ( 26.06.16 10:37:39 )
Последнее исправление: Iron_Bug 26.06.16 10:41:16 (всего исправлений: 2)


Тем не менее, первая попавшая мне мамка - мой старый комп с двумя PCIe x16 разъемами - прекрасно видит мою платку, настраивает ее как надо (хотя для обмена данных еще предстоит драйвер, но именно сам процесс настройки делает корка). После прочтения книги, мне начинает казаться что PCI - прост как тапок, особенно по части настройки именно endpoint-а.

Я такой еж, что меня голой не напугаешь :)

К тому же альтеровская платка с PCIe тоже увиделась настроилась определилась - тоже с пол пинка.


Если не секрет, что вы в Vendor ID писали? :)


Если не секрет, что вы в Vendor ID писали?

1234 :) мы делали очень специфические девайсы для своих машин (банковское оборудование), они отдельно от этих машин нигде не применялись. когда начинали всё это дело - было ваще пофигу. а потом регистрацию этого ID всё откладывали. так и жило там написанное когда-то 1234. я не знаю, сейчас они зарегистрировали их или нет (фирма существует и продолжает разработку). при том, что всякие там гостовские номера у нас были выделены под техническую документацию и прочие бумажки и сертификаты типа ISO имелись. но в целом эти идентификаторы нужны только если собираешься заниматься ширпотребом и продавать карты напрямую потребителям.



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

Iron_Bug ★★★★★ ( 26.06.16 13:47:03 )
Последнее исправление: Iron_Bug 26.06.16 13:55:41 (всего исправлений: 2)


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

Iron_Bug ★★★★★ ( 26.06.16 14:03:36 )
Последнее исправление: Iron_Bug 26.06.16 14:05:09 (всего исправлений: 1)


а как тогда вообще материнки с рандомными ширпотребными девайсами работают? :) Их же должны как-то тестировать на производстве, втыканием какой-нибудь эталонной PCIe карты хотя бы


+1, если бы в общем случае всё было так - тогда бы ничего в мире не работало.

На самом низком физическом уровне PCIe прост как тапок, достаточно соблюдать несложные правила, к тому же стабильность клока допустима до 300 ppm если не ошибаюсь, что весьма комфортно. Да и пакеты тоже стандартизированы.

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

Ладно, спасибо Iron_Bug за предупреждение, буду иметь ввиду что могут быть проблемы.

Но главная тема: написать драйвер. Пока исходим из того что у меня есть - железо определилось, обменялось конфигурационными пакетами, считаем что линк рабочий и всё. Железные проблемы пока выносим за скобки - их нет, в слот лезет, питание поступает.


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

Iron_Bug ★★★★★ ( 26.06.16 15:32:27 )
Последнее исправление: Iron_Bug 26.06.16 15:33:14 (всего исправлений: 1)


Топовым видеокартам не нужны скорости и нагрузки? Ладно, не суть.

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




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

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

Iron_Bug ★★★★★ ( 26.06.16 17:12:29 )
Последнее исправление: Iron_Bug 26.06.16 17:13:10 (всего исправлений: 1)



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

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


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

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

1. Простой пример драйвера

Файл драйвера hello.c

Файл драйвера в основном включает в себя функции hello_open, hello_write, hello_init, hello_exit. Тестовый пример не дает модулю драйвера практическую функцию, а только печатает журнал, чтобы сообщить консоли некоторую отладочную информацию, чтобы мы могли понять выполнение драйвера ,

При печати с использованием printk добавление «KERN_EMERG» к параметрам может обеспечить вывод информации для печати на консоль. Поскольку printk print разделен на 8 уровней, верхний уровень выводится на консоль, а нижний уровень выводится в файл журнала.

Makefile требуется для компиляции драйвера

Для компиляции файла драйвера необходим подходящий make-файл, потому что при компиляции драйвера нужно знать файл заголовка ядра, правила компиляции и т. Д.

Тестовый верхний код приложения hellotest.c

В верхнем тестовом случае сначала откройте файл устройства, а затем запишите данные на устройство. Таким образом, будут вызваны соответствующие функции xxx_open и xxx_write в драйвере, и информация о печати драйвера может быть использована для определения, действительно ли соответствующая функция выполняется должным образом.

Во-вторых, пример теста диска

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

1. Скомпилируйте драйвер

Создайте команду, напрямую вызовите Makefile для компиляции hello.c и, наконец, сгенерируйте «hellomodule.ko».

2. Скомпилируйте приложение верхнего уровня

С помощью этой команды вы можете скомпилировать приложение hellotest верхнего уровня.

3. Загрузите драйвер

Когда insmod загружает драйвер, вызывается функция hello_init (), и распечатанная отладочная информация выглядит следующим образом.


Кроме того, вы можете увидеть загруженные модули в "/ proc / devices".


4. Создайте узел

Хотя драйвер hellomodule.ko был загружен, и загруженный модуль HelloModule также отображается в файле / proc / devices, этот модуль не может использоваться, поскольку в каталоге устройств / dev нет соответствующего файла устройства. Поэтому вам необходимо создать узел устройства.

Номер основного устройства модуля HelloModule в / proc / devices равен 231. Когда узел создается, файл устройства / dev / hellodev подключается к номеру основного устройства. Таким образом, когда приложение манипулирует файлом / dev / hellodev, оно обнаружит модуль HelloModule.


  • Устройство в / proc / devices генерируется драйвером, оно может генерировать мажор для mknod в качестве параметра. Содержимое этого файла показывает модули, которые в настоящее время смонтированы в системе. Когда загружается драйвер HelloModule, соответствующий файл устройства не генерируется для абстрактной инкапсуляции устройства для доступа к приложениям верхнего уровня.
  • Устройство в / dev добавляется mknod, и пользователь получает доступ к драйверу через это имя устройства. Я думал, что файлы в / dev можно рассматривать как абстрактный пакет аппаратных модулей, и все устройства в Linux были упакованы в виде файлов.

5. Вызов драйвера приложения верхнего уровня

Приложение hellotest сначала открывает файл "/ dev / hellodev", а затем записывает переменную val в этот файл. В течение этого периода будут вызваны функции hello_open и hello_write в базовом драйвере. Ниже приведены результаты работы hellotest.


6. Удалите драйвер

Когда insmod удаляет драйвер, он вызывает функцию hello_exit (), и печатная информация об отладке выглядит следующим образом.


Суммируйте последовательность операций модуля:

(1) Зарегистрируйте модуль с помощью команды insmod

(2) Создайте файл устройства «xxx» в каталоге / dev с помощью команды mknod и установите соединение с модулем через основной номер устройства.

(3) Прикладной уровень управляет базовым модулем через файл устройства / dev / xxx

Три, диск шаблон

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

1. Заголовочные файлы

init.h определяет функции, связанные с инициализацией и выходом драйвера
kernel.h определяет часто используемые прототипы функций и определения макросов
module.h определяет функции, переменные и макросы, связанные с модулем ядра

2. Функция инициализации

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

Функция инициализации используется для инициализации модуля, как следует из названия. Обычно используемая функция заключается в регистрации функции через register_chrdev. Ядро выделяет часть памяти (массив) для хранения набора функций символьного устройства.Функция register_chrdev заполнит содержимое hello_flops в позиции HELLO_MAJOR этого массива, которое должно зарегистрировать адрес функции функции HelloModule для набора памяти управления устройством.


3. Выход из функции

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

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

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

4. Информация об авторских правах

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

5. Функциональная функция

В-четвертых, процесс выполнения от приложения верхнего уровня до драйвера нижнего уровня

1. Иерархическая структура системы Linux


Многоуровневая структура системы Linux: прикладной уровень ----> библиотека ----> ядро ​​----> драйвер ----> аппаратное устройство.

2. Процесс выполнения от приложения верхнего уровня до драйвера нижнего уровня

Возьмем функцию "open (" / dev / hellodev ", O_RDWR)" в качестве примера для иллюстрации.

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

(2) Библиотека выполняет swi-инструкцию в соответствии с параметрами, переданными функцией open, что приведет к сбоям в работе процессора и попаданию в ядро.

(3) Функция обработки исключений ядра находит соответствующий драйвер в соответствии с этими параметрами.

(4) Выполните соответствующий драйвер.

(5) Верните дескриптор файла в библиотеку, а затем в приложение.

3. Характеристики исполнения водителя

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

Драйвер работает в «пространстве ядра», которое является частью «доверия» системы. Ошибки драйвера могут привести к сбою всей системы.

«Полное руководство по разработке приложений для встроенного Linux»

Читайте также: