[alsa-devel] [RFC] SoC WM8940 Driver
From: Jonathan Cameron jic23@cam.ac.uk
Initial support for the WM8940 Mono Codec with speaker driver
---
This is my first foray into the world of alsa and aSoC, so I thought I'd make an initial posting of this driver before getting bogged down in testing every last feature works.
The board I have has it wired up to i2c limiting options for interface testing and I've currently only tested it as a slave to a pxa271 using i2s. Board is an xbow Imote 2.
I will hopefully be able to test a few other interface combinations.
All comments welcomed! My apologies if parts of this are complete rubbish!
diff --git a/sound/soc/codecs/wm8940.c b/sound/soc/codecs/wm8940.c new file mode 100644 index 0000000..3491b9c --- /dev/null +++ b/sound/soc/codecs/wm8940.c @@ -0,0 +1,921 @@ +/* + * wm8940.c -- WM8940 ALSA Soc Audio driver + * + * Author: Jonathan Cameron jic23@cam.ac.uk + * + * Based on wm8510.c + * Copyright 2006 Wolfson Microelectronics PLC. + * Author: Liam Girdwood lrg@slimlogic.co.uk + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * Not currently handled: + * VROI (resistance control for unused outputs. + * AUXMode (inverting vs mixer) + * No means to obtain current gain if alc enabled. + * No use made of gpio + * Auto increment writes (can't think why you'd want to disable it! + * Fast VMID discharge for power down + * Soft Start. + * LoutR control + * DLR and ALR Swaps not enabled + */ +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/i2c.h> +#include <linux/platform_device.h> +#include <linux/spi/spi.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/initval.h> +#include <sound/tlv.h> + +#include "wm8940.h" + +struct wm8940_priv { + unsigned int sysclk; + unsigned int mode; + unsigned int clk_inversion; +}; + +static u16 wm8940_reg_defaults[] = { + [WM8940_SOFTRESET] = 0x8940, + [WM8940_POWER1] = 0x0000, + [WM8940_POWER2] = 0x0000, + [WM8940_POWER3] = 0x0000, + [WM8940_IFACE] = 0x0010, + [WM8940_COMPANDINGCTL] = 0x0000, + [WM8940_CLOCK] = 0x0140, + [WM8940_ADDCNTRL] = 0x0000, + [WM8940_GPIO] = 0x0000, + [WM8940_CTLINT] = 0x0002, + [WM8940_DAC] = 0x0000, + [WM8940_DACVOL] = 0x00FF, + + [WM8940_ADC] = 0x0100, + [WM8940_ADCVOL] = 0x00FF, + [WM8940_NOTCH1 ... WM8940_NOTCH8] = 0x0000, + [WM8940_DACLIM1] = 0x0032, + [WM8940_DACLIM2] = 0x0000, + + [WM8940_ALC1] = 0x0038, + [WM8940_ALC2] = 0x000B, + [WM8940_ALC3] = 0x0032, + [WM8940_NOISEGATE] = 0x0000, + [WM8940_PLLN] = 0x0041, + [WM8940_PLLK1] = 0x000C, + [WM8940_PLLK2] = 0x0093, + [WM8940_PLLK3] = 0x00E9, + + [WM8940_ALC4] = 0x0030, + + [WM8940_INPUTCTL] = 0x0002, + [WM8940_PGAGAIN] = 0x0050, + + [WM8940_ADCBOOST] = 0x0002, + + [WM8940_OUTPUTCTL] = 0x0002, + [WM8940_SPKMIX] = 0x0000, + + [WM8940_SPKVOL] = 0x0079, + + [WM8940_MONOMIX] = 0x0000, +}; + +struct snd_soc_codec_device soc_codec_dev_wm8940; + +#define WM8940_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 | \ + SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 | \ + SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) + +#define WM8940_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_S20_3LE | \ + SNDRV_PCM_FMTBIT_S24_LE | \ + SNDRV_PCM_FMTBIT_S32_LE) + + +static inline unsigned int wm8940_read_reg_cache(struct snd_soc_codec *codec, + unsigned int reg) +{ + u16 *cache = codec->reg_cache; + + if (reg > ARRAY_SIZE(wm8940_reg_defaults)) + return -1; + + return cache[reg]; +} + +static inline int wm8940_write_reg_cache(struct snd_soc_codec *codec, + u16 reg, unsigned int value) +{ + u16 *cache = codec->reg_cache; + + if (reg > ARRAY_SIZE(wm8940_reg_defaults)) + return -1; + + cache[reg] = value; + + return 0; +} + +static int wm8940_write(struct snd_soc_codec *codec, unsigned int reg, + unsigned int value) +{ + u8 data[3] = { reg, + (value & 0xff00) >> 8, + (value & 0x00ff) + }; + + wm8940_write_reg_cache(codec, reg, value); + + return (codec->hw_write(codec->control_data, data, 3) == 3) ? 0 : -EIO; + +} + +static const char *wm8940_companding[] = { "Off", "NC", "u-law", "A-law" }; +static const char *wm8940_alc_mode_text[] = {"ALC", "Limiter"}; +static const char *wm8940_mic_bias_levels_text[] = {"0.9", "0.65"}; +static const char *wm8940_filter_mode_text[] = {"Audio", "Application"}; + +static const struct soc_enum wm8940_enum[] = { + SOC_ENUM_SINGLE(WM8940_COMPANDINGCTL, 1, 4, wm8940_companding), + SOC_ENUM_SINGLE(WM8940_COMPANDINGCTL, 3, 4, wm8940_companding), + SOC_ENUM_SINGLE(WM8940_ALC3, 8, 2, wm8940_alc_mode_text), + SOC_ENUM_SINGLE(WM8940_INPUTCTL, 8, 2, wm8940_mic_bias_levels_text), + SOC_ENUM_SINGLE(WM8940_ADC, 7, 2, wm8940_filter_mode_text), +}; + +DECLARE_TLV_DB_SCALE(wm8940_spk_vol_tlv, -5700, 100, 1); +DECLARE_TLV_DB_SCALE(wm8940_att_tlv, -1000, 1000, 0); +DECLARE_TLV_DB_SCALE(wm8940_pga_vol_tlv, -1200, 75, 0); +DECLARE_TLV_DB_SCALE(wm8940_alc_min_tlv, -1200, 600, 0); +DECLARE_TLV_DB_SCALE(wm8940_alc_max_tlv, 675, 600, 0); +DECLARE_TLV_DB_SCALE(wm8940_alc_tar_tlv, -2250, 50, 0); +DECLARE_TLV_DB_SCALE(wm8940_lim_boost_tlv, 0, 100, 0); +DECLARE_TLV_DB_SCALE(wm8940_lim_thresh_tlv, -600, 100, 0); +DECLARE_TLV_DB_SCALE(wm8940_adc_tlv, -127500, 50, 1); + +static const struct snd_kcontrol_new wm8940_snd_controls[] = { + SOC_SINGLE("Digital Loopback Switch dac to adc", WM8940_COMPANDINGCTL, + 6, 1, 0), + SOC_ENUM("DAC Companding", wm8940_enum[1]), + SOC_ENUM("ADC Companding", wm8940_enum[0]), + SOC_SINGLE("Companding 8 bit", WM8940_COMPANDINGCTL, 5, 1, 0), + SOC_SINGLE("Digital Loopback Switch adc to dac", WM8940_COMPANDINGCTL, + 0, 1, 0), + + /* Auto level control */ + SOC_ENUM("ALC Mode", wm8940_enum[2]), + SOC_SINGLE("ALC Enable Switch", WM8940_ALC1, 8, 1, 0), + SOC_SINGLE_TLV("ALC Capture Max Gain", WM8940_ALC1, + 3, 7, 1, wm8940_alc_max_tlv), + SOC_SINGLE_TLV("ALC Capture Min Gain", WM8940_ALC1, + 0, 7, 0, wm8940_alc_min_tlv), + SOC_SINGLE_TLV("ALC Capture Target", WM8940_ALC2, + 0, 14, 0, wm8940_alc_tar_tlv), + SOC_SINGLE("ALC Capture Hold", WM8940_ALC2, 4, 10, 0), + SOC_SINGLE("ALC Capture Decay", WM8940_ALC3, 4, 10, 0), + SOC_SINGLE("ALC Capture Attach", WM8940_ALC3, 0, 10, 0), + SOC_SINGLE("ALC ZC Switch", WM8940_ALC4, 1, 1, 0), + SOC_SINGLE("ALC Capture Noise Gate Switch", WM8940_NOISEGATE, + 3, 1, 0), + SOC_SINGLE("ALC Capture Noise Gate Threshold", WM8940_NOISEGATE, + 0, 7, 0), + + SOC_SINGLE("DAC Playback Limiter Switch", WM8940_DACLIM1, 8, 1, 0), + SOC_SINGLE("DAC Playback Limiter Attack", WM8940_DACLIM1, 0, 9, 0), + SOC_SINGLE("DAC Playback Limiter Decay", WM8940_DACLIM1, 4, 11, 0), + SOC_SINGLE_TLV("DAC Playback Limiter Threshold", WM8940_DACLIM2, + 4, 9, 1, wm8940_lim_thresh_tlv), + SOC_SINGLE_TLV("DAC Playback Limiter Boost", WM8940_DACLIM2, + 0, 12, 0, wm8940_lim_boost_tlv), + + SOC_SINGLE("Capture PGA ZC Switch", WM8940_PGAGAIN, 7, 1, 0), + SOC_SINGLE_TLV("Capture PGA Volume", WM8940_PGAGAIN, + 0, 63, 0, wm8940_pga_vol_tlv), + SOC_SINGLE_TLV("Digital Playback Volume", WM8940_DACVOL, + 0, 255, 0, wm8940_adc_tlv), + SOC_SINGLE_TLV("Digital Capture Volume", WM8940_ADCVOL, + 0, 255, 0, wm8940_adc_tlv), + SOC_ENUM("Mic Bias Level", wm8940_enum[3]), + SOC_SINGLE("Capture Boost(+20dB)", WM8940_ADCBOOST, 8, 1, 0), + + SOC_SINGLE_TLV("Speaker Playback Volume", WM8940_SPKVOL, + 0, 63, 0, wm8940_spk_vol_tlv), + SOC_SINGLE("Speaker Playback Mute", WM8940_SPKVOL, 6, 1, 0), + SOC_SINGLE_TLV("Speaker Playback Attenuation", WM8940_SPKVOL, + 8, 1, 1, wm8940_att_tlv), + SOC_SINGLE("Speaker Playback ZC", WM8940_SPKVOL, 7, 1, 0), + + SOC_SINGLE("Mono Out Mute", WM8940_MONOMIX, 6, 1, 0), + SOC_SINGLE_TLV("Mono Out Attenuation", WM8940_MONOMIX, + 7, 1, 1, wm8940_att_tlv), + + SOC_SINGLE("High Pass Filter Switch", WM8940_ADC, 8, 1, 0), + SOC_ENUM("High Pass Filter Mode", wm8940_enum[4]), + SOC_SINGLE("High Pass Filter Cut Off", WM8940_ADC, 4, 7, 0), + SOC_SINGLE("ADC Inversion Switch", WM8940_ADC, 0, 1, 0), + SOC_SINGLE("DAC Inversion Switch", WM8940_DAC, 0, 1, 0), + SOC_SINGLE("DAC Auto Mute Switch", WM8940_DAC, 2, 1, 0), + SOC_SINGLE("ZC Timeout Clock Enable Switch", WM8940_ADDCNTRL, 0, 1, 0), + + /* Notch filters */ + SOC_SINGLE("Notch Filter 1 Switch", WM8940_NOTCH1, 14, 1, 0), + SOC_SINGLE("Notch Filter 2 Switch", WM8940_NOTCH3, 14, 1, 0), + SOC_SINGLE("Notch Filter 3 Switch", WM8940_NOTCH5, 14, 1, 0), + SOC_SINGLE("Notch Filter 4 Switch", WM8940_NOTCH7, 14, 1, 0), + + SOC_SINGLE("Notch Filter 1 Update Switch", WM8940_NOTCH1, 15, 1, 0), + SOC_SINGLE("Notch Filter 2 Update Switch", WM8940_NOTCH3, 15, 1, 0), + SOC_SINGLE("Notch Filter 3 Update Switch", WM8940_NOTCH5, 15, 1, 0), + SOC_SINGLE("Notch Filter 4 Update Switch", WM8940_NOTCH7, 15, 1, 0), + + SOC_SINGLE("Notch Filter 1 Coefficient a0", WM8940_NOTCH1, 0, 16383, 1), + SOC_SINGLE("Notch Filter 2 Coefficient a0", WM8940_NOTCH3, 0, 16383, 1), + SOC_SINGLE("Notch Filter 3 Coefficient a0", WM8940_NOTCH5, 0, 16383, 1), + SOC_SINGLE("Notch Filter 4 Coefficient a0", WM8940_NOTCH7, 0, 16383, 1), + + SOC_SINGLE("Notch Filter 1 Coefficient a1", WM8940_NOTCH2, 0, 16383, 1), + SOC_SINGLE("Notch Filter 2 Coefficient a1", WM8940_NOTCH4, 0, 16383, 1), + SOC_SINGLE("Notch Filter 3 Coefficient a1", WM8940_NOTCH6, 0, 16383, 1), + SOC_SINGLE("Notch Filter 4 Coefficient a1", WM8940_NOTCH8, 0, 16383, 1), +}; + +static const struct snd_kcontrol_new wm8940_speaker_mixer_controls[] = { + SOC_DAPM_SINGLE("Line Bypass Switch", WM8940_SPKMIX, 1, 1, 0), + SOC_DAPM_SINGLE("Aux Playback Switch", WM8940_SPKMIX, 5, 1, 0), + SOC_DAPM_SINGLE("PCM Playback Switch", WM8940_SPKMIX, 0, 1, 0), +}; + +static const struct snd_kcontrol_new wm8940_mono_mixer_controls[] = { + SOC_DAPM_SINGLE("Line Bypass Switch", WM8940_MONOMIX, 1, 1, 0), + SOC_DAPM_SINGLE("Aux Playback Switch", WM8940_MONOMIX, 2, 1, 0), + SOC_DAPM_SINGLE("PCM Playback Switch", WM8940_MONOMIX, 0, 1, 0), +}; + +DECLARE_TLV_DB_SCALE(wm8940_boost_vol_tlv, -1500, 300, 1); +static const struct snd_kcontrol_new wm8940_input_boost_controls[] = { + SOC_DAPM_SINGLE("Mic PGA Switch", WM8940_PGAGAIN, 6, 1, 1), + SOC_DAPM_SINGLE_TLV("Aux Volume", WM8940_ADCBOOST, + 0, 7, 0, wm8940_boost_vol_tlv), + SOC_DAPM_SINGLE_TLV("Mic Volume", WM8940_ADCBOOST, + 4, 7, 0, wm8940_boost_vol_tlv), +}; + +static const struct snd_kcontrol_new wm8940_micpga_controls[] = { + SOC_DAPM_SINGLE("AUX Switch", WM8940_INPUTCTL, 2, 1, 0), + SOC_DAPM_SINGLE("MICP Switch", WM8940_INPUTCTL, 0, 1, 0), + SOC_DAPM_SINGLE("MICN Switch", WM8940_INPUTCTL, 1, 1, 0), +}; + +static const struct snd_soc_dapm_widget wm8940_dapm_widgets[] = { + + SND_SOC_DAPM_MIXER("Speaker Mixer", WM8940_POWER3, 2, 0, + &wm8940_speaker_mixer_controls[0], + ARRAY_SIZE(wm8940_speaker_mixer_controls)), + SND_SOC_DAPM_MIXER("Mono Mixer", WM8940_POWER3, 3, 0, + &wm8940_mono_mixer_controls[0], + ARRAY_SIZE(wm8940_mono_mixer_controls)), + SND_SOC_DAPM_DAC("DAC", "HiFi Playback", WM8940_POWER3, 0, 0), + + SND_SOC_DAPM_PGA("SpkN Out", WM8940_POWER3, 5, 0, NULL, 0), + SND_SOC_DAPM_PGA("SpkP Out", WM8940_POWER3, 6, 0, NULL, 0), + SND_SOC_DAPM_PGA("Mono Out", WM8940_POWER3, 7, 0, NULL, 0), + SND_SOC_DAPM_OUTPUT("MONOOUT"), + SND_SOC_DAPM_OUTPUT("SPKOUTP"), + SND_SOC_DAPM_OUTPUT("SPKOUTN"), + + SND_SOC_DAPM_PGA("Aux Input", WM8940_POWER1, 6, 0, NULL, 0), + SND_SOC_DAPM_ADC("ADC", "HiFi Capture", WM8940_POWER2, 0, 0), + SND_SOC_DAPM_MIXER("Mic PGA", WM8940_POWER2, 2, 0, + &wm8940_micpga_controls[0], + ARRAY_SIZE(wm8940_micpga_controls)), + SND_SOC_DAPM_MIXER("Boost Mixer", WM8940_POWER2, 4, 0, + &wm8940_input_boost_controls[0], + ARRAY_SIZE(wm8940_input_boost_controls)), + SND_SOC_DAPM_MICBIAS("Mic Bias", WM8940_POWER1, 4, 0), + + SND_SOC_DAPM_INPUT("MICN"), + SND_SOC_DAPM_INPUT("MICP"), + SND_SOC_DAPM_INPUT("AUX"), +}; + +static const struct snd_soc_dapm_route audio_map[] = { + /* Mono output mixer */ + {"Mono Mixer", "PCM Playback Switch", "DAC"}, + {"Mono Mixer", "Aux Playback Switch", "Aux Input"}, + {"Mono Mixer", "Line Bypass Switch", "Boost Mixer"}, + + /* Speaker output mixer */ + {"Speaker Mixer", "PCM Playback Switch", "DAC"}, + {"Speaker Mixer", "Aux Playback Switch", "Aux Input"}, + {"Speaker Mixer", "Line Bypass Switch", "Boost Mixer"}, + + /* Outputs */ + {"Mono Out", NULL, "Mono Mixer"}, + {"MONOOUT", NULL, "Mono Out"}, + {"SpkN Out", NULL, "Speaker Mixer"}, + {"SpkP Out", NULL, "Speaker Mixer"}, + {"SPKOUTN", NULL, "SpkN Out"}, + {"SPKOUTP", NULL, "SpkP Out"}, + + /* Microphone PGA */ + {"Mic PGA", "MICN Switch", "MICN"}, + {"Mic PGA", "MICP Switch", "MICP"}, + {"Mic PGA", "AUX Switch", "AUX"}, + + /* Boost Mixer */ + {"Boost Mixer", "Mic PGA Switch", "Mic PGA"}, + {"Boost Mixer", "Mic Volume", "MICP"}, + {"Boost Mixer", "Aux Volume", "Aux Input"}, + + {"ADC", NULL, "Boost Mixer"}, +}; + +static int wm8940_add_widgets(struct snd_soc_codec *codec) +{ + snd_soc_dapm_new_controls(codec, wm8940_dapm_widgets, + ARRAY_SIZE(wm8940_dapm_widgets)); + snd_soc_dapm_add_routes(codec, audio_map, ARRAY_SIZE(audio_map)); + snd_soc_dapm_new_widgets(codec); +} +static int wm8940_add_controls(struct snd_soc_codec *codec) +{ + int err, i; + + for (i = 0; i < ARRAY_SIZE(wm8940_snd_controls); i++) { + err = snd_ctl_add(codec->card, + snd_soc_cnew(&wm8940_snd_controls[i], codec, + NULL)); + if (err < 0) + return err; + } + + return 0; +} + +static int wm8940_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->codec; + struct wm8940_priv *wm8940 = codec->private_data; + + /* clear fmt and wl */ + u16 iface = wm8940_read_reg_cache(codec, WM8940_IFACE) & 0xFF07; + u16 addcntrl = wm8940_read_reg_cache(codec, WM8940_ADDCNTRL) & 0xFFF1; + + switch (params_rate(params)) { + case SNDRV_PCM_RATE_8000: + addcntrl |= (0x5 << 1); + break; + case SNDRV_PCM_RATE_11025: + addcntrl |= (0x4 << 1); + break; + case SNDRV_PCM_RATE_16000: + addcntrl |= (0x3 << 1); + break; + case SNDRV_PCM_RATE_22050: + addcntrl |= (0x2 << 1); + break; + case SNDRV_PCM_RATE_32000: + addcntrl |= (0x1 << 1); + break; + case SNDRV_PCM_RATE_44100: + case SNDRV_PCM_RATE_48000: + break; + } + wm8940_write(codec, WM8940_ADDCNTRL, addcntrl); + + switch (params_format(params)) { + case SNDRV_PCM_FORMAT_S16_LE: + break; + case SNDRV_PCM_FORMAT_S20_3LE: + iface |= (1 << 5); + break; + case SNDRV_PCM_FORMAT_S24_LE: + iface |= (2 << 5); + break; + case SNDRV_PCM_FORMAT_S32_LE: + iface |= (3 << 5); + break; + } + + switch (wm8940->mode) { + case SND_SOC_DAIFMT_I2S: + iface |= (2 << 3); + break; + case SND_SOC_DAIFMT_LEFT_J: + iface |= (1 << 3); + break; + case SND_SOC_DAIFMT_RIGHT_J: + break; + case SND_SOC_DAIFMT_DSP_A: + iface |= (3 << 3); + break; + case SND_SOC_DAIFMT_DSP_B: + iface |= (3 << 3) | (1 << 7); + break; + } + + switch (wm8940->clk_inversion) { + case SND_SOC_DAIFMT_NB_NF: + break; + case SND_SOC_DAIFMT_NB_IF: + iface |= (1 << 7); + break; + case SND_SOC_DAIFMT_IB_NF: + iface |= (1 << 8); + break; + case SND_SOC_DAIFMT_IB_IF: + iface |= (1 << 8) | (1 << 7); + break; + } + wm8940_write(codec, WM8940_IFACE, iface); + + return 0; +} + +static int wm8940_mute(struct snd_soc_dai *dai, int mute) +{ + struct snd_soc_codec *codec = dai->codec; + u16 mute_reg = wm8940_read_reg_cache(codec, WM8940_DAC) & 0xffbf; + + return wm8940_write(codec, + WM8940_DAC, + mute ? mute_reg | 0x40 : mute_reg); + +} + +static int wm8940_set_dai_fmt(struct snd_soc_dai *codec_dai, + unsigned int fmt) +{ + struct snd_soc_codec *codec = codec_dai->codec; + struct wm8940_priv *wm8940 = codec->private_data; + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + case SND_SOC_DAIFMT_LEFT_J: + case SND_SOC_DAIFMT_RIGHT_J: + case SND_SOC_DAIFMT_DSP_A: + case SND_SOC_DAIFMT_DSP_B: + wm8940->mode = fmt & SND_SOC_DAIFMT_FORMAT_MASK; + break; + } + wm8940->clk_inversion = fmt & SND_SOC_DAIFMT_INV_MASK; + + return 0; +} + +static int wm8940_set_dai_clkdiv(struct snd_soc_dai *codec_dai, + int div_id, int div) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u16 reg; + + switch (div_id) { + case WM8940_BCLKDIV: + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK) & 0xFFEF3; + wm8940_write(codec, WM8940_CLOCK, reg | (div << 2)); + break; + case WM8940_MCLKDIV: + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK) & 0xFF1F; + wm8940_write(codec, WM8940_CLOCK, reg | (div << 5)); + break; + case WM8940_OPCLKDIV: + reg = wm8940_read_reg_cache(codec, WM8940_ADDCNTRL) & 0xFFCF; + wm8940_write(codec, WM8940_ADDCNTRL, reg | (div << 4)); + break; + } + return 0; +} + +struct pll_ { + unsigned int pre_scale:2; + unsigned int n:4; + unsigned int k; +}; + +static struct pll_ pll_div; + +/* The size in bits of the pll divide multiplied by 10 + * to allow rounding later */ +#define FIXED_PLL_SIZE ((1 << 24) * 10) +/* This unfortunately is rather different from the wm8510 */ +static void pll_factors(unsigned int target, unsigned int source) +{ + unsigned long long Kpart; + unsigned int K, Ndiv, Nmod; + + Ndiv = target / source; + /* Ndiv would be greater than 12, so add a pre multiply by 2*/ + if (Ndiv > 12) { + /* FIXME: This will loose accuracy, how to deal? */ + printk(KERN_WARNING "Incorrectly handled case\n"); + source <<= 1; + pll_div.pre_scale = 0; + Ndiv = target / source; + } else if (Ndiv < 3) { + source >>= 2; + pll_div.pre_scale = 3; + Ndiv = target / source; + } else if (Ndiv < 6) { + source >>= 1; + pll_div.pre_scale = 2; + } else + pll_div.pre_scale = 1; + + if ((Ndiv < 6) || (Ndiv > 12)) + printk(KERN_WARNING + "WM8510 N value %d outwith recommended range!d\n", + Ndiv); + + pll_div.n = Ndiv; + Nmod = target % source; + Kpart = FIXED_PLL_SIZE * (long long)Nmod; + + do_div(Kpart, source); + + K = Kpart & 0xFFFFFFFF; + + /* Check if we need to round */ + if ((K % 10) >= 5) + K += 5; + + /* Move down to proper range now rounding is done */ + K /= 10; + + pll_div.k = K; +} + +/* Untested at the moment */ +static int wm8940_set_dai_pll(struct snd_soc_dai *codec_dai, + int pll_id, unsigned int freq_in, unsigned int freq_out) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u16 reg; + if (freq_in == 0 || freq_out == 0) { + /* Clock CODEC directly from MCLK */ + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK); + wm8940_write(codec, WM8940_CLOCK, reg & 0x0ff); + + /* Turn off PLL */ + reg = wm8940_read_reg_cache(codec, WM8940_POWER1); + wm8940_write(codec, WM8940_POWER1, reg & 0x1df); + + /* It is also possible to turn it off completely - TODO */ + return 0; + } + + pll_factors(freq_out*8, freq_in); + + wm8940_write(codec, WM8940_PLLN, (pll_div.pre_scale << 4) | pll_div.n); + wm8940_write(codec, WM8940_PLLK1, pll_div.k >> 18); + wm8940_write(codec, WM8940_PLLK2, (pll_div.k >> 9) & 0x1ff); + wm8940_write(codec, WM8940_PLLK3, pll_div.k & 0x1ff); + /* Enable the PLL */ + reg = wm8940_read_reg_cache(codec, WM8940_POWER1); + wm8940_write(codec, WM8940_POWER1, reg | 0x020); + + /* Run CODEC from PLL instead of MCLK */ + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK); + wm8940_write(codec, WM8940_CLOCK, reg | 0x100); + + return 0; +} + +static int wm8940_set_bias_level(struct snd_soc_codec *codec, + enum snd_soc_bias_level level) +{ + u16 val; + u16 pwr_reg = wm8940_read_reg_cache(codec, WM8940_POWER1) & 0x1F0; + + switch (level) { + case SND_SOC_BIAS_ON: + /* ensure bufioen and biasen */ + pwr_reg |= (1 << 2) | (1 << 3); + /* Enable thermal shutdown */ + val = wm8940_read_reg_cache(codec, WM8940_OUTPUTCTL); + wm8940_write(codec, WM8940_OUTPUTCTL, val | 0x2); + /* set vmid to 75k and unmute dac */ + wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x1); + break; + case SND_SOC_BIAS_PREPARE: + /* ensure bufioen and biasen */ + pwr_reg |= (1 << 2) | (1 << 3); + /* set vmid to 2.5k for fast start */ + wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x3); + break; + case SND_SOC_BIAS_STANDBY: + /* ensure bufioen and biasen */ + pwr_reg |= (1 << 2) | (1 << 3); + /* set vmid to 300k for standby */ + wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x2); + break; + case SND_SOC_BIAS_OFF: + wm8940_write(codec, WM8940_POWER1, pwr_reg); + break; + } + + return 0; +} + +static int wm8940_set_dai_sysclk(struct snd_soc_dai *codec_dai, + int clk_id, unsigned int freq, int dir) +{ + struct snd_soc_codec *codec = codec_dai->codec; + struct wm8940_priv *wm8940 = codec->private_data; + + wm8940_write(codec, WM8940_CLOCK, 0); + switch (freq) { + case 11289600: + case 12000000: + case 12288000: + case 16934400: + case 18432000: + wm8940->sysclk = freq; + return 0; + } + return -EINVAL; +} + +struct snd_soc_dai wm8940_dai = { + .name = "WM8940", + .playback = { + .stream_name = "Playback", + .channels_min = 1, + .channels_max = 2, /* lie - pxa-i2s has min of 2*/ + .rates = WM8940_RATES, + .formats = WM8940_FORMATS, + }, + .capture = { + .stream_name = "Capture", + .channels_min = 1, + .channels_max = 2, /* lie */ + .rates = WM8940_RATES, + .formats = WM8940_FORMATS, + }, + .ops = { + .hw_params = wm8940_i2s_hw_params, + .set_sysclk = wm8940_set_dai_sysclk, + .digital_mute = wm8940_mute, + .set_fmt = wm8940_set_dai_fmt, + .set_clkdiv = wm8940_set_dai_clkdiv, + .set_pll = wm8940_set_dai_pll, + }, +}; +EXPORT_SYMBOL_GPL(wm8940_dai); + +static int wm8940_suspend(struct platform_device *pdev, pm_message_t state) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + wm8940_set_bias_level(codec, SND_SOC_BIAS_OFF); + return 0; +} + +static int wm8940_resume(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + int i; + u16 *cache = codec->reg_cache; + + /* Sync reg_cache with the hardware + * Could use auto incremented writes to speed this up + */ + for (i = 0; i < ARRAY_SIZE(wm8940_reg_defaults); i++) + wm8940_write(codec, i, cache[i]); + wm8940_set_bias_level(codec, SND_SOC_BIAS_STANDBY); + wm8940_set_bias_level(codec, codec->suspend_bias_level); + + return 0; +} + +#define wm8940_reset(c) wm8940_write(c, WM8940_SOFTRESET, 0); + +static int wm8940_init(struct snd_soc_device *socdev) +{ + struct snd_soc_codec *codec = socdev->codec; + int ret = 0; + + codec->name = "WM8940"; + codec->owner = THIS_MODULE; + codec->read = wm8940_read_reg_cache; + codec->write = wm8940_write; + codec->set_bias_level = wm8940_set_bias_level; + codec->dai = &wm8940_dai; + codec->num_dai = 1; + codec->reg_cache_size = ARRAY_SIZE(wm8940_reg_defaults); + codec->reg_cache = kmemdup(wm8940_reg_defaults, + sizeof wm8940_reg_defaults, + GFP_KERNEL); + + if (codec->reg_cache == NULL) { + ret = -ENOMEM; + goto error_free_cache; + } + ret = wm8940_reset(codec); + if (ret) + goto error_free_cache; + + ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); + if (ret < 0) + goto error_free_cache; + + codec->bias_level = SND_SOC_BIAS_OFF; + wm8940_set_bias_level(codec, SND_SOC_BIAS_STANDBY); + wm8940_add_controls(codec); + wm8940_add_widgets(codec); + + /* Enables the level shifters and non-vmid derived bias + * current generator + */ + ret = wm8940_write(codec, WM8940_POWER1, 0x180); + if (ret < 0) + goto card_err; + + ret = snd_soc_init_card(socdev); + if (ret < 0) { + printk(KERN_ERR "wm8940: failed to register card\n"); + goto card_err; + } + + return ret; + +card_err: + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +error_free_cache: + kfree(codec->reg_cache); + return ret; +} + +static struct snd_soc_device *wm8940_socdev; + +#if defined(CONFIG_I2C) || defined(CONFIG_I2C_MODULE) +static int wm8940_i2c_probe(struct i2c_client *i2c, + const struct i2c_device_id *id) +{ + struct snd_soc_device *socdev = wm8940_socdev; + struct snd_soc_codec *codec = socdev->codec; + int ret; + + i2c_set_clientdata(i2c, codec); + codec->control_data = i2c; + ret = wm8940_init(socdev); + if (ret < 0) + pr_err("Failed to initialise WM8940\n"); + + return ret; +} +static int wm8940_i2c_remove(struct i2c_client *client) +{ + return 0; +} + +static const struct i2c_device_id wm8940_i2c_id[] = { + { "wm8940", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, wm8940_i2c_id); + +static struct i2c_driver wm8940_i2c_driver = { + .driver = { + .name = "WM8940 I2C Codec", + .owner = THIS_MODULE, + }, + .probe = wm8940_i2c_probe, + .remove = wm8940_i2c_remove, + .id_table = wm8940_i2c_id, +}; + +static int wm8940_add_i2c_device(struct platform_device *pdev, + const struct wm8940_setup_data *setup) +{ + struct i2c_board_info info = { + .type = "wm8940", + .addr = setup->i2c_address, + }; + struct i2c_adapter *adapter; + struct i2c_client *client; + int ret; + + ret = i2c_add_driver(&wm8940_i2c_driver); + if (ret != 0) { + dev_err(&pdev->dev, "can't add i2c driver\n"); + return ret; + } + + adapter = i2c_get_adapter(setup->i2c_bus); + if (!adapter) { + dev_err(&pdev->dev, "can't get i2c adapter %d\n", + setup->i2c_bus); + goto err_driver; + } + + client = i2c_new_device(adapter, &info); + i2c_put_adapter(adapter); + if (!client) { + dev_err(&pdev->dev, "can't add i2c device at 0x%x\n", + (unsigned int)info.addr); + goto err_driver; + } + + return 0; + +err_driver: + i2c_del_driver(&wm8940_i2c_driver); + return -ENODEV; +} + +#endif + +static int wm8940_probe(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct wm8940_setup_data *setup; + struct snd_soc_codec *codec; + struct wm8940_priv *wm8940; + int ret = 0; + + setup = socdev->codec_data; + codec = kzalloc(sizeof *codec, GFP_KERNEL); + if (codec == NULL) + return -ENOMEM; + + wm8940 = kzalloc(sizeof(*wm8940), GFP_KERNEL); + if (wm8940 == NULL) + return -ENOMEM; + codec->private_data = wm8940; + socdev->codec = codec; + mutex_init(&codec->mutex); + INIT_LIST_HEAD(&codec->dapm_widgets); + INIT_LIST_HEAD(&codec->dapm_paths); + wm8940_socdev = socdev; + +#if defined(CONFIG_I2C) || defined(CONFIG_I2C_MODULE) + if (setup->i2c_address) { + codec->hw_write = (hw_write_t)i2c_master_send; + ret = wm8940_add_i2c_device(pdev, setup); + } +#endif + if (ret != 0) + kfree(codec); + return ret; +} + +static int wm8940_remove(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->codec; + + if (codec->control_data) + wm8940_set_bias_level(codec, SND_SOC_BIAS_OFF); + + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +#if defined(CONFIG_I2C) || defined(CONFIG_I2C_MODULE) + i2c_unregister_device(codec->control_data); + i2c_del_driver(&wm8940_i2c_driver); +#endif + kfree(codec); + + return 0; +} + +struct snd_soc_codec_device soc_codec_dev_wm8940 = { + .probe = wm8940_probe, + .remove = wm8940_remove, + .suspend = wm8940_suspend, + .resume = wm8940_resume, +}; +EXPORT_SYMBOL_GPL(soc_codec_dev_wm8940); + +static int __init wm8940_modinit(void) +{ + return snd_soc_register_dai(&wm8940_dai); +} +module_init(wm8940_modinit); + +static void __exit wm8940_exit(void) +{ + snd_soc_unregister_dai(&wm8940_dai); +} +module_exit(wm8940_exit); + +MODULE_DESCRIPTION("ASoC WM8940 driver"); +MODULE_AUTHOR("Jonathan Cameron"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wm8940.h b/sound/soc/codecs/wm8940.h new file mode 100644 index 0000000..ab1221c --- /dev/null +++ b/sound/soc/codecs/wm8940.h @@ -0,0 +1,102 @@ +/* + * wm8940.h -- WM8940 Soc Audio driver + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#ifndef _WM8940_H +#define _WM8940_H + +struct wm8940_setup_data { + /* The one I have isn't spi wired, so can't test */ + int i2c_bus; + unsigned short i2c_address; +}; +extern struct snd_soc_dai wm8940_dai; +extern struct snd_soc_codec_device soc_codec_dev_wm8940; + +/* WM8940 register space */ +#define WM8940_SOFTRESET 0x00 +#define WM8940_POWER1 0x01 +#define WM8940_POWER2 0x02 +#define WM8940_POWER3 0x03 +#define WM8940_IFACE 0x04 +#define WM8940_COMPANDINGCTL 0x05 +#define WM8940_CLOCK 0x06 +#define WM8940_ADDCNTRL 0x07 +#define WM8940_GPIO 0x08 +#define WM8940_CTLINT 0x09 +#define WM8940_DAC 0x0A +#define WM8940_DACVOL 0x0B + +#define WM8940_ADC 0x0E +#define WM8940_ADCVOL 0x0F +#define WM8940_NOTCH1 0x10 +#define WM8940_NOTCH2 0x11 +#define WM8940_NOTCH3 0x12 +#define WM8940_NOTCH4 0x13 +#define WM8940_NOTCH5 0x14 +#define WM8940_NOTCH6 0x15 +#define WM8940_NOTCH7 0x16 +#define WM8940_NOTCH8 0x17 +#define WM8940_DACLIM1 0x18 +#define WM8940_DACLIM2 0x19 + +#define WM8940_ALC1 0x20 +#define WM8940_ALC2 0x21 +#define WM8940_ALC3 0x22 +#define WM8940_NOISEGATE 0x23 +#define WM8940_PLLN 0x24 +#define WM8940_PLLK1 0x25 +#define WM8940_PLLK2 0x26 +#define WM8940_PLLK3 0x27 + +#define WM8940_ALC4 0x2A + +#define WM8940_INPUTCTL 0x2C +#define WM8940_PGAGAIN 0x2D + +#define WM8940_ADCBOOST 0x2F + +#define WM8940_OUTPUTCTL 0x31 +#define WM8940_SPKMIX 0x32 + +#define WM8940_SPKVOL 0x36 + +#define WM8940_MONOMIX 0x38 + + + +/* Clock divider Id's */ +#define WM8940_BCLKDIV 0 +#define WM8940_MCLKDIV 1 +#define WM8940_OPCLKDIV 2 + +/* MCLK clock dividers */ +#define WM8940_MCLKDIV_1 0 +#define WM8940_MCLKDIV_1_5 1 +#define WM8940_MCLKDIV_2 2 +#define WM8940_MCLKDIV_3 3 +#define WM8940_MCLKDIV_4 4 +#define WM8940_MCLKDIV_6 5 +#define WM8940_MCLKDIV_8 6 +#define WM8940_MCLKDIV_12 7 + +/* BCLK clock dividers */ +#define WM8940_BCLKDIV_1 0 +#define WM8940_BCLKDIV_2 1 +#define WM8940_BCLKDIV_4 2 +#define WM8940_BCLKDIV_8 3 +#define WM8940_BCLKDIV_16 4 +#define WM8940_BCLKDIV_32 5 + +/* PLL Out Dividers */ +#define WM8940_OPCLKDIV_1 0 +#define WM8940_OPCLKDIV_2 1 +#define WM8940_OPCLKDIV_3 2 +#define WM8940_OPCLKDIV_4 3 + +#endif /* _WM8940_H */ +
On Fri, Apr 24, 2009 at 07:29:20PM +0000, Jonathan Cameron wrote:
Initial support for the WM8940 Mono Codec with speaker driver
Overall this looks very good but there are a few things it'd be good to correct here. A lot of this is due to copying from older drivers or the fact that you're working against an older kernel.
- VROI (resistance control for unused outputs.
This will be system dependant, the machine driver should configure this in the DAI init() function or you could let it be set by platform data.
+static u16 wm8940_reg_defaults[] = {
- [WM8940_SOFTRESET] = 0x8940,
- [WM8940_POWER1] = 0x0000,
I'd really rather use the more standard ASoC style here with a simple table of values. The driver relies on the fact that the register cache is fully specified for suspend and resume and having a straight table makes sure that there aren't any missing values in the cache.
+#define WM8940_RATES (SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_11025 | \
SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_22050 | \
SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000)
SNDRV_PCM_RATE_8000_48000
+static inline unsigned int wm8940_read_reg_cache(struct snd_soc_codec *codec,
unsigned int reg)
+{
- u16 *cache = codec->reg_cache;
- if (reg > ARRAY_SIZE(wm8940_reg_defaults))
return -1;
Should be >=
+static inline int wm8940_write_reg_cache(struct snd_soc_codec *codec,
u16 reg, unsigned int value)
+{
- u16 *cache = codec->reg_cache;
- if (reg > ARRAY_SIZE(wm8940_reg_defaults))
return -1;
Should be >=
- wm8940_write_reg_cache(codec, reg, value);
- return (codec->hw_write(codec->control_data, data, 3) == 3) ? 0 : -EIO;
Don't use the ternery operation here: apart from the legibility issues if an error code is returned by hw_write() (as opposed to a short value) you should pass it on.
+static const struct soc_enum wm8940_enum[] = {
- SOC_ENUM_SINGLE(WM8940_COMPANDINGCTL, 1, 4, wm8940_companding),
- SOC_ENUM_SINGLE(WM8940_COMPANDINGCTL, 3, 4, wm8940_companding),
- SOC_ENUM_SINGLE(WM8940_ALC3, 8, 2, wm8940_alc_mode_text),
- SOC_ENUM_SINGLE(WM8940_INPUTCTL, 8, 2, wm8940_mic_bias_levels_text),
- SOC_ENUM_SINGLE(WM8940_ADC, 7, 2, wm8940_filter_mode_text),
+};
Please declare individual variables for these - it saves trying to look up indexes in the array. This is done by some older drivers mostly as a result of conversion from pre-ASoC drivers where the array was useful.
+static const struct snd_kcontrol_new wm8940_snd_controls[] = {
- SOC_SINGLE("Digital Loopback Switch dac to adc", WM8940_COMPANDINGCTL,
6, 1, 0),
Switch should always be the last component of the name (ALSA userspace cares, see Documentation/sound/alsa/ControlNames.txt) and DAC and ADC should be capitalised. I'd just call it Digital Loopback Switch.
SOC_SINGLE("Companding 8 bit", WM8940_COMPANDINGCTL, 5, 1, 0),
This should be controlled as part of the DAI format control rather than exposed to the user.
- SOC_SINGLE("Digital Loopback Switch adc to dac", WM8940_COMPANDINGCTL,
0, 1, 0),
This should be called Digital Sidetone Switch and probably ought to be a DAPM control - there's an ADC to DAC route.
- SOC_SINGLE("ALC Enable Switch", WM8940_ALC1, 8, 1, 0),
ALC Switch.
- SOC_SINGLE("Capture Boost(+20dB)", WM8940_ADCBOOST, 8, 1, 0),
You could also do this as Capture Boost Volume and provide TLV information but it's not terribly important either way.
- SOC_SINGLE_TLV("Speaker Playback Volume", WM8940_SPKVOL,
0, 63, 0, wm8940_spk_vol_tlv),
- SOC_SINGLE("Speaker Playback Mute", WM8940_SPKVOL, 6, 1, 0),
Speaker Playback Switch; this also means that the sense of the control needs to be inverted. Look at the control in alsamixer and you'll see that it figures out that these both control the same thing and displays them as a single Speaker Playback control in the UI.
- SOC_SINGLE_TLV("Speaker Playback Attenuation", WM8940_SPKVOL,
8, 1, 1, wm8940_att_tlv),
This should be Speaker Mixer Line Bypass Volume; it only applies to the bypass path and volume controls always need a Volume at the end of the name.
- SOC_SINGLE("Speaker Playback ZC", WM8940_SPKVOL, 7, 1, 0),
ZC Switch.
- SOC_SINGLE("Mono Out Mute", WM8940_MONOMIX, 6, 1, 0),
Mono Out Switch.
- SOC_SINGLE_TLV("Mono Out Attenuation", WM8940_MONOMIX,
7, 1, 1, wm8940_att_tlv),
Again, needs Volume at the end and should be adjusted to match the name of the mixer control that's generated.
- SOC_SINGLE("ZC Timeout Clock Enable Switch", WM8940_ADDCNTRL, 0, 1, 0),
Doesn't need the Enable in there.
- SOC_SINGLE("Notch Filter 1 Update Switch", WM8940_NOTCH1, 15, 1, 0),
- SOC_SINGLE("Notch Filter 2 Update Switch", WM8940_NOTCH3, 15, 1, 0),
- SOC_SINGLE("Notch Filter 3 Update Switch", WM8940_NOTCH5, 15, 1, 0),
- SOC_SINGLE("Notch Filter 4 Update Switch", WM8940_NOTCH7, 15, 1, 0),
The notch filter configuration is dependant on the sample rate so the driver probably ought to be exposing a control based on the cutoff frequency desired and then configuring the filter appropriately as the sample rate changes.
I'd remove the notch filter stuff for now if you're not actively using it.
- /* Boost Mixer */
- {"Boost Mixer", "Mic PGA Switch", "Mic PGA"},
- {"Boost Mixer", "Mic Volume", "MICP"},
- {"Boost Mixer", "Aux Volume", "Aux Input"},
{"ADC", NULL, "Boost Mixer"},
Something odd with the indentation here?
+static int wm8940_add_widgets(struct snd_soc_codec *codec) +{
- snd_soc_dapm_new_controls(codec, wm8940_dapm_widgets,
ARRAY_SIZE(wm8940_dapm_widgets));
- snd_soc_dapm_add_routes(codec, audio_map, ARRAY_SIZE(audio_map));
- snd_soc_dapm_new_widgets(codec);
+} +static int wm8940_add_controls(struct snd_soc_codec *codec)
Blank line between the two functions please.
+{
- int err, i;
- for (i = 0; i < ARRAY_SIZE(wm8940_snd_controls); i++) {
err = snd_ctl_add(codec->card,
snd_soc_cnew(&wm8940_snd_controls[i], codec,
NULL));
if (err < 0)
return err;
- }
snd_soc_add_controls().
+static int wm8940_i2s_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
...
- switch (wm8940->mode) {
- switch (wm8940->clk_inversion) {
Just set these in the register when the DAI format is configured - no need to wait until hw_params().
Also, for the capture stream you should set LOUTR here if there are two channels being recorded so that the data is output on both channels.
+static int wm8940_mute(struct snd_soc_dai *dai, int mute) +{
- struct snd_soc_codec *codec = dai->codec;
- u16 mute_reg = wm8940_read_reg_cache(codec, WM8940_DAC) & 0xffbf;
- return wm8940_write(codec,
WM8940_DAC,
mute ? mute_reg | 0x40 : mute_reg);
I'm really not a fan of the ternary operator :/
- if (Ndiv > 12) {
/* FIXME: This will loose accuracy, how to deal? */
printk(KERN_WARNING "Incorrectly handled case\n");
Printing an error is fine; this all comes from the machine driver so it's not something we expect to ever happen at runtime except in development.
+static int wm8940_set_dai_pll(struct snd_soc_dai *codec_dai,
int pll_id, unsigned int freq_in, unsigned int freq_out)
+{
- struct snd_soc_codec *codec = codec_dai->codec;
- u16 reg;
- if (freq_in == 0 || freq_out == 0) {
/* Clock CODEC directly from MCLK */
reg = wm8940_read_reg_cache(codec, WM8940_CLOCK);
wm8940_write(codec, WM8940_CLOCK, reg & 0x0ff);
/* Turn off PLL */
reg = wm8940_read_reg_cache(codec, WM8940_POWER1);
wm8940_write(codec, WM8940_POWER1, reg & 0x1df);
This will need to be done when reconfiguring the PLL as well so it ought to be in the main path too.
/* set vmid to 75k and unmute dac */
wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x1);
The comment is out of sync with reality. Also...
- case SND_SOC_BIAS_PREPARE:
/* ensure bufioen and biasen */
pwr_reg |= (1 << 2) | (1 << 3);
/* set vmid to 2.5k for fast start */
wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x3);
break;
This isn't what the low resistance VMID setting is for;
+struct snd_soc_dai wm8940_dai = {
- .name = "WM8940",
- .playback = {
.stream_name = "Playback",
.channels_min = 1,
.channels_max = 2, /* lie - pxa-i2s has min of 2*/
What's going on here is that I2S is inhernantly stereo even if only one of the channels is actually being output - the CODEC is actually taking a stereo stream, it's just ignoring one of the channels.
.rates = WM8940_RATES,
.formats = WM8940_FORMATS,
- },
- .capture = {
.stream_name = "Capture",
.channels_min = 1,
.channels_max = 2, /* lie */
See above for setting LOUTR hw_params() - the CODEC is actually able to output data on both channels.
- .ops = {
.hw_params = wm8940_i2s_hw_params,
.set_sysclk = wm8940_set_dai_sysclk,
.digital_mute = wm8940_mute,
.set_fmt = wm8940_set_dai_fmt,
.set_clkdiv = wm8940_set_dai_clkdiv,
.set_pll = wm8940_set_dai_pll,
- },
This needs updating for current ASoC - see the topic/asoc branch of
git://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound-2.6
where this has been pulled out into a separate ops structure.
+}; +EXPORT_SYMBOL_GPL(wm8940_dai);
You should also set the symmetric_rates flag here since the CODEC has a single LRCLK and therefore can't run the DAC and ADC at different rates.
- /* Enables the level shifters and non-vmid derived bias
* current generator
*/
- ret = wm8940_write(codec, WM8940_POWER1, 0x180);
- if (ret < 0)
goto card_err;
set_bias_level() ought to be doing this.
+static int __init wm8940_modinit(void) +{
- return snd_soc_register_dai(&wm8940_dai);
+} +module_init(wm8940_modinit);
+static void __exit wm8940_exit(void) +{
- snd_soc_unregister_dai(&wm8940_dai);
+} +module_exit(wm8940_exit);
Please take a look at the way in which drivers such as wm8988 and wm8960 register themselves - they have been converted to use the standard device probing, registering the DAI once the I2C (or SPI) device has been probed based on normal I2C setup in the arch code.
Broadly speaking you should move everything in the startup path except the registration of the ALSA controls into the I2C device registration with the ASoC functions doing the ALSA bits once the rest of the sound card has registered with it. You can see some conversions if you look in git history; cut'n'pasting the probe code for a converted driver will often get you a long way.
Hi Mark,
Thanks for your comments. The ones on naming are particularly helpful as tracking down the standard choices can be tricky without reading lots of data sheets!
I've cut the vast majority of your comments out of here as I agreed with them (and wasn't much else to say!)
Initial support for the WM8940 Mono Codec with speaker driver
Overall this looks very good but there are a few things it'd be good to correct here. A lot of this is due to copying from older drivers or the fact that you're working against an older kernel.
Not that old, (2.6.29.1) but things do seem to moving fast in here.
- VROI (resistance control for unused outputs.
This will be system dependant, the machine driver should configure this in the DAI init() function or you could let it be set by platform data.
Will do.
+static u16 wm8940_reg_defaults[] = {
- [WM8940_SOFTRESET] = 0x8940,
- [WM8940_POWER1] = 0x0000,
I'd really rather use the more standard ASoC style here with a simple table of values. The driver relies on the fact that the register cache is fully specified for suspend and resume and having a straight table makes sure that there aren't any missing values in the cache.
Ok, though I'd place the alternate argument that it lead to rather less readable code than this sort of syntax where it readily apparent exactly what is in each register.
Big tables of 'magic' numbers are definitely not a good thing. If we argue it's up to the driver writer to fill those tables in right, then they have no inherent advantage over this syntax where it's also up to the writer to not get it wrong.
SOC_SINGLE("Companding 8 bit", WM8940_COMPANDINGCTL, 5, 1, 0),
This should be controlled as part of the DAI format control rather than exposed to the user.
Ah, I'd miss understood the data sheet on this one. Didn't realize it overrides the format.
- SOC_SINGLE("Digital Loopback Switch adc to dac", WM8940_COMPANDINGCTL,
0, 1, 0),
This should be called Digital Sidetone Switch and probably ought to be a DAPM control - there's an ADC to DAC route.
I'll take your word for it!
- SOC_SINGLE_TLV("Speaker Playback Volume", WM8940_SPKVOL,
0, 63, 0, wm8940_spk_vol_tlv),
- SOC_SINGLE("Speaker Playback Mute", WM8940_SPKVOL, 6, 1, 0),
Speaker Playback Switch; this also means that the sense of the control needs to be inverted. Look at the control in alsamixer and you'll see that it figures out that these both control the same thing and displays them as a single Speaker Playback control in the UI.
I wish, alsamixer isn't exactly playing ball with busy box for some reason I haven't chased down. Looking at this lot using amixer isn't much fun!
- SOC_SINGLE("Notch Filter 1 Update Switch", WM8940_NOTCH1, 15, 1, 0),
- SOC_SINGLE("Notch Filter 2 Update Switch", WM8940_NOTCH3, 15, 1, 0),
- SOC_SINGLE("Notch Filter 3 Update Switch", WM8940_NOTCH5, 15, 1, 0),
- SOC_SINGLE("Notch Filter 4 Update Switch", WM8940_NOTCH7, 15, 1, 0),
The notch filter configuration is dependant on the sample rate so the driver probably ought to be exposing a control based on the cutoff frequency desired and then configuring the filter appropriately as the sample rate changes.
I'd remove the notch filter stuff for now if you're not actively using it.
Will do though they are something I want to have a play with down the line. What you suggest makes sense but is going to be fiddly to get right.
e.
+{
- int err, i;
- for (i = 0; i < ARRAY_SIZE(wm8940_snd_controls); i++) {
err = snd_ctl_add(codec->card,
snd_soc_cnew(&wm8940_snd_controls[i], codec,
NULL));
if (err < 0)
return err;
- }
snd_soc_add_controls().
Ok, looks like I'm going to be moving to the topic branch you give below anyway so this'll be available. Might be a bit of a delay as I had a few regressions issues with the board under 2.6.30 last time I tried it.
+static int wm8940_mute(struct snd_soc_dai *dai, int mute) +{
- struct snd_soc_codec *codec = dai->codec;
- u16 mute_reg = wm8940_read_reg_cache(codec, WM8940_DAC) & 0xffbf;
- return wm8940_write(codec,
WM8940_DAC,
mute ? mute_reg | 0x40 : mute_reg);
I'm really not a fan of the ternary operator :/
Fair enough, it seems to be an acquired taste ;)
- if (Ndiv > 12) {
/* FIXME: This will loose accuracy, how to deal? */
printk(KERN_WARNING "Incorrectly handled case\n");
Printing an error is fine; this all comes from the machine driver so it's not something we expect to ever happen at runtime except in development.
Sure though in this case it's in there to observe that my code is wrong and I should fix it rather than machine driver has done something wrong ;)
I'll try and work out how to do it right before next posting! Need to lure the board side of things into a configuration where this is relevant.
- case SND_SOC_BIAS_PREPARE:
/* ensure bufioen and biasen */
pwr_reg |= (1 << 2) | (1 << 3);
/* set vmid to 2.5k for fast start */
wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x3);
break;
This isn't what the low resistance VMID setting is for;
I admit I didn't really understand what was going on here, so I think I cribbed it from another driver. What should the setting be? (and what is that setting for?)
+struct snd_soc_dai wm8940_dai = {
- .name = "WM8940",
- .playback = {
.stream_name = "Playback",
.channels_min = 1,
.channels_max = 2, /* lie - pxa-i2s has min of 2*/
What's going on here is that I2S is inhernantly stereo even if only one of the channels is actually being output - the CODEC is actually taking a stereo stream, it's just ignoring one of the channels.
Ah, fair enough.
- .ops = {
.hw_params = wm8940_i2s_hw_params,
.set_sysclk = wm8940_set_dai_sysclk,
.digital_mute = wm8940_mute,
.set_fmt = wm8940_set_dai_fmt,
.set_clkdiv = wm8940_set_dai_clkdiv,
.set_pll = wm8940_set_dai_pll,
- },
This needs updating for current ASoC - see the topic/asoc branch of
git://git.kernel.org/pub/scm/linux/kernel/git/tiwai/sound-2.6
where this has been pulled out into a separate ops structure.
+}; +EXPORT_SYMBOL_GPL(wm8940_dai);
You should also set the symmetric_rates flag here since the CODEC has a single LRCLK and therefore can't run the DAC and ADC at different rates.
- /* Enables the level shifters and non-vmid derived bias
* current generator
*/
- ret = wm8940_write(codec, WM8940_POWER1, 0x180);
- if (ret < 0)
goto card_err;
set_bias_level() ought to be doing this.
+static int __init wm8940_modinit(void) +{
- return snd_soc_register_dai(&wm8940_dai);
+} +module_init(wm8940_modinit);
+static void __exit wm8940_exit(void) +{
- snd_soc_unregister_dai(&wm8940_dai);
+} +module_exit(wm8940_exit);
Please take a look at the way in which drivers such as wm8988 and wm8960 register themselves - they have been converted to use the standard device probing, registering the DAI once the I2C (or SPI) device has been probed based on normal I2C setup in the arch code.
Broadly speaking you should move everything in the startup path except the registration of the ALSA controls into the I2C device registration with the ASoC functions doing the ALSA bits once the rest of the sound card has registered with it. You can see some conversions if you look in git history; cut'n'pasting the probe code for a converted driver will often get you a long way.
I'll have a go at the conversion. I've done a fair bit of i2c stuff before so glad to see you've moved over to this approach.
Thanks again for the comments,
Jonathan
On Sat, Apr 25, 2009 at 05:17:28PM +0000, Jonathan Cameron wrote:
+static u16 wm8940_reg_defaults[] = {
- [WM8940_SOFTRESET] = 0x8940,
- [WM8940_POWER1] = 0x0000,
I'd really rather use the more standard ASoC style here with a simple table of values. The driver relies on the fact that the register cache is fully specified for suspend and resume and having a straight table makes sure that there aren't any missing values in the cache.
Ok, though I'd place the alternate argument that it lead to rather less readable code than this sort of syntax where it readily apparent exactly what is in each register.
Sure - if you do this using an alternative syntax that'd be fine. For example, several of the other drivers use comments to provide the register names.
- SOC_SINGLE("Digital Loopback Switch adc to dac", WM8940_COMPANDINGCTL,
0, 1, 0),
This should be called Digital Sidetone Switch and probably ought to be a DAPM control - there's an ADC to DAC route.
I'll take your word for it!
If you switch the ADC to the DAC then you've got an audio path between them.
- SOC_SINGLE_TLV("Speaker Playback Volume", WM8940_SPKVOL,
0, 63, 0, wm8940_spk_vol_tlv),
- SOC_SINGLE("Speaker Playback Mute", WM8940_SPKVOL, 6, 1, 0),
Speaker Playback Switch; this also means that the sense of the control needs to be inverted. Look at the control in alsamixer and you'll see that it figures out that these both control the same thing and displays them as a single Speaker Playback control in the UI.
I wish, alsamixer isn't exactly playing ball with busy box for some reason I haven't chased down. Looking at this lot using amixer isn't much fun!
amixer should show you the same information - yo'll get a simple mixer control with both switch and volume capabilites.
I'll try and work out how to do it right before next posting! Need to lure the board side of things into a configuration where this is relevant.
If you've got a scope you can use that to generate clocks with broken audio.
- case SND_SOC_BIAS_PREPARE:
/* ensure bufioen and biasen */
pwr_reg |= (1 << 2) | (1 << 3);
/* set vmid to 2.5k for fast start */
wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x3);
break;
This isn't what the low resistance VMID setting is for;
I admit I didn't really understand what was going on here, so I think I cribbed it from another driver. What should the setting be? (and what is that setting for?)
Sorry, didn't finish writing that bit. You should be setting the bias for normal operation in prepare - the fast startup is only for use during initial bringup of VMID since the lower resistrance allows the capacitor used to charge more quickly.
The resistance of the VMID resistor string allows you to trade the accuracy with which the VMID reference voltage is held (and therefore audio performance) for power consumption. Since it can take time to charge the capacitors and bring VMID up to the required voltage it's desirable to maintian VMID while the system is active in order to allow rapid startup of an audio stream but doing so consumes power so the higher resistance value is provided in order to allow that consumption to be reduced. The savings involved are very small but can produce a noticable benefit in small, power sensitive devices like MP3 players.
Hi Mark,
+static u16 wm8940_reg_defaults[] = {
- [WM8940_SOFTRESET] = 0x8940,
- [WM8940_POWER1] = 0x0000,
I'd really rather use the more standard ASoC style here with a simple table of values. The driver relies on the fact that the register cache is fully specified for suspend and resume and having a straight table makes sure that there aren't any missing values in the cache.
Ok, though I'd place the alternate argument that it lead to rather less readable code than this sort of syntax where it readily apparent exactly what is in each register.
Sure - if you do this using an alternative syntax that'd be fine. For example, several of the other drivers use comments to provide the register names.
Yup, that's what I've moved over to.
- SOC_SINGLE("Digital Loopback Switch adc to dac", WM8940_COMPANDINGCTL,
0, 1, 0),
This should be called Digital Sidetone Switch and probably ought to be a DAPM control - there's an ADC to DAC route.
I'll take your word for it!
If you switch the ADC to the DAC then you've got an audio path between them.
Got that. It was the fact it was a Sidetone Switch that I was surprised by!
- SOC_SINGLE_TLV("Speaker Playback Volume", WM8940_SPKVOL,
0, 63, 0, wm8940_spk_vol_tlv),
- SOC_SINGLE("Speaker Playback Mute", WM8940_SPKVOL, 6, 1, 0),
Speaker Playback Switch; this also means that the sense of the control needs to be inverted. Look at the control in alsamixer and you'll see that it figures out that these both control the same thing and displays them as a single Speaker Playback control in the UI.
I wish, alsamixer isn't exactly playing ball with busy box for some reason I haven't chased down. Looking at this lot using amixer isn't much fun!
amixer should show you the same information - yo'll get a simple mixer control with both switch and volume capabilites.
It does indeed, but there are so many controls it is a pain to find the one you want making checking this lot rather time consuming.
I'll try and work out how to do it right before next posting! Need to lure the board side of things into a configuration where this is relevant.
If you've got a scope you can use that to generate clocks with broken audio.
Yup, fun and games to come.
- case SND_SOC_BIAS_PREPARE:
/* ensure bufioen and biasen */
pwr_reg |= (1 << 2) | (1 << 3);
/* set vmid to 2.5k for fast start */
wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x3);
break;
This isn't what the low resistance VMID setting is for;
I admit I didn't really understand what was going on here, so I think I cribbed it from another driver. What should the setting be? (and what is that setting for?)
Sorry, didn't finish writing that bit. You should be setting the bias for normal operation in prepare - the fast startup is only for use during initial bringup of VMID since the lower resistrance allows the capacitor used to charge more quickly.
The resistance of the VMID resistor string allows you to trade the accuracy with which the VMID reference voltage is held (and therefore audio performance) for power consumption. Since it can take time to charge the capacitors and bring VMID up to the required voltage it's desirable to maintian VMID while the system is active in order to allow rapid startup of an audio stream but doing so consumes power so the higher resistance value is provided in order to allow that consumption to be reduced. The savings involved are very small but can produce a noticable benefit in small, power sensitive devices like MP3 players.
Ah, that makes sense now.
Thanks
Jonathan
- SOC_SINGLE("Digital Loopback Switch adc to dac", WM8940_COMPANDINGCTL,
0, 1, 0),
This should be called Digital Sidetone Switch and probably ought to be a DAPM control - there's an ADC to DAC route.
I'll take your word for it!
If you switch the ADC to the DAC then you've got an audio path between them.
Hi Mark,
I'm having some trouble getting my head around how to actually specify the audio route for this Sidetone route.
As far as I can tell you can't specify the following,
{"DAC", "Digtal Sidetone Switch", "ADC"} As in snd_soc_dapm_add_route only mux, switch and mixer controls can take a control element.
I can find plenty of examples for cases where this path routes through a mixer or mux, but in this simple case (it's either connected or not and no volume controls are on that path) what do I do?
Nearest I can currently come up with is to insert a mux with only one option...
Thanks,
Jonathan
On Mon, Apr 27, 2009 at 11:40:43AM +0000, Jonathan Cameron wrote:
As in snd_soc_dapm_add_route only mux, switch and mixer controls can take a control element.
You can modify the core, or if you're not keen on doing that just drop this feature for now - it's relatively rarely used.
Nearest I can currently come up with is to insert a mux with only one option...
It'd need to be a mixer, at least. A mux with only one option could never be disabled.
Mark Brown wrote:
On Mon, Apr 27, 2009 at 11:40:43AM +0000, Jonathan Cameron wrote:
As in snd_soc_dapm_add_route only mux, switch and mixer controls can take a control element.
You can modify the core, or if you're not keen on doing that just drop this feature for now - it's relatively rarely used.
I'll go with dropping it for now. Too many other things to do and to be honest it's not a feature I care about. Definitely one for another day.
Thanks,
Jonathan
From: Jonathan Cameron jic23@cam.ac.uk
aSoC: Initial support for the Wolfson Micro WM8940 codec
Signed-off-by: Jonathan Cameron jic23@cam.ac.uk
--- This is based on the sound-2.6 git tree merged with Linus' current (needed to fix some non sound related regressions).
Thanks to Mark Brown for all his help.
Changes since first version:
Moved over to new registration methods. Numerous name changes. Lots of minor cleanups and more thorough error handling. Notch filtering and Digital Sidetone support removed for now.
As ever, all comments welcome!
Board patches will follow to here and arm-linux-devel if people are happy with this.
diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index 121d63f..1c19ad5 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -35,6 +35,7 @@ config SND_SOC_ALL_CODECS select SND_SOC_WM8753 if SND_SOC_I2C_AND_SPI select SND_SOC_WM8900 if I2C select SND_SOC_WM8903 if I2C + select SND_SOC_WM8940 if I2C select SND_SOC_WM8960 if I2C select SND_SOC_WM8971 if I2C select SND_SOC_WM8988 if SND_SOC_I2C_AND_SPI @@ -140,6 +141,9 @@ config SND_SOC_WM8900 config SND_SOC_WM8903 tristate
+config SND_SOC_WM8940 + tristate + config SND_SOC_WM8960 tristate
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 8116968..3d31b6b 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -23,6 +23,7 @@ snd-soc-wm8750-objs := wm8750.o snd-soc-wm8753-objs := wm8753.o snd-soc-wm8900-objs := wm8900.o snd-soc-wm8903-objs := wm8903.o +snd-soc-wm8940-objs := wm8940.o snd-soc-wm8960-objs := wm8960.o snd-soc-wm8971-objs := wm8971.o snd-soc-wm8988-objs := wm8988.o @@ -57,6 +58,7 @@ obj-$(CONFIG_SND_SOC_WM8753) += snd-soc-wm8753.o obj-$(CONFIG_SND_SOC_WM8900) += snd-soc-wm8900.o obj-$(CONFIG_SND_SOC_WM8903) += snd-soc-wm8903.o obj-$(CONFIG_SND_SOC_WM8971) += snd-soc-wm8971.o +obj-$(CONFIG_SND_SOC_WM8940) += snd-soc-wm8940.o obj-$(CONFIG_SND_SOC_WM8960) += snd-soc-wm8960.o obj-$(CONFIG_SND_SOC_WM8988) += snd-soc-wm8988.o obj-$(CONFIG_SND_SOC_WM8990) += snd-soc-wm8990.o diff --git a/sound/soc/codecs/wm8940.c b/sound/soc/codecs/wm8940.c new file mode 100644 index 0000000..26987dc --- /dev/null +++ b/sound/soc/codecs/wm8940.c @@ -0,0 +1,955 @@ +/* + * wm8940.c -- WM8940 ALSA Soc Audio driver + * + * Author: Jonathan Cameron jic23@cam.ac.uk + * + * Based on wm8510.c + * Copyright 2006 Wolfson Microelectronics PLC. + * Author: Liam Girdwood lrg@slimlogic.co.uk + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * Not currently handled: + * Notch filter control + * AUXMode (inverting vs mixer) + * No means to obtain current gain if alc enabled. + * No use made of gpio + * Fast VMID discharge for power down + * Soft Start + * DLR and ALR Swaps not enabled + * Digital Sidetone not supported + */ +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/i2c.h> +#include <linux/platform_device.h> +#include <linux/spi/spi.h> +#include <sound/core.h> +#include <sound/pcm.h> +#include <sound/pcm_params.h> +#include <sound/soc.h> +#include <sound/soc-dapm.h> +#include <sound/initval.h> +#include <sound/tlv.h> + +#include "wm8940.h" + +struct wm8940_priv { + unsigned int sysclk; + u16 reg_cache[WM8940_CACHEREGNUM]; + struct snd_soc_codec codec; +}; + +static u16 wm8940_reg_defaults[] = { + 0x8940, /* Soft Reset */ + 0x0000, /* Power 1 */ + 0x0000, /* Power 2 */ + 0x0000, /* Power 3 */ + 0x0010, /* Interface Control */ + 0x0000, /* Companding Control */ + 0x0140, /* Clock Control */ + 0x0000, /* Additional Controls */ + 0x0000, /* GPIO Control */ + 0x0002, /* Auto Increment Control */ + 0x0000, /* DAC Control */ + 0x00FF, /* DAC Volume */ + 0, + 0, + 0x0100, /* ADC Control */ + 0x00FF, /* ADC Volume */ + 0x0000, /* Notch Filter 1 Control 1 */ + 0x0000, /* Notch Filter 1 Control 2 */ + 0x0000, /* Notch Filter 2 Control 1 */ + 0x0000, /* Notch Filter 2 Control 2 */ + 0x0000, /* Notch Filter 3 Control 1 */ + 0x0000, /* Notch Filter 3 Control 2 */ + 0x0000, /* Notch Filter 4 Control 1 */ + 0x0000, /* Notch Filter 4 Control 2 */ + 0x0032, /* DAC Limit Control 1 */ + 0x0000, /* DAC Limit Control 2 */ + 0, + 0, + 0, + 0, + 0, + 0, + 0x0038, /* ALC Control 1 */ + 0x000B, /* ALC Control 2 */ + 0x0032, /* ALC Control 3 */ + 0x0000, /* Noise Gate */ + 0x0041, /* PLLN */ + 0x000C, /* PLLK1 */ + 0x0093, /* PLLK2 */ + 0x00E9, /* PLLK3 */ + 0, + 0, + 0x0030, /* ALC Control 4 */ + 0, + 0x0002, /* Input Control */ + 0x0050, /* PGA Gain */ + 0, + 0x0002, /* ADC Boost Control */ + 0, + 0x0002, /* Output Control */ + 0x0000, /* Speaker Mixer Control */ + 0, + 0, + 0, + 0x0079, /* Speaker Volume */ + 0, + 0x0000, /* Mono Mixer Control */ +}; + +static inline unsigned int wm8940_read_reg_cache(struct snd_soc_codec *codec, + unsigned int reg) +{ + u16 *cache = codec->reg_cache; + + if (reg >= ARRAY_SIZE(wm8940_reg_defaults)) + return -1; + + return cache[reg]; +} + +static inline int wm8940_write_reg_cache(struct snd_soc_codec *codec, + u16 reg, unsigned int value) +{ + u16 *cache = codec->reg_cache; + + if (reg >= ARRAY_SIZE(wm8940_reg_defaults)) + return -1; + + cache[reg] = value; + + return 0; +} + +static int wm8940_write(struct snd_soc_codec *codec, unsigned int reg, + unsigned int value) +{ + int ret; + u8 data[3] = { reg, + (value & 0xff00) >> 8, + (value & 0x00ff) + }; + + wm8940_write_reg_cache(codec, reg, value); + + ret = codec->hw_write(codec->control_data, data, 3); + + if (ret < 0) + return ret; + else if (ret != 3) + return -EIO; + return 0; +} + +static const char *wm8940_companding[] = { "Off", "NC", "u-law", "A-law" }; +static const struct soc_enum wm8940_adc_companding_enum += SOC_ENUM_SINGLE(WM8940_COMPANDINGCTL, 1, 4, wm8940_companding); +static const struct soc_enum wm8940_dac_companding_enum += SOC_ENUM_SINGLE(WM8940_COMPANDINGCTL, 3, 4, wm8940_companding); + +static const char *wm8940_alc_mode_text[] = {"ALC", "Limiter"}; +static const struct soc_enum wm8940_alc_mode_enum += SOC_ENUM_SINGLE(WM8940_ALC3, 8, 2, wm8940_alc_mode_text); + +static const char *wm8940_mic_bias_level_text[] = {"0.9", "0.65"}; +static const struct soc_enum wm8940_mic_bias_level_enum += SOC_ENUM_SINGLE(WM8940_INPUTCTL, 8, 2, wm8940_mic_bias_level_text); + +static const char *wm8940_filter_mode_text[] = {"Audio", "Application"}; +static const struct soc_enum wm8940_filter_mode_enum += SOC_ENUM_SINGLE(WM8940_ADC, 7, 2, wm8940_filter_mode_text); + +DECLARE_TLV_DB_SCALE(wm8940_spk_vol_tlv, -5700, 100, 1); +DECLARE_TLV_DB_SCALE(wm8940_att_tlv, -1000, 1000, 0); +DECLARE_TLV_DB_SCALE(wm8940_pga_vol_tlv, -1200, 75, 0); +DECLARE_TLV_DB_SCALE(wm8940_alc_min_tlv, -1200, 600, 0); +DECLARE_TLV_DB_SCALE(wm8940_alc_max_tlv, 675, 600, 0); +DECLARE_TLV_DB_SCALE(wm8940_alc_tar_tlv, -2250, 50, 0); +DECLARE_TLV_DB_SCALE(wm8940_lim_boost_tlv, 0, 100, 0); +DECLARE_TLV_DB_SCALE(wm8940_lim_thresh_tlv, -600, 100, 0); +DECLARE_TLV_DB_SCALE(wm8940_adc_tlv, -12750, 50, 1); +DECLARE_TLV_DB_SCALE(wm8940_capture_boost_vol_tlv, 0, 2000, 0); + +static const struct snd_kcontrol_new wm8940_snd_controls[] = { + SOC_SINGLE("Digital Loopback Switch", WM8940_COMPANDINGCTL, + 6, 1, 0), + SOC_ENUM("DAC Companding", wm8940_dac_companding_enum), + SOC_ENUM("ADC Companding", wm8940_adc_companding_enum), + + SOC_ENUM("ALC Mode", wm8940_alc_mode_enum), + SOC_SINGLE("ALC Switch", WM8940_ALC1, 8, 1, 0), + SOC_SINGLE_TLV("ALC Capture Max Gain", WM8940_ALC1, + 3, 7, 1, wm8940_alc_max_tlv), + SOC_SINGLE_TLV("ALC Capture Min Gain", WM8940_ALC1, + 0, 7, 0, wm8940_alc_min_tlv), + SOC_SINGLE_TLV("ALC Capture Target", WM8940_ALC2, + 0, 14, 0, wm8940_alc_tar_tlv), + SOC_SINGLE("ALC Capture Hold", WM8940_ALC2, 4, 10, 0), + SOC_SINGLE("ALC Capture Decay", WM8940_ALC3, 4, 10, 0), + SOC_SINGLE("ALC Capture Attach", WM8940_ALC3, 0, 10, 0), + SOC_SINGLE("ALC ZC Switch", WM8940_ALC4, 1, 1, 0), + SOC_SINGLE("ALC Capture Noise Gate Switch", WM8940_NOISEGATE, + 3, 1, 0), + SOC_SINGLE("ALC Capture Noise Gate Threshold", WM8940_NOISEGATE, + 0, 7, 0), + + SOC_SINGLE("DAC Playback Limiter Switch", WM8940_DACLIM1, 8, 1, 0), + SOC_SINGLE("DAC Playback Limiter Attack", WM8940_DACLIM1, 0, 9, 0), + SOC_SINGLE("DAC Playback Limiter Decay", WM8940_DACLIM1, 4, 11, 0), + SOC_SINGLE_TLV("DAC Playback Limiter Threshold", WM8940_DACLIM2, + 4, 9, 1, wm8940_lim_thresh_tlv), + SOC_SINGLE_TLV("DAC Playback Limiter Boost", WM8940_DACLIM2, + 0, 12, 0, wm8940_lim_boost_tlv), + + SOC_SINGLE("Capture PGA ZC Switch", WM8940_PGAGAIN, 7, 1, 0), + SOC_SINGLE_TLV("Capture PGA Volume", WM8940_PGAGAIN, + 0, 63, 0, wm8940_pga_vol_tlv), + SOC_SINGLE_TLV("Digital Playback Volume", WM8940_DACVOL, + 0, 255, 0, wm8940_adc_tlv), + SOC_SINGLE_TLV("Digital Capture Volume", WM8940_ADCVOL, + 0, 255, 0, wm8940_adc_tlv), + SOC_ENUM("Mic Bias Level", wm8940_mic_bias_level_enum), + SOC_SINGLE_TLV("Capture Boost Volue", WM8940_ADCBOOST, + 8, 1, 0, wm8940_capture_boost_vol_tlv), + SOC_SINGLE_TLV("Speaker Playback Volume", WM8940_SPKVOL, + 0, 63, 0, wm8940_spk_vol_tlv), + SOC_SINGLE("Speaker Playback Switch", WM8940_SPKVOL, 6, 1, 1), + + SOC_SINGLE_TLV("Speaker Mixer Line Bypass Volume", WM8940_SPKVOL, + 8, 1, 1, wm8940_att_tlv), + SOC_SINGLE("Speaker Playback ZC Switch", WM8940_SPKVOL, 7, 1, 0), + + SOC_SINGLE("Mono Out Switch", WM8940_MONOMIX, 6, 1, 1), + SOC_SINGLE_TLV("Mono Mixer Line Bypass Volume", WM8940_MONOMIX, + 7, 1, 1, wm8940_att_tlv), + + SOC_SINGLE("High Pass Filter Switch", WM8940_ADC, 8, 1, 0), + SOC_ENUM("High Pass Filter Mode", wm8940_filter_mode_enum), + SOC_SINGLE("High Pass Filter Cut Off", WM8940_ADC, 4, 7, 0), + SOC_SINGLE("ADC Inversion Switch", WM8940_ADC, 0, 1, 0), + SOC_SINGLE("DAC Inversion Switch", WM8940_DAC, 0, 1, 0), + SOC_SINGLE("DAC Auto Mute Switch", WM8940_DAC, 2, 1, 0), + SOC_SINGLE("ZC Timeout Clock Switch", WM8940_ADDCNTRL, 0, 1, 0), +}; + +static const struct snd_kcontrol_new wm8940_speaker_mixer_controls[] = { + SOC_DAPM_SINGLE("Line Bypass Switch", WM8940_SPKMIX, 1, 1, 0), + SOC_DAPM_SINGLE("Aux Playback Switch", WM8940_SPKMIX, 5, 1, 0), + SOC_DAPM_SINGLE("PCM Playback Switch", WM8940_SPKMIX, 0, 1, 0), +}; + +static const struct snd_kcontrol_new wm8940_mono_mixer_controls[] = { + SOC_DAPM_SINGLE("Line Bypass Switch", WM8940_MONOMIX, 1, 1, 0), + SOC_DAPM_SINGLE("Aux Playback Switch", WM8940_MONOMIX, 2, 1, 0), + SOC_DAPM_SINGLE("PCM Playback Switch", WM8940_MONOMIX, 0, 1, 0), +}; + +DECLARE_TLV_DB_SCALE(wm8940_boost_vol_tlv, -1500, 300, 1); +static const struct snd_kcontrol_new wm8940_input_boost_controls[] = { + SOC_DAPM_SINGLE("Mic PGA Switch", WM8940_PGAGAIN, 6, 1, 1), + SOC_DAPM_SINGLE_TLV("Aux Volume", WM8940_ADCBOOST, + 0, 7, 0, wm8940_boost_vol_tlv), + SOC_DAPM_SINGLE_TLV("Mic Volume", WM8940_ADCBOOST, + 4, 7, 0, wm8940_boost_vol_tlv), +}; + +static const struct snd_kcontrol_new wm8940_micpga_controls[] = { + SOC_DAPM_SINGLE("AUX Switch", WM8940_INPUTCTL, 2, 1, 0), + SOC_DAPM_SINGLE("MICP Switch", WM8940_INPUTCTL, 0, 1, 0), + SOC_DAPM_SINGLE("MICN Switch", WM8940_INPUTCTL, 1, 1, 0), +}; + +static const struct snd_soc_dapm_widget wm8940_dapm_widgets[] = { + SND_SOC_DAPM_MIXER("Speaker Mixer", WM8940_POWER3, 2, 0, + &wm8940_speaker_mixer_controls[0], + ARRAY_SIZE(wm8940_speaker_mixer_controls)), + SND_SOC_DAPM_MIXER("Mono Mixer", WM8940_POWER3, 3, 0, + &wm8940_mono_mixer_controls[0], + ARRAY_SIZE(wm8940_mono_mixer_controls)), + SND_SOC_DAPM_DAC("DAC", "HiFi Playback", WM8940_POWER3, 0, 0), + + SND_SOC_DAPM_PGA("SpkN Out", WM8940_POWER3, 5, 0, NULL, 0), + SND_SOC_DAPM_PGA("SpkP Out", WM8940_POWER3, 6, 0, NULL, 0), + SND_SOC_DAPM_PGA("Mono Out", WM8940_POWER3, 7, 0, NULL, 0), + SND_SOC_DAPM_OUTPUT("MONOOUT"), + SND_SOC_DAPM_OUTPUT("SPKOUTP"), + SND_SOC_DAPM_OUTPUT("SPKOUTN"), + + SND_SOC_DAPM_PGA("Aux Input", WM8940_POWER1, 6, 0, NULL, 0), + SND_SOC_DAPM_ADC("ADC", "HiFi Capture", WM8940_POWER2, 0, 0), + SND_SOC_DAPM_MIXER("Mic PGA", WM8940_POWER2, 2, 0, + &wm8940_micpga_controls[0], + ARRAY_SIZE(wm8940_micpga_controls)), + SND_SOC_DAPM_MIXER("Boost Mixer", WM8940_POWER2, 4, 0, + &wm8940_input_boost_controls[0], + ARRAY_SIZE(wm8940_input_boost_controls)), + SND_SOC_DAPM_MICBIAS("Mic Bias", WM8940_POWER1, 4, 0), + + SND_SOC_DAPM_INPUT("MICN"), + SND_SOC_DAPM_INPUT("MICP"), + SND_SOC_DAPM_INPUT("AUX"), +}; + +static const struct snd_soc_dapm_route audio_map[] = { + /* Mono output mixer */ + {"Mono Mixer", "PCM Playback Switch", "DAC"}, + {"Mono Mixer", "Aux Playback Switch", "Aux Input"}, + {"Mono Mixer", "Line Bypass Switch", "Boost Mixer"}, + + /* Speaker output mixer */ + {"Speaker Mixer", "PCM Playback Switch", "DAC"}, + {"Speaker Mixer", "Aux Playback Switch", "Aux Input"}, + {"Speaker Mixer", "Line Bypass Switch", "Boost Mixer"}, + + /* Outputs */ + {"Mono Out", NULL, "Mono Mixer"}, + {"MONOOUT", NULL, "Mono Out"}, + {"SpkN Out", NULL, "Speaker Mixer"}, + {"SpkP Out", NULL, "Speaker Mixer"}, + {"SPKOUTN", NULL, "SpkN Out"}, + {"SPKOUTP", NULL, "SpkP Out"}, + + /* Microphone PGA */ + {"Mic PGA", "MICN Switch", "MICN"}, + {"Mic PGA", "MICP Switch", "MICP"}, + {"Mic PGA", "AUX Switch", "AUX"}, + + /* Boost Mixer */ + {"Boost Mixer", "Mic PGA Switch", "Mic PGA"}, + {"Boost Mixer", "Mic Volume", "MICP"}, + {"Boost Mixer", "Aux Volume", "Aux Input"}, + + {"ADC", NULL, "Boost Mixer"}, +}; + +static int wm8940_add_widgets(struct snd_soc_codec *codec) +{ + int ret; + + ret = snd_soc_dapm_new_controls(codec, wm8940_dapm_widgets, + ARRAY_SIZE(wm8940_dapm_widgets)); + if (ret) + goto error_ret; + ret = snd_soc_dapm_add_routes(codec, audio_map, ARRAY_SIZE(audio_map)); + if (ret) + goto error_ret; + ret = snd_soc_dapm_new_widgets(codec); + +error_ret: + return ret; +} + +#define wm8940_reset(c) wm8940_write(c, WM8940_SOFTRESET, 0); + +static int wm8940_set_dai_fmt(struct snd_soc_dai *codec_dai, + unsigned int fmt) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u16 iface = wm8940_read_reg_cache(codec, WM8940_IFACE) & 0xFE67; + u16 clk = wm8940_read_reg_cache(codec, WM8940_CLOCK) & 0x1fe; + + switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) { + case SND_SOC_DAIFMT_CBM_CFM: + clk |= 1; + break; + case SND_SOC_DAIFMT_CBS_CFS: + break; + default: + return -EINVAL; + } + wm8940_write(codec, WM8940_CLOCK, clk); + + switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { + case SND_SOC_DAIFMT_I2S: + iface |= (2 << 3); + break; + case SND_SOC_DAIFMT_LEFT_J: + iface |= (1 << 3); + break; + case SND_SOC_DAIFMT_RIGHT_J: + break; + case SND_SOC_DAIFMT_DSP_A: + iface |= (3 << 3); + break; + case SND_SOC_DAIFMT_DSP_B: + iface |= (3 << 3) | (1 << 7); + break; + } + + switch (fmt & SND_SOC_DAIFMT_INV_MASK) { + case SND_SOC_DAIFMT_NB_NF: + break; + case SND_SOC_DAIFMT_NB_IF: + iface |= (1 << 7); + break; + case SND_SOC_DAIFMT_IB_NF: + iface |= (1 << 8); + break; + case SND_SOC_DAIFMT_IB_IF: + iface |= (1 << 8) | (1 << 7); + break; + } + + wm8940_write(codec, WM8940_IFACE, iface); + + return 0; +} + +static int wm8940_i2s_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = substream->private_data; + struct snd_soc_device *socdev = rtd->socdev; + struct snd_soc_codec *codec = socdev->card->codec; + u16 iface = wm8940_read_reg_cache(codec, WM8940_IFACE) & 0xFD9F; + u16 addcntrl = wm8940_read_reg_cache(codec, WM8940_ADDCNTRL) & 0xFFF1; + u16 companding = wm8940_read_reg_cache(codec, + WM8940_COMPANDINGCTL) & 0xFFDF; + int ret; + + /* LoutR control */ + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE + && params_channels(params) == 2) + iface |= (1 << 9); + + switch (params_rate(params)) { + case SNDRV_PCM_RATE_8000: + addcntrl |= (0x5 << 1); + break; + case SNDRV_PCM_RATE_11025: + addcntrl |= (0x4 << 1); + break; + case SNDRV_PCM_RATE_16000: + addcntrl |= (0x3 << 1); + break; + case SNDRV_PCM_RATE_22050: + addcntrl |= (0x2 << 1); + break; + case SNDRV_PCM_RATE_32000: + addcntrl |= (0x1 << 1); + break; + case SNDRV_PCM_RATE_44100: + case SNDRV_PCM_RATE_48000: + break; + } + ret = wm8940_write(codec, WM8940_ADDCNTRL, addcntrl); + if (ret) + goto error_ret; + + switch (params_format(params)) { + case SNDRV_PCM_FORMAT_S8: + companding = companding | (1 << 5); + break; + case SNDRV_PCM_FORMAT_S16_LE: + break; + case SNDRV_PCM_FORMAT_S20_3LE: + iface |= (1 << 5); + break; + case SNDRV_PCM_FORMAT_S24_LE: + iface |= (2 << 5); + break; + case SNDRV_PCM_FORMAT_S32_LE: + iface |= (3 << 5); + break; + } + ret = wm8940_write(codec, WM8940_COMPANDINGCTL, companding); + if (ret) + goto error_ret; + ret = wm8940_write(codec, WM8940_IFACE, iface); + +error_ret: + return ret; +} + +static int wm8940_mute(struct snd_soc_dai *dai, int mute) +{ + struct snd_soc_codec *codec = dai->codec; + u16 mute_reg = wm8940_read_reg_cache(codec, WM8940_DAC) & 0xffbf; + + if (mute) + mute_reg |= 0x40; + + return wm8940_write(codec, WM8940_DAC, mute_reg); +} + +static int wm8940_set_bias_level(struct snd_soc_codec *codec, + enum snd_soc_bias_level level) +{ + u16 val; + u16 pwr_reg = wm8940_read_reg_cache(codec, WM8940_POWER1) & 0x1F0; + int ret = 0; + + switch (level) { + case SND_SOC_BIAS_ON: + /* ensure bufioen and biasen */ + pwr_reg |= (1 << 2) | (1 << 3); + /* Enable thermal shutdown */ + val = wm8940_read_reg_cache(codec, WM8940_OUTPUTCTL); + ret = wm8940_write(codec, WM8940_OUTPUTCTL, val | 0x2); + if (ret) + break; + /* set vmid to 75k */ + ret = wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x1); + break; + case SND_SOC_BIAS_PREPARE: + /* ensure bufioen and biasen */ + pwr_reg |= (1 << 2) | (1 << 3); + ret = wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x1); + break; + case SND_SOC_BIAS_STANDBY: + /* ensure bufioen and biasen */ + pwr_reg |= (1 << 2) | (1 << 3); + /* set vmid to 300k for standby */ + ret = wm8940_write(codec, WM8940_POWER1, pwr_reg | 0x2); + break; + case SND_SOC_BIAS_OFF: + ret = wm8940_write(codec, WM8940_POWER1, pwr_reg); + break; + } + + return ret; +} + +struct pll_ { + unsigned int pre_scale:2; + unsigned int n:4; + unsigned int k; +}; + +static struct pll_ pll_div; + +/* The size in bits of the pll divide multiplied by 10 + * to allow rounding later */ +#define FIXED_PLL_SIZE ((1 << 24) * 10) +static void pll_factors(unsigned int target, unsigned int source) +{ + unsigned long long Kpart; + unsigned int K, Ndiv, Nmod; + /* The left shift ist to avoid accuracy loss when right shifting */ + Ndiv = target / source; + + if (Ndiv > 12) { + source <<= 1; + /* Multiply by 2 */ + pll_div.pre_scale = 0; + Ndiv = target / source; + } else if (Ndiv < 3) { + source >>= 2; + /* Divide by 4 */ + pll_div.pre_scale = 3; + Ndiv = target / source; + } else if (Ndiv < 6) { + source >>= 1; + /* divide by 2 */ + pll_div.pre_scale = 2; + Ndiv = target / source; + } else + pll_div.pre_scale = 1; + + if ((Ndiv < 6) || (Ndiv > 12)) + printk(KERN_WARNING + "WM8940 N value %d outwith recommended range!d\n", + Ndiv); + + pll_div.n = Ndiv; + Nmod = target % source; + Kpart = FIXED_PLL_SIZE * (long long)Nmod; + + do_div(Kpart, source); + + K = Kpart & 0xFFFFFFFF; + + /* Check if we need to round */ + if ((K % 10) >= 5) + K += 5; + + /* Move down to proper range now rounding is done */ + K /= 10; + + pll_div.k = K; +} + +/* Untested at the moment */ +static int wm8940_set_dai_pll(struct snd_soc_dai *codec_dai, + int pll_id, unsigned int freq_in, unsigned int freq_out) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u16 reg; + + /* Turn off PLL */ + reg = wm8940_read_reg_cache(codec, WM8940_POWER1); + wm8940_write(codec, WM8940_POWER1, reg & 0x1df); + + if (freq_in == 0 || freq_out == 0) { + /* Clock CODEC directly from MCLK */ + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK); + wm8940_write(codec, WM8940_CLOCK, reg & 0x0ff); + /* Pll power down */ + wm8940_write(codec, WM8940_PLLN, (1 << 7)); + return 0; + } + + /* Pll is followed by a frequency divide by 4 */ + pll_factors(freq_out*4, freq_in); + if (pll_div.k) + wm8940_write(codec, WM8940_PLLN, + (pll_div.pre_scale << 4) | pll_div.n | (1 << 6)); + else /* No factional component */ + wm8940_write(codec, WM8940_PLLN, + (pll_div.pre_scale << 4) | pll_div.n); + wm8940_write(codec, WM8940_PLLK1, pll_div.k >> 18); + wm8940_write(codec, WM8940_PLLK2, (pll_div.k >> 9) & 0x1ff); + wm8940_write(codec, WM8940_PLLK3, pll_div.k & 0x1ff); + /* Enable the PLL */ + reg = wm8940_read_reg_cache(codec, WM8940_POWER1); + wm8940_write(codec, WM8940_POWER1, reg | 0x020); + + /* Run CODEC from PLL instead of MCLK */ + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK); + wm8940_write(codec, WM8940_CLOCK, reg | 0x100); + + return 0; +} + +static int wm8940_set_dai_sysclk(struct snd_soc_dai *codec_dai, + int clk_id, unsigned int freq, int dir) +{ + struct snd_soc_codec *codec = codec_dai->codec; + struct wm8940_priv *wm8940 = codec->private_data; + + switch (freq) { + case 11289600: + case 12000000: + case 12288000: + case 16934400: + case 18432000: + wm8940->sysclk = freq; + return 0; + } + return -EINVAL; +} + +static int wm8940_set_dai_clkdiv(struct snd_soc_dai *codec_dai, + int div_id, int div) +{ + struct snd_soc_codec *codec = codec_dai->codec; + u16 reg; + int ret = 0; + + switch (div_id) { + case WM8940_BCLKDIV: + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK) & 0xFFEF3; + ret = wm8940_write(codec, WM8940_CLOCK, reg | (div << 2)); + break; + case WM8940_MCLKDIV: + reg = wm8940_read_reg_cache(codec, WM8940_CLOCK) & 0xFF1F; + ret = wm8940_write(codec, WM8940_CLOCK, reg | (div << 5)); + break; + case WM8940_OPCLKDIV: + reg = wm8940_read_reg_cache(codec, WM8940_ADDCNTRL) & 0xFFCF; + ret = wm8940_write(codec, WM8940_ADDCNTRL, reg | (div << 4)); + break; + } + return ret; +} + +#define WM8940_RATES SNDRV_PCM_RATE_8000_48000 + +#define WM8940_FORMATS (SNDRV_PCM_FMTBIT_S8 | \ + SNDRV_PCM_FMTBIT_S16_LE | \ + SNDRV_PCM_FMTBIT_S20_3LE | \ + SNDRV_PCM_FMTBIT_S24_LE | \ + SNDRV_PCM_FMTBIT_S32_LE) + +static struct snd_soc_dai_ops wm8940_dai_ops = { + .hw_params = wm8940_i2s_hw_params, + .set_sysclk = wm8940_set_dai_sysclk, + .digital_mute = wm8940_mute, + .set_fmt = wm8940_set_dai_fmt, + .set_clkdiv = wm8940_set_dai_clkdiv, + .set_pll = wm8940_set_dai_pll, +}; + +struct snd_soc_dai wm8940_dai = { + .name = "WM8940", + .playback = { + .stream_name = "Playback", + .channels_min = 1, + .channels_max = 2, + .rates = WM8940_RATES, + .formats = WM8940_FORMATS, + }, + .capture = { + .stream_name = "Capture", + .channels_min = 1, + .channels_max = 2, + .rates = WM8940_RATES, + .formats = WM8940_FORMATS, + }, + .ops = &wm8940_dai_ops, + .symmetric_rates = 1, +}; +EXPORT_SYMBOL_GPL(wm8940_dai); + +static int wm8940_suspend(struct platform_device *pdev, pm_message_t state) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->card->codec; + + return wm8940_set_bias_level(codec, SND_SOC_BIAS_OFF); +} + +static int wm8940_resume(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->card->codec; + int i; + int ret; + u8 data[3]; + u16 *cache = codec->reg_cache; + + /* Sync reg_cache with the hardware + * Could use auto incremented writes to speed this up + */ + for (i = 0; i < ARRAY_SIZE(wm8940_reg_defaults); i++) { + data[0] = i; + data[1] = (cache[i] & 0xFF00) >> 8; + data[2] = cache[i] & 0x00FF; + ret = codec->hw_write(codec->control_data, data, 3); + if (ret < 0) + goto error_ret; + else if (ret != 3) { + ret = -EIO; + goto error_ret; + } + } + ret = wm8940_set_bias_level(codec, SND_SOC_BIAS_STANDBY); + if (ret) + goto error_ret; + ret = wm8940_set_bias_level(codec, codec->suspend_bias_level); + +error_ret: + return ret; +} + +static struct snd_soc_codec *wm8940_codec; + +static int wm8940_probe(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec; + + int ret = 0; + + if (wm8940_codec == NULL) { + dev_err(&pdev->dev, "Codec device not registered\n"); + return -ENODEV; + } + + socdev->card->codec = wm8940_codec; + codec = wm8940_codec; + + mutex_init(&codec->mutex); + /* register pcms */ + ret = snd_soc_new_pcms(socdev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1); + if (ret < 0) { + dev_err(codec->dev, "failed to create pcms: %d\n", ret); + goto pcm_err; + } + + ret = snd_soc_add_controls(codec, wm8940_snd_controls, + ARRAY_SIZE(wm8940_snd_controls)); + if (ret) + goto error_free_pcms; + ret = wm8940_add_widgets(codec); + if (ret) + goto error_free_pcms; + + ret = snd_soc_init_card(socdev); + if (ret < 0) { + dev_err(codec->dev, "failed to register card: %d\n", ret); + goto error_free_pcms; + } + + return ret; + +error_free_pcms: + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +pcm_err: + return ret; +} + +static int wm8940_remove(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); + + return 0; +} + +struct snd_soc_codec_device soc_codec_dev_wm8940 = { + .probe = wm8940_probe, + .remove = wm8940_remove, + .suspend = wm8940_suspend, + .resume = wm8940_resume, +}; +EXPORT_SYMBOL_GPL(soc_codec_dev_wm8940); + +static int wm8940_register(struct wm8940_priv *wm8940) +{ + struct wm8940_setup_data *pdata = wm8940->codec.dev->platform_data; + struct snd_soc_codec *codec = &wm8940->codec; + int ret; + u16 reg; + if (wm8940_codec) { + dev_err(codec->dev, "Another WM8940 is registered\n"); + return -EINVAL; + } + + INIT_LIST_HEAD(&codec->dapm_widgets); + INIT_LIST_HEAD(&codec->dapm_paths); + + codec->private_data = wm8940; + codec->name = "WM8940"; + codec->owner = THIS_MODULE; + codec->read = wm8940_read_reg_cache; + codec->write = wm8940_write; + codec->bias_level = SND_SOC_BIAS_OFF; + codec->set_bias_level = wm8940_set_bias_level; + codec->dai = &wm8940_dai; + codec->num_dai = 1; + codec->reg_cache_size = ARRAY_SIZE(wm8940_reg_defaults); + codec->reg_cache = &wm8940->reg_cache; + + memcpy(codec->reg_cache, wm8940_reg_defaults, + sizeof(wm8940_reg_defaults)); + + ret = wm8940_reset(codec); + if (ret < 0) { + dev_err(codec->dev, "Failed to issue reset\n"); + return ret; + } + + wm8940_dai.dev = codec->dev; + + wm8940_set_bias_level(codec, SND_SOC_BIAS_STANDBY); + + ret = wm8940_write(codec, WM8940_POWER1, 0x180); + if (ret < 0) + return ret; + + if (!pdata) + dev_warn(codec->dev, "No platform data supplied\n"); + else { + reg = wm8940_read_reg_cache(codec, WM8940_OUTPUTCTL); + ret = wm8940_write(codec, WM8940_OUTPUTCTL, reg | pdata->vroi); + if (ret < 0) + return ret; + } + + + wm8940_codec = codec; + + ret = snd_soc_register_codec(codec); + if (ret) { + dev_err(codec->dev, "Failed to register codec: %d\n", ret); + return ret; + } + + ret = snd_soc_register_dai(&wm8940_dai); + if (ret) { + dev_err(codec->dev, "Failed to register DAI: %d\n", ret); + snd_soc_unregister_codec(codec); + return ret; + } + + return 0; +} + +static void wm8940_unregister(struct wm8940_priv *wm8940) +{ + wm8940_set_bias_level(&wm8940->codec, SND_SOC_BIAS_OFF); + snd_soc_unregister_dai(&wm8940_dai); + snd_soc_unregister_codec(&wm8940->codec); + kfree(wm8940); + wm8940_codec = NULL; +} + +static int wm8940_i2c_probe(struct i2c_client *i2c, + const struct i2c_device_id *id) +{ + struct wm8940_priv *wm8940; + struct snd_soc_codec *codec; + + wm8940 = kzalloc(sizeof *wm8940, GFP_KERNEL); + if (wm8940 == NULL) + return -ENOMEM; + + codec = &wm8940->codec; + codec->hw_write = (hw_write_t)i2c_master_send; + i2c_set_clientdata(i2c, wm8940); + codec->control_data = i2c; + codec->dev = &i2c->dev; + + return wm8940_register(wm8940); +} + +static int wm8940_i2c_remove(struct i2c_client *client) +{ + struct wm8940_priv *wm8940 = i2c_get_clientdata(client); + + wm8940_unregister(wm8940); + + return 0; +} + +static const struct i2c_device_id wm8940_i2c_id[] = { + { "wm8940", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, wm8940_i2c_id); + +static struct i2c_driver wm8940_i2c_driver = { + .driver = { + .name = "WM8940 I2C Codec", + .owner = THIS_MODULE, + }, + .probe = wm8940_i2c_probe, + .remove = __devexit_p(wm8940_i2c_remove), + .id_table = wm8940_i2c_id, +}; + +static int __init wm8940_modinit(void) +{ + int ret; + + ret = i2c_add_driver(&wm8940_i2c_driver); + if (ret) + printk(KERN_ERR "Failed to register WM8940 I2C driver: %d\n", + ret); + return ret; +} +module_init(wm8940_modinit); + +static void __exit wm8940_exit(void) +{ + i2c_del_driver(&wm8940_i2c_driver); +} +module_exit(wm8940_exit); + +MODULE_DESCRIPTION("ASoC WM8940 driver"); +MODULE_AUTHOR("Jonathan Cameron"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/wm8940.h b/sound/soc/codecs/wm8940.h new file mode 100644 index 0000000..8410eed --- /dev/null +++ b/sound/soc/codecs/wm8940.h @@ -0,0 +1,104 @@ +/* + * wm8940.h -- WM8940 Soc Audio driver + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +#ifndef _WM8940_H +#define _WM8940_H + +struct wm8940_setup_data { + /* Vref to analogue output resistance */ +#define WM8940_VROI_1K 0 +#define WM8940_VROI_30K 1 + unsigned int vroi:1; +}; +extern struct snd_soc_dai wm8940_dai; +extern struct snd_soc_codec_device soc_codec_dev_wm8940; + +/* WM8940 register space */ +#define WM8940_SOFTRESET 0x00 +#define WM8940_POWER1 0x01 +#define WM8940_POWER2 0x02 +#define WM8940_POWER3 0x03 +#define WM8940_IFACE 0x04 +#define WM8940_COMPANDINGCTL 0x05 +#define WM8940_CLOCK 0x06 +#define WM8940_ADDCNTRL 0x07 +#define WM8940_GPIO 0x08 +#define WM8940_CTLINT 0x09 +#define WM8940_DAC 0x0A +#define WM8940_DACVOL 0x0B + +#define WM8940_ADC 0x0E +#define WM8940_ADCVOL 0x0F +#define WM8940_NOTCH1 0x10 +#define WM8940_NOTCH2 0x11 +#define WM8940_NOTCH3 0x12 +#define WM8940_NOTCH4 0x13 +#define WM8940_NOTCH5 0x14 +#define WM8940_NOTCH6 0x15 +#define WM8940_NOTCH7 0x16 +#define WM8940_NOTCH8 0x17 +#define WM8940_DACLIM1 0x18 +#define WM8940_DACLIM2 0x19 + +#define WM8940_ALC1 0x20 +#define WM8940_ALC2 0x21 +#define WM8940_ALC3 0x22 +#define WM8940_NOISEGATE 0x23 +#define WM8940_PLLN 0x24 +#define WM8940_PLLK1 0x25 +#define WM8940_PLLK2 0x26 +#define WM8940_PLLK3 0x27 + +#define WM8940_ALC4 0x2A + +#define WM8940_INPUTCTL 0x2C +#define WM8940_PGAGAIN 0x2D + +#define WM8940_ADCBOOST 0x2F + +#define WM8940_OUTPUTCTL 0x31 +#define WM8940_SPKMIX 0x32 + +#define WM8940_SPKVOL 0x36 + +#define WM8940_MONOMIX 0x38 + +#define WM8940_CACHEREGNUM 0x57 + + +/* Clock divider Id's */ +#define WM8940_BCLKDIV 0 +#define WM8940_MCLKDIV 1 +#define WM8940_OPCLKDIV 2 + +/* MCLK clock dividers */ +#define WM8940_MCLKDIV_1 0 +#define WM8940_MCLKDIV_1_5 1 +#define WM8940_MCLKDIV_2 2 +#define WM8940_MCLKDIV_3 3 +#define WM8940_MCLKDIV_4 4 +#define WM8940_MCLKDIV_6 5 +#define WM8940_MCLKDIV_8 6 +#define WM8940_MCLKDIV_12 7 + +/* BCLK clock dividers */ +#define WM8940_BCLKDIV_1 0 +#define WM8940_BCLKDIV_2 1 +#define WM8940_BCLKDIV_4 2 +#define WM8940_BCLKDIV_8 3 +#define WM8940_BCLKDIV_16 4 +#define WM8940_BCLKDIV_32 5 + +/* PLL Out Dividers */ +#define WM8940_OPCLKDIV_1 0 +#define WM8940_OPCLKDIV_2 1 +#define WM8940_OPCLKDIV_3 2 +#define WM8940_OPCLKDIV_4 3 + +#endif /* _WM8940_H */ +
On Mon, Apr 27, 2009 at 01:49:44PM +0000, Jonathan Cameron wrote:
From: Jonathan Cameron jic23@cam.ac.uk
aSoC: Initial support for the Wolfson Micro WM8940 codec
ASoC.
I've applied this, thanks! I've also applied a commit staticising the TLV values - these aren't exported from the driver.
participants (2)
-
Jonathan Cameron
-
Mark Brown