版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/ 发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2021 凌云物网智科实验室·郭工

  • Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292

wechat_pub

3.5. 中断子系统——按键中断实验

这里主要介绍在驱动中如何使用中断,对于中断的概念及GIC中断控制器相关内容回顾一下关于中断相关的单片机部分相关视频。

3.5.1. 在设备树中添中断信息

打开 ./arch/arm/boot/dts/ 目录下的 imx6ul.dtsi 设备树文件, 找到“interrupt-controller”节点,如下所示。

intc: interrupt-controller@a01000 {
                        compatible = "arm,gic-400", "arm,cortex-a7-gic";
                        interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(1) | IRQ_TYPE_LEVEL_HIGH)>;
                        #interrupt-cells = <3>;
                        interrupt-controller;
                        interrupt-parent = <&intc>;
                        reg = <0x00a01000 0x1000>,
                              <0x00a02000 0x2000>,
                              <0x00a04000 0x2000>,
                              <0x00a06000 0x2000>;
                };
  • compatible:compatible属性用于平台设备驱动的匹配

  • reg:reg指定中断控制器相关寄存器的地址及大小

  • interrupt-controller:声明该设备树节点是一个中断控制器。

  • #interrupt-cells :指定它的“子”中断控制器用几个cells来描述一个中断,可理解为用几个参数来描述一个中断信息。 在这里的意思是在intc节点的子节点将用3个参数来描述中断。

GIC架构分为了:分发器(Distributor)和 CPU接口(CPU Interface),上面设备树节点就是用来描述整个GIC控制器的。

3.5.1.1. gpc一级子中断控制器

在imx6ul.dtsi文件中直接搜索节点标签“intc”即可找到“一级子中断控制器”

      gpc: gpc@20dc000 {
                                compatible = "fsl,imx6ul-gpc", "fsl,imx6q-gpc";
                                reg = <0x020dc000 0x4000>;
                                interrupt-controller;
                                #interrupt-cells = <3>;
                                interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
                                interrupt-parent = <&intc>;
                                fsl,mf-mix-wakeup-irq = <0x7c00000 0x7d00 0x0 0x1400640>;
                        };

结合以上代码介绍如下:

  • interrupt-controller:声明该设备树节点是一个中断控制器,只要是中断控制器都要用该标签声明。

  • #interrupt-cells:用于规定该节点的“子”中断控制器将使用三个参数来描述子控制器的信息。

  • interrupt-parent:指定该中断控制器的“父”中断控制器。除了“顶层中断控制器”其他中断控制器都要声明“父”中断控制器。

  • interrupts:具体的中断描述信息,在该节点的中断控制器的“父”中断控制器,规定了使用三个cells来描述子控制器的信息。 三个参数表示的含义如下:

    第一个参数用于指定中断类型,在GIC中中断的类型有三种(SPI共享中断、PPI私有中断、SGI软件中断), 我们使用的外部中断均属于SPI中断类型。

    第二个参数用于设定中断编号,范围和第一个参数有关。PPI中断范围是[0-15],SPI中断范围是[0-987]。

    第三个参数指定中断触发方式,参数是一个u32类型,其中后四位[0-3]用于设置中断触发类型。 每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:

#define IRQ_TYPE_NONE           0
#define IRQ_TYPE_EDGE_RISING    1
#define IRQ_TYPE_EDGE_FALLING   2
#define IRQ_TYPE_EDGE_BOTH      (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH     4
#define IRQ_TYPE_LEVEL_LOW      8

[8-15]位在PPI中断中用于设置“CPU屏蔽”。在多核系统中这8位用于设置PPI中断发送到那个CPU,一位代表一个CPU, 为1则将PPI中断发送到CPU,否则屏蔽。imx6ull是单核CPU,所以我们不用设置这些位。

3.5.1.2. 二级子中断控制器

同样在imx6ul.dtsi文件中直接搜索节点标签“gpc”即可找到“二级子中断控制器”如下所示。

 soc {
                #address-cells = <1>;
                #size-cells = <1>;
                compatible = "simple-bus";
                interrupt-parent = <&gpc>;
                ranges;
  			   //busfreq子节点
                busfreq {
                        compatible = "fsl,imx_busfreq";
                    /*....省略....*/
                };
       };                

soc 节点即片上外设“总节点”,翻阅源码可以发现该节点很长,我们使用的外设大多包含在里面。 具体外设(例如GPIO)也可作为中断控制器,这里声明了它们的“父”中断控制器是 <&gpc>节点。

soc节点内包含的中断控制器很多,几乎用到中断的外设都是中断控制器,我们使用的是开发板上的按键, 使用的是GPIO4_14,所以这里以GPIO4为例介绍。在imx6ul.dtsi文件中直接搜索GPIO4,找到GPIO4对应的设备树节点,如下所示。

 gpio4: gpio@20a8000 {
                                compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
                                reg = <0x020a8000 0x4000>;
                                interrupts = <GIC_SPI 72 IRQ_TYPE_LEVEL_HIGH>,
                                             <GIC_SPI 73 IRQ_TYPE_LEVEL_HIGH>;
                                clocks = <&clks IMX6UL_CLK_GPIO4>;
                                gpio-controller;
                                #gpio-cells = <2>;
                                interrupt-controller;
                                #interrupt-cells = <2>;
                                gpio-ranges = <&iomuxc 0 94 17>, <&iomuxc 17 117 12>;
                        };

以上是gpio4节点的全部内容,这里主要介绍和中断相关的节点信息。

  • interrupts:用来描述GPIO4能产生中断类型及中断编号、触发方式,查看imx6ull的数据手册我们可以知道, GPIO4能产生的中断只有两个,分配的中断ID为106、107,对于SPI中断它们的编号是72(106-32),73(107-32)。

  • interrupt-controller:声明该节点是一个中断控制器

  • #interrupt-cells:声明该节点的子节点将用多少个参数来描述中断信息。

3.5.1.3. 按键设备树节点

以上三部分的内容是内核为我们提供的,我们要做的内容很简单, 只需要在我们编写的设备树节点中 引用已经写好的中断控制器父节点以及配置中断信息即可,如下所示。

