[alsa-devel] [PATCH 1/3] ASoC: TWL6030: Add twl6030 codec driver
Initial version of TWL6030 codec driver.
The TWL6030 codec uses a propietary digital audio interface called McPDM. TWL6030 codec has two power modes: low-power and high-performance, this initial version only supports high-performance mode.
Signed-off-by: Misael Lopez Cruz x0052729@ti.com --- include/sound/soc-dai.h | 3 + sound/soc/codecs/Kconfig | 4 + sound/soc/codecs/Makefile | 2 + sound/soc/codecs/twl6030.c | 670 ++++++++++++++++++++++++++++++++++++++++++++ sound/soc/codecs/twl6030.h | 95 +++++++ 5 files changed, 774 insertions(+), 0 deletions(-) create mode 100644 sound/soc/codecs/twl6030.c create mode 100644 sound/soc/codecs/twl6030.h
diff --git a/include/sound/soc-dai.h b/include/sound/soc-dai.h index 352d7ee..8b0d04b 100644 --- a/include/sound/soc-dai.h +++ b/include/sound/soc-dai.h @@ -35,6 +35,9 @@ struct snd_pcm_substream; #define SND_SOC_DAIFMT_MSB SND_SOC_DAIFMT_LEFT_J #define SND_SOC_DAIFMT_LSB SND_SOC_DAIFMT_RIGHT_J
+/* propietary formats */ +#define SND_SOC_DAIFMT_MCPDM 0x10 /* Texas Instruments McPDM */ + /* * DAI Clock gating. * diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig index bbc97fd..0effb52 100644 --- a/sound/soc/codecs/Kconfig +++ b/sound/soc/codecs/Kconfig @@ -25,6 +25,7 @@ config SND_SOC_ALL_CODECS select SND_SOC_TLV320AIC26 if SPI_MASTER select SND_SOC_TLV320AIC3X if I2C select SND_SOC_TWL4030 if TWL4030_CORE + select SND_SOC_TWL6030 if TWL6030_CORE select SND_SOC_UDA134X select SND_SOC_UDA1380 if I2C select SND_SOC_WM8350 if MFD_WM8350 @@ -114,6 +115,9 @@ config SND_SOC_TLV320AIC3X config SND_SOC_TWL4030 tristate
+config SND_SOC_TWL6030 + tristate + config SND_SOC_UDA134X tristate
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile index 8b75305..b70c8a1 100644 --- a/sound/soc/codecs/Makefile +++ b/sound/soc/codecs/Makefile @@ -13,6 +13,7 @@ snd-soc-tlv320aic23-objs := tlv320aic23.o snd-soc-tlv320aic26-objs := tlv320aic26.o snd-soc-tlv320aic3x-objs := tlv320aic3x.o snd-soc-twl4030-objs := twl4030.o +snd-soc-twl6030-objs := twl6030.o snd-soc-uda134x-objs := uda134x.o snd-soc-uda1380-objs := uda1380.o snd-soc-wm8350-objs := wm8350.o @@ -50,6 +51,7 @@ obj-$(CONFIG_SND_SOC_TLV320AIC23) += snd-soc-tlv320aic23.o obj-$(CONFIG_SND_SOC_TLV320AIC26) += snd-soc-tlv320aic26.o obj-$(CONFIG_SND_SOC_TLV320AIC3X) += snd-soc-tlv320aic3x.o obj-$(CONFIG_SND_SOC_TWL4030) += snd-soc-twl4030.o +obj-$(CONFIG_SND_SOC_TWL6030) += snd-soc-twl6030.o obj-$(CONFIG_SND_SOC_UDA134X) += snd-soc-uda134x.o obj-$(CONFIG_SND_SOC_UDA1380) += snd-soc-uda1380.o obj-$(CONFIG_SND_SOC_WM8350) += snd-soc-wm8350.o diff --git a/sound/soc/codecs/twl6030.c b/sound/soc/codecs/twl6030.c new file mode 100644 index 0000000..c5e76fa --- /dev/null +++ b/sound/soc/codecs/twl6030.c @@ -0,0 +1,670 @@ +/* + * ALSA SoC TWL6030 codec driver + * + * Author: Misael Lopez Cruz x0052729@ti.com + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/init.h> +#include <linux/delay.h> +#include <linux/pm.h> +#include <linux/i2c.h> +#include <linux/platform_device.h> +#include <linux/i2c/twl.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 "twl6030.h" + +#define TWL6030_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) +#define TWL6030_FORMATS (SNDRV_PCM_FMTBIT_S32_LE) + +/* + * twl6030 register cache & default register settings + */ +static const u8 twl6030_reg[TWL6030_CACHEREGNUM] = { + 0x00, /* not used 0x00 */ + 0x4B, /* TWL6030_ASICID (ro) 0x01 */ + 0x00, /* TWL6030_ASICREV (ro) 0x02 */ + 0x00, /* TWL6030_INTID 0x03 */ + 0x41, /* TWL6030_INTMR 0x04 */ + 0x00, /* TWL6030_NCPCTRL 0x05 */ + 0x00, /* TWL6030_LDOCTL 0x06 */ + 0x00, /* TWL6030_HPPLLCTL 0x07 */ + 0x00, /* TWL6030_LPPLLCTL 0x08 */ + 0x00, /* TWL6030_LPPLLDIV 0x09 */ + 0x00, /* TWL6030_AMICBCTL 0x0A */ + 0x00, /* TWL6030_DMICBCTL 0x0B */ + 0x18, /* TWL6030_MICLCTL 0x0C */ + 0x18, /* TWL6030_MICRCTL 0x0D */ + 0x00, /* TWL6030_MICGAIN 0x0E */ + 0x1B, /* TWL6030_LINEGAIN 0x0F */ + 0x00, /* TWL6030_HSLCTL 0x10 */ + 0x00, /* TWL6030_HSRCTL 0x11 */ + 0x00, /* TWL6030_HSGAIN 0x12 */ + 0x06, /* TWL6030_EARCTL 0x13 */ + 0x00, /* TWL6030_HFLCTL 0x14 */ + 0x03, /* TWL6030_HFLGAIN 0x15 */ + 0x00, /* TWL6030_HFRCTL 0x16 */ + 0x03, /* TWL6030_HFRGAIN 0x17 */ + 0x00, /* TWL6030_VIBCTLL 0x18 */ + 0x00, /* TWL6030_VIBDATL 0x19 */ + 0x00, /* TWL6030_VIBCTLR 0x1A */ + 0x00, /* TWL6030_VIBDATR 0x1B */ + 0x00, /* TWL6030_HKCTL1 0x1C */ + 0x00, /* TWL6030_HKCTL2 0x1D */ + 0x00, /* TWL6030_GPOCTL 0x1E */ + 0x00, /* TWL6030_ALB 0x1F */ + 0x00, /* TWL6030_DLB 0x20 */ + 0x00, /* not used 0x21 */ + 0x00, /* not used 0x22 */ + 0x00, /* not used 0x23 */ + 0x00, /* not used 0x24 */ + 0x00, /* not used 0x25 */ + 0x00, /* not used 0x26 */ + 0x00, /* not used 0x27 */ + 0x00, /* TWL6030_TRIM1 0x28 */ + 0x00, /* TWL6030_TRIM2 0x29 */ + 0x00, /* TWL6030_TRIM3 0x2A */ + 0x00, /* TWL6030_HSOTRIM 0x2B */ + 0x00, /* TWL6030_HFOTRIM 0x2C */ + 0x09, /* TWL6030_ACCCTL 0x2D */ + 0x00, /* TWL6030_STATUS (ro) 0x2E */ +}; + +/* + * read twl6030 register cache + */ +static inline unsigned int twl6030_read_reg_cache(struct snd_soc_codec *codec, + unsigned int reg) +{ + u8 *cache = codec->reg_cache; + + if (reg >= TWL6030_CACHEREGNUM) + return -EIO; + + return cache[reg]; +} + +/* + * write twl6030 register cache + */ +static inline void twl6030_write_reg_cache(struct snd_soc_codec *codec, + u8 reg, u8 value) +{ + u8 *cache = codec->reg_cache; + + if (reg >= TWL6030_CACHEREGNUM) + return; + cache[reg] = value; +} + +/* + * write to the twl6030 register space + */ +static int twl6030_write(struct snd_soc_codec *codec, + unsigned int reg, unsigned int value) +{ + if ((reg < TWL6030_REG_INTID) || (reg > TWL6030_REG_ACCCTL)) + /* read-only registers */ + return -EIO; + + twl6030_write_reg_cache(codec, reg, value); + return twl_i2c_write_u8(TWL6030_MODULE_AUDIO, value, reg); +} + +static void twl6030_init_chip(struct snd_soc_codec *codec) +{ + u8 *cache = codec->reg_cache; + int i; + + /* allow registers to be accessed by i2c */ + twl6030_write(codec, TWL6030_REG_ACCCTL, cache[TWL6030_REG_ACCCTL]); + + /* set all audio section registers to reasonable defaults */ + /* avoid read-only (ASICID, ASICREV, STATUS) and non-used registers */ + for (i = TWL6030_REG_INTMR; i <= TWL6030_REG_DLB; i++) + twl6030_write(codec, i, cache[i]); + for (i = TWL6030_REG_TRIM1; i < TWL6030_REG_ACCCTL; i++) + twl6030_write(codec, i, cache[i]); +} + +static void twl6030_power_up(struct snd_soc_codec *codec) +{ + struct snd_soc_device *socdev = codec->socdev; + struct twl6030_setup_data *setup = socdev->codec_data; + + setup->codec_enable(1); + + /* sync registers updated during power-up sequence */ + twl6030_write_reg_cache(codec, TWL6030_REG_NCPCTL, 0x81); + twl6030_write_reg_cache(codec, TWL6030_REG_LDOCTL, 0x45); + twl6030_write_reg_cache(codec, TWL6030_REG_LPPLLCTL, 0x01); +} + +static void twl6030_power_down(struct snd_soc_codec *codec) +{ + struct snd_soc_device *socdev = codec->socdev; + struct twl6030_setup_data *setup = socdev->codec_data; + + setup->codec_enable(0); + + /* sync registers updated during power-down sequence */ + twl6030_write_reg_cache(codec, TWL6030_REG_NCPCTL, 0x00); + twl6030_write_reg_cache(codec, TWL6030_REG_LDOCTL, 0x00); + twl6030_write_reg_cache(codec, TWL6030_REG_LPPLLCTL, 0x00); +} + +/* + * MICATT volume control: + * from -6 to 0 dB in 6 dB steps + */ +static DECLARE_TLV_DB_SCALE(mic_preamp_tlv, -600, 600, 0); + +/* + * MICGAIN volume control: + * from 6 to 30 dB in 6 dB steps + */ +static DECLARE_TLV_DB_SCALE(mic_amp_tlv, 600, 600, 0); + +/* + * HSGAIN volume control: + * from -30 to 0 dB in 2 dB steps + */ +static DECLARE_TLV_DB_SCALE(hs_tlv, -3000, 200, 0); + +/* + * HFGAIN volume control: + * from -52 to 6 dB in 2 dB steps + */ +static DECLARE_TLV_DB_SCALE(hf_tlv, -5200, 200, 0); + +/* Left analog microphone selection */ +static const char *twl6030_amicl_texts[] = + {"Headset Mic", "Main Mic", "Aux/FM Left", "Off"}; + +/* Right analog microphone selection */ +static const char *twl6030_amicr_texts[] = + {"Headset Mic", "Sub Mic", "Aux/FM Right", "Off"}; + +static const struct soc_enum twl6030_enum[] = { + SOC_ENUM_SINGLE(TWL6030_REG_MICLCTL, 3, 3, twl6030_amicl_texts), + SOC_ENUM_SINGLE(TWL6030_REG_MICRCTL, 3, 3, twl6030_amicr_texts), +}; + +static const struct snd_kcontrol_new amicl_control = + SOC_DAPM_ENUM("Route", twl6030_enum[0]); + +static const struct snd_kcontrol_new amicr_control = + SOC_DAPM_ENUM("Route", twl6030_enum[1]); + +/* Headset DAC playback switches */ +static const struct snd_kcontrol_new hsdacl_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HSLCTL, 5, 1, 0); + +static const struct snd_kcontrol_new hsdacr_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HSRCTL, 5, 1, 0); + +/* Handsfree DAC playback switches */ +static const struct snd_kcontrol_new hfdacl_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HFLCTL, 2, 1, 0); + +static const struct snd_kcontrol_new hfdacr_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HFRCTL, 2, 1, 0); + +/* Headset driver switches */ +static const struct snd_kcontrol_new hsl_driver_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HSLCTL, 2, 1, 0); + +static const struct snd_kcontrol_new hsr_driver_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HSRCTL, 2, 1, 0); + +/* Handsfree driver switches */ +static const struct snd_kcontrol_new hfl_driver_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HFLCTL, 4, 1, 0); + +static const struct snd_kcontrol_new hfr_driver_switch_controls = + SOC_DAPM_SINGLE("Switch", TWL6030_REG_HFRCTL, 4, 1, 0); + +static const struct snd_kcontrol_new twl6030_snd_controls[] = { + /* Capture gains */ + SOC_DOUBLE_TLV("Capture Preamplifier (Attenuator) Volume", + TWL6030_REG_MICGAIN, 6, 7, 1, 1, mic_preamp_tlv), + SOC_DOUBLE_TLV("Capture Amplifier Volume", + TWL6030_REG_MICGAIN, 0, 3, 4, 0, mic_amp_tlv), + + /* Playback gains */ + SOC_DOUBLE_TLV("Headset Playback Volume", + TWL6030_REG_HSGAIN, 0, 4, 0xF, 1, hs_tlv), + SOC_DOUBLE_R_TLV("Handsfree Playback Volume", + TWL6030_REG_HFLGAIN, TWL6030_REG_HFRGAIN, 0, 0x1D, 1, hf_tlv), + +}; + +static const struct snd_soc_dapm_widget twl6030_dapm_widgets[] = { + /* Inputs */ + SND_SOC_DAPM_INPUT("MAINMIC"), + SND_SOC_DAPM_INPUT("HSMIC"), + SND_SOC_DAPM_INPUT("SUBMIC"), + SND_SOC_DAPM_INPUT("AFML"), + SND_SOC_DAPM_INPUT("AFMR"), + + /* Outputs */ + SND_SOC_DAPM_OUTPUT("HSOL"), + SND_SOC_DAPM_OUTPUT("HSOR"), + SND_SOC_DAPM_OUTPUT("HFL"), + SND_SOC_DAPM_OUTPUT("HFR"), + + /* Analog input muxers for the capture amplifiers */ + SND_SOC_DAPM_MUX("Analog Left Capture Route", + SND_SOC_NOPM, 0, 0, &amicl_control), + SND_SOC_DAPM_MUX("Analog Right Capture Route", + SND_SOC_NOPM, 0, 0, &amicr_control), + + /* Analog capture PGAs */ + SND_SOC_DAPM_PGA("MicAmpL", + TWL6030_REG_MICLCTL, 0, 0, NULL, 0), + SND_SOC_DAPM_PGA("MicAmpR", + TWL6030_REG_MICRCTL, 0, 0, NULL, 0), + + /* ADCs */ + SND_SOC_DAPM_ADC("ADC Left", "Left Front Capture", + TWL6030_REG_MICLCTL, 2, 0), + SND_SOC_DAPM_ADC("ADC Right", "Right Front Capture", + TWL6030_REG_MICRCTL, 2, 0), + + /* Microphone bias */ + SND_SOC_DAPM_MICBIAS("Headset Mic Bias", + TWL6030_REG_AMICBCTL, 0, 0), + SND_SOC_DAPM_MICBIAS("Main Mic Bias", + TWL6030_REG_AMICBCTL, 4, 0), + SND_SOC_DAPM_MICBIAS("Digital Mic1 Bias", + TWL6030_REG_DMICBCTL, 0, 0), + SND_SOC_DAPM_MICBIAS("Digital Mic2 Bias", + TWL6030_REG_DMICBCTL, 4, 0), + + /* DACs */ + SND_SOC_DAPM_DAC("HSDAC Left", "Headset Playback", + TWL6030_REG_HSLCTL, 0, 0), + SND_SOC_DAPM_DAC("HSDAC Right", "Headset Playback", + TWL6030_REG_HSRCTL, 0, 0), + SND_SOC_DAPM_DAC("HFDAC Left", "Handsfree Playback", + TWL6030_REG_HFLCTL, 0, 0), + SND_SOC_DAPM_DAC("HFDAC Right", "Handsfree Playback", + TWL6030_REG_HFRCTL, 0, 0), + + /* Analog playback switches */ + SND_SOC_DAPM_SWITCH("HSDAC Left Playback", + SND_SOC_NOPM, 0, 0, &hsdacl_switch_controls), + SND_SOC_DAPM_SWITCH("HSDAC Right Playback", + SND_SOC_NOPM, 0, 0, &hsdacr_switch_controls), + SND_SOC_DAPM_SWITCH("HFDAC Left Playback", + SND_SOC_NOPM, 0, 0, &hfdacl_switch_controls), + SND_SOC_DAPM_SWITCH("HFDAC Right Playback", + SND_SOC_NOPM, 0, 0, &hfdacr_switch_controls), + + SND_SOC_DAPM_SWITCH("Headset Left Driver", + SND_SOC_NOPM, 0, 0, &hsl_driver_switch_controls), + SND_SOC_DAPM_SWITCH("Headset Right Driver", + SND_SOC_NOPM, 0, 0, &hsr_driver_switch_controls), + SND_SOC_DAPM_SWITCH("Handsfree Left Driver", + SND_SOC_NOPM, 0, 0, &hfl_driver_switch_controls), + SND_SOC_DAPM_SWITCH("Handsfree Right Driver", + SND_SOC_NOPM, 0, 0, &hfr_driver_switch_controls), + + /* Analog playback PGAs */ + SND_SOC_DAPM_PGA("HFDAC Left PGA", + TWL6030_REG_HFLCTL, 1, 0, NULL, 0), + SND_SOC_DAPM_PGA("HFDAC Right PGA", + TWL6030_REG_HFRCTL, 1, 0, NULL, 0), + +}; + +static const struct snd_soc_dapm_route intercon[] = { + /* Capture path */ + {"Analog Left Capture Route", "Headset Mic", "HSMIC"}, + {"Analog Left Capture Route", "Main Mic", "MAINMIC"}, + {"Analog Left Capture Route", "Aux/FM Left", "AFML"}, + + {"Analog Right Capture Route", "Headset Mic", "HSMIC"}, + {"Analog Right Capture Route", "Sub Mic", "SUBMIC"}, + {"Analog Right Capture Route", "Aux/FM Right", "AFMR"}, + + {"MicAmpL", NULL, "Analog Left Capture Route"}, + {"MicAmpR", NULL, "Analog Right Capture Route"}, + + {"ADC Left", NULL, "MicAmpL"}, + {"ADC Right", NULL, "MicAmpR"}, + + /* Headset playback path */ + {"HSDAC Left Playback", "Switch", "HSDAC Left"}, + {"HSDAC Right Playback", "Switch", "HSDAC Right"}, + + {"Headset Left Driver", "Switch", "HSDAC Left Playback"}, + {"Headset Right Driver", "Switch", "HSDAC Right Playback"}, + + {"HSOL", NULL, "Headset Left Driver"}, + {"HSOR", NULL, "Headset Right Driver"}, + + /* Handsfree playback path */ + {"HFDAC Left Playback", "Switch", "HFDAC Left"}, + {"HFDAC Right Playback", "Switch", "HFDAC Right"}, + + {"HFDAC Left PGA", NULL, "HFDAC Left Playback"}, + {"HFDAC Right PGA", NULL, "HFDAC Right Playback"}, + + {"Handsfree Left Driver", "Switch", "HFDAC Left PGA"}, + {"Handsfree Right Driver", "Switch", "HFDAC Right PGA"}, + + {"HFL", NULL, "Handsfree Left Driver"}, + {"HFR", NULL, "Handsfree Right Driver"}, +}; + +static int twl6030_add_widgets(struct snd_soc_codec *codec) +{ + snd_soc_dapm_new_controls(codec, twl6030_dapm_widgets, + ARRAY_SIZE(twl6030_dapm_widgets)); + + snd_soc_dapm_add_routes(codec, intercon, ARRAY_SIZE(intercon)); + + snd_soc_dapm_new_widgets(codec); + return 0; +} + +static int twl6030_set_bias_level(struct snd_soc_codec *codec, + enum snd_soc_bias_level level) +{ + switch (level) { + case SND_SOC_BIAS_ON: + twl6030_power_up(codec); + break; + case SND_SOC_BIAS_PREPARE: + twl6030_power_up(codec); + break; + case SND_SOC_BIAS_STANDBY: + twl6030_power_up(codec); + break; + case SND_SOC_BIAS_OFF: + twl6030_power_down(codec); + break; + } + codec->bias_level = level; + + return 0; +} + +static int twl6030_hw_params(struct snd_pcm_substream *substream, + struct snd_pcm_hw_params *params, + struct snd_soc_dai *dai) +{ + int rate, format; + + /* hardware dai (McPDM) requires bit stream of twice + * the actual rate, application processor (audio backend) + * should upsample accordingly + */ + rate = params_rate(params); + switch (rate) { + case 44100: + case 48000: + break; + default: + dev_err(codec->dev, "unknown rate %d\n", rate); + return -EINVAL; + } + + /* the sample lenght handled by codec at A-D and D-A + * conversion is 16-bits, but the dai requires 32-bits + */ + format = params_format(params); + if (format != SNDRV_PCM_FORMAT_S32_LE) { + dev_err(codec->dev, "unknown format %d\n", format); + return -EINVAL; + } + + return 0; +} + +static int twl6030_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; + u8 hppll, lppll; + + hppll = twl6030_read_reg_cache(codec, TWL6030_REG_HPPLLCTL); + hppll &= TWL6030_HPLLRST; + + switch (freq) { + case 12000000: + /* MCLK input, PLL enabled */ + hppll = TWL6030_MCLK_12000KHZ + | TWL6030_HPLLSQRBP + | TWL6030_HPLLENA; + break; + case 19200000: + /* MCLK input, PLL disabled */ + hppll = TWL6030_MCLK_19200KHZ + | TWL6030_HPLLSQRBP + | TWL6030_HPLLBP; + break; + case 26000000: + /* MCLK input, PLL enabled */ + hppll = TWL6030_MCLK_26000KHZ + | TWL6030_HPLLSQRBP + | TWL6030_HPLLENA; + break; + case 38400000: + /* clk slicer input, PLL disabled */ + hppll = TWL6030_MCLK_38400KHZ + | TWL6030_HPLLSQRENA + | TWL6030_HPLLBP; + break; + default: + dev_err(codec->dev, "unknown sysclk rate %d\n", freq); + return -EINVAL; + } + + twl6030_write(codec, TWL6030_REG_HPPLLCTL, hppll); + + /* Disable LPPLL and select HPPLL */ + lppll = TWL6030_HPLLSEL; + twl6030_write(codec, TWL6030_REG_LPPLLCTL, lppll); + + return 0; +} + +static int twl6030_set_dai_fmt(struct snd_soc_dai *codec_dai, + unsigned int fmt) +{ + int format = fmt & SND_SOC_DAIFMT_FORMAT_MASK; + + /* propietary dai format */ + if (format != SND_SOC_DAIFMT_MCPDM) + return -EINVAL; + + return 0; +} + +static struct snd_soc_dai_ops twl6030_dai_ops = { + .hw_params = twl6030_hw_params, + .set_sysclk = twl6030_set_dai_sysclk, + .set_fmt = twl6030_set_dai_fmt, +}; + +struct snd_soc_dai twl6030_dai = { + .name = "twl6030", + .playback = { + .stream_name = "Playback", + .channels_min = 1, + .channels_max = 4, + .rates = TWL6030_RATES, + .formats = TWL6030_FORMATS, + }, + .capture = { + .stream_name = "Capture", + .channels_min = 1, + .channels_max = 2, + .rates = TWL6030_RATES, + .formats = TWL6030_FORMATS, + }, + .ops = &twl6030_dai_ops, +}; +EXPORT_SYMBOL_GPL(twl6030_dai); + +static int twl6030_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; + + twl6030_set_bias_level(codec, SND_SOC_BIAS_OFF); + + return 0; +} + +static int twl6030_resume(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->card->codec; + + twl6030_set_bias_level(codec, SND_SOC_BIAS_STANDBY); + twl6030_set_bias_level(codec, codec->suspend_bias_level); + return 0; +} + +/* + * initialize the driver + */ +static int twl6030_init(struct snd_soc_device *socdev) +{ + struct snd_soc_codec *codec = socdev->card->codec; + int ret = 0; + + dev_info(codec->dev, "TWL6030 Audio Codec init\n"); + + codec->name = "twl6030"; + codec->owner = THIS_MODULE; + codec->read = twl6030_read_reg_cache; + codec->write = twl6030_write; + codec->set_bias_level = twl6030_set_bias_level; + codec->dai = &twl6030_dai; + codec->num_dai = 1; + codec->reg_cache_size = ARRAY_SIZE(twl6030_reg); + codec->reg_cache = kmemdup(twl6030_reg, sizeof(twl6030_reg), + GFP_KERNEL); + if (codec->reg_cache == NULL) + return -ENOMEM; + + /* 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\n"); + goto pcm_err; + } + + /* power on device */ + twl6030_set_bias_level(codec, SND_SOC_BIAS_STANDBY); + + twl6030_init_chip(codec); + + snd_soc_add_controls(codec, twl6030_snd_controls, + ARRAY_SIZE(twl6030_snd_controls)); + twl6030_add_widgets(codec); + + ret = snd_soc_init_card(socdev); + if (ret < 0) { + dev_err(codec->dev, "failed to register card\n"); + goto card_err; + } + + return ret; + +card_err: + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); +pcm_err: + kfree(codec->reg_cache); + return ret; +} + +static struct snd_soc_device *twl6030_socdev; + +static int twl6030_probe(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec; + + codec = kzalloc(sizeof(struct snd_soc_codec), GFP_KERNEL); + if (codec == NULL) + return -ENOMEM; + + codec->dev = &pdev->dev; + socdev->card->codec = codec; + mutex_init(&codec->mutex); + INIT_LIST_HEAD(&codec->dapm_widgets); + INIT_LIST_HEAD(&codec->dapm_paths); + + twl6030_socdev = socdev; + twl6030_init(socdev); + + return 0; +} + +static int twl6030_remove(struct platform_device *pdev) +{ + struct snd_soc_device *socdev = platform_get_drvdata(pdev); + struct snd_soc_codec *codec = socdev->card->codec; + + dev_info(codec->dev, "TWL6030 Audio Codec remove\n"); + twl6030_set_bias_level(codec, SND_SOC_BIAS_OFF); + snd_soc_free_pcms(socdev); + snd_soc_dapm_free(socdev); + kfree(codec); + + return 0; +} + +struct snd_soc_codec_device soc_codec_dev_twl6030 = { + .probe = twl6030_probe, + .remove = twl6030_remove, + .suspend = twl6030_suspend, + .resume = twl6030_resume, +}; +EXPORT_SYMBOL_GPL(soc_codec_dev_twl6030); + +static int __init twl6030_modinit(void) +{ + return snd_soc_register_dai(&twl6030_dai); +} +module_init(twl6030_modinit); + +static void __exit twl6030_exit(void) +{ + snd_soc_unregister_dai(&twl6030_dai); +} +module_exit(twl6030_exit); + +MODULE_DESCRIPTION("ASoC TWL6030 codec driver"); +MODULE_AUTHOR("Misael Lopez Cruz"); +MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/twl6030.h b/sound/soc/codecs/twl6030.h new file mode 100644 index 0000000..a83f9e7 --- /dev/null +++ b/sound/soc/codecs/twl6030.h @@ -0,0 +1,95 @@ +/* + * ALSA SoC TWL6030 codec driver + * + * Author: Misael Lopez Cruz x0052729@ti.com + * + * 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. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA + * + */ + +#ifndef __TWL6030_H__ +#define __TWL6030_H__ + +#define TWL6030_REG_ASICID 0x01 +#define TWL6030_REG_ASICREV 0x02 +#define TWL6030_REG_INTID 0x03 +#define TWL6030_REG_INTMR 0x04 +#define TWL6030_REG_NCPCTL 0x05 +#define TWL6030_REG_LDOCTL 0x06 +#define TWL6030_REG_HPPLLCTL 0x07 +#define TWL6030_REG_LPPLLCTL 0x08 +#define TWL6030_REG_LPPLLDIV 0x09 +#define TWL6030_REG_AMICBCTL 0x0A +#define TWL6030_REG_DMICBCTL 0x0B +#define TWL6030_REG_MICLCTL 0x0C +#define TWL6030_REG_MICRCTL 0x0D +#define TWL6030_REG_MICGAIN 0x0E +#define TWL6030_REG_LINEGAIN 0x0F +#define TWL6030_REG_HSLCTL 0x10 +#define TWL6030_REG_HSRCTL 0x11 +#define TWL6030_REG_HSGAIN 0x12 +#define TWL6030_REG_EARCTL 0x13 +#define TWL6030_REG_HFLCTL 0x14 +#define TWL6030_REG_HFLGAIN 0x15 +#define TWL6030_REG_HFRCTL 0x16 +#define TWL6030_REG_HFRGAIN 0x17 +#define TWL6030_REG_VIBCTLL 0x18 +#define TWL6030_REG_VIBDATL 0x19 +#define TWL6030_REG_VIBCTLR 0x1A +#define TWL6030_REG_VIBDATR 0x1B +#define TWL6030_REG_HKCTL1 0x1C +#define TWL6030_REG_HKCTL2 0x1D +#define TWL6030_REG_GPOCTL 0x1E +#define TWL6030_REG_ALB 0x1F +#define TWL6030_REG_DLB 0x20 +#define TWL6030_REG_TRIM1 0x28 +#define TWL6030_REG_TRIM2 0x29 +#define TWL6030_REG_TRIM3 0x2A +#define TWL6030_REG_HSOTRIM 0x2B +#define TWL6030_REG_HFOTRIM 0x2C +#define TWL6030_REG_ACCCTL 0x2D +#define TWL6030_REG_STATUS 0x2E + +#define TWL6030_CACHEREGNUM (TWL6030_REG_STATUS + 1) + +/* HPPLLCTL (0x07) fields */ + +#define TWL6030_HPLLENA 0x01 +#define TWL6030_HPLLRST 0x02 +#define TWL6030_HPLLBP 0x04 +#define TWL6030_HPLLSQRENA 0x08 +#define TWL6030_HPLLSQRBP 0x10 +#define TWL6030_MCLK_12000KHZ (0 << 5) +#define TWL6030_MCLK_19200KHZ (1 << 5) +#define TWL6030_MCLK_26000KHZ (2 << 5) +#define TWL6030_MCLK_38400KHZ (3 << 5) +#define TWL6030_MCLK_MSK 0x60 + +/* LPPLLCTL (0x08) fields */ + +#define TWL6030_LPLLENA 0x01 +#define TWL6030_LPLLRST 0x02 +#define TWL6030_LPLLSEL 0x04 +#define TWL6030_FIN 0x08 +#define TWL6030_HPLLSEL 0x10 + +extern struct snd_soc_dai twl6030_dai; +extern struct snd_soc_codec_device soc_codec_dev_twl6030; + +struct twl6030_setup_data { + void (*codec_enable)(int enable); +}; + +#endif /* End of __TWL6030_H__ */ -- 1.5.4.3
On Mon, Sep 14, 2009 at 12:00:25PM -0500, Lopez Cruz, Misael wrote:
+/* propietary formats */ +#define SND_SOC_DAIFMT_MCPDM 0x10 /* Texas Instruments McPDM */
This should really be split out into a separate patch. Are you absolutely positive that this is a proprietary interface that won't interoperate with standard PDM?
Also note that your format doesn't match up with the existing numbering scheme - they're all just 0, 1, 2, 3, ...
+#define TWL6030_RATES (SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000) +#define TWL6030_FORMATS (SNDRV_PCM_FMTBIT_S32_LE)
+static void twl6030_power_up(struct snd_soc_codec *codec) +{
struct snd_soc_device *socdev = codec->socdev;
struct twl6030_setup_data *setup = socdev->codec_data;
setup->codec_enable(1);
That's interesting...?
/* Capture gains */
SOC_DOUBLE_TLV("Capture Preamplifier (Attenuator) Volume",
TWL6030_REG_MICGAIN, 6, 7, 1, 1, mic_preamp_tlv),
No need to mention that it's an attenuator.
SOC_DOUBLE_TLV("Capture Amplifier Volume",
TWL6030_REG_MICGAIN, 0, 3, 4, 0, mic_amp_tlv),
Just "Capture Volume", probably.
+static int twl6030_set_bias_level(struct snd_soc_codec *codec,
enum snd_soc_bias_level level)
+{
switch (level) {
case SND_SOC_BIAS_ON:
twl6030_power_up(codec);
break;
case SND_SOC_BIAS_PREPARE:
twl6030_power_up(codec);
break;
case SND_SOC_BIAS_STANDBY:
twl6030_power_up(codec);
break;
case SND_SOC_BIAS_OFF:
twl6030_power_down(codec);
break;
Is there any reason not to just fold these functions into the bias management? It looks like the only caller and it'd save jumping around the file to find stuff.
+static int twl6030_init(struct snd_soc_device *socdev) +{
struct snd_soc_codec *codec = socdev->card->codec;
int ret = 0;
dev_info(codec->dev, "TWL6030 Audio Codec init\n");
The driver should be probing as a platform device rather than using old style registration - see wm8350 and wm8400 for examples.
/* power on device */
twl6030_set_bias_level(codec, SND_SOC_BIAS_STANDBY);
twl6030_init_chip(codec);
Is the the right ordering? I'd have expected to see the one time init stuff done prior to bringing up the power for the first time.
Mark Brown wrote:
On Mon, Sep 14, 2009 at 12:00:25PM -0500, Lopez Cruz, Misael wrote:
+/* propietary formats */ +#define SND_SOC_DAIFMT_MCPDM 0x10 /* Texas Instruments McPDM */
This should really be split out into a separate patch. Are you absolutely positive that this is a proprietary interface that won't interoperate with standard PDM?
I think channel slot definition won't make it able to interoperate with other PDM interfaces. But I may be wrong.
+static void twl6030_power_up(struct snd_soc_codec *codec) +{
struct snd_soc_device *socdev = codec->socdev;
struct twl6030_setup_data *setup = socdev->codec_data; +
setup->codec_enable(1);
That's interesting...?
The codec is turned on/off through an external line (i.e. with a gpio). Then, codec enable is board-dependent.
+static int twl6030_set_bias_level(struct snd_soc_codec *codec,
enum snd_soc_bias_level level) +{
switch (level) {
case SND_SOC_BIAS_ON:
twl6030_power_up(codec);
break;
case SND_SOC_BIAS_PREPARE:
twl6030_power_up(codec);
break;
case SND_SOC_BIAS_STANDBY:
twl6030_power_up(codec);
break;
case SND_SOC_BIAS_OFF:
twl6030_power_down(codec);
break;
Is there any reason not to just fold these functions into the bias management? It looks like the only caller and it'd save jumping around the file to find stuff.
For the moment, there is no reason. I thought it was more clear to have separate power_up/power_down functions, but I can merge them in bias management function.
/* power on device */
twl6030_set_bias_level(codec, SND_SOC_BIAS_STANDBY); +
twl6030_init_chip(codec);
Is the the right ordering? I'd have expected to see the one time init stuff done prior to bringing up the power for the first time.
Yes, it's the right order. codec chip cannot be initialized if the codec is not already power-up, registers are not accesible before that.
On Tue, Sep 15, 2009 at 12:59:44PM -0500, Lopez Cruz, Misael wrote:
Mark Brown wrote:
On Mon, Sep 14, 2009 at 12:00:25PM -0500, Lopez Cruz, Misael wrote:
+/* propietary formats */ +#define SND_SOC_DAIFMT_MCPDM 0x10 /* Texas Instruments McPDM */
This should really be split out into a separate patch. Are you absolutely positive that this is a proprietary interface that won't interoperate with standard PDM?
I think channel slot definition won't make it able to interoperate with other PDM interfaces. But I may be wrong.
I'd not expect full interoperability but I'd expect that at least the basic PDM support would interoperate happily. It wouldn't surprise me if more than one manufacturer came up with the same extension for multi channel PDM.
+static void twl6030_power_up(struct snd_soc_codec *codec) +{
struct snd_soc_device *socdev = codec->socdev;
struct twl6030_setup_data *setup = socdev->codec_data; +
setup->codec_enable(1);
That's interesting...?
The codec is turned on/off through an external line (i.e. with a gpio). Then, codec enable is board-dependent.
Might it make more sense to specify a GPIO line instead, at least by default?
+static int twl6030_set_bias_level(struct snd_soc_codec *codec,
enum snd_soc_bias_level level) +{
switch (level) {
case SND_SOC_BIAS_ON:
twl6030_power_up(codec);
Is there any reason not to just fold these functions into the bias management? It looks like the only caller and it'd save jumping around the file to find stuff.
For the moment, there is no reason. I thought it was more clear to have separate power_up/power_down functions, but I can merge them in bias management function.
It would be helpful since twl6030_power_up() will be called repeatedly during normal operation as we move STANDBY <-> PREPARE <-> ON so needs to be checked to ensure that it can safely be called repeatedly.
/* power on device */
twl6030_set_bias_level(codec, SND_SOC_BIAS_STANDBY); +
twl6030_init_chip(codec);
Is the the right ordering? I'd have expected to see the one time init stuff done prior to bringing up the power for the first time.
Yes, it's the right order. codec chip cannot be initialized if the codec is not already power-up, registers are not accesible before that.
OK. Looking at this from another angle, shouldn't the chip init be rolled into the bias level function to ensure that there aren't any cases where it is omitted.
It's possible that the core may get facilities to allow more use of SND_SOC_BIAS_OFF at runtime which would make this more important.
Mark Brown wrote:
On Tue, Sep 15, 2009 at 12:59:44PM -0500, Lopez Cruz, Misael wrote:
Mark Brown wrote:
On Mon, Sep 14, 2009 at 12:00:25PM -0500, Lopez Cruz, Misael wrote:
+/* propietary formats */ +#define SND_SOC_DAIFMT_MCPDM 0x10 /* Texas Instruments McPDM */
This should really be split out into a separate patch. Are you absolutely positive that this is a proprietary interface that won't interoperate with standard PDM?
I think channel slot definition won't make it able to interoperate with other PDM interfaces. But I may be wrong.
I'd not expect full interoperability but I'd expect that at least the basic PDM support would interoperate happily. It wouldn't surprise me if more than one manufacturer came up with the same extension for multi channel PDM.
If that's the case, then a more appropriate name should be chosen. Or is it fine for you _MCPDM?
I was thinking in adding _OMAP4_MCPDM, but if you think someone else can use the same extension, then _OMAP4 should not go in the name.
+static void twl6030_power_up(struct snd_soc_codec *codec) +{
struct snd_soc_device *socdev = codec->socdev;
struct twl6030_setup_data *setup = socdev->codec_data; +
setup->codec_enable(1);
That's interesting...?
The codec is turned on/off through an external line (i.e. with a gpio). Then, codec enable is board-dependent.
Might it make more sense to specify a GPIO line instead, at least by default?
Not sure, if the GPIO line is in TWL6030 (mfd) as well then probably it's fine, which may be the case for now. But isn't it violating CODEC independency anyway?
If you mean to sustitute the codec_enable function by the GPIO line, then it opens the possibility to make the CODEC to request and operate a GPIO line belonging to a different chip, for example to the application processor.
On the other hand, if a default GPIO line is provided and if it's not the correct one, the driver will be waiting forever for power-up sequence to finish (wait_for_completion). Anyway the wait_for_completion seems too agresive since codec power-up sequence might fail and boot process will hang.
/* power on device */
twl6030_set_bias_level(codec, SND_SOC_BIAS_STANDBY); +
twl6030_init_chip(codec);
Is the the right ordering? I'd have expected to see the one time init stuff done prior to bringing up the power for the first time.
Yes, it's the right order. codec chip cannot be initialized if the codec is not already power-up, registers are not accesible before that.
OK. Looking at this from another angle, shouldn't the chip init be rolled into the bias level function to ensure that there aren't any cases where it is omitted.
It's possible that the core may get facilities to allow more use of SND_SOC_BIAS_OFF at runtime which would make this more important.
Yes, that's true. Some register may get unconfigured when codec goes off. I'll check for those scenarios.
On Tue, Sep 15, 2009 at 05:39:09PM -0500, Lopez Cruz, Misael wrote:
Mark Brown wrote:
I'd not expect full interoperability but I'd expect that at least the basic PDM support would interoperate happily. It wouldn't surprise me if more than one manufacturer came up with the same extension for multi channel PDM.
If that's the case, then a more appropriate name should be chosen. Or is it fine for you _MCPDM?
I'd suggest just plain _PDM.
The codec is turned on/off through an external line (i.e. with a gpio). Then, codec enable is board-dependent.
Might it make more sense to specify a GPIO line instead, at least by default?
Not sure, if the GPIO line is in TWL6030 (mfd) as well then probably it's fine, which may be the case for now. But isn't it violating CODEC independency anyway?
I don't see an abstraction problem - gpiolib can handle this well enough.
If you mean to sustitute the codec_enable function by the GPIO line, then it opens the possibility to make the CODEC to request and operate a GPIO line belonging to a different chip, for example to the application processor.
If the CODEC GPIOs are supported via gpiolib that shouldn't be a problem. If you specify the GPIO as an int then it'll also be possible to have invalid values so that cases with the CODEC hard wired to power up will also be handled.
It just seems easier for boards to have the CODEC do the gpio request and so on so that it's a simple data value that needs to be set.
participants (2)
-
Lopez Cruz, Misael
-
Mark Brown