Linux驱动开发笔记(十)——中断

发布于:2025-09-11 ⋅ 阅读:(17) ⋅ 点赞:(0)

视频:第13.1讲 Linux中断实验-Linux内核中断框架简介_哔哩哔哩_bilibili

第13.1讲 Linux中断实验-Linux内核中断框架简介_哔哩哔哩_bilibili

文档:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》五十一章


1. 中断API函数

        每个中断都有一个中断号,通过中断号即可区分不同的中断。在Linux 内核中使用一个 int 变量表示中断号。

1.1 申请 request_irq

        申请一个中断。

request_irq(unsigned int irq,      // 要申请中断的中断号
            irq_handler_t handler, // 中断处理函数
            unsigned long flags,   // 中断标志,/linux/interrupt.h里面查看所有的中断标志
        	const char *name,      // 中断名字。设置以后可以在/proc/interrupts文件中看到对应的中断名字
            void *dev)             // 如果将flags设置为IRQF_SHARED的话,dev用来区分不同的中断,
                                   // 一般情况下将dev设置为设备结构体,dev会传递给中断处理函数irq_handler_t的第二个参数。 
        //  return : 0中断申请成功,其他负值中断申请失败,返回-EBUSY表示中断已经被申请

1.2 释放 free_irq

        使用完后释放中断。

void free_irq(unsigned int irq,  // 要释放的中断号
              void *dev)         // 如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断
                                 // 共享中断只有在释放最后中断处理函数的时候才会被禁止掉

1.3 中断处理函数

        中断处理函数格式:

irqreturn_t (*irq_handler_t) (int, void *)

//第一个int参数是要中断处理函数要相应的中断号
//第二个void*参数是一个指向void的指针,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,
//            dev也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型

1.4 中断使能 / 禁止

        使能一个中断:

void enable_irq(unsigned int irq) 
// irq : 要使能的中断号

        禁止一个中断:

void disable_irq(unsigned int irq) 
// irq : 要禁止的中断号

       该函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。

void disable_irq_nosync(unsigned int irq)
// irq : 要禁止的中断号

        函数调用以后立即返回,不会等待当前中断处理程序执行完毕。

1.5 全局使能 / 禁止

        开启或关闭当前处理器的整个中断系统/全局中断:

local_irq_enable() 
local_irq_disable() 

       local_irq_enable用于使能当前处理器中断系统,local_irq_disable用于禁止当前处理器中断 系统。

 (来自《指南》的例子)A任务要关闭全局中断10s,但关了2s后B任务要关闭全局中断3s,3s后B又打开了全局中断。此时就破坏了A任务关闭全局中断10s的要求,可能会导致一些意外。因此B不能简单地直接打开全局中断,而是将中断恢复到关闭时的状态。就可以用这两个函数:

local_irq_save(flags)      // 禁止中断,并将中断状态保存在flags中
local_irq_restore(flags)   // 恢复中断,并将中断状态恢复到flags状态

1.6  获取中断号

(这部分应该放到下面的设备树里面,为了整齐我扔到这里来了)

从设备树中的interupts属性中获取中断号:

// 定义在include/linux/of_irq.h
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
// dev   : 设备节点
// index : 索引号
// return: 中断号

如果使用GPIO,可以直接获取gpio对应的中断号:

int gpio_to_irq(unsigned int gpio)
// gpio  : 要获取的GPIO编号
// return: GPIO对应的中断号

2. 上半部和下半部 / 顶半部和底半部

(直接把《指南》里的内容精简一下贴过来了:)

        中断处理函数一定要快点执行完毕,越短越好,但是有些中断处理过程就是比较费时间,必须要对其进行处理,缩短中断处理函数的执行时间。

        比如电容触摸屏通过中断通知SOC有触摸事件发生,SOC响应中断,然后通过IIC读取触摸坐标值并将其上报给系统。但是IIC的速度最高也只有400Kbit/s,中断通过IIC读取数据就会浪费时间。我们可以将IIC读取触摸数据的操作暂后执行,中断处理函数仅仅响应中断,然后清除中断标志位即可。

这个时候中断处理过程就分为了两部分:

        上半部:中断处理函数,那些处理过程比较快、不会占用很长时间的处理就可以放在上半部完成。

        下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码交给下半部去执行。

        因此,Linux内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快 出,至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定, 一切根据实际使用情况去判断,一些参考点:

        ①、如果要处理的内容不希望被其他中断打断,可以放到上半部。

        ②、如果要处理的任务对时间敏感,可以放到上半部。

        ③、如果要处理的任务与硬件有关,可以放到上半部 

        ④、除了上述三点以外的其他任务,优先考虑放到下半部。

上半部处理直接编写中断处理函数就可以。对于下半部,Linux内核提供了多种下半部机制。

2.1 软中断

        (基本不用,随便看看就星)

        结构体softirq_action表示软中断:

// 定义在include/linux/interrupt.h
struct softirq_action{
	void (*action)(struct softirq_action *); // action成员变量就是软中断的服务函数
};

        共有10种软中断:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