&key_irq {
        compatible = "my-gpio-keys";
        pinctrl-names = "default";
        pinctrl-0 = <&pinctrl_my_gpio_keys>;
        key_gpio = <&gpio4 14 GPIO_ACTIVE_LOW>;//默认低电平,按键按下高电平
        status = "okay";

        interrupt-parent = <&gpio4>;
        interrupts = <14 IRQ_TYPE_EDGE_RISING>;// 指定中断,触发方式为上升沿触发
};

这里主要介绍和中断相关部分的内容。

  • interrupt-parent:指定“父控制器节点 ”。我们按键所在的引脚是gpio4_14,故我们按键所在的中断控制父节点 为gpio4。

  • interrupts:在gpio4节点中定义使用两个cells来描述我们的按键信息,‘14’表示的是我们按键GPIO4中引脚编号, “IRQ_TYPE_EDGE_RISING”表示的是触发方式。触发方式宏定义如下:

#define IRQ_TYPE_NONE           0
#define IRQ_TYPE_EDGE_RISING    1
#define IRQ_TYPE_EDGE_FALLING   2
#define IRQ_TYPE_EDGE_BOTH      (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
#define IRQ_TYPE_LEVEL_HIGH     4
#define IRQ_TYPE_LEVEL_LOW      8

需要注意的是,我们编写的这个节点并不是个中断控制器,所以没有“interrupt-controller”标签。

3.5.2. 中断相关函数

在Linux内核中也提供了大量的中断相关的API函数,每个中断都有一个中断号,通过中断号即可区分不同的中断。在Linux内核中使用一个int变量表示中断号。

3.5.2.1. request_irq中断注册函数

在linux内核中要想使用某个中断是需要申请的,request_irq函数用于申请中断,request_irq函数可能会导致休眠,因此不能在中断上下文或其他禁止睡眠的代码段中使用request_irq函数。

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

参数

  • irq:用于指定“内核中断号”,这个参数我们会从设备树中获取或转换得到。在内核空间中它代表一个唯一的中断编号。

  • handler:用于指定中断处理函数,中断发生后跳转到该函数去执行。

  • flags:中断触发条件,也就是我们常说的上升沿触发、下降沿触发等等 触发方式通过“|”进行组合(注意,这里的设置会覆盖设备树中的默认设置),宏定义如下所示:

#define IRQF_TRIGGER_NONE       0x00000000
#define IRQF_TRIGGER_RISING     0x00000001
#define IRQF_TRIGGER_FALLING        0x00000002
#define IRQF_TRIGGER_HIGH       0x00000004
#define IRQF_TRIGGER_LOW        0x00000008
#define IRQF_TRIGGER_MASK       (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
                                IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE      0x00000010

#define IRQF_SHARED         0x00000080 ---------①//多个设备共享一个中断
/*-----------以下宏定义省略------------*/
  • name:中断的名字,中断申请成功后会在“/proc/interrupts”目录下看到对应的文件。

  • dev: 如果使用了IRQF_SHARED 宏,则开启了共享中断。“共享中断”指的是多个驱动程序共用同一个中断。 开启了共享中断之后,中断发生后内核会依次调用这些驱动的“中断服务函数”。 这就需要我们在中断服务函数里判断中断是否来自本驱动,这里就可以用dev参数做中断判断。 即使不用dev参数判断中断来自哪个驱动,在申请中断时也要加上dev参数 因为在注销驱动时内核会根据dev参数决定删除哪个中断服务函数。

返回值

  • 成功:返回0

  • 失败:返回负数。

3.5.2.2. free_irq中断注销函数

使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。

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

参数

  • irq:从设备树中得到或者转换得到的中断编号。

  • dev:与request_irq函数中dev传入的参数一致。

3.5.2.3. 中断处理函数

在中断申请时需要指定一个中断处理函数,书写格式如下所示。

irqreturn_t (*irq_handler_t)(int irq, void * dev); 

参数

  • irq:用于指定“内核中断号”。

  • dev:在共享中断中,用来判断中断产生的驱动是哪个,具体介绍同上中断注册函数。 不同的是dev参数是内核“带回”的。如果使用了共享中断还得根据dev带回的硬件信息判断中断是否来自本驱动,或者不使用dev,直接读取硬件寄存器判断中断是否来自本驱动。如果不是,应当立即跳出中断服务函数,否则正常执行中断服务函数。

返回值

  • irqreturn_t类型:枚举类型变量,如下所示。

enum irqreturn {
    IRQ_NONE                = (0 << 0),
    IRQ_HANDLED             = (1 << 0),
    IRQ_WAKE_THREAD         = (1 << 1),
};

typedef enum irqreturn irqreturn_t;

如果是“共享中断”并且在中断服务函数中发现中断不是来自本驱动则应当返回 IRQ_NONE , 如果没有开启共享中断或者开启了并且中断来自本驱动则返回 IRQ_HANDLED,表示中断请求已经被正常处理。 第三个参数涉及到我们后面会讲到的中断服务函数的“上半部分”和“下半部分”, 如果在中断服务函数是使用“上半部分”和“下半部分”实现,则应当返回IRQ_WAKE_THREAD。

3.5.2.4. 关闭和开启全局中断相关函数(宏定义)

关闭和开启全局中断相关函数

local_irq_enable() 
local_irq_disable() 
local_irq_save(flags)
local_irq_restore(flags) 

由于“全局中断”的特殊性,通常情况下载关闭之前要使用local_irq_save保存当前中断状态, 开启之后使用local_irq_restore宏恢复关闭之前的状态。flags是一个unsigned long 类型的数据。

了解了以上函数的作用,我们就可以编写中断的驱动程序了, 如有遗漏的内容我们将会在代码介绍中,驱动程序介绍如下。

3.5.3. 内核定时器

Linux内核中有大量的函数需要时间管理,比如周期性的调度程序、延时程序。硬件定时器提供时钟源,时钟源的频率可以设置,设置以后就周期性的产生定时中断,系统使用定时中断来计时。

3.5.3.1. 定时器时间单位

可以在内核源码根目录下用“ls -a”看到一个隐藏文件,它就是内核配置文件。打开 Linux 内核源码根目录下的.config 文件可以看到如下这项:

系统节拍率为 100Hz、200Hz、250Hz、300Hz、500Hz

CONFIG_HZ=100 //CONFIG_HZ=100 表示每个滴答是 10ms。

这表示内核每秒中会发生 100 次系统滴答中断(tick),这就像人类的心跳一样,这是 Linux系统的心跳。每发生一次 tick 中断,全局变量 jiffies 就会累加 1。jiffies/HZ 就 是系统运行时间,单位为秒。

