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)