OSDN Git Service

Merge tag 'sound-4.2-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/tiwai...
[uclinux-h8/linux.git] / sound / soc / codecs / tas571x.c
diff --git a/sound/soc/codecs/tas571x.c b/sound/soc/codecs/tas571x.c
new file mode 100644 (file)
index 0000000..85bcc37
--- /dev/null
@@ -0,0 +1,514 @@
+/*
+ * TAS571x amplifier audio driver
+ *
+ * Copyright (C) 2015 Google, Inc.
+ * Copyright (c) 2013 Daniel Mack <zonque@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <linux/clk.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/stddef.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+#include "tas571x.h"
+
+#define TAS571X_MAX_SUPPLIES           6
+
+struct tas571x_chip {
+       const char                      *const *supply_names;
+       int                             num_supply_names;
+       const struct snd_kcontrol_new   *controls;
+       int                             num_controls;
+       const struct regmap_config      *regmap_config;
+       int                             vol_reg_size;
+};
+
+struct tas571x_private {
+       const struct tas571x_chip       *chip;
+       struct regmap                   *regmap;
+       struct regulator_bulk_data      supplies[TAS571X_MAX_SUPPLIES];
+       struct clk                      *mclk;
+       unsigned int                    format;
+       struct gpio_desc                *reset_gpio;
+       struct gpio_desc                *pdn_gpio;
+       struct snd_soc_codec_driver     codec_driver;
+};
+
+static int tas571x_register_size(struct tas571x_private *priv, unsigned int reg)
+{
+       switch (reg) {
+       case TAS571X_MVOL_REG:
+       case TAS571X_CH1_VOL_REG:
+       case TAS571X_CH2_VOL_REG:
+               return priv->chip->vol_reg_size;
+       default:
+               return 1;
+       }
+}
+
+static int tas571x_reg_write(void *context, unsigned int reg,
+                            unsigned int value)
+{
+       struct i2c_client *client = context;
+       struct tas571x_private *priv = i2c_get_clientdata(client);
+       unsigned int i, size;
+       uint8_t buf[5];
+       int ret;
+
+       size = tas571x_register_size(priv, reg);
+       buf[0] = reg;
+
+       for (i = size; i >= 1; --i) {
+               buf[i] = value;
+               value >>= 8;
+       }
+
+       ret = i2c_master_send(client, buf, size + 1);
+       if (ret == size + 1)
+               return 0;
+       else if (ret < 0)
+               return ret;
+       else
+               return -EIO;
+}
+
+static int tas571x_reg_read(void *context, unsigned int reg,
+                           unsigned int *value)
+{
+       struct i2c_client *client = context;
+       struct tas571x_private *priv = i2c_get_clientdata(client);
+       uint8_t send_buf, recv_buf[4];
+       struct i2c_msg msgs[2];
+       unsigned int size;
+       unsigned int i;
+       int ret;
+
+       size = tas571x_register_size(priv, reg);
+       send_buf = reg;
+
+       msgs[0].addr = client->addr;
+       msgs[0].len = sizeof(send_buf);
+       msgs[0].buf = &send_buf;
+       msgs[0].flags = 0;
+
+       msgs[1].addr = client->addr;
+       msgs[1].len = size;
+       msgs[1].buf = recv_buf;
+       msgs[1].flags = I2C_M_RD;
+
+       ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
+       if (ret < 0)
+               return ret;
+       else if (ret != ARRAY_SIZE(msgs))
+               return -EIO;
+
+       *value = 0;
+
+       for (i = 0; i < size; i++) {
+               *value <<= 8;
+               *value |= recv_buf[i];
+       }
+
+       return 0;
+}
+
+static int tas571x_set_dai_fmt(struct snd_soc_dai *dai, unsigned int format)
+{
+       struct tas571x_private *priv = snd_soc_codec_get_drvdata(dai->codec);
+
+       priv->format = format;
+
+       return 0;
+}
+
+static int tas571x_hw_params(struct snd_pcm_substream *substream,
+                            struct snd_pcm_hw_params *params,
+                            struct snd_soc_dai *dai)
+{
+       struct tas571x_private *priv = snd_soc_codec_get_drvdata(dai->codec);
+       u32 val;
+
+       switch (priv->format & SND_SOC_DAIFMT_FORMAT_MASK) {
+       case SND_SOC_DAIFMT_RIGHT_J:
+               val = 0x00;
+               break;
+       case SND_SOC_DAIFMT_I2S:
+               val = 0x03;
+               break;
+       case SND_SOC_DAIFMT_LEFT_J:
+               val = 0x06;
+               break;
+       default:
+               return -EINVAL;
+       }
+
+       if (params_width(params) >= 24)
+               val += 2;
+       else if (params_width(params) >= 20)
+               val += 1;
+
+       return regmap_update_bits(priv->regmap, TAS571X_SDI_REG,
+                                 TAS571X_SDI_FMT_MASK, val);
+}
+
+static int tas571x_set_bias_level(struct snd_soc_codec *codec,
+                                 enum snd_soc_bias_level level)
+{
+       struct tas571x_private *priv = snd_soc_codec_get_drvdata(codec);
+       int ret;
+
+       switch (level) {
+       case SND_SOC_BIAS_ON:
+               break;
+       case SND_SOC_BIAS_PREPARE:
+               break;
+       case SND_SOC_BIAS_STANDBY:
+               if (codec->dapm.bias_level == SND_SOC_BIAS_OFF) {
+                       if (!IS_ERR(priv->mclk)) {
+                               ret = clk_prepare_enable(priv->mclk);
+                               if (ret) {
+                                       dev_err(codec->dev,
+                                               "Failed to enable master clock: %d\n",
+                                               ret);
+                                       return ret;
+                               }
+                       }
+
+                       gpiod_set_value(priv->pdn_gpio, 0);
+                       usleep_range(5000, 6000);
+
+                       regcache_cache_only(priv->regmap, false);
+                       ret = regcache_sync(priv->regmap);
+                       if (ret)
+                               return ret;
+               }
+               break;
+       case SND_SOC_BIAS_OFF:
+               regcache_cache_only(priv->regmap, true);
+               gpiod_set_value(priv->pdn_gpio, 1);
+
+               if (!IS_ERR(priv->mclk))
+                       clk_disable_unprepare(priv->mclk);
+               break;
+       }
+
+       return 0;
+}
+
+static const struct snd_soc_dai_ops tas571x_dai_ops = {
+       .set_fmt        = tas571x_set_dai_fmt,
+       .hw_params      = tas571x_hw_params,
+};
+
+static const char *const tas5711_supply_names[] = {
+       "AVDD",
+       "DVDD",
+       "PVDD_A",
+       "PVDD_B",
+       "PVDD_C",
+       "PVDD_D",
+};
+
+static const DECLARE_TLV_DB_SCALE(tas5711_volume_tlv, -10350, 50, 1);
+
+static const struct snd_kcontrol_new tas5711_controls[] = {
+       SOC_SINGLE_TLV("Master Volume",
+                      TAS571X_MVOL_REG,
+                      0, 0xff, 1, tas5711_volume_tlv),
+       SOC_DOUBLE_R_TLV("Speaker Volume",
+                        TAS571X_CH1_VOL_REG,
+                        TAS571X_CH2_VOL_REG,
+                        0, 0xff, 1, tas5711_volume_tlv),
+       SOC_DOUBLE("Speaker Switch",
+                  TAS571X_SOFT_MUTE_REG,
+                  TAS571X_SOFT_MUTE_CH1_SHIFT, TAS571X_SOFT_MUTE_CH2_SHIFT,
+                  1, 1),
+};
+
+static const struct reg_default tas5711_reg_defaults[] = {
+       { 0x04, 0x05 },
+       { 0x05, 0x40 },
+       { 0x06, 0x00 },
+       { 0x07, 0xff },
+       { 0x08, 0x30 },
+       { 0x09, 0x30 },
+       { 0x1b, 0x82 },
+};
+
+static const struct regmap_config tas5711_regmap_config = {
+       .reg_bits                       = 8,
+       .val_bits                       = 32,
+       .max_register                   = 0xff,
+       .reg_read                       = tas571x_reg_read,
+       .reg_write                      = tas571x_reg_write,
+       .reg_defaults                   = tas5711_reg_defaults,
+       .num_reg_defaults               = ARRAY_SIZE(tas5711_reg_defaults),
+       .cache_type                     = REGCACHE_RBTREE,
+};
+
+static const struct tas571x_chip tas5711_chip = {
+       .supply_names                   = tas5711_supply_names,
+       .num_supply_names               = ARRAY_SIZE(tas5711_supply_names),
+       .controls                       = tas5711_controls,
+       .num_controls                   = ARRAY_SIZE(tas5711_controls),
+       .regmap_config                  = &tas5711_regmap_config,
+       .vol_reg_size                   = 1,
+};
+
+static const char *const tas5717_supply_names[] = {
+       "AVDD",
+       "DVDD",
+       "HPVDD",
+       "PVDD_AB",
+       "PVDD_CD",
+};
+
+static const DECLARE_TLV_DB_SCALE(tas5717_volume_tlv, -10375, 25, 0);
+
+static const struct snd_kcontrol_new tas5717_controls[] = {
+       /* MVOL LSB is ignored - see comments in tas571x_i2c_probe() */
+       SOC_SINGLE_TLV("Master Volume",
+                      TAS571X_MVOL_REG, 1, 0x1ff, 1,
+                      tas5717_volume_tlv),
+       SOC_DOUBLE_R_TLV("Speaker Volume",
+                        TAS571X_CH1_VOL_REG, TAS571X_CH2_VOL_REG,
+                        1, 0x1ff, 1, tas5717_volume_tlv),
+       SOC_DOUBLE("Speaker Switch",
+                  TAS571X_SOFT_MUTE_REG,
+                  TAS571X_SOFT_MUTE_CH1_SHIFT, TAS571X_SOFT_MUTE_CH2_SHIFT,
+                  1, 1),
+};
+
+static const struct reg_default tas5717_reg_defaults[] = {
+       { 0x04, 0x05 },
+       { 0x05, 0x40 },
+       { 0x06, 0x00 },
+       { 0x07, 0x03ff },
+       { 0x08, 0x00c0 },
+       { 0x09, 0x00c0 },
+       { 0x1b, 0x82 },
+};
+
+static const struct regmap_config tas5717_regmap_config = {
+       .reg_bits                       = 8,
+       .val_bits                       = 32,
+       .max_register                   = 0xff,
+       .reg_read                       = tas571x_reg_read,
+       .reg_write                      = tas571x_reg_write,
+       .reg_defaults                   = tas5717_reg_defaults,
+       .num_reg_defaults               = ARRAY_SIZE(tas5717_reg_defaults),
+       .cache_type                     = REGCACHE_RBTREE,
+};
+
+/* This entry is reused for tas5719 as the software interface is identical. */
+static const struct tas571x_chip tas5717_chip = {
+       .supply_names                   = tas5717_supply_names,
+       .num_supply_names               = ARRAY_SIZE(tas5717_supply_names),
+       .controls                       = tas5717_controls,
+       .num_controls                   = ARRAY_SIZE(tas5717_controls),
+       .regmap_config                  = &tas5717_regmap_config,
+       .vol_reg_size                   = 2,
+};
+
+static const struct snd_soc_dapm_widget tas571x_dapm_widgets[] = {
+       SND_SOC_DAPM_DAC("DACL", NULL, SND_SOC_NOPM, 0, 0),
+       SND_SOC_DAPM_DAC("DACR", NULL, SND_SOC_NOPM, 0, 0),
+
+       SND_SOC_DAPM_OUTPUT("OUT_A"),
+       SND_SOC_DAPM_OUTPUT("OUT_B"),
+       SND_SOC_DAPM_OUTPUT("OUT_C"),
+       SND_SOC_DAPM_OUTPUT("OUT_D"),
+};
+
+static const struct snd_soc_dapm_route tas571x_dapm_routes[] = {
+       { "DACL",  NULL, "Playback" },
+       { "DACR",  NULL, "Playback" },
+
+       { "OUT_A", NULL, "DACL" },
+       { "OUT_B", NULL, "DACL" },
+       { "OUT_C", NULL, "DACR" },
+       { "OUT_D", NULL, "DACR" },
+};
+
+static const struct snd_soc_codec_driver tas571x_codec = {
+       .set_bias_level = tas571x_set_bias_level,
+       .idle_bias_off = true,
+
+       .dapm_widgets = tas571x_dapm_widgets,
+       .num_dapm_widgets = ARRAY_SIZE(tas571x_dapm_widgets),
+       .dapm_routes = tas571x_dapm_routes,
+       .num_dapm_routes = ARRAY_SIZE(tas571x_dapm_routes),
+};
+
+static struct snd_soc_dai_driver tas571x_dai = {
+       .name = "tas571x-hifi",
+       .playback = {
+               .stream_name = "Playback",
+               .channels_min = 2,
+               .channels_max = 2,
+               .rates = SNDRV_PCM_RATE_8000_48000,
+               .formats = SNDRV_PCM_FMTBIT_S32_LE |
+                          SNDRV_PCM_FMTBIT_S24_LE |
+                          SNDRV_PCM_FMTBIT_S16_LE,
+       },
+       .ops = &tas571x_dai_ops,
+};
+
+static const struct of_device_id tas571x_of_match[];
+
+static int tas571x_i2c_probe(struct i2c_client *client,
+                            const struct i2c_device_id *id)
+{
+       struct tas571x_private *priv;
+       struct device *dev = &client->dev;
+       const struct of_device_id *of_id;
+       int i, ret;
+
+       priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+       if (!priv)
+               return -ENOMEM;
+       i2c_set_clientdata(client, priv);
+
+       of_id = of_match_device(tas571x_of_match, dev);
+       if (!of_id) {
+               dev_err(dev, "Unknown device type\n");
+               return -EINVAL;
+       }
+       priv->chip = of_id->data;
+
+       priv->mclk = devm_clk_get(dev, "mclk");
+       if (IS_ERR(priv->mclk) && PTR_ERR(priv->mclk) != -ENOENT) {
+               dev_err(dev, "Failed to request mclk: %ld\n",
+                       PTR_ERR(priv->mclk));
+               return PTR_ERR(priv->mclk);
+       }
+
+       BUG_ON(priv->chip->num_supply_names > TAS571X_MAX_SUPPLIES);
+       for (i = 0; i < priv->chip->num_supply_names; i++)
+               priv->supplies[i].supply = priv->chip->supply_names[i];
+
+       ret = devm_regulator_bulk_get(dev, priv->chip->num_supply_names,
+                                     priv->supplies);
+       if (ret) {
+               dev_err(dev, "Failed to get supplies: %d\n", ret);
+               return ret;
+       }
+       ret = regulator_bulk_enable(priv->chip->num_supply_names,
+                                   priv->supplies);
+       if (ret) {
+               dev_err(dev, "Failed to enable supplies: %d\n", ret);
+               return ret;
+       }
+
+       priv->regmap = devm_regmap_init(dev, NULL, client,
+                                       priv->chip->regmap_config);
+       if (IS_ERR(priv->regmap))
+               return PTR_ERR(priv->regmap);
+
+       priv->pdn_gpio = devm_gpiod_get_optional(dev, "pdn", GPIOD_OUT_LOW);
+       if (IS_ERR(priv->pdn_gpio)) {
+               dev_err(dev, "error requesting pdn_gpio: %ld\n",
+                       PTR_ERR(priv->pdn_gpio));
+               return PTR_ERR(priv->pdn_gpio);
+       }
+
+       priv->reset_gpio = devm_gpiod_get_optional(dev, "reset",
+                                                  GPIOD_OUT_HIGH);
+       if (IS_ERR(priv->reset_gpio)) {
+               dev_err(dev, "error requesting reset_gpio: %ld\n",
+                       PTR_ERR(priv->reset_gpio));
+               return PTR_ERR(priv->reset_gpio);
+       } else if (priv->reset_gpio) {
+               /* pulse the active low reset line for ~100us */
+               usleep_range(100, 200);
+               gpiod_set_value(priv->reset_gpio, 0);
+               usleep_range(12000, 20000);
+       }
+
+       ret = regmap_write(priv->regmap, TAS571X_OSC_TRIM_REG, 0);
+       if (ret)
+               return ret;
+
+       ret = regmap_update_bits(priv->regmap, TAS571X_SYS_CTRL_2_REG,
+                                TAS571X_SYS_CTRL_2_SDN_MASK, 0);
+       if (ret)
+               return ret;
+
+       memcpy(&priv->codec_driver, &tas571x_codec, sizeof(priv->codec_driver));
+       priv->codec_driver.controls = priv->chip->controls;
+       priv->codec_driver.num_controls = priv->chip->num_controls;
+
+       if (priv->chip->vol_reg_size == 2) {
+               /*
+                * The master volume defaults to 0x3ff (mute), but we ignore
+                * (zero) the LSB because the hardware step size is 0.125 dB
+                * and TLV_DB_SCALE_ITEM has a resolution of 0.01 dB.
+                */
+               ret = regmap_update_bits(priv->regmap, TAS571X_MVOL_REG, 1, 0);
+               if (ret)
+                       return ret;
+       }
+
+       regcache_cache_only(priv->regmap, true);
+       gpiod_set_value(priv->pdn_gpio, 1);
+
+       return snd_soc_register_codec(&client->dev, &priv->codec_driver,
+                                     &tas571x_dai, 1);
+}
+
+static int tas571x_i2c_remove(struct i2c_client *client)
+{
+       struct tas571x_private *priv = i2c_get_clientdata(client);
+
+       snd_soc_unregister_codec(&client->dev);
+       regulator_bulk_disable(priv->chip->num_supply_names, priv->supplies);
+
+       return 0;
+}
+
+static const struct of_device_id tas571x_of_match[] = {
+       { .compatible = "ti,tas5711", .data = &tas5711_chip, },
+       { .compatible = "ti,tas5717", .data = &tas5717_chip, },
+       { .compatible = "ti,tas5719", .data = &tas5717_chip, },
+       { }
+};
+MODULE_DEVICE_TABLE(of, tas571x_of_match);
+
+static const struct i2c_device_id tas571x_i2c_id[] = {
+       { "tas5711", 0 },
+       { "tas5717", 0 },
+       { "tas5719", 0 },
+       { }
+};
+MODULE_DEVICE_TABLE(i2c, tas571x_i2c_id);
+
+static struct i2c_driver tas571x_i2c_driver = {
+       .driver = {
+               .name = "tas571x",
+               .of_match_table = of_match_ptr(tas571x_of_match),
+       },
+       .probe = tas571x_i2c_probe,
+       .remove = tas571x_i2c_remove,
+       .id_table = tas571x_i2c_id,
+};
+module_i2c_driver(tas571x_i2c_driver);
+
+MODULE_DESCRIPTION("ASoC TAS571x driver");
+MODULE_AUTHOR("Kevin Cernekee <cernekee@chromium.org>");
+MODULE_LICENSE("GPL");