/* * Copyright (c) 2019 Intel Corporation * Copyright (c) 2022 Nuvoton Technology Corporation. * * SPDX-License-Identifier: Apache-2.0 */ #define DT_DRV_COMPAT nuvoton_npcx_kscan #include "soc_miwu.h" #include #include #include #include #include #include #define LOG_LEVEL CONFIG_KSCAN_LOG_LEVEL LOG_MODULE_REGISTER(kscan_npcx); #define KEYBOARD_COLUMN_DRIVE_ALL -2 #define KEYBOARD_COLUMN_DRIVE_NONE -1 /* Number of tracked scan times */ #define SCAN_OCURRENCES 30U #define KSCAN_ROW_SIZE DT_INST_PROP(0, row_size) #define KSCAN_COL_SIZE DT_INST_PROP(0, col_size) #define HAS_GHOSTING_ENABLED !DT_INST_PROP(0, no_ghostkey_check) /* Driver config */ struct kscan_npcx_config { /* keyboard scan controller base address */ struct kbs_reg *base; /* clock configuration */ struct npcx_clk_cfg clk_cfg; /* pinmux configuration */ const struct pinctrl_dev_config *pcfg; /* Keyboard scan input (KSI) wake-up irq */ int irq; /* Size of keyboard inputs-wui mapping array */ int wui_size; uint8_t row_size; uint8_t col_size; uint32_t deb_time_press; uint32_t deb_time_rel; /* Mapping table between keyboard inputs and wui */ struct npcx_wui wui_maps[]; }; struct kscan_npcx_data { /* variables in usec units */ int64_t poll_timeout; uint32_t poll_period; uint8_t matrix_stable_state[KSCAN_COL_SIZE]; uint8_t matrix_unstable_state[KSCAN_COL_SIZE]; uint8_t matrix_previous_state[KSCAN_COL_SIZE]; uint8_t matrix_new_state[KSCAN_COL_SIZE]; /* Index in to the scan_clock_cycle to indicate start of debouncing */ uint8_t scan_cycle_idx[KSCAN_COL_SIZE * KSCAN_ROW_SIZE]; struct miwu_dev_callback ksi_callback[KSCAN_ROW_SIZE]; /* Track previous "elapsed clock cycles" per matrix scan. This * is used to calculate the debouncing time for every key */ uint8_t scan_clk_cycle[SCAN_OCURRENCES]; struct k_sem poll_lock; uint8_t scan_cycles_idx; kscan_callback_t callback; struct k_thread thread; atomic_t enable_scan; K_KERNEL_STACK_MEMBER(thread_stack, CONFIG_KSCAN_NPCX_THREAD_STACK_SIZE); }; /* Keyboard Scan local functions */ static void kscan_npcx_ksi_isr(const struct device *dev, struct npcx_wui *wui) { ARG_UNUSED(wui); struct kscan_npcx_data *const data = dev->data; k_sem_give(&data->poll_lock); } static int kscan_npcx_resume_detection(const struct device *dev, bool resume) { const struct kscan_npcx_config *const config = dev->config; if (resume) { irq_enable(config->irq); } else { irq_disable(config->irq); } return 0; } static int kscan_npcx_drive_column(const struct device *dev, int col) { const struct kscan_npcx_config *config = dev->config; struct kbs_reg *const inst = config->base; uint32_t mask; if (col >= config->col_size) { return -EINVAL; } if (col == KEYBOARD_COLUMN_DRIVE_NONE) { /* Drive all lines to high. ie. Key detection is disabled. */ mask = ~0; } else if (col == KEYBOARD_COLUMN_DRIVE_ALL) { /* Drive all lines to low for detection any key press */ mask = ~(BIT(config->col_size) - 1); } else { /* * Drive one line to low for determining which key's state * changed. */ mask = ~BIT(col); } LOG_DBG("Drive col mask:%x", mask); /* Set KBSOUT */ inst->KBSOUT0 = (mask & 0xFFFF); inst->KBSOUT1 = ((mask >> 16) & 0x03); return 0; } static int kscan_npcx_read_row(const struct device *dev) { const struct kscan_npcx_config *config = dev->config; struct kbs_reg *const inst = config->base; int val; val = inst->KBSIN; /* 1 means key pressed, otherwise means key released. */ val = (~val & (BIT(config->row_size) - 1)); return val; } static bool is_matrix_ghosting(const struct device *dev, const uint8_t *state) { const struct kscan_npcx_config *const config = dev->config; /* * 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 < config->col_size; c++) { if (!state[c]) { continue; } for (int c_next = c + 1; c_next < config->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. */ uint8_t common_row_bits = state[c] & state[c_next]; if (common_row_bits & (common_row_bits - 1)) { return true; } } } return false; } static bool read_keyboard_matrix(const struct device *dev, uint8_t *new_state) { const struct kscan_npcx_config *const config = dev->config; int row; uint8_t key_event = 0U; for (int col = 0; col < config->col_size; col++) { kscan_npcx_drive_column(dev, col); /* Allow the matrix to stabilize before reading it */ k_busy_wait(CONFIG_KSCAN_NPCX_POLL_COL_OUTPUT_SETTLE_TIME_US); row = kscan_npcx_read_row(dev); new_state[col] = row & 0xFF; key_event |= row; } kscan_npcx_drive_column(dev, KEYBOARD_COLUMN_DRIVE_NONE); return key_event != 0U; } static void update_matrix_state(const struct device *dev, uint8_t *matrix_new_state) { const struct kscan_npcx_config *const config = dev->config; struct kscan_npcx_data *const data = dev->data; uint32_t cycles_now = k_cycle_get_32(); uint8_t row_changed = 0U; uint8_t deb_col; 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 < config->col_size; c++) { /* Check if there was an update from the previous scan */ row_changed = matrix_new_state[c] ^ data->matrix_previous_state[c]; if (!row_changed) { continue; } for (int r = 0; r < config->row_size; r++) { uint8_t cyc_idx = c * config->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)) { data->scan_cycle_idx[cyc_idx] = data->scan_cycles_idx; } } data->matrix_unstable_state[c] |= row_changed; data->matrix_previous_state[c] = matrix_new_state[c]; } for (int c = 0; c < config->col_size; c++) { deb_col = data->matrix_unstable_state[c]; if (!deb_col) { continue; } /* Debouncing for each row key occurs here */ for (int r = 0; r < config->row_size; r++) { uint8_t mask = BIT(r); uint8_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 * config->row_size + r; /* Convert the clock cycle differences to usec */ uint32_t debt = k_cyc_to_us_floor32( cycles_now - data->scan_clk_cycle[data->scan_cycle_idx[cyc_idx]]); /* Does the key requires more time to be debounced? */ if (debt < (row_bit ? config->deb_time_press : config->deb_time_rel)) { /* Need more time to debounce */ continue; } data->matrix_unstable_state[c] &= ~row_bit; /* Check if there was a change in the stable state */ if ((data->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. */ data->matrix_stable_state[c] ^= mask; if (data->callback) { data->callback(dev, r, c, row_bit ? true : false); } } } } static bool check_key_events(const struct device *dev) { const struct kscan_npcx_config *const config = dev->config; struct kscan_npcx_data *const data = dev->data; uint8_t *matrix_new_state = data->matrix_new_state; bool key_pressed = false; if (++data->scan_cycles_idx >= SCAN_OCURRENCES) { data->scan_cycles_idx = 0U; } /* Scan the matrix */ key_pressed = read_keyboard_matrix(dev, matrix_new_state); for (int c = 0; c < config->col_size; c++) { LOG_DBG("U%x, P%x, N%x", data->matrix_unstable_state[c], data->matrix_previous_state[c], matrix_new_state[c]); } /* Abort if ghosting is detected */ if (HAS_GHOSTING_ENABLED && is_matrix_ghosting(dev, matrix_new_state)) { return key_pressed; } update_matrix_state(dev, matrix_new_state); return key_pressed; } static void ksan_matrix_poll(const struct device *dev) { struct kscan_npcx_data *const data = dev->data; uint64_t poll_time_end = sys_clock_timeout_end_calc(K_USEC(data->poll_timeout)); uint32_t current_cycles; uint32_t cycles_diff; uint32_t wait_period; while (atomic_get(&data->enable_scan) == 1U) { uint32_t start_period_cycles = k_cycle_get_32(); if (check_key_events(dev)) { poll_time_end = sys_clock_timeout_end_calc(K_USEC(data->poll_timeout)); } else if (start_period_cycles > 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 = data->poll_period - k_cyc_to_us_floor32(cycles_diff); /* Override wait_period in case it is less than 1 ms */ if (wait_period < USEC_PER_MSEC) { wait_period = USEC_PER_MSEC; } /* * wait period results in a larger number when current cycles * counter wrap. In this case, the whole poll period is used */ if (wait_period > data->poll_period) { LOG_DBG("wait_period : %u", wait_period); wait_period = data->poll_period; } /* Allow other threads to run while we sleep */ k_usleep(wait_period); } } static void kscan_matrix_polling_thread(const struct device *dev, void *dummy2, void *dummy3) { struct kscan_npcx_data *const data = dev->data; ARG_UNUSED(dummy2); ARG_UNUSED(dummy3); while (true) { /* Enable interrupt of KSI pins */ kscan_npcx_resume_detection(dev, true); kscan_npcx_drive_column(dev, KEYBOARD_COLUMN_DRIVE_ALL); k_sem_take(&data->poll_lock, K_FOREVER); LOG_DBG("Start KB scan!!"); /* Disable interrupt of KSI pins and start polling */ kscan_npcx_resume_detection(dev, false); ksan_matrix_poll(dev); } } static void kscan_npcx_init_ksi_wui_callback(const struct device *dev, struct miwu_dev_callback *callback, const struct npcx_wui *wui, miwu_dev_callback_handler_t handler) { /* KSI signal which has no wake-up input source */ if (wui->table == NPCX_MIWU_TABLE_NONE) { return; } /* Install callback function */ npcx_miwu_init_dev_callback(callback, wui, handler, dev); npcx_miwu_manage_dev_callback(callback, 1); /* Configure MIWU setting and enable its interrupt */ npcx_miwu_interrupt_configure(wui, NPCX_MIWU_MODE_EDGE, NPCX_MIWU_TRIG_BOTH); npcx_miwu_irq_enable(wui); } static int kscan_npcx_init(const struct device *dev) { const struct device *clk_dev = DEVICE_DT_GET(NPCX_CLK_CTRL_NODE); const struct kscan_npcx_config *const config = dev->config; struct kscan_npcx_data *const data = dev->data; struct kbs_reg *const inst = config->base; int ret; if (!device_is_ready(clk_dev)) { LOG_ERR("%s device not ready", clk_dev->name); return -ENODEV; } /* Turn on KBSCAN controller device clock */ ret = clock_control_on(clk_dev, (clock_control_subsys_t *)&config->clk_cfg); if (ret < 0) { LOG_ERR("Turn on KBSCAN clock fail %d", ret); } /* Pull-up KBSIN0-7 internally */ inst->KBSINPU = 0xFF; /* * Keyboard Scan Control Register * * [6:7] - KBHDRV KBSOUTn signals output buffers are open-drain. * [3] - KBSINC Auto-increment of Buffer Data register is disabled * [2] - KBSIEN Interrupt of Auto-Scan is disabled * [1] - KBSMODE Key detection mechanism is implemented by firmware * [0] - START Write 0 to this field is not affected */ inst->KBSCTL = 0x00; /* * Select quasi-bidirectional buffers for KSO pins. It reduces the * low-to-high transition time. This feature only supports in npcx7. */ if (IS_ENABLED(CONFIG_KSCAN_NPCX_KSO_HIGH_DRIVE)) { SET_FIELD(inst->KBSCTL, NPCX_KBSCTL_KBHDRV_FIELD, 0x01); } /* Drive all column lines to low for detection any key press */ kscan_npcx_drive_column(dev, KEYBOARD_COLUMN_DRIVE_NONE); /* Configure wake-up input and callback for keyboard input signal */ for (int i = 0; i < config->row_size; i++) kscan_npcx_init_ksi_wui_callback(dev, &data->ksi_callback[i], &config->wui_maps[i], kscan_npcx_ksi_isr); /* Configure pin-mux for kscan device */ ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT); if (ret < 0) { LOG_ERR("kscan pinctrl setup failed (%d)", ret); return ret; } /* Initialize semaphore usbed by kscan task and driver */ k_sem_init(&data->poll_lock, 0, 1); /* Time figures are transformed from msec to usec */ data->poll_period = (uint32_t)(CONFIG_KSCAN_NPCX_POLL_PERIOD_MS * USEC_PER_MSEC); data->poll_timeout = 100 * USEC_PER_MSEC; k_thread_create(&data->thread, data->thread_stack, CONFIG_KSCAN_NPCX_THREAD_STACK_SIZE, (void (*)(void *, void *, void *))kscan_matrix_polling_thread, (void *)dev, NULL, NULL, K_PRIO_COOP(4), 0, K_NO_WAIT); return 0; } static int kscan_npcx_configure(const struct device *dev, kscan_callback_t callback) { struct kscan_npcx_data *const data = dev->data; if (!callback) { return -EINVAL; } data->callback = callback; return 0; } static int kscan_npcx_enable_interface(const struct device *dev) { struct kscan_npcx_data *const data = dev->data; atomic_set(&data->enable_scan, 1); return 0; } static int kscan_npcx_disable_interface(const struct device *dev) { struct kscan_npcx_data *const data = dev->data; atomic_set(&data->enable_scan, 0); return 0; } static const struct kscan_driver_api kscan_npcx_driver_api = { .config = kscan_npcx_configure, .enable_callback = kscan_npcx_enable_interface, .disable_callback = kscan_npcx_disable_interface, }; PINCTRL_DT_INST_DEFINE(0); static const struct kscan_npcx_config kscan_cfg_0 = { .base = (struct kbs_reg *)DT_INST_REG_ADDR(0), .pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(0), .clk_cfg = NPCX_DT_CLK_CFG_ITEM(0), .irq = DT_INST_IRQN(0), .wui_size = NPCX_DT_WUI_ITEMS_LEN(0), .wui_maps = NPCX_DT_WUI_ITEMS_LIST(0), .row_size = KSCAN_ROW_SIZE, .col_size = KSCAN_COL_SIZE, .deb_time_press = DT_INST_PROP(0, debounce_down_ms), .deb_time_rel = DT_INST_PROP(0, debounce_up_ms), }; static struct kscan_npcx_data kscan_data_0; DEVICE_DT_INST_DEFINE(0, kscan_npcx_init, NULL, &kscan_data_0, &kscan_cfg_0, POST_KERNEL, CONFIG_KSCAN_INIT_PRIORITY, &kscan_npcx_driver_api); BUILD_ASSERT(DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) == 1, "only one 'nuvoton_npcx_kscan' compatible node can be supported");