drivers: dai: Add driver for NXP's SAI
This commit introduces a new DAI driver used for NXP'S SAI IP. Signed-off-by: Laurentiu Mihalcea <laurentiu.mihalcea@nxp.com>
This commit is contained in:
parent
897897970b
commit
fe64d840cc
7 changed files with 1366 additions and 0 deletions
|
@ -4,3 +4,4 @@ add_subdirectory_ifdef(CONFIG_DAI_INTEL_SSP intel/ssp)
|
|||
add_subdirectory_ifdef(CONFIG_DAI_INTEL_ALH intel/alh)
|
||||
add_subdirectory_ifdef(CONFIG_DAI_INTEL_DMIC intel/dmic)
|
||||
add_subdirectory_ifdef(CONFIG_DAI_INTEL_HDA intel/hda)
|
||||
add_subdirectory_ifdef(CONFIG_DAI_NXP_SAI nxp/sai)
|
||||
|
|
|
@ -29,5 +29,6 @@ source "drivers/dai/intel/ssp/Kconfig.ssp"
|
|||
source "drivers/dai/intel/alh/Kconfig.alh"
|
||||
source "drivers/dai/intel/dmic/Kconfig.dmic"
|
||||
source "drivers/dai/intel/hda/Kconfig.hda"
|
||||
source "drivers/dai/nxp/sai/Kconfig.sai"
|
||||
|
||||
endif # DAI
|
||||
|
|
5
drivers/dai/nxp/sai/CMakeLists.txt
Normal file
5
drivers/dai/nxp/sai/CMakeLists.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Copyright 2023 NXP
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
zephyr_library()
|
||||
zephyr_library_sources(sai.c)
|
30
drivers/dai/nxp/sai/Kconfig.sai
Normal file
30
drivers/dai/nxp/sai/Kconfig.sai
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Copyright 2023 NXP
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
config DAI_NXP_SAI
|
||||
bool "NXP Synchronous Audio Interface (SAI) driver"
|
||||
default y
|
||||
depends on DT_HAS_NXP_DAI_SAI_ENABLED
|
||||
help
|
||||
Select this to enable NXP SAI driver.
|
||||
|
||||
if DAI_NXP_SAI
|
||||
|
||||
config SAI_HAS_MCLK_CONFIG_OPTION
|
||||
bool "Set if SAI has MCLK configuration options"
|
||||
default n
|
||||
help
|
||||
Select this if the SAI IP allows configuration
|
||||
of the master clock. Master clock configuration
|
||||
refers to enabling/disabling the master clock,
|
||||
setting the signal as input or output or dividing
|
||||
the master clock output.
|
||||
|
||||
config SAI_FIFO_WORD_SIZE
|
||||
int "Size (in bytes) of a FIFO word"
|
||||
default 4
|
||||
help
|
||||
Use this to set the size (in bytes) of a SAI
|
||||
FIFO word.
|
||||
|
||||
endif # DAI_NXP_SAI
|
758
drivers/dai/nxp/sai/sai.c
Normal file
758
drivers/dai/nxp/sai/sai.c
Normal file
|
@ -0,0 +1,758 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
/* TODO: for now, the only supported operation mode is RX sync with TX.
|
||||
* Is there a need to support other modes?
|
||||
*/
|
||||
tx_config->syncMode = kSAI_ModeAsync;
|
||||
rx_config->syncMode = kSAI_ModeSync;
|
||||
|
||||
/* 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 */
|
||||
|
||||
/* 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 int sai_tx_rx_disable(struct sai_data *data, enum dai_dir 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 ((dir == DAI_DIR_TX && !data->rx_enabled) || dir == DAI_DIR_RX) {
|
||||
/* 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
|
||||
* hadrware 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.
|
||||
*/
|
||||
ret = WAIT_FOR(!SAI_TX_RX_IS_HW_ENABLED(dir, data->regmap),
|
||||
SAI_TX_RX_HW_DISABLE_TIMEOUT, k_busy_wait(1));
|
||||
if (!ret) {
|
||||
LOG_ERR("timed out while waiting for dir %d disable", dir);
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
|
||||
/* if TX wasn't explicitly enabled (via sai_trigger_start(TX))
|
||||
* then that means it was enabled by a sai_trigger_start(RX). As
|
||||
* such, data->tx_enabled will be false.
|
||||
*/
|
||||
if (dir == DAI_DIR_RX && !data->tx_enabled) {
|
||||
sai_tx_rx_force_disable(DAI_DIR_TX, data->regmap);
|
||||
|
||||
ret = WAIT_FOR(!SAI_TX_RX_IS_HW_ENABLED(DAI_DIR_TX, data->regmap),
|
||||
SAI_TX_RX_HW_DISABLE_TIMEOUT, k_busy_wait(1));
|
||||
if (!ret) {
|
||||
LOG_ERR("timed out while waiting for dir TX disable");
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sai_trigger_pause(const struct device *dev,
|
||||
enum dai_dir dir)
|
||||
{
|
||||
struct sai_data *data;
|
||||
int ret;
|
||||
|
||||
data = dev->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_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, dir);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
int ret;
|
||||
uint32_t old_state;
|
||||
|
||||
data = dev->data;
|
||||
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_dline_disable;
|
||||
}
|
||||
|
||||
ret = sai_tx_rx_disable(data, dir);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* update the software state of TX/RX */
|
||||
sai_tx_rx_sw_enable_disable(dir, data, false);
|
||||
|
||||
out_dline_disable:
|
||||
/* disable TX/RX data line */
|
||||
sai_tx_rx_set_dline_mask(dir, data->regmap, 0x0);
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
static int sai_trigger_start(const struct device *dev,
|
||||
enum dai_dir dir)
|
||||
{
|
||||
struct sai_data *data;
|
||||
uint32_t old_state;
|
||||
int ret;
|
||||
|
||||
data = dev->data;
|
||||
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_tx_rx;
|
||||
}
|
||||
|
||||
LOG_DBG("start on direction %d", dir);
|
||||
|
||||
if (dir == DAI_DIR_RX) {
|
||||
/* this is fine because TX is async so it won't be
|
||||
* affected by an RX software reset.
|
||||
*/
|
||||
SAI_TX_RX_SW_RESET(dir, data->regmap);
|
||||
|
||||
/* do a TX software reset only if not already enabled */
|
||||
if (!data->tx_enabled) {
|
||||
SAI_TX_RX_SW_RESET(DAI_DIR_TX, data->regmap);
|
||||
}
|
||||
} else {
|
||||
/* a software reset should be issued for TX
|
||||
* only if RX was not already enabled.
|
||||
*/
|
||||
if (!data->rx_enabled) {
|
||||
SAI_TX_RX_SW_RESET(dir, data->regmap);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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);
|
||||
|
||||
/* 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, 0x1);
|
||||
|
||||
out_enable_tx_rx:
|
||||
/* 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");\
|
||||
\
|
||||
static const struct dai_properties sai_tx_props_##inst = { \
|
||||
.fifo_address = SAI_TX_FIFO_BASE(inst), \
|
||||
.fifo_depth = SAI_FIFO_DEPTH(inst) * CONFIG_SAI_FIFO_WORD_SIZE, \
|
||||
.dma_hs_id = SAI_TX_DMA_MUX(inst), \
|
||||
}; \
|
||||
\
|
||||
static const struct dai_properties sai_rx_props_##inst = { \
|
||||
.fifo_address = SAI_RX_FIFO_BASE(inst), \
|
||||
.fifo_depth = SAI_FIFO_DEPTH(inst) * CONFIG_SAI_FIFO_WORD_SIZE, \
|
||||
.dma_hs_id = SAI_RX_DMA_MUX(inst), \
|
||||
}; \
|
||||
\
|
||||
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_OR(inst, mclk_is_output, false), \
|
||||
.tx_props = &sai_tx_props_##inst, \
|
||||
.rx_props = &sai_rx_props_##inst, \
|
||||
.irq_config = irq_config_##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);
|
511
drivers/dai/nxp/sai/sai.h
Normal file
511
drivers/dai/nxp/sai/sai.h
Normal file
|
@ -0,0 +1,511 @@
|
|||
/* Copyright 2023 NXP
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#ifndef ZEPHYR_DRIVERS_DAI_NXP_SAI_H_
|
||||
#define ZEPHYR_DRIVERS_DAI_NXP_SAI_H_
|
||||
|
||||
#include <zephyr/drivers/clock_control.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <fsl_sai.h>
|
||||
|
||||
LOG_MODULE_REGISTER(nxp_dai_sai);
|
||||
|
||||
#ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION
|
||||
#define SAI_MCLK_MCR_MSEL_SHIFT 24
|
||||
#define SAI_MCLK_MCR_MSEL_MASK GENMASK(24, 25)
|
||||
#endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */
|
||||
/* workaround the fact that device_map() doesn't exist for SoCs with no MMU */
|
||||
#ifndef DEVICE_MMIO_IS_IN_RAM
|
||||
#define device_map(virt, phys, size, flags) *(virt) = (phys)
|
||||
#endif /* DEVICE_MMIO_IS_IN_RAM */
|
||||
|
||||
/* used to convert an uint to I2S_Type * */
|
||||
#define UINT_TO_I2S(x) ((I2S_Type *)(uintptr_t)(x))
|
||||
|
||||
/* macros used for parsing DTS data */
|
||||
|
||||
/* used instead of IDENTITY because LISTIFY expects the used macro function
|
||||
* to also take a variable number of arguments.
|
||||
*/
|
||||
#define IDENTITY_VARGS(V, ...) IDENTITY(V)
|
||||
|
||||
/* used to generate the list of clock indexes */
|
||||
#define _SAI_CLOCK_INDEX_ARRAY(inst)\
|
||||
LISTIFY(DT_INST_PROP_LEN_OR(inst, clocks, 0), IDENTITY_VARGS, (,))
|
||||
|
||||
/* used to retrieve a clock's ID using its index generated via _SAI_CLOCK_INDEX_ARRAY */
|
||||
#define _SAI_GET_CLOCK_ID(clock_idx, inst)\
|
||||
DT_INST_CLOCKS_CELL_BY_IDX(inst, clock_idx, name)
|
||||
|
||||
/* used to retrieve a clock's name using its index generated via _SAI_CLOCK_INDEX_ARRAY */
|
||||
#define _SAI_GET_CLOCK_NAME(clock_idx, inst)\
|
||||
DT_INST_PROP_BY_IDX(inst, clock_names, clock_idx)
|
||||
|
||||
/* used to convert the clocks property into an array of clock IDs */
|
||||
#define _SAI_CLOCK_ID_ARRAY(inst)\
|
||||
FOR_EACH_FIXED_ARG(_SAI_GET_CLOCK_ID, (,), inst, _SAI_CLOCK_INDEX_ARRAY(inst))
|
||||
|
||||
/* used to convert the clock-names property into an array of clock names */
|
||||
#define _SAI_CLOCK_NAME_ARRAY(inst)\
|
||||
FOR_EACH_FIXED_ARG(_SAI_GET_CLOCK_NAME, (,), inst, _SAI_CLOCK_INDEX_ARRAY(inst))
|
||||
|
||||
/* used to convert a clocks property into an array of clock IDs. If the property
|
||||
* is not specified then this macro will return {}.
|
||||
*/
|
||||
#define _SAI_GET_CLOCK_ARRAY(inst)\
|
||||
COND_CODE_1(DT_NODE_HAS_PROP(DT_INST(inst, nxp_dai_sai), clocks),\
|
||||
({ _SAI_CLOCK_ID_ARRAY(inst) }),\
|
||||
({ }))
|
||||
|
||||
/* used to retrieve a const struct device *dev pointing to the clock controller.
|
||||
* It is assumed that all SAI clocks come from a single clock provider.
|
||||
* This macro returns a NULL if the clocks property doesn't exist.
|
||||
*/
|
||||
#define _SAI_GET_CLOCK_CONTROLLER(inst)\
|
||||
COND_CODE_1(DT_NODE_HAS_PROP(DT_INST(inst, nxp_dai_sai), clocks),\
|
||||
(DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(inst))),\
|
||||
(NULL))
|
||||
|
||||
/* used to convert a clock-names property into an array of clock names. If the
|
||||
* property is not specified then this macro will return {}.
|
||||
*/
|
||||
#define _SAI_GET_CLOCK_NAMES(inst)\
|
||||
COND_CODE_1(DT_NODE_HAS_PROP(DT_INST(inst, nxp_dai_sai), clocks),\
|
||||
({ _SAI_CLOCK_NAME_ARRAY(inst) }),\
|
||||
({ }))
|
||||
|
||||
/* used to declare a struct clock_data */
|
||||
#define SAI_CLOCK_DATA_DECLARE(inst) \
|
||||
{ \
|
||||
.clocks = (uint32_t [])_SAI_GET_CLOCK_ARRAY(inst), \
|
||||
.clock_num = DT_INST_PROP_LEN_OR(inst, clocks, 0), \
|
||||
.dev = _SAI_GET_CLOCK_CONTROLLER(inst), \
|
||||
.clock_names = (const char *[])_SAI_GET_CLOCK_NAMES(inst), \
|
||||
}
|
||||
|
||||
/* used to parse the tx-fifo-watermark property. If said property is not
|
||||
* specified then this macro will return half of the number of words in the
|
||||
* FIFO.
|
||||
*/
|
||||
#define SAI_TX_FIFO_WATERMARK(inst)\
|
||||
DT_INST_PROP_OR(inst, tx_fifo_watermark,\
|
||||
FSL_FEATURE_SAI_FIFO_COUNTn(UINT_TO_I2S(DT_INST_REG_ADDR(inst))) / 2)
|
||||
|
||||
/* used to parse the rx-fifo-watermark property. If said property is not
|
||||
* specified then this macro will return half of the number of words in the
|
||||
* FIFO.
|
||||
*/
|
||||
#define SAI_RX_FIFO_WATERMARK(inst)\
|
||||
DT_INST_PROP_OR(inst, rx_fifo_watermark,\
|
||||
FSL_FEATURE_SAI_FIFO_COUNTn(UINT_TO_I2S(DT_INST_REG_ADDR(inst))) / 2)
|
||||
|
||||
/* used to retrieve TFR0's address based on SAI's physical address */
|
||||
#define SAI_TX_FIFO_BASE(inst)\
|
||||
FSL_FEATURE_SAI_TX_FIFO_BASEn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)), 0)
|
||||
|
||||
/* used to retrieve RFR0's address based on SAI's physical address */
|
||||
#define SAI_RX_FIFO_BASE(inst)\
|
||||
FSL_FEATURE_SAI_RX_FIFO_BASEn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)), 0)
|
||||
|
||||
/* internal macro used to retrieve the default TX/RX FIFO's size (in FIFO words) */
|
||||
#define _SAI_FIFO_DEPTH(inst)\
|
||||
FSL_FEATURE_SAI_FIFO_COUNTn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)))
|
||||
|
||||
/* used to retrieve the TX/RX FIFO's size (in FIFO words) */
|
||||
#define SAI_FIFO_DEPTH(inst)\
|
||||
DT_INST_PROP_OR(inst, fifo_depth, _SAI_FIFO_DEPTH(inst))
|
||||
|
||||
/* used to retrieve the DMA MUX for transmitter */
|
||||
#define SAI_TX_DMA_MUX(inst)\
|
||||
FSL_FEATURE_SAI_TX_DMA_MUXn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)))
|
||||
|
||||
/* used to retrieve the DMA MUX for receiver */
|
||||
#define SAI_RX_DMA_MUX(inst)\
|
||||
FSL_FEATURE_SAI_RX_DMA_MUXn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)))
|
||||
|
||||
/* utility macros */
|
||||
|
||||
/* invert a clock's polarity. This works because a clock's polarity is expressed
|
||||
* as a 0 or as a 1.
|
||||
*/
|
||||
#define SAI_INVERT_POLARITY(polarity) (polarity) = !(polarity)
|
||||
|
||||
/* used to issue a software reset of the transmitter/receiver */
|
||||
#define SAI_TX_RX_SW_RESET(dir, regmap)\
|
||||
((dir) == DAI_DIR_RX ? SAI_RxSoftwareReset(UINT_TO_I2S(regmap), kSAI_ResetTypeSoftware) :\
|
||||
SAI_TxSoftwareReset(UINT_TO_I2S(regmap), kSAI_ResetTypeSoftware))
|
||||
|
||||
/* used to enable/disable the transmitter/receiver.
|
||||
* When enabling the SYNC component, the ASYNC component will also be enabled.
|
||||
* Attempting to disable the SYNC component will fail unless the SYNC bit is
|
||||
* cleared. It is recommended to use sai_tx_rx_force_disable() instead of this
|
||||
* macro when disabling transmitter/receiver.
|
||||
*/
|
||||
#define SAI_TX_RX_ENABLE_DISABLE(dir, regmap, enable)\
|
||||
((dir) == DAI_DIR_RX ? SAI_RxEnable(UINT_TO_I2S(regmap), enable) :\
|
||||
SAI_TxEnable(UINT_TO_I2S(regmap), enable))
|
||||
|
||||
/* used to enable/disable the DMA requests for transmitter/receiver */
|
||||
#define SAI_TX_RX_DMA_ENABLE_DISABLE(dir, regmap, enable)\
|
||||
((dir) == DAI_DIR_RX ? SAI_RxEnableDMA(UINT_TO_I2S(regmap),\
|
||||
kSAI_FIFORequestDMAEnable, enable) :\
|
||||
SAI_TxEnableDMA(UINT_TO_I2S(regmap), kSAI_FIFORequestDMAEnable, enable))
|
||||
|
||||
/* used to check if the hardware transmitter/receiver is enabled */
|
||||
#define SAI_TX_RX_IS_HW_ENABLED(dir, regmap)\
|
||||
((dir) == DAI_DIR_RX ? (UINT_TO_I2S(regmap)->RCSR & I2S_RCSR_RE_MASK) : \
|
||||
(UINT_TO_I2S(regmap)->TCSR & I2S_TCSR_TE_MASK))
|
||||
|
||||
/* used to enable various transmitter/receiver interrupts */
|
||||
#define _SAI_TX_RX_ENABLE_IRQ(dir, regmap, which)\
|
||||
((dir) == DAI_DIR_RX ? SAI_RxEnableInterrupts(UINT_TO_I2S(regmap), which) : \
|
||||
SAI_TxEnableInterrupts(UINT_TO_I2S(regmap), which))
|
||||
|
||||
/* used to disable various transmitter/receiver interrupts */
|
||||
#define _SAI_TX_RX_DISABLE_IRQ(dir, regmap, which)\
|
||||
((dir) == DAI_DIR_RX ? SAI_RxDisableInterrupts(UINT_TO_I2S(regmap), which) : \
|
||||
SAI_TxDisableInterrupts(UINT_TO_I2S(regmap), which))
|
||||
|
||||
/* used to enable/disable various transmitter/receiver interrupts */
|
||||
#define SAI_TX_RX_ENABLE_DISABLE_IRQ(dir, regmap, which, enable)\
|
||||
((enable == true) ? _SAI_TX_RX_ENABLE_IRQ(dir, regmap, which) :\
|
||||
_SAI_TX_RX_DISABLE_IRQ(dir, regmap, which))
|
||||
|
||||
/* used to check if a status flag is set */
|
||||
#define SAI_TX_RX_STATUS_IS_SET(dir, regmap, which)\
|
||||
((dir) == DAI_DIR_RX ? ((UINT_TO_I2S(regmap))->RCSR & (which)) : \
|
||||
((UINT_TO_I2S(regmap))->TCSR & (which)))
|
||||
|
||||
struct sai_clock_data {
|
||||
uint32_t *clocks;
|
||||
uint32_t clock_num;
|
||||
/* assumption: all clocks belong to the same producer */
|
||||
const struct device *dev;
|
||||
const char **clock_names;
|
||||
};
|
||||
|
||||
struct sai_data {
|
||||
mm_reg_t regmap;
|
||||
sai_transceiver_t rx_config;
|
||||
sai_transceiver_t tx_config;
|
||||
bool tx_enabled;
|
||||
bool rx_enabled;
|
||||
enum dai_state tx_state;
|
||||
enum dai_state rx_state;
|
||||
struct dai_config cfg;
|
||||
};
|
||||
|
||||
struct sai_config {
|
||||
uint32_t regmap_phys;
|
||||
uint32_t regmap_size;
|
||||
struct sai_clock_data clk_data;
|
||||
bool mclk_is_output;
|
||||
/* if the tx/rx-fifo-watermark properties are not specified, it's going
|
||||
* to be assumed that the watermark should be set to half of the FIFO
|
||||
* size.
|
||||
*/
|
||||
uint32_t rx_fifo_watermark;
|
||||
uint32_t tx_fifo_watermark;
|
||||
const struct dai_properties *tx_props;
|
||||
const struct dai_properties *rx_props;
|
||||
uint32_t dai_index;
|
||||
void (*irq_config)(void);
|
||||
};
|
||||
|
||||
/* this needs to perfectly match SOF's struct sof_ipc_dai_sai_params */
|
||||
struct sai_bespoke_config {
|
||||
uint32_t reserved0;
|
||||
|
||||
uint16_t reserved1;
|
||||
uint16_t mclk_id;
|
||||
uint32_t mclk_direction;
|
||||
|
||||
/* CLOCK-related data */
|
||||
uint32_t mclk_rate;
|
||||
uint32_t fsync_rate;
|
||||
uint32_t bclk_rate;
|
||||
|
||||
/* TDM-related data */
|
||||
uint32_t tdm_slots;
|
||||
uint32_t rx_slots;
|
||||
uint32_t tx_slots;
|
||||
uint16_t tdm_slot_width;
|
||||
uint16_t reserved2;
|
||||
};
|
||||
|
||||
#ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION
|
||||
static int get_msel(sai_bclk_source_t bclk_source, uint32_t *msel)
|
||||
{
|
||||
switch (bclk_source) {
|
||||
case kSAI_BclkSourceMclkOption1:
|
||||
*msel = 0;
|
||||
break;
|
||||
case kSAI_BclkSourceMclkOption2:
|
||||
*msel = (0x2 << SAI_MCLK_MCR_MSEL_SHIFT);
|
||||
break;
|
||||
case kSAI_BclkSourceMclkOption3:
|
||||
*msel = (0x3 << SAI_MCLK_MCR_MSEL_SHIFT);
|
||||
break;
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void set_msel(uint32_t regmap, int msel)
|
||||
{
|
||||
UINT_TO_I2S(regmap)->MCR &= ~SAI_MCLK_MCR_MSEL_MASK;
|
||||
UINT_TO_I2S(regmap)->MCR |= msel;
|
||||
}
|
||||
|
||||
static int clk_lookup_by_name(const struct sai_clock_data *clk_data, char *name)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < clk_data->clock_num; i++) {
|
||||
if (!strcmp(name, clk_data->clock_names[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
static int get_mclk_rate(const struct sai_clock_data *clk_data,
|
||||
sai_bclk_source_t bclk_source,
|
||||
uint32_t *rate)
|
||||
{
|
||||
int clk_idx;
|
||||
char *clk_name;
|
||||
|
||||
switch (bclk_source) {
|
||||
case kSAI_BclkSourceMclkOption1:
|
||||
clk_name = "mclk1";
|
||||
break;
|
||||
case kSAI_BclkSourceMclkOption2:
|
||||
clk_name = "mclk2";
|
||||
break;
|
||||
case kSAI_BclkSourceMclkOption3:
|
||||
clk_name = "mclk3";
|
||||
break;
|
||||
default:
|
||||
LOG_ERR("invalid bitclock source: %d", bclk_source);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
clk_idx = clk_lookup_by_name(clk_data, clk_name);
|
||||
if (clk_idx < 0) {
|
||||
LOG_ERR("failed to get clock index for %s", clk_name);
|
||||
return clk_idx;
|
||||
}
|
||||
|
||||
return clock_control_get_rate(clk_data->dev,
|
||||
UINT_TO_POINTER(clk_data->clocks[clk_idx]),
|
||||
rate);
|
||||
}
|
||||
#endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */
|
||||
|
||||
static inline void get_bclk_default_config(sai_bit_clock_t *cfg)
|
||||
{
|
||||
memset(cfg, 0, sizeof(sai_bit_clock_t));
|
||||
|
||||
/* by default, BCLK has the following properties:
|
||||
*
|
||||
* 1) BCLK is active HIGH.
|
||||
* 2) BCLK uses MCLK1 source. (only applicable to master mode)
|
||||
* 3) No source swap.
|
||||
* 4) No input delay.
|
||||
*/
|
||||
cfg->bclkPolarity = kSAI_PolarityActiveHigh;
|
||||
cfg->bclkSource = kSAI_BclkSourceMclkOption1;
|
||||
}
|
||||
|
||||
static inline void get_fsync_default_config(sai_frame_sync_t *cfg)
|
||||
{
|
||||
memset(cfg, 0, sizeof(sai_frame_sync_t));
|
||||
|
||||
/* by default, FSYNC has the following properties:
|
||||
*
|
||||
* 1) FSYNC is asserted one bit early with respect to the next
|
||||
* frame.
|
||||
* 2) FSYNC is active HIGH.
|
||||
*/
|
||||
cfg->frameSyncEarly = true;
|
||||
cfg->frameSyncPolarity = kSAI_PolarityActiveHigh;
|
||||
}
|
||||
|
||||
static inline void get_serial_default_config(sai_serial_data_t *cfg)
|
||||
{
|
||||
memset(cfg, 0, sizeof(sai_serial_data_t));
|
||||
|
||||
/* by default, the serial configuration has the following quirks:
|
||||
*
|
||||
* 1) Data pin is not tri-stated.
|
||||
* 2) MSB is first.
|
||||
*/
|
||||
/* note: this is equivalent to checking if the SAI has xCR4's CHMOD bit */
|
||||
#if FSL_FEATURE_SAI_HAS_CHANNEL_MODE
|
||||
cfg->dataMode = kSAI_DataPinStateOutputZero;
|
||||
#endif /* FSL_FEATURE_SAI_HAS_CHANNEL_MODE */
|
||||
cfg->dataOrder = kSAI_DataMSB;
|
||||
}
|
||||
|
||||
static inline void get_fifo_default_config(sai_fifo_t *cfg)
|
||||
{
|
||||
memset(cfg, 0, sizeof(sai_fifo_t));
|
||||
}
|
||||
|
||||
static inline uint32_t sai_get_state(enum dai_dir dir,
|
||||
struct sai_data *data)
|
||||
{
|
||||
if (dir == DAI_DIR_RX) {
|
||||
return data->rx_state;
|
||||
} else {
|
||||
return data->tx_state;
|
||||
}
|
||||
}
|
||||
|
||||
static int sai_update_state(enum dai_dir dir,
|
||||
struct sai_data *data,
|
||||
enum dai_state new_state)
|
||||
{
|
||||
enum dai_state old_state = sai_get_state(dir, data);
|
||||
|
||||
LOG_DBG("attempting to transition from %d to %d", old_state, new_state);
|
||||
|
||||
/* check if transition is possible */
|
||||
switch (new_state) {
|
||||
case DAI_STATE_NOT_READY:
|
||||
/* this shouldn't be possible */
|
||||
return -EPERM;
|
||||
case DAI_STATE_READY:
|
||||
if (old_state != DAI_STATE_NOT_READY &&
|
||||
old_state != DAI_STATE_READY &&
|
||||
old_state != DAI_STATE_STOPPING) {
|
||||
return -EPERM;
|
||||
}
|
||||
break;
|
||||
case DAI_STATE_RUNNING:
|
||||
if (old_state != DAI_STATE_PAUSED &&
|
||||
old_state != DAI_STATE_STOPPING &&
|
||||
old_state != DAI_STATE_READY) {
|
||||
return -EPERM;
|
||||
}
|
||||
break;
|
||||
case DAI_STATE_PAUSED:
|
||||
if (old_state != DAI_STATE_RUNNING) {
|
||||
return -EPERM;
|
||||
}
|
||||
break;
|
||||
case DAI_STATE_STOPPING:
|
||||
if (old_state != DAI_STATE_READY &&
|
||||
old_state != DAI_STATE_RUNNING &&
|
||||
old_state != DAI_STATE_PAUSED) {
|
||||
return -EPERM;
|
||||
}
|
||||
break;
|
||||
case DAI_STATE_ERROR:
|
||||
case DAI_STATE_PRE_RUNNING:
|
||||
/* these states are not used so transitioning to them
|
||||
* is considered invalid.
|
||||
*/
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
if (dir == DAI_DIR_RX) {
|
||||
data->rx_state = new_state;
|
||||
} else {
|
||||
data->tx_state = new_state;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void sai_tx_rx_force_disable(enum dai_dir dir,
|
||||
uint32_t regmap)
|
||||
{
|
||||
I2S_Type *base = UINT_TO_I2S(regmap);
|
||||
|
||||
if (dir == DAI_DIR_RX) {
|
||||
base->RCSR = ((base->RCSR & 0xFFE3FFFFU) & (~I2S_RCSR_RE_MASK));
|
||||
} else {
|
||||
base->TCSR = ((base->TCSR & 0xFFE3FFFFU) & (~I2S_TCSR_TE_MASK));
|
||||
}
|
||||
}
|
||||
|
||||
static inline void sai_tx_rx_sw_enable_disable(enum dai_dir dir,
|
||||
struct sai_data *data,
|
||||
bool enable)
|
||||
{
|
||||
if (dir == DAI_DIR_RX) {
|
||||
data->rx_enabled = enable;
|
||||
} else {
|
||||
data->tx_enabled = enable;
|
||||
}
|
||||
}
|
||||
|
||||
static inline int count_leading_zeros(uint32_t word)
|
||||
{
|
||||
int num = 0;
|
||||
|
||||
while (word) {
|
||||
if (!(word & 0x1)) {
|
||||
num++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
word = word >> 1;
|
||||
}
|
||||
|
||||
return num;
|
||||
}
|
||||
|
||||
static inline void sai_tx_rx_set_dline_mask(enum dai_dir dir, uint32_t regmap, uint32_t mask)
|
||||
{
|
||||
I2S_Type *base = UINT_TO_I2S(regmap);
|
||||
|
||||
if (dir == DAI_DIR_RX) {
|
||||
base->RCR3 &= ~I2S_RCR3_RCE_MASK;
|
||||
base->RCR3 |= I2S_RCR3_RCE(mask);
|
||||
} else {
|
||||
base->TCR3 &= ~I2S_TCR3_TCE_MASK;
|
||||
base->TCR3 |= I2S_TCR3_TCE(mask);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void sai_dump_register_data(uint32_t regmap)
|
||||
{
|
||||
I2S_Type *base = UINT_TO_I2S(regmap);
|
||||
|
||||
LOG_DBG("TCSR: 0x%x", base->TCSR);
|
||||
LOG_DBG("RCSR: 0x%x", base->RCSR);
|
||||
|
||||
LOG_DBG("TCR1: 0x%x", base->TCR1);
|
||||
LOG_DBG("RCR1: 0x%x", base->RCR1);
|
||||
|
||||
LOG_DBG("TCR2: 0x%x", base->TCR2);
|
||||
LOG_DBG("RCR2: 0x%x", base->RCR2);
|
||||
|
||||
LOG_DBG("TCR3: 0x%x", base->TCR3);
|
||||
LOG_DBG("RCR3: 0x%x", base->RCR3);
|
||||
|
||||
LOG_DBG("TCR4: 0x%x", base->TCR4);
|
||||
LOG_DBG("RCR4: 0x%x", base->RCR4);
|
||||
|
||||
LOG_DBG("TCR5: 0x%x", base->TCR5);
|
||||
LOG_DBG("RCR5: 0x%x", base->RCR5);
|
||||
|
||||
LOG_DBG("TMR: 0x%x", base->TMR);
|
||||
LOG_DBG("RMR: 0x%x", base->RMR);
|
||||
|
||||
#ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION
|
||||
LOG_DBG("MCR: 0x%x", base->MCR);
|
||||
#endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */
|
||||
}
|
||||
|
||||
#endif /* ZEPHYR_DRIVERS_DAI_NXP_SAI_H_ */
|
60
dts/bindings/dai/nxp,dai-sai.yaml
Normal file
60
dts/bindings/dai/nxp,dai-sai.yaml
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Copyright 2023 NXP
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
description: NXP Synchronous Audio Interface (SAI) node
|
||||
|
||||
compatible: "nxp,dai-sai"
|
||||
|
||||
include: base.yaml
|
||||
|
||||
properties:
|
||||
reg:
|
||||
required: true
|
||||
mclk-is-output:
|
||||
type: boolean
|
||||
description: |
|
||||
Use this property to set the SAI MCLK as output or as input.
|
||||
By default, if this property is not specified, MCLK will be
|
||||
set as input. Setting the MCLK as output for SAIs which don't
|
||||
support MCLK configuration will result in a BUILD_ASSERT()
|
||||
failure.
|
||||
rx-fifo-watermark:
|
||||
type: int
|
||||
description: |
|
||||
Use this property to specify the watermark value for the TX
|
||||
FIFO. This value needs to be in FIFO words (NOT BYTES). This
|
||||
value needs to be in the following interval: (0, DEFAULT_FIFO_DEPTH],
|
||||
otherwise a BUILD_ASSERT() failure will be raised.
|
||||
tx-fifo-watermark:
|
||||
type: int
|
||||
description: |
|
||||
Use this property to specify the watermark value for the RX
|
||||
FIFO. This value needs to be in FIFO words (NOT BYTES). This
|
||||
value needs to be in the following interval: (0, DEFAULT_FIFO_DEPTH],
|
||||
otherwise a BUILD_ASSERT() failure will be raised.
|
||||
interrupts:
|
||||
required: true
|
||||
fifo-depth:
|
||||
type: int
|
||||
description: |
|
||||
Use this property to set the FIFO depth that will be reported
|
||||
to other applications calling dai_get_properties(). This value
|
||||
should be in the following interval: (0, DEFAULT_FIFO_DEPTH],
|
||||
otherwise a BUILD_ASSERT() failure will be raised.
|
||||
By DEFAULT_FIFO_DEPTH we mean the actual (hardware) value of
|
||||
the FIFO depth. This is needed because some applications (e.g: SOF)
|
||||
use this value to compute the DMA burst size, in which case
|
||||
DEFAULT_FIFO_DEPTH cannot be used. Generally, reporting a false
|
||||
FIFO depth should be avoided. Please note that the sanity check
|
||||
for tx/rx-fifo-watermark uses DEFAULT_FIFO_DEPTH instead of this
|
||||
value so use with caution. If unsure, it's better to simply not
|
||||
use this property, in which case the reported value will be
|
||||
DEFAULT_FIFO_DEPTH.
|
||||
dai-index:
|
||||
type: int
|
||||
description: |
|
||||
Use this property to specify the index of the DAI. At the
|
||||
moment, this is only used by SOF to fetch the "struct device"
|
||||
associated with the DAI whose index Linux passes to SOF
|
||||
through an IPC. If this property is not specified, the DAI
|
||||
index will be considered 0.
|
Loading…
Add table
Add a link
Reference in a new issue