Chinaunix首页 | 论坛 | 博客
  • 博客访问: 80716
  • 博文数量: 11
  • 博客积分: 1514
  • 博客等级: 上尉
  • 技术积分: 150
  • 用 户 组: 普通用户
  • 注册时间: 2009-09-19 19:10
文章分类
文章存档

2010年(11)

我的朋友

分类: 嵌入式

2010-06-08 23:06:58

    卡尔说过:“在科学上没有平坦的大道,只有那些不畏劳苦而沿着陡峭的山路攀登的人,才有可能达到辉煌的顶点!”
    在深刻了解“Hello World”之后,我们再向前迈进一小步,真正在linux下进行设备驱动程序开发。
Linux 系统的设备分为字符设备(char device),块设备(block device)和网络设备(network device)三种。字符设备是指存取时没有缓存的设备。块设备的读写都有缓存来支持,并且块设备必须能够随机存取(random access),字符设备则没有这个要求。典型的字符设备包括鼠标,键盘,串行口等。块设备主要包括硬盘软盘设备,CD-ROM等。一个文件系统要安装进入操作系统必须在块设备上。网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD unix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。
    首先我们要接触是字符设备驱动程序,此类驱动程序适合于大多数简单的硬件设备,比起块设备或网络设备驱动程序更加容易理解。那么要学习掌握它,要有以下几方面的预备知识。
一、设备编号在内核中的表达
    设备编号即主设备号和次设备号,在内核中用dev_t类型(在中定义)来表达。在内核2.6版本中,dev_t是一个32位的数,其中的12位有来表示主设备号,而其余20位用来表示次设备号。
    掌握以下三个宏(见),它们用来处理dev_t和设备编号之间的转换
:
 dev_t -> 设备编号
主设备号:  MAJOR(dev_t dev)  
次设备号:  MINOR(dev_t dev)  
 设备编号-> dev_t
MKDEV(int major, int minor) 
二、分配和释放设备编号
    在建立一个字符设备之前,驱动程序首先要获得一个或多个设备编号,可以通过两种途径完成:

指定设备编号
int register_chrdev_region(dev_t frist, unsigned int count, char *name);

动态分配
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
    这些分配的设备编号不再使用时,应该释放掉,使用的函数为:
释放编号 void unregister_chrdev_region(dev_t first,unsigned int count);
    以上三个函数中声明。    
温馨提示:分配主设备号的最佳方式:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。

三、字符设备注册 
    注册字符设备有两种方式,一种是2.6内核以前的早期方式及2.6内核开始的新技术,当然2.6内核兼容前者。
    早期方式:
    它是注册一个字符设备驱动程序的经典方式。

int register_chrdev(unsigned int major,const char *name,
                    struct file_operations *fops);
    其中,major是设备的主设备号,name是驱动程序的名称,fops为指向操作这个设备方式的file_operations结构指针。
    与注册相对的操作就是将设备从系统中移除,早期调用下面函数:
int unregister_chrdev(unsigned int major,const char *name);
    新方法:
    在这里就必须引进一个新的结构体——struct cdev,它就用来表示字符设备(见)。在内核调用设备的操作之前,必须分配并注册一个或多个struct cdev
struct cdev{
              struct kobject kobj;        
              struct module *owner;        //  所属模块
              struct file_operations *ops; //  操作字符设备的方式
              struct list_head list;
              dev_t  dev;                 //  设备编号
              unsigned int count;
};
     新技术下,分配和初始化字符设备有两种方式:静态方式和动态方式。
    静态内存定义初始化:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
    动态内存定义初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
    两种使用方式的功能是一样的,只是使用的内存区不一样,一般视实际的数据结构需求而定
    提示:cdev_initcdev_alloc两个函数完成的功能基本一致(实现过程见内核fs目录下char_dev.c文件),只是cdev_alloc没有对cdev结构中ops字段进行初始化,需要其函数体外进行。另外两个方式都需要对cdev结构中的owner字段进行单独初始化。
    注意:尽信书不如无书。LDD中文版60页很容易让人产生先调用
cdev_alloc再调用cdev_init误解。
    到此为止,我们的事还没完。接下来应该告诉内核该结构信息:
