3-10 字符型设备驱动模型

(本节笔记的实验代码,在这里

一.  使用字符设备驱动程序

1.  编译和安装驱动程序模块
    1.1  将驱动模块源码memdev.c复制到Linux系统中,编写Makefile,编译成内核模块memdev.ko并拷贝到开发板的NFS的rootfs中。
    2.2  把memdev.ko加载(insmod)到开发板的Linux系统内核中。

2.  创建设备文件
      通过字符设备文件,应用程序可以使用相应的字符设备驱动程序来控制字符设备。创建字符设备文件的方法有两种:1)使用mknod命令,如:【mknod /dev/文件名 c 主设备号 次设备号】;2)使用函数在驱动程序中创建。


    //可cat/proc/devices 查看已挂载的设备驱动的主设备号

    //而次设备号一般取非负切小于255的整数(0~254)

    //memdev.ko模块的功能是模拟一个硬件,通过往设备写入数据,模拟向硬件设备中写入数据

    创建设备文件:mknod/dev/memdev0 c 253 0

 

3.  应用程序通过设备文件访问设备

touch write_mem.c

 #include <stdio.h>

 #include <linux/types.h>

 #include <linux/stat.h>

 #include <fcntl.h>

 

 int main()

 {

    int src = 2015;

    int fd = 0;

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

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

    close(fd);

    return 0;

 }

                                                             』

 

    对write_mem.c进行交叉编译:arm-linux-gccwrite_men.c -o write_men

    把write_men拷贝到rootfs中,在开发板上运行write_men,若出现以下提示:~/bin/sh:./write_mem:notfound,证明开发板Linux系统中并不包含write_mem所需要的库(可通过arm-linux-readelf -d write_mem来检查该应用程序所需要的动态链接库),因此需要加入-static选项进行静态编译。

touch read_mem.c

 #include <stdio.h>

 #include <linux/types.h>

 #include <linux/stat.h>

 #include <fcntl.h>

 

 

 int main()

 {

    int dst = 0;

    int fd = 0;

    fd = open("/dev/memdev0",0_RDWR);

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

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

    close(fd);

    return 0;

 }

                                                             』

//通过read_mem把写入到设备中的数据读出来。


二.  字符设备驱动模型

    在Linux系统中,设备的类型繁多,如:字符设备、块设备、网络接口设备、USB设备、PCI设备、平台设备和混杂设备等,设备类型的不同意味着对应的驱动程序模型的不同。


0.  Linux设备驱动模型
  0.1  驱动程序初始化
    1)分配设备描述符
    2)初始化设备描述结构
    3)注册设备描述结构
    4)硬件初始化
  0.2  实现设备操作
  0.3  驱动注销