高节拍率和低节拍率的优缺点:

①、高节拍率会提高系统时间精度,如果采用 100Hz 的节拍率,时间精度就是 10ms,采用1000Hz 的话时间精度就是 1ms,精度提高了 10 倍。高精度时钟的好处有很多,对于那些对时间要求严格的函数来说,能够以更高的精度运行,时间测量也更加准确。

②、高节拍率会导致中断的产生更加频繁,频繁的中断会加剧系统的负担,1000Hz 和 100Hz的系统节拍率相比,系统要花费 10 倍的“精力”去处理中断。中断服务函数占用处理器的时间增加,但是现在的处理器性能都很强大,所以采用 1000Hz 的系统节拍率并不会增加太大的负载压力。

3.5.3.2. 内核定定时器的使用

Linux 内核定时器使 用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行。

要注意一点,内核定时器并不是周期 性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。

1、先定义一个 timer_list 变量

表示定时器,tiemr_list 结构体的 expires 成员变量表示超时时间,单位为节拍数。

struct timer_list {  /*  * All  fields that change during normal runtime grouped to the  * same  cacheline  */  	struct hlist_node  entry;  
 	unsigned long  expires;  /*定时器超时时间,单位是节拍数*/
 	void (*function)(struct timer_list  *);  /*定时处理函数*/
 	u32  flags;  
 	#ifdef CONFIG_LOCKDEP  struct lockdep_map  lockdep_map;  
 	#endif  
 };  

比如我们要定义一个周期为2s的定时器,那么这个定时器的超时时间就是 $$ expires=jiffies+(2*HZ) $$ 定义好以后还需要一系列的API来初始化定时器。

2、 定时器初始化 timer_list 结构体

负责初始化timer_list类型变量,当我们定义了一个tiemr_list 变量以后一定要先初始化一下。

void setup_timer(timer,  fn, data);

参数

  • timer:要初始化定时器。

  • fn:函数

  • 返回值:没有返回值。

3、向内核添加定时器

add_timer函数用于向linux内核注册定时器,使用add_timer函数向内核注册定时器以后,定时器就会开始运行。

void add_timer(struct timer_list *timer);

参数

  • timer:要注册的定时器。

  • 返回值:没有返回值。

  • timer->expires 表示超时时间。 当超时时间到达,内核就会调用这个函数:timer->function(timer->data)。

4、修改定时器的超时时间

mod_timer函数用于修改定时值,如果定时器还没有激活的话,mod_timer函数会激活定时器。

 int  mod_timer(struct timer_list *timer, unsigned long expires);

参考

timer:要修改超时时间(定时值)的定时器。

expires:修改后的超时时间。

返回值: 0,调用 mod_timer 函数前定时器未被激活; 1,调用 mod_timer 函数前定时器已被激活。

它等同于:del_timer(timer); timer->expires = expires; add_timer(timer); 但是更加高效。

5、删除定时器

用于删除一个定时器,不管定时器有没有被激活,都可以使用函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用del_timer函数删除定时器之前要先等待其他处理器的定时器函数退出。

int del_timer(struct timer_list *timer);

timer:要删除的定时器。

**返回值:**0,定时器还没被激活;1,定时器已经激活。

int del_timer_sync(struct timer_list *timer);

del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除

timer:要删除的定时器。

**返回值:**0,定时器还没被激活;1,定时器已经激活。

3.5.4. 按键中断程序实现

3.5.4.1. 设备树实现

下面介绍两种实现设备树的方式,一种是直接在设备树下添加设备的设备树,第二种是使用打补丁的方式实现。

3.5.4.1.1. 设备树实现

1、添加pinctrl节点

pinctrl_my_gpio_keys: my_gpio-keys {
		fsl,pins = <
		MX6UL_PAD_NAND_CE1_B__GPIO4_IO14 0x17059 /* gpio key */
		>;
	};

2、添加KEY设备节点

&key_irq {
	compatible = "my-gpio-keys";
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_my_gpio_keys>;
	gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
	status = "okay";
	interrupt-parent = <&gpio4>;
	interrupts = <14 3>;
};

3、检查PIN是否被其他外设使用

先检查NAND_CE1_B这个PIN有没有被其他pinctrl节点使用, 如果有使用的话就要屏蔽掉,然后再检查GPIO4_IO14这个GPIO有没有被其他外设使用,如果有的话也要屏蔽掉。

3.5.4.1.2. 设备树插件实现

本文只是实现设备树插件,设备树插件的工作原理参考链接:Linux内核设备树overlays (qq.com)

1、/下新增的key_irq节点

先在igkboard.dts下根节点下添加key_irq节点

	/*KEY*/
	key_irq{
		compatible = "gpio-keys"; 
		status = "okay";
	};

2、在overlay下追加key_irq节点

在imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays/下添加key.dts文件,然后添加key_irq的设备树。

/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include "../imx6ul-pinfunc.h"
/* key on Pin#55 (GPIO4_IO014) */
&key_irq {
	compatible = "my-gpio-keys";
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_my_gpio_keys>;
	gpios = <&gpio4 14 GPIO_ACTIVE_LOW>;
	status = "okay";
	interrupt-parent = <&gpio4>;
	interrupts = <14 3>;
};

&iomuxc {
	pinctrl_my_gpio_keys: my_gpio-keys {
		fsl,pins = <
		MX6UL_PAD_NAND_CE1_B__GPIO4_IO14 0x17059 /* gpio key */
		>;
	};
};

注意:节点下追加一点要同名

3、修改Makefile

# SPDX-License-Identifier: GPL-2.0
# required for overlay support
DTC_FLAGS += -@
dtb-y += can1.dtbo
dtb-y += can2.dtbo
dtb-y += i2c1.dtbo
dtb-y += spi1.dtbo
dtb-y += uart2.dtbo
dtb-y += uart3.dtbo
dtb-y += uart4.dtbo
dtb-y += uart7.dtbo
dtb-y += pwm7.dtbo
dtb-y += pwm8.dtbo
dtb-y += w1.dtbo
dtb-y += lcd.dtbo
dtb-y += cam.dtbo
dtb-y += nbiot-4g.dtbo
dtb-y += oled_spi.dtbo
dtb-y += key.dtbo

然后回到源码目录下执行make dtbs,这样就会在之前的overlay文件下生成相应的.dtbo文件

