ARM Linux字符设备驱动程序

2024-08-21  

1、主设备号和次设备号(二者一起为设备号):

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反  映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。


linux内核中,设备号用dev_t来描述,2.6.28中定义如下:


typedef u_long dev_t;

在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。


可以使用下列宏从dev_t中获得主次设备号:                   


也可以使用下列宏通过主次设备号生成dev_t:

MAJOR(dev_t dev);


MKDEV(int major,int minor);


MINOR(dev_t dev);


//宏定义:

#define MINORBITS    20

#define MINORMASK    ((1U << MINORBITS) - 1)  //1U后面的U表示1是unsigned int (如果是在VS编译器下的话,就是32位),把1U左移(<<,这是位运算符号,按位左移)20位就相当于1 * 2^20,然后-1,也就是mask替代了(2^20-1)。

                                              //得到的结果就是高12位全为0,低20位全为1

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

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

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


2.分配设备号(两种方法)


(1静态申请


int register_chrdev_region(dev_t from,unsigned count,const char *name);


 (2)动态分配


int alloc_chardev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);


注销设备号:


void unregister_chrdev_region(dev_t from, unsigned count);


创建设备文件:


利用cat /proc/devices查看申请到的设备名,设备号。


(1)使用mknod手工创建:mknod filename type  major minor


 (2) 自动创建:


利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox


配置,在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create


创建对应的设备。


3、字符设备驱动程序重要的数据结构:

(1)struct file:


代表一个打开的文件描述符,系统中每一个打开的文件在内核中都有一个关联的struct file。它由内核在open时创建,并传递给在文件上操作的任何函数,直到最后关闭。当文件的所有实例都关闭之后,内核释放这个数据结构。


 //重要成员


const struct file_operations *f_op;  //该操作是定义文件关联操作的。内核在执行open时对这个指针赋值。


 off_t f_pos;                  //该文件读写位置。


 void  *private_data;          //该成员是系统调用时保存状态信息非常有用的资源。


  2)struct inode:


用来记录文件的物理信息。它和代表打开的file结构是不同的。一个文件可以对应多个file结构,但只有一个inode结构。inode一般作为file_operations结构中函数的参数传递过来。


inode译成中文就是索引节点。每个存储设备或存储设备的分区(存储设备是硬盘、软盘、U盘 ... ... )被格式化为文件系统后,应该有两部份,一部份是inode,另一部份是Block,Block是用来存储数据用的。而inode呢,就是用来存储这些数 据的信息,这些信息包括文件大小、属主、归属的用户组、读写权限等。inode为每个文件进行信息索引,所以就有了inode的数值。操作系统根据指令, 能通过inode值最快的找到相对应的文件。


 dev_t i_rdev;   //对表示设备文件的inode结构,该字段包含了真正的设备编号。


 struct cdev *i_cdev;   //是表示字符设备的内核的内部结构,当inode指向一个字符设备文件时,该字段包含了指向struct_cdev结构的指针。


我们也可以使用下边两个宏从inode中获得主设备号和次设备号


unsigned int iminor(struct inode *inode);


unsigned int imajor(struct inode *inode);


 


 (3)struct file_operations


   struct file_operation ***_ops={


 .owner=THIS_MODULE,


 .llseek=***_llseek,


 .read=***_read,


 .write=***_write,


 .ioctl=***_ioctl,


 .open=***_open,


 .release=***_release,


 .....


};   //strude file_operations 是一个函数指针的集合


struct module *owner;


字符设备驱动程序注意事项


 1)open()调用可能由于几个原因而失败。


(2)成功运行的read()和write()返回的字节数可能是1至请求的字节数之间的任意值,因此应用程序必须能处理这些情况。

(3)即使1字节的数据读或写就绪,select()也会返回成功。

(4)很多字符驱动程序方法是可选的,并不是所有的方法都提供。

另外,字符驱动程序不仅在drivers/char/目录下。下面是一些“超级”字符驱动程序:

(1)串行驱动程序,放在drivers/serial/目录下。

(2)输入驱动程序,放在drivers/input/目录下。