int cdev_add(struct cdev *dev,dev_t num,unsigned int count);
    牢记一点:调用这个函数可能会失败。如果它返回一个负的错误码,则设备不会被添加到系统中。
    从系统中移除一个字符设备:
void cdev_del(struct cdev *dev);
四、一些重要的数据结构
大部分基本的驱动程序操作涉及及到三个重要的内核数据结构,分别是file_operations、file和inode,它们的定义都在

  
struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带, 是一系列指针的集合,每个被打开的文件都对应于一系列的操作,这就是file_operations,用 来执行一系列的系统调用。
    struct file
代表一个打开的文件,在执行file_operation中 的open操作时被创建,这里需要注意的是与用户空间inode指 针的区别,一个在内核,而file指针在用户空间,由c库 来定义。

    struct inode
ITPUB个 人空间MPd+L:R6[T#ZK:         被内核用来代表一个文件,注意和struct file的 区别,struct inode一个是代表文件,struct file一个是代表打开的文件
  
struct inode包括很重要的二个成员:
   
dev_t       i_rdev;          设备文件的设备号
  
struct cdev  *i_cdev;   代表字符设备的数据结构
     struct inode结构是用来在内核内部表示文件的。同 一个文件可以被打开好多次,所以可以对应很多struct file,但是只对应一个struct inode。
五、具体应用
    对于驱动一个字符设备,掌握上面的知识点是必须的,不难。但难的是如何灵活有效地去使用它们,这才是我们所追求的。
    贯穿整个LDD第三章,作者在给我们介绍一个真正的设备驱动程序:scull。现在我把它抽出来进行集中的讲解。scull是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个设备。因此它与硬件无关,只是对内核分配的内存进行操作。
    1) 首先我们有必要掌握两个数据结构(scull_dev和scull_qset)及由它们构成scull设备的结构关系,这有助于我们理解scull设备驱动程序。
    scull_dev结构用来表示一个scull设备,该结构如下:
struct scull_dev{
    struct scull_qset *data;  /* 指向第一个量子集的指针 */
    int quantum;              /* 当前量子的大小 */    
    int qset;                 /* 当前数组的大小 */
    unsigned long size;       /* 保存在其中的数据总量 */
    unsigned int access_key;  /* 由sculluid和scullpriv使用 */
    struct semaphore sem;     /* 互斥型信号量 */
    struct cdev cdev;         /* 字符设备结构 */
};
    可以看出,scull设备把cdev结构嵌入其中,我们的注意力要集中在cdev上,cdev才是内核与设备间的接口。
    scull_qset结构用来对scull设备的数据进行处理:
struct scull_qset {
     void **data;
     struct scull_qset *next;
};
    两个数据结构即构成scull设备的模型,它们之间的关系如下图:

    如上图,在scull中,每个设备都是一个指针链表,其中每个指针指向一个scull_qset结构。

scull_qset结构指向一个中间指针数组,数据大小由scull_dev中的qset字段表示,我们称这样一个数组为量子集。指针数组中的每一个元素指向一个内存区(称之为量子),量子的大小由scull_dev中的quantum字段表示。
    2) 获取scull设备编号
    scull驱动程序获取设备编号采用就是动态分配和指定设备编号相结方式,具体代码如下:
if (scull_major) {
    dev = MKDEV(scull_major, scull_minor);
    result = register_chrdev_region(dev, scull_nr_devs, "scull");
} else {
    result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,"scull");
    scull_major = MAJOR(dev);
}
if (result < 0) {
    printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
    return result;
}
    3) 注册scull设备
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
    int err, devno = MKDEV(scull_major, scull_minor + index);
    
    cdev_init(&dev->cdev, &scull_fops);
    dev->cdev.owner = THIS_MODULE;
    // dev->cdev.ops = &scull_fops; 
    //这句可以省略,在cdev_init中已经做过
    err = cdev_add (&dev->cdev, devno, 1);
    /* Fail gracefully if need be 这步值得注意*/
    if (err)
        printk(KERN_NOTICE "Error %d adding scull%d", err, index);
}
    4) 实现对scull设备的操作
    open
    open提供给驱动程序以初始化的能力,为以后的操作作准备。应完成的工作如下:
    (1) 检查设备特定的错误(如设备未就绪或硬件问题);
    (2) 如果设备是首次打开,则对其进行初始化;
    (3) 如有必要,更新f_op指针;
    (4) 分配并填写置于filp->private_data里的数据结构。
    而根据scull的实际情况,open函数只要完成第四步,即将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用。从传入给open函数的实参inode类型数据,我们只能得到cdev结构,但我们希望得到包含cdev结构的scull_dev结构。幸好linux内核可以帮我们实现,可通过定义在中的container_of宏得以实现

