按键驱动程序设计---混杂设备、中断分层处理、工作队列、阻塞型驱动

混杂设备

概念:
在Linux系统中,存在一类字符设备,它们拥有相同的主设备号主设备号都为10,但次设备号不同,我们称这类设备为混杂设备(miscdevice)。所有的混杂设备形成一个链表,对设备访问时内核根据次设备号查找到相应的混杂设备。

设备描述符:
Linux中使用 结构体struct miscdevice 来描述一个混杂设备。

//至少前三项需要指明
struct miscdevice {
    int minor; /* 次设备号 */
    const char *name; /* 设备名 */
    const struct file_operations *fops; /* 文件操作 */
    struct list_head list;
    struct device *parent;
    struct device *this_device;
};

设备注册和注销
Linux中使用 misc_ register 函数 和 misc_ dregister函数 来注册或注销一个混杂设备驱动。

//注册:
int misc_register(struct miscdevice * misc)

//注销:
int misc_dregister(struct miscdevice * misc)

中断处理程序

中断处理流程:
这里写图片描述

首先获取中断号,根据中断号来找到中断处理程序,一个中断号可以对应多个中断处理程序,通过dev_id来区分。

中断注册:
request_irq函数用于注册中断。

int request_irq
(
    unsigned int irq,
    //中断号

    void (*handler)(int, void*, struct  pt_regs *), 
    //中断处理函数

    unsigned long flags,
    //与中断管理有关的各种选项

    const char *devname,
    //设备名

    void *dev_id
    //共享中断时使用
)

函数返回0表示成功,或者返回一个错误码。

在flags参数中,可以选择一些与中断管理有关的选项,如:

IRQF_ DISABLED(SA_INTERRUPT)
如果设置该位,表示是一个“快速”中断处理程序;如果没有设置这位,那么是一个“慢速”中断处理程序。

快/慢速中断的主要区别在于:快速中断保证中断处理的原子性(不被打断),而慢速中断则不保证。换句话说,也就是“开启中断”标志位(处理器IF)在运行快速中断处理程序时是关闭的,因此在服务该中断时,不会被其他类型的中断打断;而调用慢速中断处理时,其它类型的中断仍可以得到服务。

IRQF_ SHARED(SA_SHIRQ)
该位表明该中断号是多个设备共享的。

中断处理:
中断处理程序的特别之处是在中断上下文中运行的,它的行为受到某些限制:
1、不能使用可能引起阻塞的函数
2、不能使用可能引起调度的函数

这里写图片描述

注销中断:
当设备不再需要使用中断时(通常在驱动卸载时),应当把它们注销,使用函数:

void free_irq(unsigned int irq, void *dev_id)
//irq指定了中断号,然后用 dev_id 来区分共享中断。

中断分层处理

在慢速中断的情况下,如果正在处理某一中断,此时,发生了相同类型的中断,CPU会忽略后面发生的中断。在快速中断的情况下,CPU会忽略其他任何类型的中断。因此,在处理中断时,无论快速还是慢速中断都会产生忽略其他中断的情况,如果可以尽可能的缩短中断处理的时间,则可以让忽略的中断数量尽可能的减少。

因此,可以对中断进行分层的处理:
这里写图片描述
上半部:当中断发生时,它进行相应地硬件读写,并“登记”该中断。通常由中断处理程序充当上半部。
下半部:在系统空闲的时候对上半部“登记”的中断进行后续处理。

工作队列:
可以采用工作队列的的方式实现中断处理的分层:

工作队列是一种将任务推后执行的形式,它把推后的任务交由一个内核线程去执行。这样下半部会在进程上下文执行,它允许重新调度甚至睡眠。 每个被推后的任务叫做“工作”,由这些工作组成的队列称为工作队列

这里写图片描述
挂载的时候并不是work执行的时候,是内核线程来执行的work的工作。

Linux内核使用 struct work_struct 结构体 来描述一个工作队列:

struct workqueue_struct {
    struct cpu_workqueue_struct *cpu_wq;
    struct list_head list;
    const char *name; /*workqueue name*/
    int singlethread;
    int freezeable; /* Freeze threads during suspend */
    int rt;
};

Linux内核使用 struct work_struct 结构体 来描述一个工作项:

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
};
typedef void (*work_func_t)(struct work_struct *work);

工作流程:
1、创建工作队列 create_workqueue
2、创建工作 INIT_WORK
3、提交工作 queue_work