// NR_SOFTIRQS为10,数组softirq_vec有10个元素

        数组softirq_vec是全局数组,所有的CPU都可以访问,每个CPU都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个CPU所执行的软中断服务函数是相同的,都是数组softirq_vec中定义的action函数。

// 定义在include/linux/interrupt.h
enum{
	HI_SOFTIRQ=0,    // 高优先级软中断
	TIMER_SOFTIRQ,   // 定时器软中断
	NET_TX_SOFTIRQ,  // 网络数据发送软中断
	NET_RX_SOFTIRQ,  // 网络数据接收软中断
	BLOCK_SOFTIRQ,   // 块设备软中断
	BLOCK_IOPOLL_SOFTIRQ, // 块设备IO轮询软中断
	TASKLET_SOFTIRQ, // Tasklet软中断
	SCHED_SOFTIRQ,   // 调度器软中断
	HRTIMER_SOFTIRQ, // 高精度定时器软中断
	RCU_SOFTIRQ,     // RCU 回调软中断
                     /* Preferable RCU should always be the last softirq */
	NR_SOFTIRQS      // softirq的总数
};

2.2 tasklet

        也需要用到上半部,只是上半部的中断处理函数重点在调用tasklet_schedule。

// 定义在include/linux/interrupt.h
struct tasklet_struct { 
    struct tasklet_struct *next;     /* 下一个tasklet */ 
    unsigned long state;             /* tasklet状态 */ 
    atomic_t count;                  /* 计数器,记录对tasklet的引用数 */ 
    void (*func)(unsigned long);     /* tasklet执行的函数,相当于中断处理函数 */ 
    unsigned long data;              /* 函数func的参数 */ 
};

2.2.1 初始化

// 定义在kernel/softirq.c
void tasklet_init(
                  struct tasklet_struct *t,    // 要初始化的tasklet
		          void (*func)(unsigned long), // tasklet的处理函数
                  unsigned long data           // 要传递给func函数的参数
                 )

也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化, DECLARE_TASKLET定义在include/linux/interrupt.h文件中,定义如下:

// 定义在include/linux/interrupt.h
#define DECLARE_TASKLET(name, func, data) \   // name要定义的tasklet,func要处理的函数,data要传给处理函数的参数
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

2.2.2 示例代码

《指南》中给出了示例代码:

/* 定义taselet    */ 
struct tasklet_struct testtasklet; 
 
/* tasklet处理函数   */ 
void testtasklet_func(unsigned long data) { 
    /* tasklet具体处理内容 */ 
} 
/* 中断处理函数 */ 
irqreturn_t test_handler(int irq, void *dev_id) { 
    ...... 
    /* 调度tasklet   */ 
    tasklet_schedule(&testtasklet); 
    ...... 
} 
/* 驱动入口函数     */ 
static int __init xxxx_init(void) { 
    ...... 
    /* 初始化tasklet   */ 
    tasklet_init(&testtasklet, testtasklet_func, data); 

    /* 注册中断处理函数   */ 
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); 
    ...... 
} 

2.3 工作队列

        工作队列将要推后的工作交给一个内核线程去执行。因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度

        因此如果你要推后的工作可以睡眠,那么就可以选择工作队列,否则的话就只能选择软中断或tasklet。

        工作结构体定义如下:

// 定义在include/linux/workqueue.h
struct work_struct {
	atomic_long_t data;
	struct list_head entry;
	work_func_t func;
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

        许多个工作(work_struct)组成工作队列(workqueue_struct),内核使用工作者线程(worker)来处理工作队列中的工作。在实际驱动开发中,只需要工作work_struct即可,工作队列和工作者线程由系统完成,我们不用去管。

        初始化:

#define INIT_WORK(_work, _func) 
// _work: 要初始化的工作
// _func: 工作对应的处理函数

#define DECLARE_WORK(n, f) 
// n: 定义的工作。这个宏会声明并定义一个struct work_struct类型的变量n并初始化它。
// f: 工作对应的处理函数

示例:
    DECLARE_WORK(mywork, myfunc);
    等价于
    struct work_struct mywork;
    INIT_WORK(&mywork, myfunc);

        调度:

bool schedule_work(struct work_struct *work)
// work  : 要调度的工作
// return: 0成功,else失败

        《指南》给出了工作队列使用示例代码51.1.2.11:

/* 定义工作(work)           */ 
struct  work_struct testwork; 

/* work处理函数          */
void testwork_func_t(struct work_struct *work); { 
    /* work具体处理内容  */ 
} 
/* 中断处理函数     */ 
irqreturn_t test_handler(int irq, void *dev_id) { 
    ...... 
    /* 调度work         */ 
    schedule_work(&testwork); 
    ...... 
} 
/* 驱动入口函数            */ 
static int __init xxxx_init(void) { 
    ...... 
    /* 初始化work        */ 
    INIT_WORK(&testwork, testwork_func_t); 
    /* 注册中断处理函数      */ 
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev); 
    ...... 
} 

