现象
设备为开机状态下,按下电源键(PWRON_KEY),无法关机
复现方法
按下电源键进行开机操作,在系统起来的过程中,更准确的描述是在开机logo出现的过程中,不断的连续按下电源键,当系统起来后,再点击电源键,设备就无法完成关机的功能。
调查过程:
查询内核源码
刚开始肯定是想到查看内核关于按键的源码,即找到以下文档
linux/kernel/drivers/input/keyboard/adc-keys.c
linux/kernel/drivers/input/keyboard/gpio_keys.c
后来发现没有看到关机相关的功能,随即查看原理图发现电源键是在rk806芯片上的一个按键,查看该芯片的数据手册,找到该芯片的驱动文件。
linux/ultr/linux/kernel/drivers/mfd/rk806-spi.c由下图找到806-core

/home/ultr/linux/ultr/linux/kernel/drivers/mfd/rk806-core.c 找到rk805-pwrkey

linux/ultr/linux/kernel/drivers/input/misc/rk805-pwrkey.c

电源键的上升沿中断处理函数中通过input子系统上报电源键的状态即同步电源键释放事件
而此时下降沿中断处理函数中的内容被注释
static int rk805_pwrkey_probe(struct platform_device *pdev)
{
struct input_dev *pwr;
int fall_irq, rise_irq;
struct device_node *np;
int err;
np = of_get_child_by_name(pdev->dev.parent->of_node, "pwrkey");
if (np && !of_device_is_available(np)) {
dev_info(&pdev->dev, "device is disabled
");
return -EINVAL;
}
pwr = devm_input_allocate_device(&pdev->dev);
if (!pwr) {
dev_err(&pdev->dev, "Can't allocate power button
");
return -ENOMEM;
}
pwr->name = "rk805 pwrkey";
pwr->phys = "rk805_pwrkey/input0";
pwr->id.bustype = BUS_HOST;
input_set_capability(pwr, EV_KEY, KEY_POWER);
fall_irq = platform_get_irq(pdev, 0);
if (fall_irq < 0)
return fall_irq;
rise_irq = platform_get_irq(pdev, 1);
if (rise_irq < 0)
return rise_irq;
err = devm_request_any_context_irq(&pwr->dev, fall_irq,
pwrkey_fall_irq,
IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
"rk805_pwrkey_fall", pwr);
if (err < 0) {
dev_err(&pdev->dev, "Can't register fall irq: %d
", err);
return err;
}
err = devm_request_any_context_irq(&pwr->dev, rise_irq,
pwrkey_rise_irq,
IRQF_TRIGGER_RISING | IRQF_ONESHOT,
"rk805_pwrkey_rise", pwr);
if (err < 0) {
dev_err(&pdev->dev, "Can't register rise irq: %d
", err);
return err;
}
err = input_register_device(pwr);
if (err) {
dev_err(&pdev->dev, "Can't register power button: %d
", err);
return err;
}
platform_set_drvdata(pdev, pwr);
device_init_wakeup(&pdev->dev, true);
return 0;
}
用户态查看是否有此输入设备 rk805 pwrkey
cat /proc/bus/input/devices

正常情况下的事件上报

非正常情况下没有事件上报的打印,但是却有中断
root@ubuntu2004:/home/rpdzkj#
root@ubuntu2004:/home/rpdzkj# cat /proc/interrupts | grep rk805
128: 0 0 0 0 16 17 0 0 rk806 0 Edge rk805_pwrkey_fall
129: 0 0 0 0 15 18 0 0 rk806 1 Edge rk805_pwrkey_rise
root@ubuntu2004:/home/rpdzkj#
root@ubuntu2004:/home/rpdzkj#
root@ubuntu2004:/home/rpdzkj# cat /proc/interrupts | grep rk805
128: 0 0 0 0 16 22 0 0 rk806 0 Edge rk805_pwrkey_fall
129: 0 0 0 0 15 23 0 0 rk806 1 Edge rk805_pwrkey_rise
root@ubuntu2004:/home/rpdzkj#
从内核态往用户态的线索止步于此,由因往果推无法继续,那就由果往因推

这是点击电源键正常关机的打印日志,systemd-shutdown出现次数最多,于是在根文件系统和内核中搜索此关键词,最终了解到 systemctl poweroff关机命令、 systemd-logind.service服务(systemd-logind)。
明白整个流程:按电源键–>电源中断–>通过input子系统上发按键事件到用户态–>systemd-logind.service–>服务调用关机命令–>进行关机。
目前就处于input子系统没有上传事件至用户态,这也是bug的根源所在。
猜想时刻
由于按键下降沿中断处理函数内容被注释,因此,当系统起来过程中点击按键时input子系统上传按键按下事件,但此时根文件系统还没有挂载好,因此无法进行关机操作;
而且由于下降沿中断处理函数内容被注释导致没有改变按键的状态。当系统起来后再点击电源键,由于上传按键的状态没有变化,因此在用户态看不到事件打印log,只能看到中断次数增加。
似乎猜想很合理,看下input子系统机制
按键状态管理
Linux输入子系统内部维护着每个按键的当前状态。当你调用:
input_report_key(pwr, KEY_POWER, 1); // 按下
input_sync(pwr);
输入子系统会:记录 KEY_POWER 的状态为 按下 (1),将事件发送到用户空间
如果按键一直处于”按下”状态而没有释放,当你再次按下物理按键时:
// 第二次按下物理按键
input_report_key(pwr, KEY_POWER, 1); // 再次尝试上报"按下"
input_sync(pwr);
输入子系统会检查当前状态:
当前状态:KEY_POWER = 1 (已按下)
新上报状态:KEY_POWER = 1 (按下)
由于状态没有变化,输入子系统会认为这不是一个新事件,因此不会向用户空间发送任何数据!
输入子系统基于状态变化工作:只有状态改变时才产生事件
解决方法
两个独立中断:下降沿中断处理函数中添加注释掉的内容,以改变按键状态。
单个中断:如果仅用一个中断,那在中断处理函数中单次按下立即释放,以改变按键状态。
①

②

扩展
当点击电源键时,不直接关机,而是在屏幕上弹出一个窗口,让用户去选择是否关机
电源键触发关机窗口


