drivers: adc: add DMA support for Silabs IADC

This commit introduces DMA support for the Silabs IADC driver.
A new Kconfig option is added to enable DMA support, ensuring
compatibility with the existing ADC configuration.

DMA can be used for synch/asynch operation.

Signed-off-by: Martin Hoff <martin.hoff@silabs.com>
diff --git a/drivers/adc/Kconfig.silabs b/drivers/adc/Kconfig.silabs
index 244edab..2d954a3 100644
--- a/drivers/adc/Kconfig.silabs
+++ b/drivers/adc/Kconfig.silabs
@@ -9,3 +9,12 @@
 	select ADC_CONFIGURABLE_INPUTS
 	help
 	  Enable the driver implementation for Silabs Incremental ADC
+
+config ADC_SILABS_IADC_DMA
+	bool "Silabs IADC async DMA support"
+	depends on ADC_SILABS_IADC
+	depends on DMA
+	help
+	  Enable DMA support with the Silabs IADC driver.
+	  This allows ADC conversions to be performed (asynchronously or not)
+	  using DMA for improved performance.
diff --git a/drivers/adc/adc_silabs_iadc.c b/drivers/adc/adc_silabs_iadc.c
index 81319f4..fe731c2 100644
--- a/drivers/adc/adc_silabs_iadc.c
+++ b/drivers/adc/adc_silabs_iadc.c
@@ -9,6 +9,8 @@
 #include <zephyr/drivers/adc.h>
 #include <zephyr/drivers/clock_control.h>
 #include <zephyr/drivers/clock_control/clock_control_silabs.h>
+#include <zephyr/drivers/dma.h>
+#include <zephyr/drivers/dma/dma_silabs_ldma.h>
 #include <zephyr/drivers/pinctrl.h>
 #include <zephyr/logging/log.h>
 #include <zephyr/pm/device.h>
@@ -45,6 +47,14 @@
 #define IADC_PORT_MASK 0xF0
 #define IADC_PIN_MASK  0x0F
 
+struct iadc_dma_channel {
+	const struct device *dma_dev;
+	struct dma_block_config blk_cfg;
+	struct dma_config dma_cfg;
+	int dma_channel;
+	bool enabled;
+};
+
 struct iadc_chan_conf {
 	sl_hal_iadc_analog_gain_t gain;
 	sl_hal_iadc_voltage_reference_t reference;
@@ -60,6 +70,7 @@
 	const struct device *dev;
 	struct adc_context ctx;
 	struct iadc_chan_conf chan_conf[SL_HAL_IADC_CHANNEL_ID_MAX];
+	struct iadc_dma_channel dma;
 	uint8_t adc_config_count; /* Number of ADC configs created (max 2) */
 	uint32_t clock_rate;
 	uint32_t channels;
@@ -119,6 +130,104 @@
 	};
 }
 
+#ifdef CONFIG_ADC_SILABS_IADC_DMA
+static int iadc_dma_init(const struct device *dev)
+{
+	const struct iadc_config *config = dev->config;
+	struct iadc_data *data = dev->data;
+	struct iadc_dma_channel *dma = &data->dma;
+
+	if (!dma->dma_dev) {
+		return 0;
+	}
+
+	if (!device_is_ready(dma->dma_dev)) {
+		LOG_ERR("DMA device not ready");
+		return -ENODEV;
+	}
+
+	dma->dma_channel = dma_request_channel(dma->dma_dev, NULL);
+	if (dma->dma_channel < 0) {
+		LOG_ERR("Failed to request DMA channel");
+		return -ENODEV;
+	}
+
+	memset(&dma->blk_cfg, 0, sizeof(dma->blk_cfg));
+	dma->blk_cfg.source_address = (uintptr_t)&(config->base)->SCANFIFODATA;
+	dma->blk_cfg.source_addr_adj = DMA_ADDR_ADJ_NO_CHANGE;
+	dma->blk_cfg.dest_addr_adj = DMA_ADDR_ADJ_INCREMENT;
+	dma->dma_cfg.complete_callback_en = 1;
+	dma->dma_cfg.channel_priority = 3;
+	dma->dma_cfg.channel_direction = PERIPHERAL_TO_MEMORY;
+	dma->dma_cfg.head_block = &dma->blk_cfg;
+	dma->dma_cfg.user_data = data;
+
+	return 0;
+}
+
+static int iadc_dma_start(const struct device *dev)
+{
+	struct iadc_data *data = dev->data;
+	struct iadc_dma_channel *dma = &data->dma;
+	int ret;
+
+	if (!dma->dma_dev) {
+		return -ENODEV;
+	}
+
+	if (dma->enabled) {
+		return -EBUSY;
+	}
+
+	ret = dma_config(dma->dma_dev, dma->dma_channel, &dma->dma_cfg);
+	if (ret) {
+		LOG_ERR("DMA config error: %d", ret);
+		return ret;
+	}
+
+	dma->enabled = true;
+
+	ret = dma_start(dma->dma_dev, dma->dma_channel);
+	if (ret) {
+		LOG_ERR("DMA start error: %d", ret);
+		dma->enabled = false;
+		return ret;
+	}
+
+	return 0;
+}
+
+static void iadc_dma_stop(const struct device *dev)
+{
+	struct iadc_data *data = dev->data;
+	struct iadc_dma_channel *dma = &data->dma;
+
+	if (!dma->enabled) {
+		return;
+	}
+
+	dma_stop(dma->dma_dev, dma->dma_channel);
+
+	dma->enabled = false;
+}
+
+static void iadc_dma_cb(const struct device *dma_dev, void *user_data, uint32_t channel, int status)
+{
+	struct iadc_data *data = user_data;
+	const struct device *dev = data->dev;
+
+	if (status < 0) {
+		LOG_ERR("DMA transfer error: %d", status);
+		adc_context_complete(&data->ctx, status);
+		return;
+	}
+
+	iadc_dma_stop(dev);
+
+	adc_context_on_sampling_done(&data->ctx, dev);
+}
+#endif /* CONFIG_ADC_SILABS_IADC_DMA */
+
 /* Oversampling and resolution are common for both ADC configs
  * because they are not configurable per channel inside a ADC
  * sequence and are common for a sequence.
@@ -149,8 +258,28 @@
 	uint32_t channels;
 	int res;
 
+	if (data->dma.dma_dev) {
+		scan_init.data_valid_level = _IADC_SCANFIFOCFG_DVL_VALID1;
+		/* Only needed to wake up DMA if EM is 2/3 */
+		scan_init.fifo_dma_wakeup = true;
+	}
+
 	data->adc_config_count = 0;
 
