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

Обновлено: 03.07.2024

Вот очень простой пример драйвера устройства ввода. Устройство имеет только одну кнопку, и эта кнопка доступна через порт ввода/вывода BUTTON_PORT. При нажатии или отпускании происходит BUTTON_IRQ. Драйвер может выглядеть так:

1.2. Что делает пример¶

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

Затем он выделяет новую структуру устройства ввода с помощью input_allocate_device() и устанавливает входные битовые поля. Таким образом, драйвер устройства сообщает другим частям системы ввода, что это такое — какие события могут быть сгенерированы или приняты этим устройством ввода. Наше примерное устройство может генерировать только события типа EV_KEY, а из них только код события BTN_0. Таким образом, мы устанавливаем только эти два бита. Мы могли бы использовать:

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

Затем пример драйвера регистрирует структуру устройства ввода, вызывая:

Это добавляет структуру button_dev в связанные списки драйвера ввода и вызывает функции _connect модулей обработчиков устройств, чтобы сообщить им о появлении нового устройства ввода. input_register_device() может находиться в спящем режиме и поэтому не должен вызываться из прерывания или с удерживаемой спин-блокировкой.

Во время использования драйвера используются только следующие функции:

который при каждом прерывании от кнопки проверяет ее состояние и сообщает об этом через:

вызов системы ввода. Нет необходимости проверять, не сообщает ли процедура прерывания о двух событиях с одинаковым значением (например, нажатие, нажатие) в систему ввода, потому что функции input_report_* проверяют это сами.

Затем:

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

1.3. dev->open() и dev->close()¶

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

Обратите внимание, что ядро ​​ввода отслеживает количество пользователей устройства и гарантирует, что dev->open() вызывается только тогда, когда первый пользователь подключается к устройству, а dev->close() вызывается, когда последний пользователь отключается. Вызовы обоих обратных вызовов сериализуются.

Обратный вызов open() должен возвращать 0 в случае успеха или любое ненулевое значение в случае неудачи. Обратный вызов close() (который является недействительным) всегда должен выполняться успешно.

1.4. Основные типы событий¶

Самый простой тип события — EV_KEY, который используется для клавиш и кнопок. Об этом сообщается в систему ввода через:

См. uapi/linux/input-event-codes.h для получения информации о допустимых значениях кода (от 0 до KEY_MAX). Значение интерпретируется как истинное значение, т.е. любое ненулевое значение означает, что клавиша нажата, нулевое значение означает, что клавиша отпущена. Входной код генерирует события только в том случае, если значение отличается от предыдущего.

В дополнение к EV_KEY есть еще два основных типа событий: EV_REL и EV_ABS. Они используются для относительных и абсолютных значений, предоставляемых устройством. Относительным значением может быть, например, движение мыши по оси X. Мышь сообщает об этом как об относительном отличии от последней позиции, потому что у нее нет абсолютной системы координат для работы. Абсолютные события предназначены именно для джойстиков и дигитайзеров — устройств, которые работают в абсолютных системах координат.

Настроить кнопки EV_REL для отчета устройства так же просто, как и с EV_KEY, просто установите соответствующие биты и вызовите:

функция. События генерируются только для ненулевого значения.

Однако EV_ABS требует особого внимания. Прежде чем вызывать input_register_device, вы должны заполнить дополнительные поля в структуре input_dev для каждой абсолютной оси вашего устройства. Если бы у нашего кнопочного устройства была также ось ABS_X:

Или вы можете просто сказать:

Этот параметр подходит для оси X джойстика с минимальным значением 0 и максимальным значением 255 (которое джойстик должен достичь, не проблема, если он иногда сообщает больше, но это должен иметь возможность всегда достигать минимального и максимального значений), с шумом в данных до +- 4 и с плоской центральной позицией размером 8.

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

1,5. BITS_TO_LONGS(), BIT_WORD(), BIT_MASK()¶

Эти три макроса из битопа.h помогите с некоторыми вычислениями битового поля:

1.6. Поля id* и name¶

Имя dev-> должно быть установлено до регистрации устройства ввода драйвером устройства ввода. Это строка типа «Универсальное кнопочное устройство», содержащая удобное для пользователя имя устройства.

Поля id* содержат идентификатор шины (PCI, USB, . ), идентификатор поставщика и идентификатор устройства. Идентификаторы шин определены в input.h. Идентификаторы производителя и устройства определены в файлах pci_ids.h, usb_ids.h и подобных включаемых файлах. Эти поля должны быть установлены драйвером устройства ввода перед его регистрацией.

