在我们做按键设备开发之前我们需要掌握必要的基础知识,下面详细介绍。
主要参考资料:野火i.MX Linux开发实战指南
GIC 是 Generic Interrupt Controller 的缩写,直译为通用中断控制器,它由 ARM 公司设计,目前共有 4 个版本 V1~V4,i.MX 6U 使用的是 GIC V2。
GIC架构分为了: 分发器(Distributor) 和 CPU接口(CPU Interface)
分发器用于管理 CPU 所有中断源,确定每个中断的优先级,管理中断的屏蔽和中断抢占。最终将优先级最高的中断转发到一个或者多个 CPU 接口。
CPU 的中断源分为三类,讲解如下:
分发器提供了一些编程接口或者说是寄存器,我们可以通过分发器的编程接口实现如下操作:
CPU 接口为链接到 GIC 的处理器提供接口,与分发器类似它也提供了一些编程接口,我们可以通过 CPU 接口实现以下功能:
简单来说,CPU 接口可以开启或关闭发往 CPU 的中断请求,CPU 中断开启后只有优先级高于“中断优先级掩码”的中断请求才能被发送到 CPU。在任何时候 CPU 都可以从其 GICC_Hppir(CPU接口寄存器) 读取当前活动的最高优先级。
让我们先来了解一下设备树是如何描述整个中断系统信息的。
打开 ./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>;
};
GIC 架构分为了:分发器 (Distributor)和 CPU 接口 (CPU Interface), 上面设备树节点就是用来描述整个 GIC 控制器的。
在 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>;
};
第一个参数用于指定 中断类型 ,在 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
同样在 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>;
};
以上三部分的内容是内核为我们提供的,我们要做的内容很简单, 只需要在我们编写的设备树节点中 引用已经写好的中断控制器父节点以及配置中断信息即可。
举个栗子:
&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”标签。
参考文档:Linux内核设备树overlays
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
设备树插件拥有相对固定的格式,甚至可以认为它只是把设备节点加了一个“壳”编译后内核能够动态加载它。格式如下,具体节点省略。
/dts-v1/;
/plugin/;
/ {
fragment@0 {
target-path = "/";
__overlay__ {
/* 在此添加要插入的节点 */
};
};
举个栗子(但是一般不推荐):
/dts-v1/;
/plugin/;
#include
#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内核定时器是一种基于未来时间点的计时方式,它以当前时刻为启动的时间点,以未来的某一时刻为终止点,类似于我们的闹钟。
内核定时器的精度不高,不能作为高精度定时器使用,其内核定时器不是周期性运行的,超时以后就会自动关闭,因此要想实现周期性的定时,就需要在定时处理函数中重新开启定时器。
我们在后面做按键设备驱动的时候,会用到内核定时器,来消抖。
我们来解释一下什么是消抖,在实际的按键操作中,可能会有机械抖动:
按下或松开一个按键,它的 GPIO 电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。 如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。我们需要使用定时器来解决这个问题:
如果 10ms 内又发生了 GPIO 中断,那就认为是抖动,这时再次修改超时时间为 10ms。 只有 10ms 之内再无 GPIO 中断发生,那么定时器的函数才会被调用。在定时器函数中记录按键值。
可以在内核源码根目录下用“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 种方法:
timer.expires = jiffies + 200; // 200 表示多少200个滴答后超时,也就是 200*10ms = 2000ms = 2s
timer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 s
mod_timer(&timer, jiffies + 200); // 200 表示多少个滴答后超时,也就是 200*10ms = 2000ms = 2s
mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ,2*HZ 就相当于 2 秒
函数后面都会介绍。
Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行。
要注意一点,内核定时器并不是周期 性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。
表示定时器,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
};
负责初始化timer_list类型变量,当我们定义了一个tiemr_list 变量以后一定要先初始化一下。
void timer_setup(timer, fn, data);
参数
返回值:没有返回值。
add_timer函数用于向linux内核注册定时器,使用add_timer函数向内核注册定时器以后,定时器就会开始运行。
void add_timer(struct timer_list *timer);
参数
返回值:没有返回值。
timer->expires 表示超时时间。当超时时间到达,内核就会调用这个函数:timer->function。
mod_timer函数用于修改定时值,如果定时器还没有激活的话,mod_timer函数会激活定时器。
int mod_timer(struct timer_list *timer, unsigned long expires);
参考
返回值:
它等同于:del_timer(timer); timer->expires = expires; add_timer(timer); 但是更加高效。
用于删除一个定时器,不管定时器有没有被激活,都可以使用函数删除。在多处理器系统上,定时器可能会在其他的处理器上运行,因此在调用del_timer函数删除定时器之前要先等待其他处理器的定时器函数退出。
int del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);
del_timer_sync 函数是 del_timer 函数的同步版,会等待其他处理器使用完定时器再删除。
#include
#include
#include
#include
#include
#include
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秒。