How to Develop Linux Driver from Scratch
Recently, when I was studying IoT, due to the lack of devices, simulating running firmware would often be short of /dev/xxx , so I began to wonder if I could write a driver myself to make the firmware run. No matter how hard it is and whether it can achieve my original intention or not, it pays off a lot if you learn how to develop Linux driver.
Introduction
The series I wrote is mainly about practice, which doesn’t talk much about theory. I learn how to develop the driver from the book Linux Device Drivers, and there is the code for the examples explained in this book on the GitHub [1].
As for the basic concept, Linux system is divided into kernel mode and user mode. The hardware device can only be accessed in the kernel mode, and the driver can be regarded as an API provided in the kernel mode to let the code of the user mode access the hardware device.
With the basic concepts in mind, I have come up with a series of problems, which inspire me to learn the development of driver.
- All code learning starts with Hello World, so how to write a Hello World program?
- How does the driver generate device files under /dev?
- How does the driver access the actual hardware?
- How do I get system-driven code? Or can it reverse the driver without code? Where are the binaries that store the drivers? In the future, there may be opportunities to try to study the drive security.
Everything Starts from Hello World
My Hello World code is as follows [2]:
The Linux driver is developed by means of C Language, which is different form the normal one we use. What we often use is Libc library, which doesn’t exist in the kernel. While the driver is a program running in the kernel, we use the library functions in the kernel.
For example, printk is analogous to printf in Libc, an output function defined in the kernel. But I think it's more like the logger function in Python, because the output of printk is printed in the kernel's log, which can be viewed via dmesg command.
There is only one entry point and one exit point in the driver code. Loading the driver into the kernel will execute the function defined by the module_init function, which in the above code is the hello_init function. When the driver is unloaded from the kernel, the function defined by the module_exit function is called, which in the above code is the hello_exit function.
The code above makes it clear that when the driver is loaded, it prints Hello World and when the driver is unloaded, it prints Goodbye World .
PS: MODULE_LICENSE and MODULE_AUTHOR are not very important. I'm not a professional development driver, so needn’t pay attention to them.
PSS: There should add a newline for the output of printk , otherwise the buffer will not be flushed.
Compile the Driver
The driver needs to be compiled by the make command, and the Makefile is shown below:
In general, the source code of the kernel exists in the /usr/src/linux-headers-$(shell uname -r)/ directory, such as:
And what we need is the compiled source directory, which is /usr/src/linux-headers-4.4.0-135-generic/ .
The header files of the driver code need to be searched from this directory.
The parameter M=$(PWD) indicates that the output of the driver compilation is in the current directory.
Finally, through the command obj-m := hello.o , which means to compile hello.o into hello.ko , and the ko file is the kernel module file.
Load the Driver into the Kernel
Some system commands that need to be used:
Lsmod : View the kernel module that is currently loaded.
Insmod : Loads the kernel module and requires root permissions.
Rmmod : Remove the module.
The old kernel is using the above method to load and remove the kernel, but the new version of the Linux kernel adds verification of the module. The current actual situation is as follows:
From a security perspective, the current kernel assumes that the module is untrustworthy and needs to be signed with a trusted certificate to load the module.
- Enter the BIOS and turn off the Secure Boot of UEFI.
- Add a self-signed certificate to the kernel and use it to sign the driver module (You can refer to [3]).
View the Results
Add Device Files under /dev
Once again, we firstly provide the code, and then explain the example code [4].
Knowledge Point 1 — Classification of Drivers
Drivers are divided into three categories: character devices, block devices and network interface. The above code is an example of character devices, and the other two will be discussed later.
As shown above, brw-rw— — the permission bar, block devices starts with "b" and the character devices starting with "c".
Knowledge Point 2 — The Major and Minor Numbers
The major number is used to distinguish the driver. In general, the same major number indicates that it is controlled by the same driver.
Multiple devices can be created in one drive, distinguished by minor numbers. The major and minor numbers determine a driver device together (as shown above).
The major number of equipment sda and sda1 is 8, and one minor number is 0 and the other minor number is 1.
Knowledge Point 3 — How the Driver Provides the API
In my mind, the interface provided by the driver is /dev/xxx , and under Linux, "everything is about file", so the operation of the driver device is actually the operation of the file and the driver is used to define/open/read/write. what /dev/xxx will happen. The API of driver you can think is all about file operations.
What file operations are there? They are all defined in the file_operations structure of the kernel <linux/fs.h> [5] header file.
In the code I illustrated above:
I declare a structure and assign it. Except for the owner , the values of other members are function pointers.
Then I used cdev_add to register the file operation structure with each driver in the scull_setup_cdev function.
For example, if I perform “open” operation on the driver device, I will execute the scull_open function, which is equivalent to "hooking" the open function in the system call.
Knowledge Point 4 — Generate the Corresponding Device under /dev
Compile the above code, get scull.ko , then sign it, and finally load it into the kernel via insmod .
Check if it is loaded successfully:
Although the driver has been loaded successfully, it does not create a device file in the /dev directory. We need to manually use mknod for device linking:
Summary
In this example, there is no operation on the actual physical device, just simply use kmalloc to apply for a block of memory in the kernel space.
No more details about the code, which can be found by looking up the header files or Google.
Here I would like to share my way of learning the development of drivers: read books to understand the basic concept firstly, and then look up for the details when you need to use them.
For example, I don’t need to know what API the driver can provide, and all I need to know is that the API provided by the drivers is all about file operations. As for the file operations, currently I only need to open , close , read and write . I will look up for more file operations when necessary.
Reference
About Knownsec & 404 Team
Beijing Knownsec Information Technology Co., Ltd. was established by a group of high-profile international security experts. It has over a hundred frontier security talents nationwide as the core security research team to provide long-term internationally advanced network security solutions for the government and enterprises.
Knownsec’s specialties include network attack and defense integrated technologies and product R&D under new situations. It provides visualization solutions that meet the world-class security technology standards and enhances the security monitoring, alarm and defense abilities of customer networks with its industry-leading capabilities in cloud computing and big data processing. The company’s technical strength is strongly recognized by the State Ministry of Public Security, the Central Government Procurement Center, the Ministry of Industry and Information Technology (MIIT), China National Vulnerability Database of Information Security (CNNVD), the Central Bank, the Hong Kong Jockey Club, Microsoft, Zhejiang Satellite TV and other well-known clients.
404 Team, the core security team of Knownsec, is dedicated to the research of security vulnerability and offensive and defensive technology in the fields of Web, IoT, industrial control, blockchain, etc. 404 team has submitted vulnerability research to many well-known vendors such as Microsoft, Apple, Adobe, Tencent, Alibaba, Baidu, etc. And has received a high reputation in the industry.
Создание собственных драйверов под Linux