container_of(pointer, container_type, container_field)

其作用就是:通过指向container_field字段的pointer指针,获得container_type结构指针,container_type结构包含container_field字段

     release
release
方法提供释放内存,关闭设备的功能。应完成的工作如下:
(1) 释放由open分配的、保存在file->private_data中的所有内容;
(2) 在最后一次关闭操作时关闭设备。
由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。

read和write
read和write方法的主要作用就是实现内核与用户空间之间的数据拷贝。因为Linux的内核空间和用户空间隔离的,所以要实现数据拷贝就必须使用在中定义的

unsigned long copy_to_user(void __user *to,
                           const void *from,
                           unsigned long count);
unsigned long copy_from_user(void *to,
                             const void __user *from,
                             unsigned long count);

而值得一提的是以上两个函数和

#define __copy_from_user(to,from,n)    (memcpy(to, (void __force *)from, n), 0)
#define __copy_to_user(to,from,n)    (memcpy((void __force *)to, from, n), 0)

之间的关系:通过源码可知,前者调用后者,但前者在调用前对用户空间指针进行了检查。

     5)模块测试
    模块测试使用平台:Linux2.6.16内核、友善之臂QQ2440开发板
    a、编译scull设备驱动程序:make modules。将生成的模块scull.ko通过nfs放到开发板中的/lib目录下。
     b、在/lib目录下加载模块:insmod scull.ko
     c、查看动态分配得到的主设备号:cat /proc/devices
[root@FriendlyARM /lib]# insmod scull.ko
[root@FriendlyARM /lib]# cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
...
232 buttons
253 scull
254 devfs
 
Block devices:
  1 ramdisk
  7 loop 
...
    d、将分配到的主设备号建立设备文件:mknod /dev/scull0 c 253 0
[root@FriendlyARM /lib]# mknod /dev/scull0 c 253 0
    此时在/dev目录下可以看到多一个scull0文件
    e、编译用户测试程序:make。并将得到可执行文件scull_test同样通过nfs放到开发板的某个目录下。
    f、可执行文件:./scull_test。结果显示如下:
[root@FriendlyARM /mnt]# ./scull_test
write ok! code=20
read ok! code=20
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19
    g、进入/lib目录卸载模块:rmmod scull
[root@FriendlyARM /lib]# rmmod scull
    这时可通过cat /proc/devices命令查看到,主设备号253没有了,卸载成功。
    h、重新加载模块,同时修改模块中的某些参数,看一下实验结果:
[root@FriendlyARM /lib]# insmod scull.ko scull_quantum=6 scull_qset=2
[root@FriendlyARM /lib]# cd /mnt
[root@FriendlyARM /mnt]# ls
hello       scull       scull_test
[root@FriendlyARM /mnt]# ./scull_test
write error! code=6
write error! code=6
write error! code=6
write ok! code=2
read error! code=6
read error! code=6
read error! code=6
read ok! code=2
[0]=0 [1]=1 [2]=2 [3]=3 [4]=4
[5]=5 [6]=6 [7]=7 [8]=8 [9]=9
[10]=10 [11]=11 [12]=12 [13]=13 [14]=14
[15]=15 [16]=16 [17]=17 [18]=18 [19]=19
   实验结束。前后两次实验验证了模块的读写能力,同时验证了对量子的有效读写。每一次读写最多只处理一个量子。

附件:scull设备驱动程序及用户测试程序:


文件:3_scull.tar.gz
大小:5KB
下载:下载

感谢:本文要特别素未谋面及交流的网友Tekkaman Ninja,他的博客使我受益匪浅。


  

阅读(1744) | 评论(0) | 转发(0) |
0

上一篇:LDD--Hello World

下一篇:LDD--调试技术

给主人留下些什么吧!~~