• 第四季-专题10-字符设备驱动模型


    专题10-字符设备驱动模型

    第1课-使用字符驱动程序

    1. 编译/安装驱动

    在Linux系统中,驱动程序通常采用内核模块的程序结构来进行编码。因此,编译/安装一个驱动程序,其实质就是编译/安装一个内核模块。

    例子:字符设备驱动程序

    编写makefile文件:

           obj-m := memdev.o

    KDIR := /home/S5-driver/lesson7/linux-tiny6410/

    all:

        make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm

    clean:

        rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.bak *.order

    编写memdev.c文件:

        #include <linux/module.h>

    #include <linux/fs.h>

    #include <linux/init.h>

    #include <linux/cdev.h>

    #include <asm/uaccess.h>

    int dev1_registers[5];

    int dev2_registers[5];

    struct cdev cdev;

    dev_t devno;

    /*文件打开函数*/

    int mem_open(struct inode *inode, struct file *filp)

    {

       

        /*获取次设备号*/

        int num = MINOR(inode->i_rdev);

       

        if (num==0)

            filp->private_data = dev1_registers;

        else if(num == 1)

            filp->private_data = dev2_registers;

        else

            return -ENODEV;  //无效的次设备号

       

        return 0;

    }

    /*文件释放函数*/

    int mem_release(struct inode *inode, struct file *filp)

    {

      return 0;

    }

    /*读函数*/

    static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/

      /*判断读位置是否有效*/

      if (p >= 5*sizeof(int))

        return 0;

      if (count > 5*sizeof(int) - p)

        count = 5*sizeof(int) - p;

      /*读数据到用户空间*/

      if (copy_to_user(buf, register_addr+p, count))

      {

        ret = -EFAULT;

      }

      else

      {

        *ppos += count;

        ret = count;

      }

      return ret;

    }

    /*写函数*/

    static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      int *register_addr = filp->private_data; /*获取设备的寄存器地址*/

      /*分析和获取有效的写长度*/

      if (p >= 5*sizeof(int))

        return 0;

      if (count > 5*sizeof(int) - p)

        count = 5*sizeof(int) - p;

       

      /*从用户空间写入数据*/

      if (copy_from_user(register_addr + p, buf, count))

        ret = -EFAULT;

      else

      {

        *ppos += count;

        ret = count;

      }

      return ret;

    }

    /* seek文件定位函数 */

    static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)

    {

        loff_t newpos;

        switch(whence) {

          case SEEK_SET:

            newpos = offset;

            break;

          case SEEK_CUR:

            newpos = filp->f_pos + offset;

            break;

          case SEEK_END:

            newpos = 5*sizeof(int)-1 + offset;

            break;

          default:

            return -EINVAL;

        }

        if ((newpos<0) || (newpos>5*sizeof(int)))

        return -EINVAL;

       

        filp->f_pos = newpos;

        return newpos;

    }

    /*文件操作结构体*/

    static const struct file_operations mem_fops =

    {

      .llseek = mem_llseek,

      .read = mem_read,

      .write = mem_write,

      .open = mem_open,

      .release = mem_release,

    };

    /*设备驱动模块加载函数*/

    static int memdev_init(void)

    {

      /*初始化cdev结构*/

      cdev_init(&cdev, &mem_fops);

      /* 注册字符设备 */

      alloc_chrdev_region(&devno, 0, 2, "memdev");

      cdev_add(&cdev, devno, 2);

    }

    /*模块卸载函数*/

    static void memdev_exit(void)

    {

      cdev_del(&cdev);   /*注销设备*/

      unregister_chrdev_region(devno, 2); /*释放设备号*/

    }

    MODULE_LICENSE("GPL");

    module_init(memdev_init);

    module_exit(memdev_exit);

        编译文件,我们就会得到memdev.ko文件,这就是我们编写的驱动模块。

    1. 字符设备文件

    通过字符设备文件,应用程序可以使用相应的字符设备驱动程序来控制字符设备。创建字符设备文件的方法一般有两种:

    [应用程序]传递文件名给[字符设备文件]传递主设备号给[设备驱动程序]。

    (1)    使用mknod命令

    mknod /dev/文件名 c 主设备号 次设备号

    (2)    使用函数在驱动程序中创建

    例子:创建字符设备文件

    write-men.c

    #include <stdio.h>

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <fcntl.h>

    int main()

    {

      int fd = 0;

      int src = 2013;

      /*打开设备文件*/

      fd = open("/dev/memdev0",O_RDWR);

      /*写入数据*/

      write(fd, &src, sizeof(int));

      /*关闭设备*/

      close(fd);

      return 0;   

    }

    read-men.c

    #include <stdio.h>

    #include <sys/types.h>

    #include <sys/stat.h>

    #include <fcntl.h>

    int main()

    {

      int fd = 0;

      int dst = 0;

      /*打开设备文件*/

      fd = open("/dev/memdev0",O_RDWR);

      /*写入数据*/

      read(fd, &dst, sizeof(int));

      printf("dst is %d ",dst);

      /*关闭设备*/

      close(fd);

      return 0;   

    }

    第2课-字符设备驱动模型

    1. 设备描述结构cdev

           在Linux系统中,设备的类型非常繁多,如:字符设备,块设备,网络接口设备,USB设备,PCI设备,平台设备,混杂设备……,而设备类型不同,也意味着其对应的驱动程序模型不同,这样就导致了我们需要去掌握众多的驱动程序模型。那么能不能从这些众多的驱动模型中提炼出一些具有共性的规则,这是我们能不能学好Linux驱动的关键。

    1)设备驱动模型:

           设备驱动模型主要分为三个部分,分别是:驱动初始化,实现设备操作和驱动注销。

    驱动的初始化主要包括:分配设备描述结构;初始化设备描述结构;注册设备描述结构和硬件初始化。

    (2)设备描述结构

           在任何一种驱动模型中,设备都会用内核中的一种结构来描述。我们的字符设备在内核中使用struct cdev来描述。

    struct cdev {

    struct kobject kobj;

    struct module *owner;

    const struct file_operations *ops; //设备操作集

    struct list_head list;

    dev_t dev; //设备号

    unsigned int count; //设备数

    };

    (3)    设备号

    在命令/dev目录下可以查看设备号,逗号前面的是主设备号,后面的是次设备号。

           我们使用的字符设备驱动是依靠字符设备文件来找到的,字符设备文件与字符驱动程序是依靠主设备号建立起对应关系的。

           一个驱动程序不可能只处理一个驱动设备,比如开发板上有两个串口接口。为了区分不同的设备,它们对应的设备文件的主设备号一样但是次设备号不一样。

    • 设备号-操作

    Linux内核中使用dev_t类型来定义设备号,dev_t这种类型其实质为32位的unsigned int,其中高12位为主设备号,低20位为次设备号。

    问1:如果知道主设备号,次设备号,怎么组合成dev_t类型

    答:dev_t dev = MKDEV(主设备号,次设备号)

    问2: 如何从dev_t中分解出主设备号?

    答: 主设备号= MAJOR(dev_t dev)

    问3: 如何从dev_t中分解出次设备号?

    答: 次设备号=MINOR(dev_t dev)

    • 设备号-分配

    静态申请

    开发者自己选择一个数字作为主设备号,然后通过register_chrdev_region向内核申请使用。缺点:如果申请使用的设备号已经被内核中的其他驱动使用了,则申请失败。

    动态分配

    使用alloc_chrdev_region由内核分配一个可用的主设备号。

    优点:因为内核知道哪些号已经被使用了,所以不会导致分配到已经被使用的号。

    • 设备号-注销

    不论使用何种方法分配设备号,都应该在驱动退出时,使用unregister_chrdev

    _region函数释放这些设备号。

    (4)    操作函数集

    应用程序依靠一系列的函数(read,write,open)打开字符设备文件,字符设备文件根据映射关系表,将不同指令函数出发的程序发送给字符设备驱动。这个表就是file_operations结构。

    Struct file_operations是一个函数指针的集合,定义能在设备上进行的操作。结构中的函数指针指向驱动中的函数,这些函数实现一个针对设备的操作, 对于不支持的操作则设

    置函数指针为NULL。例如:

    struct file_operations dev_fops = {

    .llseek = NULL,

    .read = dev_read,

    .write = dev_write,

    .ioctl = dev_ioctl,

    .open = dev_open,

    .release = dev_release,

    };

    2. 字符设备驱动模型

    (1)驱动初始化

    • 分配cdev

    cdev变量的定义可以采用静态和动态两种办法。

    静态分配:struct cdev mdev;

    动态分配:struct cdev *pdev = cdev_alloc();

    • 初始化cdev

    struct cdev的初始化使用cdev_init函数来完成。

    cdev_init(struct cdev *cdev, const struct file_operations *fops)

    参数:

    cdev: 待初始化的cdev结构,

    fops: 设备对应的操作函数集。

    • 注册cdev

    字符设备的注册使用cdev_add函数来完成。

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

    参数:

    p: 待添加到内核的字符设备结构,

    dev: 设备号,

    count: 该类设备的设备个数。

    • 硬件初始化

    根据硬件芯片手册,完成初始化。

    (2)实现设备操作

           open, read, write, lseek, close,也叫做设备方法

    int (*open) (struct inode *, struct file *):打开设备,响应open系统。

    int (*release) (struct inode *, struct file *):关闭设备,响应close系统调用。

    loff_t (*llseek) (struct file *, loff_t, int):重定位读写指针,响应lseek系统调用

    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *):从设备读取数据,响应read系统调用。

    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *):向设备写入数据,响应write系统调用。

    Struct file

    在Linux系统中,每一个打开的文件,在内核中都会关联一个struct file,它由内核在打开文件时创建, 在文件关闭后释放。

    重要成员:

    loff_t f_pos /*文件读写指针*/

    struct file_operations *f_op /*该文件所对应的操作*/

    Struct inode

    每一个存在于文件系统里面的文件都会关联一个inode 结构,该结构主要用来记录文件物理上的信息。因此, 它和代表打开文件的file结构是不同的。一个文件没有被打开时不会关联file结构,但是却会关联一个inode 结构。

    重要成员,dev_t i_rdev:设备号

    open

    open设备方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成如下工作:标明次设备号,启动设备。

    release

           release方法的作用正好与open相反。这个设备方法有时也称为close,它应该:关闭设备。

    read

    read设备方法通常完成2件事情:从设备中读取数据(属于硬件访问类操作);将读取到的数据返回给应用程序。

    ssize_t (*read) (struct file *filp, char __user *buff, size_t count, loff_t *offp)

    参数分析:

    filp:与字符设备文件关联的file结构指针, 由内核创建。

    buff : 从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。

    count: 请求传输的数据量,由read系统调用提供该参数。

    offp: 文件的读写位置,由内核从file结构中取出后,传递进来。

    buff参数是来源于用户空间的指针,这类指针都不能被内核代码直接引用,必须使用专门的函数。

    int copy_from_user(void *to, const void __user *from, int n)

    int copy_to_user(void __user *to, const void *from, int n)

    write

           write设备方法通常完成2件事情:从应用程序提供的地址中取出数据;将数据写入设备(属于硬件访问类操作)

    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)

    其参数类似于read

    (3)驱动注销

    当我们从内核中卸载驱动程序的时候,需要使用cdev_del函数来完成字符设备的注销。

    3.  范例驱动分析

    #include <linux/module.h>

    #include <linux/types.h>

    #include <linux/fs.h>

    #include <linux/errno.h>

    #include <linux/init.h>

    #include <linux/cdev.h>

    #include <asm/uaccess.h>

    #include <linux/slab.h>

    int dev1_registers[5];

    int dev2_registers[5];

    struct cdev cdev;

    dev_t devno;

    /*文件打开函数*/

    int mem_open(struct inode *inode, struct file *filp)

    {

       

        /*获取次设备号*/

        int num = MINOR(inode->i_rdev);

       

        if (num==0)

            filp->private_data = dev1_registers;

        else if(num == 1)

            filp->private_data = dev2_registers;

        else

            return -ENODEV;  //无效的次设备号

       

        return 0;

    }

    /*文件释放函数*/

    int mem_release(struct inode *inode, struct file *filp)

    {

      return 0;

    }

    /*读函数*/

    static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/

      /*判断读位置是否有效*/

      if (p >= 5*sizeof(int))

        return 0;

      if (count > 5*sizeof(int) - p)

        count = 5*sizeof(int) - p;

      /*读数据到用户空间*/

      if (copy_to_user(buf, register_addr+p, count))

      {

        ret = -EFAULT;

      }

      else

      {

        *ppos += count;

        ret = count;

      }

      return ret;

    }

    /*写函数*/

    static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      int *register_addr = filp->private_data; /*获取设备的寄存器地址*/

      /*分析和获取有效的写长度*/

      if (p >= 5*sizeof(int))

        return 0;

      if (count > 5*sizeof(int) - p)

        count = 5*sizeof(int) - p;

       

      /*从用户空间写入数据*/

      if (copy_from_user(register_addr + p, buf, count))

        ret = -EFAULT;

      else

      {

        *ppos += count;

        ret = count;

      }

      return ret;

    }

    /* seek文件定位函数 */

    static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)

    {

        loff_t newpos;

        switch(whence) {

          case SEEK_SET:

            newpos = offset;

            break;

          case SEEK_CUR:

            newpos = filp->f_pos + offset;

            break;

          case SEEK_END:

            newpos = 5*sizeof(int)-1 + offset;

            break;

          default:

            return -EINVAL;

        }

        if ((newpos<0) || (newpos>5*sizeof(int)))

        return -EINVAL;

       

        filp->f_pos = newpos;

        return newpos;

    }

    /*文件操作结构体*/

    static const struct file_operations mem_fops =

    {

      .llseek = mem_llseek,

      .read = mem_read,

      .write = mem_write,

      .open = mem_open,

      .release = mem_release,

    };

    /*设备驱动模块加载函数*/

    static int memdev_init(void)

    {

      /*初始化cdev结构*/

      cdev_init(&cdev, &mem_fops);

      /* 注册字符设备 */

      alloc_chrdev_region(&devno, 0, 2, "memdev");

      cdev_add(&cdev, devno, 2);

    }

    /*模块卸载函数*/

    static void memdev_exit(void)

    {

      cdev_del(&cdev);   /*注销设备*/

      unregister_chrdev_region(devno, 2); /*释放设备号*/

    }

    MODULE_LICENSE("GPL");

    module_init(memdev_init);

    module_exit(memdev_exit);

    第3课-自己动手写驱动

    表写驱动程序主要依赖于上节课的框架,大纲如下:

    1. 驱动初始化:

    (1)    分配cdev:静态分配、动态分配

    (2)    初始化cdev:cdev_int

    (3)    注册cdev:cdev_add

    (4)    硬件初始化

    1. 实现设备操作

    (1)    open

    (2)    read:copy_to_user

    (3)    write:copy_from_user

    (4)    lseek

    (5)    close

    1. 驱动注销

    (1)    cdev_del

    (2)    unregister_chrdev_region

    例子:

    #include <linux/module.h>

    #include <linux/types.h>

    #include <linux/fs.h>

    #include <linux/errno.h>

    #include <linux/init.h>

    #include <linux/cdev.h>

    #include <asm/uaccess.h>

    #include <linux/slab.h>

    int dev1_registers[5];

    int dev2_registers[5];

    struct cdev cdev;

    dev_t devno;

    /*文件打开函数*/

    int mem_open(struct inode *inode, struct file *filp)

    {

       

        /*获取次设备号*/

        int num = MINOR(inode->i_rdev);

       

        if (num==0)

            filp->private_data = dev1_registers;

        else if(num == 1)

            filp->private_data = dev2_registers;

        else

            return -ENODEV;  //无效的次设备号

       

        return 0;

    }

    /*文件释放函数*/

    int mem_release(struct inode *inode, struct file *filp)

    {

      return 0;

    }

    /*读函数*/

    static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      int *register_addr = filp->private_data; /*获取设备的寄存器基地址*/

      /*判断读位置是否有效*/

      if (p >= 5*sizeof(int))

        return 0;

      if (count > 5*sizeof(int) - p)

        count = 5*sizeof(int) - p;

      /*读数据到用户空间*/

      if (copy_to_user(buf, register_addr+p, count))

      {

        ret = -EFAULT;

      }

      else

      {

        *ppos += count;

        ret = count;

      }

      return ret;

    }

    /*写函数*/

    static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)

    {

      unsigned long p =  *ppos;

      unsigned int count = size;

      int ret = 0;

      int *register_addr = filp->private_data; /*获取设备的寄存器地址*/

      /*分析和获取有效的写长度*/

      if (p >= 5*sizeof(int))

        return 0;

      if (count > 5*sizeof(int) - p)

        count = 5*sizeof(int) - p;

       

      /*从用户空间写入数据*/

      if (copy_from_user(register_addr + p, buf, count))

        ret = -EFAULT;

      else

      {

        *ppos += count;

        ret = count;

      }

      return ret;

    }

    /* seek文件定位函数 */

    static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)

    {

        loff_t newpos;

        switch(whence) {

          case SEEK_SET:

            newpos = offset;

            break;

          case SEEK_CUR:

            newpos = filp->f_pos + offset;

            break;

          case SEEK_END:

            newpos = 5*sizeof(int)-1 + offset;

            break;

          default:

            return -EINVAL;

        }

        if ((newpos<0) || (newpos>5*sizeof(int)))

          return -EINVAL;

         

        filp->f_pos = newpos;

        return newpos;

    }

    /*文件操作结构体*/

    static const struct file_operations mem_fops =

    {

      .owner = THIS_MODULE,

      .llseek = mem_llseek,

      .read = mem_read,

      .write = mem_write,

      .open = mem_open,

      .release = mem_release,

    };

    /*设备驱动模块加载函数*/

    static int memdev_init(void)

    {

      /*初始化cdev结构*/

      cdev_init(&cdev, &mem_fops);

      /* 注册字符设备 */

      alloc_chrdev_region(&devno, 0, 2, "memdev");

      cdev_add(&cdev, devno, 2);

    }

    /*模块卸载函数*/

    static void memdev_exit(void)

    {

      cdev_del(&cdev);   /*注销设备*/

      unregister_chrdev_region(devno, 2); /*释放设备号*/

    }

    MODULE_LICENSE("GPL");

    module_init(memdev_init);

    module_exit(memdev_exit);

    第4课-字符驱动访问揭秘

             因为内核的原因,应用程序才能访问驱动程序。

  • 相关阅读:
    java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header.
    spring-session-data-redis依赖冲突问题
    centos7启动iptables时报Job for iptables.service failed because the control process exited with error cod
    图片上传后台服务报内存溢出 Out Of Memory Java heap space
    mysql 数据库密码忘记重置 进行远程连接
    打Jar包
    Type interface com.innovationV2.mapper.UserMapper is not known to the MapperRegistry
    关于java基础类型Integer String的clone()
    clion使用clang编译
    token & refresh token 机制总结
  • 原文地址:https://www.cnblogs.com/free-1122/p/11452259.html
Copyright © 2020-2023  润新知