Поле idtype можно использовать для конкретной информации о драйвере устройства ввода.

Поля id и name можно передать в пользовательскую среду через интерфейс evdev.

1.7. Поля keycode, keycodemax, keycodesize¶

Эти три поля должны использоваться устройствами ввода с плотными раскладками. Keycode — это массив, используемый для сопоставления скан-кодов с входными системными кодами клавиш. Значение keycode max должно содержать размер массива, а keycodesize — размер каждой записи в нем (в байтах).

Пользовательское пространство может запрашивать и изменять сопоставление текущего скан-кода с кодом клавиши, используя ioctl-коды EVIOCGKEYCODE и EVIOCSKEYCODE в соответствующем интерфейсе evdev. Когда на устройстве заполнены все 3 вышеупомянутых поля, драйвер может полагаться на стандартную реализацию ядра для установки и запроса сопоставлений кодов клавиш.

1.8. dev->getkeycode() и dev->setkeycode()¶

Обратные вызовы getkeycode() и setkeycode() позволяют драйверам переопределять стандартный механизм сопоставления keycode/keycodesize/keycodemax, предоставляемый ядром ввода, и реализовывать разреженные карты кодов клавиш.

1.9. Автоповтор ключа¶

<р>. просто. Это обрабатывается модулем input.c. Аппаратный автоповтор не используется, так как во многих устройствах его нет и даже там, где он есть, он иногда ломается (на клавиатурах: ноутбуки Toshiba). Чтобы включить автоповтор для вашего устройства, просто установите EV_REP в dev->evbit. Все будет обрабатываться системой ввода.

1.10. Другие типы событий, обработка выходных событий¶

  • EV_LED – используется для светодиодов клавиатуры.
  • EV_SND — используется для звуковых сигналов клавиатуры.

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

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

Концепция операционной системы (ОС) должна быть хорошо понята, прежде чем предпринимать какие-либо попытки перемещаться внутри нее. Для ОС доступно несколько определений:

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

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

Определение из книги Таненбаума (см. Ресурсы): Операционная система — это [программа], которая контролирует все ресурсы компьютера и предлагает поддержку, с помощью которой пользователи могут разрабатывать прикладные программы.

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


< /p>

Рисунок 1. Схема программного и аппаратного обеспечения

На рис. 1 показана взаимосвязь между пользовательскими программами, ОС и устройствами. В этой схеме четко указаны различия между программным и аппаратным обеспечением. С левой стороны пользовательские программы могут взаимодействовать с устройствами (например, жестким диском) через набор высокоуровневых библиотечных функций. Например, мы можем открывать и записывать в файл на жестком диске, вызывая библиотечные функции C fopen, fprintf и close:

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