(3)帧缓存区(/dev/fb/*)提供对显存的访问,/dev/mem提供对系统内存的访问途径。

(4)一些设备类支持少量采用字符接口的硬件。

(5)一些子系统提供额外的字符接口,以向用户空间提供原始的设备模型。例如MTD子系统。

(6)一些内核层提供钩子,通过导出相应的字符接口实现用户空间的设备驱动程序。 例如ioctl。

在drivers/目录下的register_chrdev上运行grep-r可了解内核中字符驱动程序的大致情况。

 


 设备驱动程序是内核的一部分,它完成以下的功能


1、对设备初始化和释放;


(1)字符设备cdev结构体初始化:


***********不是每个字符设备驱动都需要,cdev是为了构建设备模型,便于设备文件的管理所产生的。如果你的字符设备比较简单或者你不需要构建设备模型,是可以不需要cdev.

file_operation结构是虚拟层上的东西,这样使得驱动程序可以操作设备。*******************

           内核中每个字符设备都对应一个 cdev 结构的变量,下面是它的定义: linux-2.6.22/include/linux/cdev.h

struct cdev {

   struct kobject kobj;          // 每个 cdev 都是一个 kobject

   struct module *owner;       // 指向实现驱动的模块

   const struct file_operations *ops;   // 操纵这个字符设备文件的方法

   struct list_head list;       // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头

   dev_t dev;                   // 起始设备编号

   unsigned int count;       // 设备范围号大小

};

        静态内存定义初始化:

                        struct cdev my_cdev;

                        cdev_init(&my_cdev, &fops);

                        my_cdev.owner = THIS_MODULE;


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

                        {

                           memset(cdev, 0, sizeof *cdev);

                           INIT_LIST_HEAD(&cdev->list);

                           kobject_init(&cdev->kobj, &ktype_cdev_default);

                           cdev->ops = fops;

}      


      


        动态内存定义初始化:

                        struct cdev *my_cdev = cdev_alloc();

                        my_cdev->ops = &fops;

                        my_cdev->owner = THIS_MODULE;

                        struct cdev *cdev_alloc(void)

                        {

                         struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);

                         if (p) {

                                 INIT_LIST_HEAD(&p->list);

                                 kobject_init(&p->kobj, &ktype_cdev_dynamic);

                                }

                         return p;

                        }

 


        


             由此可见,两个函数完成都功能基本一致,只是 cdev_init() 还多赋了一个 cdev->ops 的值。

      初始化 cdev 后,需要把它添加到系统中去。为此可以调用 cdev_add() 函数。传入 cdev 结构的指针,起始设备编号,以及设备编号范围。

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

{

   p->dev = dev;

   p->count = count;

   return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);

}

关 于 kobj_map() 函数就不展开了,我只是大致讲一下它的原理。内核中所有都字符设备都会记录在一个 kobj_map 结构的 cdev_map 变量中。这个结构的变量中包含一个散列表用来快速存取所有的对象。kobj_map() 函数就是用来把字符设备编号和 cdev 结构变量一起保存到 cdev_map 这个散列表里。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。

当一个字符设备驱动不再需要的时候(比如模块卸载),就可以用 cdev_del() 函数来释放 cdev 占用的内存。

void cdev_del(struct cdev *p)

{

   cdev_unmap(p->dev, p->count);

   kobject_put(&p->kobj);

}

其中 cdev_unmap() 调用 kobj_unmap() 来释放 cdev_map 散列表中的对象。kobject_put() 释放 cdev 结构本身。

 


下面这个是必须要有的:


设备初始化


 


 static int first_drv_init(void)           

 {

   major=register_chrdev(0,'first_drv',&first_drv_fops); //注册,告诉内核。

   firstdrv_class=class_create(THIS_MODULE,'firstdrv');   //创建一个类

   if(IS_ERR(firstdrv_class))

   return PTR_ERR(firstdrv_class);

   

   firstdrv_class_dev=class_device_create(firstdrv_class,NULL,MKDEV(major,0),NULL,'xyz');//xyz为设备名字

   gpfcon=(volatile unsigned long *)ioremap(0x56000050,16);

   gpfdat=gpfcon+1;

   if(unlikely(IS_ERR(firstdrv_class_dev)))

   return PTR_ERR(firstdev_class_drv);

   return 0;

 }

 

 设备卸载

 static vodi first_drv_exit(void)

 {

   unregister_chrdev(major,'first_drv');//卸载

   class_device_unregister(firstdrv_class_dev);

   class_destroy(firstdrv_class);

   iounremap();

 }

 

 


  2、把数据从内核传送到硬件(copy_to_user)和从硬件读取数据(copy_form_user);


 结构体file_operations在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的 事务的函数的地址。


     fileoprations结构体上定义的函数进行具体的数据操作。


 


static struct  file_operations second_drv fops={

        .owner=THIS_MODULE;

        .write=second_drv_write;

        .open=second_drv_open;

        .read=second_drv_read;

        }    

        

      int major;

 

 


      


static int first_drv_open(struct inode *inode,struct file *file)

    {

     gpfcon &= ~((0x3<<(4*2))|(0x3<<(5*2))|(0x3<<(6*2)));    //GPF4,5,6为输出

     gpfcon |=  ((0x1<<(4*2))|(0x1<<(5*2))|(0x1<<(6*2)));    //

     

     return 0;

    }

 

 static ssize_t first_drv_read(struct inode *inode,struct file *file)

    {

      

      return 0;

    }

    

    static ssize_t first_drv_write(struct file *file,const char _user *buf,size_t count,loff_t *ppos)

    {

      int val;

文章来源于:电子工程世界    原文链接
本站所有转载文章系出于传递更多信息之目的,且明确注明来源,不希望被转载的媒体或个人可与我们联系,我们将立即进行删除处理。