代码范例:

#include <linux/init.h>
#include <linux/module.h>

struct workqueue_struct *my_wq; //工作队列结构体
struct work_struct *work1;      //工作结构体
struct work_struct *work2;

MODULE_LICENSE("GPL");

void work1_func(struct work_struct *work)
{
    printk("this is work1->\n");    
}

void work2_func(struct work_struct *work)
{
    printk("this is work2->\n");    
}

int init_que(void)
{   
    //1. 创建工作队列
    my_wq = create_workqueue("my_que");

    //2. 创建工作
    work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1, work1_func);

    //3. 挂载(提交)工作
    queue_work(my_wq,work1);

    //2. 创建工作(创建第二个work)
    work2 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work2, work2_func);

    //3. 挂载(提交)工作
    queue_work(my_wq,work2);

    return 0;
}

void clean_que(){ }

module_init(init_que);
module_exit(clean_que);

在大多数情况下, 驱动并不需要自己建立工作队列,只需定义工作, 然后将工作提交到内核已经定义好的工作队列 keventd_wq 中。
可以采用下面的函数,提交工作到默认队列: schedule_work 函数

使用默认工作队列范例代码:

#include <linux/init.h>
#include <linux/module.h>

struct work_struct *work1;
struct work_struct *work2;

MODULE_LICENSE("GPL");

void work1_func(struct work_struct *work)
{
    printk("this is work1->\n");    
}

void work2_func(struct work_struct *work)
{
    printk("this is work2->\n");    
}

int init_que(void)
{   
    //2. 创建工作
    work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1, work1_func);

    //3. 挂载(提交)工作,采用默认的工作队列,不需要创建工作队列
    schedule_work(work1);

    //2. 创建工作
    work2 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work2, work2_func);

    //3. 挂载(提交)工作
    schedule_work(work2);

    return 0;
}

void clean_que(){ }

module_init(init_que);
module_exit(clean_que);

采用定时器来按键去抖

按键抖动
按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,开关不会马上稳定地接通或断开。因而在闭合及断开的瞬间总是伴随有一连串的抖动。

按键去抖动的方法主要有二种,一种是硬件电路去抖;另一种就是软件延时去抖。而延时又一般分为二种,一种是for循环等待,另一种是定时器延时。在操作系统中,由于效率方面的原因,一般不允许使用for循环来等待,只能使用定时器。

内核定时器
Linux内核使用 struct timer_list 结构体 来描述一个定时器:

struct timer_list {
    struct list_head entry;
    unsigned long expires;
    void (*function)(unsigned long);
    unsigned long data;
    struct tvec_base *base;
};

这里写图片描述

定时器范例代码见下方。

阻塞型驱动程序设计

阻塞必要性
当一个设备无法立刻满足用户的读写请求时应当如何处理? 例如:调用read时,设备没有数据提供, 但以后可能会有;或者一个进程试图向设备写入数据,但是设备暂时没有准备好接收数据。当上述情况发生的时候,驱动程序应当(缺省地)阻塞进程,使它进入等待(睡眠)状态,直到请求可以得到满足。

内核等待队列
在实现阻塞驱动的过程中,也需要有一个“ 候车室 ”来安排被阻塞的进程“休息”,当唤醒它们的条件成熟时,则可以从“候车室”中将这些进程唤醒。而这个“候车室”就是等待队列。

1、定义等待队列

wait_queue_head_t my_queue

2、初始化等待队列

init_waitqueue_head(&my_queue)

3、定义+初始化等待队列

DECLARE_WAIT_QUEUE_HEAD(my_queue)

4、进入等待队列,睡眠

wait_event(queue,condition)
/*
当condition(布尔表达式)为真时,立即返回;否则让进程进入TASK_UNINTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。
*/
wait_event_interruptible(queue,condition)
/*
当condition(布尔表达式)为真时,立即返回;否则让进程进入TASK_INTERRUPTIBLE的睡眠,并挂在queue参数所指定的等待队列上。
*/
int wait_event_killable(queue, condition)
/*
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_KILLABLE的睡眠,并挂在queue参数所指定的等待队列上。
*/

5、从等待队列中唤醒进程

wake_up(wait_queue_t *q)
/*
从等待队列q中唤醒状态为TASK_UNINTERRUPTIBLE,
TASK_INTERRUPTIBLE , TASK_KILLABLE 的所有进程。
*/
wake_up_interruptible(wait_queue_t *q)
/*
从等待队列q中唤醒状态为TASK_INTERRUPTIBLE 的进程。
*/