4、修改开发板配置

到mmcblkp1目录下修改文件

  root@igkboard:~#  cd /run/media/mmcblk1p1/  
  root@igkboard:/run/media/mmcblk1p1#  ls  
  config-lcd.txt config.txt   igkboard.dtb overlays zImage  root@igkboard:/run/media/mmcblk1p1#    

先删除旧的设备树文件

下载新的igkboard.dtb文件、和生成的.dtbo文件(放在overlays文件夹下)

  root@igkboard:/run/media/mmcblk1p1/overlays#  ls 
  adc.dtbo can1.dtbo   i2c1.dtbo led.dtbo    oled_spi.dtbo pwm8.dtbo   uart2.dtbo uart4.dtbo w1.dtbo  cam.dtbo can2.dtbo   lcd.dtbo  	   nbiot-4g.dtbo pwm7.dtbo   spi1.dtbo uart3.dtbo   uart7.dtbo  key.dtbo
  root@igkboard:/run/media/mmcblk1p1/overlays#  cd  
 

5、然后修改config.txt

  # Enable LCD  overlay  
  dtoverlay_lcd=yes     
  
  # Enable Camera  overlay  
  #dtoverlay_cam=no     
  
  # Enable I2C  overlay  
  dtoverlay_i2c1=yes   
  
  # Enable SPI  overlay, SPI1 conflict with UART8(NB-IoT/4G module) 
  dtoverlay_spi1=yes     
  
  # Enable UART  overlays  
  dtoverlay_uart=2 3  4 7     
  
  # Enable CAN  overlays  
  #dtoverlay_can=1 2   
  
  # Enable PWM  overlays, PWM8 conflict with UART8(NB-IoT/4G module)  
  #dtoverlay_pwm=7 8        
  
  # Enable extra  overlays 
  #dtoverlay_extra=w1  adc oled_spi  
  dtoverlay_extra=key  

然后sync一下再reboot开发板,sync就是把刚刚修改的config.txt配置写入uboot,防止修改没有保存成功,重启开发板后就能看到新增的key文件启动成功。

6、检查修改是否成功

cd /proc/device-tree设备树下已经有了 key_irq设备节点

3.5.4.2. 按键中断驱动程序实现

实现板载key0按键驱动,并使用定时器消抖。

#include <linux/init.h>                     // 驱动程序必须的头文件
#include <linux/module.h>                   // 驱动程序必须的头文件
#include <linux/errno.h>                    // ENODEV,ENOMEM存放的头文件
#include <linux/types.h>                    // u8,u32,dev_t等类型在该头文件中定义          
#include <linux/kernel.h>                   // printk(),内核打印函数
#include <linux/fs.h>                       // file_operations,用于联系系统调用和驱动程序
#include <linux/cdev.h>                     // cdev_alloc(),分配cdev结构体
#include <linux/device.h>                   // 用于自动生成设备节点的函数头文件
#include <linux/of_gpio.h>                  // gpio子系统的api
#include <linux/platform_device.h>          // platform总线驱动头文件
#include <linux/gpio.h>
#include <linux/of_device.h>
#include <asm/uaccess.h>                    // 内核和用户传输数据的函数
#include <linux/of_irq.h>                   // 中断相关函数
#include <linux/irq.h>                      // 中断相关函数
#include <linux/interrupt.h>
#include <linux/timer.h>                    // 定时器相关函数


#define KEY_NAME                "key_irq"
#define KEY0VALUE               0XF0        // 按键值 
#define INVAKEY                 0X00        // 无效的按键值 

static int                      dev_major = 0;

// 存放key信息的结构体
struct platform_key_data 
{
    char                        name[16];       // 设备名字
    int                         key_gpio;       // gpio编号
    unsigned char                value;         // 按键值

    int                         irq;            // 中断号
    irqreturn_t (*handler)(int, void *);        // 中断处理函数
};

// 存放key的私有属性
struct platform_key_priv 
{
    struct cdev                 cdev;                   // cdev结构体
    struct class                *dev_class;             // 自动创建设备节点的类
    int                         num_key;                // key的数量
    struct platform_key_data    key;                    // 存放key信息的结构体数组

    atomic_t                    keyvalue;               // 有效的按键键值,用于向应用层上报
    atomic_t                    releasekey;             // 标记是否完成一次完成的按键,用于向应用层上报
    struct timer_list           timer;                  // 定时器,用于消抖 
};

// 为key私有属性开辟存储空间的函数
static inline int sizeof_platform_key_priv(int num_key)
{
    return sizeof(struct platform_key_priv) + (sizeof(struct platform_key_data) * num_key);
}

// 中断服务函数,初始化定时器用于消抖
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    struct platform_key_priv *priv = (struct platform_key_priv *)dev_id;

    // 开启定时器
    //priv->timer.data = (volatile long)dev_id;
    mod_timer(&(priv->timer), jiffies + msecs_to_jiffies(10));

    return IRQ_RETVAL(IRQ_HANDLED);
}

// 定时器服务函数,定时器到了后的操作
// 定时器到了以后再次读取按键,如果按键还是按下状态则有效
void timer_function(struct timer_list *t)
{
	unsigned char value;
	struct platform_key_priv *priv = from_timer(priv, t, timer);

    // 读取按下的io值
	value = gpio_get_value(priv->key.key_gpio); 	
	if(value == 0) // 按下按键
    { 					
		atomic_set(&(priv->keyvalue), value);
        printk("keypress\n");
	}
	else // 按键松开
    { 									
		atomic_set(&(priv->keyvalue), 0x80 | value);
		atomic_set(&(priv->releasekey), 1); // 标记松开按键,即完成一次完整的按键过程 	
        printk("keyrelease\n");
	}	
}

