zephyr/drivers/input/input_kbd_matrix.c
Fabio Baltieri 13a2f42d50 input: kbd_matrix: implement stable poll period support
Implement a new stable-poll-period-ms property to specify a new (slower)
polling rate for when the matrix is stable.

The keyboard thread can eat up a surprisingly high amount of cpu cycles in
busy waiting if the specific hardware implementation happen to have a
particularly slow settle time, but high frequency polling is really only
needed when debouncing.

The new property allow slowing down the polling rate when the matrix is
stable (either key pressed but none to be debounced or idle in the case
of the gpio implementation with no interrupts), this allows reducing the
overall cpu time taken by the keyboard scanning thread when keys are
persistently pressed.

Signed-off-by: Fabio Baltieri <fabiobaltieri@google.com>
2024-11-17 19:06:15 -05:00

421 lines
11 KiB
C

/*
* Copyright 2019 Intel Corporation
* Copyright 2022 Nuvoton Technology Corporation.
* Copyright 2023 Google LLC
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <zephyr/device.h>
#include <zephyr/input/input.h>
#include <zephyr/input/input_kbd_matrix.h>
#include <zephyr/kernel.h>
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <zephyr/pm/device.h>
#include <zephyr/pm/device_runtime.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/util.h>
LOG_MODULE_REGISTER(input_kbd_matrix, CONFIG_INPUT_LOG_LEVEL);
void input_kbd_matrix_poll_start(const struct device *dev)
{
struct input_kbd_matrix_common_data *data = dev->data;
k_sem_give(&data->poll_lock);
}
static bool input_kbd_matrix_ghosting(const struct device *dev)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
const kbd_row_t *state = cfg->matrix_new_state;
/*
* Matrix keyboard designs are suceptible to ghosting.
* An extra key appears to be pressed when 3 keys belonging to the same
* block are pressed. For example, in the following block:
*
* . . w . q .
* . . . . . .
* . . . . . .
* . . m . a .
*
* the key m would look as pressed if the user pressed keys w, q and a
* simultaneously. A block can also be formed, with not adjacent
* columns.
*/
for (int c = 0; c < cfg->col_size; c++) {
if (!state[c]) {
continue;
}
for (int c_next = c + 1; c_next < cfg->col_size; c_next++) {
/*
* We AND the columns to detect a "block". This is an
* indication of ghosting, due to current flowing from
* a key which was never pressed. In our case, current
* flowing is a bit set to 1 as we flipped the bits
* when the matrix was scanned. Now we OR the colums
* using z&(z-1) which is non-zero only if z has more
* than one bit set.
*/
kbd_row_t common_row_bits = state[c] & state[c_next];
if (common_row_bits & (common_row_bits - 1)) {
return true;
}
}
}
return false;
}
static void input_kbd_matrix_drive_column(const struct device *dev, int col)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
const struct input_kbd_matrix_api *api = cfg->api;
api->drive_column(dev, col);
#ifdef CONFIG_INPUT_KBD_DRIVE_COLUMN_HOOK
input_kbd_matrix_drive_column_hook(dev, col);
#endif
}
static bool input_kbd_matrix_is_suspended(const struct device *dev)
{
#ifdef CONFIG_PM_DEVICE
struct input_kbd_matrix_common_data *data = dev->data;
return atomic_get(&data->suspended) == 1;
#else
return false;
#endif
}
static bool input_kbd_matrix_scan(const struct device *dev)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
const struct input_kbd_matrix_api *api = cfg->api;
kbd_row_t row;
kbd_row_t key_event = 0U;
for (int col = 0; col < cfg->col_size; col++) {
if (cfg->actual_key_mask != NULL &&
cfg->actual_key_mask[col] == 0) {
continue;
}
if (input_kbd_matrix_is_suspended(dev)) {
cfg->matrix_new_state[col] = 0;
continue;
};
input_kbd_matrix_drive_column(dev, col);
/* Allow the matrix to stabilize before reading it */
k_busy_wait(cfg->settle_time_us);
row = api->read_row(dev);
if (cfg->actual_key_mask != NULL) {
row &= cfg->actual_key_mask[col];
}
cfg->matrix_new_state[col] = row;
key_event |= row;
}
input_kbd_matrix_drive_column(dev, INPUT_KBD_MATRIX_COLUMN_DRIVE_NONE);
return key_event != 0U;
}
static void input_kbd_matrix_update_state(const struct device *dev)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
struct input_kbd_matrix_common_data *data = dev->data;
kbd_row_t *matrix_new_state = cfg->matrix_new_state;
uint32_t cycles_now;
kbd_row_t row_changed;
kbd_row_t deb_col;
cycles_now = k_cycle_get_32();
data->scan_clk_cycle[data->scan_cycles_idx] = cycles_now;
/*
* The intent of this loop is to gather information related to key
* changes.
*/
for (int c = 0; c < cfg->col_size; c++) {
/* Check if there was an update from the previous scan */
row_changed = matrix_new_state[c] ^ cfg->matrix_previous_state[c];
if (!row_changed) {
continue;
}
for (int r = 0; r < cfg->row_size; r++) {
uint8_t cyc_idx = c * cfg->row_size + r;
/*
* Index all they keys that changed for each row in
* order to debounce each key in terms of it
*/
if (row_changed & BIT(r)) {
cfg->scan_cycle_idx[cyc_idx] = data->scan_cycles_idx;
}
}
cfg->matrix_unstable_state[c] |= row_changed;
cfg->matrix_previous_state[c] = matrix_new_state[c];
}
for (int c = 0; c < cfg->col_size; c++) {
deb_col = cfg->matrix_unstable_state[c];
if (!deb_col) {
continue;
}
/* Debouncing for each row key occurs here */
for (int r = 0; r < cfg->row_size; r++) {
kbd_row_t mask = BIT(r);
kbd_row_t row_bit = matrix_new_state[c] & mask;
/* Continue if we already debounce a key */
if (!(deb_col & mask)) {
continue;
}
uint8_t cyc_idx = c * cfg->row_size + r;
uint8_t scan_cyc_idx = cfg->scan_cycle_idx[cyc_idx];
uint32_t scan_clk_cycle = data->scan_clk_cycle[scan_cyc_idx];
/* Convert the clock cycle differences to usec */
uint32_t deb_t_us = k_cyc_to_us_floor32(cycles_now - scan_clk_cycle);
/* Does the key requires more time to be debounced? */
if (deb_t_us < (row_bit ? cfg->debounce_down_us : cfg->debounce_up_us)) {
/* Need more time to debounce */
continue;
}
cfg->matrix_unstable_state[c] &= ~mask;
/* Check if there was a change in the stable state */
if ((cfg->matrix_stable_state[c] & mask) == row_bit) {
/* Key state did not change */
continue;
}
/*
* The current row has been debounced, therefore update
* the stable state. Then, proceed to notify the
* application about the keys pressed.
*/
cfg->matrix_stable_state[c] ^= mask;
input_report_abs(dev, INPUT_ABS_X, c, false, K_FOREVER);
input_report_abs(dev, INPUT_ABS_Y, r, false, K_FOREVER);
input_report_key(dev, INPUT_BTN_TOUCH, row_bit, true, K_FOREVER);
}
}
data->scan_cycles_idx = (data->scan_cycles_idx + 1) % INPUT_KBD_MATRIX_SCAN_OCURRENCES;
}
static bool input_kbd_matrix_check_key_events(const struct device *dev)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
bool key_pressed;
/* Scan the matrix */
key_pressed = input_kbd_matrix_scan(dev);
for (int c = 0; c < cfg->col_size; c++) {
LOG_DBG("c=%2d u=%" PRIkbdrow " p=%" PRIkbdrow " n=%" PRIkbdrow,
c,
cfg->matrix_unstable_state[c],
cfg->matrix_previous_state[c],
cfg->matrix_new_state[c]);
}
/* Abort if ghosting is detected */
if (cfg->ghostkey_check && input_kbd_matrix_ghosting(dev)) {
return key_pressed;
}
input_kbd_matrix_update_state(dev);
return key_pressed;
}
static k_timepoint_t input_kbd_matrix_poll_timeout(const struct device *dev)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
if (cfg->poll_timeout_ms == 0) {
return sys_timepoint_calc(K_FOREVER);
}
return sys_timepoint_calc(K_MSEC(cfg->poll_timeout_ms));
}
static bool input_kbd_matrix_is_unstable(const struct device *dev)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
for (uint8_t c = 0; c < cfg->col_size; c++) {
if (cfg->matrix_unstable_state[c] != 0) {
return true;
}
}
return false;
}
static void input_kbd_matrix_poll(const struct device *dev)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
k_timepoint_t poll_time_end;
uint32_t current_cycles;
uint32_t cycles_diff;
uint32_t wait_period_us;
uint32_t poll_period_us;
poll_time_end = input_kbd_matrix_poll_timeout(dev);
while (true) {
uint32_t start_period_cycles = k_cycle_get_32();
if (input_kbd_matrix_check_key_events(dev)) {
poll_time_end = input_kbd_matrix_poll_timeout(dev);
} else if (sys_timepoint_expired(poll_time_end)) {
break;
}
/*
* Subtract the time invested from the sleep period in order to
* compensate for the time invested in debouncing a key
*/
current_cycles = k_cycle_get_32();
cycles_diff = current_cycles - start_period_cycles;
if (input_kbd_matrix_is_unstable(dev)) {
poll_period_us = cfg->poll_period_us;
} else {
poll_period_us = cfg->stable_poll_period_us;
}
wait_period_us = CLAMP(poll_period_us - k_cyc_to_us_floor32(cycles_diff),
USEC_PER_MSEC, poll_period_us);
LOG_DBG("wait_period_us: %d", wait_period_us);
/* Allow other threads to run while we sleep */
k_usleep(wait_period_us);
}
}
static void input_kbd_matrix_polling_thread(void *arg1, void *unused2, void *unused3)
{
const struct device *dev = arg1;
const struct input_kbd_matrix_common_config *cfg = dev->config;
const struct input_kbd_matrix_api *api = cfg->api;
struct input_kbd_matrix_common_data *data = dev->data;
ARG_UNUSED(unused2);
ARG_UNUSED(unused3);
while (true) {
if (!input_kbd_matrix_is_suspended(dev)) {
input_kbd_matrix_drive_column(dev, INPUT_KBD_MATRIX_COLUMN_DRIVE_ALL);
api->set_detect_mode(dev, true);
/* Check the rows again after enabling the interrupt to catch
* any potential press since the last read.
*/
if (api->read_row(dev) != 0) {
input_kbd_matrix_poll_start(dev);
}
}
k_sem_take(&data->poll_lock, K_FOREVER);
LOG_DBG("scan start");
/* Disable interrupt of KSI pins and start polling */
api->set_detect_mode(dev, false);
input_kbd_matrix_poll(dev);
}
}
#ifdef CONFIG_PM_DEVICE
int input_kbd_matrix_pm_action(const struct device *dev,
enum pm_device_action action)
{
struct input_kbd_matrix_common_data *data = dev->data;
switch (action) {
case PM_DEVICE_ACTION_SUSPEND:
atomic_set(&data->suspended, 1);
break;
case PM_DEVICE_ACTION_RESUME:
atomic_set(&data->suspended, 0);
break;
default:
return -ENOTSUP;
}
input_kbd_matrix_poll_start(dev);
return 0;
}
#endif
int input_kbd_matrix_common_init(const struct device *dev)
{
struct input_kbd_matrix_common_data *data = dev->data;
int ret;
k_sem_init(&data->poll_lock, 0, 1);
k_thread_create(&data->thread, data->thread_stack,
K_KERNEL_STACK_SIZEOF(data->thread_stack),
input_kbd_matrix_polling_thread, (void *)dev, NULL, NULL,
CONFIG_INPUT_KBD_MATRIX_THREAD_PRIORITY, 0, K_NO_WAIT);
k_thread_name_set(&data->thread, dev->name);
ret = pm_device_runtime_enable(dev);
if (ret < 0) {
LOG_ERR("Failed to enable runtime power management");
return ret;
}
return 0;
}
#if CONFIG_INPUT_KBD_ACTUAL_KEY_MASK_DYNAMIC
int input_kbd_matrix_actual_key_mask_set(const struct device *dev,
uint8_t row, uint8_t col, bool enabled)
{
const struct input_kbd_matrix_common_config *cfg = dev->config;
if (row >= cfg->row_size || col >= cfg->col_size) {
return -EINVAL;
}
if (cfg->actual_key_mask == NULL) {
LOG_WRN("actual-key-mask not defined for %s", dev->name);
return -EINVAL;
}
WRITE_BIT(cfg->actual_key_mask[col], row, enabled);
return 0;
}
#endif