Для выполнения этой команды и оболочка, и библиотечные функции выполняют вызов низкоуровневой функции ОС, например, open() , write() или close() . Каждое устройство может называться специальным файл с именем /dev/*. Внутри ОС состоит из набора драйверов, которые представляют собой части программного обеспечения, обеспечивающие низкоуровневую связь с каждым устройством. На этом уровне выполнения ядро ​​вызывает функции драйвера, такие как lp_open() или lp_write().

В правой части рисунка 1 аппаратное обеспечение состоит из устройства (видеодисплея или канала Ethernet) и интерфейса (карта VGA или сетевая карта). Наконец, драйвер устройства представляет собой физический интерфейс между программным обеспечением и оборудованием. Драйвер считывает и записывает в аппаратное обеспечение через порты (адреса памяти, с которыми аппаратное обеспечение связано физически), используя внутренние функции out_p и in_p :

Обратите внимание, что эти функции недоступны пользователю. Поскольку ядро ​​Linux работает в защищенном режиме, адреса нижней памяти, где находятся адреса портов, недоступны для пользователя. Функции, эквивалентные низкоуровневым функциям ввода и вывода, не существуют в библиотеке высокого уровня, как в других операционных системах, таких как MS-DOS.

Он выполняет управление вводом/выводом (I/O).

Он обеспечивает прозрачное управление устройствами, избегая низкоуровневого программирования (портов).

Увеличивает скорость ввода-вывода, поскольку обычно оптимизирован.

Включает управление программными и аппаратными ошибками.

Это обеспечивает одновременный доступ к оборудованию несколькими процессами.

Существует четыре типа драйверов: символьные драйверы, блочные драйверы, терминальные драйверы и потоки. Драйверы символов передают информацию от пользователя к устройству (или наоборот) побайтно (см. рис. 2). Два примера: принтер /dev/lp и память (да, память — это тоже устройство) /dev/mem.


< /p>

Рисунок 2. Драйверы персонажей

Драйверы блоков (см. рис. 3) передают блок информации за блоком. Это означает, что входящие данные (от пользователя или от устройства) сохраняются в буфере до тех пор, пока буфер не заполнится. Когда это происходит, содержимое буфера физически отправляется на устройство или пользователю. По этой причине не все напечатанные сообщения появляются на экране при сбое пользовательской программы (сообщения в буфере были потеряны) или не всегда загорается индикатор дисковода гибких дисков, когда пользователь записывает в файл. Наиболее яркими примерами драйверов этого типа являются диски: гибкие диски (/dev/fd0), жесткие диски IDE (/dev/hda) и жесткие диски SCSI (/dev/sd1).


< /p>

Рисунок 3. Драйверы блоков

Драйверы терминала (см. рис. 4) представляют собой специальный набор драйверов символов для взаимодействия с пользователем. Например, командные инструменты в открытой среде Windows, X-терминал или консоль — это устройства, которым требуются специальные функции, например, стрелки вверх и вниз для диспетчера командного буфера или табуляция в оболочке bash. Примерами блочных драйверов являются /dev/tty0 или /dev/ttya (последовательный порт). В обоих случаях ядро ​​включает специальные подпрограммы и специальные процедуры драйвера, чтобы справиться со всеми специфическими функциями.


< /p>

Рисунок 4. Драйверы терминала

Потоки — самые молодые драйверы (см. рис. 5), они предназначены для очень высокоскоростных потоков данных. И ядро, и драйвер включают несколько уровней протокола. Лучшим примером этого типа является сетевой драйвер.


< /p>

Рисунок 5. Потоковые драйверы

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

lp_init(): инициализирует драйвер и вызывается только во время загрузки.

lp_open() : открывает соединение с устройством.

lp_read() : чтение с устройства.

lp_write(): записывает на устройство.

lp_ioctl(): выполняет операции настройки устройства.

lp_release() : прерывает соединение с устройством.

lp_irqhandler() : специальные функции, вызываемые устройством для обработки прерываний.

Для определенных приложений доступны некоторые дополнительные функции, такие как *_lseek() , *_readdir() , *_select() и *_mmap() . Дополнительную информацию о них можно найти в Руководстве хакера Майкла Джонсона (см. Ресурсы).

Есть несколько причин для написания собственного драйвера устройства:

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

Чтобы использовать аппаратные прерывания: поскольку ядро ​​работает в защищенном режиме, пользователь не может управлять прерываниями непосредственно из программы.

Для обработки других необычных приложений, таких как управление виртуальным устройством (RAM-диском или симулятором устройства).

Для получения удовольствия программистом: написание драйвера повышает личную мотивацию, а также контроль над компьютером.

Чтобы узнать о внутренних частях системы.

С другой стороны, есть несколько причин не писать собственный драйвер:

Это требует хорошей умственной подготовки.

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

В процессе отладки ядро ​​легко зависает, и невозможно использовать отладчики или функции библиотеки C, такие как printf .

Чтобы понять следующее пояснение, вы должны знать язык программирования C, основные процедуры ввода-вывода, как минимум знать внутреннюю архитектуру ПК и иметь некоторый опыт разработки программных приложений для систем Unix.< /p>

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

Первый вопрос, на который мы отвечаем: зачем использовать Linux в качестве примера написания драйвера? Ответ двоякий: все исходные файлы доступны в Linux, и у меня есть рабочий пример в моей лаборатории в UPM-DISAM, Испания.

Однако и структура каталогов, и интерфейс драйвера с ядром зависят от ОС. Действительно, небольшие изменения могут появляться от одной версии или выпуска к другой. Например, некоторые вещи изменились с Linux 1.2.x на Linux 2.0.x, такие как прототипы функций драйвера, метод конфигурации ядра и файлы Makefile для компиляции ядра.

Устройство, которое мы выбрали для нашего объяснения, — это мобильный робот MRV-4 от американской компании Denning-Brach International Robotics. Хотя робот использует ПК со специальной платой для аппаратного взаимодействия (карта двигателя/сонара), компания не поставляет драйвер для Linux. Тем не менее, все исходные файлы программного обеспечения, управляющего роботом через моторную/гидролокационную карту, доступны на языке C для MS-DOS. Решение состоит в том, чтобы написать драйвер для Linux. В примере мы используем версию ядра 2.0.24, хотя она будет работать и в более поздних версиях с небольшими изменениями.

Мобильная платформа состоит из набора колес, соединенных с двумя двигателями (ведущий и рулевой), набора из 24 сонаров, которые действуют как датчики приближения для обнаружения препятствий, и набора бамперов, которые обнаруживают столкновения. Нам нужно реализовать драйвер, по крайней мере, со следующими службами (init, open и release обязательны):

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

read : чтение показаний сонара и значений энкодера

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

Команды ioctl: go to home, которая посылает колесам постоянную угловую скорость и активирует флаг go to home; и настройка моторов и гидролокаторов

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

Возвращаясь к исходной схеме (рис. 1), устройство — робот MRV-4, аппаратный интерфейс — карта мотора/гидролокатора, исходный файл драйвера — mrv4.c, новое ядро ​​сгенерируем будет vmlinuz, пользовательская программа для тестирования ядра будет mrv4test.c, а устройство будет /dev/mrv4 (см. рис. 6).


< /p>

Рис. 6. Схема mrv4hard MRV-4

Чтобы создать драйвер, выполните следующие действия:

Запрограммируйте исходные файлы драйвера, уделяя особое внимание интерфейсу ядра.

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

Настройте и скомпилируйте новое ядро.

Протестировать драйвер, написав пользовательскую программу.

Структура каталогов исходных файлов Linux может быть описана следующим образом: /usr/src содержит подкаталоги, такие как /xview и /linux. Внутри каталога /linux различные части ядра классифицируются по подкаталогам: init, kernel, ipc, drivers и т. д. Каталог /usr/src/linux/drivers/ содержит исходные коды драйверов, классифицированные по таким категориям, как блочные, символ, сеть и т. д.

Еще один интересный каталог — /usr/include, где расположены основные файлы заголовков, такие как stdio.h. Он содержит два специальных подкаталога:

/usr/include/system/, который включает файлы системных заголовков, такие как types.h

/usr/include/linux/, который включает заголовки ядра Linux, такие как lp.h, serial.h, mem.h и mrv4.h.

Первой задачей при программировании исходных файлов драйвера является выбор имени для его уникальной идентификации, например hd, sd, fd, lp и т. д. В нашем случае мы решили использовать mrv4. Наш драйвер будет символьным, поэтому мы запишем исходный код в файл /usr/src/linux/drivers/char/mrv4.c, а его заголовок — в /usr/include/linux/mrv4.h.< /p>

Вторая задача — реализовать функции ввода/вывода драйвера. В нашем случае это mrv4_open(), mrv4_read(), mrv4_write(), mrv4_ioctl() и mrv4_release().

При программировании драйвера следует соблюдать особую осторожность из-за следующих ограничений:

Стандартные функции библиотеки недоступны.

Некоторые операции с плавающей запятой недоступны.

Размер стека ограничен.

Невозможно дождаться событий, так как ядро ​​и все процессы остановлены.

Функции ОС, поддерживаемые на уровне ядра, — это, конечно, только те функции, которые запрограммированы внутри него:

kmalloc() , kfree() : управление памятью

cli() , sti() : включить/отключить прерывания

add_timer() , init_timer() , del_timer() : управление временем

request_irq() , free_irq() : управление прерываниями

inb_p() , outb_p() : управление портами

memcpy_*fs(): управление данными

printk() : ввод/вывод

register_*dev() , unregister_*dev() : управление устройством

*sleep_on() , wake_up*() : управление процессами

Подробная информация об этих функциях приведена в Руководстве Джонсона (см. Ресурсы) или даже в исходных файлах ядра.

Доступ к аппаратному интерфейсу (карте) осуществляется через адресацию с малым объемом памяти. Регистры ввода/вывода карты, где мы можем читать/записывать информацию, физически связаны с адресами памяти ПК (то есть портами). Например, моторная/гидролокационная карта мобильного робота MRV-4 связана с адресом 0x1b0. В этой плате используется шестнадцать регистров, поэтому карта портов включает адреса от 0x1b0 до 0x1be. Типовой список адресов портов показан в таблице 1.:

Необходимо найти свободную область адресов, чтобы выделить порты для новой платы. В таблице 1 адреса от 1b0 до 1be были свободны. Пример исходного кода, foo.c, доступен на FTP-сайте SSC (см. конец статьи) и включает вызов системной функции, которая позволяет нам увидеть предыдущую таблицу адресов. Наконец, доступ к портам предоставляется с помощью функций inb_p и outb_p .

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

Продолжая наш пример, нам нужны три обработчика, по одному для каждого из аппаратных прерываний, которые может генерировать карта: обработчик сонаров (с прерыванием 0x0a), домашний обработчик (с прерыванием 0x0b) и обработчик бампера (с прерыванием 0x0c). В качестве примера того, что должен делать исходный код, мы показываем структуру функции sonar_irq_hdlr. Каждый раз, когда принимается эхосигнал от сонара, он должен:

Отключить аппаратные прерывания.

Чтение значения сонара из его порта и сохранение его во внутренней переменной драйвера.

Снова включите прерывания.

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

Хотя мы объясним рекомендации по реализации каждой из функций драйвера, при программировании собственного драйвера рекомендуется использовать в качестве примера драйвер, наиболее похожий на ваш. В нашем случае моделями для mrv4.c и mrv4.h являются lp.c и lp.h соответственно.

Файл mrv4.c включает функции инициализации и ввода/вывода. Функция инициализации mrv4_init должна выполнить следующие шаги (см. рекомендации в файле foo.c):

Проверьте устройство.

Получить свободный регион для адресации портов.

Проверьте наличие оборудования.

Проверить, свободны ли номера прерываний.

Инициализировать внутренние переменные драйвера.

Вернуть статус OK.

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

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

Доступные команды определены в файле mrv4.h (см. инструкции в файле foo.h, также доступном на FTP-сайте):

Макрос _IO используется для команд без аргументов. _IOW используется для команд с входными аргументами.В этом случае макросу нужен тип аргумента, например, указатель может иметь тип unsigned int. Магический номер должен быть выбран программистом. Попробуйте выбрать тот, который не зарезервирован системой (см. другие заголовочные файлы в /usr/include/linux). Константы определены в файле /usr/include/linux/mrv4.h, который должен быть включен как драйвером (mrv4.c), так и пользовательскими программами. Как правило, файл mrv4.h может включать:

Определения констант и макросов

Структуры данных для обмена между водителем и пользователем

Прототип функции mrv4_init()

Задача интеграции драйвера в ядро ​​включает несколько шагов:

Вставьте вызовы ядра в новый драйвер.

Добавить драйвер в список драйверов.

Изменить сценарии компиляции.

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

Вызов ОС mrv4_init() вставляется в файл /usr/src/linux/kernel/mem.c. Другие вызовы функций драйвера (open, read, write, ioctl, release и т. д.) прозрачны для пользователя. Они осуществляются через структуру file_operations. Основной номер драйвера должен быть добавлен в список, расположенный в /usr/include/linux/major.h. Поиск бесплатного номера водителя; например, если номер 62 свободен, вы должны добавить в файл одну или обе следующие строки, в зависимости от версии Linux:

Каждое устройство имеет один старший и один дополнительный номер. Старший номер представляет собой номер водителя. Младший номер различает несколько устройств, управляемых одним и тем же устройством (например, несколько жестких дисков, управляемых одним и тем же драйвером IDE: hd0, hd1, hd2).

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

где 62 — старший, 0 — второстепенный (только одно физическое устройство), а c — символьное устройство. Установите разрешения по мере необходимости, хотя вы можете изменить их позже с помощью команды chmod. Например, включите rw, если вы хотите разрешить всем пользователям доступ к устройству:

Чтобы разрешить компиляцию драйвера в ядре, в файл скрипта /usr/src/linux/arch/i386/config.in необходимо добавить следующие строки:

и следующие строки в /usr/src/linux/drivers/char/Makefile:

Рекомендуется скомпилировать драйвер отдельно, перед компоновкой ядра. Этот метод сэкономит время на тестирование синтаксических ошибок: И когда все будет хорошо, удалите объектный файл: Затем настройте ядро, набрав: Ответьте yes, когда скрипт спросит вас об установке драйвера MRV-4 (это устанавливает константу CONFIG_MRV4). Наконец, вставьте пустую дискету и пересоберите ядро, введя следующие команды: Убедившись, что ядро ​​работает, вы можете перезаписать файл vmlinuz новым ядром. Чтобы протестировать новое ядро, перезапустите систему (введите reboot) и . удачи! Нет доступных отладчиков.

Мне нужно написать драйвер символьного устройства SPI Linux для omap4 с нуля. Я знаю некоторые основы написания драйверов устройств. Но я не знаю, как начать писать драйвер устройства для конкретной платформы с нуля.

Я написал несколько базовых драйверов символов и подумал, что написание драйвера устройства SPI будет похоже на это. Драйверы Char имеют структуру file_operations, которая содержит функции, реализованные в драйвере.

Теперь я просматриваю код spi-omap2-mcspi.c в качестве справочного материала, чтобы понять, как начать разработку драйвера SPI с нуля.

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


Просто вопрос: почему вы хотите переписать драйвер SPI? Раньше я использовал SPI-драйвер OMAP4, и у меня не было с ним проблем.

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

4 ответа 4

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

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

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

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

Когда Светлана и Пагс добрались до своего класса, они уже опоздали. Их профессор уже был там. Они посмотрели друг на друга, и Светлана застенчиво спросила: «Можно войти, сэр». "Да ладно. вы, ребята, снова опоздали», — крикнула профессор Гопи. «А какое у тебя сегодня оправдание?». «Сэр, мы обсуждали только вашу тему. Я объяснял ей про драйверы устройств в Linux», — поспешно ответил Пагс. "Неплохо!! Итак, объясните мне про динамическую загрузку в Linux. Вы все правильно поняли, и вы оба свободны», — подчеркнул профессор. Пагс был более чем счастлив. И он прекрасно знал, как осчастливить своего профессора – покритиковать Windows. Итак, вот что он сказал.

Как мы знаем, типичная установка драйвера в Windows требует перезагрузки для его активации. Это действительно неприемлемо, если нам нужно это сделать, скажем, на сервере. Вот где Linux выигрывает гонку. В Linux мы можем загрузить (/ установить) или выгрузить (/ удалить) драйвер на лету. И он активен для использования сразу после загрузки. Кроме того, он отключается при выгрузке, мгновенно. В Linux это называется динамической загрузкой и выгрузкой драйверов.

Как и ожидалось, он произвел впечатление на профессора. "Хорошо! занять свои места. Но смотри, чтобы ты снова не опоздал». С этим профессор продолжил класс: «Итак, как вы уже знаете, что такое динамическая загрузка и выгрузка драйверов в ядро ​​​​(Linux) и из него. Я научу вас, как это сделать. А затем мы приступим к написанию нашего первого драйвера для Linux сегодня».

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

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

Рисунок 4

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

Вот список различных команд (оболочек), относящихся к динамическим операциям:

  • lsmod: список загруженных в данный момент модулей.
  • insmod: вставить/загрузить модуль, указанный
  • modprobe — вставка/загрузка вместе с зависимостями
  • rmmod — удалить/выгрузить

Они находятся в каталоге /sbin и должны выполняться с привилегиями root. Давайте возьмем драйверы, связанные с файловой системой FAT, для нашего эксперимента. Различные файлы модулей будут находиться в каталогах fat.ko, vfat.ko и т.д. В случае, если они находятся в сжатом формате .gz, их необходимо распаковать с помощью gunzip для использования с insmod. модуль vfat зависит от модуля fat. Итак, fat.ko нужно загрузить перед vfat.ko. Чтобы выполнить все эти шаги (распаковка и загрузка зависимостей) автоматически, вместо этого можно использовать modprobe. Обратите внимание, что в имени модуля для modprobe нет .ko. rmmod используется для выгрузки модулей. На Рисунке 5 показан весь этот эксперимент.

Рисунок 5

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

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

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

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

Создаем наш первый драйвер для Linux

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

*Примечание 1: файлы Makefile очень чувствительны к объему памяти. Строки, не начинающиеся с первого столбца, имеют табуляцию, а не пробелы.*

*Примечание 2. Для создания драйвера Linux в системе должен быть установлен исходный код ядра (или, по крайней мере, заголовочные файлы ядра).*

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

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

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

lsmod должен показать загруженный драйвер OFD.

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

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