Linux驱动开发之中断编程

2020-02-12

关键字:


在 Linux 内核当中,处理一个中断事件需要知道两件事:

1、中断号码

2、中断处理函数

而在 ARM 中处理中断则需要知道以下四件事:

1、中断源号码

2、初始化中断控制器

3、初始化 CPU 中断功能

4、中断处理函数

获取中断号有以下两种方式:

1、宏定义

通过查询芯片手册上记载的相应中断编号,再经过系统预置的 IRQ_EINT(编号) 来得到中断号。

2、设备树文件

首先查询相应设备树头文件 dtsi,找到我们要用的那个中断组的描述,形如下所示:

gpx1: gpx1{

  gpio-controller;

  #gpio-cells = <2>;

 

  interrupt-controller;

  interrupt-parent = <&gic>;

  interrupts = <0 24 0>, <0 25 0>, <0 26 0>, <0 27 0>, <0 28 0>;

  #interrupt-cells = <2>

};

上面的 interrupt-controller; 表示以下描述的是和中断控制器相关的信息。 interrupts 描述的是 gpx1 这个节点所要控制或者说使用的中断号列表。 #interrupt-cells 描述的是长度,记住是长度就好了。

 

其次再在我们自己的 dts 里添加上我们想要的中断信息节点:

key_int_node {

  compatible = "test_key";

  interrupt-parent = <&gpx1>;

  interrupts = <2 4>;

};

interrupts 中只描述我们想要用到的中断编号信息。其中 2 表示在 gpx1 中 interrupts 中第 2 组中断编号,从 0 开始数的第 2 组,即 <0 26 0>。4 表示中断触发方式,这里可以随便选择 0、2、4、8 填写。

以下是一个获取中断号的驱动示例源码:

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

int get_irq_from_node()
{
    //获取到设备树中的节点。
    struct device_node *np = of_find_node_by_path("/key_int_node");//在根节点中。
    
    //通过节点获取中断号码。
    int irq = irq_of_parse_and_map(np, 0);//0表示interrupts列表中第0组。
    
    return irq;
}

static int __init key_drv_init()
{
    //获取中断号。
    int irq = get_irq_from_node();
    
    return 0;
}

static int __exit key_drv_exit()
{
    
}

module_init(key_drv_init);
module_init(key_drv_exit);
MODULE_LICENSE("GPL");

获取到中断号以后就要在驱动代码中去申请中断资源,注册中断处理函数。 

申请中断所使用到的函数签名如下:

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);

参数1 表示要申请中断的设备的中断号,即上面示例代码中得到的 irq。

参数2 表示中断处理函数。这个参数结构体的原型为:typedef irqreturn_t (*irq_handler_t)(int, void *); 这个函数指针的第一个参数为中断号,由系统自动填充传入。第二个参数由参数5传入。

参数3 表示中断触发方式。共有四种触发方式:1、上升沿触发(IRQF_TRIGGER_RISING);2、下降沿触发(IRQF_TRIGGER_FALLING);3、高电平触发(IRQF_TRIGGER_HIGH);4、低电平触发(IRQF_TRIGGER_LOW)。

参数4 表示中断的描述,是一个自定义的字符串。这里定义的名称将会在 /proc/interrupts 中查看到。

参数5 表示传递给参数2中的无类型指针的值。

返回值为0表示申请成功。

与之相反,释放中断申请的函数签名为:

void free_irq(unsigned int irq, void *dev_id);

以下是一个响应按键中断事件的示例代码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>

static int irq;

