本帖最后由 东吴 于 2024-7-1 14:24 编辑
一、Linux 应用程序如何调用驱动程序
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备读写数据是分先后顺序的。比如我们最常见的点灯、按键、I2C、SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
在 Linux 中一切皆为文件,驱动加载成功以后会在"/dev”目录下生成一个相应的文件,应用程序通过对这个名为"/dev/xxx”的文件进行相应的操作即可实现对硬件的操作。比如现在有个叫做 /dev/ed 的驱动文件,此文件是led 灯的驱动文件。应用程序使用 open 函数来打开文件 /dev/led,使用完成以后使用 close 函数关闭 /dev/led 这个文件。open 和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作也就是向此驱动写入数据,这个数据就是要关闭还是要打开led 的控制参数。如果要获取 led 灯的状态,就用read 函数从驱动中读取相应的态。
应用程序运行在用户空间,而 Linux 驱动运行于内核空间。当我们在用户空间想要实现对内核的操作,比如使用open 函数打开 /dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做“系统调用”的方法来实现从用户空间“陷入“到内核空间,这样才能实现对底层驱动的操作。open、close、write 和 read 等这些函数是由 C库提供的,在 Linux 系统中,系统调用作为 C库的一部分。
其中关于 C库以及如何通过系统调用“陷入“到内核空间这个我们不用去管,我们重点关注的是应用程序和具体的驱动,应用程序使用到的函数在具体驱动程序中都有与之对应的函数,比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux内核文件 include/linux/fs.h 中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合内容。
如下所示:
1.struct file_operations {
2. struct module *owner;
3. loff_t (*llseek) (struct file *,loff_t,int);
4. ssize_t (*read) (struct file *, char __user *,size_t, loff_t *);
5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_ t *);
6. ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
7. ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
8. int (*iterate) (struct file *, struct dir_context *);
9. int (*iterate_shared) (struct file *, struct dir_context *);
10. unsigned int (*poll) (struct file *, struct poll_table_struct *);
11. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
12. long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
13. int (*mmap) (struct file *, struct vm_area_struct *);
14. int (*open) (struct inode *, struct file *);
15. int (*flush) (struct file *, fl_owner_t id);
16. int (*release) (struct inode *, struct file *);
17. int (*fsync) (struct file *, loff_t, loff_t, int datasync);
18. int (*fasync) (int, struct file *,int);
19. int (*lock) (struct file *, int, struct file_lock *);
简单介绍-下 file_operation 结构体中比较重要的、常用的函数:
(1)owner 拥有该结构体的模块的指针,一般设置为 THIS MODULE。
(2)llseek 函数用于修改文件当前的读写位置。
(3)read 函数用于读取设备文件。
(4)write 函数用于向设备文件写入(发送)数据。
(5)poll 是个轮询函数,用于查询设备是否可以进行非阻塞的读写。
(6)unlocked_ioctl 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应。
(7)compat ioctl 函数与 unlocked_ioctl 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是 unlocked_ioctl。
(8)mmap 函数用于将设备的内存映射到进程空间中(也就是用户空间),一般帧缓冲设备会使用此函数,比如 LCD 驱动的显存,将帧缓冲(LCD 显存)映射到用户空间中以后应用程序就可以直接操作显存了,这样就不用在用户空间和内核空间之间来回复制。
(9)open 函数用于打开设备文件。
(10)release 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
字符设备驱动的看框架如下图所示:
二、Linux设备号
1、设备号的组成
Linux 里每个设备都有一个设备号,设备号由主设备号和次设备号组成,主设备号代表一个驱动,次设备号对应单个驱动中的各个设备。
设备号是一个 32 位(unsigned int)数据,主设备为高 12位,次设备为低 20 位,所以主设备号的范围为 0 ~ 4095(0~212)。
2、设备号的分配
在注册设备的时候需要给设备分配一个设备号,设备号的分配有两种方式,静态分配和动态分配。
静态设备号分配:设备号的分配由驱动开发者静态指定设备号,分配可能产生设备号冲突。
动态设备号分配:设备号的分配由系统自动分配一个没有被使用的设备号,避免了设备号冲突。
推荐使用动态设备号分配,分配函数如下:
1.int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
注销设备后要释放掉设备号,释放函数如下:
1.void unregister_chrdev_region(dev_t from, unsigned count)
如下用常见的 open、clease、read 和 write 写一个简单的示例:(模板,不包含头文件)1./* 打开设备 */
2.static int test_open(struct inode *inode, struct file * filp)
3.{
4. printk("Chrdev was opened.\n");
5. return 0;
6.}
7./* 读设备 */
8.static ssize_t test_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
9.{
40. return 0;
11.}
12./* 写设备 */
13.static ssize_t test_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
14.{
15. return 0;
16.}
17./* 释放设备 */
18.static int test_release(struct inode *inode, struct file *filp)
19.{
20. printk("Chrdev was closed.\n");
21. return 0;
22.}
23.// 定义文件操作结构体变量
24.static struct file_operations test_fops = {
25. .owner = THIS_MODULE,
26. .open = test_open,
27. .read = test_read,
28. .write = test_write,
29. .release = test_release,
30.};
31./* 驱动入口函数 */
32.static int __init test_init(void)
33.{
34. int ret = 0;
35. /* 注册字符设备驱动 */
36. ret = register_chrdev(100, "chrdev_test", &test_fops);
37. if(ret < 0)
38. {
39. printk("Chrdev register failed.\n");
40. }
41. printk("Driver installed\n");
42. return 0;
43.}
44./* 驱动出口函数 */
45.static void __exit test_exit(void)
46.{
47. /* 注销字符设备驱动 */
48. unregister_chrdev(100, "chrdev_test");
49. printk("Driver uninstalled\n");
50.}
51./* 指定入口函数和出口函数 */
52.module_init(test_init);
53.module_exit(test_exit);
三、字符设备注册与注销
1、注册函数
函数原型:
1. #include <linux/fs.h>
2. static inline int register_chrdev(unsigned int major, const char *name,
3. const struct file_operations *fops)
功能:注册一个字符设备
参数:
major:主设备号(填 0 时,会自动分配)
name:设备名
fops:文件操作方法结构体指针
返回值:成功返回分配的主设备号,失败返回负数。
2、注销函数
函数原型:
1 .#include <linux/fs.h>
2 .static inline void unregister_chrdev(unsigned int major, const char *name)
功能:注销一个已经存在字符设备
参数:
major: 主设备号
name: 设备名
四、注册一个字符类设备的步骤
1、定义一个cdev结构体
2、使用cdev_init函数初始化cdev结构体成员变量
void cdev_init(struct cdev *, const struct file_operations *);
参数一:要初始化的cdev
参数二:文件操作集
cdev->ops = fops;//实际就是把文件操作集写给ops
3、使用cdev_add函数注册到内核
int cdev_add(struct cdev *, dev_t, unsigned);
参数一:cdev的结构体指针
参数二:设备号
参数三:次设备号的数量
4、注销字符类设备
void cdev_del(struct cdev *);
五、示例函数
1.#include <linux/module.h>
2.#include <linux/fs.h>
3.#include <linux/cdev.h>
4.#include <linux/uaccess.h>
5.#define DEVICE_NAME "mydevice"
6.#define BUF_SIZE 1024
7.static dev_t dev;
8.static struct cdev cdev;
9.static char buffer[BUF_SIZE];
10.static int buffer_len = 0;
11.static int device_open(struct inode *inode, struct file *filp)
12.{
13. // 设备打开时的操作
14. return 0;
15.}
16.tatic int device_release(struct inode *inode, struct file *filp)
17.{
18. // 设备关闭时的操作
19. return 0;
20.}
21.static ssize_t device_read(struct file *filp, char *user_buf, size_t count, loff_t *f_pos)
22.{
23. // 从设备读取数据
24. size_t to_copy = min(count, (size_t)buffer_len);
25. if (copy_to_user(user_buf, buffer, to_copy) != 0)
26. return -EFAULT;
27. return to_copy;
28.}
29.static ssize_t device_write(struct file *filp, const char *user_buf, size_t count, loff_t *f_pos)
30.{
31. // 向设备写入数据
32. size_t to_copy = min(count, (size_t)BUF_SIZE);
33. if (copy_from_user(buffer, user_buf, to_copy) != 0)
34. return -EFAULT;
35. buffer_len = to_copy;
36. return to_copy;
37.}
38.static struct file_operations fops = {
39. .owner = THIS_MODULE,
40. .open = device_open,
41. .release = device_release,
42. .read = device_read,
43. .write = device_write,
44.};
45.static int __init chardev_init(void)
46.{
47. // 模块初始化函数
48. if (alloc_chrdev_region(&dev, 0, 1, DEVICE_NAME) < 0)
49. {
50. printk(KERN_ALERT "Failed to allocate character device region\n");
51. return -1;
52. }
53. cdev_init(&cdev, &fops);
54. if (cdev_add(&cdev, dev, 1) < 0)
55. {
56. unregister_chrdev_region(dev, 1);
57. printk(KERN_ALERT "Failed to add character device\n");
58. return -1;
59. }
60. printk(KERN_INFO "Character device registered: %s\n", DEVICE_NAME);
61. return 0;
62.}
63.static void __exit chardev_exit(void)
64.{
65. // 模块退出函数
66. cdev_del(&cdev);
67. unregister_chrdev_region(dev, 1);
68. printk(KERN_INFO "Character device unregistered\n");
69.}
70.module_init(chardev_init);
71.module_exit(chardev_exit);
72.MODULE_LICENSE("GPL");
六、字符设备驱动开发总结
字符驱动是一种在操作系统内核中实现的设备驱动程序,用于与字符设备进行交互。字符设备是一种以字节为单位进行输入和输出的设备,例如终端、串口、打印机等。字符驱动框架提供了一组函数和数据结构,使得开发者可以编写自定义的字符设备驱动程序。
在Linux内核中,字符驱动的实现基于以下几个核心组件:
1.设备号:每个字符设备驱动在注册时都会被分配一个唯一的设备号。设备号包括主设备号和次设备号。主设备号标识设备驱动程序,次设备号标识具体的设备实例。
2.file_operations结构体:这是一个函数指针结构体,定义了设备驱动程序对外提供的操作接口。常见的函数包括'open'、'release'、'read'、'write'、'ioctl' 等。开发者需要实现这些函数来处理设备的打开、关闭、读取和写入等操作。
3.cdev 结构体:'cdev' 是字符设备驱动的核心结构体,它代表一个字符设备实例。它包含了对应的 file_operations 结构体指针以及设备号等信息。通过使用 'cdev' 结构体,开发者可以注册和管理字符设备。
4.字符设备注册和注销:在字符驱动的初始化阶段,需要使用 'alloc_chrdev_region' 函数为驱动程序分配设备号。然后使用 'cdev_init' 初始化 'cdev' 结构体,并使用 'cdev_add' 将设备添加到系统中。在驱动程序退出时,需要使用 'cdev_del' 和 'unregister_chrdev_region' 函数来注销设备。
5.用户空间交互:字符驱动允许用户空间程序通过系统调用来访问设备。用户程序可以打开设备、读取和写入设备数据,并通过 'ioctl' 等方式发送控制命令。
|
|