3. 设备树中的中断信息节点

        中断控制器的设备树绑定信息参考文档:linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\ Documentation\devicetree\bindings\arm\gic.txt

- compatible : should be one of:
    "arm,gic-400"
    "arm,cortex-a15-gic"
    "arm,cortex-a9-gic"
    "arm,cortex-a7-gic"
    "arm,arm11mp-gic"
    "brcm,brahma-b15-gic"
    "arm,arm1176jzf-devchip-gic"
    "qcom,msm-8660-qgic"
    "qcom,msm-qgic2"
- interrupt-controller : Identifies the node as an interrupt controller
- #interrupt-cells : Specifies the number of cells needed to encode an interrupt source.  The type shall be a <u32> and the value shall be 3.

  The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI interrupts.

  The 2nd cell contains the interrupt number for the interrupt type.
  SPI interrupts are in the range [0-987].  PPI interrupts are in the range [0-15].

  The 3rd cell is the flags, encoded as follows:
    bits[3:0] trigger type and level flags.
        1 = low-to-high edge triggered 
        2 = high-to-low edge triggered (invalid for SPIs)
        4 = active high level-sensitive
        8 = active low level-sensitive (invalid for SPIs).

    bits[15:8] PPI interrupt cpu mask.  Each bit corresponds to each of
    the 8 possible cpus attached to the GIC.  A bit set to '1' indicated
    the interrupt is wired to that CPU.  Only valid for PPI interrupts.
    Also note that the configurability of PPI interrupts is IMPLEMENTATION
    DEFINED and as such not guaranteed to be present (most SoC available
    in 2014 seem to ignore the setting of this flag and use the hardware
    default value).

        举个例子,imx6ull.dtsi中的intc节点就是 I.MX6ULL的中断控制器节点:

	intc: interrupt-controller@00a01000 {
		compatible = "arm,cortex-a7-gic";
		#interrupt-cells = <3>;    // 此中断控制器下设备的cells大小为3
		interrupt-controller;      // 表示当前节点是中断控制器
		reg = <0x00a01000 0x1000>,
		      <0x00a02000 0x100>;
	};

        gpio节点也可以作为中断控制器。

        如imx6ull.dtsi文件中的gpio5节点:

gpio5: gpio@020ac000 {
	compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
	reg = <0x020ac000 0x4000>;
	interrupts = <GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH>,
				 <GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH>;
	gpio-controller;    // 中断控制器,用于控制gpio5所有IO的中断
	#gpio-cells = <2>;
	interrupt-controller;
	#interrupt-cells = <2>;
};

        该节点的interrupts这个属性得细说啊。

        其中GIC_SPI==0  (定义在include/dt-bindings/interrupt-controller/arm-gic.h),IRQ_TYPE_LEVEL_HIGH==4  (定义在include/dt-bindings/interrupt-controller/irq.h)
        再参考上方引用块绑定信息加粗的#interrupt-cells部分,这两行表示SPI中断、中断号为74 75、高电平触发。

        至于这个74和75怎么来的,查 《IMX6ULL参考手册》185页可以找到下表,即GPIO5有两个中断号——74和75。74对应GPIO5_IO0015低16位IO,75对应GPIO5_IO1631高16位IO。

以下内容来自linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek\Documentation\devicetree \bindings\gpio\gpio-mxs.txt:

The Freescale MXS GPIO controller is part of MXS PIN controller.  The
GPIOs are organized in port/bank.  Each port consists of 32 GPIOs.

As the GPIO controller is embedded in the PIN controller and all the
GPIO ports share the same IO space with PIN controller, the GPIO node
will be represented as sub-nodes of MXS pinctrl node.

Required properties for GPIO node:
- compatible : Should be "fsl,<soc>-gpio".  The supported SoCs include imx23 and imx28.
- interrupts : Should be the port interrupt shared by all 32 pins.
- gpio-controller : Marks the device node as a gpio controller.
- #gpio-cells : Should be two.  The first cell is the pin number and the second cell is used to specify the gpio polarity:
      0 = active high
      1 = active low
- interrupt-controller: Marks the device node as an interrupt controller.
- #interrupt-cells : Should be 2.  The first cell is the GPIO number.
  The second cell bits[3:0] is used to specify trigger type and level flags:
      1 = low-to-high edge triggered.
      2 = high-to-low edge triggered.
      4 = active high level-sensitive.
      8 = active low level-sensitive.

// imx6ull-alientek-emmc.dts
	fxls8471@1e {
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
		position = <0>;
		interrupt-parent = <&gpio5>; // 父中断
		interrupts = <0 8>;   // 参照上面引用块里的interrupts部分。表示GPIO5_IO00,低电平触发
	};

4. 实验:按键中断+定时器消抖+tasklet下半部

视频:第13.4讲 Linux中断实验-按键中断实验驱动编写(上)_哔哩哔哩_bilibili

手册:《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81.pdf》51.3节

