aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorPeter Johanson <[email protected]>2023-03-15 21:48:30 -0400
committerPete Johanson <[email protected]>2024-03-27 20:59:26 -0700
commitadb3a13dc583e1876d5aa17da0ade4c1024cad7d (patch)
treee3cfcb746af8e65e3f8edf6d2ab0432d7a5d3db9
parent58ccc5970dc804857d81609857d4217278ec025c (diff)
downloadzmk-adb3a13dc583e1876d5aa17da0ade4c1024cad7d.tar.gz
zmk-adb3a13dc583e1876d5aa17da0ade4c1024cad7d.zip
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 <[email protected]>
-rw-r--r--app/CMakeLists.txt4
-rw-r--r--app/Kconfig9
-rw-r--r--app/Kconfig.behaviors10
-rw-r--r--app/dts/behaviors.dtsi1
-rw-r--r--app/dts/behaviors/soft_off.dtsi15
-rw-r--r--app/dts/bindings/zmk,behavior-key-scanned.yaml31
-rw-r--r--app/dts/bindings/zmk,behavior-key.yaml31
-rw-r--r--app/dts/bindings/zmk,soft-off-wakeup-sources.yaml14
-rw-r--r--app/dts/bindings/zmk,wakeup-trigger-key.yaml18
-rw-r--r--app/include/zmk/pm.h9
-rw-r--r--app/src/behavior_key.c159
-rw-r--r--app/src/behavior_key_scanned.c194
-rw-r--r--app/src/kscan.c6
-rw-r--r--app/src/pm.c59
-rw-r--r--app/src/wakeup_trigger_key.c87
-rw-r--r--docs/docs/behaviors/soft-off.md38
-rw-r--r--docs/docs/development/new-shield.mdx1
-rw-r--r--docs/docs/features/soft-off.md164
-rw-r--r--docs/sidebars.js2
19 files changed, 852 insertions, 0 deletions
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index b12d04742e..ac83091c5e 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -29,7 +29,11 @@ target_sources(app PRIVATE src/matrix_transform.c)
target_sources(app PRIVATE src/sensors.c)
target_sources_ifdef(CONFIG_ZMK_WPM app PRIVATE src/wpm.c)
target_sources(app PRIVATE src/event_manager.c)
+target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY app PRIVATE src/behavior_key.c)
+target_sources_ifdef(CONFIG_ZMK_BEHAVIOR_KEY_SCANNED app PRIVATE src/behavior_key_scanned.c)
+target_sources_ifdef(CONFIG_ZMK_PM_SOFT_OFF app PRIVATE src/pm.c)
target_sources_ifdef(CONFIG_ZMK_EXT_POWER app PRIVATE src/ext_power_generic.c)
+target_sources_ifdef(CONFIG_ZMK_WAKEUP_TRIGGER_KEY app PRIVATE src/wakeup_trigger_key.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/sensor_event.c)
diff --git a/app/Kconfig b/app/Kconfig
index 21e97ac635..60a959d498 100644
--- a/app/Kconfig
+++ b/app/Kconfig
@@ -423,6 +423,15 @@ config ZMK_EXT_POWER
bool "Enable support to control external power output"
default y
+config ZMK_PM_SOFT_OFF
+ bool "Soft-off support"
+ select PM_DEVICE
+
+config ZMK_WAKEUP_TRIGGER_KEY
+ bool "Hardware supported wakeup (GPIO)"
+ default y
+ depends on DT_HAS_ZMK_WAKEUP_TRIGGER_KEY_ENABLED && ZMK_PM_SOFT_OFF
+
#Power Management
endmenu
diff --git a/app/Kconfig.behaviors b/app/Kconfig.behaviors
index 7a1e44f6db..e5e0c4d7a2 100644
--- a/app/Kconfig.behaviors
+++ b/app/Kconfig.behaviors
@@ -1,6 +1,16 @@
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
+config ZMK_BEHAVIOR_KEY
+ bool
+ default y
+ depends on DT_HAS_ZMK_BEHAVIOR_KEY_ENABLED
+
+config ZMK_BEHAVIOR_KEY_SCANNED
+ bool
+ default y
+ depends on DT_HAS_ZMK_BEHAVIOR_KEY_SCANNED_ENABLED
+
config ZMK_BEHAVIOR_KEY_TOGGLE
bool
default y
diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi
index 23f2fee280..fde7527189 100644
--- a/app/dts/behaviors.dtsi
+++ b/app/dts/behaviors.dtsi
@@ -20,3 +20,4 @@
#include <behaviors/backlight.dtsi>
#include <behaviors/macros.dtsi>
#include <behaviors/mouse_key_press.dtsi>
+#include <behaviors/soft_off.dtsi>
diff --git a/app/dts/behaviors/soft_off.dtsi b/app/dts/behaviors/soft_off.dtsi
new file mode 100644
index 0000000000..fa6571a111
--- /dev/null
+++ b/app/dts/behaviors/soft_off.dtsi
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2023 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/ {
+ behaviors {
+ /omit-if-no-ref/ soft_off: behavior_soft_off {
+ compatible = "zmk,behavior-soft-off";
+ label = "SOFTOFF";
+ #binding-cells = <0>;
+ };
+ };
+};
diff --git a/app/dts/bindings/zmk,behavior-key-scanned.yaml b/app/dts/bindings/zmk,behavior-key-scanned.yaml
new file mode 100644
index 0000000000..bdb3abaff0
--- /dev/null
+++ b/app/dts/bindings/zmk,behavior-key-scanned.yaml
@@ -0,0 +1,31 @@
+# Copyright (c) 2023 The ZMK Contributors
+# SPDX-License-Identifier: MIT
+
+description: |
+ Driver for a dedicated key triggered by matrix scanning for invoking a connected behavior.
+
+compatible: "zmk,behavior-key-scanned"
+
+include: base.yaml
+
+properties:
+ key:
+ type: phandle
+ required: true
+ description: The GPIO key that triggers wake via interrupt
+ bindings:
+ type: phandle
+ required: true
+ description: The GPIO key that triggers wake via interrupt
+ debounce-press-ms:
+ type: int
+ default: 5
+ description: Debounce time for key press in milliseconds. Use 0 for eager debouncing.
+ debounce-release-ms:
+ type: int
+ default: 5
+ description: Debounce time for key release in milliseconds.
+ debounce-scan-period-ms:
+ type: int
+ default: 1
+ description: Time between reads in milliseconds when any key is pressed.
diff --git a/app/dts/bindings/zmk,behavior-key.yaml b/app/dts/bindings/zmk,behavior-key.yaml
new file mode 100644
index 0000000000..ff7a585eaf
--- /dev/null
+++ b/app/dts/bindings/zmk,behavior-key.yaml
@@ -0,0 +1,31 @@
+# Copyright (c) 2023 The ZMK Contributors
+# SPDX-License-Identifier: MIT
+
+description: |
+ Driver for a dedicated key for invoking a connected behavior.
+
+compatible: "zmk,behavior-key"
+
+include: base.yaml
+
+properties:
+ key:
+ type: phandle
+ required: true
+ description: The GPIO key that triggers wake via interrupt
+ bindings:
+ type: phandle
+ required: true
+ description: The GPIO key that triggers wake via interrupt
+ debounce-press-ms:
+ type: int
+ default: 5
+ description: Debounce time for key press in milliseconds. Use 0 for eager debouncing.
+ debounce-release-ms:
+ type: int
+ default: 5
+ description: Debounce time for key release in milliseconds.
+ debounce-scan-period-ms:
+ type: int
+ default: 1
+ description: Time between reads in milliseconds when any key is pressed.
diff --git a/app/dts/bindings/zmk,soft-off-wakeup-sources.yaml b/app/dts/bindings/zmk,soft-off-wakeup-sources.yaml
new file mode 100644
index 0000000000..f98039a0ca
--- /dev/null
+++ b/app/dts/bindings/zmk,soft-off-wakeup-sources.yaml
@@ -0,0 +1,14 @@
+# Copyright (c) 2023 The ZMK Contributors
+# SPDX-License-Identifier: MIT
+
+description: |
+ Description of all possible wakeup-sources from a forces
+ soft-off state.
+
+compatible: "zmk,soft-off-wakeup-sources"
+
+properties:
+ wakeup-sources:
+ type: phandles
+ required: true
+ description: List of wakeup-sources that should be enabled to wake the system from forces soft-off state.
diff --git a/app/dts/bindings/zmk,wakeup-trigger-key.yaml b/app/dts/bindings/zmk,wakeup-trigger-key.yaml
new file mode 100644
index 0000000000..fa7636d1f3
--- /dev/null
+++ b/app/dts/bindings/zmk,wakeup-trigger-key.yaml
@@ -0,0 +1,18 @@
+# Copyright (c) 2023 The ZMK Contributors
+# SPDX-License-Identifier: MIT
+
+description: |
+ Driver for a dedicated key for waking the device from sleep
+
+compatible: "zmk,wakeup-trigger-key"
+
+include: base.yaml
+
+properties:
+ trigger:
+ type: phandle
+ required: true
+ description: The GPIO key that triggers wake via interrupt
+ extra-gpios:
+ type: phandle-array
+ description: Optional set of pins that should be set active before sleeping.
diff --git a/app/include/zmk/pm.h b/app/include/zmk/pm.h
new file mode 100644
index 0000000000..dff217afd2
--- /dev/null
+++ b/app/include/zmk/pm.h
@@ -0,0 +1,9 @@
+/*
+ * Copyright (c) 2023 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#pragma once
+
+int zmk_pm_soft_off(void); \ No newline at end of file
diff --git a/app/src/behavior_key.c b/app/src/behavior_key.c
new file mode 100644
index 0000000000..3633ce39a4
--- /dev/null
+++ b/app/src/behavior_key.c
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2023 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#define DT_DRV_COMPAT zmk_behavior_key
+
+#include <zephyr/device.h>
+#include <drivers/behavior.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/pm/device.h>
+
+#include <zmk/event_manager.h>
+#include <zmk/behavior.h>
+#include <zmk/debounce.h>
+#include <zmk/keymap.h>
+
+LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
+
+struct behavior_key_config {
+ struct zmk_debounce_config debounce_config;
+ int32_t debounce_scan_period_ms;
+ struct gpio_dt_spec key;
+};
+
+struct behavior_key_data {
+ struct zmk_behavior_binding binding;
+ struct zmk_debounce_state debounce_state;
+ struct gpio_callback key_callback;
+ const struct device *dev;
+ struct k_work_delayable update_work;
+ uint32_t read_time;
+};
+
+static void bk_enable_interrupt(const struct device *dev) {
+ const struct behavior_key_config *config = dev->config;
+
+ gpio_pin_interrupt_configure_dt(&config->key, GPIO_INT_LEVEL_ACTIVE);
+}
+
+static void bk_disable_interrupt(const struct device *dev) {
+ const struct behavior_key_config *config = dev->config;
+
+ gpio_pin_interrupt_configure_dt(&config->key, GPIO_INT_DISABLE);
+}
+
+static void bk_read(const struct device *dev) {
+ const struct behavior_key_config *config = dev->config;
+ struct behavior_key_data *data = dev->data;
+
+ zmk_debounce_update(&data->debounce_state, gpio_pin_get_dt(&config->key),
+ config->debounce_scan_period_ms, &config->debounce_config);
+
+ if (zmk_debounce_get_changed(&data->debounce_state)) {
+ const bool pressed = zmk_debounce_is_pressed(&data->debounce_state);
+
+ struct zmk_behavior_binding_event event = {.position = INT32_MAX,
+ .timestamp = k_uptime_get()};
+
+ if (pressed) {
+ behavior_keymap_binding_pressed(&data->binding, event);
+ } else {
+ behavior_keymap_binding_released(&data->binding, event);
+ }
+ }
+
+ if (zmk_debounce_is_active(&data->debounce_state)) {
+ data->read_time += config->debounce_scan_period_ms;
+
+ k_work_reschedule(&data->update_work, K_TIMEOUT_ABS_MS(data->read_time));
+ } else {
+ bk_enable_interrupt(dev);
+ }
+}
+
+static void bk_update_work(struct k_work *work) {
+ struct k_work_delayable *dwork = CONTAINER_OF(work, struct k_work_delayable, work);
+ struct behavior_key_data *data = CONTAINER_OF(dwork, struct behavior_key_data, update_work);
+ bk_read(data->dev);
+}
+
+static void bk_gpio_irq_callback(const struct device *port, struct gpio_callback *cb,
+ const gpio_port_pins_t pin) {
+ struct behavior_key_data *data = CONTAINER_OF(cb, struct behavior_key_data, key_callback);
+
+ bk_disable_interrupt(data->dev);
+
+ data->read_time = k_uptime_get();
+ k_work_reschedule(&data->update_work, K_NO_WAIT);
+}
+
+static int behavior_key_init(const struct device *dev) {
+ const struct behavior_key_config *config = dev->config;
+ struct behavior_key_data *data = dev->data;
+
+ if (!device_is_ready(config->key.port)) {
+ LOG_ERR("GPIO port is not ready");
+ return -ENODEV;
+ }
+
+ k_work_init_delayable(&data->update_work, bk_update_work);
+ data->dev = dev;
+
+ gpio_pin_configure_dt(&config->key, GPIO_INPUT);
+ gpio_init_callback(&data->key_callback, bk_gpio_irq_callback, BIT(config->key.pin));
+ gpio_add_callback(config->key.port, &data->key_callback);
+
+ while (gpio_pin_get_dt(&config->key)) {
+ k_sleep(K_MSEC(100));
+ }
+
+ bk_enable_interrupt(dev);
+
+ return 0;
+}
+
+static int behavior_key_pm_action(const struct device *dev, enum pm_device_action action) {
+ const struct behavior_key_config *config = dev->config;
+ struct behavior_key_data *data = dev->data;
+
+ int ret;
+
+ switch (action) {
+ case PM_DEVICE_ACTION_SUSPEND:
+ bk_disable_interrupt(dev);
+ ret = gpio_remove_callback(config->key.port, &data->key_callback);
+ break;
+ case PM_DEVICE_ACTION_RESUME:
+ ret = gpio_add_callback(config->key.port, &data->key_callback);
+ bk_enable_interrupt(dev);
+ break;
+ default:
+ ret = -ENOTSUP;
+ break;
+ }
+
+ return ret;
+}
+
+#define BK_INST(n) \
+ const struct behavior_key_config bk_config_##n = { \
+ .key = GPIO_DT_SPEC_GET(DT_INST_PHANDLE(n, key), gpios), \
+ .debounce_config = \
+ { \
+ .debounce_press_ms = DT_INST_PROP(n, debounce_press_ms), \
+ .debounce_release_ms = DT_INST_PROP(n, debounce_release_ms), \
+ }, \
+ .debounce_scan_period_ms = DT_INST_PROP(n, debounce_scan_period_ms), \
+ }; \
+ struct behavior_key_data bk_data_##n = { \
+ .binding = ZMK_KEYMAP_EXTRACT_BINDING(0, DT_DRV_INST(n)), \
+ }; \
+ PM_DEVICE_DT_INST_DEFINE(n, behavior_key_pm_action); \
+ DEVICE_DT_INST_DEFINE(n, behavior_key_init, PM_DEVICE_DT_INST_GET(n), &bk_data_##n, \
+ &bk_config_##n, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, NULL);
+
+DT_INST_FOREACH_STATUS_OKAY(BK_INST)
diff --git a/app/src/behavior_key_scanned.c b/app/src/behavior_key_scanned.c
new file mode 100644
index 0000000000..c961b29262
--- /dev/null
+++ b/app/src/behavior_key_scanned.c
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2023 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#define DT_DRV_COMPAT zmk_behavior_key_scanned
+
+#include <zephyr/device.h>
+#include <drivers/behavior.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/pm/device.h>
+
+#include <zmk/event_manager.h>
+#include <zmk/behavior.h>
+#include <zmk/debounce.h>
+#include <zmk/keymap.h>
+
+LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
+
+struct behavior_key_scanned_config {
+ struct zmk_debounce_config debounce_config;
+ int32_t debounce_scan_period_ms;
+ struct gpio_dt_spec key;
+};
+
+struct behavior_key_scanned_data {
+ struct zmk_behavior_binding binding;
+ struct zmk_debounce_state debounce_state;
+ struct gpio_callback key_callback;
+ const struct device *dev;
+ struct k_work_delayable update_work;
+ uint32_t read_time;
+ bool pin_active;
+ bool active_scan_detected;
+ struct k_sem sem;
+};
+
+static void bks_enable_interrupt(const struct device *dev, bool active_scanning) {
+ const struct behavior_key_scanned_config *config = dev->config;
+
+ gpio_pin_interrupt_configure_dt(&config->key, active_scanning ? GPIO_INT_EDGE_TO_ACTIVE
+ : GPIO_INT_LEVEL_ACTIVE);
+}
+
+static void bks_disable_interrupt(const struct device *dev) {
+ const struct behavior_key_scanned_config *config = dev->config;
+
+ gpio_pin_interrupt_configure_dt(&config->key, GPIO_INT_DISABLE);
+}
+
+static void bks_read(const struct device *dev) {
+ const struct behavior_key_scanned_config *config = dev->config;
+ struct behavior_key_scanned_data *data = dev->data;
+
+ if (k_sem_take(&data->sem, K_NO_WAIT) < 0) {
+ // k_work_reschedule(&data->update_work, K_NO_WAIT);
+ return;
+ }
+
+ zmk_debounce_update(&data->debounce_state, data->active_scan_detected,
+ config->debounce_scan_period_ms, &config->debounce_config);
+
+ if (zmk_debounce_get_changed(&data->debounce_state)) {
+ const bool pressed = zmk_debounce_is_pressed(&data->debounce_state);
+
+ struct zmk_behavior_binding_event event = {.position = INT32_MAX,
+ .timestamp = k_uptime_get()};
+
+ if (pressed) {
+ behavior_keymap_binding_pressed(&data->binding, event);
+ } else {
+ behavior_keymap_binding_released(&data->binding, event);
+ }
+ }
+
+ if (zmk_debounce_is_active(&data->debounce_state)) {
+ data->active_scan_detected = false;
+ data->read_time += config->debounce_scan_period_ms;
+
+ k_work_schedule(&data->update_work, K_TIMEOUT_ABS_MS(data->read_time));
+ } else {
+ bks_enable_interrupt(dev, false);
+ }
+
+ k_sem_give(&data->sem);
+}
+
+static void bks_update_work(struct k_work *work) {
+ struct k_work_delayable *dwork = CONTAINER_OF(work, struct k_work_delayable, work);
+ struct behavior_key_scanned_data *data =
+ CONTAINER_OF(dwork, struct behavior_key_scanned_data, update_work);
+ bks_read(data->dev);
+}
+
+static void bks_gpio_irq_callback(const struct device *port, struct gpio_callback *cb,
+ const gpio_port_pins_t pin) {
+ struct behavior_key_scanned_data *data =
+ CONTAINER_OF(cb, struct behavior_key_scanned_data, key_callback);
+ const struct behavior_key_scanned_config *config = data->dev->config;
+
+ uint32_t time = k_uptime_get();
+
+ if (k_sem_take(&data->sem, K_MSEC(10)) < 0) {
+ LOG_ERR("FAILED TO TAKE THE SEMAPHORE");
+ // Do more?
+ return;
+ }
+
+ data->active_scan_detected = true;
+ data->read_time = time;
+
+ if (!zmk_debounce_is_active(&data->debounce_state)) {
+ // When we get that very first interrupt, we need to schedule the update checks to fall in
+ // between each of the real scans, so we can do our checks for state *after* each scan has
+ // occurred.
+ k_work_reschedule(&data->update_work,
+ K_TIMEOUT_ABS_MS(time + (config->debounce_scan_period_ms / 2)));
+
+ bks_enable_interrupt(data->dev, true);
+ }
+
+ k_sem_give(&data->sem);
+}
+
+static int behavior_key_scanned_init(const struct device *dev) {
+ const struct behavior_key_scanned_config *config = dev->config;
+ struct behavior_key_scanned_data *data = dev->data;
+
+ if (!device_is_ready(config->key.port)) {
+ LOG_ERR("GPIO port is not ready");
+ return -ENODEV;
+ }
+
+ k_work_init_delayable(&data->update_work, bks_update_work);
+ k_sem_init(&data->sem, 1, 1);
+ data->dev = dev;
+
+ gpio_pin_configure_dt(&config->key, GPIO_INPUT);
+ gpio_init_callback(&data->key_callback, bks_gpio_irq_callback, BIT(config->key.pin));
+ gpio_add_callback(config->key.port, &data->key_callback);
+
+ while (gpio_pin_get_dt(&config->key)) {
+ k_sleep(K_MSEC(100));
+ }
+
+ bks_enable_interrupt(dev, false);
+
+ return 0;
+}
+
+static int behavior_key_scanned_pm_action(const struct device *dev, enum pm_device_action action) {
+ const struct behavior_key_scanned_config *config = dev->config;
+ struct behavior_key_scanned_data *data = dev->data;
+
+ int ret;
+
+ switch (action) {
+ case PM_DEVICE_ACTION_SUSPEND:
+ bks_disable_interrupt(dev);
+ ret = gpio_remove_callback(config->key.port, &data->key_callback);
+ break;
+ case PM_DEVICE_ACTION_RESUME:
+ ret = gpio_add_callback(config->key.port, &data->key_callback);
+ bks_enable_interrupt(dev, false);
+ break;
+ default:
+ ret = -ENOTSUP;
+ break;
+ }
+
+ return ret;
+}
+
+#define BK_INST(n) \
+ const struct behavior_key_scanned_config bks_config_##n = { \
+ .key = GPIO_DT_SPEC_GET(DT_INST_PHANDLE(n, key), gpios), \
+ .debounce_config = \
+ { \
+ .debounce_press_ms = DT_INST_PROP(n, debounce_press_ms), \
+ .debounce_release_ms = DT_INST_PROP(n, debounce_release_ms), \
+ }, \
+ .debounce_scan_period_ms = DT_INST_PROP(n, debounce_scan_period_ms), \
+ }; \
+ struct behavior_key_scanned_data bks_data_##n = { \
+ .binding = ZMK_KEYMAP_EXTRACT_BINDING(0, DT_DRV_INST(n)), \
+ }; \
+ PM_DEVICE_DT_INST_DEFINE(n, behavior_key_scanned_pm_action); \
+ DEVICE_DT_INST_DEFINE(n, behavior_key_scanned_init, PM_DEVICE_DT_INST_GET(n), &bks_data_##n, \
+ &bks_config_##n, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \
+ NULL);
+
+DT_INST_FOREACH_STATUS_OKAY(BK_INST)
diff --git a/app/src/kscan.c b/app/src/kscan.c
index ff55290a3c..c04ce2d879 100644
--- a/app/src/kscan.c
+++ b/app/src/kscan.c
@@ -6,6 +6,7 @@
#include <zephyr/kernel.h>
#include <zephyr/device.h>
+#include <zephyr/pm/device.h>
#include <zephyr/bluetooth/addr.h>
#include <zephyr/drivers/kscan.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_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;
}
diff --git a/app/src/pm.c b/app/src/pm.c
new file mode 100644
index 0000000000..af12623918
--- /dev/null
+++ b/app/src/pm.c
@@ -0,0 +1,59 @@
+/*
+ * 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 HAS_WAKERS DT_HAS_COMPAT_STATUS_OKAY(zmk_soft_off_wakeup_sources)
+
+#if HAS_WAKERS
+
+#define DEVICE_WITH_SEP(node_id, prop, idx) DEVICE_DT_GET(DT_PROP_BY_IDX(node_id, prop, idx)),
+
+const struct device *soft_off_wakeup_sources[] = {
+ DT_FOREACH_PROP_ELEM(DT_INST(0, zmk_soft_off_wakeup_sources), wakeup_sources, DEVICE_WITH_SEP)};
+
+#endif
+
+int zmk_pm_soft_off(void) {
+#if IS_ENABLED(CONFIG_PM_DEVICE)
+ size_t device_count;
+ const struct device *devs;
+
+ device_count = z_device_get_all_static(&devs);
+
+ // 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 < device_count; i++) {
+ const struct device *dev = &devs[i];
+
+ LOG_DBG("soft-on-off pressed cb: suspend device");
+ if (pm_device_wakeup_is_enabled(dev)) {
+ pm_device_wakeup_enable(dev, false);
+ }
+ pm_device_action_run(dev, PM_DEVICE_ACTION_SUSPEND);
+ }
+#endif // IS_ENABLED(CONFIG_PM_DEVICE)
+
+#if HAS_WAKERS
+ for (int i = 0; i < ARRAY_SIZE(soft_off_wakeup_sources); i++) {
+ const struct device *dev = soft_off_wakeup_sources[i];
+ pm_device_wakeup_enable(dev, true);
+ pm_device_action_run(dev, PM_DEVICE_ACTION_RESUME);
+ }
+#endif // HAS_WAKERS
+
+ LOG_DBG("soft-on-off interrupt: go to sleep");
+ return pm_state_force(0U, &(struct pm_state_info){PM_STATE_SOFT_OFF, 0, 0});
+}
diff --git a/app/src/wakeup_trigger_key.c b/app/src/wakeup_trigger_key.c
new file mode 100644
index 0000000000..0cc4f25070
--- /dev/null
+++ b/app/src/wakeup_trigger_key.c
@@ -0,0 +1,87 @@
+/*
+ * 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_wakeup_trigger_key
+
+struct wakeup_trigger_key_config {
+ struct gpio_dt_spec trigger;
+ size_t extra_gpios_count;
+ struct gpio_dt_spec extra_gpios[];
+};
+
+static int zmk_wakeup_trigger_key_init(const struct device *dev) {
+#if IS_ENABLED(CONFIG_PM_DEVICE)
+ pm_device_init_suspended(dev);
+ pm_device_wakeup_enable(dev, true);
+#endif
+
+ return 0;
+}
+
+#if IS_ENABLED(CONFIG_PM_DEVICE)
+
+static int wakeup_trigger_key_pm_action(const struct device *dev, enum pm_device_action action) {
+ const struct wakeup_trigger_key_config *config = dev->config;
+ int ret = 0;
+
+ switch (action) {
+ case PM_DEVICE_ACTION_RESUME:
+ ret = gpio_pin_interrupt_configure_dt(&config->trigger, GPIO_INT_LEVEL_ACTIVE);
+ if (ret < 0) {
+ LOG_ERR("Failed to configure wakeup trigger key GPIO pin interrupt (%d)", ret);
+ return ret;
+ }
+
+ for (int i = 0; i < config->extra_gpios_count; i++) {
+ ret = gpio_pin_configure_dt(&config->extra_gpios[i], GPIO_OUTPUT_ACTIVE);
+ if (ret < 0) {
+ LOG_WRN("Failed to set extra GPIO pin active for waker (%d)", ret);
+ }
+ }
+ break;
+ case PM_DEVICE_ACTION_SUSPEND:
+
+ ret = gpio_pin_interrupt_configure_dt(&config->trigger, GPIO_INT_DISABLE);
+ if (ret < 0) {
+ LOG_ERR("Failed to configure wakeup trigger key GPIO pin interrupt (%d)", ret);
+ return ret;
+ }
+ break;
+ default:
+ ret = -ENOTSUP;
+ break;
+ }
+
+ return ret;
+}
+
+#endif // IS_ENABLED(CONFIG_PM_DEVICE)
+
+#define WAKEUP_TRIGGER_EXTRA_GPIO_SPEC(idx, n) \
+ GPIO_DT_SPEC_GET_BY_IDX(DT_DRV_INST(n), extra_gpios, idx)
+
+#define WAKEUP_TRIGGER_KEY_INST(n) \
+ const struct wakeup_trigger_key_config wtk_cfg_##n = { \
+ .trigger = GPIO_DT_SPEC_GET(DT_INST_PROP(n, trigger), gpios), \
+ .extra_gpios = {LISTIFY(DT_PROP_LEN_OR(DT_DRV_INST(n), extra_gpios, 0), \
+ WAKEUP_TRIGGER_EXTRA_GPIO_SPEC, (, ), n)}, \
+ .extra_gpios_count = DT_PROP_LEN_OR(DT_DRV_INST(n), extra_gpios, 0), \
+ }; \
+ PM_DEVICE_DT_INST_DEFINE(n, wakeup_trigger_key_pm_action); \
+ DEVICE_DT_INST_DEFINE(n, zmk_wakeup_trigger_key_init, PM_DEVICE_DT_INST_GET(n), NULL, \
+ &wtk_cfg_##n, PRE_KERNEL_2, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, NULL);
+
+DT_INST_FOREACH_STATUS_OKAY(WAKEUP_TRIGGER_KEY_INST)
diff --git a/docs/docs/behaviors/soft-off.md b/docs/docs/behaviors/soft-off.md
new file mode 100644
index 0000000000..0537400495
--- /dev/null
+++ b/docs/docs/behaviors/soft-off.md
@@ -0,0 +1,38 @@
+---
+title: Soft Off Behavior
+sidebar_label: Soft Off
+---
+
+## Summary
+
+The soft off behavior is used to force the keyboard into an off state. Depending on the specific keyboard hardware, the keyboard can be turned back on again either with a dedicated on/off button that is available, or using the reset button found on the device.
+
+For more information, see the [Soft Off Feature](../features/soft-off.md) page.
+
+### Behavior Binding
+
+- Reference: `&soft_off`
+
+Example:
+
+```
+&soft_off
+```
+
+### Configuration
+
+#### Hold Time
+
+By default, the keyboard will be turned off as soon as the key bound to the behavior is released, even if the key is only tapped briefly. If you would prefer that the key need be held a certain amount of time before releasing, you can set the `hold-time-ms` to a non-zero value in your keymap:
+
+```
+&soft_off {
+ hold-time-ms = <5000>; // Only turn off it the key is held for 5 seconds or longer.
+};
+
+/ {
+ keymap {
+ ...
+ };
+};
+```
diff --git a/docs/docs/development/new-shield.mdx b/docs/docs/development/new-shield.mdx
index e99332a807..867ccbc8ce 100644
--- a/docs/docs/development/new-shield.mdx
+++ b/docs/docs/development/new-shield.mdx
@@ -171,6 +171,7 @@ this might look something like:
kscan0: kscan_0 {
compatible = "zmk,kscan-gpio-matrix";
diode-direction = "col2row";
+ wakeup-source;
col-gpios
= <&pro_micro 15 GPIO_ACTIVE_HIGH>
diff --git a/docs/docs/features/soft-off.md b/docs/docs/features/soft-off.md
new file mode 100644
index 0000000000..b0206825cb
--- /dev/null
+++ b/docs/docs/features/soft-off.md
@@ -0,0 +1,164 @@
+---
+title: Soft Off Feature
+sidebar_label: Soft Off
+---
+
+Similar to the deep sleep feature that sends the keyboard into a low power state after a certain period of inactivity, the soft off feature is used to turn the keyboard on and off explicitly. Depending on the keyboard, this may be through a dedicated on/off push button, or merely through an additional binding in the keymap to turn the device off and the existing reset button to turn the device back on.
+
+The feature is intended as an alternative to using a hardware switch to physically cut power from the battery to the keyboard. This can be useful for existing PCBs not designed for wireless that don't have a power switch, or for new designs that favor a push button on/off like found on other devices.
+
+:::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.
+
+## Soft Off With Existing Designs
+
+For existing designs, using soft off is as simple as placing the [Soft Off Behavior](../behaviors/soft-off.md) in your keymap and then invoking it. For splits, at least for now, you'll need to place it somewhere on each side of your keymap and trigger on both sides, starting from the peripheral side first.
+
+You can then wake up the keyboard by pressing the reset button once, and repeating this for each side for split keyboards.
+
+## Adding Soft On/Off To New Designs
+
+### 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.
+
+#### Simple Direct Pin
+
+The simplest way to achieve this is with a push button between a GPIO pin and ground.
+
+#### Matrix-Integrated Hardware Combo
+
+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. To make this work best, the two switches used should both be driven by the same matrix input pin so that both will be active simultaneously on the AND gate inputs. The alternative is to use a combination of diodes and capacitors to ensure both pins are active/high at the same time even if scanning sets them high at different times.
+
+### Firmware Changes
+
+Several items work together to make both triggering soft off properly, and setting up the device to _wake_ from soft off work as expected.
+
+#### GPIO Key
+
+Zephyr's basic GPIO Key concept is used to configure the GPIO pin that will be used for both triggering soft off and waking the device later. Here is an example for a keyboard with a dedicated on/off push button that is a direct wire between the GPIO pin and ground:
+
+```
+/ {
+ keys {
+ compatible = "gpio-keys";
+ wakeup_key: wakeup_key {
+ gpios = <&gpio0 2 (GPIO_ACTIVE_LOW | GPIO_PULL_UP)>;
+ };
+ };
+};
+```
+
+GPIO keys are defined using child nodes under the `gpio-keys` compatible node. Each child needs just one property defined:
+
+- The `gpios` property should be a phandle-array with a fully defined GPIO pin and with the correct pull up/down and active high/low flags set. In the above example the soft on/off would be triggered by pulling the specified pin low, typically by pressing a switch that has the other leg connected to ground.
+
+#### Behavior Key
+
+Next, we will create a new "behavior key". Behavior keys are an easy way to tie a keymap behavior to a GPIO key outside of the normal keymap processing. They do _not_ do the normal keymap processing, so they are only suitable for use with basic behaviors, not complicated macros, hold-taps, etc.
+
+In this case, we will be creating a dedicated instance of the [Soft Off Behavior](../behaviors/soft-off.md) that will be used only for our hardware on/off button, then binding it to our key:
+
+```
+/ {
+ behaviors {
+ hw_soft_off: behavior_hw_soft_off {
+ compatible = "zmk,behavior-soft-off";
+ #binding-cells = <0>;
+ label = "HW_SO";
+ hold-time-ms = <5000>;
+ };
+ };
+
+ soft_off_behavior_key {
+ compatible = "zmk,behavior-key";
+ bindings = <&hw_soft_off>;
+ key = <&wakeup_key>;
+ };
+};
+```
+
+Here are the properties for the behavior key node:
+
+- The `compatible` property for the node must be `zmk,behavior-key`.
+- The `bindings` property is a phandle to the soft off behavior defined above.
+- The `key` property is a phandle to the GPIO key defined earlier.
+
+If you have set up your on/off to be controlled by a matrix-integrated combo, the behavior key needs use a different driver that will handle detecting the pressed state when the pin is toggled by the other matrix kscan driver:
+
+```
+/ {
+ soft_off_behavior_key {
+ compatible = "zmk,behavior-key-scanned";
+ status = "okay";
+ bindings = <&hw_soft_off>;
+ key = <&wakeup_key>;
+ };
+};
+```
+
+Note that the only difference from the `soft_off_behavior_key` definition for GPIO keys above is the `compatible` value of `zmk,behavior-key-scanned`.
+
+#### Wakeup Sources
+
+Zephyr has general support for the concept of a device as a "wakeup source", which ZMK has not previously used. Adding soft off requires properly updating the existing `kscan` devices with the `wakeup-source` property, e.g.:
+
+```
+/ {
+ kscan0: kscan_0 {
+ compatible = "zmk,kscan-gpio-matrix";
+ label = "KSCAN";
+ diode-direction = "col2row";
+ wakeup-source;
+
+ ...
+ };
+};
+```
+
+#### Soft Off Waker
+
+Next, we need to add another device which will be enabled only when the keyboard is going into soft off state, and will configure the previously declared GPIO key with the correct interrupt configuration to wake the device from soft off once it is pressed.
+
+```
+/ {
+ wakeup_source: wakeup_source {
+ compatible = "zmk,wakeup-trigger-key";
+
+ trigger = <&wakeup_key>;
+ wakeup-source;
+ };
+};
+```
+
+Here are the properties for the node:
+
+- The `compatible` property for the node must be `zmk,wakeup-trigger-key`.
+- The `trigger` property is a phandle to the GPIO key defined earlier.
+- The `wakeup-source` property signals to Zephyr this device should not be suspended during the shutdown procedure.
+- An optional `output-gpios` property contains a list of GPIO pins (including the appropriate flags) to set active before going into power off, if needed to ensure the GPIO pin will trigger properly to wake the keyboard. This is only needed for matrix integrated combos. For those keyboards, the list should include the matrix output needs needed so the combo hardware is properly "driven" when the keyboard is off.
+
+Once that is declared, we will list it in an additional configuration section so that the ZMK soft off process knows it needs to enable this device as part of the soft off processing:
+
+```
+/ {
+ soft_off_wakers {
+ compatible = "zmk,soft-off-wakeup-sources";
+ wakeup-sources = <&wakeup_source>;
+ };
+};
+```
+
+Here are the properties for the node:
+
+- The `compatible` property for the node must be `zmk,soft-off-wakeup-sources`.
+- The `wakeup-sources` property is a [phandle array](../config/index.md#devicetree-property-types) pointing to all the devices that should be enabled during the shutdown process to be sure they can later wake the keyboard.
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 37613d568a..ebf0aef76e 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -19,6 +19,7 @@ module.exports = {
"features/underglow",
"features/backlight",
"features/battery",
+ "features/soft-off",
"features/beta-testing",
],
Behaviors: [
@@ -44,6 +45,7 @@ module.exports = {
"behaviors/underglow",
"behaviors/backlight",
"behaviors/power",
+ "behaviors/soft-off",
],
Codes: [
"codes/index",