Introduction
I am writing this guide to lend a hand to everyone who has some curiosity about the device driver programming or like to play a bit with the kernel and write some sample kernel modules.
Coding for the kernel is not the same that developing in user space. It has other implications. The kernel is the chunk of executable program that manages of the available resources of a machine (CPU, memory, file system, I/O, networking…) for all the programs running over it. The access to all of the resources, included the piece of HW for which you are writing the driver, is done by system calls (which is an API for the user programs to access to this resources). Hence the kernel is very structured and when you code in the kernel space you have to meet some special requirements and procedures. Otherwise you could end up in a kernel panic.
But don’t worry In here we will go over it.
There are plenty of others guides out there. I have gone through some of them but, although their content is useful and accurate, the format is a bit old and the explanations usually are very light since it assumes you have a considerable knowledge about how the kernel works.
Prerequisites
Before start coding in your favorite editor you will need:
Kernel headers
The kernel headers for your current kernel: normally you won’t have it on your system unless you have compiled your own kernel or you have an insane liking for crashing your system and repairing it. To install them issue as root or with sudo and according to your linux distro:
After a successfully install you can find them in
The $(uname -r) is a bash command that will expand to your current kernel version.
Editor
I like Atom with some C plugins for small projects and testing, it is fast to install and the plugins are easily available and installable.
Regarding to the C spelling checker, most of the editors won’t recognize the #include <linux/. > so I recommend to copy the Linux to the development directory just to make happy the C spelling checker of our editor. Assuming you are inside your development directory:
The program
This is the hello world of the device driver programming. Note that the code is not mine, you can find it on internet in several places. So in your development directory create a file called myDriver.c with this content:
Let’s break it down, this module basically does:
- Load the libraries init.h and module.h . The first one provide macros for initialized data and the second defines the functions module_init() and module_exit()
- MODULE_LICENSE("GPL") it basically tells the kernel that this driver is under GPL license. If the module is licensed under a proprietary license the kernel will complain saying it is tainted. Read more here.
- hello_init(void) prints “Hello, world” in the kernel messages log with a log level of KERN_ALERT (this level is set this severe to make it visible in the logs, lesser server level may pass unnoticed)
- hello_exit(void) prints “Goodbye, cruel world” when called in the kernel log with same log level.
- module_init() tells the kernel which function to call when the module is loaded. Yeap, you guessed it right, it will be hello_init() 😉
- module_exit() defines which function to call when the module is removed. It is usually used for clean up all the things (memory reserved and stuff) you did in module_init()
Compiling
Unfortunately, compiling kernel modules isn’t as easy as gcc -W -Wall myDriver.c -o myDriver . To compile kernel modules, the proper approach is using kbuild. However this may lead into very complicated makefiles, so for the purposes of this guide we will use a very simplified makefile. Create a file called makefile and write on it:
Once you have this you should be able to see in your working directory:
Note that the linux folder is there only so the C spelling checker doesn’t complain. For compiling we just use make . You will be able to see something like this:
You may have guessed that my kernel version (4.12.8–2-ARCH) very likely won’t match yours, and the /path/to/your/working/directory/ will be substituted with your current path (also in the variable $PWD ). Now we are ready for loading our fantastic, great, feature rich module!! 😉
Loading and removing the module
For loading and removing the modules there are different tools: insmod , rmmod and modprobe . We are going to use the first two because are simpler. modprobe is more intelligent and includes many features such as loading an inserted module at next boot and many others, and it is intended more for module management rather than module development. In here we are going to use insmod for loading and rmmod for removing modules.
For loading the module, assuming you are in your working directory:
After issuing this command you will be able to see “Hello, world” at the end of the kernel messages log. For reading the kernel log type:
dmesg shows the kernel log and since it is too big we filter it to show only the last 10 lines with tail . At the end (and if nothing else is generating kernel log messages) you will be able to see your string.
You can also check how the module is effectively loaded into the kernel with lsmod or displaying the content of the /proc/modules file:
Finally, and when you are run out of tears of happiness for having been able to load your first module you may remove it 😉
Note that if you press Tab to autocomplete after rmmod it will list all the possibles modules to remove, including yours. Also note that the module is removed by its name myDriver no by is module file name myDriver.ko . Now let’s check the kernel log again:
At the end of the log, we can see how the module printed “Goodbye, cruel world” when it was removed.
Last words
I hope this little superficial tutorial has shed some light into how to get your first linux module loaded and removed.
I think that one of the barriers for starting to program device drivers is the complex set up you have to perform before starting to code/debug. I wish this guide will make the start up process a bit less rough for others.
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

Многие мои друзья и знакомые крутят пальцем у виска или задаются вопросом: не жмёт ли мне череп, когда узнают, что я пишу драйвера под 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 Вот я создал группу, для желающих участвовать меседжере 🙂
Для участников, желающих, прошу присоединяться :)))
Пишем свой драйвер под 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.
Ещё много функций можно заменить на более подходящие аналоги, но это
усложнило бы понимание статьи новичками 🙂