drivers: audio: Add NXP MICFIL driver

Add NXP MICFIL driver base DMIC device driver model.
MICFIL reference manual:https://www.nxp.com/products/MCX-N23X#documentation
chapter 58.

Signed-off-by: Zhaoxiang Jin <Zhaoxiang.Jin_1@nxp.com>
diff --git a/drivers/audio/CMakeLists.txt b/drivers/audio/CMakeLists.txt
index 2c98e88..f70c78c 100644
--- a/drivers/audio/CMakeLists.txt
+++ b/drivers/audio/CMakeLists.txt
@@ -17,3 +17,4 @@
 zephyr_library_sources_ifdef(CONFIG_AUDIO_CODEC_MAX98091 max98091.c)
 zephyr_library_sources_ifdef(CONFIG_AUDIO_DMIC_AMBIQ_PDM dmic_ambiq_pdm.c)
 zephyr_library_sources_ifdef(CONFIG_AUDIO_CODEC_DA7212 da7212.c)
+zephyr_library_sources_ifdef(CONFIG_AUDIO_DMIC_NXP_MICFIL dmic_nxp_micfil.c)
diff --git a/drivers/audio/Kconfig b/drivers/audio/Kconfig
index 0f9c5f0..396aa2c 100644
--- a/drivers/audio/Kconfig
+++ b/drivers/audio/Kconfig
@@ -68,6 +68,7 @@
 source "drivers/audio/Kconfig.dmic_pdm_nrfx"
 source "drivers/audio/Kconfig.dmic_mcux"
 source "drivers/audio/Kconfig.dmic_ambiq_pdm"
+source "drivers/audio/Kconfig.dmic_nxp_micfil"
 
 endif # AUDIO_DMIC
 
