blob: 39d74e6f08bbb0a81ef46a99be296a3671ca2a67 [file] [log] [blame]
/*
* Copyright (c) 2022 Intel Corporation.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/spinlock.h>
#include <zephyr/devicetree.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_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_SOC_SERIES_INTEL_CAVS_V15
uint32_t shim_reg;
shim_reg = sys_read32(dai_shim_base(dp) + SHIM_CLKCTL) |
(index < DAI_INTEL_SSP_NUM_BASE ?
SHIM_CLKCTL_I2SFDCGB(index) :
SHIM_CLKCTL_I2SEFDCGB(index - 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_SOC_SERIES_INTEL_CAVS_V15
uint32_t shim_reg;
shim_reg = sys_read32(dai_shim_base(dp) + SHIM_CLKCTL) &
~(index < DAI_INTEL_SSP_NUM_BASE ?
SHIM_CLKCTL_I2SFDCGB(index) :
SHIM_CLKCTL_I2SEFDCGB(index - 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_SOC_SERIES_INTEL_CAVS_V25
int ret;
LOG_INF("%s en_ssp_power index %d", __func__, index);
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);
if (ret) {
LOG_WRN("%s warning: timeout", __func__);
}
LOG_INF("%s I2SLCTL", __func__);
#endif
}
static void dai_ssp_pm_runtime_dis_ssp_power(struct dai_intel_ssp *dp, uint32_t index)
{
#if CONFIG_SOC_SERIES_INTEL_CAVS_V25
int ret;
LOG_INF("%s index %d", __func__, index);
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);
if (ret) {
LOG_WRN("%s warning: timeout", __func__);
}
LOG_INF("%s I2SLCTL", __func__);
#endif
}
/* 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_set_config_blob(struct dai_intel_ssp *dp, const void *spec_config)
{
const struct dai_intel_ipc4_ssp_configuration_blob *blob = spec_config;
struct dai_intel_ssp_pdata *ssp = dai_get_drvdata(dp);
uint32_t ssc0, sstsa, ssrsa;
/* set config only once for playback or capture */
if (dp->sref > 1) {
return 0;
}
ssc0 = blob->i2s_driver_config.i2s_config.ssc0;
sstsa = blob->i2s_driver_config.i2s_config.sstsa;
ssrsa = blob->i2s_driver_config.i2s_config.ssrsa;
sys_write32(ssc0, dai_base(dp) + SSCR0);
sys_write32(blob->i2s_driver_config.i2s_config.ssc1, dai_base(dp) + SSCR1);
sys_write32(blob->i2s_driver_config.i2s_config.ssc2, dai_base(dp) + SSCR2);
sys_write32(blob->i2s_driver_config.i2s_config.ssc3, dai_base(dp) + SSCR3);
sys_write32(blob->i2s_driver_config.i2s_config.sspsp, dai_base(dp) + SSPSP);
sys_write32(blob->i2s_driver_config.i2s_config.sspsp2, dai_base(dp) + SSPSP2);
sys_write32(blob->i2s_driver_config.i2s_config.ssioc, dai_base(dp) + SSIOC);
sys_write32(blob->i2s_driver_config.i2s_config.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, blob->i2s_driver_config.i2s_config.ssc1,
blob->i2s_driver_config.i2s_config.sscto,
blob->i2s_driver_config.i2s_config.sspsp);
LOG_INF("%s sscr2 = 0x%08x, sspsp2 = 0x%08x, sscr3 = 0x%08x", __func__,
blob->i2s_driver_config.i2s_config.ssc2, blob->i2s_driver_config.i2s_config.sspsp2,
blob->i2s_driver_config.i2s_config.ssc3);
LOG_ERR("%s ssioc = 0x%08x, ssrsa = 0x%08x, sstsa = 0x%08x", __func__,
blob->i2s_driver_config.i2s_config.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 = 48000;
ssp->state[DAI_DIR_PLAYBACK] = DAI_STATE_PRE_RUNNING;
ssp->state[DAI_DIR_CAPTURE] = DAI_STATE_PRE_RUNNING;
/* 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, blob->i2s_driver_config.mclk_config.mdivc,
blob->i2s_driver_config.mclk_config.mdivr);
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) {
if (!(ssp->clk_active & SSP_CLK_BCLK_ES_REQ)) {
/* 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_INF("%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 const struct dai_config *dai_ssp_config_get(const struct device *dev, 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);
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;
return params;
}
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;
if (cfg->type == DAI_INTEL_SSP) {
return dai_ssp_set_config_tplg(dp, cfg, bespoke_cfg);
} else {
return dai_ssp_set_config_blob(dp, bespoke_cfg);
}
}
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 dai_ssp_probe_wrapper(const struct device *dev)
{
struct dai_intel_ssp *dp = (struct dai_intel_ssp *)dev->data;
k_spinlock_key_t key;
int ret = 0;
key = k_spin_lock(&dp->lock);
if (dp->sref == 0) {
ret = dai_ssp_probe(dp);
}
if (!ret) {
dp->sref++;
}
k_spin_unlock(&dp->lock, key);
return ret;
}
static int dai_ssp_remove_wrapper(const struct device *dev)
{
struct dai_intel_ssp *dp = (struct dai_intel_ssp *)dev->data;
k_spinlock_key_t key;
int ret = 0;
key = k_spin_lock(&dp->lock);
if (--dp->sref == 0) {
ret = dai_ssp_remove(dp);
}
k_spin_unlock(&dp->lock, key);
return ret;
}
static int ssp_init(const struct device *dev)
{
return 0;
}
static struct dai_driver_api dai_intel_ssp_api_funcs = {
.probe = dai_ssp_probe_wrapper,
.remove = dai_ssp_remove_wrapper,
.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; \
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), \
.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, \
}, \
}; \
\
DEVICE_DT_INST_DEFINE(n, \
ssp_init, NULL, \
&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)