![嵌入式Linux设备驱动程序开发指南(原书第2版)](https://wfqqreader-1252317822.image.myqcloud.com/cover/453/40381453/b_40381453.jpg)
4.1 实验4-1:“helloworld字符设备”模块
传统上,Linux系统一般使用静态设备创建方式。不管对应的物理设备是否存在,/dev
目录下创建了大量的设备节点(有时多达上千个)。通常这项工作由MAKEDEV脚本完成。对于可能存在的每一个设备,该脚本根据对应的主从设备号调用mknod
程序来创建设备节点。
如今,这并不是创建设备节点的正确方式。因为你必须手动创建块设备文件或字符设备文件,并将这些文件与设备关联,如下面i.MX7D开发板的终端命令行所示:
![077-01](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/077-01.jpg?sign=1739296281-aGsShrwoarnwttoxAHmwndvXi4dGsraU-0-a6bd66fa24dd81a9d05f0f2ea432f8c0)
尽管如此,出于教学目的,你开发的下一个驱动将简单使用这种静态方式。在随后的几个驱动中你将看到创建设备节点的更佳方式:使用设备文件系统和杂项框架。
在本章的内核模块实验中,你将通过ioctl_test
这个应用程序来和用户态打交道。你的应用程序将使用open()
和ioctl()
这两个系统调用。在内核部分你需要开发相应驱动的回调操作。这样的设计为用户态和内核态提供了交互能力。
在第一个实验中,你将看到一个原始的helloworld
驱动。这个驱动仅在安装和移除时打印一些文本信息。在下一个实验中,你将对这个驱动进行扩展,使用一个主设备号和一个从设备号创建一个设备。你还会创建一个应用程序来和驱动进行交互。最后,你将在驱动中处理文件操作以满足来自用户态的请求。
在内核中,cdev
数据结构用来表示一个字符类型的设备,该数据结构用于将设备注册到系统中。
字符设备的注册与注销
字符设备的注册/注销是通过指定主从设备号来实现的。dev_t
类型用于保存设备的标识信息(主设备号和从设备号),该信息可以通过MKDEV宏获取。
register_chrdev_region()
和unregister_chrdev_region()
分别负责一组设备标识的静态分配和释放。第一个设备标识则通过MKDEV宏获取。
![078-01](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/078-01.jpg?sign=1739296281-aB10olxvfzh9qlU8QmhKLGIGrh7TDg5W-0-6a99bffd8026ac889604fd892efdeafd)
推荐使用alloc_chrdev_region()
函数动态地分配设备标识。该函数分配一组字符设备编号。主设备号是动态选择的并通过dev
参数返回(同时包含第一个从设备号)。这个函数返回0或者负的错误码。
![078-02](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/078-02.jpg?sign=1739296281-feQjcpXqnpOZXcJEHF2zlk52S4ooMmpz-0-7a9f0a2ab559431519285dd2b162fe56)
下面是函数参数的具体描述:
dev
:输出参数,存放分配的第一个设备编号。baseminor
:起始从设备编号。count
:请求从设备编号的数量。name
:关联的设备或驱动的名称。
在下面的这行代码中,第二个参数my_minor_count
标明了要分配的设备个数。起始设备的主设备编号是my_major
,从设备编号是my_first_minor
。register_chrdev_region()
函数的第一个参数是起始设备标识。后续的设备标识可以通过MKDEV宏获取。
![078-03](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/078-03.jpg?sign=1739296281-bzWThrtBpFakWgPkeYQDOeMQtJbyNpT5-0-8b5de0d050fe53977dfd507b3ae371be)
分配完设备标识之后,通过cdev_init()
函数初始化字符设备并由cdev_add()
函数注册到内核。分配的设备标识的数量决定了这两个函数被调用的次数。
下面的一系列代码注册并初始化MY_MAX_MINORS个设备:
![078-04](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/078-04.jpg?sign=1739296281-qu8Sbhe79W1ggZJbaKmZeDDjD7vJ4Vqa-0-84a506479f0fc4e972c2fd9f7d7878fc)
接下来的这些代码删除并注销这些字符设备:
![079-01](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/079-01.jpg?sign=1739296281-DeFO7fm51mzzalpEbBsXmy3nts4BtHyK-0-d6ceda8b16b0bf477eb61f0862540a78)
新驱动的主要代码段描述如下:
1. 包含支持字符设备的头文件:
![079-02](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/079-02.jpg?sign=1739296281-N7jaHR2i8EZ9gi71YyoJxlpEmVZnS4fa-0-8d866ba6da431ac3bd0744e262193660)
2. 定义主设备号:
![079-03](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/079-03.jpg?sign=1739296281-pMmjLVu3IR298DaLdy5C5vbACyp3iSil-0-5880fbadd4fa4679401ba4ef1ef91900)
3. 设置字符设备时,驱动首先要做的就是获取一个或者多个设备标识。完成这项任务需要的必备函数就是register_chrdev_region()
,该函数定义在include/linux/fs.h
。把下面的这几行代码添加到hello_init()
函数中,当模块加载的时候这些代码负责分配设备编号。MKDEV使用一个主设备编号和一个从设备编号组合出一个dev_t
数据类型,该数据类型用于存放第一个设备标识。
![079-04](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/079-04.jpg?sign=1739296281-ZOBsIAhDEMNoNj8tyoloOdbIioEISnpZ-0-38aea6fb8848f1fe449aa2c6e5bb5633)
4. 把下面的这行代码添加到hello_exit()
函数,当模块移除时设备编号也会被回收。
![080-01](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/080-01.jpg?sign=1739296281-XHx7iN92ec93mcYOIu8YLDsQnITxobAW-0-8ff6673c9c00d7f6f098823c5978cf07)
5. 创建一个名为my_dev_fops
的file_operations
数据结构。这个数据结构定义了打开、读取、写入设备等操作的函数指针。
![080-02](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/080-02.jpg?sign=1739296281-dMjyzzhMyWgeRuanJF2KxDcGglQJXbPU-0-25b795cd422bf6fec7c06035a06ab765)
6. 实现定义在file_operations
数据结构中的各回调函数:
![080-03](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/080-03.jpg?sign=1739296281-QgGBxOLPZfn4vCW6ZkSzDcKe74nlJf8s-0-bda92bdfd102f6edaec3cfef7c970681)
7. 把这些文件操作功能添加到你的字符设备中。内核内部使用一个叫作cdev
的数据结构来描述字符设备。因此,你创建一个名为my_dev
的cdev
数据结构变量并使用cdev_init()
函数将其初始化。cdev_init()
函数使用my_dev
变量和名为my_dev_fops
的file_operations
数据结构作为参数。一旦cdev
数据结构设置完成,调用cdev_add()
函数通知内核。分配的字符设备标识数量决定了你调用这两个函数的次数(这个驱动里面只调用了一次)。
![080-04](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/080-04.jpg?sign=1739296281-DUFNjeKRIJYoNBoyvBzm0sy15BjKTJqE-0-69e221aee5aea60591ec17332259b4f2)
8. 添加下面这行代码到hello_exit()
函数以删除cdev
数据结构。
![080-05](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/080-05.jpg?sign=1739296281-PW6RyW1Z6tKUgQUwZVyfLjyZbvD3QfEw-0-41e4853688cf46dda69e259d58902996)
9. 一旦内核模块被动态加载,用户需要创建一个设备节点来引用对应的驱动。Linux提供了mknod
工具来完成该任务。mknod
命令有4个参数。第一个参数是将被创建的设备节点名称。第二个参数用来区分与设备节点交互的驱动是块设备驱动还是字符设备驱动。mknod
的最后两个参数就是主从设备号。/proc/devices
文件中列出了所有分配的主设备编号,通过cat
命令可以查看这些设备编号。创建的设备节点会被放在/dev
目录下。
![081-01](https://epubservercos.yuewen.com/D1DB85/20966230701867406/epubprivate/OEBPS/Images/081-01.jpg?sign=1739296281-d0IFmEzBdvx9OF9KmRTi3DDirDaukuMQ-0-c67b3da1d874240926113f1187fe35be)
在接下来的代码清单4-1中,展示了针对i.MX7D处理器的“helloworld字符设备”驱动源代码(helloworld_imx_char_driver.c
)。
注意:针对SAMA5D2(helloworld_sam_char_driver.c
)和BCM2837(helloworld_rpi_char_driver.c
)的驱动源代码可以从本书的GitHub仓库下载。