435 lines · c
1// SPDX-License-Identifier: GPL-2.0-or-later2/*3 * cx20442.c -- CX20442 ALSA Soc Audio driver4 *5 * Copyright 2009 Janusz Krzysztofik <jkrzyszt@tis.icnet.pl>6 *7 * Initially based on sound/soc/codecs/wm8400.c8 * Copyright 2008, 2009 Wolfson Microelectronics PLC.9 * Author: Mark Brown <broonie@opensource.wolfsonmicro.com>10 */11 12#include <linux/tty.h>13#include <linux/slab.h>14#include <linux/module.h>15#include <linux/regulator/consumer.h>16 17#include <sound/core.h>18#include <sound/initval.h>19#include <sound/soc.h>20 21#include "cx20442.h"22 23 24struct cx20442_priv {25 struct tty_struct *tty;26 struct regulator *por;27 u8 reg_cache;28};29 30#define CX20442_PM 0x031 32#define CX20442_TELIN 033#define CX20442_TELOUT 134#define CX20442_MIC 235#define CX20442_SPKOUT 336#define CX20442_AGC 437 38static 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 64static 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 88static unsigned int cx20442_read_reg_cache(struct snd_soc_component *component,89 unsigned int reg)90{91 struct cx20442_priv *cx20442 = snd_soc_component_get_drvdata(component);92 93 if (reg >= 1)94 return -EINVAL;95 96 return cx20442->reg_cache;97}98 99enum v253_vls {100 V253_VLS_NONE = 0,101 V253_VLS_T,102 V253_VLS_L,103 V253_VLS_LT,104 V253_VLS_S,105 V253_VLS_ST,106 V253_VLS_M,107 V253_VLS_MST,108 V253_VLS_S1,109 V253_VLS_S1T,110 V253_VLS_MS1T,111 V253_VLS_M1,112 V253_VLS_M1ST,113 V253_VLS_M1S1T,114 V253_VLS_H,115 V253_VLS_HT,116 V253_VLS_MS,117 V253_VLS_MS1,118 V253_VLS_M1S,119 V253_VLS_M1S1,120 V253_VLS_TEST,121};122 123static int cx20442_pm_to_v253_vls(u8 value)124{125 switch (value & ~(1 << CX20442_AGC)) {126 case 0:127 return V253_VLS_T;128 case (1 << CX20442_SPKOUT):129 case (1 << CX20442_MIC):130 case (1 << CX20442_SPKOUT) | (1 << CX20442_MIC):131 return V253_VLS_M1S1;132 case (1 << CX20442_TELOUT):133 case (1 << CX20442_TELIN):134 case (1 << CX20442_TELOUT) | (1 << CX20442_TELIN):135 return V253_VLS_L;136 case (1 << CX20442_TELOUT) | (1 << CX20442_MIC):137 return V253_VLS_NONE;138 }139 return -EINVAL;140}141static int cx20442_pm_to_v253_vsp(u8 value)142{143 switch (value & ~(1 << CX20442_AGC)) {144 case (1 << CX20442_SPKOUT):145 case (1 << CX20442_MIC):146 case (1 << CX20442_SPKOUT) | (1 << CX20442_MIC):147 return (bool)(value & (1 << CX20442_AGC));148 }149 return (value & (1 << CX20442_AGC)) ? -EINVAL : 0;150}151 152static int cx20442_write(struct snd_soc_component *component, unsigned int reg,153 unsigned int value)154{155 struct cx20442_priv *cx20442 = snd_soc_component_get_drvdata(component);156 int vls, vsp, old, len;157 char buf[18];158 159 if (reg >= 1)160 return -EINVAL;161 162 /* tty and write pointers required for talking to the modem163 * are expected to be set by the line discipline initialization code */164 if (!cx20442->tty || !cx20442->tty->ops->write)165 return -EIO;166 167 old = cx20442->reg_cache;168 cx20442->reg_cache = value;169 170 vls = cx20442_pm_to_v253_vls(value);171 if (vls < 0)172 return vls;173 174 vsp = cx20442_pm_to_v253_vsp(value);175 if (vsp < 0)176 return vsp;177 178 if ((vls == V253_VLS_T) ||179 (vls == cx20442_pm_to_v253_vls(old))) {180 if (vsp == cx20442_pm_to_v253_vsp(old))181 return 0;182 len = snprintf(buf, ARRAY_SIZE(buf), "at+vsp=%d\r", vsp);183 } else if (vsp == cx20442_pm_to_v253_vsp(old))184 len = snprintf(buf, ARRAY_SIZE(buf), "at+vls=%d\r", vls);185 else186 len = snprintf(buf, ARRAY_SIZE(buf),187 "at+vls=%d;+vsp=%d\r", vls, vsp);188 189 if (unlikely(len > (ARRAY_SIZE(buf) - 1)))190 return -ENOMEM;191 192 dev_dbg(component->dev, "%s: %s\n", __func__, buf);193 if (cx20442->tty->ops->write(cx20442->tty, buf, len) != len)194 return -EIO;195 196 return 0;197}198 199/*200 * Line discpline related code201 *202 * Any of the callback functions below can be used in two ways:203 * 1) registerd by a machine driver as one of line discipline operations,204 * 2) called from a machine's provided line discipline callback function205 * in case when extra machine specific code must be run as well.206 */207 208/* Modem init: echo off, digital speaker off, quiet off, voice mode */209static const char v253_init[] = "ate0m0q0+fclass=8\r";210 211/* Line discipline .open() */212static int v253_open(struct tty_struct *tty)213{214 int ret, len = strlen(v253_init);215 216 /* Doesn't make sense without write callback */217 if (!tty->ops->write)218 return -EINVAL;219 220 /* Won't work if no codec pointer has been passed by a card driver */221 if (!tty->disc_data)222 return -ENODEV;223 224 tty->receive_room = 16;225 if (tty->ops->write(tty, v253_init, len) != len) {226 ret = -EIO;227 goto err;228 }229 /* Actual setup will be performed after the modem responds. */230 return 0;231err:232 tty->disc_data = NULL;233 return ret;234}235 236/* Line discipline .close() */237static void v253_close(struct tty_struct *tty)238{239 struct snd_soc_component *component = tty->disc_data;240 struct cx20442_priv *cx20442;241 242 tty->disc_data = NULL;243 244 if (!component)245 return;246 247 cx20442 = snd_soc_component_get_drvdata(component);248 249 /* Prevent the codec driver from further accessing the modem */250 cx20442->tty = NULL;251 component->card->pop_time = 0;252}253 254/* Line discipline .hangup() */255static void v253_hangup(struct tty_struct *tty)256{257 v253_close(tty);258}259 260/* Line discipline .receive_buf() */261static void v253_receive(struct tty_struct *tty, const u8 *cp, const u8 *fp,262 size_t count)263{264 struct snd_soc_component *component = tty->disc_data;265 struct cx20442_priv *cx20442;266 267 if (!component)268 return;269 270 cx20442 = snd_soc_component_get_drvdata(component);271 272 if (!cx20442->tty) {273 /* First modem response, complete setup procedure */274 275 /* Set up codec driver access to modem controls */276 cx20442->tty = tty;277 component->card->pop_time = 1;278 }279}280 281struct tty_ldisc_ops v253_ops = {282 .name = "cx20442",283 .owner = THIS_MODULE,284 .open = v253_open,285 .close = v253_close,286 .hangup = v253_hangup,287 .receive_buf = v253_receive,288};289EXPORT_SYMBOL_GPL(v253_ops);290 291 292/*293 * Codec DAI294 */295 296static struct snd_soc_dai_driver cx20442_dai = {297 .name = "cx20442-voice",298 .playback = {299 .stream_name = "Playback",300 .channels_min = 1,301 .channels_max = 1,302 .rates = SNDRV_PCM_RATE_8000,303 .formats = SNDRV_PCM_FMTBIT_S16_LE,304 },305 .capture = {306 .stream_name = "Capture",307 .channels_min = 1,308 .channels_max = 1,309 .rates = SNDRV_PCM_RATE_8000,310 .formats = SNDRV_PCM_FMTBIT_S16_LE,311 },312};313 314static int cx20442_set_bias_level(struct snd_soc_component *component,315 enum snd_soc_bias_level level)316{317 struct cx20442_priv *cx20442 = snd_soc_component_get_drvdata(component);318 int err = 0;319 320 switch (level) {321 case SND_SOC_BIAS_PREPARE:322 if (snd_soc_component_get_bias_level(component) != SND_SOC_BIAS_STANDBY)323 break;324 if (IS_ERR(cx20442->por))325 err = PTR_ERR(cx20442->por);326 else327 err = regulator_enable(cx20442->por);328 break;329 case SND_SOC_BIAS_STANDBY:330 if (snd_soc_component_get_bias_level(component) != SND_SOC_BIAS_PREPARE)331 break;332 if (IS_ERR(cx20442->por))333 err = PTR_ERR(cx20442->por);334 else335 err = regulator_disable(cx20442->por);336 break;337 default:338 break;339 }340 341 return err;342}343 344static int cx20442_component_probe(struct snd_soc_component *component)345{346 struct cx20442_priv *cx20442;347 348 cx20442 = kzalloc(sizeof(struct cx20442_priv), GFP_KERNEL);349 if (cx20442 == NULL)350 return -ENOMEM;351 352 cx20442->por = regulator_get(component->dev, "POR");353 if (IS_ERR(cx20442->por)) {354 int err = PTR_ERR(cx20442->por);355 356 dev_warn(component->dev, "failed to get POR supply (%d)", err);357 /*358 * When running on a non-dt platform and requested regulator359 * is not available, regulator_get() never returns360 * -EPROBE_DEFER as it is not able to justify if the regulator361 * may still appear later. On the other hand, the board can362 * still set full constraints flag at late_initcall in order363 * to instruct regulator_get() to return a dummy one if364 * sufficient. Hence, if we get -ENODEV here, let's convert365 * it to -EPROBE_DEFER and wait for the board to decide or366 * let Deferred Probe infrastructure handle this error.367 */368 if (err == -ENODEV)369 err = -EPROBE_DEFER;370 kfree(cx20442);371 return err;372 }373 374 cx20442->tty = NULL;375 376 snd_soc_component_set_drvdata(component, cx20442);377 component->card->pop_time = 0;378 379 return 0;380}381 382/* power down chip */383static void cx20442_component_remove(struct snd_soc_component *component)384{385 struct cx20442_priv *cx20442 = snd_soc_component_get_drvdata(component);386 387 if (cx20442->tty) {388 struct tty_struct *tty = cx20442->tty;389 tty_hangup(tty);390 }391 392 if (!IS_ERR(cx20442->por)) {393 /* should be already in STANDBY, hence disabled */394 regulator_put(cx20442->por);395 }396 397 snd_soc_component_set_drvdata(component, NULL);398 kfree(cx20442);399}400 401static const struct snd_soc_component_driver cx20442_component_dev = {402 .probe = cx20442_component_probe,403 .remove = cx20442_component_remove,404 .set_bias_level = cx20442_set_bias_level,405 .read = cx20442_read_reg_cache,406 .write = cx20442_write,407 .dapm_widgets = cx20442_dapm_widgets,408 .num_dapm_widgets = ARRAY_SIZE(cx20442_dapm_widgets),409 .dapm_routes = cx20442_audio_map,410 .num_dapm_routes = ARRAY_SIZE(cx20442_audio_map),411 .idle_bias_on = 1,412 .use_pmdown_time = 1,413 .endianness = 1,414};415 416static int cx20442_platform_probe(struct platform_device *pdev)417{418 return devm_snd_soc_register_component(&pdev->dev,419 &cx20442_component_dev, &cx20442_dai, 1);420}421 422static struct platform_driver cx20442_platform_driver = {423 .driver = {424 .name = "cx20442-codec",425 },426 .probe = cx20442_platform_probe,427};428 429module_platform_driver(cx20442_platform_driver);430 431MODULE_DESCRIPTION("ASoC CX20442-11 voice modem codec driver");432MODULE_AUTHOR("Janusz Krzysztofik");433MODULE_LICENSE("GPL");434MODULE_ALIAS("platform:cx20442-codec");435