irqreturn_t key_irq_handler(int irq, void *devid)
{
    printk("key_irq_handler()
");
    
    return IRQ_HANDLED;
}

int get_irq_from_node()
{
    //获取到设备树中的节点。
    struct device_node *np = of_find_node_by_path("/key_int_node");//在根节点中。
    
    //通过节点获取中断号码。
    int irq = irq_of_parse_and_map(np, 0);//0表示interrupts列表中第0组。
    
    return irq;
}

static int __init key_drv_init()
{
    //获取中断号。
    irq = get_irq_from_node();
    
    //中断申请。
    int ret = request_irq(irq, key_irq_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "key3_eint10", NULL);
    
    return 0;
}

static int __exit key_drv_exit()
{
    free_irq(irq);
}

module_init(key_drv_init);
module_init(key_drv_exit);
MODULE_LICENSE("GPL");

上面这段代码在成功运行起来以后每次按一下指定按键在控制台会打印两次 key_irq_handler() 打印。因为我们在申请中断时设置了下降沿与上升沿都会触发中断。

以上我们就实现了一个监听外部按键事件的驱动程序。接下来就该考虑如何将这个按键事件上报给用户空间的应用程序了。

考虑到字符设备对用户来说是通过普通文件IO模型来通信的,它的中断事件自然也是通过普通的 read()、write() 来传递的。用户的应用程序要想能及时地接收到中断事件,那必须在一个死循环中不断地 read() 来检测。但这种方式又极其消耗系统的运算资源。为了即能减少不必要的资源消耗,又能及时地让客户端接收到来自驱动的中断事件,就有了 IO模型 这种东西,它说白了就是一种阻塞机制,在驱动中无中断事件时将进程休眠以让出CPU调度权,在有中断产生时再唤醒进程以上报中断事件给应用程序。

在驱动代码中实现的阻塞型IO模型的步骤如下:

1、将当前进程加入到等待队列中

add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

2、将当前进程状态设置成 TASK_INTERRUPTIBLE

set_current_state(TASK_INTERRUPTIBLE);

3、让出调度权,让自己进入休眠状态

schedule(void);

一个更加智能的接口函数:

wait_event_interruptible(wq, condition);

它的使用步骤如下:

1、创建等待队列头

wait_queue_head_t 结构体对象。

2、在合适的位置进行休眠

调用 wait_event_interruptible(wait_queue_head_t wq, condition) 函数。参数1表示等待队列。参数2是条件位,当它为假时进程会进入等待状态。当程序执行到这句代码并进入等待状态后,这个进程就阻塞在这一行代码中了,

3、驱动接收到中断事件后将进程唤醒

调用 wake_up_interruptible(wait_queue_head_t *q) 函数。

以下是基于上面的源码修改而来的应用了队列阻塞模型的驱动源码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/Slab.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/Sched.h>
#include <asm/io.h>
#include <asm/uaccess.h>

//设计一个描述按键的数据的对象。
struct key_event{
    int code; //按键的类型,哪个按键。
    int value; //按键的状态值。
};

struct key_desc{
    unsigned int dev_major;
    struct class *cls;
    struct device *dev;
    int irq;
    void *reg_base;
    struct key_event event;
    wait_queue_head_t wq_head;
    int key_state;//表示是否有按键事件上来。
};

struct key_desc *key_dev;

int key_drv_open(struct inode *inode, struct file *fp)
{
    printk("%s()
", __FUNC__);
    return 0;
}

int key_drv_close(struct inode *inode, struct file *fp)
{
    printk("%s()
", __FUNC__);
    return 0;
}

ssize_t key_drv_read(struct file *fp, char __user *buf, size_t count, loff_t *fpos)
{
    printk("%s()
", __FUNC__);
    wait_event_interruptible(key_dev->wq_head, key_dev->key_state);//程序是继续往下执行还是在这里阻塞要看参数2的值。
    
    int ret = copy_to_user(buf, &key_dev->event, count);
    
    //数据传递给用户之后要清除掉。
    memset(&key_dev->event, 0, sizeof(struct key_event));
    key_dev->key_state = 0;//这行其实没有必要。
    
    return count;
}

ssize_t key_drv_write(struct file *fp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("%s()
", __FUNC__);
    return 0;
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .release = key_drv_close,
    .read = key_drv_read,
    .write = key_drv_write,
};

irqreturn_t key_irq_handler(int irq, void *devid)
{
    printk("key_irq_handler()
");
    int value = readl(key_dev->reg_base + 4/*这个平台中控制寄存器+4就是数据寄存器*/) & (1 << 2);
    if(value) //抬起事件。依据按键导通时电平值的变化来确定。
    {
        printk("key up
");
        key_dev->event.code = 0;
        key_dev->event.value = 0;
    }
    else //按下事件
    {
        printk("key down
");
        key_dev->event.code = 0;
        key_dev->event.value = 1;
    }
    
    //有中断事件上来了,要唤醒等待队列。
    wake_up_interruptible(&key_dev->wq_head);
    key_dev->key_state = 1;
    
    return IRQ_HANDLED;
}

int get_irq_from_node()
{
    //获取到设备树中的节点。
    struct device_node *np = of_find_node_by_path("/key_int_node");//在根节点中。
    
    //通过节点获取中断号码。
    int irq = irq_of_parse_and_map(np, 0);//0表示interrupts列表中第0组。
    
    return irq;
}

static int __init key_drv_init()
{
    //1、分配全局设备对象
    //kzalloc 与 kmalloc 功能一致。
    //GFP_KERNEL 表示当没有空间可以申请时就一直阻塞直到有空间可以申请为止。
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    
    //2、申请主设备号
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    
    //3、创建设备节点文件
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), NULL, "key%d", 0);
    
    //4、硬件初始化--地址映射或中断申请。
    //获取中断号。
    key_dev->irq = get_irq_from_node();
    
    //中断申请。
    int ret = request_irq(key_dev->irq, key_irq_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "key3_eint10", NULL);
    
    //硬件如何获取数据?
    key_dev->reg_base = ioremap(0x11000c20, 8);//把控制寄存器的物理地址映射成虚拟地址。
   
    //初始化等待队列头
    init_waitqueue_head(&key_dev->wq_head);
    
    return 0;
}

static int __exit key_drv_exit()
{
    free_irq(key_dev->irq);
    device_destroy(key_dev->dev, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
}

module_init(key_drv_init);
module_init(key_drv_exit);
MODULE_LICENSE("GPL");

Linux驱动开发中的 select/poll 模型

select/poll 模型其实就是 IO 的多路复用。笔者早期曾写过一篇网络IO多路复用的博文,里面有提到一些 select 与 poll 的知识点与示例代码:Linux下网络IO的多路复用

这里再来记载一份 poll 模型的示例代码,这份代码完全由笔者自己编写并验证过,它是笔者对 poll 更进一步的理解的产物:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>

int main()
{
    printf("hello world!
");
    struct pollfd pfd[1] = {0};
    
    pfd[0].fd = 0; //0即 stdin 的 fd。
    pfd[0].events = POLLIN;
    
    while(1)
    {
        printf("--- start to poll ---
");
        int ret = poll(pfd, 1, -1);
        printf("--- poll ret:%d ---
", ret);
        if(ret > 0)
        {
            if(pfd[0].revents == POLLIN)
            {
                int rlen = 0;
                #define BUF_SIZE 4
                char buf[BUF_SIZE] = {0};
                while(1)
                {
                    memset(buf, 0, BUF_SIZE);
                    rlen = read(0, buf, BUF_SIZE);
                    
                    if(rlen <= 0)
                    {
                        break;
                    }
                    else if(rlen < BUF_SIZE)
                    {
                        printf("%s
", buf);
                        break;
                    }
                    else
                    {
                        printf("%s", buf);
                        if(*(buf + BUF_SIZE - 1) == '
')
                        {
                            break;
                        }
                    }
                }
                #undef BUF_SIZE
                
                printf("--- read end!
");
            }
        }
    }
    
    return 0;
}

这段代码能够很好地应用 poll 来监听键盘输入事件,并将键入字符打印出来。这里对这段代码稍加解释一下。

struct pollfd 结构体的原型如下图所示:

其中 fd 与 events 是需要用户自行指定的。 event 表示要监听的事件类型,可选值比较多,常用的有 POLLIN, POLLOUT, POLLERR。更具体完整定义与释义的可以通过 man poll 在帮助手册中查询得知。 revents 是由内核自动赋值的,当程序接收到 poll 返回事件时只需通过这个标志位来判断所发生的事件类型即可,如上示例代码所示。

这里需要提一点,要监听键盘输入事件,直接填数字0即可,不要填 stdin,stdin 在 stdio.h 中其实是一个文件类型指针,是一个对象。

以上是在普通应用程序中 poll 的演示代码。那在驱动开发中,如果应用程序也想使用 poll 模型来保持与驱动的监听通信的话,就需要驱动作相应的匹配了。说白了就是我们上上面的监听按键按下事件的驱动源码并不能很好地去匹配应用程序中的 poll 模型监听。如果要让驱动代码适配应用程序的 poll 就需要改造一下。改造的地方倒不多,仅需加多一个 file_operations 的实现即可。以下贴出新增的驱动代码:

unsigned int key_drv_poll(struct file *fp, struct poll_table_struct *pts)
{
    unsigned int mask;
    poll_wait(fp, key_dev->wq_head, pts);//将当前等待队列注册到系统中去。
    
    if(!key_dev->key_state)
        mask = 0;
    
    else
        mask |= POLLIN;
    
    return mask;
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .release = key_drv_close,
    .read = key_drv_read,
    .write = key_drv_write,
    .poll = key_drv_poll,
};

以上介绍的驱动将消息发送给普通应用程序的方式都离不开客户要另起线程专门去等待读取数据。不管怎么说这种方式都稍微有点 low。这种类型的信息传递,最合理的方式应该是“通知”式传递。即当没有中断事件或其它消息时,应用程序和驱动程序互不干扰,可以各干各的事,只有当驱动中有事件或消息要上报给应用程序了,驱动才主动地通知应用来接收消息或事件。这种通信模型被称为“异步信号通信”。

在应用程序端,我们需要设置好要拦截的信号类型,在这个例子中是 SIGIO 事件。具体的代码如下所示:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

static int fd;
static struct key_event event;

void signal_handler(int i)
{
    printf("signal_handler()
");
    if(i == SIGIO)
    {
        read(fd, &event, sizeof(struct key_event));
        if(event.code == KEY_ENTER)
        {
            if(event.value)
            {
                printf("key down
");
            }
            else
            {
                printf("key up
");
            }
        }
    }
}

int main()
{
    fd = open("/dev/key0", O_RDWR);
    
    //1、注册要处理的信号类型
    signal(SIGIO, signal_handler);
    
    //2、将当前进程设置成 SIGIO 的属主进程。即系统的信号只发送给本进程,避免被其它程序误接收。
    fcntl(fd, F_SETOWN, getpid());
    
    //3、将读写模式设置成异步模式。其实就是修改 open() 时设置的文件权限标识。
    int flags = fcntl(fd, F_GETFL);//拿到文件在 open() 时设置的权限标识符。
    fcntl(fd, F_SETFL, flags & FASYNC);
    
    while(1)
    {
        //可以去做其它事情了。
        sleep(1);
    }
    
    return 0;
}

在驱动代码中,则需要在接收到按键一中断事件后设置好对应的数据并发送 SIGIO 信号。

在这之前,驱动要知道到底要将 SIGIO 信号发送给哪一个进程。因为前面我们在应用程序代码中有将驱动文件的读写模式改写成异步模式(FASYNC),因此,我们要在 file_operations 中定义 fasync 接口,在驱动中拦截这一设置异步读写模式的调用,并在这里记录下设置异步读写的应用程序的进程号,这个记录用的函数签名为:

int fasync_helper(int fd, struct file *fp, int on, struct fasync_struct **fapp);

在记录下接收信号的进程以后就可以在有中断事件上来时发送这个信号了。发送信号的函数签名为:

void kill_fasync(struct fasync_struct **fasync, int sig, int event);

这个使用异步信号通知按键中断事件的驱动具体代码如下:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/Slab.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/Sched.h>
#include <asm/io.h>
#include <asm/uaccess.h>

//设计一个描述按键的数据的对象。
struct key_event{
    int code; //按键的类型,哪个按键。
    int value; //按键的状态值。
};

struct key_desc{
    unsigned int dev_major;
    struct class *cls;
    struct device *dev;
    int irq;
    void *reg_base;
    struct key_event event;
    wait_queue_head_t wq_head;
    int key_state;//表示是否有按键事件上来。
    struct fasync_struct *fasync;
};

struct key_desc *key_dev;

int key_drv_open(struct inode *inode, struct file *fp)
{
    printk("%s()
", __FUNC__);
    return 0;
}

int key_drv_close(struct inode *inode, struct file *fp)
{
    printk("%s()
", __FUNC__);
    return 0;
}

ssize_t key_drv_read(struct file *fp, char __user *buf, size_t count, loff_t *fpos)
{
    printk("%s()
", __FUNC__);
    wait_event_interruptible(key_dev->wq_head, key_dev->key_state);//程序是继续往下执行还是在这里阻塞要看参数2的值。
    
    int ret = copy_to_user(buf, &key_dev->event, count);
    
    //数据传递给用户之后要清除掉。
    memset(&key_dev->event, 0, sizeof(struct key_event));
    key_dev->key_state = 0;//这行其实没有必要。
    
    return count;
}

ssize_t key_drv_write(struct file *fp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("%s()
", __FUNC__);
    return 0;
}

unsigned int key_drv_poll(struct file *fp, struct poll_table_struct *pts)
{
    unsigned int mask;
    poll_wait(fp, key_dev->wq_head, pts);//将当前等待队列注册到系统中去。
    
    if(!key_dev->key_state)
        mask = 0;
    
    else
        mask |= POLLIN;
    
    return mask;
}

int key_drv_fasync(int fd, struct file *fp, int on)
{
    //只需要调用一个函数来记录信号应发送给哪个进程。
    return fasync_helper(fd, fp, on, &key_dev->fasync);
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .release = key_drv_close,
    .read = key_drv_read,
    .write = key_drv_write,
    .poll = key_drv_poll,
    .fasync = key_drv_fasync,
};

irqreturn_t key_irq_handler(int irq, void *devid)
{
    printk("key_irq_handler()
");
    int value = readl(key_dev->reg_base + 4/*这个平台中控制寄存器+4就是数据寄存器*/) & (1 << 2);
    if(value) //抬起事件。依据按键导通时电平值的变化来确定。
    {
        printk("key up
");
        key_dev->event.code = 0;
        key_dev->event.value = 0;
    }
    else //按下事件
    {
        printk("key down
");
        key_dev->event.code = 0;
        key_dev->event.value = 1;
    }
    
    //有中断事件上来了,要唤醒等待队列。
    wake_up_interruptible(&key_dev->wq_head);
    key_dev->key_state = 1;
    
    //发送信号。
    kill_fasync(&key_dev->fasync, SIGIO, POLLIN);
    
    return IRQ_HANDLED;
}

int get_irq_from_node()
{
    //获取到设备树中的节点。
    struct device_node *np = of_find_node_by_path("/key_int_node");//在根节点中。
    
    //通过节点获取中断号码。
    int irq = irq_of_parse_and_map(np, 0);//0表示interrupts列表中第0组。
    
    return irq;
}

static int __init key_drv_init()
{
    //1、分配全局设备对象
    //kzalloc 与 kmalloc 功能一致。
    //GFP_KERNEL 表示当没有空间可以申请时就一直阻塞直到有空间可以申请为止。
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    
    //2、申请主设备号
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    
    //3、创建设备节点文件
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), NULL, "key%d", 0);
    
    //4、硬件初始化--地址映射或中断申请。
    //获取中断号。
    key_dev->irq = get_irq_from_node();
    
    //中断申请。
    int ret = request_irq(key_dev->irq, key_irq_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "key3_eint10", NULL);
    
    //硬件如何获取数据?
    key_dev->reg_base = ioremap(0x11000c20, 8);//把控制寄存器的物理地址映射成虚拟地址。
   
    //初始化等待队列头
    init_waitqueue_head(&key_dev->wq_head);
    
    return 0;
}

static int __exit key_drv_exit()
{
    free_irq(key_dev->irq);
    device_destroy(key_dev->dev, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
}

module_init(key_drv_init);
module_init(key_drv_exit);
MODULE_LICENSE("GPL");

中断的下半部

在 Linux 中,中断的响应处理可分为“上半部”与“下半部”两个组成部分。就拿上面的驱动代码来说,整个中断响应函数 key_irq_handler() 函数中的代码都属于“上半部”。在 Linux 中,当系统去响应中断事件时,系统的执行资源是被这个中断所“独占”的,即在这个中断处理完之前无法响应其它请求。这就导致了中断响应处理必须是“尽可能短暂”的,否则就会导致系统卡住的现象。这种响应中断过程,也即要尽可能短暂的过程就被称为中断的“上半部”。但是,有的时候中断响应处理确实是无法短无法快,必须要消耗一定的时间,这怎么办呢?于是就有了中断的“下半部”机制。对于那些需要消耗一定的时间的中断处理,会先在“上半部”将所有需要的数据先拿过来,再在“上半部”结束之前启动“下半部”,将耗时的操作扔在“下半部”中去处理。由此我们可知,中断的下半部只是一个可选项,我们可以视实际需要来决定是否使用“下半部”。

中断的下半部的实现有以下三种:

1、softirq

这种方式的下半部处理或者说响应速度会比较快。但是它是属于内核级别的机制,若要使用它,则需要修改Linux内核代码,因此它在日常驱动开发中用的比较少。

2、tasklet

这种方式的内部原理其实就是调用了 softirq。

3、workqueue

工作队列,与前面提到的等待队列 wait_queue 不同。

1、tasklet

tasklet 是一个结构体对象,它的原型如下:

struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long); //下半部的处理函数。
    unsigned long data; //这个参数将会传递由 func 函数。只要下半部的处理函数被调用,这个参数的值就会被传过去。
};

使用 tasklet 的步骤如下:

1、初始化 tasklet 结构体对象

初始化的函数接口签名为:

int tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

2、在上半部中将下半部的信息放入到内核线程中,即启动下半部

启动的函数只有一个,其函数签名如下:

int tasklet_schedule(struct tasklet_struct *t);

释放tasklet的函数签名如下:

void tasklet_kill(struct tasklet_struct *t);

因为 tasklet 的结构体对象中有一个链表指针,因此可以很方便地将比较复杂的响应分拆在多个 tasklet 中处理。

2、workqueue

workqueue 与 tasklet 类似。它的结构体原型如下:

struct work_struct{
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
};

使用 workqueue 的步骤如下:

1、初始化

INIT_WORK(struct work_struct *work, work_func_t func);

2、启动

schedule_work(struct work_struct *work);

以下是根据应用了 tasklet 与 workqueue 的驱动示例源码:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/Slab.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/Sched.h>
#include <asm/io.h>
#include <asm/uaccess.h>

//设计一个描述按键的数据的对象。
struct key_event{
    int code; //按键的类型,哪个按键。
    int value; //按键的状态值。
};

struct key_desc{
    unsigned int dev_major;
    struct class *cls;
    struct device *dev;
    int irq;
    void *reg_base;
    struct key_event event;
    wait_queue_head_t wq_head;
    int key_state;//表示是否有按键事件上来。
    struct fasync_struct *fasync;
    struct tasklet_struct my_tasklet;
    struct work_struct my_work;
};

struct key_desc *key_dev;

int key_drv_open(struct inode *inode, struct file *fp)
{
    printk("%s()
", __FUNC__);
    return 0;
}

int key_drv_close(struct inode *inode, struct file *fp)
{
    printk("%s()
", __FUNC__);
    return 0;
}

ssize_t key_drv_read(struct file *fp, char __user *buf, size_t count, loff_t *fpos)
{
    printk("%s()
", __FUNC__);
    wait_event_interruptible(key_dev->wq_head, key_dev->key_state);//程序是继续往下执行还是在这里阻塞要看参数2的值。
    
    int ret = copy_to_user(buf, &key_dev->event, count);
    
    //数据传递给用户之后要清除掉。
    memset(&key_dev->event, 0, sizeof(struct key_event));
    key_dev->key_state = 0;//这行其实没有必要。
    
    return count;
}

ssize_t key_drv_write(struct file *fp, const char __user *buf, size_t count, loff_t *fpos)
{
    printk("%s()
", __FUNC__);
    return 0;
}

unsigned int key_drv_poll(struct file *fp, struct poll_table_struct *pts)
{
    unsigned int mask;
    poll_wait(fp, key_dev->wq_head, pts);//将当前等待队列注册到系统中去。
    
    if(!key_dev->key_state)
        mask = 0;
    
    else
        mask |= POLLIN;
    
    return mask;
}

int key_drv_fasync(int fd, struct file *fp, int on)
{
    //只需要调用一个函数来记录信号应发送给哪个进程。
    return fasync_helper(fd, fp, on, &key_dev->fasync);
}

const struct file_operations key_fops = {
    .open = key_drv_open,
    .release = key_drv_close,
    .read = key_drv_read,
    .write = key_drv_write,
    .poll = key_drv_poll,
    .fasync = key_drv_fasync,
};

irqreturn_t key_irq_handler(int irq, void *devid)
{
    printk("key_irq_handler()
");
    int value = readl(key_dev->reg_base + 4/*这个平台中控制寄存器+4就是数据寄存器*/) & (1 << 2);
    if(value) //抬起事件。依据按键导通时电平值的变化来确定。
    {
        printk("key up
");
        key_dev->event.code = 0;
        key_dev->event.value = 0;
    }
    else //按下事件
    {
        printk("key down
");
        key_dev->event.code = 0;
        key_dev->event.value = 1;
    }
    
    //有中断事件上来了,要唤醒等待队列。
    wake_up_interruptible(&key_dev->wq_head);
    key_dev->key_state = 1;
    
    //发送信号。
    kill_fasync(&key_dev->fasync, SIGIO, POLLIN);
    
    //启动下半部。
    tasklet_schedule(&key_dev->my_tasklet);
    
    //启动workqueue下半部。
    schedule_work(&key_dev->my_work);
    
    return IRQ_HANDLED;
}

void key_tasklet_half_irq(unsigned long)
{
    printf("key_tasklet_half_irq()
");
    //do anything you wanna do.
}

void key_workqueue_half_irq(struct work_struct *work)
{
    printf("key_workqueue_half_irq()
");
    // do anything you wanna do too.
}

int get_irq_from_node()
{
    //获取到设备树中的节点。
    struct device_node *np = of_find_node_by_path("/key_int_node");//在根节点中。
    
    //通过节点获取中断号码。
    int irq = irq_of_parse_and_map(np, 0);//0表示interrupts列表中第0组。
    
    return irq;
}

static int __init key_drv_init()
{
    //1、分配全局设备对象
    //kzalloc 与 kmalloc 功能一致。
    //GFP_KERNEL 表示当没有空间可以申请时就一直阻塞直到有空间可以申请为止。
    key_dev = kzalloc(sizeof(struct key_desc), GFP_KERNEL);
    
    //2、申请主设备号
    key_dev->dev_major = register_chrdev(0, "key_drv", &key_fops);
    
    //3、创建设备节点文件
    key_dev->cls = class_create(THIS_MODULE, "key_cls");
    key_dev->dev = device_create(key_dev->cls, NULL, MKDEV(key_dev->dev_major, 0), NULL, "key%d", 0);
    
    //4、硬件初始化--地址映射或中断申请。
    //获取中断号。
    key_dev->irq = get_irq_from_node();
    
    //中断申请。
    int ret = request_irq(key_dev->irq, key_irq_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "key3_eint10", NULL);
    
    //硬件如何获取数据?
    key_dev->reg_base = ioremap(0x11000c20, 8);//把控制寄存器的物理地址映射成虚拟地址。
   
    //初始化等待队列头
    init_waitqueue_head(&key_dev->wq_head);
    
    //初始化 tasklet
    tasklet_init(&key_dev->my_tasklet, key_tasklet_half_irq, 45);
    
    //初始化 workqueue
    INIT_WORK(&key_dev->my_work, key_workqueue_half_irq);
    
    return 0;
}

static int __exit key_drv_exit()
{
    tasklet_kill(&key_dev->my_tasklet);
    free_irq(key_dev->irq);
    device_destroy(key_dev->dev, MKDEV(key_dev->dev_major, 0));
    class_destroy(key_dev->cls);
    unregister_chrdev(key_dev->dev_major, "key_drv");
    kfree(key_dev);
}

module_init(key_drv_init);
module_init(key_drv_exit);
MODULE_LICENSE("GPL");

以上就是嵌入式 Linux 中断开发的基本知识。


原文地址:https://www.cnblogs.com/chorm590/p/12294386.html