// 解析设备树,初始化key属性并初始化中断
int parser_dt_init_key(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;     // 当前设备节点
    struct platform_key_priv *priv;                 // 存放私有属性
    int num_key, gpio;                              // key数量和gpio编号      
    int ret;                           

    /* 1)按键初始化 */
    // 获取该节点下子节点的数量
    num_key = 1;
    if(num_key <= 0) 
    {
        dev_err(&pdev->dev, "fail to find node\n");
        return -EINVAL;
    }

    // 分配存储空间用于存储按键的私有数据
    priv = devm_kzalloc(&pdev->dev, sizeof_platform_key_priv(num_key), GFP_KERNEL);
    if (!priv)
    {
        return -ENOMEM;
    }

    // 通过dts属性名称获取gpio编号
    gpio = of_get_named_gpio(np, "gpios", 0);

    // 将子节点的名字,传给私有属性结构体中的key信息结构体中的name属性
    strncpy(priv->key.name, np->name, sizeof(priv->key.name)); 

    // 将gpio编号和控制亮灭的标志传给结构体
    priv->key.key_gpio = gpio; 

    // 申请gpio口,相较于gpio_request增加了gpio资源获取与释放功能
    if( (ret = devm_gpio_request(&pdev->dev, priv->key.key_gpio, priv->key.name)) < 0 ) 
    {
        dev_err(&pdev->dev, "fail to request gpio for %s\n", priv->key.name); 
        return ret;
    }

    // 设置gpio为输入模式,并设置初始状态
    if( (ret = gpio_direction_input(priv->key.key_gpio))< 0 ) 
    {
        dev_err(&pdev->dev, "can't request gpio output for %s\n", priv->key.name); 
    }

    /* 2)中断初始化 */
    // 从设备树中获取中断号
    priv->key.irq = irq_of_parse_and_map(np, 0);
    // 申请中断,并初始化value和中断处理函数
    priv->key.handler = key0_handler;
	priv->key.value = KEY0VALUE;

    ret = request_irq(priv->key.irq, priv->key.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, priv->key.name, priv);
    if(ret < 0)
    {
        printk("fail to request irq %d\n", priv->key.irq);
        return -EFAULT;
	}

    /*初始化 timer,设置定时器处理函数,还未设置周期,所以不会激活定时器*/
    timer_setup(&(priv->timer), timer_function, 0);
    priv->timer.expires = jiffies + HZ/5;
    priv->timer.function = timer_function;
    add_timer(&priv->timer);

    // 暂时先解决一个按键的问题
    priv->num_key = 1;
    dev_info(&pdev->dev, "success to get %d valid key\n", priv->num_key);

    // 将key的私有属性放入platform_device结构体的device结构体中的私有数据中
    platform_set_drvdata(pdev, priv);

    return 0;
}

static int key_open(struct inode *inode, struct file *file)
{
    struct platform_key_priv *priv;

    priv = container_of(inode->i_cdev, struct platform_key_priv, cdev);
    file->private_data = priv;

    return 0;
}

static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    unsigned char value;
    int ret = 0;

    struct platform_key_priv  *priv;
    priv = filp->private_data;

    if (0 == gpio_get_value(priv->key.key_gpio))    // key0 按下 
    { 
        while(!gpio_get_value(priv->key.key_gpio)); // 等待按键释放 
        atomic_set(&(priv->keyvalue), KEY0VALUE);
    } 
    else // 无效的按键值
    {
        atomic_set(&(priv->keyvalue), INVAKEY);
    }

    value = atomic_read(&(priv->keyvalue));         // 保存按键值 
    ret = copy_to_user(buf, &value, sizeof(value));

    return ret;
}

static int key_release(struct inode *inode, struct file *file)
{
    return 0;
}

static struct file_operations key_fops = 
{
    .owner = THIS_MODULE,
    .open = key_open,
    .read = key_read,
    .release = key_release,
};

static int platform_key_probe(struct platform_device *pdev)
{
    struct platform_key_priv    *priv;          // 临时存放私有属性的结构体
    struct device               *dev;           // 设备结构体
    dev_t                       devno;          // 设备的主次设备号
    int                         i, rv = 0;      

    // 1)解析设备树并初始化key状态
    rv = parser_dt_init_key(pdev);
    if( rv < 0 )
            return rv;

    // 将之前存入的私有属性,放入临时的结构体中
    priv = platform_get_drvdata(pdev);

    // 2)分配主次设备号
    if (0 != dev_major) 
    {   
        // 静态分配主次设备号
        devno = MKDEV(dev_major, 0); 	
        rv = register_chrdev_region(devno, priv->num_key, "KEY_NAME"); /*proc/devices/key_irq*/
    }   
    else 
    {   
        // 动态分配主次设备号
        rv = alloc_chrdev_region(&devno, 0, priv->num_key, "KEY_NAME"); 
        dev_major = MAJOR(devno); 
    }   
    if (rv < 0) 
    {   
        dev_err(&pdev->dev, "major can't be allocated\n"); 
        return rv; 
    }   

    // 3)分配cdev结构体
    cdev_init(&priv->cdev, &key_fops);
    priv->cdev.owner  = THIS_MODULE;

    rv = cdev_add (&priv->cdev, devno , priv->num_key); 
    if( rv < 0) 
    {
        dev_err(&pdev->dev, "struture cdev can't be allocated\n");
        goto undo_major;
    }

    // 4)创建类,实现自动创建设备节点
    priv->dev_class = class_create(THIS_MODULE, "key"); /* /sys/class/key */
    if( IS_ERR(priv->dev_class) ) 
    {
        dev_err(&pdev->dev, "fail to create class\n");
        rv = -ENOMEM;
        goto undo_cdev;
    }

    // 5)创建设备
    for(i=0; i<priv->num_key; i++)
    {
        devno = MKDEV(dev_major, i);
        dev = device_create(priv->dev_class, NULL, devno, NULL, "key%d", i);  /* /dev/key0 */
        if( IS_ERR(dev) ) 
        {
            dev_err(&pdev->dev, "fail to create device\n");
            rv = -ENOMEM;
            goto undo_class;
        }
    }

    printk("success to install driver[major=%d]!\n", dev_major);

    return 0;

undo_class:
    class_destroy(priv->dev_class);

undo_cdev:
    cdev_del(&priv->cdev);

undo_major:
    unregister_chrdev_region(devno, priv->num_key);

    return rv;
}

static int platform_key_remove(struct platform_device *pdev)
{
    struct platform_key_priv *priv = platform_get_drvdata(pdev);
    int i;
    dev_t devno = MKDEV(dev_major, 0);

    // 注销设备结构体,class结构体和cdev结构体
    for(i=0; i<priv->num_key; i++)
    {
        devno = MKDEV(dev_major, i);
        device_destroy(priv->dev_class, devno);
    }
    class_destroy(priv->dev_class);

    cdev_del(&priv->cdev); 
    unregister_chrdev_region(MKDEV(dev_major, 0), priv->num_key);

    // 将key的状态设置为0
    for (i = 0; i < priv->num_key; i++) 
    {
        gpio_set_value(priv->key.key_gpio, 0);
    }   

    // 删除定时器
    del_timer_sync(&(priv->timer));

    // 释放中断
    free_irq(priv->key.irq, priv);

    printk("success to remove driver[major=%d]!\n", dev_major);
    return 0;
} 