Многие мои друзья и знакомые крутят пальцем у виска или задаются вопросом: не жмёт ли мне череп, когда узнают, что я пишу драйвера под Linux. Слово “драйвер” окутано каким-то почти мистическим смыслом, и постичь Дао его написания способны лишь избранные гуру.
К счастью это не так. Не знаю, как обстоят дела с написанием драйверов под другие операционные системы, в т.ч. и наиболее популярные, но под linux, вне зависимости от аппаратной архитектуры драйвера пишутся очень просто. Для написания драйвера необходимы базовые знания языка си, представление о работе ОС линукс (базовые), понимание того, что мы хотим получить, желание чтения документации и исходных кодов, ну и усидчивость. Всё.
Вы хотите посмотреть как написать драйвер для своего устройства? Тогда ныряйте под кат!
Лёгкость написания драйверов объясняется тем, что исходный код linux великолепно задокументирован и доступен в исходниках, в сети множество примеров и куча литературы. А для создания драйвера вам нужен только любимый дистрибутив и исходники ядра, ну и компилятор под нужную вам архитектуру. Свой первый, не учебный, драйвер я писал для процессора PowerPC 8360, он взаимодействовал с микросхемой ПЛИС и осуществлял сброс настроек по умолчанию. Как это ни странно звучит, я его сделал, немного переписав драйвер SPI для этой архитектуры. Я это говорю к тому, что единая стандартизация написания программ в ядре Linux позволяет проводить такие штуки.
Настольная книга разработчика драйверов под Linux — это “Linux Device Drivers”. Книжка является исчерпывающим руководством по разработке и если что-то не хватает в книге, то это точно можно найти самостоятельно в исходниках ядра. Разумеется, данный пост не претендует на то, чтобы заменить эту замечательную книгу. Более того, написан он по мотивам прочтения оных трудов, но всё же не является кратким пересказом данной книги. Автор этих строк ставит собой цель заинтересовать читателя простотой и изяществом ОС Linux, и понизить порог вхождения людей в разработку драйверов. Поверьте — это очень просто!
То, для чего будем писать драйвер
Поскольку основная цель данной статьи — написание драйверов, железу мы уделим минимум внимания. Я надеюсь все дружат с паяльником, а те кто не дружат — настало время начать дружить, я кратко расскажу, что и как делалось.
Мы будем писать драйвер для дисплея семейства HD44780 подключённого через LPT-порт к компьютеру. Знаю, что порт древний, а типу дисплеев лет не меньше, чем мне, но всё же — это красивый и простой пример написания драйверов под linux, не углубляясь в аппаратные подробности. И тем более, что переделать этот драйвер для этого экрана для других архитектур будет делом замены одной строчки!
Итак, нам понадобится LPT-порт; шнурок для старого принтера и опционально разъём CENTRONICS на 36 контактов, хотя можно просто распатронить кабель; разумеется экран семейства HD44780, самое лучшее — это минимум 4х40 символов, хотя подойдут даже самые маленькие; ну и переменный резистор на 10 кОм, для регулировки контраста. Поскольку LPT-порта у меня нет уже давным-давно, я прикупил первую попавшуюся плату на PCI (можно и PCI-E). Причём плата попалась на столько китайская, что я даже не смог найти никакой документации по напаянному чипу на ней. Ничего, будем ещё и реверсить её :)))).
Аппаратные исходники
Будем считать, что достать эти компоненты не составило труда, и ваши руки горят от нетерпения всё сделать.
Предупреждение.
Внимательно прочитать!

