diff --git a/app/drivers/kscan/CMakeLists.txt b/app/drivers/kscan/CMakeLists.txt index 8fc7ed58..3740c9c1 100644 --- a/app/drivers/kscan/CMakeLists.txt +++ b/app/drivers/kscan/CMakeLists.txt @@ -11,3 +11,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_DEGHOST_DRIVER kscan_deghost.c) diff --git a/app/drivers/kscan/Kconfig b/app/drivers/kscan/Kconfig index 1d165669..15e0f35e 100644 --- a/app/drivers/kscan/Kconfig +++ b/app/drivers/kscan/Kconfig @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT DT_COMPAT_ZMK_KSCAN_COMPOSITE := zmk,kscan-composite +DT_COMPAT_ZMK_KSCAN_DEGHOST := zmk,kscan-deghost 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 @@ -11,6 +12,10 @@ config ZMK_KSCAN_COMPOSITE_DRIVER bool default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_COMPOSITE)) +config ZMK_KSCAN_DEGHOST_DRIVER + bool + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_DEGHOST)) + config ZMK_KSCAN_GPIO_DRIVER bool select GPIO diff --git a/app/drivers/kscan/kscan_deghost.c b/app/drivers/kscan/kscan_deghost.c new file mode 100644 index 00000000..739f93d9 --- /dev/null +++ b/app/drivers/kscan/kscan_deghost.c @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_kscan_deghost + +#include +#include +#include +#include +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +struct kscan_deghost_config { + const struct device *child_kscan; + kscan_callback_t callback_for_child; + int rows; + int cols; + const uint8_t *transform_filled; + int transform_filled_len; +}; + +struct kscan_deghost_data { + kscan_callback_t callback; + uint8_t *key_status; +}; + +static int kscan_deghost_enable_callback(const struct device *dev) { + const struct kscan_deghost_config *config = dev->config; + return kscan_enable_callback(config->child_kscan); +} + +static int kscan_deghost_disable_callback(const struct device *dev) { + const struct kscan_deghost_config *config = dev->config; + return kscan_disable_callback(config->child_kscan); +} + +static int kscan_deghost_configure(const struct device *dev, kscan_callback_t callback) { + const struct kscan_deghost_config *config = dev->config; + struct kscan_deghost_data *data = dev->data; + + if (!callback) { + return -EINVAL; + } + + data->callback = callback; + return kscan_config(config->child_kscan, config->callback_for_child); +} + +#define KEY_STATUS_FORCE_NOT_RECHECK (1 << 2) +#define KEY_STATUS_REPORTED_AS_PRESSED_MASK (1 << 1) +#define KEY_STATUS_SEEN_AS_PRESSED_MASK 1 +#define CNT_KEY_STATUS_SEEN_AS_PRESSED(value) ((value)&1) +#define WAS_KEY_STATUS_GHOSTING_BEFORE(value) ((value) == 1) +#define KEY(row, col) data->key_status[(row)*config->cols + (col)] +#define EXISTS(row, col) \ + ((config->transform_filled_len == 0) \ + ? 1 \ + : ((config->transform_filled_len <= (row)*config->cols + (col)) \ + ? 0 \ + : (config->transform_filled[(row)*config->cols + (col)]))) + +static void kscan_deghost_callback_for_child(const struct device *deghost_dev, uint32_t row, + uint32_t col, bool pressed) { + const struct kscan_deghost_config *config = deghost_dev->config; + struct kscan_deghost_data *data = deghost_dev->data; + if (!EXISTS(row, col)) + return; + if (pressed) { + KEY(row, col) |= KEY_STATUS_SEEN_AS_PRESSED_MASK; + bool ghosting = false; + for (uint32_t orow = 0; (orow < config->rows) && !ghosting; orow++) { + if (orow == row || !EXISTS(orow, col)) + continue; + const int other_row_pressed = CNT_KEY_STATUS_SEEN_AS_PRESSED(KEY(orow, col)); + for (uint32_t ocol = 0; ocol < config->cols; ocol++) { + if (ocol == col || (!EXISTS(row, ocol)) || (!EXISTS(orow, ocol))) + continue; + const int pressed_in_rectangle = 1 + other_row_pressed + + CNT_KEY_STATUS_SEEN_AS_PRESSED(KEY(row, ocol)) + + CNT_KEY_STATUS_SEEN_AS_PRESSED(KEY(orow, ocol)); + if (pressed_in_rectangle > 2) { + ghosting = true; + break; + } + } + } + if (!ghosting) { + KEY(row, col) |= KEY_STATUS_REPORTED_AS_PRESSED_MASK; + data->callback(deghost_dev, row, col, pressed); + } + } else { + if (KEY(row, col) & KEY_STATUS_REPORTED_AS_PRESSED_MASK) { + KEY(row, col) = 0; + data->callback(deghost_dev, row, col, pressed); + } else { + KEY(row, col) = 0; + } + for (uint32_t orow = 0; (orow < config->rows); orow++) { + if (orow == row || !EXISTS(orow, col)) + continue; + const int other_row_pressed = CNT_KEY_STATUS_SEEN_AS_PRESSED(KEY(orow, col)); + int check_orow_col = WAS_KEY_STATUS_GHOSTING_BEFORE(KEY(orow, col)); + for (uint32_t ocol = 0; ocol < config->cols; ocol++) { + if (ocol == col || (!EXISTS(row, ocol)) || (!EXISTS(orow, ocol))) + continue; + const int pressed_in_rectangle = 0 + other_row_pressed + + CNT_KEY_STATUS_SEEN_AS_PRESSED(KEY(row, ocol)) + + CNT_KEY_STATUS_SEEN_AS_PRESSED(KEY(orow, ocol)); + if (pressed_in_rectangle == 2) { + if (WAS_KEY_STATUS_GHOSTING_BEFORE(KEY(row, ocol))) { + kscan_deghost_callback_for_child(deghost_dev, row, ocol, true); + KEY(row, ocol) |= + KEY_STATUS_FORCE_NOT_RECHECK; // optimization, next + // WAS_KEY_STATUS_GHOSTING_BEFORE() + // call will be false + } + if (WAS_KEY_STATUS_GHOSTING_BEFORE(KEY(orow, ocol))) { + kscan_deghost_callback_for_child(deghost_dev, orow, ocol, true); + } + if (check_orow_col) { + kscan_deghost_callback_for_child(deghost_dev, orow, col, true); + check_orow_col = 0; // for optimization + } + } + } + } + for (uint32_t ocol = 0; ocol < config->cols; ocol++) { + KEY(row, ocol) &= ~KEY_STATUS_FORCE_NOT_RECHECK; + } + } +} + +static int kscan_deghost_init(const struct device *dev) { return 0; } + +static const struct kscan_driver_api deghost_driver_api = { + .config = kscan_deghost_configure, + .enable_callback = kscan_deghost_enable_callback, + .disable_callback = kscan_deghost_disable_callback, +}; + +#define _TRANSFORM_ENTRY(i, transform_node) \ + [(KT_ROW(DT_PROP_BY_IDX(transform_node, map, i)) * DT_PROP(transform_node, columns)) + \ + KT_COL(DT_PROP_BY_IDX(transform_node, map, i))] = 1 + +#define KSCAN_DEGHOST_INIT(n) \ + static const uint8_t transform_filled_##n[] = { \ + COND_CODE_1(DT_INST_NODE_HAS_PROP(n, transform), \ + (LISTIFY(DT_PROP_LEN(DT_INST_PHANDLE(n, transform), map), _TRANSFORM_ENTRY, \ + (, ), DT_INST_PHANDLE(n, transform))), \ + ())}; \ + static void kscan_deghost_callback_for_child_##n(const struct device *child_dev, uint32_t row, \ + uint32_t column, bool pressed) { \ + const struct device *deghost_dev = DEVICE_DT_GET(DT_DRV_INST(n)); \ + kscan_deghost_callback_for_child(deghost_dev, row, column, pressed); \ + } \ + \ + static uint8_t \ + key_status_##n[DT_INST_PROP_OR( \ + n, rows, \ + DT_PROP_OR(DT_INST_PHANDLE(n, transform), rows, \ + DT_PROP_LEN_OR(DT_INST_PHANDLE(n, kscan), row_gpios, ZMK_MATRIX_ROWS)))] \ + [DT_INST_PROP_OR(n, columns, \ + DT_PROP_OR(DT_INST_PHANDLE(n, transform), columns, \ + DT_PROP_LEN_OR(DT_INST_PHANDLE(n, kscan), \ + col_gpios, ZMK_MATRIX_COLS)))]; \ + \ + static const struct kscan_deghost_config kscan_deghost_config_##n = { \ + .child_kscan = DEVICE_DT_GET(DT_INST_PHANDLE(n, kscan)), \ + .callback_for_child = &kscan_deghost_callback_for_child_##n, \ + .rows = ARRAY_SIZE(key_status_##n), \ + .cols = ARRAY_SIZE(key_status_##n[0]), \ + .transform_filled = transform_filled_##n, \ + .transform_filled_len = ARRAY_SIZE(transform_filled_##n), \ + }; \ + \ + static struct kscan_deghost_data kscan_deghost_data_##n = { \ + .key_status = &key_status_##n[0][0], \ + }; \ + \ + DEVICE_DT_INST_DEFINE(n, kscan_deghost_init, NULL, &kscan_deghost_data_##n, \ + &kscan_deghost_config_##n, APPLICATION, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, °host_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KSCAN_DEGHOST_INIT); diff --git a/app/dts/bindings/zmk,kscan-deghost.yaml b/app/dts/bindings/zmk,kscan-deghost.yaml new file mode 100644 index 00000000..9f1ce594 --- /dev/null +++ b/app/dts/bindings/zmk,kscan-deghost.yaml @@ -0,0 +1,15 @@ +description: | + Allows deghosting a 2-key rollover matrix KSCAN device, resulting int a deghosted virtual device + +compatible: "zmk,kscan-deghost" + +properties: + rows: + type: int + columns: + type: int + kscan: + type: phandle + required: true + transform: + type: phandle diff --git a/docs/docs/config/kscan.md b/docs/docs/config/kscan.md index 296cb4a1..ba60d771 100644 --- a/docs/docs/config/kscan.md +++ b/docs/docs/config/kscan.md @@ -265,6 +265,100 @@ One possible way to do this is a 3x4 matrix where the direct GPIO keys are shift } ``` +## Deghost Driver + +Keyboard scan driver which takes another keyboard scan driver and applies deghosting on it. +Deghosting is needed for keyboard matrices that are not built to be n-key-rollover capable, +for example contact switch matrices that don't have one diode per key. + +In keyboards such as these typically the output pins will be configured GPIO_OPEN_DRAIN, or +GPIO_OPEN_SOURCE, with pull resistors enabled in the opposite direction on the inputs, and +often on the outputs as well. Alternatively external circuitry can also be used to achieve +this if the outputs don't support single-ended output, or sometimes discrete pull-resistors +can be added per input pin. + +On keyboards such as these, if 3 keys are pressed that form a right angle on the keyboard +matrix, then the 4th key will also appear to be incorrectly pressed from the perspective +of the matrix driver, for example: + + + + + + + + + + + + +
...Col 1...Col 2...
..................
Row 1...Pressed...Pressed...
..................
Row 2...Pressed...Ghosting...
..................
+ +In the above example, if the keys were ressed in the following order: + +- (Row1,Col1) +- (Row1,Col2) +- (Row2,Col1) + +then in fact it is impossible to determine if the 3rd key pressed was actually +(Row2,Col1) or (Row2,Col2), because both matrix positions become active at the same time, +and with a simple switch matrix it's not possible to differentiate between those two. + +Note that there are 4 possible rotation of the above example. + +Deghosting works by looking for rectangles in the keyboard matrix on the corners of which +at least 2 keys have already been reported as pressed. If new keypresses come in on additional +corners, then they will be ignored. Since only two keys are guaranteed to be accepted, these +keyboards are often called 2-key rollover keyboards, even though in some cases many more keys +could be pressed, and successfully reported, as long as they don't form right angles on the +matrix. + +It's possible to further optimize this behaviour, if the deghosting driver knows which matrix +positions are guaranteed to be empty, and never make contact. In this case certain right-angle +combinations can also be accepted. This can be done by giving the dehost driver a transform node. + +### Devicetree + +Applies to : `compatible = "zmk,kscan-deghost"` + +Definition file: [zmk/app/dts/bindings/zmk,kscan-deghost.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/dts/bindings/zmk,kscan-deghost.yaml) + +| Property | Type | Description | Default | +| ----------- | ------- | ------------------------------------------- | ----------------------------------------------------------------------------------------- | +| `kscan` | phandle | The kscan driver to perform deghosting on | | +| `transform` | phandle | Optional transform node for optimization | | +| `rows` | int | The number of rows in the deghost matrix | It will try to extract the row count from the transform, or from the referenced kscan. | +| `columnss` | int | The number of columns in the deghost matrix | It will try to extract the column count from the transform, or from the referenced kscan. | + +### Example Configuration + +The deghost driver will typically sit on top of a matrix driver, +like the following Devicetree code: + +```devicetree +/ { + chosen { + zmk,kscan = &kscan0; + zmk,matrix_transform = &default_transform; + }; + + kscan0: kscan_deghost { + compatible = "zmk,kscan-deghost"; + kscan = <&kscan1>; + transform = <&default_transform>; + }; + + kscan1: kscan_matrix { + compatible = "zmk,kscan-gpio-matrix"; + // define matrix here... + }; + + default_transform: keymap_transform_0 { + // define default transform here... + } +} +``` + ## Mock Driver Mock keyboard scan driver that simulates key events.