4.1 设备树

        直接在上次按键实验设备树节点基础上增加两行中断描述即可。将如下代码添加到imx6ull-alientek-emmc.dts根节点最后:

	key{
		compatible = "alientek,key";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_key>;
		key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;  

		status = "okay";
		interrupt-parent = <&gpio1>;
		interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
	};

如果需要配置多个中断,则写法为:

编译为dtb并复制到tftproot,重启开发板:

make dtbs
sudo cp arch/arm/boot/dts/imx6ull-alientek-emmc.dtb /.../tftpboot/ -f

可以去开发板检查一下,应当能看到key节点:

4.2 文件结构

        和其他实验一样,直接贴过来改个文件名:

14_IRQ (工作区)
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── 14_irq.code-workspace
├── Makefile
├── irq.c
└── irqAPP.c

4.3 Makefile

CFLAGS_MODULE += -w

KERNELDIR := /home/for/linux/imx6ull/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek  # 内核路径
# KERNELDIR改成自己的 linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek文件路径(这个文件从正点原子“01、例程源码”中直接搜,cp到虚拟机里面)

CURRENT_PATH := $(shell pwd)	# 当前路径

obj-m := irq.o			# 编译文件

build: kernel_modules			# 编译模块

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean	

4.4 irq.c

按键→中断处理函数→调用tasklet处理函数→tasklet启动定时器→定时器处理函数

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>

#define DEV_CNT  1          /* 设备号数量 */
#define DEV_NAME "timer"    /* 设备名 */
#define KEY_NUM 1           /* 按键数量 */
#define KEY0_VALUE 0x01     /* 按键值(假定释放为1,按下为0)*/
#define INVAKEY    0xFF     /* 按键无效值 */

/* 按键结构体 */
struct irq_keydesc{
    int gpio;   /* io号 */
    int irqnum; /* 中断号 */
    unsigned char value; /* 键值 (默认/空闲电平,比如 1) */
    char name[10];  /* 名字 */
    irqreturn_t (*handler) (int, void *);   /* 中断处理函数*/
    struct tasklet_struct tasklet;
};

/* 中断设备结构体 */
struct irq{
    dev_t devid;    // 设备号
    int major;      // 主设备号
    int minor;      // 次
    struct cdev cdev;       // cdev
    struct device *device;  // 设备
    struct class *class;    // 类
    struct device_node *nd; // 节点
    struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键
    struct timer_list timer;// 定时器

    atomic_t releasekey;  // 0表示按键释放,1表示按键按下
    atomic_t keyvalue;    // 第八位表示数据是否被读取:每完成一次按键释放,第八位就会置1,用户态调用.read时会将其置0,表示数据被读取
                          // 其余七位表示按键的值
};
static struct irq key_irq;

/* 打开/读操作 */
static int irq_open(struct inode *inode, struct file *filp){
    filp->private_data = &key_irq;
    return 0;
}
static ssize_t irq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
    int ret = 0;
    unsigned char keyvalue = 0;
    unsigned char releasekey = 0;
    struct irq* dev = filp->private_data;

    keyvalue = (unsigned char)atomic_read(&dev->keyvalue);
    releasekey = (unsigned char)atomic_read(&dev->releasekey);

    if(releasekey){ // 如果按键释放,此时可以读取数据
        if(keyvalue & 0x80){  // 如果最高位为1,表示此时数据还未被读取
            unsigned char out = keyvalue & 0x7F; // 将最高位置0(最高位为有效位,低7位为实际value)
            if(copy_to_user(buf, &out, sizeof(out))){            // 返回按键值给用户态
                return -EFAULT;
            }
            ret = sizeof(out);
        } else {
            goto data_error;
        }
        atomic_set(&dev->releasekey, 0); // 释放标志清零
        return ret;
    } else {
        goto data_error;
    }

data_error:
    return -EINVAL;
}

/* 操作集 */
static const struct file_operations key_fops = {
    .owner = THIS_MODULE,
    .open = irq_open,
    .read = irq_read,
};

/* 中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id){
    struct irq *dev = (struct irq*)dev_id;
    int current_level;  // 保存中断时的电平

    current_level = gpio_get_value(dev->irqkey[0].gpio);
    atomic_set(&dev->keyvalue, current_level);  // 保存基准电平

    tasklet_schedule(&dev->irqkey[0].tasklet); // 调度tasklet处理后续逻辑,避免中断耗时

    return IRQ_HANDLED;
}

/* 定时器处理函数 */
static void timer_func(unsigned long arg){
    struct irq* dev = (struct irq*)arg;
    int current_value; // 保存定时器延时后的电平

    /* 读取当前按键电平 */
    current_value = gpio_get_value(dev->irqkey[0].gpio);

    /* 若当前电平与中断发生时读取到的电平一致,则有效 */
    if (current_value == atomic_read(&dev->keyvalue)) {
        if (current_value == 1) { // 释放
            printk("KEY0 release!\r\n");
            atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效
            atomic_set(&dev->releasekey, 1);  // 表示已释放,此时用户态可读
        } else { // 按下
            printk("KEY0 Push!\r\n");
            atomic_set(&dev->keyvalue, current_value); // 这里0 表示按下
            // atomic_set(&dev->releasekey, 0);  // 表示未释放
        }
    }
}

