在消费类电子中,功耗是很重要的,甚至项目后期一直在调功耗,看看哪里还可以再省电。由此就有了 Linux 电源管理子系统,该子系统包含很多方面:什么时候可以降帧、什么时候可以关掉其他 CPU core、系统运行时如果某外设很少用需要让它运行时休眠、系统休眠时要保证哪些外设可以唤醒系统。
博主今天要讨论的,就是一个按键如何唤醒系统,类似于手机的电源键。
这个功能并不是新功能,所以 Linux 内部有一个 demo 可以使用,先教大家如何使用该 demo,然后较大家如何撰写中断唤醒系统驱动。
官方 demo
demo 目录:/kernel4.14/drivers/input/keyboard/gpio_keys.c
该驱动是专门为按键准备的,是一个身经百战的驱动,任何时候测试按键中断或者中断唤醒系统都可以用它,很多时候比自己写的驱动靠谱。
要想使用该驱动,首先在该目录的 Makefile 中增加:
obj-y += gpio_keys.o
- 1.
设备树中增加:
gpio-keys {
compatible = "gpio-keys";
#address-cells = <1>;
#size-cells = <0>;
autorepeat;
key0 {
label = "GPIO Key Enter";
linux,code = <KEY_ENTER>;
gpios = <&gpio1 18 GPIO_ACTIVE_LOW>;
gpio-key,wakeup;
};
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
compatible 属性是 “gpio-keys”,gpio_keys.c 文件的674行会匹配这个属性,匹配到了该驱动就会运行。
linux,code 属性是按键值,Linux 对所有按键事件都有编号,所以KEY_ENTER 实际是一个数字,是驱动向上层报告的一个按键值。
gpios 属性是标明哪一个 GPIO 口,低电平触发,大家可以自己选一个 GPIO。
gpio-key,wakeup 是代表此GPIO支持中断唤醒,你也可以写成:wakeup-source。新老版本而已。
修改就是这么简单,不过语法要符合各位手中的开发板平台。然后编译出内核和设备树文件,下载到板子中。(Linux 内核根目录会有 .config 文件,确保 CONFIG_PM_SLEEP=y 有打开)
如果驱动加载成功,在 /proc/interrupts 中可以看到:
从左往右第一列是软件中断号(唯一)。
第二列是 CPU,表示该中断在该CPU上触发了多少次,多核会有多列。
第三列是中断控制器,imx6ull开发板根中断控制器是GPC,外部中断控制器是gpio-mxc,两者是级联关系。
第四列是硬件中断号,也就是GPIO口编号。
第五列表示该中断是边沿触发还是电平触发。
第六列是中断名称,可以找到一个 GPIO Key Enter,如果驱动加载成功就能看到,如果失败就看不到。
验证方法
在内核中,休眠方式有很多种,可以通过下面命令查看
# cat /sys/power/state
- 1.
常用的休眠方式有freeze、standby、mem、disk
freeze:冻结I/O设备,将它们置于低功耗状态,使处理器进入空闲状态,唤醒最快,耗电比其它standby, mem, disk方式高
standby:除了冻结I/O设备外,还会暂停系统,唤醒较快,耗电比其它 mem, disk方式高
mem:将运行状态数据存到内存,并关闭外设,进入等待模式,唤醒较慢,耗电比disk方式高
disk:将运行状态数据存到硬盘,然后关机,唤醒最慢
示例:
# echo mem > /sys/power/state
- 1.
系统进入睡眠后,基本都会停掉UI、停掉串口,串口无法操作,如图:
按下按键,系统恢复:
当然这里的 log 并不完整,输入 dmesg 可以看到完整 log:
PM:power manager
具体干了什么,图中有解释,分为 suspend 过程和 resume 过程。
其实一个中断让它支持唤醒系统,最主要是多了两个函数:suspend、resume。
suspend 函数在系统整体 suspend 的时候,会调用每个外设注册的 suspend,我们在这个函数中调用 enable_irq_wake,表示该中断在系统休眠时是 enable 状态。
resume 函数在系统整体 resume 的时候,会调用每个外设注册的 resume 函数,在 resume 函数中调用 disable_irq_wake ,表示该中断在系统运行时不需要。两者成对使用。
具体参看下面文章,写的很好:
http://www.wowotech.net/irq_subsystem/irq_handle_procedure.html
大家也可以研究一下 gpio_keys.c,该驱动看起来比较复杂,但是很完善,毕竟身经百战,什么因素都考虑到了,测试就用它!
博主写的 demo
博主下面给的是简化版,并且自测OK,分享给大家,以后如果需要可以copy
xxx.c
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/uaccess.h>
#include <linux/pm.h>
#include <linux/slab.h>
#include <linux/sysctl.h>
#include <linux/proc_fs.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/gpio_keys.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_platform.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/spinlock.h>
#include <linux/cdev.h>
static int gpionum = 0;
static int irqnum = 0;
static irqreturn_t my_handler(int irq, void *dev_id)
{
printk("%s\r\n",__FUNCTION__);
return IRQ_HANDLED;
}
static int gpio_keys_probe(struct platform_device *pdev)
{
int ret = 0;
struct device_node *node = NULL;; /* 设备节点*/
node = of_find_compatible_node(NULL,NULL,"atkalpha-key");
if (node == NULL){
printk("%s:atkalpha-key node not find!\r\n",__FUNCTION__);
return -EINVAL;
}
/* 提取 GPIO */
gpionum = of_get_named_gpio(node,"key-gpio", 0);
if (gpionum < 0) {
printk("of_get_named_gpio can't get key\r\n");
}
/* 初始化 key 所使用的 IO,并且设置成中断模式 */
gpio_request(gpionum, "key-gpio");
gpio_direction_input(gpionum);
irqnum = gpio_to_irq(gpionum);
ret = request_irq(irqnum,my_handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, "my-key", NULL);
if(ret < 0){
printk("irq %d request failed!\r\n", irqnum);
return -EFAULT;
}
return 0;
}
static const struct of_device_id gpio_keys_of_match[] = {
{ .compatible = "atkalpha-key", },
{ },
};
MODULE_DEVICE_TABLE(of, gpio_keys_of_match);
static int gpio_keys_remove(struct platform_device *pdev)
{
return 0;
}
static int gpio_keys_suspend(struct device *dev)
{
printk("%s\r\n",__FUNCTION__);
enable_irq_wake(irqnum);
return 0;
}
static int gpio_keys_resume(struct device *dev)
{
printk("%s\r\n",__FUNCTION__);
disable_irq_wake(irqnum);
return 0;
}
static SIMPLE_DEV_PM_OPS(gpio_keys_pm_ops, gpio_keys_suspend, gpio_keys_resume);
static struct platform_driver gpio_keys_device_driver = {
.probe = gpio_keys_probe,
.remove = gpio_keys_remove,
.driver = {
.name = "my-key",
.pm = &gpio_keys_pm_ops,
.of_match_table = of_match_ptr(gpio_keys_of_match),
}
};
static int __init gpio_keys_init(void)
{
return platform_driver_register(&gpio_keys_device_driver);
}
static void __exit gpio_keys_exit(void)
{
platform_driver_unregister(&gpio_keys_device_driver);
}
module_init(gpio_keys_init);
module_exit(gpio_keys_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jason");
MODULE_DESCRIPTION("Keyboard driver for GPIOs");
MODULE_ALIAS("platform:gpio-keys");
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
xxx.dts
key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-key";
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
gpio-key,wakeup;
status = "okay";
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
最后再总结一下:中断唤醒系统和普通的驱动区别在于,多了两个函数:suspend 和 resume,在 suspend 函数中,调用 enable_irq_wake,表示该中断号在系统休眠时也是 enable 状态,可以触发中断。在 resume 函数中,调用 disable_irq_wake ,恢复原始的中断触发路径。
然后使用 SIMPLE_DEV_PM_OPS 宏将 suspend 和 resume 函数注册到 gpio_keys_pm_ops 操作集,最终由 platform 注册到系统中。这样完成后,系统休眠过程中就会调用到设备注册的 suspend,系统唤醒过程中就会调用设备注册的 resume 函数。
至于 probe 函数的书写,我在 GPIO 子系统和中断子系统系列文章都讲过这些函数的使用,大家可以去我的网站查看:
http://www.linuxer.vip
note:该 demo 只用来唤醒系统,如果你的中断是在 I2C 等设备驱动中,唤醒系统后要立刻在中断处理函数中进行 I2C 通信,写法不太一样,但是框架相同。
另外,该驱动的中断处理函数中没做什么东西,因此唤醒后执行完中断处理函数后又会睡过去。如果你想要该中断唤醒系统后让系统一直处于唤醒状态,请在中断处理函数中使用 __pm_stay_awake() 和 __pm_relax()函数。