GIC中断控制器、设备树插件(Device Tree Overlay)以及内核定时器介绍


在我们做按键设备开发之前我们需要掌握必要的基础知识,下面详细介绍。

主要参考资料:野火i.MX Linux开发实战指南


一、GIC中断控制器

GIC 是 Generic Interrupt Controller 的缩写,直译为通用中断控制器,它由 ARM 公司设计,目前共有 4 个版本 V1~V4,i.MX 6U 使用的是 GIC V2。

GIC架构分为了: 分发器(Distributor)CPU接口(CPU Interface)

(1)分发器

分发器用于管理 CPU 所有中断源,确定每个中断的优先级,管理中断的屏蔽和中断抢占。最终将优先级最高的中断转发到一个或者多个 CPU 接口。

CPU 的中断源分为三类,讲解如下:

  • SPI:SPI 是共享中断,也就是我们在常用的串口中断、DMA 中断等等。
  • SGI:SGI 是软件中断,PPI 是 CPU 私有中断。SGI 中断,共有 16 个中断,中断编号为 0~15, SGI 一般用于 CPU 之间通信,i.MX 6U 是单核处理器,我们暂时不用过多关心 SGI 中断。
  • PPI:PPI 有 16 个中断,中断编号为 16~31,SGI 与 PPI 中断都属于 CPU 私有中断,每个 CPU都有各自的 SGI 和 PPI,这些中断被存储在 GIC 分发器中。CPU 之间通过中断号和 CPU 编号唯一确定一个 SGI 或 PPI 中断。

分发器提供了一些编程接口或者说是寄存器,我们可以通过分发器的编程接口实现如下操作:

  • 全局的开启或关闭 CPU 的中断;
  • 控制任意一个中断请求的开启和关闭;
  • 设置每个中断请求的中断优先级;
  • 指定中断发生时将中断请求发送到那些 CPU(i.MX 6U 是单核);
  • 设置每个”外部中断”的触发方式(边缘触发或者电平触发)。

(2)CPU接口

CPU 接口为链接到 GIC 的处理器提供接口,与分发器类似它也提供了一些编程接口,我们可以通过 CPU 接口实现以下功能:

  • 开启或关闭向处理器发送中断请求.;
  • 确认中断(acknowledging an interrupt);
  • 指示中断处理的完成;
  • 为处理器设置中断优先级掩码;
  • 定义处理器的抢占策略;
  • 确定挂起的中断请求中优先级最高的中断请求。

简单来说,CPU 接口可以开启或关闭发往 CPU 的中断请求,CPU 中断开启后只有优先级高于“中断优先级掩码”的中断请求才能被发送到 CPU。在任何时候 CPU 都可以从其 GICC_Hppir(CPU接口寄存器) 读取当前活动的最高优先级。


二、设备树中的中断相关内容

让我们先来了解一下设备树是如何描述整个中断系统信息的。

(1)顶层中断控制器

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

intc: interrupt-controller@a01000 {
 		compatible = "arm,cortex-a7-gic";
 		#interrupt-cells = <3>;
 		interrupt-controller;
 		reg  =  <0xa01000 0x1000>,
 				<0xa02000 0x100>;
};
  • compatible:compatible 属性用于平台设备驱动的匹配;
  • reg:reg 指定中断控制器相关寄存器的地址及大小;
  • interrupt-controller:声明该设备树节点是一个中断控制器;
  • #interrupt-cells :指定它的“子”中断控制器用几个 cells 来描述一个中断,可理解为用几个参数来描述一个中断信息。在这里的意思是在 intc 节点的子节点将用 3 个参数来描述中断。

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

(2)gpc一级子中断控制器

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

gpc: gpc@20dc000 {
		compatible = "fsl,imx6ul-gpc", "fsl,imx6q-gpc";
		reg = <0x20dc000 0x4000>;
		interrupt-controller;
		#interrupt-cells = <3>;
		interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
		interrupt-parent = <&intc>;
		fsl,mf-mix-wakeup-irq = <0xfc00000 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] 用于设置 中断触发类型 。每一位代表一个触发方式,可进行组合,系统提供了相对的宏顶义我们可以直接使用,如下所示:

