字符设备驱动之常用内核函数(一)

       字符设备驱动含有open、read、write、ioctl等函数,用于用户层和内核之间的通信,所以当用户要获得内核驱动的一些数据或者发送一些控制命令,就需要使用设备驱动了。对于一些中断类型的驱动,比如输入子系统,用户层不需要对其进行操作,可以不使用设备驱动。

1.register_chrdev

       说起字符设备驱动程序,肯定少不了register_chrdev函数,它在内存中分配了一块区域用于字符设备驱动的热拔插。这个区域里面的驱动拥有相同的主设备号,不同的次设备号,但它们都指向同一个file_operations结构体。就好比USB接口,一台电脑上有好几个USB接口,它们的属性都是一样的,鼠标接上去后左键右键功能都是相同的,这是因为它们有相同的usb_device结构体。

       对于linux2.x的内核一般都是采用这种方式注册字符设备驱动,但这种方式的缺点是同一结构体下分配了过多的字符设备驱动的接口。所以linux3.x的内核对其进行了改进,register_chrdev函数可以拆分成3给独立的函数来对字符设备驱动进行注册。register_chrdev = register_chrdev_region/alloc_chrdev_region + cdev_init + cdev_add

       如果主设备号已经指定了,就是定义了major = ?,就使用register_chrdev_region来开辟驱动的接口;如果让内核自动分配major的值,则使用alloc_chrdev_region。具体的函数形式如下:

         if(major){
               devid = MKDEV(major,0);   //次设备号
               error = register_chrdev_region(devid,MAXPIN,"ledkey");
               if(error<0){
                     printk("register_chrdev_region error!\n");
                     return -1;
                    }
          }else{
                     error = alloc_chrdev_region(&devid,0,MAXPIN,"ledkey");
                     if(error<0){
                           printk("alloc_chrdev_region error!\n");
                           return -1;
                        }
                     major = MAJOR(devid); 
          }
         cdev = cdev_alloc();
 
         cdev_init(cdev,&key_fops);
         cdev_add(cdev,devid,MAXPIN);

        这里要说一下cdev_alloc()这个函数,如果是static struct cdev *cdev这么定义的,则使用到cdev_alloc,如果定义的是结构体而不是指针(static struct cdev cdev),就不需要使用cdev_alloc这个函数了。宏定义MAXPIN表示对该file_operations结构体可以支持的设备驱动个数。

        对于register_chrdev我们注册了file_operations结构体之后,device_create(fb_class, fb_info->device,  MKDEV(FB_MAJOR, i), NULL, "fb%d", i)的时候这些设备的主设备号相同,次设备号不同,它们在同一类下面,尽管可以执行open("/dev/fb0", O_RDWR), open("/dev/fb1", O_RDWR) ... 但它们其实执行的是同一个open函数,即register_chrdev注册的fops,所以可以实现一类设备的驱动,写好通用的file_operations,当执行open时,根据次设备号得到一个和/dev/fb0相关的结构体,执行里面的open函数。在这个通用的程序里面register_chrdev然后创建类class_create,,在实例化设备下创建设备device_create就可以了,,这些实例化设备都在相同的类下面创建。所以有/dev/fb0, /dev/fb1, 同样有/sys/class/fb/fb0和/sys/class/fb/fb1。register_chrdev这个类下面255个设备的fops都一样,如果将register_chrdev分三步来注册,在alloc_chrdev_region/register_chrdev_region里面可以指定fops被哪些次设备号共享。

2. class_create

        这个函数用来创建类,在类下创建具体的设备。创建设备正如英文名,是device_create(这是linux3.x的内核,对于2.x的内核使用的是class_device_create)。为了方便理解,具体的函数形式如下:

                       key_class = class_create(THIS_MODULE,"keyled"); //"keyled"类的名字
                       device_create(key_class,NULL,MKDEV(major,0),NULL,"key0");// 创建了设备/dev/key0

3. request_irq

        具体形式如下:

        error = request_irq(IRQ_EINT0,key_handler,IRQ_TYPE_EDGE_BOTH,"key1",NULL);

        这里要注意的是第一个参数是中断的类型,像上面直接写的IRQ_EINT0,这样就启动了外部中断0,在Linux内核里面就不需要再像之前的裸板程序一样配置相关引脚为中断模式了,这里内核已经帮我们做好了;然后第二个参数是中断服务函数,发生中断之后进入这个函数。第三给参数很重要,它是中断的触发类型,这里我用的是双边沿触发。如果只有一个中断,最后一个参数写NULL就可以了,如果有多个中断,最好是分配一个结构体,里面记录每个中断的信息,这些中断可以使用相同的中断服务函数,根据中断信息就可以知道是哪个中断发生了。

       比如: /*中断*/

       struct input_inode {
               char *name;
               unsigned int irq_type;
               unsigned int pin;
               int key_val;
       };

       static struct input_inode key[] = {
                 [0] = {
                        "L",
                      IRQ_EINT0,
                     S3C2410_GPF(0),
                      KEY_L},
                [1] = {
                       "S",
                     IRQ_EINT2,
                    S3C2410_GPF(2),
                    KEY_S},
                [2] = {
                        "ENTER",
                   IRQ_EINT11,
                   S3C2410_GPG(3),
                  KEY_ENTER},
        };


       for(i = 0; i < 3; i++){
                 error = request_irq(key[i].irq_type,key_irq,IRQ_TYPE_EDGE_BOTH,key[i].name,&key[i]);
                 if(error){
                       printk("couldn't get irq!\n");
                       return -1;
                    }
        }

       而发生中断后,进入static irqreturn_t key_irq(int irq, void *dev_id),它的dev_id正是request_irq的最后一个参数&key[i],通过(&key[i])->pin我们就能知道到底是GPF0,还是GPF2或者是GPG3发生了中断,也就是外部中断0,2或者11处发生了中断。

