diff --git a/boards/adi/max32655fthr/max32655fthr_max32655_m4.dts b/boards/adi/max32655fthr/max32655fthr_max32655_m4.dts index 7d09e56521725..a20a987e90a58 100644 --- a/boards/adi/max32655fthr/max32655fthr_max32655_m4.dts +++ b/boards/adi/max32655fthr/max32655fthr_max32655_m4.dts @@ -138,6 +138,12 @@ status = "okay"; }; +&i2c1 { + status = "okay"; + pinctrl-0 = <&i2c1_scl_p0_16 &i2c1_sda_p0_17>; + pinctrl-names = "default"; +}; + &i2c2 { status = "okay"; pinctrl-0 = <&i2c2_scl_p0_30 &i2c2_sda_p0_31>; @@ -148,6 +154,12 @@ status = "okay"; }; +&i2s0 { + status = "okay"; + pinctrl-0 = <&i2s_sck_p1_2 &i2s_ws_p1_3 &i2s_sdi_p1_4 &i2s_sdo_p1_5>; + pinctrl-names = "default"; +}; + &wdt0 { status = "okay"; }; diff --git a/drivers/dma/dma_max32.c b/drivers/dma/dma_max32.c index ffbe70c1f559e..11d3dd0c07d2d 100644 --- a/drivers/dma/dma_max32.c +++ b/drivers/dma/dma_max32.c @@ -258,6 +258,11 @@ static void max32_dma_isr(const struct device *dev) continue; } + /* check if only enabled bit is set (interrupt is not there) and skip it */ + if (flags == ADI_MAX32_DMA_STATUS_ST) { + continue; + } + /* Check for error interrupts */ if (flags & (ADI_MAX32_DMA_STATUS_BUS_ERR | ADI_MAX32_DMA_STATUS_TO_IF)) { status = -EIO; diff --git a/drivers/i2s/CMakeLists.txt b/drivers/i2s/CMakeLists.txt index a81c083046ba2..6c149a007a547 100644 --- a/drivers/i2s/CMakeLists.txt +++ b/drivers/i2s/CMakeLists.txt @@ -9,6 +9,7 @@ zephyr_library_sources_ifdef(CONFIG_I2S_SAM_SSC i2s_sam_ssc.c) zephyr_library_sources_ifdef(CONFIG_USERSPACE i2s_handlers.c) zephyr_library_sources_ifdef(CONFIG_I2S_STM32 i2s_ll_stm32.c) zephyr_library_sources_ifdef(CONFIG_I2S_LITEX i2s_litex.c) +zephyr_library_sources_ifdef(CONFIG_I2S_MAX32 i2s_max32.c) zephyr_library_sources_ifdef(CONFIG_I2S_MCUX_FLEXCOMM i2s_mcux_flexcomm.c) zephyr_library_sources_ifdef(CONFIG_I2S_NRFX i2s_nrfx.c) zephyr_library_sources_ifdef(CONFIG_I2S_NRF_TDM i2s_nrf_tdm.c) diff --git a/drivers/i2s/Kconfig.max32 b/drivers/i2s/Kconfig.max32 new file mode 100644 index 0000000000000..112c315ded612 --- /dev/null +++ b/drivers/i2s/Kconfig.max32 @@ -0,0 +1,21 @@ +# Copyright (c) 2025 Croxel Inc. +# SPDX-License-Identifier: Apache-2.0 + +config I2S_MAX32 + bool "Analog Devices MAX32 series I2S driver" + default y + depends on DT_HAS_ADI_MAX32_I2S_ENABLED + select DMA + help + Enable support for Analog Devices MAX32 series I2S driver. + +if I2S_MAX32 + +config I2S_MAX32_QUEUE_SIZE + int "Queue size" + default 8 + help + Size of the Tx/Rx pending block queue. This is the maximum number of + blocks that can be queued for transmission or reception. + +endif # I2S_MAX32 diff --git a/drivers/i2s/i2s_max32.c b/drivers/i2s/i2s_max32.c new file mode 100644 index 0000000000000..d698ace5810ab --- /dev/null +++ b/drivers/i2s/i2s_max32.c @@ -0,0 +1,729 @@ +/* + * Copyright (c) 2025 Croxel Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +#define DT_DRV_COMPAT adi_max32_i2s + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(max32_i2s, CONFIG_I2S_LOG_LEVEL); + +struct i2s_mem_block { + void *block; + size_t size; +}; + +struct i2s_max32_stream_data { + volatile enum i2s_state state; + volatile bool drain; + struct i2s_mem_block cur_block; + struct k_msgq *queue; + struct i2s_config i2s_cfg; +}; + +struct i2s_max32_stream { + struct i2s_max32_stream_data *data; + struct { + mxc_i2s_regs_t *reg; + const struct device *dev; + } i2s; + struct { + const struct device *dev; + uint32_t channel; + uint32_t slot; + } dma; +}; + +struct i2s_max32_cfg { + const struct i2s_max32_stream tx; + const struct i2s_max32_stream rx; + const struct pinctrl_dev_config *pcfg; +}; + +static void i2s_max32_tx_dma_callback(const struct device *dma_dev, void *arg, uint32_t channel, + int status); + +static void i2s_max32_rx_dma_callback(const struct device *dma_dev, void *arg, uint32_t channel, + int status); + +static inline void free_mem_block(const struct i2s_max32_stream *stream) +{ + struct i2s_max32_stream_data *stream_data = stream->data; + + k_mem_slab_free(stream_data->i2s_cfg.mem_slab, stream_data->cur_block.block); + stream_data->cur_block.block = NULL; +} + +static inline void trigger_stream_stop(const struct i2s_max32_stream *stream, bool drain) +{ + struct i2s_max32_stream_data *stream_data = stream->data; + + LOG_DBG("Stopping stream %p, drain: %d", (void *)stream, drain); + /* signal stopping to handle in the dma callback */ + stream_data->state = I2S_STATE_STOPPING; + /* use to control drain/drop behaviour */ + stream_data->drain = drain; +} + +static inline void clean_stream(const struct i2s_max32_stream *stream) +{ + struct i2s_max32_stream_data *stream_data = stream->data; + struct i2s_mem_block mem_block; + + /* Clear transient block */ + if (stream->data->cur_block.block != NULL) { + k_mem_slab_free(stream_data->i2s_cfg.mem_slab, stream_data->cur_block.block); + stream->data->cur_block.block = NULL; + } + + /* Clear the pending blocks from the queue */ + while (k_msgq_get(stream_data->queue, &mem_block, K_NO_WAIT) == 0) { + k_mem_slab_free(stream_data->i2s_cfg.mem_slab, mem_block.block); + } + + /* mark as ready */ + stream_data->state = I2S_STATE_READY; +} + +static int terminate_stream(const struct i2s_max32_stream *stream) +{ + int ret; + struct i2s_max32_stream_data *stream_data = stream->data; + + if (stream_data->state != I2S_STATE_RUNNING) { + LOG_ERR("Stream not running, state: %d", (int)stream_data->state); + return -EIO; + } + stream_data->state = I2S_STATE_STOPPING; + /* stop dma immediately */ + ret = dma_stop(stream->dma.dev, stream->dma.channel); + if (ret < 0) { + LOG_ERR("Failed to stop DMA channel[%d]: %d", (int)stream->dma.channel, ret); + return ret; + } + /* Clear the queue */ + clean_stream(stream); + return 0; +} + +static inline int start_stream(const struct i2s_max32_stream *stream, enum i2s_dir dir) +{ + int ret; + struct i2s_max32_stream_data *stream_data = stream->data; + struct dma_block_config dma_block; + /* + * For TX destination size and RX source size is always word, + * and thus burst length is always 4, refer to 14.6.4 of MAX32655 user manual + */ + struct dma_config dma_cfg = { + .dma_slot = stream->dma.slot, + .source_data_size = stream_data->i2s_cfg.word_size / 8, + .source_burst_length = 4, + .dest_data_size = stream_data->i2s_cfg.word_size / 8, + .dest_burst_length = 4, + .block_count = 1, + .user_data = (void *)stream, + .head_block = &dma_block, + }; + + if ((dir != I2S_DIR_RX) && (dir != I2S_DIR_TX)) { + LOG_ERR("Invalid I2S direction: %d", (int)dir); + return -EINVAL; + } + + if (stream_data->cur_block.block != NULL) { + LOG_ERR("Stream already running"); + return -EIO; + } + + if (dir == I2S_DIR_RX) { + ret = k_mem_slab_alloc(stream_data->i2s_cfg.mem_slab, &stream_data->cur_block.block, + K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to allocate RX mem block: %d", ret); + return -ENOMEM; + } + + /* set block size manually */ + stream_data->cur_block.size = stream_data->i2s_cfg.mem_slab->info.block_size; + /* Configure DMA Block */ + dma_block.block_size = stream_data->i2s_cfg.mem_slab->info.block_size, + dma_block.source_address = (uint32_t)NULL; + dma_block.source_addr_adj = DMA_ADDR_ADJ_NO_CHANGE; + dma_block.dest_address = (uint32_t)stream_data->cur_block.block; + dma_block.dest_addr_adj = DMA_ADDR_ADJ_INCREMENT; + /* Configure DMA */ + dma_cfg.channel_direction = PERIPHERAL_TO_MEMORY; + dma_cfg.dma_callback = i2s_max32_rx_dma_callback; + dma_cfg.channel_priority = 1; + } else { + ret = k_msgq_get(stream_data->queue, &stream_data->cur_block, K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to get item from TX queue: %d", ret); + return ret; + } + /* Configure DMA Block */ + dma_block.block_size = stream_data->cur_block.size, + dma_block.source_address = (uint32_t)stream_data->cur_block.block; + dma_block.source_addr_adj = DMA_ADDR_ADJ_INCREMENT; + dma_block.dest_address = (uint32_t)NULL; + dma_block.dest_addr_adj = DMA_ADDR_ADJ_NO_CHANGE; + /* Configure DMA */ + dma_cfg.channel_direction = MEMORY_TO_PERIPHERAL; + dma_cfg.dma_callback = i2s_max32_tx_dma_callback; + dma_cfg.channel_priority = 0; + } + + /* Configure DMA channel */ + ret = dma_config(stream->dma.dev, stream->dma.channel, &dma_cfg); + if (ret < 0) { + LOG_ERR("DMA config failed with error: %d", ret); + free_mem_block(stream); + return ret; + } + + /* TX/RX FIFO size is 8 word, thus using 4 words to trigger DMA transfer */ + if (dir == I2S_DIR_RX) { + Wrap_MXC_I2S_SetDMARxThreshold(stream->i2s.reg, 4); + Wrap_MXC_I2S_EnableDMARx(stream->i2s.reg); + } else { + Wrap_MXC_I2S_SetDMATxThreshold(stream->i2s.reg, 4); + Wrap_MXC_I2S_EnableDMATx(stream->i2s.reg); + } + + /* Start DMA transfer */ + ret = dma_start(stream->dma.dev, stream->dma.channel); + if (ret < 0) { + LOG_ERR("DMA start failed with error: %d", ret); + free_mem_block(stream); + return ret; + } + + stream_data->state = I2S_STATE_RUNNING; + return 0; +} + +static inline int restart_stream(const struct i2s_max32_stream *stream, enum i2s_dir dir) +{ + int ret; + struct i2s_max32_stream_data *stream_data = stream->data; + + if ((stream_data->state != I2S_STATE_RUNNING) && + (stream_data->state != I2S_STATE_STOPPING)) { + LOG_ERR("Stream not running"); + return -EIO; + } + + if ((dir != I2S_DIR_TX) && (dir != I2S_DIR_RX)) { + LOG_ERR("Invalid I2S direction: %d", (int)dir); + return -ENOTSUP; + } + + if (stream_data->cur_block.block != NULL) { + LOG_ERR("Stream already running"); + return -EIO; + } + + if (dir == I2S_DIR_RX) { + ret = k_mem_slab_alloc(stream_data->i2s_cfg.mem_slab, &stream_data->cur_block.block, + K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to allocate RX mem block: %d", ret); + return -ENOMEM; + } + } else { + ret = k_msgq_get(stream_data->queue, &stream_data->cur_block, K_NO_WAIT); + if (ret < 0) { + LOG_ERR("Failed to get item from TX queue: %d", ret); + return ret; + } + } + + /* + * source address in case of RX and destination address in case of TX is ignored, + * thus it is safe to use same address irrespective of the direction + */ + ret = dma_reload(stream->dma.dev, stream->dma.channel, + (uint32_t)stream_data->cur_block.block, + (uint32_t)stream_data->cur_block.block, stream_data->i2s_cfg.block_size); + if (ret < 0) { + LOG_ERR("Error reloading DMA channel[%d]: %d", (int)stream->dma.channel, ret); + free_mem_block(stream); + return ret; + } + + ret = dma_start(stream->dma.dev, stream->dma.channel); + if (ret < 0) { + LOG_ERR("Error starting DMA channel[%d]: %d", (int)stream->dma.channel, ret); + free_mem_block(stream); + return ret; + } + + return 0; +} + +static void i2s_max32_tx_dma_callback(const struct device *dma_dev, void *arg, uint32_t channel, + int status) +{ + int err; + const struct i2s_max32_stream *stream = (const struct i2s_max32_stream *)arg; + struct i2s_max32_stream_data *stream_data = stream->data; + + if (stream_data->cur_block.block == NULL) { + LOG_ERR("TX DMA callback called with NULL block"); + stream_data->state = I2S_STATE_ERROR; + return; + } + + /* free the block we are working with irrespective of success/fail */ + free_mem_block(stream); + + /* check the status of the DMA transfer */ + if (status < 0) { + LOG_ERR("TX DMA status bad: %d", status); + stream_data->state = I2S_STATE_ERROR; + return; + } + + /* + * if a stop was requested without draining, + * or the queue has drained completely, + * then we can stop the stream + */ + if ((stream_data->state == I2S_STATE_STOPPING) && + ((stream_data->drain == false) || (k_msgq_num_used_get(stream_data->queue) == 0))) { + stream_data->state = I2S_STATE_READY; + return; + } + + err = restart_stream(stream, I2S_DIR_TX); + if (err < 0) { + LOG_ERR("Failed to restart TX transfer: %d", err); + stream_data->state = I2S_STATE_ERROR; + } +} + +static void i2s_max32_rx_dma_callback(const struct device *dma_dev, void *arg, uint32_t channel, + int status) +{ + int err; + const struct i2s_max32_stream *stream = (const struct i2s_max32_stream *)arg; + struct i2s_max32_stream_data *stream_data = stream->data; + + if (stream_data->cur_block.block == NULL) { + LOG_ERR("RX DMA callback called with NULL block"); + stream_data->state = I2S_STATE_ERROR; + return; + } + + if (status < 0) { + LOG_ERR("RX DMA status bad: %d", status); + stream_data->state = I2S_STATE_ERROR; + return; + } + + /* we have completely received the block, push to queue to let user handle and free it*/ + err = k_msgq_put(stream_data->queue, &stream_data->cur_block, K_NO_WAIT); + if (err < 0) { + LOG_ERR("Failed to put item to RX queue: %d", err); + free_mem_block(stream); + stream_data->state = I2S_STATE_ERROR; + return; + } + + /* remove reference and let user free it when done */ + stream_data->cur_block.block = NULL; + + if (stream_data->state == I2S_STATE_STOPPING) { + stream_data->state = I2S_STATE_READY; + return; + } + + err = restart_stream(stream, I2S_DIR_RX); + if (err < 0) { + LOG_ERR("Failed to restart RX transfer: %d", err); + stream_data->state = I2S_STATE_ERROR; + } +} + +static int i2s_max32_trigger_single(const struct device *dev, enum i2s_dir dir, + enum i2s_trigger_cmd cmd, const struct i2s_max32_stream *stream) +{ + struct i2s_max32_stream_data *stream_data = stream->data; + + switch (cmd) { + case I2S_TRIGGER_START: + if (stream_data->state != I2S_STATE_READY) { + LOG_ERR("START - Invalid state: %d", (int)stream_data->state); + return -EIO; + } + return start_stream(stream, dir); + + case I2S_TRIGGER_STOP: + if (stream_data->state != I2S_STATE_RUNNING) { + LOG_ERR("STOP - Invalid state: %d", (int)stream_data->state); + return -EIO; + } + trigger_stream_stop(stream, false); + return 0; + case I2S_TRIGGER_DRAIN: + if (stream_data->state != I2S_STATE_RUNNING) { + LOG_ERR("DRAIN - Invalid state: %d", (int)stream_data->state); + return -EIO; + } + trigger_stream_stop(stream, true); + return 0; + case I2S_TRIGGER_DROP: + if (stream_data->state == I2S_STATE_NOT_READY) { + LOG_ERR("DROP - Invalid state: %d", (int)stream_data->state); + return -EIO; + } + return terminate_stream(stream); + case I2S_TRIGGER_PREPARE: + if (stream_data->state != I2S_STATE_ERROR) { + LOG_ERR("PREPARE - Invalid state: %d", (int)stream_data->state); + return -EIO; + } + clean_stream(stream); + return 0; + default: + return -ENOTSUP; + } +} + +static int i2s_max32_trigger(const struct device *dev, enum i2s_dir dir, enum i2s_trigger_cmd cmd) +{ + int ret; + const struct i2s_max32_cfg *cfg = dev->config; + + LOG_DBG("trigger with dir=%d, cmd=%d", (int)dir, (int)cmd); + + switch (dir) { + case I2S_DIR_TX: + return i2s_max32_trigger_single(dev, dir, cmd, &cfg->tx); + case I2S_DIR_RX: + return i2s_max32_trigger_single(dev, dir, cmd, &cfg->rx); + case I2S_DIR_BOTH: + /* + * If user requests both TX and RX trigger, we trigger them one by one here. + * Failing to trigger any stream will result in an error. + * This could mean one has been trigger successfully and the other has failed. + * This is not an error condition, as the user can choose to trigger only one + * stream at a time. + */ + + ret = i2s_max32_trigger_single(dev, I2S_DIR_TX, cmd, &cfg->tx); + if (ret < 0) { + return ret; + } + + ret = i2s_max32_trigger_single(dev, I2S_DIR_RX, cmd, &cfg->rx); + if (ret < 0) { + return ret; + } + + return 0; + default: + LOG_ERR("Invalid I2S direction: %d", (int)dir); + return -EINVAL; + } +} + +static int i2s_cfg_to_max32_cfg(const struct i2s_config *i2s_cfg, mxc_i2s_req_t *max_cfg) +{ + /* Input validation */ + __ASSERT(i2s_cfg != NULL, "i2s_cfg cannot be NULL"); + __ASSERT(max_cfg != NULL, "max_cfg cannot be NULL"); + + /* Clear configuration struct */ + memset(max_cfg, 0, sizeof(mxc_i2s_req_t)); + + /* Validate word size */ + switch (i2s_cfg->word_size) { + case 8: + max_cfg->wordSize = MXC_I2S_WSIZE_BYTE; + max_cfg->sampleSize = MXC_I2S_SAMPLESIZE_EIGHT; + break; + case 16: + max_cfg->wordSize = MXC_I2S_WSIZE_HALFWORD; + max_cfg->sampleSize = MXC_I2S_SAMPLESIZE_SIXTEEN; + break; + case 32: + max_cfg->wordSize = MXC_I2S_WSIZE_WORD; + max_cfg->sampleSize = MXC_I2S_SAMPLESIZE_THIRTYTWO; + break; + default: + LOG_ERR("Unsupported word size: %u", i2s_cfg->word_size); + return -EINVAL; + } + + /* Validate channels */ + if (i2s_cfg->channels == 2) { + max_cfg->stereoMode = MXC_I2S_STEREO; + } else if (i2s_cfg->channels == 1) { + max_cfg->stereoMode = MXC_I2S_MONO_RIGHT_CH; + } else { + LOG_ERR("Unsupported number of channels: %u", i2s_cfg->channels); + return -EINVAL; + } + + /* Validate format */ + switch (i2s_cfg->format & I2S_FMT_DATA_FORMAT_MASK) { + case I2S_FMT_DATA_FORMAT_I2S: + case I2S_FMT_DATA_FORMAT_LEFT_JUSTIFIED: + max_cfg->justify = MXC_I2S_LSB_JUSTIFY; + break; + case I2S_FMT_DATA_FORMAT_RIGHT_JUSTIFIED: + max_cfg->justify = MXC_I2S_MSB_JUSTIFY; + break; + default: + LOG_ERR("Unsupported data format: 0x%02x", i2s_cfg->format); + return -EINVAL; + } + + /* Check unsupported format options */ + if (i2s_cfg->format & + (I2S_FMT_DATA_ORDER_LSB | I2S_FMT_BIT_CLK_INV | I2S_FMT_FRAME_CLK_INV)) { + LOG_ERR("Unsupported format options: 0x%02x", i2s_cfg->format); + return -EINVAL; + } + + /* Set master/slave mode */ + if (i2s_cfg->options & I2S_OPT_FRAME_CLK_SLAVE) { + max_cfg->channelMode = MXC_I2S_EXTERNAL_SCK_EXTERNAL_WS; + } else { + max_cfg->channelMode = MXC_I2S_INTERNAL_SCK_WS_0; + } + + /* Check unsupported options */ + if (i2s_cfg->options & (I2S_OPT_LOOPBACK | I2S_OPT_PINGPONG)) { + LOG_ERR("Unsupported options: 0x%02x", i2s_cfg->options); + return -EINVAL; + } + + /* Set standard values */ + max_cfg->bitOrder = MXC_I2S_LSB_FIRST; + max_cfg->wsPolarity = MXC_I2S_POL_NORMAL; + max_cfg->bitsWord = i2s_cfg->word_size; + max_cfg->adjust = MXC_I2S_ADJUST_LEFT; + + /* Calculate clock divider for sample rate */ + max_cfg->clkdiv = + Wrap_MXC_I2S_CalculateClockDiv(i2s_cfg->frame_clk_freq, max_cfg->wordSize); + if (max_cfg->clkdiv < 0) { + LOG_ERR("Invalid frame clock frequency: %u", i2s_cfg->frame_clk_freq); + return -EINVAL; + } + + return 0; +} + +static int i2s_max32_configure_single(const struct device *dev, enum i2s_dir dir, + const struct i2s_config *i2s_cfg, + const struct i2s_max32_stream *stream) +{ + int ret; + struct i2s_max32_stream_data *stream_data = stream->data; + mxc_i2s_req_t mxc_cfg = {0}; + + if ((stream_data->state != I2S_STATE_NOT_READY) && + (stream_data->state != I2S_STATE_READY)) { + LOG_ERR("Invalid state: %d", (int)stream_data->state); + return -EINVAL; + } + + if (i2s_cfg->frame_clk_freq == 0) { + terminate_stream(stream); + return 0; + } + + ret = i2s_cfg_to_max32_cfg(i2s_cfg, &mxc_cfg); + if (ret < 0) { + LOG_ERR("Failed to convert I2S config to MAX32 config"); + return ret; + } + + ret = Wrap_MXC_I2S_Init(stream->i2s.reg, &mxc_cfg); + if (ret < 0) { + LOG_ERR("Failed to initialize I2S: %d", ret); + return ret; + } + + stream_data->i2s_cfg = *i2s_cfg; + stream_data->state = I2S_STATE_READY; + return 0; +} + +static int i2s_max32_configure(const struct device *dev, enum i2s_dir dir, + const struct i2s_config *i2s_cfg) +{ + int ret; + const struct i2s_max32_cfg *cfg = dev->config; + + LOG_DBG("configure with dir=%d, word_size=%u, channels=%u, frame_clk_freq=%u", + (int)dir, i2s_cfg->word_size, i2s_cfg->channels, i2s_cfg->frame_clk_freq); + + switch (dir) { + case I2S_DIR_TX: + return i2s_max32_configure_single(dev, dir, i2s_cfg, &cfg->tx); + case I2S_DIR_RX: + return i2s_max32_configure_single(dev, dir, i2s_cfg, &cfg->rx); + case I2S_DIR_BOTH: + /* + * If user requests both TX and RX configuration, we can configure both streams + * with the same configuration. This is useful for full-duplex operation. + * Failing to configure any stream will result in an error. + * This could mean one has been configured successfully and the other has failed. + * This is not an error condition, as the user can choose to configure only one + * stream at a time. + */ + ret = i2s_max32_configure_single(dev, I2S_DIR_TX, i2s_cfg, &cfg->tx); + if (ret < 0) { + return ret; + } + + ret = i2s_max32_configure_single(dev, I2S_DIR_RX, i2s_cfg, &cfg->rx); + if (ret < 0) { + return ret; + } + return 0; + default: + LOG_ERR("Invalid I2S direction: %d", (int)dir); + return -EINVAL; + } +} + +static int i2s_max32_read(const struct device *dev, void **mem_block, size_t *size) +{ + int err; + const struct i2s_max32_cfg *cfg = dev->config; + const struct i2s_max32_stream *stream = &cfg->rx; + struct i2s_max32_stream_data *stream_data = stream->data; + struct i2s_mem_block block; + + if (stream_data->state == I2S_STATE_NOT_READY) { + LOG_ERR("RX invalid state: %d", (int)stream_data->state); + return -EIO; + } else if (stream_data->state == I2S_STATE_ERROR && + k_msgq_num_used_get(stream_data->queue) == 0) { + LOG_ERR("RX queue empty"); + return -EIO; + } + + err = k_msgq_get(stream_data->queue, &block, K_MSEC(stream_data->i2s_cfg.timeout)); + if (err < 0) { + LOG_ERR("RX queue empty"); + return err; + } + + *mem_block = block.block; + *size = block.size; + + return 0; +} + +static int i2s_max32_write(const struct device *dev, void *mem_block, size_t size) +{ + int err; + const struct i2s_max32_cfg *cfg = dev->config; + const struct i2s_max32_stream *stream = &cfg->tx; + struct i2s_max32_stream_data *stream_data = stream->data; + struct i2s_mem_block block = {.block = mem_block, .size = size}; + + if (stream_data->state != I2S_STATE_READY && stream_data->state != I2S_STATE_RUNNING) { + LOG_ERR("TX Invalid state: %d", (int)stream_data->state); + return -EIO; + } + + if (size > stream_data->i2s_cfg.block_size) { + LOG_ERR("Max write size is: %u", (unsigned int)stream_data->i2s_cfg.block_size); + return -EINVAL; + } + + err = k_msgq_put(stream_data->queue, &block, K_MSEC(stream_data->i2s_cfg.timeout)); + if (err < 0) { + LOG_ERR("TX queue full"); + return err; + } + + return 0; +} + +static const struct i2s_driver_api i2s_max32_driver_api = { + .read = i2s_max32_read, + .write = i2s_max32_write, + .configure = i2s_max32_configure, + .trigger = i2s_max32_trigger, +}; + +static int i2s_max32_init(const struct device *dev) +{ + const struct i2s_max32_cfg *config = dev->config; + int err; + + err = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT); + if (err < 0) { + return err; + } + + return 0; +} + +#define MAX32_DT_INST_DMA_CTLR(n, name) DEVICE_DT_GET(DT_INST_DMAS_CTLR_BY_NAME(n, name)) + +#define MAX32_DT_INST_DMA_CELL(n, name, cell) DT_INST_DMAS_CELL_BY_NAME(n, name, cell) + +#define I2S_MAX32_STREAM_DATA_CREATE_AND_INIT(n, dir) \ + K_MSGQ_DEFINE(i2s_max32_##dir##_q_##n, sizeof(struct i2s_mem_block), \ + CONFIG_I2S_MAX32_QUEUE_SIZE, 1); \ + static struct i2s_max32_stream_data i2s_max32_##dir##_data_##n = { \ + .state = I2S_STATE_NOT_READY, \ + .drain = false, \ + .queue = &i2s_max32_##dir##_q_##n, \ + }; + +#define I2S_MAX32_STREAM_INIT(n, dir) \ + { \ + .data = &i2s_max32_##dir##_data_##n, \ + .i2s = \ + { \ + .reg = (mxc_i2s_regs_t *)DT_INST_REG_ADDR(n), \ + .dev = DEVICE_DT_INST_GET(n), \ + }, \ + .dma = \ + { \ + .dev = MAX32_DT_INST_DMA_CTLR(n, dir), \ + .channel = MAX32_DT_INST_DMA_CELL(n, dir, channel), \ + .slot = MAX32_DT_INST_DMA_CELL(n, dir, slot), \ + }, \ + } + +#define I2S_MAX32_INIT(n) \ + PINCTRL_DT_DEFINE(DT_DRV_INST(n)); \ + \ + I2S_MAX32_STREAM_DATA_CREATE_AND_INIT(n, tx); \ + I2S_MAX32_STREAM_DATA_CREATE_AND_INIT(n, rx); \ + \ + static const struct i2s_max32_cfg i2s_max32_cfg_##n = { \ + .tx = I2S_MAX32_STREAM_INIT(n, tx), \ + .rx = I2S_MAX32_STREAM_INIT(n, rx), \ + .pcfg = PINCTRL_DT_DEV_CONFIG_GET(DT_DRV_INST(n)), \ + }; \ + DEVICE_DT_INST_DEFINE(n, i2s_max32_init, NULL, NULL, &i2s_max32_cfg_##n, POST_KERNEL, \ + CONFIG_I2S_INIT_PRIORITY, &i2s_max32_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(I2S_MAX32_INIT) diff --git a/dts/arm/adi/max32/max32655.dtsi b/dts/arm/adi/max32/max32655.dtsi index 2f3e33341224c..599b153fb2e71 100644 --- a/dts/arm/adi/max32/max32655.dtsi +++ b/dts/arm/adi/max32/max32655.dtsi @@ -149,5 +149,23 @@ interrupts = <67 0>; status = "disabled"; }; + + i2s0: i2s0@40060000 { + compatible = "adi,max32-i2s"; + reg = <0x40060000 0x1000>; + #address-cells = <1>; + #size-cells = <0>; + /* + * DMA Configuration is provided as an example. + * user is free to change dma channel index, + * but tx and rx slot should be 0x3e and 0x1e respectively. + * + * ref to MAX32655 Reference Manual + * Table 6-1. I2S DMA Channel Assignments + */ + dmas = <&dma0 0 0x3e>, <&dma0 1 0x1e>; + dma-names = "tx", "rx"; + status = "disabled"; + }; }; }; diff --git a/dts/bindings/i2s/adi,max32-i2s.yaml b/dts/bindings/i2s/adi,max32-i2s.yaml new file mode 100644 index 0000000000000..1b3e31b280a5b --- /dev/null +++ b/dts/bindings/i2s/adi,max32-i2s.yaml @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Croxel Inc. +# SPDX-License-Identifier: Apache-2.0 + +description: Analog Devices MAX32 series I2S controller + +compatible: "adi,max32-i2s" + +include: [i2s-controller.yaml, pinctrl-device.yaml] + +properties: + reg: + required: true + + dmas: + required: true + description: | + DMA channels for RX and TX. Two channels must be provided in the following order: + - RX DMA channel + - TX DMA channel + + dma-names: + required: true + description: | + Names of DMA channels for RX and TX. Must be provided in the following order: + - rx + - tx diff --git a/samples/drivers/i2s/echo/Kconfig b/samples/drivers/i2s/echo/Kconfig index 623e2ae6318ad..803f31750d26b 100644 --- a/samples/drivers/i2s/echo/Kconfig +++ b/samples/drivers/i2s/echo/Kconfig @@ -1,10 +1,12 @@ # Copyright (c) 2021 Nordic Semiconductor ASA +# Copyright (c) 2025 Croxel Inc. # SPDX-License-Identifier: Apache-2.0 source "Kconfig.zephyr" config I2C - default $(dt_compat_on_bus,$(DT_COMPAT_WOLFSON_WM8731),i2c) + default $(dt_compat_on_bus,$(DT_COMPAT_WOLFSON_WM8731),i2c) \ + || $(dt_compat_on_bus,$(DT_COMPAT_ADI_MAX9867),i2c) config TOGGLE_ECHO_EFFECT_SW0 bool "Toggle echo effect when pressing sw0" diff --git a/samples/drivers/i2s/echo/boards/max32655fthr_max32655_m4.overlay b/samples/drivers/i2s/echo/boards/max32655fthr_max32655_m4.overlay new file mode 100644 index 0000000000000..e30d831ed1ca3 --- /dev/null +++ b/samples/drivers/i2s/echo/boards/max32655fthr_max32655_m4.overlay @@ -0,0 +1,10 @@ +i2s_rxtx: &i2s0 { + status = "okay"; +}; + +&i2c1 { + max9867: max9867@18 { + compatible = "adi,max9867"; + reg = <0x18>; + }; +}; diff --git a/samples/drivers/i2s/echo/dts/bindings/adi,max9867.yaml b/samples/drivers/i2s/echo/dts/bindings/adi,max9867.yaml new file mode 100644 index 0000000000000..e82e51855de3a --- /dev/null +++ b/samples/drivers/i2s/echo/dts/bindings/adi,max9867.yaml @@ -0,0 +1,12 @@ +# Copyright (c) 2025 Croxel Inc. +# SPDX-License-Identifier: Apache-2.0 + +# This binding has been added solely for the purpose of the overlay files used +# in the drivers/i2s/output sample. That's why it is located in this sample source +# directory. It can be moved and made available more widely if the need arises. + +description: MAX9867 Audio CODEC + +compatible: "adi,max9867" + +include: i2c-device.yaml diff --git a/samples/drivers/i2s/echo/src/codec.c b/samples/drivers/i2s/echo/src/codec.c index bcbf8ac7c04a1..7f0cc29a2474b 100644 --- a/samples/drivers/i2s/echo/src/codec.c +++ b/samples/drivers/i2s/echo/src/codec.c @@ -1,5 +1,6 @@ /* * Copyright (c) 2021 Nordic Semiconductor ASA + * Copyright (c) 2021 Croxel Inc * * SPDX-License-Identifier: Apache-2.0 */ @@ -123,3 +124,190 @@ bool init_wm8731_i2c(void) } #endif /* DT_ON_BUS(WM8731_NODE, i2c) */ + +#if DT_ON_BUS(MAX9867_NODE, i2c) + +#define MAX9867_I2C_NODE DT_BUS(MAX9867_NODE) +#define MAX9867_I2C_ADDR DT_REG_ADDR(MAX9867_NODE) +/* Register addresses */ +#define MAX9867_00_STATUS 0x00 +#define MAX9867_01_JACKSENSE 0x01 +#define MAX9867_02_AUX_HIGH 0x02 +#define MAX9867_03_AUX_LOW 0x03 +#define MAX9867_04_INT_EN 0x04 +#define MAX9867_05_SYS_CLK 0x05 +#define MAX9867_06_CLK_HIGH 0x06 +#define MAX9867_07_CLK_LOW 0x07 +#define MAX9867_08_DAI_FORMAT 0x08 +#define MAX9867_09_DAI_CLOCK 0x09 +#define MAX9867_0A_DIG_FILTER 0x0A +#define MAX9867_0B_SIDETONE 0x0B +#define MAX9867_0C_LVL_DAC 0x0C +#define MAX9867_0D_LVL_ADC 0x0D +#define MAX9867_0E_LVL_LINE_IN_LEFT 0x0E +#define MAX9867_0F_LVL_LINE_IN_RIGHT 0x0F +#define MAX9867_10_VOL_LEFT 0x10 +#define MAX9867_11_VOL_RIGHT 0x11 +#define MAX9867_12_MIC_GAIN_LEFT 0x12 +#define MAX9867_13_MIC_GAIN_RIGHT 0x13 +#define MAX9867_14_ADC_INPUT 0x14 +#define MAX9867_15_MIC 0x15 +#define MAX9867_16_MODE 0x16 +#define MAX9867_17_PWR_SYS 0x17 +#define MAX9867_FF_REV_ID 0xFF + +/* MAX9867_04_INT_EN */ +#define MAX9867_ICLD (1 << 7) +#define MAX9867_SDODLY (1 << 2) + +/* MAX9867_05_SYS_CLK */ +#define MAX9867_PSCLK_POS 4 + +/* MAX9867_06_CLK_HIGH */ +#define MAX9867_PLL (1 << 7) +#define MAX9867_NI_UPPER_8KHZ 0x10 +#define MAX9867_NI_UPPER_16KHZ 0x20 +#define MAX9867_NI_UPPER_24KHZ 0x30 +#define MAX9867_NI_UPPER_32KHZ 0x40 +#define MAX9867_NI_UPPER_44p1KHZ 0x58 +#define MAX9867_NI_UPPER_48KHZ 0x60 + +/* MAX9867_07_CLK_LOW */ +#define MAX9867_NI_LOWER_OTHER 0x00 +#define MAX9867_NI_LOWER_44p1KHZ 0x33 + +/* MAX9867_08_DAI_FORMAT */ +#define MAX9867_MAS (1 << 7) +#define MAX9867_WCI (1 << 6) +#define MAX9867_BCI (1 << 5) +#define MAX9867_DLY (1 << 4) +#define MAX9867_HIZOFF (1 << 3) +#define MAX9867_TDM (1 << 2) + +/* MAX9867_09_DAI_CLOCK */ +#define MAX9867_BSEL_PCLK_DIV8 0x06 + +/* MAX9867_0D_LVL_ADC */ +#define MAX9867_AVL_POS 4 +#define MAX9867_AVR_POS 0 + +/* + * MAX9867_0E_LVL_LINE_IN_LEFT + * MAX9867_0F_LVL_LINE_IN_RIGHT + */ +#define MAX9867_LI_MUTE (1 << 6) +#define MAX9867_LI_GAIN_POS 0 + +/* + * MAX9867_10_VOL_LEFT + * MAX9867_11_VOL_RIGHT + */ +#define MAX9867_VOL_POS 0 + +/* MAX9867_14_ADC_INPUT */ +#define MAX9867_MXINL_POS 6 +#define MAX9867_MXINR_POS 4 +#define MAX9867_MXIN_DIS 0 +#define MAX9867_MXIN_ANALOG_MIC 1 +#define MAX9867_MXIN_LINE 2 + +/* MAX9867_15_MIC */ +#define MAX9867_MICCLK_POS 6 +#define MAX9867_DIGMICL_POS 5 +#define MAX9867_DIGMICR_POS 4 + +/* MAX9867_16_MODE */ +#define MAX9867_HPMODE_POS 0 +#define MAX9867_STEREO_SE_CLICKLESS 4 +#define MAX9867_MONO_SE_CLICKLESS 5 + +/* MAX9867_17_PWR_SYS */ +#define MAX9867_SHDN (1 << 7) +#define MAX9867_LNLEN (1 << 6) +#define MAX9867_LNREN (1 << 5) +#define MAX9867_DALEN (1 << 3) +#define MAX9867_DAREN (1 << 2) +#define MAX9867_ADLEN (1 << 1) +#define MAX9867_ADREN (1 << 0) + +bool init_max9867_i2c(void) +{ + const struct device *const i2c_dev = DEVICE_DT_GET(MAX9867_I2C_NODE); + + /* Initialization data for MAX9867 registers. */ + static const uint8_t init[][2] = { + /* Shutdown MAX9867 during configuration */ + {MAX9867_17_PWR_SYS, 0x00}, + /* + * Clear all regs to POR state. The MAX9867 does not not have an external + * reset signal. So manually writing 0, from (0x04 - 0x17) + */ + {MAX9867_04_INT_EN, 0x00}, + {MAX9867_05_SYS_CLK, 0x00}, + {MAX9867_06_CLK_HIGH, 0x00}, + {MAX9867_07_CLK_LOW, 0x00}, + {MAX9867_08_DAI_FORMAT, 0x00}, + {MAX9867_09_DAI_CLOCK, 0x00}, + {MAX9867_0A_DIG_FILTER, 0x00}, + {MAX9867_0B_SIDETONE, 0x00}, + {MAX9867_0C_LVL_DAC, 0x00}, + {MAX9867_0D_LVL_ADC, 0x00}, + {MAX9867_0E_LVL_LINE_IN_LEFT, 0x00}, + {MAX9867_0F_LVL_LINE_IN_RIGHT, 0x00}, + {MAX9867_10_VOL_LEFT, 0x00}, + {MAX9867_11_VOL_RIGHT, 0x00}, + {MAX9867_12_MIC_GAIN_LEFT, 0x00}, + {MAX9867_13_MIC_GAIN_RIGHT, 0x00}, + {MAX9867_14_ADC_INPUT, 0x00}, + {MAX9867_15_MIC, 0x00}, + {MAX9867_16_MODE, 0x00}, + {MAX9867_17_PWR_SYS, 0x00}, + /* + * Select MCLK prescaler. PSCLK divides MCLK to generate a PCLK between 10MHz and + * 20MHz. Set prescaler, FREQ field is 0 for Normal or PLL mode, < 20MHz. + */ + {MAX9867_05_SYS_CLK, 0x01 << MAX9867_PSCLK_POS}, + /* Configure codec to generate 48kHz sampling frequency in master mode */ + {MAX9867_06_CLK_HIGH, MAX9867_NI_UPPER_44p1KHZ}, + {MAX9867_07_CLK_LOW, MAX9867_NI_LOWER_44p1KHZ}, + {MAX9867_09_DAI_CLOCK, MAX9867_BSEL_PCLK_DIV8}, + /* I2S format */ + {MAX9867_08_DAI_FORMAT, MAX9867_MAS | MAX9867_DLY | MAX9867_HIZOFF}, + /* */ + {MAX9867_0A_DIG_FILTER, 0xA2}, + /* Select Digital microphone input */ + {MAX9867_15_MIC, ((0x1 << MAX9867_DIGMICR_POS))}, + /* ADC level */ + {MAX9867_0D_LVL_ADC, (3 << MAX9867_AVL_POS) | (3 << MAX9867_AVR_POS)}, + /*Set line-in level, disconnect line input from playback amplifiers */ + {MAX9867_0E_LVL_LINE_IN_LEFT, (0x0C << MAX9867_LI_GAIN_POS) | MAX9867_LI_MUTE}, + {MAX9867_0F_LVL_LINE_IN_RIGHT, (0x0C << MAX9867_LI_GAIN_POS) | MAX9867_LI_MUTE}, + /* Headphone mode */ + {MAX9867_16_MODE, MAX9867_STEREO_SE_CLICKLESS << MAX9867_HPMODE_POS}, + /* Set playback volume */ + {MAX9867_10_VOL_LEFT, 0x04 << MAX9867_VOL_POS}, + {MAX9867_11_VOL_RIGHT, 0x04 << MAX9867_VOL_POS}, + /* Enable */ + {MAX9867_17_PWR_SYS, MAX9867_SHDN | MAX9867_DALEN | MAX9867_DAREN | MAX9867_ADLEN}, + }; + + if (!device_is_ready(i2c_dev)) { + printk("%s is not ready\n", i2c_dev->name); + return false; + } + + for (int i = 0; i < ARRAY_SIZE(init); ++i) { + const uint8_t *entry = init[i]; + int ret; + + ret = i2c_reg_write_byte(i2c_dev, MAX9867_I2C_ADDR, entry[0], entry[1]); + if (ret < 0) { + printk("Initialization step %d failed with %d\n", i, ret); + return false; + } + } + + return true; +} + +#endif /* DT_ON_BUS(MAX9867_NODE, i2c) */ diff --git a/samples/drivers/i2s/echo/src/codec.h b/samples/drivers/i2s/echo/src/codec.h index 38d629c5d2f7d..3daf6ae161158 100644 --- a/samples/drivers/i2s/echo/src/codec.h +++ b/samples/drivers/i2s/echo/src/codec.h @@ -1,11 +1,17 @@ /* * Copyright (c) 2021 Nordic Semiconductor ASA + * Copyright (c) 2025 Croxel Inc * * SPDX-License-Identifier: Apache-2.0 */ #define WM8731_NODE DT_NODELABEL(wm8731) +#define MAX9867_NODE DT_NODELABEL(max9867) #if DT_ON_BUS(WM8731_NODE, i2c) bool init_wm8731_i2c(void); #endif + +#if DT_ON_BUS(MAX9867_NODE, i2c) +bool init_max9867_i2c(void); +#endif diff --git a/samples/drivers/i2s/echo/src/main.c b/samples/drivers/i2s/echo/src/main.c index d9619bf94c7a4..5bd063042f0d6 100644 --- a/samples/drivers/i2s/echo/src/main.c +++ b/samples/drivers/i2s/echo/src/main.c @@ -24,8 +24,8 @@ #define SAMPLE_BIT_WIDTH 16 #define BYTES_PER_SAMPLE sizeof(int16_t) #define NUMBER_OF_CHANNELS 2 -/* Such block length provides an echo with the delay of 100 ms. */ -#define SAMPLES_PER_BLOCK ((SAMPLE_FREQUENCY / 10) * NUMBER_OF_CHANNELS) +/* Such block length provides an echo with the delay of 40 ms. */ +#define SAMPLES_PER_BLOCK ((SAMPLE_FREQUENCY / 25) * NUMBER_OF_CHANNELS) #define INITIAL_BLOCKS 2 #define TIMEOUT 1000 @@ -258,6 +258,12 @@ int main(void) } #endif +#if DT_ON_BUS(MAX9867_NODE, i2c) + if (!init_max9867_i2c()) { + return 0; + } +#endif + if (!init_buttons()) { return 0; } @@ -275,7 +281,15 @@ int main(void) config.word_size = SAMPLE_BIT_WIDTH; config.channels = NUMBER_OF_CHANNELS; config.format = I2S_FMT_DATA_FORMAT_I2S; + /* + * On MAX32655FTHR, MAX9867 MCLK is connected to external 12.2880 crystal + * thus using slave mode + */ +#if CONFIG_BOARD_MAX32655FTHR_MAX32655_M4 + config.options = I2S_OPT_BIT_CLK_SLAVE | I2S_OPT_FRAME_CLK_SLAVE; +#else config.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER; +#endif config.frame_clk_freq = SAMPLE_FREQUENCY; config.mem_slab = &mem_slab; config.block_size = BLOCK_SIZE; diff --git a/west.yml b/west.yml index fc43ee8198522..b8dab031b40d8 100644 --- a/west.yml +++ b/west.yml @@ -23,6 +23,8 @@ manifest: url-base: https://github.com/zephyrproject-rtos - name: babblesim url-base: https://github.com/BabbleSim + - name: croxel + url-base: https://github.com/croxel group-filter: [-babblesim, -optional] @@ -144,7 +146,8 @@ manifest: groups: - fs - name: hal_adi - revision: f8f65473168a4e9f71f20c0c5387f6b80fe54cf3 + remote: croxel + revision: feature-adi-wrapper-for-i2s-tx-rx path: modules/hal/adi groups: - hal