All of lore.kernel.org
 help / color / mirror / Atom feed
From: Pavel Machek <pavel@ucw.cz>
To: Mark Brown <broonie@kernel.org>
Cc: peter.ujfalusi@ti.com, lgirdwood@gmail.com, tony@atomide.com,
	perex@perex.cz, linux-kernel@vger.kernel.org, tiwai@suse.com,
	alsa-devel@alsa-project.org, kuninori.morimoto.gx@renesas.com,
	Sebastian Reichel <sre@kernel.org>,
	"Arthur D." <spinal.by@gmail.com>,
	Merlijn Wajer <merlijn@wizzup.org>,
	phone-devel@vger.kernel.org
Subject: ASoC: audio-graph-card: Add audio mixer for Motorola mdm6600
Date: Sat, 23 Jan 2021 19:28:33 +0100	[thread overview]
Message-ID: <20210123182833.GB14771@duo.ucw.cz> (raw)
In-Reply-To: <161118753482.45718.9232559568095752872.b4-ty@kernel.org>

[-- Attachment #1: Type: text/plain, Size: 26670 bytes --]

motmdm.c handles audio configuration on "gsmtty2"; it needs to know
whether we are in call or not; that part is in motmdm-state.c and
listens on "gsmtty1".

To configure Alsamixer for voice calls do for example:
    
Speaker Right -> Voice
Call Noise Cancellation -> Unmute
Call Output -> Speakerphone
Call -> 100
Mic2 -> 40
Left -> Mic 2
Voice -> 55
Mic2 -> 40
Left -> Mic 2

Tony wrote original version using custom interface to n_gsm, Pavel
switched it to plain serdev and split it to two drivers to be easier
to debug and understand. Credit is Tony's, bugs are probably Pavel's.
    
Signed-off-by: Pavel Machek <pavel@ucw.cz>
Co-authored-by: Tony Lindgren <tony@atomide.com>

diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index 946a70210f49..ee4e994f145e 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -902,6 +902,14 @@ config SND_SOC_MAX9860
 	depends on I2C
 	select REGMAP_I2C
 
+config SND_SOC_MOTMDM
+	tristate "Motorola Modem TS 27.010 Voice Call Codec"
+	depends on SERIAL_DEV_BUS
+	help
+	  Enable support for Motorola TS 27.010 serdev voice
+	  call codec driver for Motorola Mapphone series of
+	  devices such as Droid 4.
+
 config SND_SOC_MSM8916_WCD_ANALOG
 	tristate "Qualcomm MSM8916 WCD Analog Codec"
 	depends on SPMI || COMPILE_TEST
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index 0140c60db695..e02bacd7fb3d 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -122,6 +122,8 @@ snd-soc-max9850-objs := max9850.o
 snd-soc-max9860-objs := max9860.o
 snd-soc-mc13783-objs := mc13783.o
 snd-soc-ml26124-objs := ml26124.o
+snd-soc-motmdm-objs := motmdm.o
+snd-soc-motmdm-state-objs := motmdm-state.o
 snd-soc-msm8916-analog-objs := msm8916-wcd-analog.o
 snd-soc-msm8916-digital-objs := msm8916-wcd-digital.o
 snd-soc-mt6351-objs := mt6351.o
@@ -427,6 +429,8 @@ obj-$(CONFIG_SND_SOC_MAX9850)	+= snd-soc-max9850.o
 obj-$(CONFIG_SND_SOC_MAX9860)	+= snd-soc-max9860.o
 obj-$(CONFIG_SND_SOC_MC13783)	+= snd-soc-mc13783.o
 obj-$(CONFIG_SND_SOC_ML26124)	+= snd-soc-ml26124.o
+obj-$(CONFIG_SND_SOC_MOTMDM)	+= snd-soc-motmdm.o
+obj-$(CONFIG_SND_SOC_MOTMDM)	+= snd-soc-motmdm-state.o
 obj-$(CONFIG_SND_SOC_MSM8916_WCD_ANALOG) +=snd-soc-msm8916-analog.o
 obj-$(CONFIG_SND_SOC_MSM8916_WCD_DIGITAL) +=snd-soc-msm8916-digital.o
 obj-$(CONFIG_SND_SOC_MT6351)	+= snd-soc-mt6351.o
diff --git a/sound/soc/codecs/motmdm-state.c b/sound/soc/codecs/motmdm-state.c
new file mode 100644
index 000000000000..d02e89e4498b
--- /dev/null
+++ b/sound/soc/codecs/motmdm-state.c
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Motorola Mapphone MDM6600 voice call audio support
+ * Copyright 2018 - 2020 Tony Lindgren <tony@atomide.com>
+ * Copyright 2020 - 2021 Pavel Machek <pavel@ucw.cz>
+ *
+ * Designed to provide notifications about voice call state to the
+ * motmdm.c driver. This one listens on "gsmtty1".
+ */
+
+#include <linux/init.h>
+#include <linux/kfifo.h>
+#include <linux/module.h>
+#include <linux/of_graph.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/serdev.h>
+#include <linux/serdev-gsm.h>
+
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+#define MOTMDM_HEADER_LEN	5			/* U1234 */
+#define MOTMDM_AUDIO_MAX_LEN	128
+#define MOTMDM_VOICE_RESP_LEN	7			/* U1234~+CIEV= */
+
+struct motmdm_driver_data {
+	struct serdev_device *serdev;
+	unsigned char *buf;
+	size_t len;
+	spinlock_t lock;	/* enable/disabled lock */
+};
+
+static BLOCKING_NOTIFIER_HEAD(modem_state_chain_head);
+
+int register_modem_state_notifier(struct notifier_block *nb)
+{
+        return blocking_notifier_chain_register(&modem_state_chain_head, nb);
+}
+EXPORT_SYMBOL_GPL(register_modem_state_notifier);
+
+int unregister_modem_state_notifier(struct notifier_block *nb)
+{
+	return blocking_notifier_chain_unregister(&modem_state_chain_head, nb);
+}
+EXPORT_SYMBOL_GPL(unregister_modem_state_notifier);
+
+static int modem_state_notifier_call_chain(unsigned long val)
+{
+        int ret;
+        ret = __blocking_notifier_call_chain(&modem_state_chain_head, val, NULL,
+					     -1, NULL);
+        return notifier_to_errno(ret);
+}
+
+/* Parses the voice call state from unsolicited notifications on dlci1 */
+static int motmdm_voice_get_state(struct motmdm_driver_data *ddata,
+				   const unsigned char *buf,
+				   size_t len)
+{
+	struct device *dev = &ddata->serdev->dev;
+	bool enable;
+	const unsigned char *state;
+
+	if (len < MOTMDM_HEADER_LEN + MOTMDM_VOICE_RESP_LEN + 5)
+		return 0;
+
+	/* We only care about the unsolicted messages */
+	if (buf[MOTMDM_HEADER_LEN] != '~')
+		return 0;
+
+	if (strncmp(buf + MOTMDM_HEADER_LEN + 1, "+CIEV=", 6))
+		return len;
+
+	state = buf + MOTMDM_HEADER_LEN + MOTMDM_VOICE_RESP_LEN;
+	dev_info(dev, "%s: ciev=%5s\n", __func__, state);
+
+	if (!strncmp(state, "1,1,0", 5) ||	/* connecting */
+	    !strncmp(state, "1,4,0", 5) ||	/* incoming call */
+	    !strncmp(state, "1,2,0", 5))	/* connected */
+		enable = true;
+	else if (!strncmp(state, "1,0,0", 5) ||	/* disconnected */
+		!strncmp(state, "1,0,2", 5))	/* call failed */
+		enable = false;
+	else
+		return len;
+
+	modem_state_notifier_call_chain(enable);
+	return len;
+}
+
+static int voice_receive_data(struct serdev_device *serdev,
+			     const unsigned char *buf,
+			     size_t len)
+{
+        struct motmdm_driver_data *ddata = serdev_device_get_drvdata(serdev);
+
+	if (len > MOTMDM_AUDIO_MAX_LEN)
+		len = MOTMDM_AUDIO_MAX_LEN;
+
+	if (len <= MOTMDM_HEADER_LEN)
+		return 0;
+
+	if (buf[MOTMDM_HEADER_LEN] == '~')
+		motmdm_voice_get_state(ddata, buf, len);
+
+	return len;
+}
+
+static const struct serdev_device_ops voice_serdev_ops = {
+        .receive_buf    = voice_receive_data,
+        .write_wakeup   = serdev_device_write_wakeup,
+};
+
+static void motmdm_free_voice_serdev(struct motmdm_driver_data *ddata)
+{
+	serdev_device_close(ddata->serdev);
+}
+
+static int motmdm_soc_probe(struct serdev_device *serdev)
+{
+	struct device *dev = &serdev->dev;
+	struct motmdm_driver_data *ddata;
+	int error;
+
+	ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL);
+	if (!ddata)
+		return -ENOMEM;
+
+	ddata->serdev = serdev;
+	spin_lock_init(&ddata->lock);
+	ddata->len = MOTMDM_AUDIO_MAX_LEN;
+
+	ddata->buf = devm_kzalloc(dev, ddata->len, GFP_KERNEL);
+	if (!ddata->buf)
+		return -ENOMEM;
+
+	serdev_device_set_drvdata(ddata->serdev, ddata);
+        serdev_device_set_client_ops(ddata->serdev, &voice_serdev_ops);
+
+        error = serdev_device_open(ddata->serdev);
+	return error;
+}
+
+static void motmdm_state_remove(struct serdev_device *serdev)
+{
+	struct motmdm_driver_data *ddata = serdev_device_get_drvdata(serdev);
+
+	motmdm_free_voice_serdev(ddata);
+}
+
+static int motmdm_state_probe(struct serdev_device *serdev)
+{
+	return motmdm_soc_probe(serdev);
+}
+
+#ifdef CONFIG_OF
+static const struct of_device_id motmdm_of_match[] = {
+	{ .compatible = "motorola,mapphone-mdm6600-modem" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, motmdm_of_match);
+#endif
+
+static struct serdev_device_driver motmdm_state_driver = {
+	.driver	= {
+		.name		= "mot-mdm6600-modem",
+		.of_match_table	= of_match_ptr(motmdm_of_match),
+	},
+	.probe	= motmdm_state_probe,
+	.remove	= motmdm_state_remove,
+};
+module_serdev_device_driver(motmdm_state_driver);
+
+MODULE_ALIAS("platform:motmdm-state");
+MODULE_DESCRIPTION("Motorola Mapphone MDM6600 modem state driver");
+MODULE_AUTHOR("Pavel Machek <pavel@ucw.cz>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/codecs/motmdm.c b/sound/soc/codecs/motmdm.c
new file mode 100644
index 000000000000..9cba6006e4c8
--- /dev/null
+++ b/sound/soc/codecs/motmdm.c
@@ -0,0 +1,689 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Motorola Mapphone MDM6600 voice call audio support
+ * Copyright 2018 - 2020 Tony Lindgren <tony@atomide.com>
+ * Copyright 2020 - 2021 Pavel Machek <pavel@ucw.cz>
+ *
+ * This one handles audio configuration on "gsmtty2"; it needs to know
+ * whether we are in call or not, and that part is in motmdm-state.c
+ * and listens on "gsmtty1".
+ */
+
+#include <linux/init.h>
+#include <linux/kfifo.h>
+#include <linux/module.h>
+#include <linux/of_graph.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/serdev.h>
+#include <linux/serdev-gsm.h>
+
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+#include "motmdm-state.h"
+
+#define MOTMDM_HEADER_LEN	5			/* U1234 */
+
+#define MOTMDM_AUDIO_RESP_LEN	6			/* U1234+XXXX= */
+#define MOTMDM_AUDIO_MAX_LEN	128
+
+#define MOTMDM_VOICE_RESP_LEN	7			/* U1234~+CIEV= */
+
+struct motmdm_driver_data {
+	struct notifier_block notifier;
+	struct snd_soc_component *component;
+	struct snd_soc_dai *master_dai;
+	struct device *modem;
+	struct serdev_device *serdev;
+	struct regmap *regmap;
+	unsigned char *buf;
+	size_t len;
+	unsigned int parsed:1;
+	unsigned int enabled:1;
+	spinlock_t lock;	/* enable/disabled lock */
+	struct mutex mutex;	/* for sending commands */
+	wait_queue_head_t read_queue;
+
+	unsigned int dtmf_val;
+	unsigned int dtmf_en;
+};
+
+enum motmdm_cmd {
+	CMD_AT_EACC,
+	CMD_AT_CLVL,
+	CMD_AT_NREC,
+};
+
+const char * const motmd_read_fmt[] = {
+	[CMD_AT_EACC] = "AT+EACC?",
+	[CMD_AT_CLVL] = "AT+CLVL?",
+	[CMD_AT_NREC] = "AT+NREC?",
+};
+
+const char * const motmd_write_fmt[] = {
+	[CMD_AT_EACC] = "AT+EACC=%u,0",
+	[CMD_AT_CLVL] = "AT+CLVL=%u",
+	[CMD_AT_NREC] = "AT+NREC=%u",
+};
+
+/*
+ * Currently unconfigured additional inactive (error producing) options
+ * seem to be:
+ * "TTY Headset", "HCQ Headset", "VCQ Headset", "No-Mic Headset",
+ * "Handset Fluence Med", "Handset Fluence Low", "Car Dock", "Lapdock"
+ */
+static const char * const motmdm_out_mux_texts[] = {
+	"Handset", "Headset", "Speakerphone", "Bluetooth",
+};
+
+static SOC_ENUM_SINGLE_EXT_DECL(motmdm_out_enum, motmdm_out_mux_texts);
+
+static const DECLARE_TLV_DB_SCALE(motmdm_gain_tlv, 0, 100, 0);
+
+int motmdm_send_command(struct motmdm_driver_data *ddata,
+			     const u8 *buf, int len)
+{
+	struct device *dev = ddata->component->dev;
+	const int timeout_ms = 1000;
+	unsigned char cmd[MOTMDM_AUDIO_MAX_LEN];
+	int ret, cmdlen;
+
+	cmdlen = len + 5 + 1;
+	if (cmdlen > MOTMDM_AUDIO_MAX_LEN)
+		return -EINVAL;
+
+	mutex_lock(&ddata->mutex);
+	memset(ddata->buf, 0, ddata->len);
+	ddata->parsed = false;
+	snprintf(cmd, cmdlen, "U%04li%s", jiffies % 10000, buf);
+
+	ret = serdev_device_write(ddata->serdev, cmd, cmdlen, MAX_SCHEDULE_TIMEOUT);
+	if (ret < 0)
+		goto out_unlock;
+
+        serdev_device_wait_until_sent(ddata->serdev, 0);
+
+	ret = wait_event_timeout(ddata->read_queue, ddata->parsed,
+				 msecs_to_jiffies(timeout_ms));
+	if (ret == 0) {
+		ret = -ETIMEDOUT;
+		goto out_unlock;
+	} else if (ret < 0) {
+		goto out_unlock;
+	}
+
+	if (strstr(ddata->buf, "ERROR")) {
+		dev_err(dev, "command %s error %s\n", cmd, ddata->buf);
+		ret = -EPIPE;
+	}
+
+	ret = len;
+
+out_unlock:
+	mutex_unlock(&ddata->mutex);
+	printk("send_command -- ret %d\n", ret);
+
+	return ret;
+}
+
+static int motmdm_read_reg(void *context, unsigned int reg,
+			   unsigned int *value)
+{
+	struct snd_soc_component *component = context;
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	const unsigned char *cmd;
+	unsigned int val;
+	int error;
+
+	cmd = motmd_read_fmt[reg];
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0) {
+		dev_err(component->dev, "%s: %s failed with %i\n",
+			__func__, cmd, error);
+
+		return error;
+	}
+
+	error = kstrtouint(ddata->buf + MOTMDM_AUDIO_RESP_LEN, 0, &val);
+	if (error)
+		return -ENODEV;
+
+	*value = val;
+
+	return error;
+}
+
+static int motmdm_write_reg(void *context, unsigned int reg,
+			    unsigned int value)
+{
+	struct snd_soc_component *component = context;
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	const unsigned char *fmt, *cmd;
+	int error;
+
+	fmt = motmd_write_fmt[reg];
+	cmd = kasprintf(GFP_KERNEL, fmt, value);
+	if (!cmd) {
+		error = -ENOMEM;
+		goto free;
+	}
+
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0)
+		dev_err(component->dev, "%s: %s failed with %i\n",
+			__func__, cmd, error);
+
+free:
+	kfree(cmd);
+
+	return error;
+}
+
+static const struct reg_default motmdm_reg_defaults[] = {
+	{ CMD_AT_EACC, 0x0 },
+	{ CMD_AT_CLVL, 0x0 },
+};
+
+static const struct regmap_config motmdm_regmap = {
+	.reg_bits = 32,
+	.reg_stride = 1,
+	.val_bits = 32,
+	.max_register = CMD_AT_NREC,
+	.reg_defaults = motmdm_reg_defaults,
+	.num_reg_defaults = ARRAY_SIZE(motmdm_reg_defaults),
+	.cache_type = REGCACHE_RBTREE,
+	.reg_read = motmdm_read_reg,
+	.reg_write = motmdm_write_reg,
+};
+
+static int motmdm_value_get(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_value *ucontrol,
+			    enum motmdm_cmd reg,
+			    int cmd_base)
+{
+	struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	unsigned int val;
+	int error;
+
+	error = regmap_read(ddata->regmap, reg, &val);
+	if (error)
+		return error;
+
+	if (val >= cmd_base)
+		val -= cmd_base;
+
+	ucontrol->value.enumerated.item[0] = val;
+
+	return 0;
+}
+
+static int motmdm_value_put(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_value *ucontrol,
+			    enum motmdm_cmd reg,
+			    int cmd_base)
+{
+	struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	int error;
+
+	error = regmap_write(ddata->regmap, reg,
+			     ucontrol->value.enumerated.item[0] + cmd_base);
+	if (error)
+		return error;
+
+	regcache_mark_dirty(ddata->regmap);
+
+	return error;
+}
+
+static int motmdm_audio_out_get(struct snd_kcontrol *kcontrol,
+				     struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_get(kcontrol, ucontrol, CMD_AT_EACC, 1);
+}
+
+static int motmdm_audio_out_put(struct snd_kcontrol *kcontrol,
+				     struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_put(kcontrol, ucontrol, CMD_AT_EACC, 1);
+}
+
+static int motmdm_gain_get(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_get(kcontrol, ucontrol, CMD_AT_CLVL, 0);
+}
+
+static int motmdm_gain_put(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_put(kcontrol, ucontrol, CMD_AT_CLVL, 0);
+}
+
+static int motmdm_noise_get(struct snd_kcontrol *kcontrol,
+				 struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_get(kcontrol, ucontrol, CMD_AT_NREC, 0);
+}
+
+static int motmdm_noise_put(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_put(kcontrol, ucontrol, CMD_AT_NREC, 0);
+}
+
+static const char * const motmdm_tonegen_dtmf_key_txt[] = {
+	"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D",
+	"*", "#"
+};
+
+static SOC_ENUM_SINGLE_EXT_DECL(motmd_tonegen_dtmf_enum,
+				motmdm_tonegen_dtmf_key_txt);
+
+static int motmdm_dtmf_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+
+	ucontrol->value.enumerated.item[0] = ddata->dtmf_val;
+
+	return 0;
+}
+
+static int motmdm_dtmf_put(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+
+	ddata->dtmf_val = ucontrol->value.enumerated.item[0];
+
+	return 0;
+}
+
+static int motmdm_tonegen_dtmf_send_get(struct snd_kcontrol *kcontrol,
+					struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+
+	ucontrol->value.enumerated.item[0] = ddata->dtmf_en;
+
+	return 0;
+}
+
+static int motmdm_tonegen_dtmf_send_put(struct snd_kcontrol *kcontrol,
+					struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	const unsigned char *cmd, *fmt = "AT+DTSE=%s,%i";
+	const char *tone = "";
+	int error;
+
+	if (!ddata->enabled)
+		return 0;
+
+	ddata->dtmf_en = ucontrol->value.enumerated.item[0];
+	if (ddata->dtmf_en)
+		tone = motmdm_tonegen_dtmf_key_txt[ddata->dtmf_val];
+
+	/* Value 0 enables tone generator, 1 disables it */
+	cmd = kasprintf(GFP_KERNEL, fmt, tone, !ddata->dtmf_en);
+
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0) {
+		dev_err(component->dev, "%s: %s failed with %i\n",
+			__func__, cmd, error);
+		goto free;
+	}
+
+free:
+	kfree(cmd);
+
+	return error;
+}
+
+static int
+motmdm_enable_primary_dai(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	int error;
+
+	if (!ddata->master_dai)
+		return -ENODEV;
+
+	error = snd_soc_dai_set_sysclk(ddata->master_dai, 1, 19200000,
+				       SND_SOC_CLOCK_OUT);
+	if (error)
+		return error;
+
+	error = snd_soc_dai_set_fmt(ddata->master_dai,
+				    SND_SOC_DAIFMT_I2S |
+				    SND_SOC_DAIFMT_NB_NF |
+				    SND_SOC_DAIFMT_CBM_CFM);
+	if (error)
+		return error;
+
+	error = snd_soc_dai_set_tdm_slot(ddata->master_dai, 0, 1, 1, 8);
+	if (error)
+		return error;
+
+	return error;
+}
+
+static int
+motmdm_disable_primary_dai(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	int error;
+
+	if (!ddata->master_dai) {
+		return -ENODEV;
+	}
+
+	error = snd_soc_dai_set_sysclk(ddata->master_dai, 0, 26000000,
+				       SND_SOC_CLOCK_OUT);
+	if (error) {
+		return error;
+	}
+
+	error = snd_soc_dai_set_fmt(ddata->master_dai,
+				    SND_SOC_DAIFMT_CBM_CFM);
+	if (error) {
+		return error;
+	}
+
+	error = snd_soc_dai_set_tdm_slot(ddata->master_dai, 0, 0, 0, 48);
+	if (error) {
+		return error;
+	}
+
+	return error;
+}
+
+static int motmdm_find_primary_dai(struct snd_soc_component *component,
+	const char *name)
+{
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	struct device_node *bitclkmaster = NULL, *framemaster = NULL;
+	struct device_node *ep, *master_ep, *master = NULL;
+	struct snd_soc_dai_link_component dlc = { 0 };
+	unsigned int daifmt;
+
+	ep = of_graph_get_next_endpoint(component->dev->of_node, NULL);
+	if (!ep)
+		return -ENODEV;
+
+	master_ep = of_graph_get_remote_endpoint(ep);
+	of_node_put(ep);
+	if (!master_ep)
+		return -ENODEV;
+
+	daifmt = snd_soc_of_parse_daifmt(master_ep, NULL,
+					 &bitclkmaster, &framemaster);
+	of_node_put(master_ep);
+	if (bitclkmaster && framemaster)
+		master = of_graph_get_port_parent(bitclkmaster);
+	of_node_put(bitclkmaster);
+	of_node_put(framemaster);
+	if (!master)
+		return -ENODEV;
+
+	dlc.of_node = master;
+	dlc.dai_name = name;
+	ddata->master_dai = snd_soc_find_dai(&dlc);
+	of_node_put(master);
+	if (!ddata->master_dai)
+		return -EPROBE_DEFER;
+
+	dev_info(component->dev, "Master DAI is %s\n",
+		 dev_name(ddata->master_dai->dev));
+
+	return 0;
+}
+
+static int motmdm_parse_tdm(struct snd_soc_component *component)
+{
+	return motmdm_find_primary_dai(component, "cpcap-voice");
+}
+
+static const struct snd_kcontrol_new motmdm_snd_controls[] = {
+        SOC_ENUM_EXT("Call Output", motmdm_out_enum,
+                     motmdm_audio_out_get,
+                     motmdm_audio_out_put),
+        SOC_SINGLE_EXT_TLV("Call Volume",
+			   0, 0, 7, 0,
+			   motmdm_gain_get,
+			   motmdm_gain_put,
+			   motmdm_gain_tlv),
+	SOC_SINGLE_BOOL_EXT("Call Noise Cancellation", 0,
+			    motmdm_noise_get,
+			    motmdm_noise_put),
+	SOC_ENUM_EXT("Call DTMF", motmd_tonegen_dtmf_enum,
+		     motmdm_dtmf_get,
+		     motmdm_dtmf_put),
+	SOC_SINGLE_BOOL_EXT("Call DTMF Send", 0,
+			    motmdm_tonegen_dtmf_send_get,
+			    motmdm_tonegen_dtmf_send_put),
+};
+
+static struct snd_soc_dai_driver motmdm_dai[] = {
+	{
+		.name = "mdm-call",
+		.playback = {
+			.stream_name = "Voice Call Playback",
+			.channels_min = 1,
+			.channels_max = 2,
+			.rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000,
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+		.capture = {
+			.stream_name = "Voice Call Capture",
+			.channels_min = 1,
+			.channels_max = 2,
+			.rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000,
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+	},
+};
+
+static int
+motmdm_notifier_call(struct notifier_block *bl, unsigned long state,
+                                                        void *unused)
+{
+	struct motmdm_driver_data *ddata =
+		container_of(bl, struct motmdm_driver_data, notifier);
+	bool enable, notify = false;
+	unsigned long flags;
+
+	enable = !!state;
+
+	spin_lock_irqsave(&ddata->lock, flags);
+	if (ddata->enabled != enable) {
+		ddata->enabled = enable;
+		notify = true;
+	}
+	spin_unlock_irqrestore(&ddata->lock, flags);
+
+	if (!notify)
+		return NOTIFY_DONE;
+
+	if (enable)
+		motmdm_enable_primary_dai(ddata->component);
+	else
+		motmdm_disable_primary_dai(ddata->component);
+
+	return NOTIFY_DONE;
+}
+
+static int voice_receive_data(struct serdev_device *serdev,
+			     const unsigned char *buf,
+			     size_t len)
+{
+        struct motmdm_driver_data *ddata = serdev_device_get_drvdata(serdev);
+	struct device *dev = ddata->component->dev;
+
+	printk("voice_receive_data: have %s %d\n", buf, len);
+
+	if (len > MOTMDM_AUDIO_MAX_LEN)
+		len = MOTMDM_AUDIO_MAX_LEN;
+
+	if (len <= MOTMDM_HEADER_LEN)
+		return 0;
+
+	printk("voice_receive_data: command reply? -- %s %d\n", buf, len);
+
+	snprintf(ddata->buf, len - MOTMDM_HEADER_LEN, buf + MOTMDM_HEADER_LEN);
+	dev_info(dev, "%s: received: %s\n", __func__, ddata->buf);
+	ddata->parsed = true;
+	wake_up(&ddata->read_queue);
+
+	return len;
+}
+
+static const struct serdev_device_ops voice_serdev_ops = {
+        .receive_buf    = voice_receive_data,
+        .write_wakeup   = serdev_device_write_wakeup,
+};
+
+static void motmdm_free_voice_serdev(struct motmdm_driver_data *ddata)
+{
+	serdev_device_close(ddata->serdev);
+}
+
+static int motmdm_soc_probe(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata;
+	const unsigned char *cmd = "AT+CMUT=0";
+	int error;
+	u32 line;
+
+	ddata = devm_kzalloc(component->dev, sizeof(*ddata), GFP_KERNEL);
+	if (!ddata)
+		return -ENOMEM;
+
+	error = of_property_read_u32(component->dev->of_node, "reg", &line);
+	if (error)
+		return error;
+
+	ddata->serdev = (struct serdev_device *) component->dev;
+	ddata->component = component;
+	ddata->modem = component->dev->parent;
+	mutex_init(&ddata->mutex);
+	init_waitqueue_head(&ddata->read_queue);
+	ddata->len = PAGE_SIZE;
+	spin_lock_init(&ddata->lock);
+	snd_soc_component_set_drvdata(component, ddata);
+	ddata->len = MOTMDM_AUDIO_MAX_LEN;
+
+	ddata->buf = devm_kzalloc(component->dev, ddata->len, GFP_KERNEL);
+	if (!ddata->buf)
+		return -ENOMEM;
+
+	ddata->regmap = devm_regmap_init(component->dev, NULL, component,
+					 &motmdm_regmap);
+	if (IS_ERR(ddata->regmap)) {
+		error = PTR_ERR(ddata->regmap);
+		dev_err(component->dev, "%s: Failed to allocate regmap: %d\n",
+			__func__, error);
+
+		return error;
+	}
+
+	serdev_device_set_drvdata(ddata->serdev, ddata);
+        serdev_device_set_client_ops(ddata->serdev, &voice_serdev_ops);
+
+        error = serdev_device_open(ddata->serdev);
+        if (error)
+                return error;
+
+	error = motmdm_parse_tdm(component);
+	if (error)
+		goto unregister_serdev;
+
+	regcache_sync(ddata->regmap);
+
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0)
+		goto unregister_serdev;
+
+	error = motmdm_disable_primary_dai(ddata->component);
+	if (error)
+		goto unregister_serdev;
+
+	ddata->notifier.notifier_call = motmdm_notifier_call;
+	register_modem_state_notifier(&ddata->notifier);
+
+	return 0;
+
+unregister_serdev:
+	motmdm_free_voice_serdev(ddata);
+	serdev_device_close(ddata->serdev);
+
+	return error;
+}
+
+static void motmdm_soc_remove(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+
+	unregister_modem_state_notifier(&ddata->notifier);
+
+	motmdm_free_voice_serdev(ddata);
+}
+
+static struct snd_soc_component_driver soc_codec_dev_motmdm = {
+	.probe = motmdm_soc_probe,
+	.remove = motmdm_soc_remove,
+	.controls = motmdm_snd_controls,
+	.num_controls = ARRAY_SIZE(motmdm_snd_controls),
+	.idle_bias_on = 1,
+	.use_pmdown_time = 1,
+	.endianness = 1,
+	.non_legacy_dai_naming = 1,
+};
+
+static int motmdm_codec_probe(struct serdev_device *serdev)
+{
+	return devm_snd_soc_register_component(&serdev->dev,
+					       &soc_codec_dev_motmdm,
+					       motmdm_dai,
+					       ARRAY_SIZE(motmdm_dai));
+}
+
+#ifdef CONFIG_OF
+static const struct of_device_id motmdm_of_match[] = {
+	{ .compatible = "motorola,mapphone-mdm6600-codec" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, motmdm_of_match);
+#endif
+
+static struct serdev_device_driver motmdm_driver = {
+	.probe = motmdm_codec_probe,
+	.driver = {
+		.name = "mot-mdm6600-codec",
+		.of_match_table = of_match_ptr(motmdm_of_match),
+	},
+};
+module_serdev_device_driver(motmdm_driver);
+
+MODULE_ALIAS("platform:motmdm-codec");
+MODULE_DESCRIPTION("ASoC Motorola Mapphone MDM6600 codec driver");
+MODULE_AUTHOR("Tony Lindgren <tony@atomide.com>");
+MODULE_LICENSE("GPL v2");

-- 
http://www.livejournal.com/~pavelmachek

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 195 bytes --]

WARNING: multiple messages have this Message-ID (diff)
From: Pavel Machek <pavel@ucw.cz>
To: Mark Brown <broonie@kernel.org>
Cc: alsa-devel@alsa-project.org, phone-devel@vger.kernel.org,
	kuninori.morimoto.gx@renesas.com, tony@atomide.com,
	linux-kernel@vger.kernel.org, Merlijn Wajer <merlijn@wizzup.org>,
	tiwai@suse.com, lgirdwood@gmail.com, peter.ujfalusi@ti.com,
	Sebastian Reichel <sre@kernel.org>,
	"Arthur D." <spinal.by@gmail.com>
Subject: ASoC: audio-graph-card: Add audio mixer for Motorola mdm6600
Date: Sat, 23 Jan 2021 19:28:33 +0100	[thread overview]
Message-ID: <20210123182833.GB14771@duo.ucw.cz> (raw)
In-Reply-To: <161118753482.45718.9232559568095752872.b4-ty@kernel.org>

[-- Attachment #1: Type: text/plain, Size: 26670 bytes --]

motmdm.c handles audio configuration on "gsmtty2"; it needs to know
whether we are in call or not; that part is in motmdm-state.c and
listens on "gsmtty1".

To configure Alsamixer for voice calls do for example:
    
Speaker Right -> Voice
Call Noise Cancellation -> Unmute
Call Output -> Speakerphone
Call -> 100
Mic2 -> 40
Left -> Mic 2
Voice -> 55
Mic2 -> 40
Left -> Mic 2

Tony wrote original version using custom interface to n_gsm, Pavel
switched it to plain serdev and split it to two drivers to be easier
to debug and understand. Credit is Tony's, bugs are probably Pavel's.
    
Signed-off-by: Pavel Machek <pavel@ucw.cz>
Co-authored-by: Tony Lindgren <tony@atomide.com>

diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index 946a70210f49..ee4e994f145e 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -902,6 +902,14 @@ config SND_SOC_MAX9860
 	depends on I2C
 	select REGMAP_I2C
 
+config SND_SOC_MOTMDM
+	tristate "Motorola Modem TS 27.010 Voice Call Codec"
+	depends on SERIAL_DEV_BUS
+	help
+	  Enable support for Motorola TS 27.010 serdev voice
+	  call codec driver for Motorola Mapphone series of
+	  devices such as Droid 4.
+
 config SND_SOC_MSM8916_WCD_ANALOG
 	tristate "Qualcomm MSM8916 WCD Analog Codec"
 	depends on SPMI || COMPILE_TEST
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index 0140c60db695..e02bacd7fb3d 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -122,6 +122,8 @@ snd-soc-max9850-objs := max9850.o
 snd-soc-max9860-objs := max9860.o
 snd-soc-mc13783-objs := mc13783.o
 snd-soc-ml26124-objs := ml26124.o
+snd-soc-motmdm-objs := motmdm.o
+snd-soc-motmdm-state-objs := motmdm-state.o
 snd-soc-msm8916-analog-objs := msm8916-wcd-analog.o
 snd-soc-msm8916-digital-objs := msm8916-wcd-digital.o
 snd-soc-mt6351-objs := mt6351.o
@@ -427,6 +429,8 @@ obj-$(CONFIG_SND_SOC_MAX9850)	+= snd-soc-max9850.o
 obj-$(CONFIG_SND_SOC_MAX9860)	+= snd-soc-max9860.o
 obj-$(CONFIG_SND_SOC_MC13783)	+= snd-soc-mc13783.o
 obj-$(CONFIG_SND_SOC_ML26124)	+= snd-soc-ml26124.o
+obj-$(CONFIG_SND_SOC_MOTMDM)	+= snd-soc-motmdm.o
+obj-$(CONFIG_SND_SOC_MOTMDM)	+= snd-soc-motmdm-state.o
 obj-$(CONFIG_SND_SOC_MSM8916_WCD_ANALOG) +=snd-soc-msm8916-analog.o
 obj-$(CONFIG_SND_SOC_MSM8916_WCD_DIGITAL) +=snd-soc-msm8916-digital.o
 obj-$(CONFIG_SND_SOC_MT6351)	+= snd-soc-mt6351.o
diff --git a/sound/soc/codecs/motmdm-state.c b/sound/soc/codecs/motmdm-state.c
new file mode 100644
index 000000000000..d02e89e4498b
--- /dev/null
+++ b/sound/soc/codecs/motmdm-state.c
@@ -0,0 +1,178 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Motorola Mapphone MDM6600 voice call audio support
+ * Copyright 2018 - 2020 Tony Lindgren <tony@atomide.com>
+ * Copyright 2020 - 2021 Pavel Machek <pavel@ucw.cz>
+ *
+ * Designed to provide notifications about voice call state to the
+ * motmdm.c driver. This one listens on "gsmtty1".
+ */
+
+#include <linux/init.h>
+#include <linux/kfifo.h>
+#include <linux/module.h>
+#include <linux/of_graph.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/serdev.h>
+#include <linux/serdev-gsm.h>
+
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+#define MOTMDM_HEADER_LEN	5			/* U1234 */
+#define MOTMDM_AUDIO_MAX_LEN	128
+#define MOTMDM_VOICE_RESP_LEN	7			/* U1234~+CIEV= */
+
+struct motmdm_driver_data {
+	struct serdev_device *serdev;
+	unsigned char *buf;
+	size_t len;
+	spinlock_t lock;	/* enable/disabled lock */
+};
+
+static BLOCKING_NOTIFIER_HEAD(modem_state_chain_head);
+
+int register_modem_state_notifier(struct notifier_block *nb)
+{
+        return blocking_notifier_chain_register(&modem_state_chain_head, nb);
+}
+EXPORT_SYMBOL_GPL(register_modem_state_notifier);
+
+int unregister_modem_state_notifier(struct notifier_block *nb)
+{
+	return blocking_notifier_chain_unregister(&modem_state_chain_head, nb);
+}
+EXPORT_SYMBOL_GPL(unregister_modem_state_notifier);
+
+static int modem_state_notifier_call_chain(unsigned long val)
+{
+        int ret;
+        ret = __blocking_notifier_call_chain(&modem_state_chain_head, val, NULL,
+					     -1, NULL);
+        return notifier_to_errno(ret);
+}
+
+/* Parses the voice call state from unsolicited notifications on dlci1 */
+static int motmdm_voice_get_state(struct motmdm_driver_data *ddata,
+				   const unsigned char *buf,
+				   size_t len)
+{
+	struct device *dev = &ddata->serdev->dev;
+	bool enable;
+	const unsigned char *state;
+
+	if (len < MOTMDM_HEADER_LEN + MOTMDM_VOICE_RESP_LEN + 5)
+		return 0;
+
+	/* We only care about the unsolicted messages */
+	if (buf[MOTMDM_HEADER_LEN] != '~')
+		return 0;
+
+	if (strncmp(buf + MOTMDM_HEADER_LEN + 1, "+CIEV=", 6))
+		return len;
+
+	state = buf + MOTMDM_HEADER_LEN + MOTMDM_VOICE_RESP_LEN;
+	dev_info(dev, "%s: ciev=%5s\n", __func__, state);
+
+	if (!strncmp(state, "1,1,0", 5) ||	/* connecting */
+	    !strncmp(state, "1,4,0", 5) ||	/* incoming call */
+	    !strncmp(state, "1,2,0", 5))	/* connected */
+		enable = true;
+	else if (!strncmp(state, "1,0,0", 5) ||	/* disconnected */
+		!strncmp(state, "1,0,2", 5))	/* call failed */
+		enable = false;
+	else
+		return len;
+
+	modem_state_notifier_call_chain(enable);
+	return len;
+}
+
+static int voice_receive_data(struct serdev_device *serdev,
+			     const unsigned char *buf,
+			     size_t len)
+{
+        struct motmdm_driver_data *ddata = serdev_device_get_drvdata(serdev);
+
+	if (len > MOTMDM_AUDIO_MAX_LEN)
+		len = MOTMDM_AUDIO_MAX_LEN;
+
+	if (len <= MOTMDM_HEADER_LEN)
+		return 0;
+
+	if (buf[MOTMDM_HEADER_LEN] == '~')
+		motmdm_voice_get_state(ddata, buf, len);
+
+	return len;
+}
+
+static const struct serdev_device_ops voice_serdev_ops = {
+        .receive_buf    = voice_receive_data,
+        .write_wakeup   = serdev_device_write_wakeup,
+};
+
+static void motmdm_free_voice_serdev(struct motmdm_driver_data *ddata)
+{
+	serdev_device_close(ddata->serdev);
+}
+
+static int motmdm_soc_probe(struct serdev_device *serdev)
+{
+	struct device *dev = &serdev->dev;
+	struct motmdm_driver_data *ddata;
+	int error;
+
+	ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL);
+	if (!ddata)
+		return -ENOMEM;
+
+	ddata->serdev = serdev;
+	spin_lock_init(&ddata->lock);
+	ddata->len = MOTMDM_AUDIO_MAX_LEN;
+
+	ddata->buf = devm_kzalloc(dev, ddata->len, GFP_KERNEL);
+	if (!ddata->buf)
+		return -ENOMEM;
+
+	serdev_device_set_drvdata(ddata->serdev, ddata);
+        serdev_device_set_client_ops(ddata->serdev, &voice_serdev_ops);
+
+        error = serdev_device_open(ddata->serdev);
+	return error;
+}
+
+static void motmdm_state_remove(struct serdev_device *serdev)
+{
+	struct motmdm_driver_data *ddata = serdev_device_get_drvdata(serdev);
+
+	motmdm_free_voice_serdev(ddata);
+}
+
+static int motmdm_state_probe(struct serdev_device *serdev)
+{
+	return motmdm_soc_probe(serdev);
+}
+
+#ifdef CONFIG_OF
+static const struct of_device_id motmdm_of_match[] = {
+	{ .compatible = "motorola,mapphone-mdm6600-modem" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, motmdm_of_match);
+#endif
+
+static struct serdev_device_driver motmdm_state_driver = {
+	.driver	= {
+		.name		= "mot-mdm6600-modem",
+		.of_match_table	= of_match_ptr(motmdm_of_match),
+	},
+	.probe	= motmdm_state_probe,
+	.remove	= motmdm_state_remove,
+};
+module_serdev_device_driver(motmdm_state_driver);
+
+MODULE_ALIAS("platform:motmdm-state");
+MODULE_DESCRIPTION("Motorola Mapphone MDM6600 modem state driver");
+MODULE_AUTHOR("Pavel Machek <pavel@ucw.cz>");
+MODULE_LICENSE("GPL v2");
diff --git a/sound/soc/codecs/motmdm.c b/sound/soc/codecs/motmdm.c
new file mode 100644
index 000000000000..9cba6006e4c8
--- /dev/null
+++ b/sound/soc/codecs/motmdm.c
@@ -0,0 +1,689 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Motorola Mapphone MDM6600 voice call audio support
+ * Copyright 2018 - 2020 Tony Lindgren <tony@atomide.com>
+ * Copyright 2020 - 2021 Pavel Machek <pavel@ucw.cz>
+ *
+ * This one handles audio configuration on "gsmtty2"; it needs to know
+ * whether we are in call or not, and that part is in motmdm-state.c
+ * and listens on "gsmtty1".
+ */
+
+#include <linux/init.h>
+#include <linux/kfifo.h>
+#include <linux/module.h>
+#include <linux/of_graph.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/serdev.h>
+#include <linux/serdev-gsm.h>
+
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+#include "motmdm-state.h"
+
+#define MOTMDM_HEADER_LEN	5			/* U1234 */
+
+#define MOTMDM_AUDIO_RESP_LEN	6			/* U1234+XXXX= */
+#define MOTMDM_AUDIO_MAX_LEN	128
+
+#define MOTMDM_VOICE_RESP_LEN	7			/* U1234~+CIEV= */
+
+struct motmdm_driver_data {
+	struct notifier_block notifier;
+	struct snd_soc_component *component;
+	struct snd_soc_dai *master_dai;
+	struct device *modem;
+	struct serdev_device *serdev;
+	struct regmap *regmap;
+	unsigned char *buf;
+	size_t len;
+	unsigned int parsed:1;
+	unsigned int enabled:1;
+	spinlock_t lock;	/* enable/disabled lock */
+	struct mutex mutex;	/* for sending commands */
+	wait_queue_head_t read_queue;
+
+	unsigned int dtmf_val;
+	unsigned int dtmf_en;
+};
+
+enum motmdm_cmd {
+	CMD_AT_EACC,
+	CMD_AT_CLVL,
+	CMD_AT_NREC,
+};
+
+const char * const motmd_read_fmt[] = {
+	[CMD_AT_EACC] = "AT+EACC?",
+	[CMD_AT_CLVL] = "AT+CLVL?",
+	[CMD_AT_NREC] = "AT+NREC?",
+};
+
+const char * const motmd_write_fmt[] = {
+	[CMD_AT_EACC] = "AT+EACC=%u,0",
+	[CMD_AT_CLVL] = "AT+CLVL=%u",
+	[CMD_AT_NREC] = "AT+NREC=%u",
+};
+
+/*
+ * Currently unconfigured additional inactive (error producing) options
+ * seem to be:
+ * "TTY Headset", "HCQ Headset", "VCQ Headset", "No-Mic Headset",
+ * "Handset Fluence Med", "Handset Fluence Low", "Car Dock", "Lapdock"
+ */
+static const char * const motmdm_out_mux_texts[] = {
+	"Handset", "Headset", "Speakerphone", "Bluetooth",
+};
+
+static SOC_ENUM_SINGLE_EXT_DECL(motmdm_out_enum, motmdm_out_mux_texts);
+
+static const DECLARE_TLV_DB_SCALE(motmdm_gain_tlv, 0, 100, 0);
+
+int motmdm_send_command(struct motmdm_driver_data *ddata,
+			     const u8 *buf, int len)
+{
+	struct device *dev = ddata->component->dev;
+	const int timeout_ms = 1000;
+	unsigned char cmd[MOTMDM_AUDIO_MAX_LEN];
+	int ret, cmdlen;
+
+	cmdlen = len + 5 + 1;
+	if (cmdlen > MOTMDM_AUDIO_MAX_LEN)
+		return -EINVAL;
+
+	mutex_lock(&ddata->mutex);
+	memset(ddata->buf, 0, ddata->len);
+	ddata->parsed = false;
+	snprintf(cmd, cmdlen, "U%04li%s", jiffies % 10000, buf);
+
+	ret = serdev_device_write(ddata->serdev, cmd, cmdlen, MAX_SCHEDULE_TIMEOUT);
+	if (ret < 0)
+		goto out_unlock;
+
+        serdev_device_wait_until_sent(ddata->serdev, 0);
+
+	ret = wait_event_timeout(ddata->read_queue, ddata->parsed,
+				 msecs_to_jiffies(timeout_ms));
+	if (ret == 0) {
+		ret = -ETIMEDOUT;
+		goto out_unlock;
+	} else if (ret < 0) {
+		goto out_unlock;
+	}
+
+	if (strstr(ddata->buf, "ERROR")) {
+		dev_err(dev, "command %s error %s\n", cmd, ddata->buf);
+		ret = -EPIPE;
+	}
+
+	ret = len;
+
+out_unlock:
+	mutex_unlock(&ddata->mutex);
+	printk("send_command -- ret %d\n", ret);
+
+	return ret;
+}
+
+static int motmdm_read_reg(void *context, unsigned int reg,
+			   unsigned int *value)
+{
+	struct snd_soc_component *component = context;
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	const unsigned char *cmd;
+	unsigned int val;
+	int error;
+
+	cmd = motmd_read_fmt[reg];
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0) {
+		dev_err(component->dev, "%s: %s failed with %i\n",
+			__func__, cmd, error);
+
+		return error;
+	}
+
+	error = kstrtouint(ddata->buf + MOTMDM_AUDIO_RESP_LEN, 0, &val);
+	if (error)
+		return -ENODEV;
+
+	*value = val;
+
+	return error;
+}
+
+static int motmdm_write_reg(void *context, unsigned int reg,
+			    unsigned int value)
+{
+	struct snd_soc_component *component = context;
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	const unsigned char *fmt, *cmd;
+	int error;
+
+	fmt = motmd_write_fmt[reg];
+	cmd = kasprintf(GFP_KERNEL, fmt, value);
+	if (!cmd) {
+		error = -ENOMEM;
+		goto free;
+	}
+
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0)
+		dev_err(component->dev, "%s: %s failed with %i\n",
+			__func__, cmd, error);
+
+free:
+	kfree(cmd);
+
+	return error;
+}
+
+static const struct reg_default motmdm_reg_defaults[] = {
+	{ CMD_AT_EACC, 0x0 },
+	{ CMD_AT_CLVL, 0x0 },
+};
+
+static const struct regmap_config motmdm_regmap = {
+	.reg_bits = 32,
+	.reg_stride = 1,
+	.val_bits = 32,
+	.max_register = CMD_AT_NREC,
+	.reg_defaults = motmdm_reg_defaults,
+	.num_reg_defaults = ARRAY_SIZE(motmdm_reg_defaults),
+	.cache_type = REGCACHE_RBTREE,
+	.reg_read = motmdm_read_reg,
+	.reg_write = motmdm_write_reg,
+};
+
+static int motmdm_value_get(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_value *ucontrol,
+			    enum motmdm_cmd reg,
+			    int cmd_base)
+{
+	struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	unsigned int val;
+	int error;
+
+	error = regmap_read(ddata->regmap, reg, &val);
+	if (error)
+		return error;
+
+	if (val >= cmd_base)
+		val -= cmd_base;
+
+	ucontrol->value.enumerated.item[0] = val;
+
+	return 0;
+}
+
+static int motmdm_value_put(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_value *ucontrol,
+			    enum motmdm_cmd reg,
+			    int cmd_base)
+{
+	struct snd_soc_component *component = snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+	int error;
+
+	error = regmap_write(ddata->regmap, reg,
+			     ucontrol->value.enumerated.item[0] + cmd_base);
+	if (error)
+		return error;
+
+	regcache_mark_dirty(ddata->regmap);
+
+	return error;
+}
+
+static int motmdm_audio_out_get(struct snd_kcontrol *kcontrol,
+				     struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_get(kcontrol, ucontrol, CMD_AT_EACC, 1);
+}
+
+static int motmdm_audio_out_put(struct snd_kcontrol *kcontrol,
+				     struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_put(kcontrol, ucontrol, CMD_AT_EACC, 1);
+}
+
+static int motmdm_gain_get(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_get(kcontrol, ucontrol, CMD_AT_CLVL, 0);
+}
+
+static int motmdm_gain_put(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_put(kcontrol, ucontrol, CMD_AT_CLVL, 0);
+}
+
+static int motmdm_noise_get(struct snd_kcontrol *kcontrol,
+				 struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_get(kcontrol, ucontrol, CMD_AT_NREC, 0);
+}
+
+static int motmdm_noise_put(struct snd_kcontrol *kcontrol,
+				  struct snd_ctl_elem_value *ucontrol)
+{
+	return motmdm_value_put(kcontrol, ucontrol, CMD_AT_NREC, 0);
+}
+
+static const char * const motmdm_tonegen_dtmf_key_txt[] = {
+	"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D",
+	"*", "#"
+};
+
+static SOC_ENUM_SINGLE_EXT_DECL(motmd_tonegen_dtmf_enum,
+				motmdm_tonegen_dtmf_key_txt);
+
+static int motmdm_dtmf_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+
+	ucontrol->value.enumerated.item[0] = ddata->dtmf_val;
+
+	return 0;
+}
+
+static int motmdm_dtmf_put(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+
+	ddata->dtmf_val = ucontrol->value.enumerated.item[0];
+
+	return 0;
+}
+
+static int motmdm_tonegen_dtmf_send_get(struct snd_kcontrol *kcontrol,
+					struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+
+	ucontrol->value.enumerated.item[0] = ddata->dtmf_en;
+
+	return 0;
+}
+
+static int motmdm_tonegen_dtmf_send_put(struct snd_kcontrol *kcontrol,
+					struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	const unsigned char *cmd, *fmt = "AT+DTSE=%s,%i";
+	const char *tone = "";
+	int error;
+
+	if (!ddata->enabled)
+		return 0;
+
+	ddata->dtmf_en = ucontrol->value.enumerated.item[0];
+	if (ddata->dtmf_en)
+		tone = motmdm_tonegen_dtmf_key_txt[ddata->dtmf_val];
+
+	/* Value 0 enables tone generator, 1 disables it */
+	cmd = kasprintf(GFP_KERNEL, fmt, tone, !ddata->dtmf_en);
+
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0) {
+		dev_err(component->dev, "%s: %s failed with %i\n",
+			__func__, cmd, error);
+		goto free;
+	}
+
+free:
+	kfree(cmd);
+
+	return error;
+}
+
+static int
+motmdm_enable_primary_dai(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	int error;
+
+	if (!ddata->master_dai)
+		return -ENODEV;
+
+	error = snd_soc_dai_set_sysclk(ddata->master_dai, 1, 19200000,
+				       SND_SOC_CLOCK_OUT);
+	if (error)
+		return error;
+
+	error = snd_soc_dai_set_fmt(ddata->master_dai,
+				    SND_SOC_DAIFMT_I2S |
+				    SND_SOC_DAIFMT_NB_NF |
+				    SND_SOC_DAIFMT_CBM_CFM);
+	if (error)
+		return error;
+
+	error = snd_soc_dai_set_tdm_slot(ddata->master_dai, 0, 1, 1, 8);
+	if (error)
+		return error;
+
+	return error;
+}
+
+static int
+motmdm_disable_primary_dai(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	int error;
+
+	if (!ddata->master_dai) {
+		return -ENODEV;
+	}
+
+	error = snd_soc_dai_set_sysclk(ddata->master_dai, 0, 26000000,
+				       SND_SOC_CLOCK_OUT);
+	if (error) {
+		return error;
+	}
+
+	error = snd_soc_dai_set_fmt(ddata->master_dai,
+				    SND_SOC_DAIFMT_CBM_CFM);
+	if (error) {
+		return error;
+	}
+
+	error = snd_soc_dai_set_tdm_slot(ddata->master_dai, 0, 0, 0, 48);
+	if (error) {
+		return error;
+	}
+
+	return error;
+}
+
+static int motmdm_find_primary_dai(struct snd_soc_component *component,
+	const char *name)
+{
+	struct motmdm_driver_data *ddata =
+		snd_soc_component_get_drvdata(component);
+	struct device_node *bitclkmaster = NULL, *framemaster = NULL;
+	struct device_node *ep, *master_ep, *master = NULL;
+	struct snd_soc_dai_link_component dlc = { 0 };
+	unsigned int daifmt;
+
+	ep = of_graph_get_next_endpoint(component->dev->of_node, NULL);
+	if (!ep)
+		return -ENODEV;
+
+	master_ep = of_graph_get_remote_endpoint(ep);
+	of_node_put(ep);
+	if (!master_ep)
+		return -ENODEV;
+
+	daifmt = snd_soc_of_parse_daifmt(master_ep, NULL,
+					 &bitclkmaster, &framemaster);
+	of_node_put(master_ep);
+	if (bitclkmaster && framemaster)
+		master = of_graph_get_port_parent(bitclkmaster);
+	of_node_put(bitclkmaster);
+	of_node_put(framemaster);
+	if (!master)
+		return -ENODEV;
+
+	dlc.of_node = master;
+	dlc.dai_name = name;
+	ddata->master_dai = snd_soc_find_dai(&dlc);
+	of_node_put(master);
+	if (!ddata->master_dai)
+		return -EPROBE_DEFER;
+
+	dev_info(component->dev, "Master DAI is %s\n",
+		 dev_name(ddata->master_dai->dev));
+
+	return 0;
+}
+
+static int motmdm_parse_tdm(struct snd_soc_component *component)
+{
+	return motmdm_find_primary_dai(component, "cpcap-voice");
+}
+
+static const struct snd_kcontrol_new motmdm_snd_controls[] = {
+        SOC_ENUM_EXT("Call Output", motmdm_out_enum,
+                     motmdm_audio_out_get,
+                     motmdm_audio_out_put),
+        SOC_SINGLE_EXT_TLV("Call Volume",
+			   0, 0, 7, 0,
+			   motmdm_gain_get,
+			   motmdm_gain_put,
+			   motmdm_gain_tlv),
+	SOC_SINGLE_BOOL_EXT("Call Noise Cancellation", 0,
+			    motmdm_noise_get,
+			    motmdm_noise_put),
+	SOC_ENUM_EXT("Call DTMF", motmd_tonegen_dtmf_enum,
+		     motmdm_dtmf_get,
+		     motmdm_dtmf_put),
+	SOC_SINGLE_BOOL_EXT("Call DTMF Send", 0,
+			    motmdm_tonegen_dtmf_send_get,
+			    motmdm_tonegen_dtmf_send_put),
+};
+
+static struct snd_soc_dai_driver motmdm_dai[] = {
+	{
+		.name = "mdm-call",
+		.playback = {
+			.stream_name = "Voice Call Playback",
+			.channels_min = 1,
+			.channels_max = 2,
+			.rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000,
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+		.capture = {
+			.stream_name = "Voice Call Capture",
+			.channels_min = 1,
+			.channels_max = 2,
+			.rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000,
+			.formats = SNDRV_PCM_FMTBIT_S16_LE,
+		},
+	},
+};
+
+static int
+motmdm_notifier_call(struct notifier_block *bl, unsigned long state,
+                                                        void *unused)
+{
+	struct motmdm_driver_data *ddata =
+		container_of(bl, struct motmdm_driver_data, notifier);
+	bool enable, notify = false;
+	unsigned long flags;
+
+	enable = !!state;
+
+	spin_lock_irqsave(&ddata->lock, flags);
+	if (ddata->enabled != enable) {
+		ddata->enabled = enable;
+		notify = true;
+	}
+	spin_unlock_irqrestore(&ddata->lock, flags);
+
+	if (!notify)
+		return NOTIFY_DONE;
+
+	if (enable)
+		motmdm_enable_primary_dai(ddata->component);
+	else
+		motmdm_disable_primary_dai(ddata->component);
+
+	return NOTIFY_DONE;
+}
+
+static int voice_receive_data(struct serdev_device *serdev,
+			     const unsigned char *buf,
+			     size_t len)
+{
+        struct motmdm_driver_data *ddata = serdev_device_get_drvdata(serdev);
+	struct device *dev = ddata->component->dev;
+
+	printk("voice_receive_data: have %s %d\n", buf, len);
+
+	if (len > MOTMDM_AUDIO_MAX_LEN)
+		len = MOTMDM_AUDIO_MAX_LEN;
+
+	if (len <= MOTMDM_HEADER_LEN)
+		return 0;
+
+	printk("voice_receive_data: command reply? -- %s %d\n", buf, len);
+
+	snprintf(ddata->buf, len - MOTMDM_HEADER_LEN, buf + MOTMDM_HEADER_LEN);
+	dev_info(dev, "%s: received: %s\n", __func__, ddata->buf);
+	ddata->parsed = true;
+	wake_up(&ddata->read_queue);
+
+	return len;
+}
+
+static const struct serdev_device_ops voice_serdev_ops = {
+        .receive_buf    = voice_receive_data,
+        .write_wakeup   = serdev_device_write_wakeup,
+};
+
+static void motmdm_free_voice_serdev(struct motmdm_driver_data *ddata)
+{
+	serdev_device_close(ddata->serdev);
+}
+
+static int motmdm_soc_probe(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata;
+	const unsigned char *cmd = "AT+CMUT=0";
+	int error;
+	u32 line;
+
+	ddata = devm_kzalloc(component->dev, sizeof(*ddata), GFP_KERNEL);
+	if (!ddata)
+		return -ENOMEM;
+
+	error = of_property_read_u32(component->dev->of_node, "reg", &line);
+	if (error)
+		return error;
+
+	ddata->serdev = (struct serdev_device *) component->dev;
+	ddata->component = component;
+	ddata->modem = component->dev->parent;
+	mutex_init(&ddata->mutex);
+	init_waitqueue_head(&ddata->read_queue);
+	ddata->len = PAGE_SIZE;
+	spin_lock_init(&ddata->lock);
+	snd_soc_component_set_drvdata(component, ddata);
+	ddata->len = MOTMDM_AUDIO_MAX_LEN;
+
+	ddata->buf = devm_kzalloc(component->dev, ddata->len, GFP_KERNEL);
+	if (!ddata->buf)
+		return -ENOMEM;
+
+	ddata->regmap = devm_regmap_init(component->dev, NULL, component,
+					 &motmdm_regmap);
+	if (IS_ERR(ddata->regmap)) {
+		error = PTR_ERR(ddata->regmap);
+		dev_err(component->dev, "%s: Failed to allocate regmap: %d\n",
+			__func__, error);
+
+		return error;
+	}
+
+	serdev_device_set_drvdata(ddata->serdev, ddata);
+        serdev_device_set_client_ops(ddata->serdev, &voice_serdev_ops);
+
+        error = serdev_device_open(ddata->serdev);
+        if (error)
+                return error;
+
+	error = motmdm_parse_tdm(component);
+	if (error)
+		goto unregister_serdev;
+
+	regcache_sync(ddata->regmap);
+
+	error = motmdm_send_command(ddata, cmd, strlen(cmd));
+	if (error < 0)
+		goto unregister_serdev;
+
+	error = motmdm_disable_primary_dai(ddata->component);
+	if (error)
+		goto unregister_serdev;
+
+	ddata->notifier.notifier_call = motmdm_notifier_call;
+	register_modem_state_notifier(&ddata->notifier);
+
+	return 0;
+
+unregister_serdev:
+	motmdm_free_voice_serdev(ddata);
+	serdev_device_close(ddata->serdev);
+
+	return error;
+}
+
+static void motmdm_soc_remove(struct snd_soc_component *component)
+{
+	struct motmdm_driver_data *ddata = snd_soc_component_get_drvdata(component);
+
+	unregister_modem_state_notifier(&ddata->notifier);
+
+	motmdm_free_voice_serdev(ddata);
+}
+
+static struct snd_soc_component_driver soc_codec_dev_motmdm = {
+	.probe = motmdm_soc_probe,
+	.remove = motmdm_soc_remove,
+	.controls = motmdm_snd_controls,
+	.num_controls = ARRAY_SIZE(motmdm_snd_controls),
+	.idle_bias_on = 1,
+	.use_pmdown_time = 1,
+	.endianness = 1,
+	.non_legacy_dai_naming = 1,
+};
+
+static int motmdm_codec_probe(struct serdev_device *serdev)
+{
+	return devm_snd_soc_register_component(&serdev->dev,
+					       &soc_codec_dev_motmdm,
+					       motmdm_dai,
+					       ARRAY_SIZE(motmdm_dai));
+}
+
+#ifdef CONFIG_OF
+static const struct of_device_id motmdm_of_match[] = {
+	{ .compatible = "motorola,mapphone-mdm6600-codec" },
+	{},
+};
+MODULE_DEVICE_TABLE(of, motmdm_of_match);
+#endif
+
+static struct serdev_device_driver motmdm_driver = {
+	.probe = motmdm_codec_probe,
+	.driver = {
+		.name = "mot-mdm6600-codec",
+		.of_match_table = of_match_ptr(motmdm_of_match),
+	},
+};
+module_serdev_device_driver(motmdm_driver);
+
+MODULE_ALIAS("platform:motmdm-codec");
+MODULE_DESCRIPTION("ASoC Motorola Mapphone MDM6600 codec driver");
+MODULE_AUTHOR("Tony Lindgren <tony@atomide.com>");
+MODULE_LICENSE("GPL v2");

-- 
http://www.livejournal.com/~pavelmachek

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 195 bytes --]

  reply	other threads:[~2021-01-23 18:29 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-01-12 17:47 ASoC: cpcap: Implement set_tdm_slot for voice call support Pavel Machek
2021-01-12 17:47 ` Pavel Machek
2021-01-21  0:05 ` Mark Brown
2021-01-21  0:05   ` Mark Brown
2021-01-23 18:28   ` Pavel Machek [this message]
2021-01-23 18:28     ` ASoC: audio-graph-card: Add audio mixer for Motorola mdm6600 Pavel Machek

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20210123182833.GB14771@duo.ucw.cz \
    --to=pavel@ucw.cz \
    --cc=alsa-devel@alsa-project.org \
    --cc=broonie@kernel.org \
    --cc=kuninori.morimoto.gx@renesas.com \
    --cc=lgirdwood@gmail.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=merlijn@wizzup.org \
    --cc=perex@perex.cz \
    --cc=peter.ujfalusi@ti.com \
    --cc=phone-devel@vger.kernel.org \
    --cc=spinal.by@gmail.com \
    --cc=sre@kernel.org \
    --cc=tiwai@suse.com \
    --cc=tony@atomide.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.