1 /* 2 * cx20442.c -- CX20442 ALSA Soc Audio driver 3 * 4 * Copyright 2009 Janusz Krzysztofik <jkrzyszt@tis.icnet.pl> 5 * 6 * Initially based on sound/soc/codecs/wm8400.c 7 * Copyright 2008, 2009 Wolfson Microelectronics PLC. 8 * Author: Mark Brown <broonie@opensource.wolfsonmicro.com> 9 * 10 * This program is free software; you can redistribute it and/or modify it 11 * under the terms of the GNU General Public License as published by the 12 * Free Software Foundation; either version 2 of the License, or (at your 13 * option) any later version. 14 */ 15 16 #include <linux/tty.h> 17 18 #include <sound/core.h> 19 #include <sound/initval.h> 20 #include <sound/soc-dapm.h> 21 22 #include "cx20442.h" 23 24 25 struct cx20442_priv { 26 struct snd_soc_codec codec; 27 u8 reg_cache[1]; 28 }; 29 30 #define CX20442_PM 0x0 31 32 #define CX20442_TELIN 0 33 #define CX20442_TELOUT 1 34 #define CX20442_MIC 2 35 #define CX20442_SPKOUT 3 36 #define CX20442_AGC 4 37 38 static const struct snd_soc_dapm_widget cx20442_dapm_widgets[] = { 39 SND_SOC_DAPM_OUTPUT("TELOUT"), 40 SND_SOC_DAPM_OUTPUT("SPKOUT"), 41 SND_SOC_DAPM_OUTPUT("AGCOUT"), 42 43 SND_SOC_DAPM_MIXER("SPKOUT Mixer", SND_SOC_NOPM, 0, 0, NULL, 0), 44 45 SND_SOC_DAPM_PGA("TELOUT Amp", CX20442_PM, CX20442_TELOUT, 0, NULL, 0), 46 SND_SOC_DAPM_PGA("SPKOUT Amp", CX20442_PM, CX20442_SPKOUT, 0, NULL, 0), 47 SND_SOC_DAPM_PGA("SPKOUT AGC", CX20442_PM, CX20442_AGC, 0, NULL, 0), 48 49 SND_SOC_DAPM_DAC("DAC", "Playback", SND_SOC_NOPM, 0, 0), 50 SND_SOC_DAPM_ADC("ADC", "Capture", SND_SOC_NOPM, 0, 0), 51 52 SND_SOC_DAPM_MIXER("Input Mixer", SND_SOC_NOPM, 0, 0, NULL, 0), 53 54 SND_SOC_DAPM_MICBIAS("TELIN Bias", CX20442_PM, CX20442_TELIN, 0), 55 SND_SOC_DAPM_MICBIAS("MIC Bias", CX20442_PM, CX20442_MIC, 0), 56 57 SND_SOC_DAPM_PGA("MIC AGC", CX20442_PM, CX20442_AGC, 0, NULL, 0), 58 59 SND_SOC_DAPM_INPUT("TELIN"), 60 SND_SOC_DAPM_INPUT("MIC"), 61 SND_SOC_DAPM_INPUT("AGCIN"), 62 }; 63 64 static const struct snd_soc_dapm_route cx20442_audio_map[] = { 65 {"TELOUT", NULL, "TELOUT Amp"}, 66 67 {"SPKOUT", NULL, "SPKOUT Mixer"}, 68 {"SPKOUT Mixer", NULL, "SPKOUT Amp"}, 69 70 {"TELOUT Amp", NULL, "DAC"}, 71 {"SPKOUT Amp", NULL, "DAC"}, 72 73 {"SPKOUT Mixer", NULL, "SPKOUT AGC"}, 74 {"SPKOUT AGC", NULL, "AGCIN"}, 75 76 {"AGCOUT", NULL, "MIC AGC"}, 77 {"MIC AGC", NULL, "MIC"}, 78 79 {"MIC Bias", NULL, "MIC"}, 80 {"Input Mixer", NULL, "MIC Bias"}, 81 82 {"TELIN Bias", NULL, "TELIN"}, 83 {"Input Mixer", NULL, "TELIN Bias"}, 84 85 {"ADC", NULL, "Input Mixer"}, 86 }; 87 88 static int cx20442_add_widgets(struct snd_soc_codec *codec) 89 { 90 snd_soc_dapm_new_controls(codec, cx20442_dapm_widgets, 91 ARRAY_SIZE(cx20442_dapm_widgets)); 92 93 snd_soc_dapm_add_routes(codec, cx20442_audio_map, 94 ARRAY_SIZE(cx20442_audio_map)); 95 96 return 0; 97 } 98 99 static unsigned int cx20442_read_reg_cache(struct snd_soc_codec *codec, 100 unsigned int reg) 101 { 102 u8 *reg_cache = codec->reg_cache; 103 104 if (reg >= codec->reg_cache_size) 105 return -EINVAL; 106 107 return reg_cache[reg]; 108 } 109 110 enum v253_vls { 111 V253_VLS_NONE = 0, 112 V253_VLS_T, 113 V253_VLS_L, 114 V253_VLS_LT, 115 V253_VLS_S, 116 V253_VLS_ST, 117 V253_VLS_M, 118 V253_VLS_MST, 119 V253_VLS_S1, 120 V253_VLS_S1T, 121 V253_VLS_MS1T, 122 V253_VLS_M1, 123 V253_VLS_M1ST, 124 V253_VLS_M1S1T, 125 V253_VLS_H, 126 V253_VLS_HT, 127 V253_VLS_MS, 128 V253_VLS_MS1, 129 V253_VLS_M1S, 130 V253_VLS_M1S1, 131 V253_VLS_TEST, 132 }; 133 134 static int cx20442_pm_to_v253_vls(u8 value) 135 { 136 switch (value & ~(1 << CX20442_AGC)) { 137 case 0: 138 return V253_VLS_T; 139 case (1 << CX20442_SPKOUT): 140 case (1 << CX20442_MIC): 141 case (1 << CX20442_SPKOUT) | (1 << CX20442_MIC): 142 return V253_VLS_M1S1; 143 case (1 << CX20442_TELOUT): 144 case (1 << CX20442_TELIN): 145 case (1 << CX20442_TELOUT) | (1 << CX20442_TELIN): 146 return V253_VLS_L; 147 case (1 << CX20442_TELOUT) | (1 << CX20442_MIC): 148 return V253_VLS_NONE; 149 } 150 return -EINVAL; 151 } 152 static int cx20442_pm_to_v253_vsp(u8 value) 153 { 154 switch (value & ~(1 << CX20442_AGC)) { 155 case (1 << CX20442_SPKOUT): 156 case (1 << CX20442_MIC): 157 case (1 << CX20442_SPKOUT) | (1 << CX20442_MIC): 158 return (bool)(value & (1 << CX20442_AGC)); 159 } 160 return (value & (1 << CX20442_AGC)) ? -EINVAL : 0; 161 } 162 163 static int cx20442_write(struct snd_soc_codec *codec, unsigned int reg, 164 unsigned int value) 165 { 166 u8 *reg_cache = codec->reg_cache; 167 int vls, vsp, old, len; 168 char buf[18]; 169 170 if (reg >= codec->reg_cache_size) 171 return -EINVAL; 172 173 /* hw_write and control_data pointers required for talking to the modem 174 * are expected to be set by the line discipline initialization code */ 175 if (!codec->hw_write || !codec->control_data) 176 return -EIO; 177 178 old = reg_cache[reg]; 179 reg_cache[reg] = value; 180 181 vls = cx20442_pm_to_v253_vls(value); 182 if (vls < 0) 183 return vls; 184 185 vsp = cx20442_pm_to_v253_vsp(value); 186 if (vsp < 0) 187 return vsp; 188 189 if ((vls == V253_VLS_T) || 190 (vls == cx20442_pm_to_v253_vls(old))) { 191 if (vsp == cx20442_pm_to_v253_vsp(old)) 192 return 0; 193 len = snprintf(buf, ARRAY_SIZE(buf), "at+vsp=%d\r", vsp); 194 } else if (vsp == cx20442_pm_to_v253_vsp(old)) 195 len = snprintf(buf, ARRAY_SIZE(buf), "at+vls=%d\r", vls); 196 else 197 len = snprintf(buf, ARRAY_SIZE(buf), 198 "at+vls=%d;+vsp=%d\r", vls, vsp); 199 200 if (unlikely(len > (ARRAY_SIZE(buf) - 1))) 201 return -ENOMEM; 202 203 dev_dbg(codec->dev, "%s: %s\n", __func__, buf); 204 if (codec->hw_write(codec->control_data, buf, len) != len) 205 return -EIO; 206 207 return 0; 208 } 209 210 211 /* Moved up here as line discipline referres it during initialization */ 212 static struct snd_soc_codec *cx20442_codec; 213 214 215 /* 216 * Line discpline related code 217 * 218 * Any of the callback functions below can be used in two ways: 219 * 1) registerd by a machine driver as one of line discipline operations, 220 * 2) called from a machine's provided line discipline callback function 221 * in case when extra machine specific code must be run as well. 222 */ 223 224 /* Modem init: echo off, digital speaker off, quiet off, voice mode */ 225 static const char *v253_init = "ate0m0q0+fclass=8\r"; 226 227 /* Line discipline .open() */ 228 static int v253_open(struct tty_struct *tty) 229 { 230 struct snd_soc_codec *codec = cx20442_codec; 231 int ret, len = strlen(v253_init); 232 233 /* Doesn't make sense without write callback */ 234 if (!tty->ops->write) 235 return -EINVAL; 236 237 /* Pass the codec structure address for use by other ldisc callbacks */ 238 tty->disc_data = codec; 239 240 if (tty->ops->write(tty, v253_init, len) != len) { 241 ret = -EIO; 242 goto err; 243 } 244 /* Actual setup will be performed after the modem responds. */ 245 return 0; 246 err: 247 tty->disc_data = NULL; 248 return ret; 249 } 250 251 /* Line discipline .close() */ 252 static void v253_close(struct tty_struct *tty) 253 { 254 struct snd_soc_codec *codec = tty->disc_data; 255 256 tty->disc_data = NULL; 257 258 if (!codec) 259 return; 260 261 /* Prevent the codec driver from further accessing the modem */ 262 codec->hw_write = NULL; 263 codec->control_data = NULL; 264 codec->pop_time = 0; 265 } 266 267 /* Line discipline .hangup() */ 268 static int v253_hangup(struct tty_struct *tty) 269 { 270 v253_close(tty); 271 return 0; 272 } 273 274 /* Line discipline .receive_buf() */ 275 static void v253_receive(struct tty_struct *tty, 276 const unsigned char *cp, char *fp, int count) 277 { 278 struct snd_soc_codec *codec = tty->disc_data; 279 280 if (!codec) 281 return; 282 283 if (!codec->control_data) { 284 /* First modem response, complete setup procedure */ 285 286 /* Set up codec driver access to modem controls */ 287 codec->control_data = tty; 288 codec->hw_write = (hw_write_t)tty->ops->write; 289 codec->pop_time = 1; 290 } 291 } 292 293 /* Line discipline .write_wakeup() */ 294 static void v253_wakeup(struct tty_struct *tty) 295 { 296 } 297 298 struct tty_ldisc_ops v253_ops = { 299 .magic = TTY_LDISC_MAGIC, 300 .name = "cx20442", 301 .owner = THIS_MODULE, 302 .open = v253_open, 303 .close = v253_close, 304 .hangup = v253_hangup, 305 .receive_buf = v253_receive, 306 .write_wakeup = v253_wakeup, 307 }; 308 EXPORT_SYMBOL_GPL(v253_ops); 309 310 311 /* 312 * Codec DAI 313 */ 314 315 struct snd_soc_dai cx20442_dai = { 316 .name = "CX20442", 317 .playback = { 318 .stream_name = "Playback", 319 .channels_min = 1, 320 .channels_max = 1, 321 .rates = SNDRV_PCM_RATE_8000, 322 .formats = SNDRV_PCM_FMTBIT_S16_LE, 323 }, 324 .capture = { 325 .stream_name = "Capture", 326 .channels_min = 1, 327 .channels_max = 1, 328 .rates = SNDRV_PCM_RATE_8000, 329 .formats = SNDRV_PCM_FMTBIT_S16_LE, 330 }, 331 }; 332 EXPORT_SYMBOL_GPL(cx20442_dai); 333 334 static int cx20442_codec_probe(struct platform_device *pdev) 335 { 336 struct snd_soc_device *socdev = platform_get_drvdata(pdev); 337 struct snd_soc_codec *codec; 338 int ret; 339 340 if (!cx20442_codec) { 341 dev_err(&pdev->dev, "cx20442 not yet discovered\n"); 342 return -ENODEV; 343 } 344 codec = cx20442_codec; 345 346 socdev->card->codec = codec; 347 348 /* register pcms */ 349 ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); 350 if (ret < 0) { 351 dev_err(&pdev->dev, "failed to create pcms\n"); 352 goto pcm_err; 353 } 354 355 cx20442_add_widgets(codec); 356 357 pcm_err: 358 return ret; 359 } 360 361 /* power down chip */ 362 static int cx20442_codec_remove(struct platform_device *pdev) 363 { 364 struct snd_soc_device *socdev = platform_get_drvdata(pdev); 365 366 snd_soc_free_pcms(socdev); 367 snd_soc_dapm_free(socdev); 368 369 return 0; 370 } 371 372 struct snd_soc_codec_device cx20442_codec_dev = { 373 .probe = cx20442_codec_probe, 374 .remove = cx20442_codec_remove, 375 }; 376 EXPORT_SYMBOL_GPL(cx20442_codec_dev); 377 378 static int cx20442_register(struct cx20442_priv *cx20442) 379 { 380 struct snd_soc_codec *codec = &cx20442->codec; 381 int ret; 382 383 mutex_init(&codec->mutex); 384 INIT_LIST_HEAD(&codec->dapm_widgets); 385 INIT_LIST_HEAD(&codec->dapm_paths); 386 387 codec->name = "CX20442"; 388 codec->owner = THIS_MODULE; 389 codec->private_data = cx20442; 390 391 codec->dai = &cx20442_dai; 392 codec->num_dai = 1; 393 394 codec->reg_cache = &cx20442->reg_cache; 395 codec->reg_cache_size = ARRAY_SIZE(cx20442->reg_cache); 396 codec->read = cx20442_read_reg_cache; 397 codec->write = cx20442_write; 398 399 codec->bias_level = SND_SOC_BIAS_OFF; 400 401 cx20442_dai.dev = codec->dev; 402 403 cx20442_codec = codec; 404 405 ret = snd_soc_register_codec(codec); 406 if (ret != 0) { 407 dev_err(codec->dev, "Failed to register codec: %d\n", ret); 408 goto err; 409 } 410 411 ret = snd_soc_register_dai(&cx20442_dai); 412 if (ret != 0) { 413 dev_err(codec->dev, "Failed to register DAI: %d\n", ret); 414 goto err_codec; 415 } 416 417 return 0; 418 419 err_codec: 420 snd_soc_unregister_codec(codec); 421 err: 422 cx20442_codec = NULL; 423 kfree(cx20442); 424 return ret; 425 } 426 427 static void cx20442_unregister(struct cx20442_priv *cx20442) 428 { 429 snd_soc_unregister_dai(&cx20442_dai); 430 snd_soc_unregister_codec(&cx20442->codec); 431 432 cx20442_codec = NULL; 433 kfree(cx20442); 434 } 435 436 static int cx20442_platform_probe(struct platform_device *pdev) 437 { 438 struct cx20442_priv *cx20442; 439 struct snd_soc_codec *codec; 440 441 cx20442 = kzalloc(sizeof(struct cx20442_priv), GFP_KERNEL); 442 if (cx20442 == NULL) 443 return -ENOMEM; 444 445 codec = &cx20442->codec; 446 447 codec->control_data = NULL; 448 codec->hw_write = NULL; 449 codec->pop_time = 0; 450 451 codec->dev = &pdev->dev; 452 platform_set_drvdata(pdev, cx20442); 453 454 return cx20442_register(cx20442); 455 } 456 457 static int __exit cx20442_platform_remove(struct platform_device *pdev) 458 { 459 struct cx20442_priv *cx20442 = platform_get_drvdata(pdev); 460 461 cx20442_unregister(cx20442); 462 return 0; 463 } 464 465 static struct platform_driver cx20442_platform_driver = { 466 .driver = { 467 .name = "cx20442", 468 .owner = THIS_MODULE, 469 }, 470 .probe = cx20442_platform_probe, 471 .remove = __exit_p(cx20442_platform_remove), 472 }; 473 474 static int __init cx20442_init(void) 475 { 476 return platform_driver_register(&cx20442_platform_driver); 477 } 478 module_init(cx20442_init); 479 480 static void __exit cx20442_exit(void) 481 { 482 platform_driver_unregister(&cx20442_platform_driver); 483 } 484 module_exit(cx20442_exit); 485 486 MODULE_DESCRIPTION("ASoC CX20442-11 voice modem codec driver"); 487 MODULE_AUTHOR("Janusz Krzysztofik"); 488 MODULE_LICENSE("GPL"); 489 MODULE_ALIAS("platform:cx20442"); 490