Implement deghost kscan driver

This commit is contained in:
Purdea Andrei 2023-01-31 07:54:53 +02:00
parent ace11e327f
commit 9486e96bda
5 changed files with 302 additions and 0 deletions

View file

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

View file

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

View file

@ -0,0 +1,187 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_kscan_deghost
#include <zephyr/device.h>
#include <zephyr/drivers/kscan.h>
#include <zephyr/logging/log.h>
#include <dt-bindings/zmk/matrix_transform.h>
#include <zmk/matrix.h>
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, &deghost_driver_api);
DT_INST_FOREACH_STATUS_OKAY(KSCAN_DEGHOST_INIT);

View file

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

View file

@ -236,6 +236,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:
<table>
<thead>
<tr><th></th><th>...</th><th>Col 1</th><th>...</th><th>Col 2</th><th>...</th></tr>
</thead>
<tbody>
<tr><th>...</th><td>...</td><td>...</td><td>...</td><td>...</td><td>...</td></tr>
<tr><th>Row 1</th><td>...</td><td>Pressed</td><td>...</td><td>Pressed</td><td>...</td></tr>
<tr><th>...</th><td>...</td><td>...</td><td>...</td><td>...</td><td>...</td></tr>
<tr><th>Row 2</th><td>...</td><td>Pressed</td><td>...</td><td><b>Ghosting</b></td><td>...</td></tr>
<tr><th>...</th><td>...</td><td>...</td><td>...</td><td>...</td><td>...</td></tr>
</tbody>
</table>
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.