diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt index a043a8b7429..31177ae24c4 100644 --- a/drivers/CMakeLists.txt +++ b/drivers/CMakeLists.txt @@ -24,6 +24,7 @@ add_subdirectory_ifdef(CONFIG_CRYPTO crypto) add_subdirectory_ifdef(CONFIG_DAC dac) add_subdirectory_ifdef(CONFIG_DAI dai) add_subdirectory_ifdef(CONFIG_DISPLAY display) +add_subdirectory_ifdef(CONFIG_AUXDISPLAY auxdisplay) add_subdirectory_ifdef(CONFIG_DMA dma) add_subdirectory_ifdef(CONFIG_EDAC edac) add_subdirectory_ifdef(CONFIG_EEPROM eeprom) diff --git a/drivers/Kconfig b/drivers/Kconfig index 94e5e6c5be4..d615249727c 100644 --- a/drivers/Kconfig +++ b/drivers/Kconfig @@ -6,6 +6,7 @@ menu "Device Drivers" source "drivers/adc/Kconfig" +source "drivers/auxdisplay/Kconfig" source "drivers/audio/Kconfig" source "drivers/bbram/Kconfig" source "drivers/bluetooth/Kconfig" diff --git a/drivers/auxdisplay/CMakeLists.txt b/drivers/auxdisplay/CMakeLists.txt new file mode 100644 index 00000000000..6ad590d5d66 --- /dev/null +++ b/drivers/auxdisplay/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library() +zephyr_library_sources_ifdef(CONFIG_AUXDISPLAY_ITRON auxdisplay_itron.c) diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig new file mode 100644 index 00000000000..d6f24775f3f --- /dev/null +++ b/drivers/auxdisplay/Kconfig @@ -0,0 +1,25 @@ +# Auxiliary Display drivers + +# Copyright (c) 2022 Jamie McCrae +# SPDX-License-Identifier: Apache-2.0 + +menuconfig AUXDISPLAY + bool "Auxiliary (textual) Display Drivers" + help + Enable auxiliary/texual display drivers (e.g. alphanumerical displays) + +if AUXDISPLAY + +config AUXDISPLAY_INIT_PRIORITY + int "Auxiliary display devices init priority" + default 85 + help + Auxiliary (textual) display devices initialization priority. + +module = AUXDISPLAY +module-str = auxdisplay +source "subsys/logging/Kconfig.template.log_config" + +source "drivers/auxdisplay/Kconfig.itron" + +endif # AUXDISPLAY diff --git a/drivers/auxdisplay/Kconfig.itron b/drivers/auxdisplay/Kconfig.itron new file mode 100644 index 00000000000..44873f78e1e --- /dev/null +++ b/drivers/auxdisplay/Kconfig.itron @@ -0,0 +1,11 @@ +# Copyright (c) 2022 Jamie McCrae +# SPDX-License-Identifier: Apache-2.0 + +config AUXDISPLAY_ITRON + bool "Noritake Itron VFD driver" + default y + select GPIO + select SERIAL + depends on DT_HAS_NORITAKE_ITRON_ENABLED + help + Enable driver for Noritake Itron VFD. diff --git a/drivers/auxdisplay/auxdisplay_itron.c b/drivers/auxdisplay/auxdisplay_itron.c new file mode 100644 index 00000000000..1783d8d7d66 --- /dev/null +++ b/drivers/auxdisplay/auxdisplay_itron.c @@ -0,0 +1,448 @@ +/* + * Copyright (c) 2022-2023 Jamie McCrae + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT noritake_itron + +#include +#include +#include +#include +#include +#include +#include +#include +#include "auxdisplay_itron.h" + +LOG_MODULE_REGISTER(auxdisplay_itron, CONFIG_AUXDISPLAY_LOG_LEVEL); + +/* Display commands */ +#define AUXDISPLAY_ITRON_CMD_USER_SETTING 0x1f +#define AUXDISPLAY_ITRON_CMD_ESCAPE 0x1b +#define AUXDISPLAY_ITRON_CMD_BRIGHTNESS 0x58 +#define AUXDISPLAY_ITRON_CMD_DISPLAY_CLEAR 0x0c +#define AUXDISPLAY_ITRON_CMD_CURSOR 0x43 +#define AUXDISPLAY_ITRON_CMD_CURSOR_SET 0x24 +#define AUXDISPLAY_ITRON_CMD_ACTION 0x28 +#define AUXDISPLAY_ITRON_CMD_N 0x61 +#define AUXDISPLAY_ITRON_CMD_SCREEN_SAVER 0x40 + +/* Time values when multithreading is disabled */ +#define AUXDISPLAY_ITRON_RESET_TIME K_MSEC(2) +#define AUXDISPLAY_ITRON_RESET_WAIT_TIME K_MSEC(101) +#define AUXDISPLAY_ITRON_BUSY_DELAY_TIME_CHECK K_MSEC(4) +#define AUXDISPLAY_ITRON_BUSY_WAIT_LOOPS 125 + +/* Time values when multithreading is enabled */ +#define AUXDISPLAY_ITRON_BUSY_MAX_TIME K_MSEC(500) + +struct auxdisplay_itron_data { + uint16_t character_x; + uint16_t character_y; + uint8_t brightness; + bool powered; +#ifdef CONFIG_MULTITHREADING + struct k_sem lock_sem; + struct k_sem busy_wait_sem; + struct gpio_callback busy_wait_callback; +#endif +}; + +struct auxdisplay_itron_config { + const struct device *uart; + struct auxdisplay_capabilities capabilities; + struct gpio_dt_spec reset_gpio; + struct gpio_dt_spec busy_gpio; +}; + +static int send_cmd(const struct device *dev, const uint8_t *command, uint8_t length, bool pm, + bool lock); +static int auxdisplay_itron_is_busy(const struct device *dev); +static int auxdisplay_itron_clear(const struct device *dev); +static int auxdisplay_itron_set_powered(const struct device *dev, bool enabled); + +#ifdef CONFIG_MULTITHREADING +void auxdisplay_itron_busy_gpio_change_callback(const struct device *port, + struct gpio_callback *cb, + gpio_port_pins_t pins) +{ + struct auxdisplay_itron_data *data = CONTAINER_OF(cb, + struct auxdisplay_itron_data, busy_wait_callback); + k_sem_give(&data->busy_wait_sem); +} +#endif + +static int auxdisplay_itron_init(const struct device *dev) +{ + const struct auxdisplay_itron_config *config = dev->config; + struct auxdisplay_itron_data *data = dev->data; + int rc; + + if (!device_is_ready(config->uart)) { + LOG_ERR("UART device not ready"); + return -ENODEV; + } + + /* Configure and set busy GPIO */ + if (config->busy_gpio.port) { + rc = gpio_pin_configure_dt(&config->busy_gpio, GPIO_INPUT); + + if (rc < 0) { + LOG_ERR("Configuration of text display busy GPIO failed: %d", rc); + return rc; + } + +#ifdef CONFIG_MULTITHREADING + k_sem_init(&data->lock_sem, 1, 1); + k_sem_init(&data->busy_wait_sem, 0, 1); + + gpio_init_callback(&data->busy_wait_callback, + auxdisplay_itron_busy_gpio_change_callback, + BIT(config->busy_gpio.pin)); + rc = gpio_add_callback(config->busy_gpio.port, &data->busy_wait_callback); + + if (rc != 0) { + LOG_ERR("Configuration of busy interrupt failed: %d", rc); + return rc; + } +#endif + } + + /* Configure and set reset GPIO */ + if (config->reset_gpio.port) { + rc = gpio_pin_configure_dt(&config->reset_gpio, GPIO_OUTPUT_INACTIVE); + if (rc < 0) { + LOG_ERR("Configuration of text display reset GPIO failed"); + return rc; + } + } + + data->character_x = 0; + data->character_y = 0; + data->brightness = 0; + + /* Reset display to known configuration */ + if (config->reset_gpio.port) { + uint8_t wait_loops = 0; + + gpio_pin_set_dt(&config->reset_gpio, 1); + k_sleep(AUXDISPLAY_ITRON_RESET_TIME); + gpio_pin_set_dt(&config->reset_gpio, 0); + k_sleep(AUXDISPLAY_ITRON_RESET_WAIT_TIME); + + while (auxdisplay_itron_is_busy(dev) == 1) { + /* Display is busy, wait */ + k_sleep(AUXDISPLAY_ITRON_BUSY_DELAY_TIME_CHECK); + ++wait_loops; + + if (wait_loops >= AUXDISPLAY_ITRON_BUSY_WAIT_LOOPS) { + /* Waited long enough for display not to be busy, bailing */ + return -EIO; + } + } + } else { + /* Ensure display is powered on so that it can be initialised */ + (void)auxdisplay_itron_set_powered(dev, true); + auxdisplay_itron_clear(dev); + } + + return 0; +} + +static int auxdisplay_itron_set_powered(const struct device *dev, bool enabled) +{ + int rc = 0; + uint8_t cmd[] = {AUXDISPLAY_ITRON_CMD_USER_SETTING, AUXDISPLAY_ITRON_CMD_ACTION, + AUXDISPLAY_ITRON_CMD_N, AUXDISPLAY_ITRON_CMD_SCREEN_SAVER, 0}; + + if (enabled) { + cmd[4] = 1; + } + + return send_cmd(dev, cmd, sizeof(cmd), true, true); +} + +static bool auxdisplay_itron_is_powered(const struct device *dev) +{ + struct auxdisplay_itron_data *data = dev->data; + bool is_powered; + +#ifdef CONFIG_MULTITHREADING + k_sem_take(&data->lock_sem, K_FOREVER); +#endif + + is_powered = data->powered; + +#ifdef CONFIG_MULTITHREADING + k_sem_give(&data->lock_sem); +#endif + + return is_powered; +} + +static int auxdisplay_itron_display_on(const struct device *dev) +{ + return auxdisplay_itron_set_powered(dev, true); +} + +static int auxdisplay_itron_display_off(const struct device *dev) +{ + return auxdisplay_itron_set_powered(dev, false); +} + +static int auxdisplay_itron_cursor_set_enabled(const struct device *dev, bool enabled) +{ + uint8_t cmd[] = {AUXDISPLAY_ITRON_CMD_USER_SETTING, AUXDISPLAY_ITRON_CMD_CURSOR, + (uint8_t)enabled}; + + return send_cmd(dev, cmd, sizeof(cmd), false, true); +} + +static int auxdisplay_itron_cursor_position_set(const struct device *dev, + enum auxdisplay_position type, + int16_t x, int16_t y) +{ + uint8_t cmd[] = {AUXDISPLAY_ITRON_CMD_USER_SETTING, AUXDISPLAY_ITRON_CMD_CURSOR_SET, + 0, 0, 0, 0}; + + if (type != AUXDISPLAY_POSITION_ABSOLUTE) { + return -EINVAL; + } + + sys_put_le16(x, &cmd[2]); + sys_put_le16(y, &cmd[4]); + + return send_cmd(dev, cmd, sizeof(cmd), false, true); +} + +static int auxdisplay_itron_capabilities_get(const struct device *dev, + struct auxdisplay_capabilities *capabilities) +{ + const struct auxdisplay_itron_config *config = dev->config; + + memcpy(capabilities, &config->capabilities, sizeof(struct auxdisplay_capabilities)); + + return 0; +} + +static int auxdisplay_itron_clear(const struct device *dev) +{ + uint8_t cmd[] = {AUXDISPLAY_ITRON_CMD_DISPLAY_CLEAR}; + + return send_cmd(dev, cmd, sizeof(cmd), false, true); +} + +static int auxdisplay_itron_brightness_get(const struct device *dev, uint8_t *brightness) +{ + struct auxdisplay_itron_data *data = dev->data; + +#ifdef CONFIG_MULTITHREADING + k_sem_take(&data->lock_sem, K_FOREVER); +#endif + + *brightness = data->brightness; + +#ifdef CONFIG_MULTITHREADING + k_sem_give(&data->lock_sem); +#endif + + return 0; +} + +static int auxdisplay_itron_brightness_set(const struct device *dev, uint8_t brightness) +{ + struct auxdisplay_itron_data *data = dev->data; + uint8_t cmd[] = {AUXDISPLAY_ITRON_CMD_USER_SETTING, AUXDISPLAY_ITRON_CMD_BRIGHTNESS, + brightness}; + int rc; + + if (brightness < AUXDISPLAY_ITRON_BRIGHTNESS_MIN || + brightness > AUXDISPLAY_ITRON_BRIGHTNESS_MAX) { + return -EINVAL; + } + +#ifdef CONFIG_MULTITHREADING + k_sem_take(&data->lock_sem, K_FOREVER); +#endif + + rc = send_cmd(dev, cmd, sizeof(cmd), false, false); + + if (rc == 0) { + data->brightness = brightness; + } + +#ifdef CONFIG_MULTITHREADING + k_sem_give(&data->lock_sem); +#endif + + return rc; +} + +static int auxdisplay_itron_is_busy(const struct device *dev) +{ + const struct auxdisplay_itron_config *config = dev->config; + int rc; + + if (config->busy_gpio.port == NULL) { + return -ENOTSUP; + } + + rc = gpio_pin_get_dt(&config->busy_gpio); + + return rc; +} + +static int auxdisplay_itron_is_busy_check(const struct device *dev) +{ + struct auxdisplay_itron_data *data = dev->data; + int rc; + +#ifdef CONFIG_MULTITHREADING + k_sem_take(&data->lock_sem, K_FOREVER); +#endif + + rc = auxdisplay_itron_is_busy(dev); + +#ifdef CONFIG_MULTITHREADING + k_sem_give(&data->lock_sem); +#endif + + return rc; +} + +static int send_cmd(const struct device *dev, const uint8_t *command, uint8_t length, bool pm, + bool lock) +{ + uint8_t i = 0; + const struct auxdisplay_itron_config *config = dev->config; + const struct device *uart = config->uart; + int rc = 0; +#ifdef CONFIG_MULTITHREADING + struct auxdisplay_itron_data *data = dev->data; +#endif + + if (pm == false && auxdisplay_itron_is_powered(dev) == false) { + /* Display is not powered, only PM commands can be used */ + return -ESHUTDOWN; + } + +#ifdef CONFIG_MULTITHREADING + if (lock) { + k_sem_take(&data->lock_sem, K_FOREVER); + } +#endif + +#ifdef CONFIG_MULTITHREADING + /* Enable interrupt triggering */ + rc = gpio_pin_interrupt_configure_dt(&config->busy_gpio, GPIO_INT_EDGE_TO_INACTIVE); + + if (rc != 0) { + LOG_ERR("Failed to enable busy interrupt: %d", rc); + goto end; + } +#endif + + while (i < length) { +#ifdef CONFIG_MULTITHREADING + if (auxdisplay_itron_is_busy(dev) == 1) { + if (k_sem_take(&data->busy_wait_sem, + AUXDISPLAY_ITRON_BUSY_MAX_TIME) != 0) { + rc = -EIO; + goto cleanup; + } + } +#else + uint8_t wait_loops = 0; + + while (auxdisplay_itron_is_busy(dev) == 1) { + /* Display is busy, wait */ + k_sleep(AUXDISPLAY_ITRON_BUSY_DELAY_TIME_CHECK); + ++wait_loops; + + if (wait_loops >= AUXDISPLAY_ITRON_BUSY_WAIT_LOOPS) { + /* Waited long enough for display not to be busy, bailing */ + return -EIO; + } + } +#endif + + uart_poll_out(uart, command[i]); + ++i; + } + +#ifdef CONFIG_MULTITHREADING +cleanup: + (void)gpio_pin_interrupt_configure_dt(&config->busy_gpio, GPIO_INT_DISABLE); +#endif + +end: +#ifdef CONFIG_MULTITHREADING + if (lock) { + k_sem_give(&data->lock_sem); + } +#endif + + return rc; +} + +static int auxdisplay_itron_write(const struct device *dev, const uint8_t *data, uint16_t len) +{ + uint16_t i = 0; + + /* Check all characters are valid */ + while (i < len) { + if (data[i] < AUXDISPLAY_ITRON_CHARACTER_MIN && + data[i] != AUXDISPLAY_ITRON_CHARACTER_BACK_SPACE && + data[i] != AUXDISPLAY_ITRON_CHARACTER_TAB && + data[i] != AUXDISPLAY_ITRON_CHARACTER_LINE_FEED && + data[i] != AUXDISPLAY_ITRON_CHARACTER_CARRIAGE_RETURN) { + return -EINVAL; + } + + ++i; + } + + return send_cmd(dev, data, len, false, true); +} + +static const struct auxdisplay_driver_api auxdisplay_itron_auxdisplay_api = { + .display_on = auxdisplay_itron_display_on, + .display_off = auxdisplay_itron_display_off, + .cursor_set_enabled = auxdisplay_itron_cursor_set_enabled, + .cursor_position_set = auxdisplay_itron_cursor_position_set, + .capabilities_get = auxdisplay_itron_capabilities_get, + .clear = auxdisplay_itron_clear, + .brightness_get = auxdisplay_itron_brightness_get, + .brightness_set = auxdisplay_itron_brightness_set, + .is_busy = auxdisplay_itron_is_busy_check, + .write = auxdisplay_itron_write, +}; + +#define AUXDISPLAY_ITRON_DEVICE(inst) \ + static struct auxdisplay_itron_data auxdisplay_itron_data_##inst; \ + static const struct auxdisplay_itron_config auxdisplay_itron_config_##inst = { \ + .uart = DEVICE_DT_GET(DT_INST_BUS(inst)), \ + .capabilities = { \ + .columns = DT_INST_PROP(inst, columns), \ + .rows = DT_INST_PROP(inst, rows), \ + .mode = AUXDISPLAY_ITRON_MODE_UART, \ + .brightness.minimum = AUXDISPLAY_ITRON_BRIGHTNESS_MIN, \ + .brightness.maximum = AUXDISPLAY_ITRON_BRIGHTNESS_MAX, \ + .backlight.minimum = AUXDISPLAY_LIGHT_NOT_SUPPORTED, \ + .backlight.maximum = AUXDISPLAY_LIGHT_NOT_SUPPORTED, \ + }, \ + .busy_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, busy_gpios, {0}), \ + .reset_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, reset_gpios, {0}), \ + }; \ + DEVICE_DT_INST_DEFINE(inst, \ + &auxdisplay_itron_init, \ + NULL, \ + &auxdisplay_itron_data_##inst, \ + &auxdisplay_itron_config_##inst, \ + POST_KERNEL, \ + CONFIG_AUXDISPLAY_INIT_PRIORITY, \ + &auxdisplay_itron_auxdisplay_api); + +DT_INST_FOREACH_STATUS_OKAY(AUXDISPLAY_ITRON_DEVICE) diff --git a/drivers/auxdisplay/auxdisplay_itron.h b/drivers/auxdisplay/auxdisplay_itron.h new file mode 100644 index 00000000000..91e0b2eb5e3 --- /dev/null +++ b/drivers/auxdisplay/auxdisplay_itron.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2022-2023 Jamie McCrae + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef H_AUXDISPLAY_ITRON_ +#define H_AUXDISPLAY_ITRON_ + +#define AUXDISPLAY_ITRON_BRIGHTNESS_MIN 1 +#define AUXDISPLAY_ITRON_BRIGHTNESS_MAX 8 + +#define AUXDISPLAY_ITRON_CHARACTER_MIN 0x20 +#define AUXDISPLAY_ITRON_CHARACTER_BACK_SPACE 0x08 +#define AUXDISPLAY_ITRON_CHARACTER_TAB 0x09 +#define AUXDISPLAY_ITRON_CHARACTER_LINE_FEED 0x0a +#define AUXDISPLAY_ITRON_CHARACTER_CARRIAGE_RETURN 0x0d + +enum { + AUXDISPLAY_ITRON_MODE_UNKNOWN = 0, + AUXDISPLAY_ITRON_MODE_UART, +}; + +#endif /* H_AUXDISPLAY_ITRON_ */ diff --git a/dts/bindings/auxdisplay/noritake,itron.yaml b/dts/bindings/auxdisplay/noritake,itron.yaml new file mode 100644 index 00000000000..670363fab69 --- /dev/null +++ b/dts/bindings/auxdisplay/noritake,itron.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2022 Jamie McCrae +# +# SPDX-License-Identifier: Apache-2.0 +# + +description: Noritake Itron VFD + +compatible: "noritake,itron" + +include: [auxdisplay-device.yaml, uart-device.yaml] + +properties: + reset-gpios: + type: phandle-array + description: Optional GPIO used to reset the display + + busy-gpios: + type: phandle-array + description: Optional GPIO used for busy detection diff --git a/dts/bindings/vendor-prefixes.txt b/dts/bindings/vendor-prefixes.txt index dd01c5b57a6..8668bee1b0f 100644 --- a/dts/bindings/vendor-prefixes.txt +++ b/dts/bindings/vendor-prefixes.txt @@ -416,6 +416,7 @@ nintendo Nintendo nlt NLT Technologies, Ltd. nokia Nokia nordic Nordic Semiconductor +noritake Noritake Co., Inc. Electronics Division novtech NovTech, Inc. nutsboard NutsBoard nuclei Nuclei System Technology