/usr/src/linux-headers-5.15.0-25/include/dt-bindings/interrupt-controller/irq.h

/* SPDX-License-Identifier: GPL-2.0 OR MIT */
/*
 * This header provides constants for most IRQ bindings.
 *
 * Most IRQ bindings include a flags cell as part of the IRQ specifier.
 * In most cases, the format of the flags cell uses the standard values
 * defined in this header.
 */

#ifndef _DT_BINDINGS_INTERRUPT_CONTROLLER_IRQ_H
#define _DT_BINDINGS_INTERRUPT_CONTROLLER_IRQ_H

#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

#endif

(3)二级子中断控制器

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

soc {
		#address-cells = <1>;
		#size-cells = <1>;
		compatible = "simple-bus";
		interrupt-parent = <&gpc>;
		ranges;

		//busfreq 子节点
		busfreq {
 ................ //表示省略
}
 ............... //表示省略

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

soc 节点内包含的中断控制器很多,几乎用到中断的外设都是中断控制器,我们使用的是开发板上的按键,使用的是 GPIO4_14, 所以这里以 GPIO4 为例介绍。在 imx6ull.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>;
            };

  • interrupts:用来描述GPIO4能产生中断类型及中断编号、触发方式,查看imx6ull的数据手册我们可以知道, GPIO4能产生的中断只有两个,分配的中断ID为106、107,对于SPI中断它们的编号是72(106-32),73(107-32);
  • interrupt-controller:声明该节点是一个中断控制器;
  • #interrupt-cells:声明该节点的子节点将用多少个参数来描述中断信息。

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

举个栗子:

&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-controller”标签。


三、设备树插件(Device Tree Overlay)介绍

参考文档:Linux内核设备树overlays

(1)设备树插件简介

4.4版本之前我们增加或修改设备的时候,需要进入内核源码中修改设备树并且编译下载到开发板中。为了使得开发更加方便快捷,Linux4.4 以后引入了动态设备树(Dynamic DeviceTree)。类似于设备树“补丁”的设备树插件。我们只要写好设备树插件,就可以直接被内核识别并且将里面的内容补充到设备树中,不需要重新编译设备树。这样子还有一个好处就是通过修改设备树插件的内容来使能或者失能某些设备以便于复用为不同功能。

注意设备树插件和设备树不是互相替代的关系,而是互补的关系。设备树插件可以在主设备树定型的情况下,再对主设备树未描述的功能进行动态的拓展。比如 A 板的设备树没有开启串口 1 的功能,但 B 板需要开启串口 1 的功能,那么可以直接沿用 A 板的设备树,并用设备树插件拓展出串口 1,满足 B 板的需求。

我们可以在如下路径【arch/arm/boot/dts/overlays】下看见我们的设备树插件:

adc.dtbo  cam.dts    can2.dtbo  i2c1.dts      lcd_drm.dtbo  lcd.dts        nbiot-4g.dts  pwm8.dtbo  spi1.dts    uart3.dtbo  uart4.dts   w1.dtbo
adc.dts   can1.dtbo  can2.dts   key_irq.dtbo  lcd_drm.dts   Makefile       pwm7.dtbo     pwm8.dts   uart2.dtbo  uart3.dts   uart7.dtbo  w1.dts
cam.dtbo  can1.dts   i2c1.dtbo  key_irq.dts   lcd.dtbo      nbiot-4g.dtbo  pwm7.dts      spi1.dtbo  uart2.dts   uart4.dtbo  uart7.dts

(2)设备树插件编写格式

设备树插件拥有相对固定的格式,甚至可以认为它只是把设备节点加了一个“壳”编译后内核能够动态加载它。格式如下,具体节点省略。

/dts-v1/;
/plugin/;

