403 lines
10 KiB
C
403 lines
10 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 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;
|
|
|
|
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;
|
|
wait_period_us = cfg->poll_period_us - k_cyc_to_us_floor32(cycles_diff);
|
|
|
|
wait_period_us = CLAMP(wait_period_us,
|
|
USEC_PER_MSEC, cfg->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
|