| /* |
| * Copyright (c) 2017 comsuisse AG |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #define DT_DRV_COMPAT atmel_sam_ssc |
| |
| /** @file |
| * @brief I2S bus (SSC) driver for Atmel SAM MCU family. |
| * |
| * Limitations: |
| * - TX and RX path share a common bit clock divider and as a result they cannot |
| * be configured independently. If RX and TX path are set to different bit |
| * clock frequencies the latter setting will quietly override the former. |
| * We should return an error in such a case. |
| * - DMA is used in simple single block transfer mode and as such is not able |
| * to handle high speed data. To support higher transfer speeds the DMA |
| * linked list mode should be used. |
| */ |
| |
| #include <errno.h> |
| #include <string.h> |
| #include <zephyr/sys/__assert.h> |
| #include <zephyr/kernel.h> |
| #include <zephyr/device.h> |
| #include <zephyr/init.h> |
| #include <zephyr/drivers/dma.h> |
| #include <zephyr/drivers/i2s.h> |
| #include <zephyr/drivers/pinctrl.h> |
| #include <zephyr/drivers/clock_control/atmel_sam_pmc.h> |
| #include <soc.h> |
| |
| #define LOG_DOMAIN dev_i2s_sam_ssc |
| #define LOG_LEVEL CONFIG_I2S_LOG_LEVEL |
| #include <zephyr/logging/log.h> |
| #include <zephyr/irq.h> |
| LOG_MODULE_REGISTER(LOG_DOMAIN); |
| |
| #if __DCACHE_PRESENT == 1 |
| #define DCACHE_INVALIDATE(addr, size) \ |
| SCB_InvalidateDCache_by_Addr((uint32_t *)addr, size) |
| #define DCACHE_CLEAN(addr, size) \ |
| SCB_CleanDCache_by_Addr((uint32_t *)addr, size) |
| #else |
| #define DCACHE_INVALIDATE(addr, size) {; } |
| #define DCACHE_CLEAN(addr, size) {; } |
| #endif |
| |
| #define SAM_SSC_WORD_SIZE_BITS_MIN 2 |
| #define SAM_SSC_WORD_SIZE_BITS_MAX 32 |
| #define SAM_SSC_WORD_PER_FRAME_MIN 1 |
| #define SAM_SSC_WORD_PER_FRAME_MAX 16 |
| |
| struct queue_item { |
| void *mem_block; |
| size_t size; |
| }; |
| |
| /* Minimal ring buffer implementation */ |
| struct ring_buf { |
| struct queue_item *buf; |
| uint16_t len; |
| uint16_t head; |
| uint16_t tail; |
| }; |
| |
| /* Device constant configuration parameters */ |
| struct i2s_sam_dev_cfg { |
| const struct device *dev_dma; |
| Ssc *regs; |
| void (*irq_config)(void); |
| const struct atmel_sam_pmc_config clock_cfg; |
| const struct pinctrl_dev_config *pcfg; |
| uint8_t irq_id; |
| }; |
| |
| struct stream { |
| int32_t state; |
| struct k_sem sem; |
| uint32_t dma_channel; |
| uint8_t dma_perid; |
| uint8_t word_size_bytes; |
| bool last_block; |
| struct i2s_config cfg; |
| struct ring_buf mem_block_queue; |
| void *mem_block; |
| int (*stream_start)(struct stream *, Ssc *const, |
| const struct device *); |
| void (*stream_disable)(struct stream *, Ssc *const, |
| const struct device *); |
| void (*queue_drop)(struct stream *); |
| int (*set_data_format)(const struct i2s_sam_dev_cfg *const, |
| const struct i2s_config *); |
| }; |
| |
| /* Device run time data */ |
| struct i2s_sam_dev_data { |
| struct stream rx; |
| struct stream tx; |
| }; |
| |
| #define MODULO_INC(val, max) { val = (++val < max) ? val : 0; } |
| |
| static const struct device *get_dev_from_dma_channel(uint32_t dma_channel); |
| static void dma_rx_callback(const struct device *, void *, uint32_t, int); |
| static void dma_tx_callback(const struct device *, void *, uint32_t, int); |
| static void rx_stream_disable(struct stream *, Ssc *const, |
| const struct device *); |
| static void tx_stream_disable(struct stream *, Ssc *const, |
| const struct device *); |
| |
| /* |
| * Get data from the queue |
| */ |
| static int queue_get(struct ring_buf *rb, void **mem_block, size_t *size) |
| { |
| unsigned int key; |
| |
| key = irq_lock(); |
| |
| if (rb->tail == rb->head) { |
| /* Ring buffer is empty */ |
| irq_unlock(key); |
| return -ENOMEM; |
| } |
| |
| *mem_block = rb->buf[rb->tail].mem_block; |
| *size = rb->buf[rb->tail].size; |
| MODULO_INC(rb->tail, rb->len); |
| |
| irq_unlock(key); |
| |
| return 0; |
| } |
| |
| /* |
| * Put data in the queue |
| */ |
| static int queue_put(struct ring_buf *rb, void *mem_block, size_t size) |
| { |
| uint16_t head_next; |
| unsigned int key; |
| |
| key = irq_lock(); |
| |
| head_next = rb->head; |
| MODULO_INC(head_next, rb->len); |
| |
| if (head_next == rb->tail) { |
| /* Ring buffer is full */ |
| irq_unlock(key); |
| return -ENOMEM; |
| } |
| |
| rb->buf[rb->head].mem_block = mem_block; |
| rb->buf[rb->head].size = size; |
| rb->head = head_next; |
| |
| irq_unlock(key); |
| |
| return 0; |
| } |
| |
| static int reload_dma(const struct device *dev_dma, uint32_t channel, |
| void *src, void *dst, size_t size) |
| { |
| int ret; |
| |
| ret = dma_reload(dev_dma, channel, (uint32_t)src, (uint32_t)dst, size); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| ret = dma_start(dev_dma, channel); |
| |
| return ret; |
| } |
| |
| static int start_dma(const struct device *dev_dma, uint32_t channel, |
| struct dma_config *cfg, void *src, void *dst, |
| uint32_t blk_size) |
| { |
| struct dma_block_config blk_cfg; |
| int ret; |
| |
| (void)memset(&blk_cfg, 0, sizeof(blk_cfg)); |
| blk_cfg.block_size = blk_size; |
| blk_cfg.source_address = (uint32_t)src; |
| blk_cfg.dest_address = (uint32_t)dst; |
| |
| cfg->head_block = &blk_cfg; |
| |
| ret = dma_config(dev_dma, channel, cfg); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| ret = dma_start(dev_dma, channel); |
| |
| return ret; |
| } |
| |
| /* This function is executed in the interrupt context */ |
| static void dma_rx_callback(const struct device *dma_dev, void *user_data, |
| uint32_t channel, int status) |
| { |
| const struct device *dev = get_dev_from_dma_channel(channel); |
| const struct i2s_sam_dev_cfg *const dev_cfg = dev->config; |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| Ssc *const ssc = dev_cfg->regs; |
| struct stream *stream = &dev_data->rx; |
| int ret; |
| |
| ARG_UNUSED(user_data); |
| __ASSERT_NO_MSG(stream->mem_block != NULL); |
| |
| /* Stop reception if there was an error */ |
| if (stream->state == I2S_STATE_ERROR) { |
| goto rx_disable; |
| } |
| |
| /* All block data received */ |
| ret = queue_put(&stream->mem_block_queue, stream->mem_block, |
| stream->cfg.block_size); |
| if (ret < 0) { |
| stream->state = I2S_STATE_ERROR; |
| goto rx_disable; |
| } |
| stream->mem_block = NULL; |
| k_sem_give(&stream->sem); |
| |
| /* Stop reception if we were requested */ |
| if (stream->state == I2S_STATE_STOPPING) { |
| stream->state = I2S_STATE_READY; |
| goto rx_disable; |
| } |
| |
| /* Prepare to receive the next data block */ |
| ret = k_mem_slab_alloc(stream->cfg.mem_slab, &stream->mem_block, |
| K_NO_WAIT); |
| if (ret < 0) { |
| stream->state = I2S_STATE_ERROR; |
| goto rx_disable; |
| } |
| |
| /* Assure cache coherency before DMA write operation */ |
| DCACHE_INVALIDATE(stream->mem_block, stream->cfg.block_size); |
| |
| ret = reload_dma(dev_cfg->dev_dma, stream->dma_channel, |
| (void *)&(ssc->SSC_RHR), stream->mem_block, |
| stream->cfg.block_size); |
| if (ret < 0) { |
| LOG_DBG("Failed to reload RX DMA transfer: %d", ret); |
| goto rx_disable; |
| } |
| |
| return; |
| |
| rx_disable: |
| rx_stream_disable(stream, ssc, dev_cfg->dev_dma); |
| } |
| |
| /* This function is executed in the interrupt context */ |
| static void dma_tx_callback(const struct device *dma_dev, void *user_data, |
| uint32_t channel, int status) |
| { |
| const struct device *dev = get_dev_from_dma_channel(channel); |
| const struct i2s_sam_dev_cfg *const dev_cfg = dev->config; |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| Ssc *const ssc = dev_cfg->regs; |
| struct stream *stream = &dev_data->tx; |
| size_t mem_block_size; |
| int ret; |
| |
| ARG_UNUSED(user_data); |
| __ASSERT_NO_MSG(stream->mem_block != NULL); |
| |
| /* All block data sent */ |
| k_mem_slab_free(stream->cfg.mem_slab, stream->mem_block); |
| stream->mem_block = NULL; |
| |
| /* Stop transmission if there was an error */ |
| if (stream->state == I2S_STATE_ERROR) { |
| LOG_DBG("TX error detected"); |
| goto tx_disable; |
| } |
| |
| /* Stop transmission if we were requested */ |
| if (stream->last_block) { |
| stream->state = I2S_STATE_READY; |
| goto tx_disable; |
| } |
| |
| /* Prepare to send the next data block */ |
| ret = queue_get(&stream->mem_block_queue, &stream->mem_block, |
| &mem_block_size); |
| if (ret < 0) { |
| if (stream->state == I2S_STATE_STOPPING) { |
| stream->state = I2S_STATE_READY; |
| } else { |
| stream->state = I2S_STATE_ERROR; |
| } |
| goto tx_disable; |
| } |
| k_sem_give(&stream->sem); |
| |
| /* Assure cache coherency before DMA read operation */ |
| DCACHE_CLEAN(stream->mem_block, mem_block_size); |
| |
| ret = reload_dma(dev_cfg->dev_dma, stream->dma_channel, |
| stream->mem_block, (void *)&(ssc->SSC_THR), |
| mem_block_size); |
| if (ret < 0) { |
| LOG_DBG("Failed to reload TX DMA transfer: %d", ret); |
| goto tx_disable; |
| } |
| |
| return; |
| |
| tx_disable: |
| tx_stream_disable(stream, ssc, dev_cfg->dev_dma); |
| } |
| |
| static int set_rx_data_format(const struct i2s_sam_dev_cfg *const dev_cfg, |
| const struct i2s_config *i2s_cfg) |
| { |
| Ssc *const ssc = dev_cfg->regs; |
| const bool pin_rk_en = IS_ENABLED(CONFIG_I2S_SAM_SSC_0_PIN_RK_EN); |
| const bool pin_rf_en = IS_ENABLED(CONFIG_I2S_SAM_SSC_0_PIN_RF_EN); |
| uint8_t word_size_bits = i2s_cfg->word_size; |
| uint8_t num_words = i2s_cfg->channels; |
| uint8_t fslen = 0U; |
| uint32_t ssc_rcmr = 0U; |
| uint32_t ssc_rfmr = 0U; |
| bool frame_clk_master = !(i2s_cfg->options & I2S_OPT_FRAME_CLK_SLAVE); |
| |
| switch (i2s_cfg->format & I2S_FMT_DATA_FORMAT_MASK) { |
| |
| case I2S_FMT_DATA_FORMAT_I2S: |
| num_words = 2U; |
| fslen = word_size_bits - 1; |
| |
| ssc_rcmr = SSC_RCMR_CKI |
| | (pin_rf_en ? SSC_RCMR_START_RF_FALLING : 0) |
| | SSC_RCMR_STTDLY(1); |
| |
| ssc_rfmr = (pin_rf_en && frame_clk_master |
| ? SSC_RFMR_FSOS_NEGATIVE : SSC_RFMR_FSOS_NONE); |
| break; |
| |
| case I2S_FMT_DATA_FORMAT_PCM_SHORT: |
| ssc_rcmr = (pin_rf_en ? SSC_RCMR_START_RF_FALLING : 0) |
| | SSC_RCMR_STTDLY(0); |
| |
| ssc_rfmr = (pin_rf_en && frame_clk_master |
| ? SSC_RFMR_FSOS_POSITIVE : SSC_RFMR_FSOS_NONE); |
| break; |
| |
| case I2S_FMT_DATA_FORMAT_PCM_LONG: |
| fslen = num_words * word_size_bits / 2U - 1; |
| |
| ssc_rcmr = (pin_rf_en ? SSC_RCMR_START_RF_RISING : 0) |
| | SSC_RCMR_STTDLY(0); |
| |
| ssc_rfmr = (pin_rf_en && frame_clk_master |
| ? SSC_RFMR_FSOS_POSITIVE : SSC_RFMR_FSOS_NONE); |
| break; |
| |
| case I2S_FMT_DATA_FORMAT_LEFT_JUSTIFIED: |
| fslen = num_words * word_size_bits / 2U - 1; |
| |
| ssc_rcmr = SSC_RCMR_CKI |
| | (pin_rf_en ? SSC_RCMR_START_RF_RISING : 0) |
| | SSC_RCMR_STTDLY(0); |
| |
| ssc_rfmr = (pin_rf_en && frame_clk_master |
| ? SSC_RFMR_FSOS_POSITIVE : SSC_RFMR_FSOS_NONE); |
| break; |
| |
| default: |
| LOG_ERR("Unsupported I2S data format"); |
| return -EINVAL; |
| } |
| |
| if (pin_rk_en) { |
| ssc_rcmr |= ((i2s_cfg->options & I2S_OPT_BIT_CLK_SLAVE) |
| ? SSC_RCMR_CKS_RK : SSC_RCMR_CKS_MCK) |
| | ((i2s_cfg->options & I2S_OPT_BIT_CLK_GATED) |
| ? SSC_RCMR_CKO_TRANSFER : SSC_RCMR_CKO_CONTINUOUS); |
| } else { |
| ssc_rcmr |= SSC_RCMR_CKS_TK |
| | SSC_RCMR_CKO_NONE; |
| } |
| /* SSC_RCMR.PERIOD bit filed does not support setting the |
| * frame period with one bit resolution. In case the required |
| * frame period is an odd number set it to be one bit longer. |
| */ |
| ssc_rcmr |= (pin_rf_en ? 0 : SSC_RCMR_START_TRANSMIT) |
| | SSC_RCMR_PERIOD((num_words * word_size_bits + 1) / 2U - 1); |
| |
| /* Receive Clock Mode Register */ |
| ssc->SSC_RCMR = ssc_rcmr; |
| |
| ssc_rfmr |= SSC_RFMR_DATLEN(word_size_bits - 1) |
| | ((i2s_cfg->format & I2S_FMT_DATA_ORDER_LSB) |
| ? 0 : SSC_RFMR_MSBF) |
| | SSC_RFMR_DATNB(num_words - 1) |
| | SSC_RFMR_FSLEN(fslen) |
| | SSC_RFMR_FSLEN_EXT(fslen >> 4); |
| |
| /* Receive Frame Mode Register */ |
| ssc->SSC_RFMR = ssc_rfmr; |
| |
| return 0; |
| } |
| |
| static int set_tx_data_format(const struct i2s_sam_dev_cfg *const dev_cfg, |
| const struct i2s_config *i2s_cfg) |
| { |
| Ssc *const ssc = dev_cfg->regs; |
| uint8_t word_size_bits = i2s_cfg->word_size; |
| uint8_t num_words = i2s_cfg->channels; |
| uint8_t fslen = 0U; |
| uint32_t ssc_tcmr = 0U; |
| uint32_t ssc_tfmr = 0U; |
| |
| switch (i2s_cfg->format & I2S_FMT_DATA_FORMAT_MASK) { |
| |
| case I2S_FMT_DATA_FORMAT_I2S: |
| num_words = 2U; |
| fslen = word_size_bits - 1; |
| |
| ssc_tcmr = SSC_TCMR_START_TF_FALLING |
| | SSC_TCMR_STTDLY(1); |
| |
| ssc_tfmr = SSC_TFMR_FSOS_NEGATIVE; |
| break; |
| |
| case I2S_FMT_DATA_FORMAT_PCM_SHORT: |
| ssc_tcmr = SSC_TCMR_CKI |
| | SSC_TCMR_START_TF_FALLING |
| | SSC_TCMR_STTDLY(0); |
| |
| ssc_tfmr = SSC_TFMR_FSOS_POSITIVE; |
| break; |
| |
| case I2S_FMT_DATA_FORMAT_PCM_LONG: |
| fslen = num_words * word_size_bits / 2U - 1; |
| |
| ssc_tcmr = SSC_TCMR_CKI |
| | SSC_TCMR_START_TF_RISING |
| | SSC_TCMR_STTDLY(0); |
| |
| ssc_tfmr = SSC_TFMR_FSOS_POSITIVE; |
| break; |
| |
| case I2S_FMT_DATA_FORMAT_LEFT_JUSTIFIED: |
| fslen = num_words * word_size_bits / 2U - 1; |
| |
| ssc_tcmr = SSC_TCMR_START_TF_RISING |
| | SSC_TCMR_STTDLY(0); |
| |
| ssc_tfmr = SSC_TFMR_FSOS_POSITIVE; |
| break; |
| |
| default: |
| LOG_ERR("Unsupported I2S data format"); |
| return -EINVAL; |
| } |
| |
| /* SSC_TCMR.PERIOD bit filed does not support setting the |
| * frame period with one bit resolution. In case the required |
| * frame period is an odd number set it to be one bit longer. |
| */ |
| ssc_tcmr |= ((i2s_cfg->options & I2S_OPT_BIT_CLK_SLAVE) |
| ? SSC_TCMR_CKS_TK : SSC_TCMR_CKS_MCK) |
| | ((i2s_cfg->options & I2S_OPT_BIT_CLK_GATED) |
| ? SSC_TCMR_CKO_TRANSFER : SSC_TCMR_CKO_CONTINUOUS) |
| | SSC_TCMR_PERIOD((num_words * word_size_bits + 1) / 2U - 1); |
| |
| /* Transmit Clock Mode Register */ |
| ssc->SSC_TCMR = ssc_tcmr; |
| |
| if (i2s_cfg->options & I2S_OPT_FRAME_CLK_SLAVE) { |
| ssc_tfmr &= ~SSC_TFMR_FSOS_Msk; |
| ssc_tfmr |= SSC_TFMR_FSOS_NONE; |
| } |
| |
| ssc_tfmr |= SSC_TFMR_DATLEN(word_size_bits - 1) |
| | ((i2s_cfg->format & I2S_FMT_DATA_ORDER_LSB) |
| ? 0 : SSC_TFMR_MSBF) |
| | SSC_TFMR_DATNB(num_words - 1) |
| | SSC_TFMR_FSLEN(fslen) |
| | SSC_TFMR_FSLEN_EXT(fslen >> 4); |
| |
| /* Transmit Frame Mode Register */ |
| ssc->SSC_TFMR = ssc_tfmr; |
| |
| return 0; |
| } |
| |
| /* Calculate number of bytes required to store a word of bit_size length */ |
| static uint8_t get_word_size_bytes(uint8_t bit_size) |
| { |
| uint8_t byte_size_min = (bit_size + 7) / 8U; |
| uint8_t byte_size; |
| |
| byte_size = (byte_size_min == 3U) ? 4 : byte_size_min; |
| |
| return byte_size; |
| } |
| |
| static int bit_clock_set(Ssc *const ssc, uint32_t bit_clk_freq) |
| { |
| uint32_t clk_div = SOC_ATMEL_SAM_MCK_FREQ_HZ / bit_clk_freq / 2U; |
| |
| if (clk_div == 0U || clk_div >= (1 << 12)) { |
| LOG_ERR("Invalid bit clock frequency"); |
| return -EINVAL; |
| } |
| |
| ssc->SSC_CMR = clk_div; |
| |
| LOG_DBG("freq = %d", bit_clk_freq); |
| |
| return 0; |
| } |
| |
| static const struct i2s_config *i2s_sam_config_get(const struct device *dev, |
| enum i2s_dir dir) |
| { |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| struct stream *stream; |
| |
| if (dir == I2S_DIR_RX) { |
| stream = &dev_data->rx; |
| } else { |
| stream = &dev_data->tx; |
| } |
| |
| if (stream->state == I2S_STATE_NOT_READY) { |
| return NULL; |
| } |
| |
| return &stream->cfg; |
| } |
| |
| static int i2s_sam_configure(const struct device *dev, enum i2s_dir dir, |
| const struct i2s_config *i2s_cfg) |
| { |
| const struct i2s_sam_dev_cfg *const dev_cfg = dev->config; |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| Ssc *const ssc = dev_cfg->regs; |
| uint8_t num_words = i2s_cfg->channels; |
| uint8_t word_size_bits = i2s_cfg->word_size; |
| uint32_t bit_clk_freq; |
| struct stream *stream; |
| int ret; |
| |
| if (dir == I2S_DIR_RX) { |
| stream = &dev_data->rx; |
| } else if (dir == I2S_DIR_TX) { |
| stream = &dev_data->tx; |
| } else if (dir == I2S_DIR_BOTH) { |
| return -ENOSYS; |
| } else { |
| LOG_ERR("Either RX or TX direction must be selected"); |
| return -EINVAL; |
| } |
| |
| if (stream->state != I2S_STATE_NOT_READY && |
| stream->state != I2S_STATE_READY) { |
| LOG_ERR("invalid state"); |
| return -EINVAL; |
| } |
| |
| if (i2s_cfg->frame_clk_freq == 0U) { |
| stream->queue_drop(stream); |
| (void)memset(&stream->cfg, 0, sizeof(struct i2s_config)); |
| stream->state = I2S_STATE_NOT_READY; |
| return 0; |
| } |
| |
| if (i2s_cfg->format & I2S_FMT_FRAME_CLK_INV) { |
| LOG_ERR("Frame clock inversion is not implemented"); |
| LOG_ERR("Please submit a patch"); |
| return -EINVAL; |
| } |
| |
| if (i2s_cfg->format & I2S_FMT_BIT_CLK_INV) { |
| LOG_ERR("Bit clock inversion is not implemented"); |
| LOG_ERR("Please submit a patch"); |
| return -EINVAL; |
| } |
| |
| if (word_size_bits < SAM_SSC_WORD_SIZE_BITS_MIN || |
| word_size_bits > SAM_SSC_WORD_SIZE_BITS_MAX) { |
| LOG_ERR("Unsupported I2S word size"); |
| return -EINVAL; |
| } |
| |
| if (num_words < SAM_SSC_WORD_PER_FRAME_MIN || |
| num_words > SAM_SSC_WORD_PER_FRAME_MAX) { |
| LOG_ERR("Unsupported words per frame number"); |
| return -EINVAL; |
| } |
| |
| memcpy(&stream->cfg, i2s_cfg, sizeof(struct i2s_config)); |
| |
| bit_clk_freq = i2s_cfg->frame_clk_freq * word_size_bits * num_words; |
| ret = bit_clock_set(ssc, bit_clk_freq); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| ret = stream->set_data_format(dev_cfg, i2s_cfg); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* Set up DMA channel parameters */ |
| stream->word_size_bytes = get_word_size_bytes(word_size_bits); |
| |
| if (i2s_cfg->options & I2S_OPT_LOOPBACK) { |
| ssc->SSC_RFMR |= SSC_RFMR_LOOP; |
| } |
| |
| stream->state = I2S_STATE_READY; |
| |
| return 0; |
| } |
| |
| static int rx_stream_start(struct stream *stream, Ssc *const ssc, |
| const struct device *dev_dma) |
| { |
| int ret; |
| |
| ret = k_mem_slab_alloc(stream->cfg.mem_slab, &stream->mem_block, |
| K_NO_WAIT); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* Workaround for a hardware bug: DMA engine will read first data |
| * item even if SSC_SR.RXEN (Receive Enable) is not set. An extra read |
| * before enabling DMA engine sets hardware FSM in the correct state. |
| */ |
| (void)ssc->SSC_RHR; |
| |
| struct dma_config dma_cfg = { |
| .source_data_size = stream->word_size_bytes, |
| .dest_data_size = stream->word_size_bytes, |
| .block_count = 1, |
| .dma_slot = stream->dma_perid, |
| .channel_direction = PERIPHERAL_TO_MEMORY, |
| .source_burst_length = 1, |
| .dest_burst_length = 1, |
| .dma_callback = dma_rx_callback, |
| }; |
| |
| ret = start_dma(dev_dma, stream->dma_channel, &dma_cfg, |
| (void *)&(ssc->SSC_RHR), stream->mem_block, |
| stream->cfg.block_size); |
| if (ret < 0) { |
| LOG_ERR("Failed to start RX DMA transfer: %d", ret); |
| return ret; |
| } |
| |
| /* Clear status register */ |
| (void)ssc->SSC_SR; |
| |
| ssc->SSC_IER = SSC_IER_OVRUN; |
| |
| ssc->SSC_CR = SSC_CR_RXEN; |
| |
| return 0; |
| } |
| |
| static int tx_stream_start(struct stream *stream, Ssc *const ssc, |
| const struct device *dev_dma) |
| { |
| size_t mem_block_size; |
| int ret; |
| |
| ret = queue_get(&stream->mem_block_queue, &stream->mem_block, |
| &mem_block_size); |
| if (ret < 0) { |
| return ret; |
| } |
| k_sem_give(&stream->sem); |
| |
| /* Workaround for a hardware bug: DMA engine will transfer first data |
| * item even if SSC_SR.TXEN (Transmit Enable) is not set. An extra write |
| * before enabling DMA engine sets hardware FSM in the correct state. |
| * This data item will not be output on I2S interface. |
| */ |
| ssc->SSC_THR = 0; |
| |
| struct dma_config dma_cfg = { |
| .source_data_size = stream->word_size_bytes, |
| .dest_data_size = stream->word_size_bytes, |
| .block_count = 1, |
| .dma_slot = stream->dma_perid, |
| .channel_direction = MEMORY_TO_PERIPHERAL, |
| .source_burst_length = 1, |
| .dest_burst_length = 1, |
| .dma_callback = dma_tx_callback, |
| }; |
| |
| /* Assure cache coherency before DMA read operation */ |
| DCACHE_CLEAN(stream->mem_block, mem_block_size); |
| |
| ret = start_dma(dev_dma, stream->dma_channel, &dma_cfg, |
| stream->mem_block, (void *)&(ssc->SSC_THR), |
| mem_block_size); |
| if (ret < 0) { |
| LOG_ERR("Failed to start TX DMA transfer: %d", ret); |
| return ret; |
| } |
| |
| /* Clear status register */ |
| (void)ssc->SSC_SR; |
| |
| ssc->SSC_IER = SSC_IER_TXEMPTY; |
| |
| ssc->SSC_CR = SSC_CR_TXEN; |
| |
| return 0; |
| } |
| |
| static void rx_stream_disable(struct stream *stream, Ssc *const ssc, |
| const struct device *dev_dma) |
| { |
| ssc->SSC_CR = SSC_CR_RXDIS; |
| ssc->SSC_IDR = SSC_IDR_OVRUN; |
| dma_stop(dev_dma, stream->dma_channel); |
| if (stream->mem_block != NULL) { |
| k_mem_slab_free(stream->cfg.mem_slab, stream->mem_block); |
| stream->mem_block = NULL; |
| } |
| } |
| |
| static void tx_stream_disable(struct stream *stream, Ssc *const ssc, |
| const struct device *dev_dma) |
| { |
| ssc->SSC_CR = SSC_CR_TXDIS; |
| ssc->SSC_IDR = SSC_IDR_TXEMPTY; |
| dma_stop(dev_dma, stream->dma_channel); |
| if (stream->mem_block != NULL) { |
| k_mem_slab_free(stream->cfg.mem_slab, stream->mem_block); |
| stream->mem_block = NULL; |
| } |
| } |
| |
| static void rx_queue_drop(struct stream *stream) |
| { |
| size_t size; |
| void *mem_block; |
| |
| while (queue_get(&stream->mem_block_queue, &mem_block, &size) == 0) { |
| k_mem_slab_free(stream->cfg.mem_slab, mem_block); |
| } |
| |
| k_sem_reset(&stream->sem); |
| } |
| |
| static void tx_queue_drop(struct stream *stream) |
| { |
| size_t size; |
| void *mem_block; |
| unsigned int n = 0U; |
| |
| while (queue_get(&stream->mem_block_queue, &mem_block, &size) == 0) { |
| k_mem_slab_free(stream->cfg.mem_slab, mem_block); |
| n++; |
| } |
| |
| for (; n > 0; n--) { |
| k_sem_give(&stream->sem); |
| } |
| } |
| |
| static int i2s_sam_trigger(const struct device *dev, enum i2s_dir dir, |
| enum i2s_trigger_cmd cmd) |
| { |
| const struct i2s_sam_dev_cfg *const dev_cfg = dev->config; |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| Ssc *const ssc = dev_cfg->regs; |
| struct stream *stream; |
| unsigned int key; |
| int ret; |
| |
| if (dir == I2S_DIR_RX) { |
| stream = &dev_data->rx; |
| } else if (dir == I2S_DIR_TX) { |
| stream = &dev_data->tx; |
| } else if (dir == I2S_DIR_BOTH) { |
| return -ENOSYS; |
| } else { |
| LOG_ERR("Either RX or TX direction must be selected"); |
| return -EINVAL; |
| } |
| |
| switch (cmd) { |
| case I2S_TRIGGER_START: |
| if (stream->state != I2S_STATE_READY) { |
| LOG_DBG("START trigger: invalid state"); |
| return -EIO; |
| } |
| |
| __ASSERT_NO_MSG(stream->mem_block == NULL); |
| |
| ret = stream->stream_start(stream, ssc, dev_cfg->dev_dma); |
| if (ret < 0) { |
| LOG_DBG("START trigger failed %d", ret); |
| return ret; |
| } |
| |
| stream->state = I2S_STATE_RUNNING; |
| stream->last_block = false; |
| break; |
| |
| case I2S_TRIGGER_STOP: |
| key = irq_lock(); |
| if (stream->state != I2S_STATE_RUNNING) { |
| irq_unlock(key); |
| LOG_DBG("STOP trigger: invalid state"); |
| return -EIO; |
| } |
| stream->state = I2S_STATE_STOPPING; |
| irq_unlock(key); |
| stream->last_block = true; |
| break; |
| |
| case I2S_TRIGGER_DRAIN: |
| key = irq_lock(); |
| if (stream->state != I2S_STATE_RUNNING) { |
| irq_unlock(key); |
| LOG_DBG("DRAIN trigger: invalid state"); |
| return -EIO; |
| } |
| stream->state = I2S_STATE_STOPPING; |
| irq_unlock(key); |
| break; |
| |
| case I2S_TRIGGER_DROP: |
| if (stream->state == I2S_STATE_NOT_READY) { |
| LOG_DBG("DROP trigger: invalid state"); |
| return -EIO; |
| } |
| stream->stream_disable(stream, ssc, dev_cfg->dev_dma); |
| stream->queue_drop(stream); |
| stream->state = I2S_STATE_READY; |
| break; |
| |
| case I2S_TRIGGER_PREPARE: |
| if (stream->state != I2S_STATE_ERROR) { |
| LOG_DBG("PREPARE trigger: invalid state"); |
| return -EIO; |
| } |
| stream->state = I2S_STATE_READY; |
| stream->queue_drop(stream); |
| break; |
| |
| default: |
| LOG_ERR("Unsupported trigger command"); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int i2s_sam_read(const struct device *dev, void **mem_block, |
| size_t *size) |
| { |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| int ret; |
| |
| if (dev_data->rx.state == I2S_STATE_NOT_READY) { |
| LOG_DBG("invalid state"); |
| return -EIO; |
| } |
| |
| if (dev_data->rx.state != I2S_STATE_ERROR) { |
| ret = k_sem_take(&dev_data->rx.sem, |
| SYS_TIMEOUT_MS(dev_data->rx.cfg.timeout)); |
| if (ret < 0) { |
| return ret; |
| } |
| } |
| |
| /* Get data from the beginning of RX queue */ |
| ret = queue_get(&dev_data->rx.mem_block_queue, mem_block, size); |
| if (ret < 0) { |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static int i2s_sam_write(const struct device *dev, void *mem_block, |
| size_t size) |
| { |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| int ret; |
| |
| if (dev_data->tx.state != I2S_STATE_RUNNING && |
| dev_data->tx.state != I2S_STATE_READY) { |
| LOG_DBG("invalid state"); |
| return -EIO; |
| } |
| |
| ret = k_sem_take(&dev_data->tx.sem, |
| SYS_TIMEOUT_MS(dev_data->tx.cfg.timeout)); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* Add data to the end of the TX queue */ |
| queue_put(&dev_data->tx.mem_block_queue, mem_block, size); |
| |
| return 0; |
| } |
| |
| static void i2s_sam_isr(const struct device *dev) |
| { |
| const struct i2s_sam_dev_cfg *const dev_cfg = dev->config; |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| Ssc *const ssc = dev_cfg->regs; |
| uint32_t isr_status; |
| |
| /* Retrieve interrupt status */ |
| isr_status = ssc->SSC_SR & ssc->SSC_IMR; |
| |
| /* Check for RX buffer overrun */ |
| if (isr_status & SSC_SR_OVRUN) { |
| dev_data->rx.state = I2S_STATE_ERROR; |
| /* Disable interrupt */ |
| ssc->SSC_IDR = SSC_IDR_OVRUN; |
| LOG_DBG("RX buffer overrun error"); |
| } |
| /* Check for TX buffer underrun */ |
| if (isr_status & SSC_SR_TXEMPTY) { |
| dev_data->tx.state = I2S_STATE_ERROR; |
| /* Disable interrupt */ |
| ssc->SSC_IDR = SSC_IDR_TXEMPTY; |
| LOG_DBG("TX buffer underrun error"); |
| } |
| } |
| |
| static int i2s_sam_initialize(const struct device *dev) |
| { |
| const struct i2s_sam_dev_cfg *const dev_cfg = dev->config; |
| struct i2s_sam_dev_data *const dev_data = dev->data; |
| Ssc *const ssc = dev_cfg->regs; |
| int ret; |
| |
| /* Configure interrupts */ |
| dev_cfg->irq_config(); |
| |
| /* Initialize semaphores */ |
| k_sem_init(&dev_data->rx.sem, 0, CONFIG_I2S_SAM_SSC_RX_BLOCK_COUNT); |
| k_sem_init(&dev_data->tx.sem, CONFIG_I2S_SAM_SSC_TX_BLOCK_COUNT, |
| CONFIG_I2S_SAM_SSC_TX_BLOCK_COUNT); |
| |
| if (!device_is_ready(dev_cfg->dev_dma)) { |
| LOG_ERR("%s device not ready", dev_cfg->dev_dma->name); |
| return -ENODEV; |
| } |
| |
| /* Connect pins to the peripheral */ |
| ret = pinctrl_apply_state(dev_cfg->pcfg, PINCTRL_STATE_DEFAULT); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* Enable SSC clock in PMC */ |
| (void)clock_control_on(SAM_DT_PMC_CONTROLLER, |
| (clock_control_subsys_t)&dev_cfg->clock_cfg); |
| |
| /* Reset the module, disable receiver & transmitter */ |
| ssc->SSC_CR = SSC_CR_RXDIS | SSC_CR_TXDIS | SSC_CR_SWRST; |
| |
| /* Enable module's IRQ */ |
| irq_enable(dev_cfg->irq_id); |
| |
| LOG_INF("Device %s initialized", dev->name); |
| |
| return 0; |
| } |
| |
| static const struct i2s_driver_api i2s_sam_driver_api = { |
| .configure = i2s_sam_configure, |
| .config_get = i2s_sam_config_get, |
| .read = i2s_sam_read, |
| .write = i2s_sam_write, |
| .trigger = i2s_sam_trigger, |
| }; |
| |
| /* I2S0 */ |
| |
| static const struct device *get_dev_from_dma_channel(uint32_t dma_channel) |
| { |
| return &DEVICE_DT_NAME_GET(DT_DRV_INST(0)); |
| } |
| |
| static void i2s0_sam_irq_config(void) |
| { |
| IRQ_CONNECT(DT_INST_IRQN(0), DT_INST_IRQ(0, priority), i2s_sam_isr, |
| DEVICE_DT_INST_GET(0), 0); |
| } |
| |
| PINCTRL_DT_INST_DEFINE(0); |
| |
| static const struct i2s_sam_dev_cfg i2s0_sam_config = { |
| .dev_dma = DEVICE_DT_GET(DT_INST_DMAS_CTLR_BY_NAME(0, tx)), |
| .regs = (Ssc *)DT_INST_REG_ADDR(0), |
| .irq_config = i2s0_sam_irq_config, |
| .clock_cfg = SAM_DT_INST_CLOCK_PMC_CFG(0), |
| .irq_id = DT_INST_IRQN(0), |
| .pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(0), |
| }; |
| |
| struct queue_item rx_0_ring_buf[CONFIG_I2S_SAM_SSC_RX_BLOCK_COUNT + 1]; |
| struct queue_item tx_0_ring_buf[CONFIG_I2S_SAM_SSC_TX_BLOCK_COUNT + 1]; |
| |
| static struct i2s_sam_dev_data i2s0_sam_data = { |
| .rx = { |
| .dma_channel = DT_INST_DMAS_CELL_BY_NAME(0, rx, channel), |
| .dma_perid = DT_INST_DMAS_CELL_BY_NAME(0, rx, perid), |
| .mem_block_queue.buf = rx_0_ring_buf, |
| .mem_block_queue.len = ARRAY_SIZE(rx_0_ring_buf), |
| .stream_start = rx_stream_start, |
| .stream_disable = rx_stream_disable, |
| .queue_drop = rx_queue_drop, |
| .set_data_format = set_rx_data_format, |
| }, |
| .tx = { |
| .dma_channel = DT_INST_DMAS_CELL_BY_NAME(0, tx, channel), |
| .dma_perid = DT_INST_DMAS_CELL_BY_NAME(0, tx, perid), |
| .mem_block_queue.buf = tx_0_ring_buf, |
| .mem_block_queue.len = ARRAY_SIZE(tx_0_ring_buf), |
| .stream_start = tx_stream_start, |
| .stream_disable = tx_stream_disable, |
| .queue_drop = tx_queue_drop, |
| .set_data_format = set_tx_data_format, |
| }, |
| }; |
| |
| DEVICE_DT_INST_DEFINE(0, &i2s_sam_initialize, NULL, |
| &i2s0_sam_data, &i2s0_sam_config, POST_KERNEL, |
| CONFIG_I2S_INIT_PRIORITY, &i2s_sam_driver_api); |