From 5f76c571ec23cad3c9f79d634141293eae080643 Mon Sep 17 00:00:00 2001
From: Aurelien Jarno <aurelien@aurel32.net>
Date: Mon, 13 Apr 2020 18:54:23 +0200
Subject: Add preliminary kernel module

---
 README.md                  |  23 +++-
 tas5825m-module/Makefile   |  21 +++
 tas5825m-module/dts.patch  |  69 ++++++++++
 tas5825m-module/tas5825m.c | 324 +++++++++++++++++++++++++++++++++++++++++++++
 tas5825m-module/tas5825m.h |  24 ++++
 5 files changed, 459 insertions(+), 2 deletions(-)
 create mode 100644 tas5825m-module/Makefile
 create mode 100644 tas5825m-module/dts.patch
 create mode 100644 tas5825m-module/tas5825m.c
 create mode 100644 tas5825m-module/tas5825m.h

diff --git a/README.md b/README.md
index 7fa2427..66db318 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,25 @@ control, audio processor, etc). It is also not good quality enough to be
 upstreamable. It has been sufficient for my usage, so I didn't take the time
 to improve it.
 
+The Linux kernel module should build fine on a system with a recent kernel
+(so far I only tested 5.4 and 5.5) and with the kernel headers available. It
+could be built and installed with the following command:
+
+```
+  cd tas5825m-module
+  make
+  make modules_install
+```
+
+The tricky part is to modify the device tree to get the board recognized by
+the kernel. In the long term the 24L32 EEPROM should be used for that, but I
+haven't look at how it works yet. The patch I use on my system is
+[available](tas5825m-module/dts.patch), it should probably be adapted
+depending on the system and the kernel version. I used the `dtc` tool to dump
+the original device tree binary file into a source format, and later to
+rebuild the binary version. It should be easier to use device tree overlays
+but again I haven't looked at that yet.
+
 
 ## Sources
 
@@ -68,5 +87,5 @@ All the sources of the repository are available
 
 ## License
 
