| /* |
| * Copyright (c) 2022 Intel Corporation. |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| |
| #include <errno.h> |
| #include <zephyr/sys/util_macro.h> |
| #include <stdbool.h> |
| #include <stdint.h> |
| #include <zephyr/spinlock.h> |
| #include <zephyr/devicetree.h> |
| #include <zephyr/pm/device.h> |
| #include <zephyr/pm/device_runtime.h> |
| #define LOG_DOMAIN dai_intel_ssp |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(LOG_DOMAIN); |
| |
| #include "ssp.h" |
| |
| #define DT_DRV_COMPAT intel_ssp_dai |
| |
| #define dai_set_drvdata(dai, data) (dai->priv_data = data) |
| #define dai_get_drvdata(dai) dai->priv_data |
| #define dai_get_mn(dai) dai->plat_data.mn_inst |
| #define dai_get_ftable(dai) dai->plat_data.ftable |
| #define dai_get_fsources(dai) dai->plat_data.fsources |
| #define dai_mn_base(dai) dai->plat_data.mn_inst->base |
| #define dai_base(dai) dai->plat_data.base |
| #define dai_ip_base(dai) dai->plat_data.ip_base |
| #define dai_shim_base(dai) dai->plat_data.shim_base |
| #define dai_hdamlssp_base(dai) dai->plat_data.hdamlssp_base |
| #define dai_i2svss_base(dai) dai->plat_data.i2svss_base |
| |
| #define DAI_DIR_PLAYBACK 0 |
| #define DAI_DIR_CAPTURE 1 |
| #define SSP_ARRAY_INDEX(dir) dir == DAI_DIR_RX ? DAI_DIR_CAPTURE : DAI_DIR_PLAYBACK |
| |
| static void dai_ssp_update_bits(struct dai_intel_ssp *dp, uint32_t reg, uint32_t mask, uint32_t val) |
| { |
| uint32_t dest = dai_base(dp) + reg; |
| |
| LOG_INF("%s base %x, reg %x, mask %x, value %x", __func__, |
| dai_base(dp), reg, mask, val); |
| |
| sys_write32((sys_read32(dest) & (~mask)) | (val & mask), dest); |
| } |
| |
| #if CONFIG_INTEL_MN |
| static int dai_ssp_gcd(int a, int b) |
| { |
| int aux; |
| int k; |
| |
| if (a == 0) { |
| return b; |
| } |
| |
| if (b == 0) { |
| return a; |
| } |
| |
| /* If the numbers are negative, convert them to positive numbers |
| * gcd(a, b) = gcd(-a, -b) = gcd(-a, b) = gcd(a, -b) |
| */ |
| if (a < 0) { |
| a = -a; |
| } |
| |
| if (b < 0) { |
| b = -b; |
| } |
| |
| /* Find the greatest power of 2 that devides both a and b */ |
| for (k = 0; ((a | b) & 1) == 0; k++) { |
| a >>= 1; |
| b >>= 1; |
| } |
| |
| /* divide by 2 until a becomes odd */ |
| while ((a & 1) == 0) { |
| a >>= 1; |
| } |
| |
| do { |
| /*if b is even, remove all factors of 2*/ |
| while ((b & 1) == 0) { |
| b >>= 1; |
| } |
| |
| /* both a and b are odd now. Swap so a <= b |
| * then set b = b - a, which is also even |
| */ |
| if (a > b) { |
| aux = a; |
| a = b; |
| b = aux; |
| } |
| |
| b = b - a; |
| |
| } while (b != 0); |
| |
| /* restore common factors of 2 */ |
| return a << k; |
| } |
| #endif |
| |
| /** |
| * \brief Checks if given clock is used as source for any MCLK. |
| * |
| * \return true if any port use given clock source, false otherwise. |
| */ |
| static bool dai_ssp_is_mclk_source_in_use(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| bool ret = false; |
| int i; |
| |
| for (i = 0; i < ARRAY_SIZE(mp->mclk_sources_ref); i++) { |
| if (mp->mclk_sources_ref[i] > 0) { |
| ret = true; |
| break; |
| } |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * \brief Configures source clock for MCLK. |
| * All MCLKs share the same source, so it should be changed |
| * only if there are no other ports using it already. |
| * \param[in] mclk_rate main clock frequency. |
| * \return 0 on success, error code otherwise. |
| */ |
| static int dai_ssp_setup_initial_mclk_source(struct dai_intel_ssp *dp, uint32_t mclk_id, |
| uint32_t mclk_rate) |
| { |
| struct dai_intel_ssp_freq_table *ft = dai_get_ftable(dp); |
| uint32_t *fs = dai_get_fsources(dp); |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| int clk_index = -1; |
| uint32_t mdivc; |
| int ret = 0; |
| int i; |
| |
| if (mclk_id >= DAI_INTEL_SSP_NUM_MCLK) { |
| LOG_ERR("%s can't configure MCLK %d, only %d mclk[s] existed!", |
| __func__, mclk_id, DAI_INTEL_SSP_NUM_MCLK); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* searching the smallest possible mclk source */ |
| for (i = 0; i <= DAI_INTEL_SSP_MAX_FREQ_INDEX; i++) { |
| if (ft[i].freq % mclk_rate == 0) { |
| clk_index = i; |
| break; |
| } |
| } |
| |
| if (clk_index < 0) { |
| LOG_ERR("%s MCLK %d, no valid source", __func__, mclk_rate); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| mp->mclk_source_clock = clk_index; |
| |
| mdivc = sys_read32(dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| /* enable MCLK divider */ |
| mdivc |= MN_MDIVCTRL_M_DIV_ENABLE(mclk_id); |
| |
| /* clear source mclk clock - bits 17-16 */ |
| mdivc &= ~MCDSS(MN_SOURCE_CLKS_MASK); |
| |
| /* select source clock */ |
| mdivc |= MCDSS(fs[clk_index]); |
| |
| sys_write32(mdivc, dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| mp->mclk_sources_ref[mclk_id]++; |
| out: |
| |
| return ret; |
| } |
| |
| /** |
| * \brief Checks if requested MCLK can be achieved with current source. |
| * \param[in] mclk_rate main clock frequency. |
| * \return 0 on success, error code otherwise. |
| */ |
| static int dai_ssp_check_current_mclk_source(struct dai_intel_ssp *dp, uint16_t mclk_id, |
| uint32_t mclk_rate) |
| { |
| struct dai_intel_ssp_freq_table *ft = dai_get_ftable(dp); |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| uint32_t mdivc; |
| int ret = 0; |
| |
| LOG_INF("%s MCLK %d, source = %d", __func__, mclk_rate, mp->mclk_source_clock); |
| |
| if (ft[mp->mclk_source_clock].freq % mclk_rate != 0) { |
| LOG_ERR("%s MCLK %d, no valid configuration for already selected source = %d", |
| __func__, mclk_rate, mp->mclk_source_clock); |
| ret = -EINVAL; |
| } |
| |
| /* if the mclk is already used, can't change its divider, just increase ref count */ |
| if (mp->mclk_sources_ref[mclk_id] > 0) { |
| if (mp->mclk_rate[mclk_id] != mclk_rate) { |
| LOG_ERR("%s Can't set MCLK %d to %d, it is already configured to %d", |
| __func__, mclk_id, mclk_rate, mp->mclk_rate[mclk_id]); |
| return -EINVAL; |
| } |
| |
| mp->mclk_sources_ref[mclk_id]++; |
| } else { |
| mdivc = sys_read32(dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| /* enable MCLK divider */ |
| mdivc |= MN_MDIVCTRL_M_DIV_ENABLE(mclk_id); |
| sys_write32(mdivc, dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| mp->mclk_sources_ref[mclk_id]++; |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * \brief Sets MCLK divider to given value. |
| * \param[in] mclk_id ID of MCLK. |
| * \param[in] mdivr_val divider value. |
| * \return 0 on success, error code otherwise. |
| */ |
| static int dai_ssp_set_mclk_divider(struct dai_intel_ssp *dp, uint16_t mclk_id, uint32_t mdivr_val) |
| { |
| uint32_t mdivr; |
| |
| LOG_INF("%s mclk_id %d mdivr_val %d", __func__, mclk_id, mdivr_val); |
| switch (mdivr_val) { |
| case 1: |
| mdivr = 0x00000fff; /* bypass divider for MCLK */ |
| break; |
| case 2 ... 8: |
| mdivr = mdivr_val - 2; /* 1/n */ |
| break; |
| default: |
| LOG_ERR("%s invalid mdivr_val %d", __func__, mdivr_val); |
| return -EINVAL; |
| } |
| |
| sys_write32(mdivr, dai_mn_base(dp) + MN_MDIVR(mclk_id)); |
| |
| return 0; |
| } |
| |
| static int dai_ssp_mn_set_mclk(struct dai_intel_ssp *dp, uint16_t mclk_id, uint32_t mclk_rate) |
| { |
| struct dai_intel_ssp_freq_table *ft = dai_get_ftable(dp); |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| k_spinlock_key_t key; |
| int ret = 0; |
| |
| if (mclk_id >= DAI_INTEL_SSP_NUM_MCLK) { |
| LOG_ERR("%s mclk ID (%d) >= %d", __func__, mclk_id, DAI_INTEL_SSP_NUM_MCLK); |
| return -EINVAL; |
| } |
| |
| key = k_spin_lock(&mp->lock); |
| |
| if (dai_ssp_is_mclk_source_in_use(dp)) { |
| ret = dai_ssp_check_current_mclk_source(dp, mclk_id, mclk_rate); |
| } else { |
| ret = dai_ssp_setup_initial_mclk_source(dp, mclk_id, mclk_rate); |
| } |
| |
| if (ret < 0) { |
| goto out; |
| } |
| |
| LOG_INF("%s mclk_rate %d, mclk_source_clock %d", __func__, |
| mclk_rate, mp->mclk_source_clock); |
| |
| ret = dai_ssp_set_mclk_divider(dp, mclk_id, ft[mp->mclk_source_clock].freq / mclk_rate); |
| if (!ret) { |
| mp->mclk_rate[mclk_id] = mclk_rate; |
| } |
| |
| out: |
| k_spin_unlock(&mp->lock, key); |
| |
| return ret; |
| } |
| |
| static int dai_ssp_mn_set_mclk_blob(struct dai_intel_ssp *dp, uint32_t mdivc, uint32_t mdivr) |
| { |
| sys_write32(mdivc, dai_mn_base(dp) + MN_MDIVCTRL); |
| sys_write32(mdivr, dai_mn_base(dp) + MN_MDIVR(0)); |
| |
| return 0; |
| } |
| |
| static void dai_ssp_mn_release_mclk(struct dai_intel_ssp *dp, uint32_t mclk_id) |
| { |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| k_spinlock_key_t key; |
| uint32_t mdivc; |
| |
| key = k_spin_lock(&mp->lock); |
| |
| mp->mclk_sources_ref[mclk_id]--; |
| |
| /* disable MCLK divider if nobody use it */ |
| if (!mp->mclk_sources_ref[mclk_id]) { |
| mdivc = sys_read32(dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| mdivc &= ~MN_MDIVCTRL_M_DIV_ENABLE(mclk_id); |
| sys_write32(mdivc, dai_mn_base(dp) + MN_MDIVCTRL); |
| } |
| |
| /* release the clock source if all mclks are released */ |
| if (!dai_ssp_is_mclk_source_in_use(dp)) { |
| mdivc = sys_read32(dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| /* clear source mclk clock - bits 17-16 */ |
| mdivc &= ~MCDSS(MN_SOURCE_CLKS_MASK); |
| |
| sys_write32(mdivc, dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| mp->mclk_source_clock = 0; |
| } |
| k_spin_unlock(&mp->lock, key); |
| } |
| |
| #if CONFIG_INTEL_MN |
| /** |
| * \brief Finds valid M/(N * SCR) values for given frequencies. |
| * \param[in] freq SSP clock frequency. |
| * \param[in] bclk Bit clock frequency. |
| * \param[out] out_scr_div SCR divisor. |
| * \param[out] out_m M value of M/N divider. |
| * \param[out] out_n N value of M/N divider. |
| * \return true if found suitable values, false otherwise. |
| */ |
| static bool dai_ssp_find_mn(uint32_t freq, uint32_t bclk, uint32_t *out_scr_div, uint32_t *out_m, |
| uint32_t *out_n) |
| { |
| uint32_t m, n, mn_div; |
| uint32_t scr_div = freq / bclk; |
| |
| LOG_INF("%s for freq %d bclk %d", __func__, freq, bclk); |
| /* check if just SCR is enough */ |
| if (freq % bclk == 0 && scr_div < (SSCR0_SCR_MASK >> 8) + 1) { |
| *out_scr_div = scr_div; |
| *out_m = 1; |
| *out_n = 1; |
| |
| return true; |
| } |
| |
| /* M/(N * scr_div) has to be less than 1/2 */ |
| if ((bclk * 2) >= freq) { |
| return false; |
| } |
| |
| /* odd SCR gives lower duty cycle */ |
| if (scr_div > 1 && scr_div % 2 != 0) { |
| --scr_div; |
| } |
| |
| /* clamp to valid SCR range */ |
| scr_div = MIN(scr_div, (SSCR0_SCR_MASK >> 8) + 1); |
| |
| /* find highest even divisor */ |
| while (scr_div > 1 && freq % scr_div != 0) { |
| scr_div -= 2; |
| } |
| |
| /* compute M/N with smallest dividend and divisor */ |
| mn_div = dai_ssp_gcd(bclk, freq / scr_div); |
| |
| m = bclk / mn_div; |
| n = freq / scr_div / mn_div; |
| |
| /* M/N values can be up to 24 bits */ |
| if (n & (~0xffffff)) { |
| return false; |
| } |
| |
| *out_scr_div = scr_div; |
| *out_m = m; |
| *out_n = n; |
| |
| LOG_INF("%s m %d n %d", __func__, m, n); |
| return true; |
| } |
| |
| /** |
| * \brief Finds index of clock valid for given BCLK rate. |
| * Clock that can use just SCR is preferred. |
| * M/N other than 1/1 is used only if there are no other possibilities. |
| * \param[in] bclk Bit clock frequency. |
| * \param[out] scr_div SCR divisor. |
| * \param[out] m M value of M/N divider. |
| * \param[out] n N value of M/N divider. |
| * \return index of suitable clock if could find it, -EINVAL otherwise. |
| */ |
| static int dai_ssp_find_bclk_source(struct dai_intel_ssp *dp, uint32_t bclk, uint32_t *scr_div, |
| uint32_t *m, uint32_t *n) |
| { |
| struct dai_intel_ssp_freq_table *ft = dai_get_ftable(dp); |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| int i; |
| |
| /* check if we can use MCLK source clock */ |
| if (dai_ssp_is_mclk_source_in_use(dp)) { |
| if (dai_ssp_find_mn(ft[mp->mclk_source_clock].freq, bclk, scr_div, m, n)) { |
| return mp->mclk_source_clock; |
| } |
| |
| LOG_WRN("%s BCLK %d warning: cannot use MCLK source %d", |
| __func__, bclk, ft[mp->mclk_source_clock].freq); |
| } |
| |
| /* searching the smallest possible bclk source */ |
| for (i = 0; i <= DAI_INTEL_SSP_MAX_FREQ_INDEX; i++) |
| if (ft[i].freq % bclk == 0) { |
| *scr_div = ft[i].freq / bclk; |
| return i; |
| } |
| |
| /* check if we can get target BCLK with M/N */ |
| for (i = 0; i <= DAI_INTEL_SSP_MAX_FREQ_INDEX; i++) { |
| if (dai_ssp_find_mn(ft[i].freq, bclk, scr_div, m, n)) { |
| return i; |
| } |
| } |
| |
| return -EINVAL; |
| } |
| |
| /** |
| * \brief Finds index of SSP clock with the given clock source encoded index. |
| * \return the index in ssp_freq if could find it, -EINVAL otherwise. |
| */ |
| static int dai_ssp_find_clk_ssp_index(struct dai_intel_ssp *dp, uint32_t src_enc) |
| { |
| uint32_t *fs = dai_get_fsources(dp); |
| int i; |
| |
| /* searching for the encode value matched bclk source */ |
| for (i = 0; i <= DAI_INTEL_SSP_MAX_FREQ_INDEX; i++) { |
| if (fs[i] == src_enc) { |
| return i; |
| } |
| } |
| |
| return -EINVAL; |
| } |
| |
| /** |
| * \brief Checks if given clock is used as source for any BCLK. |
| * \param[in] clk_src Bit clock source. |
| * \return true if any port use given clock source, false otherwise. |
| */ |
| static bool dai_ssp_is_bclk_source_in_use(struct dai_intel_ssp *dp, enum bclk_source clk_src) |
| { |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| bool ret = false; |
| int i; |
| |
| for (i = 0; i < ARRAY_SIZE(mp->bclk_sources); i++) { |
| if (mp->bclk_sources[i] == clk_src) { |
| ret = true; |
| break; |
| } |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * \brief Configures M/N source clock for BCLK. |
| * All ports that use M/N share the same source, so it should be changed |
| * only if there are no other ports using M/N already. |
| * \param[in] bclk Bit clock frequency. |
| * \param[out] scr_div SCR divisor. |
| * \param[out] m M value of M/N divider. |
| * \param[out] n N value of M/N divider. |
| * \return 0 on success, error code otherwise. |
| */ |
| static int dai_ssp_setup_initial_bclk_mn_source(struct dai_intel_ssp *dp, uint32_t bclk, |
| uint32_t *scr_div, uint32_t *m, uint32_t *n) |
| { |
| uint32_t *fs = dai_get_fsources(dp); |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| uint32_t mdivc; |
| int clk_index = dai_ssp_find_bclk_source(dp, bclk, scr_div, m, n); |
| |
| if (clk_index < 0) { |
| LOG_ERR("%s BCLK %d, no valid source", __func__, bclk); |
| return -EINVAL; |
| } |
| |
| mp->bclk_source_mn_clock = clk_index; |
| |
| mdivc = sys_read32(dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| /* clear source bclk clock - 21-20 bits */ |
| mdivc &= ~MNDSS(MN_SOURCE_CLKS_MASK); |
| |
| /* select source clock */ |
| mdivc |= MNDSS(fs[clk_index]); |
| |
| sys_write32(mdivc, dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| return 0; |
| } |
| |
| /** |
| * \brief Reset M/N source clock for BCLK. |
| * If no port is using bclk, reset to use SSP_CLOCK_XTAL_OSCILLATOR |
| * as the default clock source. |
| */ |
| static void dai_ssp_reset_bclk_mn_source(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| uint32_t mdivc; |
| int clk_index = dai_ssp_find_clk_ssp_index(dp, DAI_INTEL_SSP_CLOCK_XTAL_OSCILLATOR); |
| |
| if (clk_index < 0) { |
| LOG_ERR("%s BCLK reset failed, no SSP_CLOCK_XTAL_OSCILLATOR source!", |
| __func__); |
| return; |
| } |
| |
| mdivc = sys_read32(dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| /* reset to use XTAL Oscillator */ |
| mdivc &= ~MNDSS(MN_SOURCE_CLKS_MASK); |
| mdivc |= MNDSS(DAI_INTEL_SSP_CLOCK_XTAL_OSCILLATOR); |
| |
| sys_write32(mdivc, dai_mn_base(dp) + MN_MDIVCTRL); |
| |
| mp->bclk_source_mn_clock = clk_index; |
| } |
| |
| /** |
| * \brief Finds valid M/(N * SCR) values for source clock that is already locked |
| * because other ports use it. |
| * \param[in] bclk Bit clock frequency. |
| * \param[out] scr_div SCR divisor. |
| * \param[out] m M value of M/N divider. |
| * \param[out] n N value of M/N divider. |
| * \return 0 on success, error code otherwise. |
| */ |
| static int dai_ssp_setup_current_bclk_mn_source(struct dai_intel_ssp *dp, uint32_t bclk, |
| uint32_t *scr_div, uint32_t *m, uint32_t *n) |
| { |
| struct dai_intel_ssp_freq_table *ft = dai_get_ftable(dp); |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| int ret = 0; |
| |
| /* source for M/N is already set, no need to do it */ |
| if (dai_ssp_find_mn(ft[mp->bclk_source_mn_clock].freq, bclk, scr_div, m, n)) { |
| goto out; |
| } |
| |
| LOG_ERR("%s BCLK %d, no valid configuration for already selected source = %d", |
| __func__, bclk, mp->bclk_source_mn_clock); |
| ret = -EINVAL; |
| |
| out: |
| |
| return ret; |
| } |
| |
| static bool dai_ssp_check_bclk_xtal_source(uint32_t bclk, bool mn_in_use, |
| uint32_t *scr_div) |
| { |
| /* since cAVS 2.0 bypassing XTAL (ECS=0) is not supported */ |
| return false; |
| } |
| |
| static int dai_ssp_mn_set_bclk(struct dai_intel_ssp *dp, uint32_t dai_index, uint32_t bclk_rate, |
| uint32_t *out_scr_div, bool *out_need_ecs) |
| { |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| k_spinlock_key_t key; |
| uint32_t m = 1; |
| uint32_t n = 1; |
| int ret = 0; |
| bool mn_in_use; |
| |
| key = k_spin_lock(&mp->lock); |
| |
| mp->bclk_sources[dai_index] = MN_BCLK_SOURCE_NONE; |
| |
| mn_in_use = dai_ssp_is_bclk_source_in_use(dp, MN_BCLK_SOURCE_MN); |
| |
| if (dai_ssp_check_bclk_xtal_source(bclk_rate, mn_in_use, out_scr_div)) { |
| mp->bclk_sources[dai_index] = MN_BCLK_SOURCE_XTAL; |
| *out_need_ecs = false; |
| goto out; |
| } |
| |
| *out_need_ecs = true; |
| |
| if (mn_in_use) { |
| ret = dai_ssp_setup_current_bclk_mn_source(dp, bclk_rate, out_scr_div, &m, &n); |
| } else { |
| ret = dai_ssp_setup_initial_bclk_mn_source(dp, bclk_rate, out_scr_div, &m, &n); |
| } |
| |
| if (ret >= 0) { |
| mp->bclk_sources[dai_index] = MN_BCLK_SOURCE_MN; |
| |
| LOG_INF("%s bclk_rate %d, *out_scr_div %d, m %d, n %d", |
| __func__, bclk_rate, *out_scr_div, m, n); |
| |
| sys_write32(m, dai_mn_base(dp) + MN_MDIV_M_VAL(dai_index)); |
| sys_write32(n, dai_mn_base(dp) + MN_MDIV_N_VAL(dai_index)); |
| } |
| |
| out: |
| |
| k_spin_unlock(&mp->lock, key); |
| |
| return ret; |
| } |
| |
| static void dai_ssp_mn_release_bclk(struct dai_intel_ssp *dp, uint32_t dai_index) |
| { |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| k_spinlock_key_t key; |
| bool mn_in_use; |
| |
| key = k_spin_lock(&mp->lock); |
| mp->bclk_sources[dai_index] = MN_BCLK_SOURCE_NONE; |
| |
| mn_in_use = dai_ssp_is_bclk_source_in_use(dp, MN_BCLK_SOURCE_MN); |
| /* release the M/N clock source if not used */ |
| if (!mn_in_use) { |
| dai_ssp_reset_bclk_mn_source(dp); |
| } |
| |
| k_spin_unlock(&mp->lock, key); |
| } |
| |
| static void dai_ssp_mn_reset_bclk_divider(struct dai_intel_ssp *dp, uint32_t dai_index) |
| { |
| struct dai_intel_ssp_mn *mp = dai_get_mn(dp); |
| k_spinlock_key_t key; |
| |
| key = k_spin_lock(&mp->lock); |
| |
| sys_write32(1, dai_mn_base(dp) + MN_MDIV_M_VAL(dai_index)); |
| sys_write32(1, dai_mn_base(dp) + MN_MDIV_N_VAL(dai_index)); |
| |
| k_spin_unlock(&mp->lock, key); |
| } |
| #endif |
| |
| static int dai_ssp_poll_for_register_delay(uint32_t reg, uint32_t mask, |
| uint32_t val, uint64_t us) |
| { |
| if (!WAIT_FOR((sys_read32(reg) & mask) == val, us, k_busy_wait(1))) { |
| LOG_ERR("%s poll timeout reg %u mask %u val %u us %u", |
| __func__, reg, mask, val, (uint32_t)us); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static inline void dai_ssp_pm_runtime_dis_ssp_clk_gating(struct dai_intel_ssp *dp, uint32_t index) |
| { |
| #if CONFIG_DAI_SSP_CLK_FORCE_DYNAMIC_CLOCK_GATING |
| uint32_t shim_reg; |
| |
| shim_reg = sys_read32(dai_shim_base(dp) + SHIM_CLKCTL) | |
| (index < CONFIG_DAI_INTEL_SSP_NUM_BASE ? |
| SHIM_CLKCTL_I2SFDCGB(index) : |
| SHIM_CLKCTL_I2SEFDCGB(index - |
| CONFIG_DAI_INTEL_SSP_NUM_BASE)); |
| |
| sys_write32(shim_reg, dai_shim_base(dp) + SHIM_CLKCTL); |
| |
| LOG_INF("%s index %d CLKCTL %08x", __func__, index, shim_reg); |
| #endif |
| } |
| |
| static inline void dai_ssp_pm_runtime_en_ssp_clk_gating(struct dai_intel_ssp *dp, uint32_t index) |
| { |
| #if CONFIG_DAI_SSP_CLK_FORCE_DYNAMIC_CLOCK_GATING |
| uint32_t shim_reg; |
| |
| shim_reg = sys_read32(dai_shim_base(dp) + SHIM_CLKCTL) & |
| ~(index < CONFIG_DAI_INTEL_SSP_NUM_BASE ? |
| SHIM_CLKCTL_I2SFDCGB(index) : |
| SHIM_CLKCTL_I2SEFDCGB(index - |
| CONFIG_DAI_INTEL_SSP_NUM_BASE)); |
| |
| sys_write32(shim_reg, dai_shim_base(dp) + SHIM_CLKCTL); |
| |
| LOG_INF("%s index %d CLKCTL %08x", __func__, index, shim_reg); |
| #endif |
| } |
| |
| static void dai_ssp_pm_runtime_en_ssp_power(struct dai_intel_ssp *dp, uint32_t index) |
| { |
| #if CONFIG_DAI_SSP_HAS_POWER_CONTROL |
| int ret; |
| |
| LOG_INF("%s en_ssp_power index %d", __func__, index); |
| #if CONFIG_SOC_INTEL_ACE15_MTPM || CONFIG_SOC_SERIES_INTEL_ADSP_CAVS |
| sys_write32(sys_read32(dai_ip_base(dp) + I2SLCTL_OFFSET) | I2SLCTL_SPA(index), |
| dai_ip_base(dp) + I2SLCTL_OFFSET); |
| |
| /* Check if powered on. */ |
| ret = dai_ssp_poll_for_register_delay(dai_ip_base(dp) + I2SLCTL_OFFSET, |
| I2SLCTL_CPA(index), 0, |
| DAI_INTEL_SSP_MAX_SEND_TIME_PER_SAMPLE); |
| #elif CONFIG_SOC_INTEL_ACE20_LNL |
| sys_write32(sys_read32(dai_hdamlssp_base(dp) + I2SLCTL_OFFSET) | |
| I2SLCTL_SPA(index), |
| dai_hdamlssp_base(dp) + I2SLCTL_OFFSET); |
| /* Check if powered on. */ |
| ret = dai_ssp_poll_for_register_delay(dai_hdamlssp_base(dp) + I2SLCTL_OFFSET, |
| I2SLCTL_CPA(index), 0, |
| DAI_INTEL_SSP_MAX_SEND_TIME_PER_SAMPLE); |
| #else |
| #error need to define SOC |
| #endif |
| if (ret) { |
| LOG_WRN("%s warning: timeout", __func__); |
| } |
| |
| LOG_INF("%s I2SLCTL", __func__); |
| #else |
| ARG_UNUSED(dp); |
| ARG_UNUSED(index); |
| #endif /* CONFIG_DAI_SSP_HAS_POWER_CONTROL */ |
| } |
| |
| static void dai_ssp_pm_runtime_dis_ssp_power(struct dai_intel_ssp *dp, uint32_t index) |
| { |
| #if CONFIG_DAI_SSP_HAS_POWER_CONTROL |
| int ret; |
| |
| LOG_INF("%s index %d", __func__, index); |
| #if CONFIG_SOC_INTEL_ACE15_MTPM || CONFIG_SOC_SERIES_INTEL_ADSP_CAVS |
| sys_write32(sys_read32(dai_ip_base(dp) + I2SLCTL_OFFSET) & (~I2SLCTL_SPA(index)), |
| dai_ip_base(dp) + I2SLCTL_OFFSET); |
| |
| /* Check if powered off. */ |
| ret = dai_ssp_poll_for_register_delay(dai_ip_base(dp) + I2SLCTL_OFFSET, |
| I2SLCTL_CPA(index), I2SLCTL_CPA(index), |
| DAI_INTEL_SSP_MAX_SEND_TIME_PER_SAMPLE); |
| #elif CONFIG_SOC_INTEL_ACE20_LNL |
| sys_write32(sys_read32(dai_hdamlssp_base(dp) + I2SLCTL_OFFSET) & (~I2SLCTL_SPA(index)), |
| dai_hdamlssp_base(dp) + I2SLCTL_OFFSET); |
| |
| /* Check if powered off. */ |
| ret = dai_ssp_poll_for_register_delay(dai_hdamlssp_base(dp) + I2SLCTL_OFFSET, |
| I2SLCTL_CPA(index), I2SLCTL_CPA(index), |
| DAI_INTEL_SSP_MAX_SEND_TIME_PER_SAMPLE); |
| #else |
| #error need to define SOC |
| #endif |
| if (ret) { |
| LOG_WRN("%s warning: timeout", __func__); |
| } |
| |
| LOG_INF("%s I2SLCTL", __func__); |
| #else |
| ARG_UNUSED(dp); |
| ARG_UNUSED(index); |
| #endif /* CONFIG_DAI_SSP_HAS_POWER_CONTROL */ |
| } |
| |
| static void dai_ssp_program_channel_map(struct dai_intel_ssp *dp, |
| const struct dai_config *cfg, uint32_t index) |
| { |
| #ifdef CONFIG_SOC_INTEL_ACE20_LNL |
| uint16_t pcmsycm = cfg->link_config; |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| |
| /* Set upper slot number from configuration */ |
| pcmsycm = pcmsycm | (ssp->params.tdm_slots - 1) << 4; |
| |
| if (DAI_INTEL_SSP_IS_BIT_SET(pcmsycm, 15)) { |
| uint32_t reg_add = dai_ip_base(dp) + 0x1000 * index + PCMS0CM_OFFSET; |
| /* Program HDA output stream parameters */ |
| sys_write16((pcmsycm & 0xffff), reg_add); |
| } else { |
| uint32_t reg_add = dai_ip_base(dp) + 0x1000 * index + PCMS1CM_OFFSET; |
| /* Program HDA input stream parameters */ |
| sys_write16((pcmsycm & 0xffff), reg_add); |
| } |
| #else |
| ARG_UNUSED(dp); |
| ARG_UNUSED(cfg); |
| ARG_UNUSED(index); |
| #endif /* CONFIG_SOC_INTEL_ACE20_LNL */ |
| } |
| |
| /* empty SSP transmit FIFO */ |
| static void dai_ssp_empty_tx_fifo(struct dai_intel_ssp *dp) |
| { |
| int ret; |
| uint32_t sssr; |
| |
| /* |
| * SSSR_TNF is cleared when TX FIFO is empty or full, |
| * so wait for set TNF then for TFL zero - order matter. |
| */ |
| ret = dai_ssp_poll_for_register_delay(dai_base(dp) + SSSR, SSSR_TNF, SSSR_TNF, |
| DAI_INTEL_SSP_MAX_SEND_TIME_PER_SAMPLE); |
| ret |= dai_ssp_poll_for_register_delay(dai_base(dp) + SSCR3, SSCR3_TFL_MASK, 0, |
| DAI_INTEL_SSP_MAX_SEND_TIME_PER_SAMPLE * |
| (DAI_INTEL_SSP_FIFO_DEPTH - 1) / 2); |
| |
| if (ret) { |
| LOG_WRN("%s warning: timeout", __func__); |
| } |
| |
| sssr = sys_read32(dai_base(dp) + SSSR); |
| |
| /* clear interrupt */ |
| if (sssr & SSSR_TUR) { |
| sys_write32(sssr, dai_base(dp) + SSSR); |
| } |
| } |
| |
| /* empty SSP receive FIFO */ |
| static void dai_ssp_empty_rx_fifo(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| uint32_t retry = DAI_INTEL_SSP_RX_FLUSH_RETRY_MAX; |
| uint32_t entries; |
| uint32_t i; |
| |
| /* |
| * To make sure all the RX FIFO entries are read out for the flushing, |
| * we need to wait a minimal SSP port delay after entries are all read, |
| * and then re-check to see if there is any subsequent entries written |
| * to the FIFO. This will help to make sure there is no sample mismatched |
| * issue for the next run with the SSP RX. |
| */ |
| while ((sys_read32(dai_base(dp) + SSSR) & SSSR_RNE) && retry--) { |
| entries = SSCR3_RFL_VAL(sys_read32(dai_base(dp) + SSCR3)); |
| LOG_DBG("%s before flushing, entries %d", __func__, entries); |
| for (i = 0; i < entries + 1; i++) { |
| /* read to try empty fifo */ |
| sys_read32(dai_base(dp) + SSDR); |
| } |
| |
| /* wait to get valid fifo status and re-check */ |
| k_busy_wait(ssp->params.fsync_rate ? 1000000 / ssp->params.fsync_rate : 0); |
| entries = SSCR3_RFL_VAL(sys_read32(dai_base(dp) + SSCR3)); |
| LOG_DBG("%s after flushing, entries %d", __func__, entries); |
| } |
| |
| /* clear interrupt */ |
| dai_ssp_update_bits(dp, SSSR, SSSR_ROR, SSSR_ROR); |
| } |
| |
| static int dai_ssp_mclk_prepare_enable(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| int ret; |
| |
| if (ssp->clk_active & SSP_CLK_MCLK_ACTIVE) { |
| return 0; |
| } |
| |
| /* MCLK config */ |
| ret = dai_ssp_mn_set_mclk(dp, ssp->params.mclk_id, ssp->params.mclk_rate); |
| if (ret < 0) { |
| LOG_ERR("%s invalid mclk_rate = %d for mclk_id = %d", __func__, |
| ssp->params.mclk_rate, ssp->params.mclk_id); |
| } else { |
| ssp->clk_active |= SSP_CLK_MCLK_ACTIVE; |
| } |
| |
| return ret; |
| } |
| |
| static void dai_ssp_mclk_disable_unprepare(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| |
| if (!(ssp->clk_active & SSP_CLK_MCLK_ACTIVE)) { |
| return; |
| } |
| |
| dai_ssp_mn_release_mclk(dp, ssp->params.mclk_id); |
| |
| ssp->clk_active &= ~SSP_CLK_MCLK_ACTIVE; |
| } |
| |
| static int dai_ssp_bclk_prepare_enable(struct dai_intel_ssp *dp) |
| { |
| #if !(CONFIG_INTEL_MN) |
| struct dai_intel_ssp_freq_table *ft = dai_get_ftable(dp); |
| #endif |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| struct dai_config *config = &ssp->config; |
| uint32_t sscr0; |
| uint32_t mdiv; |
| bool need_ecs = false; |
| int ret = 0; |
| |
| if (ssp->clk_active & SSP_CLK_BCLK_ACTIVE) { |
| return 0; |
| } |
| |
| sscr0 = sys_read32(dai_base(dp) + SSCR0); |
| |
| #if CONFIG_INTEL_MN |
| /* BCLK config */ |
| ret = dai_ssp_mn_set_bclk(dp, config->dai_index, ssp->params.bclk_rate, |
| &mdiv, &need_ecs); |
| if (ret < 0) { |
| LOG_ERR("%s invalid bclk_rate = %d for dai_index = %d", __func__, |
| ssp->params.bclk_rate, config->dai_index); |
| goto out; |
| } |
| #else |
| if (ft[DAI_INTEL_SSP_DEFAULT_IDX].freq % ssp->params.bclk_rate != 0) { |
| LOG_ERR("%s invalid bclk_rate = %d for dai_index = %d", __func__, |
| ssp->params.bclk_rate, config->dai_index); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| mdiv = ft[DAI_INTEL_SSP_DEFAULT_IDX].freq / ssp->params.bclk_rate; |
| #endif |
| |
| if (need_ecs) { |
| sscr0 |= SSCR0_ECS; |
| } |
| |
| /* clock divisor is SCR + 1 */ |
| mdiv -= 1; |
| |
| /* divisor must be within SCR range */ |
| if (mdiv > (SSCR0_SCR_MASK >> 8)) { |
| LOG_ERR("%s divisor %d is not within SCR range", __func__, mdiv); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* set the SCR divisor */ |
| sscr0 &= ~SSCR0_SCR_MASK; |
| sscr0 |= SSCR0_SCR(mdiv); |
| |
| sys_write32(sscr0, dai_base(dp) + SSCR0); |
| |
| LOG_INF("%s sscr0 = 0x%08x", __func__, sscr0); |
| out: |
| if (!ret) { |
| ssp->clk_active |= SSP_CLK_BCLK_ACTIVE; |
| } |
| |
| return ret; |
| } |
| |
| static void dai_ssp_bclk_disable_unprepare(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| |
| if (!(ssp->clk_active & SSP_CLK_BCLK_ACTIVE)) { |
| return; |
| } |
| #if CONFIG_INTEL_MN |
| dai_ssp_mn_release_bclk(dp, dp->index); |
| #endif |
| ssp->clk_active &= ~SSP_CLK_BCLK_ACTIVE; |
| } |
| |
| static void dai_ssp_log_ssp_data(struct dai_intel_ssp *dp) |
| { |
| LOG_INF("%s dai index: %u", __func__, dp->index); |
| LOG_INF("%s plat_data base: %u", __func__, dp->plat_data.base); |
| LOG_INF("%s plat_data irq: %u", __func__, dp->plat_data.irq); |
| LOG_INF("%s plat_data fifo playback offset: %u", __func__, |
| dp->plat_data.fifo[DAI_DIR_PLAYBACK].offset); |
| LOG_INF("%s plat_data fifo playback handshake: %u", __func__, |
| dp->plat_data.fifo[DAI_DIR_PLAYBACK].handshake); |
| LOG_INF("%s plat_data fifo capture offset: %u", __func__, |
| dp->plat_data.fifo[DAI_DIR_CAPTURE].offset); |
| LOG_INF("%s plat_data fifo capture handshake: %u", __func__, |
| dp->plat_data.fifo[DAI_DIR_CAPTURE].handshake); |
| } |
| |
| /* Digital Audio interface formatting */ |
| static int dai_ssp_set_config_tplg(struct dai_intel_ssp *dp, const struct dai_config *config, |
| const void *bespoke_cfg) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| struct dai_intel_ssp_freq_table *ft = dai_get_ftable(dp); |
| uint32_t sscr0; |
| uint32_t sscr1; |
| uint32_t sscr2; |
| uint32_t sscr3; |
| uint32_t sspsp; |
| uint32_t sspsp2; |
| uint32_t sstsa; |
| uint32_t ssrsa; |
| uint32_t ssto; |
| uint32_t ssioc; |
| uint32_t bdiv; |
| uint32_t data_size; |
| uint32_t frame_end_padding; |
| uint32_t slot_end_padding; |
| uint32_t frame_len = 0; |
| uint32_t bdiv_min; |
| uint32_t tft; |
| uint32_t rft; |
| uint32_t active_tx_slots = 2; |
| uint32_t active_rx_slots = 2; |
| uint32_t sample_width = 2; |
| |
| bool inverted_bclk = false; |
| bool inverted_frame = false; |
| bool cfs = false; |
| bool start_delay = false; |
| k_spinlock_key_t key; |
| int ret = 0; |
| |
| dai_ssp_log_ssp_data(dp); |
| |
| key = k_spin_lock(&dp->lock); |
| |
| /* ignore config if SSP is already configured */ |
| if (ssp->state[DAI_DIR_PLAYBACK] > DAI_STATE_READY || |
| ssp->state[DAI_DIR_CAPTURE] > DAI_STATE_READY) { |
| if (!memcmp(&ssp->params, bespoke_cfg, sizeof(struct dai_intel_ipc3_ssp_params))) { |
| LOG_INF("%s Already configured. Ignore config", __func__); |
| goto clk; |
| } |
| |
| if (ssp->clk_active & (SSP_CLK_MCLK_ACTIVE | SSP_CLK_BCLK_ACTIVE)) { |
| LOG_WRN("%s SSP active, cannot change config", __func__); |
| goto clk; |
| } |
| |
| /* safe to proceed and change HW config */ |
| } |
| |
| LOG_INF("%s", __func__); |
| |
| /* reset SSP settings */ |
| /* sscr0 dynamic settings are DSS, EDSS, SCR, FRDC, ECS */ |
| /* |
| * FIXME: MOD, ACS, NCS are not set, |
| * no support for network mode for now |
| */ |
| sscr0 = SSCR0_PSP | SSCR0_RIM | SSCR0_TIM; |
| |
| /* sscr1 dynamic settings are SFRMDIR, SCLKDIR, SCFR, RSRE, TSRE */ |
| sscr1 = SSCR1_TTE | SSCR1_TTELP | SSCR1_TRAIL; |
| |
| /* sscr2 dynamic setting is LJDFD */ |
| sscr2 = SSCR2_SDFD | SSCR2_TURM1; |
| |
| /* sscr3 dynamic settings are TFT, RFT */ |
| sscr3 = 0; |
| |
| /* sspsp dynamic settings are SCMODE, SFRMP, DMYSTRT, SFRMWDTH */ |
| sspsp = 0; |
| |
| ssp->config = *config; |
| memcpy(&ssp->params, bespoke_cfg, sizeof(struct dai_intel_ipc3_ssp_params)); |
| |
| /* sspsp2 no dynamic setting */ |
| sspsp2 = 0x0; |
| |
| /* ssioc dynamic setting is SFCR */ |
| ssioc = SSIOC_SCOE; |
| |
| /* ssto no dynamic setting */ |
| ssto = 0x0; |
| |
| /* sstsa dynamic setting is TTSA, default 2 slots */ |
| sstsa = SSTSA_SSTSA(ssp->params.tx_slots); |
| |
| /* ssrsa dynamic setting is RTSA, default 2 slots */ |
| ssrsa = SSRSA_SSRSA(ssp->params.rx_slots); |
| |
| switch (config->format & DAI_INTEL_IPC3_SSP_FMT_CLOCK_PROVIDER_MASK) { |
| case DAI_INTEL_IPC3_SSP_FMT_CBP_CFP: |
| sscr1 |= SSCR1_SCLKDIR | SSCR1_SFRMDIR; |
| break; |
| case DAI_INTEL_IPC3_SSP_FMT_CBC_CFC: |
| sscr1 |= SSCR1_SCFR; |
| cfs = true; |
| break; |
| case DAI_INTEL_IPC3_SSP_FMT_CBP_CFC: |
| sscr1 |= SSCR1_SCLKDIR; |
| /* FIXME: this mode has not been tested */ |
| |
| cfs = true; |
| break; |
| case DAI_INTEL_IPC3_SSP_FMT_CBC_CFP: |
| sscr1 |= SSCR1_SCFR | SSCR1_SFRMDIR; |
| /* FIXME: this mode has not been tested */ |
| break; |
| default: |
| LOG_ERR("%s format & PROVIDER_MASK EINVAL", __func__); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* clock signal polarity */ |
| switch (config->format & DAI_INTEL_IPC3_SSP_FMT_INV_MASK) { |
| case DAI_INTEL_IPC3_SSP_FMT_NB_NF: |
| break; |
| case DAI_INTEL_IPC3_SSP_FMT_NB_IF: |
| inverted_frame = true; /* handled later with format */ |
| break; |
| case DAI_INTEL_IPC3_SSP_FMT_IB_IF: |
| inverted_bclk = true; /* handled later with bclk idle */ |
| inverted_frame = true; /* handled later with format */ |
| break; |
| case DAI_INTEL_IPC3_SSP_FMT_IB_NF: |
| inverted_bclk = true; /* handled later with bclk idle */ |
| break; |
| default: |
| LOG_ERR("%s format & INV_MASK EINVAL", __func__); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* supporting bclk idle state */ |
| if (ssp->params.clks_control & DAI_INTEL_IPC3_SSP_CLKCTRL_BCLK_IDLE_HIGH) { |
| /* bclk idle state high */ |
| sspsp |= SSPSP_SCMODE((inverted_bclk ^ 0x3) & 0x3); |
| } else { |
| /* bclk idle state low */ |
| sspsp |= SSPSP_SCMODE(inverted_bclk); |
| } |
| |
| sscr0 |= SSCR0_MOD | SSCR0_ACS; |
| |
| /* Additional hardware settings */ |
| |
| /* Receiver Time-out Interrupt Disabled/Enabled */ |
| sscr1 |= (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_TINTE) ? |
| SSCR1_TINTE : 0; |
| |
| /* Peripheral Trailing Byte Interrupts Disable/Enable */ |
| sscr1 |= (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_PINTE) ? |
| SSCR1_PINTE : 0; |
| |
| /* Enable/disable internal loopback. Output of transmit serial |
| * shifter connected to input of receive serial shifter, internally. |
| */ |
| sscr1 |= (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_LBM) ? |
| SSCR1_LBM : 0; |
| |
| if (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_LBM) { |
| LOG_INF("%s going for loopback!", __func__); |
| } else { |
| LOG_INF("%s no loopback!", __func__); |
| } |
| |
| /* Transmit data are driven at the same/opposite clock edge specified |
| * in SSPSP.SCMODE[1:0] |
| */ |
| sscr2 |= (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_SMTATF) ? |
| SSCR2_SMTATF : 0; |
| |
| /* Receive data are sampled at the same/opposite clock edge specified |
| * in SSPSP.SCMODE[1:0] |
| */ |
| sscr2 |= (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_MMRATF) ? |
| SSCR2_MMRATF : 0; |
| |
| /* Enable/disable the fix for PSP consumer mode TXD wait for frame |
| * de-assertion before starting the second channel |
| */ |
| sscr2 |= (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_PSPSTWFDFD) ? |
| SSCR2_PSPSTWFDFD : 0; |
| |
| /* Enable/disable the fix for PSP provider mode FSRT with dummy stop & |
| * frame end padding capability |
| */ |
| sscr2 |= (ssp->params.quirks & DAI_INTEL_IPC3_SSP_QUIRK_PSPSRWFDFD) ? |
| SSCR2_PSPSRWFDFD : 0; |
| |
| if (!ssp->params.mclk_rate || |
| ssp->params.mclk_rate > ft[DAI_INTEL_SSP_MAX_FREQ_INDEX].freq) { |
| LOG_ERR("%s invalid MCLK = %d Hz (valid < %d)", __func__, |
| ssp->params.mclk_rate, |
| ft[DAI_INTEL_SSP_MAX_FREQ_INDEX].freq); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| if (!ssp->params.bclk_rate || ssp->params.bclk_rate > ssp->params.mclk_rate) { |
| LOG_ERR("%s BCLK %d Hz = 0 or > MCLK %d Hz", __func__, |
| ssp->params.bclk_rate, ssp->params.mclk_rate); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* calc frame width based on BCLK and rate - must be divisable */ |
| if (ssp->params.bclk_rate % ssp->params.fsync_rate) { |
| LOG_ERR("%s BCLK %d is not divisable by rate %d", __func__, |
| ssp->params.bclk_rate, ssp->params.fsync_rate); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* must be enough BCLKs for data */ |
| bdiv = ssp->params.bclk_rate / ssp->params.fsync_rate; |
| if (bdiv < ssp->params.tdm_slot_width * ssp->params.tdm_slots) { |
| LOG_ERR("%s not enough BCLKs need %d", __func__, |
| ssp->params.tdm_slot_width * |
| ssp->params.tdm_slots); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* tdm_slot_width must be <= 38 for SSP */ |
| if (ssp->params.tdm_slot_width > 38) { |
| LOG_ERR("%s tdm_slot_width %d > 38", __func__, |
| ssp->params.tdm_slot_width); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| bdiv_min = ssp->params.tdm_slots * |
| (ssp->params.tdm_per_slot_padding_flag ? |
| ssp->params.tdm_slot_width : ssp->params.sample_valid_bits); |
| if (bdiv < bdiv_min) { |
| LOG_ERR("%s bdiv(%d) < bdiv_min(%d)", __func__, |
| bdiv, bdiv_min); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| frame_end_padding = bdiv - bdiv_min; |
| if (frame_end_padding > SSPSP2_FEP_MASK) { |
| LOG_ERR("%s frame_end_padding too big: %u", __func__, |
| frame_end_padding); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* format */ |
| switch (config->format & DAI_INTEL_IPC3_SSP_FMT_FORMAT_MASK) { |
| case DAI_INTEL_IPC3_SSP_FMT_I2S: |
| |
| start_delay = true; |
| |
| sscr0 |= SSCR0_FRDC(ssp->params.tdm_slots); |
| |
| if (bdiv % 2) { |
| LOG_ERR("%s bdiv %d is not divisible by 2", __func__, bdiv); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* set asserted frame length to half frame length */ |
| frame_len = bdiv / 2; |
| |
| /* |
| * handle frame polarity, I2S default is falling/active low, |
| * non-inverted(inverted_frame=0) -- active low(SFRMP=0), |
| * inverted(inverted_frame=1) -- rising/active high(SFRMP=1), |
| * so, we should set SFRMP to inverted_frame. |
| */ |
| sspsp |= SSPSP_SFRMP(inverted_frame); |
| |
| /* |
| * for I2S/LEFT_J, the padding has to happen at the end |
| * of each slot |
| */ |
| if (frame_end_padding % 2) { |
| LOG_ERR("%s frame_end_padding %d is not divisible by 2", |
| __func__, frame_end_padding); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| slot_end_padding = frame_end_padding / 2; |
| |
| if (slot_end_padding > DAI_INTEL_IPC3_SSP_SLOT_PADDING_MAX) { |
| /* too big padding */ |
| LOG_ERR("%s slot_end_padding > %d", __func__, |
| DAI_INTEL_IPC3_SSP_SLOT_PADDING_MAX); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| sspsp |= SSPSP_DMYSTOP(slot_end_padding); |
| slot_end_padding >>= SSPSP_DMYSTOP_BITS; |
| sspsp |= SSPSP_EDMYSTOP(slot_end_padding); |
| |
| break; |
| |
| case DAI_INTEL_IPC3_SSP_FMT_LEFT_J: |
| |
| /* default start_delay value is set to false */ |
| |
| sscr0 |= SSCR0_FRDC(ssp->params.tdm_slots); |
| |
| /* LJDFD enable */ |
| sscr2 &= ~SSCR2_LJDFD; |
| |
| if (bdiv % 2) { |
| LOG_ERR("%s bdiv %d is not divisible by 2", __func__, bdiv); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| /* set asserted frame length to half frame length */ |
| frame_len = bdiv / 2; |
| |
| /* |
| * handle frame polarity, LEFT_J default is rising/active high, |
| * non-inverted(inverted_frame=0) -- active high(SFRMP=1), |
| * inverted(inverted_frame=1) -- falling/active low(SFRMP=0), |
| * so, we should set SFRMP to !inverted_frame. |
| */ |
| sspsp |= SSPSP_SFRMP(!inverted_frame); |
| |
| /* |
| * for I2S/LEFT_J, the padding has to happen at the end |
| * of each slot |
| */ |
| if (frame_end_padding % 2) { |
| LOG_ERR("%s frame_end_padding %d is not divisible by 2", |
| __func__, frame_end_padding); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| slot_end_padding = frame_end_padding / 2; |
| |
| if (slot_end_padding > 15) { |
| /* can't handle padding over 15 bits */ |
| LOG_ERR("%s slot_end_padding %d > 15 bits", __func__, |
| slot_end_padding); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| sspsp |= SSPSP_DMYSTOP(slot_end_padding); |
| slot_end_padding >>= SSPSP_DMYSTOP_BITS; |
| sspsp |= SSPSP_EDMYSTOP(slot_end_padding); |
| |
| break; |
| case DAI_INTEL_IPC3_SSP_FMT_DSP_A: |
| |
| start_delay = true; |
| |
| /* fallthrough */ |
| |
| case DAI_INTEL_IPC3_SSP_FMT_DSP_B: |
| |
| /* default start_delay value is set to false */ |
| |
| sscr0 |= SSCR0_MOD | SSCR0_FRDC(ssp->params.tdm_slots); |
| |
| /* set asserted frame length */ |
| frame_len = 1; /* default */ |
| |
| if (cfs && ssp->params.frame_pulse_width > 0 && |
| ssp->params.frame_pulse_width <= |
| DAI_INTEL_IPC3_SSP_FRAME_PULSE_WIDTH_MAX) { |
| frame_len = ssp->params.frame_pulse_width; |
| } |
| |
| /* frame_pulse_width must less or equal 38 */ |
| if (ssp->params.frame_pulse_width > |
| DAI_INTEL_IPC3_SSP_FRAME_PULSE_WIDTH_MAX) { |
| LOG_ERR("%s frame_pulse_width > %d", __func__, |
| DAI_INTEL_IPC3_SSP_FRAME_PULSE_WIDTH_MAX); |
| ret = -EINVAL; |
| goto out; |
| } |
| /* |
| * handle frame polarity, DSP_B default is rising/active high, |
| * non-inverted(inverted_frame=0) -- active high(SFRMP=1), |
| * inverted(inverted_frame=1) -- falling/active low(SFRMP=0), |
| * so, we should set SFRMP to !inverted_frame. |
| */ |
| sspsp |= SSPSP_SFRMP(!inverted_frame); |
| |
| active_tx_slots = POPCOUNT(ssp->params.tx_slots); |
| active_rx_slots = POPCOUNT(ssp->params.rx_slots); |
| |
| /* |
| * handle TDM mode, TDM mode has padding at the end of |
| * each slot. The amount of padding is equal to result of |
| * subtracting slot width and valid bits per slot. |
| */ |
| if (ssp->params.tdm_per_slot_padding_flag) { |
| frame_end_padding = bdiv - ssp->params.tdm_slots * |
| ssp->params.tdm_slot_width; |
| |
| slot_end_padding = ssp->params.tdm_slot_width - |
| ssp->params.sample_valid_bits; |
| |
| if (slot_end_padding > |
| DAI_INTEL_IPC3_SSP_SLOT_PADDING_MAX) { |
| LOG_ERR("%s slot_end_padding > %d", __func__, |
| DAI_INTEL_IPC3_SSP_SLOT_PADDING_MAX); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| sspsp |= SSPSP_DMYSTOP(slot_end_padding); |
| slot_end_padding >>= SSPSP_DMYSTOP_BITS; |
| sspsp |= SSPSP_EDMYSTOP(slot_end_padding); |
| } |
| |
| sspsp2 |= (frame_end_padding & SSPSP2_FEP_MASK); |
| |
| break; |
| default: |
| LOG_ERR("%s invalid format 0x%04x", __func__, |
| config->format); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| if (start_delay) { |
| sspsp |= SSPSP_FSRT; |
| } |
| |
| sspsp |= SSPSP_SFRMWDTH(frame_len); |
| |
| data_size = ssp->params.sample_valid_bits; |
| |
| if (data_size > 16) { |
| sscr0 |= (SSCR0_EDSS | SSCR0_DSIZE(data_size - 16)); |
| } else { |
| sscr0 |= SSCR0_DSIZE(data_size); |
| } |
| |
| /* setting TFT and RFT */ |
| switch (ssp->params.sample_valid_bits) { |
| case 16: |
| /* use 2 bytes for each slot */ |
| sample_width = 2; |
| break; |
| case 24: |
| case 32: |
| /* use 4 bytes for each slot */ |
| sample_width = 4; |
| break; |
| default: |
| LOG_ERR("%s sample_valid_bits %d", __func__, |
| ssp->params.sample_valid_bits); |
| ret = -EINVAL; |
| goto out; |
| } |
| |
| tft = MIN(DAI_INTEL_SSP_FIFO_DEPTH - DAI_INTEL_SSP_FIFO_WATERMARK, |
| sample_width * active_tx_slots); |
| rft = MIN(DAI_INTEL_SSP_FIFO_DEPTH - DAI_INTEL_SSP_FIFO_WATERMARK, |
| sample_width * active_rx_slots); |
| |
| sscr3 |= SSCR3_TX(tft) | SSCR3_RX(rft); |
| |
| sys_write32(sscr0, dai_base(dp) + SSCR0); |
| sys_write32(sscr1, dai_base(dp) + SSCR1); |
| sys_write32(sscr2, dai_base(dp) + SSCR2); |
| sys_write32(sscr3, dai_base(dp) + SSCR3); |
| sys_write32(sspsp, dai_base(dp) + SSPSP); |
| sys_write32(sspsp2, dai_base(dp) + SSPSP2); |
| sys_write32(ssioc, dai_base(dp) + SSIOC); |
| sys_write32(ssto, dai_base(dp) + SSTO); |
| sys_write32(sstsa, dai_base(dp) + SSTSA); |
| sys_write32(ssrsa, dai_base(dp) + SSRSA); |
| |
| LOG_INF("%s sscr0 = 0x%08x, sscr1 = 0x%08x, ssto = 0x%08x, sspsp = 0x%0x", |
| __func__, sscr0, sscr1, ssto, sspsp); |
| LOG_INF("%s sscr2 = 0x%08x, sspsp2 = 0x%08x, sscr3 = 0x%08x, ssioc = 0x%08x", |
| __func__, sscr2, sspsp2, sscr3, ssioc); |
| LOG_INF("%s ssrsa = 0x%08x, sstsa = 0x%08x", |
| __func__, ssrsa, sstsa); |
| |
| ssp->state[DAI_DIR_PLAYBACK] = DAI_STATE_PRE_RUNNING; |
| ssp->state[DAI_DIR_CAPTURE] = DAI_STATE_PRE_RUNNING; |
| |
| clk: |
| switch (config->options & DAI_INTEL_IPC3_SSP_CONFIG_FLAGS_CMD_MASK) { |
| case DAI_INTEL_IPC3_SSP_CONFIG_FLAGS_HW_PARAMS: |
| if (ssp->params.clks_control & DAI_INTEL_IPC3_SSP_CLKCTRL_MCLK_ES) { |
| ret = dai_ssp_mclk_prepare_enable(dp); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| ssp->clk_active |= SSP_CLK_MCLK_ES_REQ; |
| |
| LOG_INF("%s hw_params stage: enabled MCLK clocks for SSP%d...", |
| __func__, dp->index); |
| } |
| |
| if (ssp->params.clks_control & DAI_INTEL_IPC3_SSP_CLKCTRL_BCLK_ES) { |
| bool enable_sse = false; |
| |
| if (!(ssp->clk_active & SSP_CLK_BCLK_ACTIVE)) { |
| enable_sse = true; |
| } |
| |
| ret = dai_ssp_bclk_prepare_enable(dp); |
| if (ret < 0) { |
| goto out; |
| } |
| |
| ssp->clk_active |= SSP_CLK_BCLK_ES_REQ; |
| |
| if (enable_sse) { |
| |
| /* enable TRSE/RSRE before SSE */ |
| dai_ssp_update_bits(dp, SSCR1, |
| SSCR1_TSRE | SSCR1_RSRE, |
| SSCR1_TSRE | SSCR1_RSRE); |
| |
| /* enable port */ |
| dai_ssp_update_bits(dp, SSCR0, SSCR0_SSE, SSCR0_SSE); |
| |
| LOG_INF("%s SSE set for SSP%d", __func__, dp->index); |
| } |
| |
| LOG_INF("%s hw_params stage: enabled BCLK clocks for SSP%d...", |
| __func__, dp->index); |
| } |
| break; |
| case DAI_INTEL_IPC3_SSP_CONFIG_FLAGS_HW_FREE: |
| /* disable SSP port if no users */ |
| if (ssp->state[DAI_DIR_CAPTURE] != DAI_STATE_PRE_RUNNING || |
| ssp->state[DAI_DIR_PLAYBACK] != DAI_STATE_PRE_RUNNING) { |
| LOG_INF("%s hw_free stage: ignore since SSP%d still in use", |
| __func__, dp->index); |
| break; |
| } |
| |
| if (ssp->params.clks_control & DAI_INTEL_IPC3_SSP_CLKCTRL_BCLK_ES) { |
| LOG_INF("%s hw_free stage: releasing BCLK clocks for SSP%d...", |
| __func__, dp->index); |
| if (ssp->clk_active & SSP_CLK_BCLK_ACTIVE) { |
| /* clear TRSE/RSRE before SSE */ |
| dai_ssp_update_bits(dp, SSCR1, |
| SSCR1_TSRE | SSCR1_RSRE, |
| 0); |
| |
| dai_ssp_update_bits(dp, SSCR0, SSCR0_SSE, 0); |
| LOG_INF("%s SSE clear for SSP%d", __func__, dp->index); |
| } |
| dai_ssp_bclk_disable_unprepare(dp); |
| ssp->clk_active &= ~SSP_CLK_BCLK_ES_REQ; |
| } |
| if (ssp->params.clks_control & DAI_INTEL_IPC3_SSP_CLKCTRL_MCLK_ES) { |
| LOG_INF("%s hw_free stage: releasing MCLK clocks for SSP%d...", |
| __func__, dp->index); |
| dai_ssp_mclk_disable_unprepare(dp); |
| ssp->clk_active &= ~SSP_CLK_MCLK_ES_REQ; |
| } |
| break; |
| default: |
| break; |
| } |
| out: |
| |
| k_spin_unlock(&dp->lock, key); |
| |
| return ret; |
| } |
| |
| static int dai_ssp_check_aux_data(struct ssp_intel_aux_tlv *aux_tlv, int aux_len, int parsed_len) |
| { |
| struct ssp_intel_sync_ctl *sync; |
| int size, size_left; |
| |
| switch (aux_tlv->type) { |
| case SSP_MN_DIVIDER_CONTROLS: |
| size = sizeof(struct ssp_intel_mn_ctl); |
| break; |
| case SSP_DMA_CLK_CONTROLS: |
| size = sizeof(struct ssp_intel_clk_ctl); |
| break; |
| case SSP_DMA_TRANSMISSION_START: |
| case SSP_DMA_TRANSMISSION_STOP: |
| size = sizeof(struct ssp_intel_tr_ctl); |
| case SSP_DMA_ALWAYS_RUNNING_MODE: |
| size = sizeof(struct ssp_intel_run_ctl); |
| break; |
| case SSP_DMA_SYNC_DATA: |
| size = sizeof(struct ssp_intel_sync_ctl); |
| sync = (struct ssp_intel_sync_ctl *)&aux_tlv->val; |
| size += sync->count * sizeof(struct ssp_intel_node_ctl); |
| break; |
| case SSP_DMA_CLK_CONTROLS_EXT: |
| size = sizeof(struct ssp_intel_ext_ctl); |
| break; |
| case SSP_LINK_CLK_SOURCE: |
| #ifdef CONFIG_SOC_SERIES_INTEL_ACE |
| size = sizeof(struct ssp_intel_link_ctl); |
| break; |
| #else |
| return 0; |
| #endif |
| default: |
| LOG_ERR("%s undefined aux data type %u", __func__, aux_tlv->type); |
| return -EINVAL; |
| } |
| |
| /* check for malformed struct, size greater than aux_data or described in tlv */ |
| size_left = aux_len - parsed_len - sizeof(struct ssp_intel_aux_tlv); |
| if (size > size_left || size != aux_tlv->size) { |
| LOG_ERR("%s malformed struct, size %d, size_left %d, tlv_size %d", __func__, size, |
| size_left, aux_tlv->size); |
| return -EINVAL; |
| } |
| |
| return 0; |
| } |
| |
| static int dai_ssp_parse_aux_data(struct dai_intel_ssp *dp, const void *spec_config) |
| { |
| const struct dai_intel_ipc4_ssp_configuration_blob_ver_1_5 *blob = spec_config; |
| int aux_tlv_size = sizeof(struct ssp_intel_aux_tlv); |
| int hop, i, j, cfg_len, pre_aux_len, aux_len; |
| struct ssp_intel_aux_tlv *aux_tlv; |
| struct ssp_intel_mn_ctl *mn; |
| struct ssp_intel_clk_ctl *clk; |
| struct ssp_intel_tr_ctl *tr; |
| struct ssp_intel_run_ctl *run; |
| struct ssp_intel_node_ctl *node; |
| struct ssp_intel_sync_ctl *sync; |
| struct ssp_intel_ext_ctl *ext; |
| #ifdef CONFIG_SOC_SERIES_INTEL_ACE |
| struct ssp_intel_link_ctl *link; |
| #endif |
| uint8_t *aux_ptr; |
| |
| cfg_len = blob->size; |
| pre_aux_len = sizeof(*blob) + blob->i2s_mclk_control.mdivrcnt * sizeof(uint32_t); |
| aux_len = cfg_len - pre_aux_len; |
| aux_ptr = (uint8_t *)blob + pre_aux_len; |
| |
| if (aux_len <= 0) |
| return 0; |
| |
| for (i = 0; i < aux_len; i += hop) { |
| aux_tlv = (struct ssp_intel_aux_tlv *)(aux_ptr); |
| if (dai_ssp_check_aux_data(aux_tlv, aux_len, i)) { |
| return -EINVAL; |
| } |
| switch (aux_tlv->type) { |
| case SSP_MN_DIVIDER_CONTROLS: |
| mn = (struct ssp_intel_mn_ctl *)&aux_tlv->val; |
| LOG_INF("%s mn div_m %u, div_n %u", __func__, mn->div_m, mn->div_n); |
| break; |
| case SSP_DMA_CLK_CONTROLS: |
| clk = (struct ssp_intel_clk_ctl *)&aux_tlv->val; |
| LOG_INF("%s clk start %u, stop %u", __func__, clk->start, clk->stop); |
| break; |
| case SSP_DMA_TRANSMISSION_START: |
| case SSP_DMA_TRANSMISSION_STOP: |
| tr = (struct ssp_intel_tr_ctl *)&aux_tlv->val; |
| LOG_INF("%s tr sampling frequency %u, bit_depth %u, channel_map %u,", |
| __func__, tr->sampling_frequency, tr->bit_depth, tr->channel_map); |
| LOG_INF("channel_config %u, interleaving_style %u, format %u", |
| tr->channel_config, tr->interleaving_style, tr->format); |
| break; |
| case SSP_DMA_ALWAYS_RUNNING_MODE: |
| run = (struct ssp_intel_run_ctl *)&aux_tlv->val; |
| LOG_INF("%s run enabled %u", __func__, run->enabled); |
| break; |
| case SSP_DMA_SYNC_DATA: |
| sync = (struct ssp_intel_sync_ctl *)&aux_tlv->val; |
| LOG_INF("%s sync sync_denominator %u, count %u", __func__, |
| sync->sync_denominator, sync->count); |
| node = (struct ssp_intel_node_ctl *)((uint8_t *)sync + |
| sizeof(struct ssp_intel_sync_ctl)); |
| for (j = 0; j < sync->count; j++) { |
| LOG_INF("%s node node_id %u, sampling_rate %u", __func__, |
| node->node_id, node->sampling_rate); |
| node++; |
| } |
| break; |
| case SSP_DMA_CLK_CONTROLS_EXT: |
| ext = (struct ssp_intel_ext_ctl *)&aux_tlv->val; |
| LOG_INF("%s ext ext_data %u", __func__, ext->ext_data); |
| break; |
| case SSP_LINK_CLK_SOURCE: |
| #ifdef CONFIG_SOC_SERIES_INTEL_ACE |
| link = (struct ssp_intel_link_ctl *)&aux_tlv->val; |
| |
| #if CONFIG_SOC_INTEL_ACE15_MTPM |
| sys_write32(sys_read32(dai_ip_base(dp) + I2SLCTL_OFFSET) | |
| I2CLCTL_MLCS(link->clock_source), dai_ip_base(dp) + |
| I2SLCTL_OFFSET); |
| #elif CONFIG_SOC_INTEL_ACE20_LNL |
| sys_write32(sys_read32(dai_i2svss_base(dp) + I2SLCTL_OFFSET) | |
| I2CLCTL_MLCS(link->clock_source), dai_i2svss_base(dp) + |
| I2SLCTL_OFFSET); |
| #endif |
| LOG_INF("%s link clock_source %u", __func__, link->clock_source); |
| #endif |
| break; |
| default: |
| LOG_ERR("%s undefined aux data type %u", __func__, aux_tlv->type); |
| return -EINVAL; |
| } |
| |
| hop = aux_tlv->size + aux_tlv_size; |
| aux_ptr += hop; |
| } |
| |
| return 0; |
| } |
| |
| static int dai_ssp_set_clock_control_ver_1_5(struct dai_intel_ssp *dp, |
| const struct dai_intel_ipc4_ssp_mclk_config_2 *cc) |
| { |
| /* currently we only support 1 divider */ |
| if (cc->mdivrcnt != 1) { |
| LOG_ERR("%s bad clock divider count %u", __func__, |
| cc->mdivrcnt); |
| return -EINVAL; |
| } |
| |
| /* ssp blob is set by pcm_hw_params for ipc4 stream, so enable |
| * mclk and bclk at this time. |
| */ |
| dai_ssp_mn_set_mclk_blob(dp, cc->mdivctlr, cc->mdivr[0]); |
| |
| return 0; |
| } |
| |
| static int dai_ssp_set_clock_control_ver_1(struct dai_intel_ssp *dp, |
| const struct dai_intel_ipc4_ssp_mclk_config *cc) |
| { |
| /* ssp blob is set by pcm_hw_params for ipc4 stream, so enable |
| * mclk and bclk at this time. |
| */ |
| dai_ssp_mn_set_mclk_blob(dp, cc->mdivc, cc->mdivr); |
| |
| return 0; |
| } |
| |
| static void dai_ssp_set_reg_config(struct dai_intel_ssp *dp, const struct dai_config *cfg, |
| const struct dai_intel_ipc4_ssp_config *regs) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| uint32_t ssc0, sstsa, ssrsa; |
| |
| ssc0 = regs->ssc0; |
| sstsa = regs->sstsa; |
| ssrsa = regs->ssrsa; |
| |
| sys_write32(ssc0, dai_base(dp) + SSCR0); |
| sys_write32(regs->ssc2 & ~SSCR2_SFRMEN, dai_base(dp) + SSCR2); /* hardware specific flow */ |
| sys_write32(regs->ssc1, dai_base(dp) + SSCR1); |
| sys_write32(regs->ssc2 | SSCR2_SFRMEN, dai_base(dp) + SSCR2); /* hardware specific flow */ |
| sys_write32(regs->ssc2, dai_base(dp) + SSCR2); |
| sys_write32(regs->ssc3, dai_base(dp) + SSCR3); |
| sys_write32(regs->sspsp, dai_base(dp) + SSPSP); |
| sys_write32(regs->sspsp2, dai_base(dp) + SSPSP2); |
| sys_write32(regs->ssioc, dai_base(dp) + SSIOC); |
| sys_write32(regs->sscto, dai_base(dp) + SSTO); |
| sys_write32(sstsa, dai_base(dp) + SSTSA); |
| sys_write32(ssrsa, dai_base(dp) + SSRSA); |
| |
| LOG_INF("%s sscr0 = 0x%08x, sscr1 = 0x%08x, ssto = 0x%08x, sspsp = 0x%0x", __func__, |
| ssc0, regs->ssc1, regs->sscto, regs->sspsp); |
| LOG_INF("%s sscr2 = 0x%08x, sspsp2 = 0x%08x, sscr3 = 0x%08x", __func__, |
| regs->ssc2, regs->sspsp2, regs->ssc3); |
| LOG_INF("%s ssioc = 0x%08x, ssrsa = 0x%08x, sstsa = 0x%08x", __func__, |
| regs->ssioc, ssrsa, sstsa); |
| |
| ssp->params.sample_valid_bits = SSCR0_DSIZE_GET(ssc0); |
| if (ssc0 & SSCR0_EDSS) { |
| ssp->params.sample_valid_bits += 16; |
| } |
| |
| ssp->params.tdm_slots = SSCR0_FRDC_GET(ssc0); |
| ssp->params.tx_slots = SSTSA_GET(sstsa); |
| ssp->params.rx_slots = SSRSA_GET(ssrsa); |
| ssp->params.fsync_rate = cfg->rate; |
| |
| ssp->state[DAI_DIR_PLAYBACK] = DAI_STATE_PRE_RUNNING; |
| ssp->state[DAI_DIR_CAPTURE] = DAI_STATE_PRE_RUNNING; |
| } |
| |
| static int dai_ssp_set_config_blob(struct dai_intel_ssp *dp, const struct dai_config *cfg, |
| const void *spec_config) |
| { |
| const struct dai_intel_ipc4_ssp_configuration_blob_ver_1_5 *blob15 = spec_config; |
| const struct dai_intel_ipc4_ssp_configuration_blob *blob = spec_config; |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| int err; |
| |
| /* set config only once for playback or capture */ |
| if (ssp->state[DAI_DIR_PLAYBACK] > DAI_STATE_READY || |
| ssp->state[DAI_DIR_CAPTURE] > DAI_STATE_READY) |
| return 0; |
| |
| if (blob15->version == SSP_BLOB_VER_1_5) { |
| err = dai_ssp_parse_aux_data(dp, spec_config); |
| if (err) |
| return err; |
| dai_ssp_set_reg_config(dp, cfg, &blob15->i2s_ssp_config); |
| err = dai_ssp_set_clock_control_ver_1_5(dp, &blob15->i2s_mclk_control); |
| if (err) |
| return err; |
| } else { |
| dai_ssp_set_reg_config(dp, cfg, &blob->i2s_driver_config.i2s_config); |
| err = dai_ssp_set_clock_control_ver_1(dp, &blob->i2s_driver_config.mclk_config); |
| if (err) |
| return err; |
| } |
| |
| ssp->clk_active |= SSP_CLK_MCLK_ES_REQ; |
| /* enable TRSE/RSRE before SSE */ |
| dai_ssp_update_bits(dp, SSCR1, SSCR1_TSRE | SSCR1_RSRE, SSCR1_TSRE | SSCR1_RSRE); |
| |
| /* enable port */ |
| dai_ssp_update_bits(dp, SSCR0, SSCR0_SSE, SSCR0_SSE); |
| ssp->clk_active |= SSP_CLK_BCLK_ES_REQ; |
| |
| return 0; |
| } |
| |
| /* |
| * Portion of the SSP configuration should be applied just before the |
| * SSP dai is activated, for either power saving or params runtime |
| * configurable flexibility. |
| */ |
| static int dai_ssp_pre_start(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| int ret = 0; |
| |
| LOG_INF("%s", __func__); |
| |
| /* |
| * We will test if mclk/bclk is configured in |
| * ssp_mclk/bclk_prepare_enable/disable functions |
| */ |
| if (!(ssp->clk_active & SSP_CLK_MCLK_ES_REQ)) { |
| /* MCLK config */ |
| ret = dai_ssp_mclk_prepare_enable(dp); |
| if (ret < 0) { |
| return ret; |
| } |
| } |
| |
| if (!(ssp->clk_active & SSP_CLK_BCLK_ES_REQ)) { |
| ret = dai_ssp_bclk_prepare_enable(dp); |
| } |
| |
| return ret; |
| } |
| |
| /* |
| * For power saving, we should do kinds of power release when the SSP |
| * dai is changed to inactive, though the runtime param configuration |
| * don't have to be reset. |
| */ |
| static void dai_ssp_post_stop(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| |
| /* release clocks if SSP is inactive */ |
| if (ssp->state[DAI_DIR_PLAYBACK] != DAI_STATE_RUNNING && |
| ssp->state[DAI_DIR_CAPTURE] != DAI_STATE_RUNNING) { |
| if (!(ssp->clk_active & SSP_CLK_BCLK_ES_REQ)) { |
| LOG_INF("%s releasing BCLK clocks for SSP%d...", |
| __func__, dp->index); |
| dai_ssp_bclk_disable_unprepare(dp); |
| } |
| if (!(ssp->clk_active & SSP_CLK_MCLK_ES_REQ)) { |
| LOG_INF("%s releasing MCLK clocks for SSP%d...", |
| __func__, dp->index); |
| dai_ssp_mclk_disable_unprepare(dp); |
| } |
| } |
| } |
| |
| static void dai_ssp_early_start(struct dai_intel_ssp *dp, int direction) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| k_spinlock_key_t key; |
| |
| key = k_spin_lock(&dp->lock); |
| |
| /* request mclk/bclk */ |
| dai_ssp_pre_start(dp); |
| |
| if (!(ssp->clk_active & SSP_CLK_BCLK_ES_REQ)) { |
| /* enable TRSE/RSRE before SSE */ |
| dai_ssp_update_bits(dp, SSCR1, |
| SSCR1_TSRE | SSCR1_RSRE, |
| SSCR1_TSRE | SSCR1_RSRE); |
| |
| /* enable port */ |
| dai_ssp_update_bits(dp, SSCR0, SSCR0_SSE, SSCR0_SSE); |
| LOG_INF("%s SSE set for SSP%d", __func__, dp->index); |
| } |
| |
| k_spin_unlock(&dp->lock, key); |
| } |
| |
| /* start the SSP for either playback or capture */ |
| static void dai_ssp_start(struct dai_intel_ssp *dp, int direction) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| k_spinlock_key_t key; |
| |
| key = k_spin_lock(&dp->lock); |
| |
| LOG_INF("%s", __func__); |
| |
| /* enable DMA */ |
| if (direction == DAI_DIR_PLAYBACK) { |
| dai_ssp_update_bits(dp, SSTSA, SSTSA_TXEN, SSTSA_TXEN); |
| } else { |
| dai_ssp_update_bits(dp, SSRSA, SSRSA_RXEN, SSRSA_RXEN); |
| } |
| |
| ssp->state[direction] = DAI_STATE_RUNNING; |
| |
| /* |
| * Wait to get valid fifo status in clock consumer mode. TODO it's |
| * uncertain which SSP clock consumer modes need the delay atm, but |
| * these can be added here when confirmed. |
| */ |
| switch (ssp->config.format & DAI_INTEL_IPC3_SSP_FMT_CLOCK_PROVIDER_MASK) { |
| case DAI_INTEL_IPC3_SSP_FMT_CBC_CFC: |
| break; |
| default: |
| /* delay for all SSP consumed clocks atm - see above */ |
| /* ssp_wait_delay(PLATFORM_SSP_DELAY); */ |
| k_busy_wait(DAI_INTEL_SSP_PLATFORM_DELAY_US); |
| break; |
| } |
| |
| k_spin_unlock(&dp->lock, key); |
| } |
| |
| /* stop the SSP for either playback or capture */ |
| static void dai_ssp_stop(struct dai_intel_ssp *dp, int direction) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| k_spinlock_key_t key; |
| |
| key = k_spin_lock(&dp->lock); |
| |
| /* |
| * Wait to get valid fifo status in clock consumer mode. TODO it's |
| * uncertain which SSP clock consumer modes need the delay atm, but |
| * these can be added here when confirmed. |
| */ |
| switch (ssp->config.format & DAI_INTEL_IPC3_SSP_FMT_CLOCK_PROVIDER_MASK) { |
| case DAI_INTEL_IPC3_SSP_FMT_CBC_CFC: |
| break; |
| default: |
| /* delay for all SSP consumed clocks atm - see above */ |
| k_busy_wait(DAI_INTEL_SSP_PLATFORM_DELAY_US); |
| break; |
| } |
| |
| /* stop Rx if neeed */ |
| if (direction == DAI_DIR_CAPTURE && |
| ssp->state[DAI_DIR_CAPTURE] != DAI_STATE_PRE_RUNNING) { |
| dai_ssp_update_bits(dp, SSRSA, SSRSA_RXEN, 0); |
| dai_ssp_empty_rx_fifo(dp); |
| ssp->state[DAI_DIR_CAPTURE] = DAI_STATE_PRE_RUNNING; |
| LOG_INF("%s RX stop", __func__); |
| } |
| |
| /* stop Tx if needed */ |
| if (direction == DAI_DIR_PLAYBACK && |
| ssp->state[DAI_DIR_PLAYBACK] != DAI_STATE_PRE_RUNNING) { |
| dai_ssp_empty_tx_fifo(dp); |
| dai_ssp_update_bits(dp, SSTSA, SSTSA_TXEN, 0); |
| ssp->state[DAI_DIR_PLAYBACK] = DAI_STATE_PRE_RUNNING; |
| LOG_INF("%sTX stop", __func__); |
| } |
| |
| /* disable SSP port if no users */ |
| if (ssp->state[DAI_DIR_CAPTURE] == DAI_STATE_PRE_RUNNING && |
| ssp->state[DAI_DIR_PLAYBACK] == DAI_STATE_PRE_RUNNING) { |
| bool clear_rse_bits = COND_CODE_1(CONFIG_INTEL_ADSP_CAVS, |
| (!(ssp->clk_active & SSP_CLK_BCLK_ES_REQ)), |
| (true)); |
| if (clear_rse_bits) { |
| /* clear TRSE/RSRE before SSE */ |
| dai_ssp_update_bits(dp, SSCR1, SSCR1_TSRE | SSCR1_RSRE, 0); |
| dai_ssp_update_bits(dp, SSCR0, SSCR0_SSE, 0); |
| LOG_INF("%s SSE clear SSP%d", __func__, dp->index); |
| } |
| } |
| |
| dai_ssp_post_stop(dp); |
| |
| k_spin_unlock(&dp->lock, key); |
| } |
| |
| static void dai_ssp_pause(struct dai_intel_ssp *dp, int direction) |
| { |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| |
| if (direction == DAI_DIR_CAPTURE) { |
| LOG_INF("%s RX", __func__); |
| } else { |
| LOG_INF("%s TX", __func__); |
| } |
| |
| ssp->state[direction] = DAI_STATE_PAUSED; |
| } |
| |
| static int dai_ssp_trigger(const struct device *dev, enum dai_dir dir, |
| enum dai_trigger_cmd cmd) |
| { |
| struct dai_intel_ssp *dp = (struct dai_intel_ssp *)dev->data; |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| int array_index = SSP_ARRAY_INDEX(dir); |
| |
| LOG_DBG("%s cmd %d", __func__, cmd); |
| |
| switch (cmd) { |
| case DAI_TRIGGER_START: |
| if (ssp->state[array_index] == DAI_STATE_PAUSED || |
| ssp->state[array_index] == DAI_STATE_PRE_RUNNING) { |
| dai_ssp_start(dp, array_index); |
| } |
| break; |
| case DAI_TRIGGER_STOP: |
| dai_ssp_stop(dp, array_index); |
| break; |
| case DAI_TRIGGER_PAUSE: |
| dai_ssp_pause(dp, array_index); |
| break; |
| case DAI_TRIGGER_PRE_START: |
| dai_ssp_early_start(dp, array_index); |
| break; |
| default: |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int dai_ssp_config_get(const struct device *dev, struct dai_config *cfg, enum dai_dir dir) |
| { |
| struct dai_config *params = (struct dai_config *)dev->config; |
| struct dai_intel_ssp *dp = (struct dai_intel_ssp *)dev->data; |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| |
| if (!cfg) { |
| return -EINVAL; |
| } |
| |
| if (!ssp) { |
| *cfg = *params; |
| return 0; |
| } |
| |
| params->rate = ssp->params.fsync_rate; |
| |
| if (dir == DAI_DIR_PLAYBACK) { |
| params->channels = POPCOUNT(ssp->params.tx_slots); |
| } else { |
| params->channels = POPCOUNT(ssp->params.rx_slots); |
| } |
| |
| params->word_size = ssp->params.sample_valid_bits; |
| |
| *cfg = *params; |
| |
| return 0; |
| } |
| |
| static int dai_ssp_config_set(const struct device *dev, const struct dai_config *cfg, |
| const void *bespoke_cfg) |
| { |
| struct dai_intel_ssp *dp = (struct dai_intel_ssp *)dev->data; |
| int ret; |
| |
| if (cfg->type == DAI_INTEL_SSP) { |
| ret = dai_ssp_set_config_tplg(dp, cfg, bespoke_cfg); |
| } else { |
| ret = dai_ssp_set_config_blob(dp, cfg, bespoke_cfg); |
| } |
| dai_ssp_program_channel_map(dp, cfg, dp->index); |
| return ret; |
| } |
| |
| static const struct dai_properties *dai_ssp_get_properties(const struct device *dev, |
| enum dai_dir dir, int stream_id) |
| { |
| struct dai_intel_ssp *dp = (struct dai_intel_ssp *)dev->data; |
| struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp); |
| struct dai_properties *prop = &ssp->props; |
| int array_index = SSP_ARRAY_INDEX(dir); |
| |
| prop->fifo_address = dp->plat_data.fifo[array_index].offset; |
| prop->dma_hs_id = dp->plat_data.fifo[array_index].handshake; |
| |
| if (ssp->clk_active & SSP_CLK_BCLK_ACTIVE) { |
| prop->reg_init_delay = 0; |
| } else { |
| prop->reg_init_delay = ssp->params.bclk_delay; |
| } |
| |
| LOG_INF("%s dai_index %u", __func__, dp->index); |
| LOG_INF("%s fifo %u", __func__, prop->fifo_address); |
| LOG_INF("%s handshake %u", __func__, prop->dma_hs_id); |
| LOG_INF("%s init delay %u", __func__, prop->reg_init_delay); |
| |
| return prop; |
| } |
| |
| static int dai_ssp_probe(struct dai_intel_ssp *dp) |
| { |
| struct dai_intel_ssp_pdata *ssp; |
| |
| if (dai_get_drvdata(dp)) { |
| return -EEXIST; /* already created */ |
| } |
| |
| /* allocate private data */ |
| ssp = k_calloc(1, sizeof(*ssp)); |
| if (!ssp) { |
| LOG_ERR("%s alloc failed", __func__); |
| return -ENOMEM; |
| } |
| dai_set_drvdata(dp, ssp); |
| |
| ssp->state[DAI_DIR_PLAYBACK] = DAI_STATE_READY; |
| ssp->state[DAI_DIR_CAPTURE] = DAI_STATE_READY; |
| |
| #if CONFIG_INTEL_MN |
| /* Reset M/N, power-gating functions need it */ |
| dai_ssp_mn_reset_bclk_divider(dp, dp->index); |
| #endif |
| |
| /* Enable SSP power */ |
| dai_ssp_pm_runtime_en_ssp_power(dp, dp->index); |
| |
| /* Disable dynamic clock gating before touching any register */ |
| dai_ssp_pm_runtime_dis_ssp_clk_gating(dp, dp->index); |
| |
| dai_ssp_empty_rx_fifo(dp); |
| |
| return 0; |
| } |
| |
| static int dai_ssp_remove(struct dai_intel_ssp *dp) |
| { |
| dai_ssp_pm_runtime_en_ssp_clk_gating(dp, dp->index); |
| |
| dai_ssp_mclk_disable_unprepare(dp); |
| dai_ssp_bclk_disable_unprepare(dp); |
| |
| /* Disable SSP power */ |
| dai_ssp_pm_runtime_dis_ssp_power(dp, dp->index); |
| |
| k_free(dai_get_drvdata(dp)); |
| dai_set_drvdata(dp, NULL); |
| |
| return 0; |
| } |
| |
| static int ssp_pm_action(const struct device *dev, enum pm_device_action action) |
| { |
| struct dai_intel_ssp *dp = (struct dai_intel_ssp *)dev->data; |
| switch (action) { |
| case PM_DEVICE_ACTION_SUSPEND: |
| dai_ssp_remove(dp); |
| break; |
| case PM_DEVICE_ACTION_RESUME: |
| dai_ssp_probe(dp); |
| break; |
| case PM_DEVICE_ACTION_TURN_OFF: |
| case PM_DEVICE_ACTION_TURN_ON: |
| /* All device pm is handled during resume and suspend */ |
| break; |
| default: |
| return -ENOTSUP; |
| } |
| |
| return 0; |
| } |
| |
| static int ssp_init(const struct device *dev) |
| { |
| if (pm_device_on_power_domain(dev)) { |
| pm_device_init_off(dev); |
| } else { |
| pm_device_init_suspended(dev); |
| } |
| |
| return pm_device_runtime_enable(dev); |
| } |
| |
| static struct dai_driver_api dai_intel_ssp_api_funcs = { |
| .probe = pm_device_runtime_get, |
| .remove = pm_device_runtime_put, |
| .config_set = dai_ssp_config_set, |
| .config_get = dai_ssp_config_get, |
| .trigger = dai_ssp_trigger, |
| .get_properties = dai_ssp_get_properties, |
| }; |
| |
| static struct dai_intel_ssp_freq_table ssp_freq_table[] = { |
| { DT_PROP(DT_NODELABEL(audioclk), clock_frequency), |
| DT_PROP(DT_NODELABEL(audioclk), clock_frequency) / 1000}, |
| { DT_PROP(DT_NODELABEL(sysclk), clock_frequency), |
| DT_PROP(DT_NODELABEL(sysclk), clock_frequency) / 1000}, |
| { DT_PROP(DT_NODELABEL(pllclk), clock_frequency), |
| DT_PROP(DT_NODELABEL(pllclk), clock_frequency) / 1000}, |
| }; |
| |
| static uint32_t ssp_freq_sources[] = { |
| DAI_INTEL_SSP_CLOCK_AUDIO_CARDINAL, |
| DAI_INTEL_SSP_CLOCK_XTAL_OSCILLATOR, |
| DAI_INTEL_SSP_CLOCK_PLL_FIXED, |
| }; |
| |
| static struct dai_intel_ssp_mn ssp_mn_divider = { |
| .base = DT_REG_ADDR_BY_IDX(DT_NODELABEL(ssp0), 1), |
| }; |
| |
| static const char irq_name_level5_z[] = "level5"; |
| |
| #define DAI_INTEL_SSP_DEVICE_INIT(n) \ |
| static struct dai_config dai_intel_ssp_config_##n = { \ |
| .type = DAI_INTEL_SSP, \ |
| .dai_index = n, \ |
| }; \ |
| static struct dai_intel_ssp dai_intel_ssp_data_##n = { \ |
| .index = n, \ |
| .plat_data = { \ |
| .base = DT_INST_REG_ADDR_BY_IDX(n, 0), \ |
| IF_ENABLED(DT_NODE_EXISTS(DT_NODELABEL(sspbase)), \ |
| (.ip_base = DT_REG_ADDR_BY_IDX(DT_NODELABEL(sspbase), 0),))\ |
| .shim_base = DT_REG_ADDR_BY_IDX(DT_NODELABEL(shim), 0), \ |
| IF_ENABLED(DT_NODE_EXISTS(DT_NODELABEL(hdamlssp)), \ |
| (.hdamlssp_base = DT_REG_ADDR(DT_NODELABEL(hdamlssp)),))\ |
| IF_ENABLED(DT_INST_PROP_HAS_IDX(n, i2svss, 0), \ |
| (.i2svss_base = DT_INST_PROP_BY_IDX(n, i2svss, 0),))\ |
| .irq = n, \ |
| .irq_name = irq_name_level5_z, \ |
| .fifo[DAI_DIR_PLAYBACK].offset = \ |
| DT_INST_REG_ADDR_BY_IDX(n, 0) + SSDR, \ |
| .fifo[DAI_DIR_PLAYBACK].handshake = \ |
| DT_INST_DMAS_CELL_BY_NAME(n, tx, channel), \ |
| .fifo[DAI_DIR_CAPTURE].offset = \ |
| DT_INST_REG_ADDR_BY_IDX(n, 0) + SSDR, \ |
| .fifo[DAI_DIR_CAPTURE].handshake = \ |
| DT_INST_DMAS_CELL_BY_NAME(n, rx, channel), \ |
| .mn_inst = &ssp_mn_divider, \ |
| .ftable = ssp_freq_table, \ |
| .fsources = ssp_freq_sources, \ |
| }, \ |
| }; \ |
| \ |
| PM_DEVICE_DT_INST_DEFINE(n, ssp_pm_action); \ |
| \ |
| DEVICE_DT_INST_DEFINE(n, \ |
| ssp_init, PM_DEVICE_DT_INST_GET(n), \ |
| &dai_intel_ssp_data_##n, \ |
| &dai_intel_ssp_config_##n, \ |
| POST_KERNEL, 32, \ |
| &dai_intel_ssp_api_funcs); |
| |
| DT_INST_FOREACH_STATUS_OKAY(DAI_INTEL_SSP_DEVICE_INIT) |