// 匹配列表
static const struct of_device_id platform_key_of_match[] = {
	{ .compatible = "my-gpio-keys" },
	{}
};

MODULE_DEVICE_TABLE(of, platform_key_of_match);

// platform驱动结构体
static struct platform_driver platform_key_driver = {
	.driver		= {
		.name	= "key_irq",			                // 无设备树时,用于设备和驱动间的匹配
		.of_match_table	= platform_key_of_match,    // 有设备树后,利用设备树匹配表
	},
	.probe		= platform_key_probe,
	.remove		= platform_key_remove,
};

module_platform_driver(platform_key_driver);

MODULE_LICENSE("GPL");

3.5.4.3. 测试应用程序实现

测试应用程序工作是读取按键状态然后打印状态,源码如下:

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

/*
 * ./led_App /dev/key0
 *
 */
#define KEY0VALUE       0XF0
#define INVAKEY         0X00

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

	
    
	/* 2. 打开文件 */
	fd = open("/dev/key0", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file %s\n");
		return -1;
	}

	while (1)
	{
		/* 3. 读文件 */
		read(fd, &keyvalue, sizeof(keyvalue));
        if (keyvalue == KEY0VALUE) 
        { 
            printf("KEY0 Press, value = %#X\r\n", keyvalue);
        }
		
	}
	
	close(fd);
	
	return 0;
}

测试应用程序仅仅是测试驱动是否正常,我们只需要打开、读取状态、关闭文件即可。 需要注意的是打开之后需要关闭才能再次打开,如果连续打开两次由于第一次打开申请的GPIO和中断还没有释放打开会失败。

3.5.4.4. Makefile

KERNAL_DIR := /home/zying/imx6ull/bsp/kernel/linux-imx
CROSS_COMPILE := /opt/gcc-arm-11.2-2022.02/bin/arm-none-linux-gnueabihf-
TFTP_DTR := /tftp/zouying
PWD := $(shell pwd)
obj-m := key_irq_timer.o

modules:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
	$(CROSS_COMPILE)gcc key_app.c -o key_app
	@make clear
	cp key_irq.ko key_app $(TFTP_DTR) -f

clear:
	@rm -f *.o *.cmd *.mod *.mod.c
	@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
	@rm -f .*ko.cmd .*.o.cmd .*.o.d
	@rm -f *.unsigned
	
clean:
	@rm -f *.ko

3.5.4.5. 程序运行结果

3.5.4.5.1. 加载设备树和驱动文件

将之前编译好的key_irq.dtbo文件传送到开发板中dt overlay文件所在位置,并修改config.txt文件,在该文件中添加对key_irq的dt overlay支持。

# Enable extra overlays
dtoverlay_extra=key_irq 

重启开发板后可以看到如下的打印信息

Applying DT overlay ==> key_irq.dtbo 

然后将之前编译好的 key_irq_timer.ko 文件传送到开发板中,使用 insmod key_irq_timer.ko 装载该模块文件。

root@igkboard:~/driver/05key_irq# insmod key_irq_timer.ko 
root@igkboard:~/driver/05key_irq# dmesg|tail -10
[ 2839.051094] success to remove driver[major=243]!
[ 2849.874020] key_irq key_irq: success to get 1 valid key
[ 2849.879043] success to install driver[major=243]!
[ 2850.082340] keyrelease

装载完成后,可以在 /dev 文件夹下看到 oled_spi 设备

root@igkboard:/dev# find -name key*
./key0

3.5.4.5.2. 测试效果

这个驱动程序只是简单的获取按键值。

当按键按下以后就会打印出”KEY0 Press, value = 0XF0“,表示按键按下。

root@igkboard:~/driver/05key_irq# insmod key_irq_timer.ko 
root@igkboard:~/driver/05key_irq# ./key_App 
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0
KEY0 Press, value = 0XF0

如果要卸载驱动的话输入如下命令即可:

rmmod key_irq_timer

3.5.5. 中断的上半部分和下半部分

我们在使用request_irq申请中断的中断服务函数属于中断处理的上半部,只要中断触发。那么中断处理函数就会执行。

在linux中断我们需要知道以下两点:1、Linux中断与中断之间不能嵌套。2、中断服务函数运行时间应当尽量短,做到快进快出。

然而一些中断的产生之后需要较长的时间来处理,如由于网络传输产生的中断, 在产生网络传输中断后需要比较长的时间来处理接收或者发送数据,因为在linux中断并不能被嵌套如果这时有其他中断产生就不能够及时的响应,为了解决这个问题,linux对中断的处理引入了“中断上半部”和 “中断下半部”的概念,在中断的上半部中只对中断做简单的处理,把需要耗时处理的部分放在中断下半部中,使得能够 对其他中断作为及时的响应,提供系统的实时性。这一概念又被称为中断分层。

  • “上半部分”是指在中断服务函数中执行的那部分代码,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。

  • “下半部分”是指那些原本应当在中断服务函数中执行但通过某种方式把它们放到中断服务函数外执行,这样中断处理函数就会快进快出。

并不是所有的中断处理都需要用到“上半部分”和“下半部分”,如果像我们上面编写的按键中断程序一样并不需要用到相对耗时的处理,对中断的处理只需放在中断“上半部分”即可。

至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际情况去判断,有一些可以借鉴的参考点:如果要处理的内容不希望被其他中断打断、要处理的任务对时间很敏感、要处理的任务与硬件有关,可以放到上半部。

上半部的处理很简单,直接编写中断服务程序就好了,关键是下半部。

为了学习如何使用中断分层,这里模拟一个耗时操作,加上中断分层的“下半部分”。

中断分层实现方法常用的有三种,分别为软中断、tasklet、和工作队列,下面分别介绍这三种方式。

tasklet是基于软中断实现,它们有很多相似之处,我们把它两个放到一块介绍。

3.5.5.1. 软中断

Linux内核使用结构体softirq_action表示软中断。

struct softirq_action
{
    void (*action)(struct softirq_action *);  
};

