From 26b3e99c570a7d6de3d34771d6db8b1c34031876 Mon Sep 17 00:00:00 2001 From: Mykhailo Skorokhodov Date: Mon, 23 Jun 2025 14:25:13 +0300 Subject: [PATCH] feat(kscan): Add Hall keyscan driver Signed-off-by: Mykhailo Skorokhodov --- app/module/drivers/kscan/CMakeLists.txt | 1 + app/module/drivers/kscan/Kconfig | 6 + app/module/drivers/kscan/kscan_gpio_hall.c | 327 +++++++++++++++++++++ 3 files changed, 334 insertions(+) create mode 100644 app/module/drivers/kscan/kscan_gpio_hall.c diff --git a/app/module/drivers/kscan/CMakeLists.txt b/app/module/drivers/kscan/CMakeLists.txt index 5b05af767..9d928d885 100644 --- a/app/module/drivers/kscan/CMakeLists.txt +++ b/app/module/drivers/kscan/CMakeLists.txt @@ -7,6 +7,7 @@ zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_DRIVER kscan_gpio.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_MATRIX kscan_gpio_matrix.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_CHARLIEPLEX kscan_gpio_charlieplex.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_DIRECT kscan_gpio_direct.c) +zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_HALL kscan_gpio_hall.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_GPIO_DEMUX kscan_gpio_demux.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_MOCK_DRIVER kscan_mock.c) zephyr_library_sources_ifdef(CONFIG_ZMK_KSCAN_COMPOSITE_DRIVER kscan_composite.c) diff --git a/app/module/drivers/kscan/Kconfig b/app/module/drivers/kscan/Kconfig index 88848dff9..f38150c75 100644 --- a/app/module/drivers/kscan/Kconfig +++ b/app/module/drivers/kscan/Kconfig @@ -5,6 +5,7 @@ DT_COMPAT_ZMK_KSCAN_COMPOSITE := zmk,kscan-composite DT_COMPAT_ZMK_KSCAN_GPIO_DEMUX := zmk,kscan-gpio-demux DT_COMPAT_ZMK_KSCAN_GPIO_DIRECT := zmk,kscan-gpio-direct DT_COMPAT_ZMK_KSCAN_GPIO_MATRIX := zmk,kscan-gpio-matrix +DT_COMPAT_ZMK_KSCAN_GPIO_MATRIX := zmk,kscan-gpio-hall DT_COMPAT_ZMK_KSCAN_GPIO_CHARLIEPLEX := zmk,kscan-gpio-charlieplex DT_COMPAT_ZMK_KSCAN_MOCK := zmk,kscan-mock @@ -42,6 +43,11 @@ config ZMK_KSCAN_GPIO_MATRIX default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_GPIO_MATRIX)) select ZMK_KSCAN_GPIO_DRIVER +config ZMK_KSCAN_GPIO_HALL + bool + default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_GPIO_HALL)) + select ZMK_KSCAN_GPIO_DRIVER + config ZMK_KSCAN_GPIO_CHARLIEPLEX bool default $(dt_compat_enabled,$(DT_COMPAT_ZMK_KSCAN_GPIO_CHARLIEPLEX)) diff --git a/app/module/drivers/kscan/kscan_gpio_hall.c b/app/module/drivers/kscan/kscan_gpio_hall.c new file mode 100644 index 000000000..5c86c3059 --- /dev/null +++ b/app/module/drivers/kscan/kscan_gpio_hall.c @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define DT_DRV_COMPAT zmk_kscan_gpio_hall + +#define EMA_SHIFT 4 + +#define INST_INPUTS_LEN(n) DT_INST_PROP_LEN(n, io_channels) + +#define KSCAN_ADC_GET_BY_IDX(node_id, idx) \ + ((struct kscan_adc){.spec = ADC_DT_SPEC_GET_BY_IDX(node_id, idx), .index = idx}) + +#define KSCAN_ADC_HALL_INPUT_CFG_INIT(idx, inst_idx) \ + KSCAN_ADC_GET_BY_IDX(DT_DRV_INST(inst_idx), idx) + +#define KSCAN_ADC_LIST(adc_array) \ + ((struct kscan_adc_list){.adcs = adc_array, .len = ARRAY_SIZE(adc_array)}) + +struct kscan_adc { + struct adc_dt_spec spec; + struct adc_channel_cfg cfg; + struct adc_sequence as; + size_t index; +}; + +struct kscan_adc_list { + struct kscan_adc *adcs; + size_t len; +}; + +struct kscan_key_state { + int last_value; + int idle_value; + int max_value; + int min_value; + int range; + bool pressed : 1; +}; + +struct kscan_hall_data { + const struct device *dev; + struct kscan_adc_list inputs; + kscan_callback_t callback; + struct k_work_delayable work; + /** Timestamp of the current or scheduled scan. */ + int64_t scan_time; + /** Current state of the inputs as an array of length config->inputs.len */ + struct kscan_key_state *state; +}; + +struct kscan_hall_config { + int min_trigger_value; + int max_trigger_value; + int trigger_value; + int init_range; + int idle_steps; +}; + +static int16_t current_value; + +int kscan_adc_read(const struct kscan_adc *adc) { + int err = adc_read(adc->spec.dev, &adc->as); + if (err != 0) { + LOG_ERR("Failed to read %s, channel: %i", adc->spec.dev->name, adc->spec.channel_id); + return err; + } + + return 0; +} + +static int kscan_hall_read(const struct device *dev) { + struct kscan_hall_data *data = dev->data; + const struct kscan_hall_config *config = dev->config; + + for (int i = 0; i < data->inputs.len; i++) { + struct kscan_adc *adc = &data->inputs.adcs[i]; + + const int read_err = kscan_adc_read(adc); + if (read_err != 0) { + return read_err; + } + + struct kscan_key_state *state = &data->state[adc->index]; + + if (state->max_value == 0) { + state->max_value = state->min_value = state->last_value = current_value + 1; + continue; + } + + state->last_value += (current_value - state->last_value) >> EMA_SHIFT; + + if (state->max_value < state->last_value) { + state->max_value = state->last_value + 1; + + int t = state->max_value - state->idle_value; + if (state->range < t) + state->range = t; + } + + if (state->min_value > state->last_value) { + state->min_value = state->last_value - 1; + + int t = abs(state->min_value - state->idle_value); + if (state->range < t) + state->range = t; + } + + int value = (abs(state->last_value - state->idle_value) / (float)state->range) * 100; + + if (value >= config->max_trigger_value) { + continue; + } + + if (value <= config->min_trigger_value) { + if (state->pressed) { + state->pressed = false; + LOG_DBG("Sending event at 0,%i state %s, value %i", adc->index, + state->pressed ? "on" : "off", value); + data->callback(dev, 0, adc->index, state->pressed); + } + continue; + } + + bool key_state = state->pressed; + + // Press + if (!state->pressed && value >= config->trigger_value) { + key_state = true; + } + + if (state->pressed != key_state) { + state->pressed = key_state; + LOG_DBG("Sending event at 0,%i state %s, value %i.", adc->index, + state->pressed ? "on" : "off", value); + data->callback(dev, 0, adc->index, state->pressed); + } + } + + k_work_reschedule(&data->work, K_TIMEOUT_ABS_MS(data->scan_time)); + return 0; +} + +static int kscan_hall_init_input_inst(struct kscan_adc *adc) { + struct adc_dt_spec *spec = &adc->spec; + + // TODO: check spec->channel_cfg_dt_node_exists + if (!device_is_ready(spec->dev)) { + LOG_ERR("ADC is not ready: %s", spec->dev->name); + return -ENODEV; + } + +#ifdef CONFIG_ADC_NRFX_SAADC + adc->cfg = (struct adc_channel_cfg){ + .channel_id = spec->channel_id, + .reference = ADC_REF_INTERNAL, + .gain = ADC_GAIN_1_6, + // TODO: adjust relative to the power mode + .acquisition_time = ADC_ACQ_TIME_DEFAULT, + .input_positive = SAADC_CH_PSELP_PSELP_AnalogInput0 + spec->channel_id, + }; +#endif + + // TODO: channel_cfg_dt_node_exists + + int err = adc_channel_setup(spec->dev, &adc->cfg); + if (err != 0) { + LOG_ERR("Unable to configure channel %u of device %s. Err: %i", spec->channel_id, + spec->dev->name, err); + return err; + } + + // TODO: create per device (adc1, adc2, MCP3208, etc) + adc->as = (struct adc_sequence){ + .channels = BIT(spec->channel_id), + .buffer = ¤t_value, + .buffer_size = sizeof(current_value), + .oversampling = 2, + .calibrate = true, + .resolution = 12, + }; + + LOG_DBG("%s: AIN%u setup returned %d", spec->dev->name, spec->channel_id, err); + return 0; +} + +static int kscan_hall_init_idle_value(const struct kscan_adc *adc, struct kscan_key_state *state, + int steps) { + int32_t sum = 1; + + for (int i = 0; i < steps; i++) { + int err = kscan_adc_read(adc); + if (err) { + return err; + } + sum += current_value; + k_sleep(K_MSEC(5)); + } + + state->idle_value = sum / steps; + return 0; +} + +static int kscan_hall_init_inputs(const struct device *dev) { + const struct kscan_hall_data *data = dev->data; + const struct kscan_hall_config *config = dev->config; + + for (int i = 0; i < data->inputs.len; i++) { + struct kscan_adc *adc = &data->inputs.adcs[i]; + struct kscan_key_state *state = &data->state[i]; + + int err = kscan_hall_init_input_inst(adc); + if (err) { + return err; + } + + err = kscan_hall_init_idle_value(adc, state, config->idle_steps); + if (err) { + return err; + } + + // Disable calibration. No further need. + adc->as.calibrate = false; + + state->range = config->init_range; + } + return 0; +} + +static void kscan_hall_work_handler(struct k_work *work) { + struct k_work_delayable *dwork = CONTAINER_OF(work, struct k_work_delayable, work); + struct kscan_hall_data *data = CONTAINER_OF(dwork, struct kscan_hall_data, work); + kscan_hall_read(data->dev); +} + +static int compare_ports2(const void *a, const void *b) { + const struct kscan_adc *adc_a = a; + const struct kscan_adc *adc_b = b; + + return (adc_a->spec.dev + adc_a->spec.channel_id) - (adc_b->spec.dev + adc_b->spec.channel_id); +} + +void kscan_adc_list_sort_by_port(struct kscan_adc_list *list) { + qsort(list->adcs, list->len, sizeof(list->adcs[0]), compare_ports2); +} + +static int kscan_hall_init(const struct device *dev) { + struct kscan_hall_data *data = dev->data; + + data->dev = dev; + + // Sort inputs by port so we can read each port just once per scan. + kscan_adc_list_sort_by_port(&data->inputs); + + k_work_init_delayable(&data->work, kscan_hall_work_handler); + + kscan_hall_init_inputs(dev); + return 0; +} + +static int kscan_hall_configure(const struct device *dev, kscan_callback_t callback) { + struct kscan_hall_data *data = dev->data; + + if (!callback) { + return -EINVAL; + } + + data->callback = callback; + return 0; +} + +static int kscan_hall_enable(const struct device *dev) { + struct kscan_hall_data *data = dev->data; + data->scan_time = k_uptime_get(); + return kscan_hall_read(dev); +} + +static int kscan_hall_disable(const struct device *dev) { + struct kscan_hall_data *data = dev->data; + k_work_cancel_delayable(&data->work); + return 0; +} + +static const struct kscan_driver_api kscan_hall_api = { + .config = kscan_hall_configure, + .enable_callback = kscan_hall_enable, + .disable_callback = kscan_hall_disable, +}; + +#define KSCAN_HALL_INIT(n) \ + static struct kscan_adc kscan_hall_inputs_##n[] = { \ + LISTIFY(INST_INPUTS_LEN(n), KSCAN_ADC_HALL_INPUT_CFG_INIT, (, ), n)}; \ + \ + static struct kscan_key_state kscan_hall_state_##n[INST_INPUTS_LEN(n)]; \ + \ + static struct kscan_hall_data kscan_hall_data_##n = { \ + .inputs = KSCAN_ADC_LIST(kscan_hall_inputs_##n), \ + .state = kscan_hall_state_##n, \ + }; \ + \ + static const struct kscan_hall_config kscan_hall_config_##n = { \ + .min_trigger_value = DT_INST_PROP(n, min_trigger_value), \ + .max_trigger_value = DT_INST_PROP(n, max_trigger_value), \ + .trigger_value = DT_INST_PROP(n, trigger_value), \ + .init_range = DT_INST_PROP(n, init_range), \ + .idle_steps = DT_INST_PROP(n, idle_steps), \ + }; \ + DEVICE_DT_INST_DEFINE(n, &kscan_hall_init, PM_DEVICE_DT_INST_GET(n), &kscan_hall_data_##n, \ + &kscan_hall_config_##n, POST_KERNEL, CONFIG_KSCAN_INIT_PRIORITY, \ + &kscan_hall_api); + +DT_INST_FOREACH_STATUS_OKAY(KSCAN_HALL_INIT);