Chinaunix首页 | 论坛 | 博客
  • 博客访问: 866243
  • 博文数量: 156
  • 博客积分: 6553
  • 博客等级: 准将
  • 技术积分: 3965
  • 用 户 组: 普通用户
  • 注册时间: 2010-06-22 18:36
文章存档

2012年(3)

2011年(43)

2010年(110)

分类: 嵌入式

2010-10-19 20:10:02

下面将以linux设备驱动开发详解上的globalmem设备驱动为例来详细分析字符设备驱动的过程。

 

#include //模块所需的大量符号和函数定义

#include

#include //文件系统相关的函数和头文件

#include

#include

#include //包含驱动程序使用的大部分内核API的定义,包括睡眠函数以及各种变量声明

#include //指定初始化和清除函数

#include //cdev结构的头文件 包含

#include

#include

#include //在内核和用户空间中移动数据的函数

 

#define GLOBALMEM_SIZE  0x1000  /*全局内存最大4K字节*/

#define MEM_CLEAR 0x1  /*0全局内存*/

#define GLOBALMEM_MAJOR 254    /*预设的globalmem的主设备号*/

 

static int globalmem_major = GLOBALMEM_MAJOR;

/*globalmem设备结构体*/

struct globalmem_dev                                    

{                                                       

  struct cdev cdev; /*cdev结构体*/                      

  unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/       

};

 

struct globalmem_dev *globalmem_devp; /*设备结构体指针*/

/*文件打开函数*/

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

{

  /*将设备结构体指针赋值给文件私有数据指针.

 

  系统在调用驱动程序的open方法前将这个指针置为NULL

  驱动程序可以将这个字段用于任意目的,也可以忽略这个字段。

  驱动程序可以用这个字段指向已分配的数据,

  但是一定要在内核释放file结构前的release方法中清除它*/

  filp->private_data = globalmem_devp;

  //将文件私有数据private_data指向设备结构体,readwriteioctlllseek等函数

  //通过private_data访问设备结构体

  return 0;

}

/*文件释放函数*/

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

{

  return 0;

}

/*

备方法应当进行下面的任务:

1、释放 open 分配在 filp->private_data 中的任何东西

2、在最后的 close 关闭设备globalmem_release的基本形式没有硬件去关闭, 因此需要的代码是最少的.不是每个 close 系统调用引起调用 release 方法. 只有真正释放设备数据结构的调用会调用这个方法

*/

/* ioctl设备控制函数 */

static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned

  int cmd, unsigned long arg)

{

  struct globalmem_dev *dev = filp->private_data;/*获得设备结构体指针*/

 

  switch (cmd)

  {

    case MEM_CLEAR://清除全局内存

      memset(dev->mem, 0, GLOBALMEM_SIZE);     

      printk(KERN_INFO "globalmem is set to zero\n");

      break;

 

    default://其他不支持的命令

      return  - EINVAL;

  }

  return 0;

}

 

/*读函数*/

static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size,

  loff_t *ppos)

{

  unsigned long p =  *ppos;

  unsigned int count = size;

  int ret = 0;

  struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

 

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

  if (p >= GLOBALMEM_SIZE)//0x1000 要读的偏移位置越界

    return count ?  - ENXIO: 0;

  if (count > GLOBALMEM_SIZE - p)//要读的字节数太大

    count = GLOBALMEM_SIZE - p;

 

  /*内核空间->用户空间*/

  //将内核空间的内容复制到用户空间,所复制的内容是从from来,到to去,复制n个字节。

  if (copy_to_user(buf, (void*)(dev->mem + p), count))

  {

    ret =  - EFAULT;

  }

  else

  {

    *ppos += count;

    ret = count;

   

    printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);

  }

 

  return ret;

}

 

/*写函数*/

static ssize_t globalmem_write(struct file *filp, const char __user *buf,

  size_t size, loff_t *ppos)

 /*

 filp 是文件指针,count 是请求的传输数据长度,buff 参数是指向用户空间的缓冲区,这个缓冲区或者保存要写入的数据,或者是一个存放新读入数据的空缓冲区,该地址在内核空间不能直接读写,offp 是一个指针指向一个"long offset type"对象, 它指出用户正在存取的文件位置. 返回值是一个"signed size type。写的位置相对于文件开头的偏移。

 */

{

  unsigned long p =  *ppos;

  unsigned int count = size;

  int ret = 0;

  struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

 

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

  if (p >= GLOBALMEM_SIZE)//0x1000 要写的偏移位置越界

    return count ?  - ENXIO: 0; //???

  if (count > GLOBALMEM_SIZE - p)//将数据写入该缓存区,直到结尾。这个是判断要写的字节数太多

    count = GLOBALMEM_SIZE - p;

   

  /*用户空间->内核空间*/

  if (copy_from_user(dev->mem + p, buf, count))

 //目的是从用户空间拷贝数据到内核空间,失败返回没有被拷贝的字节数,成功返回0

    ret =  - EFAULT;

  else

  {

    *ppos += count;

    ret = count;

   

    printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);

  }

 

  return ret;//如果ret=count,则完成了所请求数目的字节传送。

}

 

