1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * cx20442.c -- CX20442 ALSA Soc Audio driver |
4 | * |
5 | * Copyright 2009 Janusz Krzysztofik <jkrzyszt@tis.icnet.pl> |
6 | * |
7 | * Initially based on sound/soc/codecs/wm8400.c |
8 | * 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 | |
24 | struct cx20442_priv { |
25 | struct tty_struct *tty; |
26 | struct regulator *por; |
27 | u8 reg_cache; |
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 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(c: component); |
92 | |
93 | if (reg >= 1) |
94 | return -EINVAL; |
95 | |
96 | return cx20442->reg_cache; |
97 | } |
98 | |
99 | enum 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 | |
123 | static 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 | } |
141 | static 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 | |
152 | static 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(c: 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 modem |
163 | * 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(value: old))) { |
180 | if (vsp == cx20442_pm_to_v253_vsp(value: old)) |
181 | return 0; |
182 | len = snprintf(buf, ARRAY_SIZE(buf), fmt: "at+vsp=%d\r" , vsp); |
183 | } else if (vsp == cx20442_pm_to_v253_vsp(value: old)) |
184 | len = snprintf(buf, ARRAY_SIZE(buf), fmt: "at+vls=%d\r" , vls); |
185 | else |
186 | len = snprintf(buf, ARRAY_SIZE(buf), |
187 | fmt: "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 code |
201 | * |
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 function |
205 | * 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 */ |
209 | static const char v253_init[] = "ate0m0q0+fclass=8\r" ; |
210 | |
211 | /* Line discipline .open() */ |
212 | static 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; |
231 | err: |
232 | tty->disc_data = NULL; |
233 | return ret; |
234 | } |
235 | |
236 | /* Line discipline .close() */ |
237 | static 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(c: 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() */ |
255 | static void v253_hangup(struct tty_struct *tty) |
256 | { |
257 | v253_close(tty); |
258 | } |
259 | |
260 | /* Line discipline .receive_buf() */ |
261 | static 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(c: 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 | |
281 | struct 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 | }; |
289 | EXPORT_SYMBOL_GPL(v253_ops); |
290 | |
291 | |
292 | /* |
293 | * Codec DAI |
294 | */ |
295 | |
296 | static 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 | |
314 | static 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(c: 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(ptr: cx20442->por)) |
325 | err = PTR_ERR(ptr: cx20442->por); |
326 | else |
327 | err = regulator_enable(regulator: 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(ptr: cx20442->por)) |
333 | err = PTR_ERR(ptr: cx20442->por); |
334 | else |
335 | err = regulator_disable(regulator: cx20442->por); |
336 | break; |
337 | default: |
338 | break; |
339 | } |
340 | |
341 | return err; |
342 | } |
343 | |
344 | static int cx20442_component_probe(struct snd_soc_component *component) |
345 | { |
346 | struct cx20442_priv *cx20442; |
347 | |
348 | cx20442 = kzalloc(size: sizeof(struct cx20442_priv), GFP_KERNEL); |
349 | if (cx20442 == NULL) |
350 | return -ENOMEM; |
351 | |
352 | cx20442->por = regulator_get(dev: component->dev, id: "POR" ); |
353 | if (IS_ERR(ptr: cx20442->por)) { |
354 | int err = PTR_ERR(ptr: 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 regulator |
359 | * is not available, regulator_get() never returns |
360 | * -EPROBE_DEFER as it is not able to justify if the regulator |
361 | * may still appear later. On the other hand, the board can |
362 | * still set full constraints flag at late_initcall in order |
363 | * to instruct regulator_get() to return a dummy one if |
364 | * sufficient. Hence, if we get -ENODEV here, let's convert |
365 | * it to -EPROBE_DEFER and wait for the board to decide or |
366 | * let Deferred Probe infrastructure handle this error. |
367 | */ |
368 | if (err == -ENODEV) |
369 | err = -EPROBE_DEFER; |
370 | kfree(objp: cx20442); |
371 | return err; |
372 | } |
373 | |
374 | cx20442->tty = NULL; |
375 | |
376 | snd_soc_component_set_drvdata(c: component, data: cx20442); |
377 | component->card->pop_time = 0; |
378 | |
379 | return 0; |
380 | } |
381 | |
382 | /* power down chip */ |
383 | static void cx20442_component_remove(struct snd_soc_component *component) |
384 | { |
385 | struct cx20442_priv *cx20442 = snd_soc_component_get_drvdata(c: component); |
386 | |
387 | if (cx20442->tty) { |
388 | struct tty_struct *tty = cx20442->tty; |
389 | tty_hangup(tty); |
390 | } |
391 | |
392 | if (!IS_ERR(ptr: cx20442->por)) { |
393 | /* should be already in STANDBY, hence disabled */ |
394 | regulator_put(regulator: cx20442->por); |
395 | } |
396 | |
397 | snd_soc_component_set_drvdata(c: component, NULL); |
398 | kfree(objp: cx20442); |
399 | } |
400 | |
401 | static 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 | |
416 | static int cx20442_platform_probe(struct platform_device *pdev) |
417 | { |
418 | return devm_snd_soc_register_component(dev: &pdev->dev, |
419 | component_driver: &cx20442_component_dev, dai_drv: &cx20442_dai, num_dai: 1); |
420 | } |
421 | |
422 | static struct platform_driver cx20442_platform_driver = { |
423 | .driver = { |
424 | .name = "cx20442-codec" , |
425 | }, |
426 | .probe = cx20442_platform_probe, |
427 | }; |
428 | |
429 | module_platform_driver(cx20442_platform_driver); |
430 | |
431 | MODULE_DESCRIPTION("ASoC CX20442-11 voice modem codec driver" ); |
432 | MODULE_AUTHOR("Janusz Krzysztofik" ); |
433 | MODULE_LICENSE("GPL" ); |
434 | MODULE_ALIAS("platform:cx20442-codec" ); |
435 | |