精华内容
下载资源
问答
  • 虚拟字符设备驱动编写

    这是基于老罗的书《Android系统源代码情景分析》一书中关于虚拟字符设备驱动的来写的
    以下是freg.h中的代码

    #ifndef _FAKE_REG_H_
    #define _FAKE_REG_H_
    
    #include <linux/cdev.h>         //支持字符设备的头文件
    #include <linux/semaphore.h>    //支持信号量的头文件
    
    #define FREG_DEVICE_NODE_NAME "freg"
    #define FREG_DEVICE_FILE_NAME "freg"
    #define FREG_DEVICE_PROC_NAME "freg"
    #define FREG_DEVICE_CLASS_NAME "freg"
    
    struct fake_reg_dev {
        int val;
        struct semaphore sem;
        struct cdev dev;
    };
    
    #endif

    以下是freg.c中的代码

    #include <linux/init.h>         //用于标记函数的宏,如__init,__exit
    #include <linux/module.h>       //将内核模块加载到内核中的核心头文件,个人认为module_init,module_exit与之相关
    #include <linux/types.h>
    #include <linux/fs.h>           //支持linux文件系统的文件
    #include <linux/proc_fs.h>
    #include <linux/device.h>       //支持内核驱动模型的头文件
    #include <asm/uaccess.h>        //复制用户用户空间函数需要的头文件
    
    #include "freg.h"
    
    static int freg_major = 0;  //主设备号
    static int freg_minor = 0;  //从设备号
    
    static struct class* freg_class = NULL; //设备类别
    static struct fake_reg_dev* freg_dev = NULL;    //设备
    
    //传统设备文件操作方法
    static int freg_open(struct inode* inode, struct file* filp);
    static int freg_release(struct inode* inode, struct file* filp);
    static ssize_t freg_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos);
    static ssize_t freg_write(struct file* filp,const char __user *buf,size_t count,loff_t* f_pos);
    
    // /dev/freg设备节点的操作方法
    static struct file_operations freg_fops = {
        .owner = THIS_MODULE,
        .open = freg_open,
        .release = freg_release,
        .read = freg_read,
        .write = freg_write,
    };
    
    /*devfs文件系统的设备属性操作方法*/
    static ssize_t freg_val_show(struct device* dev,struct device_attribute* attr, char* buf);
    static ssize_t freg_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count);
    
    /*devfs文件系统的设备属性*/
    static DEVICE_ATTR(val,S_IRUGO | S_IWUSR,freg_val_show,freg_val_store);
    
    
    
    /*打开设备方法*/
    static int freg_open(struct inode* inode, struct file* filp){
        printk(KERN_ALERT"freg device open.\n");
        struct fake_reg_dev* dev;
    
        /*将自定义设备结构题保存在文件指针的私有数据域中,以便访问设备时可以直接拿来用*/
        dev = container_of(inode->i_cdev,struct fake_reg_dev, dev);
        filp->private_data = dev;
    
        return 0;
    }
    
    /*设备文件释放时调用,空实现*/
    static int freg_release(struct inode* inode, struct file* filp){
        printk(KERN_ALERT"freg device release.\n");
        return 0;
    }
    
    
    /*读取设备的寄存器val的值*/
    static ssize_t freg_read(struct file* filp, char __user *buf, size_t count, loff_t* f_pos){
        printk(KERN_ALERT"freg device read.\n");
        ssize_t err = 0;
        struct fake_reg_dev* dev = filp->private_data;
    
        /*同步访问*/
        if(down_interruptible(&(dev->sem))){
            return -ERESTARTSYS;
        }
    
        if(count < sizeof(dev->val)){
            goto out;
        }
    
        /*将寄存器val的值拷贝到用户提供的缓存区中*/
        if(copy_to_user(buf,&(dev->val),sizeof(dev->val))){
            err = -EFAULT;
            goto out;
        }
        err = sizeof(dev->val);
    
    out:
        up(&(dev->sem));
        return err;
    }
    
    
    /*写设备的寄存器val的值*/
    static ssize_t freg_write(struct file* filp,const char __user *buf,size_t count,loff_t* f_pos){
        printk(KERN_ALERT"freg device write.\n");
        struct fake_reg_dev* dev = filp->private_data;
        ssize_t err = 0;
    
        /*同步访问*/
        if(down_interruptible(&(dev->sem))){
            return -ERESTARTSYS;
        }
    
        if(count != sizeof(dev->val)){
            goto out;
        }
        if(copy_from_user(&(dev->val),buf,count)){
            err = -EFAULT;
            goto out;
        }
    
        err = sizeof(dev->val);
    out:
        up(&(dev->sem));
        return err;
    }
    
    
    /*将寄存器val的值读取到缓存区buf中,内部使用*/
    static ssize_t __freg_get_val(struct fake_reg_dev* dev, char* buf){
        int val = 0;
    
        /*同步访问*/
        if(down_interruptible(&(dev->sem))){
            return -ERESTARTSYS;
        }
        val = dev->val;
        up(&(dev->sem));
    
        return snprintf(buf,PAGE_SIZE,"%d\n",val);
    }
    
    /*把缓存区buf的值写到设备寄存器val中,内部使用*/
    static ssize_t __freg_set_val(struct fake_reg_dev* dev,const char* buf, size_t count){
        int val = 0;
    
        /*将字符串转换成数字*/
        val = simple_strtol(buf,NULL,10);
    
        /*同步访问*/
        if(down_interruptible(&(dev->sem))){
            return -ERESTARTSYS;
        }
        dev->val = val;
        up(&(dev->sem));
    
        return count;
    }
    //读取/sys/class/freg/freg/val的方法
    static ssize_t freg_val_show(struct device* dev,struct device_attribute* attr, char* buf){
        struct fake_reg_dev* hdev = (struct fake_reg_dev*) dev_get_drvdata(dev);
    
        printk(KERN_ALERT"get /sys/class/freg/freg/val.\n");
        return __freg_get_val(hdev,buf);
    }
    
    
    
    //将值写入/sys/class/freg/freg/val的方法
    static ssize_t freg_val_store(struct device* dev, struct device_attribute* attr, const char* buf, size_t count){
        struct fake_reg_dev* hdev = (struct fake_reg_dev*) dev_get_drvdata(dev);
    
        printk(KERN_ALERT"set /sys/class/freg/freg/val.\n");
        return __freg_set_val(hdev,buf,count);
    }
    
    
    /*读取设备寄存器val的值,保存到page缓存区中*/
    static ssize_t freg_proc_read(char* page, char** start, off_t off,int count,int* eof, void* data){
        printk(KERN_ALERT"freg_proc_read.\n");
        if(off>0){
            *eof = 1;
            return 0;
        }
        return __freg_get_val(freg_dev,page);
    }
    
    /*把缓存区的值buff保存到设备寄存器val中*/
    static ssize_t freg_proc_write(struct file* filp, const char __user *buff, unsigned long len, void* data){
        printk(KERN_ALERT"freg_proc_write.\n");
        int err = 0;
        char* page = NULL;
    
        if(len >PAGE_SIZE){
            printk(KERN_ALERT"The buff is too large: %lu. \n",len);
            return -EFAULT;
        }
    
        page = (char*) __get_free_page(GFP_KERNEL);
        if(!page) {
            printk(KERN_ALERT"Failed to alloc page .\n");
            return -ENOMEM;
        }
        /*先把用户提供的缓冲区的值拷贝到内核缓冲区中*/
        if(copy_from_user(page,buff,len)){
            printk(KERN_ALERT"Failed to copy buff from user. \n");
            err = -EFAULT;
            goto out;
        }
    
        err = __freg_set_val(freg_dev,page,len);
    
    out:
        free_page((unsigned long)page);
        return err;
    }
    
    /*创建/proc/freg文件*/
    //以及对应/proc/freg读写方法
    static void freg_create_proc(void) {
        printk(KERN_ALERT"freg_create_proc.\n");
        struct proc_dir_entry* entry;
    
        entry = create_proc_entry(FREG_DEVICE_PROC_NAME,0,NULL);
        if(entry){
            //entry->owner = THIS_MODULE;   //结构体中没有owner这个成员
            entry->read_proc = freg_proc_read;
            entry->write_proc = freg_proc_write;
        }
    }
    
    /*删除/proc/freg文件*/
    static void freg_remove_proc(void){
        printk(KERN_ALERT"freg_remove_proc.\n");
        remove_proc_entry(FREG_DEVICE_PROC_NAME,NULL);
    }
    
    /*初始化设备*/
    static int __freg_setup_dev(struct fake_reg_dev* dev){
        int err;
        dev_t devno = MKDEV(freg_major,freg_minor);
    
        memset(dev,0,sizeof(struct fake_reg_dev));
    
        /*初始化字符设备*/
        cdev_init(&(dev->dev),&freg_fops);
        dev->dev.owner = THIS_MODULE;
        dev->dev.ops = &freg_fops;
    
        /*注册字符设备*/
        err = cdev_add(&(dev->dev),devno,1);
        if(err){
            return err;
        }
    
        /*初始化信号量和寄存器val的值*/
        //init_MUTEX(&(dev->sem));  //新版本linux中初始化信号量没有这个函数,用下面这个函数替代
        sema_init(&(dev->sem),1);
        dev->val = 0;
    
        return 0;
    }
    
    /*模块加载方法*/
    static int __init freg_init(void){
        int err = -1;
        dev_t dev = 0;
        struct device* temp = NULL;
    
        printk(KERN_ALERT"Initializing freg device .\n");
    
        /*动态分配主设备号和从设备号*/
        err = alloc_chrdev_region(&dev,0,1,FREG_DEVICE_NODE_NAME);
        if(err < 0){
            printk(KERN_ALERT"Failed to alloc char dev region.\n");
            goto fail;
        }
        //将动态分配到的设备的主设备号和从设备号保存
        freg_major = MAJOR(dev);
        freg_minor = MINOR(dev);
    
        /*分配freg设备结构体*/
        freg_dev = kmalloc(sizeof(struct fake_reg_dev),GFP_KERNEL);
        if(!freg_dev){
            err = -ENOMEM;
            printk(KERN_ALERT"Failed to alloc freg device. \n");
            goto unregister;
        }
    
        /*初始化设备*/
        err = __freg_setup_dev(freg_dev);
        if(err) {
            printk(KERN_ALERT"Failed to setup freg device:%d .\n",err);
            goto cleanup;
        }
    
        /*在/sys/class/目录下创建设备类别目录freg*/
        freg_class = class_create(THIS_MODULE,FREG_DEVICE_CLASS_NAME);
        if(IS_ERR(freg_class)){
            err = PTR_ERR(freg_class);
            printk(KERN_ALERT"Failed to create freg device class.\n");
            goto destroy_cdev;
        }
    
        /*在/dev/目录和/sys/class/freg目录下分别创建设备文件freg*/
        temp = device_create(freg_class,NULL,dev,NULL,"%s",FREG_DEVICE_FILE_NAME);
        if(IS_ERR(temp)){
            err = PTR_ERR(temp);
            printk(KERN_ALERT"Failed to create freg device.\n");
            goto destroy_class;
        }
        /*在/sys/class/freg/freg目录下创建属性文件val*/
        err = device_create_file(temp,&dev_attr_val);
        if(err < 0){
            printk(KERN_ALERT"Failed to create attribute val of freg device.\n");
            goto destroy_device;
        }
    
        //  static inline void *dev_get_drvdata(const struct device *dev)
        //  {
        //      return dev->driver_data;
        //  }
        //
        //  static inline void dev_set_drvdata(struct device *dev, void *data)
        //  {
        //      dev->driver_data = data;
        //  }
        //  相当于将freg_dev这个指针保存到temp中
        dev_set_drvdata(temp, freg_dev);
    
        /*创建/proc/freg文件*/
        freg_create_proc();
    
        printk(KERN_ALERT"Succedded to initialize freg device.\n");
    
        return 0;
    
    destroy_device:
        device_destroy(freg_class,dev);
    destroy_class:
        class_destroy(freg_class);
    destroy_cdev:
        cdev_del(&(freg_dev->dev));
    cleanup:
        kfree(freg_dev);
    unregister:
        unregister_chrdev_region(MKDEV(freg_major,freg_minor),1);
    fail:
        return err;
    }
    
    
    
    /*模块卸载方法*/
    static void __exit freg_exit(void){
        printk(KERN_ALERT"Destroy freg device.\n");
        dev_t devno = MKDEV(freg_major, freg_minor);
    
    
        /*删除/proc/freg文件*/
        freg_remove_proc();
    
        /*销毁设备类别和设备*/
        if(freg_class){
            device_destroy(freg_class,MKDEV(freg_major,freg_minor));
            class_destroy(freg_class);
        }
    
        /*删除字符设备和释放设备内存*/
        if(freg_dev){
            cdev_del(&(freg_dev->dev));
            kfree(freg_dev);
        }
        /*释放设备号资源*/
        unregister_chrdev_region(devno,1);
    }
    
    MODULE_LICENSE("GPL");
    MODULE_DESCRIPTION("Fake Register Driver");
    
    module_init(freg_init);
    module_exit(freg_exit);
    
    

    由于我和老罗的内核版本不一样所以修改了一点他的代码,完成来驱动的编译。经过测试驱动编写成功。代码的注释应该足够看懂了。
    ll /dev/freg可以查看设备类型和主从设备号
    /proc 为运行的内核信息映射
    /sys 硬件设备的驱动程序信息
    本程序可以通过/sys/class/freg/freg/val和/proc/freg来读写寄存器val的值

    展开全文
  • 主要介绍了linux 驱动编写虚拟字符设备编写实例详解的相关资料,需要的朋友可以参考下
  • linux驱动编写虚拟字符设备编写

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

    也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

                   


    【 声明:版权所有,欢迎转载,请勿用于商业用途。  联系信箱:feixiaoxing @163.com】


        昨天我们说了一些简单模块编写方法,但是终归没有涉及到设备的编写内容,今天我们就可以了解一下相关方面的内容,并且用一个实例来说明在linux上面设备是如何编写的。虽然我不是专门做linux驱动的,却也经常收到一些朋友们的来信。在信件中,很多做驱动的朋友对自己的工作不是很满意,认为自己的工作就是把代码拷贝来拷贝去,或者说是改来改去,没有什么技术含量。有这种想法的朋友不在少数,我想这主要还是因为他们对自己的工作缺少了解导致。如果有可能,我们可以问问自己这样几个问题:


        (1)我真的搞懂设备的开发驱动流程了吗?我是否可以从0开始,编写一个独立的驱动代码呢?

        (2)我真的了解设备的初始化、关闭、运行的流程吗?

        (3)当前的设备驱动流程是否合理,有没有可以改进的地方?

        (4)对于内核开发中涉及的api调用,我自己是否真正了解、是否明白它们在使用上有什么区别?

        (5)如果我要驱动的设备只是在一个前后台系统中运行,在没有框架帮助的情况下,我是否有信心把它启动和运行起来?


        当然,上面的内容只是我个人的想法,也不一定都正确。但是,知其然,更要知其所以然,熟悉了当前开发流程的优缺点才能真正掌握和了解驱动开发的本质。这听上去有些玄乎,其实也很简单,就是要有一种刨根问底、不断改进的精神,这样才能做好自己的工作。因为我们是在pc linux上学习驱动的,因此暂时没有真实的外接设备可以使用,但是这丝毫不影响我们学习的热情。通过定时器、进程,我们可以仿真出真实设备的各种需求,所以对于系统来说,它是无所谓真设备、假设备的,基本的处理流程对它来说都是一样的。只要大家一步一步做下去,肯定可以了解linux驱动设备的开发工程的。


        下面,为了说明问题,我们可以编写一段简单的char设备驱动代码,文件名为char.c,

    #include <linux/module.h>#include <linux/kernel.h>#include <linux/fs.h>#include <linux/cdev.h>static struct cdev chr_dev;static dev_t ndev;static int chr_open(struct inode* nd, struct file* filp)int major ; int minor;  major = MAJOR(nd->i_rdev); minor = MINOR(nd->i_rdev);  printk("chr_open, major = %d, minor = %d\n", major, minor); return 0;}static ssize_t chr_read(struct file* filp, char __user* u, size_t sz, loff_t* off){ printk("chr_read process!\n"); return 0;}struct file_operations chr_ops = { .owner = THIS_MODULE, .open = chr_open, .read = chr_read};static int demo_init(void)int ret;  cdev_init(&chr_dev, &chr_ops); ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev"); if(ret < 0 ) {  return ret; }  printk("demo_init(): major = %d, minor = %d\n", MAJOR(ndev), MINOR(ndev)); ret = cdev_add(&chr_dev, ndev, 1); if(ret < 0) {  return ret; }  return 0;}static void demo_exit(void){ printk("demo_exit process!\n"); cdev_del(&chr_dev); unregister_chrdev_region(ndev, 1);}module_init(demo_init);module_exit(demo_exit);MODULE_LICENSE("GPL");MODULE_AUTHOR("feixiaoxing@163.com");MODULE_DESCRIPTION("A simple device example!");
        在module_init中的函数是模块加载时处理的函数,而模块卸载的函数则是在module_exit中。每一个设备都要对应一个基本的设备数据,当然为了使得这个设备注册在整个系统当中,我们还需要分配一个设备节点,alloc_chrdev_region就完成这样一个功能。等到cdev_add的时候,整个设备注册的过程就全部完成了,就是这么简单。当然为了编写这个文件,我们还需要编写一个Makefile文件,
    ifneq ($(KERNELRELEASE),)obj-m := char.oelsePWD  := $(shell pwd)KVER := $(shell uname -r)KDIR := /lib/modules/$(KVER)/buildall: $(MAKE) -C $(KDIR) M=$(PWD) modulesclean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions modules.*  Module.*endif
        这个Makefile文件和我们之前编写的makefile基本上没有区别,唯一的区别就是文件名称改成了char.o,仅此而已。为了编写模块,我们直接输入make即可。这时候,char.ko文件就可以生成了。然后,模块需要被注册在系统当中,insmod char.ko是少不了的。如果此时,我们还不确信是否模块已经加入到系统当中,完全可以通过输入lsmod | grep char进行查找和验证。为了创建设备节点,我们需要知道设备为我们创建的major、minor数值是多少,所以dmesg | tail 查找一下数值。在我hp的机器上,这两个数值分别是249和0,所以下面可以利用它们直接创建设备节点了,输入mknod /dev/chr_dev c 249 0即可,此时可以输入ls /dev/chr_dev验证一下。那么,按照这种方法,真的可以访问这个虚拟设备了吗,我们可以编写一段简单的代码验证一下,
    #include <stdio.h>#include <fcntl.h>#include <unistd.h>#define CHAR_DEV_NAME "/dev/chr_dev"int main()int ret; int fd; char buf[32]; fd = open(CHAR_DEV_NAME, O_RDONLY | O_NDELAY); if(fd < 0) {  printf("open failed!\n");  return -1; }  read(fd, buf, 32); close(fd);  return 0;}

        代码的内容非常简单,就是利用CHAR_DEV_NAME直接打开设备,读写设备。当然。首先还是需要对这个文件进行编译,文件名为test.c,输入gcc test.c -o test,其次就是运行这个文件,直接输入./test即可。如果没有问题的话,那么说明我们的代码是ok的,但是我们还是没有看到任何内容。没关系,我们还是通过dmesg这个命令查看内核中是否存在相关的打印内容,直接输入dmesg | tail即可。此时如果没有意外的话,我们就可以看到之前在chr_open和chr_read中留下的printk打印,这说明我们的代码完全是ok的。


        上面的代码只是一段小例子,真实的内容要比这复杂一下。不过既然我们都已经入门了,那么后面的内容其实也没有什么好怕的了。最后有两个事情补充一下:(1)如果大家在创建节点后想删除设备节点,直接rm -rf /dev/chr_dev即可;(2)上面这段代码的原型来自于《深入linux设备驱动程序内核机制》这本书,稍作修改,如果大家对内核机制的内容感兴趣,可以参考这本书的内容。



               

    给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

    这里写图片描述
    展开全文
  • 昨天我们说了一些简单模块编写方法,但是终归没有涉及到设备编写内容,我们可以了解一下相关方面的内容,并且用一个实例来说明在Linux上面设备是如何编写的。虽然我不是专门做Linux驱动的,却也经常收到一些朋友们...
  • 2.字符设备驱动开发基础(一个虚拟字符设备驱动开发流程) 字符设备驱动编写,主要工作就是驱动对应的open close read write函数的编写说白了,就是 对file_operations结构体的成员变量的实现; 在linux内核代码中...

    一、字符设备驱动框架

    具体的对应关系见上一篇文章,这里只对需要实现的部分进行说明
    2.字符设备驱动开发基础(一个虚拟的字符设备驱动开发流程)
    字符设备驱动编写,主要工作就是驱动对应的open close read write函数的编写说白了,就是
    file_operations结构体的成员变量的实现;
    在linux内核代码中,file_operations的定义在/include/linux/fs.h这在个.h文件中定义了很多对于linux文件很重要的概念,包括inode、file_operations等等;

    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述

    1.1 使用vscode作为linux下编辑器的设置

    在当前目录下创建.vscode文件夹,其下创建如下设置当前目录的头文件搜索路径(注意选择自己的内核源码路径)
    c_cpp_properties.json

    {
        "configurations": [
            {
                "name": "Linux",
                "includePath": [
                    "${workspaceFolder}/**",
                    "/home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include", 
                    "/home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include", 
                    "/home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"
                ],
                "defines": [],
                "compilerPath": "/usr/bin/clang",
                "cStandard": "c11",
                "cppStandard": "c++17",
                "intelliSenseMode": "clang-x64"
            }
        ],
        "version": 4
    }
    

    1.2 驱动模块加载卸载测试(仅含有模块注册和卸载函数的驱动测试,具体框架在1.4中)

    内容:编写一个仅含有驱动的注册和卸载函数的.c文件,将其编译成驱动模块后加载到内核,观察是否加载成功
    目的:验证环境搭建是否存在问题;
    printk()的头文件:#include <linux/kernel.h>
    chrdevbase.c

    #include <linux/module.h>
    #include <linux/kernel.h>
    
    static int __init chrdevbase_init(void)
    {
        printk("chrdevbase_init\r\n");
        return 0;
    }
    static void __exit chrdevbase_exit(void)
    {
        printk("chrdevbase_exit\r\n");
    }
    /**
     * 模块入口与出口
     */
    
    module_init(chrdevbase_init);
    module_exit(chrdevbase_exit);
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("QJY");
    

    1.3 Makefile

    KERNELDIR := /home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
    
    CURRENT_PATH := $(shell pwd)
    
    obj-m := chrdevbase.o
    
    build: kernel_modules
    
    kernel_modules:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
    clean:
    	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
    

    编译之后的目录为:(编译——拷贝到ubuntu nfs下的rootfs中)
    在这里插入图片描述

    开发板中加载模块:
    在这里插入图片描述

    1.4 字符设备驱动框架搭建与函数实现

    下图所示是本实验使用的字符设备驱动框架,并不完善;
    在这里插入图片描述

    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/fs.h>
    #include <linux/uaccess.h>  // copy from user
    
    #define CHRDEVBASE_MAJOR    200             // 主设备号
    #define CHRDEVBASE_NAME     "chrdevbase"    // 名字
    
    static char readbuf[100];   // 读缓冲区
    static char writebuf[100];  // 写缓冲区
    static char kernel_data[] = {"kernel data!"};
    
    
    static int chrdevbase_open(struct inode *inode, struct file *filp)
    {
        printk("chrdevbase_open\r\n");
        return 0;
    }
    
    static int chrdevbase_close(struct inode *inode, struct file *filp)
    {
        printk("chrdevbase_release\r\n");
        return 0;
    }
    
    static ssize_t chrdevbase_read(struct file *filp, __user char *buf, 
                                    size_t cnt, loff_t *ppos)
    {
        int retval = 0;
        printk("chrdevbase_reade\r\n");
        // 从内核中读取数据
        memcpy(readbuf, kernel_data, sizeof(kernel_data));
        retval = copy_to_user(buf, readbuf, cnt);
        if(retval == 0) {
            printk("kernel data sended! OK\r\n");
        }else {
            printk("kernel data sended failed!\r\n");
        }
    
        return 0;
    }
    
    static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, 
                                    size_t cnt, loff_t *ppos)
    {
        int retval = 0;
        printk("chrdevbase_write\r\n");
        // 从用户空间向内核空间写数据
        copy_from_user(writebuf, buf, cnt);
        if(retval == 0) {
            printk("kernel reveived OK\r\n");
        }else {
            printk("kernel reveived failed!\r\n");
        }
        return 0;
    }
    
    
    /* 字符设备操作集合*/
    static struct file_operations chrdevbase_fops={
        .owner = THIS_MODULE,
        .open  = chrdevbase_open,
        .release = chrdevbase_close,
        .read = chrdevbase_read,
        .write = chrdevbase_write, 
    };
    
    
    
    static int __init chrdevbase_init(void)
    {   
        int ret = 0;
        printk("chrdevbase_init\r\n");
    
        /* 注册字符设备*/
        ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, 
                        &chrdevbase_fops);
        if(ret < 0) {
            printk("chrdevbase init failed!\r\n");
        }
        
        return 0;
    }
    
    static void __exit chrdevbase_exit(void)
    {
        printk("chrdevbase_exit\r\n");
        /* 注销字符设备*/
        unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
    }
    
    /**
     * 模块入口与出口
     */
    
    module_init(chrdevbase_init);
    module_exit(chrdevbase_exit);
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("QJY");
    

    二、驱动模块的加载和卸载

    驱动的加载有两种方式:

    1. 直接写进内核文件:编译内核后内核(zImage)与驱动融为一体
    2. 编译成.ko模块文件,使用modprobe指令加载模块;(我们使用这种)

    第二种方法的好处在于,方便调试,可以随时加载和卸载模块,而不是重新编译整个内核文件;

    2.1 开发板的准备工作(alpha-i.mx6ull)

    1. 启动:uboot代码编译后,烧写在sd卡中,开发板从sd卡启动
    2. 内核zImage和设备树文件.dtb:使用tftp从ubuntu中读取;
    3. rootfs根文件系统在ubuntu中,使用nfs挂载;
    4. 设置uboot变量 bootargs 和 bootcmd完成2、3的设置;

    下面是设置之后的uboot环境变量

    => print
    baudrate=115200
    board_name=EVK
    board_rev=14X14
    boot_fdt=try
    bootargs=console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.5.103:/home/qjy/linux/nfs/rootfs ip=192.168.5.111:192.168.5.103:192.168.5.1:255.255.255.0::eth0:off
    bootcmd=tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000
    bootcmd_mfg=run mfgtool_args;bootz ${loadaddr} ${initrd_addr} ${fdt_addr};
    bootdelay=3
    bootscript=echo Running bootscript from mmc ...; source
    console=ttymxc0
    ethact=FEC1
    ethaddr=00:04:9f:04:d2:35
    ethprime=FEC
    fdt_addr=0x83000000
    fdt_file=imx6ull-alientek-emmc.dtb
    fdt_high=0xffffffff
    fileaddr=83000000
    filesize=8d32
    findfdt=if test $fdt_file = undefined; then if test $board_name = EVK && test $board_rev = 9X9; then setenv fdt_file imx6ull-9x9-evk.dtb; fi; if test $board_name = EVK && test $board_rev = 14X14; then setenv fdt_file imx6ull-14x14-evk.dtb; fi; if test $fdt_file = undefined; then echo WARNING: Could not determine dtb to use; fi; fi;
    gatewayip=192.168.5.1
    image=zImage
    initrd_addr=0x83800000
    initrd_high=0xffffffff
    ip_dyn=yes
    ipaddr=192.168.5.111
    loadaddr=0x80800000
    loadbootscript=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${script};
    loadfdt=fatload mmc ${mmcdev}:${mmcpart} ${fdt_addr} ${fdt_file}
    loadimage=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${image}
    mfgtool_args=setenv bootargs console=${console},${baudrate} rdinit=/linuxrc g_mass_storage.stall=0 g_mass_storage.removable=1 g_mass_storage.file=/fat g_mass_storage.ro=1 g_mass_storage.idVendor=0x066F g_mass_storage.idProduct=0x37FF g_mass_storage.iSerialNumber="" clk_ignore_unused
    mmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot}
    mmcautodetect=yes
    mmcboot=echo Booting from mmc ...; run mmcargs; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if run loadfdt; then bootz ${loadaddr} - ${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;
    mmcdev=0
    mmcpart=1
    mmcroot=/dev/mmcblk0p2 rootwait rw
    netargs=setenv bootargs console=${console},${baudrate} root=/dev/nfs ip=dhcp nfsroot=${serverip}:${nfsroot},v3,tcp
    netboot=echo Booting from net ...; run netargs; if test ${ip_dyn} = yes; then setenv get_cmd dhcp; else setenv get_cmd tftp; fi; ${get_cmd} ${image}; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if ${get_cmd} ${fdt_addr} ${fdt_file}; then bootz ${loadaddr} - ${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;
    netmask=255.255.255.0
    panel=TFT7016
    script=boot.scr
    serverip=192.168.5.103
    

    如果重新配置,需要更新的环境变量为:bootcmd、bootargs、ipaddr、ethaddr、gatewayip、netmask、serverip;

    2.2 驱动模块的加载卸载

    将编译出来的.ko模块放到根文件系统中;
    使用depmod指令分析模块依赖心,为使用modprobe准备;
    使用modprobe / insmod指令完成对模块的加载;
    使用modprobe -r / rmmod指令完成对模块的卸载;
    使用lsmod查看已经加载的模块;
    如果使用modprobe出现问题,见
    Linux驱动加载问题“.ko模块无法加载modprobe: module ‘xxx.ko’ not found”解决方法
    Linux depmod命令用于分析可载入模块的相依性。
    depmod(depend module)可检测模块的相依性,供modprobe在安装模块时使用。

    3. 应用程序的编写

    /**
     * 本文件对应驱动模块CharDevBase的测试APP
     * argv[1] : filename:要进行互动的驱动设备名称(通过/dev/
     * argv[2]  : 1:表示从驱动中读取数据操作;   2:表示向驱动中写数据操作
     */
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <errno.h>
    #include <string.h>
    #include <stdlib.h>
    
    #define READ_SIZE   50
    #define WRITE_SIZE  50
    
    int main(int argc,char* argv[])
    {
        int fd;
        int read_ret = 0, write_ret = 0;
        char read_buf[100];     // 用户空间读取数据缓冲区
        char write_buf[100];    // 用户空间写入数据缓冲区
        char write_data[] = "Im the written data, I come from UserSapce!\n";
        memcpy(write_buf,write_data, sizeof(write_data));
        if(argc != 3){
            printf("参数不足!\n");
            return -1;
        }
    
        fd = open(argv[1], O_RDWR);
        if(fd < 0){
            printf("Now in userSpace: open error!\n");
            return errno;
        }
    
        if(atoi(argv[2]) == 1){// 读操作
            if((read_ret = read(fd, read_buf,READ_SIZE)) == -1){
                printf("Now in userSpace: reading error~\n");
                return errno;
            }else{
                printf("Now in userSpace: the received data: %s\n", read_buf);
            }
        }
        
        if(atoi(argv[2]) == 2){// 写操作
            if((write_ret = write(fd, write_buf,WRITE_SIZE)) == -1){
                printf("Now in userSpace: reading error~\n");
                return errno;
            }
        }
        usleep(200);
        if(-1 == close(fd)){
            printf("Now in userSpace: close error!\n");
            return errno;
        }
        return 0;
    }
    

    3.1编译应用程序并将.ko与App文件拷贝到rootfs中

    注意,驱动模块的编译方法在之前的makefile中

    arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
    sudo cp ./*.ko ./chrdevbaseApp ../../../nfs/rootfs/lib/modules/4.1.15
    

    4. 测试

    4.1 首先在系统中创建设备结点

    4.2 测试驱动程序

    在这里插入图片描述

    展开全文
  • linux内核编译以及字符设备驱动程序的编写 关键词: 内核编译; globalmem字符设备驱动编写; linux; linux设备驱动编写; 字符设备驱动程序主要内容 虚拟内存设备globalmem驱动实现,编写程序,然后将生成的...

    linux内核编译以及字符设备驱动程序的编写

    关键词: 内核编译; globalmem字符设备驱动编写; linux; linux设备驱动编写;

    字符设备驱动程序主要内容

    虚拟内存设备globalmem驱动实现,编写程序,然后将生成的驱动模块插入到驱动之中,接着编写测试程序,对设备globalmem进行测试。(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

    事先准备

    • linux-3.2.96内核镜像 传送门
    • Ubuntu 14.04 64位系统(内核版本3.13.0-24-generic
    • 编译环境配置:使用命令
      sudo apt-get install build-essential kernel-package libncurses5-dev

      其中gcc版本为4.8.2(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

    内核编译

    使用linux-3.2.96内核,下载的是.tar.xz格式,首先将下载的内核移动到/usr/src/目录下(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

    sudo mv linux-3.2.96.tar.xz /usr/src

    切换到/usr/src/目录下,解压缩内核压缩包。由于下载的压缩包是.tar.xz后缀,解压分为两步:

    xz -d linux-3.2.96.tar.xz
    tar -xvf linux-3.2.96.tar

    进入/usr/src/linux-3.2.96中,清理旧的编译文件

    cd /usr/src/linux-3.2.96
    make mrproper

    现在/usr/src的样子就是这样的:

    usr-src.png

    /usr/src/linux-3.2.96中使用命令复制运行中的内核config到linux-3.2.96文件夹中(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

    sudo cp /boot/config-3.13.0-24-generic /usr/src/linux-3.2.96

    接着复制现有内核中的.config文件也复制到linux-3.2.96中(以我的Ubuntu 14.04为例,现有内核中的.config即为/usr/src/linux-headers-3.13.0-24-generic/.config

    sudo cp /usr/src/linux-headers-3.13.0-24-generic/.config /usr/src/linux-3.2.96

    config_file_copy.png

    接着运行menuconfig

    sudo make menuconfig

    menuconfig1.png

    选择load an Alternate Configuration file

    menuconfig2.png

    或者直接用默认的.config,都一样能编译

    接着直接make编译,如果不指定参数,make默认单线程编译,所以我加了参数j4,多作业充分利用处理器核心

    多作业开始编译

    sudo make -j4 all

    makej4-1.png

    开始编译:

    makej4-2.png

    globalmem字符设备驱动编写及安装挂载

    globalmem字符设备驱动编写核心代码如下

    int  globalmem_init(void)  
    {  
        int  result;  
        dev_t  devno  =  MKDEV(globalmem_major,  0);  
    
        /*    申请设备号   */  
        if  (globalmem_major)  
            result  =  register_chrdev_region(devno,  1,  "globalmem");  
        else    /*    动态申请设备号(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)    */  
        {  
            result  =  alloc_chrdev_region(&devno,  0,  1,  "globalmem");  
            globalmem_major  =  MAJOR(devno);  
        }      
        if  (result  <  0)  
            return  result;  
    
        /*    动态申请设备结构体的内存(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved) */  
        globalmem_devp  =  kmalloc(sizeof(struct  globalmem_dev),  GFP_KERNEL);  
        if  (!globalmem_devp)        /*申请失败*/  
        {  
            result  =    -  ENOMEM;  
            goto  fail_malloc;  
        }  
        memset(globalmem_devp,  0,  sizeof(struct  globalmem_dev));  
        globalmem_setup_cdev(globalmem_devp,  0);  
        return  0;  
    fail_malloc:  unregister_chrdev_region(devno,  1);  
        return  result;  
    }  
    
    void  globalmem_exit(void)  
    {  
        cdev_del(&globalmem_devp->cdev);        /*注销  cdev*/  
        kfree(globalmem_devp);            /*释放设备结构体内存  (Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)*/
        unregister_chrdev_region(MKDEV(globalmem_major,  0),  1);  /*释放设备号*/  
    } 

    完整代码见[传送门]

    Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved

    globalmem字符设备驱动的编译makefile编写

    在编写makefile的时候,我参考了网上的很多写法,都不能很有效地成功编译设备驱动。以下的makefile写法是我自己摸索出来的,在我的测试环境下,保证能编译成功。但也只保证在我的测试环境下,因为我没有测试其他的环境,如Ubuntu 16.04等。

    代码如下:

    # Makefile (Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)
    ifneq ($(KERNELRELEASE),)
    # kbuild syntax. dependency relationshsip of files and target modules are listed here.
    obj-m := globalmem.o  
    else    
        PWD := $(shell pwd)
        KERNEL_VER ?= $(shell uname -r)
        KERNEL_DIR := /lib/modules/$(KERNEL_VER)/build
    all:    
        $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules
    clean:  
        rm ‐rf *.o *~ core .depend .*.cmd *.ko *.mod.c
    endif

    运行make命令

    编译成功:

    globalmemCompiled.png

    得到globalmem.ko文件

    运行sudo insmod globalmem.ko命令加载模块,通过lsmod命令,发现globalmem模块已被加载。

    globalmemInstalled.png

    再通过cat /proc/devices命令查看,发现多出了主设备号为150的globalmem字符设备驱动,如下所示

    globalmemNo150.png

    接着sudo mknod /dev/globalmem c 150 0这一步我不确定是否需要,因为之前我没sudo insmod,可能权限不够所以/dev里面没有globalmem,现在我使用了sudo insmod,接着又mknod/dev里面有globalmem了,所以我不确定是哪条命令的作用。

    虚拟的字符设备驱动编写完成,也挂载成功了,该如何验证呢?

    两种方法:

    方法1:

    直接在终端中输入

    sudo su
    ehco 'gy1' > /dev/globalmem

    作用是向字符设备中写入字符数据”gy1”。然后,在终端中继续输入(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

    cat /dev/globalmem

    作用是从字符设备中读取已经写入完成的数据(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)

    globalmemTest1.png

    在方法1中为什么会先出现gy1然后又出现cat: /dev/globalmem: 没有那个设备或地址这个报错?

    答:返回错误值才是正确的,因为读的位置已经越界,所以要返回错误。该返回错误的时候返回正确,那就是错误。至于网上说将globalmem.c中的if (p > = GLOBALMEM_SIZE)改成if (p > GLOBALMEM_SIZE)是错误的。输出的错误信息是正常的debug信息,因为cat调用了read两次。如果不想看到错误信息输出,将cat换成more即可

    方法2:

    编写自己的test.c文件

    #include <sys/types.h>  
    #include <sys/stat.h>  
    #include <stdio.h>  
    #include <fcntl.h>  
    //(Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)
    
    int main()  
    {
        int fd;   
        char num[128] = {'\0'};  
        int arg = 2015;  
    
        //打开字符设备  
        fd = open("/dev/globalmem", O_RDWR); //可读写方式打开设备文件  
        if(fd != -1) {  
            //读取字符设备中的初始值  
            read(fd, num, sizeof(num)); //读取设备变量  
            printf("The globalmem is %s\n", num);  
    
            //文件指针复位  
            lseek(fd, 0, SEEK_SET);  
    
            //获取输入数据  
            printf("Please input the num written to globalmem\n");  
            scanf("%s", num);  
    
            //写入到字符设备中  
            write(fd, num, sizeof(num)); //写设备变量  
    
            //文件指针复位  
            lseek(fd, 0, SEEK_SET);  
    
            //从字符设备中读取出来  
            read(fd, num, sizeof(num)); //再次读取刚才写的值  
            printf("The globalmem is %s\n", num);  
    
            //文件指针复位  
            lseek(fd, 0, SEEK_SET);  
    
            //清空文件中的数据  
            if (0 > ioctl(fd, 0x01, &arg)) {  
                printf("Call cmd MEM_CLEAR fail\n");  
                perror("open globalmem");  
            }  
    
            //关闭字符设备  
            close(fd); //关闭设备文件  
        } else {  
            //打开失败  
            printf("Device open failure\n");  
            perror("open globalmem");  
        }  
        return 0;
    }

    globalmemTest2.png

    发现读出了之前方法1中输入到globalmem中的字符gy1,并写入了新的字符gy2

    至此实验成功。

    常见报错troubleshoot

    报错1

    编译内核过程遇到问题,提示:

    ERROR: "__modver_version_show" [drivers/staging/rts5139/rts5139.ko] undefined!
    make[1]: *** [__modpost] Error 1 
    make: *** [modules] Error 2

    解决方法:

    sudo gedit /.config

    设置CONFIG_RTS5139=n

    继续编译,成功:

    kernelCompiled.png

    报错2

    globalmem的make过程中提示system.h没有这个文件

    globaklmemError1.png

    解决方法:

    在globalmem.c中#include <asm/system.h>换为

    #include <linux/version.h>
    #if LINUX_VERSION_CODE > KERNEL_VERSION(3, 3, 0)
        #include <asm/switch_to.h>
    #else
        #include <asm/system.h>
    #endif

    报错3

    .ioctl部分有错误

    globaklmemError2.png

    原因

    linux 2.6.29和linux 2.6.38的内核在file_operations结构发生了变化,否则在linux 2.6.38内核中,继续使 用.ioctl成员,编译时就会报错:error: unknown field 'ioctl' specified in initializerstruct file_operations结构体定义在include/linux/fs.h文件中。

    linux 2.6.38内核取消了原有的ioctl成员,添加来新的成员
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    原来的ioctl 返回值变为
    long long (*compat_ioctl) (struct file *, unsigned int, unsigned long);内核空间64位,用户空间32位

    warning: initialization from incompatible pointer type出现此种warnning 的原因 “不兼容的指针类型初始化”。是你定义的函数类型与接口函数的类型不一样,如把返回值long定义成了int

    这两个问题都对驱动有影响。

    解决方法

    static const struct file_operations 。。。 = {
    。。。。。。
    .unlocked_ioctl = 。。。,
    。。。。。。
    };

    报错4

    驱动模块编译时出现如下错误:

    error: implicit declaration of function 'kmalloc'
    error: implicit declaration of function 'kfree'

    解决方法

    添加如下头文件即可: #include <linux/slab.h>


    参考资料

    (防止CSDN总因为外链屏蔽我的文章,去掉网址中的¥¥即可)

    ht¥¥tp://www¥¥.linuxidc.com/Linux/2016-04/130459.htm
    ht¥¥tp://mzqthu¥¥.iteye.com/blog/2001167
    ht¥¥tp://387424-student-sina-com¥¥.iteye.com/blog/728021
    ht¥¥tp://blog¥¥.sina.com.cn/s/blog_5dbc002d0100h7hj.html
    ht¥¥tp://blog¥¥.sina.com.cn/s/blog_85998e3801011fpf.html
    http://blog.csdn.net/fang_yang_wa/article/details/55805560
    http://blog.csdn.net/djinglan/article/details/7372956
    http://blog.csdn.net/xiaowulang20082008/article/details/50586985
    ht¥¥tps://forums¥¥.fedoraforum.org/showthread.php?282144-Missing-system-h
    http://blog.csdn.net/qiaoliang328/article/details/4874238
    http://blog.csdn.net/rocky_zhm/article/details/47274879
    http://blog.csdn.net/qiaoliang328/article/details/4874238
    ht¥¥tps://www¥¥.cnblogs.com/feisky/archive/2010/05/29/1746885.html

    (Copyright © http://blog.csdn.net/s_gy_zetrov. All Rights Reserved)


    visitor tracker
    访客追踪插件


    1

    展开全文
  • 字符设备驱动开发

    2020-07-17 10:06:57
    本章会以一个虚拟的设备为例,讲解如何进行字符设备驱动开发,以及如何编写 测试 APP 来测试驱动工作是否正常,为以后的学习打下坚实的基础。 40.1 字符设备驱动简介 40.1 字符设备驱动简介 字符设备是 Linux 驱动...
  • 在内存中开辟一块4K的空间,将4K的内存看作一个字符设备,编写一个字符设备驱动程序来操作这块内存。 2、实验环境  内核版本:linux-2.6.35-32 3、字符设备驱动程序设计步骤  Step1:设备注册  在linux 2.6...
  • 1.设备驱动程序的定义方式 两种: 1.全局静态变量; 2.内核提供的API. 这里采用第二种方式来实现一个简单的虚拟设备驱动并且实现它的读写功能.
  • 3 字符设备驱动编写(含代码和makefile) 4 GTK编写系统监视器,可以监测系统很多方面(含代码和makefile) 5 虚拟文件系统(实现的比较简单)(含代码和makefile) 说明:本内容实现自己添加设备驱动,含有代码...
  • 练习字符设备驱动的两种模板之后,编写一个字符驱动程序 chartest虚拟设备:由驱动程序4管理,所指向的设备是64号设备,类似于串口终端或者字符设备终端 创建文件 #include <linux/types.h> #include <...
  •  之所以称之为虚拟字符设备驱动程序,主要原因是该驱动程序并没有真正操作外部设备,只是一个字符设备驱动程序框架,这为后面我们开发正是设备的驱动程序(LED、蜂鸣器等)奠定了基础。  作者:沧
  • 1)实验平台:正点原子Linux开发板2)摘自《正点原子I.MX6U嵌入式Linux...本章会以一个虚拟的设备为例,讲解如何进行字符设备驱动开发,以及如何编写测试APP来测试驱动工作是否正常,为以后的学习打下坚实的基础。40...
  • 05课_字符设备驱动

    2021-02-02 16:35:20
    05课_字符设备驱动 文章目录05课_字符设备驱动1、介绍2、用户程序与驱动程序关联3、测试程序3.1、first_drv:点灯3.1.1、测试第一个驱动程序,手动选择主设备号3.1.2、测试,自动分配主设备号3.1.3、测试,自动分配...
  •  之所以称之为虚拟字符设备驱动程序,主要原因是该驱动程序并没有真正操作外部设备,只是一个字符设备驱动程序框架,这为后面我们开发正是设备的驱动程序(LED、蜂鸣器等)奠定了基础。  作者:沧海

空空如也

空空如也

1 2 3 4 5 ... 8
收藏数 145
精华内容 58
关键字:

编写虚拟字符设备驱动