嵌入式Linux设备驱动程序:编写内核设备驱动程序
Embedded Linux device drivers: Writing a kernel device driver
编写内核设备驱动程序
最终,当您用尽了之前所有的用户空间选项后,您将发现自己必须编写一个设备驱动程序来访问连接到设备上的硬件。字符驱动程序是最灵活的,应该能满足你90%的需求;网络驱动程序适用于使用网络接口,而块驱动程序用于大容量存储。编写内核驱动程序的任务很复杂,超出了本文的范围。最后有一些参考资料可以帮助你。概述一下与驱动程序交互时可用的选项,这是一个通常不会涉及的主题,并向您展示角色设备驱动程序的基本知识。
设计字符驱动接口
主字符驱动程序接口基于字节流,就像使用串行端口一样。然而,许多设备并不符合这种描述:例如,机器人手臂的控制器需要功能来移动和旋转每个关节。幸运的是,除了读写之外,还有其他与设备驱动程序通信的方法:
ioctl:ioctl函数允许您将两个参数传递给您的驱动程序,这两个参数可以具有您喜欢的任何含义。按照惯例,第一个参数是一个命令,它选择驱动程序中几个函数中的一个,第二个参数是指向结构的指针,它充当输入和输出参数的容器。一个像画布这样的程序可以让你设计任何一个空白的界面。当驱动程序和应用程序紧密链接并由同一个团队编写时,这种情况很常见。但是,ioctl在内核中是不推荐使用的,而且您会发现很难在上游使用ioctl的任何驱动程序得到接受。内核维护者不喜欢ioctl,因为它使内核代码和应用程序代码过于相互依赖,而且很难在内核版本和体系结构中保持两者同步。
sysfs:这是现在的首选方式,前面描述的GPIO接口就是一个很好的例子。它的优点是,只要为文件选择描述性名称,它就有点自文档化。它也是可编写脚本的,因为文件内容通常是文本字符串。另一方面,如果需要一次更改多个值,则每个文件都必须包含一个值,这使得实现原子性变得很困难。相反,ioctl在单个函数调用中传递结构中的所有参数。
mmap:通过将内核内存映射到用户空间,绕过内核,可以直接访问内核缓冲区和硬件寄存器。内核和DMA可能仍然需要处理一些代码中断。uio文档中有更多的uio驱动程序,例如,在文档中有更多的uio。
sigio:您可以使用名为kill_fasync()的内核函数从驱动程序发送信号,以通知应用程序输入准备就绪或接收到中断等事件。按照惯例,使用SIGIO信号,但可以是任何信号。您可以在UIO驱动程序drivers/UIO/UIO.c和RTC驱动程序drivers/char/RTC.c中看到一些示例。主要问题是很难在用户空间中编写可靠的信号处理程序,因此它仍然是一个很少使用的工具。
debugfs:这是另一个伪文件系统,它将内核数据表示为文件和目录,类似于proc和sysfs。主要区别在于debugfs不能包含系统正常运行所需的信息;它只包含调试和跟踪信息。它挂载为mount-t debugfs debug/sys/kernel/debug。内核文档documentation/filesystems中对debugfs有很好的描述/调试文件.txt.
proc:proc文件系统对于所有新代码都是不推荐使用的,除非它与进程相关,这是文件系统最初的目的。但是,您可以使用proc发布您选择的任何信息。而且,与sysfs和debugfs不同,它可用于非GPL模块。
netlink:这是一个socket协议族。AFüNETLINK创建一个将内核空间链接到用户空间的套接字。它最初是为了让网络工具可以与Linux网络代码通信来访问路由表和其他细节。udev也使用它将事件从内核传递到udev,这在一般设备驱动程序中很少使用。
在内核源代码中有许多前面提到的文件系统的例子,您可以为您的驱动程序代码设计真正有趣的接口。唯一的普遍规则是最小惊奇原则。换言之,使用驱动程序的应用程序编写者应该发现,一切都以逻辑方式工作,没有任何怪癖或怪癖。
设备驱动程序的剖析 ‘
现在是时候通过查看一个简单的设备驱动程序的代码来绘制一些线程。下面是一个名为dummy的设备驱动程序,它创建了四个通过
dev/dummy0 to /dev/dummy3 .
驱动程序的完整源代码如下:您将在
MELP/chapter_09/dummy-driver :
#include #include #include #include #include #define DEVICE_NAME "dummy"#define MAJOR_NUM 42#define NUM_DEVICES 4static struct class *dummy_class;static int dummy_open(struct inode *inode, struct file *file){ pr_info("%sn", __func__); return 0;}static int dummy_release(struct inode *inode, struct file *file){ pr_info("%sn", __func__); return 0;}static ssize_t dummy_read(struct file *file, char *buffer, size_t length, loff_t * offset){ pr_info("%s %un", __func__, length); return 0;}static ssize_t dummy_write(struct file *file, const char *buffer, size_t length, loff_t * offset){ pr_info("%s %un", __func__, length); return length;}struct file_operations dummy_fops = { .owner = THIS_MODULE, .open = dummy_open, .release = dummy_release, .read = dummy_read, .write = dummy_write,};int __init dummy_init(void){ int ret; int i; printk("Dummy loadedn"); ret = register_chrdev(MAJOR_NUM, DEVICE_NAME, &dummy_fops); if (ret != 0) return ret; dummy_class = class_create(THIS_MODULE, DEVICE_NAME); for (i = 0; i < NUM_DEVICES; i++) { device_create(dummy_class, NULL, MKDEV(MAJOR_NUM, i), NULL, "dummy%d", i); } return 0;}void __exit dummy_exit(void){ int i; for (i = 0; i < NUM_DEVICES; i++) { device_destroy(dummy_class, MKDEV(MAJOR_NUM, i)); } class_destroy(dummy_class); unregister_chrdev(MAJOR_NUM, DEVICE_NAME); printk("Dummy unloadedn");}module_init(dummy_init);module_exit(dummy_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("Chris Simmonds");MODULE_DESCRIPTION("A dummy driver");
At the end of the code, the macros called module_init and module_exit specify the functions to be called when the module is loaded and unloaded. The three macros named MODULE_* add some basic information about the module, which can be retrieved from the compiled kernel module using the modinfo
When the module is loaded, the dummy_init() function is called. You can see the point at which it becomes a character device when is makes the call to register_chrdev , passing a pointer to struct file_operations , which contains pointers to the four functions that the driver implements. While register_chrdev tells the kernel that there is a driver with a major number of 42, it doesn't say anything about the class of driver, and so it will not create an entry in /sys/class . Without an entry in /sys/class , the device manager cannot create device nodes. So, the next few lines of code create a device class, dummy and four devices of that class called dummy0 to dummy3 . The result is that the /sys/class/dummy directory is created when the driver is initialized, containing subdirectories dummy0 to dummy3 . Each of the subdirectories contains a file, dev , with the major and minor numbers of the device. This is all that a device manager needs to create device nodes: /dev/dummy0 to /dev/dummy3 .
The dummy_exit function has to release the resources claimed by dummy_init , which here means freeing up the device class and major number.
The file operations for this driver are implemented by dummy_open() , dummy_read(), dummy_write(), and dummy_release() and are called when a user space program calls open(2), read(2), write(2), and close(2). They just print a kernel message so that you can see that they were called. You can demonstrate this from the command line using the echo command:
# echo hello > /dev/dummy0
dummy_open
dummy_write 6
dummy_release
In this case, the messages appear because I was logged on to the console, and kernel messages are printed to the console by default. If you are not logged onto the console, you can still see the kernel messages using the command dmesg .
The full source code for this driver is less than 100 lines, but it is enough to illustrate how the linkage between a device node and driver code works, how the device class is created, allowing a device manager to create device nodes automatically when the driver is loaded, and how the data is moved between user and kernel spaces. Next, you need to build it.
Compiling kernel modules
At this point, you have some driver code that you want to compile and test on your target system. You can copy it into the kernel source tree and modify makefiles to build it, or you can compile it as a module out of tree. Let's start by building out of tree.
You need a simple makefile which uses the kernel build system to do the hard work:
LINUXDIR := $(HOME)/MELP/build/linux
obj-m := dummy.o
all:
make ARCH=arm
CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-
-C
$(LINUXDIR) M=$(shell pwd)
clean:
make -C
$(LINUXDIR) M=$(shell pwd) clean
Set LINUXDIR to the directory of the kernel for your target device that you will be running the module on. The obj-m := dummy.o code will invoke the kernel build rule to take the source file, dummy.c , and create kernel module, dummy.ko . I will show you how to load kernel modules in the next section.
If you want to build a driver in the kernel source tree, the procedure is quite simple. Choose a directory appropriate to the type of driver you have. The driver is a basic character device, so I would put dummy.c in drivers/char . Then, edit the makefile in the directory, and add a line to build the driver unconditionally as a module, as follows:
obj-m += dummy.o
Or add the following line to build it unconditionally as a built-in:
obj-y += dummy.o
If you want to make the driver optional, you can add a menu option to the Kconfig file and make the compilation conditional on the configuration option, as I described in Chapter 4, Configuring and Building the Kernel, in the section, Understanding kernel configuration .
Loading kernel modules
You can load, unload, and list modules using the simple insmod, lsmod, and rmmod commands. Here they are shown loading the dummy driver:
# insmod /lib/modules/4.8.12-yocto-standard/kernel/drivers/dummy.ko
# lsmod
Tainted:
G
dummy 2062 0 - Live 0xbf004000 (O)
# rmmod dummy
If the module is placed in a subdirectory in /lib/modules/ , you can create a modules dependency database using the command, depmod -a:
# depmod -a
# ls /lib/modules/4.8.12-yocto-standard
kernel modules.alias
modules.dep modules.symbols
The information in the module.* files is used by the modprobe command to locate a module by name rather than the full path. modprobe has many other features, which are described on the manual page modprobe(8) .
The next article in this series will describe how to discover the system's hardware configuration.