/* * Copyright 2023 NXP * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include #include #include #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_WRN("FIFO underrun detected"); SAI_TX_RX_STATUS_CLEAR(DAI_DIR_TX, data->regmap, kSAI_FIFOErrorFlag); } /* check for RX FIFO error */ if (SAI_TX_RX_STATUS_IS_SET(DAI_DIR_RX, data->regmap, kSAI_FIFOErrorFlag)) { LOG_WRN("FIFO overrun detected"); SAI_TX_RX_STATUS_CLEAR(DAI_DIR_RX, data->regmap, kSAI_FIFOErrorFlag); } } 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; ret = pm_device_runtime_get(dev); if (ret < 0) { LOG_ERR("failed to get() SAI device: %d", ret); return ret; } /* 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"); pm_device_runtime_put(dev); 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 pm_device_runtime_put(dev); } /* 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); irq_disable(cfg->irq); return pm_device_runtime_put(dev); } /* 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, i; 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); ret = pm_device_runtime_get(dev); if (ret < 0) { LOG_ERR("failed to get() SAI device: %d", ret); return ret; } sai_tx_rx_sw_reset(data, cfg, dir); irq_enable(cfg->irq); /* enable error interrupt */ SAI_TX_RX_ENABLE_DISABLE_IRQ(dir, data->regmap, kSAI_FIFOErrorInterruptEnable, true); /* avoid initial underrun by writing a frame's worth of 0s */ if (dir == DAI_DIR_TX) { for (i = 0; i < data->cfg.channels; i++) { SAI_WriteData(UINT_TO_I2S(data->regmap), cfg->tx_dline, 0x0); } } /* 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 DEVICE_API(dai, 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_clks_enable_disable(const struct device *dev, bool enable) { int i, ret; const struct sai_config *cfg; void *clk_id; cfg = dev->config; for (i = 0; i < cfg->clk_data.clock_num; i++) { clk_id = UINT_TO_POINTER(cfg->clk_data.clocks[i]); if (enable) { ret = clock_control_on(cfg->clk_data.dev, clk_id); } else { ret = clock_control_off(cfg->clk_data.dev, clk_id); } if (ret < 0) { LOG_ERR("failed to gate/ungate clock %u: %d", cfg->clk_data.clocks[i], ret); return ret; } } return 0; } __maybe_unused static int sai_pm_action(const struct device *dev, enum pm_device_action action) { bool enable = true; switch (action) { case PM_DEVICE_ACTION_RESUME: break; case PM_DEVICE_ACTION_SUSPEND: enable = false; break; case PM_DEVICE_ACTION_TURN_ON: case PM_DEVICE_ACTION_TURN_OFF: return 0; default: return -ENOTSUP; } return sai_clks_enable_disable(dev, enable); } static int sai_init(const struct device *dev) { const struct sai_config *cfg; struct sai_data *data; int ret; cfg = dev->config; data = dev->data; device_map(&data->regmap, cfg->regmap_phys, cfg->regmap_size, K_MEM_CACHE_NONE); #ifndef CONFIG_PM_DEVICE_RUNTIME ret = sai_clks_enable_disable(dev, true); if (ret < 0) { return ret; } #endif /* CONFIG_PM_DEVICE_RUNTIME */ /* note: optional operation so -ENOENT is allowed (i.e: we * allow the default state to not be defined) */ ret = pinctrl_apply_state(cfg->pincfg, PINCTRL_STATE_DEFAULT); if (ret < 0 && ret != -ENOENT) { return ret; } /* set TX/RX default states */ data->tx_state = DAI_STATE_NOT_READY; data->rx_state = DAI_STATE_NOT_READY; /* register ISR */ cfg->irq_config(); return pm_device_runtime_enable(dev); } #define SAI_INIT(inst) \ \ PINCTRL_DT_INST_DEFINE(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); \ } \ \ static struct sai_config sai_config_##inst = { \ .regmap_phys = DT_INST_REG_ADDR(inst), \ .regmap_size = DT_INST_REG_SIZE(inst), \ .irq = DT_INST_IRQN(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), \ .pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(inst), \ }; \ \ static struct sai_data sai_data_##inst = { \ .cfg.type = DAI_IMX_SAI, \ .cfg.dai_index = DT_INST_PROP_OR(inst, dai_index, 0), \ }; \ \ PM_DEVICE_DT_INST_DEFINE(inst, sai_pm_action); \ \ DEVICE_DT_INST_DEFINE(inst, &sai_init, PM_DEVICE_DT_INST_GET(inst), \ &sai_data_##inst, &sai_config_##inst, \ POST_KERNEL, CONFIG_DAI_INIT_PRIORITY, \ &sai_api); \ DT_INST_FOREACH_STATUS_OKAY(SAI_INIT);