i3c: add I3C controller driver for MCUX

This adds a very basic driver to utilize the I3C IP block
on MCUX (e.g. RT685). Note that, for now, this only supports
being the active controller on the bus.

Origin: NXP MCUXpresso SDK
License: BSD 3-Clause
URL: https://github.com/zephyrproject-rtos/hal_nxp
Commit: 2302a1e94f5bc00ce59db4e249b688ad2e959f58
Purpose: Enabling the I3C controller on RT685.

Signed-off-by: Daniel Leung <daniel.leung@intel.com>
diff --git a/drivers/clock_control/clock_control_mcux_syscon.c b/drivers/clock_control/clock_control_mcux_syscon.c
index e3f5b37..c4d00fe 100644
--- a/drivers/clock_control/clock_control_mcux_syscon.c
+++ b/drivers/clock_control/clock_control_mcux_syscon.c
@@ -148,6 +148,12 @@
 	case MCUX_BUS_CLK:
 		*rate = CLOCK_GetFreq(kCLOCK_BusClk);
 		break;
+
+#if defined(CONFIG_I3C_MCUX)
+	case MCUX_I3C_CLK:
+		*rate = CLOCK_GetI3cClkFreq();
+		break;
+#endif
 	}
 
 	return 0;
diff --git a/drivers/i3c/CMakeLists.txt b/drivers/i3c/CMakeLists.txt
index 889e953..5b06686 100644
--- a/drivers/i3c/CMakeLists.txt
+++ b/drivers/i3c/CMakeLists.txt
@@ -18,3 +18,8 @@
   CONFIG_I3C_IBI_WORKQUEUE
   i3c_ibi_workq.c
 )
+
+zephyr_library_sources_ifdef(
+  CONFIG_I3C_MCUX
+  i3c_mcux.c
+)
diff --git a/drivers/i3c/Kconfig b/drivers/i3c/Kconfig
index 60714f0..bc32519 100644
--- a/drivers/i3c/Kconfig
+++ b/drivers/i3c/Kconfig
@@ -98,4 +98,6 @@
 
 comment "Device Drivers"
 
+rsource "Kconfig.nxp"
+
 endif # I3C