/* seek文件定位函数 */

/*

seek()函数对文件定位的起始地址可以是文件开头(SEEK_SET0)、当前位置(SEEK_CUR1)和文件尾(SEEK_END2),globalmem支持从文件开头和当前位置相对偏移。在定位的时候,应该检查用户请求的合法性,若不合法,函数返回- EINVAL,合法时返回文件的当前位置*/

static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)

{

  loff_t ret = 0;

  switch (orig)

  {

    case 0:   /*相对文件开始位置偏移*/

      if (offset < 0)

      {

        ret =  - EINVAL;

        break;

      }

      if ((unsigned int)offset > GLOBALMEM_SIZE)//偏移越界

      {

        ret =  - EINVAL;

        break;

      }

      filp->f_pos = (unsigned int)offset;//struct file

      ret = filp->f_pos;

      break;

    case 1:   /*相对文件当前位置偏移*/

      if ((filp->f_pos + offset) > GLOBALMEM_SIZE)//偏移越界

      {

        ret =  - EINVAL;

        break;

      }

      if ((filp->f_pos + offset) < 0)

      {

        ret =  - EINVAL;

        break;

      }

      filp->f_pos += offset;

      ret = filp->f_pos;

      break;

    default:

      ret =  - EINVAL;

      break;

  }

  return ret;//合法时返回文件的当前位置

}

 

/*文件操作结构体*/

static const struct file_operations globalmem_fops =

{

  .owner = THIS_MODULE,

  .llseek = globalmem_llseek,

  .read = globalmem_read,

  .write = globalmem_write,

  .ioctl = globalmem_ioctl,

  .open = globalmem_open,

  .release = globalmem_release,

};

 

/*初始化并注册cdev

globalmem_setup_cdev(globalmem_devp, 0);*/

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)

{

  int err, devno = MKDEV(globalmem_major, index);

 

  cdev_init(&dev->cdev, &globalmem_fops);

  /* cdev 结构嵌入一个你自己的设备特定的结构,你应当初始化你已经分配的结构使用以上函数,有一个其他的 struct cdev 成员你需要初始化. file_operations 结构,struct cdev 有一个拥有者成员,应当设置为 THIS_MODULE,一旦 cdev 结构建立, 最后的步骤是把它告诉内核, 调用:

   cdev_add(&dev->cdev, devno, 1);*/

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

{

       memset(cdev, 0, sizeof *cdev);

       INIT_LIST_HEAD(&cdev->list);

       cdev->kobj.ktype = &ktype_cdev_default;

       kobject_init(&cdev->kobj);

       cdev->ops = fops;

}  */

/*

struct cdev {

       struct kobject kobj;

       struct module *owner;

       const struct file_operations *ops;

       struct list_head list;

       dev_t dev;

       unsigned int count;

};

/*globalmem设备结构体*/

struct globalmem_dev                                    

{                                                       

  struct cdev cdev; /*cdev结构体*/                      

  unsigned char mem[GLOBALMEM_SIZE]; /*全局内存*/       

};

*/

  dev->cdev.owner = THIS_MODULE;

  dev->cdev.ops = &globalmem_fops;

  err = cdev_add(&dev->cdev, devno, 1);

 /*

 cdev cdev 结构, devno 是这个设备响应的第一个设备号, count 是应当关联到设备的设备号的数目. 常常 count 1, 但是有多个设备号对应于一个特定的设备的情形. 例如, 设想 SCSI 磁带驱动, 它允许用户空间来选择操作模式(例如密度), 通过安排多个次编号给每一个物理设备.在使用 cdev_add 是有几个重要事情要记住. 第一个是这个调用可能失败. 如果它返回一个负的错误码,

你的设备没有增加到系统中. 它几乎会一直成功, 但是, 并且带起了其他的点: cdev_add 一返回, 你的设备就是"活的"并且内核可以调用它的操作. 除非你的驱动完全准备好处理设备上的操作, 你不应当调用 cdev_add */

 

  if (err)

    printk(KERN_NOTICE "Error %d adding LED%d", err, index);

}

 

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

/*_init()__init是一个宏,编译时会用到。对于静态编译在内核的  模块有意义.

内核使用了大量不同的宏来标记具有不同作用的函数和数据结构。如宏__init__devinit等。

这些宏在include/linux/init.h头文件中定义。编译器通过这些宏可以把代码优化放到合适的内存位置,

以减少内存占用和提高内核效率。

init()标记内核启动时使用的初始化代码,内核启动完成后不再需要。以此标记的代码位于.init.text内存区域。

#define _ _init    _ _attribute_ _ ((_ _section_ _ (".text.init")))

初始化代码的特点是:在系统启动运行,且一旦运行后马上退出内存,不再占用内存。

*/

int globalmem_init(void)