Хочу обозначить, что мы работаем в самых тяжёлых условиях, с железом, да ещё и будем работать на уровне ядра. Живём, так сказать, внутри пациента. Любая ошибка в программе и ядро падает. При чём оно может даже не успеть помахать вам kernel_panic-ом, просто внезапно всё перестаёт работать. Обратите внимание, что вы запустили драйвер, выгрузили его, и вдруг начались неявные глюки — всё, ваш путь — перезагрузка. Если система жива, то командой reboot. Если уже нет, то аппаратный reset. Хотя при тяжёлых случаях иногда система может сама себя перезагрузить.
Плюс, если вы не знаете что делаете (пишете в порт, обращаетесь к областям памяти и т.п.), то лучше этого не делать. Ибо всё это в лучшем случае может сбить настройки (записанное неверное значение в порт 70h раньше сбрасывало BIOS в х86) или даже попортить оборудование (например посадив ножку GPIO на землю). Поэтому чётко понимайте что вы делаете, как на аппаратном, так на программном уровне!
Приступаем
Для счастливых обладателей встроенного lpt-порта в своём компьютере скажу, что в вашем случае порт живёт по стандартному адресу 378h. Меня можно среди ночи разбудить и я скажу эту цифру, столько с ней связанно. Но ныне нам дали вражескую плату, и нам предстоит её победить!
Вставляем плату в слот PCI или PCI-E, загружаемся и смотрим что у нас происходит на шине PCI следующей командой:
Как видим линух нашёл что-то на шине PCI, и даже попытался дать ему какой-то левый, явно неправильный драйвер. Но нас более всего интересует адреса, на которых сидит данный порт. Это строки: I/O ports, а именно цифры c030h и c020h.
Нам теперь нужно найти какие же адреса за что отвечают. Для этого к порту вывода подключим светодиоды с резисторами примерно по такой схеме (резисторы примерно 310 Ом, или больше или меньше, не важно)
Схема проверки
Поскольку у меня уже есть заготовка светодиодов для теста такого рода, то я вставлял провода прямо в разъем CENTRONICS и у меня не влезло все 8 штук проводов, поэтому вставил только 6 (первые 4 и последние два).
Собранный тестер порта
И набросаем небольшую программку для проверки.
Для работы с портом я взял пример отсюда www.faqs.org/docs/Linux-mini/IO-Port-Programming.html#s9. Единственное, что надо исправить строку:
Делаем нормальные задержки в секунду (вместо usleep(100000);
, пишем sleep(1);) и делаем последовательно три вывода в порт:
Дефайн #define BASEPORT меняем на порт 0xc030. Компилируем, запускаем из под рута: светодиоды не горят. Меняем на другую цифру — 0xc020 — але оп:
Нуль
На выводах AAh
И, разумеется через секунду FFh
Результат налицо. Т.е. с нумерацией портов мы угадали, можно приступать к дровам. LPT-порт данной платы живёт по адресу. 0Xc020!
Хочу обратить внимание программистов-жестянщиков на магическое число 0xAA — это последовательность единиц и нулей. Вот так: 10101010b. Это очень удобно для всяких отладок. А вообще всякий кто работает с железом должен легко в уме переводить двоичное, десятичное, шестнадцатеричное туда-сюда-обратно.
Таки дисплей
Барабанная дробь, настало время собрать железку воедино! Для этого нам осталось всё спаять вместе. Всё собираем по следующей схеме.
Знаю, что схема не по ГОСТу, но это и не журнал «Радио»
Для питания экрана я распатронил провод USB, и сейчас у меня к экрану идут два провода. Есть хитрый вариант, просто до него никак не доходят руки — это пустить питание USB прямо в проводе LPT, отпаяв один из многочисленных земляных проводов. Как дойдут руки — обязательно сделаю. Допишу только эту статью :)))).
В результате, после подачи питания, мы должны получить что-то вроде этого. Подрегулируйте яркость резистором, чтобы появились две полоски. Они свидетельствуют нам о том, что дисплей не проинициализирован.
Девайс в сборе
Фух, с аппаратной частью мы закончили, пришла пора переходить к программе.
Приступаем злобным опытам с экраном в программе
В качестве готовой либы для работы с дисплеем использовал код от Arduino взятый отсюда. Я тоже не люблю эту платформу, но код годный для дисплеев, подключаемых по I2C. Если последнее слово вам ни о чём не говорит, не пугайтесь. Суть такова, что тот же самый дисплей подключён по двум проводам (как следует из названия шины), а далее там стоит регистр, который снова преобразует переданный байт в параллельную шину, прям как у нашего LPT-порта. В результате выкинув часть работы с I2C, получим годную либу.
Для чистоты переписал на сях, добавил парочку нужных функций, переименовал некоторые конфликтные, сделал ещё какие-то изменения (не помню, много воды утекло) и получил конфетку.
К слову сказать код использовался на STM32, так же с дисплеями подключающимися по I2C. А теперь мы его портируем уже на х86 да под linux. Вот она — великая сила переносимости кода! Потратив всего 10 минут, чтобы код адаптировать уже под новую платформу.
Работа с портом осуществляется в одной единственной функции — это непереносимый узел, и в зависимости от платформы способ вывода следует менять. Тут был вывод по I2C:
Обратите внимание на эту функцию. Заменив её, код можно запустить в AVR, STM32, MIPS и т. д.
Уже знакомый нам вывод в порт. Дефайн порта определяем в хедере lcd.h. Всё, теперь можно попробовать сделать вывод. Программа стала ещё проще и нагляднее.
Для вывода на экран используется функция-обёртка print_to_string
Первый параметр номер столбца (от нуля до 19), второй номер строки (от нуля до трёх), третий — указатель на символьную строку и последний — длина строки. Строка не проверяется на окончание “\0”, длину надо контролировать самостоятельно! В результате попробуем дисплейчик:
В результате на экране должно появится что-то типа этого:
Наш первый дебют!
Не пугайтесь этой порнографии. Этот экран был списан за битые пиксели, точнее там просто погиб контроллер от статики (их там несколько). Для работы он не годится, а для всяких подобных опытов — самое оно. Такие хреновые экраны делают китайцы!
На этом можно было бы остановиться, т.к. с данным примером можно выводить любые символы, загружать шрифты и т.п. Но, не в качестве драйвера. Хотя можно переделать в демон или как-то ещё, насколько позволит фантазия.
Поэтому, для тех кому стало скучно читать дальше, полную версию этого примера выкладываю тут.
Наша глобальная цель — драйвер устройства, поэтому к нему и приступим.
Инструментарий для сборки драйвера
Чтобы приступить к написанию драйвера, надо поставить необходимый инструментарий. Как подготовить инструментарий для Ubuntu и Debian хорошо рассказывается вот в этой статье blog.markloiseau.com/2012/04/hello-world-loadable-kernel-module-tutorial
Лишь кратко скажу, что если вам не нужно собирать debian-пакеты, а вы пишете модуль только для себя, то достаточно будет установить необходимые компоненты следующей командой:
Всячески рекомендую так же скачать исходные тексты своего ядра. Есть два варианта это сделать. Первый, и очевидный, но возможно слегка геморройный — это узнать версию своего ядра командой:
и скачать его с www.kernel.org. Я же лентяй лазать по ссылкам и искать версию ядра, доверю это дело автоматике.
Усё, запомните этот путь или добавьте ссылку на него, например /usr/src/linux, но не рекомендую это делать, т.к. ядра имеют тенденцию к обновлению. Тут вопрос спорный быть или не быть, решайте сами.
Теперь вы имеете у себя настоящее сокровище: кладезь актуальный документации и чудовищное количество примеров исходного кода и разумеется необходимый плацдарм для сборки модуля.
Настоятельнейше рекомендую лазать в папочку Documents, drivers и прочие, просматривать исходные тексты — это невероятно полезно!
Собстна драйвер
Если вы думаете, что мы сейчас просто поправим мейкфайл и получим готовый драйвер, то вы глубоко заблуждаетесь. Всё, теперь мы находимся в другом мире: не в пространстве пользователя, в пространстве ядра. И тут действуют совсем другие правила.
Чтобы осознать всю бедовость ситуации, то вы должны понимать, что ядро — это одна большая-пребольшая программа. А это означает, что глобальные переменные, функции и т.п. могут быть доступны в других местах ядра! Поэтому ВСЕ глобальные переменные (их следует избегать по максимуму) должны быть объявлены как static!
Мы не будем пересобирать ядро, встраивая в него драйвер, и каждый раз перезапускаться, пробуя его. Это конечно забавно, но очень долго. Вместо этого, мы сделаем модуль ядра, который мы будем загружать и выгружать прямо во время работы.
В нашем будущем модуле, по сравнении с программой для пространства пользователя, изменения будут не очень значительные. Во первых, перенесём все сишные файлы в один, чтобы не иметь проблем с компиляцией (ну или инклудим сишники в друг друга). Хедер оставляем, но чётко определяем все параметры функций (если никаких параметров, то надо прописать void). Вспоминаем, что мы в пространстве ядра, и все библиотеки у нас другие. Меняем все хидеры на ядерные:
Обратите внимание, что даже unistd.h и io.h стали ядерными.
В качестве первичной основы драйвера возьмём модуль hello world. Поправим функцию init:
Здесь всё практически без изменений. Меняется у нас функция работы с железом (самая платформозависимая), т.к. будет работать уже с ядерной функцией вывода в порт:
И, как это ни странно, функция задержек. Вообще задержки — отдельная песня, которой можно посвятить целую статью. У нас же должно быть всё быстро, и как можно меньше занимать ядро на всякую ерунду. Но всё же:
Объяснение такой махинации простое: udelay внутри ядра не может принимать значения больше тысячи. Чтобы избежать казусов, необходима такая заглушка.
После этого собираем примерно таким мейкфайлом:
И загружаем модуль в систему следующей командой:
Если мы всё сделали правильно, то нам не будет выведено ошибок, а экран верно проинициализируется и dmesg покажет последней строкой сообщение вашего модуля:
[10036.950566] Lpt module init
Выгрузить модуль можно командой
[10077.176714] Cleaning up module
Вообще, чтобы не жать постоянно dmesg существует скрипт существует решение, которое мне подсказал камрад Jtu:
Или, если вы используете дистрибутив отличный от Ubuntu, то следует исполнять это под root, и убрать sudo.
Результат на экране:
Привет вам!
Если вы всё это увидели — поздравляю, это ваш первый рабочий бесполезный модуль ядра, который выводит надпись на экран! Это конечно занимательно, но надо таки превращать наши эксперименты в нормальный драйвер, с которым можно работать. А для этого всё же придётся рассказать о матчасти, хоть немного.
Пару слов о матчасти
Как ни крути, но без знания некой матчасти далеко не уйдёшь.Есть символьные, блочные и сетевые устройства. Также у каждого устройства есть старший и младший номер устройства. Лучше, чем в книге “The Linux Kernel Module Programming Guide” сказать нельзя, по этому приведу цитату из книги в спойлере:
Старший и младший номер устройства
Давайте взглянем на некоторые файлы устройств. Ниже перечислены те из них, которые представляют первые три раздела на первичном жестком диске:
Обратили внимание на столбец с числами, разделенными запятой? Первое число называют «Старшим номером» устройства. Второе — «Младшим номером». Старший номер говорит о том, какой драйвер используется для обслуживания аппаратного обеспечения. Каждый драйвер имеет свой уникальный старший номер. Все файлы устройств с одинаковым старшим номером управляются одним и тем же драйвером. Все из выше перечисленных файлов устройств имеют старший номер, равный 3, потому что все они управляются одним и тем же драйвером.
Младший номер используется драйвером, для различения аппаратных средств, которыми он управляет. Возвращаясь к примеру выше, заметим, что хотя все три устройства обслуживаются одним и тем же драйвером, тем не менее каждое из них имеет уникальный младший номер, поэтому драйвер «видит» их как различные аппаратные устройства.
Устройства подразделяются на две большие группы — блочные и символьные. Основное различие блочных и символьных устройств состоит в том, что обмен данными с блочным устройством производится порциями байт — блоками. Они имеют внутренний буфер, благодаря чему повышается скорость обмена. В большинстве Unix-систем размер одного блока равен 1 килобайту или другому числу, являющемуся степенью числа 2. Символьные же устройства — это лишь каналы передачи информации, по которым данные следуют последовательно, байт за байтом. Большинство устройств относятся к классу символьных, поскольку они не ограничены размером блока и не нуждаются в буферизации. Если первый символ в списке, полученном командой ls-l /dev, ‘b’, тогда это блочное устройство, если ‘c’, тогда — символьное. Устройства, которые были приведены в примере выше — блочные. Ниже приводится список некоторых символьных устройств (последовательные порты):
Если вам интересно узнать, как назначаются старшие номера устройств, загляните в файл /usr/src/linux/documentation/devices.txt.
Все файлы устройств создаются в процессе установки системы с помощью утилиты mknod. Чтобы создать новое устройство, например с именем «coffee», со старшим номером 12 и младшим номером 2, нужно выполнить команду mknod /dev/coffee c 12 2. Вас никто не обязывает размещать файлы устройств в каталоге /dev, тем не менее, делается это в соответствии с принятыми соглашениями. Однако, при разработке драйвера устройства, на период отладки, размещать файл устройства в своем домашнем каталоге — наверное не такая уж и плохая идея. Единственное — не забудьте исправить место для размещения файла устройства после того, как отладка будет закончена.
Еще несколько замечаний, которые явно не касаются обсуждаемой темы, но которые мне хотелось бы сделать. Когда происходит обращение к файлу устройства, ядро использует старший номер файла, для определения драйвера, который должен обработать это обращение. Это означает, что ядро в действительности не использует и даже ничего не знает о младшем номере. Единственный, кто обеспокоен этим — это сам драйвер. Он использует младший номер, чтобы отличить разные физические устройства.
Между прочим, когда я говорю «устройства», я подразумеваю нечто более абстрактное чем, скажем, PCI плата, которую вы можете подержать в руке. Взгляните на эти два файла устройств:
К настоящему моменту вы можете сказать об этих файлах устройств, что оба они — блочные устройства, что обслуживаются одним и тем же драйвером (старший номер 2). Вы можете даже заявить, что они оба представляют ваш дисковод для гибких дисков, несмотря на то, что у вас стоит только один дисковод. Но почему два файла? А дело вот в чем, один из них представляет дисковод для дискет, емкостью 1.44 Мб. Другой — тот же самый дисковод, но для дискет емкостью 1.68 Мб, и соответствует тому, что некоторые люди называют «суперотформатированным» диском («superformatted» disk). Такие дискеты могут хранить больший объем данных, чем стандартно-отформатированная дискета. Вот тот случай, когда два файла устройства, с различным младшими номерами, фактически представляют одно и то же физическое устройство. Так что, слово «устройство», в нашем обсуждении, может означать нечто более абстрактное.
Создаём файл устройства
Мы будем использовать файл-драйвер символьного устройства. Чтобы не заниматься пересказом книги Linux Device Driver, а так же упростить себе жизнь, то я взял готовый пример регистрации символьного драйвера из этой статьи. В данной статье не занимаются поиском свободного фиксированного минора, а используется динамический минор. Для нас — это самое то.
Итак, модуль теперь будет выглядеть следующем образом: работа с экраном, которую мы реализовали выше остаётся без изменений, а вот остальную часть переписываем с нуля.
Определяем структуру file_operations
Где dev_read и dev_write — это указатели на функции (регистрация обратного вызова), которые будут обрабатывать соответственно чтение и запись из файла устройства. Здесь ещё можно, да наверное и нужно добавить указатели .open и .release для проверки однократного открытия и закрытия файла устройства, но мне пока лениво.
Для регистрации драйвера заводим вот такую структуру
где MISC_DYNAMIC_MINOR — макрос для динамического минора, строка «lptlcd» — это название устройства, как оно будет выглядеть в папке /dev/ (в нашем случае будет /dev/lptlcd), &lptlcd_fops — указатель на структуру file_operations.
Код регистрации драйвера тоже весьма прост
Дерегистрация устройства проста:
Ну и не забываем макросы установки колбеков, лицензии, автора и версии
Магия начинается в функции dev_read и dev_write. Начну с первой
Делает она следующее: если мы произведём чтение файла устройства, например так:
То нам на экран будет выведена строка за указателем info_str.
Код настолько очевидный, что в комментариях, надеюсь, не нуждается. Единственное, что функция copy_to_user наравне с функцией copy_from_user используется копирования данных между адресными пространствами пользователя и ядра.
Другая функции dev_write, она-то и пихает данные в экран.
На мой взгляд, не добавить и не убрать. Надеюсь тут всё понятно.
Компилируем, добавляем модуль в ядро и смотрим, что у нас появился файл:
Пробуем прочитать из него и записать что-то:
И увидим вот это:
Я водитель НЛО
Всё, мы теперь имеем полностью рабочий драйвер вместе с отвечающим взаимностью устройством! Поздравляю, можно бежать в лабаз за шампанским, и потом в юзерспейсе под волшебные пузырьки писать программы, работающие с этим девайсом.
Но всё же меня лично гложут какие-то сомнения, вот что-то не так…
Хочется БОЛЬШЕГО.
Итак, сейчас мы написали вполне рабочий драйвер, который уже можно использовать в повседневной практике. Выводить на него любой текст, даже текст из файла, использовать в своих программах. Но есть ощущение незавершённости, несовершенства.
Если вы возьмёте и почитаете любую документацию на экран, например статью, которую написал DIHALT о инициализации дисплея для AVR, то будете удивлены богатством возможностей. Например:
1. Можно загрузить текст в память, а затем просто двигать видимую область, экономя такты на загрузку новых данных (как помним, у нас могучие задержки).
2. Можно выводить текст в произвольную позицию.
3. Можно очищать экран.
4. И конечно, можно загружать свои шрифты. Не стоит обольщаться по этому поводу, доступно всего 8 символов. Например, задача реализация русского меню с этим экраном без русского шрифта, с латинскими буквами и частично загруженными русскими (8 символов), превращается во вполне занимательный ребус для целого отдела (было, плавали). Так что это юзается для каких-то специфических символов.
Хороший пример применения возможности загрузки шрифтов я нашёл на кофейном автомате на работе, в котором как раз установлен аналогичный экран, только русифицированный.
Экран на кофейном автомате
Вот тут отлично видно использование символов:
— два символа на сахар, пустой кружок (не догадался снять) и полный;
— пять символов бегущей строки (в символе 5 столбцов).
Итого 2+5 — остаётся всего один запасной символ, который так же, вероятно, используется. Не очень-то разбежишься.
В общем фишек у дисплея полно, так много, что мне уже лениво читать в документации. Но, как мы видим, всего этого мы делать пока не можем. И в этот чудесный момент настало время достать мой рояль из кустов, который уже давным-давно там стоит.
Прежде, чем я приступил к реализации данной идеи (даже до того, как начал покупать детали), я погуглил, а делал ли кто-то подобное. И, о чудо(!) таковой драйвер уже существует. Его написал Michael McLellan и обитает драйвер тут.
Честно скажу, данный драйвер на меня произвёл смешанное впечатление. Скажем так, он стал ценным пособием, как НЕ надо писать драйвера — переписать его под другую схему включения будет нетривиальной задачей, поскольку идёт тупая запись байтов, а каких, куда и зачем — непонятно. Переносимость отсутствует как класс. А куча define-ов для разных ядер выносят моск. Плюс, изначально драйвер задумывался под полную схему включения (8 бит на порт данных, см. схему lcd-mod.sourceforge.net/wiring.php ), а я сразу был нацелен на 4-х битную шину. Она хоть и медленнее (примерно в два раза, т.к. байт посылается двумя посылками), но во-первых, будет меньше паять, а во-вторых — не потребуется отыскивать другие порты. Ну и в третьих, у меня уже был отличный и логичный код для 4-х битной шины. В результате я решил таки написать свой драйвер сам и с нуля, попутно рассказывая как это сделать.
Ладно, к чёрту лирику. В том драйвере есть бесценная штука — это обработка управляющих или ESC-последовательностей. Если потрудится и качнуть этот “рояль”, то можно там найти всякие ридми и хау-ту. Так же на офсайте есть фак. Чтобы вам не пришлось лазать по этим всем данным, я решил собрать всё в одну кучу и на русском языке. Во первых, пару слов об этих последовательностях, вы все уже с ними сталкивались, когда операторе printf добавляли перевод строки в виде ‘\n’ — это и есть простейшая ESC-последовательность, которая интерпретируется, как символ 0Ah в ASCII. Такая же петрушка лежит в управлении данным дисплеем.
Итак, управляющие последовательности, для нашего экрана:
- \033 = Отправка ESC-последовательности, с которой начинаются команды
- [A = Переместить курсор на одну строку вверх
- [B = Переместить курсор на одну строку вниз
- [C = Сдвинуть курсор на одну позицию вправо
- [D = Сдвинуть курсор на одну позицию влево
- [H = Переместить курсор в левый верхний угол — домой (позиция 0,0)
- [J = Очистить всё, НЕ возвращает курсор домой!
- [K = Стирает до конца строки, НЕ возвращает курсор домой!
- [M = Новая карта символов (ДЗ — объяснить зачем!)
- [Y = Позиция Y (см. FAQ ниже)
- [X = Позиция X (см. FAQ ниже)
- [R = CGRAM Выбор ячейки памяти
- [V = Прокрутка включена
- [W = Прокрутка выключена
- [b = Подсветка включена-выключена (у нас работать не будет).
Другие полезные команды, работают без префикса \033!
- \r = Возврат каретки (возвращают курсор в позицию 0 на текущей линии!)
- \n = Новая линия
- \t = Табуляция (по умолчанию 3 символа)
Идея мне показалось очень крутой, и я нагло решил позаимствовать эту функцию в нашем проекте.
К слову, все эти последовательности чудесно работают в обычной консоли, ими можно пользоваться при выводе текста.
Кто ещё не понял, что это и зачем, то более подробно у него описано тут lcd-mod.sourceforge.net/faq.php. Чтобы не мучаться, я вольно перевёл его и заботливо положил в спойлер с картинками
Q. Хорошо, я установил модуль, как теперь я могу его использовать?
A. Экран будет вести себя почти так же, как vt52-терминал ru.wikipedia.org/wiki/VT52, отличие состоит только в использовании пользовательских шрифтов. Если вы хотите просто вывести текст на экран, вам следует послать его в формате ASCII на устройство. Для начала новой строки, следует отправить символ подачи строки — ‘\r’ и символ перевода каретки ‘\n’ (см. википедию ). Например, дав команду:
Перевод каретки
на экран будет выведено “Line One” и “Line Two” на двух строках экрана. От себя замечу, что опции команды “echo” означают:
-n — не завершать строку символом перевода каретки ‘\n’
-e — включается поддержка интерпретации управляющих последовательностей
Q. Когда я вывожу новую строку на экран, курсор не перемещается в начало строки?
A. Вы должны так же послать на экран символ перевода на новую строку и перевод каретки. Например:
(см. предыдущий пример)
Q. Почему, когда я пишу более, чем n линий на мой n-строчный эран, я вижу только n-1 линий, а последняя линия пустая?
A. Это потому, что вы используете команду “echo”. Данная комманда, без опций добавляет символ перевода каретки ‘\n’ в конце строки. Необходимо использовать аргумент -n, как я говорил выше.
Q. Как я могу передвигать курсор по кругу?
A. Вы можете установить курсор где хотите с помощью специальной управляющей последовательности: ESC-Y[Y-координата+037][X-координата+037]. Например:
Hello в позиции 0:0
Курсор будет перемещён в нулевую строку, нулевой столбец и будет выведено на экран слово “Hello”;
будет выведено тоже самое, но в позицию 1, 1;
Hello в позиции 1:1 (без очистки экрана)
аналогично, но напечатано будет в позицию 2, 2
Hello в позиции 2:2 (без очистки экрана, с предыдущими сообщениями)
Число “037” может немного запутать особенно, если вы не привыкли использовать восьмиричную арифметику, в будущих версиях я думаю о том, чтобы отбросить весь протокол vt52 и заменить его в более понятную реализацию (как автор жестоко ошибается 🙂 )..
Q. Как я могу использовать мои собственные шрифты?
A. Контроллер экрана HD4480 поддерживат до восьми символов определённых пользователем и наш модуль ядра поддерживает это, но это может быть довольной сложной операцией. Я собираюсь написать красивую GUI делающее это для вас, когда я вернусь к ней (или когда вы сможете написать об этом мне). Готов поспорить, что это предложение прочтут сущие единицы. Трололо, проверка на внимательность.
Вы можете установить один символ, отправив в устройство следующую команду: Esc-R[позиция шрифта][8 байт, определяющих битовую маску символа]. Символ определяет только последние пять бит во всём байте, поскольку в символе используется всего 5 столбцов. Я уже говорил об этом выше. Например:
Устанавливает символпо адресу 1, представляющую собой полностью закрашенный блок. Такой же, как я выше приводил у кофейного автомата в полосе готовности.
Символ полностью закрашенного блока
Устанавливает символ по адресу 0 в виде горизонтальных полос.
Символ горизонтальных полос
Чтобы вывести все «левые» символы, надо ввести следующую команду:
Вся порнография, что у нас есть сейчас в памяти.
В общем, как видно, требуется переписать функцию dev_write. И она теперь стала выглядеть так:
А всю грязную работу по разбору полётов на себя взяла функция handleInput. Код данной функции монструозен, поэтому я его приводить тут не буду. Оставлю вам на домашнее задание разобраться с ним и найти бекдор. Кто найдёт пасхальное яйцо в коде, отмечайтесь в комментах, тому конфетка.
Для затравки видос бекдора (он реализован в драйвере)
ASCII-ART Move! Запускается пасхалкой
Те, кто покажут мне аналогичный видос на своих экранах получат от меня подарок!
В общем, в результате мы получили кошерный экран, который можно использовать для вывода разной полезной и бесполезной информации. Вот, например на него можно выводить текущее время.
И они таки ходят!
Разумеется, написал — поделись с народом! Исходные коды доступны по ссылке (всё никак не соберусь юзать гитхаб для обмена кодом, позор).
Чего хотелось бы сделать
Просто так экранчик на столе мало понятно зачем нужен, это правда. Тем более в век планшетов и прочей техники. Когда всё можно перенаправить на ваш смартфон. Другое дело, что скоро грядёт Новый Год, и вспоминается один замечательный новогодний девайс — ёлочка на процессоре lpc2104
Embedded Artists Cristmas Tree
Не стоит гуглить этот девайс, все упоминания о нём убраны даже с официального сайта, а редкие упоминания о нём сложно найти даже на просторах интернета.
Я даже снял видео работы этой ёлочки
Как работает эмбеддеровская ёлочка
Кроме весёлого мигания огоньками у этого забавного устройства была крутая особенность — можно было вести переписку с другими владельцами таких ёлочек. Был некий сервант, куда через специальную форму можно было вбить сообщение и оно рассылалось по ёлочкам и выводилось на экран. Это было очень и очень круто и весело.
Теперь внимание: идея! Сейчас ничего не мешает заплатить на месяц 500 рублей (а если скинемся, сумма будет совсем мелкой), и арендовать сервак на виртуалке. Туда поставить программулину, которая будет рассылать сообщения. Либо jabber либо какие-то другие способы (да хоть nc и telnet всё сделать). А на компе поставить приёмник этих сообщений. И на Новый Год все мы будем иметь поздравления.
Кто готов помочь это реализовать, и кто будет делать себе такие экраны? Если нас наберётся хотя бы три человека, то будет весьма и весьма весело! Вливайтесь в тусовку разработчиков :)))). Выслушаю ЛЮБЫЕ идеи!
Буду краток. Данным постом я ставил цель не сделать пересказ книги Linux Device Drivers, а просто показать, что написание дров это тривиальная задача. Я умышленно ушёл от USB, т.к. там достаточно много теории, подводных камней и прочего геморроя. Но вы вполне можете переписать данный модуль для USB-LPT-шнурков, типа таких

Делается относительно просто: перехватывается протокол обмена с usb и реализуется в вашей программе. Поверьте — это просто. Будет интересно как — я вам расскажу!
А данный драйвер, поменяв всего ОДНУ(. ) строку вывода в порт можно переписать для применения, например на Raspberry Pi на порт GPIO. Точно, что написать я вам не скажу, но всё решается чтением даташита на используемый проц и занимает всего несколько минут.
Благодарности:
1. Хочу высказать главную благодарность своему учителю Боронину Сергею Сергеевичу sboronin за обучению программированию под linux и прекрасному курсу разработки драйверов под linux. Благодаря его курсам я весьма успешно работаю разработчиком.
2. Камраду Ariman с его замечательной серией статей про разработку дисплея для роутера (первая, вторая, третья часть (а ведь обещалась четвёртая. )). Эта серия, хоть и весьма специфическая, прекрасно иллюстрирует создание устройства и написание для него соответствующих драйверов. И я неоднократно обращался к ней, как к источнику бесценной информации, как в разработке драйверов, так и в разработке модулей для OpenWRT.
3. Разумеется автору книги “Linux Device Driver” Greg Kroah-Hartman. Эта книга просто библия, и в ядре часто можно встретить драйвера, которые базируются на его примерах. Например, в драйверах USB встретил такой:
И так во многих драйверах. Так, что пользуйтесь книгой — это библия.
4. Michael McLellan автору аналогичного драйвера. Как он говорит в своём драйвере:
К сожалению у меня нет его координат, кроме ссылки на linkedin. Почта из его программ уже не работает. Так, что как ему лично сказать спасибо, я не знаю… Если кто ему напишет, я буду очень признателен.
Ссылки
1. Исходники моей программы для rootfs работающая с дисплеем качнуть
2. Исходники моего модуля ядра, который мы разбирали в этом посте качнуть
3. «Рояль в кустах»
4. dmilvdv.narod.ru/translate.html Переводы статей. Настольная книга — Linux Device Drivers, Third Edition».
5. Отличная библиотека статей по программированию на русском от IBM обитает тут. Конкретно по разработке модулей ядра
6. Описание дисплея HD44780 (для AVR, но в целом оно универсально) easyelectronics.ru/avr-uchebnyj-kurs-podklyuchenie-k-avr-lcd-displeya-hd44780.html
7. Программы для дисплея (модуль ядра и спектральный анализатор для этого модуля) от Michael McLellan linux.downloadatoz.com/developer-michael-mclellan.html
P.S. Не стреляйте в пианиста, он играет как умеет. Такую громадную статью при моей врождённой неграмотности написать нормально нельзя. Обязательно присылайте мне замечания, правки, и дополнения, буду очень признателен.
P.P.S. Ничто не греет душу так, как оставленный комментарий.
P.P.P.S. В тексте тоже есть пасхалка, кто найдёт — тому конфетка ;)))
UPD Вот я создал группу, для желающих участвовать меседжере 🙂
Для участников, желающих, прошу присоединяться :)))
Pre-requisites
In order to develop Linux device drivers, it is necessary to have an understanding of the following:
- C programming. Some in-depth knowledge of C programming is needed, like pointer usage, bit manipulating functions, etc.
- Microprocessor programming. It is necessary to know how microcomputers work internally: memory addressing, interrupts, etc. All of these concepts should be familiar to an assembler programmer.
There are several different devices in Linux. For simplicity, this brief tutorial will only cover type char devices loaded as modules. Kernel 2.6.x will be used (in particular, kernel 2.6.8 under Debian Sarge, which is now Debian Stable).
User space and kernel space
When you write device drivers, it’s important to make the distinction between “user space” and “kernel space”.
- Kernel space. Linux (which is a kernel) manages the machine’s hardware in a simple and efficient manner, offering the user a simple and uniform programming interface. In the same way, the kernel, and in particular its device drivers, form a bridge or interface between the end-user/programmer and the hardware. Any subroutines or functions forming part of the kernel (modules and device drivers, for example) are considered to be part of kernel space.
- User space. End-user programs, like the UNIX shell or other GUI based applications ( kpresenter for example), are part of the user space. Obviously, these applications need to interact with the system’s hardware . However, they don’t do so directly, but through the kernel supported functions.
All of this is shown in figure 1.
Figure 1: User space where applications reside, and kernel space where modules or device drivers reside
Interfacing functions between user space and kernel space
The kernel offers several subroutines or functions in user space, which allow the end-user application programmer to interact with the hardware. Usually, in UNIX or Linux systems, this dialogue is performed through functions or subroutines in order to read and write files. The reason for this is that in Unix devices are seen, from the point of view of the user, as files.
On the other hand, in kernel space Linux also offers several functions or subroutines to perform the low level interactions directly with the hardware, and allow the transfer of information from kernel to user space.
Usually, for each function in user space (allowing the use of devices or files), there exists an equivalent in kernel space (allowing the transfer of information from the kernel to the user and vice-versa). This is shown in Table 1, which is, at this point, empty. It will be filled when the different device drivers concepts are introduced.
| Events | User functions | Kernel functions |
| Load module | ||
| Open device | ||
| Read device | ||
| Write device | ||
| Close device | ||
| Remove module |
Table 1. Device driver events and their associated interfacing functions in kernel space and user space.
Interfacing functions between kernel space and the hardware device
There are also functions in kernel space which control the device or exchange information between the kernel and the hardware. Table 2 illustrates these concepts. This table will also be filled as the concepts are introduced.
| Events | Kernel functions |
| Read data | |
| Write data |
Table 2. Device driver events and their associated functions between kernel space and the hardware device.
The first driver: loading and removing the driver in user space
I’ll now show you how to develop your first Linux device driver, which will be introduced in the kernel as a module.
For this purpose I’ll write the following program in a file named nothing.c
Since the release of kernel version 2.6.x, compiling modules has become slightly more complicated. First, you need to have a complete, compiled kernel source-code-tree. If you have a Debian Sarge system, you can follow the steps in Appendix B (towards the end of this article). In the following, I’ll assume that a kernel version 2.6.8 is being used.
Next, you need to generate a makefile. The makefile for this example, which should be named Makefile , will be:
Unlike with previous versions of the kernel, it’s now also necessary to compile the module using the same kernel that you’re going to load and use the module with. To compile it, you can type:
This extremely simple module belongs to kernel space and will form part of it once it’s loaded.
In user space, you can load the module as root by typing the following into the command line:
The insmod command allows the installation of the module in the kernel. However, this particular module isn’t of much use.
It is possible to check that the module has been installed correctly by looking at all installed modules:
Finally, the module can be removed from the kernel using the command:
By issuing the lsmod command again, you can verify that the module is no longer in the kernel.
The summary of all this is shown in Table 3.
| Events | User functions | Kernel functions |
| Load module | insmod | |
| Open device | ||
| Read device | ||
| Write device | ||
| Close device | ||
| Remove module | rmmod |
Table 3. Device driver events and their associated interfacing functions between kernel space and user space.
The “Hello world” driver: loading and removing the driver in kernel space
When a module device driver is loaded into the kernel, some preliminary tasks are usually performed like resetting the device, reserving RAM, reserving interrupts, and reserving input/output ports, etc.
These tasks are performed, in kernel space, by two functions which need to be present (and explicitly declared): module_init and module_exit ; they correspond to the user space commands insmod and rmmod , which are used when installing or removing a module. To sum up, the user commands insmod and rmmod use the kernel space functions module_init and module_exit .
Let’s see a practical example with the classic program Hello world :
The actual functions hello_init and hello_exit can be given any name desired. However, in order for them to be identified as the corresponding loading and removing functions, they have to be passed as parameters to the functions module_init and module_exit .
The printk function has also been introduced. It is very similar to the well known printf apart from the fact that it only works inside the kernel. The <1> symbol shows the high priority of the message (low number). In this way, besides getting the message in the kernel system log files, you should also receive this message in the system console.
This module can be compiled using the same command as before, after adding its name into the Makefile.
In the rest of the article, I have left the Makefiles as an exercise for the reader. A complete Makefile that will compile all of the modules of this tutorial is shown in Appendix A.
When the module is loaded or removed, the messages that were written in the printk statement will be displayed in the system console. If these messages do not appear in the console, you can view them by issuing the dmesg command or by looking at the system log file with cat /var/log/syslog .
Table 4 shows these two new functions.
| Events | User functions | Kernel functions |
| Load module | insmod | module_init() |
| Open device | ||
| Read device | ||
| Write device | ||
| Close device | ||
| Remove module | rmmod | module_exit() |
Table 4. Device driver events and their associated interfacing functions between kernel space and user space.
The complete driver “memory”: initial part of the driver
I’ll now show how to build a complete device driver: memory.c . This device will allow a character to be read from or written into it. This device, while normally not very useful, provides a very illustrative example since it is a complete driver; it’s also easy to implement, since it doesn’t interface to a real hardware device (besides the computer itself).
To develop this driver, several new #include statements which appear frequently in device drivers need to be added:
After the #include files, the functions that will be defined later are declared. The common functions which are typically used to manipulate files are declared in the definition of the file_operations structure. These will also be explained in detail later. Next, the initialization and exit functions—used when loading and removing the module—are declared to the kernel. Finally, the global variables of the driver are declared: one of them is the major number of the driver, the other is a pointer to a region in memory, memory_buffer , which will be used as storage for the driver data.
The “memory” driver: connection of the device with its files
In UNIX and Linux, devices are accessed from user space in exactly the same way as files are accessed. These device files are normally subdirectories of the /dev directory.
To link normal files with a kernel module two numbers are used: major number and minor number . The major number is the one the kernel uses to link a file with its driver. The minor number is for internal use of the device and for simplicity it won’t be covered in this article.
To achieve this, a file (which will be used to access the device driver) must be created, by typing the following command as root:
# mknod /dev/memory c 60 0
In the above, c means that a char device is to be created, 60 is the major number and 0 is the minor number .
Within the driver, in order to link it with its corresponding /dev file in kernel space, the register_chrdev function is used. It is called with three arguments: major number , a string of characters showing the module name, and a file_operations structure which links the call with the file functions it defines. It is invoked, when installing the module, in this way:
Also, note the use of the kmalloc function. This function is used for memory allocation of the buffer in the device driver which resides in kernel space. Its use is very similar to the well known malloc function. Finally, if registering the major number or allocating the memory fails, the module acts accordingly.
The “memory” driver: removing the driver
In order to remove the module inside the memory_exit function, the function unregsiter_chrdev needs to be present. This will free the major number for the kernel.
The buffer memory is also freed in this function, in order to leave a clean kernel when removing the device driver.
The “memory” driver: opening the device as a file
The kernel space function, which corresponds to opening a file in user space ( fopen ), is the member open: of the file_operations structure in the call to register_chrdev . In this case, it is the memory_open function. It takes as arguments: an inode structure, which sends information to the kernel regarding the major number and minor number ; and a file structure with information relative to the different operations that can be performed on a file. Neither of these functions will be covered in depth within this article.
When a file is opened, it’s normally necessary to initialize driver variables or reset the device. In this simple example, though, these operations are not performed.
The memory_open function can be seen below:
This new function is now shown in Table 5.
| Events | User functions | Kernel functions |
| Load module | insmod | module_init() |
| Open device | fopen | file_operations: open |
| Read device | ||
| Write device | ||
| Close device | ||
| Remove module | rmmod | module_exit() |
Table 5. Device driver events and their associated interfacing functions between kernel space and user space.
The “memory” driver: closing the device as a file
The corresponding function for closing a file in user space ( fclose ) is the release: member of the file_operations structure in the call to register_chrdev . In this particular case, it is the function memory_release , which has as arguments an inode structure and a file structure, just like before.
When a file is closed, it’s usually necessary to free the used memory and any variables related to the opening of the device. But, once again, due to the simplicity of this example, none of these operations are performed.
The memory_release function is shown below:
This new function is shown in Table 6.
| Events | User functions | Kernel functions |
| Load module | insmod | module_init() |
| Open device | fopen | file_operations: open |
| Read device | ||
| Write device | ||
| Close device | fclose | file_operations: release |
| Remove module | rmmod | module_exit() |
Table 6. Device driver events and their associated interfacing functions between kernel space and user space.
The “memory” driver: reading the device
To read a device with the user function fread or similar, the member read: of the file_operations structure is used in the call to register_chrdev . This time, it is the function memory_read . Its arguments are: a type file structure; a buffer ( buf ), from which the user space function ( fread ) will read; a counter with the number of bytes to transfer ( count ), which has the same value as the usual counter in the user space function ( fread ); and finally, the position of where to start reading the file ( f_pos ).
In this simple case, the memory_read function transfers a single byte from the driver buffer ( memory_buffer ) to user space with the function copy_to_user :
The reading position in the file ( f_pos ) is also changed. If the position is at the beginning of the file, it is increased by one and the number of bytes that have been properly read is given as a return value, 1 . If not at the beginning of the file, an end of file ( 0 ) is returned since the file only stores one byte.
In Table 7 this new function has been added.
| Events | User functions | Kernel functions |
| Load module | insmod | module_init() |
| Open device | fopen | file_operations: open |
| Read device | fread | file_operations: read |
| Write device | ||
| Close device | fclose | file_operations: release |
| Remove modules | rmmod | module_exit() |
Table 7. Device driver events and their associated interfacing functions between kernel space and user space.
The “memory” driver: writing to a device
To write to a device with the user function fwrite or similar, the member write: of the file_operations structure is used in the call to register_chrdev . It is the function memory_write , in this particular example, which has the following as arguments: a type file structure; buf , a buffer in which the user space function ( fwrite ) will write; count , a counter with the number of bytes to transfer, which has the same values as the usual counter in the user space function ( fwrite ); and finally, f_pos , the position of where to start writing in the file.
In this case, the function copy_from_user transfers the data from user space to kernel space.
In Table 8 this new function is shown.
| Events | User functions | Kernel functions |
| Load module | insmod | module_init() |
| Open device | fopen | file_operations: open |
| Close device | fread | file_operations: read |
| Write device | fwrite | file_operations: write |
| Close device | fclose | file_operations: release |
| Remove module | rmmod | module_exit() |
Device driver events and their associated interfacing functions between kernel space and user space.
The complete “memory” driver
By joining all of the previously shown code, the complete driver is achieved:
Before this module can be used, you will need to compile it in the same way as with previous modules. The module can then be loaded with:
It’s also convenient to unprotect the device:
# chmod 666 /dev/memory
If everything went well, you will have a device /dev/memory to which you can write a string of characters and it will store the last one of them. You can perform the operation like this:
$ echo -n abcdef >/dev/memory
To check the content of the device you can use a simple cat :
The stored character will not change until it is overwritten or the module is removed.
The real “parlelport” driver: description of the parallel port
I’ll now proceed by modifying the driver that I just created to develop one that does a real task on a real device. I’ll use the simple and ubiquitous computer parallel port and the driver will be called parlelport .
The parallel port is effectively a device that allows the input and output of digital information. More specifically it has a female D-25 connector with twenty-five pins. Internally, from the point of view of the CPU, it uses three bytes of memory. In a PC, the base address (the one from the first byte of the device) is usually 0x378 . In this basic example, I’ll use just the first byte, which consists entirely of digital outputs.
The connection of the above-mentioned byte with the external connector pins is shown in figure 2.
Figure 2: The first byte of the parallel port and its pin connections with the external female D-25 connector
The “parlelport” driver: initializing the module
The previous memory_init function needs modification—changing the RAM memory allocation for the reservation of the memory address of the parallel port ( 0x378 ). To achieve this, use the function for checking the availability of a memory region ( check_region ), and the function to reserve the memory region for this device ( request_region ). Both have as arguments the base address of the memory region and its length. The request_region function also accepts a string which defines the module.
The “parlelport” driver: removing the module
It will be very similar to the memory module but substituting the freeing of memory with the removal of the reserved memory of the parallel port. This is done by the release_region function, which has the same arguments as check_region .
The “parlelport” driver: reading the device
In this case, a real device reading action needs to be added to allow the transfer of this information to user space. The inb function achieves this; its arguments are the address of the parallel port and it returns the content of the port.
Table 9 (the equivalent of Table 2) shows this new function.
| Events | Kernel functions |
| Read data | inb |
| Write data |
Device driver events and their associated functions between kernel space and the hardware device.
The “parlelport” driver: writing to the device
Again, you have to add the “writing to the device” function to be able to transfer later this data to user space. The function outb accomplishes this; it takes as arguments the content to write in the port and its address.
Table 10 summarizes this new function.
| Events | Kernel functions |
| Read data | inb |
| Write data | outb |
Device driver events and their associated functions between kernel space and the hardware device.
The complete “parlelport” driver
I’ll proceed by looking at the whole code of the parlelport module. You have to replace the word memory for the word parlelport throughout the code for the memory module. The final result is shown below:
Initial section
In the initial section of the driver a different major number is used ( 61 ). Also, the global variable memory_buffer is changed to port and two more #include lines are added: ioport.h and io.h .
Module init
In this module-initializing-routine I’ll introduce the memory reserve of the parallel port as was described before.
Removing the module
This routine will include the modifications previously mentioned.
Opening the device as a file
This routine is identical to the memory driver.
Closing the device as a file
Again, the match is perfect.
Reading the device
The reading function is similar to the memory one with the corresponding modifications to read from the port of a device.
Writing to the device
It is analogous to the memory one except for writing to a device.
LEDs to test the use of the parallel port
In this section I’ll detail the construction of a piece of hardware that can be used to visualize the state of the parallel port with some simple LEDs.
WARNING: Connecting devices to the parallel port can harm your computer. Make sure that you are properly earthed and your computer is turned off when connecting the device. Any problems that arise due to undertaking these experiments is your sole responsibility.
The circuit to build is shown in figure 3 You can also read “PC & Electronics: Connecting Your PC to the Outside World” by Zoller as reference.
In order to use it, you must first ensure that all hardware is correctly connected. Next, switch off the PC and connect the device to the parallel port. The PC can then be turned on and all device drivers related to the parallel port should be removed (for example, lp , parport , parport_pc , etc.). The hotplug module of the Debian Sarge distribution is particularly annoying and should be removed. If the file /dev/parlelport does not exist, it must be created as root with the command:
# mknod /dev/parlelport c 61 0
Then it needs to be made readable and writable by anybody with:
# chmod 666 /dev/parlelport
The module can now be installed, parlelport . You can check that it is effectively reserving the input/output port addresses 0x378 with the command:
To turn on the LEDs and check that the system is working, execute the command:
$ echo -n A >/dev/parlelport
This should turn on LED zero and six, leaving all of the others off.
You can check the state of the parallel port issuing the command:
Figure 3: Electronic diagram of the LED matrix to monitor the parallel port
Final application: flashing lights
Finally, I’ll develop a pretty application which will make the LEDs flash in succession. To achieve this, a program in user space needs to be written with which only one bit at a time will be written to the /dev/parlelport device.
It can be compiled in the usual way:
$ gcc -o lights lights.c
and can be executed with the command:
The lights will flash successively one after the other! The flashing LEDs and the Linux computer running this program are shown in figure 4.
Conclusion
Having followed this brief tutorial you should now be capable of writing your own complete device driver for simple hardware like a relay board (see Appendix C), or a minimal device driver for complex hardware. Learning to understand some of these simple concepts behind the Linux kernel allows you, in a quick and easy way, to get up to speed with respect to writing device drivers. And, this will bring you another step closer to becoming a true Linux kernel developer.
Figure 4: Flashing LEDs mounted on the circuit board and the computer running Linux. Two terminals are shown: one where the “parlelport” module is loaded and another one where the “lights” program is run. Tux is closely following what is going on
Bibliography
A. Rubini, J. Corbert. 2001. Linux device drivers (second edition). Ed. O’Reilly. This book is available for free on the internet.
Jonathan Corbet. 2003/2004. Porting device drivers to the 2.6 kernel. This is a very valuable resource for porting drivers to the new 2.6 Linux kernel and also for learning about Linux device drivers.
B. Zoller. 1998. PC & Electronics: Connecting Your PC to the Outside World (Productivity Series). Nowadays it is probably easier to surf the web for hardware projects like this one.
M. Waite, S. Prata. 1990. C Programming. Any other good book on C programming would suffice.
Appendix A. Complete Makefile
Appendix B. Compiling the kernel on a Debian Sarge system
To compile a 2.6.x kernel on a Debian Sarge system you need to perform the following steps, which should be run as root:
- Install the “kernel-image-2.6.x” package.
- Reboot the machine to make this the running kernel image. This is done semi-automatically by Debian. You may need to tweak the lilo configuration file /etc/lilo.conf and then run lilo to achieve this.
- Install the “kernel-source-2.6.x” package.
- Change to the source code directory, cd /usr/src and unzip and untar the source code with bunzip2 kernel-source-2.6.x.tar.bz2 and tar xvf kernel-source-2.6.x.tar . Change to the kernel source directory with cd /usr/src/kernel-source-2.6.x
- Copy the default Debian kernel configuration file to your local kernel source directory cp /boot/config-2.6.x .config .
- Make the kernel and the modules with make and then make modules .
Appendix C. Exercises
If you would like to take on some bigger challenges, here are a couple of exercises you can do:
- I once wrote two device drivers for two ISA Meilhaus boards, an analog to digital converter (ME26) and a relay control board (ME53). The software is available from the ADQ project. Get the newer PCI versions of these Meilhaus boards and update the software.
- Take any device that doesn’t work on Linux, but has a very similar chipset to another device which does have a proven device driver for Linux. Try to modify the working device driver to make it work for the new device. If you achieve this, submit your code to the kernel and become a kernel developer yourself!
Comments and acknowledgements
Three years have elapsed since the first version of this document was written. It was originally written in Spanish and intended for version 2.2 of the kernel, but kernel 2.4 was already making its first steps at that time. The reason for this choice is that good documentation for writing device drivers, the Linux device drivers book (see bibliography), lagged the release of the kernel in some months. This new version is also coming out soon after the release of the new 2.6 kernel, but up to date documentation is now readily available in Linux Weekly News making it possible to have this document synchronized with the newest kernel.
Fortunately enough, PCs still come with a built-in parallel port, despite the actual trend of changing everything inside a PC to render it obsolete in no time. Let us hope that PCs still continue to have built-in parallel ports for some time in the future, or that at least, parallel port PCI cards are still being sold.
This tutorial has been originally typed using a text editor (i.e. emacs ) in noweb format. This text is then processed with the noweb tool to create a LaTeX file ( .tex ) and the source code files ( .c ). All this can be done using the supplied makefile.document with the command make -f makefile.document .
I would like to thank the “Instituto Politécnico de Bragança”, the “Núcleo Estudantil de Linux del Instituto Politécnico de Bragança (NUX)”, the “Asociación de Software Libre de León (SLeón)” and the “Núcleo de Estudantes de Engenharia Informática da Universidade de Évora” for making this update possible.
Пишем свой драйвер под 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 <linux/kernel.h> /* Для printk() и т.д. */
#include <linux/module.h> /* Эта частичка древней магии, которая оживляет модули */
#include <linux/init.h> /* Определения макросов */
#include <linux/fs.h>
#include <asm/uaccess.h> /* put_user */
// Ниже мы задаём информацию о модуле, которую можно будет увидеть с помощью Modinfo
MODULE_LICENSE( «GPL» );
MODULE_AUTHOR( «Alex Petrov <petroff.alex@gmail.com>» );
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 < 0 )
<
printk( «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;
while ( length && *text_ptr )
<
put_user( *( text_ptr++ ), buffer++ );
length—;
byte_read++;
>
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 <druid@joker.botik.ru>
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;
if ( is_device_open ) <<<<
return -EBUSY;
Если один процесс увеличит is_device_open во время исполнения другим
процессом команд между if (is_device_open) и is_device_open++, то в
итоге файл откроется 2 раза. Для атомарных действий нужно использовать
функцию из серии atomic_XXX().
Атомарные операции нужно использовать во всех местах работы с данными. В данном случае и в close().
2) device_write() можно было вообще не писать, т.к. обработчик по
умолчанию write() сам возвращает ошибку.
3) у put_user() ОБЯЗАТЕЛЬНО нужно проверять результат. Если не ноль, то
нужно либо
а) вернуть результат -EFAULT и сделать вид, что ничего не было (т.е.
не удалять не до конца считанные данные из внутренних буферов, в данном
случае данные константные и ничего изменять не надо)
б) вернуть кол-во УЖЕ записанный байт (это называется partial read,
позволено POSIX’ом). При этом нужно следить за тем, чтобы не вернуть 0:
read() = 0 означает, что файл подошёл к концу, а это не так.
4) В ядре в качестве успешного кода завершения используется 0, а не
задефайненная константа SUCCESS. Есть исключения, например, в
обработчике сетевого пакета, но там, где возвращается либо -EXXX (код
ошибки), либо 0 (всё хорошо), используется именно константа 0.
Ещё много функций можно заменить на более подходящие аналоги, но это
усложнило бы понимание статьи новичками 🙂