请选择 进入手机版 | 继续访问电脑版
[X]关闭

关于字符设备驱动

文档创建者:东吴
浏览次数:166
最后更新:2024-07-01
本帖最后由 东吴 于 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 函数对应。

字符设备驱动的看框架如下图所示:
微信图片_20240701131834.jpg

二、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)
参数
功能
dev
保存申请到的设备号
baseminor
次设备号起始地址
count
要申请的设备号数量
name
设备名字

注销设备后要释放掉设备号,释放函数如下:
1.void unregister_chrdev_region(dev_t from, unsigned count)

参数
功能
from
要释放的设备号
count
from开始,要释放的设备号数量

如下用常见的 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' 等方式发送控制命令。







您需要登录后才可以回帖 登录 | 立即注册

本版积分规则