{

  int result;

  dev_t devno = MKDEV(globalmem_major, 0);

  /*通过主次设备号生成dev_t.

主次设备号的数据类型是dev_t,在/linux/types.h中定义。

2.6内核中,dev_t是一个32位的数,其中12位用来表示主设备号,

其余20位用来表示次设备号。要获得设备的主次设备号可以使用内核提供的宏:

MAJOR(dev_t dev);        #获得主设备号

MINOR(dev_t dev);        #获得次设备号

这些宏定义位于linux/kdev_t.h中。如果要把主次设备号转换成dev_t类型,

则可使用:

MKDEV(int major, int minor);*/

 

  /* 申请设备号*/

  if (globalmem_major)/*静态申请设备号*/

    result = register_chrdev_region(devno, 1, "globalmem");

    /*linux/fs.h中,

    devno:是你要分配的起始设备编号. devno的次编号部分常常是 0, 但是没有要求是那个效果;

    1:是所请求的连续设备号的个数;

globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices/sysfs中。

如果分配成功则返回0,分配失败则返回一个负的错误码,所请求的设备号无效。

    */

  else  /* 动态申请设备号*/

  {

    result = alloc_chrdev_region(&devno, 0, 1, "globalmem");

    /*dev_t *dev

    dev:是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数;

    firstminor:应当是请求的第一个要用的次编号; 它常常是 0

    1     是所请求的连续设备号的个数;

    Globalmem:是和该设备号范围关联的设备名称,它将出现在/proc/devices/sysfs中。*/   

    globalmem_major = MAJOR(devno);/*获得主设备号,dev_t结构中分解出主设备号*/

  } 

 /*

 如果globalmem_major=0,利用udev工具自动向系统动态申请未被占用的设备号相关函数定义在include/linux/kdev_t.h

 #define MINORBITS       20

 #define MINORMASK   ((1U << MINORBITS) - 1)

 //(1<<20 -1) 此操作后,MINORMASK宏的低20位为1,高12位为0

 

 #define MAJOR(dev)       ((unsigned int) ((dev) >> MINORBITS))

 #define MINOR(dev)       ((unsigned int) ((dev) & MINORMASK))

 #define MKDEV(ma,mi)  (((ma) << MINORBITS) | (mi))

*/

  if (result < 0)

    return result;

   

  /* 动态申请设备结构体的内存*/

  globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);

  /*

  在设备驱动程序中动态开辟内存,不是用malloc,而是kmalloc,或者用get_free_pages直接申请页。释放内存用的是kfree,或free_pages. 请注意,kmalloc等函数返回的是物理地址!而malloc等返回的是线性地址!要注意kmalloc最大只能开辟128k-1616个字节是被页描述符结构占用了。内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数vremap获得重新映射以后的地址*/

  if (!globalmem_devp)    /*申请失败*/

  {

    result =  - ENOMEM;

    goto fail_malloc;

  }

  memset(globalmem_devp, 0, sizeof(struct globalmem_dev));

  /*memset会将参数globalmem_devp所指的内存区域中前sizeof(struct globalmem_dev)个字节以参数0填入,然后返回指向globalmem_devp的指针*/

 

  globalmem_setup_cdev(globalmem_devp, 0);

  /*初始化并注册cdev*/

  return 0;

 

  fail_malloc: unregister_chrdev_region(devno, 1);

  /*释放原先申请的设备号

  如果我们不再使用设备号,则要使用unregister_chrdev_region()函数释放它。

    devno:是要分配的主设备号范围的起始值,次设备号一般设置为0

    1:是所请求的连续设备号的个数;*/

  return result;

}

 

/*模块卸载函数*/

/*

__exit,标记退出代码,对于非模块无效。

*/

void globalmem_exit(void)

{

  cdev_del(&globalmem_devp->cdev);   /*注销cdev,为从系统去除一个字符设备*/

  kfree(globalmem_devp);     /*释放设备结构体内存*/

  unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);

  /*释放设备号

  我们一般在模块的清除函数中调用设备号释放函数。*/

}

 

MODULE_AUTHOR("Song Baohua");

MODULE_LICENSE("Dual BSD/GPL");//指定代码使用的许可证

 

module_param(globalmem_major, int, S_IRUGO);

/*module_param宏的第一个参数是选项名,可在/sys虚拟文件系统中该模块的parameter目录中中查看到。第二个参数是选项类型,第三个参数是选项的值*/

 

module_init(globalmem_init);

module_exit(globalmem_exit);

/*向操作系统注册自己定义的这两个函数,该项目在Kconfig中配置项目为布尔型的话为YN两种选项,Y为编译进内核,N不编译,如果为三态型(tristate),为YNM三种选项M为编译为模块,模块也可以在内核编译后根据需要进行手动加载,也可以写个脚本自动加载*/

 

 

总结:

字符设备是3大类设备(字符设备、块设备和网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_opersation结构体中的操作函数,实现file_opersation结构体中的read()write()ioctl()等函数是驱动设计的主体工作。

阅读(5468) | 评论(0) | 转发(2) |
给主人留下些什么吧!~~