feat: Add soft on/off support.
Initial work on a soft on/off support for ZMK. Triggering soft off puts the device into deep sleep with only a specific GPIO pin configured to wake the device, avoiding waking from other key presses in the matrix like the normal deep sleep. Co-authored-by: Cem Aksoylar <caksoylar@users.noreply.github.com>
This commit is contained in:
parent
ee1e135104
commit
5d880bda00
8 changed files with 262 additions and 1 deletions
|
@ -28,6 +28,7 @@ target_sources(app PRIVATE src/sensors.c)
|
||||||
target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/wpm.c)
|
target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/wpm.c)
|
||||||
target_sources(app PRIVATE src/event_manager.c)
|
target_sources(app PRIVATE src/event_manager.c)
|
||||||
target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/ext_power_generic.c)
|
target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/ext_power_generic.c)
|
||||||
|
target_sources_ifdef(CONFIG_ZMK_SOFT_ON_OFF_GPIO app PRIVATE src/soft_on_off_gpio.c)
|
||||||
target_sources(app PRIVATE src/events/activity_state_changed.c)
|
target_sources(app PRIVATE src/events/activity_state_changed.c)
|
||||||
target_sources(app PRIVATE src/events/position_state_changed.c)
|
target_sources(app PRIVATE src/events/position_state_changed.c)
|
||||||
target_sources(app PRIVATE src/events/sensor_event.c)
|
target_sources(app PRIVATE src/events/sensor_event.c)
|
||||||
|
|
|
@ -340,6 +340,11 @@ config ZMK_EXT_POWER
|
||||||
bool "Enable support to control external power output"
|
bool "Enable support to control external power output"
|
||||||
default y
|
default y
|
||||||
|
|
||||||
|
config ZMK_SOFT_ON_OFF_GPIO
|
||||||
|
bool "Hardware supported soft on/off (GPIO)"
|
||||||
|
default y
|
||||||
|
depends on DT_HAS_ZMK_SOFT_ON_OFF_GPIO_ENABLED
|
||||||
|
|
||||||
#Power Management
|
#Power Management
|
||||||
endmenu
|
endmenu
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
#include <zephyr/devicetree.h>
|
#include <zephyr/devicetree.h>
|
||||||
#include <zephyr/drivers/gpio.h>
|
#include <zephyr/drivers/gpio.h>
|
||||||
#include <zephyr/drivers/kscan.h>
|
#include <zephyr/drivers/kscan.h>
|
||||||
|
#include <zephyr/pm/device.h>
|
||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
#include <zephyr/sys/__assert.h>
|
#include <zephyr/sys/__assert.h>
|
||||||
|
@ -420,6 +421,28 @@ static int kscan_matrix_init(const struct device *dev) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if IS_ENABLED(CONFIG_PM_DEVICE)
|
||||||
|
|
||||||
|
static int kscan_matrix_pm_action(const struct device *dev, enum pm_device_action action) {
|
||||||
|
int ret = 0;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case PM_DEVICE_ACTION_SUSPEND:
|
||||||
|
kscan_matrix_disable(dev);
|
||||||
|
break;
|
||||||
|
case PM_DEVICE_ACTION_RESUME:
|
||||||
|
kscan_matrix_enable(dev);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ret = -ENOTSUP;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
|
||||||
|
|
||||||
static const struct kscan_driver_api kscan_matrix_api = {
|
static const struct kscan_driver_api kscan_matrix_api = {
|
||||||
.config = kscan_matrix_configure,
|
.config = kscan_matrix_configure,
|
||||||
.enable_callback = kscan_matrix_enable,
|
.enable_callback = kscan_matrix_enable,
|
||||||
|
@ -464,7 +487,9 @@ static const struct kscan_driver_api kscan_matrix_api = {
|
||||||
.diode_direction = INST_DIODE_DIR(n), \
|
.diode_direction = INST_DIODE_DIR(n), \
|
||||||
}; \
|
}; \
|
||||||
\
|
\
|
||||||
DEVICE_DT_INST_DEFINE(n, &kscan_matrix_init, NULL, &kscan_matrix_data_##n, \
|
PM_DEVICE_DT_INST_DEFINE(n, kscan_matrix_pm_action); \
|
||||||
|
\
|
||||||
|
DEVICE_DT_INST_DEFINE(n, &kscan_matrix_init, PM_DEVICE_DT_INST_GET(n), &kscan_matrix_data_##n, \
|
||||||
&kscan_matrix_config_##n, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY, \
|
&kscan_matrix_config_##n, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY, \
|
||||||
&kscan_matrix_api);
|
&kscan_matrix_api);
|
||||||
|
|
||||||
|
|
22
app/dts/bindings/zmk,soft-on-off-gpio.yaml
Normal file
22
app/dts/bindings/zmk,soft-on-off-gpio.yaml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Copyright (c) 2023 The ZMK Contributors
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Driver for controlling the device's power status
|
||||||
|
by listening for activity on a GPIO pin for sleep
|
||||||
|
and wake.
|
||||||
|
|
||||||
|
compatible: "zmk,soft-on-off-gpio"
|
||||||
|
|
||||||
|
properties:
|
||||||
|
input-gpios:
|
||||||
|
type: phandle-array
|
||||||
|
required: true
|
||||||
|
description: The GPIO pin that triggers wake/sleep via interrupt
|
||||||
|
wakeup-sources:
|
||||||
|
type: phandles
|
||||||
|
required: true
|
||||||
|
description: List of wakeup-sources that need to be deactivated so that only this driver/pin wakes the device.
|
||||||
|
output-gpios:
|
||||||
|
type: phandle-array
|
||||||
|
description: Optional set of pins that should be set active before sleeping.
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
#include <zephyr/device.h>
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/pm/device.h>
|
||||||
#include <zephyr/bluetooth/addr.h>
|
#include <zephyr/bluetooth/addr.h>
|
||||||
#include <zephyr/drivers/kscan.h>
|
#include <zephyr/drivers/kscan.h>
|
||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
|
@ -75,6 +76,11 @@ int zmk_kscan_init(const struct device *dev) {
|
||||||
|
|
||||||
kscan_config(dev, zmk_kscan_callback);
|
kscan_config(dev, zmk_kscan_callback);
|
||||||
kscan_enable_callback(dev);
|
kscan_enable_callback(dev);
|
||||||
|
#if IS_ENABLED(CONFIG_PM_DEVICE)
|
||||||
|
if (pm_device_wakeup_is_capable(dev)) {
|
||||||
|
pm_device_wakeup_enable(dev, true);
|
||||||
|
}
|
||||||
|
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
147
app/src/soft_on_off_gpio.c
Normal file
147
app/src/soft_on_off_gpio.c
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 The ZMK Contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <zephyr/drivers/gpio.h>
|
||||||
|
#include <zephyr/devicetree.h>
|
||||||
|
#include <zephyr/init.h>
|
||||||
|
#include <zephyr/pm/device.h>
|
||||||
|
#include <zephyr/pm/pm.h>
|
||||||
|
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
|
||||||
|
|
||||||
|
#define DT_DRV_COMPAT zmk_soft_on_off_gpio
|
||||||
|
|
||||||
|
struct soft_on_off_config {
|
||||||
|
struct gpio_dt_spec input_gpio;
|
||||||
|
struct gpio_callback callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
#define WAKEUP_SOURCE_AND_COMMA(node, prop, idx) DEVICE_DT_GET(DT_PHANDLE_BY_IDX(node, prop, idx)),
|
||||||
|
|
||||||
|
#define WAKEUP_SOURCES_NUM DT_INST_PROP_LEN(0, wakeup_sources)
|
||||||
|
const struct device *wakeup_sources[WAKEUP_SOURCES_NUM] = {
|
||||||
|
DT_INST_FOREACH_PROP_ELEM(0, wakeup_sources, WAKEUP_SOURCE_AND_COMMA)};
|
||||||
|
|
||||||
|
#define OUTPUT_AND_COMMA(node, prop, idx) GPIO_DT_SPEC_GET_BY_IDX(node, prop, idx),
|
||||||
|
|
||||||
|
#if DT_INST_NODE_HAS_PROP(0, output_gpios)
|
||||||
|
const struct gpio_dt_spec outputs[DT_INST_PROP_LEN(0, output_gpios)] = {
|
||||||
|
DT_INST_FOREACH_PROP_ELEM(0, output_gpios, OUTPUT_AND_COMMA)};
|
||||||
|
|
||||||
|
#endif // DT_INST_NODE_HAS_PROP(0, output_gpios)
|
||||||
|
|
||||||
|
static struct soft_on_off_config config = {
|
||||||
|
.input_gpio = GPIO_DT_SPEC_INST_GET(0, input_gpios),
|
||||||
|
};
|
||||||
|
|
||||||
|
static void zmk_soft_on_off_pressed_work_cb(struct k_work *work);
|
||||||
|
K_WORK_DELAYABLE_DEFINE(zmk_soft_on_off_gpio_work, zmk_soft_on_off_pressed_work_cb);
|
||||||
|
|
||||||
|
static void zmk_soft_on_off_pressed_work_cb(struct k_work *work) {
|
||||||
|
int err;
|
||||||
|
|
||||||
|
// Delay again if our pin is still active
|
||||||
|
if (gpio_pin_get_dt(&config.input_gpio) > 0) {
|
||||||
|
LOG_DBG("soft-on-off work cbt: pin still enabled");
|
||||||
|
k_work_schedule(&zmk_soft_on_off_gpio_work, K_SECONDS(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#if IS_ENABLED(CONFIG_PM_DEVICE)
|
||||||
|
// There may be some matrix/direct kscan devices that would be used for wakeup
|
||||||
|
// from normal "inactive goes to sleep" behavior, so disable them as wakeup devices
|
||||||
|
// and then suspend them so we're ready to take over setting up our system
|
||||||
|
// and then putting it into an off state.
|
||||||
|
for (int i = 0; i < WAKEUP_SOURCES_NUM; i++) {
|
||||||
|
const struct device *dev = wakeup_sources[i];
|
||||||
|
|
||||||
|
LOG_DBG("soft-on-off pressed cb: suspend device");
|
||||||
|
if (pm_device_wakeup_is_capable(dev)) {
|
||||||
|
pm_device_wakeup_enable(dev, false);
|
||||||
|
}
|
||||||
|
pm_device_action_run(dev, PM_DEVICE_ACTION_SUSPEND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
|
||||||
|
|
||||||
|
#if DT_INST_NODE_HAS_PROP(0, output_gpios)
|
||||||
|
for (int i = 0; i < ARRAY_SIZE(outputs); i++) {
|
||||||
|
const struct gpio_dt_spec *spec = &outputs[i];
|
||||||
|
|
||||||
|
LOG_DBG("soft-on-off pressed cb: setting output active");
|
||||||
|
gpio_pin_configure_dt(spec, GPIO_OUTPUT_ACTIVE);
|
||||||
|
}
|
||||||
|
#endif // DT_INST_NODE_HAS_PROP(0, output_gpios)
|
||||||
|
|
||||||
|
err = gpio_remove_callback(config.input_gpio.port, &config.callback);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Error removing the callback to the soft on_off GPIO input device: %i", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gpio_pin_interrupt_configure_dt(&config.input_gpio, GPIO_INT_LEVEL_ACTIVE);
|
||||||
|
if (err < 0) {
|
||||||
|
LOG_ERR("Failed to configure soft on_off GPIO pin interrupt (%d)", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DBG("soft-on-off interrupt: go to sleep");
|
||||||
|
pm_state_force(0U, &(struct pm_state_info){PM_STATE_SOFT_OFF, 0, 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
static void zmk_soft_on_off_gpio_interrupt_cb(const struct device *port, struct gpio_callback *cb,
|
||||||
|
const gpio_port_pins_t pin) {
|
||||||
|
// Some super simple debounce, since our interrupt may be triggered by scanning of some matrix,
|
||||||
|
// and we can't schedule our reads to debounce perfectly with any active scanning going on
|
||||||
|
// in parallel.
|
||||||
|
LOG_DBG("soft-on-off interrupt: %d vs %d", pin, config.input_gpio.pin);
|
||||||
|
if ((pin & (gpio_port_pins_t)BIT(config.input_gpio.pin)) != 0 &&
|
||||||
|
gpio_pin_get_dt(&config.input_gpio) > 0) {
|
||||||
|
LOG_DBG("soft-on-off scheduling the work");
|
||||||
|
int err = gpio_pin_interrupt_configure_dt(&config.input_gpio, GPIO_INT_DISABLE);
|
||||||
|
if (err < 0) {
|
||||||
|
LOG_ERR("Failed to disable soft on_off GPIO pin interrupt (%d)", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
k_work_schedule(&zmk_soft_on_off_gpio_work, K_SECONDS(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void zmk_soft_on_off_finish_init(struct k_work *work) {
|
||||||
|
int err;
|
||||||
|
|
||||||
|
gpio_init_callback(&config.callback, zmk_soft_on_off_gpio_interrupt_cb,
|
||||||
|
BIT(config.input_gpio.pin));
|
||||||
|
err = gpio_add_callback(config.input_gpio.port, &config.callback);
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Error adding the callback to the soft on_off GPIO input device: %i", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gpio_pin_interrupt_configure_dt(&config.input_gpio, GPIO_INT_LEVEL_ACTIVE);
|
||||||
|
if (err < 0) {
|
||||||
|
LOG_ERR("Failed to configure soft on_off GPIO pin interrupt (%d)", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
K_WORK_DELAYABLE_DEFINE(finish_init_work, zmk_soft_on_off_finish_init);
|
||||||
|
|
||||||
|
static int zmk_soft_on_off_gpio_init(const struct device *_arg) {
|
||||||
|
int err;
|
||||||
|
|
||||||
|
err = gpio_pin_configure_dt(&config.input_gpio, GPIO_INPUT);
|
||||||
|
if (err < 0) {
|
||||||
|
LOG_ERR("Failed to configure soft on_off GPIO pin for input (%d)", err);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
k_work_schedule(&finish_init_work, K_SECONDS(2));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SYS_INIT(zmk_soft_on_off_gpio_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
|
54
docs/docs/features/soft-on-off.md
Normal file
54
docs/docs/features/soft-on-off.md
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
---
|
||||||
|
title: Soft On/Off
|
||||||
|
sidebar_label: Soft On/Off
|
||||||
|
---
|
||||||
|
|
||||||
|
If your keyboard has been built with support for it, ZMK has the ability to be powered on/off with the push
|
||||||
|
of a button, instead of using a power switch to cut the battery circuit.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
|
||||||
|
The power off is accomplished by putting the MCU into a "soft off" state. Power is _not_ technically removed from the entire system, but the device will only be woken from the state by a few possible events.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
Once powered off, the keyboard will only wake up when:
|
||||||
|
|
||||||
|
- You press the same button/sequence that you pressed to power off the keyboard, or
|
||||||
|
- You press a reset button found on the keyboard
|
||||||
|
|
||||||
|
## Adding Soft On/Off to a Board
|
||||||
|
|
||||||
|
### Hardware Design
|
||||||
|
|
||||||
|
ZMK's soft on/off requires a dedicated GPIO pin to be used to trigger powering off, and to wake the core from the
|
||||||
|
soft off state when it goes active again later.
|
||||||
|
|
||||||
|
The simplest way to achieve this is with a push button between a GPIO pin and ground.
|
||||||
|
|
||||||
|
Another, more complicated option is to tie two of the switch outputs in the matrix together through an AND gate and connect that to the dedicated GPIO pin. This way you can use a key combination in your existing keyboard matrix to trigger soft on/off.
|
||||||
|
|
||||||
|
### Firmware Changes
|
||||||
|
|
||||||
|
To add soft on/off to a board or shield, a new node needs to be added to your devicetree source. Here is an example complete node:
|
||||||
|
|
||||||
|
```
|
||||||
|
/ {
|
||||||
|
...
|
||||||
|
|
||||||
|
soft_on_off {
|
||||||
|
compatible = "zmk,soft-on-off-gpio";
|
||||||
|
input-gpios = <&gpio0 6 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
|
||||||
|
wakeup-sources = <&kscan0>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Here are the requirements for the node:
|
||||||
|
|
||||||
|
- The `compatible` property for the node must be `zmk,soft-on-off-gpio`.
|
||||||
|
- The `input-gpios` property is a phandle to the GPIO pin, with the correct pull up/down and active high/low flag set. In the above example the soft on/off would be triggered by pulling the pin low.
|
||||||
|
- The `wakeup-sources` is a list of devices that needs to be shutdown before going into power off state, to ensure
|
||||||
|
that _only_ our soft on/off device will wake up the keyboard later. Typically this points to the `kscan` node that
|
||||||
|
is configured for the keyboard, which can also wake the keyboard from a ["deep sleep from idle"](../config/power.md#idlesleep) state.
|
||||||
|
- An optional `output-gpios` property contains a list of GPIO pins to set active before going into power off, if needed to ensure the GPIO pin will trigger properly to wake the keyboard.
|
|
@ -19,6 +19,7 @@ module.exports = {
|
||||||
"features/underglow",
|
"features/underglow",
|
||||||
"features/backlight",
|
"features/backlight",
|
||||||
"features/battery",
|
"features/battery",
|
||||||
|
"features/soft-on-off",
|
||||||
"features/beta-testing",
|
"features/beta-testing",
|
||||||
],
|
],
|
||||||
Behaviors: [
|
Behaviors: [
|
||||||
|
|
Loading…
Add table
Reference in a new issue