| // SPDX-License-Identifier: GPL-2.0-only | |
| /* | |
| * GPIO driven charlieplex keypad driver | |
| * | |
| * Copyright (c) 2026 Hugo Villeneuve <hvilleneuve@dimonoff.com> | |
| * | |
| * Based on matrix_keyboard.c | |
| */ | |
| #include <linux/bitops.h> | |
| #include <linux/delay.h> | |
| #include <linux/dev_printk.h> | |
| #include <linux/device/devres.h> | |
| #include <linux/err.h> | |
| #include <linux/gpio/consumer.h> | |
| #include <linux/input.h> | |
| #include <linux/input/matrix_keypad.h> | |
| #include <linux/math.h> | |
| #include <linux/module.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/property.h> | |
| #include <linux/string_helpers.h> | |
| #include <linux/types.h> | |
| 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 <hvilleneuve@dimonoff.com>"); | |
| MODULE_DESCRIPTION("GPIO driven charlieplex keypad driver"); | |
| MODULE_LICENSE("GPL"); |