607 lines · c
1// SPDX-License-Identifier: GPL-2.0-only2/*3 * ams-delta.c -- SoC audio for Amstrad E3 (Delta) videophone4 *5 * Copyright (C) 2009 Janusz Krzysztofik <jkrzyszt@tis.icnet.pl>6 *7 * Initially based on sound/soc/omap/osk5912.x8 * Copyright (C) 2008 Mistral Solutions9 */10 11#include <linux/gpio/consumer.h>12#include <linux/spinlock.h>13#include <linux/tty.h>14#include <linux/module.h>15 16#include <sound/soc.h>17#include <sound/jack.h>18 19#include <linux/platform_data/asoc-ti-mcbsp.h>20 21#include "omap-mcbsp.h"22#include "../codecs/cx20442.h"23 24static struct gpio_desc *handset_mute;25static struct gpio_desc *handsfree_mute;26 27static int ams_delta_event_handset(struct snd_soc_dapm_widget *w,28 struct snd_kcontrol *k, int event)29{30 gpiod_set_value_cansleep(handset_mute, !SND_SOC_DAPM_EVENT_ON(event));31 return 0;32}33 34static int ams_delta_event_handsfree(struct snd_soc_dapm_widget *w,35 struct snd_kcontrol *k, int event)36{37 gpiod_set_value_cansleep(handsfree_mute, !SND_SOC_DAPM_EVENT_ON(event));38 return 0;39}40 41/* Board specific DAPM widgets */42static const struct snd_soc_dapm_widget ams_delta_dapm_widgets[] = {43 /* Handset */44 SND_SOC_DAPM_MIC("Mouthpiece", NULL),45 SND_SOC_DAPM_HP("Earpiece", ams_delta_event_handset),46 /* Handsfree/Speakerphone */47 SND_SOC_DAPM_MIC("Microphone", NULL),48 SND_SOC_DAPM_SPK("Speaker", ams_delta_event_handsfree),49};50 51/* How they are connected to codec pins */52static const struct snd_soc_dapm_route ams_delta_audio_map[] = {53 {"TELIN", NULL, "Mouthpiece"},54 {"Earpiece", NULL, "TELOUT"},55 56 {"MIC", NULL, "Microphone"},57 {"Speaker", NULL, "SPKOUT"},58};59 60/*61 * Controls, functional after the modem line discipline is activated.62 */63 64/* Virtual switch: audio input/output constellations */65static const char *ams_delta_audio_mode[] =66 {"Mixed", "Handset", "Handsfree", "Speakerphone"};67 68/* Selection <-> pin translation */69#define AMS_DELTA_MOUTHPIECE 070#define AMS_DELTA_EARPIECE 171#define AMS_DELTA_MICROPHONE 272#define AMS_DELTA_SPEAKER 373#define AMS_DELTA_AGC 474 75#define AMS_DELTA_MIXED ((1 << AMS_DELTA_EARPIECE) | \76 (1 << AMS_DELTA_MICROPHONE))77#define AMS_DELTA_HANDSET ((1 << AMS_DELTA_MOUTHPIECE) | \78 (1 << AMS_DELTA_EARPIECE))79#define AMS_DELTA_HANDSFREE ((1 << AMS_DELTA_MICROPHONE) | \80 (1 << AMS_DELTA_SPEAKER))81#define AMS_DELTA_SPEAKERPHONE (AMS_DELTA_HANDSFREE | (1 << AMS_DELTA_AGC))82 83static const unsigned short ams_delta_audio_mode_pins[] = {84 AMS_DELTA_MIXED,85 AMS_DELTA_HANDSET,86 AMS_DELTA_HANDSFREE,87 AMS_DELTA_SPEAKERPHONE,88};89 90static unsigned short ams_delta_audio_agc;91 92/*93 * Used for passing a codec structure pointer94 * from the board initialization code to the tty line discipline.95 */96static struct snd_soc_component *cx20442_codec;97 98static int ams_delta_set_audio_mode(struct snd_kcontrol *kcontrol,99 struct snd_ctl_elem_value *ucontrol)100{101 struct snd_soc_card *card = snd_kcontrol_chip(kcontrol);102 struct snd_soc_dapm_context *dapm = &card->dapm;103 struct soc_enum *control = (struct soc_enum *)kcontrol->private_value;104 unsigned short pins;105 int pin, changed = 0;106 107 /* Refuse any mode changes if we are not able to control the codec. */108 if (!cx20442_codec->card->pop_time)109 return -EUNATCH;110 111 if (ucontrol->value.enumerated.item[0] >= control->items)112 return -EINVAL;113 114 snd_soc_dapm_mutex_lock(dapm);115 116 /* Translate selection to bitmap */117 pins = ams_delta_audio_mode_pins[ucontrol->value.enumerated.item[0]];118 119 /* Setup pins after corresponding bits if changed */120 pin = !!(pins & (1 << AMS_DELTA_MOUTHPIECE));121 122 if (pin != snd_soc_dapm_get_pin_status(dapm, "Mouthpiece")) {123 changed = 1;124 if (pin)125 snd_soc_dapm_enable_pin_unlocked(dapm, "Mouthpiece");126 else127 snd_soc_dapm_disable_pin_unlocked(dapm, "Mouthpiece");128 }129 pin = !!(pins & (1 << AMS_DELTA_EARPIECE));130 if (pin != snd_soc_dapm_get_pin_status(dapm, "Earpiece")) {131 changed = 1;132 if (pin)133 snd_soc_dapm_enable_pin_unlocked(dapm, "Earpiece");134 else135 snd_soc_dapm_disable_pin_unlocked(dapm, "Earpiece");136 }137 pin = !!(pins & (1 << AMS_DELTA_MICROPHONE));138 if (pin != snd_soc_dapm_get_pin_status(dapm, "Microphone")) {139 changed = 1;140 if (pin)141 snd_soc_dapm_enable_pin_unlocked(dapm, "Microphone");142 else143 snd_soc_dapm_disable_pin_unlocked(dapm, "Microphone");144 }145 pin = !!(pins & (1 << AMS_DELTA_SPEAKER));146 if (pin != snd_soc_dapm_get_pin_status(dapm, "Speaker")) {147 changed = 1;148 if (pin)149 snd_soc_dapm_enable_pin_unlocked(dapm, "Speaker");150 else151 snd_soc_dapm_disable_pin_unlocked(dapm, "Speaker");152 }153 pin = !!(pins & (1 << AMS_DELTA_AGC));154 if (pin != ams_delta_audio_agc) {155 ams_delta_audio_agc = pin;156 changed = 1;157 if (pin)158 snd_soc_dapm_enable_pin_unlocked(dapm, "AGCIN");159 else160 snd_soc_dapm_disable_pin_unlocked(dapm, "AGCIN");161 }162 163 if (changed)164 snd_soc_dapm_sync_unlocked(dapm);165 166 snd_soc_dapm_mutex_unlock(dapm);167 168 return changed;169}170 171static int ams_delta_get_audio_mode(struct snd_kcontrol *kcontrol,172 struct snd_ctl_elem_value *ucontrol)173{174 struct snd_soc_card *card = snd_kcontrol_chip(kcontrol);175 struct snd_soc_dapm_context *dapm = &card->dapm;176 unsigned short pins, mode;177 178 pins = ((snd_soc_dapm_get_pin_status(dapm, "Mouthpiece") <<179 AMS_DELTA_MOUTHPIECE) |180 (snd_soc_dapm_get_pin_status(dapm, "Earpiece") <<181 AMS_DELTA_EARPIECE));182 if (pins)183 pins |= (snd_soc_dapm_get_pin_status(dapm, "Microphone") <<184 AMS_DELTA_MICROPHONE);185 else186 pins = ((snd_soc_dapm_get_pin_status(dapm, "Microphone") <<187 AMS_DELTA_MICROPHONE) |188 (snd_soc_dapm_get_pin_status(dapm, "Speaker") <<189 AMS_DELTA_SPEAKER) |190 (ams_delta_audio_agc << AMS_DELTA_AGC));191 192 for (mode = 0; mode < ARRAY_SIZE(ams_delta_audio_mode); mode++)193 if (pins == ams_delta_audio_mode_pins[mode])194 break;195 196 if (mode >= ARRAY_SIZE(ams_delta_audio_mode))197 return -EINVAL;198 199 ucontrol->value.enumerated.item[0] = mode;200 201 return 0;202}203 204static SOC_ENUM_SINGLE_EXT_DECL(ams_delta_audio_enum,205 ams_delta_audio_mode);206 207static const struct snd_kcontrol_new ams_delta_audio_controls[] = {208 SOC_ENUM_EXT("Audio Mode", ams_delta_audio_enum,209 ams_delta_get_audio_mode, ams_delta_set_audio_mode),210};211 212/* Hook switch */213static struct snd_soc_jack ams_delta_hook_switch;214static struct snd_soc_jack_gpio ams_delta_hook_switch_gpios[] = {215 {216 .name = "hook_switch",217 .report = SND_JACK_HEADSET,218 .invert = 1,219 .debounce_time = 150,220 }221};222 223/* After we are able to control the codec over the modem,224 * the hook switch can be used for dynamic DAPM reconfiguration. */225static struct snd_soc_jack_pin ams_delta_hook_switch_pins[] = {226 /* Handset */227 {228 .pin = "Mouthpiece",229 .mask = SND_JACK_MICROPHONE,230 },231 {232 .pin = "Earpiece",233 .mask = SND_JACK_HEADPHONE,234 },235 /* Handsfree */236 {237 .pin = "Microphone",238 .mask = SND_JACK_MICROPHONE,239 .invert = 1,240 },241 {242 .pin = "Speaker",243 .mask = SND_JACK_HEADPHONE,244 .invert = 1,245 },246};247 248 249/*250 * Modem line discipline, required for making above controls functional.251 * Activated from userspace with ldattach, possibly invoked from udev rule.252 */253 254/* To actually apply any modem controlled configuration changes to the codec,255 * we must connect codec DAI pins to the modem for a moment. Be careful not256 * to interfere with our digital mute function that shares the same hardware. */257static struct timer_list cx81801_timer;258static bool cx81801_cmd_pending;259static bool ams_delta_muted;260static DEFINE_SPINLOCK(ams_delta_lock);261static struct gpio_desc *gpiod_modem_codec;262 263static void cx81801_timeout(struct timer_list *unused)264{265 int muted;266 267 spin_lock(&ams_delta_lock);268 cx81801_cmd_pending = 0;269 muted = ams_delta_muted;270 spin_unlock(&ams_delta_lock);271 272 /* Reconnect the codec DAI back from the modem to the CPU DAI273 * only if digital mute still off */274 if (!muted)275 gpiod_set_value(gpiod_modem_codec, 0);276}277 278/* Line discipline .open() */279static int cx81801_open(struct tty_struct *tty)280{281 int ret;282 283 if (!cx20442_codec)284 return -ENODEV;285 286 /*287 * Pass the codec structure pointer for use by other ldisc callbacks,288 * both the card and the codec specific parts.289 */290 tty->disc_data = cx20442_codec;291 292 ret = v253_ops.open(tty);293 294 if (ret < 0)295 tty->disc_data = NULL;296 297 return ret;298}299 300/* Line discipline .close() */301static void cx81801_close(struct tty_struct *tty)302{303 struct snd_soc_component *component = tty->disc_data;304 struct snd_soc_dapm_context *dapm;305 306 del_timer_sync(&cx81801_timer);307 308 /* Prevent the hook switch from further changing the DAPM pins */309 INIT_LIST_HEAD(&ams_delta_hook_switch.pins);310 311 if (!component)312 return;313 314 v253_ops.close(tty);315 316 dapm = &component->card->dapm;317 318 /* Revert back to default audio input/output constellation */319 snd_soc_dapm_mutex_lock(dapm);320 321 snd_soc_dapm_disable_pin_unlocked(dapm, "Mouthpiece");322 snd_soc_dapm_enable_pin_unlocked(dapm, "Earpiece");323 snd_soc_dapm_enable_pin_unlocked(dapm, "Microphone");324 snd_soc_dapm_disable_pin_unlocked(dapm, "Speaker");325 snd_soc_dapm_disable_pin_unlocked(dapm, "AGCIN");326 327 snd_soc_dapm_sync_unlocked(dapm);328 329 snd_soc_dapm_mutex_unlock(dapm);330}331 332/* Line discipline .hangup() */333static void cx81801_hangup(struct tty_struct *tty)334{335 cx81801_close(tty);336}337 338/* Line discipline .receive_buf() */339static void cx81801_receive(struct tty_struct *tty, const u8 *cp, const u8 *fp,340 size_t count)341{342 struct snd_soc_component *component = tty->disc_data;343 const unsigned char *c;344 int apply, ret;345 346 if (!component)347 return;348 349 if (!component->card->pop_time) {350 /* First modem response, complete setup procedure */351 352 /* Initialize timer used for config pulse generation */353 timer_setup(&cx81801_timer, cx81801_timeout, 0);354 355 v253_ops.receive_buf(tty, cp, fp, count);356 357 /* Link hook switch to DAPM pins */358 ret = snd_soc_jack_add_pins(&ams_delta_hook_switch,359 ARRAY_SIZE(ams_delta_hook_switch_pins),360 ams_delta_hook_switch_pins);361 if (ret)362 dev_warn(component->dev,363 "Failed to link hook switch to DAPM pins, "364 "will continue with hook switch unlinked.\n");365 366 return;367 }368 369 v253_ops.receive_buf(tty, cp, fp, count);370 371 for (c = &cp[count - 1]; c >= cp; c--) {372 if (*c != '\r')373 continue;374 /* Complete modem response received, apply config to codec */375 376 spin_lock_bh(&ams_delta_lock);377 mod_timer(&cx81801_timer, jiffies + msecs_to_jiffies(150));378 apply = !ams_delta_muted && !cx81801_cmd_pending;379 cx81801_cmd_pending = 1;380 spin_unlock_bh(&ams_delta_lock);381 382 /* Apply config pulse by connecting the codec to the modem383 * if not already done */384 if (apply)385 gpiod_set_value(gpiod_modem_codec, 1);386 break;387 }388}389 390/* Line discipline .write_wakeup() */391static void cx81801_wakeup(struct tty_struct *tty)392{393 v253_ops.write_wakeup(tty);394}395 396static struct tty_ldisc_ops cx81801_ops = {397 .name = "cx81801",398 .num = N_V253,399 .owner = THIS_MODULE,400 .open = cx81801_open,401 .close = cx81801_close,402 .hangup = cx81801_hangup,403 .receive_buf = cx81801_receive,404 .write_wakeup = cx81801_wakeup,405};406 407 408/*409 * Even if not very useful, the sound card can still work without any of the410 * above functionality activated. You can still control its audio input/output411 * constellation and speakerphone gain from userspace by issuing AT commands412 * over the modem port.413 */414 415static struct snd_soc_ops ams_delta_ops;416 417 418/* Digital mute implemented using modem/CPU multiplexer.419 * Shares hardware with codec config pulse generation */420static bool ams_delta_muted = 1;421 422static int ams_delta_mute(struct snd_soc_dai *dai, int mute, int direction)423{424 int apply;425 426 if (ams_delta_muted == mute)427 return 0;428 429 spin_lock_bh(&ams_delta_lock);430 ams_delta_muted = mute;431 apply = !cx81801_cmd_pending;432 spin_unlock_bh(&ams_delta_lock);433 434 if (apply)435 gpiod_set_value(gpiod_modem_codec, !!mute);436 return 0;437}438 439/* Our codec DAI probably doesn't have its own .ops structure */440static const struct snd_soc_dai_ops ams_delta_dai_ops = {441 .mute_stream = ams_delta_mute,442 .no_capture_mute = 1,443};444 445/* Will be used if the codec ever has its own digital_mute function */446static int ams_delta_startup(struct snd_pcm_substream *substream)447{448 return ams_delta_mute(NULL, 0, substream->stream);449}450 451static void ams_delta_shutdown(struct snd_pcm_substream *substream)452{453 ams_delta_mute(NULL, 1, substream->stream);454}455 456 457/*458 * Card initialization459 */460 461static int ams_delta_cx20442_init(struct snd_soc_pcm_runtime *rtd)462{463 struct snd_soc_dai *codec_dai = snd_soc_rtd_to_codec(rtd, 0);464 struct snd_soc_card *card = rtd->card;465 struct snd_soc_dapm_context *dapm = &card->dapm;466 int ret;467 /* Codec is ready, now add/activate board specific controls */468 469 /* Store a pointer to the codec structure for tty ldisc use */470 cx20442_codec = snd_soc_rtd_to_codec(rtd, 0)->component;471 472 /* Add hook switch - can be used to control the codec from userspace473 * even if line discipline fails */474 ret = snd_soc_card_jack_new_pins(card, "hook_switch", SND_JACK_HEADSET,475 &ams_delta_hook_switch, NULL, 0);476 if (ret)477 dev_warn(card->dev,478 "Failed to allocate resources for hook switch, "479 "will continue without one.\n");480 else {481 ret = snd_soc_jack_add_gpiods(card->dev, &ams_delta_hook_switch,482 ARRAY_SIZE(ams_delta_hook_switch_gpios),483 ams_delta_hook_switch_gpios);484 if (ret)485 dev_warn(card->dev,486 "Failed to set up hook switch GPIO line, "487 "will continue with hook switch inactive.\n");488 }489 490 gpiod_modem_codec = devm_gpiod_get(card->dev, "modem_codec",491 GPIOD_OUT_HIGH);492 if (IS_ERR(gpiod_modem_codec)) {493 dev_warn(card->dev, "Failed to obtain modem_codec GPIO\n");494 return 0;495 }496 497 /* Set up digital mute if not provided by the codec */498 if (!codec_dai->driver->ops) {499 codec_dai->driver->ops = &ams_delta_dai_ops;500 } else {501 ams_delta_ops.startup = ams_delta_startup;502 ams_delta_ops.shutdown = ams_delta_shutdown;503 }504 505 /* Register optional line discipline for over the modem control */506 ret = tty_register_ldisc(&cx81801_ops);507 if (ret) {508 dev_warn(card->dev,509 "Failed to register line discipline, "510 "will continue without any controls.\n");511 return 0;512 }513 514 /* Set up initial pin constellation */515 snd_soc_dapm_disable_pin(dapm, "Mouthpiece");516 snd_soc_dapm_disable_pin(dapm, "Speaker");517 snd_soc_dapm_disable_pin(dapm, "AGCIN");518 snd_soc_dapm_disable_pin(dapm, "AGCOUT");519 520 return 0;521}522 523/* DAI glue - connects codec <--> CPU */524SND_SOC_DAILINK_DEFS(cx20442,525 DAILINK_COMP_ARRAY(COMP_CPU("omap-mcbsp.1")),526 DAILINK_COMP_ARRAY(COMP_CODEC("cx20442-codec", "cx20442-voice")),527 DAILINK_COMP_ARRAY(COMP_PLATFORM("omap-mcbsp.1")));528 529static struct snd_soc_dai_link ams_delta_dai_link = {530 .name = "CX20442",531 .stream_name = "CX20442",532 .init = ams_delta_cx20442_init,533 .ops = &ams_delta_ops,534 .dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_NB_NF |535 SND_SOC_DAIFMT_CBM_CFM,536 SND_SOC_DAILINK_REG(cx20442),537};538 539/* Audio card driver */540static struct snd_soc_card ams_delta_audio_card = {541 .name = "AMS_DELTA",542 .owner = THIS_MODULE,543 .dai_link = &ams_delta_dai_link,544 .num_links = 1,545 546 .controls = ams_delta_audio_controls,547 .num_controls = ARRAY_SIZE(ams_delta_audio_controls),548 .dapm_widgets = ams_delta_dapm_widgets,549 .num_dapm_widgets = ARRAY_SIZE(ams_delta_dapm_widgets),550 .dapm_routes = ams_delta_audio_map,551 .num_dapm_routes = ARRAY_SIZE(ams_delta_audio_map),552};553 554/* Module init/exit */555static int ams_delta_probe(struct platform_device *pdev)556{557 struct snd_soc_card *card = &ams_delta_audio_card;558 int ret;559 560 card->dev = &pdev->dev;561 562 handset_mute = devm_gpiod_get(card->dev, "handset_mute",563 GPIOD_OUT_HIGH);564 if (IS_ERR(handset_mute))565 return PTR_ERR(handset_mute);566 567 handsfree_mute = devm_gpiod_get(card->dev, "handsfree_mute",568 GPIOD_OUT_HIGH);569 if (IS_ERR(handsfree_mute))570 return PTR_ERR(handsfree_mute);571 572 ret = snd_soc_register_card(card);573 if (ret) {574 dev_err(&pdev->dev, "snd_soc_register_card failed (%d)\n", ret);575 card->dev = NULL;576 return ret;577 }578 return 0;579}580 581static void ams_delta_remove(struct platform_device *pdev)582{583 struct snd_soc_card *card = platform_get_drvdata(pdev);584 585 tty_unregister_ldisc(&cx81801_ops);586 587 snd_soc_unregister_card(card);588 card->dev = NULL;589}590 591#define DRV_NAME "ams-delta-audio"592 593static struct platform_driver ams_delta_driver = {594 .driver = {595 .name = DRV_NAME,596 },597 .probe = ams_delta_probe,598 .remove = ams_delta_remove,599};600 601module_platform_driver(ams_delta_driver);602 603MODULE_AUTHOR("Janusz Krzysztofik <jkrzyszt@tis.icnet.pl>");604MODULE_DESCRIPTION("ALSA SoC driver for Amstrad E3 (Delta) videophone");605MODULE_LICENSE("GPL");606MODULE_ALIAS("platform:" DRV_NAME);607