• 正点原子嵌入式linux驱动开发——新字符设备驱动实验


    经过之前两篇笔记的实战操作,已经掌握了Linux字符设备驱动开发的基本步骤,字符设备驱动开发重点是使用register_chrdev函数注册字符设备,当不再使用设备的时候就使用unregister_chrdev函数注销字符设备,驱动模块加载成功以后还需要手动使用mknod命令创建设备节点。register_chrdev和unregister_chrdev这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用Linux内核推荐的新字符设备驱动 API函数。本节就学习一下如何编写新字符设备驱动,并且在驱动模块加载的时候自动创建设备节点文件

    新字符设备驱动原理

    使用register_chrdev函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来问题:需要实现确定主设备号的使用情况,且会将该主设备号下的所有此设备号都使用掉。

    解决这两个问题最好的方法就是在使用设备号的时候向Linux内核申请,需要几个就申请几个,由Linux内核分配设备可以使用的设备号。如果没有指定设备号就可以这样申请:

    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

    如果给定了主设备号和次设备号可以这样申请:

    int register_chrdev_region(dev_t from, unsigned count, const char *name)

    from就是申请的其实设备号,即给定设备号;count就是申请数量,name即设备名。

    注销字符设备后要释放设备号,可统一使用如下函数:

    void unregister_chrdev_region(dev_t from, unsigned count)

    可以如此来分配设备号:

    1 int major; /* 主设备号 */ 
    2 int minor; /* 次设备号 */ 
    3 dev_t devid; /* 设备号 */ 
    4 
    5 if (major) { /* 定义了主设备号 */ 
    6     devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择0 */ 
    7     register_chrdev_region(devid, 1, "test"); 
    8 } else { /* 没有定义设备号 */ 
    9     alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */ 
    10     major = MAJOR(devid); /* 获取分配号的主设备号 */ 
    11     minor = MINOR(devid); /* 获取分配号的次设备号 */ 
    12 }
    13 
    14 unregister_chrdev_region(devid, 1); /* 注销设备号 */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    新的字符设备注册方法

    字符设备结构

    可以在Linux中使用cdev结构体表示字符设备,定义在include/linux/cdev.h中:

    1 struct cdev { 
    2     struct kobject kobj; 
    3     struct module *owner; 
    4     const struct file_operations *ops; 
    5     struct list_head list; 
    6     dev_t dev; 
    7     unsigned int count;
    8 } __randomize_layout
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这其中,重要的是ops和dev,即字符设备文件操作函数集合file_operations以及设备号dev_t。使用如下:

    struct cdev test_cdev
    
    • 1

    cdev_init函数

    定义好cdev变量之后要调用cdev_init函数来初始化:

    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    
    • 1

    cdev就是要初始化的cdev结构体变量,fops就是字符设备文件操作函数集合。

    cdev_add函数

    用于像Linux系统添加字符设备。首先cdev初始化,之后使用cdev_add来向Linux系统添加,原型如下:

    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    
    • 1

    p指向要添加的字符设备(cdev结构体变量),dev就是设备号,count是设备数量。

    cdev_del函数

    卸载驱动就是调用这个函数从Linux内核删除相应字符设备,原型如下:

    void cdev_del(struct cdev *p)
    
    • 1

    p就是要删除的设备。

    cdev_del和unregister_chedev_region合起来的功能相当于unregister_chedev函数。

    自动创建设备节点

    在前面的Linux驱动实验中,使用modprobe加载驱动程序以后还需要使用命令“mknod”手动创建设备节点。本节讲解一下如何实现自动创建设备节点,在驱动中实现自动创建设备节点的功能以后,使用modprobe加载驱动模块成功的话就会自动在/dev目录下创建对应的设备文件

    mdev机制

    udev是一个用户程序,在Linux下通过udev来实现设备文件的创建与删除,udev可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。在使用buildroot构建根文件系统的时候选择了udev的简化版本mdev,所以在嵌入式Linux中用mdev来实现设备节点文件的自动创建与删除,Linux系统中的热插拔事件也由mdev管理,如果使用busybox构建根文件系统,会在/etc/init.d/rcS文件中如下语句:

    echo /sbin/mdev > /proc/sys/kernel/hotplug

    上述命令设置热插拔事件由mdev来管理。 buildroot构建的根文件系统已经全部处理好mdev了,不需要在修改什么文件。

    创建和删除类

    自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在cdev_add函数后面添加自动创建设备节点相关代码。首先要创建一个 class类,class是个结构体,定义在文件include/linux/device.h里面。class_create是类创建函数,class_create是个宏定义。最后将宏class_create展开后如下:

    struct class *class_create (struct module *owner, const char *name)

    owner一般为THIS_MODULE,name是类名字,返回值是指向结构体class的指针。

    卸载驱动程序也需要删除类,函数为class_destroy,原型如下:

    void class_destroy(struct class *cls);
    
    • 1

    cls就是要删除的类。

    创建设备

    创建好类之后,还需要在这个类下创建一个设备,使用函数device_create,原型如下:

    struct device *device_create(struct class *cls,
                                 struct device *parent, 
                                 dev_t devt, 
                                 void *drvdata, 
                                 const char *fmt, ...)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    device_create是个可变参数函数,cls就是设备要创建哪个类下面;parent是父设备,一般为NULL,也就是没有父设备;devt是设备号;drvdata是设备可能会使用的一些数据,一般为NULL;fmt是设备名字,如果设置fmt=xxx的话,就会生成 /dev/xxx这个设备文件,返回值就是创建好的设备。

    同样,卸载驱动的时候需要删除创建的设备,函数为device_destroy,原型如下:

    void device_destroy(struct class *cls, dev_t devt)
    
    • 1

    class是要删除设备所处类,devt是删除的设备号。

    参考示例

    struct class *class; /* 类 */ 
    struct device *device; /* 设备 */ 
    dev_t devid; /* 设备号 */ 
    
    /* 驱动入口函数 */ 
    static int __init xxx_init(void) 
    { 
    	/* 创建类 */ 
    	class = class_create(THIS_MODULE, "xxx"); 
    	/* 创建设备 */ 
    	device = device_create(class, NULL, devid, NULL, "xxx"); 
    	return 0; 1
    } 
    
    /* 驱动出口函数 */ 
    static void __exit led_exit(void) 
    {
    	/* 删除设备 */
    	device_destroy(newchrled.class, newchrled.devid); 
    	/* 删除类 */ 
    	class_destroy(newchrled.class); 
    } 
    	
    module_init(led_init); 
    module_exit(led_exit);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    设备私有数据

    每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式,但最好的做法试讲所有属性信息做成结构体。编写驱动open的时候将设备结构体作为私有数据添加到设备文件中。举例如下:

    /* 设备结构体 */ 
    struct test_dev{ 
    	dev_t devid; /* 设备号 */ 
    	struct cdev cdev; /* cdev */ 
    	struct class *class; /* 类 */ 
    	struct device *device; /* 设备 */ 
    	int major; /* 主设备号 */ 
    	int minor; /* 次设备号 */ 
    }; 
    
    struct test_dev testdev; 
    
    /* open函数 */ 
    static int test_open(struct inode *inode, struct file *filp) 
    { 
    	filp->private_data = &testdev; /* 设置私有数据 */ 
    	return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    设置好后,在write、read、close函数中直接读取private_data即可获得设备结构体。

    实验程序编写

    与之前LED的实验相比,重点就是使用了新的字符设备驱动,设置了文件四有数据,添加了自动创建设备节点相关内容。

    LED灯驱动程序编写

    区别点在于,申请好__iomem*的映射后虚拟地址指针,就申请一个newchrled_dev的设备结构体newchrdev;然后在led_open中设置私有数据private_data指向newchrdev;最后在led_init中申请设备号、添加字符设备、创建类和设备,并在led_exit中注销字符新设备、删除类和设备。

    编写测试APP

    直接使用LED实验的APP即可。

    运行测试

    编译驱动程序和测试APP

    把Makefile中obj-m的值改为newchrled.o即可,“make”之后就会有“newchrled.ko”驱动模块文件。

    ledAPP.c则通过下述命令编译:

    arm-none-linux-gnueabihf-gcc ledApp.c -o ledApp

    运行测试

    将两个程序拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板并进入到目录lib/modules/5.4.31中,输入如下命令加载newchrled.ko:

    depmod //第一次加载驱动的时候需要运行此命令
    modprobe newchrled //加载驱动

    加载成功后会输出申请到的主设备号和次设备号,如下图所示:
    申请到的设备号
    驱动加载成功后会自动在/dev目录下创建设备节点文件/dev/newchrdev,输入如下命令查看:

    ls /dev/newchrled -l

    驱动节点创建成功后就可以使用ledApp软件来测试,测试命令是一样的:

    ./ledApp /dev/newchrled 1 //打开 LED灯
    ./ledApp /dev/newchrled 0 //关闭 LED灯

    卸载也是一样:

    rmmod newchrled

    总结

    本篇还是在LED驱动的基础上,完成了自动创建驱动节点的代码编写,在led_open将私有数据private_data指向事先声明的结构体来管理设备文件;驱动open函数中添加类和设备。如此在加载好驱动之后,可以直接通过测试APP来测试,不需要自己设定驱动节点了

  • 相关阅读:
    kubectl常用命令
    localStorage封装代码
    JavaWeb 尚硅谷书城项目
    C++经典面试题:内存泄露是什么?如何排查?
    漫谈:C语言 C++ 迷惑的语句、分号、大括号
    修改mysql 数据表主键
    GPS北斗校时器(NTP网络校时服务器)医院应用方案
    在Qt中使用MySQL
    Linux Framebuffer驱动框架、接口实现和使用
    Spring Cloud Alibaba —— 高可用流量控制组件
  • 原文地址:https://blog.csdn.net/xhj12138/article/details/133856933