diff mbox series

[1/2] ASoC: add support for TAS5805M digital amplifier

Message ID 61dccc59.1c69fb81.e1d98.02e3@mx.google.com (mailing list archive)
State Superseded
Headers show
Series ASoC: add support for TAS5805M digital amplifier | expand

Commit Message

Daniel Beer Jan. 10, 2022, 11:53 p.m. UTC
The Texas Instruments TAS5805M is a class D audio amplifier with an
integrated DSP. DSP configuration is expected to be supplied via a
device-tree attribute. See the bindings file for more details.

Signed-off-by: Daniel Beer <daniel.beer@igorinstitute.com>
---
 sound/soc/codecs/Kconfig    |   9 +
 sound/soc/codecs/Makefile   |   2 +
 sound/soc/codecs/tas5805m.c | 534 ++++++++++++++++++++++++++++++++++++
 3 files changed, 545 insertions(+)
 create mode 100644 sound/soc/codecs/tas5805m.c

Comments

Mark Brown Jan. 11, 2022, 5:13 p.m. UTC | #1
On Tue, Jan 11, 2022 at 12:53:11PM +1300, Daniel Beer wrote:

> +++ b/sound/soc/codecs/tas5805m.c
> @@ -0,0 +1,534 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Driver for the TAS5805M Audio Amplifier
> + *

Please make the entire comment a C++ one so things look more
intentional.

