From patchwork Sun Mar 28 11:07:52 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Pavel Machek X-Patchwork-Id: 12168911 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-15.2 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID,HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,SPF_HELO_NONE,SPF_PASS,URIBL_BLOCKED,USER_AGENT_SANE_1 autolearn=unavailable autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 2764EC433C1 for ; Sun, 28 Mar 2021 11:09:03 +0000 (UTC) Received: from alsa0.perex.cz (alsa0.perex.cz [77.48.224.243]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.kernel.org (Postfix) with ESMTPS id 36D086197C for ; Sun, 28 Mar 2021 11:09:01 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mail.kernel.org 36D086197C Authentication-Results: mail.kernel.org; dmarc=none (p=none dis=none) header.from=ucw.cz Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=alsa-devel-bounces@alsa-project.org Received: from alsa1.perex.cz (alsa1.perex.cz [207.180.221.201]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by alsa0.perex.cz (Postfix) with ESMTPS id 036D8886; Sun, 28 Mar 2021 13:08:09 +0200 (CEST) DKIM-Filter: OpenDKIM Filter v2.11.0 alsa0.perex.cz 036D8886 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=alsa-project.org; s=default; t=1616929739; bh=9ewGyj0KjWi3JkWMqLY0nGtQxVpgxVqRddS5bCrMgFg=; h=Date:From:To:Subject:Cc:List-Id:List-Unsubscribe:List-Archive: List-Post:List-Help:List-Subscribe:From; b=n4CzrwJjlcfuB6ZxEh1aBZsEByGtUaG5F3UugFIf9mmYsP9wdHQWQjNZenlv7xrXQ O1tg5P5L+juUWOZbLwVv70xOvidw5sXS7QYOB2mB7vNBExaaDKsL4t9yk/1j3HIkL1 tne+VH20Tn505xqNw4dlfjpd01e4UhCFhhptRVuY= Received: from alsa1.perex.cz (localhost.localdomain [127.0.0.1]) by alsa1.perex.cz (Postfix) with ESMTP id 456C0F8023E; Sun, 28 Mar 2021 13:08:08 +0200 (CEST) Received: by alsa1.perex.cz (Postfix, from userid 50401) id DA632F80240; Sun, 28 Mar 2021 13:08:06 +0200 (CEST) Received: from jabberwock.ucw.cz (jabberwock.ucw.cz [46.255.230.98]) (using TLSv1.2 with cipher ADH-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by alsa1.perex.cz (Postfix) with ESMTPS id EC482F800B9 for ; Sun, 28 Mar 2021 13:07:57 +0200 (CEST) DKIM-Filter: OpenDKIM Filter v2.11.0 alsa1.perex.cz EC482F800B9 Received: by jabberwock.ucw.cz (Postfix, from userid 1017) id 4FADB1C0B78; Sun, 28 Mar 2021 13:07:53 +0200 (CEST) Date: Sun, 28 Mar 2021 13:07:52 +0200 From: Pavel Machek To: kernel list , broonie@kernel.org Subject: [PATCH] ASoC: audio-graph-card: Add audio mixer for Motorola mdm6600 Message-ID: <20210328110752.GA6424@duo.ucw.cz> MIME-Version: 1.0 Content-Disposition: inline User-Agent: Mutt/1.10.1 (2018-07-13) Cc: alsa-devel@alsa-project.org, phone-devel@vger.kernel.org, kuninori.morimoto.gx@renesas.com, tony@atomide.com, Merlijn Wajer , tiwai@suse.com, lgirdwood@gmail.com, peter.ujfalusi@ti.com, Sebastian Reichel , "Arthur D." X-BeenThere: alsa-devel@alsa-project.org X-Mailman-Version: 2.1.15 Precedence: list List-Id: "Alsa-devel mailing list for ALSA developers - http://www.alsa-project.org" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: alsa-devel-bounces@alsa-project.org Sender: "Alsa-devel" 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 Co-authored-by: Tony Lindgren Reported-by: kernel test robot --- v2: Fix compile error. --- sound/soc/codecs/Kconfig | 8 + sound/soc/codecs/Makefile | 4 + sound/soc/codecs/motmdm-state.c | 177 ++++++++ sound/soc/codecs/motmdm.c | 688 ++++++++++++++++++++++++++++++++ 4 files changed, 877 insertions(+) create mode 100644 sound/soc/codecs/motmdm-state.c create mode 100644 sound/soc/codecs/motmdm.c diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index ba4eb54aafcb..94000ab3cf3e 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -932,6 +932,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 d277f0366e09..f63022e0a292 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -128,6 +128,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 @@ -443,6 +445,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..a3b2870c5167 --- /dev/null +++ b/sound/soc/codecs/motmdm-state.c @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motorola Mapphone MDM6600 voice call audio support + * Copyright 2018 - 2020 Tony Lindgren + * Copyright 2020 - 2021 Pavel Machek + * + * Designed to provide notifications about voice call state to the + * motmdm.c driver. This one listens on "gsmtty1". + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 "); +MODULE_LICENSE("GPL v2"); diff --git a/sound/soc/codecs/motmdm.c b/sound/soc/codecs/motmdm.c new file mode 100644 index 000000000000..93615a01ff1b --- /dev/null +++ b/sound/soc/codecs/motmdm.c @@ -0,0 +1,688 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motorola Mapphone MDM6600 voice call audio support + * Copyright 2018 - 2020 Tony Lindgren + * Copyright 2020 - 2021 Pavel Machek + * + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 "); +MODULE_LICENSE("GPL v2");