diff --git a/drivers/i3c/Kconfig.nxp b/drivers/i3c/Kconfig.nxp
new file mode 100644
index 0000000..4c493c4
--- /dev/null
+++ b/drivers/i3c/Kconfig.nxp
@@ -0,0 +1,19 @@
+# Copyright (c) 2022 Intel Corporation
+#
+# SPDX-License-Identifier: Apache-2.0
+
+DT_COMPAT_NXP_MCUX_I3C := nxp,mcux-i3c
+
+module = I3C_MCUX
+module-str = i3c-mcux
+source "subsys/logging/Kconfig.template.log_config"
+
+config I3C_MCUX
+	bool "MCUX I3C driver"
+	depends on HAS_MCUX
+	depends on DT_HAS_NXP_MCUX_I3C_ENABLED
+	select PINCTRL
+	select I3C_IBI_WORKQUEUE if I3C_USE_IBI
+	default y
+	help
+	  Enable mcux I3C driver.
diff --git a/drivers/i3c/i3c_mcux.c b/drivers/i3c/i3c_mcux.c
new file mode 100644
index 0000000..ce8a04c
--- /dev/null
+++ b/drivers/i3c/i3c_mcux.c
@@ -0,0 +1,2178 @@
+/*
+ * Copyright (c) 2016 Freescale Semiconductor, Inc.
+ * Copyright (c) 2019 NXP
+ * Copyright (c) 2022 Intel Corporation
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define DT_DRV_COMPAT nxp_mcux_i3c
+
+#include <string.h>
+
+#include <zephyr/device.h>
+#include <zephyr/irq.h>
+#include <zephyr/sys/__assert.h>
+#include <zephyr/sys/sys_io.h>
+
+#include <zephyr/drivers/clock_control.h>
+#include <zephyr/drivers/i3c.h>
+
+#ifdef CONFIG_PINCTRL
+#include <zephyr/drivers/pinctrl.h>
+#endif
+
+/*
+ * This is from NXP HAL which contains register bits macros
+ * which are used in this driver.
+ */
+#include <fsl_i3c.h>
+
+#include <zephyr/logging/log.h>
+LOG_MODULE_REGISTER(i3c_mcux, CONFIG_I3C_MCUX_LOG_LEVEL);
+
+#define I3C_MCTRL_REQUEST_NONE			I3C_MCTRL_REQUEST(0)
+#define I3C_MCTRL_REQUEST_EMIT_START_ADDR	I3C_MCTRL_REQUEST(1)
+#define I3C_MCTRL_REQUEST_EMIT_STOP		I3C_MCTRL_REQUEST(2)
+#define I3C_MCTRL_REQUEST_IBI_ACK_NACK		I3C_MCTRL_REQUEST(3)
+#define I3C_MCTRL_REQUEST_PROCESS_DAA		I3C_MCTRL_REQUEST(4)
+#define I3C_MCTRL_REQUEST_FORCE_EXIT		I3C_MCTRL_REQUEST(6)
+#define I3C_MCTRL_REQUEST_AUTO_IBI		I3C_MCTRL_REQUEST(7)
+
+#define I3C_MCTRL_IBIRESP_ACK			I3C_MCTRL_IBIRESP(0)
+#define I3C_MCTRL_IBIRESP_ACK_AUTO		I3C_MCTRL_IBIRESP(0)
+#define I3C_MCTRL_IBIRESP_NACK			I3C_MCTRL_IBIRESP(1)
+#define I3C_MCTRL_IBIRESP_ACK_WITH_BYTE		I3C_MCTRL_IBIRESP(2)
+#define I3C_MCTRL_IBIRESP_MANUAL		I3C_MCTRL_IBIRESP(3)
+
+#define I3C_MCTRL_TYPE_I3C			I3C_MCTRL_TYPE(0)
+#define I3C_MCTRL_TYPE_I2C			I3C_MCTRL_TYPE(1)
+
+#define I3C_MCTRL_DIR_WRITE			I3C_MCTRL_DIR(0)
+#define I3C_MCTRL_DIR_READ			I3C_MCTRL_DIR(1)
+
+#define I3C_MSTATUS_STATE_IDLE			I3C_MSTATUS_STATE(0)
+#define I3C_MSTATUS_STATE_SLVREQ		I3C_MSTATUS_STATE(1)
+#define I3C_MSTATUS_STATE_MSGSDR		I3C_MSTATUS_STATE(2)
+#define I3C_MSTATUS_STATE_NORMACT		I3C_MSTATUS_STATE(3)
+#define I3C_MSTATUS_STATE_MSGDDR		I3C_MSTATUS_STATE(4)
+#define I3C_MSTATUS_STATE_DAA			I3C_MSTATUS_STATE(5)
+#define I3C_MSTATUS_STATE_IBIACK		I3C_MSTATUS_STATE(6)
+#define I3C_MSTATUS_STATE_IBIRCV		I3C_MSTATUS_STATE(7)
+
+#define I3C_MSTATUS_IBITYPE_NONE		I3C_MSTATUS_IBITYPE(0)
+#define I3C_MSTATUS_IBITYPE_IBI			I3C_MSTATUS_IBITYPE(1)
+#define I3C_MSTATUS_IBITYPE_MR			I3C_MSTATUS_IBITYPE(2)
+#define I3C_MSTATUS_IBITYPE_HJ			I3C_MSTATUS_IBITYPE(3)
+
+struct mcux_i3c_config {
+	/** Pointer to controller registers. */
+	I3C_Type *base;
+
+	/** Pointer to the clock device. */
+	const struct device *clock_dev;
+
+	/** Clock control subsys related struct. */
+	clock_control_subsys_t clock_subsys;
+
+#ifdef CONFIG_PINCTRL
+	/** Pointer to pin control device. */
+	const struct pinctrl_dev_config *pincfg;
+#endif
+
+	/** I3C/I2C device list struct. */
+	struct i3c_dev_list device_list;
+
+	/** Interrupt configuration function. */
+	void (*irq_config_func)(const struct device *dev);
+};
+
+struct mcux_i3c_data {
+	/** Configuration parameter to be used with HAL. */
+	i3c_master_config_t ctrl_config_hal;
+
+	/** Controller configuration parameters */
+	struct i3c_config_controller ctrl_config;
+
+	/** Address slots */
+	struct i3c_addr_slots addr_slots;
+
+	/** Semaphore to serialize access for applications. */
+	struct k_sem lock;
+
+	/** Semaphore to serialize access for IBIs. */
+	struct k_sem ibi_lock;
+
+	struct {
+		/**
+		 * Clock divider for use when generating clock for
+		 * I3C Push-pull mode.
+		 */
+		uint8_t clk_div_pp;
+
+		/**
+		 * Clock divider for use when generating clock for
+		 * I3C open drain mode.
+		 */
+		uint8_t clk_div_od;
+
+		/**
+		 * Clock divider for the slow time control clock.
+		 */
+		uint8_t clk_div_tc;
+
+		/** I3C open drain clock frequency in Hz. */
+		uint32_t i3c_od_scl_hz;
+	} clocks;
+
+#ifdef CONFIG_I3C_USE_IBI
+	struct {
+		/** List of addresses used in the MIBIRULES register. */
+		uint8_t addr[5];
+
+		/** Number of valid addresses in MIBIRULES. */
+		uint8_t num_addr;
+
+		/** True if all addresses have MSB set. */
+		bool msb;
+
+		/**
+		 * True if all target devices require mandatory byte
+		 * for IBI.
+		 */
+		bool has_mandatory_byte;
+	} ibi;
+#endif
+};
+
+/**
+ * @brief Read a register and test for bit matches with timeout.
+ *
+ * Please be aware that this uses @see k_busy_wait.
+ *
+ * @param reg Pointer to 32-bit Register.
+ * @param mask Mask to the register value.
+ * @param match Value to match for masked register value.
+ * @param init_delay_us Initial delay in microsecond before reading register
+ *                      (can be 0).
+ * @param step_delay_us Delay in microsecond between each read of register
+ *                      (cannot be 0).
+ * @param total_delay_us Total delay in microsecond before bailing out.
+ *
+ * @retval 0 If masked register value matches before time out.
+ * @retval -ETIMEDOUT Exhausted all delays without matching.
+ */
+static int reg32_poll_timeout(volatile uint32_t *reg,
+			      uint32_t mask, uint32_t match,
+			      uint32_t init_delay_us, uint32_t step_delay_us,
+			      uint32_t total_delay_us)
+{
+	uint32_t delayed = init_delay_us;
+	int ret = -ETIMEDOUT;
+
+	if (init_delay_us > 0U) {
+		k_busy_wait(init_delay_us);
+	}
+
+	while (delayed <= total_delay_us) {
+		if ((sys_read32((mm_reg_t)reg) & mask) == match) {
+			ret = 0;
+			break;
+		}
+
+		k_busy_wait(step_delay_us);
+		delayed += step_delay_us;
+	}
+
+	return ret;
+}
+
+/**
+ * @brief Update register value.
+ *
+ * @param reg Pointer to 32-bit Register.
+ * @param mask Mask to the register value.
+ * @param update Value to be updated in register.
+ */
+static inline void reg32_update(volatile uint32_t *reg,
+				uint32_t mask, uint32_t update)
+{
+	uint32_t val = sys_read32((mem_addr_t)reg);
+
+	val &= ~mask;
+	val |= (update & mask);
+
+	sys_write32(val, (mem_addr_t)reg);
+}
+
+/**
+ * @brief Test if masked register value has certain value.
+ *
+ * @param reg Pointer to 32-bit register.
+ * @param mask Mask to test.
+ * @param match Value to match.
+ *
+ * @return True if bits in @p mask mask matches @p match, false otherwise.
+ */
+static inline bool reg32_test_match(volatile uint32_t *reg,
+				    uint32_t mask, uint32_t match)
+{
+	uint32_t val = sys_read32((mem_addr_t)reg);
+
+	return (val & mask) == match;
+}
+
+/**
+ * @brief Test if masked register value is the same as the mask.
+ *
+ * @param reg Pointer to 32-bit register.
+ * @param mask Mask to test.
+ *
+ * @return True if bits in @p mask are all set, false otherwise.
+ */
+static inline bool reg32_test(volatile uint32_t *reg, uint32_t mask)
+{
+	return reg32_test_match(reg, mask, mask);
+}
+
+/**
+ * @breif Disable all interrupts.
+ *
+ * @param base Pointer to controller registers.
+ *
+ * @return Previous enabled interrupts.
+ */
+static uint32_t mcux_i3c_interrupt_disable(I3C_Type *base)
+{
+	uint32_t intmask = base->MINTSET;
+
+	base->MINTCLR = intmask;
+
+	return intmask;
+}
+
+/**
+ * @brief Enable interrupts according to mask.
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Interrupts to be enabled.
+ *
+ */
+static void mcux_i3c_interrupt_enable(I3C_Type *base, uint32_t mask)
+{
+	base->MINTSET = mask;
+}
+
+/**
+ * @brief Check if there are any errors.
+ *
+ * This checks if MSTATUS has ERRWARN bit set.
+ *
+ * @retval True if there are any errors.
+ * @retval False if no errors.
+ */
+static bool mcux_i3c_has_error(I3C_Type *base)
+{
+	uint32_t mstatus, merrwarn;
+
+	mstatus = base->MSTATUS;
+	if ((mstatus & I3C_MSTATUS_ERRWARN_MASK) == I3C_MSTATUS_ERRWARN_MASK) {
+		merrwarn = base->MERRWARN;
+
+		/*
+		 * Note that this uses LOG_DBG() for displaying
+		 * register values for debugging. In production builds,
+		 * printing any error messages should be handled in
+		 * callers of this function.
+		 */
+		LOG_DBG("ERROR: MSTATUS 0x%08x MERRWARN 0x%08x",
+			mstatus, merrwarn);
+
+		return true;
+	}
+
+	return false;
+}
+
+/**
+ * @brief Check if there are any errors, and if one of them is time out error.
+ *
+ * @retval True if controller times out on operation.
+ * @retval False if no time out error.
+ */
+static inline bool mcux_i3c_error_is_timeout(I3C_Type *base)
+{
+	if (mcux_i3c_has_error(base)) {
+		if (reg32_test(&base->MERRWARN, I3C_MERRWARN_TIMEOUT_MASK)) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/**
+ * @brief Check if there are any errors, and if one of them is NACK.
+ *
+ * NACK is generated when:
+ * 1. Target does not ACK the last used address.
+ * 2. All targets do not ACK on 0x7E.
+ *
+ * @retval True if NACK is received.
+ * @retval False if no NACK error.
+ */
+static inline bool mcux_i3c_error_is_nack(I3C_Type *base)
+{
+	if (mcux_i3c_has_error(base)) {
+		if (reg32_test(&base->MERRWARN, I3C_MERRWARN_NACK_MASK)) {
+			return true;
+		}
+	}
+
+	return false;
+}
+
+/**
+ * @brief Test if certain bits are set in MSTATUS.
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be tested.
+ *
+ * @retval True if @p mask bits are set.
+ * @retval False if @p mask bits are not set.
+ */
+static inline bool mcux_i3c_status_is_set(I3C_Type *base, uint32_t mask)
+{
+	return reg32_test(&base->MSTATUS, mask);
+}
+
+/**
+ * @brief Spin wait for MSTATUS bit to be set.
+ *
+ * This spins forever for the bits to be set.
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be tested.
+ */
+static inline void mcux_i3c_status_wait(I3C_Type *base, uint32_t mask)
+{
+	/* Wait for bits to be set */
+	while (!mcux_i3c_status_is_set(base, mask)) {
+		k_busy_wait(1);
+	};
+}
+
+/**
+ * @brief Wait for MSTATUS bits to be set with time out.
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be tested.
+ * @param init_delay_us Initial delay in microsecond before reading register
+ *                      (can be 0).
+ * @param step_delay_us Delay in microsecond between each read of register
+ *                      (cannot be 0).
+ * @param total_delay_us Total delay in microsecond before bailing out.
+ *
+ * @retval 0 If bits are set before time out.
+ * @retval -ETIMEDOUT Exhausted all delays.
+ */
+static inline int mcux_i3c_status_wait_timeout(I3C_Type *base, uint32_t mask,
+					       uint32_t init_delay_us,
+					       uint32_t step_delay_us,
+					       uint32_t total_delay_us)
+{
+	return reg32_poll_timeout(&base->MSTATUS, mask, mask,
+				  init_delay_us, step_delay_us, total_delay_us);
+}
+
+/**
+ * @brief Clear the MSTATUS bits and wait for them to be cleared.
+ *
+ * This spins forever for the bits to be cleared;
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be cleared.
+ */
+static inline void mcux_i3c_status_clear(I3C_Type *base, uint32_t mask)
+{
+	/* Try to clear bit until it is cleared */
+	while (1) {
+		base->MSTATUS = mask;
+
+		if (!mcux_i3c_status_is_set(base, mask)) {
+			break;
+		}
+
+		k_busy_wait(1);
+	}
+}
+
+/**
+ * @brief Clear transfer and IBI related bits in MSTATUS.
+ *
+ * This spins forever for those bits to be cleared;
+ *
+ * @see I3C_MSTATUS_SLVSTART_MASK
+ * @see I3C_MSTATUS_MCTRLDONE_MASK
+ * @see I3C_MSTATUS_COMPLETE_MASK
+ * @see I3C_MSTATUS_IBIWON_MASK
+ * @see I3C_MSTATUS_ERRWARN_MASK
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_status_clear_all(I3C_Type *base)
+{
+	uint32_t mask = I3C_MSTATUS_SLVSTART_MASK |
+			I3C_MSTATUS_MCTRLDONE_MASK |
+			I3C_MSTATUS_COMPLETE_MASK |
+			I3C_MSTATUS_IBIWON_MASK |
+			I3C_MSTATUS_ERRWARN_MASK;
+
+	mcux_i3c_status_clear(base, mask);
+}
+
+/**
+ * @brief Clear the MSTATUS bits and wait for them to be cleared with time out.
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be cleared.
+ * @param init_delay_us Initial delay in microsecond before reading register
+ *                      (can be 0).
+ * @param step_delay_us Delay in microsecond between each read of register
+ *                      (cannot be 0).
+ * @param total_delay_us Total delay in microsecond before bailing out.
+ *
+ * @retval 0 If bits are cleared before time out.
+ * @retval -ETIMEDOUT Exhausted all delays.
+ */
+static inline int mcux_i3c_status_clear_timeout(I3C_Type *base, uint32_t mask,
+						uint32_t init_delay_us,
+						uint32_t step_delay_us,
+						uint32_t total_delay_us)
+{
+	uint32_t delayed = init_delay_us;
+	int ret = -ETIMEDOUT;
+
+	/* Try to clear bit until it is cleared */
+	while (delayed <= total_delay_us) {
+		base->MSTATUS = mask;
+
+		if (!mcux_i3c_status_is_set(base, mask)) {
+			ret = 0;
+			break;
+		}
+
+		k_busy_wait(step_delay_us);
+		delayed += step_delay_us;
+	}
+
+	return ret;
+}
+
+/**
+ * @brief Spin wait for MSTATUS bit to be set, and clear it afterwards.
+ *
+ * Note that this spins forever waiting for bits to be set, and
+ * to be cleared.
+ *
+ * @see mcux_i3c_status_wait
+ * @see mcux_i3c_status_clear
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be set and to be cleared;
+ */
+static inline void mcux_i3c_status_wait_clear(I3C_Type *base, uint32_t mask)
+{
+	mcux_i3c_status_wait(base, mask);
+	mcux_i3c_status_clear(base, mask);
+}
+
+/**
+ * @brief Wait for MSTATUS bit to be set, and clear it afterwards, with time out.
+ *
+ * @see mcux_i3c_status_wait_timeout
+ * @see mcux_i3c_status_clear_timeout
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be set and to be cleared.
+ * @param init_delay_us Initial delay in microsecond before reading register
+ *                      (can be 0).
+ * @param step_delay_us Delay in microsecond between each read of register
+ *                      (cannot be 0).
+ * @param total_delay_us Total delay in microsecond before bailing out.
+ *
+ * @retval 0 If masked register value matches before time out.
+ * @retval -ETIMEDOUT Exhausted all delays without matching.
+ */
+static inline int mcux_i3c_status_wait_clear_timeout(I3C_Type *base, uint32_t mask,
+						     uint32_t init_delay_us,
+						     uint32_t step_delay_us,
+						     uint32_t total_delay_us)
+{
+	int ret;
+
+	ret = mcux_i3c_status_wait_timeout(base, mask, init_delay_us,
+					   step_delay_us, total_delay_us);
+	if (ret != 0) {
+		goto out;
+	}
+
+	ret = mcux_i3c_status_clear_timeout(base, mask, init_delay_us,
+					    step_delay_us, total_delay_us);
+
+out:
+	return ret;
+}
+
+/**
+ * @brief Clear the MERRWARN register.
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_errwarn_clear_all_nowait(I3C_Type *base)
+{
+	base->MERRWARN = base->MERRWARN;
+}
+
+/**
+ * @brief Tell controller to start DAA process.
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_request_daa(I3C_Type *base)
+{
+	reg32_update(&base->MCTRL,
+		     I3C_MCTRL_REQUEST_MASK | I3C_MCTRL_IBIRESP_MASK | I3C_MCTRL_RDTERM_MASK,
+		     I3C_MCTRL_REQUEST_PROCESS_DAA | I3C_MCTRL_IBIRESP_NACK);
+}
+
+/**
+ * @brief Tell controller to start auto IBI.
+ *
+ * This also waits for the controller to indicate auto IBI
+ * has started before returning.
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_request_auto_ibi(I3C_Type *base)
+{
+	reg32_update(&base->MCTRL,
+		     I3C_MCTRL_REQUEST_MASK | I3C_MCTRL_IBIRESP_MASK | I3C_MCTRL_RDTERM_MASK,
+		     I3C_MCTRL_REQUEST_AUTO_IBI | I3C_MCTRL_IBIRESP_ACK_AUTO);
+
+	mcux_i3c_status_wait_clear(base, I3C_MSTATUS_MCTRLDONE_MASK);
+}
+
+/**
+ * @brief Get the controller state.
+ *
+ * @param base Pointer to controller registers.
+ *
+ * @retval I3C_MSTATUS_STATE_IDLE
+ * @retval I3C_MSTATUS_STATE_SLVREQ
+ * @retval I3C_MSTATUS_STATE_MSGSDR
+ * @retval I3C_MSTATUS_STATE_NORMACT
+ * @retval I3C_MSTATUS_STATE_MSGDDR
+ * @retval I3C_MSTATUS_STATE_DAA
+ * @retval I3C_MSTATUS_STATE_IBIACK
+ * @retval I3C_MSTATUS_STATE_IBIRCV
+ */
+static inline uint32_t mcux_i3c_state_get(I3C_Type *base)
+{
+	uint32_t mstatus = base->MSTATUS;
+	uint32_t state;
+
+	/* Make sure we are in a state where we can emit STOP */
+	state = (mstatus & I3C_MSTATUS_STATE_MASK) >> I3C_MSTATUS_STATE_SHIFT;
+
+	return state;
+}
+
+/**
+ * @brief Wait for MSTATUS bit to be set, and clear it afterwards with time out.
+ *
+ * @param base Pointer to controller registers.
+ * @param mask Bits to be set.
+ * @param init_delay_us Initial delay in microsecond before reading register
+ *                      (can be 0).
+ * @param step_delay_us Delay in microsecond between each read of register
+ *                      (cannot be 0).
+ * @param total_delay_us Total delay in microsecond before bailing out.
+ *
+ * @retval 0 If masked register value matches before time out.
+ * @retval -ETIMEDOUT Exhausted all delays without matching.
+ */
+static inline int mcux_i3c_state_wait_timeout(I3C_Type *base, uint32_t state,
+					      uint32_t init_delay_us,
+					      uint32_t step_delay_us,
+					      uint32_t total_delay_us)
+{
+	uint32_t delayed = init_delay_us;
+	int ret = -ETIMEDOUT;
+
+	/* Try to clear bit until it is cleared */
+	while (delayed <= total_delay_us) {
+		if (mcux_i3c_state_get(base) == state) {
+			ret = 0;
+			break;
+		}
+
+		k_busy_wait(step_delay_us);
+		delayed += step_delay_us;
+	}
+
+	return ret;
+}
+
+/**
+ * @brief Tell controller to emit START.
+ *
+ * @param base Pointer to controller registers.
+ * @param addr Target address.
+ * @param is_i2c True if this is I2C transactions, false if I3C.
+ * @param is_read True if this is a read transaction, false if write.
+ * @param read_sz Number of bytes to read if @p is_read is true.
+ *
+ * @return 0 if successful, or negative if error.
+ */
+static int mcux_i3c_request_emit_start(I3C_Type *base, uint8_t addr, bool is_i2c,
+				       bool is_read, size_t read_sz)
+{
+	uint32_t mctrl;
+	int ret = 0;
+
+	mctrl = is_i2c ? I3C_MCTRL_TYPE_I2C : I3C_MCTRL_TYPE_I3C;
+	mctrl |= I3C_MCTRL_IBIRESP_NACK;
+
+	if (is_read) {
+		mctrl |= I3C_MCTRL_DIR_READ;
+
+		/* How many bytes to read */
+		mctrl |= I3C_MCTRL_RDTERM(read_sz);
+	} else {
+		mctrl |= I3C_MCTRL_DIR_WRITE;
+	}
+
+	mctrl |= I3C_MCTRL_REQUEST_EMIT_START_ADDR | I3C_MCTRL_ADDR(addr);
+
+	base->MCTRL = mctrl;
+
+	/* Wait for controller to say the operation is done */
+	ret = mcux_i3c_status_wait_clear_timeout(base, I3C_MSTATUS_MCTRLDONE_MASK,
+						 0, 10, 1000);
+	if (ret == 0) {
+		/* Check for NACK */
+		if (mcux_i3c_error_is_nack(base)) {
+			ret = -ENODEV;
+		}
+	}
+
+	return ret;
+}
+
+/**
+ * @brief Tell controller to emit STOP.
+ *
+ * This emits STOP when controller is in NORMACT state as this is
+ * the only valid state where STOP can be emitted. This also waits
+ * for the controller to get out of NORMACT before returning.
+ *
+ * @param base Pointer to controller registers.
+ * @param wait_stop True if need to wait for controller to be
+ *                  no longer in NORMACT.
+ */
+static inline void mcux_i3c_request_emit_stop(I3C_Type *base, bool wait_stop)
+{
+	/* Make sure we are in a state where we can emit STOP */
+	if (mcux_i3c_state_get(base) != I3C_MSTATUS_STATE_NORMACT) {
+		return;
+	}
+
+	reg32_update(&base->MCTRL,
+		     I3C_MCTRL_REQUEST_MASK | I3C_MCTRL_DIR_MASK | I3C_MCTRL_RDTERM_MASK,
+		     I3C_MCTRL_REQUEST_EMIT_STOP);
+
+	mcux_i3c_status_wait_clear(base, I3C_MSTATUS_MCTRLDONE_MASK);
+
+	if (wait_stop) {
+		/*
+		 * Note that we don't exactly wait for I3C_MSTATUS_STATE_IDLE.
+		 * If there is an incoming IBI, it will get stuck forever
+		 * as state would be I3C_MSTATUS_STATE_SLVREQ.
+		 */
+		while (reg32_test_match(&base->MSTATUS, I3C_MSTATUS_STATE_MASK,
+					I3C_MSTATUS_STATE_NORMACT)) {
+			if (mcux_i3c_has_error(base)) {
+				/*
+				 * Bail out if there is any error so
+				 * we won't loop forever.
+				 */
+				return;
+			}
+
+			k_busy_wait(10);
+		};
+	}
+}
+
+/**
+ * @brief Tell controller to NACK the incoming IBI.
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_ibi_respond_nack(I3C_Type *base)
+{
+	reg32_update(&base->MCTRL,
+		     I3C_MCTRL_REQUEST_MASK | I3C_MCTRL_IBIRESP_MASK,
+		     I3C_MCTRL_REQUEST_IBI_ACK_NACK | I3C_MCTRL_IBIRESP_NACK);
+
+	mcux_i3c_status_wait_clear(base, I3C_MSTATUS_MCTRLDONE_MASK);
+}
+
+/**
+ * @brief Tell controller to ACK the incoming IBI.
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_ibi_respond_ack(I3C_Type *base)
+{
+	reg32_update(&base->MCTRL,
+		     I3C_MCTRL_REQUEST_MASK | I3C_MCTRL_IBIRESP_MASK,
+		     I3C_MCTRL_REQUEST_IBI_ACK_NACK | I3C_MCTRL_IBIRESP_ACK_AUTO);
+
+	mcux_i3c_status_wait_clear(base, I3C_MSTATUS_MCTRLDONE_MASK);
+}
+
+/**
+ * @brief Get the number of bytes in RX FIFO.
+ *
+ * This returns the number of bytes in RX FIFO which
+ * can be read.
+ *
+ * @param base Pointer to controller registers.
+ *
+ * @return Number of bytes in RX FIFO.
+ */
+static inline int mcux_i3c_fifo_rx_count_get(I3C_Type *base)
+{
+	uint32_t mdatactrl = base->MDATACTRL;
+
+	return (int)((mdatactrl & I3C_MDATACTRL_RXCOUNT_MASK) >> I3C_MDATACTRL_RXCOUNT_SHIFT);
+}
+
+/**
+ * @brief Tell controller to flush both TX and RX FIFOs.
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_fifo_flush(I3C_Type *base)
+{
+	base->MDATACTRL = I3C_MDATACTRL_FLUSHFB_MASK | I3C_MDATACTRL_FLUSHTB_MASK;
+}
+
+/**
+ * @brief Prepare the controller for transfers.
+ *
+ * This is simply a wrapper to clear out status bits,
+ * and error bits. Also this tells the controller to
+ * flush both TX and RX FIFOs.
+ *
+ * @param base Pointer to controller registers.
+ */
+static inline void mcux_i3c_xfer_reset(I3C_Type *base)
+{
+	mcux_i3c_status_clear_all(base);
+	mcux_i3c_errwarn_clear_all_nowait(base);
+	mcux_i3c_fifo_flush(base);
+}
+
+/**
+ * @brief Drain RX FIFO.
+ *
+ * @param dev Pointer to controller device driver instance.
+ */
+static void mcux_i3c_fifo_rx_drain(const struct device *dev)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	I3C_Type *base = config->base;
+	uint8_t buf;
+
+	/* Read from FIFO as long as RXPEND is set. */
+	while (mcux_i3c_status_is_set(base, I3C_MSTATUS_RXPEND_MASK)) {
+		buf = base->MRDATAB;
+	}
+}
+
+/**
+ * @brief Find a registered I3C target device.
+ *
+ * This returns the I3C device descriptor of the I3C device
+ * matching the incoming @p id.
+ *
+ * @param dev Pointer to controller device driver instance.
+ * @param id Pointer to I3C device ID.
+ *
+ * @return @see i3c_device_find.
+ */
+static
+struct i3c_device_desc *mcux_i3c_device_find(const struct device *dev,
+					     const struct i3c_device_id *id)
+{
+	const struct mcux_i3c_config *config = dev->config;
+
+	return i3c_dev_list_find(&config->device_list, id);
+}
+
+/**
+ * Find a registered I2C target device.
+ *
+ * Controller only API.
+ *
+ * This returns the I2C device descriptor of the I2C device
+ * matching the device address @p addr.
+ *
+ * @param dev Pointer to controller device driver instance.
+ * @param id I2C target device address.
+ *
+ * @return @see i3c_i2c_device_find.
+ */
+static struct i3c_i2c_device_desc *
+mcux_i3c_i2c_device_find(const struct device *dev, uint16_t addr)
+{
+	const struct mcux_i3c_config *config = dev->config;
+
+	return i3c_dev_list_i2c_addr_find(&config->device_list, addr);
+}
+
+/**
+ * @brief Perform bus recovery.
+ *
+ * @param dev Pointer to controller device driver instance.
+ */
+static int mcux_i3c_recover_bus(const struct device *dev)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	I3C_Type *base = config->base;
+	int ret = 0;
+
+	/*
+	 * If the controller is in NORMACT state, tells it to emit STOP
+	 * so it can return to IDLE, or is ready to clear any pending
+	 * target initiated IBIs.
+	 */
+	if (mcux_i3c_state_get(base) == I3C_MSTATUS_STATE_NORMACT) {
+		mcux_i3c_request_emit_stop(base, true);
+	};
+
+	/* Exhaust all target initiated IBI */
+	while (mcux_i3c_status_is_set(base, I3C_MSTATUS_SLVSTART_MASK)) {
+		/* Tell the controller to perform auto IBI. */
+		mcux_i3c_request_auto_ibi(base);
+
+		if (mcux_i3c_status_wait_clear_timeout(base, I3C_MSTATUS_COMPLETE_MASK,
+						       0, 10, 1000) == -ETIMEDOUT) {
+			break;
+		}
+
+		/* Once auto IBI is done, discard bytes in FIFO. */
+		mcux_i3c_fifo_rx_drain(dev);
+
+		/*
+		 * There might be other IBIs waiting.
+		 * So pause a bit to let other targets initiates
+		 * their IBIs.
+		 */
+		k_busy_wait(100);
+	}
+
+	if (reg32_poll_timeout(&base->MSTATUS, I3C_MSTATUS_STATE_MASK,
+			       I3C_MSTATUS_STATE_IDLE, 0, 10, 1000) == -ETIMEDOUT) {
+		ret = -EBUSY;
+	}
+
+	return ret;
+}
+
+/**
+ * @brief Perform one read transaction.
+ *
+ * This reads from RX FIFO until COMPLETE bit is set in MSTATUS
+ * or time out.
+ *
+ * @param base Pointer to controller registers.
+ * @param buf Buffer to store data.
+ * @param buf_sz Buffer size in bytes.
+ *
+ * @return Number of bytes read, or negative if error.
+ */
+static int mcux_i3c_do_one_xfer_read(I3C_Type *base, uint8_t *buf, uint8_t buf_sz)
+{
+	int rx_count;
+	bool completed = false;
+	bool overflow = false;
+	int ret = 0;
+	int offset = 0;
+
+	while (!completed) {
+		/*
+		 * Test if the COMPLETE bit is set.
+		 */
+		if (mcux_i3c_status_is_set(base, I3C_MSTATUS_COMPLETE_MASK)) {
+			completed = true;
+		}
+
+		/*
+		 * If controller says timed out, we abort the transaction.
+		 */
+		if (mcux_i3c_has_error(base)) {
+			if (mcux_i3c_error_is_timeout(base)) {
+
+				ret = -ETIMEDOUT;
+			}
+
+			base->MERRWARN = base->MERRWARN;
+
+			goto one_xfer_read_out;
+		}
+
+		/*
+		 * Transfer data from FIFO into buffer.
+		 */
+		rx_count = mcux_i3c_fifo_rx_count_get(base);
+		while (rx_count > 0) {
+			uint8_t data = (uint8_t)base->MRDATAB;
+
+			if (offset < buf_sz) {
+				buf[offset] = data;
+				offset += 1;
+			} else {
+				overflow = true;
+			}
+
+			rx_count -= 1;
+		}
+	}
+
+	if (overflow) {
+		ret = -EINVAL;
+	} else {
+		ret = offset;
+	}
+
+one_xfer_read_out:
+	return ret;
+}
+
+/**
+ * @brief Perform one write transaction.
+ *
+ * This writes all data in @p buf to TX FIFO or time out
+ * waiting for FIFO spaces.
+ *
+ * @param base Pointer to controller registers.
+ * @param buf Buffer containing data to be sent.
+ * @param buf_sz Number of bytes in @p buf to send.
+ * @param no_ending True if not to signal end of write message.
+ *
+ * @return Number of bytes written, or negative if error.
+ */
+static int mcux_i3c_do_one_xfer_write(I3C_Type *base, uint8_t *buf, uint8_t buf_sz, bool no_ending)
+{
+	int offset = 0;
+	int remaining = buf_sz;
+	int ret = 0;
+
+	while (remaining > 0) {
+		ret = reg32_poll_timeout(&base->MDATACTRL, I3C_MDATACTRL_TXFULL_MASK, 0,
+					 0, 10, 1000);
+		if (ret == -ETIMEDOUT) {
+			goto one_xfer_write_out;
+		}
+
+		if ((remaining > 1) || no_ending) {
+			base->MWDATAB = (uint32_t)buf[offset];
+		} else {
+			base->MWDATABE = (uint32_t)buf[offset];
+		}
+
+		offset += 1;
+		remaining -= 1;
+	}
+
+	ret = offset;
+
+one_xfer_write_out:
+	return ret;
+}
+
+/**
+ * @brief Perform one transfer transaction.
+ *
+ * @param base Pointer to controller registers.
+ * @param data Pointer to controller device instance data.
+ * @param addr Target address.
+ * @param is_i2c True if this is I2C transactions, false if I3C.
+ * @param buf Buffer for data to be sent or received.
+ * @param buf_sz Buffer size in bytes.
+ * @param is_read True if this is a read transaction, false if write.
+ * @param emit_start True if START is needed before read/write.
+ * @param emit_stop True if STOP is needed after read/write.
+ * @param no_ending True if not to signal end of write message.
+ *
+ * @return Number of bytes read/written, or negative if error.
+ */
+static int mcux_i3c_do_one_xfer(I3C_Type *base, struct mcux_i3c_data *data,
+				uint8_t addr, bool is_i2c,
+				uint8_t *buf, size_t buf_sz,
+				bool is_read, bool emit_start, bool emit_stop,
+				bool no_ending)
+{
+	int ret = 0;
+
+	mcux_i3c_status_clear_all(base);
+	mcux_i3c_errwarn_clear_all_nowait(base);
+
+	/* Emit START if so desired */
+	if (emit_start) {
+		ret = mcux_i3c_request_emit_start(base, addr, is_i2c, is_read, buf_sz);
+		if (ret != 0) {
+			emit_stop = true;
+
+			goto out_one_xfer;
+		}
+	}
+
+	if (buf == NULL) {
+		goto out_one_xfer;
+	}
+
+	if (is_read) {
+		ret = mcux_i3c_do_one_xfer_read(base, buf, buf_sz);
+	} else {
+		ret = mcux_i3c_do_one_xfer_write(base, buf, buf_sz, no_ending);
+	}
+
+	if (is_read || !no_ending) {
+		/* Wait for controller to say the operation is done */
+		ret = mcux_i3c_status_wait_clear_timeout(base, I3C_MSTATUS_COMPLETE_MASK,
+							 0, 10, 1000);
+		if (ret != 0) {
+			emit_stop = true;
+
+			goto out_one_xfer;
+		}
+	}
+
+	if (mcux_i3c_has_error(base)) {
+		ret = -EIO;
+	}
+
+out_one_xfer:
+	if (emit_stop) {
+		mcux_i3c_request_emit_stop(base, true);
+	}
+
+	return ret;
+}
+
+/**
+ * @brief Transfer messages in I3C mode.
+ *
+ * @see i3c_transfer
+ *
+ * @param dev Pointer to device driver instance.
+ * @param target Pointer to target device descriptor.
+ * @param msgs Pointer to I3C messages.
+ * @param num_msgs Number of messages to transfers.
+ *
+ * @return @see i3c_transfer
+ */
+static int mcux_i3c_transfer(const struct device *dev,
+			     struct i3c_device_desc *target,
+			     struct i3c_msg *msgs,
+			     uint8_t num_msgs)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *dev_data = dev->data;
+	I3C_Type *base = config->base;
+	uint32_t intmask;
+	int ret;
+
+	if (target->dynamic_addr == 0U) {
+		ret = -EINVAL;
+		goto out_xfer_i3c;
+	}
+
+	k_sem_take(&dev_data->lock, K_FOREVER);
+
+	intmask = mcux_i3c_interrupt_disable(base);
+
+	ret = mcux_i3c_state_wait_timeout(base, I3C_MSTATUS_STATE_IDLE, 0, 100, 100000);
+	if (ret == -ETIMEDOUT) {
+		goto out_xfer_i3c_unlock;
+	}
+
+	mcux_i3c_xfer_reset(base);
+
+	/* Iterate over all the messages */
+	for (int i = 0; i < num_msgs; i++) {
+		bool is_read = (msgs[i].flags & I3C_MSG_RW_MASK) == I3C_MSG_READ;
+		bool no_ending = false;
+
+		/*
+		 * Emit start if this is the first message or that
+		 * the RESTART flag is set in message.
+		 */
+		bool emit_start = (i == 0) ||
+				  ((msgs[i].flags & I3C_MSG_RESTART) == I3C_MSG_RESTART);
+
+		bool emit_stop = (msgs[i].flags & I3C_MSG_STOP) == I3C_MSG_STOP;
+
+		/*
+		 * The controller requires special treatment of last byte of
+		 * a write message. Since the API permits having a bunch of
+		 * write messages without RESTART in between, this is just some
+		 * logic to determine whether to treat the last byte of this
+		 * message to be the last byte of a series of write mssages.
+		 * If not, tell the write function not to treat it that way.
+		 */
+		if (!is_read && !emit_stop && ((i + 1) != num_msgs)) {
+			bool next_is_write =
+				(msgs[i + 1].flags & I3C_MSG_RW_MASK) == I3C_MSG_WRITE;
+			bool next_is_restart =
+				((msgs[i + 1].flags & I3C_MSG_RESTART) == I3C_MSG_RESTART);
+
+			if (next_is_write && !next_is_restart) {
+				no_ending = true;
+			}
+		}
+
+		ret = mcux_i3c_do_one_xfer(base, dev_data, target->dynamic_addr, false,
+					   msgs[i].buf, msgs[i].len,
+					   is_read, emit_start, emit_stop, no_ending);
+		if (ret < 0) {
+			goto out_xfer_i3c_stop_unlock;
+		}
+	}
+
+	ret = 0;
+
+out_xfer_i3c_stop_unlock:
+	mcux_i3c_request_emit_stop(base, true);
+
+out_xfer_i3c_unlock:
+	mcux_i3c_errwarn_clear_all_nowait(base);
+	mcux_i3c_status_clear_all(base);
+
+	mcux_i3c_interrupt_enable(base, intmask);
+
+	k_sem_give(&dev_data->lock);
+
+out_xfer_i3c:
+	return ret;
+}
+
+/**
+ * @brief Transfer messages in I2C mode.
+ *
+ * @see i3c_i2c_transfer
+ *
+ * @param dev Pointer to device driver instance.
+ * @param target Pointer to target device descriptor.
+ * @param msgs Pointer to I2C messages.
+ * @param num_msgs Number of messages to transfers.
+ *
+ * @return @see i3c_i2c_transfer
+ */
+static int mcux_i3c_i2c_transfer(const struct device *dev,
+				 struct i3c_i2c_device_desc *i2c_dev,
+				 struct i2c_msg *msgs,
+				 uint8_t num_msgs)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *dev_data = dev->data;
+	I3C_Type *base = config->base;
+	uint32_t intmask;
+	int ret;
+
+	k_sem_take(&dev_data->lock, K_FOREVER);
+
+	intmask = mcux_i3c_interrupt_disable(base);
+
+	ret = mcux_i3c_state_wait_timeout(base, I3C_MSTATUS_STATE_IDLE, 0, 100, 100000);
+	if (ret == -ETIMEDOUT) {
+		goto out_xfer_i2c_unlock;
+	}
+
+	mcux_i3c_xfer_reset(base);
+
+	/* Iterate over all the messages */
+	for (int i = 0; i < num_msgs; i++) {
+		bool is_read = (msgs[i].flags & I2C_MSG_RW_MASK) == I2C_MSG_READ;
+		bool no_ending = false;
+
+		/*
+		 * Emit start if this is the first message or that
+		 * the RESTART flag is set in message.
+		 */
+		bool emit_start = (i == 0) ||
+				  ((msgs[i].flags & I2C_MSG_RESTART) == I2C_MSG_RESTART);
+
+		bool emit_stop = (msgs[i].flags & I2C_MSG_STOP) == I2C_MSG_STOP;
+
+		/*
+		 * The controller requires special treatment of last byte of
+		 * a write message. Since the API permits having a bunch of
+		 * write messages without RESTART in between, this is just some
+		 * logic to determine whether to treat the last byte of this
+		 * message to be the last byte of a series of write mssages.
+		 * If not, tell the write function not to treat it that way.
+		 */
+		if (!is_read && !emit_stop && ((i + 1) != num_msgs)) {
+			bool next_is_write =
+				(msgs[i + 1].flags & I2C_MSG_RW_MASK) == I2C_MSG_WRITE;
+			bool next_is_restart =
+				((msgs[i + 1].flags & I2C_MSG_RESTART) == I2C_MSG_RESTART);
+
+			if (next_is_write && !next_is_restart) {
+				no_ending = true;
+			}
+		}
+
+		ret = mcux_i3c_do_one_xfer(base, dev_data, i2c_dev->addr, true,
+					   msgs[i].buf, msgs[i].len,
+					   is_read, emit_start, emit_stop, no_ending);
+		if (ret < 0) {
+			goto out_xfer_i2c_stop_unlock;
+		}
+	}
+
+	ret = 0;
+
+out_xfer_i2c_stop_unlock:
+	mcux_i3c_request_emit_stop(base, true);
+
+out_xfer_i2c_unlock:
+	mcux_i3c_errwarn_clear_all_nowait(base);
+	mcux_i3c_status_clear_all(base);
+
+	mcux_i3c_interrupt_enable(base, intmask);
+
+	k_sem_give(&dev_data->lock);
+
+	return ret;
+}
+
+/**
+ * @brief Perform Dynamic Address Assignment.
+ *
+ * @see i3c_do_daa
+ *
+ * @param dev Pointer to controller device driver instance.
+ *
+ * @return @see i3c_do_daa
+ */
+static int mcux_i3c_do_daa(const struct device *dev)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *data = dev->data;
+	I3C_Type *base = config->base;
+	int ret = 0;
+	uint8_t rx_buf[8] = {0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU, 0xFFU};
+	size_t rx_count;
+	uint8_t rx_size = 0;
+	uint32_t intmask;
+
+	k_sem_take(&data->lock, K_FOREVER);
+
+	ret = mcux_i3c_state_wait_timeout(base, I3C_MSTATUS_STATE_IDLE, 0, 100, 100000);
+	if (ret == -ETIMEDOUT) {
+		goto out_daa_unlock;
+	}
+
+	LOG_DBG("DAA: ENTDAA");
+
+	/* Disable I3C IRQ sources while we configure stuff. */
+	intmask = mcux_i3c_interrupt_disable(base);
+
+	mcux_i3c_xfer_reset(base);
+
+	/* Emit process DAA */
+	mcux_i3c_request_daa(base);
+
+	/* Loop until no more responses from devices */
+	do {
+		/* Loop to grab data from devices (Provisioned ID, BCR and DCR) */
+		do {
+			if (mcux_i3c_has_error(base)) {
+				LOG_ERR("DAA recv error");
+
+				ret = -EIO;
+
+				goto out_daa;
+			}
+
+			rx_count = mcux_i3c_fifo_rx_count_get(base);
+			while (mcux_i3c_status_is_set(base, I3C_MSTATUS_RXPEND_MASK) &&
+			       (rx_count != 0U)) {
+				rx_buf[rx_size] = (uint8_t)(base->MRDATAB &
+							    I3C_MRDATAB_VALUE_MASK);
+				rx_size++;
+				rx_count--;
+			}
+		} while (!mcux_i3c_status_is_set(base, I3C_MSTATUS_MCTRLDONE_MASK));
+
+		mcux_i3c_status_clear(base, I3C_MSTATUS_MCTRLDONE_MASK);
+
+		/* Figure out what address to assign to device */
+		if ((mcux_i3c_state_get(base) == I3C_MSTATUS_STATE_DAA) &&
+		    (mcux_i3c_status_is_set(base, I3C_MSTATUS_BETWEEN_MASK))) {
+			struct i3c_device_desc *target;
+			uint16_t vendor_id;
+			uint32_t part_no;
+			uint64_t pid;
+			uint8_t dyn_addr;
+
+			rx_size = 0;
+
+			/* Vendor ID portion of Provisioned ID */
+			vendor_id = (((uint16_t)rx_buf[0] << 8U) | (uint16_t)rx_buf[1]) &
+				    0xFFFEU;
+
+			/* Part Number portion of Provisioned ID */
+			part_no = (uint32_t)rx_buf[2] << 24U | (uint32_t)rx_buf[3] << 16U |
+				  (uint32_t)rx_buf[4] << 8U | (uint32_t)rx_buf[5];
+
+			/* ... and combine into one Provisioned ID */
+			pid = (uint64_t)vendor_id << 32U | (uint64_t)part_no;
+
+			LOG_DBG("DAA: Rcvd PID 0x%04x%08x", vendor_id, part_no);
+
+			ret = i3c_dev_list_daa_addr_helper(&data->addr_slots,
+							   &config->device_list, pid,
+							   false, false,
+							   &target, &dyn_addr);
+			if (ret != 0) {
+				goto out_daa;
+			}
+
+			/* Update target descriptor */
+			target->dynamic_addr = dyn_addr;
+			target->bcr = rx_buf[6];
+			target->dcr = rx_buf[7];
+
+			/* Mark the address as I3C device */
+			i3c_addr_slots_mark_i3c(&data->addr_slots, dyn_addr);
+
+			/*
+			 * If the device has static address, after address assignment,
+			 * the device will not respond to the static address anymore.
+			 * So free the static one from address slots if different from
+			 * newly assigned one.
+			 */
+			if ((target->static_addr != 0U) && (dyn_addr != target->static_addr)) {
+				i3c_addr_slots_mark_free(&data->addr_slots, dyn_addr);
+			}
+
+			/* Emit process DAA again to send the address to the device */
+			base->MWDATAB = dyn_addr;
+			mcux_i3c_request_daa(base);
+
+			LOG_DBG("PID 0x%04x%08x assigned dynamic address 0x%02x",
+				vendor_id, part_no, dyn_addr);
+		}
+
+	} while (!mcux_i3c_status_is_set(base, I3C_MSTATUS_COMPLETE_MASK));
+
+out_daa:
+	/* Clear all flags. */
+	mcux_i3c_errwarn_clear_all_nowait(base);
+	mcux_i3c_status_clear_all(base);
+
+	/* Re-Enable I3C IRQ sources. */
+	mcux_i3c_interrupt_enable(base, intmask);
+
+out_daa_unlock:
+	k_sem_give(&data->lock);
+
+	return ret;
+
+	return -EIO;
+}
+
+/**
+ * @brief Send Common Command Code (CCC).
+ *
+ * @see i3c_do_ccc
+ *
+ * @param dev Pointer to controller device driver instance.
+ * @param payload Pointer to CCC payload.
+ *
+ * @return @see i3c_do_ccc
+ */
+static int mcux_i3c_do_ccc(const struct device *dev,
+			   struct i3c_ccc_payload *payload)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *data = dev->data;
+	I3C_Type *base = config->base;
+	int ret = 0;
+	uint32_t intmask;
+
+	if (payload == NULL) {
+		return -EINVAL;
+	}
+
+	k_sem_take(&data->lock, K_FOREVER);
+
+	intmask = mcux_i3c_interrupt_disable(base);
+
+	mcux_i3c_xfer_reset(base);
+
+	LOG_DBG("CCC[0x%02x]", payload->ccc.id);
+
+	/* Emit START */
+	ret = mcux_i3c_request_emit_start(base, I3C_BROADCAST_ADDR, false, false, 0);
+	if (ret < 0) {
+		LOG_ERR("CCC[0x%02x] %s START error (%d)",
+			payload->ccc.id,
+			i3c_ccc_is_payload_broadcast(payload) ? "broadcast" : "direct",
+			ret);
+
+		goto out_ccc_stop;
+	}
+
+	/* Write the CCC code */
+	mcux_i3c_status_clear_all(base);
+	mcux_i3c_errwarn_clear_all_nowait(base);
+	ret = mcux_i3c_do_one_xfer_write(base, &payload->ccc.id, 1,
+					 payload->ccc.data_len > 0);
+	if (ret < 0) {
+		LOG_ERR("CCC[0x%02x] %s command error (%d)",
+			payload->ccc.id,
+			i3c_ccc_is_payload_broadcast(payload) ? "broadcast" : "direct",
+			ret);
+
+		goto out_ccc_stop;
+	}
+
+	/* Write additional data for CCC if needed */
+	if (payload->ccc.data_len > 0) {
+		mcux_i3c_status_clear_all(base);
+		mcux_i3c_errwarn_clear_all_nowait(base);
+		ret = mcux_i3c_do_one_xfer_write(base, payload->ccc.data,
+						 payload->ccc.data_len, false);
+		if (ret < 0) {
+			LOG_ERR("CCC[0x%02x] %s command payload error (%d)",
+				payload->ccc.id,
+				i3c_ccc_is_payload_broadcast(payload) ? "broadcast" : "direct",
+				ret);
+
+			goto out_ccc_stop;
+		}
+	}
+
+	/* Wait for controller to say the operation is done */
+	ret = mcux_i3c_status_wait_clear_timeout(base, I3C_MSTATUS_COMPLETE_MASK, 0, 10, 1000);
+	if (ret != 0) {
+		goto out_ccc_stop;
+	}
+
+	if (!i3c_ccc_is_payload_broadcast(payload)) {
+		/*
+		 * If there are payload(s) for each target,
+		 * RESTART and then send payload for each target.
+		 */
+		for (int idx = 0; idx < payload->targets.num_targets; idx++) {
+			struct i3c_ccc_target_payload *tgt_payload =
+				&payload->targets.payloads[idx];
+
+			bool is_read = tgt_payload->rnw == 1U;
+			bool emit_start = idx == 0;
+
+			ret = mcux_i3c_do_one_xfer(base, data,
+						   tgt_payload->addr, false,
+						   tgt_payload->data,
+						   tgt_payload->data_len,
+						   is_read, emit_start, false, false);
+			if (ret < 0) {
+				LOG_ERR("CCC[0x%02x] target payload error (%d)",
+					payload->ccc.id, ret);
+
+				goto out_ccc_stop;
+			}
+		}
+	}
+
+out_ccc_stop:
+	mcux_i3c_request_emit_stop(base, true);
+
+	if (ret > 0) {
+		ret = 0;
+	}
+
+	mcux_i3c_interrupt_enable(base, intmask);
+
+	k_sem_give(&data->lock);
+
+	return ret;
+}
+
+#ifdef CONFIG_I3C_USE_IBI
+/**
+ * @brief Callback to service target initiated IBIs.
+ *
+ * @param work Pointer to k_work item.
+ */
+static void mcux_i3c_ibi_work(struct k_work *work)
+{
+	uint8_t payload[CONFIG_I3C_IBI_MAX_PAYLOAD_SIZE];
+	size_t payload_sz = 0;
+
+	struct i3c_ibi_work *i3c_ibi_work = CONTAINER_OF(work, struct i3c_ibi_work, work);
+	const struct device *dev = i3c_ibi_work->controller;
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *data = dev->data;
+	const struct i3c_dev_list *dev_list = &config->device_list;
+	I3C_Type *base = config->base;
+	struct i3c_device_desc *target = NULL;
+	uint32_t mstatus, ibitype, ibiaddr;
+	int ret;
+
+	k_sem_take(&data->ibi_lock, K_FOREVER);
+
+	if (mcux_i3c_state_get(base) != I3C_MSTATUS_STATE_SLVREQ) {
+		LOG_DBG("IBI work %p running not because of IBI", work);
+		LOG_DBG("MSTATUS 0x%08x MERRWARN 0x%08x",
+			base->MSTATUS, base->MERRWARN);
+
+		mcux_i3c_request_emit_stop(base, true);
+
+		goto out_ibi_work;
+	};
+
+	/* Use auto IBI to service the IBI */
+	mcux_i3c_request_auto_ibi(base);
+
+	mstatus = sys_read32((mem_addr_t)&base->MSTATUS);
+	ibiaddr = (mstatus & I3C_MSTATUS_IBIADDR_MASK) >> I3C_MSTATUS_IBIADDR_SHIFT;
+
+	/*
+	 * Note that the I3C_MSTATUS_IBI_TYPE_* are not shifted right.
+	 * So no need to shift here.
+	 */
+	ibitype = (mstatus & I3C_MSTATUS_IBITYPE_MASK);
+
+	/*
+	 * Wait for COMPLETE bit to be set to indicate auto IBI
+	 * has finished for hot-join and controller role request.
+	 * For target interrupts, the IBI payload may be longer
+	 * than the RX FIFO so we won't get the COMPLETE bit set
+	 * at the first round of data read. So checking of
+	 * COMPLETE bit is deferred to the reading.
+	 */
+	switch (ibitype) {
+	case I3C_MSTATUS_IBITYPE_HJ:
+		__fallthrough;
+
+	case I3C_MSTATUS_IBITYPE_MR:
+		if (mcux_i3c_status_wait_timeout(base, I3C_MSTATUS_COMPLETE_MASK,
+						 0, 10, 1000) == -ETIMEDOUT) {
+			LOG_ERR("Timeout waiting for COMPLETE");
+
+			mcux_i3c_request_emit_stop(base, true);
+
+			goto out_ibi_work;
+		}
+		break;
+
+	default:
+		break;
+	};
+
+	switch (ibitype) {
+	case I3C_MSTATUS_IBITYPE_IBI:
+		target = i3c_dev_list_i3c_addr_find(dev_list, (uint8_t)ibiaddr);
+		if (target != NULL) {
+			ret = mcux_i3c_do_one_xfer_read(base, &payload[0],
+							sizeof(payload));
+			if (ret >= 0) {
+				payload_sz = (size_t)ret;
+			} else {
+				LOG_ERR("Error reading IBI payload");
+
+				mcux_i3c_request_emit_stop(base, true);
+
+				goto out_ibi_work;
+			}
+		} else {
+			/* NACK IBI coming from unknown device */
+			mcux_i3c_ibi_respond_nack(base);
+		}
+		break;
+	case I3C_MSTATUS_IBITYPE_HJ:
+		mcux_i3c_ibi_respond_ack(base);
+		break;
+	case I3C_MSTATUS_IBITYPE_MR:
+		LOG_DBG("Controller role handoff not supported");
+		mcux_i3c_ibi_respond_nack(base);
+		break;
+	default:
+		break;
+	}
+
+	if (mcux_i3c_has_error(base)) {
+		/*
+		 * If the controller detects any errors, simply
+		 * emit a STOP to abort the IBI. The target will
+		 * raise IBI again if so desired.
+		 */
+		mcux_i3c_request_emit_stop(base, true);
+
+		goto out_ibi_work;
+	}
+
+	switch (ibitype) {
+	case I3C_MSTATUS_IBITYPE_IBI:
+		if (target != NULL) {
+			if (i3c_ibi_work_enqueue_target_irq(target,
+							    &payload[0], payload_sz) != 0) {
+				LOG_ERR("Error enqueue IBI IRQ work");
+			}
+		}
+
+		/* Finishing the IBI transaction */
+		mcux_i3c_request_emit_stop(base, true);
+		break;
+	case I3C_MSTATUS_IBITYPE_HJ:
+		if (i3c_ibi_work_enqueue_hotjoin(dev) != 0) {
+			LOG_ERR("Error enqueue IBI HJ work");
+		}
+		break;
+	case I3C_MSTATUS_IBITYPE_MR:
+		break;
+	default:
+		break;
+	}
+
+out_ibi_work:
+	mcux_i3c_xfer_reset(base);
+
+	k_sem_give(&data->ibi_lock);
+
+	/* Re-enable target initiated IBI interrupt. */
+	base->MINTSET = I3C_MINTSET_SLVSTART_MASK;
+}
+
+static void mcux_i3c_ibi_rules_setup(struct mcux_i3c_data *data,
+				     I3C_Type *base)
+{
+	uint32_t ibi_rules;
+	int idx;
+
+	ibi_rules = 0;
+
+	for (idx = 0; idx < ARRAY_SIZE(data->ibi.addr); idx++) {
+		uint32_t addr_6bit;
+
+		/* Extract the lower 6-bit of target address */
+		addr_6bit = (uint32_t)data->ibi.addr[idx] & I3C_MIBIRULES_ADDR0_MASK;
+
+		/* Shift into correct place */
+		addr_6bit <<= idx * I3C_MIBIRULES_ADDR1_SHIFT;
+
+		/* Put into the temporary IBI Rules register */
+		ibi_rules |= addr_6bit;
+	}
+
+	if (!data->ibi.msb) {
+		/* The MSB0 field is 1 if MSB is 0 */
+		ibi_rules |= I3C_MIBIRULES_MSB0_MASK;
+	}
+
+	if (!data->ibi.has_mandatory_byte) {
+		/* The NOBYTE field is 1 if there is no mandatory byte */
+		ibi_rules |= I3C_MIBIRULES_NOBYTE_MASK;
+	}
+
+	/* Update the register */
+	base->MIBIRULES = ibi_rules;
+
+	LOG_DBG("MIBIRULES 0x%08x", ibi_rules);
+}
+
+int mcux_i3c_ibi_enable(const struct device *dev,
+			struct i3c_device_desc *target)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *data = dev->data;
+	I3C_Type *base = config->base;
+	struct i3c_ccc_events i3c_events;
+	uint8_t idx;
+	bool msb, has_mandatory_byte;
+	int ret = 0;
+
+	if (!i3c_device_is_ibi_capable(target)) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	if (data->ibi.num_addr >= ARRAY_SIZE(data->ibi.addr)) {
+		/* No more free entries in the IBI Rules table */
+		ret = -ENOMEM;
+		goto out;
+	}
+
+	/* Check for duplicate */
+	for (idx = 0; idx < ARRAY_SIZE(data->ibi.addr); idx++) {
+		if (data->ibi.addr[idx] == target->dynamic_addr) {
+			ret = -EINVAL;
+			goto out;
+		}
+	}
+
+	/* Disable controller interrupt while we configure IBI rules. */
+	base->MINTCLR = I3C_MINTCLR_SLVSTART_MASK;
+
+	LOG_DBG("IBI enabling for 0x%02x (BCR 0x%02x)",
+		target->dynamic_addr, target->bcr);
+
+	msb = (target->dynamic_addr & BIT(6)) == BIT(6);
+	has_mandatory_byte = i3c_ibi_has_payload(target);
+
+	/*
+	 * If there are already addresses in the table, we must
+	 * check if the incoming entry is compatible with
+	 * the existing ones.
+	 */
+	if (data->ibi.num_addr > 0) {
+		/*
+		 * 1. All devices in the table must all use mandatory
+		 *    bytes, or do not.
+		 *
+		 * 2. Each address in entry only captures the lowest 6-bit.
+		 *    The MSB (7th bit) is captured separated in another bit
+		 *    in the register. So all addresses must have the same MSB.
+		 */
+		if ((has_mandatory_byte != data->ibi.has_mandatory_byte) ||
+		    (msb != data->ibi.msb)) {
+			ret = -EINVAL;
+			goto out;
+		}
+
+		/* Find an empty address slot */
+		for (idx = 0; idx < ARRAY_SIZE(data->ibi.addr); idx++) {
+			if (data->ibi.addr[idx] == 0U) {
+				break;
+			}
+		}
+	} else {
+		/*
+		 * If the incoming address is the first in the table,
+		 * it dictates future compatibilities.
+		 */
+		data->ibi.has_mandatory_byte = has_mandatory_byte;
+		data->ibi.msb = msb;
+
+		idx = 0;
+	}
+
+	data->ibi.addr[idx] = target->dynamic_addr;
+	data->ibi.num_addr += 1U;
+
+	mcux_i3c_ibi_rules_setup(data, base);
+
+	/* Tell target to enable IBI */
+	i3c_events.events = I3C_CCC_EVT_INTR;
+	ret = i3c_ccc_do_events_set(target, true, &i3c_events);
+	if (ret != 0) {
+		LOG_ERR("Error sending IBI ENEC for 0x%02x (%d)",
+			target->dynamic_addr, ret);
+	}
+
+out:
+	if (data->ibi.num_addr > 0U) {
+		/*
+		 * Enable controller to raise interrupt when a target
+		 * initiates IBI.
+		 */
+		base->MINTSET = I3C_MINTSET_SLVSTART_MASK;
+	}
+
+	return ret;
+}
+
+int mcux_i3c_ibi_disable(const struct device *dev,
+			 struct i3c_device_desc *target)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *data = dev->data;
+	I3C_Type *base = config->base;
+	struct i3c_ccc_events i3c_events;
+	int ret = 0;
+	int idx;
+
+	if (!i3c_device_is_ibi_capable(target)) {
+		ret = -EINVAL;
+		goto out;
+	}
+
+	for (idx = 0; idx < ARRAY_SIZE(data->ibi.addr); idx++) {
+		if (target->dynamic_addr == data->ibi.addr[idx]) {
+			break;
+		}
+	}
+
+	if (idx == ARRAY_SIZE(data->ibi.addr)) {
+		/* Target is not in list of registered addresses. */
+		ret = -ENODEV;
+		goto out;
+	}
+
+	/* Disable controller interrupt while we configure IBI rules. */
+	base->MINTCLR = I3C_MINTCLR_SLVSTART_MASK;
+
+	data->ibi.addr[idx] = 0U;
+	data->ibi.num_addr -= 1U;
+
+	/* Tell target to disable IBI */
+	i3c_events.events = I3C_CCC_EVT_INTR;
+	ret = i3c_ccc_do_events_set(target, false, &i3c_events);
+	if (ret != 0) {
+		LOG_ERR("Error sending IBI DISEC for 0x%02x (%d)",
+			target->dynamic_addr, ret);
+
+		goto out;
+	}
+
+	mcux_i3c_ibi_rules_setup(data, base);
+
+out:
+	if (data->ibi.num_addr > 0U) {
+		/*
+		 * Enable controller to raise interrupt when a target
+		 * initiates IBI.
+		 */
+		base->MINTSET = I3C_MINTSET_SLVSTART_MASK;
+	}
+
+	return ret;
+}
+#endif /* CONFIG_I3C_USE_IBI */
+
+/**
+ * @brief Interrupt Service Routine
+ *
+ * Currently only services interrupts when any target initiates IBIs.
+ *
+ * @param dev Pointer to controller device driver instance.
+ */
+static void mcux_i3c_isr(const struct device *dev)
+{
+#ifdef CONFIG_I3C_USE_IBI
+	const struct mcux_i3c_config *config = dev->config;
+	I3C_Type *base = config->base;
+
+	/* Target initiated IBIs */
+	if (mcux_i3c_status_is_set(base, I3C_MSTATUS_SLVSTART_MASK)) {
+		/*
+		 * Disable further target initiated IBI interrupt
+		 * while we try to service the current one.
+		 */
+		base->MINTCLR = I3C_MINTCLR_SLVSTART_MASK;
+
+		/*
+		 * Handle IBI in workqueue.
+		 */
+		i3c_ibi_work_enqueue_cb(dev, mcux_i3c_ibi_work);
+	}
+#else
+	ARG_UNUSED(dev);
+#endif
+}
+
+/**
+ * @brief Configure I3C hardware.
+ *
+ * @param dev Pointer to controller device driver instance.
+ * @param type Type of configuration parameters being passed
+ *             in @p config.
+ * @param config Pointer to the configuration parameters.
+ *
+ * @retval 0 If successful.
+ * @retval -EINVAL If invalid configure parameters.
+ * @retval -EIO General Input/Output errors.
+ * @retval -ENOSYS If not implemented.
+ */
+static int mcux_i3c_configure(const struct device *dev,
+			      enum i3c_config_type type, void *config)
+{
+	const struct mcux_i3c_config *dev_cfg = dev->config;
+	struct mcux_i3c_data *dev_data = dev->data;
+	I3C_Type *base = dev_cfg->base;
+	i3c_master_config_t *ctrl_config_hal = &dev_data->ctrl_config_hal;
+	struct i3c_config_controller *ctrl_cfg = config;
+	uint32_t clock_freq;
+	int ret = 0;
+
+	if (type != I3C_CONFIG_CONTROLLER) {
+		ret = -EINVAL;
+		goto out_configure;
+	}
+
+	/*
+	 * Check for valid configuration parameters.
+	 *
+	 * Currently, must be the primary controller.
+	 */
+	if ((!ctrl_cfg->is_primary) ||
+	    (ctrl_cfg->scl.i2c == 0U) ||
+	    (ctrl_cfg->scl.i3c == 0U)) {
+		ret = -EINVAL;
+		goto out_configure;
+	}
+
+	/* Get the clock frequency */
+	if (clock_control_get_rate(dev_cfg->clock_dev, dev_cfg->clock_subsys,
+				   &clock_freq)) {
+		ret = -EINVAL;
+		goto out_configure;
+	}
+
+	ctrl_config_hal->baudRate_Hz.i2cBaud = ctrl_cfg->scl.i2c;
+	ctrl_config_hal->baudRate_Hz.i3cPushPullBaud = ctrl_cfg->scl.i3c;
+
+	/* Initialize hardware */
+	I3C_MasterInit(base, ctrl_config_hal, clock_freq);
+
+out_configure:
+	return ret;
+}
+
+/**
+ * @brief Get configuration of the I3C hardware.
+ *
+ * This provides a way to get the current configuration of the I3C hardware.
+ *
+ * This can return cached config or probed hardware parameters, but it has to
+ * be up to date with current configuration.
+ *
+ * @param[in] dev Pointer to controller device driver instance.
+ * @param[in] type Type of configuration parameters being passed
+ *                 in @p config.
+ * @param[in,out] config Pointer to the configuration parameters.
+ *
+ * Note that if @p type is @c I3C_CONFIG_CUSTOM, @p config must contain
+ * the ID of the parameter to be retrieved.
+ *
+ * @retval 0 If successful.
+ * @retval -EIO General Input/Output errors.
+ * @retval -ENOSYS If not implemented.
+ */
+static int mcux_i3c_config_get(const struct device *dev,
+			       enum i3c_config_type type, void *config)
+{
+	struct mcux_i3c_data *data = dev->data;
+	int ret = 0;
+
+	if ((type != I3C_CONFIG_CONTROLLER) || (config == NULL)) {
+		ret = -EINVAL;
+		goto out_configure;
+	}
+
+	(void)memcpy(config, &data->ctrl_config, sizeof(data->ctrl_config));
+
+out_configure:
+	return ret;
+}
+
+/**
+ * @brief Initialize the hardware.
+ *
+ * @param dev Pointer to controller device driver instance.
+ */
+static int mcux_i3c_init(const struct device *dev)
+{
+	const struct mcux_i3c_config *config = dev->config;
+	struct mcux_i3c_data *data = dev->data;
+	I3C_Type *base = config->base;
+	struct i3c_config_controller *ctrl_config = &data->ctrl_config;
+	int ret = 0;
+
+	ret = i3c_addr_slots_init(&data->addr_slots, &config->device_list);
+	if (ret != 0) {
+		goto err_out;
+	}
+
+	CLOCK_SetClkDiv(kCLOCK_DivI3cClk, data->clocks.clk_div_pp);
+	CLOCK_SetClkDiv(kCLOCK_DivI3cSlowClk, data->clocks.clk_div_od);
+	CLOCK_SetClkDiv(kCLOCK_DivI3cTcClk, data->clocks.clk_div_tc);
+
+#ifdef CONFIG_PINCTRL
+	ret = pinctrl_apply_state(config->pincfg, PINCTRL_STATE_DEFAULT);
+	if (ret != 0) {
+		goto err_out;
+	}
+#endif
+
+	k_sem_init(&data->lock, 1, 1);
+	k_sem_init(&data->ibi_lock, 1, 1);
+
+	/*
+	 * Default controller configuration to act as the primary
+	 * and active controller.
+	 */
+	I3C_MasterGetDefaultConfig(&data->ctrl_config_hal);
+
+	/* Set default SCL clock rate (in Hz) */
+	if (ctrl_config->scl.i2c == 0U) {
+		ctrl_config->scl.i2c = data->ctrl_config_hal.baudRate_Hz.i2cBaud;
+	}
+
+	if (ctrl_config->scl.i3c == 0U) {
+		ctrl_config->scl.i3c = data->ctrl_config_hal.baudRate_Hz.i3cPushPullBaud;
+	}
+
+	if (data->clocks.i3c_od_scl_hz != 0U) {
+		data->ctrl_config_hal.baudRate_Hz.i3cOpenDrainBaud = data->clocks.i3c_od_scl_hz;
+	}
+
+	/* Currently can only act as primary controller. */
+	data->ctrl_config.is_primary = true;
+
+	/* HDR mode not supported at the moment. */
+	data->ctrl_config.supported_hdr = 0U;
+
+	ret = mcux_i3c_configure(dev, I3C_CONFIG_CONTROLLER, ctrl_config);
+	if (ret != 0) {
+		ret = -EINVAL;
+		goto err_out;
+	}
+
+	/* Disable all interrupts */
+	base->MINTCLR = I3C_MINTCLR_SLVSTART_MASK |
+			I3C_MINTCLR_MCTRLDONE_MASK |
+			I3C_MINTCLR_COMPLETE_MASK |
+			I3C_MINTCLR_RXPEND_MASK |
+			I3C_MINTCLR_TXNOTFULL_MASK |
+			I3C_MINTCLR_IBIWON_MASK |
+			I3C_MINTCLR_ERRWARN_MASK |
+			I3C_MINTCLR_NOWMASTER_MASK;
+
+	/* Just in case the bus is not in idle. */
+	ret = mcux_i3c_recover_bus(dev);
+	if (ret != 0) {
+		ret = -EIO;
+		goto err_out;
+	}
+
+	/* Configure interrupt */
+	config->irq_config_func(dev);
+
+	/* Perform bus initialization */
+	ret = i3c_bus_init(dev, &config->device_list);
+
+err_out:
+	return ret;
+}
+
+static int mcux_i3c_i2c_api_configure(const struct device *dev, uint32_t dev_config)
+{
+	return -ENOSYS;
+}
+
+static int mcux_i3c_i2c_api_transfer(const struct device *dev,
+				     struct i2c_msg *msgs,
+				     uint8_t num_msgs,
+				     uint16_t addr)
+{
+	struct i3c_i2c_device_desc *i2c_dev =
+		mcux_i3c_i2c_device_find(dev, addr);
+	int ret;
+
+	if (i2c_dev == NULL) {
+		ret = -ENODEV;
+	} else {
+		ret = mcux_i3c_i2c_transfer(dev, i2c_dev, msgs, num_msgs);
+	}
+
+	return ret;
+}
+
+static const struct i3c_driver_api mcux_i3c_driver_api = {
+	.i2c_api.configure = mcux_i3c_i2c_api_configure,
+	.i2c_api.transfer = mcux_i3c_i2c_api_transfer,
+	.i2c_api.recover_bus = mcux_i3c_recover_bus,
+
+	.configure = mcux_i3c_configure,
+	.config_get = mcux_i3c_config_get,
+
+	.recover_bus = mcux_i3c_recover_bus,
+
+	.do_daa = mcux_i3c_do_daa,
+	.do_ccc = mcux_i3c_do_ccc,
+
+	.i3c_device_find = mcux_i3c_device_find,
+
+	.i3c_xfers = mcux_i3c_transfer,
+
+#ifdef CONFIG_I3C_USE_IBI
+	.ibi_enable = mcux_i3c_ibi_enable,
+	.ibi_disable = mcux_i3c_ibi_disable,
+#endif
+};
+
+#ifdef CONFIG_PINCTRL
+#define I3C_MCUX_PINCTRL_DEFINE(n) PINCTRL_DT_INST_DEFINE(n);
+#define I3C_MCUX_PINCTRL_INIT(n) .pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n),
+#else
+#define I3C_MCUX_PINCTRL_DEFINE(n)
+#define I3C_MCUX_PINCTRL_INIT(n)
+#endif
+
+#define I3C_MCUX_DEVICE(id)							\
+	I3C_MCUX_PINCTRL_DEFINE(id)						\
+	static void mcux_i3c_config_func_##id(const struct device *dev);	\
+	static struct i3c_device_desc mcux_i3c_device_array[] =			\
+		I3C_DEVICE_ARRAY_DT_INST(id);					\
+	static struct i3c_i2c_device_desc mcux_i3c_i2c_device_array[] =		\
+		I3C_I2C_DEVICE_ARRAY_DT_INST(id);				\
+	static const struct mcux_i3c_config mcux_i3c_config_##id = {		\
+		.base = (I3C_Type *) DT_INST_REG_ADDR(id),			\
+		.clock_dev = DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(id)),		\
+		.clock_subsys =							\
+			(clock_control_subsys_t)DT_INST_CLOCKS_CELL(id, name),	\
+		.irq_config_func = mcux_i3c_config_func_##id,			\
+		.device_list.i3c = mcux_i3c_device_array,			\
+		.device_list.num_i3c = ARRAY_SIZE(mcux_i3c_device_array),	\
+		.device_list.i2c = mcux_i3c_i2c_device_array,			\
+		.device_list.num_i2c = ARRAY_SIZE(mcux_i3c_i2c_device_array),	\
+		I3C_MCUX_PINCTRL_INIT(id)					\
+	};									\
+	static struct mcux_i3c_data mcux_i3c_data_##id = {			\
+		.clocks.i3c_od_scl_hz = DT_INST_PROP_OR(id, i3c_od_scl_hz, 0),	\
+		.ctrl_config.scl.i3c = DT_INST_PROP_OR(id, i3c_scl_hz, 0),	\
+		.ctrl_config.scl.i2c = DT_INST_PROP_OR(id, i2c_scl_hz, 0),	\
+		.clocks.clk_div_pp = DT_INST_PROP(id, clk_divider),		\
+		.clocks.clk_div_od = DT_INST_PROP(id, clk_divider_slow),	\
+		.clocks.clk_div_tc = DT_INST_PROP(id, clk_divider_tc),		\
+	};									\
+	DEVICE_DT_INST_DEFINE(id,						\
+			      mcux_i3c_init,					\
+			      NULL,						\
+			      &mcux_i3c_data_##id,				\
+			      &mcux_i3c_config_##id,				\
+			      POST_KERNEL,					\
+			      CONFIG_I3C_CONTROLLER_INIT_PRIORITY,		\
+			      &mcux_i3c_driver_api);				\
+	static void mcux_i3c_config_func_##id(const struct device *dev)		\
+	{									\
+		IRQ_CONNECT(DT_INST_IRQN(id),					\
+			    DT_INST_IRQ(id, priority),				\
+			    mcux_i3c_isr,					\
+			    DEVICE_DT_INST_GET(id),				\
+			    0);							\
+		irq_enable(DT_INST_IRQN(id));					\
+	};									\
+
+DT_INST_FOREACH_STATUS_OKAY(I3C_MCUX_DEVICE)
diff --git a/include/zephyr/dt-bindings/clock/mcux_lpc_syscon_clock.h b/include/zephyr/dt-bindings/clock/mcux_lpc_syscon_clock.h
index 9f2c383..787df5c 100644
--- a/include/zephyr/dt-bindings/clock/mcux_lpc_syscon_clock.h
+++ b/include/zephyr/dt-bindings/clock/mcux_lpc_syscon_clock.h
@@ -42,4 +42,6 @@
 
 #define MCUX_SDIF_CLK			29
 
+#define MCUX_I3C_CLK			30
+
 #endif /* ZEPHYR_INCLUDE_DT_BINDINGS_CLOCK_MCUX_LPC_SYSCON_H_ */