软中断由软件发送中断指令产生,在Linux内核中使用一个枚举变量列出所有可用的软中断,如下所示。

enum {   
	HI_SOFTIRQ=0,    	/*高优先级软中断*/
	TIMER_SOFTIRQ,    	/*定时器软中断*/
	NET_TX_SOFTIRQ,    	/*网络数据发送软中断*/
	NET_RX_SOFTIRQ,     /*网络数据接收软中断*/
	BLOCK_SOFTIRQ,    	
	BLOCK_IOPOLL_SOFTIRQ,     
	TASKLET_SOFTIRQ,    /*tasklet软中断*/
	SCHED_SOFTIRQ,    	/*调度软中断*/
	HRTIMER_SOFTIRQ,    /*高精度定时器软中断*/
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */     
	NR_SOFTIRQS 
}; 

一共10个软中断,因此NR_SOFTIRQS为10,类比硬中断,这个枚举类型列出了软中断的中断编号,我们“注册”软中断以及触发软中断都会用到软中断的中断编号。softrq_action结构体中的action成员变量就是软中断的服务函数,数组softirq_vec是个全局数组,因此所有的CPU都可以访问到,每个CPU都有自己的触发和控制机制,并且只执行自己所触发的软中断。但所执行的软中断服务函数是相同的。

1.软中断“注册”函数

要使用软中断必须先使用open_softirq函数注册对应的软中断处理函数。

void open_softirq(int nr, void (*action)(struct softirq_action *));

参数

  • nr:用于指定要“注册”的软中断中断编号

  • action:指定软中断的中断服务函数

返回值

2.触发软中断

软中断注册之后还要调用“触发”函数触发软中断,进而执行软中断中断服务函数,函数如下所示:

void raise_softirq(unsigned int nr); 

参数

  • nr:要触发的软中断

返回值

3.初始化软中断

软中断必须在编译的时候静态注册,使用softirq_init函数初始化软中断

void __init softirq_init(void)
{
	int cpu;
	for_each_possible_cpu(cpu){
		per_cpu(tasklet_vec, cpu).tail =
		&per_cpu(tasklet_vec, cpu).head;
		per_cpu(tasklet_hi_vec, cpu).tail =
		&per_cpu(tasklet_hi_vec, cpu).head;
	}
	open_softirq(TASKLET_SOFTIRQ, tasklet_action);
	open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。

3.5.5.2. tasklet

tasklet是基于软中断实现,如果对效率没有特殊要求推荐是用tasklet实现中断分层。为什么这么说, 根据之前讲解软中断的中断服务函数是一个全局的数组,在多CPU系统中,所有CPU都可以访问, 所以在多CPU系统中需要用户自己考虑并发、可重入等问题,增加编程负担。 软中断资源非常有限一些软中断是为特定的外设准备的(不是说、只能用于特定外设)。如“NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,” 从名字可以看出它们用于网络的TX和RX。像网络这种对效率要求较高的场合还是会使用软中断实现中断分层的。

相比软中断tasklet使用起来更简单,最重要的一点是在多CPU系统中同一时间只有一个CPU运行tasklet, 所以并发、可重入问题就变得很容易处理(一个tasklet甚至不用去考虑)。而且使用时也比较简单,介绍如下。

1.tasklet_struct结构体

在驱动中使用tasklet_struct结构体表示一个tasklet,结构体定义如下所示:

struct tasklet_struct {    
struct tasklet_struct *next;    
unsigned long state;    
atomic_t count;    
void (*func)(unsigned long);    
unsigned long data; }; 

参数介绍如下:

  • next:指向链表的下一个tasklet_struct,这个参数我们不需要自己去配置。

  • state:保存tasklet状态,等于0表示tasklet还没有被调度,等于TASKLET_STATE_SCHED表示tasklet被调度正准备运行。 等于TASKLET_STATE_RUN表示正在运行。

  • count:引用计数器,如果为0表示tasklet可用否则表示tasklet被禁止。

  • func:指定tasklet要执行的处理函数。

  • data:指定tasklet处理函数的参数。

2.tasklet初始化函数

要使用tasklet,必须先定义一个tasklet然后使用tasket_init函数初始化tasklet,函数原型如下:

void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}

参数

  • t:指定要初始化的tasklet_struct结构体

  • func:指定tasklet处理函数,等同于中断中的中断服务函数

  • data:指定tasklet处理函数的参数。函数实现就是根据设置的参数填充tasklet_struct结构体结构体。

返回值

也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化

DECLARE_TASKLET(name, func, data)

参数:

  • name:要定义的tasklet名字,这个名字就是一个tasklet_struct类型的时候变量

  • func:tasklet的处理函数

  • data:要传递给func函数的参数

3.触发tasklet

在上半部,也就中断处理函数中调用tasklet_schedule函数就能使tasklet在合适的时间运行。

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
            __tasklet_schedule(t);
}

参数

  • t:tasklet_struct结构体,要调度的tasklet,也就是DECLARE_TASKLET宏里面的name。

返回值

3.5.5.3. tasklet实现中断分层实现

实验在按键中断程序基础上完成,按键中断原本不需要使用中断分层,这里只是以它为例简单介绍tasklet的具体使用方法。 tasklet使用非常简单,主要包括定义tasklet结构体、初始化定义的tasklet结构体、实现tasklet中断处理函数、触发tasklet中断。

下面结合源码介绍如下。注意,源码是在“按键中断程序”基础上添加tasklet相关代码,这里只列出了tasklet相关代码。

/*--------------定义taselet--------------- */
struct tasklet_struct testtasklet;  //定义全局tasklet_struct类型结构体

/*--------------tasklet处理函数-----------------*/
//定义tasklet的“中断服务函数”可以看到我们在tasklet的中断服务函数中使用延时 和printk语句模拟一个耗时的操作。
void testtasklet_func(unsigned long data)
{
    int counter = 1;
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testtasklet_func counter = %d \n", counter++);
}

/*--------------中断处理函数-----------------*/
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    printk(KERN_ERR "key0_irq_hander----------inter");
    /*调度tasklet*/
    tasklet_schedule(&testtasklet);

    printk(KERN_ERR "key0_irq_hander-----------exit");
    return IRQ_RETVAL(IRQ_HANDLED);
}