+	if (data->dma.dma_dev) {
+		if (data->alignment == _IADC_SCANFIFOCFG_ALIGNMENT_RIGHT20) {
+			data->dma.dma_cfg.source_data_size = 4;
+			data->dma.dma_cfg.dest_data_size = 4;
+			data->dma.dma_cfg.source_burst_length = 4;
+			data->dma.dma_cfg.dest_burst_length = 4;
+		} else {
+			data->dma.dma_cfg.source_data_size = 2;
+			data->dma.dma_cfg.dest_data_size = 2;
+			data->dma.dma_cfg.source_burst_length = 2;
+			data->dma.dma_cfg.dest_burst_length = 2;
+		}
+	}
+
 	channels = data->channels;
 
 	/*
@@ -340,6 +469,11 @@
 	data->buffer = sequence->buffer;
 	data->active_channels = channel_count;
 
+	if (data->dma.dma_dev) {
+		data->dma.blk_cfg.dest_address = (uintptr_t)data->buffer;
+		data->dma.blk_cfg.block_size = channel_count * data->data_size;
+	}
+
 	data->channels = sequence->channels;
 
 	res = iadc_set_config(data->dev);
@@ -357,9 +491,19 @@
 static void iadc_start_scan(const struct device *dev)
 {
 	const struct iadc_config *config = dev->config;
+	__maybe_unused struct iadc_data *data = dev->data;
 	IADC_TypeDef *iadc = (IADC_TypeDef *)config->base;
 
+#ifdef CONFIG_ADC_SILABS_IADC_DMA
+	if (data->dma.dma_dev) {
+		data->dma.blk_cfg.dest_address = (uintptr_t)data->buffer;
+		iadc_dma_start(dev);
+	} else {
+		sl_hal_iadc_enable_interrupts(iadc, IADC_IEN_SCANTABLEDONE);
+	}
+#else
 	sl_hal_iadc_enable_interrupts(iadc, IADC_IEN_SCANTABLEDONE);
+#endif
 
 	sl_hal_iadc_start_scan(iadc);
 }
@@ -569,6 +713,13 @@
 		return ret;
 	}
 
+#ifdef CONFIG_ADC_SILABS_IADC_DMA
+	ret = iadc_dma_init(dev);
+	if (ret < 0) {
+		data->dma.dma_dev = NULL;
+	}
+#endif
+
 	config->irq_cfg_func();
 
 	adc_context_unlock_unconditionally(&data->ctx);
@@ -585,6 +736,17 @@
 	.ref_internal = SL_HAL_IADC_DEFAULT_VREF,
 };
 
+#ifdef CONFIG_ADC_SILABS_IADC_DMA
+#define IADC_DMA_CHANNEL_INIT(n)                                                                   \
+	.dma.dma_dev = DEVICE_DT_GET(DT_INST_DMAS_CTLR(n)),                                        \
+	.dma.dma_cfg.dma_slot = SILABS_LDMA_REQSEL_TO_SLOT(DT_INST_DMAS_CELL_BY_IDX(n, 0, slot)),  \
+	.dma.dma_cfg.dma_callback = iadc_dma_cb,
+#define IADC_DMA_CHANNEL(n)                                                                        \
+	COND_CODE_1(DT_INST_NODE_HAS_PROP(n, dmas), (IADC_DMA_CHANNEL_INIT(n)), ())
+#else
+#define IADC_DMA_CHANNEL(n)
+#endif
+
 #define IADC_INIT(n)                                                                               \
 	PINCTRL_DT_INST_DEFINE(n);                                                                 \
 	PM_DEVICE_DT_INST_DEFINE(n, iadc_pm_action);                                               \
@@ -603,6 +765,7 @@
 		ADC_CONTEXT_INIT_TIMER(iadc_data_##n, ctx),                                        \
 		ADC_CONTEXT_INIT_LOCK(iadc_data_##n, ctx),                                         \
 		ADC_CONTEXT_INIT_SYNC(iadc_data_##n, ctx),                                         \
+		IADC_DMA_CHANNEL(n)                                                                \
 	};                                                                                         \
                                                                                                    \
 	static void iadc_config_func_##n(void)                                                     \