> +static void tas5805m_refresh_unlocked(struct snd_soc_component *component)
> +{
> +	struct tas5805m_priv *tas5805m =
> +		snd_soc_component_get_drvdata(component);
> +	uint8_t v[4];
> +	unsigned int i;
> +	uint32_t x;

> +	snd_soc_component_write(component, REG_PAGE, 0x00);
> +	snd_soc_component_write(component, REG_BOOK, 0x8c);
> +	snd_soc_component_write(component, REG_PAGE, 0x2a);

This isn't using the regmap paging support and I'm not seeing anything
that resets the page here.

> +	for (i = 0; i < 4; i++)
> +		snd_soc_component_write(component, 0x24 + i, v[i]);
> +	for (i = 0; i < 4; i++)
> +		snd_soc_component_write(component, 0x28 + i, v[i]);

This looks like it's potentially a stereo control but we're not allowing
the two channels to be configured separately?

> +	/* Volume controls */
> +	snd_soc_component_write(component, REG_DEVICE_CTRL_2,
> +		tas5805m->is_muted ?  0x0b : 0x03);

This comment doesn't seem terribly descriptive and this is very much a
magic number.

> +static int tas5805m_vol_get(struct snd_kcontrol *kcontrol,
> +			    struct snd_ctl_elem_value *ucontrol)
> +{
> +	struct snd_soc_component *component =
> +		snd_soc_kcontrol_component(kcontrol);
> +	struct tas5805m_priv *tas5805m =
> +		snd_soc_component_get_drvdata(component);
> +
> +	mutex_lock(&tas5805m->lock);
> +	ucontrol->value.integer.value[0] = tas5805m->vol;
> +	mutex_unlock(&tas5805m->lock);

What is this lock intended to protect?  We take the lock, do a read of a
single value and then unlock which doesn't seem like it's adding a huge
amount.

> +
> +	mutex_lock(&tas5805m->lock);
> +	tas5805m->vol = clamp((int)ucontrol->value.integer.value[0],
> +			      TAS5805M_VOLUME_MIN, TAS5805M_VOLUME_MAX);

It would be better to reject out of bounds values with an error.

> +static void send_cfg(struct snd_soc_component *component,
> +		     const uint8_t *s, unsigned int len)
> +{
> +	unsigned int i;
> +
> +	for (i = 0; i + 1 < len; i += 2)
> +		snd_soc_component_write(component, s[i], s[i + 1]);

This looks like the driver might be happier using regmap directly, this
looks like an open coded _multi_reg_write().

> +/* The TAS5805M can't be configured or brought out of power-down without
> + * an I2S clock. In power-down, registers are reset.
> + *
> + * We rely on DAPM not powering up the DAC widget until the source for
> + * it is ready, which we think implies that the I2S clock is present and
> + * stable.
> + */

That's not true, we run DAPM in prepare() prior to calling trigger() and
controllers might not start clocking the bus until trigger() has been
called.  There was some other device with similar issues which was
scheduling things from the trigger() callback IIRC, if you search around
in the CODEC directory you should be able to find something.

> +	dev_set_drvdata(dev, tas5805m);
> +	tas5805m->regmap = regmap;
> +	tas5805m->gpio_pdn_n = of_get_named_gpio(dev->of_node, "pdn-gpio", 0);
> +	if (!gpio_is_valid(tas5805m->gpio_pdn_n)) {
> +		dev_err(dev, "power-down GPIO not specified\n");
> +		return -EINVAL;
> +	}

Use the gpiod APIs, you shouldn't need to do anything specific to OF
here.  Also GPIO properties need to have names ending -gpios even if
they're for a single GPIO.

> +	ret = devm_snd_soc_register_component(dev, &soc_codec_dev_tas5805m,
> +					      &tas5805m_dai, 1);
> +	if (ret < 0) {
> +		dev_err(dev, "unable to register codec: %d\n", ret);
> +		return ret;
> +	}
> +
> +	ret = regulator_enable(tas5805m->pvdd);
> +	if (ret < 0) {
> +		dev_err(dev, "failed to enable pvdd: %d\n", ret);
> +		return ret;
> +	}
> +
> +	usleep_range(100000, 150000);
> +	gpio_set_value(tas5805m->gpio_pdn_n, 1);
> +	usleep_range(10000, 15000);

This seems broken - the card might be instantiated and userspace start
using it as soon as the component is registered but we don't power on
the device until after registering it and there's no runtime power up or
down.  For power management like this you need to complete the initial
power on prior to registering the component.

> +static int tas5805m_i2c_remove(struct i2c_client *i2c)
> +{
> +	struct tas5805m_priv *tas5805m = dev_get_drvdata(&i2c->dev);
> +
> +	gpio_set_value(tas5805m->gpio_pdn_n, 0);
> +	usleep_range(10000, 15000);
> +	regulator_disable(tas5805m->pvdd);

This will power down the device prior to unregistering the component so
userspace could still be running.  You need to not use devm_ for
component registration and manually unregister here to avoid that issue
(or register a custom devm action).
Daniel Beer Jan. 11, 2022, 7:28 p.m. UTC | #2
On Tue, Jan 11, 2022 at 05:13:11PM +0000, Mark Brown wrote:
> > +static void tas5805m_refresh_unlocked(struct snd_soc_component *component)
> > +{
> > +	struct tas5805m_priv *tas5805m =
> > +		snd_soc_component_get_drvdata(component);
> > +	uint8_t v[4];
> > +	unsigned int i;
> > +	uint32_t x;
> 
> > +	snd_soc_component_write(component, REG_PAGE, 0x00);
> > +	snd_soc_component_write(component, REG_BOOK, 0x8c);
> > +	snd_soc_component_write(component, REG_PAGE, 0x2a);
> 
> This isn't using the regmap paging support and I'm not seeing anything
> that resets the page here.

Hi Mark,

The blob of register writes we get given from PPC3 contains a whole lot
of explicit page and book changes, and there's not an easy way to tell
regmap about this, as far as I know. Do you think it's acceptable to
stick with explicit paging for this reason, or is there a way to make
this work with regmap's paging?

I'll go ahead and address your other comments before resubmitting.

Cheers,
Daniel
Mark Brown Jan. 12, 2022, 8:26 p.m. UTC | #3
On Wed, Jan 12, 2022 at 08:28:06AM +1300, Daniel Beer wrote:

> The blob of register writes we get given from PPC3 contains a whole lot
> of explicit page and book changes, and there's not an easy way to tell
> regmap about this, as far as I know. Do you think it's acceptable to
> stick with explicit paging for this reason, or is there a way to make
> this work with regmap's paging?

That's probably fine.  It's *really* hard to get enthusiastic about this
system design TBH, just injecting a stream of unverified register writes 
is going to make the driver very fragile against changes but I'm not
sure you're going to much better there.
diff mbox series

Patch

diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index d3e5ae8310ef..d6b8f5cb6ef8 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -1485,6 +1485,15 @@  config SND_SOC_TAS5720
 	  Enable support for Texas Instruments TAS5720L/M high-efficiency mono
 	  Class-D audio power amplifiers.
 
+config SND_SOC_TAS5805M
+	tristate "Texas Instruments TAS5805M speaker amplifier"
+	depends on I2C
+	help
+	  Enable support for Texas Instruments TAS5805M Class-D
+	  amplifiers. This is a speaker amplifier with an integrated
+	  DSP. DSP configuration for each instance needs to be supplied
+	  via a device-tree attribute.
+
 config SND_SOC_TAS6424
 	tristate "Texas Instruments TAS6424 Quad-Channel Audio amplifier"
 	depends on I2C
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index ac7f20972470..b4e11c3e4a08 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -236,6 +236,7 @@  snd-soc-sti-sas-objs := sti-sas.o
 snd-soc-tas5086-objs := tas5086.o
 snd-soc-tas571x-objs := tas571x.o
 snd-soc-tas5720-objs := tas5720.o
+snd-soc-tas5805m-objs := tas5805m.o
 snd-soc-tas6424-objs := tas6424.o
 snd-soc-tda7419-objs := tda7419.o
 snd-soc-tas2770-objs := tas2770.o
@@ -574,6 +575,7 @@  obj-$(CONFIG_SND_SOC_TAS2764)	+= snd-soc-tas2764.o
 obj-$(CONFIG_SND_SOC_TAS5086)	+= snd-soc-tas5086.o
 obj-$(CONFIG_SND_SOC_TAS571X)	+= snd-soc-tas571x.o
 obj-$(CONFIG_SND_SOC_TAS5720)	+= snd-soc-tas5720.o
+obj-$(CONFIG_SND_SOC_TAS5805M)	+= snd-soc-tas5805m.o
 obj-$(CONFIG_SND_SOC_TAS6424)	+= snd-soc-tas6424.o
 obj-$(CONFIG_SND_SOC_TDA7419)	+= snd-soc-tda7419.o
 obj-$(CONFIG_SND_SOC_TAS2770) += snd-soc-tas2770.o
diff --git a/sound/soc/codecs/tas5805m.c b/sound/soc/codecs/tas5805m.c
new file mode 100644
index 000000000000..efbdff0f5180
--- /dev/null
+++ b/sound/soc/codecs/tas5805m.c
@@ -0,0 +1,534 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Driver for the TAS5805M Audio Amplifier
+ *
+ * Author: Andy Liu <andy-liu@ti.com>
+ * Author: Daniel Beer <daniel.beer@igorinstitute.com>
+ *
+ * This is based on a driver originally written by Andy Liu at TI and
+ * posted here:
+ *
+ *    https://e2e.ti.com/support/audio-group/audio/f/audio-forum/722027/linux-tas5825m-linux-drivers
+ *
+ * It has been simplified a little and reworked for the 5.x ALSA SoC API.
+ */
+
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/of.h>
+#include <linux/init.h>
+#include <linux/i2c.h>
+#include <linux/regmap.h>
+#include <linux/gpio.h>
+#include <linux/of_gpio.h>
+#include <linux/regulator/consumer.h>
+#include <linux/atomic.h>
+#include <linux/workqueue.h>
+
+#include <sound/soc.h>
+#include <sound/pcm.h>
+#include <sound/initval.h>
+
+#define REG_PAGE		0x00
+#define REG_DEVICE_CTRL_1	0x02
+#define REG_DEVICE_CTRL_2	0x03
+#define REG_SIG_CH_CTRL		0x28
+#define REG_SAP_CTRL_1		0x33
+#define REG_FS_MON		0x37
+#define REG_BCK_MON		0x38
+#define REG_CLKDET_STATUS	0x39
+#define REG_VOL_CTL		0x4c
+#define REG_AGAIN		0x54
+#define REG_ADR_PIN_CTRL	0x60
+#define REG_ADR_PIN_CONFIG	0x61
+#define REG_CHAN_FAULT		0x70
+#define REG_GLOBAL_FAULT1	0x71
+#define REG_GLOBAL_FAULT2	0x72
+#define REG_FAULT		0x78
+#define REG_BOOK		0x7f
+
+/* This sequence of register writes must always be sent, prior to the
+ * 5ms delay while we wait for the DSP to boot.
+ */
+static const uint8_t dsp_cfg_preboot[] = {
+	0x00, 0x00, 0x7f, 0x00, 0x03, 0x02, 0x01, 0x11,
+	0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	0x00, 0x00, 0x7f, 0x00, 0x03, 0x02,
+};
+
+static const uint32_t tas5805m_volume[] = {
+	0x0000001B, /*   0, -110dB */ 0x0000001E, /*   1, -109dB */
+	0x00000021, /*   2, -108dB */ 0x00000025, /*   3, -107dB */
+	0x0000002A, /*   4, -106dB */ 0x0000002F, /*   5, -105dB */
+	0x00000035, /*   6, -104dB */ 0x0000003B, /*   7, -103dB */
+	0x00000043, /*   8, -102dB */ 0x0000004B, /*   9, -101dB */
+	0x00000054, /*  10, -100dB */ 0x0000005E, /*  11,  -99dB */
+	0x0000006A, /*  12,  -98dB */ 0x00000076, /*  13,  -97dB */
+	0x00000085, /*  14,  -96dB */ 0x00000095, /*  15,  -95dB */
+	0x000000A7, /*  16,  -94dB */ 0x000000BC, /*  17,  -93dB */
+	0x000000D3, /*  18,  -92dB */ 0x000000EC, /*  19,  -91dB */
+	0x00000109, /*  20,  -90dB */ 0x0000012A, /*  21,  -89dB */
+	0x0000014E, /*  22,  -88dB */ 0x00000177, /*  23,  -87dB */
+	0x000001A4, /*  24,  -86dB */ 0x000001D8, /*  25,  -85dB */
+	0x00000211, /*  26,  -84dB */ 0x00000252, /*  27,  -83dB */
+	0x0000029A, /*  28,  -82dB */ 0x000002EC, /*  29,  -81dB */
+	0x00000347, /*  30,  -80dB */ 0x000003AD, /*  31,  -79dB */
+	0x00000420, /*  32,  -78dB */ 0x000004A1, /*  33,  -77dB */
+	0x00000532, /*  34,  -76dB */ 0x000005D4, /*  35,  -75dB */
+	0x0000068A, /*  36,  -74dB */ 0x00000756, /*  37,  -73dB */
+	0x0000083B, /*  38,  -72dB */ 0x0000093C, /*  39,  -71dB */
+	0x00000A5D, /*  40,  -70dB */ 0x00000BA0, /*  41,  -69dB */
+	0x00000D0C, /*  42,  -68dB */ 0x00000EA3, /*  43,  -67dB */
+	0x0000106C, /*  44,  -66dB */ 0x0000126D, /*  45,  -65dB */
+	0x000014AD, /*  46,  -64dB */ 0x00001733, /*  47,  -63dB */
+	0x00001A07, /*  48,  -62dB */ 0x00001D34, /*  49,  -61dB */
+	0x000020C5, /*  50,  -60dB */ 0x000024C4, /*  51,  -59dB */
+	0x00002941, /*  52,  -58dB */ 0x00002E49, /*  53,  -57dB */
+	0x000033EF, /*  54,  -56dB */ 0x00003A45, /*  55,  -55dB */
+	0x00004161, /*  56,  -54dB */ 0x0000495C, /*  57,  -53dB */
+	0x0000524F, /*  58,  -52dB */ 0x00005C5A, /*  59,  -51dB */
+	0x0000679F, /*  60,  -50dB */ 0x00007444, /*  61,  -49dB */
+	0x00008274, /*  62,  -48dB */ 0x0000925F, /*  63,  -47dB */
+	0x0000A43B, /*  64,  -46dB */ 0x0000B845, /*  65,  -45dB */
+	0x0000CEC1, /*  66,  -44dB */ 0x0000E7FB, /*  67,  -43dB */
+	0x00010449, /*  68,  -42dB */ 0x0001240C, /*  69,  -41dB */
+	0x000147AE, /*  70,  -40dB */ 0x00016FAA, /*  71,  -39dB */
+	0x00019C86, /*  72,  -38dB */ 0x0001CEDC, /*  73,  -37dB */
+	0x00020756, /*  74,  -36dB */ 0x000246B5, /*  75,  -35dB */
+	0x00028DCF, /*  76,  -34dB */ 0x0002DD96, /*  77,  -33dB */
+	0x00033718, /*  78,  -32dB */ 0x00039B87, /*  79,  -31dB */
+	0x00040C37, /*  80,  -30dB */ 0x00048AA7, /*  81,  -29dB */
+	0x00051884, /*  82,  -28dB */ 0x0005B7B1, /*  83,  -27dB */
+	0x00066A4A, /*  84,  -26dB */ 0x000732AE, /*  85,  -25dB */
+	0x00081385, /*  86,  -24dB */ 0x00090FCC, /*  87,  -23dB */
+	0x000A2ADB, /*  88,  -22dB */ 0x000B6873, /*  89,  -21dB */
+	0x000CCCCD, /*  90,  -20dB */ 0x000E5CA1, /*  91,  -19dB */
+	0x00101D3F, /*  92,  -18dB */ 0x0012149A, /*  93,  -17dB */
+	0x00144961, /*  94,  -16dB */ 0x0016C311, /*  95,  -15dB */
+	0x00198A13, /*  96,  -14dB */ 0x001CA7D7, /*  97,  -13dB */
+	0x002026F3, /*  98,  -12dB */ 0x00241347, /*  99,  -11dB */
+	0x00287A27, /* 100,  -10dB */ 0x002D6A86, /* 101,  -9dB */
+	0x0032F52D, /* 102,   -8dB */ 0x00392CEE, /* 103,   -7dB */
+	0x004026E7, /* 104,   -6dB */ 0x0047FACD, /* 105,   -5dB */
+	0x0050C336, /* 106,   -4dB */ 0x005A9DF8, /* 107,   -3dB */
+	0x0065AC8C, /* 108,   -2dB */ 0x00721483, /* 109,   -1dB */
+	0x00800000, /* 110,    0dB */ 0x008F9E4D, /* 111,    1dB */
+	0x00A12478, /* 112,    2dB */ 0x00B4CE08, /* 113,    3dB */
+	0x00CADDC8, /* 114,    4dB */ 0x00E39EA9, /* 115,    5dB */
+	0x00FF64C1, /* 116,    6dB */ 0x011E8E6A, /* 117,    7dB */
+	0x0141857F, /* 118,    8dB */ 0x0168C0C6, /* 119,    9dB */
+	0x0194C584, /* 120,   10dB */ 0x01C62940, /* 121,   11dB */
+	0x01FD93C2, /* 122,   12dB */ 0x023BC148, /* 123,   13dB */
+	0x02818508, /* 124,   14dB */ 0x02CFCC01, /* 125,   15dB */
+	0x0327A01A, /* 126,   16dB */ 0x038A2BAD, /* 127,   17dB */
+	0x03F8BD7A, /* 128,   18dB */ 0x0474CD1B, /* 129,   19dB */
+	0x05000000, /* 130,   20dB */ 0x059C2F02, /* 131,   21dB */
+	0x064B6CAE, /* 132,   22dB */ 0x07100C4D, /* 133,   23dB */
+	0x07ECA9CD, /* 134,   24dB */ 0x08E43299, /* 135,   25dB */
+	0x09F9EF8E, /* 136,   26dB */ 0x0B319025, /* 137,   27dB */
+	0x0C8F36F2, /* 138,   28dB */ 0x0E1787B8, /* 139,   29dB */
+	0x0FCFB725, /* 140,   30dB */ 0x11BD9C84, /* 141,   31dB */
+	0x13E7C594, /* 142,   32dB */ 0x16558CCB, /* 143,   33dB */
+	0x190F3254, /* 144,   34dB */ 0x1C1DF80E, /* 145,   35dB */
+	0x1F8C4107, /* 146,   36dB */ 0x2365B4BF, /* 147,   37dB */
+	0x27B766C2, /* 148,   38dB */ 0x2C900313, /* 149,   39dB */
+	0x32000000, /* 150,   40dB */ 0x3819D612, /* 151,   41dB */
+	0x3EF23ECA, /* 152,   42dB */ 0x46A07B07, /* 153,   43dB */
+	0x4F3EA203, /* 154,   44dB */ 0x58E9F9F9, /* 155,   45dB */
+	0x63C35B8E, /* 156,   46dB */ 0x6FEFA16D, /* 157,   47dB */
+	0x7D982575, /* 158,   48dB */
+};
+
+#define TAS5805M_VOLUME_MAX	((int)ARRAY_SIZE(tas5805m_volume) - 1)
+#define TAS5805M_VOLUME_MIN	0
+
+struct tas5805m_priv {
+	struct regulator		*pvdd;
+	int				gpio_pdn_n;
+
+	uint8_t				*dsp_cfg_data;
+	int				dsp_cfg_len;
+
+	struct snd_soc_component	*component;
+	struct regmap			*regmap;
+	struct mutex			lock;
+
+	int				vol;
+	bool				is_powered;
+	bool				is_muted;
+};
+
+static void tas5805m_refresh_unlocked(struct snd_soc_component *component)
+{
+	struct tas5805m_priv *tas5805m =
+		snd_soc_component_get_drvdata(component);
+	uint8_t v[4];
+	unsigned int i;
+	uint32_t x;
+
+	dev_dbg(component->dev, "refresh: is_muted=%d, vol=%d\n",
+		tas5805m->is_muted, tas5805m->vol);
+
+	x = tas5805m_volume[tas5805m->vol];
+	for (i = 0; i < 4; i++) {
+		v[3 - i] = x;
+		x >>= 8;
+	}
+
+	snd_soc_component_write(component, REG_PAGE, 0x00);
+	snd_soc_component_write(component, REG_BOOK, 0x8c);
+	snd_soc_component_write(component, REG_PAGE, 0x2a);
+
+	for (i = 0; i < 4; i++)
+		snd_soc_component_write(component, 0x24 + i, v[i]);
+	for (i = 0; i < 4; i++)
+		snd_soc_component_write(component, 0x28 + i, v[i]);
+
+	/* Volume controls */
+	snd_soc_component_write(component, REG_DEVICE_CTRL_2,
+		tas5805m->is_muted ?  0x0b : 0x03);
+}
+
+static int tas5805m_vol_info(struct snd_kcontrol *kcontrol,
+			     struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+
+	uinfo->value.integer.min = TAS5805M_VOLUME_MIN;
+	uinfo->value.integer.max = TAS5805M_VOLUME_MAX;
+	return 0;
+}
+
+static int tas5805m_vol_get(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct tas5805m_priv *tas5805m =
+		snd_soc_component_get_drvdata(component);
+
+	mutex_lock(&tas5805m->lock);
+	ucontrol->value.integer.value[0] = tas5805m->vol;
+	mutex_unlock(&tas5805m->lock);
+
+	return 0;
+}
+
+static int tas5805m_vol_put(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *component =
+		snd_soc_kcontrol_component(kcontrol);
+	struct tas5805m_priv *tas5805m =
+		snd_soc_component_get_drvdata(component);
+
+	mutex_lock(&tas5805m->lock);
+	tas5805m->vol = clamp((int)ucontrol->value.integer.value[0],
+			      TAS5805M_VOLUME_MIN, TAS5805M_VOLUME_MAX);
+	dev_dbg(component->dev, "set vol=%d (is_powered=%d)\n",
+		tas5805m->vol, tas5805m->is_powered);
+	if (tas5805m->is_powered)
+		tas5805m_refresh_unlocked(component);
+	mutex_unlock(&tas5805m->lock);
+
+	return 0;
+}
+
+static const struct snd_kcontrol_new tas5805m_snd_controls[] = {
+	{
+		.iface	= SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name	= "Master Playback Volume",
+		.access	= SNDRV_CTL_ELEM_ACCESS_TLV_READ |
+			  SNDRV_CTL_ELEM_ACCESS_READWRITE,
+		.info	= tas5805m_vol_info,
+		.get	= tas5805m_vol_get,
+		.put	= tas5805m_vol_put,
+	},
+};
+
+/* This must not run until after the I2S clocks (BCLK, LRCLK) are up and
+ * stable.
+ */
+static void send_cfg(struct snd_soc_component *component,
+		     const uint8_t *s, unsigned int len)
+{
+	unsigned int i;
+
+	for (i = 0; i + 1 < len; i += 2)
+		snd_soc_component_write(component, s[i], s[i + 1]);
+}
+
+/* The TAS5805M can't be configured or brought out of power-down without
+ * an I2S clock. In power-down, registers are reset.
+ *
+ * We rely on DAPM not powering up the DAC widget until the source for
+ * it is ready, which we think implies that the I2S clock is present and
+ * stable.
+ */
+static int tas5805m_dac_event(struct snd_soc_dapm_widget *w,
+			      struct snd_kcontrol *kcontrol, int event)
+{
+	struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm);
+	struct tas5805m_priv *tas5805m =
+		snd_soc_component_get_drvdata(component);
+
+	if (event & SND_SOC_DAPM_POST_PMU) {
+		dev_dbg(component->dev, "SND_SOC_DAPM_POST_PMU\n");
+
+		/* We mustn't issue any I2C transactions until the I2S
+		 * clock is stable. Furthermore, we must allow a 5ms
+		 * delay after the first set of register writes to
+		 * allow the DSP to boot before configuring it.
+		 */
+		mutex_lock(&tas5805m->lock);
+		usleep_range(5000, 10000);
+		send_cfg(component, dsp_cfg_preboot,
+			ARRAY_SIZE(dsp_cfg_preboot));
+		usleep_range(5000, 15000);
+		send_cfg(component, tas5805m->dsp_cfg_data,
+			tas5805m->dsp_cfg_len);
+
+		tas5805m->is_powered = true;
+		tas5805m_refresh_unlocked(component);
+		mutex_unlock(&tas5805m->lock);
+	} else if (event & SND_SOC_DAPM_PRE_PMD) {
+		unsigned int chan, global1, global2;
+
+		dev_dbg(component->dev, "SND_SOC_DAPM_PRE_PMD\n");
+		mutex_lock(&tas5805m->lock);
+		tas5805m->is_powered = false;
+
+		snd_soc_component_write(component, REG_PAGE, 0x00);
+		snd_soc_component_write(component, REG_BOOK, 0x00);
+
+		chan = snd_soc_component_read(component, REG_CHAN_FAULT);
+		global1 = snd_soc_component_read(component, REG_GLOBAL_FAULT1);
+		global2 = snd_soc_component_read(component, REG_GLOBAL_FAULT2);
+
+		dev_dbg(component->dev,
+			"fault regs: CHAN=%02x, GLOBAL1=%02x, GLOBAL2=%02x\n",
+			chan, global1, global2);
+
+		snd_soc_component_write(component, REG_DEVICE_CTRL_2,
+			0x02); /* Hi-Z mode */
+		mutex_unlock(&tas5805m->lock);
+	}
+
+	return 0;
+}
+
+static int tas5805m_probe(struct snd_soc_component *component)
+{
+	struct tas5805m_priv *tas5805m =
+		snd_soc_component_get_drvdata(component);
+
+	tas5805m->component = component;
+	return 0;
+}
+
+static const struct snd_soc_dapm_route tas5805m_audio_map[] = {
+	{ "DAC", NULL, "DAC IN" },
+	{ "OUT", NULL, "DAC" },
+};
+
+static const struct snd_soc_dapm_widget tas5805m_dapm_widgets[] = {
+	SND_SOC_DAPM_AIF_IN("DAC IN", "Playback", 0, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_DAC_E("DAC", NULL, SND_SOC_NOPM, 0, 0,
+		tas5805m_dac_event,
+		SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_PRE_PMD),
+	SND_SOC_DAPM_OUTPUT("OUT")
+};
+
+static const struct snd_soc_component_driver soc_codec_dev_tas5805m = {
+	.probe			= tas5805m_probe,
+	.controls		= tas5805m_snd_controls,
+	.num_controls		= ARRAY_SIZE(tas5805m_snd_controls),
+	.dapm_widgets		= tas5805m_dapm_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(tas5805m_dapm_widgets),
+	.dapm_routes		= tas5805m_audio_map,
+	.num_dapm_routes	= ARRAY_SIZE(tas5805m_audio_map),
+	.use_pmdown_time	= 1,
+	.endianness		= 1,
+	.non_legacy_dai_naming	= 1,
+};
+
+static int tas5805m_mute(struct snd_soc_dai *dai, int mute, int direction)
+{
+	struct snd_soc_component *component = dai->component;
+	struct tas5805m_priv *tas5805m =
+		snd_soc_component_get_drvdata(component);
+
+	mutex_lock(&tas5805m->lock);
+	dev_dbg(component->dev, "set mute=%d (is_powered=%d)\n",
+		mute, tas5805m->is_powered);
+	tas5805m->is_muted = !!mute;
+	if (tas5805m->is_powered)
+		tas5805m_refresh_unlocked(component);
+	mutex_unlock(&tas5805m->lock);
+
+	return 0;
+}
+
+static const struct snd_soc_dai_ops tas5805m_dai_ops = {
+	.mute_stream		= tas5805m_mute,
+	.no_capture_mute	= 1,
+};
+
+static struct snd_soc_dai_driver tas5805m_dai = {
+	.name		= "tas5805m-amplifier",
+	.playback	= {
+		.stream_name	= "Playback",
+		.channels_min	= 2,
+		.channels_max	= 2,
+		.rates		= SNDRV_PCM_RATE_48000,
+		.formats	= SNDRV_PCM_FMTBIT_S32_LE,
+	},
+	.ops		= &tas5805m_dai_ops,
+};
+
+static const struct regmap_config tas5805m_regmap = {
+	.reg_bits	= 8,
+	.val_bits	= 8,
+
+	/* We have quite a lot of multi-level bank switching and a
+	 * relatively small number of register writes between bank
+	 * switches.
+	 */
+	.cache_type	= REGCACHE_NONE,
+};
+
+static int tas5805m_i2c_probe(struct i2c_client *i2c)
+{
+	struct device *dev = &i2c->dev;
+	struct regmap *regmap;
+	struct tas5805m_priv *tas5805m;
+	int ret;
+
+	regmap = devm_regmap_init_i2c(i2c, &tas5805m_regmap);
+	if (IS_ERR(regmap)) {
+		ret = PTR_ERR(regmap);
+		dev_err(dev, "unable to allocate register map: %d\n", ret);
+		return ret;
+	}
+
+	tas5805m = devm_kzalloc(dev, sizeof(struct tas5805m_priv), GFP_KERNEL);
+	if (!tas5805m)
+		return -ENOMEM;
+
+	tas5805m->pvdd = devm_regulator_get(dev, "pvdd");
+	if (IS_ERR(tas5805m->pvdd)) {
+		dev_err(dev, "failed to get pvdd supply: %ld\n",
+			PTR_ERR(tas5805m->pvdd));
+		return PTR_ERR(tas5805m->pvdd);
+	}
+
+	dev_set_drvdata(dev, tas5805m);
+	tas5805m->regmap = regmap;
+	tas5805m->gpio_pdn_n = of_get_named_gpio(dev->of_node, "pdn-gpio", 0);
+	if (!gpio_is_valid(tas5805m->gpio_pdn_n)) {
+		dev_err(dev, "power-down GPIO not specified\n");
+		return -EINVAL;
+	}
+
+	tas5805m->dsp_cfg_len = of_property_count_elems_of_size(dev->of_node,
+		"ti,dsp-config", 1);
+	if (tas5805m->dsp_cfg_len < 0) {
+		dev_err(dev, "no DSP config provided\n");
+		return tas5805m->dsp_cfg_len;
+	}
+
+	tas5805m->dsp_cfg_data = devm_kmalloc(dev, tas5805m->dsp_cfg_len,
+		GFP_KERNEL);
+	if (!tas5805m->dsp_cfg_data)
+		return -ENOMEM;
+
+	of_property_read_u8_array(dev->of_node, "ti,dsp-config",
+		tas5805m->dsp_cfg_data, tas5805m->dsp_cfg_len);
+	dev_dbg(dev, "%d bytes of DSP config loaded\n",
+		tas5805m->dsp_cfg_len);
+
+	ret = devm_gpio_request(dev, tas5805m->gpio_pdn_n,
+		"TAS5805M power-down");
+	if (ret < 0) {
+		dev_err(dev,
+			"unable to request power-down GPIO: %d\n", ret);
+		return ret;
+	}
+
+	/* Do the first part of the power-on here, while we can expect
+	 * the I2S interface to be quiet. We must raise PDN# and then
+	 * wait 5ms before any I2S clock is sent, or else the internal
+	 * regulator apparently won't come on.
+	 *
+	 * Also, we must keep the device in power down for 100ms or so
+	 * after PVDD is applied, or else the ADR pin is sampled
+	 * incorrectly and the device comes up with an unpredictable I2C
+	 * address.
+	 */
+	gpio_direction_output(tas5805m->gpio_pdn_n, 0);
+	tas5805m->vol = TAS5805M_VOLUME_MIN;
+	mutex_init(&tas5805m->lock);
+
+	ret = devm_snd_soc_register_component(dev, &soc_codec_dev_tas5805m,
+					      &tas5805m_dai, 1);
+	if (ret < 0) {
+		dev_err(dev, "unable to register codec: %d\n", ret);
+		return ret;
+	}
+
+	ret = regulator_enable(tas5805m->pvdd);
+	if (ret < 0) {
+		dev_err(dev, "failed to enable pvdd: %d\n", ret);
+		return ret;
+	}
+
+	usleep_range(100000, 150000);
+	gpio_set_value(tas5805m->gpio_pdn_n, 1);
+	usleep_range(10000, 15000);
+
+	return 0;
+}
+
+static int tas5805m_i2c_remove(struct i2c_client *i2c)
+{
+	struct tas5805m_priv *tas5805m = dev_get_drvdata(&i2c->dev);
+
+	gpio_set_value(tas5805m->gpio_pdn_n, 0);
+	usleep_range(10000, 15000);
+	regulator_disable(tas5805m->pvdd);
+	return 0;
+}
+
+static const struct i2c_device_id tas5805m_i2c_id[] = {
+	{ "tas5805m", },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, tas5805m_i2c_id);
+
+#if IS_ENABLED(CONFIG_OF)
+static const struct of_device_id tas5805m_of_match[] = {
+	{ .compatible = "ti,tas5805m", },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, tas5805m_of_match);
+#endif
+
+static struct i2c_driver tas5805m_i2c_driver = {
+	.probe_new	= tas5805m_i2c_probe,
+	.remove		= tas5805m_i2c_remove,
+	.id_table	= tas5805m_i2c_id,
+	.driver		= {
+		.name		= "tas5805m",
+		.of_match_table = of_match_ptr(tas5805m_of_match),
+	},
+};
+
+module_i2c_driver(tas5805m_i2c_driver);
+
+MODULE_AUTHOR("Andy Liu <andy-liu@ti.com>");
+MODULE_AUTHOR("Daniel Beer <daniel.beer@igorinstitute.com>");
+MODULE_DESCRIPTION("TAS5805M Audio Amplifier Driver");
+MODULE_LICENSE("GPL v2");