-The contents of this repository is released under the
-[Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)](LICENSE).
+The contents of this repository, excluding the `tas5825m-module` directory, is
+released under the [Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)](LICENSE).
diff --git a/tas5825m-module/Makefile b/tas5825m-module/Makefile
new file mode 100644
index 0000000..2147d32
--- /dev/null
+++ b/tas5825m-module/Makefile
@@ -0,0 +1,21 @@
+ifneq ($(KERNELRELEASE),)
+# kbuild part of makefile
+obj-m := tas5825m.o
+else
+
+# Normal Makefile
+
+KERNELDIR := /lib/modules/`uname -r`/build
+all: 
+	$(MAKE) -C $(KERNELDIR) M=`pwd` modules
+
+debug:
+	$(MAKE) -C $(KERNELDIR) EXTRA_CFLAGS=-DDEBUG M=`pwd` modules
+
+modules_install:
+	$(MAKE) -C $(KERNELDIR) M=`pwd` modules_install
+
+clean:
+	rm -rf *.o *.ko *.mod.c *.mod .*.cmd Module.symvers modules.order .tmp_versions
+
+endif
diff --git a/tas5825m-module/dts.patch b/tas5825m-module/dts.patch
new file mode 100644
index 0000000..6bfddcb
--- /dev/null
+++ b/tas5825m-module/dts.patch
@@ -0,0 +1,69 @@
+--- bcm2837-rpi-3-b-plus.orig.dts
++++ bcm2837-rpi-3-b-plus.rpi-amp.dts
+@@ -368,6 +368,12 @@
+ 				brcm,pins = < 0x04 0x05 0x07 0x08 0x09 0x0a 0x0b >;
+ 				brcm,function = < 0x04 >;
+ 			};
++
++			i2s_alt0: i2s_alt0 {
++				brcm,pins = <18 19 20 21>;
++				brcm,function = < 0x04 >;
++			};
++
+ 		};
+ 
+ 		serial@7e201000 {
+@@ -401,13 +407,16 @@
+ 			bus-width = < 0x04 >;
+ 		};
+ 
+-		i2s@7e203000 {
++		i2s0: i2s@7e203000 {
++			#sound-dai-cells= <0>;
+ 			compatible = "brcm,bcm2835-i2s";
+ 			reg = < 0x7e203000 0x24 >;
+ 			clocks = < 0x04 0x1f >;
+ 			dmas = < 0x0c 0x02 0x0c 0x03 >;
+ 			dma-names = "tx\0rx";
+-			status = "disabled";
++			status = "okay";
++			pinctrl-names = "default";
++			pinctrl-0 = <&i2s_alt0>;
+ 		};
+ 
+ 		spi@7e204000 {
+@@ -582,6 +591,13 @@
+ 			pinctrl-names = "default";
+ 			pinctrl-0 = < 0x16 >;
+ 			clock-frequency = < 0x186a0 >;
++
++			tas5825m: tas5825m@4c {
++				compatible = "ti,tas5825m";
++				reg = < 0x4c >;
++				pdn-gpios = < 0x18 0x1b 0x01 >;
++				#sound-dai-cells = < 0x00 >;
++			};
+ 		};
+ 
+ 		i2c@7e805000 {
+@@ -823,4 +839,20 @@
+ 		reset-gpios = < 0x0b 0x01 0x01 >;
+ 		phandle = < 0x15 >;
+ 	};
++
++	sound {
++		compatible = "simple-audio-card";
++		simple-audio-card,name = "tas5825m";
++		simple-audio-card,format = "i2s";
++		simple-audio-card,bitclock-master = <&dailink0_master>;
++		simple-audio-card,frame-master = <&dailink0_master>;
++
++		dailink0_master: simple-audio-card,cpu {
++			sound-dai = <&i2s0>;
++		};
++
++		simple-audio-card,codec {
++			sound-dai = <&tas5825m>;
++		};
++	};
+ };
diff --git a/tas5825m-module/tas5825m.c b/tas5825m-module/tas5825m.c
new file mode 100644
index 0000000..fdca808
--- /dev/null
+++ b/tas5825m-module/tas5825m.c
@@ -0,0 +1,324 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tas5825.c - ALSA SoC Texas Instruments TAS5825M Audio Amplifier
+ *
+ * Copyright (C) 2020 Aurelien Jarno <aurelien@aurel32.net>
+ *
+ * Author: Aurelien Jarno <aurelien@aurel32.net>
+ */
+
+#include <linux/module.h>
+#include <linux/device.h>
+#include <linux/i2c.h>
+#include <linux/slab.h>
+#include <linux/gpio/consumer.h>
+#include <sound/pcm.h>
+#include <sound/soc.h>
+
+#include "tas5825m.h"
+
+struct tas5825m_data {
+	struct gpio_desc *pdn_gpio;
+};
+
+static const struct snd_soc_dapm_widget tas5825m_widgets[] = {
+	SND_SOC_DAPM_OUTPUT("OUT"),
+};
+
+static const struct snd_soc_dapm_route tas5825m_routes[] = {
+	{ "OUT", NULL, "Playback" },
+};
+
+static struct snd_soc_component_driver soc_component_dev_tas5825m = {
+	.dapm_widgets		= tas5825m_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(tas5825m_widgets),
+	.dapm_routes		= tas5825m_routes,
+	.num_dapm_routes	= ARRAY_SIZE(tas5825m_routes),
+	.idle_bias_on		= 1,
+	.use_pmdown_time	= 1,
+	.endianness		= 1,
+	.non_legacy_dai_naming	= 1,
+};
+
+static struct snd_soc_dai_driver tas5825m_dai = {
+	.name		= "tas5825m-amplifier",
+	.playback 	= {
+		.stream_name	= "Playback",
+		.channels_min	= 2,
+		.channels_max	= 2,
+		.rates		= TAS5825M_RATES,
+		.formats	= TAS5825M_FORMATS,
+	},
+};
+
+#ifdef CONFIG_OF
+static const struct of_device_id tas5825m_of_ids[] = {
+	{ .compatible = "ti,tas5825m", },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, tas5825m_of_ids);
+#endif
+
+static void tas5825m_write_reg(struct i2c_client *client, uint8_t reg, uint8_t val)
+{
+	uint8_t buf[2];
+	int ret;
+
+	buf[0] = reg;
+	buf[1] = val;
+
+	ret = i2c_master_send(client, buf, sizeof(buf));
+	if (ret < 0)
+		dev_err(&client->dev, "unable to write reg 0x%02x, value 0x%02x\n", reg, val);
+}
+
+static int tas5825m_i2c_probe(struct i2c_client *client,
+			      const struct i2c_device_id *id)
+{
+	struct device *dev = &client->dev;
+	struct tas5825m_data *tas5825m;
+	int ret;
+
+	tas5825m = devm_kzalloc(dev, sizeof(*tas5825m), GFP_KERNEL);
+	if (!tas5825m)
+		return -ENOMEM;
+	dev_set_drvdata(dev, tas5825m);
+
+	/*
+	 * Get control of the pdn pin and set it LOW to take the codec
+	 * out of the power-down mode.
+	 * Note: The actual pin polarity is taken care of in the GPIO lib
+	 * according the polarity specified in the DTS.
+	 */
+	tas5825m->pdn_gpio = devm_gpiod_get_optional(dev, "pdn", GPIOD_OUT_LOW);
+
+	if (IS_ERR(tas5825m->pdn_gpio)) {
+		if (PTR_ERR(tas5825m->pdn_gpio) == -EPROBE_DEFER)
+			return -EPROBE_DEFER;
+		dev_info(dev, "failed to get pdn GPIO: %ld\n",
+			PTR_ERR(tas5825m->pdn_gpio));
+		tas5825m->pdn_gpio = NULL;
+	}
+
+	/* power-up time may be as long as 1ms */
+	mdelay(1);
+
+	/* PAGE 0x00, BOOK 0x00 */
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x7f, 0x00);
+
+	/* Reset register and digital core */
+	tas5825m_write_reg(client, 0x01, 0x11);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+
+	/* PAGE 0x00, BOOK 0x00 */
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x7f, 0x00);
+
+	/* Switch from deep sleep to Hi-z mode, keep mute enabled */
+	tas5825m_write_reg(client, 0x03, 0x12);
+
+	/* GPIO0, GPIO1 and GPIO2 as output */
+	tas5825m_write_reg(client, 0x60, 0x07);
+	tas5825m_write_reg(client, 0x61, 0x02);
+	tas5825m_write_reg(client, 0x62, 0x02);
+	tas5825m_write_reg(client, 0x63, 0x02);
+
+	/* GPIO0 = 1, GPIO1 = 0, GPIO2 = 1 */
+//	tas5825m_write_reg(client, 0x65, 0x05);
+	tas5825m_write_reg(client, 0x65, 0x00);
+
+	/* ??? */
+	tas5825m_write_reg(client, 0x7f, 0x00);
+	tas5825m_write_reg(client, 0x7d, 0x11); /* ???? */
+	tas5825m_write_reg(client, 0x7e, 0xff); /* ???? */
+	tas5825m_write_reg(client, 0x00, 0x01); /* Page 0x01 */
+	tas5825m_write_reg(client, 0x51, 0x05);
+	tas5825m_write_reg(client, 0x00, 0x02); /* Page 0x02 */
+	tas5825m_write_reg(client, 0x19, 0xa0);
+	tas5825m_write_reg(client, 0x1d, 0x01);
+	tas5825m_write_reg(client, 0x00, 0x00); /* Page 0x00 */
+	tas5825m_write_reg(client, 0x7f, 0x00); /* Book 0x00 */
+	tas5825m_write_reg(client, 0x46, 0x01); /* DSP_CTRL */
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x02, 0x42); /* # 768kHz, BTL, Hybrid mode */
+	tas5825m_write_reg(client, 0x53, 0x60); /* # 175kHz bandwidth */
+	tas5825m_write_reg(client, 0x54, 0x04); /* Analog gain of -2.0dB (originally 0) */
+	tas5825m_write_reg(client, 0x00, 0x00); /* Page 0x00 */
+	tas5825m_write_reg(client, 0x7f, 0x00); /* Book 0x00 */
+	tas5825m_write_reg(client, 0x03, 0x02); /* Hiz, unmute */
+
+	mdelay(5);
+
+	tas5825m_write_reg(client, 0x00, 0x00); 
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x00, 0x00);
+	tas5825m_write_reg(client, 0x7f, 0x8c); /* Book 0x8c */
+	tas5825m_write_reg(client, 0x00, 0x06); /* Page 0x06 */
+	tas5825m_write_reg(client, 0x38, 0x00);
+	tas5825m_write_reg(client, 0x39, 0x00);
+	tas5825m_write_reg(client, 0x3a, 0x04);
+	tas5825m_write_reg(client, 0x3b, 0x00);
+
+	/* Tuning coeffs */
+	tas5825m_write_reg(client, 0x00, 0x0b); /* Page 0x0b */
+	tas5825m_write_reg(client, 0x50, 0x00);
+	tas5825m_write_reg(client, 0x51, 0x20);
+	tas5825m_write_reg(client, 0x52, 0xc4);
+	tas5825m_write_reg(client, 0x53, 0x9c);
+	tas5825m_write_reg(client, 0x5c, 0x7f);
+	tas5825m_write_reg(client, 0x5d, 0xff);
+	tas5825m_write_reg(client, 0x5e, 0xff);
+	tas5825m_write_reg(client, 0x5f, 0xff);
+
+	/* Book 0x8c */
+	tas5825m_write_reg(client, 0x00, 0x00); /* Page 0x00 */
+	tas5825m_write_reg(client, 0x7f, 0x8c); /* Book 0x8c */
+
+	tas5825m_write_reg(client, 0x00, 0x01); /* Page 0x01 */
+	tas5825m_write_reg(client, 0x28, 0x40); /* => enable AGL */
+	tas5825m_write_reg(client, 0x29, 0x00);
+	tas5825m_write_reg(client, 0x2a, 0x00);
+	tas5825m_write_reg(client, 0x2b, 0x00);
+
+	tas5825m_write_reg(client, 0x00, 0x0a); /* Page 0x0a */
+	tas5825m_write_reg(client, 0x64, 0x00); /* Digital left from left = 0x00800000 */
+	tas5825m_write_reg(client, 0x65, 0x80);
+	tas5825m_write_reg(client, 0x66, 0x00);
+	tas5825m_write_reg(client, 0x67, 0x00);
+	tas5825m_write_reg(client, 0x68, 0x00); /* Digital left from right = 0x00000000 */
+	tas5825m_write_reg(client, 0x69, 0x00);
+	tas5825m_write_reg(client, 0x6a, 0x00);
+	tas5825m_write_reg(client, 0x6b, 0x00);
+	tas5825m_write_reg(client, 0x6c, 0x00); /* Digital right from left = 0x00000000 */
+	tas5825m_write_reg(client, 0x6d, 0x00);
+	tas5825m_write_reg(client, 0x6e, 0x00);
+	tas5825m_write_reg(client, 0x6f, 0x00);
+	tas5825m_write_reg(client, 0x70, 0x00); /* Digital right from right = 0x008000000 */
+	tas5825m_write_reg(client, 0x71, 0x80);
+	tas5825m_write_reg(client, 0x72, 0x00);
+	tas5825m_write_reg(client, 0x73, 0x00);
+	tas5825m_write_reg(client, 0x74, 0x00); /* Analog left from left = 0x008000000 */
+	tas5825m_write_reg(client, 0x75, 0x80);
+	tas5825m_write_reg(client, 0x76, 0x00);
+	tas5825m_write_reg(client, 0x77, 0x00);
+	tas5825m_write_reg(client, 0x78, 0x00); /* Analog left from right = 0x00000000 */
+	tas5825m_write_reg(client, 0x79, 0x00);
+	tas5825m_write_reg(client, 0x7a, 0x00);
+	tas5825m_write_reg(client, 0x7b, 0x00);
+	tas5825m_write_reg(client, 0x7c, 0x00); /* Analog right from left = 0x00000000 */
+	tas5825m_write_reg(client, 0x7d, 0x00);
+	tas5825m_write_reg(client, 0x7e, 0x00);
+	tas5825m_write_reg(client, 0x7f, 0x00);
+
+	tas5825m_write_reg(client, 0x00, 0x0b); /* Page 0x0b */
+	tas5825m_write_reg(client, 0x08, 0x00); /* Analog right from right = 0x008000000 */
+	tas5825m_write_reg(client, 0x09, 0x80);
+	tas5825m_write_reg(client, 0x0a, 0x00);
+	tas5825m_write_reg(client, 0x0b, 0x00);
+
+	tas5825m_write_reg(client, 0x38, 0x02); /* AGL PVDD alpha */
+	tas5825m_write_reg(client, 0x39, 0xa3);
+	tas5825m_write_reg(client, 0x3a, 0x9a);
+	tas5825m_write_reg(client, 0x3b, 0xcc);
+	tas5825m_write_reg(client, 0x3c, 0x00); /* AGL temp alpha */
+	tas5825m_write_reg(client, 0x3d, 0x06);
+	tas5825m_write_reg(client, 0x3e, 0xd3);
+	tas5825m_write_reg(client, 0x3f, 0x72);
+
+	tas5825m_write_reg(client, 0x48, 0x04); /* AGL softening filter */ 
+	tas5825m_write_reg(client, 0x49, 0xc1);
+	tas5825m_write_reg(client, 0x4a, 0xff);
+	tas5825m_write_reg(client, 0x4b, 0x93);
+	tas5825m_write_reg(client, 0x4c, 0x01); /* AGL attack rate */
+	tas5825m_write_reg(client, 0x4d, 0x12);
+	tas5825m_write_reg(client, 0x4e, 0x6e);
+	tas5825m_write_reg(client, 0x4f, 0x98);
+
+	tas5825m_write_reg(client, 0x54, 0x7b); /* AGL softening filter omega */
+	tas5825m_write_reg(client, 0x55, 0x3e);
+	tas5825m_write_reg(client, 0x56, 0x00);
+	tas5825m_write_reg(client, 0x57, 0x6d);
+	tas5825m_write_reg(client, 0x58, 0x00); /* AGL release rate */
+	tas5825m_write_reg(client, 0x59, 0x00);
+	tas5825m_write_reg(client, 0x5a, 0xae);
+	tas5825m_write_reg(client, 0x5b, 0xc3);
+
+	tas5825m_write_reg(client, 0x60, 0x08); /* AGL PVDD threshold*/
+	tas5825m_write_reg(client, 0x61, 0x13);
+	tas5825m_write_reg(client, 0x62, 0x85);
+	tas5825m_write_reg(client, 0x63, 0x62);
+	tas5825m_write_reg(client, 0x64, 0x00); /* AGL temp scale */
+	tas5825m_write_reg(client, 0x65, 0x80);
+	tas5825m_write_reg(client, 0x66, 0x00);
+	tas5825m_write_reg(client, 0x67, 0x00);
+	tas5825m_write_reg(client, 0x6c, 0x00); /* AGL volt scale */
+	tas5825m_write_reg(client, 0x6d, 0x78);
+	tas5825m_write_reg(client, 0x6e, 0xd6);
+	tas5825m_write_reg(client, 0x6f, 0xfd);
+	tas5825m_write_reg(client, 0x70, 0x00); /* Volt/temp scale */
+	tas5825m_write_reg(client, 0x71, 0x08);
+	tas5825m_write_reg(client, 0x72, 0x13);
+	tas5825m_write_reg(client, 0x73, 0x85);
+
+	/* Register Tuning */
+	tas5825m_write_reg(client, 0x00, 0x00); /* Page 0x00 */
+	tas5825m_write_reg(client, 0x7f, 0x00); /* Book 0x00 */
+	tas5825m_write_reg(client, 0x30, 0x00); /* SDOUT is the DSP output */
+	tas5825m_write_reg(client, 0x4c, 0x30); /* Digital volume 0dB */
+	tas5825m_write_reg(client, 0x03, 0x03); /* Play mode */
+	tas5825m_write_reg(client, 0x00, 0x00); /* Page 0x00 */
+	tas5825m_write_reg(client, 0x7f, 0x00); /* Book 0x00 */
+	tas5825m_write_reg(client, 0x78, 0x80); /* Clear analog fault */
+
+	ret = devm_snd_soc_register_component(dev,
+				&soc_component_dev_tas5825m,
+				&tas5825m_dai, 1);
+	if (ret < 0) {
+		dev_err(dev, "unable to register codec: %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int tas5825m_i2c_remove(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct tas5825m_data *tas5825m = dev_get_drvdata(dev);
+
+	/* put the codec in power-down */
+	if (tas5825m->pdn_gpio)
+		gpiod_set_value_cansleep(tas5825m->pdn_gpio, 1);
+
+	return 0;
+}
+
+static const struct i2c_device_id tas5825m_i2c_ids[] = {
+	{ "tas5825m", 0 },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, tas5825m_i2c_ids);
+
+static struct i2c_driver tas5825m_i2c_driver = {
+	.driver		= {
+		.name	= "tas5825m",
+		.of_match_table = of_match_ptr(tas5825m_of_ids),
+	},
+	.probe = tas5825m_i2c_probe,
+	.remove = tas5825m_i2c_remove,
+	.id_table = tas5825m_i2c_ids,
+};
+
+module_i2c_driver(tas5825m_i2c_driver);
+
+MODULE_AUTHOR("Aurelien Jarno <aurelien@aurel32.net>");
+MODULE_DESCRIPTION("TAS5825M audio amplifier driver");
+MODULE_LICENSE("GPL");
diff --git a/tas5825m-module/tas5825m.h b/tas5825m-module/tas5825m.h
new file mode 100644
index 0000000..11605d0
--- /dev/null
+++ b/tas5825m-module/tas5825m.h
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * tas5825.h - ALSA SoC Texas Instruments TAS5825M Audio Amplifier
+ *
+ * Copyright (C) 2020 Aurelien Jarno <aurelien@aurel32.net>
+ *
+ * Author: Aurelien Jarno <aurelien@aurel32.net>
+ */
+
+#ifndef __TAS5825M_H__
+#define __TAS5825M_H__
+
+#define TAS5825M_RATES (SNDRV_PCM_RATE_48000 | \
+			SNDRV_PCM_RATE_96000 | \
+			SNDRV_PCM_RATE_192000)
+#define TAS5825M_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | \
+			  SNDRV_PCM_FMTBIT_S24_LE | \
+			  SNDRV_PCM_FMTBIT_S32_LE)
+
+/* Register Address Map */
+#define TAS5825M_RESET_CTRL		0x00
+
+#endif /* __TAS5825M_H__ */
+
-- 
cgit v1.2.3