1.  驱动程序初始化
  1.0  基本概念
  1.0.1  设备描述结构cdev //#include <linux/cdev.h>
    任何一种驱动模型中,设备都会用内核中的一种结构来描述,而字符设备在内核中使用struct cdev来进行描述,具体内容如下:
    struct cdev{
        struct kobject kobj;
        struct module *cwner;
     ※ const struct file_operations *ops;      //设备操作集
        structlist_head list;
     ※ dev_t dev;              //设备号
     ※ unsigned int count;    //设备数
    }
  1.0.2  设备号
    主设备号——在/dev目录下的设备文件是通过主设备号,与内核设备驱动程序建立联系。
    次设备号——同一个设备驱动要支持多个同样的物理设备,就需要次设备号来进行区分。
    设备号的操作——Linux内核中使用dev_t类型来定义设备号,dev_t实质是32位unsigned int型数据,其中高12位为主设备号,低20位为次设备号。dev_t和主、次设备号间的转换函数为:
    dev_t dev = MKDEV(主设备号,次设备号)
    主设备号(int) = MAJOR(dev_t dev)  
    次设备号(int) = MINOR(dev_t dev)  
    设备号的分配——分为静态申请方式和动态申请方式两种:
    静态申请:用户选择一个数字作为主设备号,然后通过函数register_chrdev_region向内核申请使用,缺点是,如果申请使用的设备号已被内核中其他驱动占用,则申请失败。
    动态分配:使用alloc_chrdev_region函数,由内核分配一个可用的主设备号,其优点是内核知道哪些号可被调用,因此不会导致申请的设备号被占用的情况发生。※(一般采用这种方式进行主设备号的申请)函数原型如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
    dev:自动分配好的设备号所存放的dev_t类型数据地址。
    baseminor:指定次设备号起始号码。
    count:指定次设备号的个数。
    name:出现在/proc/devivers中的设备名。


    设备号的注销——驱动退出时,应使用unregister_chrdev_region函数释放设备号,避免系统资源的浪费。函数原型如下:
    void unregister_chrdev_region(dev_t from, unsigned count);
    from:分配的dev_t类型的起始设备号;
    count:需要注销设备的数量,一般为次设备号的数量。


  1.1  设备描述结构cdev的分配方式
    静态分配方式——struct cdev mdev;
    动态分配方式——struct cdev *pdev = cdev_alloc();


  1.2  初始化cdev
    struct cdev的初始化使用cdev_init函数来完成。原型如下:
    cdev_init(struct cdev *cdev, const struct file_operations *fops)
//关于struct file_operations操作函数集,参看下一小节。
    cdev:待初始化的cdev结构,先分配,后传进该函数。
    fops:设备操作函数集,先定义struct file_operations,把指针传进来。


  1.3  注册cdev
    struct cdev的注册使用cdev_add函数来完成。
    cdev_add(struct cdev *p, dev_t dev, unsigned count);
    p:待添加到内核的字符设备结构,也就是上面已初始化的cdev指针。
    dev:该设备驱动的设备号,先分配,后传进来。
    count:使用该设备驱动程序的设备个数。


  1.4  硬件初始化
  对硬件寄存器进行初始化。


