From 3ea866791a3e68bda9c76b6c90357d8a42d872bc Mon Sep 17 00:00:00 2001 From: Hooky Date: Sat, 4 Mar 2023 12:45:38 +0800 Subject: [PATCH] Add and enable Multiplex kscan code Multiplex handler (all->all) with single pin for interrupt handling. For wired boards/shields, the interrupt can be ignored to simplify the electronics greatly. --- app/drivers/kscan/kscan_gpio_multiplex.c | 412 ++++++++++++++++++ .../kscan/zmk,kscan-gpio-multiplex.yaml | 32 ++ app/module/drivers/kscan/CMakeLists.txt | 1 + app/module/drivers/kscan/Kconfig | 33 ++ 4 files changed, 478 insertions(+) create mode 100644 app/drivers/kscan/kscan_gpio_multiplex.c create mode 100644 app/drivers/zephyr/dts/bindings/kscan/zmk,kscan-gpio-multiplex.yaml diff --git a/app/drivers/kscan/kscan_gpio_multiplex.c b/app/drivers/kscan/kscan_gpio_multiplex.c new file mode 100644 index 00000000..59920e9f --- /dev/null +++ b/app/drivers/kscan/kscan_gpio_multiplex.c @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2020-2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include "debounce.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define DT_DRV_COMPAT zmk_kscan_gpio_multiplex + +#define INST_LEN(n) DT_INST_PROP_LEN(n, gpios) +#define INST_MULTIPLEX_LEN(n) (INST_LEN(n) * (INST_LEN(n) - 1)) + +#if CONFIG_ZMK_KSCAN_DEBOUNCE_PRESS_MS >= 0 +#define INST_DEBOUNCE_PRESS_MS(n) CONFIG_ZMK_KSCAN_DEBOUNCE_PRESS_MS +#else +#define INST_DEBOUNCE_PRESS_MS(n) \ + DT_INST_PROP_OR(n, debounce_period, DT_INST_PROP(n, debounce_press_ms)) +#endif + +#if CONFIG_ZMK_KSCAN_DEBOUNCE_RELEASE_MS >= 0 +#define INST_DEBOUNCE_RELEASE_MS(n) CONFIG_ZMK_KSCAN_DEBOUNCE_RELEASE_MS +#else +#define INST_DEBOUNCE_RELEASE_MS(n) \ + DT_INST_PROP_OR(n, debounce_period, DT_INST_PROP(n, debounce_release_ms)) +#endif + +#define USE_POLLING IS_ENABLED(CONFIG_ZMK_KSCAN_MULTIPLEX_POLLING) +#define USE_INTERRUPT (!USE_POLLING) + +#define COND_INTERRUPT(code) COND_CODE_1(CONFIG_ZMK_KSCAN_MULTIPLEX_POLLING, (), code) + +#define KSCAN_GPIO_CFG_INIT(idx, inst_idx) \ + GPIO_DT_SPEC_GET_BY_IDX(DT_DRV_INST(inst_idx), gpios, idx), + +#define KSCAN_INTR_CFG_INIT(inst_idx) GPIO_DT_SPEC_GET(DT_DRV_INST(inst_idx), interrupt_gpios) + +struct kscan_multiplex_data { + const struct device *dev; + kscan_callback_t callback; + struct k_work_delayable work; + int64_t scan_time; /* Timestamp of the current or scheduled scan. */ +#if USE_INTERRUPT + struct gpio_callback irq_callback; +#endif + /** + * Current state of the matrix as a flattened 2D array of length + * (config->cells.length ^2) + */ + struct debounce_state *multiplex_state; +}; + +struct kscan_gpio_list { + const struct gpio_dt_spec *gpios; + size_t len; +}; + +/** Define a kscan_gpio_list from a compile-time GPIO array. */ +#define KSCAN_GPIO_LIST(gpio_array) \ + ((struct kscan_gpio_list){.gpios = gpio_array, .len = ARRAY_SIZE(gpio_array)}) + +struct kscan_multiplex_config { + struct kscan_gpio_list cells; + struct debounce_config debounce_config; + int32_t debounce_scan_period_ms; + int32_t poll_period_ms; +#if USE_INTERRUPT + const struct gpio_dt_spec interrupt; +#endif +}; + +/** + * Get the index into a matrix state array from a row and column. + * There are effectively (n) cols and (n-1) rows, but we use the full col x row space + * as a safety measure against someone accidentally defining a transform RC at (p,p) + */ +static int state_index(const struct kscan_multiplex_config *config, const int row, const int col) { + __ASSERT(row < config->cells.len, "Invalid row %i", row); + __ASSERT(col < config->cells.len, "Invalid column %i", col); + __ASSERT(col != row, "Invalid column row pair %i, %i", col, row); + + return (col * config->cells.len) + row; +} + +static int kscan_multiplex_set_as_input(const struct gpio_dt_spec *gpio) { + if (!device_is_ready(gpio->port)) { + LOG_ERR("GPIO is not ready: %s", gpio->port->name); + return -ENODEV; + } + + int err = gpio_pin_configure_dt(gpio, GPIO_INPUT); + if (err) { + LOG_ERR("Unable to configure pin %u on %s for input", gpio->pin, gpio->port->name); + return err; + } + return 0; +} + +static int kscan_multiplex_set_as_output(const struct gpio_dt_spec *gpio) { + if (!device_is_ready(gpio->port)) { + LOG_ERR("GPIO is not ready: %s", gpio->port->name); + return -ENODEV; + } + + int err = gpio_pin_configure_dt(gpio, GPIO_OUTPUT); + if (err) { + LOG_ERR("Unable to configure pin %u on %s for output", gpio->pin, gpio->port->name); + return err; + } +#if CONFIG_ZMK_KSCAN_MATRIX_WAIT_BEFORE_INPUTS > 0 + k_busy_wait(CONFIG_ZMK_KSCAN_MATRIX_WAIT_BEFORE_INPUTS); +#endif + err = gpio_pin_set_dt(gpio, 1); + if (err) { + LOG_ERR("Failed to set output pin %u active: %i", gpio->pin, err); + } + return err; +} + +static int kscan_multiplex_set_all_as_input(const struct device *dev) { + const struct kscan_multiplex_config *config = dev->config; + int err = 0; + for (int i = 0; i < config->cells.len; i++) { + err = kscan_multiplex_set_as_input(&config->cells.gpios[i]); + if (err) { + return err; + } + } + + return 0; +} + +static int kscan_multiplex_set_all_outputs(const struct device *dev, const int value) { + const struct kscan_multiplex_config *config = dev->config; + + for (int i = 0; i < config->cells.len; i++) { + const struct gpio_dt_spec *gpio = &config->cells.gpios[i]; + int err = gpio_pin_configure_dt(gpio, GPIO_OUTPUT); + if (err) { + LOG_ERR("Unable to configure pin %u on %s for input", gpio->pin, gpio->port->name); + return err; + } + + err = gpio_pin_set_dt(gpio, value); + if (err) { + LOG_ERR("Failed to set output %i to %i: %i", i, value, err); + return err; + } + } + + return 0; +} + +#if USE_INTERRUPT +static int kscan_multiplex_interrupt_configure(const struct device *dev, const gpio_flags_t flags) { + const struct kscan_multiplex_config *config = dev->config; + const struct gpio_dt_spec *gpio = &config->interrupt; + + int err = gpio_pin_interrupt_configure_dt(gpio, flags); + if (err) { + LOG_ERR("Unable to configure interrupt for pin %u on %s", gpio->pin, gpio->port->name); + return err; + } + + return 0; +} +#endif + +#if USE_INTERRUPT +static int kscan_multiplex_interrupt_enable(const struct device *dev) { + int err = kscan_multiplex_interrupt_configure(dev, GPIO_INT_LEVEL_ACTIVE); + if (err) { + return err; + } + + // While interrupts are enabled, set all outputs active so an pressed key will trigger + return kscan_multiplex_set_all_outputs(dev, 1); +} +#endif + +#if USE_INTERRUPT +static void kscan_multiplex_irq_callback(const struct device *port, struct gpio_callback *cb, + const gpio_port_pins_t _pin) { + struct kscan_multiplex_data *data = CONTAINER_OF(cb, struct kscan_multiplex_data, irq_callback); + + // Disable our interrupt to avoid re-entry while we scan. + kscan_multiplex_interrupt_configure(data->dev, GPIO_INT_DISABLE); + data->scan_time = k_uptime_get(); + k_work_reschedule(&data->work, K_NO_WAIT); +} +#endif + +static void kscan_multiplex_read_continue(const struct device *dev) { + const struct kscan_multiplex_config *config = dev->config; + struct kscan_multiplex_data *data = dev->data; + + data->scan_time += config->debounce_scan_period_ms; + + k_work_reschedule(&data->work, K_TIMEOUT_ABS_MS(data->scan_time)); +} + +static void kscan_multiplex_read_end(const struct device *dev) { +#if USE_INTERRUPT + // Return to waiting for an interrupt. + kscan_multiplex_interrupt_enable(dev); +#else + struct kscan_multiplex_data *data = dev->data; + const struct kscan_multiplex_config *config = dev->config; + + data->scan_time += config->poll_period_ms; + + // Return to polling slowly. + k_work_reschedule(&data->work, K_TIMEOUT_ABS_MS(data->scan_time)); +#endif +} + +static int kscan_multiplex_read(const struct device *dev) { + struct kscan_multiplex_data *data = dev->data; + const struct kscan_multiplex_config *config = dev->config; + bool continue_scan = false; + + // NOTE: MULTI vs MATRIX: set all pins as input, in case there was a failure on a + // previous scan, and one of the pins is still set as output + int err = kscan_multiplex_set_all_as_input(dev); + if (err) { + return err; + } + + // Scan the matrix. + for (int row = 0; row < config->cells.len; row++) { + const struct gpio_dt_spec *out_gpio = &config->cells.gpios[row]; + err = kscan_multiplex_set_as_output(out_gpio); + if (err) { + return err; + } + +#if CONFIG_ZMK_KSCAN_MULTIPLEX_WAIT_BEFORE_INPUTS > 0 + k_busy_wait(CONFIG_ZMK_KSCAN_MULTIPLEX_WAIT_BEFORE_INPUTS); +#endif + + for (int col = 0; col < config->cells.len; col++) { + if (col == row) { + continue; // pin can't drive itself + } + const struct gpio_dt_spec *in_gpio = &config->cells.gpios[col]; + const int index = state_index(config, row, col); + + struct debounce_state *state = &data->multiplex_state[index]; + debounce_update(state, gpio_pin_get_dt(in_gpio), config->debounce_scan_period_ms, + &config->debounce_config); + + // NOTE: MULTI vs MATRIX: because we don't need an input/output => row/column + // setup, we can update in the same loop. + if (debounce_get_changed(state)) { + const bool pressed = debounce_is_pressed(state); + + LOG_DBG("Sending event at %i,%i state %s", row, col, pressed ? "on" : "off"); + data->callback(dev, row, col, pressed); + } + continue_scan = continue_scan || debounce_is_active(state); + } + + err = kscan_multiplex_set_as_input(out_gpio); + if (err) { + return err; + } +#if CONFIG_ZMK_KSCAN_MULTIPLEX_WAIT_BETWEEN_OUTPUTS > 0 + k_busy_wait(CONFIG_ZMK_KSCAN_MULTIPLEX_WAIT_BETWEEN_OUTPUTS); +#endif + } + + if (continue_scan) { + // At least one key is pressed or the debouncer has not yet decided if + // it is pressed. Poll quickly until everything is released. + kscan_multiplex_read_continue(dev); + } else { + // All keys are released. Return to normal. + kscan_multiplex_read_end(dev); + } + + return 0; +} + +static void kscan_multiplex_work_handler(struct k_work *work) { + struct k_work_delayable *dwork = CONTAINER_OF(work, struct k_work_delayable, work); + struct kscan_multiplex_data *data = CONTAINER_OF(dwork, struct kscan_multiplex_data, work); + kscan_multiplex_read(data->dev); +} + +static int kscan_multiplex_configure(const struct device *dev, const kscan_callback_t callback) { + if (!callback) { + return -EINVAL; + } + + struct kscan_multiplex_data *data = dev->data; + data->callback = callback; + return 0; +} + +static int kscan_multiplex_enable(const struct device *dev) { + struct kscan_multiplex_data *data = dev->data; + data->scan_time = k_uptime_get(); + + // Read will automatically start interrupts/polling once done. + return kscan_multiplex_read(dev); +} + +static int kscan_multiplex_disable(const struct device *dev) { + struct kscan_multiplex_data *data = dev->data; + k_work_cancel_delayable(&data->work); + +#if USE_INTERRUPT + return kscan_multiplex_interrupt_configure(dev, GPIO_INT_DISABLE); +#else + return 0; +#endif +} + +static int kscan_multiplex_init_inputs(const struct device *dev) { + const struct kscan_multiplex_config *config = dev->config; + + for (int i = 0; i < config->cells.len; i++) { + int err = kscan_multiplex_set_as_input(&config->cells.gpios[i]); + if (err) { + return err; + } + } + + return 0; +} + +#if USE_INTERRUPT +static int kscan_multiplex_init_interrupt(const struct device *dev) { + struct kscan_multiplex_data *data = dev->data; + + const struct kscan_multiplex_config *config = dev->config; + const struct gpio_dt_spec *gpio = &config->interrupt; + int err = kscan_multiplex_set_as_input(gpio); + if (err) { + return err; + } + + gpio_init_callback(&data->irq_callback, kscan_multiplex_irq_callback, BIT(gpio->pin)); + err = gpio_add_callback(gpio->port, &data->irq_callback); + if (err) { + LOG_ERR("Error adding the callback to the input device: %i", err); + } + return err; +} +#endif + +static int kscan_multiplex_init(const struct device *dev) { + struct kscan_multiplex_data *data = dev->data; + + data->dev = dev; + + kscan_multiplex_init_inputs(dev); + kscan_multiplex_set_all_outputs(dev, 0); +#if USE_INTERRUPT + kscan_multiplex_init_interrupt(dev); +#endif + + k_work_init_delayable(&data->work, kscan_multiplex_work_handler); + return 0; +} + +static const struct kscan_driver_api kscan_multiplex_api = { + .config = kscan_multiplex_configure, + .enable_callback = kscan_multiplex_enable, + .disable_callback = kscan_multiplex_disable, +}; + +#define KSCAN_MULTIPLEX_INIT(n) \ + BUILD_ASSERT(INST_DEBOUNCE_PRESS_MS(n) <= DEBOUNCE_COUNTER_MAX, \ + "ZMK_KSCAN_DEBOUNCE_PRESS_MS or debounce-press-ms is too large"); \ + BUILD_ASSERT(INST_DEBOUNCE_RELEASE_MS(n) <= DEBOUNCE_COUNTER_MAX, \ + "ZMK_KSCAN_DEBOUNCE_RELEASE_MS or debounce-release-ms is too large"); \ + \ + static struct debounce_state kscan_multiplex_state_##n[INST_MULTIPLEX_LEN(n)]; \ + static const struct gpio_dt_spec kscan_multiplex_cells_##n[] = { \ + UTIL_LISTIFY(INST_LEN(n), KSCAN_GPIO_CFG_INIT, n)}; \ + static struct kscan_multiplex_data kscan_multiplex_data_##n = { \ + .multiplex_state = kscan_multiplex_state_##n, \ + }; \ + \ + static struct kscan_multiplex_config kscan_multiplex_config_##n = { \ + .cells = KSCAN_GPIO_LIST(kscan_multiplex_cells_##n), \ + .debounce_config = \ + { \ + .debounce_press_ms = INST_DEBOUNCE_PRESS_MS(n), \ + .debounce_release_ms = INST_DEBOUNCE_RELEASE_MS(n), \ + }, \ + .debounce_scan_period_ms = DT_INST_PROP(n, debounce_scan_period_ms), \ + .poll_period_ms = DT_INST_PROP(n, poll_period_ms), \ + COND_INTERRUPT((.interrupt = KSCAN_INTR_CFG_INIT(n), ))}; \ + \ + DEVICE_DT_INST_DEFINE(n, &kscan_multiplex_init, NULL, &kscan_multiplex_data_##n, \ + &kscan_multiplex_config_##n, APPLICATION, \ + CONFIG_APPLICATION_INIT_PRIORITY, &kscan_multiplex_api); + +DT_INST_FOREACH_STATUS_OKAY(KSCAN_MULTIPLEX_INIT); diff --git a/app/drivers/zephyr/dts/bindings/kscan/zmk,kscan-gpio-multiplex.yaml b/app/drivers/zephyr/dts/bindings/kscan/zmk,kscan-gpio-multiplex.yaml new file mode 100644 index 00000000..aced5d50 --- /dev/null +++ b/app/drivers/zephyr/dts/bindings/kscan/zmk,kscan-gpio-multiplex.yaml @@ -0,0 +1,32 @@ +# Copyright (c) 2022 The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: GPIO keyboard full multiplexed matrix controller + +compatible: "zmk,kscan-gpio-multiplex" + +include: kscan.yaml + +properties: + gpios: + type: phandle-array + required: true + interrupt-gpios: + type: phandle-array + required: true + 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. + poll-period-ms: + type: int + default: 1 + description: Time between reads in milliseconds diff --git a/app/module/drivers/kscan/CMakeLists.txt b/app/module/drivers/kscan/CMakeLists.txt index 7ae9524c..08b0603e 100644 --- a/app/module/drivers/kscan/CMakeLists.txt +++ b/app/module/drivers/kscan/CMakeLists.txt @@ -9,3 +9,4 @@ zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_DIRECT kscan_gpio_direct.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_DEMUX kscan_gpio_demux.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_MOCK_DRIVER kscan_mock.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_COMPOSITE_DRIVER kscan_composite.c) +zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_MULTIPLEX kscan_gpio_multiplex.c) diff --git a/app/module/drivers/kscan/Kconfig b/app/module/drivers/kscan/Kconfig index 6f60b3f9..0da0588d 100644 --- a/app/module/drivers/kscan/Kconfig +++ b/app/module/drivers/kscan/Kconfig @@ -5,6 +5,7 @@ DT_COMPAT_ZMK_KSCAN_COMPOSITE := zmk,kscan-composite DT_COMPAT_ZMK_KSCAN_GPIO_DEMUX := zmk,kscan-gpio-demux DT_COMPAT_ZMK_KSCAN_GPIO_DIRECT := zmk,kscan-gpio-direct DT_COMPAT_ZMK_KSCAN_GPIO_MATRIX := zmk,kscan-gpio-matrix +DT_COMPAT_ZMK_KSCAN_GPIO_MULTIPLEX := zmk,kscan-gpio-multiplex DT_COMPAT_ZMK_KSCAN_MOCK := zmk,kscan-mock if KSCAN @@ -33,6 +34,11 @@ config ZMK_KSCAN_GPIO_MATRIX default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_GPIO_MATRIX)) select ZMK_KSCAN_GPIO_DRIVER +config ZMK_KSCAN_GPIO_MULTIPLEX + bool + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_GPIO_MULTIPLEX)) + select ZMK_KSCAN_GPIO_DRIVER + if ZMK_KSCAN_GPIO_MATRIX config ZMK_KSCAN_MATRIX_WAIT_BEFORE_INPUTS @@ -58,6 +64,30 @@ config ZMK_KSCAN_MATRIX_WAIT_BETWEEN_OUTPUTS endif # ZMK_KSCAN_GPIO_MATRIX +if ZMK_KSCAN_GPIO_MULTIPLEX + +config ZMK_KSCAN_MATRIX_WAIT_BEFORE_INPUTS + int "Ticks to wait before reading inputs after an output set active" + default 0 + help + When iterating over each output to drive it active, read inputs, then set + inactive again, some boards may take time for output to propagate to the + inputs. In that scenario, set this value to a positive value to configure + the number of ticks to wait after setting an output active before reading + the inputs for their active state. + +config ZMK_KSCAN_MULTIPLEX_WAIT_BETWEEN_OUTPUTS + int "Ticks to wait between each output when scanning round robin matrix" + default 0 + help + When iterating over each output to drive it active, read inputs, then set + inactive again, some boards may take time for the previous output to + "settle" before reading inputs for the next active output column. In that + scenario, set this value to a positive value to configure the number of + usecs to wait after reading each column of keys. + +endif # ZMK_KSCAN_GPIO_MULTIPLEX + config ZMK_KSCAN_MOCK_DRIVER bool default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_MOCK)) @@ -67,6 +97,9 @@ if ZMK_KSCAN_GPIO_DRIVER config ZMK_KSCAN_MATRIX_POLLING bool "Poll for key event triggers instead of using interrupts on matrix boards." +config ZMK_KSCAN_MULTIPLEX_POLLING + bool "Poll for key event triggers instead of using interrupts on multiplex boards." + config ZMK_KSCAN_DIRECT_POLLING bool "Poll for key event triggers instead of using interrupts on direct wired boards."