/ {
	fragment@0 {
	target-path = "/";
	__overlay__ {
	/* 在此添加要插入的节点 */
	};
};
  • /dts-v1/:用于指定 dts 的版本。
  • /plugin/:表示允许使用未定义的引用并记录它们,设备树插件中可以引用主设备树中的节点,而这些“引用的节点”对于设备树插件来说就是未定义的,所以设备树插件应该加上“/plugin/”。
  • target-path = “/”:指定设备树插件的加载位置,默认我们加载到根节点下,既“target-path =“/”
  • __ overlay __ :我们要插入的设备及节点或者要引用(追加)的设备树节点放在 overlay {…};内。

举个栗子(但是一般不推荐):

/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include "../imx6ul-pinfunc.h"
#include "dt-bindings/interrupt-controller/irq.h"

/ {
	compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

	fragment@0 {
		target-path = "/";
		__overlay__ {
key_irq:key_irq{
			compatible = "my-gpio-keys";
			status = "disabled";
		};
		};
	};/*节点名字需要和添加的根节点的名字相同*/

	fragment@1 {
		target = <&key_irq>;
		__overlay__ {
			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 IRQ_TYPE_EDGE_RISING>;
		};
	};

	fragment@2 {
		target = <&iomuxc> ;
		__overlay__ {
pinctrl_my_gpio_keys: my-gpio-keys {
						  fsl,pins = <
							  MX6UL_PAD_NAND_CE1_B__GPIO4_IO14 0x17059 /* gpio key */
							  >;
					  };
		};
	};
};

一般比较推荐的就是在设备树中添加根节点,然后编写设备树插件。

设备树插件与设备树一样都是使用 DTC 工具编译,只不过设备树编译为.dtb。而设备树插件需要编译为.dtbo。具体的编译运行等程序可以看下一篇关于按键设备驱动开发的文章。


四、内核定时器介绍

不同于单片机定时器,Linux内核定时器是一种基于未来时间点的计时方式,它以当前时刻为启动的时间点,以未来的某一时刻为终止点,类似于我们的闹钟。

内核定时器的精度不高,不能作为高精度定时器使用,其内核定时器不是周期性运行的,超时以后就会自动关闭,因此要想实现周期性的定时,就需要在定时处理函数中重新开启定时器。

(1)使用定时器处理按键抖动

我们在后面做按键设备驱动的时候,会用到内核定时器,来消抖。

我们来解释一下什么是消抖,在实际的按键操作中,可能会有机械抖动:

在这里插入图片描述

按下或松开一个按键,它的 GPIO 电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。 如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。我们需要使用定时器来解决这个问题:

在这里插入图片描述
如果 10ms 内又发生了 GPIO 中断,那就认为是抖动,这时再次修改超时时间为 10ms。 只有 10ms 之内再无 GPIO 中断发生,那么定时器的函数才会被调用。在定时器函数中记录按键值。

(2)定时器时间单位

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

# CONFIG_HZ_200 is not set
# CONFIG_HZ_250 is not set
# CONFIG_HZ_300 is not set
# CONFIG_HZ_500 is not set
# CONFIG_HZ_1000 is not set

CONFIG_HZ=100

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

定时器的时间就是基于 jiffies 的,我们修改超时时间时,一般使用这 2 种方法:

  • 在 add_timer 之前,直接修改:
timer.expires = jiffies + 200;   // 200 表示多少200个滴答后超时,也就是 200*10ms = 2000ms = 2s
timer.expires = jiffies + 2*HZ;  // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 s
  • 在 add_timer 之后,使用 mod_timer 修改定时器的超时时间:
mod_timer(&timer, jiffies + 200);   // 200 表示多少个滴答后超时,也就是 200*10ms = 2000ms = 2s 
mod_timer(&timer, jiffies + 2*HZ);  // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒 

函数后面都会介绍。

(3)内核定时器相关函数

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
};
【2】timer_setup 初始化定时器函数

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

void timer_setup(timer, fn, data);

参数

  • timer:要初始化定时器
  • fn:函数

返回值:没有返回值。

【3】add_timer 添加定时器函数

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

void add_timer(struct timer_list *timer);

参数

  • timer:要注册的定时器