/* tasklet处理函数 */
static void key_tasklet(unsigned long data){
    struct irq* dev = (struct irq*)data;
    printk("key_tasklet\r\n");
    dev->timer.data = data;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */
    
}


/* 初始化按键 */
static int keyio_init(struct irq *dev){
    int ret = 0;
    int i = 0;

    /* 1. 按键初始化 */
    dev->nd = of_find_node_by_path("/key");  // 获取设备节点
    if(dev->nd == NULL){
        ret = -EINVAL;
        goto fail_nd;
    }
    for(i=0; i<KEY_NUM; i++){
        dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 获取设备树中 gpio
        if (dev->irqkey[i].gpio < 0) {
            pr_err("get gpio %d failed\n", i);
            ret = -EINVAL;
            goto fail_nd;
        }
    }

    for(i=0; i<KEY_NUM; i++){
        memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));
        sprintf(dev->irqkey[i].name, "KEY%d", i);   // 命名
        ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申请gpio
        if (ret) {
            pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);
            goto fail_gpio_req;
        }
        gpio_direction_input(dev->irqkey[i].gpio);  // 设置io方向

        dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 获取中断号
        if (dev->irqkey[i].irqnum < 0) {
            pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);
            ret = dev->irqkey[i].irqnum;
            goto fail_gpio_req;
        }
    }

    dev->irqkey[0].handler = key0_handler; // 中断处理函数
    dev->irqkey[0].value = KEY0_VALUE;     // 例如:1 表示 release(高电平),0 表示按下(低电平)

    /* 2. 中断初始化 */
    for(i=0; i<KEY_NUM; i++){
        ret = request_irq(dev->irqkey[i].irqnum,
                          dev->irqkey[i].handler,
                          IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                          dev->irqkey[i].name,
                          &key_irq); /* dev_id 传入结构体指针 */
        if(ret < 0){
            printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);
            while (--i >= 0) {
                free_irq(dev->irqkey[i].irqnum, &key_irq);
            }
            goto fail_irq;
        }

        tasklet_init(&dev->irqkey[0].tasklet, key_tasklet, (unsigned long)dev);
    }

    return 0;

fail_irq:
fail_gpio_req:
    for (i = 0; i < KEY_NUM; i++) {
        if (gpio_is_valid(dev->irqkey[i].gpio))
            gpio_free(dev->irqkey[i].gpio);
    }
fail_nd:
    return ret;
}

/* 驱动入口 */
static int __init key_init(void){
    int ret = 0;

    /* 1. 注册字符设备驱动 */
    key_irq.devid = 0;
    alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);
    key_irq.major = MAJOR(key_irq.devid);
    key_irq.minor = MINOR(key_irq.devid);

    /* 2. 初始化 cdev */
    key_irq.cdev.owner = THIS_MODULE;
    cdev_init(&key_irq.cdev, &key_fops);

    /* 3. 添加 cdev */
    ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);
    if (ret) {
        pr_err("cdev_add failed\n");
        goto fail_cdev;
    }

    /* 4. 创建类 */
    key_irq.class = class_create(THIS_MODULE, DEV_NAME);
    if(IS_ERR(key_irq.class)){
        ret = PTR_ERR(key_irq.class);
        goto fail_class;
    }

    /* 5. 创建设备 */
    key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);
    if(IS_ERR(key_irq.device)){
        ret = PTR_ERR(key_irq.device);
        goto fail_device;
    }

    /* 6. 初始化IO*/
    ret = keyio_init(&key_irq);
    if(ret < 0){
        goto fail_keyinit;
    }

    /* 7. 初始化定时器 */
    init_timer(&key_irq.timer);
    key_irq.timer.function = timer_func;
    key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向设备结构体 */

    /* 8. 初始化原子变量 */
    atomic_set(&key_irq.keyvalue, INVAKEY);
    atomic_set(&key_irq.releasekey, 0);

    return 0;

fail_keyinit:
    device_destroy(key_irq.class, key_irq.devid);
fail_device:
    class_destroy(key_irq.class);
fail_class:
    cdev_del(&key_irq.cdev);
fail_cdev:
    unregister_chrdev_region(key_irq.devid, DEV_CNT);
    return ret;
}


/* 驱动出口 */
static void __exit key_exit(void){
    int i=0;
    /* 释放中断 */
    for(i=0; i<KEY_NUM; i++){
        free_irq(key_irq.irqkey[i].irqnum, &key_irq);
    }
    /* 释放io */
    for(i=0; i<KEY_NUM; i++){
        if (gpio_is_valid(key_irq.irqkey[i].gpio))
            gpio_free(key_irq.irqkey[i].gpio);
    }
    /* 删除定时器 */
    del_timer_sync(&key_irq.timer);

    /* 注销字符设备驱动 */
    cdev_del(&key_irq.cdev);
    unregister_chrdev_region(key_irq.devid, DEV_CNT);

    device_destroy(key_irq.class, key_irq.devid);
    class_destroy(key_irq.class);
}