4 .时间函数

      首先定义一个timer_list结构体static struct timer_list key_time。

       再初始化函数里加上    init_timer(&key_time);
                                            key_time.function = key_time_function;
                                            add_timer(&key_time);    

      然后中断服务函数里面加上 mod_timer(&key_time,jiffies + HZ/100); //延迟10ms,延迟过程中断仍然可以被触发                                                                                                       原先在中断服务函数里面实现的代码放到时间函数key_time_function里面。                                                                                                                    

       对于 mod_timer,+HZ表示延时1秒,/100就是10ms了。

       时间函数可以用于中断,去按键抖动。中断里启动时间函数后中断函数就结束了,它不管时间函数是否执行完成。这样在延时10ms的过程中中断又可以被触发,这就去了按键抖动,这是它与传统的delay函数的区别。它只要告诉内核多长时间后启动时间函数,mod_timer这个函数就执行完了,中断服务函数就执行完了,剩下的就等过完这段时间后内核启动时间函数key_time_function,然后执行里面的内容就可以了。

5. 睡眠唤醒

5.1 声明这个待唤醒事件

static DECLARE_WAIT_QUEUE_HEAD(dma_waitq);

5.2 休眠

wait_event_interruptible(dma_waitq, ev_dma);

5.3 唤醒

wake_up_interruptible(&dma_waitq);

5.4 过程分析

#define wait_event_interruptible(wq, condition)                \
({                                    \
    int __ret = 0;                            \
    if (!(condition))                        \
        __wait_event_interruptible(wq, condition, __ret);    \
    __ret;                                \
})

        从if条件语句可以看出,要进入休眠首先condition要等于0,其次再看宏__wait_event_interruptible:

#define __wait_event_interruptible(wq, condition, ret)            \
do {                                    \
    DEFINE_WAIT(__wait);                        \
                                    \
    for (;;) {                            \
        prepare_to_wait(&wq, &__wait, TASK_INTERRUPTIBLE);    \
        if (condition)                        \
            break;                        \
        if (!signal_pending(current)) {                \
            schedule();                    \
            continue;                    \
        }                            \
        ret = -ERESTARTSYS;                    \
        break;                            \
    }                                \
    finish_wait(&wq, &__wait);                    \
} while (0)

do...while(0)实际上也只是执行了一次循环,但却有重要意义:它使得__wait_event_interruptible可以像函数一样使用(实际上是宏),在程序中__wait_event_interruptible();就可以使用它。那为什么不直接将它定义为函数呢?因为它位于wait.h头文件,定义为函数,被不同的程序引用会造成重复定义。当然也可以像上面一样打一个()。

然后进入for(;;)循环,当condition == 1时break,然后执行finish_wait(&wq, &__wait),接着

if (!list_empty_careful(&wait->task_list)) {
        spin_lock_irqsave(&q->lock, flags);
        list_del_init(&wait->task_list);
        spin_unlock_irqrestore(&q->lock, flags);
    }

 

而wake_up_interruptible(&dma_waitq)做的事情是:

 

spin_lock_irqsave(&q->lock, flags);
__wake_up_common(q, mode, nr_exclusive,
0, key); spin_unlock_irqrestore(&q->lock, flags);

正好与上面的finish_wait(&wq, &__wait)对应,至于里面自旋锁的操作,我也不懂,我明白的是:

1. 要休眠就得让condition = 0,然后wait_event_interruptible(dma_waitq, ev_dma);

2. 要唤醒进程,就得先让condition = 1,然后wake_up_interruptible(&dma_waitq)

6 ioctl函数

ioctl像read、write一样读写:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)

{

     void __user *ubuf = (void __user *)arg;   //将arg当作用户层地址

     struct binder_write_read bwr;

     copy_from_user(&bwr, ubuf, sizeof(bwr));    //获取用户层传入内核数据

}

                                                                                                                                  

       

 

posted @ 2018-07-13 18:33  一条水煮鱼  阅读(586)  评论(1编辑  收藏  举报