2.  实现设备操作
  2.0  基本概念
  2.0.1  文件操作集struct file_operations // #include <linux/fs.h>
  函数原型为:
  struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
 loff_t len);
};
    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.0.2  文件描述结构struct file
    在Linux系统中,每一个打开的文件,在内核都会关联一个文件描述结构struct file,它由内核在打开文件时创建,在文件关闭后释放。
    重要成员有:
    loff_t f_pos  /* 文件读写指针 */
    struct file_operations *f_op    /* 该文件所对应的操作 */
  2.0.3  文件信息结构struct inode
    每一个存在于文件系统里面的文件都会关联一个inode结构,该结构主要用来记录文件物理上的信息。因此,它和代表打开文件的struct file结构是不同的。一个文件没有被打开时不会关联struct file结构,但却会关联一个struct inode结构。
    仲要成员有:、
    dev_t i_rdev     /* 设备号 */


  2.1 open
    int (*release)(struct inode *, struct file *);
    open设备方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open方法完成如下工作有:表明次设备号,以及启动设备。
    具体操作步骤是:
    1)获取次设备号:
      int num = MINOR(inode->i_rdev);
    2)把寄存器地址安装不同的次设备号传给struct file里面的private_data成员进行保存。
      if(num==0)
        filp->private_data = dev1_registers;
      else if(num ==1)
        filp->private_data = dev2_registers;
      else
        return -ENODEV;  //无效的次设备号


  2.2  close
    int (*release) (struct inode *, struct file *);
    release方法的作用正好与open相反,这个设备方法又是被称为close,主要工作为关闭设备。


  2.3  read


     read设备方法通常完成2件事情:1.从设备中读取数据(属于硬件访问累操作);2.将读到的数据返回给应用程序
     函数原型为:ssize_t (*read)(struct file *filp, char _user *buff, size_t count, loff_t *offp);
     filp:与字符设备文件关联的struct file结构指针,由内核创建。
     buff:从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。而该参数来源于用户空间的指针,这类指针无法被内核代码直接应用,因此不许用以下参数进行数据传递://#include <asm/uaccess.h>
      int copy_from_user(void *to, const void _user *from, int n);
      int copy_to_user(void _user *to, const void *from, int n);
      count:请求传输的数据量,由read系统调用提供该参数。
      offp:文件的读写位置,由内核从struct file结构中取出后,传递进来。
      实现步骤:
  1)获取设备寄存器基地址
    int *register_addr = filp->private_data;
  2)判断读取位置是否有效
    if (p >= 5*sizeof(int))
      return 0;
    if (count > 5*sizeof(int) - p)
      count = 5*sizeof(int) - p;
  3)读取数据到用户空间
    if (copy_to_user(buf, register_addr + p, count))
    {
      ret = -EFAULT;
    }
    else
    {
      *ppos += count;   //成功读取后,将影响文件读写指针;
      ret = count;
    }


  2.4 write
  write设备方法通常完成2件事:1.从应用程序提供的地址中取出数据;2.将数据写入设备(属于硬件访问类操作)。
  函数原型:ssize_t (*write)(struct file *filp, char _user *buff, size_t count, loff_t *offp);
  filp:与字符设备文件关联的struct file结构指针,由内核创建。
  buff:从设备读取到的数据,需要保存到的位置。由read系统调用提供该参数。而该参数来源于用户空间的指针,这类指针无法被内核代码直接应用,因此不许用以下参数进行数据传递:
    int copy_from_user(void *to, const void _user *from, int n);
    int copy_to_user(void _user *to, const void *from, int n);
  count:请求传输的数据量,由read系统调用提供该参数。
  offp:文件的读写位置,由内核从struct file结构中取出后,传递进来。


  2.5 llseek
    ssize_t (*read)(struct file *, char _user *, size_t, loff_t *);
    重定位文件读写指针,响应lseek系统调用。


3.驱动注销
  当需要从内核中卸载驱动程序时,需要使用cdev_del函数来完成字符设备的注销。
  具体步骤:
  1)注销设备
    cdev_del(&cdev);
  2)释放设备号
    unregister_chrdev_region(devno,2);


三.  字符型设备驱动的设计步骤

Linux设备驱动模型(步骤)
  1.  驱动程序初始化
    1)分配设备描述符——静态分配
    2)初始化设备描述结构——cdev_init
    3)注册设备描述结构——cdev_add
    4)硬件初始化
  2.  实现设备操作
    2.1打开设备——open
    2.2读设备数据read——copy_to_user
    2.3写数据到设备write——copy_from_user
    2.4读写位置重定位——lseek
2.5关闭设备——close
  3.  驱动注销
    3.1  注销设备——cdev_del
    3.2  释放设备号——unregister_chrdev_region


touch memdev.c

 #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);                                                                                                               』


四.  字符驱动的访问原理



    应用程序调用read系统调用,通过内核,把read系统调用转化为xx_read。
1.  对之前的read_mem应用程序进行反编译(arm-linux-objdump -D -S read_mem >dump)


  把read系统调用的编号(0X3)传进R7,调用SVC(系统调用指令)从用户态切换到内核态,进入位于/arch/arm/kernel/entry-common.S中的入口函数


  再从R7中取出编号,通过编号搜寻system call number


  通过搜索,得知表位于call.S中



根据R7中的0X3,为sys_read

而sys_read的实现代码在位于/fs/Read_write.c中的

SYSCALL_DEFINE3(read,unsignedint,fd,char __user *,buf,size_t,count)函数,该函数是一个宏,拆开后转化为一般看到的形式。

 

而该函数中包含以下代码:


通过搜索其实现函数为:



通过file->f_op->read(file,buf,count,pos)就能调用对模块中所对应的read的方法调用了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值