/*--------------解析设备树,初始化key属性并初始化中断-----------------*/  
int parser_dt_init_key(struct platform_device *pdev)
{
    /*..初始化tasklet...*/
    tasklet_init(&testtasklet, testtasklet_func, data);
    /*注册中断处理函数*/
    request_irq(priv->key.irq, priv->key.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, priv->key.name, priv);
    
}

在中断服务函数中调用tasklet_schedule函数触发tasklet中断。 在按键中断服务函数中的开始处和结束处添加打印语句,正常情况下程序会先执行按键中断的中短发服务函数, 退出中断服务函数后再执行中断的下半部分,既tasklet的“中断服务函数”。

测试效果

root@igkboard:~/driver/05key_irq# rmmod key_irq_timer_tasklet.ko   
root@igkboard:~/driver/05key_irq# insmod key_irq_timer_tasklet.ko 
root@igkboard:~/driver/05key_irq# ./key_App 
^C
root@igkboard:~/driver/05key_irq# dmesg|tail -20
[  464.466995] key0_irq_hander------inter
[  464.467018] key0_irq_hander------exit
[  464.615581] testtasklet_func counter = 1 
[  464.815677] testtasklet_func counter = 2 
[  464.983974] key0_irq_hander------inter
[  464.983985] key0_irq_hander------exit
[  465.015769] testtasklet_func counter = 3 
[  465.215861] testtasklet_func counter = 4 
[  465.216995] keypress
[  465.417412] testtasklet_func counter = 1 
[  465.466560] key0_irq_hander------inter
[  465.466573] key0_irq_hander------exit
[  465.617517] testtasklet_func counter = 2 
[  465.817611] testtasklet_func counter = 3 
[  466.017698] testtasklet_func counter = 4 
[  466.018489] keyrelease

3.5.5.4. 工作队列

与软中断和tasklet不同,工作队列运行在内核线程,允许被重新调度和睡眠。 如果中断的下部分能够接受被重新调度和睡眠,推荐使用工作队列。

“工作队列”是一个“队列”,但是对于用户来说不必关心“队列”以及队列工作的内核线程,这些内容由内核帮我们完成, 我们只需要定义一个具体的工作、初始化工作即可。和tasklet类似,从使用角度讲主要包括定义工作结构体、初始化工作、触发工作。

1.工作队列结构体

在驱动中一个工作结构体代表一个工作,工作结构体如下所示:

struct work_struct {    
	atomic_long_t data;    
	struct list_head entry;    
	work_func_t func; 	/*工作队列处理函数*/
	#ifdef CONFIG_LOCKDEP    struct lockdep_map lockdep_map; 
	#endif 
}; 

这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示。

struct workqueue_struct {
 struct list_head pwqs; 
 struct list_head list; 
 struct mutex mutex; 
 int work_color;
 int flush_color; 
 atomic_t nr_pwqs_to_flush;
 struct wq_flusher *first_flusher;
 struct list_head flusher_queue; 
 struct list_head flusher_overflow;
 struct list_head maydays; 
 struct worker *rescuer; 
 int nr_drainers; 
 int saved_max_active;
 struct workqueue_attrs *unbound_attrs;
 struct pool_workqueue *dfl_pwq; 
 char name[WQ_NAME_LEN];
 struct rcu_head rcu;
 unsigned int flags ____cacheline_aligned;
 struct pool_workqueue __percpu *cpu_pwqs;
 struct pool_workqueue __rcu *numa_pwq_tbl[];
};

Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux 内核使用worker 结构体表示工作者线程,worker 结构体内容如下:

struct worker {
 union {
 	struct list_head entry; 
 	struct hlist_node hentry;
 };
 struct work_struct *current_work; 
 work_func_t current_func; 
 struct pool_workqueue *current_pwq;
 bool desc_valid;
 struct list_head scheduled; 
 struct task_struct *task; 
 struct worker_pool *pool; 
 struct list_head node; 
 unsigned long last_active; 
 unsigned int flags; 
 int id; 
 char desc[WORKER_DESC_LEN];
 struct workqueue_struct *rescue_wq;
};

每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。

2.工作初始化函数

在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作

#define INIT_WORK(_work, _func) 

参数:

  • _work:用于指定要初始化的工作结构体。

  • _func:用于指定工作的处理函数。

也可以使用DECLARE_WOEK宏一次性完成工作的创建和初始化。

#define DECLARE_WORK(n,f)

参数:

  • n:表示定义的工作

  • f:表示工作对应的处理函数

3. 启动工作函数

驱动工作函数执行后相应内核线程将会执行工作结构体指定的处理函数,驱动函数如下所示。

static inline bool schedule_work(struct work_struct *work)

参数:

  • work:要调度的工作

**返回值:**0:成功,其他:失败

3.5.5.5. 工作队列驱动实现

工作队列实验同样在按键中断程序基础上实现,这里只列出了工作队列相关代码(这里只修改驱动程序,其他内容保持不变)

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

/*--------------work处理函数-----------------*/
void testwork_func_t(struct work_struct  *work)
{
    int counter = 1;
    mdelay(200);
    printk(KERN_ERR "testwork_func_t counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testwork_func_t counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testwork_func_t counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testwork_func_t counter = %d  \n", counter++);
    mdelay(200);
    printk(KERN_ERR "testwork_func_t counter = %d  \n", counter++);
}

/*--------------中断处理函数-----------------*/
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    /*调度work*/
    schedule_work(&testwork);
    return IRQ_HANDLED;
}

/*--------------解析设备树,初始化key属性并初始化中断-----------------*/  
int parser_dt_init_key(struct platform_device *pdev)
{
    /*..初始化work...*/
    INIT_WORK(testwork, testwork_func_t) 
    /*注册中断处理函数*/
    request_irq(priv->key.irq, priv->key.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, priv->key.name, priv);
    
}

与tasklet实现中断分层类似,使用方法几乎一样,这里不进行详细描述。

测试效果

root@igkboard:~/driver/05key_irq# dmesg|tail -10
[ 2507.232447] keypress
[ 2507.491384] testwork_func counter = 3 
[ 2507.502992] key0_irq_hander------inter
[ 2507.503022] key0_irq_hander------exit
[ 2507.522435] keyrelease
[ 2507.891387] testwork_func counter = 4 
[ 2508.291382] testwork_func counter = 1 
[ 2508.530271] testwork_func counter = 2 
[ 2508.731116] testwork_func counter = 3 
[ 2508.932004] testwork_func counter = 4