Power button and embedded Linux

Posted by Marcus Folkesson on Friday, February 9, 2024

Power button and embedded Linux

Not all embedded Linux systems has a power button, but for consumer electronic devices it could be a good thing to be able to turn it off. But how does it work in practice?

Physical button

It all starts with a physical button.

At the board level, the button is usually connected to a General-Purpose-Input-Output (GPIO) pin on the processor. It doesn't have to be directly connected to the processor though, all that matters is that the button press can somehow be mapped to the Linux input subsystem.

The input system reports certain key codes to a process in user space that listen for those codes and performs the corresponding action.

All possible keycodes is listed in linux-event-codes.h [1]. The ones that are relevant to us are:


Map the button to the input system

To map a certain GPIO to the input system we make use of the gpio-keys kernel driver [2]. Make sure you have the CONFIG_KEYBOARD_GPIO option selected in your kernel configuration.

The mapping is described in the devicetree. The key (pun intended) is to set the right key-code on the linux,code property.

See example below:

gpio-keys {
        compatible = "gpio-keys";
        pinctrl-names = "default";
        pinctrl-0 = <&button_pins>;
        status = "okay";

        key-power {
                label = "Power Button";
                gpios = <&gpio 7 GPIO_ACTIVE_LOW>;
                linux,code = <KEY_POWER>;
        key-restart {
                label = "Restart Button";
                gpios = <&gpio 8 GPIO_ACTIVE_LOW>;
                linux,code = <KEY_RESTART>;
        key-suspend {
                label = "Suspend Button";
                gpios = <&gpio 9 GPIO_ACTIVE_LOW>;
                linux,code = <KEY_SUSPEND>;

Hopefully you will now have the input device registered when you boot up:

1 $ dmesg | grep input
2 [    6.886635] input: gpio-keys as /devices/platform/gpio-keys/input/input0


We now have an input device that reports key-events when we press the button. The next step is to have something that read these reports and act on them on the userspace side.

On systems with systemd, we have the systemd-logind service. This service is responsible for a lot of things including:

  • Keeping track of users and sessions, their processes and their idle state. This is implemented by allocating a systemd slice unit for each user below user.slice, and a scope unit below it for each concurrent session of a user. Also, a per-user service manager is started as system service instance of user@.service for each logged in user.
  • Generating and managing session IDs. If auditing is available and an audit session ID is already set for a session, then this ID is reused as the session ID. Otherwise, an independent session counter is used.
  • Providing polkit[1]-based access for users for operations such as system shutdown or sleep
  • Implementing a shutdown/sleep inhibition logic for applications
  • Handling of power/sleep hardware keys
  • Multi-seat management
  • Session switch management
  • Device access management for users
  • Automatic spawning of text logins (gettys) on virtual console activation and user runtime directory management
  • Scheduled shutdown
  • Sending "wall" messages

Is is of course possible to have your own listener, but as we have systemd-logind and ... I sort of started to like systemd, we will go that way.

Configure systemd-logind

systemd-logind has its configuration file [3] in /etc/systemd/logind.conf where it's possible to set an action for each event. Possible actions are:

Action Description
ignore systemd-login will never handle these keys
poweroff System will poweroff
reboot System will reboot
halt System will halt
kexec System will start kexec
suspend System will suspend
hibernate System will hybernate
lock All running sessions will be screen-locked

The events that are configurable are:

Configuration item Listen on Default action
HandlePowerKey KEY_POWER poweroff
HandlePowerKeyLongPress KEY_POWER ignore
HandleRebootKey KEY_RESTART reboot
HandleRebootKeyLongPress KEY_RESTART poweroff
HandleSuspendKey KEY_SUSPEND suspend
HandleSuspendKeyLongPress KEY_SUSPEND hibernate
HandleHibernateKey KEY_SLEEP hibernate
HandleHibernateKeyLongPress KEY_SLEEP ignore

It's not obvious how long a long press is, I could not find anything about it in the documentation. However, the code [4] tells us that it's 5 seconds.


power-switch tag

The logind service will only watch input devices with the power-switch udev tag for keycodes, so there has to be a udev-rule that sets such a tag. The default rule that comes with systemd, 70-power-switch.rules, looks as follow:

ACTION=="remove", GOTO="power_switch_end"

SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_SWITCH}=="1", TAG+="power-switch"
SUBSYSTEM=="input", KERNEL=="event*", ENV{ID_INPUT_KEY}=="1", TAG+="power-switch"


All input devices of the type ID_INPUT_SWITCH or ID_INPUT_KEY will then be monitored.

We can confirm that the tag is set for our devices we created earlier:

 1 $ udevadm info /dev/input/event0
 2P: /devices/platform/gpio-keys/input/input0/event0
 3M: event0
 4R: 0
 5U: input
 6D: c 13:64
 7N: input/event0
 8L: 0
 9S: input/by-path/platform-gpio-keys-event
10E: DEVPATH=/devices/platform/gpio-keys/input/input0/event0
11E: DEVNAME=/dev/input/event0
12E: MAJOR=13
13E: MINOR=64
14E: SUBSYSTEM=input
18E: ID_PATH=platform-gpio-keys
19E: ID_PATH_TAG=platform-gpio-keys
20E: DEVLINKS=/dev/input/by-path/platform-gpio-keys-event
21E: TAGS=:power-switch:
22E: CURRENT_TAGS=:power-switch:


The signal path from a physical power button up to a service handler in userspace is not that complicated. Systemd makes handling the signals and what behavior they should have both smooth and flexible.