| /* |
| * Copyright 2023 NXP |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <zephyr/drivers/dai.h> |
| #include <zephyr/device.h> |
| #include <zephyr/kernel.h> |
| |
| #include "sai.h" |
| |
| /* used for binding the driver */ |
| #define DT_DRV_COMPAT nxp_dai_sai |
| |
| #define SAI_TX_RX_HW_DISABLE_TIMEOUT 50 |
| |
| /* TODO list: |
| * |
| * 1) No busy waiting should be performed in any of the operations. |
| * In the case of STOP(), the operation should be split into TRIGGER_STOP |
| * and TRIGGER_POST_STOP. (SOF) |
| * |
| * 2) The SAI ISR should stop the SAI whenever a FIFO error interrupt |
| * is raised. |
| * |
| * 3) Transmitter/receiver may remain enabled after sai_tx_rx_disable(). |
| * Fix this. |
| */ |
| |
| #ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION |
| /* note: i.MX8 boards don't seem to support the MICS field in the MCR |
| * register. As such, the MCLK source field of sai_master_clock_t is |
| * useless. I'm assuming the source is selected through xCR2's MSEL. |
| * |
| * TODO: for now, this function will set MCR's MSEL to the same value |
| * as xCR2's MSEL or, rather, to the same MCLK as the one used for |
| * generating BCLK. Is there a need to support different MCLKs in |
| * xCR2 and MCR? |
| */ |
| static int sai_mclk_config(const struct device *dev, |
| sai_bclk_source_t bclk_source, |
| const struct sai_bespoke_config *bespoke) |
| { |
| const struct sai_config *cfg; |
| struct sai_data *data; |
| sai_master_clock_t mclk_config; |
| uint32_t msel, mclk_rate; |
| int ret; |
| |
| cfg = dev->config; |
| data = dev->data; |
| |
| mclk_config.mclkOutputEnable = cfg->mclk_is_output; |
| |
| ret = get_msel(bclk_source, &msel); |
| if (ret < 0) { |
| LOG_ERR("invalid MCLK source %d for MSEL", bclk_source); |
| return ret; |
| } |
| |
| /* get MCLK's rate */ |
| ret = get_mclk_rate(&cfg->clk_data, bclk_source, &mclk_rate); |
| if (ret < 0) { |
| LOG_ERR("failed to query MCLK's rate"); |
| return ret; |
| } |
| |
| LOG_DBG("source MCLK is %u", mclk_rate); |
| |
| LOG_DBG("target MCLK is %u", bespoke->mclk_rate); |
| |
| /* source MCLK rate */ |
| mclk_config.mclkSourceClkHz = mclk_rate; |
| |
| /* target MCLK rate */ |
| mclk_config.mclkHz = bespoke->mclk_rate; |
| |
| /* commit configuration */ |
| SAI_SetMasterClockConfig(UINT_TO_I2S(data->regmap), &mclk_config); |
| |
| set_msel(data->regmap, msel); |
| |
| return 0; |
| } |
| #endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */ |
| |
| void sai_isr(const void *parameter) |
| { |
| const struct device *dev; |
| struct sai_data *data; |
| |
| dev = parameter; |
| data = dev->data; |
| |
| /* check for TX FIFO error */ |
| if (SAI_TX_RX_STATUS_IS_SET(DAI_DIR_TX, data->regmap, kSAI_FIFOErrorFlag)) { |
| LOG_ERR("FIFO underrun detected"); |
| /* TODO: this will crash the program and should be addressed as |
| * mentioned in TODO list's 2). |
| */ |
| z_irq_spurious(NULL); |
| } |
| |
| /* check for RX FIFO error */ |
| if (SAI_TX_RX_STATUS_IS_SET(DAI_DIR_RX, data->regmap, kSAI_FIFOErrorFlag)) { |
| LOG_ERR("FIFO overrun detected"); |
| /* TODO: this will crash the program and should be addressed as |
| * mentioned in TODO list's 2). |
| */ |
| z_irq_spurious(NULL); |
| } |
| } |
| |
| static int sai_config_get(const struct device *dev, |
| struct dai_config *cfg, |
| enum dai_dir dir) |
| { |
| struct sai_data *data = dev->data; |
| |
| /* dump content of the DAI configuration */ |
| memcpy(cfg, &data->cfg, sizeof(*cfg)); |
| |
| return 0; |
| } |
| |
| static const struct dai_properties |
| *sai_get_properties(const struct device *dev, enum dai_dir dir, int stream_id) |
| { |
| const struct sai_config *cfg = dev->config; |
| |
| switch (dir) { |
| case DAI_DIR_RX: |
| return cfg->rx_props; |
| case DAI_DIR_TX: |
| return cfg->tx_props; |
| default: |
| LOG_ERR("invalid direction: %d", dir); |
| return NULL; |
| } |
| |
| CODE_UNREACHABLE; |
| } |
| |
| #ifdef CONFIG_SAI_IMX93_ERRATA_051421 |
| /* notes: |
| * 1) TX and RX operate in the same mode: master/slave. As such, |
| * there's no need to check the mode for both directions. |
| * |
| * 2) Only one of the directions can operate in SYNC mode at a |
| * time. |
| * |
| * 3) What this piece of code does is it makes the SYNC direction |
| * use the ASYNC direction's BCLK that comes from its input pad. |
| * Logically speaking, this would look like: |
| * |
| * +--------+ +--------+ |
| * | TX | | RX | |
| * | module | | module | |
| * +--------+ +--------+ |
| * | ^ | |
| * | | | |
| * TX_BCLK | |____________| RX_BCLK |
| * | | |
| * V V |
| * +---------+ +---------+ |
| * | TX BCLK | | RX BCLK | |
| * | pad | | pad | |
| * +---------+ +---------+ |
| * | | |
| * | TX_BCLK | RX_BCLK |
| * V V |
| * |
| * Without BCI enabled, the TX module would use an RX_BCLK |
| * that's divided instead of the one that's obtained from |
| * bypassing the MCLK (i.e: TX_BCLK would have the value of |
| * MCLK / ((RX_DIV + 1) * 2)). If BCI is 1, then TX_BCLK will |
| * be the same as the RX_BCLK that's obtained from bypassing |
| * the MCLK on RX's side. |
| * |
| * 4) The check for BCLK == MCLK is there to see if the ASYNC |
| * direction will have the BYP bit toggled. |
| * |
| * IMPORTANT1: in the above diagram and information, RX is SYNC |
| * with TX. The same applies if RX is SYNC with TX. Also, this |
| * applies to i.MX93. For other SoCs, things may be different |
| * so use this information with caution. |
| * |
| * IMPORTANT2: for this to work, you also need to enable the |
| * pad's input path. For i.MX93, this can be achieved by setting |
| * the pad's SION bit. |
| */ |
| static void sai_config_set_err_051421(I2S_Type *base, |
| const struct sai_config *cfg, |
| const struct sai_bespoke_config *bespoke, |
| sai_transceiver_t *rx_config, |
| sai_transceiver_t *tx_config) |
| { |
| if (tx_config->masterSlave == kSAI_Master && |
| bespoke->mclk_rate == bespoke->bclk_rate) { |
| if (cfg->tx_sync_mode == kSAI_ModeSync) { |
| base->TCR2 |= I2S_TCR2_BCI(1); |
| } |
| |
| if (cfg->rx_sync_mode == kSAI_ModeSync) { |
| base->RCR2 |= I2S_RCR2_BCI(1); |
| } |
| } |
| } |
| #endif /* CONFIG_SAI_IMX93_ERRATA_051421 */ |
| |
| static int sai_config_set(const struct device *dev, |
| const struct dai_config *cfg, |
| const void *bespoke_data) |
| { |
| const struct sai_bespoke_config *bespoke; |
| sai_transceiver_t *rx_config, *tx_config; |
| struct sai_data *data; |
| const struct sai_config *sai_cfg; |
| int ret; |
| |
| if (cfg->type != DAI_IMX_SAI) { |
| LOG_ERR("wrong DAI type: %d", cfg->type); |
| return -EINVAL; |
| } |
| |
| bespoke = bespoke_data; |
| data = dev->data; |
| sai_cfg = dev->config; |
| rx_config = &data->rx_config; |
| tx_config = &data->tx_config; |
| |
| /* since this function configures the transmitter AND the receiver, that |
| * means both of them need to be stopped. As such, doing the state |
| * transition here will also result in a state check. |
| */ |
| ret = sai_update_state(DAI_DIR_TX, data, DAI_STATE_READY); |
| if (ret < 0) { |
| LOG_ERR("failed to update TX state. Reason: %d", ret); |
| return ret; |
| } |
| |
| ret = sai_update_state(DAI_DIR_RX, data, DAI_STATE_READY); |
| if (ret < 0) { |
| LOG_ERR("failed to update RX state. Reason: %d", ret); |
| return ret; |
| } |
| |
| /* condition: BCLK = FSYNC * TDM_SLOT_WIDTH * TDM_SLOTS */ |
| if (bespoke->bclk_rate != |
| (bespoke->fsync_rate * bespoke->tdm_slot_width * bespoke->tdm_slots)) { |
| LOG_ERR("bad BCLK value: %d", bespoke->bclk_rate); |
| return -EINVAL; |
| } |
| |
| /* TODO: this should be removed if we're to support sw channels != hw channels */ |
| if (count_leading_zeros(~bespoke->tx_slots) != bespoke->tdm_slots || |
| count_leading_zeros(~bespoke->rx_slots) != bespoke->tdm_slots) { |
| LOG_ERR("number of TX/RX slots doesn't match number of TDM slots"); |
| return -EINVAL; |
| } |
| |
| /* get default configurations */ |
| get_bclk_default_config(&tx_config->bitClock); |
| get_fsync_default_config(&tx_config->frameSync); |
| get_serial_default_config(&tx_config->serialData); |
| get_fifo_default_config(&tx_config->fifo); |
| |
| /* note1: this may be obvious but enabling multiple SAI |
| * channels (or data lines) may lead to FIFO starvation/ |
| * overflow if data is not written/read from the respective |
| * TDR/RDR registers. |
| * |
| * note2: the SAI data line should be enabled based on |
| * the direction (TX/RX) we're enabling. Enabling the |
| * data line for the opposite direction will lead to FIFO |
| * overrun/underrun when working with a SYNC direction. |
| * |
| * note3: the TX/RX data line shall be enabled/disabled |
| * via the sai_trigger_() suite to avoid scenarios in |
| * which one configures both direction but only starts |
| * the SYNC direction which would lead to a FIFO underrun. |
| */ |
| tx_config->channelMask = 0x0; |
| |
| /* TODO: for now, only MCLK1 is supported */ |
| tx_config->bitClock.bclkSource = kSAI_BclkSourceMclkOption1; |
| |
| /* FSYNC is asserted for tdm_slot_width BCLKs */ |
| tx_config->frameSync.frameSyncWidth = bespoke->tdm_slot_width; |
| |
| /* serial data common configuration */ |
| tx_config->serialData.dataWord0Length = bespoke->tdm_slot_width; |
| tx_config->serialData.dataWordNLength = bespoke->tdm_slot_width; |
| tx_config->serialData.dataFirstBitShifted = bespoke->tdm_slot_width; |
| tx_config->serialData.dataWordNum = bespoke->tdm_slots; |
| |
| /* clock provider configuration */ |
| switch (cfg->format & DAI_FORMAT_CLOCK_PROVIDER_MASK) { |
| case DAI_CBP_CFP: |
| tx_config->masterSlave = kSAI_Slave; |
| break; |
| case DAI_CBC_CFC: |
| tx_config->masterSlave = kSAI_Master; |
| break; |
| case DAI_CBC_CFP: |
| case DAI_CBP_CFC: |
| LOG_ERR("unsupported provider configuration: %d", |
| cfg->format & DAI_FORMAT_CLOCK_PROVIDER_MASK); |
| return -ENOTSUP; |
| default: |
| LOG_ERR("invalid provider configuration: %d", |
| cfg->format & DAI_FORMAT_CLOCK_PROVIDER_MASK); |
| return -EINVAL; |
| } |
| |
| LOG_DBG("SAI is in %d mode", tx_config->masterSlave); |
| |
| /* protocol configuration */ |
| switch (cfg->format & DAI_FORMAT_PROTOCOL_MASK) { |
| case DAI_PROTO_I2S: |
| /* BCLK is active LOW */ |
| tx_config->bitClock.bclkPolarity = kSAI_PolarityActiveLow; |
| /* FSYNC is active LOW */ |
| tx_config->frameSync.frameSyncPolarity = kSAI_PolarityActiveLow; |
| break; |
| case DAI_PROTO_DSP_A: |
| /* FSYNC is asserted for a single BCLK */ |
| tx_config->frameSync.frameSyncWidth = 1; |
| /* BCLK is active LOW */ |
| tx_config->bitClock.bclkPolarity = kSAI_PolarityActiveLow; |
| break; |
| default: |
| LOG_ERR("unsupported DAI protocol: %d", |
| cfg->format & DAI_FORMAT_PROTOCOL_MASK); |
| return -EINVAL; |
| } |
| |
| LOG_DBG("SAI uses protocol: %d", |
| cfg->format & DAI_FORMAT_PROTOCOL_MASK); |
| |
| /* clock inversion configuration */ |
| switch (cfg->format & DAI_FORMAT_CLOCK_INVERSION_MASK) { |
| case DAI_INVERSION_IB_IF: |
| SAI_INVERT_POLARITY(tx_config->bitClock.bclkPolarity); |
| SAI_INVERT_POLARITY(tx_config->frameSync.frameSyncPolarity); |
| break; |
| case DAI_INVERSION_IB_NF: |
| SAI_INVERT_POLARITY(tx_config->bitClock.bclkPolarity); |
| break; |
| case DAI_INVERSION_NB_IF: |
| SAI_INVERT_POLARITY(tx_config->frameSync.frameSyncPolarity); |
| break; |
| case DAI_INVERSION_NB_NF: |
| /* nothing to do here */ |
| break; |
| default: |
| LOG_ERR("invalid clock inversion configuration: %d", |
| cfg->format & DAI_FORMAT_CLOCK_INVERSION_MASK); |
| return -EINVAL; |
| } |
| |
| LOG_DBG("FSYNC polarity: %d", tx_config->frameSync.frameSyncPolarity); |
| LOG_DBG("BCLK polarity: %d", tx_config->bitClock.bclkPolarity); |
| |
| /* duplicate TX configuration */ |
| memcpy(rx_config, tx_config, sizeof(sai_transceiver_t)); |
| |
| tx_config->serialData.dataMaskedWord = ~bespoke->tx_slots; |
| rx_config->serialData.dataMaskedWord = ~bespoke->rx_slots; |
| |
| tx_config->fifo.fifoWatermark = sai_cfg->tx_fifo_watermark - 1; |
| rx_config->fifo.fifoWatermark = sai_cfg->rx_fifo_watermark - 1; |
| |
| LOG_DBG("RX watermark: %d", sai_cfg->rx_fifo_watermark); |
| LOG_DBG("TX watermark: %d", sai_cfg->tx_fifo_watermark); |
| |
| /* set the synchronization mode based on data passed from the DTS */ |
| tx_config->syncMode = sai_cfg->tx_sync_mode; |
| rx_config->syncMode = sai_cfg->rx_sync_mode; |
| |
| /* commit configuration */ |
| SAI_RxSetConfig(UINT_TO_I2S(data->regmap), rx_config); |
| SAI_TxSetConfig(UINT_TO_I2S(data->regmap), tx_config); |
| |
| /* a few notes here: |
| * 1) TX and RX operate in the same mode: master or slave. |
| * 2) Setting BCLK's rate needs to be performed explicitly |
| * since SetConfig() doesn't do it for us. |
| * 3) Setting BCLK's rate has to be performed after the |
| * SetConfig() call as that resets the SAI registers. |
| */ |
| if (tx_config->masterSlave == kSAI_Master) { |
| SAI_TxSetBitClockRate(UINT_TO_I2S(data->regmap), bespoke->mclk_rate, |
| bespoke->fsync_rate, bespoke->tdm_slot_width, |
| bespoke->tdm_slots); |
| |
| SAI_RxSetBitClockRate(UINT_TO_I2S(data->regmap), bespoke->mclk_rate, |
| bespoke->fsync_rate, bespoke->tdm_slot_width, |
| bespoke->tdm_slots); |
| } |
| |
| #ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION |
| ret = sai_mclk_config(dev, tx_config->bitClock.bclkSource, bespoke); |
| if (ret < 0) { |
| LOG_ERR("failed to set MCLK configuration"); |
| return ret; |
| } |
| #endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */ |
| |
| #ifdef CONFIG_SAI_IMX93_ERRATA_051421 |
| sai_config_set_err_051421(UINT_TO_I2S(data->regmap), |
| sai_cfg, bespoke, |
| rx_config, tx_config); |
| #endif /* CONFIG_SAI_IMX93_ERRATA_051421 */ |
| |
| /* this is needed so that rates different from FSYNC_RATE |
| * will not be allowed. |
| * |
| * this is because the hardware is configured to match |
| * the topology rates so attempting to play a file using |
| * a different rate from the one configured in the hardware |
| * doesn't work properly. |
| * |
| * if != 0, SOF will raise an error if the PCM rate is |
| * different than the hardware rate (a.k.a this one). |
| */ |
| data->cfg.rate = bespoke->fsync_rate; |
| /* SOF note: we don't support a variable number of channels |
| * at the moment so leaving the number of channels as 0 is |
| * unnecessary and leads to issues (e.g: the mixer buffers |
| * use this value to set the number of channels so having |
| * a 0 as this value leads to mixer buffers having 0 channels, |
| * which, in turn, leads to the DAI ending up with 0 channels, |
| * thus resulting in an error) |
| */ |
| data->cfg.channels = bespoke->tdm_slots; |
| |
| sai_dump_register_data(data->regmap); |
| |
| return 0; |
| } |
| |
| /* SOF note: please be very careful with this function as it does |
| * busy waiting and may mess up your timing in time critial applications |
| * (especially with timer domain). If this becomes unusable, the busy |
| * waiting should be removed altogether and the HW state check should |
| * be performed in sai_trigger_start() or in sai_config_set(). |
| * |
| * TODO: seems like the transmitter still remains active (even if 1ms |
| * has passed after doing a sai_trigger_stop()!). Most likely this is |
| * because sai_trigger_stop() immediately stops the data line w/o |
| * checking the HW state of the transmitter/receiver. As such, to get |
| * rid of the busy waiting, the STOP operation may have to be split into |
| * 2 operations: TRIG_STOP and TRIG_POST_STOP. |
| */ |
| static bool sai_dir_disable(struct sai_data *data, enum dai_dir dir) |
| { |
| /* VERY IMPORTANT: DO NOT use SAI_TxEnable/SAI_RxEnable |
| * here as they do not disable the ASYNC direction. |
| * Since the software logic assures that the ASYNC direction |
| * is not disabled before the SYNC direction, we can force |
| * the disablement of the given direction. |
| */ |
| sai_tx_rx_force_disable(dir, data->regmap); |
| |
| /* please note the difference between the transmitter/receiver's |
| * hardware states and their software states. The software |
| * states can be obtained by reading data->tx/rx_enabled, while |
| * the hardware states can be obtained by reading TCSR/RCSR. The |
| * hardware state can actually differ from the software state. |
| * Here, we're interested in reading the hardware state which |
| * indicates if the transmitter/receiver was actually disabled |
| * or not. |
| */ |
| return WAIT_FOR(!SAI_TX_RX_IS_HW_ENABLED(dir, data->regmap), |
| SAI_TX_RX_HW_DISABLE_TIMEOUT, k_busy_wait(1)); |
| } |
| |
| static int sai_tx_rx_disable(struct sai_data *data, |
| const struct sai_config *cfg, enum dai_dir dir) |
| { |
| enum dai_dir sync_dir, async_dir; |
| bool ret; |
| |
| /* sai_disable() should never be called from ISR context |
| * as it does some busy waiting. |
| */ |
| if (k_is_in_isr()) { |
| LOG_ERR("sai_disable() should never be called from ISR context"); |
| return -EINVAL; |
| } |
| |
| if (cfg->tx_sync_mode == kSAI_ModeAsync && |
| cfg->rx_sync_mode == kSAI_ModeAsync) { |
| ret = sai_dir_disable(data, dir); |
| if (!ret) { |
| LOG_ERR("timed out while waiting for dir %d disable", dir); |
| return -ETIMEDOUT; |
| } |
| } else { |
| sync_dir = SAI_TX_RX_GET_SYNC_DIR(cfg); |
| async_dir = SAI_TX_RX_GET_ASYNC_DIR(cfg); |
| |
| if (dir == sync_dir) { |
| ret = sai_dir_disable(data, sync_dir); |
| if (!ret) { |
| LOG_ERR("timed out while waiting for dir %d disable", |
| sync_dir); |
| return -ETIMEDOUT; |
| } |
| |
| if (!SAI_TX_RX_DIR_IS_SW_ENABLED(async_dir, data)) { |
| ret = sai_dir_disable(data, async_dir); |
| if (!ret) { |
| LOG_ERR("timed out while waiting for dir %d disable", |
| async_dir); |
| return -ETIMEDOUT; |
| } |
| } |
| } else { |
| if (!SAI_TX_RX_DIR_IS_SW_ENABLED(sync_dir, data)) { |
| ret = sai_dir_disable(data, async_dir); |
| if (!ret) { |
| LOG_ERR("timed out while waiting for dir %d disable", |
| async_dir); |
| return -ETIMEDOUT; |
| } |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int sai_trigger_pause(const struct device *dev, |
| enum dai_dir dir) |
| { |
| struct sai_data *data; |
| const struct sai_config *cfg; |
| int ret; |
| |
| data = dev->data; |
| cfg = dev->config; |
| |
| if (dir != DAI_DIR_RX && dir != DAI_DIR_TX) { |
| LOG_ERR("invalid direction: %d", dir); |
| return -EINVAL; |
| } |
| |
| /* attempt to change state */ |
| ret = sai_update_state(dir, data, DAI_STATE_PAUSED); |
| if (ret < 0) { |
| LOG_ERR("failed to transition to PAUSED from %d. Reason: %d", |
| sai_get_state(dir, data), ret); |
| return ret; |
| } |
| |
| LOG_DBG("pause on direction %d", dir); |
| |
| ret = sai_tx_rx_disable(data, cfg, dir); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* disable TX/RX data line */ |
| sai_tx_rx_set_dline_mask(dir, data->regmap, 0x0); |
| |
| /* update the software state of TX/RX */ |
| sai_tx_rx_sw_enable_disable(dir, data, false); |
| |
| return 0; |
| } |
| |
| static int sai_trigger_stop(const struct device *dev, |
| enum dai_dir dir) |
| { |
| struct sai_data *data; |
| const struct sai_config *cfg; |
| int ret; |
| uint32_t old_state; |
| |
| data = dev->data; |
| cfg = dev->config; |
| old_state = sai_get_state(dir, data); |
| |
| if (dir != DAI_DIR_RX && dir != DAI_DIR_TX) { |
| LOG_ERR("invalid direction: %d", dir); |
| return -EINVAL; |
| } |
| |
| /* attempt to change state */ |
| ret = sai_update_state(dir, data, DAI_STATE_STOPPING); |
| if (ret < 0) { |
| LOG_ERR("failed to transition to STOPPING from %d. Reason: %d", |
| sai_get_state(dir, data), ret); |
| return ret; |
| } |
| |
| LOG_DBG("stop on direction %d", dir); |
| |
| if (old_state == DAI_STATE_PAUSED) { |
| /* if SAI was previously paused then all that's |
| * left to do is disable the DMA requests and |
| * the data line. |
| */ |
| goto out_dmareq_disable; |
| } |
| |
| ret = sai_tx_rx_disable(data, cfg, dir); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* update the software state of TX/RX */ |
| sai_tx_rx_sw_enable_disable(dir, data, false); |
| |
| /* disable TX/RX data line */ |
| sai_tx_rx_set_dline_mask(dir, data->regmap, 0x0); |
| |
| out_dmareq_disable: |
| /* disable DMA requests */ |
| SAI_TX_RX_DMA_ENABLE_DISABLE(dir, data->regmap, false); |
| |
| /* disable error interrupt */ |
| SAI_TX_RX_ENABLE_DISABLE_IRQ(dir, data->regmap, |
| kSAI_FIFOErrorInterruptEnable, false); |
| |
| return 0; |
| } |
| |
| /* notes: |
| * 1) The "rx_sync_mode" and "tx_sync_mode" properties force the user to pick from |
| * SYNC and ASYNC for each direction. As such, there are 4 possible combinations |
| * that need to be covered here: |
| * a) TX ASYNC, RX ASYNC |
| * b) TX SYNC, RX ASYNC |
| * c) TX ASYNC, RX SYNC |
| * d) TX SYNC, RX SYNC |
| * |
| * Combination d) is not valid and is covered by a BUILD_ASSERT(). As such, there are 3 valid |
| * combinations that need to be supported. Since the main branch of the IF statement covers |
| * combination a), there's only combinations b) and c) to be covered here. |
| * |
| * 2) We can distinguish between 3 types of directions: |
| * a) The target direction. This is the direction on which we want to perform the |
| * software reset. |
| * b) The SYNC direction. This is, well, the direction that's in SYNC with the other |
| * direction. |
| * c) The ASYNC direction. |
| * |
| * Of course, the target direction may differ from the SYNC or ASYNC directions, but it |
| * can't differ from both of them at the same time (i.e: TARGET != SYNC AND TARGET != ASYNC). |
| * |
| * If the target direction is the same as the SYNC direction then we can safely perform the |
| * software reset on the target direction as there's nothing depending on it. We also want |
| * to do a software reset on the ASYNC direction. We can only do this if the ASYNC direction |
| * wasn't software enabled (i.e: through an explicit trigger_start() call). |
| * |
| * If the target direction is the same as the ASYNC direction then we can only perform a |
| * software reset on it only if the SYNC direction wasn't software enabled (i.e: through an |
| * explicit trigger_start() call). |
| */ |
| static void sai_tx_rx_sw_reset(struct sai_data *data, |
| const struct sai_config *cfg, enum dai_dir dir) |
| { |
| enum dai_dir sync_dir, async_dir; |
| |
| if (cfg->tx_sync_mode == kSAI_ModeAsync && |
| cfg->rx_sync_mode == kSAI_ModeAsync) { |
| /* both directions are ASYNC w.r.t each other. As such, do |
| * software reset only on the targeted direction. |
| */ |
| SAI_TX_RX_SW_RESET(dir, data->regmap); |
| } else { |
| sync_dir = SAI_TX_RX_GET_SYNC_DIR(cfg); |
| async_dir = SAI_TX_RX_GET_ASYNC_DIR(cfg); |
| |
| if (dir == sync_dir) { |
| SAI_TX_RX_SW_RESET(sync_dir, data->regmap); |
| |
| if (!SAI_TX_RX_DIR_IS_SW_ENABLED(async_dir, data)) { |
| SAI_TX_RX_SW_RESET(async_dir, data->regmap); |
| } |
| } else { |
| if (!SAI_TX_RX_DIR_IS_SW_ENABLED(sync_dir, data)) { |
| SAI_TX_RX_SW_RESET(async_dir, data->regmap); |
| } |
| } |
| } |
| } |
| |
| static int sai_trigger_start(const struct device *dev, |
| enum dai_dir dir) |
| { |
| struct sai_data *data; |
| const struct sai_config *cfg; |
| uint32_t old_state; |
| int ret; |
| |
| data = dev->data; |
| cfg = dev->config; |
| old_state = sai_get_state(dir, data); |
| |
| /* TX and RX should be triggered independently */ |
| if (dir != DAI_DIR_RX && dir != DAI_DIR_TX) { |
| LOG_ERR("invalid direction: %d", dir); |
| return -EINVAL; |
| } |
| |
| /* attempt to change state */ |
| ret = sai_update_state(dir, data, DAI_STATE_RUNNING); |
| if (ret < 0) { |
| LOG_ERR("failed to transition to RUNNING from %d. Reason: %d", |
| sai_get_state(dir, data), ret); |
| return ret; |
| } |
| |
| if (old_state == DAI_STATE_PAUSED) { |
| /* if the SAI has been paused then there's no |
| * point in issuing a software reset. As such, |
| * skip this part and go directly to the TX/RX |
| * enablement. |
| */ |
| goto out_enable_dline; |
| } |
| |
| LOG_DBG("start on direction %d", dir); |
| |
| sai_tx_rx_sw_reset(data, cfg, dir); |
| |
| /* enable error interrupt */ |
| SAI_TX_RX_ENABLE_DISABLE_IRQ(dir, data->regmap, |
| kSAI_FIFOErrorInterruptEnable, true); |
| |
| /* TODO: is there a need to write some words to the FIFO to avoid starvation? */ |
| |
| /* TODO: for now, only DMA mode is supported */ |
| SAI_TX_RX_DMA_ENABLE_DISABLE(dir, data->regmap, true); |
| |
| out_enable_dline: |
| /* enable TX/RX data line. This translates to TX_DLINE0/RX_DLINE0 |
| * being enabled. |
| * |
| * TODO: for now we only support 1 data line per direction. |
| */ |
| sai_tx_rx_set_dline_mask(dir, data->regmap, |
| SAI_TX_RX_DLINE_MASK(dir, cfg)); |
| |
| /* this will also enable the async side */ |
| SAI_TX_RX_ENABLE_DISABLE(dir, data->regmap, true); |
| |
| /* update the software state of TX/RX */ |
| sai_tx_rx_sw_enable_disable(dir, data, true); |
| |
| return 0; |
| } |
| |
| static int sai_trigger(const struct device *dev, |
| enum dai_dir dir, |
| enum dai_trigger_cmd cmd) |
| { |
| switch (cmd) { |
| case DAI_TRIGGER_START: |
| return sai_trigger_start(dev, dir); |
| case DAI_TRIGGER_PAUSE: |
| return sai_trigger_pause(dev, dir); |
| case DAI_TRIGGER_STOP: |
| return sai_trigger_stop(dev, dir); |
| case DAI_TRIGGER_PRE_START: |
| case DAI_TRIGGER_COPY: |
| /* COPY and PRE_START don't require the SAI |
| * driver to do anything at the moment so |
| * mark them as successful via a NULL return |
| * |
| * note: although the rest of the unhandled |
| * trigger commands may be valid, return |
| * an error code for them as they aren't |
| * implemented ATM (since they're not |
| * mandatory for the SAI driver to work). |
| */ |
| return 0; |
| default: |
| LOG_ERR("invalid trigger command: %d", cmd); |
| return -EINVAL; |
| } |
| |
| CODE_UNREACHABLE; |
| } |
| |
| static int sai_probe(const struct device *dev) |
| { |
| /* nothing to be done here but sadly mandatory to implement */ |
| return 0; |
| } |
| |
| static int sai_remove(const struct device *dev) |
| { |
| /* nothing to be done here but sadly mandatory to implement */ |
| return 0; |
| } |
| |
| static const struct dai_driver_api sai_api = { |
| .config_set = sai_config_set, |
| .config_get = sai_config_get, |
| .trigger = sai_trigger, |
| .get_properties = sai_get_properties, |
| .probe = sai_probe, |
| .remove = sai_remove, |
| }; |
| |
| static int sai_init(const struct device *dev) |
| { |
| const struct sai_config *cfg; |
| struct sai_data *data; |
| int i, ret; |
| |
| cfg = dev->config; |
| data = dev->data; |
| |
| device_map(&data->regmap, cfg->regmap_phys, cfg->regmap_size, K_MEM_CACHE_NONE); |
| |
| /* enable clocks if any */ |
| for (i = 0; i < cfg->clk_data.clock_num; i++) { |
| ret = clock_control_on(cfg->clk_data.dev, |
| UINT_TO_POINTER(cfg->clk_data.clocks[i])); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| LOG_DBG("clock %s has been ungated", cfg->clk_data.clock_names[i]); |
| } |
| |
| /* set TX/RX default states */ |
| data->tx_state = DAI_STATE_NOT_READY; |
| data->rx_state = DAI_STATE_NOT_READY; |
| |
| /* register ISR and enable IRQ */ |
| cfg->irq_config(); |
| |
| return 0; |
| } |
| |
| #define SAI_INIT(inst) \ |
| \ |
| BUILD_ASSERT(SAI_FIFO_DEPTH(inst) > 0 && \ |
| SAI_FIFO_DEPTH(inst) <= _SAI_FIFO_DEPTH(inst), \ |
| "invalid FIFO depth"); \ |
| \ |
| BUILD_ASSERT(SAI_RX_FIFO_WATERMARK(inst) > 0 && \ |
| SAI_RX_FIFO_WATERMARK(inst) <= _SAI_FIFO_DEPTH(inst), \ |
| "invalid RX FIFO watermark"); \ |
| \ |
| BUILD_ASSERT(SAI_TX_FIFO_WATERMARK(inst) > 0 && \ |
| SAI_TX_FIFO_WATERMARK(inst) <= _SAI_FIFO_DEPTH(inst), \ |
| "invalid TX FIFO watermark"); \ |
| \ |
| BUILD_ASSERT(IS_ENABLED(CONFIG_SAI_HAS_MCLK_CONFIG_OPTION) || \ |
| !DT_INST_PROP(inst, mclk_is_output), \ |
| "SAI doesn't support MCLK config but mclk_is_output is specified");\ |
| \ |
| BUILD_ASSERT(SAI_TX_SYNC_MODE(inst) != SAI_RX_SYNC_MODE(inst) || \ |
| SAI_TX_SYNC_MODE(inst) != kSAI_ModeSync, \ |
| "transmitter and receiver can't be both SYNC with each other"); \ |
| \ |
| BUILD_ASSERT(SAI_DLINE_COUNT(inst) != -1, \ |
| "bad or unsupported SAI instance. Is the base address correct?"); \ |
| \ |
| BUILD_ASSERT(SAI_TX_DLINE_INDEX(inst) >= 0 && \ |
| (SAI_TX_DLINE_INDEX(inst) < SAI_DLINE_COUNT(inst)), \ |
| "invalid TX data line index"); \ |
| \ |
| BUILD_ASSERT(SAI_RX_DLINE_INDEX(inst) >= 0 && \ |
| (SAI_RX_DLINE_INDEX(inst) < SAI_DLINE_COUNT(inst)), \ |
| "invalid RX data line index"); \ |
| \ |
| static const struct dai_properties sai_tx_props_##inst = { \ |
| .fifo_address = SAI_TX_FIFO_BASE(inst, SAI_TX_DLINE_INDEX(inst)), \ |
| .fifo_depth = SAI_FIFO_DEPTH(inst) * CONFIG_SAI_FIFO_WORD_SIZE, \ |
| .dma_hs_id = SAI_TX_RX_DMA_HANDSHAKE(inst, tx), \ |
| }; \ |
| \ |
| static const struct dai_properties sai_rx_props_##inst = { \ |
| .fifo_address = SAI_RX_FIFO_BASE(inst, SAI_RX_DLINE_INDEX(inst)), \ |
| .fifo_depth = SAI_FIFO_DEPTH(inst) * CONFIG_SAI_FIFO_WORD_SIZE, \ |
| .dma_hs_id = SAI_TX_RX_DMA_HANDSHAKE(inst, rx), \ |
| }; \ |
| \ |
| void irq_config_##inst(void) \ |
| { \ |
| IRQ_CONNECT(DT_INST_IRQN(inst), \ |
| 0, \ |
| sai_isr, \ |
| DEVICE_DT_INST_GET(inst), \ |
| 0); \ |
| irq_enable(DT_INST_IRQN(inst)); \ |
| } \ |
| \ |
| static struct sai_config sai_config_##inst = { \ |
| .regmap_phys = DT_INST_REG_ADDR(inst), \ |
| .regmap_size = DT_INST_REG_SIZE(inst), \ |
| .clk_data = SAI_CLOCK_DATA_DECLARE(inst), \ |
| .rx_fifo_watermark = SAI_RX_FIFO_WATERMARK(inst), \ |
| .tx_fifo_watermark = SAI_TX_FIFO_WATERMARK(inst), \ |
| .mclk_is_output = DT_INST_PROP(inst, mclk_is_output), \ |
| .tx_props = &sai_tx_props_##inst, \ |
| .rx_props = &sai_rx_props_##inst, \ |
| .irq_config = irq_config_##inst, \ |
| .tx_sync_mode = SAI_TX_SYNC_MODE(inst), \ |
| .rx_sync_mode = SAI_RX_SYNC_MODE(inst), \ |
| .tx_dline = SAI_TX_DLINE_INDEX(inst), \ |
| .rx_dline = SAI_RX_DLINE_INDEX(inst), \ |
| }; \ |
| \ |
| static struct sai_data sai_data_##inst = { \ |
| .cfg.type = DAI_IMX_SAI, \ |
| .cfg.dai_index = DT_INST_PROP_OR(inst, dai_index, 0), \ |
| }; \ |
| \ |
| DEVICE_DT_INST_DEFINE(inst, &sai_init, NULL, \ |
| &sai_data_##inst, &sai_config_##inst, \ |
| POST_KERNEL, CONFIG_DAI_INIT_PRIORITY, \ |
| &sai_api); \ |
| |
| DT_INST_FOREACH_STATUS_OKAY(SAI_INIT); |