drivers: regulator: add GPIO-controlled regulator driver

This provides structure for the regulator device hierarchy and a
driver for GPIO-controlled regulators along with its binding.

Signed-off-by: Peter A. Bigot <pab@pabigot.com>
diff --git a/CODEOWNERS b/CODEOWNERS
index 76b912a..11e46c7 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -233,6 +233,7 @@
 /drivers/pwm/*npcx*                       @MulinChao
 /drivers/pwm/*sam0*                       @nzmichaelh
 /drivers/pwm/*stm32*                      @gmarull
+/drivers/regulator/                       @pabigot
 /drivers/sensor/                          @MaureenHelm
 /drivers/sensor/ams_iAQcore/              @alexanderwachter
 /drivers/sensor/ens210/                   @alexanderwachter
diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt
index db17b7d..07cd269 100644
--- a/drivers/CMakeLists.txt
+++ b/drivers/CMakeLists.txt
@@ -39,6 +39,7 @@
 add_subdirectory_ifdef(CONFIG_EEPROM eeprom)
 add_subdirectory_ifdef(CONFIG_LORA lora)
 add_subdirectory_ifdef(CONFIG_PECI peci)
+add_subdirectory_ifdef(CONFIG_REGULATOR regulator)
 
 add_subdirectory_ifdef(CONFIG_FLASH_HAS_DRIVER_ENABLED flash)
 add_subdirectory_ifdef(CONFIG_SERIAL_HAS_DRIVER serial)
diff --git a/drivers/Kconfig b/drivers/Kconfig
index eafe17c..3491113 100644
--- a/drivers/Kconfig
+++ b/drivers/Kconfig
@@ -99,4 +99,6 @@
 
 source "drivers/peci/Kconfig"
 
+source "drivers/regulator/Kconfig"
+
 endmenu
diff --git a/drivers/regulator/CMakeLists.txt b/drivers/regulator/CMakeLists.txt
new file mode 100644
index 0000000..e89256d
--- /dev/null
+++ b/drivers/regulator/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Copyright 2020 Peter Bigot Consulting, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+zephyr_library()
+
+zephyr_library_sources_ifdef(CONFIG_REGULATOR_FIXED regulator_fixed.c)
diff --git a/drivers/regulator/Kconfig b/drivers/regulator/Kconfig
new file mode 100644
index 0000000..8c879d4
--- /dev/null
+++ b/drivers/regulator/Kconfig
@@ -0,0 +1,18 @@
+# Copyright 2020 Peter Bigot Consulting, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+menuconfig REGULATOR
+	bool "Regulator drivers"
+	default $(dt_compat_enabled,$(DT_COMPAT_REGULATOR_FIXED))
+	help
+	  Include drivers for current/voltage regulators in system config
+
+if REGULATOR
+
+module = REGULATOR
+module-str = regulator
+source "subsys/logging/Kconfig.template.log_config"
+
+source "drivers/regulator/Kconfig.fixed"
+
+endif # REGULATOR
diff --git a/drivers/regulator/Kconfig.fixed b/drivers/regulator/Kconfig.fixed
new file mode 100644
index 0000000..2dcfbaa
--- /dev/null
+++ b/drivers/regulator/Kconfig.fixed
@@ -0,0 +1,19 @@
+# Copyright 2020 Peter Bigot Consulting, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+menuconfig REGULATOR_FIXED
+	bool "GPIO-controlled regulators"
+	default $(dt_compat_enabled,$(DT_COMPAT_REGULATOR_FIXED))
+	depends on GPIO
+	help
+	  Enable the driver for GPIO-controlled regulators
+
+if REGULATOR_FIXED
+
+config REGULATOR_FIXED_INIT_PRIORITY
+	int "Init priority"
+	default 75
+	help
+	  Device driver initialization priority
+
+endif # REGULATOR_FIXED
diff --git a/drivers/regulator/regulator_fixed.c b/drivers/regulator/regulator_fixed.c
new file mode 100644
index 0000000..89e0917
--- /dev/null
+++ b/drivers/regulator/regulator_fixed.c
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2019-2020 Peter Bigot Consulting, LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT regulator_fixed
+
+#include <kernel.h>
+#include <drivers/regulator.h>
+#include <drivers/gpio.h>
+
+#include <logging/log.h>
+LOG_MODULE_REGISTER(regulator_fixed, CONFIG_REGULATOR_LOG_LEVEL);
+
+#define OPTION_ALWAYS_ON_POS 0
+#define OPTION_ALWAYS_ON BIT(OPTION_ALWAYS_ON_POS)
+#define OPTION_BOOT_ON_POS 1
+#define OPTION_BOOT_ON BIT(OPTION_BOOT_ON_POS)
+
+struct driver_config {
+	const char *regulator_name;
+	const char *gpio_name;
+	uint32_t startup_delay_us;
+	uint32_t off_on_delay_us;
+	gpio_pin_t gpio_pin;
+	gpio_dt_flags_t gpio_flags;
+	uint8_t options;
+};
+
+enum work_task {
+	WORK_TASK_UNDEFINED,
+	WORK_TASK_ENABLE,
+	WORK_TASK_DISABLE,
+	WORK_TASK_DELAY,
+};
+
+struct driver_data_onoff {
+	const struct device *gpio;
+	const struct device *dev;
+	struct onoff_manager mgr;
+#ifdef CONFIG_MULTITHREADING
+	struct k_delayed_work delayed_work;
+#endif /* CONFIG_MULTITHREADING */
+	onoff_notify_fn notify;
+	enum work_task task;
+};
+
+/* Common initialization of GPIO device and pin state.
+ *
+ * @param dev the regulator device, whether sync or onoff
+ *
+ * @param gpiop where to store the GPIO device pointer
+ *
+ * @return negative on error, otherwise zero.
+ */
+static int common_init(const struct device *dev,
+		       const struct device **gpiop)
+{
+	const struct driver_config *cfg = dev->config;
+	const struct device *gpio = device_get_binding(cfg->gpio_name);
+
+	if (gpio == NULL) {
+		LOG_ERR("no GPIO device: %s", cfg->gpio_name);
+		return -ENODEV;
+	}
+
+	*gpiop = gpio;
+
+	gpio_flags_t flags = cfg->gpio_flags;
+	bool on = cfg->options & (OPTION_ALWAYS_ON | OPTION_BOOT_ON);
+	uint32_t delay_us = 0;
+
+	if (on) {
+		flags |= GPIO_OUTPUT_ACTIVE;
+		delay_us = cfg->startup_delay_us;
+	} else {
+		flags |= GPIO_OUTPUT_INACTIVE;
+	}
+
+	int rc = gpio_pin_configure(gpio, cfg->gpio_pin, flags);
+
+	if ((rc == 0) && (delay_us > 0)) {
+		/* Turned on and we have to wait until the on
+		 * completes.  Since this is in the driver init we
+		 * can't sleep.
+		 */
+		k_busy_wait(delay_us);
+	}
+
+	return rc;
+}
+
+static void finalize_transition(struct driver_data_onoff *data,
+				onoff_notify_fn notify,
+				uint32_t delay_us,
+				int rc)
+{
+	const struct driver_config *cfg = data->dev->config;
+
+	LOG_DBG("%s: finalize %d delay %u us", cfg->regulator_name, rc, delay_us);
+
+	/* If there's no error and we have to delay, do so. */
+	if ((rc >= 0) && (delay_us > 0)) {
+		/* If the delay is less than a tick or we're not
+		 * sleep-capable we have to busy-wait.
+		 */
+		if ((k_us_to_ticks_floor32(delay_us) == 0)
+		    || k_is_pre_kernel()
+		    || !IS_ENABLED(CONFIG_MULTITHREADING)) {
+			k_busy_wait(delay_us);
+#ifdef CONFIG_MULTITHREADING
+		} else {
+			/* Otherwise sleep in the work queue. */
+			__ASSERT_NO_MSG(data->task == WORK_TASK_UNDEFINED);
+			data->task = WORK_TASK_DELAY;
+			data->notify = notify;
+			rc = k_delayed_work_submit(&data->delayed_work,
+						   K_USEC(delay_us));
+			if (rc == 0) {
+				return;
+			}
+#endif /* CONFIG_MULTITHREADING */
+		}
+	}
+
+	notify(&data->mgr, rc);
+}
+
+#ifdef CONFIG_MULTITHREADING
+/* The worker is used for several things:
+ *
+ * * If a transition occurred in a context where the GPIO state could
+ *   not be changed that's done here.
+ * * If a start or stop transition requires a delay that exceeds one
+ *   tick the notification after the delay is performed here.
+ */
+static void onoff_worker(struct k_work *work)
+{
+	struct k_delayed_work *delayed_work
+		= CONTAINER_OF(work, struct k_delayed_work, work);
+	struct driver_data_onoff *data
+		= CONTAINER_OF(delayed_work, struct driver_data_onoff,
+			       delayed_work);
+	onoff_notify_fn notify = data->notify;
+	const struct driver_config *cfg = data->dev->config;
+	uint32_t delay_us = 0;
+	int rc = 0;
+
+	if (data->task == WORK_TASK_ENABLE) {
+		rc = gpio_pin_set(data->gpio, cfg->gpio_pin, true);
+		LOG_DBG("%s: work enable: %d", cfg->regulator_name, rc);
+		delay_us = cfg->startup_delay_us;
+	} else if (data->task == WORK_TASK_DISABLE) {
+		rc = gpio_pin_set(data->gpio, cfg->gpio_pin, false);
+		LOG_DBG("%s: work disable: %d", cfg->regulator_name, rc);
+		delay_us = cfg->off_on_delay_us;
+	} else if (data->task == WORK_TASK_DELAY) {
+		LOG_DBG("%s: work delay complete", cfg->regulator_name);
+	}
+
+	data->notify = NULL;
+	data->task = WORK_TASK_UNDEFINED;
+	finalize_transition(data, notify, delay_us, rc);
+}
+#endif /* CONFIG_MULTITHREADING */
+
+static void start(struct onoff_manager *mgr,
+		  onoff_notify_fn notify)
+{
+	struct driver_data_onoff *data =
+		CONTAINER_OF(mgr, struct driver_data_onoff, mgr);
+	const struct driver_config *cfg = data->dev->config;
+	uint32_t delay_us = cfg->startup_delay_us;
+	int rc = 0;
+
+	LOG_DBG("%s: start", cfg->regulator_name);
+
+	if ((cfg->options & OPTION_ALWAYS_ON) != 0) {
+		delay_us = 0;
+		goto finalize;
+	}
+
+	rc = gpio_pin_set(data->gpio, cfg->gpio_pin, true);
+
+#ifdef CONFIG_MULTITHREADING
+	if (rc == -EWOULDBLOCK) {
+		/* Perform the enable and finalization in a work item.
+		 */
+		LOG_DBG("%s: start deferred", cfg->regulator_name);
+		__ASSERT_NO_MSG(data->task == WORK_TASK_UNDEFINED);
+		data->task = WORK_TASK_ENABLE;
+		data->notify = notify;
+		k_work_submit(&data->delayed_work.work);
+		return;
+	}
+#endif /* CONFIG_MULTITHREADING */
+
+finalize:
+	finalize_transition(data, notify, delay_us, rc);
+
+	return;
+}
+
+static void stop(struct onoff_manager *mgr,
+		 onoff_notify_fn notify)
+{
+	struct driver_data_onoff *data =
+		CONTAINER_OF(mgr, struct driver_data_onoff, mgr);
+	const struct driver_config *cfg = data->dev->config;
+	uint32_t delay_us = cfg->off_on_delay_us;
+	int rc = 0;
+
+	LOG_DBG("%s: stop", cfg->regulator_name);
+
+	if ((cfg->options & OPTION_ALWAYS_ON) != 0) {
+		delay_us = 0;
+		goto finalize;
+	}
+
+	rc = gpio_pin_set(data->gpio, cfg->gpio_pin, false);
+
+#ifdef CONFIG_MULTITHREADING
+	if (rc == -EWOULDBLOCK) {
+		/* Perform the disable and finalization in a work
+		 * item.
+		 */
+		LOG_DBG("%s: stop deferred", cfg->regulator_name);
+		__ASSERT_NO_MSG(data->task == WORK_TASK_UNDEFINED);
+		data->task = WORK_TASK_DISABLE;
+		data->notify = notify;
+		k_work_submit(&data->delayed_work.work);
+		return;
+	}
+#endif /* CONFIG_MULTITHREADING */
+
+finalize:
+	finalize_transition(data, notify, delay_us, rc);
+
+	return;
+}
+
+static int enable_onoff(const struct device *dev, struct onoff_client *cli)
+{
+	struct driver_data_onoff *data = dev->data;
+
+	return onoff_request(&data->mgr, cli);
+}
+
+static int disable_onoff(const struct device *dev)
+{
+	struct driver_data_onoff *data = dev->data;
+
+	return onoff_release(&data->mgr);
+}
+
+static const struct onoff_transitions transitions =
+	ONOFF_TRANSITIONS_INITIALIZER(start, stop, NULL);
+
+static const struct regulator_driver_api api_onoff = {
+	.enable = enable_onoff,
+	.disable = disable_onoff,
+};
+
+static int regulator_fixed_init_onoff(const struct device *dev)
+{
+	struct driver_data_onoff *data = dev->data;
+	int rc = common_init(dev, &data->gpio);
+
+	data->dev = dev;
+	onoff_manager_init(&data->mgr, &transitions);
+#ifdef CONFIG_MULTITHREADING
+	k_delayed_work_init(&data->delayed_work, onoff_worker);
+#endif /* CONFIG_MULTITHREADING */
+
+	if (rc >= 0) {
+		rc = 0;
+	}
+
+	LOG_INF("%s onoff: %d", dev->name, rc);
+
+	return rc;
+}
+
+struct driver_data_sync {
+	const struct device *gpio;
+	struct onoff_sync_service srv;
+};
+
+#if DT_HAS_COMPAT_STATUS_OKAY(regulator_fixed_sync) - 0
+
+static int enable_sync(const struct device *dev, struct onoff_client *cli)
+{
+	struct driver_data_sync *data = dev->data;
+	const struct driver_config *cfg = dev->config;
+	k_spinlock_key_t key;
+	int rc = onoff_sync_lock(&data->srv, &key);
+
+	if ((rc == 0)
+	    && ((cfg->options & OPTION_ALWAYS_ON) == 0)) {
+		rc = gpio_pin_set(data->gpio, cfg->gpio_pin, true);
+	}
+
+	return onoff_sync_finalize(&data->srv, key, cli, rc, true);
+}
+
+static int disable_sync(const struct device *dev)
+{
+	struct driver_data_sync *data = dev->data;
+	const struct driver_config *cfg = dev->config;
+	k_spinlock_key_t key;
+	int rc = onoff_sync_lock(&data->srv, &key);
+
+	if  ((cfg->options & OPTION_ALWAYS_ON) != 0) {
+		rc = 0;
+	} else if (rc == 1) {
+		rc = gpio_pin_set(data->gpio, cfg->gpio_pin, false);
+	} else if (rc == 0) {
+		rc = -EINVAL;
+	} /* else rc > 0, leave it on */
+
+	return onoff_sync_finalize(&data->srv, key, NULL, rc, false);
+}
+
+static const struct regulator_driver_api api_sync = {
+	.enable = enable_sync,
+	.disable = disable_sync,
+};
+
+static int regulator_fixed_init_sync(const struct device *dev)
+{
+	struct driver_data_sync *data = dev->data;
+	const struct driver_config *cfg = dev->config;
+	int rc = common_init(dev, &data->gpio);
+
+	(void)regulator_fixed_init_onoff;
+	(void)api_onoff;
+	(void)cfg;
+
+	__ASSERT(cfg->startup_delay_us == 0,
+		 "sync not valid with startup delay");
+	__ASSERT(cfg->off_on_delay_us == 0,
+		 "sync not valid with shutdown delay");
+
+	LOG_INF("%s sync: %d", dev->name, rc);
+
+	return rc;
+}
+
+#endif /* DT_HAS_COMPAT_STATUS_OK(regulator_fixed_sync) */
+
+/* This should also check:
+ *  && DT_INST_PROP(id, startup_delay_us) == 0
+ *  && DT_INST_PROP(id, off_on_delay_us) == 0
+ * but the preprocessor magic doesn't seem able to do that so we'll assert
+ * in init instead.
+ */
+#define REG_IS_SYNC(id)						\
+	DT_NODE_HAS_COMPAT(DT_DRV_INST(id), regulator_fixed_sync)
+
+#define REG_DATA_TAG(id) COND_CODE_1(REG_IS_SYNC(id),	\
+		(driver_data_sync),			\
+		(driver_data_onoff))
+#define REG_API(id) COND_CODE_1(REG_IS_SYNC(id),	\
+		(api_sync),				\
+		(api_onoff))
+#define REG_INIT(id) COND_CODE_1(REG_IS_SYNC(id),	\
+		(regulator_fixed_init_sync),		\
+		(regulator_fixed_init_onoff))
+
+#define REGULATOR_DEVICE(id) \
+static const struct driver_config regulator_##id##_cfg = { \
+	.regulator_name = DT_INST_PROP(id, regulator_name), \
+	.gpio_name = DT_INST_GPIO_LABEL(id, enable_gpios), \
+	.startup_delay_us = DT_INST_PROP(id, startup_delay_us), \
+	.off_on_delay_us = DT_INST_PROP(id, off_on_delay_us), \
+	.gpio_pin = DT_INST_GPIO_PIN(id, enable_gpios),	\
+	.gpio_flags = DT_INST_GPIO_FLAGS(id, enable_gpios), \
+	.options = (DT_INST_PROP(id, regulator_boot_on)	\
+		    << OPTION_BOOT_ON_POS) \
+		  | (DT_INST_PROP(id, regulator_always_on) \
+		     << OPTION_ALWAYS_ON_POS), \
+}; \
+\
+static struct REG_DATA_TAG(id) regulator_##id##_data; \
+\
+DEVICE_AND_API_INIT(regulator_##id, DT_INST_LABEL(id), \
+		    REG_INIT(id), \
+		    &regulator_##id##_data, &regulator_##id##_cfg, \
+		    POST_KERNEL, CONFIG_REGULATOR_FIXED_INIT_PRIORITY, \
+		    &REG_API(id));
+
+DT_INST_FOREACH_STATUS_OKAY(REGULATOR_DEVICE)
diff --git a/dts/bindings/regulator/regulator-fixed.yaml b/dts/bindings/regulator/regulator-fixed.yaml
new file mode 100644
index 0000000..73ceab8
--- /dev/null
+++ b/dts/bindings/regulator/regulator-fixed.yaml
@@ -0,0 +1,50 @@
+# Copyright 2019-2020 Peter Bigot Consulting, LLC
+# SPDX-License-Identifier: Apache-2.0
+
+description: GPIO-controlled regulators
+
+include: regulator.yaml
+
+# NOTE: The driver supports "regulator-fixed-sync" as a specializing
+# variant when it's known that the GPIO state change operations are
+# isr-ok and not sleep, and both startup and off-on delays are zero.
+# This bypasses the asynchronous onoff manager which optimizes both time
+# and memory use.
+#
+# To enable this use both:
+#    compatible = "regulator-fixed-sync", "regulator-fixed";
+#
+
+compatible: "regulator-fixed"
+
+properties:
+  label:
+    required: true
+
+  regulator-name:
+    type: string
+    required: true
+    description: Descriptive name for regulator outputs.
+
+  enable-gpios:
+    type: phandle-array
+    required: true
+    description: |
+      GPIO to use to enable/disable the regulator.
+
+      Unlike the gpio property in the Linux bindings this array must
+      provide the GPIO polarity and open-drain status in the phandle
+      selector.  The Linux enable-active-high and gpio-open-drain
+      properties are not valid for Zephyr devicetree files.
+
+  startup-delay-us:
+    type: int
+    required: false
+    default: 0
+    description: Startup time, in microseconds
+
+  off-on-delay-us:
+    type: int
+    required: false
+    default: 0
+    description: Off delay time, in microseconds