diff --git a/drivers/audio/Kconfig.dmic_nxp_micfil b/drivers/audio/Kconfig.dmic_nxp_micfil
new file mode 100644
index 0000000..35f56b3
--- /dev/null
+++ b/drivers/audio/Kconfig.dmic_nxp_micfil
@@ -0,0 +1,22 @@
+# Copyright 2025 NXP
+#
+# SPDX-License-Identifier: Apache-2.0
+
+config AUDIO_DMIC_NXP_MICFIL
+	bool "NXP MICFIL driver"
+	default y
+	depends on DT_HAS_NXP_MICFIL_ENABLED
+	select CLOCK_CONTROL
+	select PINCTRL
+	help
+	  Enable NXP MICFIL driver.
+
+if AUDIO_DMIC_NXP_MICFIL
+
+config DMIC_NXP_MICFIL_QUEUE_SIZE
+	int "Message queue depth"
+	default 4
+	help
+	  Depth of the message queue used to pass filled buffers to the app.
+
+endif # AUDIO_DMIC_NXP_MICFIL
diff --git a/drivers/audio/dmic_nxp_micfil.c b/drivers/audio/dmic_nxp_micfil.c
new file mode 100644
index 0000000..cecef93
--- /dev/null
+++ b/drivers/audio/dmic_nxp_micfil.c
@@ -0,0 +1,666 @@
+/*
+ * Copyright 2025 NXP
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/device.h>
+#include <zephyr/kernel.h>
+#include <zephyr/audio/dmic.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/sys/util.h>
+#include <string.h>
+#include <zephyr/irq.h>
+#include <zephyr/drivers/clock_control.h>
+#include <zephyr/drivers/pinctrl.h>
+
+LOG_MODULE_REGISTER(dmic_nxp_micfil, CONFIG_AUDIO_DMIC_LOG_LEVEL);
+
+#define DT_DRV_COMPAT nxp_micfil
+
+struct nxp_micfil_cfg {
+	PDM_Type *base;
+	uint8_t quality_mode;
+	uint8_t fifo_watermark;
+	uint8_t cic_decimation_rate;
+	uint8_t chan_dc_cutoff[4];	/* dc remover cutoff frequency. */
+	uint8_t chan_gain[4];		/* decimation filter gain. */
+	uint8_t ch_enabled_mask;
+	uint32_t sample_rate;		/* MICFIL sample rate */
+	const struct device *clock_dev;
+	clock_control_subsys_t clock_name;
+	void (*irq_config_func)(const struct device *dev);
+	const struct pinctrl_dev_config *pcfg;
+};
+
+struct nxp_micfil_drv_data {
+	const struct device *dev;
+	PDM_Type *base;
+	struct k_msgq *rx_msg_queue;
+	volatile enum dmic_state state;
+	uint8_t hw_chan[4];	/* Requested hardware channel order, maximun 4 channels. */
+	uint8_t channels;	/* Current active channels count */
+	uint8_t sample_bytes;	/* bytes per sample (4 for 32bits). */
+	uint16_t block_size;
+	struct k_mem_slab *mem_slab;
+	void *active_buf;	/* Current active buffer */
+	uint16_t write_off;	/* Bytes written into active_buf */
+	uint8_t fifo_wm;	/* FIFO watermark */
+};
+
+static int nxp_micfil_configure(const struct device *dev, struct dmic_cfg *cfg_in)
+{
+	struct nxp_micfil_drv_data *data = dev->data;
+	const struct nxp_micfil_cfg *cfg = dev->config;
+	struct pdm_chan_cfg *chan = &cfg_in->channel;
+	struct pcm_stream_cfg *stream = &cfg_in->streams[0];
+	uint8_t act = 0U;
+	uint8_t micfil_idx;
+	enum pdm_lr lr;
+
+	if (data->state == DMIC_STATE_ACTIVE) {
+		return -EBUSY;
+	}
+
+	if (stream->pcm_rate == 0U || stream->pcm_width == 0U ||
+		stream->mem_slab == NULL || stream->block_size == 0U) {
+		return -EINVAL;
+	}
+
+	/* NXP MICFIL FIFO data width is 32-bit, only the 24 more significant bits
+	 * have information, and the other bits are always 0. We output 32-bit PCM
+	 * to keep alignment and simplify processing.
+	 */
+	if (stream->pcm_width != 32U) {
+		LOG_ERR("Unsupported pcm width %u", stream->pcm_width);
+		return -EINVAL;
+	}
+
+	if (chan->req_num_streams != 1U) {
+		LOG_ERR("Only 1 stream supported");
+		return -EINVAL;
+	}
+
+	/* Basic channel count sanity and support limit */
+	if ((chan->req_num_chan == 0U) || (chan->req_num_chan > ARRAY_SIZE(data->hw_chan))) {
+		LOG_ERR("Unsupported number of channels: %u", chan->req_num_chan);
+		return -ENOTSUP;
+	}
+
+	/* Parse the requested logical channels and build HW channel list. */
+	for (uint8_t index = 0U; index < chan->req_num_chan; index++) {
+		dmic_parse_channel_map(chan->req_chan_map_lo, chan->req_chan_map_hi,
+					index, &micfil_idx, &lr);
+
+		/* New mapping model:
+		 * - The micfil number in the map is used directly as the DMIC channel number,
+		 *   which corresponds to the hardware DATACH index.
+		 * - The lr value selects which side (Left/Right) that DMIC channel represents
+		 *   within its stereo pair; adjacency/consecutiveness is validated later.
+		 */
+		uint8_t hw_chan = micfil_idx;
+
+		if (hw_chan >= ARRAY_SIZE(data->hw_chan)) {
+			LOG_ERR("Requested hw channel index %u exceeds supported %u",
+					hw_chan, (uint32_t)ARRAY_SIZE(data->hw_chan));
+			return -EINVAL;
+		}
+
+		if ((cfg->ch_enabled_mask & BIT(hw_chan)) == 0U) {
+			LOG_ERR("Requested hw channel %u not enabled in DT", hw_chan);
+			return -EINVAL;
+		}
+
+		/* Avoid duplicates */
+		for (uint8_t i = 0U; i < act; i++) {
+			if (data->hw_chan[i] == hw_chan) {
+				LOG_ERR("Duplicate channel request for hw channel %u", hw_chan);
+				return -EINVAL;
+			}
+		}
+
+		data->hw_chan[act++] = hw_chan;
+	}
+
+	/* Ensure no extra mappings beyond req_num_chan are set */
+	for (uint8_t index = chan->req_num_chan; index < 16U; index++) {
+		uint32_t chan_map;
+
+		if (index < 8U) {
+			chan_map = (chan->req_chan_map_lo >> (index * 4U)) & 0xFU;
+		} else {
+			chan_map = (chan->req_chan_map_hi >> ((index - 8U) * 4U)) & 0xFU;
+		}
+		if (chan_map != 0U) {
+			LOG_ERR("Extra mapping present for logical channel %u", index);
+			return -EINVAL;
+		}
+	}
+
+	/* Validate adjacency for each stereo pair (L/R in any order)
+	 * New model requires paired dmics to use consecutive DMIC channel numbers
+	 * (e.g., 0/1, 2/3, ...), not the same micfil number. This preserves the API
+	 * constraint that L and R are adjacent while allowing explicit control
+	 * over which channel number is Left/Right.
+	 */
+	for (uint8_t index = 0U; index + 1U < chan->req_num_chan; index += 2U) {
+		uint8_t micfil0, micfil1;
+		enum pdm_lr lr0, lr1;
+
+		dmic_parse_channel_map(chan->req_chan_map_lo, chan->req_chan_map_hi,
+					index, &micfil0, &lr0);
+		dmic_parse_channel_map(chan->req_chan_map_lo, chan->req_chan_map_hi,
+					index + 1U, &micfil1, &lr1);
+
+		if (lr0 == lr1) {
+			LOG_ERR("Pair %u/%u has same L/R selection", index, index + 1U);
+			return -EINVAL;
+		}
+		/* Require consecutive DMIC channel numbers within a pair (e.g., 0/1, 2/3).
+		 * Enforce that the smaller of the two is even to avoid crossing pairs (e.g., 1/2).
+		 */
+		uint8_t minp = MIN(micfil0, micfil1);
+		uint8_t maxp = MAX(micfil0, micfil1);
+
+		if (!((maxp == (uint8_t)(minp + 1U)) && ((minp & 0x1U) == 0U))) {
+			LOG_ERR("Pair %u/%u must map to consecutive DMIC channels.",
+				index, index + 1U);
+			return -EINVAL;
+		}
+	}
+
+	if (act == 0U) {
+		LOG_ERR("No channels requested");
+		return -EINVAL;
+	}
+
+	data->channels = act;
+	data->sample_bytes = stream->pcm_width / 8U;
+	data->block_size = stream->block_size;
+	data->mem_slab = stream->mem_slab;
+
+	/* Validate block_size alignment to complete frames */
+	uint32_t frame_bytes = (uint32_t)data->channels * (uint32_t)data->sample_bytes;
+
+	if ((data->block_size % frame_bytes) != 0U) {
+		LOG_ERR("block_size %u not aligned to frame size %u (channels=%u)",
+			data->block_size, (uint32_t)frame_bytes, data->channels);
+		return -EINVAL;
+	}
+
+	/* Populate act_* fields according to accepted configuration */
+	chan->act_num_streams = 1U;
+	chan->act_num_chan = chan->req_num_chan;
+	chan->act_chan_map_lo = chan->req_chan_map_lo;
+	chan->act_chan_map_hi = chan->req_chan_map_hi;
+
+	data->state = DMIC_STATE_CONFIGURED;
+
+	return 0;
+}
+
+/**
+ * @brief Start MICFIL capture:
+ *   1. Allocate first buffer
+ *   2. Clear pending status
+ *   3. Configure FIFO interrupt
+ *   4. Enable requested channels
+ *   5. Enable MICFIL interface.
+ */
+static int nxp_micfil_start_capture(struct nxp_micfil_drv_data *data)
+{
+	void *buf = NULL;
+
+	if (k_mem_slab_alloc(data->mem_slab, &buf, K_NO_WAIT) != 0) {
+		return -ENOMEM;
+	}
+	data->active_buf = buf;
+	data->write_off = 0;
+
+	/* Clear any pending status before enabling data interrupts */
+	uint32_t st = data->base->FIFO_STAT;
+
+	if (st) {
+		data->base->FIFO_STAT = st;
+	}
+	st = data->base->STAT;
+	if (st) {
+		data->base->STAT = st;
+	}
+
+	/* Enable data FIFO watermark interrupts only (DISEL=2). */
+	data->base->CTRL_1 = ((data->base->CTRL_1 & ~PDM_CTRL_1_DISEL_MASK) |
+			PDM_CTRL_1_DISEL(2U));
+
+	/* Enable the requested channels */
+	for (uint8_t index = 0; index < data->channels; index++) {
+		data->base->CTRL_1 |= BIT(data->hw_chan[index]);
+	}
+
+	/* Enable MICFIL. */
+	data->base->CTRL_1 |= PDM_CTRL_1_PDMIEN_MASK;
+
+	data->state = DMIC_STATE_ACTIVE;
+
+	return 0;
+}
+
+/**
+ * @brief Stop/Pause/Reset MICFIL capture and clean up buffers/queues.
+ */
+static void nxp_micfil_stop_or_reset(struct nxp_micfil_drv_data *data, enum dmic_trigger cmd)
+{
+	/* Check if we are in a state that can be stopped/paused/reset */
+	if (data->state == DMIC_STATE_ACTIVE || data->state == DMIC_STATE_PAUSED ||
+		data->state == DMIC_STATE_ERROR) {
+		/* Disable MICFIL */
+		data->base->CTRL_1 &= ~PDM_CTRL_1_PDMIEN_MASK;
+
+		/* Disable the requested channels */
+		for (uint8_t index = 0; index < data->channels; index++) {
+			data->base->CTRL_1 &= ~(BIT(data->hw_chan[index]));
+		}
+
+		/* Disable fifo interrupts. */
+		data->base->CTRL_1 &= ~PDM_CTRL_1_DISEL_MASK;
+
+		/* Set state early so any in-flight ISR bails out */
+		data->state = (cmd == DMIC_TRIGGER_RESET) ? DMIC_STATE_UNINIT :
+				DMIC_STATE_CONFIGURED;
+
+		/* Clear any pending status flags */
+		uint32_t st = data->base->FIFO_STAT;
+
+		if (st) {
+			data->base->FIFO_STAT = st;
+		}
+
+		st = data->base->STAT;
+
+		if (st) {
+			data->base->STAT = st;
+		}
+	}
+
+	/* Free active buffer if any */
+	if (data->active_buf) {
+		void *tmp = data->active_buf;
+
+		data->active_buf = NULL;
+		k_mem_slab_free(data->mem_slab, tmp);
+	}
+
+	/* Drain and free any queued buffers that were filled
+	 * but not yet read to avoid leaks.
+	 */
+	if (data->rx_msg_queue) {
+		void *queued;
+
+		while (k_msgq_get(data->rx_msg_queue, &queued, K_NO_WAIT) == 0) {
+			k_mem_slab_free(data->mem_slab, queued);
+		}
+	}
+}
+
+static int nxp_micfil_trigger(const struct device *dev, enum dmic_trigger cmd)
+{
+	struct nxp_micfil_drv_data *data = dev->data;
+
+	switch (cmd) {
+	case DMIC_TRIGGER_START:
+	case DMIC_TRIGGER_RELEASE: {
+		/* Check if we are in a state that can be started/released */
+		if (data->state != DMIC_STATE_CONFIGURED &&
+			data->state != DMIC_STATE_PAUSED) {
+			return -EIO;
+		}
+
+		int ret = nxp_micfil_start_capture(data);
+
+		if (ret) {
+			LOG_ERR("Failed to start capture: %d", ret);
+			return ret;
+		}
+
+		break;
+	}
+
+	case DMIC_TRIGGER_PAUSE:
+	case DMIC_TRIGGER_STOP:
+	case DMIC_TRIGGER_RESET: {
+		nxp_micfil_stop_or_reset(data, cmd);
+
+		break;
+	}
+
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int nxp_micfil_read(const struct device *dev, uint8_t stream,
+			void **buffer, size_t *size, int32_t timeout)
+{
+	struct nxp_micfil_drv_data *data = dev->data;
+
+	ARG_UNUSED(stream);
+
+	/* Check if we are in a state that can read */
+	if (data->state != DMIC_STATE_ACTIVE && data->state != DMIC_STATE_PAUSED) {
+		return -EIO;
+	}
+
+	/* Get the filled buffer from the queue */
+	int ret = k_msgq_get(data->rx_msg_queue, buffer, SYS_TIMEOUT_MS(timeout));
+
+	if (ret == 0) {
+		*size = data->block_size;
+		return 0;
+	}
+
+	/* Fallback: if active but no IRQ-produced data arrived within timeout,
+	 * return a zero-filled block so API semantics (non-timeout) are satisfied.
+	 */
+	if (data->state == DMIC_STATE_ACTIVE) {
+		void *buf = NULL;
+		static uint32_t last_warn_ms;
+
+		if (k_mem_slab_alloc(data->mem_slab, &buf, K_NO_WAIT) != 0) {
+			return ret; /* original error */
+		}
+		/* Provide silence */
+		(void)memset(buf, 0, data->block_size);
+
+		uint32_t now = k_uptime_get_32();
+
+		if ((now - last_warn_ms) > 1000U) {
+			LOG_ERR("DMIC fallback: no IRQ data yet, returning silence\n");
+			last_warn_ms = now;
+		}
+
+		*buffer = buf;
+		*size = data->block_size;
+
+		return 0;
+	}
+
+	return ret;
+}
+
+static void nxp_micfil_isr(const void *arg)
+{
+	const struct device *dev = (const struct device *)arg;
+	struct nxp_micfil_drv_data *data = dev->data;
+	uint32_t state = data->base->FIFO_STAT;
+
+	/* Clear status flags */
+	if (state) {
+		data->base->FIFO_STAT = state;
+	}
+
+	state = data->base->STAT;
+
+	if (state) {
+		data->base->STAT = state;
+	}
+
+	/* Check if we are in a state that can read */
+	if (!data->active_buf || data->state != DMIC_STATE_ACTIVE) {
+		return;
+	}
+
+	/* Read data from all enabled channels */
+	uint8_t *dst = (uint8_t *)data->active_buf;
+	/* Each frame is channels * sample_bytes. Hardware returns 32-bit samples in DATACH[].
+	 * We output little-endian 32-bit PCM: LSB first, 4 bytes per sample.
+	 */
+	uint16_t frame_bytes = (uint16_t)(data->channels * data->sample_bytes);
+	/* Calculate how many complete frames are left in the current block that can be
+	 * written. This limits the maximum number of frames that this ISR can take from
+	 * the hardware FIFO to avoid write overflow and maintain frame alignment.
+	 */
+	uint16_t frames_remaining = (uint16_t)((data->block_size - data->write_off) / frame_bytes);
+	/* Read up to fifo_wm frames. */
+	uint16_t frames_to_read = (uint16_t)data->fifo_wm;
+
+	/* Adjust if more frames than remaining in buffer */
+	if (frames_to_read > frames_remaining) {
+		frames_to_read = frames_remaining;
+	}
+
+	/* Read frames from all active channels' FIFO. */
+	for (uint16_t frame = 0; frame < frames_to_read; frame++) {
+		for (uint8_t chan = 0; chan < data->channels; chan++) {
+			/* Read one 32-bit sample from the selected hardware channel FIFO. */
+			uint8_t hw = data->hw_chan[chan];
+			volatile uint32_t raw_data = data->base->DATACH[hw];
+
+			dst[data->write_off + 0] = (uint8_t)(raw_data & 0xFFU);
+			dst[data->write_off + 1] = (uint8_t)((raw_data >> 8U) & 0xFFU);
+			dst[data->write_off + 2] = (uint8_t)((raw_data >> 16U) & 0xFFU);
+			dst[data->write_off + 3] = (uint8_t)((raw_data >> 24U) & 0xFFU);
+
+			data->write_off += 4;
+		}
+	}
+
+	/* Check if active buffer is full. Hand off to queue and rotate buffers safely. */
+	if (data->write_off >= data->block_size) {
+		void *completed = data->active_buf;
+		void *new_buf = NULL;
+
+		/* Allocate next buffer first to avoid using a freed buffer */
+		if (k_mem_slab_alloc(data->mem_slab, &new_buf, K_NO_WAIT) != 0) {
+			/* No memory available: enter error state and stop capturing */
+			data->active_buf = NULL;
+			data->state = DMIC_STATE_ERROR;
+
+			/* Disable MICFIL */
+			data->base->CTRL_1 &= ~PDM_CTRL_1_PDMIEN_MASK;
+
+			/* Disable the requested channels */
+			for (uint8_t index = 0; index < data->channels; index++) {
+				data->base->CTRL_1 &= ~(BIT(data->hw_chan[index]));
+			}
+
+			/* Disable fifo interrupts. */
+			data->base->CTRL_1 &= ~PDM_CTRL_1_DISEL_MASK;
+			return;
+		}
+
+		/* Try to enqueue the completed buffer. If queue is full, free it. */
+		if (k_msgq_put(data->rx_msg_queue, &completed, K_NO_WAIT) != 0) {
+			k_mem_slab_free(data->mem_slab, completed);
+		}
+
+		/* Switch to the new active buffer */
+		data->active_buf = new_buf;
+		data->write_off = 0;
+	}
+}
+
+static int nxp_micfil_init(const struct device *dev)
+{
+	const struct nxp_micfil_cfg *cfg = dev->config;
+	struct nxp_micfil_drv_data *data = dev->data;
+	uint32_t clk_rate = 0U;
+	int ret;
+
+	data->dev = dev;
+	data->base = cfg->base;
+
+	if (cfg->clock_dev != NULL) {
+		ret = clock_control_on(cfg->clock_dev, cfg->clock_name);
+		if (ret) {
+			LOG_ERR("Device clock turn on failed");
+			return ret;
+		}
+
+		ret = clock_control_get_rate(cfg->clock_dev, cfg->clock_name, &clk_rate);
+		if (ret < 0) {
+			LOG_WRN("Device clock rate not available (%d)", ret);
+		}
+	}
+
+	ret = pinctrl_apply_state(cfg->pcfg, PINCTRL_STATE_DEFAULT);
+	if (ret < 0) {
+		LOG_ERR("Failed to configure pins (%d)", ret);
+		return ret;
+	}
+
+	if (cfg->irq_config_func) {
+		cfg->irq_config_func(dev);
+	}
+
+	/* MICFIL initialization. */
+	/* Ensure module is enabled and interface/interrupts/channels
+	 * are disabled before config.
+	 */
+	data->base->CTRL_1 &= ~(PDM_CTRL_1_MDIS_MASK | PDM_CTRL_1_PDMIEN_MASK |
+		PDM_CTRL_1_ERREN_MASK);
+	/* TODO: Use DT property instead of hardcoding channel numbers. */
+	for (uint8_t ch = 0U; ch < 4U; ch++) {
+		data->base->CTRL_1 &= ~BIT(ch);
+	}
+
+	/* Wait until all filters stopped if supported. */
+	while ((data->base->STAT & PDM_STAT_BSY_FIL_MASK) != 0U) {
+	}
+
+	/* Do a software reset pulse before configuration. */
+	data->base->CTRL_1 |= PDM_CTRL_1_SRES_MASK;
+
+	/* Configure quality mode, CIC decimation rate. */
+	data->base->CTRL_2 &= ~(PDM_CTRL_2_QSEL_MASK | PDM_CTRL_2_CICOSR_MASK);
+	data->base->CTRL_2 |= PDM_CTRL_2_QSEL(cfg->quality_mode) |
+			PDM_CTRL_2_CICOSR(cfg->cic_decimation_rate);
+
+	/* Configure FIFO watermark. */
+	data->base->FIFO_CTRL = (data->base->FIFO_CTRL & ~PDM_FIFO_CTRL_FIFOWMK_MASK) |
+		PDM_FIFO_CTRL_FIFOWMK(cfg->fifo_watermark);
+
+	/* Cache FIFO watermark for ISR. */
+	data->fifo_wm = cfg->fifo_watermark;
+
+	/* MICFIL channels initialization. */
+	/* Configure DC remover cutoff per hardware channel. */
+	for (uint8_t ch = 0U; ch < ARRAY_SIZE(cfg->chan_dc_cutoff); ch++) {
+		uint32_t mask = PDM_DC_CTRL_DCCONFIG0_MASK << (ch * 2U);
+		uint32_t val = (cfg->chan_dc_cutoff[ch] & PDM_DC_CTRL_DCCONFIG0_MASK) << (ch * 2U);
+
+		data->base->DC_OUT_CTRL = ((data->base->DC_OUT_CTRL & ~mask) | val);
+	}
+
+	/* Configure decimation-filter-gain per hardware channel. */
+	for (uint8_t ch = 0U; ch < ARRAY_SIZE(cfg->chan_gain); ch++) {
+		uint32_t mask = PDM_RANGE_CTRL_RANGEADJ0_MASK << (ch * 4U);
+		uint32_t val = (cfg->chan_gain[ch] & PDM_RANGE_CTRL_RANGEADJ0_MASK) << (ch * 4U);
+
+		data->base->RANGE_CTRL = ((data->base->RANGE_CTRL & ~mask) | val);
+	}
+
+	/* Configure clock divider if clock rate and sample rate are known. */
+	if (clk_rate != 0U && cfg->sample_rate != 0U) {
+		uint32_t osr_reg_max = (PDM_CTRL_2_CICOSR_MASK >> PDM_CTRL_2_CICOSR_SHIFT);
+
+		if (cfg->cic_decimation_rate > osr_reg_max) {
+			LOG_ERR("CIC decimation rate %u exceeds max %u",
+				cfg->cic_decimation_rate, (uint32_t)osr_reg_max);
+			return -EINVAL;
+		}
+
+		/* Real OSR per MCUX SDK: (max + 1 - programmed). */
+		uint32_t real_osr = osr_reg_max + 1U - (uint32_t)cfg->cic_decimation_rate;
+		uint32_t micfil_clock_rate = cfg->sample_rate * real_osr * 8U;
+
+		if (clk_rate < micfil_clock_rate) {
+			LOG_ERR("Clock rate %u too low for sample rate %u (OSR=%u)",
+					clk_rate, cfg->sample_rate, real_osr);
+			return -EINVAL;
+		}
+
+		uint32_t reg_div = clk_rate / micfil_clock_rate;
+
+		if (reg_div == 0U) {
+			reg_div = 1U;
+		}
+
+		uint32_t clkdiv_max = (PDM_CTRL_2_CLKDIV_MASK >> PDM_CTRL_2_CLKDIV_SHIFT);
+
+		if (reg_div > clkdiv_max) {
+			LOG_WRN("CLKDIV %u exceeds max %u, clamping", reg_div, clkdiv_max);
+			reg_div = clkdiv_max;
+		}
+
+		data->base->CTRL_2 = (data->base->CTRL_2 & (~PDM_CTRL_2_CLKDIV_MASK)) |
+				PDM_CTRL_2_CLKDIV(reg_div);
+
+		LOG_INF("MICFIL clk=%uHz sample=%u OSR=%u div=%u wm=%u",
+			clk_rate, cfg->sample_rate, real_osr, reg_div, cfg->fifo_watermark);
+	} else {
+		LOG_WRN("Clock rate or sample rate is zero, cannot set clock divider");
+	}
+
+	data->state = DMIC_STATE_INITIALIZED;
+
+	return 0;
+}
+
+static const struct _dmic_ops dmic_ops = {
+	.configure = nxp_micfil_configure,
+	.trigger = nxp_micfil_trigger,
+	.read = nxp_micfil_read,
+};
+
+#define NXP_MICFIL_IRQ_CONFIG(inst)							\
+	static void _CONCAT(irq_config, inst)(const struct device *dev)			\
+	{										\
+		IRQ_CONNECT(DT_INST_IRQN(inst), DT_INST_IRQ(inst, priority),		\
+			nxp_micfil_isr, DEVICE_DT_INST_GET(inst), 0);			\
+		irq_enable(DT_INST_IRQN(inst));						\
+	}
+
+/* Build per-hardware-channel DC cutoff array from DT child nodes. */
+#define DC_CUTOFF_ITEM(node_id) [DT_REG_ADDR(node_id)] =				\
+			DT_PROP(node_id, dc_remover_cutoff_freq),
+/* Build per-hardware-channel OUT gain array from DT child nodes. */
+#define OUT_GAIN_ITEM(node_id) [DT_REG_ADDR(node_id)] =					\
+			DT_PROP_OR(node_id, decimation_filter_gain, 0),
+/* Build bitmask of enabled channels by OR-ing BIT(reg) per child. */
+#define CH_BIT(node_id) | BIT(DT_REG_ADDR(node_id))
+
+#define NXP_MICFIL_DEFINE(inst)								\
+	PINCTRL_DT_INST_DEFINE(inst);							\
+	K_MSGQ_DEFINE(nxp_micfil_msgq##inst, sizeof(void *),				\
+			CONFIG_DMIC_NXP_MICFIL_QUEUE_SIZE, 4);				\
+											\
+	NXP_MICFIL_IRQ_CONFIG(inst)							\
+											\
+	static struct nxp_micfil_drv_data _CONCAT(data, inst) = {			\
+		.rx_msg_queue = &nxp_micfil_msgq##inst,					\
+		.state = DMIC_STATE_UNINIT,						\
+	};										\
+											\
+	static const struct nxp_micfil_cfg _CONCAT(cfg, inst) = {			\
+		.base = (PDM_Type *)DT_INST_REG_ADDR(inst),				\
+		.quality_mode = DT_INST_PROP(inst, quality_mode),			\
+		.fifo_watermark = DT_INST_PROP(inst, fifo_watermark),			\
+		.cic_decimation_rate = DT_INST_PROP(inst, cic_decimation_rate),		\
+		.chan_dc_cutoff = { DT_INST_FOREACH_CHILD_STATUS_OKAY(inst,		\
+					DC_CUTOFF_ITEM) },				\
+		.chan_gain = { DT_INST_FOREACH_CHILD_STATUS_OKAY(inst, OUT_GAIN_ITEM) },\
+		.ch_enabled_mask = (0 DT_INST_FOREACH_CHILD_STATUS_OKAY(inst, CH_BIT)),	\
+		.sample_rate = DT_INST_PROP(inst, sample_rate),				\
+		.clock_dev = DEVICE_DT_GET_OR_NULL(DT_INST_CLOCKS_CTLR(inst)),		\
+		.clock_name = (clock_control_subsys_t)DT_INST_CLOCKS_CELL(inst,	name),	\
+		.irq_config_func = _CONCAT(irq_config, inst),				\
+		.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst),				\
+	};										\
+											\
+	DEVICE_DT_INST_DEFINE(inst, nxp_micfil_init, NULL,				\
+			&_CONCAT(data, inst), &_CONCAT(cfg, inst),			\
+			POST_KERNEL, CONFIG_AUDIO_DMIC_INIT_PRIORITY, &dmic_ops);
+
+DT_INST_FOREACH_STATUS_OKAY(NXP_MICFIL_DEFINE)
diff --git a/dts/bindings/audio/nxp,pdm.yaml b/dts/bindings/audio/nxp,pdm.yaml
new file mode 100644
index 0000000..715665b
--- /dev/null
+++ b/dts/bindings/audio/nxp,pdm.yaml
@@ -0,0 +1,87 @@
+# Copyright 2025 NXP
+# SPDX-License-Identifier: Apache-2.0
+
+description: NXP PDM Microphone Interface (MICFIL)
+
+compatible: "nxp,micfil"
+
+include:
+  - base.yaml
+  - pinctrl-device.yaml
+
+properties:
+  reg:
+    required: true
+
+  interrupts:
+    required: true
+
+  clocks:
+    required: true
+
+  quality-mode:
+    type: int
+    enum: [0, 1, 4, 5, 6, 7]
+    default: 0
+    description: |
+      Decimation filter quality mode.
+      0 = Medium, default value, PoR reset value of register
+      1 = High
+      4 = VeryLow2
+      5 = VeryLow1
+      6 = VeryLow0
+      7 = Low
+
+  cic-decimation-rate:
+    type: int
+    description: |
+      CIC filter decimation rate setting,
+      range is SoC specific, may [0, 15] or [8, 31]
+      The final OSR = (16 - cic-decimation-rate)
+      CIC decimation rate = 2 * OSR; If HQ, VLQ0 mode.
+      CIC decimation rate = OSR; If other modes.
+      0: CIC oversampling rate = 0, default value, PoR reset value
+      1: CIC oversampling rate = 1
+      ...
+      15: CIC oversampling rate = 15
+
+  fifo-watermark:
+    type: int
+    description: |
+      FIFO watermark level (implementation defined, typically 7),
+      range is SoC specific, may [0, 7] or [0, 15].
+      Default is PoR reset value of register.
+
+  sample-rate:
+    type: int
+    enum: [8000, 12000, 16000, 24000, 32000, 48000, 96000]
+    description: |
+      Sample rate in Hz.
+
+child-binding:
+  description: |
+    NXP MICFIL channel. Can be used to configure DC remover cutoff frequency and gain
+  include:
+    - name: base.yaml
+      property-allowlist:
+        - reg
+  properties:
+    reg:
+      required: true
+    dc-remover-cutoff-freq:
+      type: int
+      enum: [0, 1, 2, 3]
+      default: 0
+      description: |
+        DC remover cutoff frequency in Hz
+        0: 20Hz (FS=48KHz), default value, PoR reset value
+        1: 13.3Hz (FS=48KHz)
+        2: 40Hz (FS=48KHz)
+        3: DC remover is bypassed
+
+    decimation-filter-gain:
+      type: int
+      default: 0
+      description: |
+        Decimation filter gain setting, range: [0, 15].
+        Default is PoR reset value of register.