module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");

4.5 irqAPP.c

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<sys/ioctl.h>
#include<fcntl.h>
#include<stdlib.h>
#include<string.h>

/* 
 * @description    : main主程序 
 * @param - argc   : argv数组元素个数 
 * @param - argv   : 具体参数 
 * @return         : 0 成功; else失败
 * 调用  ./irqAPP /dev/timer
 */ 


#define LEDOFF 0
#define LEDON  1

int main(int argc, char *argv[]){
    if(argc != 2){  // 判断用法是否错误
        printf("Error Usage!\r\n");
        return -1;
    }

    char *filename;
    int fd = 0, ret = 0;
    unsigned char data;

    filename = argv[1];
    fd = open(filename, O_RDWR);  // 读写模式打开驱动文件filename

    if(fd <0){
        printf("file %s open failed!\r\n");
        return -1;
    }


	while(1) {
		ret = read(fd, &data, sizeof(data));
		if (ret > 0) {
			printf("key value = %#x\r\n", data);
		} else {
			// usleep(10000); // 10ms 防止CPU占用过高
		}
	}
    printf("App finished!\r\n");

    close(fd);
    return 0;
}

4.6 测试

# VSCODE终端
make
arm-linux-gnueabihf-gcc irqAPP.c -o irqAPP
sudo cp irq.ko irqAPP /home/for/linux/nfs/rootfs/lib/modules/4.1.15/ -f

# 串口
cd /lib/modules/4.1.15/
depmod
modprobe irq.ko
cat /proc/interrupts    # 此时在列表最右一列中应当能看到KEY0

/lib/modules/4.1.15 # ./irqAPP /dev/timer
# 然后按下按键/松开按键,串口应有对应的输出


rmmod irq.k
cat /proc/interrupts    # 此时应当看不到KEY0

5. 实验:按键中断+定时器消抖+工作队列

有以下修改:
        中断设备结构体中增加了工作队列结构体work_struct

        中断处理函数key0_handler中增加了对工作队列的调用schedule_work()

        初始化函数keyio_init()中增加了对工作队列的初始化INIT_WORK

        增加了work处理函数key_work()

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/stat.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/timer.h>
#include <linux/interrupt.h>
#include <linux/atomic.h>

#define DEV_CNT  1          /* 设备号数量 */
#define DEV_NAME "timer"    /* 设备名 */
#define KEY_NUM 1           /* 按键数量 */
#define KEY0_VALUE 0x01     /* 按键值(假定释放为1,按下为0)*/
#define INVAKEY    0xFF     /* 按键无效值 */

/* 按键结构体 */
struct irq_keydesc{
    int gpio;   /* io号 */
    int irqnum; /* 中断号 */
    unsigned char value; /* 键值 (默认/空闲电平,比如 1) */
    char name[10];  /* 名字 */
    irqreturn_t (*handler) (int, void *);   /* 中断处理函数*/
    // struct tasklet_struct tasklet;
    struct work_struct work;
};

/* 中断设备结构体 */
struct irq{
    dev_t devid;    // 设备号
    int major;      // 主设备号
    int minor;      // 次
    struct cdev cdev;       // cdev
    struct device *device;  // 设备
    struct class *class;    // 类
    struct device_node *nd; // 节点
    struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键
    struct timer_list timer;// 定时器

    atomic_t releasekey;  // 0表示按键释放,1表示按键按下
    atomic_t keyvalue;    // 第八位表示数据是否被读取:每完成一次按键释放,第八位就会置1,用户态调用.read时会将其置0,表示数据被读取
                          // 其余七位表示按键的值
};
static struct irq key_irq;

/* 打开/读操作 */
static int irq_open(struct inode *inode, struct file *filp){
    filp->private_data = &key_irq;
    return 0;
}
static ssize_t irq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){
    int ret = 0;
    unsigned char keyvalue = 0;
    unsigned char releasekey = 0;
    struct irq* dev = filp->private_data;

    keyvalue = (unsigned char)atomic_read(&dev->keyvalue);
    releasekey = (unsigned char)atomic_read(&dev->releasekey);

    if(releasekey){ // 如果按键释放,此时可以读取数据
        if(keyvalue & 0x80){  // 如果最高位为1,表示此时数据还未被读取
            unsigned char out = keyvalue & 0x7F; // 将最高位置0(最高位为有效位,低7位为实际value)
            if(copy_to_user(buf, &out, sizeof(out))){            // 返回按键值给用户态
                return -EFAULT;
            }
            ret = sizeof(out);
        } else {
            goto data_error;
        }
        atomic_set(&dev->releasekey, 0); // 释放标志清零
        return ret;
    } else {
        goto data_error;
    }

data_error:
    return -EINVAL;
}