返回值:没有返回值。
timer->expires 表示超时时间。当超时时间到达,内核就会调用这个函数:timer->function。

【4】mod_timer 修改超时时间

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 删除定时器

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

int del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);

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

  • timer:要删除的定时器。
  • 返回值:0,定时器还没被激活;1,定时器已经激活。

(4)代码测试内核定时器

#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/timer.h>

struct ct_dev_{
	struct timer_list ct_timer;
	int count;
};
struct ct_dev_ *ct_dev;
 
static void ct_timer_function(struct timer_list *t)
{
	struct ct_dev_ *p = (struct ct_dev_ *)t;
	printk("%d",p->count++);
	if(p->count < 5){
		printk("2*HZ=%d,msecs_to_jiffies(2000)=%lu", 2*HZ, msecs_to_jiffies(2000));
		mod_timer(&p->ct_timer,jiffies + 2*HZ);//HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒 
	}
}
 
static int __init ct_init(void)
{
	struct ct_dev_ *p = NULL;
	ct_dev = (struct ct_dev_ *)kmalloc(sizeof(struct ct_dev_),GFP_KERNEL);
	if(IS_ERR(ct_dev)){
		printk("kmalloc error");
		return -ENOMEM;
	}
	p = ct_dev;
	/*负责初始化timer_list类型变量,当我们定义了一个tiemr_list 变量以后一定要先初始化一下*/
	timer_setup(&p->ct_timer,ct_timer_function, 0);
	p->count = 0;
	/*mod_timer函数用于修改定时值,如果定时器还没有激活的话,mod_timer函数会激活定时器*/
	mod_timer(&p->ct_timer, 2*HZ);
	printk("init ok");
	return 0;
}
static void __exit ct_exit(void)
{
	struct ct_dev_ *p = ct_dev;
	if(IS_ERR(p)){
		return;
	}
	/*del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除。*/
	del_timer_sync(&p->ct_timer);
	kfree(p);
	printk("exit ok\n");
}
/*调用函数 module_init 来声明 xxx_init 为驱动入口函数,当加载驱动的时候 xxx_init函数就会被调用.*/
module_init(ct_init);
/*调用函数module_exit来声明xxx_exit为驱动出口函数,当卸载驱动的时候xxx_exit函数就会被调用.*/
module_exit(ct_exit);

/*添加LICENSE和作者信息,是来告诉内核,该模块带有一个自由许可证;没有这样的说明,在加载模块的时内核会“抱怨”.*/
MODULE_LICENSE("Dual BSD/GPL");//许可 GPL、GPL v2、Dual MPL/GPL、Proprietary(专有)等,没有内核会提示.
MODULE_AUTHOR("WangDengtao");//作者
MODULE_VERSION("V1.0");//版本

Makefile:

KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
obj-m := timer.o

all:
	$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules

clean:
	@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
	@rm -f *.ko

执行命令:
安装驱动:sudo insmod timer.ko
删除驱动:sudo rmmod timer
查看信息:sudo dmesg | tail -11

wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/tftpboot$ sudo insmod timer.ko 
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/tftpboot$ sudo rmmod timer 
wangdengtao@wangdengtao-virtual-machine:~/wangdengtao/tftpboot$ sudo dmesg | tail -11
[ 1812.371335] exit ok
[ 1950.205553] init ok
[ 1950.209506] 0
[ 1950.209515] 2*HZ=500,msecs_to_jiffies(2000)=500
[ 1952.237211] 1
[ 1952.237214] 2*HZ=500,msecs_to_jiffies(2000)=500
[ 1954.253343] 2
[ 1954.253367] 2*HZ=500,msecs_to_jiffies(2000)=500
[ 1956.269263] 3
[ 1956.269266] 2*HZ=500,msecs_to_jiffies(2000)=500
[ 1958.141482] exit ok

虚拟机中内核每秒中会发生 250 次系统滴答中断(tick),也就是0.004秒跳一次,也就是跳一次4ms,这就像人类的心跳一样,这是 Linux系统的心跳。代码中我们需要两个周期就是跳500次,也就是需要2秒。


  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值