// SPDX-License-Identifier: GPL-2.0-only /* * GPIO driven charlieplex keypad driver * * Copyright (c) 2026 Hugo Villeneuve * * Based on matrix_keyboard.c */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct charlieplex_keypad { struct input_dev *input_dev; struct gpio_descs *line_gpios; unsigned int nlines; unsigned int settling_time_us; unsigned int debounce_threshold; unsigned int debounce_count; int debounce_code; int current_code; }; static void charlieplex_keypad_report_key(struct input_dev *input) { struct charlieplex_keypad *keypad = input_get_drvdata(input); const unsigned short *keycodes = input->keycode; if (keypad->current_code > 0) { input_event(input, EV_MSC, MSC_SCAN, keypad->current_code); input_report_key(input, keycodes[keypad->current_code], 0); input_sync(input); } if (keypad->debounce_code) { input_event(input, EV_MSC, MSC_SCAN, keypad->debounce_code); input_report_key(input, keycodes[keypad->debounce_code], 1); input_sync(input); } keypad->current_code = keypad->debounce_code; } static void charlieplex_keypad_check_switch_change(struct input_dev *input, unsigned int code) { struct charlieplex_keypad *keypad = input_get_drvdata(input); if (code != keypad->debounce_code) { keypad->debounce_count = 0; keypad->debounce_code = code; } if (keypad->debounce_code != keypad->current_code) { if (keypad->debounce_count++ >= keypad->debounce_threshold) charlieplex_keypad_report_key(input); } } static int charlieplex_keypad_scan_line(struct charlieplex_keypad *keypad, unsigned int oline) { struct gpio_descs *line_gpios = keypad->line_gpios; DECLARE_BITMAP(values, MATRIX_MAX_ROWS); int err; /* Activate only one line as output at a time. */ gpiod_direction_output(line_gpios->desc[oline], 1); if (keypad->settling_time_us) fsleep(keypad->settling_time_us); /* Read input on all other lines. */ err = gpiod_get_array_value_cansleep(line_gpios->ndescs, line_gpios->desc, line_gpios->info, values); gpiod_direction_input(line_gpios->desc[oline]); if (err) return err; for (unsigned int iline = 0; iline < keypad->nlines; iline++) { if (iline == oline) continue; /* Do not read active output line. */ /* Check if GPIO is asserted. */ if (test_bit(iline, values)) return MATRIX_SCAN_CODE(oline, iline, get_count_order(keypad->nlines)); } return 0; } static void charlieplex_keypad_poll(struct input_dev *input) { struct charlieplex_keypad *keypad = input_get_drvdata(input); int code = 0; for (unsigned int oline = 0; oline < keypad->nlines; oline++) { code = charlieplex_keypad_scan_line(keypad, oline); if (code != 0) break; } if (code >= 0) charlieplex_keypad_check_switch_change(input, code); } static int charlieplex_keypad_init_gpio(struct platform_device *pdev, struct charlieplex_keypad *keypad) { char **pin_names; char label[32]; snprintf(label, sizeof(label), "%s-pin", pdev->name); keypad->line_gpios = devm_gpiod_get_array(&pdev->dev, "line", GPIOD_IN); if (IS_ERR(keypad->line_gpios)) return PTR_ERR(keypad->line_gpios); keypad->nlines = keypad->line_gpios->ndescs; if (keypad->nlines > MATRIX_MAX_ROWS) return -EINVAL; pin_names = devm_kasprintf_strarray(&pdev->dev, label, keypad->nlines); if (IS_ERR(pin_names)) return PTR_ERR(pin_names); for (unsigned int i = 0; i < keypad->line_gpios->ndescs; i++) gpiod_set_consumer_name(keypad->line_gpios->desc[i], pin_names[i]); return 0; } static int charlieplex_keypad_probe(struct platform_device *pdev) { struct charlieplex_keypad *keypad; struct input_dev *input_dev; unsigned int debounce_interval_ms = 5; unsigned int poll_interval_ms; int err; keypad = devm_kzalloc(&pdev->dev, sizeof(*keypad), GFP_KERNEL); if (!keypad) return -ENOMEM; input_dev = devm_input_allocate_device(&pdev->dev); if (!input_dev) return -ENOMEM; keypad->input_dev = input_dev; err = device_property_read_u32(&pdev->dev, "poll-interval", &poll_interval_ms); if (err) return dev_err_probe(&pdev->dev, err, "failed to parse 'poll-interval' property\n"); if (poll_interval_ms == 0) return dev_err_probe(&pdev->dev, -EINVAL, "invalid 'poll-interval' value\n"); device_property_read_u32(&pdev->dev, "debounce-delay-ms", &debounce_interval_ms); device_property_read_u32(&pdev->dev, "settling-time-us", &keypad->settling_time_us); keypad->current_code = -1; keypad->debounce_code = -1; keypad->debounce_threshold = DIV_ROUND_UP(debounce_interval_ms, poll_interval_ms); err = charlieplex_keypad_init_gpio(pdev, keypad); if (err) return err; input_dev->name = pdev->name; input_dev->id.bustype = BUS_HOST; err = matrix_keypad_build_keymap(NULL, NULL, keypad->nlines, keypad->nlines, NULL, input_dev); if (err) return dev_err_probe(&pdev->dev, err, "failed to build keymap\n"); if (device_property_read_bool(&pdev->dev, "autorepeat")) __set_bit(EV_REP, input_dev->evbit); input_set_capability(input_dev, EV_MSC, MSC_SCAN); err = input_setup_polling(input_dev, charlieplex_keypad_poll); if (err) return dev_err_probe(&pdev->dev, err, "unable to set up polling\n"); input_set_poll_interval(input_dev, poll_interval_ms); input_set_drvdata(input_dev, keypad); err = input_register_device(keypad->input_dev); if (err) return err; return 0; } static const struct of_device_id charlieplex_keypad_dt_match[] = { { .compatible = "gpio-charlieplex-keypad" }, { } }; MODULE_DEVICE_TABLE(of, charlieplex_keypad_dt_match); static struct platform_driver charlieplex_keypad_driver = { .probe = charlieplex_keypad_probe, .driver = { .name = "charlieplex-keypad", .of_match_table = charlieplex_keypad_dt_match, }, }; module_platform_driver(charlieplex_keypad_driver); MODULE_AUTHOR("Hugo Villeneuve "); MODULE_DESCRIPTION("GPIO driven charlieplex keypad driver"); MODULE_LICENSE("GPL");