范例代码

驱动程序代码:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

#define GPFCON 0x56000050   //寄存器地址
#define GPFDAT 0x56000054

struct work_struct *work1;       //工作结构体
struct timer_list buttons_timer; //定时器结构体

unsigned int *gpio_data;
unsigned int key_num = 0;

wait_queue_head_t  key_q;  //定义等待队列

void work1_func(struct work_struct *work)
{
    mod_timer(&buttons_timer, jiffies + (HZ /10)); //启动定时器,100ms
    //jiffies是全局变量,表示当前时间。HZ 是 1 秒钟
}

void buttons_timer_function(unsigned long data)  //定时器超时函数
{
    unsigned int key_val;

    key_val = readw(gpio_data)&0x1; //定义两个按键
    if (key_val == 0)
       key_num = 4;

    key_val = readw(gpio_data)&0x4;
    if (key_val == 0)
        key_num = 3;

    wake_up(&key_q);  //唤醒等待队列中的进程
} 

irqreturn_t key_int(int irq, void *dev_id)
{
    //1. 检测是否发生了按键中断

    //2. 清除已经发生的按键中断

    //3. 提交下半部(中断处理程序)
    schedule_work(work1);

    //return 0;
    return IRQ_HANDLED;     
}

void key_hw_init()  //硬件初始化函数
{ 
    unsigned int *gpio_config;
    unsigned short data;

    gpio_config = ioremap(GPFCON,4);//转化为虚拟地址
    data = readw(gpio_config);      //读出数据
    data &= ~0b110011;              //设置中断模式
    data |= 0b100010;                

    writew(data,gpio_config);       //写回寄存器

    gpio_data = ioremap(GPFDAT,4);
}

int key_open(struct inode *node,struct file *filp)
{
    return 0;
}

ssize_t key_read(struct file *filp, char __user *buf, size_t size, loff_t *pos)
//支持应用程序对设备文件的读取
{ 
    wait_event(key_q,key_num); //进入等待队列,睡眠
    //在读数据时,如果没有数据可读,就需要进入等待队列

    printk("in kernel :key num is %d\n",key_num);
    copy_to_user(buf, &key_num, 4);

    key_num = 0;

    return 4;
}

struct file_operations key_fops = { //设备操作函数集
    .open = key_open,
    .read = key_read,
};

struct miscdevice key_miscdev = { //混杂设备结构体
    .minor = 200,
    .name = "key",
    .fops = &key_fops,  
};

static int button_init()
{
    int ret;
    ret = misc_register(&key_miscdev); //注册混杂设备

    if (ret !=0)
        printk("register fail!\n");

    //注册中断处理程序,两个按键分属不同的中断
    request_irq(IRQ_EINT0,key_int,IRQF_TRIGGER_FALLING,"key",(void *)4);//下降沿
    request_irq(IRQ_EINT2,key_int,IRQF_TRIGGER_FALLING,"key",(void *)3);

    //按键硬件初始化
    key_hw_init();

    //创建工作,将剩余的中断处理工作放到work中
    work1 = kmalloc(sizeof(struct work_struct),GFP_KERNEL);
    INIT_WORK(work1, work1_func);

    /* 初始化定时器 */  
    init_timer(&buttons_timer);   
    buttons_timer.function = buttons_timer_function;  //设置超时函数

    /* 向内核注册一个定时器 */  
    add_timer(&buttons_timer);  

    /* 初始化等待队列 */
    init_waitqueue_head(&key_q);

    return 0;   
}

static void button_exit()
{
    misc_deregister(&key_miscdev);  //注销混杂设备
}

module_init(button_init);
module_exit(button_exit);

应用程序代码:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

int main(int argc, char **argv)
{
    int fd;
    int key_num;

    fd = open("/dev/2440key", 0);
    //需要在相应目录下创建设备文件,
    //混杂设备主设备号10,次设备号在驱动程序中设为200

    if (fd<0)
        printf("open fail\n");

    read(fd, &key_num, 4);
    printf("key is %d\n",key_num);
    close(fd);

    return 0;
}

makefile文件

obj-m := key.o
KDIR := /home/S5-driver/lesson7/linux-tq2440/
all:
    make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm
clean:
    rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.bak *.order
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值