| /* |
| * Copyright (c) 2019 Intel Corporation |
| * Copyright (c) 2022 Nuvoton Technology Corporation. |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT nuvoton_npcx_kbd |
| |
| #include "soc_miwu.h" |
| |
| #include <zephyr/drivers/clock_control.h> |
| #include <zephyr/drivers/pinctrl.h> |
| #include <zephyr/input/input.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/logging/log.h> |
| |
| #include <soc.h> |
| #define LOG_LEVEL CONFIG_INPUT_LOG_LEVEL |
| LOG_MODULE_REGISTER(input_npcx_kbd); |
| |
| #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 input_npcx_kbd_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 input_npcx_kbd_data { |
| int64_t poll_timeout_us; |
| uint32_t poll_period_us; |
| 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_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; |
| struct k_thread thread; |
| |
| K_KERNEL_STACK_MEMBER(thread_stack, CONFIG_INPUT_NPCX_KBD_THREAD_STACK_SIZE); |
| }; |
| |
| /* Keyboard scan local functions */ |
| static void input_npcx_kbd_ksi_isr(const struct device *dev, struct npcx_wui *wui) |
| { |
| ARG_UNUSED(wui); |
| struct input_npcx_kbd_data *const data = dev->data; |
| |
| k_sem_give(&data->poll_lock); |
| } |
| |
| static int input_npcx_kbd_resume_detection(const struct device *dev, bool resume) |
| { |
| const struct input_npcx_kbd_config *const config = dev->config; |
| |
| if (resume) { |
| irq_enable(config->irq); |
| } else { |
| irq_disable(config->irq); |
| } |
| |
| return 0; |
| } |
| |
| static int input_npcx_kbd_drive_column(const struct device *dev, int col) |
| { |
| const struct input_npcx_kbd_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: 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); |
| |
| inst->KBSOUT0 = (mask & 0xFFFF); |
| inst->KBSOUT1 = ((mask >> 16) & 0x03); |
| |
| return 0; |
| } |
| |
| static int input_npcx_kbd_read_row(const struct device *dev) |
| { |
| const struct input_npcx_kbd_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 input_npcx_kbd_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 input_npcx_kbd_config *const config = dev->config; |
| int row; |
| uint8_t key_event = 0U; |
| |
| for (int col = 0; col < config->col_size; col++) { |
| input_npcx_kbd_drive_column(dev, col); |
| |
| /* Allow the matrix to stabilize before reading it */ |
| k_busy_wait(CONFIG_INPUT_NPCX_KBD_POLL_COL_OUTPUT_SETTLE_TIME_US); |
| |
| row = input_npcx_kbd_read_row(dev); |
| new_state[col] = row & 0xFF; |
| key_event |= row; |
| } |
| |
| input_npcx_kbd_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 input_npcx_kbd_config *const config = dev->config; |
| struct input_npcx_kbd_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; |
| |
| 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); |
| } |
| } |
| } |
| |
| static bool check_key_events(const struct device *dev) |
| { |
| const struct input_npcx_kbd_config *const config = dev->config; |
| struct input_npcx_kbd_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 kbd_matrix_poll(const struct device *dev) |
| { |
| struct input_npcx_kbd_data *const data = dev->data; |
| uint64_t poll_time_end = sys_clock_timeout_end_calc(K_USEC(data->poll_timeout_us)); |
| uint32_t current_cycles; |
| uint32_t cycles_diff; |
| uint32_t wait_period; |
| |
| while (true) { |
| 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_us)); |
| } 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_us - 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_us) { |
| LOG_DBG("wait_period: %u", wait_period); |
| |
| wait_period = data->poll_period_us; |
| } |
| |
| /* Allow other threads to run while we sleep */ |
| k_usleep(wait_period); |
| } |
| } |
| |
| static void kbd_matrix_polling_thread(const struct device *dev, void *dummy2, void *dummy3) |
| { |
| struct input_npcx_kbd_data *const data = dev->data; |
| |
| ARG_UNUSED(dummy2); |
| ARG_UNUSED(dummy3); |
| |
| while (true) { |
| /* Enable interrupt of KSI pins */ |
| input_npcx_kbd_resume_detection(dev, true); |
| |
| input_npcx_kbd_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 */ |
| input_npcx_kbd_resume_detection(dev, false); |
| |
| kbd_matrix_poll(dev); |
| } |
| } |
| |
| static void input_npcx_kbd_init_ksi_wui_callback(const struct device *dev, |
| struct miwu_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_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 input_npcx_kbd_init(const struct device *dev) |
| { |
| const struct device *clk_dev = DEVICE_DT_GET(NPCX_CLK_CTRL_NODE); |
| const struct input_npcx_kbd_config *const config = dev->config; |
| struct input_npcx_kbd_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_INPUT_NPCX_KBD_KSO_HIGH_DRIVE)) { |
| SET_FIELD(inst->KBSCTL, NPCX_KBSCTL_KBHDRV_FIELD, 0x01); |
| } |
| |
| /* Drive all column lines to low for detection any key press */ |
| input_npcx_kbd_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++) { |
| input_npcx_kbd_init_ksi_wui_callback( |
| dev, &data->ksi_callback[i], &config->wui_maps[i], |
| input_npcx_kbd_ksi_isr); |
| } |
| |
| /* Configure pin-mux for keyboard scan device */ |
| ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT); |
| if (ret < 0) { |
| LOG_ERR("keyboard scan pinctrl setup failed (%d)", ret); |
| return ret; |
| } |
| |
| /* Initialize semaphore used by keyboard scan task and driver */ |
| k_sem_init(&data->poll_lock, 0, 1); |
| |
| /* Time figures are transformed from msec to usec */ |
| data->poll_period_us = (uint32_t)(CONFIG_INPUT_NPCX_KBD_POLL_PERIOD_MS * USEC_PER_MSEC); |
| data->poll_timeout_us = 100 * USEC_PER_MSEC; |
| |
| k_thread_create(&data->thread, data->thread_stack, |
| CONFIG_INPUT_NPCX_KBD_THREAD_STACK_SIZE, |
| (k_thread_entry_t)kbd_matrix_polling_thread, (void *)dev, NULL, NULL, |
| K_PRIO_COOP(4), 0, K_NO_WAIT); |
| |
| k_thread_name_set(&data->thread, "npcx-kbd"); |
| |
| return 0; |
| } |
| |
| PINCTRL_DT_INST_DEFINE(0); |
| |
| static const struct input_npcx_kbd_config npcx_kbd_cfg = { |
| .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 input_npcx_kbd_data npcx_kbd_data; |
| |
| DEVICE_DT_INST_DEFINE(0, input_npcx_kbd_init, NULL, |
| &npcx_kbd_data, &npcx_kbd_cfg, |
| POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, NULL); |
| |
| BUILD_ASSERT(DT_NUM_INST_STATUS_OKAY(DT_DRV_COMPAT) == 1, |
| "only one nuvoton,npcx-kbd compatible node can be supported"); |