/* 操作集 */
static const struct file_operations key_fops = {
    .owner = THIS_MODULE,
    .open = irq_open,
    .read = irq_read,
};

/* 中断处理函数 */
static irqreturn_t key0_handler(int irq, void *dev_id){
    struct irq *dev = (struct irq*)dev_id;
    int current_level;  // 保存中断时的电平

    current_level = gpio_get_value(dev->irqkey[0].gpio);
    atomic_set(&dev->keyvalue, current_level);  // 保存基准电平

    // tasklet_schedule(&dev->irqkey[0].tasklet); // 调度tasklet处理后续逻辑,避免中断耗时
    schedule_work(&dev->irqkey[0].work);

    return IRQ_HANDLED;
}

/* 定时器处理函数 */
static void timer_func(unsigned long arg){
    struct irq* dev = (struct irq*)arg;
    int current_value; // 保存定时器延时后的电平

    /* 读取当前按键电平 */
    current_value = gpio_get_value(dev->irqkey[0].gpio);

    /* 若当前电平与中断发生时读取到的电平一致,则有效 */
    if (current_value == atomic_read(&dev->keyvalue)) {
        if (current_value == 1) { // 释放
            printk("KEY0 release!\r\n");
            atomic_set(&dev->keyvalue, 0x80 | dev->irqkey[0].value); // 最高位置1,表示有效
            atomic_set(&dev->releasekey, 1);  // 表示已释放,此时用户态可读
        } else { // 按下
            printk("KEY0 Push!\r\n");
            atomic_set(&dev->keyvalue, current_value); // 这里0 表示按下
            // atomic_set(&dev->releasekey, 0);  // 表示未释放
        }
    }
}

/* tasklet处理函数 */
static void key_tasklet(unsigned long data){
    struct irq* dev = (struct irq*)data;
    printk("      key_tasklet\r\n");
    dev->timer.data = data;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */
    
}
/* 工作队列处理函数 */
static void key_work(struct work_struct *work){
    printk("      key_work\r\n");
    key_irq.timer.data = (unsigned long)&key_irq;
    mod_timer(&key_irq.timer, jiffies + msecs_to_jiffies(20)); /* 20ms定时 */
}

/* 初始化按键 */
static int keyio_init(struct irq *dev){
    int ret = 0;
    int i = 0;

    /* 1. 按键初始化 */
    dev->nd = of_find_node_by_path("/key");  // 获取设备节点
    if(dev->nd == NULL){
        ret = -EINVAL;
        goto fail_nd;
    }
    for(i=0; i<KEY_NUM; i++){
        dev->irqkey[i].gpio = of_get_named_gpio(dev->nd, "key-gpios", i); // 获取设备树中 gpio
        if (dev->irqkey[i].gpio < 0) {
            pr_err("get gpio %d failed\n", i);
            ret = -EINVAL;
            goto fail_nd;
        }
    }

    for(i=0; i<KEY_NUM; i++){
        memset(dev->irqkey[i].name, 0, sizeof(dev->irqkey[i].name));
        sprintf(dev->irqkey[i].name, "KEY%d", i);   // 命名
        ret = gpio_request(dev->irqkey[i].gpio, dev->irqkey[i].name); // 申请gpio
        if (ret) {
            pr_err("gpio_request %d failed\n", dev->irqkey[i].gpio);
            goto fail_gpio_req;
        }
        gpio_direction_input(dev->irqkey[i].gpio);  // 设置io方向

        dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio);// 获取中断号
        if (dev->irqkey[i].irqnum < 0) {
            pr_err("gpio_to_irq failed for gpio %d\n", dev->irqkey[i].gpio);
            ret = dev->irqkey[i].irqnum;
            goto fail_gpio_req;
        }
    }

    dev->irqkey[0].handler = key0_handler; // 中断处理函数
    dev->irqkey[0].value = KEY0_VALUE;     // 例如:1 表示 release(高电平),0 表示按下(低电平)

    /* 2. 中断初始化 */
    for(i=0; i<KEY_NUM; i++){
        ret = request_irq(dev->irqkey[i].irqnum,
                          dev->irqkey[i].handler,
                          IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                          dev->irqkey[i].name,
                          &key_irq); /* dev_id 传入结构体指针 */
        if(ret < 0){
            printk("irq %d request failed! ret=%d\n", dev->irqkey[i].irqnum, ret);
            while (--i >= 0) {
                free_irq(dev->irqkey[i].irqnum, &key_irq);
            }
            goto fail_irq;
        }

        // tasklet_init(&dev->irqkey[i].tasklet, key_tasklet, (unsigned long)dev);
        INIT_WORK(&dev->irqkey[i].work, key_work);
    }

    return 0;

fail_irq:
fail_gpio_req:
    for (i = 0; i < KEY_NUM; i++) {
        if (gpio_is_valid(dev->irqkey[i].gpio))
            gpio_free(dev->irqkey[i].gpio);
    }
