diff options
author | Mark Brown <broonie@opensource.wolfsonmicro.com> | 2008-11-13 09:33:14 -0500 |
---|---|---|
committer | Mark Brown <broonie@opensource.wolfsonmicro.com> | 2008-11-14 09:44:53 -0500 |
commit | 71cfc9028d762419ce4dea62b4afc9c32c4b4820 (patch) | |
tree | cf1441120fe54600e0152c829bf2372742131ae6 /sound/soc/codecs/wm8728.c | |
parent | 2bef901071448e0c86af8edb4797cd5f81b6240d (diff) |
ASoC: Add WM8728 codec driver
The WM8728 is a high performance stereo DAC designed for applications
such as DVD, home theatre and digital TV.
Signed-off-by: Mark Brown <broonie@opensource.wolfsonmicro.com>
Diffstat (limited to 'sound/soc/codecs/wm8728.c')
-rw-r--r-- | sound/soc/codecs/wm8728.c | 574 |
1 files changed, 574 insertions, 0 deletions
diff --git a/sound/soc/codecs/wm8728.c b/sound/soc/codecs/wm8728.c new file mode 100644 index 000000000000..3e39dea61241 --- /dev/null +++ b/sound/soc/codecs/wm8728.c | |||
@@ -0,0 +1,574 @@ | |||
1 | /* | ||
2 | * wm8728.c -- WM8728 ALSA SoC Audio driver | ||
3 | * | ||
4 | * Copyright 2008 Wolfson Microelectronics plc | ||
5 | * | ||
6 | * Author: Mark Brown <broonie@opensource.wolfsonmicro.com> | ||
7 | * | ||
8 | * This program is free software; you can redistribute it and/or modify | ||
9 | * it under the terms of the GNU General Public License version 2 as | ||
10 | * published by the Free Software Foundation. | ||
11 | */ | ||
12 | |||
13 | #include <linux/module.h> | ||
14 | #include <linux/moduleparam.h> | ||
15 | #include <linux/init.h> | ||
16 | #include <linux/delay.h> | ||
17 | #include <linux/pm.h> | ||
18 | #include <linux/i2c.h> | ||
19 | #include <linux/platform_device.h> | ||
20 | #include <linux/spi/spi.h> | ||
21 | #include <sound/core.h> | ||
22 | #include <sound/pcm.h> | ||
23 | #include <sound/pcm_params.h> | ||
24 | #include <sound/soc.h> | ||
25 | #include <sound/soc-dapm.h> | ||
26 | #include <sound/initval.h> | ||
27 | #include <sound/tlv.h> | ||
28 | |||
29 | #include "wm8728.h" | ||
30 | |||
31 | struct snd_soc_codec_device soc_codec_dev_wm8728; | ||
32 | |||
33 | /* | ||
34 | * We can't read the WM8728 register space so we cache them instead. | ||
35 | * Note that the defaults here aren't the physical defaults, we latch | ||
36 | * the volume update bits, mute the output and enable infinite zero | ||
37 | * detect. | ||
38 | */ | ||
39 | static const u16 wm8728_reg_defaults[] = { | ||
40 | 0x1ff, | ||
41 | 0x1ff, | ||
42 | 0x001, | ||
43 | 0x100, | ||
44 | }; | ||
45 | |||
46 | static inline unsigned int wm8728_read_reg_cache(struct snd_soc_codec *codec, | ||
47 | unsigned int reg) | ||
48 | { | ||
49 | u16 *cache = codec->reg_cache; | ||
50 | BUG_ON(reg > ARRAY_SIZE(wm8728_reg_defaults)); | ||
51 | return cache[reg]; | ||
52 | } | ||
53 | |||
54 | static inline void wm8728_write_reg_cache(struct snd_soc_codec *codec, | ||
55 | u16 reg, unsigned int value) | ||
56 | { | ||
57 | u16 *cache = codec->reg_cache; | ||
58 | BUG_ON(reg > ARRAY_SIZE(wm8728_reg_defaults)); | ||
59 | cache[reg] = value; | ||
60 | } | ||
61 | |||
62 | /* | ||
63 | * write to the WM8728 register space | ||
64 | */ | ||
65 | static int wm8728_write(struct snd_soc_codec *codec, unsigned int reg, | ||
66 | unsigned int value) | ||
67 | { | ||
68 | u8 data[2]; | ||
69 | |||
70 | /* data is | ||
71 | * D15..D9 WM8728 register offset | ||
72 | * D8...D0 register data | ||
73 | */ | ||
74 | data[0] = (reg << 1) | ((value >> 8) & 0x0001); | ||
75 | data[1] = value & 0x00ff; | ||
76 | |||
77 | wm8728_write_reg_cache(codec, reg, value); | ||
78 | |||
79 | if (codec->hw_write(codec->control_data, data, 2) == 2) | ||
80 | return 0; | ||
81 | else | ||
82 | return -EIO; | ||
83 | } | ||
84 | |||
85 | static const DECLARE_TLV_DB_SCALE(wm8728_tlv, -12750, 50, 1); | ||
86 | |||
87 | static const struct snd_kcontrol_new wm8728_snd_controls[] = { | ||
88 | |||
89 | SOC_DOUBLE_R_TLV("Digital Playback Volume", WM8728_DACLVOL, WM8728_DACRVOL, | ||
90 | 0, 255, 0, wm8728_tlv), | ||
91 | |||
92 | SOC_SINGLE("Deemphasis", WM8728_DACCTL, 1, 1, 0), | ||
93 | }; | ||
94 | |||
95 | static int wm8728_add_controls(struct snd_soc_codec *codec) | ||
96 | { | ||
97 | int err, i; | ||
98 | |||
99 | for (i = 0; i < ARRAY_SIZE(wm8728_snd_controls); i++) { | ||
100 | err = snd_ctl_add(codec->card, | ||
101 | snd_soc_cnew(&wm8728_snd_controls[i], | ||
102 | codec, NULL)); | ||
103 | if (err < 0) | ||
104 | return err; | ||
105 | } | ||
106 | |||
107 | return 0; | ||
108 | } | ||
109 | |||
110 | /* | ||
111 | * DAPM controls. | ||
112 | */ | ||
113 | static const struct snd_soc_dapm_widget wm8728_dapm_widgets[] = { | ||
114 | SND_SOC_DAPM_DAC("DAC", "HiFi Playback", SND_SOC_NOPM, 0, 0), | ||
115 | SND_SOC_DAPM_OUTPUT("VOUTL"), | ||
116 | SND_SOC_DAPM_OUTPUT("VOUTR"), | ||
117 | }; | ||
118 | |||
119 | static const struct snd_soc_dapm_route intercon[] = { | ||
120 | {"VOUTL", NULL, "DAC"}, | ||
121 | {"VOUTR", NULL, "DAC"}, | ||
122 | }; | ||
123 | |||
124 | static int wm8728_add_widgets(struct snd_soc_codec *codec) | ||
125 | { | ||
126 | snd_soc_dapm_new_controls(codec, wm8728_dapm_widgets, | ||
127 | ARRAY_SIZE(wm8728_dapm_widgets)); | ||
128 | |||
129 | snd_soc_dapm_add_routes(codec, intercon, ARRAY_SIZE(intercon)); | ||
130 | |||
131 | snd_soc_dapm_new_widgets(codec); | ||
132 | |||
133 | return 0; | ||
134 | } | ||
135 | |||
136 | static int wm8728_mute(struct snd_soc_dai *dai, int mute) | ||
137 | { | ||
138 | struct snd_soc_codec *codec = dai->codec; | ||
139 | u16 mute_reg = wm8728_read_reg_cache(codec, WM8728_DACCTL); | ||
140 | |||
141 | if (mute) | ||
142 | wm8728_write(codec, WM8728_DACCTL, mute_reg | 1); | ||
143 | else | ||
144 | wm8728_write(codec, WM8728_DACCTL, mute_reg & ~1); | ||
145 | |||
146 | return 0; | ||
147 | } | ||
148 | |||
149 | static int wm8728_hw_params(struct snd_pcm_substream *substream, | ||
150 | struct snd_pcm_hw_params *params) | ||
151 | { | ||
152 | struct snd_soc_pcm_runtime *rtd = substream->private_data; | ||
153 | struct snd_soc_device *socdev = rtd->socdev; | ||
154 | struct snd_soc_codec *codec = socdev->codec; | ||
155 | u16 dac = wm8728_read_reg_cache(codec, WM8728_DACCTL); | ||
156 | |||
157 | dac &= ~0x18; | ||
158 | |||
159 | switch (params_format(params)) { | ||
160 | case SNDRV_PCM_FORMAT_S16_LE: | ||
161 | break; | ||
162 | case SNDRV_PCM_FORMAT_S20_3LE: | ||
163 | dac |= 0x10; | ||
164 | break; | ||
165 | case SNDRV_PCM_FORMAT_S24_LE: | ||
166 | dac |= 0x08; | ||
167 | break; | ||
168 | default: | ||
169 | return -EINVAL; | ||
170 | } | ||
171 | |||
172 | wm8728_write(codec, WM8728_DACCTL, dac); | ||
173 | |||
174 | return 0; | ||
175 | } | ||
176 | |||
177 | static int wm8728_set_dai_fmt(struct snd_soc_dai *codec_dai, | ||
178 | unsigned int fmt) | ||
179 | { | ||
180 | struct snd_soc_codec *codec = codec_dai->codec; | ||
181 | u16 iface = wm8728_read_reg_cache(codec, WM8728_IFCTL); | ||
182 | |||
183 | /* Currently only I2S is supported by the driver, though the | ||
184 | * hardware is more flexible. | ||
185 | */ | ||
186 | switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { | ||
187 | case SND_SOC_DAIFMT_I2S: | ||
188 | iface |= 1; | ||
189 | break; | ||
190 | default: | ||
191 | return -EINVAL; | ||
192 | } | ||
193 | |||
194 | /* The hardware only support full slave mode */ | ||
195 | switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { | ||
196 | case SND_SOC_DAIFMT_CBS_CFS: | ||
197 | break; | ||
198 | default: | ||
199 | return -EINVAL; | ||
200 | } | ||
201 | |||
202 | switch (fmt & SND_SOC_DAIFMT_INV_MASK) { | ||
203 | case SND_SOC_DAIFMT_NB_NF: | ||
204 | iface &= ~0x22; | ||
205 | break; | ||
206 | case SND_SOC_DAIFMT_IB_NF: | ||
207 | iface |= 0x20; | ||
208 | iface &= ~0x02; | ||
209 | break; | ||
210 | case SND_SOC_DAIFMT_NB_IF: | ||
211 | iface |= 0x02; | ||
212 | iface &= ~0x20; | ||
213 | break; | ||
214 | case SND_SOC_DAIFMT_IB_IF: | ||
215 | iface |= 0x22; | ||
216 | break; | ||
217 | default: | ||
218 | return -EINVAL; | ||
219 | } | ||
220 | |||
221 | wm8728_write(codec, WM8728_IFCTL, iface); | ||
222 | return 0; | ||
223 | } | ||
224 | |||
225 | static int wm8728_set_bias_level(struct snd_soc_codec *codec, | ||
226 | enum snd_soc_bias_level level) | ||
227 | { | ||
228 | u16 reg; | ||
229 | int i; | ||
230 | |||
231 | switch (level) { | ||
232 | case SND_SOC_BIAS_ON: | ||
233 | case SND_SOC_BIAS_PREPARE: | ||
234 | case SND_SOC_BIAS_STANDBY: | ||
235 | if (codec->bias_level == SND_SOC_BIAS_OFF) { | ||
236 | /* Power everything up... */ | ||
237 | reg = wm8728_read_reg_cache(codec, WM8728_DACCTL); | ||
238 | wm8728_write(codec, WM8728_DACCTL, reg & ~0x4); | ||
239 | |||
240 | /* ..then sync in the register cache. */ | ||
241 | for (i = 0; i < ARRAY_SIZE(wm8728_reg_defaults); i++) | ||
242 | wm8728_write(codec, i, | ||
243 | wm8728_read_reg_cache(codec, i)); | ||
244 | } | ||
245 | break; | ||
246 | |||
247 | case SND_SOC_BIAS_OFF: | ||
248 | reg = wm8728_read_reg_cache(codec, WM8728_DACCTL); | ||
249 | wm8728_write(codec, WM8728_DACCTL, reg | 0x4); | ||
250 | break; | ||
251 | } | ||
252 | codec->bias_level = level; | ||
253 | return 0; | ||
254 | } | ||
255 | |||
256 | #define WM8728_RATES (SNDRV_PCM_RATE_8000_192000) | ||
257 | |||
258 | #define WM8728_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |\ | ||
259 | SNDRV_PCM_FMTBIT_S24_LE) | ||
260 | |||
261 | struct snd_soc_dai wm8728_dai = { | ||
262 | .name = "WM8728", | ||
263 | .playback = { | ||
264 | .stream_name = "Playback", | ||
265 | .channels_min = 2, | ||
266 | .channels_max = 2, | ||
267 | .rates = WM8728_RATES, | ||
268 | .formats = WM8728_FORMATS, | ||
269 | }, | ||
270 | .ops = { | ||
271 | .hw_params = wm8728_hw_params, | ||
272 | }, | ||
273 | .dai_ops = { | ||
274 | .digital_mute = wm8728_mute, | ||
275 | .set_fmt = wm8728_set_dai_fmt, | ||
276 | } | ||
277 | }; | ||
278 | EXPORT_SYMBOL_GPL(wm8728_dai); | ||
279 | |||
280 | static int wm8728_suspend(struct platform_device *pdev, pm_message_t state) | ||
281 | { | ||
282 | struct snd_soc_device *socdev = platform_get_drvdata(pdev); | ||
283 | struct snd_soc_codec *codec = socdev->codec; | ||
284 | |||
285 | wm8728_set_bias_level(codec, SND_SOC_BIAS_OFF); | ||
286 | |||
287 | return 0; | ||
288 | } | ||
289 | |||
290 | static int wm8728_resume(struct platform_device *pdev) | ||
291 | { | ||
292 | struct snd_soc_device *socdev = platform_get_drvdata(pdev); | ||
293 | struct snd_soc_codec *codec = socdev->codec; | ||
294 | |||
295 | wm8728_set_bias_level(codec, codec->suspend_bias_level); | ||
296 | |||
297 | return 0; | ||
298 | } | ||
299 | |||
300 | /* | ||
301 | * initialise the WM8728 driver | ||
302 | * register the mixer and dsp interfaces with the kernel | ||
303 | */ | ||
304 | static int wm8728_init(struct snd_soc_device *socdev) | ||
305 | { | ||
306 | struct snd_soc_codec *codec = socdev->codec; | ||
307 | int ret = 0; | ||
308 | |||
309 | codec->name = "WM8728"; | ||
310 | codec->owner = THIS_MODULE; | ||
311 | codec->read = wm8728_read_reg_cache; | ||
312 | codec->write = wm8728_write; | ||
313 | codec->set_bias_level = wm8728_set_bias_level; | ||
314 | codec->dai = &wm8728_dai; | ||
315 | codec->num_dai = 1; | ||
316 | codec->bias_level = SND_SOC_BIAS_OFF; | ||
317 | codec->reg_cache_size = ARRAY_SIZE(wm8728_reg_defaults); | ||
318 | codec->reg_cache = kmemdup(wm8728_reg_defaults, | ||
319 | sizeof(wm8728_reg_defaults), | ||
320 | GFP_KERNEL); | ||
321 | if (codec->reg_cache == NULL) | ||
322 | return -ENOMEM; | ||
323 | |||
324 | /* register pcms */ | ||
325 | ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); | ||
326 | if (ret < 0) { | ||
327 | printk(KERN_ERR "wm8728: failed to create pcms\n"); | ||
328 | goto pcm_err; | ||
329 | } | ||
330 | |||
331 | /* power on device */ | ||
332 | wm8728_set_bias_level(codec, SND_SOC_BIAS_STANDBY); | ||
333 | |||
334 | wm8728_add_controls(codec); | ||
335 | wm8728_add_widgets(codec); | ||
336 | ret = snd_soc_register_card(socdev); | ||
337 | if (ret < 0) { | ||
338 | printk(KERN_ERR "wm8728: failed to register card\n"); | ||
339 | goto card_err; | ||
340 | } | ||
341 | |||
342 | return ret; | ||
343 | |||
344 | card_err: | ||
345 | snd_soc_free_pcms(socdev); | ||
346 | snd_soc_dapm_free(socdev); | ||
347 | pcm_err: | ||
348 | kfree(codec->reg_cache); | ||
349 | return ret; | ||
350 | } | ||
351 | |||
352 | static struct snd_soc_device *wm8728_socdev; | ||
353 | |||
354 | #if defined(CONFIG_I2C) || defined(CONFIG_I2C_MODULE) | ||
355 | |||
356 | /* | ||
357 | * WM8728 2 wire address is determined by GPIO5 | ||
358 | * state during powerup. | ||
359 | * low = 0x1a | ||
360 | * high = 0x1b | ||
361 | */ | ||
362 | |||
363 | static int wm8728_i2c_probe(struct i2c_client *i2c, | ||
364 | const struct i2c_device_id *id) | ||
365 | { | ||
366 | struct snd_soc_device *socdev = wm8728_socdev; | ||
367 | struct snd_soc_codec *codec = socdev->codec; | ||
368 | int ret; | ||
369 | |||
370 | i2c_set_clientdata(i2c, codec); | ||
371 | codec->control_data = i2c; | ||
372 | |||
373 | ret = wm8728_init(socdev); | ||
374 | if (ret < 0) | ||
375 | pr_err("failed to initialise WM8728\n"); | ||
376 | |||
377 | return ret; | ||
378 | } | ||
379 | |||
380 | static int wm8728_i2c_remove(struct i2c_client *client) | ||
381 | { | ||
382 | struct snd_soc_codec *codec = i2c_get_clientdata(client); | ||
383 | kfree(codec->reg_cache); | ||
384 | return 0; | ||
385 | } | ||
386 | |||
387 | static const struct i2c_device_id wm8728_i2c_id[] = { | ||
388 | { "wm8728", 0 }, | ||
389 | { } | ||
390 | }; | ||
391 | MODULE_DEVICE_TABLE(i2c, wm8728_i2c_id); | ||
392 | |||
393 | static struct i2c_driver wm8728_i2c_driver = { | ||
394 | .driver = { | ||
395 | .name = "WM8728 I2C Codec", | ||
396 | .owner = THIS_MODULE, | ||
397 | }, | ||
398 | .probe = wm8728_i2c_probe, | ||
399 | .remove = wm8728_i2c_remove, | ||
400 | .id_table = wm8728_i2c_id, | ||
401 | }; | ||
402 | |||
403 | static int wm8728_add_i2c_device(struct platform_device *pdev, | ||
404 | const struct wm8728_setup_data *setup) | ||
405 | { | ||
406 | struct i2c_board_info info; | ||
407 | struct i2c_adapter *adapter; | ||
408 | struct i2c_client *client; | ||
409 | int ret; | ||
410 | |||
411 | ret = i2c_add_driver(&wm8728_i2c_driver); | ||
412 | if (ret != 0) { | ||
413 | dev_err(&pdev->dev, "can't add i2c driver\n"); | ||
414 | return ret; | ||
415 | } | ||
416 | |||
417 | memset(&info, 0, sizeof(struct i2c_board_info)); | ||
418 | info.addr = setup->i2c_address; | ||
419 | strlcpy(info.type, "wm8728", I2C_NAME_SIZE); | ||
420 | |||
421 | adapter = i2c_get_adapter(setup->i2c_bus); | ||
422 | if (!adapter) { | ||
423 | dev_err(&pdev->dev, "can't get i2c adapter %d\n", | ||
424 | setup->i2c_bus); | ||
425 | goto err_driver; | ||
426 | } | ||
427 | |||
428 | client = i2c_new_device(adapter, &info); | ||
429 | i2c_put_adapter(adapter); | ||
430 | if (!client) { | ||
431 | dev_err(&pdev->dev, "can't add i2c device at 0x%x\n", | ||
432 | (unsigned int)info.addr); | ||
433 | goto err_driver; | ||
434 | } | ||
435 | |||
436 | return 0; | ||
437 | |||
438 | err_driver: | ||
439 | i2c_del_driver(&wm8728_i2c_driver); | ||
440 | return -ENODEV; | ||
441 | } | ||
442 | #endif | ||
443 | |||
444 | #if defined(CONFIG_SPI_MASTER) | ||
445 | static int __devinit wm8728_spi_probe(struct spi_device *spi) | ||
446 | { | ||
447 | struct snd_soc_device *socdev = wm8728_socdev; | ||
448 | struct snd_soc_codec *codec = socdev->codec; | ||
449 | int ret; | ||
450 | |||
451 | codec->control_data = spi; | ||
452 | |||
453 | ret = wm8728_init(socdev); | ||
454 | if (ret < 0) | ||
455 | dev_err(&spi->dev, "failed to initialise WM8728\n"); | ||
456 | |||
457 | return ret; | ||
458 | } | ||
459 | |||
460 | static int __devexit wm8728_spi_remove(struct spi_device *spi) | ||
461 | { | ||
462 | return 0; | ||
463 | } | ||
464 | |||
465 | static struct spi_driver wm8728_spi_driver = { | ||
466 | .driver = { | ||
467 | .name = "wm8728", | ||
468 | .bus = &spi_bus_type, | ||
469 | .owner = THIS_MODULE, | ||
470 | }, | ||
471 | .probe = wm8728_spi_probe, | ||
472 | .remove = __devexit_p(wm8728_spi_remove), | ||
473 | }; | ||
474 | |||
475 | static int wm8728_spi_write(struct spi_device *spi, const char *data, int len) | ||
476 | { | ||
477 | struct spi_transfer t; | ||
478 | struct spi_message m; | ||
479 | u8 msg[2]; | ||
480 | |||
481 | if (len <= 0) | ||
482 | return 0; | ||
483 | |||
484 | msg[0] = data[0]; | ||
485 | msg[1] = data[1]; | ||
486 | |||
487 | spi_message_init(&m); | ||
488 | memset(&t, 0, (sizeof t)); | ||
489 | |||
490 | t.tx_buf = &msg[0]; | ||
491 | t.len = len; | ||
492 | |||
493 | spi_message_add_tail(&t, &m); | ||
494 | spi_sync(spi, &m); | ||
495 | |||
496 | return len; | ||
497 | } | ||
498 | #endif /* CONFIG_SPI_MASTER */ | ||
499 | |||
500 | static int wm8728_probe(struct platform_device *pdev) | ||
501 | { | ||
502 | struct snd_soc_device *socdev = platform_get_drvdata(pdev); | ||
503 | struct wm8728_setup_data *setup; | ||
504 | struct snd_soc_codec *codec; | ||
505 | int ret = 0; | ||
506 | |||
507 | setup = socdev->codec_data; | ||
508 | codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL); | ||
509 | if (codec == NULL) | ||
510 | return -ENOMEM; | ||
511 | |||
512 | socdev->codec = codec; | ||
513 | mutex_init(&codec->mutex); | ||
514 | INIT_LIST_HEAD(&codec->dapm_widgets); | ||
515 | INIT_LIST_HEAD(&codec->dapm_paths); | ||
516 | |||
517 | wm8728_socdev = socdev; | ||
518 | ret = -ENODEV; | ||
519 | |||
520 | #if defined(CONFIG_I2C) || defined(CONFIG_I2C_MODULE) | ||
521 | if (setup->i2c_address) { | ||
522 | codec->hw_write = (hw_write_t)i2c_master_send; | ||
523 | ret = wm8728_add_i2c_device(pdev, setup); | ||
524 | } | ||
525 | #endif | ||
526 | #if defined(CONFIG_SPI_MASTER) | ||
527 | if (setup->spi) { | ||
528 | codec->hw_write = (hw_write_t)wm8728_spi_write; | ||
529 | ret = spi_register_driver(&wm8728_spi_driver); | ||
530 | if (ret != 0) | ||
531 | printk(KERN_ERR "can't add spi driver"); | ||
532 | } | ||
533 | #endif | ||
534 | |||
535 | if (ret != 0) | ||
536 | kfree(codec); | ||
537 | |||
538 | return ret; | ||
539 | } | ||
540 | |||
541 | /* power down chip */ | ||
542 | static int wm8728_remove(struct platform_device *pdev) | ||
543 | { | ||
544 | struct snd_soc_device *socdev = platform_get_drvdata(pdev); | ||
545 | struct snd_soc_codec *codec = socdev->codec; | ||
546 | |||
547 | if (codec->control_data) | ||
548 | wm8728_set_bias_level(codec, SND_SOC_BIAS_OFF); | ||
549 | |||
550 | snd_soc_free_pcms(socdev); | ||
551 | snd_soc_dapm_free(socdev); | ||
552 | #if defined(CONFIG_I2C) || defined(CONFIG_I2C_MODULE) | ||
553 | i2c_unregister_device(codec->control_data); | ||
554 | i2c_del_driver(&wm8728_i2c_driver); | ||
555 | #endif | ||
556 | #if defined(CONFIG_SPI_MASTER) | ||
557 | spi_unregister_driver(&wm8728_spi_driver); | ||
558 | #endif | ||
559 | kfree(codec); | ||
560 | |||
561 | return 0; | ||
562 | } | ||
563 | |||
564 | struct snd_soc_codec_device soc_codec_dev_wm8728 = { | ||
565 | .probe = wm8728_probe, | ||
566 | .remove = wm8728_remove, | ||
567 | .suspend = wm8728_suspend, | ||
568 | .resume = wm8728_resume, | ||
569 | }; | ||
570 | EXPORT_SYMBOL_GPL(soc_codec_dev_wm8728); | ||
571 | |||
572 | MODULE_DESCRIPTION("ASoC WM8728 driver"); | ||
573 | MODULE_AUTHOR("Mark Brown <broonie@opensource.wolfsonmicro.com>"); | ||
574 | MODULE_LICENSE("GPL"); | ||