diff --git a/drivers/input/CMakeLists.txt b/drivers/input/CMakeLists.txt index f8d04e4094f..cab9dcd3cac 100644 --- a/drivers/input/CMakeLists.txt +++ b/drivers/input/CMakeLists.txt @@ -34,6 +34,7 @@ zephyr_library_sources_ifdef(CONFIG_INPUT_SBUS input_sbus.c) zephyr_library_sources_ifdef(CONFIG_INPUT_STM32_TSC_KEYS input_tsc_keys.c) zephyr_library_sources_ifdef(CONFIG_INPUT_STMPE811 input_stmpe811.c) zephyr_library_sources_ifdef(CONFIG_INPUT_TOUCH input_touch.c) +zephyr_library_sources_ifdef(CONFIG_INPUT_VS1838B input_vs1838b.c) zephyr_library_sources_ifdef(CONFIG_INPUT_XEC_KBD input_xec_kbd.c) zephyr_library_sources_ifdef(CONFIG_INPUT_XPT2046 input_xpt2046.c) # zephyr-keep-sorted-stop diff --git a/drivers/input/Kconfig b/drivers/input/Kconfig index 889702e48db..d23b1db9bed 100644 --- a/drivers/input/Kconfig +++ b/drivers/input/Kconfig @@ -37,6 +37,7 @@ source "drivers/input/Kconfig.sdl" source "drivers/input/Kconfig.stmpe811" source "drivers/input/Kconfig.touch" source "drivers/input/Kconfig.tsc_keys" +source "drivers/input/Kconfig.vs1838b" source "drivers/input/Kconfig.xec" source "drivers/input/Kconfig.xpt2046" # zephyr-keep-sorted-stop diff --git a/drivers/input/Kconfig.vs1838b b/drivers/input/Kconfig.vs1838b new file mode 100644 index 00000000000..20ed6155105 --- /dev/null +++ b/drivers/input/Kconfig.vs1838b @@ -0,0 +1,10 @@ +# Copyright (c) 2025 Bastien Jauny +# SPDX-License-Identifier: Apache-2.0 + +config INPUT_VS1838B + bool "Vishay VS1838B infrared receiver" + default y + depends on DT_HAS_VISHAY_VS1838B_ENABLED + select GPIO + help + This option enables the driver for the VS1838B infrared receiver. diff --git a/drivers/input/input_vs1838b.c b/drivers/input/input_vs1838b.c new file mode 100644 index 00000000000..c265c8c9342 --- /dev/null +++ b/drivers/input/input_vs1838b.c @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2025 Bastien Jauny + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT vishay_vs1838b + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(input_vs1838b, CONFIG_INPUT_LOG_LEVEL); + +/* A NEC packet is defined by: + * - a lead burst (2 edges) + * - an 8-bit address followed by its logical inverse + * - an 8-bit command followed by its logical inverse + * - a trailing burst + */ + +/* Constants used for parsing the edges buffer for NEC protocol */ +#define NEC_LEAD_PULSE_EDGE_OFFSET 0 +#define NEC_LEAD_PULSE_EDGE_WIDTH 2 + +#define NEC_ADDRESS_BYTE_EDGE_OFFSET (NEC_LEAD_PULSE_EDGE_OFFSET + NEC_LEAD_PULSE_EDGE_WIDTH) +#define NEC_ADDRESS_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE) + +#define NEC_REVERSE_ADDRESS_BYTE_EDGE_OFFSET \ + (NEC_ADDRESS_BYTE_EDGE_OFFSET + NEC_ADDRESS_BYTE_EDGE_WIDTH) +#define NEC_REVERSE_ADDRESS_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE) + +#define NEC_COMMAND_BYTE_EDGE_OFFSET \ + (NEC_REVERSE_ADDRESS_BYTE_EDGE_OFFSET + NEC_REVERSE_ADDRESS_BYTE_EDGE_WIDTH) +#define NEC_COMMAND_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE) + +#define NEC_REVERSE_COMMAND_BYTE_EDGE_OFFSET \ + (NEC_COMMAND_BYTE_EDGE_OFFSET + NEC_COMMAND_BYTE_EDGE_WIDTH) +#define NEC_REVERSE_COMMAND_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE) + +#define NEC_SINGLE_COMMAND_EDGES_COUNT \ + (NEC_REVERSE_COMMAND_BYTE_EDGE_OFFSET + NEC_REVERSE_COMMAND_BYTE_EDGE_WIDTH + 2) + +/* NEC protocol values */ +#define NEC_LEAD_PULSE_PERIOD_ON_USEC 9000 +#define NEC_LEAD_PULSE_PERIOD_OFF_USEC 4500 +#define NEC_BIT_DETECT_PERIOD_NSEC 562500 +#define NEC_BIT_DETECT_PERIOD_USEC (NEC_BIT_DETECT_PERIOD_NSEC / NSEC_PER_USEC) +#define NEC_BIT_0_TOTAL_PERIOD_USEC 1125 +#define NEC_BIT_1_TOTAL_PERIOD_USEC 2250 +/* Total delay between a command and a repeat code is 108ms + * and total time of a command is 67.5ms + */ +#define NEC_TIMEOUT_REPEAT_CODE_MSEC (108 - 67) + +/* Macros to define tick ranges based on a millisecond tolerance */ +#define VS1838B_MIN_TICK(usec, tol) \ + ((((usec) - (tol)) * CONFIG_SYS_CLOCK_TICKS_PER_SEC) / USEC_PER_SEC) +#define VS1838B_MAX_TICK(usec, tol) \ + ((((usec) + (tol)) * CONFIG_SYS_CLOCK_TICKS_PER_SEC) / USEC_PER_SEC) + +/* Empiric tolerance values. Might be a good idea to put them in the Kconfig? */ +#define VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC 400 +#define VS1838B_NEC_BIT_DETECT_PERIOD_TOLERANCE_USEC 150 +#define VS1838B_NEC_BIT_0_TOTAL_TOLERANCE_USEC 200 +#define VS1838B_NEC_BIT_1_TOTAL_TOLERANCE_USEC 200 + +/* Tick ranges for the NEC elements */ +#define VS1838B_NEC_LEAD_PULSE_ON_MIN_TICK \ + VS1838B_MIN_TICK(NEC_LEAD_PULSE_PERIOD_ON_USEC, \ + VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC) +#define VS1838B_NEC_LEAD_PULSE_ON_MAX_TICK \ + VS1838B_MAX_TICK(NEC_LEAD_PULSE_PERIOD_ON_USEC, \ + VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC) + +#define VS1838B_NEC_LEAD_PULSE_OFF_MIN_TICK \ + VS1838B_MIN_TICK(NEC_LEAD_PULSE_PERIOD_OFF_USEC, \ + VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC) +#define VS1838B_NEC_LEAD_PULSE_OFF_MAX_TICK \ + VS1838B_MAX_TICK(NEC_LEAD_PULSE_PERIOD_OFF_USEC, \ + VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC) + +#define VS1838B_NEC_BIT_DETECT_MIN_TICK \ + VS1838B_MIN_TICK(NEC_BIT_DETECT_PERIOD_USEC, VS1838B_NEC_BIT_DETECT_PERIOD_TOLERANCE_USEC) +#define VS1838B_NEC_BIT_DETECT_MAX_TICK \ + VS1838B_MAX_TICK(NEC_BIT_DETECT_PERIOD_USEC, VS1838B_NEC_BIT_DETECT_PERIOD_TOLERANCE_USEC) + +#define VS1838B_NEC_BIT_0_TOTAL_MIN_TICK \ + VS1838B_MIN_TICK(NEC_BIT_0_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_0_TOTAL_TOLERANCE_USEC) +#define VS1838B_NEC_BIT_0_TOTAL_MAX_TICK \ + VS1838B_MAX_TICK(NEC_BIT_0_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_0_TOTAL_TOLERANCE_USEC) + +#define VS1838B_NEC_BIT_1_TOTAL_MIN_TICK \ + VS1838B_MIN_TICK(NEC_BIT_1_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_1_TOTAL_TOLERANCE_USEC) +#define VS1838B_NEC_BIT_1_TOTAL_MAX_TICK \ + VS1838B_MAX_TICK(NEC_BIT_1_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_1_TOTAL_TOLERANCE_USEC) + +struct vs1838b_data { + struct device const *dev; + struct gpio_callback input_cb; + struct k_work_delayable decode_work; + int64_t edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT]; + uint8_t edges_count; + struct k_sem decode_sem; +}; + +struct vs1838b_config { + struct gpio_dt_spec input; +}; + +static inline bool is_within_range(k_ticks_t const ticks, k_ticks_t const min, k_ticks_t const max) +{ + return (ticks <= max) && (ticks >= min); +} + +static bool read_byte_from(int64_t *const edges_ticks, uint8_t const offset, uint8_t *byte) +{ + /* Make sure we add bits from 0 */ + uint8_t temp_byte = 0; + k_ticks_t ticks_on; + k_ticks_t ticks_total; + + /* Bytes are transmitted LSB first */ + for (uint8_t i = 0; i < BITS_PER_BYTE; ++i) { + /* + * To detect bits and their values we analyze: + * - the initial pulse width + * - the total period + */ + ticks_on = edges_ticks[(2 * i) + offset + 1] - edges_ticks[(2 * i) + offset]; + ticks_total = edges_ticks[(2 * i) + offset + 2] - edges_ticks[(2 * i) + offset]; + + LOG_DBG("ticks_on %lld", ticks_on); + LOG_DBG("ticks_total %lld", ticks_total); + if (is_within_range(ticks_on, VS1838B_NEC_BIT_DETECT_MIN_TICK, + VS1838B_NEC_BIT_DETECT_MAX_TICK)) { + if (is_within_range(ticks_total, VS1838B_NEC_BIT_0_TOTAL_MIN_TICK, + VS1838B_NEC_BIT_0_TOTAL_MAX_TICK)) { + /* 0 detected */ + } else if (is_within_range(ticks_total, VS1838B_NEC_BIT_1_TOTAL_MIN_TICK, + VS1838B_NEC_BIT_1_TOTAL_MAX_TICK)) { + /* 1 detected */ + temp_byte += BIT(i); + } else { + LOG_WRN("Failed to identify detected bit at position %u", i); + return false; + } + } else { + LOG_WRN("Failed to detect a valid bit at position %u", i); + return false; + } + } + + *byte = temp_byte; + return true; +} + +static bool detect_leading_burst(int64_t *const edges_ticks) +{ + /* Detect leading pulse using the first 3 edges */ + int64_t lead_ticks_on = edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 1] - + edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET]; + int64_t lead_ticks_off = edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 2] - + edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 1]; + + /* Manage the corner case of an overflow */ + if ((lead_ticks_on < 0) || (lead_ticks_off < 0)) { + LOG_ERR("Ticks overflow: %lld - %lld - %lld", + edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET], + edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 1], + edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 2]); + return false; + } + + LOG_DBG("Read %lld ticks on and %lld ticks off", lead_ticks_on, lead_ticks_off); + + return is_within_range(lead_ticks_on, VS1838B_NEC_LEAD_PULSE_ON_MIN_TICK, + VS1838B_NEC_LEAD_PULSE_ON_MAX_TICK) && + is_within_range(lead_ticks_off, VS1838B_NEC_LEAD_PULSE_OFF_MIN_TICK, + VS1838B_NEC_LEAD_PULSE_OFF_MAX_TICK); +} + +static bool read_redundant_byte(int64_t *const edges_ticks, uint8_t *const byte, + uint32_t const offset) +{ + uint8_t temp_byte; + uint8_t reverse_byte; + + if (read_byte_from(edges_ticks, offset, &temp_byte) && + read_byte_from(edges_ticks, offset + (2 * BITS_PER_BYTE), &reverse_byte)) { + if (temp_byte == (uint8_t)(~reverse_byte)) { + *byte = temp_byte; + } else { + LOG_ERR("Error while decoding byte"); + return false; + } + } else { + LOG_ERR("Error while reading bytes"); + return false; + } + + return true; +} + +static bool read_address_byte(int64_t *const edges_ticks, uint8_t *const address) +{ + return read_redundant_byte(edges_ticks, address, NEC_ADDRESS_BYTE_EDGE_OFFSET); +} + +static bool read_command_byte(int64_t *const edges_ticks, uint8_t *const command) +{ + return read_redundant_byte(edges_ticks, command, NEC_COMMAND_BYTE_EDGE_OFFSET); +} + +static bool detect_last_burst(int64_t *const edges_ticks) +{ + /* Detect leading pulse using the last 3 edges */ + int64_t burst_length = edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 1] - + edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 2]; + + /* Manage the corner case of an overflow */ + if (burst_length < 0) { + LOG_ERR("Ticks overflow: %lld - %lld", + edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 1], + edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 2]); + return false; + } + + LOG_DBG("Read %lld ticks in the last burst", burst_length); + + return is_within_range(burst_length, VS1838B_NEC_BIT_DETECT_MIN_TICK, + VS1838B_NEC_BIT_DETECT_MAX_TICK); +} + +static bool get_address_and_command(int64_t *const edges_ticks, uint8_t *const address, + uint8_t *const command) +{ + if (!detect_leading_burst(edges_ticks)) { + LOG_DBG("No lead detected"); + return false; + } + + if (!read_address_byte(edges_ticks, address)) { + LOG_DBG("No address decoded"); + return false; + } + + if (!read_command_byte(edges_ticks, command)) { + LOG_DBG("No command decoded"); + return false; + } + if (!detect_last_burst(edges_ticks)) { + LOG_DBG("No trailing edge detected"); + return false; + } + + return true; +} + +/* + * Management of the decoding + */ +static void vs1838b_decode_work_handler(struct k_work *item) +{ + struct k_work_delayable *dwork = k_work_delayable_from_work(item); + struct vs1838b_data *data = CONTAINER_OF(dwork, struct vs1838b_data, decode_work); + + if (k_sem_take(&data->decode_sem, K_FOREVER) == 0) { + uint8_t address_byte; + uint8_t command_byte; + + if (get_address_and_command(data->edges_ticks, &address_byte, &command_byte)) { + LOG_DBG("Address: [0x%X] | Command: [0x%X]", address_byte, command_byte); + if (input_report(data->dev, INPUT_EV_DEVICE, INPUT_MSC_SCAN, + (address_byte << 8) | command_byte, true, K_FOREVER) < 0) { + LOG_ERR("Message failed to be enqueued"); + } + } + } + + /* Reset the record */ + data->edges_count = 0; + k_sem_give(&data->decode_sem); +} + +/* + * Internal callback + */ +static void vs1838b_input_callback(struct device const *dev, struct gpio_callback *cb, + uint32_t pins) +{ + /* + * We want to: + * - register the timestamps of interrupts + * - try and decode the received bits when we reach the appropriate threshold + */ + int64_t const tick = k_uptime_ticks(); + struct vs1838b_data *data = CONTAINER_OF(cb, struct vs1838b_data, input_cb); + + /* If we already schedule a decode, we need to cancel it. */ + if (k_work_cancel_delayable(&data->decode_work) != 0) { + LOG_WRN("Decoding not cancelled!"); + } + + if (k_sem_take(&data->decode_sem, K_NO_WAIT) != 0) { + /* Decoding might be pending */ + return; + } + + /* If more interrupts are received, they're likely to be repeat codes + * and we choose to ignore them. + */ + if (data->edges_count < NEC_SINGLE_COMMAND_EDGES_COUNT) { + data->edges_ticks[data->edges_count++] = tick; + } + + if (data->edges_count == NEC_SINGLE_COMMAND_EDGES_COUNT) { + /* There's a candidate! + * If nothing gets in during the grace period + * it *should* be an entire command. + */ + k_work_schedule(&data->decode_work, K_MSEC(NEC_TIMEOUT_REPEAT_CODE_MSEC)); + } + k_sem_give(&data->decode_sem); +} + +static int vs1838b_init(struct device const *dev) +{ + struct vs1838b_config const *config = dev->config; + struct gpio_dt_spec const *data_input = &config->input; + struct vs1838b_data *data = dev->data; + + data->dev = dev; + + if (!gpio_is_ready_dt(data_input)) { + LOG_ERR("GPIO input pin is not ready"); + return -ENODEV; + } + + /* + * Setup the input as an interrupt source + * and register an associated callback. + */ + gpio_pin_configure_dt(data_input, GPIO_INPUT); + gpio_pin_interrupt_configure_dt(data_input, GPIO_INT_EDGE_BOTH); + gpio_init_callback(&data->input_cb, vs1838b_input_callback, BIT(data_input->pin)); + gpio_add_callback_dt(data_input, &data->input_cb); + + k_sem_init(&data->decode_sem, 1, 1); + k_work_init_delayable(&data->decode_work, vs1838b_decode_work_handler); + + return 0; +} + +#define VS1838B_DEFINE(inst) \ + static struct vs1838b_data vs1838b_data_##inst; \ + \ + static struct vs1838b_config const vs1838b_config_##inst = { \ + .input = GPIO_DT_SPEC_INST_GET(inst, data_gpios), \ + }; \ + \ + DEVICE_DT_INST_DEFINE(inst, vs1838b_init, NULL, &vs1838b_data_##inst, \ + &vs1838b_config_##inst, POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, \ + NULL); + +DT_INST_FOREACH_STATUS_OKAY(VS1838B_DEFINE) diff --git a/dts/bindings/input/vishay,vs1838b.yaml b/dts/bindings/input/vishay,vs1838b.yaml new file mode 100644 index 00000000000..2e6a1bf162b --- /dev/null +++ b/dts/bindings/input/vishay,vs1838b.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2025 Bastien Jauny +# SPDX-License-Identifier: Apache-2.0 + +description: Vishay VS1838B infrared receiver + +compatible: "vishay,vs1838b" + +properties: + data-gpios: + required: true + type: phandle-array + description: | + GPIO used to transmit the received data.