mirror of
https://github.com/torvalds/linux.git
synced 2026-03-07 23:04:33 +01:00
In cs35l56_process_xu_onchip_speaker_id() the warning that the number of pulls != number of GPIOs should only be printed if pulls are defined. Pull settings are optional because there would normally be an external resistor providing the pull. The warning would still be true if pulls are not defined, but in that case is just log noise. While we're changing that block of code, also fix the indenting of the arguments to the dev_warn(). Signed-off-by: Richard Fitzgerald <rf@opensource.cirrus.com> Link: https://patch.msgid.link/20260226113511.1768838-1-rf@opensource.cirrus.com Signed-off-by: Mark Brown <broonie@kernel.org>
1999 lines
57 KiB
C
1999 lines
57 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
//
|
|
// Driver for Cirrus Logic CS35L56 smart amp
|
|
//
|
|
// Copyright (C) 2023 Cirrus Logic, Inc. and
|
|
// Cirrus Logic International Semiconductor Ltd.
|
|
|
|
#include <kunit/static_stub.h>
|
|
#include <kunit/visibility.h>
|
|
#include <linux/acpi.h>
|
|
#include <linux/array_size.h>
|
|
#include <linux/completion.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/device.h>
|
|
#include <linux/err.h>
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/interrupt.h>
|
|
#include <linux/math.h>
|
|
#include <linux/module.h>
|
|
#include <linux/pm.h>
|
|
#include <linux/pm_runtime.h>
|
|
#include <linux/property.h>
|
|
#include <linux/regmap.h>
|
|
#include <linux/regulator/consumer.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/soundwire/sdw.h>
|
|
#include <linux/types.h>
|
|
#include <linux/workqueue.h>
|
|
#include <sound/cs-amp-lib.h>
|
|
#include <sound/pcm.h>
|
|
#include <sound/pcm_params.h>
|
|
#include <sound/soc.h>
|
|
#include <sound/soc-dapm.h>
|
|
#include <sound/tlv.h>
|
|
|
|
#include "wm_adsp.h"
|
|
#include "cs35l56.h"
|
|
|
|
static int cs35l56_dsp_event(struct snd_soc_dapm_widget *w,
|
|
struct snd_kcontrol *kcontrol, int event);
|
|
|
|
static void cs35l56_wait_dsp_ready(struct cs35l56_private *cs35l56)
|
|
{
|
|
/* Wait for patching to complete */
|
|
flush_work(&cs35l56->dsp_work);
|
|
}
|
|
|
|
static int cs35l56_dspwait_get_volsw(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
|
|
cs35l56_wait_dsp_ready(cs35l56);
|
|
return snd_soc_get_volsw(kcontrol, ucontrol);
|
|
}
|
|
|
|
static int cs35l56_dspwait_put_volsw(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
|
|
cs35l56_wait_dsp_ready(cs35l56);
|
|
return snd_soc_put_volsw(kcontrol, ucontrol);
|
|
}
|
|
|
|
static DECLARE_TLV_DB_SCALE(vol_tlv, -10000, 25, 0);
|
|
|
|
static SOC_ENUM_SINGLE_DECL(cs35l56_cal_set_status_enum, SND_SOC_NOPM, 0,
|
|
cs35l56_cal_set_status_text);
|
|
|
|
static int cs35l56_cal_set_status_ctl_get(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
|
|
return cs35l56_cal_set_status_get(&cs35l56->base, ucontrol);
|
|
}
|
|
|
|
static const struct snd_kcontrol_new cs35l56_controls[] = {
|
|
SOC_SINGLE_EXT("Speaker Switch",
|
|
CS35L56_MAIN_RENDER_USER_MUTE, 0, 1, 1,
|
|
cs35l56_dspwait_get_volsw, cs35l56_dspwait_put_volsw),
|
|
SOC_SINGLE_S_EXT_TLV("Speaker Volume",
|
|
CS35L56_MAIN_RENDER_USER_VOLUME,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_SHIFT,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_MIN,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_MAX,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_SIGNBIT,
|
|
0,
|
|
cs35l56_dspwait_get_volsw,
|
|
cs35l56_dspwait_put_volsw,
|
|
vol_tlv),
|
|
SOC_SINGLE_EXT("Posture Number", CS35L56_MAIN_POSTURE_NUMBER,
|
|
0, 255, 0,
|
|
cs35l56_dspwait_get_volsw, cs35l56_dspwait_put_volsw),
|
|
SOC_ENUM_EXT_ACC("CAL_SET_STATUS", cs35l56_cal_set_status_enum,
|
|
cs35l56_cal_set_status_ctl_get, NULL,
|
|
SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE),
|
|
};
|
|
|
|
static const struct snd_kcontrol_new cs35l63_controls[] = {
|
|
SOC_SINGLE_EXT("Speaker Switch",
|
|
CS35L63_MAIN_RENDER_USER_MUTE, 0, 1, 1,
|
|
cs35l56_dspwait_get_volsw, cs35l56_dspwait_put_volsw),
|
|
SOC_SINGLE_S_EXT_TLV("Speaker Volume",
|
|
CS35L63_MAIN_RENDER_USER_VOLUME,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_SHIFT,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_MIN,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_MAX,
|
|
CS35L56_MAIN_RENDER_USER_VOLUME_SIGNBIT,
|
|
0,
|
|
cs35l56_dspwait_get_volsw,
|
|
cs35l56_dspwait_put_volsw,
|
|
vol_tlv),
|
|
SOC_SINGLE_EXT("Posture Number", CS35L63_MAIN_POSTURE_NUMBER,
|
|
0, 255, 0,
|
|
cs35l56_dspwait_get_volsw, cs35l56_dspwait_put_volsw),
|
|
SOC_ENUM_EXT_ACC("CAL_SET_STATUS", cs35l56_cal_set_status_enum,
|
|
cs35l56_cal_set_status_ctl_get, NULL,
|
|
SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE),
|
|
};
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_asp1tx1_enum,
|
|
CS35L56_ASP1TX1_INPUT,
|
|
0, CS35L56_ASP_TXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new asp1_tx1_mux =
|
|
SOC_DAPM_ENUM("ASP1TX1 SRC", cs35l56_asp1tx1_enum);
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_asp1tx2_enum,
|
|
CS35L56_ASP1TX2_INPUT,
|
|
0, CS35L56_ASP_TXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new asp1_tx2_mux =
|
|
SOC_DAPM_ENUM("ASP1TX2 SRC", cs35l56_asp1tx2_enum);
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_asp1tx3_enum,
|
|
CS35L56_ASP1TX3_INPUT,
|
|
0, CS35L56_ASP_TXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new asp1_tx3_mux =
|
|
SOC_DAPM_ENUM("ASP1TX3 SRC", cs35l56_asp1tx3_enum);
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_asp1tx4_enum,
|
|
CS35L56_ASP1TX4_INPUT,
|
|
0, CS35L56_ASP_TXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new asp1_tx4_mux =
|
|
SOC_DAPM_ENUM("ASP1TX4 SRC", cs35l56_asp1tx4_enum);
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_sdw1tx1_enum,
|
|
CS35L56_SWIRE_DP3_CH1_INPUT,
|
|
0, CS35L56_SWIRETXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new sdw1_tx1_mux =
|
|
SOC_DAPM_ENUM("SDW1TX1 SRC", cs35l56_sdw1tx1_enum);
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_sdw1tx2_enum,
|
|
CS35L56_SWIRE_DP3_CH2_INPUT,
|
|
0, CS35L56_SWIRETXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new sdw1_tx2_mux =
|
|
SOC_DAPM_ENUM("SDW1TX2 SRC", cs35l56_sdw1tx2_enum);
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_sdw1tx3_enum,
|
|
CS35L56_SWIRE_DP3_CH3_INPUT,
|
|
0, CS35L56_SWIRETXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new sdw1_tx3_mux =
|
|
SOC_DAPM_ENUM("SDW1TX3 SRC", cs35l56_sdw1tx3_enum);
|
|
|
|
static SOC_VALUE_ENUM_SINGLE_DECL(cs35l56_sdw1tx4_enum,
|
|
CS35L56_SWIRE_DP3_CH4_INPUT,
|
|
0, CS35L56_SWIRETXn_SRC_MASK,
|
|
cs35l56_tx_input_texts,
|
|
cs35l56_tx_input_values);
|
|
|
|
static const struct snd_kcontrol_new sdw1_tx4_mux =
|
|
SOC_DAPM_ENUM("SDW1TX4 SRC", cs35l56_sdw1tx4_enum);
|
|
|
|
static int cs35l56_play_event(struct snd_soc_dapm_widget *w,
|
|
struct snd_kcontrol *kcontrol, int event)
|
|
{
|
|
struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
unsigned int val;
|
|
int ret;
|
|
|
|
dev_dbg(cs35l56->base.dev, "play: %d\n", event);
|
|
|
|
switch (event) {
|
|
case SND_SOC_DAPM_PRE_PMU:
|
|
/* Don't wait for ACK, we check in POST_PMU that it completed */
|
|
return regmap_write(cs35l56->base.regmap, CS35L56_DSP_VIRTUAL1_MBOX_1,
|
|
CS35L56_MBOX_CMD_AUDIO_PLAY);
|
|
case SND_SOC_DAPM_POST_PMU:
|
|
/* Wait for firmware to enter PS0 power state */
|
|
ret = regmap_read_poll_timeout(cs35l56->base.regmap,
|
|
cs35l56->base.fw_reg->transducer_actual_ps,
|
|
val, (val == CS35L56_PS0),
|
|
CS35L56_PS0_POLL_US,
|
|
CS35L56_PS0_TIMEOUT_US);
|
|
if (ret)
|
|
dev_err(cs35l56->base.dev, "PS0 wait failed: %d\n", ret);
|
|
return ret;
|
|
case SND_SOC_DAPM_POST_PMD:
|
|
return cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_PAUSE);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static const struct snd_soc_dapm_widget cs35l56_dapm_widgets[] = {
|
|
SND_SOC_DAPM_REGULATOR_SUPPLY("VDD_B", 0, 0),
|
|
SND_SOC_DAPM_REGULATOR_SUPPLY("VDD_AMP", 0, 0),
|
|
|
|
SND_SOC_DAPM_SUPPLY("PLAY", SND_SOC_NOPM, 0, 0, cs35l56_play_event,
|
|
SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_POST_PMD),
|
|
|
|
SND_SOC_DAPM_OUT_DRV("AMP", SND_SOC_NOPM, 0, 0, NULL, 0),
|
|
SND_SOC_DAPM_OUTPUT("SPK"),
|
|
|
|
SND_SOC_DAPM_PGA_E("DSP1", SND_SOC_NOPM, 0, 0, NULL, 0, cs35l56_dsp_event,
|
|
SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD),
|
|
|
|
SND_SOC_DAPM_AIF_IN("ASP1RX1", NULL, 0, CS35L56_ASP1_ENABLES1,
|
|
CS35L56_ASP_RX1_EN_SHIFT, 0),
|
|
SND_SOC_DAPM_AIF_IN("ASP1RX2", NULL, 1, CS35L56_ASP1_ENABLES1,
|
|
CS35L56_ASP_RX2_EN_SHIFT, 0),
|
|
SND_SOC_DAPM_AIF_OUT("ASP1TX1", NULL, 0, CS35L56_ASP1_ENABLES1,
|
|
CS35L56_ASP_TX1_EN_SHIFT, 0),
|
|
SND_SOC_DAPM_AIF_OUT("ASP1TX2", NULL, 1, CS35L56_ASP1_ENABLES1,
|
|
CS35L56_ASP_TX2_EN_SHIFT, 0),
|
|
SND_SOC_DAPM_AIF_OUT("ASP1TX3", NULL, 2, CS35L56_ASP1_ENABLES1,
|
|
CS35L56_ASP_TX3_EN_SHIFT, 0),
|
|
SND_SOC_DAPM_AIF_OUT("ASP1TX4", NULL, 3, CS35L56_ASP1_ENABLES1,
|
|
CS35L56_ASP_TX4_EN_SHIFT, 0),
|
|
|
|
SND_SOC_DAPM_MUX("ASP1 TX1 Source", SND_SOC_NOPM, 0, 0, &asp1_tx1_mux),
|
|
SND_SOC_DAPM_MUX("ASP1 TX2 Source", SND_SOC_NOPM, 0, 0, &asp1_tx2_mux),
|
|
SND_SOC_DAPM_MUX("ASP1 TX3 Source", SND_SOC_NOPM, 0, 0, &asp1_tx3_mux),
|
|
SND_SOC_DAPM_MUX("ASP1 TX4 Source", SND_SOC_NOPM, 0, 0, &asp1_tx4_mux),
|
|
|
|
SND_SOC_DAPM_MUX("SDW1 TX1 Source", SND_SOC_NOPM, 0, 0, &sdw1_tx1_mux),
|
|
SND_SOC_DAPM_MUX("SDW1 TX2 Source", SND_SOC_NOPM, 0, 0, &sdw1_tx2_mux),
|
|
SND_SOC_DAPM_MUX("SDW1 TX3 Source", SND_SOC_NOPM, 0, 0, &sdw1_tx3_mux),
|
|
SND_SOC_DAPM_MUX("SDW1 TX4 Source", SND_SOC_NOPM, 0, 0, &sdw1_tx4_mux),
|
|
|
|
SND_SOC_DAPM_SIGGEN("VMON ADC"),
|
|
SND_SOC_DAPM_SIGGEN("IMON ADC"),
|
|
SND_SOC_DAPM_SIGGEN("ERRVOL ADC"),
|
|
SND_SOC_DAPM_SIGGEN("CLASSH ADC"),
|
|
SND_SOC_DAPM_SIGGEN("VDDBMON ADC"),
|
|
SND_SOC_DAPM_SIGGEN("VBSTMON ADC"),
|
|
SND_SOC_DAPM_SIGGEN("TEMPMON ADC"),
|
|
|
|
SND_SOC_DAPM_INPUT("Calibrate"),
|
|
};
|
|
|
|
#define CS35L56_SRC_ROUTE(name) \
|
|
{ name" Source", "ASP1RX1", "ASP1RX1" }, \
|
|
{ name" Source", "ASP1RX2", "ASP1RX2" }, \
|
|
{ name" Source", "VMON", "VMON ADC" }, \
|
|
{ name" Source", "IMON", "IMON ADC" }, \
|
|
{ name" Source", "ERRVOL", "ERRVOL ADC" }, \
|
|
{ name" Source", "CLASSH", "CLASSH ADC" }, \
|
|
{ name" Source", "VDDBMON", "VDDBMON ADC" }, \
|
|
{ name" Source", "VBSTMON", "VBSTMON ADC" }, \
|
|
{ name" Source", "DSP1TX1", "DSP1" }, \
|
|
{ name" Source", "DSP1TX2", "DSP1" }, \
|
|
{ name" Source", "DSP1TX3", "DSP1" }, \
|
|
{ name" Source", "DSP1TX4", "DSP1" }, \
|
|
{ name" Source", "DSP1TX5", "DSP1" }, \
|
|
{ name" Source", "DSP1TX6", "DSP1" }, \
|
|
{ name" Source", "DSP1TX7", "DSP1" }, \
|
|
{ name" Source", "DSP1TX8", "DSP1" }, \
|
|
{ name" Source", "TEMPMON", "TEMPMON ADC" }, \
|
|
{ name" Source", "INTERPOLATOR", "AMP" }, \
|
|
{ name" Source", "SDW1RX1", "SDW1 Playback" }, \
|
|
{ name" Source", "SDW1RX2", "SDW1 Playback" },
|
|
|
|
static const struct snd_soc_dapm_route cs35l56_audio_map[] = {
|
|
{ "AMP", NULL, "VDD_B" },
|
|
{ "AMP", NULL, "VDD_AMP" },
|
|
|
|
{ "ASP1 Playback", NULL, "PLAY" },
|
|
{ "SDW1 Playback", NULL, "PLAY" },
|
|
|
|
{ "ASP1RX1", NULL, "ASP1 Playback" },
|
|
{ "ASP1RX2", NULL, "ASP1 Playback" },
|
|
{ "DSP1", NULL, "ASP1RX1" },
|
|
{ "DSP1", NULL, "ASP1RX2" },
|
|
{ "DSP1", NULL, "SDW1 Playback" },
|
|
{ "DSP1", NULL, "Calibrate" },
|
|
{ "AMP", NULL, "DSP1" },
|
|
{ "SPK", NULL, "AMP" },
|
|
|
|
CS35L56_SRC_ROUTE("ASP1 TX1")
|
|
CS35L56_SRC_ROUTE("ASP1 TX2")
|
|
CS35L56_SRC_ROUTE("ASP1 TX3")
|
|
CS35L56_SRC_ROUTE("ASP1 TX4")
|
|
|
|
{ "ASP1TX1", NULL, "ASP1 TX1 Source" },
|
|
{ "ASP1TX2", NULL, "ASP1 TX2 Source" },
|
|
{ "ASP1TX3", NULL, "ASP1 TX3 Source" },
|
|
{ "ASP1TX4", NULL, "ASP1 TX4 Source" },
|
|
{ "ASP1 Capture", NULL, "ASP1TX1" },
|
|
{ "ASP1 Capture", NULL, "ASP1TX2" },
|
|
{ "ASP1 Capture", NULL, "ASP1TX3" },
|
|
{ "ASP1 Capture", NULL, "ASP1TX4" },
|
|
|
|
CS35L56_SRC_ROUTE("SDW1 TX1")
|
|
CS35L56_SRC_ROUTE("SDW1 TX2")
|
|
CS35L56_SRC_ROUTE("SDW1 TX3")
|
|
CS35L56_SRC_ROUTE("SDW1 TX4")
|
|
{ "SDW1 Capture", NULL, "SDW1 TX1 Source" },
|
|
{ "SDW1 Capture", NULL, "SDW1 TX2 Source" },
|
|
{ "SDW1 Capture", NULL, "SDW1 TX3 Source" },
|
|
{ "SDW1 Capture", NULL, "SDW1 TX4 Source" },
|
|
};
|
|
|
|
static int cs35l56_dsp_event(struct snd_soc_dapm_widget *w,
|
|
struct snd_kcontrol *kcontrol, int event)
|
|
{
|
|
struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
|
|
dev_dbg(cs35l56->base.dev, "%s: %d\n", __func__, event);
|
|
|
|
return wm_adsp_event(w, kcontrol, event);
|
|
}
|
|
|
|
static int cs35l56_asp_dai_probe(struct snd_soc_dai *codec_dai)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(codec_dai->component);
|
|
|
|
return cs35l56_set_asp_patch(&cs35l56->base);
|
|
}
|
|
|
|
static int cs35l56_asp_dai_set_fmt(struct snd_soc_dai *codec_dai, unsigned int fmt)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(codec_dai->component);
|
|
unsigned int val;
|
|
|
|
dev_dbg(cs35l56->base.dev, "%s: %#x\n", __func__, fmt);
|
|
|
|
switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) {
|
|
case SND_SOC_DAIFMT_CBC_CFC:
|
|
break;
|
|
default:
|
|
dev_err(cs35l56->base.dev, "Unsupported clock source mode\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
|
|
case SND_SOC_DAIFMT_DSP_A:
|
|
val = CS35L56_ASP_FMT_DSP_A << CS35L56_ASP_FMT_SHIFT;
|
|
cs35l56->tdm_mode = true;
|
|
break;
|
|
case SND_SOC_DAIFMT_I2S:
|
|
val = CS35L56_ASP_FMT_I2S << CS35L56_ASP_FMT_SHIFT;
|
|
cs35l56->tdm_mode = false;
|
|
break;
|
|
default:
|
|
dev_err(cs35l56->base.dev, "Unsupported DAI format\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
|
|
case SND_SOC_DAIFMT_NB_IF:
|
|
val |= CS35L56_ASP_FSYNC_INV_MASK;
|
|
break;
|
|
case SND_SOC_DAIFMT_IB_NF:
|
|
val |= CS35L56_ASP_BCLK_INV_MASK;
|
|
break;
|
|
case SND_SOC_DAIFMT_IB_IF:
|
|
val |= CS35L56_ASP_BCLK_INV_MASK | CS35L56_ASP_FSYNC_INV_MASK;
|
|
break;
|
|
case SND_SOC_DAIFMT_NB_NF:
|
|
break;
|
|
default:
|
|
dev_err(cs35l56->base.dev, "Invalid clock invert\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
regmap_update_bits(cs35l56->base.regmap,
|
|
CS35L56_ASP1_CONTROL2,
|
|
CS35L56_ASP_FMT_MASK |
|
|
CS35L56_ASP_BCLK_INV_MASK | CS35L56_ASP_FSYNC_INV_MASK,
|
|
val);
|
|
|
|
/* Hi-Z DOUT in unused slots and when all TX are disabled */
|
|
regmap_update_bits(cs35l56->base.regmap, CS35L56_ASP1_CONTROL3,
|
|
CS35L56_ASP1_DOUT_HIZ_CTRL_MASK,
|
|
CS35L56_ASP_UNUSED_HIZ_OFF_HIZ);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static unsigned int cs35l56_make_tdm_config_word(unsigned int reg_val, unsigned long mask)
|
|
{
|
|
unsigned int channel_shift;
|
|
int bit_num;
|
|
|
|
/* Enable consecutive TX1..TXn for each of the slots set in mask */
|
|
channel_shift = 0;
|
|
for_each_set_bit(bit_num, &mask, 32) {
|
|
reg_val &= ~(0x3f << channel_shift);
|
|
reg_val |= bit_num << channel_shift;
|
|
channel_shift += 8;
|
|
}
|
|
|
|
return reg_val;
|
|
}
|
|
|
|
static int cs35l56_asp_dai_set_tdm_slot(struct snd_soc_dai *dai, unsigned int tx_mask,
|
|
unsigned int rx_mask, int slots, int slot_width)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(dai->component);
|
|
|
|
if ((slots == 0) || (slot_width == 0)) {
|
|
dev_dbg(cs35l56->base.dev, "tdm config cleared\n");
|
|
cs35l56->asp_slot_width = 0;
|
|
cs35l56->asp_slot_count = 0;
|
|
return 0;
|
|
}
|
|
|
|
if (slot_width > (CS35L56_ASP_RX_WIDTH_MASK >> CS35L56_ASP_RX_WIDTH_SHIFT)) {
|
|
dev_err(cs35l56->base.dev, "tdm invalid slot width %d\n", slot_width);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* More than 32 slots would give an unsupportable BCLK frequency */
|
|
if (slots > 32) {
|
|
dev_err(cs35l56->base.dev, "tdm invalid slot count %d\n", slots);
|
|
return -EINVAL;
|
|
}
|
|
|
|
cs35l56->asp_slot_width = (u8)slot_width;
|
|
cs35l56->asp_slot_count = (u8)slots;
|
|
|
|
// Note: rx/tx is from point of view of the CPU end
|
|
if (tx_mask == 0)
|
|
tx_mask = 0x3; // ASPRX1/RX2 in slots 0 and 1
|
|
|
|
if (rx_mask == 0)
|
|
rx_mask = 0xf; // ASPTX1..TX4 in slots 0..3
|
|
|
|
/* Default unused slots to 63 */
|
|
regmap_write(cs35l56->base.regmap, CS35L56_ASP1_FRAME_CONTROL1,
|
|
cs35l56_make_tdm_config_word(0x3f3f3f3f, rx_mask));
|
|
regmap_write(cs35l56->base.regmap, CS35L56_ASP1_FRAME_CONTROL5,
|
|
cs35l56_make_tdm_config_word(0x3f3f3f, tx_mask));
|
|
|
|
dev_dbg(cs35l56->base.dev, "tdm slot width: %u count: %u tx_mask: %#x rx_mask: %#x\n",
|
|
cs35l56->asp_slot_width, cs35l56->asp_slot_count, tx_mask, rx_mask);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_asp_dai_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *params,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(dai->component);
|
|
unsigned int rate = params_rate(params);
|
|
u8 asp_width, asp_wl;
|
|
|
|
asp_wl = params_width(params);
|
|
if (cs35l56->asp_slot_width)
|
|
asp_width = cs35l56->asp_slot_width;
|
|
else
|
|
asp_width = asp_wl;
|
|
|
|
dev_dbg(cs35l56->base.dev, "%s: wl=%d, width=%d, rate=%d",
|
|
__func__, asp_wl, asp_width, rate);
|
|
|
|
if (!cs35l56->sysclk_set) {
|
|
unsigned int slots = cs35l56->asp_slot_count;
|
|
unsigned int bclk_freq;
|
|
int freq_id;
|
|
|
|
if (slots == 0) {
|
|
slots = params_channels(params);
|
|
|
|
/* I2S always has an even number of slots */
|
|
if (!cs35l56->tdm_mode)
|
|
slots = round_up(slots, 2);
|
|
}
|
|
|
|
bclk_freq = asp_width * slots * rate;
|
|
freq_id = cs35l56_get_bclk_freq_id(bclk_freq);
|
|
if (freq_id < 0) {
|
|
dev_err(cs35l56->base.dev, "%s: Invalid BCLK %u\n", __func__, bclk_freq);
|
|
return -EINVAL;
|
|
}
|
|
|
|
regmap_update_bits(cs35l56->base.regmap, CS35L56_ASP1_CONTROL1,
|
|
CS35L56_ASP_BCLK_FREQ_MASK,
|
|
freq_id << CS35L56_ASP_BCLK_FREQ_SHIFT);
|
|
}
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
regmap_update_bits(cs35l56->base.regmap, CS35L56_ASP1_CONTROL2,
|
|
CS35L56_ASP_RX_WIDTH_MASK, asp_width <<
|
|
CS35L56_ASP_RX_WIDTH_SHIFT);
|
|
regmap_update_bits(cs35l56->base.regmap, CS35L56_ASP1_DATA_CONTROL5,
|
|
CS35L56_ASP_RX_WL_MASK, asp_wl);
|
|
} else {
|
|
regmap_update_bits(cs35l56->base.regmap, CS35L56_ASP1_CONTROL2,
|
|
CS35L56_ASP_TX_WIDTH_MASK, asp_width <<
|
|
CS35L56_ASP_TX_WIDTH_SHIFT);
|
|
regmap_update_bits(cs35l56->base.regmap, CS35L56_ASP1_DATA_CONTROL1,
|
|
CS35L56_ASP_TX_WL_MASK, asp_wl);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_asp_dai_set_sysclk(struct snd_soc_dai *dai,
|
|
int clk_id, unsigned int freq, int dir)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(dai->component);
|
|
int freq_id;
|
|
|
|
if (freq == 0) {
|
|
cs35l56->sysclk_set = false;
|
|
return 0;
|
|
}
|
|
|
|
freq_id = cs35l56_get_bclk_freq_id(freq);
|
|
if (freq_id < 0)
|
|
return freq_id;
|
|
|
|
regmap_update_bits(cs35l56->base.regmap, CS35L56_ASP1_CONTROL1,
|
|
CS35L56_ASP_BCLK_FREQ_MASK,
|
|
freq_id << CS35L56_ASP_BCLK_FREQ_SHIFT);
|
|
cs35l56->sysclk_set = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct snd_soc_dai_ops cs35l56_ops = {
|
|
.probe = cs35l56_asp_dai_probe,
|
|
.set_fmt = cs35l56_asp_dai_set_fmt,
|
|
.set_tdm_slot = cs35l56_asp_dai_set_tdm_slot,
|
|
.hw_params = cs35l56_asp_dai_hw_params,
|
|
.set_sysclk = cs35l56_asp_dai_set_sysclk,
|
|
};
|
|
|
|
static void cs35l56_sdw_dai_shutdown(struct snd_pcm_substream *substream,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
snd_soc_dai_set_dma_data(dai, substream, NULL);
|
|
}
|
|
|
|
static int cs35l56_sdw_dai_set_tdm_slot(struct snd_soc_dai *dai, unsigned int tx_mask,
|
|
unsigned int rx_mask, int slots, int slot_width)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(dai->component);
|
|
|
|
/* rx/tx are from point of view of the CPU end so opposite to our rx/tx */
|
|
cs35l56->rx_mask = tx_mask;
|
|
cs35l56->tx_mask = rx_mask;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_sdw_dai_hw_params(struct snd_pcm_substream *substream,
|
|
struct snd_pcm_hw_params *params,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(dai->component);
|
|
struct sdw_stream_runtime *sdw_stream = snd_soc_dai_get_dma_data(dai, substream);
|
|
struct sdw_stream_config sconfig;
|
|
struct sdw_port_config pconfig;
|
|
int ret;
|
|
|
|
dev_dbg(cs35l56->base.dev, "%s: rate %d\n", __func__, params_rate(params));
|
|
|
|
if (!cs35l56->base.init_done)
|
|
return -ENODEV;
|
|
|
|
if (!sdw_stream)
|
|
return -EINVAL;
|
|
|
|
memset(&sconfig, 0, sizeof(sconfig));
|
|
memset(&pconfig, 0, sizeof(pconfig));
|
|
|
|
sconfig.frame_rate = params_rate(params);
|
|
sconfig.bps = snd_pcm_format_width(params_format(params));
|
|
|
|
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
|
|
sconfig.direction = SDW_DATA_DIR_RX;
|
|
pconfig.num = CS35L56_SDW1_PLAYBACK_PORT;
|
|
pconfig.ch_mask = cs35l56->rx_mask;
|
|
} else {
|
|
sconfig.direction = SDW_DATA_DIR_TX;
|
|
pconfig.num = CS35L56_SDW1_CAPTURE_PORT;
|
|
pconfig.ch_mask = cs35l56->tx_mask;
|
|
}
|
|
|
|
if (pconfig.ch_mask == 0) {
|
|
sconfig.ch_count = params_channels(params);
|
|
pconfig.ch_mask = GENMASK(sconfig.ch_count - 1, 0);
|
|
} else {
|
|
sconfig.ch_count = hweight32(pconfig.ch_mask);
|
|
}
|
|
|
|
ret = sdw_stream_add_slave(cs35l56->sdw_peripheral, &sconfig, &pconfig,
|
|
1, sdw_stream);
|
|
if (ret) {
|
|
dev_err(dai->dev, "Failed to add sdw stream: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_sdw_dai_hw_free(struct snd_pcm_substream *substream,
|
|
struct snd_soc_dai *dai)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(dai->component);
|
|
struct sdw_stream_runtime *sdw_stream = snd_soc_dai_get_dma_data(dai, substream);
|
|
|
|
if (!cs35l56->sdw_peripheral)
|
|
return -EINVAL;
|
|
|
|
sdw_stream_remove_slave(cs35l56->sdw_peripheral, sdw_stream);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_sdw_dai_set_stream(struct snd_soc_dai *dai,
|
|
void *sdw_stream, int direction)
|
|
{
|
|
snd_soc_dai_dma_data_set(dai, direction, sdw_stream);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct snd_soc_dai_ops cs35l56_sdw_dai_ops = {
|
|
.set_tdm_slot = cs35l56_sdw_dai_set_tdm_slot,
|
|
.shutdown = cs35l56_sdw_dai_shutdown,
|
|
.hw_params = cs35l56_sdw_dai_hw_params,
|
|
.hw_free = cs35l56_sdw_dai_hw_free,
|
|
.set_stream = cs35l56_sdw_dai_set_stream,
|
|
};
|
|
|
|
static struct snd_soc_dai_driver cs35l56_dai[] = {
|
|
{
|
|
.name = "cs35l56-asp1",
|
|
.id = 0,
|
|
.playback = {
|
|
.stream_name = "ASP1 Playback",
|
|
.channels_min = 1,
|
|
.channels_max = 2,
|
|
.rates = CS35L56_RATES,
|
|
.formats = CS35L56_RX_FORMATS,
|
|
},
|
|
.capture = {
|
|
.stream_name = "ASP1 Capture",
|
|
.channels_min = 1,
|
|
.channels_max = 4,
|
|
.rates = CS35L56_RATES,
|
|
.formats = CS35L56_TX_FORMATS,
|
|
},
|
|
.ops = &cs35l56_ops,
|
|
.symmetric_rate = 1,
|
|
.symmetric_sample_bits = 1,
|
|
},
|
|
{
|
|
.name = "cs35l56-sdw1",
|
|
.id = 1,
|
|
.playback = {
|
|
.stream_name = "SDW1 Playback",
|
|
.channels_min = 1,
|
|
.channels_max = 2,
|
|
.rates = CS35L56_RATES,
|
|
.formats = CS35L56_RX_FORMATS,
|
|
},
|
|
.symmetric_rate = 1,
|
|
.ops = &cs35l56_sdw_dai_ops,
|
|
},
|
|
{
|
|
.name = "cs35l56-sdw1c",
|
|
.id = 2,
|
|
.capture = {
|
|
.stream_name = "SDW1 Capture",
|
|
.channels_min = 1,
|
|
.channels_max = 4,
|
|
.rates = CS35L56_RATES,
|
|
.formats = CS35L56_TX_FORMATS,
|
|
},
|
|
.symmetric_rate = 1,
|
|
.ops = &cs35l56_sdw_dai_ops,
|
|
},
|
|
};
|
|
|
|
static int cs35l56_write_cal(struct cs35l56_private *cs35l56)
|
|
{
|
|
int ret;
|
|
|
|
if (cs35l56->base.secured || !cs35l56->base.cal_data_valid)
|
|
return -ENODATA;
|
|
|
|
ret = wm_adsp_run(&cs35l56->dsp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = cs_amp_write_cal_coeffs(&cs35l56->dsp.cs_dsp,
|
|
cs35l56->base.calibration_controls,
|
|
&cs35l56->base.cal_data);
|
|
|
|
wm_adsp_stop(&cs35l56->dsp);
|
|
|
|
if (ret == 0)
|
|
dev_info(cs35l56->base.dev, "Calibration applied\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int cs35l56_dsp_download_and_power_up(struct cs35l56_private *cs35l56,
|
|
bool load_firmware)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* Abort the first load if it didn't find the suffixed bins and
|
|
* we have an alternate fallback suffix.
|
|
*/
|
|
cs35l56->dsp.bin_mandatory = (load_firmware && cs35l56->fallback_fw_suffix);
|
|
|
|
ret = wm_adsp_power_up(&cs35l56->dsp, load_firmware);
|
|
if ((ret == -ENOENT) && cs35l56->dsp.bin_mandatory) {
|
|
cs35l56->dsp.fwf_suffix = cs35l56->fallback_fw_suffix;
|
|
cs35l56->fallback_fw_suffix = NULL;
|
|
cs35l56->dsp.bin_mandatory = false;
|
|
ret = wm_adsp_power_up(&cs35l56->dsp, load_firmware);
|
|
}
|
|
|
|
if (ret) {
|
|
dev_dbg(cs35l56->base.dev, "wm_adsp_power_up ret %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void cs35l56_reinit_patch(struct cs35l56_private *cs35l56)
|
|
{
|
|
int ret;
|
|
|
|
ret = cs35l56_dsp_download_and_power_up(cs35l56, true);
|
|
if (ret)
|
|
return;
|
|
|
|
cs35l56_write_cal(cs35l56);
|
|
|
|
/* Always REINIT after applying patch or coefficients */
|
|
cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT);
|
|
}
|
|
|
|
static void cs35l56_patch(struct cs35l56_private *cs35l56, bool firmware_missing)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* Disable SoundWire interrupts to prevent race with IRQ work.
|
|
* Setting sdw_irq_no_unmask prevents the handler re-enabling
|
|
* the SoundWire interrupt.
|
|
*/
|
|
if (cs35l56->sdw_peripheral) {
|
|
cs35l56->sdw_irq_no_unmask = true;
|
|
flush_work(&cs35l56->sdw_irq_work);
|
|
sdw_write_no_pm(cs35l56->sdw_peripheral, CS35L56_SDW_GEN_INT_MASK_1, 0);
|
|
sdw_read_no_pm(cs35l56->sdw_peripheral, CS35L56_SDW_GEN_INT_STAT_1);
|
|
sdw_write_no_pm(cs35l56->sdw_peripheral, CS35L56_SDW_GEN_INT_STAT_1, 0xFF);
|
|
flush_work(&cs35l56->sdw_irq_work);
|
|
}
|
|
|
|
ret = cs35l56_firmware_shutdown(&cs35l56->base);
|
|
if (ret)
|
|
goto err;
|
|
|
|
/*
|
|
* Use wm_adsp to load and apply the firmware patch and coefficient files,
|
|
* but only if firmware is missing. If firmware is already patched just
|
|
* power-up wm_adsp without downloading firmware.
|
|
*/
|
|
ret = cs35l56_dsp_download_and_power_up(cs35l56, firmware_missing);
|
|
if (ret)
|
|
goto err;
|
|
|
|
mutex_lock(&cs35l56->base.irq_lock);
|
|
|
|
reinit_completion(&cs35l56->init_completion);
|
|
|
|
cs35l56->soft_resetting = true;
|
|
cs35l56_system_reset(&cs35l56->base, !!cs35l56->sdw_peripheral);
|
|
|
|
if (cs35l56->sdw_peripheral) {
|
|
/*
|
|
* The system-reset causes the CS35L56 to detach from the bus.
|
|
* Wait for the manager to re-enumerate the CS35L56 and
|
|
* cs35l56_init() to run again.
|
|
*/
|
|
if (!wait_for_completion_timeout(&cs35l56->init_completion,
|
|
msecs_to_jiffies(5000))) {
|
|
dev_err(cs35l56->base.dev, "%s: init_completion timed out (SDW)\n",
|
|
__func__);
|
|
goto err_unlock;
|
|
}
|
|
} else if (cs35l56_init(cs35l56)) {
|
|
goto err_unlock;
|
|
}
|
|
|
|
/* Check if the firmware is still reported missing */
|
|
cs35l56_warn_if_firmware_missing(&cs35l56->base);
|
|
|
|
regmap_clear_bits(cs35l56->base.regmap,
|
|
cs35l56->base.fw_reg->prot_sts,
|
|
CS35L56_FIRMWARE_MISSING);
|
|
cs35l56->base.fw_patched = true;
|
|
|
|
if (cs35l56_write_cal(cs35l56) == 0)
|
|
cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT);
|
|
|
|
err_unlock:
|
|
mutex_unlock(&cs35l56->base.irq_lock);
|
|
err:
|
|
/* Re-enable SoundWire interrupts */
|
|
if (cs35l56->sdw_peripheral) {
|
|
cs35l56->sdw_irq_no_unmask = false;
|
|
sdw_write_no_pm(cs35l56->sdw_peripheral, CS35L56_SDW_GEN_INT_MASK_1,
|
|
CS35L56_SDW_INT_MASK_CODEC_IRQ);
|
|
}
|
|
}
|
|
|
|
static void cs35l56_dsp_work(struct work_struct *work)
|
|
{
|
|
struct cs35l56_private *cs35l56 = container_of(work,
|
|
struct cs35l56_private,
|
|
dsp_work);
|
|
unsigned int firmware_version;
|
|
bool firmware_missing;
|
|
int ret;
|
|
|
|
if (!cs35l56->base.init_done)
|
|
return;
|
|
|
|
pm_runtime_get_sync(cs35l56->base.dev);
|
|
|
|
ret = cs35l56_read_prot_status(&cs35l56->base, &firmware_missing, &firmware_version);
|
|
if (ret)
|
|
goto err;
|
|
|
|
/* Populate fw file qualifier with the revision and security state */
|
|
kfree(cs35l56->dsp.fwf_name);
|
|
if (firmware_missing) {
|
|
cs35l56->dsp.fwf_name = kasprintf(GFP_KERNEL, "%02x-dsp1", cs35l56->base.rev);
|
|
} else {
|
|
/* Firmware files must match the running firmware version */
|
|
cs35l56->dsp.fwf_name = kasprintf(GFP_KERNEL,
|
|
"%02x%s-%06x-dsp1",
|
|
cs35l56->base.rev,
|
|
cs35l56->base.secured ? "-s" : "",
|
|
firmware_version);
|
|
}
|
|
|
|
if (!cs35l56->dsp.fwf_name)
|
|
goto err;
|
|
|
|
dev_dbg(cs35l56->base.dev, "DSP fwf name: '%s' system name: '%s'\n",
|
|
cs35l56->dsp.fwf_name, cs35l56->dsp.system_name);
|
|
|
|
/*
|
|
* The firmware cannot be patched if it is already running from
|
|
* patch RAM. In this case the firmware files are versioned to
|
|
* match the running firmware version and will only contain
|
|
* tunings. We do not need to shutdown the firmware to apply
|
|
* tunings so can use the lower cost reinit sequence instead.
|
|
*/
|
|
if (!firmware_missing)
|
|
cs35l56_reinit_patch(cs35l56);
|
|
else
|
|
cs35l56_patch(cs35l56, firmware_missing);
|
|
|
|
cs35l56_log_tuning(&cs35l56->base, &cs35l56->dsp.cs_dsp);
|
|
err:
|
|
pm_runtime_put_autosuspend(cs35l56->base.dev);
|
|
}
|
|
|
|
static struct snd_soc_dapm_context *cs35l56_power_up_for_cal(struct cs35l56_private *cs35l56)
|
|
{
|
|
struct snd_soc_dapm_context *dapm = snd_soc_component_to_dapm(cs35l56->component);
|
|
int ret;
|
|
|
|
ret = snd_soc_dapm_enable_pin(dapm, "Calibrate");
|
|
if (ret)
|
|
return ERR_PTR(ret);
|
|
|
|
snd_soc_dapm_sync(dapm);
|
|
|
|
return dapm;
|
|
}
|
|
|
|
static void cs35l56_power_down_after_cal(struct cs35l56_private *cs35l56)
|
|
{
|
|
struct snd_soc_dapm_context *dapm = snd_soc_component_to_dapm(cs35l56->component);
|
|
|
|
snd_soc_dapm_disable_pin(dapm, "Calibrate");
|
|
snd_soc_dapm_sync(dapm);
|
|
}
|
|
|
|
static ssize_t cs35l56_debugfs_calibrate_write(struct file *file,
|
|
const char __user *from,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct cs35l56_base *cs35l56_base = file->private_data;
|
|
struct cs35l56_private *cs35l56 = cs35l56_private_from_base(cs35l56_base);
|
|
struct snd_soc_dapm_context *dapm;
|
|
ssize_t ret;
|
|
|
|
dapm = cs35l56_power_up_for_cal(cs35l56);
|
|
if (IS_ERR(dapm))
|
|
return PTR_ERR(dapm);
|
|
|
|
snd_soc_dapm_mutex_lock(dapm);
|
|
ret = cs35l56_calibrate_debugfs_write(&cs35l56->base, from, count, ppos);
|
|
snd_soc_dapm_mutex_unlock(dapm);
|
|
|
|
cs35l56_power_down_after_cal(cs35l56);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t cs35l56_debugfs_cal_temperature_write(struct file *file,
|
|
const char __user *from,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct cs35l56_base *cs35l56_base = file->private_data;
|
|
struct cs35l56_private *cs35l56 = cs35l56_private_from_base(cs35l56_base);
|
|
struct snd_soc_dapm_context *dapm;
|
|
ssize_t ret;
|
|
|
|
dapm = cs35l56_power_up_for_cal(cs35l56);
|
|
if (IS_ERR(dapm))
|
|
return PTR_ERR(dapm);
|
|
|
|
ret = cs35l56_cal_ambient_debugfs_write(&cs35l56->base, from, count, ppos);
|
|
cs35l56_power_down_after_cal(cs35l56);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t cs35l56_debugfs_cal_data_read(struct file *file,
|
|
char __user *to,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct cs35l56_base *cs35l56_base = file->private_data;
|
|
struct cs35l56_private *cs35l56 = cs35l56_private_from_base(cs35l56_base);
|
|
struct snd_soc_dapm_context *dapm;
|
|
ssize_t ret;
|
|
|
|
dapm = cs35l56_power_up_for_cal(cs35l56);
|
|
if (IS_ERR(dapm))
|
|
return PTR_ERR(dapm);
|
|
|
|
ret = cs35l56_cal_data_debugfs_read(&cs35l56->base, to, count, ppos);
|
|
cs35l56_power_down_after_cal(cs35l56);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int cs35l56_new_cal_data_apply(struct cs35l56_private *cs35l56)
|
|
{
|
|
struct snd_soc_dapm_context *dapm;
|
|
int ret;
|
|
|
|
if (!cs35l56->base.cal_data_valid)
|
|
return -ENXIO;
|
|
|
|
if (cs35l56->base.secured)
|
|
return -EACCES;
|
|
|
|
dapm = cs35l56_power_up_for_cal(cs35l56);
|
|
if (IS_ERR(dapm))
|
|
return PTR_ERR(dapm);
|
|
|
|
snd_soc_dapm_mutex_lock(dapm);
|
|
ret = cs_amp_write_cal_coeffs(&cs35l56->dsp.cs_dsp,
|
|
cs35l56->base.calibration_controls,
|
|
&cs35l56->base.cal_data);
|
|
if (ret == 0)
|
|
cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_AUDIO_REINIT);
|
|
else
|
|
ret = -EIO;
|
|
|
|
snd_soc_dapm_mutex_unlock(dapm);
|
|
cs35l56_power_down_after_cal(cs35l56);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static ssize_t cs35l56_debugfs_cal_data_write(struct file *file,
|
|
const char __user *from,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct cs35l56_base *cs35l56_base = file->private_data;
|
|
struct cs35l56_private *cs35l56 = cs35l56_private_from_base(cs35l56_base);
|
|
int ret;
|
|
|
|
ret = cs35l56_cal_data_debugfs_write(&cs35l56->base, from, count, ppos);
|
|
if (ret == -ENODATA)
|
|
return count; /* Ignore writes of empty cal blobs */
|
|
else if (ret < 0)
|
|
return -EIO;
|
|
|
|
ret = cs35l56_new_cal_data_apply(cs35l56);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return count;
|
|
}
|
|
|
|
static const struct cs35l56_cal_debugfs_fops cs35l56_cal_debugfs_fops = {
|
|
.calibrate = {
|
|
.write = cs35l56_debugfs_calibrate_write,
|
|
},
|
|
.cal_temperature = {
|
|
.write = cs35l56_debugfs_cal_temperature_write,
|
|
},
|
|
.cal_data = {
|
|
.read = cs35l56_debugfs_cal_data_read,
|
|
.write = cs35l56_debugfs_cal_data_write,
|
|
},
|
|
};
|
|
|
|
static int cs35l56_cal_data_rb_ctl_get(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
|
|
if (!cs35l56->base.cal_data_valid)
|
|
return -ENODATA;
|
|
|
|
memcpy(ucontrol->value.bytes.data, &cs35l56->base.cal_data,
|
|
sizeof(cs35l56->base.cal_data));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_cal_data_ctl_get(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
|
|
/*
|
|
* This control is write-only but mixer libraries often try to read
|
|
* a control before writing it. So we have to implement read.
|
|
* Return zeros so a write of valid data will always be a change
|
|
* from its "current value".
|
|
*/
|
|
memset(ucontrol->value.bytes.data, 0, sizeof(cs35l56->base.cal_data));
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_cal_data_ctl_set(struct snd_kcontrol *kcontrol,
|
|
struct snd_ctl_elem_value *ucontrol)
|
|
{
|
|
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
const struct cirrus_amp_cal_data *cal_data = (const void *)ucontrol->value.bytes.data;
|
|
int ret;
|
|
|
|
if (cs35l56->base.cal_data_valid)
|
|
return -EACCES;
|
|
|
|
ret = cs35l56_stash_calibration(&cs35l56->base, cal_data);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = cs35l56_new_cal_data_apply(cs35l56);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return 1;
|
|
}
|
|
|
|
static const struct snd_kcontrol_new cs35l56_cal_data_restore_controls[] = {
|
|
SND_SOC_BYTES_E("CAL_DATA", 0, sizeof(struct cirrus_amp_cal_data) / sizeof(u32),
|
|
cs35l56_cal_data_ctl_get, cs35l56_cal_data_ctl_set),
|
|
SND_SOC_BYTES_E_ACC("CAL_DATA_RB", 0, sizeof(struct cirrus_amp_cal_data) / sizeof(u32),
|
|
cs35l56_cal_data_rb_ctl_get, NULL,
|
|
SNDRV_CTL_ELEM_ACCESS_READ | SNDRV_CTL_ELEM_ACCESS_VOLATILE),
|
|
};
|
|
|
|
VISIBLE_IF_KUNIT int cs35l56_set_fw_suffix(struct cs35l56_private *cs35l56)
|
|
{
|
|
unsigned short vendor, device;
|
|
const char *vendor_id;
|
|
int ret;
|
|
|
|
if (cs35l56->dsp.fwf_suffix)
|
|
return 0;
|
|
|
|
if (cs35l56->sdw_peripheral) {
|
|
cs35l56->dsp.fwf_suffix = devm_kasprintf(cs35l56->base.dev, GFP_KERNEL,
|
|
"l%uu%u",
|
|
cs35l56->sdw_link_num,
|
|
cs35l56->sdw_unique_id);
|
|
if (!cs35l56->dsp.fwf_suffix)
|
|
return -ENOMEM;
|
|
|
|
/*
|
|
* There are published firmware files for L56 B0 silicon using
|
|
* the ALSA prefix as the filename suffix. Default to trying these
|
|
* first, with the new SoundWire suffix as a fallback.
|
|
* None of these older systems use a vendor-specific ID.
|
|
*/
|
|
if ((cs35l56->base.type == 0x56) && (cs35l56->base.rev == 0xb0)) {
|
|
cs35l56->fallback_fw_suffix = cs35l56->dsp.fwf_suffix;
|
|
cs35l56->dsp.fwf_suffix = cs35l56->component->name_prefix;
|
|
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Some manufacturers use the same SSID on multiple products and have
|
|
* a vendor-specific qualifier to distinguish different models.
|
|
* Models with the same SSID but different qualifier might require
|
|
* different audio firmware, or they might all have the same audio
|
|
* firmware.
|
|
* Try searching for a firmware with this qualifier first, else
|
|
* fallback to standard naming.
|
|
*/
|
|
if (snd_soc_card_get_pci_ssid(cs35l56->component->card, &vendor, &device) < 0) {
|
|
vendor_id = cs_amp_devm_get_vendor_specific_variant_id(cs35l56->base.dev, -1, -1);
|
|
} else {
|
|
vendor_id = cs_amp_devm_get_vendor_specific_variant_id(cs35l56->base.dev,
|
|
vendor, device);
|
|
}
|
|
ret = PTR_ERR_OR_ZERO(vendor_id);
|
|
if (ret == -ENOENT)
|
|
return 0;
|
|
else if (ret)
|
|
return ret;
|
|
|
|
if (vendor_id) {
|
|
if (cs35l56->dsp.fwf_suffix)
|
|
cs35l56->fallback_fw_suffix = cs35l56->dsp.fwf_suffix;
|
|
else
|
|
cs35l56->fallback_fw_suffix = cs35l56->component->name_prefix;
|
|
|
|
cs35l56->dsp.fwf_suffix = devm_kasprintf(cs35l56->base.dev, GFP_KERNEL,
|
|
"%s-%s",
|
|
vendor_id,
|
|
cs35l56->fallback_fw_suffix);
|
|
if (!cs35l56->dsp.fwf_suffix)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_IF_KUNIT(cs35l56_set_fw_suffix);
|
|
|
|
VISIBLE_IF_KUNIT int cs35l56_set_fw_name(struct snd_soc_component *component)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
unsigned short vendor, device;
|
|
int ret;
|
|
|
|
if ((cs35l56->speaker_id < 0) && cs35l56->base.num_onchip_spkid_gpios) {
|
|
PM_RUNTIME_ACQUIRE(cs35l56->base.dev, pm);
|
|
ret = PM_RUNTIME_ACQUIRE_ERR(&pm);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = cs35l56_configure_onchip_spkid_pads(&cs35l56->base);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = cs35l56_read_onchip_spkid(&cs35l56->base);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
cs35l56->speaker_id = ret;
|
|
}
|
|
|
|
if (!cs35l56->dsp.system_name &&
|
|
(snd_soc_card_get_pci_ssid(component->card, &vendor, &device) == 0)) {
|
|
/* Append a speaker qualifier if there is a speaker ID */
|
|
if (cs35l56->speaker_id >= 0) {
|
|
cs35l56->dsp.system_name = devm_kasprintf(cs35l56->base.dev,
|
|
GFP_KERNEL,
|
|
"%04x%04x-spkid%d",
|
|
vendor, device,
|
|
cs35l56->speaker_id);
|
|
} else {
|
|
cs35l56->dsp.system_name = devm_kasprintf(cs35l56->base.dev,
|
|
GFP_KERNEL,
|
|
"%04x%04x",
|
|
vendor, device);
|
|
}
|
|
if (!cs35l56->dsp.system_name)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_IF_KUNIT(cs35l56_set_fw_name);
|
|
|
|
static int cs35l56_component_probe(struct snd_soc_component *component)
|
|
{
|
|
struct snd_soc_dapm_context *dapm = snd_soc_component_to_dapm(component);
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
struct dentry *debugfs_root = component->debugfs_root;
|
|
int ret;
|
|
|
|
BUILD_BUG_ON(ARRAY_SIZE(cs35l56_tx_input_texts) != ARRAY_SIZE(cs35l56_tx_input_values));
|
|
|
|
if (!wait_for_completion_timeout(&cs35l56->init_completion,
|
|
msecs_to_jiffies(5000))) {
|
|
dev_err(cs35l56->base.dev, "%s: init_completion timed out\n", __func__);
|
|
return -ENODEV;
|
|
}
|
|
|
|
cs35l56->dsp.part = kasprintf(GFP_KERNEL, "cs35l%02x", cs35l56->base.type);
|
|
if (!cs35l56->dsp.part)
|
|
return -ENOMEM;
|
|
|
|
cs35l56->component = component;
|
|
ret = cs35l56_set_fw_name(component);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = cs35l56_set_fw_suffix(cs35l56);
|
|
if (ret)
|
|
return ret;
|
|
|
|
wm_adsp2_component_probe(&cs35l56->dsp, component);
|
|
|
|
debugfs_create_bool("init_done", 0444, debugfs_root, &cs35l56->base.init_done);
|
|
debugfs_create_bool("can_hibernate", 0444, debugfs_root, &cs35l56->base.can_hibernate);
|
|
debugfs_create_bool("fw_patched", 0444, debugfs_root, &cs35l56->base.fw_patched);
|
|
|
|
|
|
switch (cs35l56->base.type) {
|
|
case 0x54:
|
|
case 0x56:
|
|
case 0x57:
|
|
ret = snd_soc_add_component_controls(component, cs35l56_controls,
|
|
ARRAY_SIZE(cs35l56_controls));
|
|
break;
|
|
case 0x63:
|
|
ret = snd_soc_add_component_controls(component, cs35l63_controls,
|
|
ARRAY_SIZE(cs35l63_controls));
|
|
break;
|
|
default:
|
|
ret = -ENODEV;
|
|
break;
|
|
}
|
|
|
|
if (!ret && IS_ENABLED(CONFIG_SND_SOC_CS35L56_CAL_SET_CTRL)) {
|
|
ret = snd_soc_add_component_controls(component,
|
|
cs35l56_cal_data_restore_controls,
|
|
ARRAY_SIZE(cs35l56_cal_data_restore_controls));
|
|
}
|
|
|
|
if (ret)
|
|
return dev_err_probe(cs35l56->base.dev, ret, "unable to add controls\n");
|
|
|
|
ret = snd_soc_dapm_disable_pin(dapm, "Calibrate");
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (IS_ENABLED(CONFIG_SND_SOC_CS35L56_CAL_DEBUGFS))
|
|
cs35l56_create_cal_debugfs(&cs35l56->base, &cs35l56_cal_debugfs_fops);
|
|
|
|
queue_work(cs35l56->dsp_wq, &cs35l56->dsp_work);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void cs35l56_component_remove(struct snd_soc_component *component)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
|
|
cancel_work_sync(&cs35l56->dsp_work);
|
|
|
|
cs35l56_remove_cal_debugfs(&cs35l56->base);
|
|
|
|
if (cs35l56->dsp.cs_dsp.booted)
|
|
wm_adsp_power_down(&cs35l56->dsp);
|
|
|
|
wm_adsp2_component_remove(&cs35l56->dsp, component);
|
|
|
|
kfree(cs35l56->dsp.part);
|
|
cs35l56->dsp.part = NULL;
|
|
|
|
kfree(cs35l56->dsp.fwf_name);
|
|
cs35l56->dsp.fwf_name = NULL;
|
|
|
|
cs35l56->component = NULL;
|
|
}
|
|
|
|
static int cs35l56_set_bias_level(struct snd_soc_component *component,
|
|
enum snd_soc_bias_level level)
|
|
{
|
|
struct cs35l56_private *cs35l56 = snd_soc_component_get_drvdata(component);
|
|
struct snd_soc_dapm_context *dapm = snd_soc_component_to_dapm(component);
|
|
|
|
switch (level) {
|
|
case SND_SOC_BIAS_STANDBY:
|
|
/*
|
|
* Wait for patching to complete when transitioning from
|
|
* BIAS_OFF to BIAS_STANDBY
|
|
*/
|
|
if (snd_soc_dapm_get_bias_level(dapm) == SND_SOC_BIAS_OFF)
|
|
cs35l56_wait_dsp_ready(cs35l56);
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct snd_soc_component_driver soc_component_dev_cs35l56 = {
|
|
.probe = cs35l56_component_probe,
|
|
.remove = cs35l56_component_remove,
|
|
|
|
.dapm_widgets = cs35l56_dapm_widgets,
|
|
.num_dapm_widgets = ARRAY_SIZE(cs35l56_dapm_widgets),
|
|
.dapm_routes = cs35l56_audio_map,
|
|
.num_dapm_routes = ARRAY_SIZE(cs35l56_audio_map),
|
|
|
|
.set_bias_level = cs35l56_set_bias_level,
|
|
|
|
.suspend_bias_off = 1, /* see cs35l56_system_resume() */
|
|
};
|
|
|
|
static int __maybe_unused cs35l56_runtime_suspend_i2c_spi(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
|
|
return cs35l56_runtime_suspend_common(&cs35l56->base);
|
|
}
|
|
|
|
static int __maybe_unused cs35l56_runtime_resume_i2c_spi(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
|
|
return cs35l56_runtime_resume_common(&cs35l56->base, false);
|
|
}
|
|
|
|
int cs35l56_system_suspend(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
|
|
dev_dbg(dev, "system_suspend\n");
|
|
|
|
if (cs35l56->component)
|
|
flush_work(&cs35l56->dsp_work);
|
|
|
|
/*
|
|
* The interrupt line is normally shared, but after we start suspending
|
|
* we can't check if our device is the source of an interrupt, and can't
|
|
* clear it. Prevent this race by temporarily disabling the parent irq
|
|
* until we reach _no_irq.
|
|
*/
|
|
if (cs35l56->base.irq)
|
|
disable_irq(cs35l56->base.irq);
|
|
|
|
return pm_runtime_force_suspend(dev);
|
|
}
|
|
EXPORT_SYMBOL_GPL(cs35l56_system_suspend);
|
|
|
|
int cs35l56_system_suspend_late(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
|
|
dev_dbg(dev, "system_suspend_late\n");
|
|
|
|
/*
|
|
* Assert RESET before removing supplies.
|
|
* RESET is usually shared by all amps so it must not be asserted until
|
|
* all driver instances have done their suspend() stage.
|
|
*/
|
|
if (cs35l56->base.reset_gpio) {
|
|
gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
|
|
cs35l56_wait_min_reset_pulse();
|
|
}
|
|
|
|
regulator_bulk_disable(ARRAY_SIZE(cs35l56->supplies), cs35l56->supplies);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cs35l56_system_suspend_late);
|
|
|
|
int cs35l56_system_suspend_no_irq(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
|
|
dev_dbg(dev, "system_suspend_no_irq\n");
|
|
|
|
/* Handlers are now disabled so the parent IRQ can safely be re-enabled. */
|
|
if (cs35l56->base.irq)
|
|
enable_irq(cs35l56->base.irq);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cs35l56_system_suspend_no_irq);
|
|
|
|
int cs35l56_system_resume_no_irq(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
|
|
dev_dbg(dev, "system_resume_no_irq\n");
|
|
|
|
/*
|
|
* WAKE interrupts unmask if the CS35L56 hibernates, which can cause
|
|
* spurious interrupts, and the interrupt line is normally shared.
|
|
* We can't check if our device is the source of an interrupt, and can't
|
|
* clear it, until it has fully resumed. Prevent this race by temporarily
|
|
* disabling the parent irq until we complete resume().
|
|
*/
|
|
if (cs35l56->base.irq)
|
|
disable_irq(cs35l56->base.irq);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cs35l56_system_resume_no_irq);
|
|
|
|
int cs35l56_system_resume_early(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
int ret;
|
|
|
|
dev_dbg(dev, "system_resume_early\n");
|
|
|
|
/* Ensure a spec-compliant RESET pulse. */
|
|
if (cs35l56->base.reset_gpio) {
|
|
gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
|
|
cs35l56_wait_min_reset_pulse();
|
|
}
|
|
|
|
/* Enable supplies before releasing RESET. */
|
|
ret = regulator_bulk_enable(ARRAY_SIZE(cs35l56->supplies), cs35l56->supplies);
|
|
if (ret) {
|
|
dev_err(dev, "system_resume_early failed to enable supplies: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
/* Release shared RESET before drivers start resume(). */
|
|
gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 1);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cs35l56_system_resume_early);
|
|
|
|
int cs35l56_system_resume(struct device *dev)
|
|
{
|
|
struct cs35l56_private *cs35l56 = dev_get_drvdata(dev);
|
|
int ret;
|
|
|
|
dev_dbg(dev, "system_resume\n");
|
|
|
|
/*
|
|
* We might have done a hard reset or the CS35L56 was power-cycled
|
|
* so wait for control port to be ready.
|
|
*/
|
|
cs35l56_wait_control_port_ready();
|
|
|
|
/* Undo pm_runtime_force_suspend() before re-enabling the irq */
|
|
ret = pm_runtime_force_resume(dev);
|
|
if (cs35l56->base.irq)
|
|
enable_irq(cs35l56->base.irq);
|
|
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Firmware won't have been loaded if the component hasn't probed */
|
|
if (!cs35l56->component)
|
|
return 0;
|
|
|
|
ret = cs35l56_is_fw_reload_needed(&cs35l56->base);
|
|
dev_dbg(cs35l56->base.dev, "fw_reload_needed: %d\n", ret);
|
|
if (ret < 1)
|
|
return ret;
|
|
|
|
cs35l56->base.fw_patched = false;
|
|
wm_adsp_power_down(&cs35l56->dsp);
|
|
queue_work(cs35l56->dsp_wq, &cs35l56->dsp_work);
|
|
|
|
/*
|
|
* suspend_bias_off ensures we are now in BIAS_OFF so there will be
|
|
* a BIAS_OFF->BIAS_STANDBY transition to complete dsp patching.
|
|
*/
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cs35l56_system_resume);
|
|
|
|
static int cs35l56_control_add_nop(struct wm_adsp *dsp, struct cs_dsp_coeff_ctl *cs_ctl)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_dsp_init(struct cs35l56_private *cs35l56)
|
|
{
|
|
struct wm_adsp *dsp;
|
|
int ret;
|
|
|
|
cs35l56->dsp_wq = create_singlethread_workqueue("cs35l56-dsp");
|
|
if (!cs35l56->dsp_wq)
|
|
return -ENOMEM;
|
|
|
|
INIT_WORK(&cs35l56->dsp_work, cs35l56_dsp_work);
|
|
|
|
dsp = &cs35l56->dsp;
|
|
cs35l56_init_cs_dsp(&cs35l56->base, &dsp->cs_dsp);
|
|
|
|
/*
|
|
* dsp->part is filled in later as it is based on the DEVID. In a
|
|
* SoundWire system that cannot be read until enumeration has occurred
|
|
* and the device has attached.
|
|
*/
|
|
dsp->fw = 12;
|
|
dsp->wmfw_optional = true;
|
|
|
|
/*
|
|
* None of the firmware controls need to be exported so add a no-op
|
|
* callback that suppresses creating an ALSA control.
|
|
*/
|
|
dsp->control_add = &cs35l56_control_add_nop;
|
|
|
|
dev_dbg(cs35l56->base.dev, "DSP system name: '%s'\n", dsp->system_name);
|
|
|
|
ret = wm_halo_init(dsp);
|
|
if (ret != 0) {
|
|
dev_err(cs35l56->base.dev, "wm_halo_init failed\n");
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cs35l56_read_fwnode_u32_array(struct device *dev,
|
|
struct fwnode_handle *parent_node,
|
|
const char *prop_name,
|
|
int max_count,
|
|
u32 *dest)
|
|
{
|
|
int count, ret;
|
|
|
|
count = fwnode_property_count_u32(parent_node, prop_name);
|
|
if ((count == 0) || (count == -EINVAL) || (count == -ENODATA)) {
|
|
dev_dbg(dev, "%s not found in %s\n", prop_name, fwnode_get_name(parent_node));
|
|
return 0;
|
|
}
|
|
|
|
if (count < 0) {
|
|
dev_err(dev, "Get %s error:%d\n", prop_name, count);
|
|
return count;
|
|
}
|
|
|
|
if (count > max_count) {
|
|
dev_err(dev, "%s too many entries (%d)\n", prop_name, count);
|
|
return -EOVERFLOW;
|
|
}
|
|
|
|
ret = fwnode_property_read_u32_array(parent_node, prop_name, dest, count);
|
|
if (ret) {
|
|
dev_err(dev, "Error reading %s: %d\n", prop_name, ret);
|
|
return ret;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
static int cs35l56_process_xu_onchip_speaker_id(struct cs35l56_private *cs35l56,
|
|
struct fwnode_handle *ext_node)
|
|
{
|
|
static const char * const gpio_name = "01fa-spk-id-gpios-onchip";
|
|
static const char * const pull_name = "01fa-spk-id-gpios-onchip-pull";
|
|
u32 gpios[5], pulls[5];
|
|
int num_gpios, num_pulls;
|
|
int ret;
|
|
|
|
static_assert(ARRAY_SIZE(gpios) == ARRAY_SIZE(cs35l56->base.onchip_spkid_gpios));
|
|
static_assert(ARRAY_SIZE(pulls) == ARRAY_SIZE(cs35l56->base.onchip_spkid_pulls));
|
|
|
|
num_gpios = cs35l56_read_fwnode_u32_array(cs35l56->base.dev, ext_node, gpio_name,
|
|
ARRAY_SIZE(gpios), gpios);
|
|
if (num_gpios < 1)
|
|
return num_gpios;
|
|
|
|
num_pulls = cs35l56_read_fwnode_u32_array(cs35l56->base.dev, ext_node, pull_name,
|
|
ARRAY_SIZE(pulls), pulls);
|
|
if (num_pulls < 0)
|
|
return num_pulls;
|
|
|
|
if (num_pulls && (num_pulls != num_gpios)) {
|
|
dev_warn(cs35l56->base.dev, "%s count(%d) != %s count(%d)\n",
|
|
pull_name, num_pulls, gpio_name, num_gpios);
|
|
}
|
|
|
|
ret = cs35l56_check_and_save_onchip_spkid_gpios(&cs35l56->base,
|
|
gpios, num_gpios,
|
|
pulls, num_pulls);
|
|
if (ret) {
|
|
return dev_err_probe(cs35l56->base.dev, ret, "Error in %s/%s\n",
|
|
gpio_name, pull_name);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
VISIBLE_IF_KUNIT int cs35l56_process_xu_properties(struct cs35l56_private *cs35l56)
|
|
{
|
|
struct fwnode_handle *ext_node = NULL;
|
|
struct fwnode_handle *link;
|
|
int ret;
|
|
|
|
if (!cs35l56->sdw_peripheral)
|
|
return 0;
|
|
|
|
fwnode_for_each_child_node(dev_fwnode(cs35l56->base.dev), link) {
|
|
ext_node = fwnode_get_named_child_node(link,
|
|
"mipi-sdca-function-expansion-subproperties");
|
|
if (ext_node) {
|
|
fwnode_handle_put(link);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!ext_node)
|
|
return 0;
|
|
|
|
ret = cs35l56_process_xu_onchip_speaker_id(cs35l56, ext_node);
|
|
fwnode_handle_put(ext_node);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_IF_KUNIT(cs35l56_process_xu_properties);
|
|
|
|
static int cs35l56_get_firmware_uid(struct cs35l56_private *cs35l56)
|
|
{
|
|
struct device *dev = cs35l56->base.dev;
|
|
const char *prop;
|
|
int ret;
|
|
|
|
ret = device_property_read_string(dev, "cirrus,firmware-uid", &prop);
|
|
/* If bad sw node property, return 0 and fallback to legacy firmware path */
|
|
if (ret < 0)
|
|
return 0;
|
|
|
|
/* Append a speaker qualifier if there is a speaker ID */
|
|
if (cs35l56->speaker_id >= 0)
|
|
cs35l56->dsp.system_name = devm_kasprintf(dev, GFP_KERNEL, "%s-spkid%d",
|
|
prop, cs35l56->speaker_id);
|
|
else
|
|
cs35l56->dsp.system_name = devm_kstrdup(dev, prop, GFP_KERNEL);
|
|
|
|
if (cs35l56->dsp.system_name == NULL)
|
|
return -ENOMEM;
|
|
|
|
dev_dbg(dev, "Firmware UID: %s\n", cs35l56->dsp.system_name);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Some SoundWire laptops have a spk-id-gpios property but it points to
|
|
* the wrong ACPI Device node so can't be used to get the GPIO. Try to
|
|
* find the SDCA node containing the GpioIo resource and add a GPIO
|
|
* mapping to it.
|
|
*/
|
|
static const struct acpi_gpio_params cs35l56_af01_first_gpio = { 0, 0, false };
|
|
static const struct acpi_gpio_mapping cs35l56_af01_spkid_gpios_mapping[] = {
|
|
{ "spk-id-gpios", &cs35l56_af01_first_gpio, 1 },
|
|
{ }
|
|
};
|
|
|
|
static void cs35l56_acpi_dev_release_driver_gpios(void *adev)
|
|
{
|
|
acpi_dev_remove_driver_gpios(adev);
|
|
}
|
|
|
|
static int cs35l56_try_get_broken_sdca_spkid_gpio(struct cs35l56_private *cs35l56)
|
|
{
|
|
struct fwnode_handle *af01_fwnode;
|
|
const union acpi_object *obj;
|
|
struct gpio_desc *desc;
|
|
int ret;
|
|
|
|
/* Find the SDCA node containing the GpioIo */
|
|
af01_fwnode = device_get_named_child_node(cs35l56->base.dev, "AF01");
|
|
if (!af01_fwnode) {
|
|
dev_dbg(cs35l56->base.dev, "No AF01 node\n");
|
|
return -ENOENT;
|
|
}
|
|
|
|
ret = acpi_dev_get_property(ACPI_COMPANION(cs35l56->base.dev),
|
|
"spk-id-gpios", ACPI_TYPE_PACKAGE, &obj);
|
|
if (ret) {
|
|
dev_dbg(cs35l56->base.dev, "Could not get spk-id-gpios package: %d\n", ret);
|
|
fwnode_handle_put(af01_fwnode);
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* The broken properties we can handle are a 4-element package (one GPIO) */
|
|
if (obj->package.count != 4) {
|
|
dev_warn(cs35l56->base.dev, "Unexpected spk-id element count %d\n",
|
|
obj->package.count);
|
|
fwnode_handle_put(af01_fwnode);
|
|
return -ENOENT;
|
|
}
|
|
|
|
/* Add a GPIO mapping if it doesn't already have one */
|
|
if (!fwnode_property_present(af01_fwnode, "spk-id-gpios")) {
|
|
struct acpi_device *adev = to_acpi_device_node(af01_fwnode);
|
|
|
|
/*
|
|
* Can't use devm_acpi_dev_add_driver_gpios() because the
|
|
* mapping isn't being added to the node pointed to by
|
|
* ACPI_COMPANION().
|
|
*/
|
|
ret = acpi_dev_add_driver_gpios(adev, cs35l56_af01_spkid_gpios_mapping);
|
|
if (ret) {
|
|
fwnode_handle_put(af01_fwnode);
|
|
return dev_err_probe(cs35l56->base.dev, ret,
|
|
"Failed to add gpio mapping to AF01\n");
|
|
}
|
|
|
|
ret = devm_add_action_or_reset(cs35l56->base.dev,
|
|
cs35l56_acpi_dev_release_driver_gpios,
|
|
adev);
|
|
if (ret) {
|
|
fwnode_handle_put(af01_fwnode);
|
|
return ret;
|
|
}
|
|
|
|
dev_dbg(cs35l56->base.dev, "Added spk-id-gpios mapping to AF01\n");
|
|
}
|
|
|
|
desc = fwnode_gpiod_get_index(af01_fwnode, "spk-id", 0, GPIOD_IN, NULL);
|
|
if (IS_ERR(desc)) {
|
|
fwnode_handle_put(af01_fwnode);
|
|
ret = PTR_ERR(desc);
|
|
return dev_err_probe(cs35l56->base.dev, ret, "Get GPIO from AF01 failed\n");
|
|
}
|
|
|
|
ret = gpiod_get_value_cansleep(desc);
|
|
gpiod_put(desc);
|
|
|
|
if (ret < 0) {
|
|
fwnode_handle_put(af01_fwnode);
|
|
dev_err_probe(cs35l56->base.dev, ret, "Error reading spk-id GPIO\n");
|
|
return ret;
|
|
}
|
|
|
|
fwnode_handle_put(af01_fwnode);
|
|
|
|
dev_info(cs35l56->base.dev, "Got spk-id from AF01\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
int cs35l56_common_probe(struct cs35l56_private *cs35l56)
|
|
{
|
|
int ret;
|
|
|
|
init_completion(&cs35l56->init_completion);
|
|
mutex_init(&cs35l56->base.irq_lock);
|
|
cs35l56->base.cal_index = -1;
|
|
cs35l56->speaker_id = -ENOENT;
|
|
|
|
dev_set_drvdata(cs35l56->base.dev, cs35l56);
|
|
|
|
cs35l56_fill_supply_names(cs35l56->supplies);
|
|
ret = devm_regulator_bulk_get(cs35l56->base.dev, ARRAY_SIZE(cs35l56->supplies),
|
|
cs35l56->supplies);
|
|
if (ret != 0)
|
|
return dev_err_probe(cs35l56->base.dev, ret, "Failed to request supplies\n");
|
|
|
|
/* Reset could be controlled by the BIOS or shared by multiple amps */
|
|
cs35l56->base.reset_gpio = devm_gpiod_get_optional(cs35l56->base.dev, "reset",
|
|
GPIOD_OUT_LOW);
|
|
if (IS_ERR(cs35l56->base.reset_gpio)) {
|
|
ret = PTR_ERR(cs35l56->base.reset_gpio);
|
|
/*
|
|
* If RESET is shared the first amp to probe will grab the reset
|
|
* line and reset all the amps
|
|
*/
|
|
if (ret != -EBUSY)
|
|
return dev_err_probe(cs35l56->base.dev, ret, "Failed to get reset GPIO\n");
|
|
|
|
dev_info(cs35l56->base.dev, "Reset GPIO busy, assume shared reset\n");
|
|
cs35l56->base.reset_gpio = NULL;
|
|
}
|
|
|
|
ret = regulator_bulk_enable(ARRAY_SIZE(cs35l56->supplies), cs35l56->supplies);
|
|
if (ret != 0)
|
|
return dev_err_probe(cs35l56->base.dev, ret, "Failed to enable supplies\n");
|
|
|
|
if (cs35l56->base.reset_gpio) {
|
|
/* ACPI can override GPIOD_OUT_LOW flag so force it to start low */
|
|
gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
|
|
cs35l56_wait_min_reset_pulse();
|
|
gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 1);
|
|
}
|
|
|
|
ret = cs35l56_get_speaker_id(&cs35l56->base);
|
|
if (ACPI_COMPANION(cs35l56->base.dev) && cs35l56->sdw_peripheral && (ret == -ENOENT))
|
|
ret = cs35l56_try_get_broken_sdca_spkid_gpio(cs35l56);
|
|
|
|
if ((ret < 0) && (ret != -ENOENT))
|
|
goto err;
|
|
|
|
cs35l56->speaker_id = ret;
|
|
|
|
ret = cs35l56_get_firmware_uid(cs35l56);
|
|
if (ret != 0)
|
|
goto err;
|
|
|
|
ret = cs35l56_process_xu_properties(cs35l56);
|
|
if (ret)
|
|
goto err;
|
|
|
|
ret = cs35l56_dsp_init(cs35l56);
|
|
if (ret < 0) {
|
|
dev_err_probe(cs35l56->base.dev, ret, "DSP init failed\n");
|
|
goto err;
|
|
}
|
|
|
|
ret = devm_snd_soc_register_component(cs35l56->base.dev,
|
|
&soc_component_dev_cs35l56,
|
|
cs35l56_dai, ARRAY_SIZE(cs35l56_dai));
|
|
if (ret < 0) {
|
|
dev_err_probe(cs35l56->base.dev, ret, "Register codec failed\n");
|
|
goto err;
|
|
}
|
|
|
|
return 0;
|
|
|
|
err:
|
|
gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
|
|
regulator_bulk_disable(ARRAY_SIZE(cs35l56->supplies), cs35l56->supplies);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(cs35l56_common_probe, "SND_SOC_CS35L56_CORE");
|
|
|
|
int cs35l56_init(struct cs35l56_private *cs35l56)
|
|
{
|
|
int ret;
|
|
|
|
/*
|
|
* Check whether the actions associated with soft reset or one time
|
|
* init need to be performed.
|
|
*/
|
|
if (cs35l56->soft_resetting)
|
|
goto post_soft_reset;
|
|
|
|
if (cs35l56->base.init_done)
|
|
return 0;
|
|
|
|
pm_runtime_set_autosuspend_delay(cs35l56->base.dev, 100);
|
|
pm_runtime_use_autosuspend(cs35l56->base.dev);
|
|
pm_runtime_set_active(cs35l56->base.dev);
|
|
pm_runtime_enable(cs35l56->base.dev);
|
|
|
|
ret = cs35l56_hw_init(&cs35l56->base);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = cs35l56_set_patch(&cs35l56->base);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = cs35l56_get_calibration(&cs35l56->base);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!cs35l56->base.reset_gpio) {
|
|
dev_dbg(cs35l56->base.dev, "No reset gpio: using soft reset\n");
|
|
cs35l56->soft_resetting = true;
|
|
cs35l56_system_reset(&cs35l56->base, !!cs35l56->sdw_peripheral);
|
|
if (cs35l56->sdw_peripheral) {
|
|
/* Keep alive while we wait for re-enumeration */
|
|
pm_runtime_get_noresume(cs35l56->base.dev);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
post_soft_reset:
|
|
if (cs35l56->soft_resetting) {
|
|
cs35l56->soft_resetting = false;
|
|
|
|
/* Done re-enumerating after one-time init so release the keep-alive */
|
|
if (cs35l56->sdw_peripheral && !cs35l56->base.init_done)
|
|
pm_runtime_put_noidle(cs35l56->base.dev);
|
|
|
|
regcache_mark_dirty(cs35l56->base.regmap);
|
|
ret = cs35l56_wait_for_firmware_boot(&cs35l56->base);
|
|
if (ret)
|
|
return ret;
|
|
|
|
dev_dbg(cs35l56->base.dev, "Firmware rebooted after soft reset\n");
|
|
|
|
regcache_cache_only(cs35l56->base.regmap, false);
|
|
}
|
|
|
|
/* Disable auto-hibernate so that runtime_pm has control */
|
|
ret = cs35l56_mbox_send(&cs35l56->base, CS35L56_MBOX_CMD_PREVENT_AUTO_HIBERNATE);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Registers could be dirty after soft reset or SoundWire enumeration */
|
|
regcache_sync(cs35l56->base.regmap);
|
|
|
|
/* Set ASP1 DOUT to high-impedance when it is not transmitting audio data. */
|
|
ret = regmap_set_bits(cs35l56->base.regmap, CS35L56_ASP1_CONTROL3,
|
|
CS35L56_ASP1_DOUT_HIZ_CTRL_MASK);
|
|
if (ret)
|
|
return dev_err_probe(cs35l56->base.dev, ret, "Failed to write ASP1_CONTROL3\n");
|
|
|
|
cs35l56->base.init_done = true;
|
|
complete(&cs35l56->init_completion);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(cs35l56_init, "SND_SOC_CS35L56_CORE");
|
|
|
|
void cs35l56_remove(struct cs35l56_private *cs35l56)
|
|
{
|
|
cs35l56->base.init_done = false;
|
|
|
|
/*
|
|
* WAKE IRQs unmask if CS35L56 hibernates so free the handler to
|
|
* prevent it racing with remove().
|
|
*/
|
|
if (cs35l56->base.irq)
|
|
devm_free_irq(cs35l56->base.dev, cs35l56->base.irq, &cs35l56->base);
|
|
|
|
destroy_workqueue(cs35l56->dsp_wq);
|
|
|
|
pm_runtime_dont_use_autosuspend(cs35l56->base.dev);
|
|
pm_runtime_suspend(cs35l56->base.dev);
|
|
pm_runtime_disable(cs35l56->base.dev);
|
|
|
|
regcache_cache_only(cs35l56->base.regmap, true);
|
|
|
|
gpiod_set_value_cansleep(cs35l56->base.reset_gpio, 0);
|
|
regulator_bulk_disable(ARRAY_SIZE(cs35l56->supplies), cs35l56->supplies);
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(cs35l56_remove, "SND_SOC_CS35L56_CORE");
|
|
|
|
#if IS_ENABLED(CONFIG_SND_SOC_CS35L56_I2C) || IS_ENABLED(CONFIG_SND_SOC_CS35L56_SPI)
|
|
EXPORT_NS_GPL_DEV_PM_OPS(cs35l56_pm_ops_i2c_spi, SND_SOC_CS35L56_CORE) = {
|
|
SET_RUNTIME_PM_OPS(cs35l56_runtime_suspend_i2c_spi, cs35l56_runtime_resume_i2c_spi, NULL)
|
|
SYSTEM_SLEEP_PM_OPS(cs35l56_system_suspend, cs35l56_system_resume)
|
|
LATE_SYSTEM_SLEEP_PM_OPS(cs35l56_system_suspend_late, cs35l56_system_resume_early)
|
|
NOIRQ_SYSTEM_SLEEP_PM_OPS(cs35l56_system_suspend_no_irq, cs35l56_system_resume_no_irq)
|
|
};
|
|
#endif
|
|
|
|
MODULE_DESCRIPTION("ASoC CS35L56 driver");
|
|
MODULE_IMPORT_NS("SND_SOC_CS35L56_SHARED");
|
|
MODULE_IMPORT_NS("SND_SOC_CS_AMP_LIB");
|
|
MODULE_AUTHOR("Richard Fitzgerald <rf@opensource.cirrus.com>");
|
|
MODULE_AUTHOR("Simon Trimmer <simont@opensource.cirrus.com>");
|
|
MODULE_LICENSE("GPL");
|