fail_nd:
    return ret;
}

/* 驱动入口 */
static int __init key_init(void){
    int ret = 0;

    /* 1. 注册字符设备驱动 */
    key_irq.devid = 0;
    alloc_chrdev_region(&key_irq.devid, 0, DEV_CNT, DEV_NAME);
    key_irq.major = MAJOR(key_irq.devid);
    key_irq.minor = MINOR(key_irq.devid);

    /* 2. 初始化 cdev */
    key_irq.cdev.owner = THIS_MODULE;
    cdev_init(&key_irq.cdev, &key_fops);

    /* 3. 添加 cdev */
    ret = cdev_add(&key_irq.cdev, key_irq.devid, DEV_CNT);
    if (ret) {
        pr_err("cdev_add failed\n");
        goto fail_cdev;
    }

    /* 4. 创建类 */
    key_irq.class = class_create(THIS_MODULE, DEV_NAME);
    if(IS_ERR(key_irq.class)){
        ret = PTR_ERR(key_irq.class);
        goto fail_class;
    }

    /* 5. 创建设备 */
    key_irq.device = device_create(key_irq.class, NULL, key_irq.devid, NULL, DEV_NAME);
    if(IS_ERR(key_irq.device)){
        ret = PTR_ERR(key_irq.device);
        goto fail_device;
    }

    /* 6. 初始化IO*/
    ret = keyio_init(&key_irq);
    if(ret < 0){
        goto fail_keyinit;
    }

    /* 7. 初始化定时器 */
    init_timer(&key_irq.timer);
    key_irq.timer.function = timer_func;
    key_irq.timer.data = (unsigned long)&key_irq; /* 初始化 timer.data 指向设备结构体 */

    /* 8. 初始化原子变量 */
    atomic_set(&key_irq.keyvalue, INVAKEY);
    atomic_set(&key_irq.releasekey, 0);

    return 0;

fail_keyinit:
    device_destroy(key_irq.class, key_irq.devid);
fail_device:
    class_destroy(key_irq.class);
fail_class:
    cdev_del(&key_irq.cdev);
fail_cdev:
    unregister_chrdev_region(key_irq.devid, DEV_CNT);
    return ret;
}


/* 驱动出口 */
static void __exit key_exit(void){
    int i=0;
    /* 释放中断 */
    for(i=0; i<KEY_NUM; i++){
        free_irq(key_irq.irqkey[i].irqnum, &key_irq);
    }
    /* 释放io */
    for(i=0; i<KEY_NUM; i++){
        if (gpio_is_valid(key_irq.irqkey[i].gpio))
            gpio_free(key_irq.irqkey[i].gpio);
    }
    /* 删除定时器 */
    del_timer_sync(&key_irq.timer);

    /* 注销字符设备驱动 */
    cdev_del(&key_irq.cdev);
    unregister_chrdev_region(key_irq.devid, DEV_CNT);

    device_destroy(key_irq.class, key_irq.devid);
    class_destroy(key_irq.class);
}

module_init(key_init);
module_exit(key_exit);
MODULE_LICENSE("GPL");

连着两个key_work应该就是发生了抖动,不过都被消抖拦住了↓

6. 附录

视频第13.7讲的后半部分讲了一下container_of函数的使用方法。

        container_of通过指向结构体中某个成员的指针,反推出整个结构体的指针,并返回任意成员的地址:

// 需要#include <linux/kernel.h>

container_of(ptr, type, member)
// prt:    已知的成员指针
// type:   结构体的定义类型
// member: 要获得地址的成员名
// return: member的地址

如本次实验中的结构体定义:

/* 按键结构体 */
struct irq_keydesc{
    int gpio;   /* io号 */
    int irqnum; /* 中断号 */
    unsigned char value; /* 键值 (默认/空闲电平,比如 1) */
    char name[10];  /* 名字 */
    irqreturn_t (*handler) (int, void *);   /* 中断处理函数*/
    // struct tasklet_struct tasklet;
    struct work_struct work;
};

/* 中断设备结构体 */
struct irq{
    dev_t devid;    // 设备号
    int major;      // 主设备号
    int minor;      // 次
    struct cdev cdev;       // cdev
    struct device *device;  // 设备
    struct class *class;    // 类
    struct device_node *nd; // 节点
    struct irq_keydesc irqkey[KEY_NUM]; // KEY_NUM 个按键
    struct timer_list timer;// 定时器

    atomic_t releasekey;  // 0表示按键释放,1表示按键按下
    atomic_t keyvalue;    // 第八位表示数据是否被读取:每完成一次按键释放,第八位就会置1,用户态调用.read时会将其置0,表示数据被读取
                          // 其余七位表示按键的值
};

调用该函数就能直接获得任意成员地址:

// 工作队列处理函数
static void key_work(struct work_struct *work_){
    struct irq* dev = container_of(work_, struct irq, work); // 获得工作队列work成员的地址
}


网站公告

今日签到

点亮在社区的每一天
去签到