diff options
author | Tony Lindgren <tony@atomide.com> | 2019-03-03 21:28:39 -0800 |
---|---|---|
committer | Tony Lindgren <tony@atomide.com> | 2019-03-03 21:28:39 -0800 |
commit | db250b51bdffe8b5d649506e13b0ba7be43df40a (patch) | |
tree | 04904fe8256e9c14590b8e3d8bb15ca7282c93a8 | |
parent | ce0d9be1344641e5293ffa4a0ddf7dd0bbf6f568 (diff) | |
parent | 6695f4ec8a19f6fbfcfdccb9928ad31a614fab40 (diff) | |
download | linux-omap-droid4-pending-v5.0.tar.gz |
Merge branch 'droid4-pending-mdm-v5.0' into droid4-pending-v5.0droid4-pending-v5.0
-rw-r--r-- | Documentation/devicetree/bindings/mfd/motorola-mdm.txt | 18 | ||||
-rw-r--r-- | arch/arm/boot/dts/motorola-cpcap-mapphone.dtsi | 4 | ||||
-rw-r--r-- | arch/arm/boot/dts/omap4-droid4-xt894.dts | 62 | ||||
-rw-r--r-- | arch/arm/configs/omap2plus_defconfig | 7 | ||||
-rw-r--r-- | drivers/gnss/Kconfig | 8 | ||||
-rw-r--r-- | drivers/gnss/Makefile | 3 | ||||
-rw-r--r-- | drivers/gnss/motmdm.c | 393 | ||||
-rw-r--r-- | drivers/mfd/Kconfig | 8 | ||||
-rw-r--r-- | drivers/mfd/Makefile | 1 | ||||
-rw-r--r-- | drivers/mfd/motorola-mdm.c | 1208 | ||||
-rw-r--r-- | drivers/tty/n_gsm.c | 548 | ||||
-rw-r--r-- | include/linux/mfd/motorola-mdm.h | 84 | ||||
-rw-r--r-- | include/linux/serdev-gsm.h | 227 | ||||
-rw-r--r-- | sound/soc/codecs/Kconfig | 8 | ||||
-rw-r--r-- | sound/soc/codecs/Makefile | 2 | ||||
-rw-r--r-- | sound/soc/codecs/cpcap.c | 119 | ||||
-rw-r--r-- | sound/soc/codecs/motmdm.c | 514 | ||||
-rw-r--r-- | sound/soc/ti/omap-mcbsp-priv.h | 2 | ||||
-rw-r--r-- | sound/soc/ti/omap-mcbsp.c | 76 |
19 files changed, 3145 insertions, 147 deletions
diff --git a/Documentation/devicetree/bindings/mfd/motorola-mdm.txt b/Documentation/devicetree/bindings/mfd/motorola-mdm.txt new file mode 100644 index 0000000000000..92cb0dd70fa85 --- /dev/null +++ b/Documentation/devicetree/bindings/mfd/motorola-mdm.txt @@ -0,0 +1,18 @@ +Motorola MDM modem device tree binding + +Required properties: +- compatible : Must be one of "motorola,mapphone-mdm6600-serdev" + or a generic "motorola,mdm6600" +- phys : USB PHY with shared pins for UART power management +- phy-names : Name for the USB PHY +- reg : SPI chip select + +Example: + +&uart1 { + modem { + compatible = "motorola,mapphone-mdm6600-serdev"; + phys = <&fsusb1_phy>; + phy-names = "usb"; + }; +}; diff --git a/arch/arm/boot/dts/motorola-cpcap-mapphone.dtsi b/arch/arm/boot/dts/motorola-cpcap-mapphone.dtsi index f57acf8f66b95..60e50caf8d702 100644 --- a/arch/arm/boot/dts/motorola-cpcap-mapphone.dtsi +++ b/arch/arm/boot/dts/motorola-cpcap-mapphone.dtsi @@ -70,12 +70,16 @@ cpcap_audio: audio-codec { #sound-dai-cells = <1>; + #address-cells = <1>; + #size-cells = <0>; port@0 { + reg = <0>; cpcap_audio_codec0: endpoint { }; }; port@1 { + reg = <1>; cpcap_audio_codec1: endpoint { }; }; diff --git a/arch/arm/boot/dts/omap4-droid4-xt894.dts b/arch/arm/boot/dts/omap4-droid4-xt894.dts index 552d8bdab5c91..8b8e1c76e15fd 100644 --- a/arch/arm/boot/dts/omap4-droid4-xt894.dts +++ b/arch/arm/boot/dts/omap4-droid4-xt894.dts @@ -672,6 +672,24 @@ pinctrl-0 = <&uart1_pins>; interrupts-extended = <&wakeupgen GIC_SPI 72 IRQ_TYPE_LEVEL_HIGH &omap4_pmx_core 0xfc>; + + modem { + compatible = "motorola,mapphone-mdm6600-serdev"; + phys = <&fsusb1_phy>; + phy-names = "usb"; + + mot_mdm6600_audio: audio-codec { + #address-cells = <1>; + #size-cells = <0>; + #sound-dai-cells = <1>; + + port@0 { + mot_mdm6600_audio_codec0: endpoint { + remote-endpoint = <&cpu_dai_mdm>; + }; + }; + }; + }; }; &uart3 { @@ -748,12 +766,18 @@ pinctrl-0 = <&mcbsp2_pins>; status = "okay"; - mcbsp2_port: port { - cpu_dai2: endpoint { - dai-format = "i2s"; - remote-endpoint = <&cpcap_audio_codec0>; - frame-master = <&cpcap_audio_codec0>; - bitclock-master = <&cpcap_audio_codec0>; + ports { + #address-cells = <1>; + #size-cells = <0>; + + mcbsp2_port: port@0 { + reg = <0>; + cpu_dai2: endpoint@0 { + dai-format = "i2s"; + remote-endpoint = <&cpcap_audio_codec0>; + frame-master = <&cpcap_audio_codec0>; + bitclock-master = <&cpcap_audio_codec0>; + }; }; }; }; @@ -764,12 +788,26 @@ pinctrl-0 = <&mcbsp3_pins>; status = "okay"; - mcbsp3_port: port { - cpu_dai3: endpoint { - dai-format = "dsp_a"; - frame-master = <&cpcap_audio_codec1>; - bitclock-master = <&cpcap_audio_codec1>; - remote-endpoint = <&cpcap_audio_codec1>; + ports { + mcbsp3_port: port@0 { + #address-cells = <1>; + #size-cells = <0>; + + cpu_dai3: endpoint@0 { + reg = <0>; + dai-format = "dsp_a"; + frame-master = <&cpcap_audio_codec1>; + bitclock-master = <&cpcap_audio_codec1>; + remote-endpoint = <&cpcap_audio_codec1>; + }; + + cpu_dai_mdm: endpoint@1 { + reg = <1>; + dai-format = "dsp_a"; + frame-master = <&cpcap_audio_codec1>; + bitclock-master = <&cpcap_audio_codec1>; + remote-endpoint = <&mot_mdm6600_audio_codec0>; + }; }; }; }; diff --git a/arch/arm/configs/omap2plus_defconfig b/arch/arm/configs/omap2plus_defconfig index 96e50ee1a2f26..1f25ffbed806f 100644 --- a/arch/arm/configs/omap2plus_defconfig +++ b/arch/arm/configs/omap2plus_defconfig @@ -123,6 +123,10 @@ CONFIG_DEVTMPFS_MOUNT=y CONFIG_DMA_CMA=y CONFIG_OMAP_OCP2SCP=y CONFIG_CONNECTOR=m +CONFIG_GNSS=m +CONFIG_GNSS_MOTMDM=m +CONFIG_GNSS_SIRF_SERIAL=m +CONFIG_GNSS_UBX_SERIAL=m CONFIG_MTD=y CONFIG_MTD_CMDLINE_PARTS=y CONFIG_MTD_BLOCK=y @@ -239,6 +243,7 @@ CONFIG_INPUT_PALMAS_PWRBUTTON=m CONFIG_INPUT_PWM_VIBRA=m CONFIG_SERIO=m # CONFIG_LEGACY_PTYS is not set +CONFIG_N_GSM=m CONFIG_SERIAL_8250=y CONFIG_SERIAL_8250_CONSOLE=y CONFIG_SERIAL_8250_NR_UARTS=32 @@ -305,6 +310,7 @@ CONFIG_MFD_TI_LP87565=y CONFIG_MFD_TPS65218=y CONFIG_MFD_TPS65910=y CONFIG_TWL6040_CORE=y +CONFIG_MFD_MOTMDM=m CONFIG_REGULATOR_CPCAP=y CONFIG_REGULATOR_GPIO=y CONFIG_REGULATOR_LM363X=m @@ -391,6 +397,7 @@ CONFIG_SND_SOC_OMAP3_PANDORA=m CONFIG_SND_SOC_OMAP3_TWL4030=m CONFIG_SND_SOC_CPCAP=m CONFIG_SND_SOC_TLV320AIC23_I2C=m +CONFIG_SND_SOC_MOTMDM=m CONFIG_SND_SIMPLE_CARD=m CONFIG_SND_AUDIO_GRAPH_CARD=m CONFIG_HID_GENERIC=m diff --git a/drivers/gnss/Kconfig b/drivers/gnss/Kconfig index 6abc885145121..2ae99cc49c6d8 100644 --- a/drivers/gnss/Kconfig +++ b/drivers/gnss/Kconfig @@ -12,6 +12,14 @@ menuconfig GNSS if GNSS +config GNSS_MOTMDM + tristate "Motorola Modem TS 27.010 serdev GNSS receiver support" + depends on MFD_MOTMDM + ---help--- + Say Y here if you have a Motorola modem using TS 27.010 line + discipline for GNSS such as a Motorola Mapphone series device + like Droid 4. + config GNSS_SERIAL tristate diff --git a/drivers/gnss/Makefile b/drivers/gnss/Makefile index 5cf0ebe0330a5..0f7b3da9ac650 100644 --- a/drivers/gnss/Makefile +++ b/drivers/gnss/Makefile @@ -6,6 +6,9 @@ obj-$(CONFIG_GNSS) += gnss.o gnss-y := core.o +obj-$(CONFIG_GNSS_MOTMDM) += gnss-motmdm.o +gnss-motmdm-y := motmdm.o + obj-$(CONFIG_GNSS_SERIAL) += gnss-serial.o gnss-serial-y := serial.o diff --git a/drivers/gnss/motmdm.c b/drivers/gnss/motmdm.c new file mode 100644 index 0000000000000..5ec2fa25df6f6 --- /dev/null +++ b/drivers/gnss/motmdm.c @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motorola Modem TS 27.010 serdev GNSS driver + * + * Copyright (C) 2018 Tony Lindgren <tony@atomide.com> + */ + +#include <linux/errno.h> +#include <linux/gnss.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/platform_device.h> +#include <linux/pm.h> +#include <linux/pm_runtime.h> +#include <linux/serdev.h> +#include <linux/serdev-gsm.h> +#include <linux/slab.h> +#include <linux/wait.h> + +#include <linux/mfd/motorola-mdm.h> + +#define MOTMDM_GNSS_TIMEOUT 1000 +#define MOTMDM_GNSS_RATE 1000 + +/* + * Motorola MDM GNSS device talks AT commands on a dedicated TS 27.010 + * channel. + */ +#define MOTMDM_GNSS_MPD_LEN 4 /* +MPD */ +#define MOTMDM_GNSS_STATUS_LEN (MOTMDM_GNSS_MPD_LEN + 7) /* STATUS= */ +#define MOTMDM_GNSS_NMEA_LEN (MOTMDM_GNSS_MPD_LEN + 8) /* NMEA=NN, */ + +enum motmdm_gnss_status { + MOTMDM_GNSS_UNKNOWN, + MOTMDM_GNSS_INITIALIZED, + MOTMDM_GNSS_DATA_OR_TIMEOUT, + MOTMDM_GNSS_STARTED, + MOTMDM_GNSS_STOPPED, +}; + +struct motmdm_gnss_data { + struct gnss_device *gdev; + struct device *modem; + struct motmdm_dlci mot_dlci; + struct delayed_work restart_work; + ktime_t last_update; + int status; + unsigned char *buf; + size_t len; + unsigned char *dbg; + size_t dbglen; +}; + +static unsigned int rate_ms = MOTMDM_GNSS_RATE; +module_param(rate_ms, uint, 0644); +MODULE_PARM_DESC(rate_ms, "GNSS refresh rate between 1000 and 16000 ms (default 1000 ms)"); + +#ifdef DEBUG +#define MOTMDM_GNSS_DEBUG_BUF_LEN 64 +static void motmdm_gnss_debug(struct motmdm_gnss_data *ddata, + const unsigned char *buf, size_t len, + const char *msg) +{ + struct gnss_device *gdev = ddata->gdev; + + if (!ddata->dbg) + return; + + memset(ddata->dbg, '\0', ddata->dbglen); + snprintf(ddata->dbg, min(ddata->dbglen - 1, len), buf); + dev_dbg(&gdev->dev, "%s: %s\n", msg, ddata->dbg); +} +#else +#define MOTMDM_GNSS_DEBUG_BUF_LEN 0 +static void motmdm_gnss_debug(struct motmdm_gnss_data *ddata, + const unsigned char *buf, size_t len, + const char *msg) +{ +} +#endif + +/* + * Android uses AT+MPDSTART=0,1,100,0 which starts GNSS for a while + * and then GNSS needs to be kicked with an AT command based on a + * status message. + */ +static void motmdm_gnss_restart(struct work_struct *work) +{ + struct motmdm_gnss_data *ddata = + container_of(work, struct motmdm_gnss_data, + restart_work.work); + struct motmdm_dlci *mot_dlci = &ddata->mot_dlci; + struct gnss_device *gdev = ddata->gdev; + const unsigned char *cmd = "AT+MPDSTART=0,1,100,0"; + int error; + + ddata->last_update = ktime_get(); + + error = motmdm_send_command(ddata->modem, mot_dlci, + MOTMDM_GNSS_TIMEOUT, + cmd, strlen(cmd), + ddata->buf, ddata->len); + if (error < 0) { + /* Timeouts seem common.. Don't warn and request the data again */ + if (error != -ETIMEDOUT) + dev_warn(&gdev->dev, "%s: could not start: %i\n", + __func__, error); + + schedule_delayed_work(&ddata->restart_work, + msecs_to_jiffies(MOTMDM_GNSS_RATE)); + + return; + } +} + +static void motmdm_gnss_start(struct gnss_device *gdev, int delay_ms) +{ + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + ktime_t now, next, delta; + int next_ms; + + now = ktime_get(); + next = ktime_add_ms(ddata->last_update, delay_ms); + delta = ktime_sub(next, now); + next_ms = ktime_to_ms(delta); + + if (next_ms < 0) + next_ms = 0; + if (next_ms > delay_ms) + next_ms = delay_ms; + + schedule_delayed_work(&ddata->restart_work, msecs_to_jiffies(next_ms)); +} + +static int motmdm_gnss_stop(struct gnss_device *gdev) +{ + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + struct motmdm_dlci *mot_dlci = &ddata->mot_dlci; + const unsigned char *cmd = "AT+MPDSTOP"; + + cancel_delayed_work_sync(&ddata->restart_work); + + return motmdm_send_command(ddata->modem, mot_dlci, + MOTMDM_GNSS_TIMEOUT, + cmd, strlen(cmd), + ddata->buf, ddata->len); +} + +static int motmdm_gnss_init(struct gnss_device *gdev) +{ + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + struct motmdm_dlci *mot_dlci = &ddata->mot_dlci; + const unsigned char *cmd = "AT+MPDINIT=1"; + int error; + + error = motmdm_send_command(ddata->modem, mot_dlci, + MOTMDM_GNSS_TIMEOUT, + cmd, strlen(cmd), + ddata->buf, ddata->len); + if (error < 0) + return error; + + motmdm_gnss_start(gdev, 0); + + return 0; +} + +static int motmdm_gnss_finish(struct gnss_device *gdev) +{ + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + struct motmdm_dlci *mot_dlci = &ddata->mot_dlci; + const unsigned char *cmd = "AT+MPDINIT=0"; + int error; + + error = motmdm_gnss_stop(gdev); + if (error < 0) + return error; + + return motmdm_send_command(ddata->modem, mot_dlci, + MOTMDM_GNSS_TIMEOUT, + cmd, strlen(cmd), + ddata->buf, ddata->len); +} + +static int motmdm_gnss_receive_data(struct motmdm_dlci *mot_dlci, + const unsigned char *buf, + size_t len) +{ + struct gnss_device *gdev = mot_dlci->drvdata; + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + const unsigned char *msg; + size_t msglen; + int error = 0; + + if (len <= MOTMDM_GNSS_MPD_LEN) + return 0; + + switch (buf[MOTMDM_GNSS_MPD_LEN]) { + case 'N': /* UNNNN~+MPDNMEA=NN, */ + msg = buf + MOTMDM_GNSS_NMEA_LEN; + msglen = len - MOTMDM_GNSS_NMEA_LEN; + + /* + * Firmware bug: Strip out extra duplicate line break always + * in the data + */ + msglen--; + + /* + * Firmware bug: Strip out extra data based on an + * earlier line break in the data + */ + if (msg[msglen - 5 - 1] == 0x0a) + msglen -= 5; + + error = gnss_insert_raw(gdev, msg, msglen); + break; + case 'S': /* UNNNN~+MPDSTATUS=N,NN */ + msg = buf + MOTMDM_GNSS_STATUS_LEN; + msglen = len - MOTMDM_GNSS_STATUS_LEN; + motmdm_gnss_debug(ddata, msg, msglen, "status"); + + switch (msg[0]) { + case '1': + ddata->status = MOTMDM_GNSS_INITIALIZED; + break; + case '2': + ddata->status = MOTMDM_GNSS_DATA_OR_TIMEOUT; + if (rate_ms < MOTMDM_GNSS_RATE) + rate_ms = MOTMDM_GNSS_RATE; + if (rate_ms > 16 * MOTMDM_GNSS_RATE) + rate_ms = 16 * MOTMDM_GNSS_RATE; + motmdm_gnss_start(gdev, rate_ms); + break; + case '3': + ddata->status = MOTMDM_GNSS_STARTED; + break; + case '4': + ddata->status = MOTMDM_GNSS_STOPPED; + break; + default: + ddata->status = MOTMDM_GNSS_UNKNOWN; + break; + } + break; + case 'X': /* What does UNNNN~+MPDXREQ=N mean? */ + default: + motmdm_gnss_debug(ddata, buf, len, "unhandled"); + break; + } + + return 0; +} + +static int motmdm_gnss_open(struct gnss_device *gdev) +{ + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + struct motmdm_dlci *mot_dlci = &ddata->mot_dlci; + int error; + + mot_dlci->drvdata = gdev; + mot_dlci->line = MOTMDM_DLCI4; + mot_dlci->receive_data = motmdm_gnss_receive_data; + + error = motmdm_register_dlci(ddata->modem, mot_dlci); + if (error) + return error; + + error = motmdm_gnss_init(gdev); + if (error) { + motmdm_unregister_dlci(ddata->modem, mot_dlci); + + return error; + } + + return 0; +} + +static void motmdm_gnss_close(struct gnss_device *gdev) +{ + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + struct motmdm_dlci *mot_dlci = &ddata->mot_dlci; + int error; + + mot_dlci->receive_data = NULL; + error = motmdm_gnss_finish(gdev); + if (error < 0) + dev_warn(&gdev->dev, "%s: close failed: %i\n", + __func__, error); + + motmdm_unregister_dlci(ddata->modem, mot_dlci); +} + +static int motmdm_gnss_write_raw(struct gnss_device *gdev, + const unsigned char *buf, + size_t count) +{ + struct motmdm_gnss_data *ddata = gnss_get_drvdata(gdev); + struct motmdm_dlci *mot_dlci = &ddata->mot_dlci; + + return motmdm_send_command(ddata->modem, mot_dlci, + MOTMDM_GNSS_TIMEOUT, + buf, count, ddata->buf, ddata->len); +} + +static const struct gnss_operations motmdm_gnss_ops = { + .open = motmdm_gnss_open, + .close = motmdm_gnss_close, + .write_raw = motmdm_gnss_write_raw, +}; + +static int motmdm_gnss_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct motmdm_gnss_data *ddata; + struct gnss_device *gdev; + int ret; + + ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + INIT_DELAYED_WORK(&ddata->restart_work, motmdm_gnss_restart); + ddata->modem = dev->parent; + ddata->len = PAGE_SIZE; + ddata->dbglen = MOTMDM_GNSS_DEBUG_BUF_LEN; + + ddata->buf = devm_kzalloc(dev, ddata->len, GFP_KERNEL); + if (!ddata->buf) + return -ENOMEM; + + if (ddata->dbglen) { + ddata->dbg = devm_kzalloc(dev, ddata->dbglen, GFP_KERNEL); + if (!ddata->dbg) + return -ENOMEM; + } + + platform_set_drvdata(pdev, ddata); + + gdev = gnss_allocate_device(dev); + if (!gdev) + return -ENOMEM; + + gdev->type = GNSS_TYPE_NMEA; + gdev->ops = &motmdm_gnss_ops; + gnss_set_drvdata(gdev, ddata); + ddata->gdev = gdev; + + ret = gnss_register_device(gdev); + if (ret) + goto err_put_device; + + return 0; + +err_put_device: + gnss_put_device(ddata->gdev); + + return ret; +} + +static int motmdm_gnss_remove(struct platform_device *pdev) +{ + struct motmdm_gnss_data *data = platform_get_drvdata(pdev); + + gnss_deregister_device(data->gdev); + gnss_put_device(data->gdev); + + return 0; +}; + +#ifdef CONFIG_OF +static const struct of_device_id motmdm_gnss_of_match[] = { + { .compatible = "motorola,mapphone-mdm6600-gnss" }, + {}, +}; +MODULE_DEVICE_TABLE(of, motmdm_gnss_of_match); +#endif + +static struct platform_driver motmdm_gnss_driver = { + .driver = { + .name = "gnss-mot-mdm6600", + .of_match_table = of_match_ptr(motmdm_gnss_of_match), + }, + .probe = motmdm_gnss_probe, + .remove = motmdm_gnss_remove, +}; +module_platform_driver(motmdm_gnss_driver); + +MODULE_AUTHOR("Tony Lindgren <tony@atomide.com>"); +MODULE_DESCRIPTION("Motorola Mapphone MDM6600 GNSS receiver driver"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 76f9909cf3966..cdd6b39df12ca 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -1908,6 +1908,14 @@ config MFD_VEXPRESS_SYSREG System Registers are the platform configuration block on the ARM Ltd. Versatile Express board. +config MFD_MOTMDM + tristate "Motorola Modem TS 27.010 Serdev Driver" + depends on SERIAL_DEV_BUS && PHY_MAPPHONE_MDM6600 + help + Select this for Motorola modems using TS 27.010 serial line + discipline such as MDM6600 modem found on Motorola Mapphone + series of devices such as Droid 4. + config RAVE_SP_CORE tristate "RAVE SP MCU core driver" depends on SERIAL_DEV_BUS diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index 12980a4ad4608..89c2d6d130a49 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -239,6 +239,7 @@ obj-$(CONFIG_MFD_STM32_LPTIMER) += stm32-lptimer.o obj-$(CONFIG_MFD_STM32_TIMERS) += stm32-timers.o obj-$(CONFIG_MFD_MXS_LRADC) += mxs-lradc.o obj-$(CONFIG_MFD_SC27XX_PMIC) += sprd-sc27xx-spi.o +obj-$(CONFIG_MFD_MOTMDM) += motorola-mdm.o obj-$(CONFIG_RAVE_SP_CORE) += rave-sp.o obj-$(CONFIG_MFD_ROHM_BD718XX) += rohm-bd718x7.o diff --git a/drivers/mfd/motorola-mdm.c b/drivers/mfd/motorola-mdm.c new file mode 100644 index 0000000000000..119ac19c77581 --- /dev/null +++ b/drivers/mfd/motorola-mdm.c @@ -0,0 +1,1208 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motorola TS 27.010 serial line discipline serdev driver + * Copyright (C) 2018 Tony Lindgren <tony@atomide.com> + */ + +#include <linux/cdev.h> +#include <linux/delay.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/init.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/kfifo.h> +#include <linux/list.h> +#include <linux/module.h> +#include <linux/of.h> +#include <linux/of_platform.h> +#include <linux/platform_device.h> +#include <linux/pm_runtime.h> +#include <linux/poll.h> +#include <linux/slab.h> +#include <linux/serdev.h> +#include <linux/serdev-gsm.h> + +#include <linux/mfd/core.h> +#include <linux/mfd/motorola-mdm.h> +#include <linux/phy/phy.h> + +#include <uapi/linux/gsmmux.h> + +#define MOTMDM_C_N2 3 /* TS27.010 default value */ +#define MOTMDM_DLCI_MIN 1 +#define MOTMDM_DLCI_MAX 12 +#define MOTMDM_DLCI_MASK 0x1ffe /* MOTMDM_DLCI1 to 12 */ +#define MOTMDM_ID_LEN 5 /* U + unsigned short */ +#define MOTMDM_CMD_LEN(x) (MOTMDM_ID_LEN + (x) + 1) +#define MOTMDM_WRITE_BUF_SIZE 1024 +#define MOTMDM_READ_FIFO_SIZE 4096 + +struct motmdm_cfg { + unsigned long cdevmask; + unsigned int aggressive_pm:1; + int modem_dlci; + int codec_dlci; +}; + +struct motmdm { + struct device *dev; + struct phy *phy; + struct gsm_serdev gsd; + const struct motmdm_cfg *cfg; + struct class *class; + struct list_head dlcis; + struct list_head cdevs; + dev_t dev_id; + u16 cmdid; +}; + +struct motmdm_response { + struct list_head node; + u16 id; + const unsigned char *cmd; + size_t cmdlen; + size_t reslen; + unsigned char *buf; + size_t len; + unsigned int handled:1; +}; + +struct motmdm_cdev { + struct motmdm *ddata; + struct list_head node; + struct motmdm_dlci *dlci; + struct device *dev; + struct cdev cdev; + struct rw_semaphore rwsem; + unsigned int count; + unsigned int disconnected:1; + struct mutex read_mutex; /* char dev write lock */ + struct mutex write_mutex; /* char dev read lock */ + char *write_buf; + size_t write_buf_sz; + size_t write_offset; +}; + +static const char * const motmdm_driver_name = "motmdm"; + +static int motmdm_runtime_suspend(struct device *dev) +{ + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + int err; + + if (IS_ERR_OR_NULL(ddata->phy)) + return 0; + + err = phy_pm_runtime_put(ddata->phy); + if (err < 0) + dev_warn(dev, "%s: phy_pm_runtime_put: %i\n", + __func__, err); + + atomic_set(&ddata->gsd.asleep, 1); + + return 0; +} + +static int motmdm_runtime_resume(struct device *dev) +{ + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + int err; + + if (IS_ERR_OR_NULL(ddata->phy)) + return 0; + + err = phy_pm_runtime_get_sync(ddata->phy); + if (err < 0) + dev_warn(dev, "%s: phy_pm_runtime_get: %i\n", + __func__, err); + + atomic_set(&ddata->gsd.asleep, 0); + gsm_serdev_data_kick(&ddata->gsd); + + return 0; +} + +static const struct dev_pm_ops motmdm_pm_ops = { + SET_RUNTIME_PM_OPS(motmdm_runtime_suspend, + motmdm_runtime_resume, + NULL) +}; + +/* + * Motorola MDM6600 devices have GPIO wake pins shared between the USB PHY and + * the TS 27.010 interface. So for PM, we need to use the phy_pm_runtime + * runtime calls. Otherwise the modem won't respond to anything on the UART + * and will never idle either. + */ +static int motmdm_init_phy(struct device *dev) +{ + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + int err; + + ddata->phy = devm_of_phy_get(dev, dev->of_node, NULL); + if (IS_ERR(ddata->phy)) { + err = PTR_ERR(ddata->phy); + if (err != -EPROBE_DEFER) + dev_err(dev, "%s: no phy: %i\n", __func__, err); + + return err; + } + + return 0; +} + +/* + * Motorola MDM6600 devices add a custom packet numbering layer on top of the + * TS 27.010 DLCI channels. This is a layering violation as all the devices + * are on dedicated channels already. For some reason the packet numbering is + * not specific to each DLCI.. It is for all the DLCI instead. As both ends + * can increase packet IDs, conflicts are guaranteed but seem to be harmless. + * Who knows, maybe the packet IDs were specified by frustrated developers to + * debug buggy modem code reponding on a wrong DLCI. Valid packet numbers are + * from 0000 to 9999 decimal. We just parse the modem sent packet number to + * match the response to sent commands and don't use modem sent packet + * numbers it for new command packets we send out. + */ +static int motmdm_read_packet_id(struct gsm_serdev_dlci *gsm_dlci, + const unsigned char *buf, + size_t len) +{ + struct motmdm_dlci *mot_dlci = + container_of(gsm_dlci, struct motmdm_dlci, + gsm_dlci); + struct motmdm *ddata = mot_dlci->privdata; + unsigned char tmp[MOTMDM_ID_LEN]; + int err; + u16 id; + + if (WARN(!ddata, "%s no ddata?\n", __func__)) + return 0; + + if (len < MOTMDM_ID_LEN || buf[0] != 'U') + return -ECOMM; + + snprintf(tmp, MOTMDM_ID_LEN, "%s", buf + 1); + err = kstrtou16(tmp, 10, &id); + if (err) + return -ECOMM; + + return id; +} + +/* + * For new packets, we just use jiffies based numbering and let the modem + * deal with any possible numbering conflicts across the DLCI. + */ +static int motmdm_new_packet_id(void) +{ + return jiffies % 10000; +} + +/* + * The modem DLCI1 provides also modem status information. We just forward + * DLCI1 as a character device to userspace. However in a bit of a layering + * violation, we also need to parse the modem state from DLCI1 for modem + * state notifications. + * + * The notifications start with a '~' character and are separate from modem + * commands. So let's try to stick to just parsing the notifications here. + * + * This is needed for Alsa ASoC codec driver to reconfigure clocks and codec + * hardware with set_tdm_slot() for voice calls automatically. And it can be + * later on used for things like signal strength etc. + */ +static void motmdm_read_state(struct motmdm_dlci *mot_dlci, + const unsigned char *buf, + size_t len) +{ + struct motmdm *ddata = mot_dlci->privdata; + struct motmdm_dlci *tmp; + enum motmdm_state state = MOTMDM_STATE_IDLE; + + switch (len) { + case 12 + 1: + if (buf[0] != '~') + break; + if (!strncmp(buf, "~+CIEV=1,1,0", 12)) + state = MOTMDM_STATE_CONNECTING; + if (!strncmp(buf, "~+CIEV=1,4,0", 12)) + state = MOTMDM_STATE_INCOMING; + else if (!strncmp(buf, "~+CIEV=1,2,0", 12)) + state = MOTMDM_STATE_CONNECTED; + else if (!strncmp(buf, "~+CIEV=1,0,0", 12)) + state = MOTMDM_STATE_DISCONNECTED; + break; + default: + return; + } + + if (state == MOTMDM_STATE_IDLE) + return; + + list_for_each_entry(tmp, &ddata->dlcis, node) { + if (!tmp->notify) + continue; + + tmp->notify(tmp, state); + } +} + +/* Fix line breaks for apps if needed and feed kfifo */ +static int motmdm_dlci_feed_kfifo(struct motmdm_dlci *mot_dlci, + const unsigned char *buf, + size_t len) +{ + size_t newlen = len; + int err; + + if (len && buf[len - 1] == '\n') { + if (len > 1 && buf[len - 2] != '\r') + newlen--; + else if (len == 1) + newlen--; + } + + err = kfifo_in(&mot_dlci->read_fifo, buf, newlen); + if (err != newlen) + return -ENOSPC; + + if (newlen != len) { + err = kfifo_in(&mot_dlci->read_fifo, "\r\n", 2); + if (err != 2) + err = -ENOSPC; + else + newlen += err; + } + + return newlen; +} + +/* + * Read handling for Motorola custom layering on top of TS 27.010 + */ +static int motmdm_dlci_receive_buf(struct gsm_serdev_dlci *gsm_dlci, + const unsigned char *buf, + size_t len) +{ + struct motmdm_dlci *mot_dlci = + container_of(gsm_dlci, struct motmdm_dlci, + gsm_dlci); + struct motmdm *ddata = mot_dlci->privdata; + const unsigned char *msg; + size_t msglen; + int id, err; + + if (len < (MOTMDM_ID_LEN + 1) || buf[0] != 'U') + return 0; + + id = motmdm_read_packet_id(gsm_dlci, buf, len); + if (id < 0) + return 0; + + /* Strip out Motorola custom packet numbering */ + msg = buf + MOTMDM_ID_LEN; + msglen = len - MOTMDM_ID_LEN; + + if (mot_dlci->line == ddata->cfg->modem_dlci) + motmdm_read_state(mot_dlci, msg, msglen); + + /* Motorola custom data or a command ack? */ + if (buf[MOTMDM_ID_LEN] == '~' && mot_dlci->receive_data) + mot_dlci->receive_data(mot_dlci, msg + 1, msglen - 1); + else if (mot_dlci->handle_command) + mot_dlci->handle_command(mot_dlci, id, msg, msglen); + + if (kfifo_initialized(&mot_dlci->read_fifo)) { + err = motmdm_dlci_feed_kfifo(mot_dlci, msg, msglen); + if (err < 0) + goto err_kfifo; + } + + err = msglen; + + wake_up_interruptible(&mot_dlci->read_queue); + +err_kfifo: + + return err; +} + +/* + * Helper for child device drivers to send a command to a DLCI and wait + * for result with a matching packet ID. + */ +static int motmdm_dlci_send_command(struct device *dev, + struct motmdm_dlci *mot_dlci, + unsigned long timeout_ms, + const unsigned char *cmd, size_t cmdlen, + unsigned char *rsp, size_t rsplen) +{ + struct motmdm_response *resp, *tmp; + struct list_head *pos, *q; + unsigned char *delim; + int err; + + resp = kzalloc(sizeof(*resp), GFP_KERNEL); + if (!resp) + return -ENOMEM; + + memset(rsp, 0, rsplen); + + resp->cmd = cmd; + resp->cmdlen = cmdlen; + resp->buf = rsp; + resp->len = rsplen; + resp->id = motmdm_new_packet_id(); + + /* Firmware will return only the command without value */ + if (cmd[cmdlen - 1] != '=') { + delim = strchr(cmd, '='); + if (delim) + resp->cmdlen -= strlen(delim); + } + + list_add_tail(&resp->node, &mot_dlci->list); + + err = mot_dlci->write(dev, mot_dlci, resp->id, + cmd, cmdlen); + if (err < 0) + goto unregister; + + err = wait_event_timeout(mot_dlci->read_queue, resp->handled, + msecs_to_jiffies(timeout_ms)); + if (err < 0) + goto unregister; + + if (err == 0) { + err = -ETIMEDOUT; + goto unregister; + } + + dev_dbg(dev, "%s: %s got %s\n", __func__, cmd, resp->buf); + + err = resp->reslen; + +unregister: + list_for_each_safe(pos, q, &mot_dlci->list) { + tmp = list_entry(pos, struct motmdm_response, node); + if (tmp->id == resp->id) + list_del(pos); + } + + kfree(resp); + + return err; +} + +/* + * Helper for child device drivers to parse the command response from a DLCI + */ +static int motmdm_dlci_handle_command(struct motmdm_dlci *mot_dlci, int id, + const unsigned char *buf, size_t len) +{ + struct motmdm_response *resp = NULL; + struct list_head *pos, *q; + int resp_start; + + list_for_each_safe(pos, q, &mot_dlci->list) { + resp = list_entry(pos, struct motmdm_response, node); + if (resp->id == id) + break; + } + + if (!resp || !resp->buf) + return -ENODEV; + + /* Firmware leaves out AT from the commands */ + resp_start = resp->cmdlen - 2; + if (len < resp_start) + return -EPIPE; + + /* Only some firmware messages start with ':' */ + if (buf[resp_start] == ':') + resp_start++; + + resp->reslen = min3(len - resp_start, resp->len, len); + strncpy(resp->buf, buf + resp_start, resp->reslen); + + /* Leave out trailing line break if there */ + if (resp->reslen > 1 && resp->buf[resp->reslen - 1] == '\n') { + resp->buf[resp->reslen - 1] = '\0'; + resp->reslen--; + } + + resp->handled = true; + + return 0; +} + +/* + * Write handling for Motorola custom layering on top of TS 27.010 + */ +static int motmdm_dlci_write(struct device *dev, struct motmdm_dlci *mot_dlci, + int cmdid, const unsigned char *buf, size_t count) +{ + struct motmdm *ddata; + struct gsm_serdev *gsd; + int err, cmdlen; + char *cmd; + + if (!dev || !mot_dlci || !buf || !count) + return -EINVAL; + + ddata = gsm_serdev_get_drvdata(dev); + if (!ddata) + return -ENODEV; + + gsd = &ddata->gsd; + + err = pm_runtime_get_sync(dev); + if ((err != -EINPROGRESS) && err < 0) { + pm_runtime_put_noidle(dev); + + return err; + } + + cmdlen = MOTMDM_CMD_LEN(count); + cmd = kmalloc(cmdlen, GFP_KERNEL); + if (!cmd) + return -ENOMEM; + + switch (cmdid) { + case -ENOENT: + /* No ID number, just U for continuation messages */ + snprintf(cmd, cmdlen, "U%s\r", buf); + break; + case 0 ... 9999: + /* Valid ID */ + mot_dlci->id = cmdid; + snprintf(cmd, cmdlen, "U%04i%s\r", mot_dlci->id, buf); + break; + default: + /* Assign ID */ + mot_dlci->id = motmdm_new_packet_id(); + snprintf(cmd, cmdlen, "U%04i%s\r", mot_dlci->id, buf); + break; + } + + err = gsm_serdev_write(gsd, &mot_dlci->gsm_dlci, cmd, cmdlen); + if (err == cmdlen) + err = count; + + kfree(cmd); + pm_runtime_put(dev); + + return err; +} + + + +int motmdm_register_dlci(struct device *dev, struct motmdm_dlci *mot_dlci) +{ + struct motmdm *ddata; + struct gsm_serdev *gsd; + struct gsm_serdev_dlci *gsm_dlci; + int err; + + if (!dev || !mot_dlci || !mot_dlci->line) + return -EINVAL; + + err = pm_runtime_get_sync(dev); + if ((err != -EINPROGRESS) && err < 0) { + pm_runtime_put_noidle(dev); + + return err; + } + + ddata = gsm_serdev_get_drvdata(dev); + gsd = &ddata->gsd; + gsm_dlci = &mot_dlci->gsm_dlci; + mot_dlci->write = motmdm_dlci_write; + mot_dlci->send_command = motmdm_dlci_send_command; + mot_dlci->handle_command = motmdm_dlci_handle_command; + INIT_LIST_HEAD(&mot_dlci->list); + init_waitqueue_head(&mot_dlci->read_queue); + gsm_dlci->line = mot_dlci->line; + gsm_dlci->receive_buf = motmdm_dlci_receive_buf; + + err = gsm_serdev_register_dlci(gsd, gsm_dlci); + if (err) { + dev_warn(dev, "error registering dlci%i: %i\n", + mot_dlci->line, err); + kfifo_free(&mot_dlci->read_fifo); + memset(gsm_dlci, 0, sizeof(*gsm_dlci)); + } else { + mot_dlci->privdata = ddata; + } + + list_add_tail(&mot_dlci->node, &ddata->dlcis); + + pm_runtime_put(dev); + + return err; +} +EXPORT_SYMBOL_GPL(motmdm_register_dlci); + +void motmdm_unregister_dlci(struct device *dev, struct motmdm_dlci *mot_dlci) +{ + struct motmdm *ddata; + struct gsm_serdev *gsd; + struct gsm_serdev_dlci *gsm_dlci; + struct list_head *pos, *q; + struct motmdm_dlci *tmp; + int err; + + if (!dev || !mot_dlci || !mot_dlci->line) + return; + + err = pm_runtime_get_sync(dev); + if ((err != -EINPROGRESS) && err < 0) { + pm_runtime_put_noidle(dev); + return; + } + + ddata = gsm_serdev_get_drvdata(dev); + + list_for_each_safe(pos, q, &ddata->dlcis) { + tmp = list_entry(pos, struct motmdm_dlci, node); + if (tmp == mot_dlci) + list_del(pos); + } + + gsd = &ddata->gsd; + gsm_dlci = &mot_dlci->gsm_dlci; + gsm_serdev_unregister_dlci(gsd, gsm_dlci); + gsm_dlci->receive_buf = NULL; + mot_dlci->notify = NULL; + mot_dlci->privdata = NULL; + + pm_runtime_put(dev); +} +EXPORT_SYMBOL_GPL(motmdm_unregister_dlci); + +/* + * Character devices for DLCI channels with no serdev drivers + */ +static int motmdm_cdev_open(struct inode *inode, struct file *file) +{ + struct motmdm_cdev *cdata; + int ret = 0; + + cdata = container_of(inode->i_cdev, struct motmdm_cdev, cdev); + get_device(cdata->dev); + nonseekable_open(inode, file); + file->private_data = cdata; + + down_write(&cdata->rwsem); + if (cdata->disconnected) { + ret = -ENODEV; + goto unlock; + } + +unlock: + up_write(&cdata->rwsem); + + if (ret) + put_device(cdata->dev); + + return ret; +} + +static int motmdm_cdev_release(struct inode *inode, struct file *file) +{ + struct motmdm_cdev *cdata = file->private_data; + + down_write(&cdata->rwsem); + if (cdata->disconnected) + goto unlock; + +unlock: + up_write(&cdata->rwsem); + + put_device(cdata->dev); + + return 0; +} + +static ssize_t motmdm_cdev_read(struct file *file, char __user *buf, + size_t count, loff_t *pos) +{ + struct motmdm_cdev *cdata = file->private_data; + struct motmdm_dlci *mot_dlci = cdata->dlci; + unsigned int copied; + int err; + + mutex_lock(&cdata->read_mutex); + while (kfifo_is_empty(&mot_dlci->read_fifo)) { + mutex_unlock(&cdata->read_mutex); + + if (cdata->disconnected) + return 0; + + if (file->f_flags & O_NONBLOCK) + return -EAGAIN; + + err = wait_event_interruptible(mot_dlci->read_queue, + cdata->disconnected || + !kfifo_is_empty(&mot_dlci->read_fifo)); + if (err) + return -ERESTARTSYS; + + mutex_lock(&cdata->read_mutex); + } + + err = kfifo_to_user(&mot_dlci->read_fifo, buf, count, &copied); + if (err == 0) + err = copied; + + mutex_unlock(&cdata->read_mutex); + + return err; +} + +static ssize_t motmdm_cdev_write_packet(struct motmdm_cdev *cdata, int cmdid) +{ + struct motmdm_dlci *mot_dlci = cdata->dlci; + struct motmdm *ddata = mot_dlci->privdata; + + return mot_dlci->write(ddata->dev, mot_dlci, cmdid, + cdata->write_buf, cdata->write_offset - 1); +} + +static ssize_t motmdm_cdev_write(struct file *file, const char __user *buf, + size_t count, loff_t *pos) +{ + struct motmdm_cdev *cdata = file->private_data; + size_t written = 0; + int err, flag = -ENOMSG; + + if (cdata->disconnected) + return -EIO; + + if (!count) + return 0; + + err = mutex_lock_interruptible(&cdata->write_mutex); + if (err) + return -ERESTARTSYS; + + for (;;) { + size_t n; + bool packet = false; + + n = min(count, cdata->write_buf_sz - + cdata->write_offset - count); + if (copy_from_user(cdata->write_buf + cdata->write_offset, + buf, n)) { + err = -EFAULT; + goto out_unlock; + } + + cdata->write_offset += n; + cdata->write_buf[cdata->write_offset] = '\0'; + if (cdata->write_offset) { + u8 last = cdata->write_buf[cdata->write_offset - 1]; + + switch (last) { + case 0x1a: /* Continuation packets end with ^Z */ + flag = -ENOENT; + /* Fallthrough */ + case '\n': + case '\r': + packet = true; + break; + default: + break; + } + } + + down_read(&cdata->rwsem); + + if (cdata->disconnected) { + err = -EIO; + goto err_write; + } + + if (packet) { + err = motmdm_cdev_write_packet(cdata, flag); + if (err < 0) + goto err_write; + + cdata->write_offset = 0; + } + + err = n; + +err_write: + up_read(&cdata->rwsem); + + if (err < 0) + break; + + written += err; + buf += err; + + if (written == count) + break; + } + + if (written) + err = written; + +out_unlock: + mutex_unlock(&cdata->write_mutex); + + return err; +} + +static __poll_t motmdm_cdev_poll(struct file *file, poll_table *wait) +{ + struct motmdm_cdev *cdata = file->private_data; + struct motmdm_dlci *mot_dlci = cdata->dlci; + __poll_t mask = 0; + + poll_wait(file, &mot_dlci->read_queue, wait); + + if (!kfifo_is_empty(&mot_dlci->read_fifo)) + mask |= EPOLLIN | EPOLLRDNORM; + if (cdata->write_offset < cdata->write_buf_sz) + mask |= EPOLLOUT | EPOLLWRNORM; + if (cdata->disconnected) + mask |= EPOLLHUP; + + return mask; +} + +static const struct file_operations motmdm_fops = { + .owner = THIS_MODULE, + .open = motmdm_cdev_open, + .release = motmdm_cdev_release, + .read = motmdm_cdev_read, + .write = motmdm_cdev_write, + .poll = motmdm_cdev_poll, + .llseek = no_llseek, +}; + +static int motmdm_cdev_init_one(struct motmdm *ddata, int index) +{ + struct motmdm_cdev *cdata; + struct motmdm_dlci *mot_dlci; + int err; + + mot_dlci = kzalloc(sizeof(*mot_dlci), GFP_KERNEL); + if (!mot_dlci) + return -ENOMEM; + + mot_dlci->drvdata = ddata; + mot_dlci->line = index; + + err = kfifo_alloc(&mot_dlci->read_fifo, MOTMDM_READ_FIFO_SIZE, + GFP_KERNEL); + if (err) + goto err_free_dlci; + + cdata = kzalloc(sizeof(*cdata), GFP_KERNEL); + if (!cdata) + goto err_free_kfifo; + + cdata->dlci = mot_dlci; + init_rwsem(&cdata->rwsem); + mutex_init(&cdata->read_mutex); + mutex_init(&cdata->write_mutex); + cdata->write_buf_sz = MOTMDM_WRITE_BUF_SIZE; + cdata->write_buf = kzalloc(cdata->write_buf_sz, GFP_KERNEL); + if (!cdata->write_buf) + goto err_free_cdata; + + err = motmdm_register_dlci(ddata->dev, cdata->dlci); + if (err) + goto err_free_write_buf; + + cdata->dev = device_create(ddata->class, ddata->dev, + MKDEV(MAJOR(ddata->dev_id), index), + mot_dlci, "%s%i", motmdm_driver_name, + index); + if (IS_ERR(cdata->dev)) { + err = PTR_ERR(cdata->dev); + goto err_unregister; + } + + cdev_init(&cdata->cdev, &motmdm_fops); + err = cdev_add(&cdata->cdev, MKDEV(MAJOR(ddata->dev_id), index), + MOTMDM_DLCI_MAX); + if (err) + goto err_device_destroy; + + list_add_tail(&cdata->node, &ddata->cdevs); + + return 0; + +err_device_destroy: + device_destroy(ddata->class, MKDEV(MAJOR(ddata->dev_id), index)); +err_unregister: + motmdm_unregister_dlci(ddata->dev, cdata->dlci); +err_free_write_buf: + kfree(cdata->write_buf); +err_free_cdata: + kfree(cdata); +err_free_kfifo: + kfifo_free(&mot_dlci->read_fifo); +err_free_dlci: + kfree(mot_dlci); + + return err; +} + +static void motmdm_cdev_cleanup(struct device *dev); + +static int motmdm_cdev_init(struct device *dev) +{ + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + int bit, err; + + err = alloc_chrdev_region(&ddata->dev_id, 0, MOTMDM_DLCI_MAX, + motmdm_driver_name); + if (err) + return err; + + ddata->class = class_create(THIS_MODULE, motmdm_driver_name); + if (IS_ERR(ddata->class)) { + err = PTR_ERR(ddata->class); + motmdm_cdev_cleanup(dev); + + return err; + } + + for_each_set_bit(bit, &ddata->cfg->cdevmask, BITS_PER_LONG) { + err = motmdm_cdev_init_one(ddata, bit); + if (err) { + motmdm_cdev_cleanup(dev); + + return err; + } + } + + return 0; +} + +static void motmdm_cdev_free_one(struct motmdm_cdev *cdata) +{ + struct motmdm_dlci *mot_dlci = cdata->dlci; + struct motmdm *ddata = mot_dlci->privdata; + + down_write(&cdata->rwsem); + cdata->disconnected = true; + if (cdata->count) + wake_up_interruptible(&mot_dlci->read_queue); + up_write(&cdata->rwsem); + + cdev_del(&cdata->cdev); + device_destroy(ddata->class, + MKDEV(MAJOR(ddata->dev_id), mot_dlci->line)); + kfree(cdata->write_buf); + + motmdm_unregister_dlci(ddata->dev, cdata->dlci); + kfifo_free(&mot_dlci->read_fifo); + kfree(cdata->dlci); + kfree(cdata); +} + +static void motmdm_cdev_cleanup(struct device *dev) +{ + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + struct motmdm_cdev *cdata, *tmp; + + list_for_each_entry_safe(cdata, tmp, &ddata->cdevs, node) { + motmdm_cdev_free_one(cdata); + list_del(&cdata->node); + } + + class_destroy(ddata->class); + + unregister_chrdev_region(ddata->dev_id, MOTMDM_DLCI_MAX); +} + +static int motmdm_check_revision(struct device *dev) +{ + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + struct motmdm_dlci *mot_dlci; + const unsigned char *cmd = "AT+VERSION="; + unsigned char *buf; + int retries = 3, err; + + mot_dlci = kzalloc(sizeof(*mot_dlci), GFP_KERNEL); + if (!mot_dlci) + return -ENOMEM; + + mot_dlci->drvdata = ddata; + mot_dlci->line = MOTMDM_DLCI6; + + buf = kzalloc(PAGE_SIZE, GFP_KERNEL); + if (!buf) { + err = -ENOMEM; + goto free_dlci; + } + + err = motmdm_register_dlci(dev, mot_dlci); + if (err) + goto free_buf; + + while (retries--) { + err = motmdm_dlci_send_command(dev, mot_dlci, 1000, + cmd, strlen(cmd), + buf, PAGE_SIZE); + if (err >= 0) { + msleep(100); + break; + } + + msleep(500); + } + + if (err < 0) { + dev_err(dev, "Could not connect: %i\n", err); + } else if (!strncmp(buf, "ERROR", 5)) { + dev_err(dev, "Firmware error: %s\n", buf); + err = -ENODEV; + } else { + dev_info(dev, "Firmware: %s\n", buf); + err = 0; + } + + motmdm_unregister_dlci(dev, mot_dlci); + +free_buf: + kfree(buf); +free_dlci: + kfree(mot_dlci); + + return err; +} + +static int motmdm_set_config(struct device *dev, int retransmissions) +{ + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + struct gsm_serdev *gsd = &ddata->gsd; + struct gsm_config c; + int err; + + err = gsm_serdev_get_config(gsd, &c); + if (err) + return err; + + c.i = 1; /* 1 = UIH, 2 = UI */ + c.initiator = 1; + c.encapsulation = 0; /* basic mode */ + c.adaption = 1; + c.mru = 1024; /* from android TS 27010 driver */ + c.mtu = 1024; /* from android TS 27010 driver */ + c.t1 = 10; /* ack timer, default 10ms */ + c.t2 = 34; /* response timer, default 34 */ + c.n2 = retransmissions; /* retransmissions, default 3 */ + + err = gsm_serdev_set_config(gsd, &c); + if (err) + return err; + + return 0; +} + +static int motmdm_output(struct gsm_serdev *gsd, u8 *data, int len) +{ + struct serdev_device *serdev = gsd->serdev; + struct device *dev = &serdev->dev; + int err; + + err = pm_runtime_get(dev); + if ((err != -EINPROGRESS) && err < 0) { + pm_runtime_put_noidle(dev); + + return err; + } + + serdev_device_write_buf(serdev, data, len); + + pm_runtime_put(dev); + + return len; +} + +static const struct motmdm_cfg mapphone_mdm6600_data = { + .cdevmask = MOTMDM_DLCI_MASK & ~(BIT(MOTMDM_DLCI2) | BIT(MOTMDM_DLCI4)), + .aggressive_pm = true, + .modem_dlci = MOTMDM_DLCI1, + .codec_dlci = MOTMDM_DLCI2, +}; + +static const struct of_device_id motmdm_id_table[] = { + { + .compatible = "motorola,mapphone-mdm6600-serdev", + .data = &mapphone_mdm6600_data, + }, + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, motmdm_id_table); + +static const struct mfd_cell motmdm_mfd_devices[] = { + { + .name = "mot-mdm6600-codec", + }, + { + .name = "gnss-mot-mdm6600", + .of_compatible = "motorola,mapphone-mdm6600-gnss", + }, +}; + +static int motmdm_probe(struct serdev_device *serdev) +{ + struct device *dev = &serdev->dev; + const struct of_device_id *match; + struct gsm_serdev *gsd; + struct motmdm *ddata; + int err; + + match = of_match_device(of_match_ptr(motmdm_id_table), dev); + if (!match) + return -ENODEV; + + ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ddata->dev = dev; + ddata->cfg = match->data; + + INIT_LIST_HEAD(&ddata->dlcis); + INIT_LIST_HEAD(&ddata->cdevs); + gsd = &ddata->gsd; + gsd->serdev = serdev; + gsd->output = motmdm_output; + serdev_device_set_drvdata(serdev, gsd); + gsm_serdev_set_drvdata(dev, ddata); + + err = motmdm_init_phy(dev); + if (err) + return err; + + pm_runtime_set_autosuspend_delay(dev, 50); + pm_runtime_use_autosuspend(dev); + pm_runtime_enable(dev); + err = pm_runtime_get_sync(dev); + if (err < 0) { + pm_runtime_put_noidle(dev); + + return err; + } + + err = gsm_serdev_register_device(gsd); + if (err) + goto disable; + + err = serdev_device_open(gsd->serdev); + if (err) + goto disable; + + serdev_device_set_baudrate(gsd->serdev, 115200); + serdev_device_set_rts(gsd->serdev, true); + serdev_device_set_flow_control(gsd->serdev, true); + + /* + * Getting dlci0 connected quirk: We set initial retransmissions + * value high to get n_gsm to send SABM packets. Then after about + * three seconds we'll get a "reassembly overrun" error from firmware + * on dlci0 followed by DM(P) packets and then we're connected in ADM + * mode. Note we will set the retransmissions back to default value + * later on. + */ + err = motmdm_set_config(dev, MOTMDM_C_N2 * 10); + if (err) + goto close; + + msleep(3000); + + err = motmdm_check_revision(dev); + if (err) + goto close; + + msleep(500); + + err = motmdm_cdev_init(dev); + if (err) + goto close; + + pm_runtime_put_sync(dev); + + err = devm_mfd_add_devices(dev, 0, motmdm_mfd_devices, + ARRAY_SIZE(motmdm_mfd_devices), + NULL, 0, NULL); + if (err) + goto cdev_cleanup; + + /* Set initial retransmissions back to default value */ + err = motmdm_set_config(dev, MOTMDM_C_N2); + if (err) + goto cdev_cleanup; + + if (ddata->cfg->aggressive_pm) { + pm_runtime_set_autosuspend_delay(&serdev->ctrl->dev, 50); + pm_runtime_put(&serdev->ctrl->dev); + } + + return 0; + +cdev_cleanup: + motmdm_cdev_cleanup(dev); + +close: + serdev_device_close(serdev); + +disable: + pm_runtime_put_sync(dev); + pm_runtime_disable(dev); + pm_runtime_dont_use_autosuspend(dev); + gsm_serdev_unregister_device(gsd); + + return err; +} + +static void motmdm_remove(struct serdev_device *serdev) +{ + struct gsm_serdev *gsd = serdev_device_get_drvdata(serdev); + struct device *dev = &serdev->dev; + struct motmdm *ddata = gsm_serdev_get_drvdata(dev); + int err; + + /* Balance the put done in probe for UART */ + if (ddata->cfg->aggressive_pm) + pm_runtime_get(&serdev->ctrl->dev); + + err = pm_runtime_get_sync(dev); + if (err < 0) + dev_warn(dev, "%s: PM runtime: %i\n", __func__, err); + + motmdm_cdev_cleanup(dev); + serdev_device_close(serdev); + gsm_serdev_unregister_device(gsd); + + pm_runtime_put_sync(dev); + pm_runtime_disable(dev); + pm_runtime_dont_use_autosuspend(dev); +} + +static struct serdev_device_driver motmdm_driver = { + .driver = { + .name = "motmdm", + .of_match_table = of_match_ptr(motmdm_id_table), + .pm = &motmdm_pm_ops, + }, + .probe = motmdm_probe, + .remove = motmdm_remove, +}; + +module_serdev_device_driver(motmdm_driver); + +MODULE_ALIAS("platform:motorola-mdm"); +MODULE_DESCRIPTION("Motorola Modem TS 27.010 serdev driver"); +MODULE_AUTHOR("Tony Lindgren <tony@atomide.com"); +MODULE_LICENSE("GPL v2"); diff --git a/drivers/tty/n_gsm.c b/drivers/tty/n_gsm.c index 6f7da9a9d76f0..348b1db721359 100644 --- a/drivers/tty/n_gsm.c +++ b/drivers/tty/n_gsm.c @@ -39,6 +39,7 @@ #include <linux/file.h> #include <linux/uaccess.h> #include <linux/module.h> +#include <linux/serdev.h> #include <linux/timer.h> #include <linux/tty_flip.h> #include <linux/tty_driver.h> @@ -50,6 +51,7 @@ #include <linux/netdevice.h> #include <linux/etherdevice.h> #include <linux/gsmmux.h> +#include <linux/serdev-gsm.h> static int debug; module_param(debug, int, 0600); @@ -143,8 +145,9 @@ struct gsm_dlci { struct sk_buff *skb; /* Frame being sent */ struct sk_buff_head skb_list; /* Queued frames */ /* Data handling callback */ - void (*data)(struct gsm_dlci *dlci, u8 *data, int len); - void (*prev_data)(struct gsm_dlci *dlci, u8 *data, int len); + void (*data)(struct gsm_dlci *dlci, const u8 *data, int len); + void (*prev_data)(struct gsm_dlci *dlci, const u8 *data, int len); + struct gsm_serdev_dlci *ops; /* serdev dlci ops, if used */ struct net_device *net; /* network interface, if created */ }; @@ -179,6 +182,7 @@ struct gsm_control { */ struct gsm_mux { + struct gsm_serdev *gsd; /* Serial device bus data */ struct tty_struct *tty; /* The tty our ldisc is bound to */ spinlock_t lock; struct mutex mutex; @@ -988,7 +992,7 @@ static void gsm_dlci_data_kick(struct gsm_dlci *dlci) * Encode up and queue a UI/UIH frame containing our response. */ -static void gsm_control_reply(struct gsm_mux *gsm, int cmd, u8 *data, +static void gsm_control_reply(struct gsm_mux *gsm, int cmd, const u8 *data, int dlen) { struct gsm_msg *msg; @@ -1073,14 +1077,14 @@ static void gsm_process_modem(struct tty_struct *tty, struct gsm_dlci *dlci, * and if need be stuff a break message down the tty. */ -static void gsm_control_modem(struct gsm_mux *gsm, u8 *data, int clen) +static void gsm_control_modem(struct gsm_mux *gsm, const u8 *data, int clen) { unsigned int addr = 0; unsigned int modem = 0; unsigned int brk = 0; struct gsm_dlci *dlci; int len = clen; - u8 *dp = data; + const u8 *dp = data; struct tty_struct *tty; while (gsm_read_ea(&addr, *dp++) == 0) { @@ -1134,13 +1138,13 @@ static void gsm_control_modem(struct gsm_mux *gsm, u8 *data, int clen) * this into the uplink tty if present */ -static void gsm_control_rls(struct gsm_mux *gsm, u8 *data, int clen) +static void gsm_control_rls(struct gsm_mux *gsm, const u8 *data, int clen) { struct tty_port *port; unsigned int addr = 0; u8 bits; int len = clen; - u8 *dp = data; + const u8 *dp = data; while (gsm_read_ea(&addr, *dp++) == 0) { len--; @@ -1189,7 +1193,7 @@ static void gsm_dlci_begin_close(struct gsm_dlci *dlci); */ static void gsm_control_message(struct gsm_mux *gsm, unsigned int command, - u8 *data, int clen) + const u8 *data, int clen) { u8 buf[1]; unsigned long flags; @@ -1261,7 +1265,7 @@ static void gsm_control_message(struct gsm_mux *gsm, unsigned int command, */ static void gsm_control_response(struct gsm_mux *gsm, unsigned int command, - u8 *data, int clen) + const u8 *data, int clen) { struct gsm_control *ctrl; unsigned long flags; @@ -1553,7 +1557,7 @@ static void gsm_dlci_begin_close(struct gsm_dlci *dlci) * open we shovel the bits down it, if not we drop them. */ -static void gsm_dlci_data(struct gsm_dlci *dlci, u8 *data, int clen) +static void gsm_dlci_data(struct gsm_dlci *dlci, const u8 *data, int clen) { /* krefs .. */ struct tty_port *port = &dlci->port; @@ -1603,7 +1607,7 @@ static void gsm_dlci_data(struct gsm_dlci *dlci, u8 *data, int clen) * and we divide up the work accordingly. */ -static void gsm_dlci_command(struct gsm_dlci *dlci, u8 *data, int len) +static void gsm_dlci_command(struct gsm_dlci *dlci, const u8 *data, int len) { /* See what command is involved */ unsigned int command = 0; @@ -2214,6 +2218,424 @@ static struct gsm_mux *gsm_alloc_mux(void) return gsm; } +static void gsm_copy_config_values(struct gsm_mux *gsm, + struct gsm_config *c) +{ + memset(c, 0, sizeof(*c)); + c->adaption = gsm->adaption; + c->encapsulation = gsm->encoding; + c->initiator = gsm->initiator; + c->t1 = gsm->t1; + c->t2 = gsm->t2; + c->t3 = 0; /* Not supported */ + c->n2 = gsm->n2; + if (gsm->ftype == UIH) + c->i = 1; + else + c->i = 2; + pr_debug("Ftype %d i %d\n", gsm->ftype, c->i); + c->mru = gsm->mru; + c->mtu = gsm->mtu; + c->k = 0; +} + +static int gsm_config(struct gsm_mux *gsm, struct gsm_config *c) +{ + int need_close = 0; + int need_restart = 0; + + /* Stuff we don't support yet - UI or I frame transport, windowing */ + if ((c->adaption != 1 && c->adaption != 2) || c->k) + return -EOPNOTSUPP; + /* Check the MRU/MTU range looks sane */ + if (c->mru > MAX_MRU || c->mtu > MAX_MTU || c->mru < 8 || c->mtu < 8) + return -EINVAL; + if (c->n2 < 3) + return -EINVAL; + if (c->encapsulation > 1) /* Basic, advanced, no I */ + return -EINVAL; + if (c->initiator > 1) + return -EINVAL; + if (c->i == 0 || c->i > 2) /* UIH and UI only */ + return -EINVAL; + /* + * See what is needed for reconfiguration + */ + + /* Timing fields */ + if (c->t1 != 0 && c->t1 != gsm->t1) + need_restart = 1; + if (c->t2 != 0 && c->t2 != gsm->t2) + need_restart = 1; + if (c->encapsulation != gsm->encoding) + need_restart = 1; + if (c->adaption != gsm->adaption) + need_restart = 1; + /* Requires care */ + if (c->initiator != gsm->initiator) + need_close = 1; + if (c->mru != gsm->mru) + need_restart = 1; + if (c->mtu != gsm->mtu) + need_restart = 1; + + /* + * Close down what is needed, restart and initiate the new + * configuration + */ + + if (need_close || need_restart) { + int ret; + + ret = gsm_disconnect(gsm); + + if (ret) + return ret; + } + if (need_restart) + gsm_cleanup_mux(gsm); + + gsm->initiator = c->initiator; + gsm->mru = c->mru; + gsm->mtu = c->mtu; + gsm->encoding = c->encapsulation; + gsm->adaption = c->adaption; + gsm->n2 = c->n2; + + if (c->i == 1) + gsm->ftype = UIH; + else if (c->i == 2) + gsm->ftype = UI; + + if (c->t1) + gsm->t1 = c->t1; + if (c->t2) + gsm->t2 = c->t2; + + /* + * FIXME: We need to separate activation/deactivation from adding + * and removing from the mux array + */ + if (need_restart) + gsm_activate_mux(gsm); + if (gsm->initiator && need_close) + gsm_dlci_begin_open(gsm->dlci[0]); + return 0; +} + +#ifdef CONFIG_SERIAL_DEV_BUS +static int gsd_get_config(struct gsm_serdev *gsd, struct gsm_config *c) +{ + struct gsm_mux *gsm; + + if (!gsd || !gsd->gsm) + return -ENODEV; + + gsm = gsd->gsm; + + if (!c) + return -EINVAL; + + gsm_copy_config_values(gsm, c); + + return 0; +} + +static int gsd_set_config(struct gsm_serdev *gsd, struct gsm_config *c) +{ + struct gsm_mux *gsm; + + if (!gsd || !gsd->serdev || !gsd->gsm) + return -ENODEV; + + gsm = gsd->gsm; + + if (!c) + return -EINVAL; + + return gsm_config(gsm, c); +} + +static struct gsm_dlci *gsd_dlci_get(struct gsm_serdev *gsd, int line, + bool allocate) +{ + struct gsm_mux *gsm; + struct gsm_dlci *dlci; + + if (!gsd || !gsd->gsm) + return ERR_PTR(-ENODEV); + + gsm = gsd->gsm; + + if (line < 1 || line >= 63) + return ERR_PTR(-EINVAL); + + mutex_lock(&gsm->mutex); + + if (gsm->dlci[line]) { + dlci = gsm->dlci[line]; + goto unlock; + } else if (!allocate) { + dlci = ERR_PTR(-ENODEV); + goto unlock; + } + + dlci = gsm_dlci_alloc(gsm, line); + if (!dlci) { + gsm = ERR_PTR(-ENOMEM); + goto unlock; + } + + gsm->dlci[line] = dlci; + +unlock: + mutex_unlock(&gsm->mutex); + + return dlci; +} + +static void gsd_dlci_data(struct gsm_dlci *dlci, const u8 *buf, int len) +{ + struct gsm_mux *gsm = dlci->gsm; + struct gsm_serdev *gsd = gsm->gsd; + + if (!gsd || !dlci->ops) + return; + + switch (dlci->adaption) { + case 0: + case 1: + if (dlci->ops->receive_buf) + dlci->ops->receive_buf(dlci->ops, buf, len); + break; + default: + pr_warn("dlci%i adaption %i not yet implemented\n", + dlci->addr, dlci->adaption); + break; + } +} + +static int gsd_write(struct gsm_serdev *gsd, struct gsm_serdev_dlci *sd, + const u8 *buf, int len) +{ + struct gsm_mux *gsm; + struct gsm_dlci *dlci; + struct gsm_msg *msg; + int h, size, total_size = 0; + u8 *dp; + + if (!gsd || !gsd->gsm) + return -ENODEV; + + gsm = gsd->gsm; + + dlci = gsd_dlci_get(gsd, sd->line, false); + if (IS_ERR(dlci)) + return PTR_ERR(dlci); + + h = dlci->adaption - 1; + + if (len > gsm->mtu) + len = gsm->mtu; + + size = len + h; + + msg = gsm_data_alloc(gsm, dlci->addr, size, gsm->ftype); + if (!msg) + return -ENOMEM; + + dp = msg->data; + switch (dlci->adaption) { + case 1: + break; + case 2: + *dp++ = gsm_encode_modem(dlci); + break; + } + memcpy(dp, buf, len); + __gsm_data_queue(dlci, msg); + total_size += size; + + return total_size; +} + +static void gsd_data_kick(struct gsm_serdev *gsd) +{ + struct gsm_mux *gsm; + unsigned long flags; + + if (!gsd || !gsd->gsm) + return; + + gsm = gsd->gsm; + + spin_lock_irqsave(&gsm->tx_lock, flags); + gsm_data_kick(gsm); + spin_unlock_irqrestore(&gsm->tx_lock, flags); +} + +static int gsd_register_dlci(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops) +{ + struct gsm_dlci *dlci; + struct gsm_mux *gsm; + + if (!gsd || !gsd->gsm || !gsd->serdev) + return -ENODEV; + + gsm = gsd->gsm; + + if (!ops || !ops->line || !ops->receive_buf) + return -EINVAL; + + dlci = gsd_dlci_get(gsd, ops->line, true); + if (IS_ERR(dlci)) + return PTR_ERR(dlci); + + if (dlci->state == DLCI_OPENING || dlci->state == DLCI_OPEN || + dlci->state == DLCI_CLOSING) + return -EBUSY; + + mutex_lock(&dlci->mutex); + dlci->ops = ops; + dlci->modem_rx = 0; + dlci->prev_data = dlci->data; + dlci->data = gsd_dlci_data; + mutex_unlock(&dlci->mutex); + + gsm_dlci_begin_open(dlci); + + return 0; +} + +static void gsd_unregister_dlci(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops) +{ + struct gsm_mux *gsm; + struct gsm_dlci *dlci; + + if (!gsd || !gsd->gsm || !gsd->serdev || !gsd->unregister_dlci) + return; + + gsm = gsd->gsm; + + if (!ops || !ops->line) + return; + + dlci = gsd_dlci_get(gsd, ops->line, false); + if (IS_ERR(dlci)) + return; + + mutex_lock(&dlci->mutex); + gsm_destroy_network(dlci); + dlci->data = dlci->prev_data; + dlci->ops = NULL; + mutex_unlock(&dlci->mutex); + + gsm_dlci_begin_close(dlci); +} + +static int gsm_serdev_output(struct gsm_mux *gsm, u8 *data, int len) +{ + struct gsm_serdev *gsd = gsm->gsd; + struct serdev_device *serdev = gsm->gsd->serdev; + bool asleep; + + asleep = atomic_read(&gsd->asleep); + if (asleep) + return -ENOSPC; + + if (gsm->gsd->output) + return gsm->gsd->output(gsm->gsd, data, len); + else + return serdev_device_write_buf(serdev, data, len); +} + +static int gsd_receive_buf(struct serdev_device *serdev, const u8 *data, + size_t count) +{ + struct gsm_serdev *gsd = serdev_device_get_drvdata(serdev); + struct gsm_mux *gsm; + const unsigned char *dp; + int i; + + if (WARN_ON(!gsd)) + return 0; + + gsm = gsd->gsm; + + if (debug & 4) + print_hex_dump_bytes("gsd_receive_buf: ", + DUMP_PREFIX_OFFSET, + data, count); + + for (i = count, dp = data; i; i--, dp++) + gsm->receive(gsm, *dp); + + return count; +} + +static void gsd_write_wakeup(struct serdev_device *serdev) +{ + serdev_device_write_wakeup(serdev); +} + +static struct serdev_device_ops gsd_client_ops = { + .receive_buf = gsd_receive_buf, + .write_wakeup = gsd_write_wakeup, +}; + +int gsm_serdev_register_device(struct gsm_serdev *gsd) +{ + struct gsm_mux *gsm; + int error; + + if (WARN(!gsd || !gsd->serdev || !gsd->output, + "serdev and output must be initialized\n")) + return -EINVAL; + + serdev_device_set_client_ops(gsd->serdev, &gsd_client_ops); + + gsm = gsm_alloc_mux(); + if (!gsm) + return -ENOMEM; + + gsm->encoding = 1; + gsm->tty = NULL; + gsm->gsd = gsd; + atomic_set(&gsd->asleep, 0); + gsd->gsm = gsm; + gsd->get_config = gsd_get_config; + gsd->set_config = gsd_set_config; + gsd->register_dlci = gsd_register_dlci; + gsd->unregister_dlci = gsd_unregister_dlci; + gsm->output = gsm_serdev_output; + gsd->write = gsd_write; + gsd->kick = gsd_data_kick; + mux_get(gsd->gsm); + + error = gsm_activate_mux(gsd->gsm); + if (error) { + gsm_cleanup_mux(gsd->gsm); + mux_put(gsd->gsm); + + return error; + } + + return 0; +} +EXPORT_SYMBOL_GPL(gsm_serdev_register_device); + +void gsm_serdev_unregister_device(struct gsm_serdev *gsd) +{ + gsm_cleanup_mux(gsd->gsm); + mux_put(gsd->gsm); + gsd->gsm = NULL; +} +EXPORT_SYMBOL_GPL(gsm_serdev_unregister_device); + +#endif /* CONFIG_SERIAL_DEV_BUS */ + /** * gsmld_output - write to link * @gsm: our mux @@ -2495,89 +2917,6 @@ static __poll_t gsmld_poll(struct tty_struct *tty, struct file *file, return mask; } -static int gsmld_config(struct tty_struct *tty, struct gsm_mux *gsm, - struct gsm_config *c) -{ - int need_close = 0; - int need_restart = 0; - - /* Stuff we don't support yet - UI or I frame transport, windowing */ - if ((c->adaption != 1 && c->adaption != 2) || c->k) - return -EOPNOTSUPP; - /* Check the MRU/MTU range looks sane */ - if (c->mru > MAX_MRU || c->mtu > MAX_MTU || c->mru < 8 || c->mtu < 8) - return -EINVAL; - if (c->n2 < 3) - return -EINVAL; - if (c->encapsulation > 1) /* Basic, advanced, no I */ - return -EINVAL; - if (c->initiator > 1) - return -EINVAL; - if (c->i == 0 || c->i > 2) /* UIH and UI only */ - return -EINVAL; - /* - * See what is needed for reconfiguration - */ - - /* Timing fields */ - if (c->t1 != 0 && c->t1 != gsm->t1) - need_restart = 1; - if (c->t2 != 0 && c->t2 != gsm->t2) - need_restart = 1; - if (c->encapsulation != gsm->encoding) - need_restart = 1; - if (c->adaption != gsm->adaption) - need_restart = 1; - /* Requires care */ - if (c->initiator != gsm->initiator) - need_close = 1; - if (c->mru != gsm->mru) - need_restart = 1; - if (c->mtu != gsm->mtu) - need_restart = 1; - - /* - * Close down what is needed, restart and initiate the new - * configuration - */ - - if (need_close || need_restart) { - int ret; - - ret = gsm_disconnect(gsm); - - if (ret) - return ret; - } - if (need_restart) - gsm_cleanup_mux(gsm); - - gsm->initiator = c->initiator; - gsm->mru = c->mru; - gsm->mtu = c->mtu; - gsm->encoding = c->encapsulation; - gsm->adaption = c->adaption; - gsm->n2 = c->n2; - - if (c->i == 1) - gsm->ftype = UIH; - else if (c->i == 2) - gsm->ftype = UI; - - if (c->t1) - gsm->t1 = c->t1; - if (c->t2) - gsm->t2 = c->t2; - - /* FIXME: We need to separate activation/deactivation from adding - and removing from the mux array */ - if (need_restart) - gsm_activate_mux(gsm); - if (gsm->initiator && need_close) - gsm_dlci_begin_open(gsm->dlci[0]); - return 0; -} - static int gsmld_ioctl(struct tty_struct *tty, struct file *file, unsigned int cmd, unsigned long arg) { @@ -2586,29 +2925,14 @@ static int gsmld_ioctl(struct tty_struct *tty, struct file *file, switch (cmd) { case GSMIOC_GETCONF: - memset(&c, 0, sizeof(c)); - c.adaption = gsm->adaption; - c.encapsulation = gsm->encoding; - c.initiator = gsm->initiator; - c.t1 = gsm->t1; - c.t2 = gsm->t2; - c.t3 = 0; /* Not supported */ - c.n2 = gsm->n2; - if (gsm->ftype == UIH) - c.i = 1; - else - c.i = 2; - pr_debug("Ftype %d i %d\n", gsm->ftype, c.i); - c.mru = gsm->mru; - c.mtu = gsm->mtu; - c.k = 0; + gsm_copy_config_values(gsm, &c); if (copy_to_user((void *)arg, &c, sizeof(c))) return -EFAULT; return 0; case GSMIOC_SETCONF: if (copy_from_user(&c, (void *)arg, sizeof(c))) return -EFAULT; - return gsmld_config(tty, gsm, &c); + return gsm_config(gsm, &c); default: return n_tty_ioctl_helper(tty, file, cmd, arg); } @@ -2695,7 +3019,7 @@ static void gsm_mux_net_tx_timeout(struct net_device *net) } static void gsm_mux_rx_netchar(struct gsm_dlci *dlci, - unsigned char *in_buf, int size) + const unsigned char *in_buf, int size) { struct net_device *net = dlci->net; struct sk_buff *skb; diff --git a/include/linux/mfd/motorola-mdm.h b/include/linux/mfd/motorola-mdm.h new file mode 100644 index 0000000000000..aae61050cd34a --- /dev/null +++ b/include/linux/mfd/motorola-mdm.h @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +struct gsm_serdev_dlci; +struct kfifo; +struct motmdm_response; + +enum motmdm_dlci_nr { + MOTMDM_DLCI1 = 1, + MOTMDM_DLCI2, + MOTMDM_DLCI3, + MOTMDM_DLCI4, + MOTMDM_DLCI5, + MOTMDM_DLCI6, + MOTMDM_DLCI7, + MOTMDM_DLCI8, + MOTMDM_DLCI9, + MOTMDM_DLCI10, + MOTMDM_DLCI11, + MOTMDM_DLCI12, + MOTMDM_DLCI13, + MOTMDM_DLCI14, + MOTMDM_DLCI15, +}; + +enum motmdm_state { + MOTMDM_STATE_IDLE = 0, + MOTMDM_STATE_DIAL = 1, + MOTMDM_STATE_ANSWERING = 2, + MOTMDM_STATE_CONNECTING = 3, + MOTMDM_STATE_INCOMING = 4, + MOTMDM_STATE_CONNECTED = 5, + MOTMDM_STATE_HANGING_UP = 6, + MOTMDM_STATE_DISCONNECTED = 7, +}; + +struct motmdm_dlci { + struct gsm_serdev_dlci gsm_dlci; + struct list_head node; + wait_queue_head_t read_queue; + struct kfifo read_fifo; + int line; + u16 id; + int (*send_command)(struct device *dev, struct motmdm_dlci *mot_dlci, + unsigned long timeout_ms, const unsigned char *cmd, + size_t cmdlen, + unsigned char *rsp, size_t rsplen); + int (*handle_command)(struct motmdm_dlci *mot_dlci, int id, + const unsigned char *buf, size_t len); + int (*receive_data)(struct motmdm_dlci *mot_dlci, + const unsigned char *buf, + size_t len); + int (*write)(struct device *dev, struct motmdm_dlci *mot_dlci, + int cmdid, const unsigned char *buf, size_t count); + int (*notify)(struct motmdm_dlci *mot_dlci, enum motmdm_state); + struct list_head list; + void *privdata; /* Do not use, internal data */ + void *drvdata; /* Available for consumer drivers */ +}; + +int motmdm_register_dlci(struct device *dev, struct motmdm_dlci *mot_dlci); +void motmdm_unregister_dlci(struct device *dev, struct motmdm_dlci *mot_dlci); + +static inline +int motmdm_send_command(struct device *dev, struct motmdm_dlci *mot_dlci, + unsigned long timeout_ms, const unsigned char *cmd, + size_t cmdlen, unsigned char *rsp, size_t rsplen) +{ + if (mot_dlci && mot_dlci->send_command) + return mot_dlci->send_command(dev, mot_dlci, + timeout_ms, cmd, cmdlen, + rsp, rsplen); + else + return -EINVAL; +} + +static inline +int motmdm_write(struct device *dev, struct motmdm_dlci *mot_dlci, + const unsigned char *buf, size_t count) +{ + if (mot_dlci && mot_dlci->write) + return mot_dlci->write(dev, mot_dlci, -1, buf, count); + else + return -EINVAL; +} diff --git a/include/linux/serdev-gsm.h b/include/linux/serdev-gsm.h new file mode 100644 index 0000000000000..93499cf3ea195 --- /dev/null +++ b/include/linux/serdev-gsm.h @@ -0,0 +1,227 @@ +/* SPDX-License-Identifier: GPL-2.0 */ + +struct serdev_device; +struct gsm_mux; +struct gsm_config; +struct gsm_dlci; +struct gsm_serdev_dlci; + +/** + * struct gsm_serdev - serdev-gsm instance + * @serdev: serdev instance + * @gsm: ts 27.010 n_gsm instance + * @asleep: device is in idle state + * @drvdata: serdev-gsm consumer driver data + * @get_config: get ts 27.010 config + * @set_config: set ts 27.010 config + * @register_dlci: register ts 27.010 channel + * @unregister_dlci: unregister ts 27.010 channel + * @output: read data from ts 27.010 channel + * @write: write data to a ts 27.010 channel + * @kick: indicate more data is ready + * + * Currently only serdev and output must be initialized, the rest are + * are initialized by gsm_serdev_register_dlci(). + */ +struct gsm_serdev { + struct serdev_device *serdev; + struct gsm_mux *gsm; + atomic_t asleep; + void *drvdata; + int (*get_config)(struct gsm_serdev *gsd, struct gsm_config *c); + int (*set_config)(struct gsm_serdev *gsd, struct gsm_config *c); + int (*register_dlci)(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops); + void (*unregister_dlci)(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops); + int (*output)(struct gsm_serdev *gsd, u8 *data, int len); + int (*write)(struct gsm_serdev *gsd, struct gsm_serdev_dlci *ops, + const u8 *buf, int len); + void (*kick)(struct gsm_serdev *gsd); +}; + +/** + * struct gsm_serdev_dlci - serdev-gsm ts 27.010 channel data + * @line: ts 27.010 channel, control channel 0 is not available + * @receive_buf: function to handle data received for the channel + */ +struct gsm_serdev_dlci { + int line; + int (*receive_buf)(struct gsm_serdev_dlci *ops, + const unsigned char *buf, + size_t len); +}; + +#ifdef CONFIG_SERIAL_DEV_BUS + +int gsm_serdev_register_device(struct gsm_serdev *gsd); +void gsm_serdev_unregister_device(struct gsm_serdev *gsd); + +static inline void *gsm_serdev_get_drvdata(struct device *dev) +{ + struct serdev_device *serdev = to_serdev_device(dev); + struct gsm_serdev *gsd = serdev_device_get_drvdata(serdev); + + if (gsd) + return gsd->drvdata; + + return NULL; +} + +static inline void gsm_serdev_set_drvdata(struct device *dev, void *drvdata) +{ + struct serdev_device *serdev = to_serdev_device(dev); + struct gsm_serdev *gsd = serdev_device_get_drvdata(serdev); + + if (gsd) + gsd->drvdata = drvdata; +} + +/** + * gsm_serdev_get_config - read ts 27.010 config + * @gsd: serdev-gsm instance + * @c: ts 27.010 config data + * + * See gsm_copy_config_values() for more information. + */ +static inline +int gsm_serdev_get_config(struct gsm_serdev *gsd, struct gsm_config *c) +{ + return gsd->get_config(gsd, c); +} + +/** + * gsm_serdev_set_config - set ts 27.010 config + * @gsd: serdev-gsm instance + * @c: ts 27.010 config data + * + * See gsm_config() for more information. + */ +static inline +int gsm_serdev_set_config(struct gsm_serdev *gsd, struct gsm_config *c) +{ + if (gsd && gsd->set_config) + return gsd->set_config(gsd, c); + + return -ENODEV; +} + +/** + * gsm_serdev_register_dlci - register a ts 27.010 channel + * @gsd: serdev-gsm instance + * @ops: channel ops + */ +static inline +int gsm_serdev_register_dlci(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops) +{ + if (gsd && gsd->register_dlci) + return gsd->register_dlci(gsd, ops); + + return -ENODEV; +} + +/** + * gsm_serdev_unregister_dlci - unregister a ts 27.010 channel + * @gsd: serdev-gsm instance + * @ops: channel ops + */ +static inline +void gsm_serdev_unregister_dlci(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops) +{ + if (gsd && gsd->unregister_dlci) + gsd->unregister_dlci(gsd, ops); +} + +/** + * gsm_serdev_write - write data to a ts 27.010 channel + * @gsd: serdev-gsm instance + * @ops: channel ops + * @buf: write buffer + * @len: buffer length + */ +static inline +int gsm_serdev_write(struct gsm_serdev *gsd, struct gsm_serdev_dlci *ops, + const u8 *buf, int len) +{ + if (gsd && gsd->write) + return gsd->write(gsd, ops, buf, len); + + return -ENODEV; +} + +/** + * gsm_serdev_data_kick - indicate more data can be trasmitted + * @gsd: serdev-gsm instance + * + * See gsm_data_kick() for more information. + */ +static inline +void gsm_serdev_data_kick(struct gsm_serdev *gsd) +{ + if (gsd && gsd->kick) + gsd->kick(gsd); +} + +#else /* CONFIG_SERIAL_DEV_BUS */ + +static inline +int gsm_serdev_register_device(struct gsm_serdev *gsd) +{ + return -ENODEV; +} + +static inline +void gsm_serdev_unregister_device(struct gsm_serdev *gsd) +{ +} + +static inline void *gsm_serdev_get_drvdata(struct device *dev) +{ + return NULL; +} + +static inline +void gsm_serdev_set_drvdata(struct device *dev, void *drvdata) +{ +} + +static inline +int gsm_serdev_get_config(struct gsm_serdev *gsd, struct gsm_config *c) +{ + return -ENODEV; +} + +static inline +int gsm_serdev_set_config(struct gsm_serdev *gsd, struct gsm_config *c) +{ + return -ENODEV; +} + +static inline +int gsm_serdev_register_dlci(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops) +{ + return -ENODEV; +} + +static inline +void gsm_serdev_unregister_dlci(struct gsm_serdev *gsd, + struct gsm_serdev_dlci *ops) +{ +} + +static inline +int gsm_serdev_write(struct gsm_serdev *gsd, struct gsm_serdev_dlci *ops, + const u8 *buf, int len) +{ + return -ENODEV; +} + +static inline +void gsm_serdev_data_kick(struct gsm_serdev *gsd) +{ +} + +#endif /* CONFIG_SERIAL_DEV_BUS */ diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 62bdb7e333b84..b2e9158e780cc 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -692,6 +692,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 MFD_MOTMDM + help + Enable support for Motorola TS 27.010 line discipline 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 66f55d1856207..9c32d32e47757 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -103,6 +103,7 @@ 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-msm8916-analog-objs := msm8916-wcd-analog.o snd-soc-msm8916-digital-objs := msm8916-wcd-digital.o snd-soc-mt6351-objs := mt6351.o @@ -369,6 +370,7 @@ 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_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/cpcap.c b/sound/soc/codecs/cpcap.c index d7f05b384f1fb..d0d5ace9c4225 100644 --- a/sound/soc/codecs/cpcap.c +++ b/sound/soc/codecs/cpcap.c @@ -16,6 +16,14 @@ #include <sound/soc.h> #include <sound/tlv.h> +/* Register 512 CPCAP_REG_VAUDIOC --- Audio Regulator and Bias Voltage */ +#define CPCAP_BIT_AUDIO_LOW_PWR 6 +#define CPCAP_BIT_AUD_LOWPWR_SPEED 5 +#define CPCAP_BIT_VAUDIOPRISTBY 4 +#define CPCAP_BIT_VAUDIO_MODE1 2 +#define CPCAP_BIT_VAUDIO_MODE0 1 +#define CPCAP_BIT_V_AUDIO_EN 0 + /* Register 513 CPCAP_REG_CC --- CODEC */ #define CPCAP_BIT_CDC_CLK2 15 #define CPCAP_BIT_CDC_CLK1 14 @@ -251,6 +259,8 @@ struct cpcap_audio { int codec_clk_id; int codec_freq; int codec_format; + + unsigned int voice_call:1; }; static int cpcap_st_workaround(struct snd_soc_dapm_widget *w, @@ -1370,6 +1380,114 @@ static int cpcap_voice_set_dai_fmt(struct snd_soc_dai *codec_dai, return 0; } +/* + * Configure voice call if cpcap->voice_call is set. + * + * We can configure most with snd_soc_dai_set_sysclk(), snd_soc_dai_set_fmt() + * and snd_soc_dai_set_tdm_slot(). This function configures the rest of the + * cpcap related hardware piceses as CPU is not involved in the voice call. + */ +static int cpcap_voice_call(struct snd_soc_dai *dai) +{ + struct snd_soc_component *component = dai->component; + struct cpcap_audio *cpcap = snd_soc_component_get_drvdata(component); + int mask, err; + + /* Maybe enable modem to codec VAUDIO_MODE1? */ + mask = BIT(CPCAP_BIT_VAUDIO_MODE1); + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_VAUDIOC, + mask, cpcap->voice_call ? mask : 0); + if (err) + return err; + + /* Maybe clear MIC1_MUX? */ + mask = BIT(CPCAP_BIT_MIC1_MUX); + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_TXI, + mask, cpcap->voice_call ? 0 : mask); + if (err) + return err; + + /* Maybe set MIC2_MUX? */ + mask = BIT(CPCAP_BIT_MB_ON1L) | BIT(CPCAP_BIT_MB_ON1R) | + BIT(CPCAP_BIT_MIC2_MUX) | BIT(CPCAP_BIT_MIC2_PGA_EN); + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_TXI, + mask, cpcap->voice_call ? mask : 0); + if (err) + return err; + + /* Maybe enable LDSP? */ + mask = BIT(CPCAP_BIT_A2_LDSP_L_EN) | BIT(CPCAP_BIT_A2_LDSP_R_EN); + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_RXOA, + mask, cpcap->voice_call ? mask : 0); + if (err) + return err; + + /* Maybe enable CPCAP_BIT_PGA_CDC_EN for call? */ + mask = BIT(CPCAP_BIT_PGA_CDC_EN); + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_RXCOA, + mask, cpcap->voice_call ? mask : 0); + if (err) + return err; + + /* Maybe unmute voice? */ + err = snd_soc_dai_digital_mute(dai, !cpcap->voice_call, + SNDRV_PCM_STREAM_PLAYBACK); + if (err) + return err; + + /* Maybe enable modem to codec mic CDC and HPF? */ + mask = BIT(CPCAP_BIT_MIC2_CDC_EN) | BIT(CPCAP_BIT_CDC_EN_RX) | + BIT(CPCAP_BIT_AUDOHPF_1) | BIT(CPCAP_BIT_AUDOHPF_0) | + BIT(CPCAP_BIT_AUDIHPF_1) | BIT(CPCAP_BIT_AUDIHPF_0); + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_CC, + mask, cpcap->voice_call ? mask : 0); + if (err) + return err; + + /* Maybe enable modem to codec CDC? */ + mask = BIT(CPCAP_BIT_CDC_CLK_EN); + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_CDI, + mask, cpcap->voice_call ? mask : 0); + + return err; +} + +static int cpcap_voice_set_tdm_slot(struct snd_soc_dai *dai, + unsigned int tx_mask, unsigned int rx_mask, + int slots, int slot_width) +{ + struct snd_soc_component *component = dai->component; + struct cpcap_audio *cpcap = snd_soc_component_get_drvdata(component); + int err, ts_mask, mask; + + /* Modem to codec audio with no CPU involved? */ + if (tx_mask == 0 && rx_mask == 1 && slot_width == 8) + cpcap->voice_call = true; + else + cpcap->voice_call = false; + + ts_mask = 0x7 << CPCAP_BIT_MIC2_TIMESLOT0; + ts_mask |= 0x7 << CPCAP_BIT_MIC1_RX_TIMESLOT0; + + mask = (tx_mask & 0x7) << CPCAP_BIT_MIC2_TIMESLOT0; + mask |= (rx_mask & 0x7) << CPCAP_BIT_MIC1_RX_TIMESLOT0; + + err = regmap_update_bits(cpcap->regmap, CPCAP_REG_CDI, + ts_mask, mask); + if (err) + return err; + + err = cpcap_set_samprate(cpcap, CPCAP_DAI_VOICE, slot_width * 1000); + if (err) + return err; + + err = cpcap_voice_call(dai); + if (err) + return err; + + return 0; +} + static int cpcap_voice_set_mute(struct snd_soc_dai *dai, int mute) { struct snd_soc_component *component = dai->component; @@ -1391,6 +1509,7 @@ static const struct snd_soc_dai_ops cpcap_dai_voice_ops = { .hw_params = cpcap_voice_hw_params, .set_sysclk = cpcap_voice_set_dai_sysclk, .set_fmt = cpcap_voice_set_dai_fmt, + .set_tdm_slot = cpcap_voice_set_tdm_slot, .digital_mute = cpcap_voice_set_mute, }; diff --git a/sound/soc/codecs/motmdm.c b/sound/soc/codecs/motmdm.c new file mode 100644 index 0000000000000..b3371811d8872 --- /dev/null +++ b/sound/soc/codecs/motmdm.c @@ -0,0 +1,514 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Motorola Mapphone MDM6600 voice call audio support + * Copyright 2018 Tony Lindgren <tony@atomide.com> + */ + +#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 <linux/mfd/motorola-mdm.h> + +struct motmdm_driver_data { + struct snd_soc_component *component; + struct snd_soc_dai *master_dai; + struct device *modem; + struct motmdm_dlci mot_dlci; + struct regmap *regmap; + unsigned char *buf; + size_t len; + unsigned int enabled:1; + spinlock_t lock; /* enable/disabled lock */ +}; + +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); + + +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->modem, &ddata->mot_dlci, 1000, + cmd, strlen(cmd), + ddata->buf, ddata->len); + if (error < 0) { + dev_err(component->dev, "%s: %s failed with %i\n", + __func__, cmd, error); + + return error; + } + + error = kstrtouint(ddata->buf, 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->modem, &ddata->mot_dlci, 1000, + cmd, strlen(cmd), + ddata->buf, ddata->len); + 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 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), +}; + +/* REVISIT: Use this to reconfigure cpcap codec? */ +static const struct snd_soc_dapm_widget motmdm_dapm_widgets[] = { + SND_SOC_DAPM_INPUT("RX"), + SND_SOC_DAPM_OUTPUT("TX"), +}; + +static const struct snd_soc_dapm_route motmdm_intercon[] = { + { "Capture", NULL, "RX" }, + { "TX", NULL, "Playback" }, +}; + +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_soc_notify(struct motmdm_dlci *mot_dlci, + enum motmdm_state state) +{ + struct motmdm_driver_data *ddata = mot_dlci->drvdata; + unsigned long flags; + bool enable, notify = false; + + switch (state) { + case MOTMDM_STATE_DIAL: + case MOTMDM_STATE_ANSWERING: + case MOTMDM_STATE_CONNECTING: + case MOTMDM_STATE_CONNECTED: + enable = true; + break; + case MOTMDM_STATE_HANGING_UP: + case MOTMDM_STATE_DISCONNECTED: + enable = false; + break; + default: + return 0; + } + + spin_lock_irqsave(&ddata->lock, flags); + if (ddata->enabled != enable) { + ddata->enabled = enable; + notify = true; + } + spin_unlock_irqrestore(&ddata->lock, flags); + + if (!notify) + return 0; + + if (enable) + motmdm_enable_primary_dai(ddata->component); + else + motmdm_disable_primary_dai(ddata->component); + + return 0; +} + +static int motmdm_soc_probe(struct snd_soc_component *component) +{ + struct motmdm_driver_data *ddata; + struct motmdm_dlci *mot_dlci; + const unsigned char *cmd = "AT+CMUT=0"; + int error; + + ddata = devm_kzalloc(component->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ddata->component = component; + ddata->modem = component->dev->parent; + mot_dlci = &ddata->mot_dlci; + ddata->len = PAGE_SIZE; + spin_lock_init(&ddata->lock); + snd_soc_component_set_drvdata(component, ddata); + + 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; + } + + mot_dlci->drvdata = ddata; + mot_dlci->line = MOTMDM_DLCI2; + mot_dlci->notify = motmdm_soc_notify; + + error = motmdm_register_dlci(ddata->modem, mot_dlci); + if (error) + return error; + + error = motmdm_parse_tdm(component); + if (error) { + return error; + } + + regcache_sync(ddata->regmap); + + error = motmdm_send_command(ddata->modem, mot_dlci, 1000, + cmd, strlen(cmd), + ddata->buf, ddata->len); + if (error < 0) { + motmdm_unregister_dlci(ddata->modem, mot_dlci); + + return error; + } + + return motmdm_disable_primary_dai(ddata->component); +} + +static void motmdm_soc_remove(struct snd_soc_component *component) +{ + struct motmdm_driver_data *ddata; + struct motmdm_dlci *mot_dlci; + + ddata = snd_soc_component_get_drvdata(component); + mot_dlci = &ddata->mot_dlci; + + motmdm_unregister_dlci(ddata->modem, mot_dlci); +} + +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), + .dapm_widgets = motmdm_dapm_widgets, + .num_dapm_widgets = ARRAY_SIZE(motmdm_dapm_widgets), + .dapm_routes = motmdm_intercon, + .num_dapm_routes = ARRAY_SIZE(motmdm_intercon), + .idle_bias_on = 1, + .use_pmdown_time = 1, + .endianness = 1, + .non_legacy_dai_naming = 1, +}; + +static int motmdm_codec_probe(struct platform_device *pdev) +{ + struct device_node *codec_node = + of_get_child_by_name(pdev->dev.parent->of_node, "audio-codec"); + + pdev->dev.of_node = codec_node; + + return devm_snd_soc_register_component(&pdev->dev, + &soc_codec_dev_motmdm, + motmdm_dai, + ARRAY_SIZE(motmdm_dai)); +} + +static struct platform_driver motmdm_driver = { + .probe = motmdm_codec_probe, + .driver = { + .name = "mot-mdm6600-codec", + }, +}; +module_platform_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"); diff --git a/sound/soc/ti/omap-mcbsp-priv.h b/sound/soc/ti/omap-mcbsp-priv.h index 7865cda4bf0ad..9464f5d358221 100644 --- a/sound/soc/ti/omap-mcbsp-priv.h +++ b/sound/soc/ti/omap-mcbsp-priv.h @@ -262,6 +262,8 @@ struct omap_mcbsp { struct omap_mcbsp_platform_data *pdata; struct omap_mcbsp_st_data *st_data; struct omap_mcbsp_reg_cfg cfg_regs; + struct snd_soc_dai_driver *dais; + int dai_count; struct snd_dmaengine_dai_dma_data dma_data[2]; unsigned int dma_req[2]; int dma_op_mode; diff --git a/sound/soc/ti/omap-mcbsp.c b/sound/soc/ti/omap-mcbsp.c index a395598f1f208..d655f41bf30c1 100644 --- a/sound/soc/ti/omap-mcbsp.c +++ b/sound/soc/ti/omap-mcbsp.c @@ -28,6 +28,7 @@ #include <linux/pm_runtime.h> #include <linux/of.h> #include <linux/of_device.h> +#include <linux/of_graph.h> #include <sound/core.h> #include <sound/pcm.h> #include <sound/pcm_params.h> @@ -1318,23 +1319,53 @@ static int omap_mcbsp_remove(struct snd_soc_dai *dai) return 0; } -static struct snd_soc_dai_driver omap_mcbsp_dai = { - .probe = omap_mcbsp_probe, - .remove = omap_mcbsp_remove, - .playback = { - .channels_min = 1, - .channels_max = 16, - .rates = OMAP_MCBSP_RATES, - .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S32_LE, - }, - .capture = { - .channels_min = 1, - .channels_max = 16, - .rates = OMAP_MCBSP_RATES, - .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S32_LE, - }, - .ops = &mcbsp_dai_ops, -}; +static int omap_mcbsp_init_dais(struct omap_mcbsp *mcbsp) +{ + struct device_node *np = mcbsp->dev->of_node; + int i; + + if (np) + mcbsp->dai_count = of_graph_get_endpoint_count(np); + + if (!mcbsp->dai_count) + mcbsp->dai_count = 1; + + mcbsp->dais = devm_kcalloc(mcbsp->dev, mcbsp->dai_count, + sizeof(*mcbsp->dais), GFP_KERNEL); + if (!mcbsp->dais) + return -ENOMEM; + + for (i = 0; i < mcbsp->dai_count; i++) { + struct snd_soc_dai_driver *dai = &mcbsp->dais[i]; + + dai->name = devm_kasprintf(mcbsp->dev, GFP_KERNEL, "%s-dai%i", + dev_name(mcbsp->dev), i); + + if (i == 0) { + dai->probe = omap_mcbsp_probe; + dai->remove = omap_mcbsp_remove; + dai->ops = &mcbsp_dai_ops; + } + dai->playback.channels_min = 1; + dai->playback.channels_max = 16; + dai->playback.rates = OMAP_MCBSP_RATES; + if (mcbsp->pdata->reg_size == 2) + dai->playback.formats = SNDRV_PCM_FMTBIT_S16_LE; + else + dai->playback.formats = SNDRV_PCM_FMTBIT_S16_LE | + SNDRV_PCM_FMTBIT_S32_LE; + dai->capture.channels_min = 1; + dai->capture.channels_max = 16; + dai->capture.rates = OMAP_MCBSP_RATES; + if (mcbsp->pdata->reg_size == 2) + dai->capture.formats = SNDRV_PCM_FMTBIT_S16_LE; + else + dai->capture.formats = SNDRV_PCM_FMTBIT_S16_LE | + SNDRV_PCM_FMTBIT_S32_LE; + } + + return 0; +} static const struct snd_soc_component_driver omap_mcbsp_component = { .name = "omap-mcbsp", @@ -1423,18 +1454,17 @@ static int asoc_mcbsp_probe(struct platform_device *pdev) mcbsp->dev = &pdev->dev; platform_set_drvdata(pdev, mcbsp); - ret = omap_mcbsp_init(pdev); + ret = omap_mcbsp_init_dais(mcbsp); if (ret) return ret; - if (mcbsp->pdata->reg_size == 2) { - omap_mcbsp_dai.playback.formats = SNDRV_PCM_FMTBIT_S16_LE; - omap_mcbsp_dai.capture.formats = SNDRV_PCM_FMTBIT_S16_LE; - } + ret = omap_mcbsp_init(pdev); + if (ret) + return ret; ret = devm_snd_soc_register_component(&pdev->dev, &omap_mcbsp_component, - &omap_mcbsp_dai, 1); + mcbsp->dais, mcbsp->dai_count); if (ret) return ret; |