feat(kscan): Add charlieplex keyscan driver

* Supports matrixes with and without additional interrupt pin use.

Co-authored-by: Peter Johanson <peter@peterjohanson.com>
This commit is contained in:
Hooky 2023-12-10 06:10:05 +08:00 committed by GitHub
parent b35a5e83c0
commit 2c50cff891
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 563 additions and 7 deletions

View File

@ -1,10 +1,11 @@
# Copyright (c) 2020 The ZMK Contributors
# Copyright (c) 2020-2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
zephyr_library_amend()
zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_DRIVER kscan_gpio.c)
zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_MATRIX kscan_gpio_matrix.c)
zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_CHARLIEPLEX kscan_gpio_charlieplex.c)
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)

View File

@ -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_CHARLIEPLEX := zmk,kscan-gpio-charlieplex
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_CHARLIEPLEX
bool
default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_GPIO_CHARLIEPLEX))
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_CHARLIEPLEX
config ZMK_KSCAN_CHARLIEPLEX_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_CHARLIEPLEX_WAIT_BETWEEN_OUTPUTS
int "Ticks to wait between each output when scanning charlieplex 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_CHARLIEPLEX
config ZMK_KSCAN_MOCK_DRIVER
bool
default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_MOCK))

View File

@ -0,0 +1,420 @@
/*
* Copyright (c) 2020-2023 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zmk/debounce.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/kscan.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/__assert.h>
#include <zephyr/sys/util.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#define DT_DRV_COMPAT zmk_kscan_gpio_charlieplex
#define INST_LEN(n) DT_INST_PROP_LEN(n, gpios)
#define INST_CHARLIEPLEX_LEN(n) (INST_LEN(n) * INST_LEN(n))
#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 KSCAN_GPIO_CFG_INIT(idx, inst_idx) \
GPIO_DT_SPEC_GET_BY_IDX(DT_DRV_INST(inst_idx), gpios, idx)
#define INST_INTR_DEFINED(n) DT_INST_NODE_HAS_PROP(n, interrupt_gpios)
#define WITH_INTR(n) COND_CODE_1(INST_INTR_DEFINED(n), (+1), (+0))
#define WITHOUT_INTR(n) COND_CODE_0(INST_INTR_DEFINED(n), (+1), (+0))
#define USES_POLLING DT_INST_FOREACH_STATUS_OKAY(WITHOUT_INTR) > 0
#define USES_INTERRUPT DT_INST_FOREACH_STATUS_OKAY(WITH_INTR) > 0
#if USES_POLLING && USES_INTERRUPT
#define USES_POLL_AND_INTR 1
#else
#define USES_POLL_AND_INTR 0
#endif
#define COND_ANY_POLLING(code) COND_CODE_1(USES_POLLING, code, ())
#define COND_POLL_AND_INTR(code) COND_CODE_1(USES_POLL_AND_INTR, code, ())
#define COND_THIS_INTERRUPT(n, code) COND_CODE_1(INST_INTR_DEFINED(n), code, ())
#define KSCAN_INTR_CFG_INIT(inst_idx) GPIO_DT_SPEC_GET(DT_DRV_INST(inst_idx), interrupt_gpios)
struct kscan_charlieplex_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. */
struct gpio_callback irq_callback;
/**
* Current state of the matrix as a flattened 2D array of length
* (config->cells.length ^2)
*/
struct zmk_debounce_state *charlieplex_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_charlieplex_config {
struct kscan_gpio_list cells;
struct zmk_debounce_config debounce_config;
int32_t debounce_scan_period_ms;
int32_t poll_period_ms;
bool use_interrupt;
const struct gpio_dt_spec interrupt;
};
/**
* 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_charlieplex_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_charlieplex_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;
}
gpio_flags_t pull_flag =
((gpio->dt_flags & GPIO_ACTIVE_LOW) == GPIO_ACTIVE_LOW) ? GPIO_PULL_UP : GPIO_PULL_DOWN;
int err = gpio_pin_configure_dt(gpio, GPIO_INPUT | pull_flag);
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_charlieplex_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;
}
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_charlieplex_set_all_as_input(const struct device *dev) {
const struct kscan_charlieplex_config *config = dev->config;
int err = 0;
for (int i = 0; i < config->cells.len; i++) {
err = kscan_charlieplex_set_as_input(&config->cells.gpios[i]);
if (err) {
return err;
}
}
return 0;
}
static int kscan_charlieplex_set_all_outputs(const struct device *dev, const int value) {
const struct kscan_charlieplex_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;
}
static int kscan_charlieplex_interrupt_configure(const struct device *dev,
const gpio_flags_t flags) {
const struct kscan_charlieplex_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;
}
static int kscan_charlieplex_interrupt_enable(const struct device *dev) {
int err = kscan_charlieplex_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_charlieplex_set_all_outputs(dev, 1);
}
static void kscan_charlieplex_irq_callback(const struct device *port, struct gpio_callback *cb,
const gpio_port_pins_t _pin) {
struct kscan_charlieplex_data *data =
CONTAINER_OF(cb, struct kscan_charlieplex_data, irq_callback);
// Disable our interrupt to avoid re-entry while we scan.
kscan_charlieplex_interrupt_configure(data->dev, GPIO_INT_DISABLE);
data->scan_time = k_uptime_get();
k_work_reschedule(&data->work, K_NO_WAIT);
}
static void kscan_charlieplex_read_continue(const struct device *dev) {
const struct kscan_charlieplex_config *config = dev->config;
struct kscan_charlieplex_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_charlieplex_read_end(const struct device *dev) {
struct kscan_charlieplex_data *data = dev->data;
const struct kscan_charlieplex_config *config = dev->config;
if (config->use_interrupt) {
// Return to waiting for an interrupt.
kscan_charlieplex_interrupt_enable(dev);
} else {
data->scan_time += config->poll_period_ms;
// Return to polling slowly.
k_work_reschedule(&data->work, K_TIMEOUT_ABS_MS(data->scan_time));
}
}
static int kscan_charlieplex_read(const struct device *dev) {
struct kscan_charlieplex_data *data = dev->data;
const struct kscan_charlieplex_config *config = dev->config;
bool continue_scan = false;
// NOTE: RR 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_charlieplex_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_charlieplex_set_as_output(out_gpio);
if (err) {
return err;
}
#if CONFIG_ZMK_KSCAN_CHARLIEPLEX_WAIT_BEFORE_INPUTS > 0
k_busy_wait(CONFIG_ZMK_KSCAN_CHARLIEPLEX_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 zmk_debounce_state *state = &data->charlieplex_state[index];
zmk_debounce_update(state, gpio_pin_get_dt(in_gpio), config->debounce_scan_period_ms,
&config->debounce_config);
// NOTE: RR vs MATRIX: because we don't need an input/output => row/column
// setup, we can update in the same loop.
if (zmk_debounce_get_changed(state)) {
const bool pressed = zmk_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 || zmk_debounce_is_active(state);
}
err = kscan_charlieplex_set_as_input(out_gpio);
if (err) {
return err;
}
#if CONFIG_ZMK_KSCAN_CHARLIEPLEX_WAIT_BETWEEN_OUTPUTS > 0
k_busy_wait(CONFIG_ZMK_KSCAN_CHARLIEPLEX_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_charlieplex_read_continue(dev);
} else {
// All keys are released. Return to normal.
kscan_charlieplex_read_end(dev);
}
return 0;
}
static void kscan_charlieplex_work_handler(struct k_work *work) {
struct k_work_delayable *dwork = CONTAINER_OF(work, struct k_work_delayable, work);
struct kscan_charlieplex_data *data = CONTAINER_OF(dwork, struct kscan_charlieplex_data, work);
kscan_charlieplex_read(data->dev);
}
static int kscan_charlieplex_configure(const struct device *dev, const kscan_callback_t callback) {
if (!callback) {
return -EINVAL;
}
struct kscan_charlieplex_data *data = dev->data;
data->callback = callback;
return 0;
}
static int kscan_charlieplex_enable(const struct device *dev) {
struct kscan_charlieplex_data *data = dev->data;
data->scan_time = k_uptime_get();
// Read will automatically start interrupts/polling once done.
return kscan_charlieplex_read(dev);
}
static int kscan_charlieplex_disable(const struct device *dev) {
struct kscan_charlieplex_data *data = dev->data;
k_work_cancel_delayable(&data->work);
const struct kscan_charlieplex_config *config = dev->config;
if (config->use_interrupt) {
return kscan_charlieplex_interrupt_configure(dev, GPIO_INT_DISABLE);
}
return 0;
}
static int kscan_charlieplex_init_inputs(const struct device *dev) {
const struct kscan_charlieplex_config *config = dev->config;
for (int i = 0; i < config->cells.len; i++) {
int err = kscan_charlieplex_set_as_input(&config->cells.gpios[i]);
if (err) {
return err;
}
}
return 0;
}
static int kscan_charlieplex_init_interrupt(const struct device *dev) {
struct kscan_charlieplex_data *data = dev->data;
const struct kscan_charlieplex_config *config = dev->config;
const struct gpio_dt_spec *gpio = &config->interrupt;
int err = kscan_charlieplex_set_as_input(gpio);
if (err) {
return err;
}
gpio_init_callback(&data->irq_callback, kscan_charlieplex_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;
}
static int kscan_charlieplex_init(const struct device *dev) {
struct kscan_charlieplex_data *data = dev->data;
data->dev = dev;
kscan_charlieplex_init_inputs(dev);
kscan_charlieplex_set_all_outputs(dev, 0);
const struct kscan_charlieplex_config *config = dev->config;
if (config->use_interrupt) {
kscan_charlieplex_init_interrupt(dev);
}
k_work_init_delayable(&data->work, kscan_charlieplex_work_handler);
return 0;
}
static const struct kscan_driver_api kscan_charlieplex_api = {
.config = kscan_charlieplex_configure,
.enable_callback = kscan_charlieplex_enable,
.disable_callback = kscan_charlieplex_disable,
};
#define KSCAN_CHARLIEPLEX_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 zmk_debounce_state kscan_charlieplex_state_##n[INST_CHARLIEPLEX_LEN(n)]; \
static const struct gpio_dt_spec kscan_charlieplex_cells_##n[] = { \
LISTIFY(INST_LEN(n), KSCAN_GPIO_CFG_INIT, (, ), n)}; \
static struct kscan_charlieplex_data kscan_charlieplex_data_##n = { \
.charlieplex_state = kscan_charlieplex_state_##n, \
}; \
\
static struct kscan_charlieplex_config kscan_charlieplex_config_##n = { \
.cells = KSCAN_GPIO_LIST(kscan_charlieplex_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), \
COND_ANY_POLLING((.poll_period_ms = DT_INST_PROP(n, poll_period_ms), )) \
COND_POLL_AND_INTR((.use_interrupt = INST_INTR_DEFINED(n), )) \
COND_THIS_INTERRUPT(n, (.interrupt = KSCAN_INTR_CFG_INIT(n), ))}; \
\
DEVICE_DT_INST_DEFINE(n, &kscan_charlieplex_init, NULL, &kscan_charlieplex_data_##n, \
&kscan_charlieplex_config_##n, APPLICATION, \
CONFIG_APPLICATION_INIT_PRIORITY, &kscan_charlieplex_api);
DT_INST_FOREACH_STATUS_OKAY(KSCAN_CHARLIEPLEX_INIT);

View File

@ -0,0 +1,31 @@
# Copyright (c) 2023 The ZMK Contributors
# SPDX-License-Identifier: MIT
description: GPIO keyboard charlieplex matrix controller
compatible: "zmk,kscan-gpio-charlieplex"
include: kscan.yaml
properties:
gpios:
type: phandle-array
required: true
interrupt-gpios:
type: phandle-array
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

View File

@ -149,6 +149,39 @@ The output pins (e.g. columns for `col2row`) should have the flag `GPIO_ACTIVE_H
};
```
## Charlieplex Driver
Keyboard scan driver where keys are arranged on a matrix with each GPIO used as both input and output.
- With `interrupt-gpios` unset, this allows n pins to drive n\*(n-1) keys.
- With `interrupt-gpios` set, n pins will drive (n-1)\*(n-2) keys, but provide much improved power handling.
Definition file: [zmk/app/module/drivers/kscan/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/module/drivers/kscan/Kconfig)
| Config | Type | Description | Default |
| --------------------------------------------------- | ----------- | ------------------------------------------------------------------------- | ------- |
| `CONFIG_ZMK_KSCAN_CHARLIEPLEX_WAIT_BEFORE_INPUTS` | int (ticks) | How long to wait before reading input pins after setting output active | 0 |
| `CONFIG_ZMK_KSCAN_CHARLIEPLEX_WAIT_BETWEEN_OUTPUTS` | int (ticks) | How long to wait between each output to allow previous output to "settle" | 0 |
### Devicetree
Applies to: `compatible = "zmk,kscan-gpio-charlieplex"`
Definition file: [zmk/app/module/dts/bindings/kscan/zmk,kscan-gpio-charlieplex.yaml](https://github.com/zmkfirmware/zmk/blob/main/app/module/dts/bindings/kscan/zmk%2Ckscan-gpio-charlieplex.yaml)
| Property | Type | Description | Default |
| ------------------------- | ---------- | ------------------------------------------------------------------------------------------- | ------- |
| `gpios` | GPIO array | GPIOs used, listed in order. | |
| `interrupt-gpios` | GPIO array | A single GPIO to use for interrupt. Leaving this empty will enable continuous polling. | |
| `debounce-press-ms` | int | Debounce time for key press in milliseconds. Use 0 for eager debouncing. | 5 |
| `debounce-release-ms` | int | Debounce time for key release in milliseconds. | 5 |
| `debounce-scan-period-ms` | int | Time between reads in milliseconds when any key is pressed. | 1 |
| `poll-period-ms` | int | Time between reads in milliseconds when no key is pressed and `interrupt-gpois` is not set. | 10 |
Define the transform with a [matrix transform](#matrix-transform). The row is always the driven pin, and the column always the receiving pin (input to the controller).
For example, in `RC(5,0)` power flows from the 6th pin in `gpios` to the 1st pin in `gpios`.
Exclude all positions where the row and column are the same as these pairs will never be triggered, since no pin can be both input and output at the same time.
## Composite Driver
Keyboard scan driver which combines multiple other keyboard scan drivers.
@ -397,9 +430,50 @@ Consider a keyboard with a [duplex matrix](https://wiki.ai03.com/books/pcb-desig
RC(2,0) RC(3,0) RC(2,1) RC(3,1) // ...
RC(4,0) RC(5,0) RC(4,1) RC(5,1) // ...
RC(6,0) RC(7,0) RC(6,1) RC(7,1) // ...
RC(8,0) RC(8,1) RC(9,1) // ...
RC(8,0) RC(9,0) RC(8,1) RC(9,1) // ...
RC(10,0) RC(11,0) // ...
>;
};
};
```
### Example: Charlieplex
Since a charlieplex driver will never align with a keyboard directly due to the un-addressable positions, a matrix transform should be used to map the pairs to the layout of the keys.
Note that the entire addressable space does not need to be mapped.
```devicetree
/ {
chosen {
zmk,kscan = &kscan0;
zmk,matrix_transform = &default_transform;
};
kscan0: kscan {
compatible = "zmk,kscan-gpio-charlieplex";
interrupt-gpios = <&pro_micro 21 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN) >;
gpios
= <&pro_micro 16 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN) >
, <&pro_micro 17 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN) >
, <&pro_micro 18 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN) >
, <&pro_micro 19 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN) >
, <&pro_micro 20 (GPIO_ACTIVE_HIGH | GPIO_PULL_DOWN) >
; // addressable space is 5x5, (minus paired values)
};
default_transform: matrix_transform {
compatible = "zmk,matrix-transform";
rows = <3>;
columns = <5>;
// Q W E R
// A S D F
// Z X C V
map = <
RC(0,1) RC(0,2) RC(0,3) RC(0,4)
RC(1,0) RC(1,2) RC(1,3) RC(1,4)
RC(2,0) RC(2,1) RC(2,3) RC(2,4)
>;
};
};
```