Skip to content

Commit

Permalink
feat: Add soft on/off support.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
petejohanson committed Jul 20, 2023
1 parent ee1e135 commit 52dc443
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 1 deletion.
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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_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/position_state_changed.c)
target_sources(app PRIVATE src/events/sensor_event.c)
Expand Down
5 changes: 5 additions & 0 deletions app/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,11 @@ config ZMK_EXT_POWER
bool "Enable support to control external power output"
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
endmenu

Expand Down
27 changes: 26 additions & 1 deletion app/drivers/kscan/kscan_gpio_matrix.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/kscan.h>
#include <zephyr/pm/device.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/__assert.h>
Expand Down Expand Up @@ -420,6 +421,28 @@ static int kscan_matrix_init(const struct device *dev) {
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 = {
.config = kscan_matrix_configure,
.enable_callback = kscan_matrix_enable,
Expand Down Expand Up @@ -464,7 +487,9 @@ static const struct kscan_driver_api kscan_matrix_api = {
.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_api);

Expand Down
22 changes: 22 additions & 0 deletions app/dts/bindings/zmk,soft-on-off-gpio.yaml
Original file line number Diff line number Diff line change
@@ -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 changes: 6 additions & 0 deletions app/src/kscan.c
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
}
147 changes: 147 additions & 0 deletions app/src/soft_on_off_gpio.c
Original file line number Diff line number Diff line change
@@ -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);

0 comments on commit 52dc443

Please sign in to comment.