Linux驱动开发第3步_INPUT子系统框架下的外部中断
1、了解WKUP唤醒引脚
1)、打开STM32MP157数据手册,搜索“WKUP”,见下表:
由表可知,PA0,PA2,PC13,PI8,PI11和PC1均具有唤醒功能。
2)、打开STM32MP157参考手册,了解GIC中断向量表,搜索“Table 117”,找到MPU_WAKEUP_PIN。
下图是“STM32MP157 interrupt mapping for Cortex®-A7 GIC”的一部分。
3)、打开“linux-5.4.31/arch/arm/boot/dts/stm32mp151.dtsi”
pwr_irq: pwr@50001020 {
compatible = "st,stm32mp1-pwr";
reg = <0x50001020 0x100>;
interrupts = <GIC_SPI 149 IRQ_TYPE_LEVEL_HIGH>;
/*从参考手册中,查看Table 117,指定pwr_irq的GIC中断向量号码为149*/
interrupt-controller;
#interrupt-cells = <3>;
wakeup-gpios = <&gpioa 0 GPIO_ACTIVE_HIGH>,/*PA0高电平唤醒*/
<&gpioa 2 GPIO_ACTIVE_HIGH>,/*PA2高电平唤醒*/
<&gpioc 13 GPIO_ACTIVE_HIGH>,/*PC13高电平唤醒*/
<&gpioi 8 GPIO_ACTIVE_HIGH>,/*PI8高电平唤醒*/
<&gpioi 11 GPIO_ACTIVE_HIGH>,/*PI11高电平唤醒*/
<&gpioc 1 GPIO_ACTIVE_HIGH>;/*PC1高电平唤醒*/
/*查询数据手册,PA0,PA2,PC13,PI8,PI11和PC1均具有唤醒功能*/
};
2、了解EXTI事件
1)、打开STM32MP157参考手册,搜索“Table 118”,找到EXTI[3],即为外部中断3。
2)、打开“linux-5.4.31/arch/arm/boot/dts/stm32mp151.dtsi”
exti: interrupt-controller@5000d000 {
compatible = "st,stm32mp1-exti", "syscon";
interrupt-controller;
#interrupt-cells = <2>;
/*使用EXTI中断控制器需要用2个cells来描述一个中断*/
/*第1个cells:中断号;第2个cells:中断标志位*/
reg = <0x5000d000 0x400>;
/*表示address=0x5000d000,length=0x400,占1024个字节*/
/*EXTI的起始地址为0x5000d000,结束地址为0x5000D3FF,合计1KB*/
hwlocks = <&hsem 1 1>;
/* exti_pwr is an extra interrupt controller used for EXTI 55 to 60. It's mapped on pwr interrupt controller.*/
exti_pwr: exti-pwr {
interrupt-controller;
#interrupt-cells = <2>;
interrupt-parent = <&pwr_irq>;
/*指定exti_pwr所有子节点的中断父节点为&pwr_irq,继承唤醒功能*/
st,irq-number = <6>;
};
};
pinctrl: pin-controller@50002000 {
#address-cells = <1>;
/*定义子节点的reg和ranges的addres长度为32个位*/
#size-cells = <1>;
/*定义子节点的reg和ranges的length长度为32个位*/
compatible = "st,stm32mp157-pinctrl";
ranges = <0 0x50002000 0xa400>;
/*子节点寄存器起始地址为0*/
/*父节点寄存器起始地址为0x50002000*/
/*寄存器最大偏移地址为0xa400*/
interrupt-parent = <&exti>;
/*指定pinctrl所有子节点的中断父节点为exti*/
/*这样GPIO的中断就和EXTI联系起来*/
st,syscfg = <&exti 0x60 0xff>;
hwlocks = <&hsem 0 1>;
pins-are-numbered;
gpioa: gpio@50002000 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;/*当前节点为中断控制器*/
#interrupt-cells = <2>;
/*interrupts属性第1个cell为某个IO在所处组的编号*/
/*第2个cell表示中断触发方式*/
reg = <0x0 0x400>;
clocks = <&rcc GPIOA>;
st,bank-name = "GPIOA";
status = "disabled";
};
/*下面省略*/
gpiog: gpio@50008000 {
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;/*当前节点为中断控制器*/
#interrupt-cells = <2>;
/*interrupts属性第1个cell为某个IO在所处组的编号*/
/*第2个cell表示中断触发方式*/
reg = <0x6000 0x400>;
clocks = <&rcc GPIOG>;
st,bank-name = "GPIOG";
status = "disabled";
};
/*下面省略*/
};
这个stm32mp151.dtsi一般无需修改。
3)、打开“/linux-5.4.31/arch/arm/boot/dts/stm32mp15-pinctrl.dtsi”,添加如下:
key_pins_a: key_pins-0 {/*"key_pins_a既是标号也是节点*/
pins1 {
pinmux = <STM32_PINMUX('G', 3, GPIO)>,
/*设置PG3复用为GPIO功能*/
/*KEY0连接到PG3*/
<STM32_PINMUX('H', 7, GPIO)>;
/*设置PH7复用为GPIO功能*/
/*KEY1连接到PH7*/
bias-pull-up; /*设置引脚内部上拉*/
slew-rate = <0>;/*设置引脚的速度为0档,0最慢,3 最高*/
};
pins2 {
pinmux = <STM32_PINMUX('A', 0, GPIO)>;
/*设置PA0复用为GPIO功能*/
/*WK_UP连接到PA0*/
bias-pull-down; /*内部下拉*/
slew-rate = <0>;/*设置引脚的速度为0档,0最慢,3 最高*/
};
};
4)、打开“/linux-5.4.31/arch/arm/boot/dts/stm32mp157d-atk.dts”,添加如下:
key {
compatible = "zgq,key";/*设置属性compatible的值为"zgq,led"*/
status = "okay";/*设置属性status的值为"okay"*/
pinctrl-names = "default";
pinctrl-0 = <&key_pins_a>;/*指定key的父节点为key_pins_a*/
key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
/*“&gpiog”表示key-gpio引脚所使用的IO属于GPIOG组*/
/*“3’表示GPIOG组的第3号IO,即PG3引脚*/
/*“GPIO_ACTIVE_LOW”表示低电平有效,“GPIO_PULL_UP”表示上拉*/
interrupt-parent = <&gpiog>;
/*指定父中断器为&gpiog*/
/*通过interrupt-parent属性指定key所有子节点的中断父节点为gpiog*/
interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
/*查看参考手册“Table 118”,EXTI[3]的事件输入号码为3*/
/*IRQ_TYPE_EDGE_BOTH为边沿触发*/
/*可以用interrupts-extended = <&gpioa 3 IRQ_TYPE_EDGE_BOTH>;替换上面两句*/
};
至此,我们就可以将PG3引脚作为外部中断引脚。
3、驱动程序如下:
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/input.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#define KEYINPUT_NAME "keyinput" /* 设备名字*/
/* key设备结构体 */
struct key_dev{
struct input_dev *idev; /* 按键对应的input_dev指针 */
struct timer_list timer; /* 定时器结构参数 */
int gpio_key; /* 按键对应的GPIO编号 */
int irq_key; /* 按键对应的中断号 */
};
static struct key_dev key; /* 按键设备 */
//函数功能:按键中断服务函数
//irq: 触发该中断事件对应的中断号
//arg : arg参数可以在申请中断的时候进行配置
static irqreturn_t key_interrupt_fun(int irq,void *dev_id)
{
if(key.irq_key != irq) return IRQ_NONE;
disable_irq_nosync(irq); /*禁止按键中断*/
mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));
//设置定时器在15ms后产生中断,用于按键消抖
//如果定时器还没有激活,则该函数会激活定时器;
//返回值为0表示调用mod_timer()函数前,该定时器未被激活;
//返回值为1表示调用mod_timer()函数前,该定时器已被激活
//timer=&key.timer:要修改超时时间(定时值)的定时器;
//expires=jiffies + msecs_to_jiffies(timerperiod):修改后的超时时间;
return IRQ_HANDLED;
}
//函数功能:Linux内核定时器中断服务函数
//用于按键消抖,读取按键值,并上报相应的事件。
static void key_timer_function(struct timer_list *arg)
{
int val;
val = gpio_get_value(key.gpio_key);
//根据key.gpio_key读取按键值
input_report_key(key.idev, KEY_0, !val);
/向Linux系统通知KEY_0这个按键被按下,1表示按下
//由于key0被配置为边沿触发中断,且低电平表示按下,因此val取反
input_sync(key.idev);//同步事件
enable_irq(key.irq_key);
//irq=key.irq_key表示要使能的中断号,这里是使能按键中断;
}
//函数功能:按键初始化函数
//nd为device_node型结构指针,指向设备
//返回值为0表示成功,负数表示失败
static int key_gpio_init(struct device_node *nd)
{
int ret;
unsigned long irq_flags;
key.gpio_key = of_get_named_gpio(nd, "key-gpio", 0);
//在key节点中,key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
//np指定的“设备节点”
//propname="key-gpio",给定要读取的属性名字
//Index=0,给定的GPIO索引为0
//返回值:正值,获取到的GPIO编号;负值,失败。
if(!gpio_is_valid(key.gpio_key)) {
printk("key:Failed to get key-gpio\n");
return -EINVAL;
}
/* 申请使用GPIO */
ret = gpio_request(key.gpio_key, "KEY0");
//gpio=key.gpio_key,指定要申请的“gpio编号”
//Label="KEY0",给这个gpio引脚设置个名字为"KEY0"
//返回值:0,申请“gpio编号”成功;其他值,申请“gpio编号”失败;
if (ret) {
printk(KERN_ERR "key: Failed to request key-gpio\n");
return ret;
}
gpio_direction_input(key.gpio_key);
//设置“某个GPIO为输入口”
//gpio= key.gpio_key,指定的“gpio编号”
//返回值:0,表示设置成功;负值,表示设置失败。
key.irq_key = irq_of_parse_and_map(nd, 0);
//获取GPIO对应的中断号
//dev= nd:为设备节点;
//Index=0:索引号,intemrupts属性可能包含多条中断信息,通过index指定要获取的信息;
//返回值:中断号;
if(!key.irq_key){ return -EINVAL; }
irq_flags = irq_get_trigger_type(key.irq_key);
//根据中断号key.irq_key获取“设备树中指定的中断触发类型”
if (IRQF_TRIGGER_NONE == irq_flags)
irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
ret = request_irq(key.irq_key, key_interrupt_fun, irq_flags, "Key0_IRQ", NULL);
//用来申请“按键中断”;
//irq=key.irq_key为中断号;
//handler=key_interrupt_fun:中断处理函数,当中断发生以后就会执行此中断处理函数;
//fags=irq_flags:中断标志;可以在文件“include/linux/interrupt.h”里面查看所有的中断标志;
//name="Key0_IRQ":中断名字,设置以后可以在“/proc/interrupts”文件中看到对应的中断名字;
//dev=NULL:如果将flags设置为IRQF_SHARED的话,dev用来区分不同的中断。
//一般情况下将dev设置为“设备结构体”,dev会传递给中断处理函数irg_handler_t的第二个参数。
//返回值:0表示申请“按键中断”成功,如果返回“-EBUSY”的话表示该中断已经被申请过了, 其他负值,表示申请“按键中断”失败。
if (ret)
{
gpio_free(key.gpio_key);//释放“gpio编号”
return ret;
}
return 0;
}
//函数功能:platform驱动的probe函数
//当驱动与设备匹配成功以后此函数会被执行
//pdev为platform设备指针
//返回值为0,表示成功;其他负值,失败
static int Zhang_key_probe(struct platform_device *pdev)
{
int ret;
ret = key_gpio_init(pdev->dev.of_node);//按键初始化函数
if(ret < 0)return ret;
timer_setup(&key.timer, key_timer_function, 0);
//初始化定时器“timer_list结构变量key.timer”
//key_timer_function为定时器的中断处理函数;
//flags=0为标志位;
key.idev = input_allocate_device();
//申请input_dev结构变量为key.idev
key.idev->name = KEYINPUT_NAME;//设置“设备名字”
#if 0
__set_bit(EV_KEY, key.idev->evbit);
//设置产生“按键事件”
__set_bit(EV_REP, key.idev->evbit);
/* 重复事件,比如按下去不放开,就会一直输出信息 */
/* 初始化input_dev,设置产生哪些按键 */
__set_bit(KEY_0, key.idev->keybit);
#endif
#if 0
key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
//BIT_MASK(EV_KEY)表示将第EV_KEY位置1
//BIT_MASK(EV_REP)表示将第EV_REP位置1
key.idev->keybit[BIT_WORD(KEY_0)] |= BIT_MASK(KEY_0);
//BIT_WORD(KEY_0)表示将“KEY_0”转换为字的偏移量
//BIT_MASK(KEY_0)表示将第KEY_0)位置1
#endif
#if 1
key.idev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
//BIT_MASK(EV_KEY)表示将第EV_KEY位置1
//BIT_MASK(EV_REP)表示将第EV_REP位置1
input_set_capability(key.idev, EV_KEY, KEY_0);
//当type=EV_KEY, code=KEY_0将dev->keybit中的第code位置1
#endif
ret = input_register_device(key.idev);
//向Linux内核注册key.idev。
// key.idev就是要注册的input_dev结构变量
//返回值:0,input_dev注册成功;负值,input_dev注册失败
if (ret) {
printk("register input device failed!\r\n");
goto free_gpio;
}
return 0;
free_gpio:
free_irq(key.irq_key,NULL);
//key.irq_key:要释放的中断号。
//dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。//共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
//返回值:无。
gpio_free(key.gpio_key); //释放“gpio编号”
del_timer_sync(&key.timer);
//key.timer指向要被删除的定时器
//等待其他处理器使用完定时器后再删除该定时器,del_timer_sync()不能用在
//中断服务程序中;
return -EIO;
}
//函数功能:platform驱动的remove函数,当platform驱动模块卸载时此函数会被执行
//dev为platform设备指针
//返回值为0,成功;其他负值,失败
static int Zhang_key_remove(struct platform_device *pdev)
{
free_irq(key.irq_key,NULL);
//key.irq_key:要释放的中断号。
//dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。//共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
//返回值:无。
gpio_free(key.gpio_key); //释放“gpio编号”
del_timer_sync(&key.timer);
//key.timer指向要被删除的定时器
//等待其他处理器使用完定时器后再删除该定时器,del_timer_sync()不能用在
//中断服务程序中;
input_unregister_device(key.idev);
//释放input_dev型结构key.idev
return 0;
}
static const struct of_device_id key_of_match[] = {
{.compatible = "zgq,key"},/*这是驱动中的compatible属性*/
{/*这是一个空元素,在编写of_device_id时最后一个元素一定要为空*/}
};
static struct platform_driver Zhang_key_driver = {
.driver = {
.name = "stm32mp1-key",
.of_match_table = key_of_match,
},
.probe = Zhang_key_probe,
/*platform的probe函数为Zhang_key_probe()*/
.remove = Zhang_key_remove,
/*platform的probe函数为Zhang_key_remove()*/
};
module_platform_driver(Zhang_key_driver);
//Zhang_key_driver为platform_driver结构
//用来向linux内核注册platform驱动
MODULE_AUTHOR("zgq"); //添加作者名字
MODULE_LICENSE("GPL"); //LICENSE采用“GPL协议”
MODULE_DESCRIPTION("This is Key_Driver_Test_Module!");//模块介绍
MODULE_INFO(intree, "Y");
//去除显示“loading out-of-tree module taints kernel.”
MODULE_ALIAS("input:key-gpio");