drivers: auxdisplay: Add noritake itron VFD auxiliary display

Adds the driver for a Noritake Itron VFD auxiliary display.

Signed-off-by: Jamie McCrae <spam@helper3000.net>
diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt
index a043a8b..31177ae 100644
--- a/drivers/CMakeLists.txt
+++ b/drivers/CMakeLists.txt
@@ -24,6 +24,7 @@
 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 94e5e6c..d615249 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 0000000..6ad590d
--- /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 0000000..d6f2477
--- /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 0000000..44873f7
--- /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 0000000..1783d8d
--- /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 <string.h>
+#include <zephyr/device.h>
+#include <zephyr/devicetree.h>
+#include <zephyr/drivers/auxdisplay.h>
+#include <zephyr/drivers/gpio.h>
+#include <zephyr/drivers/uart.h>
+#include <zephyr/sys/byteorder.h>
+#include <zephyr/logging/log.h>
+#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 0000000..91e0b2e
--- /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 0000000..670363f
--- /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 dd01c5b..8668bee 100644
--- a/dts/bindings/vendor-prefixes.txt
+++ b/dts/bindings/vendor-prefixes.txt
@@ -416,6 +416,7 @@
 nlt	NLT Technologies, Ltd.
 nokia	Nokia
 nordic	Nordic Semiconductor
+noritake	Noritake Co., Inc. Electronics Division
 novtech	NovTech, Inc.
 nutsboard	NutsBoard
 nuclei	Nuclei System Technology