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:
Peter Johanson 2023-03-15 21:48:30 -04:00 committed by Pete Johanson
parent 58ccc5970d
commit adb3a13dc5
19 changed files with 852 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -20,3 +20,4 @@
#include <behaviors/backlight.dtsi>
#include <behaviors/macros.dtsi>
#include <behaviors/mouse_key_press.dtsi>
#include <behaviors/soft_off.dtsi>

View File

@ -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>;
};
};
};

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

9
app/include/zmk/pm.h Normal file
View File

@ -0,0 +1,9 @@
/*
* Copyright (c) 2023 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
int zmk_pm_soft_off(void);

159
app/src/behavior_key.c Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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;
}

59
app/src/pm.c Normal file
View File

@ -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});
}

View File

@ -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)

View File

@ -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 {
...
};
};
```

View File

@ -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>

View File

@ -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.

View File

@ -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",