All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 0/4] ASoC: add ESS Technology ES9039Q2M codec driver
@ 2026-08-20  6:26 Karl Asseily
  2026-08-20  6:26 ` [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology Karl Asseily
                   ` (3 more replies)
  0 siblings, 4 replies; 8+ messages in thread
From: Karl Asseily @ 2026-08-20  6:26 UTC (permalink / raw)
  To: broonie, lgirdwood, robh, krzk+dt, conor+dt, perex, tiwai
  Cc: linux-sound, devicetree, linux-kernel, Karl Asseily

This series adds support for the ESS Technology ES9039Q2M, a 32-bit
two-channel audio DAC with an asynchronous sample rate converter,
controlled over I2C.

The part has two control personalities selected by its MODE pin: hardware
mode, strapped by HW0/HW1/HW2 with no bus at all, and software mode over
I2C or SPI. This driver implements software mode over I2C, which MODE = GND
selects.

Three properties of the part shape the driver:

 - The ASRC in front of the DAC means MCLK need not be synchronous with
   BCLK or LRCK. A board can feed it a fixed oscillator and never touch the
   clock again. In that asynchronous mode the part requires MCLK >= 130 x
   Fs, so the driver installs a startup constraint deriving the maximum
   rate from the MCLK it was given rather than advertising rates the
   hardware cannot honour.

 - Selecting an input format is two registers, not one. Register 57
   INPUT_SEL chooses which port to listen to; register 1 SYS MODE CONFIG
   enables the corresponding decoder, and at reset only ENABLE_TDM_DECODE
   is set. Selecting DoP without also enabling ENABLE_DOP_DECODE leaves the
   part hunting for a marker with the marker decoder switched off; it finds
   no valid DoP and mutes. The driver sets both.

 - Several registers have non-zero reserved defaults - register 88 reads
   0xb8 at reset - so every write is read-modify-write.

Everything the driver exposes has been verified against the silicon on
hardware rather than against the driver's own read-back: each control is
written through ALSA and the chip's register is then read over raw I2C.
That covers the eight filter shapes, the modulator bandwidth, the IIR
bandwidth, the four signed 16-bit THD compensation coefficients at both
extremes of their range, and the automute enable, time, level and off-level
fields including the neighbouring MUTE_RAMP_TO_GND bit that shares a
register with the 11-bit time field.

Two limitations are deliberate and worth stating up front, because both
look like omissions:

 - The programmable FIR coefficient controls are write-only. The part has a
   PROG_COEFF_OUT register described as "Programmable FIR coefficient
   readback", but it is not a RAM read port: it returns the last
   coefficient written, whatever address is selected in PROG_COEFF_ADDR.
   Measured by writing two different coefficients to addresses 0 and 1 and
   reading them back with five different sequences - plain, with a settle
   delay, with PROG_COEFF_EN set, with the address written twice, and with
   a write-enable pulse after the address. All ten reads returned the value
   written to address 1. A get() built on that register would return
   something with the shape of data and none of its meaning, so there is
   none.

 - Coefficient uploads pulse PROG_COEFF_WE per coefficient, as the
   datasheet's sequence specifies. Holding it high across the whole upload
   also works on this silicon, but that is not what the documentation says.

DSD, DoP and S/PDIF input are all implemented. DoP is detected by the part
rather than announced by the host: AUTO_INPUT_SEL is enabled and both the
TDM and DoP decoders run, so a player can simply send DoP-encoded PCM, which
is the only way DoP works in practice. An earlier revision of this driver
required userspace to select DoP through a kcontrol first; no player can do
that, and the result was that DoP-encoded material rendered as the near-
silent hiss the DoP marker design deliberately degrades to. The datasheet
attaches a "data must be provided on the DATA2 pin" condition to automatic
detection; that applies to identifying DSD, whose channels arrive on separate
data lines, and DoP detects correctly on a two-channel I2S link without it.

DoP was verified end to end by generating a DSD64 test tone, carrying it as
32-bit PCM at 176400 Hz with nothing selected by hand, and confirming the
part's own DOP_VALID flag and INPUT_SELECT_OVERRIDE readback while it played.

ESS Technology has no bindings in the tree today, so the series also
introduces the vendor prefix.

Karl Asseily (4):
  dt-bindings: vendor-prefixes: add ESS Technology
  ASoC: dt-bindings: add ESS Technology ES9039Q2M
  ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver
  MAINTAINERS: add entry for the ES9039Q2M codec driver

 .../bindings/sound/ess,es9039q2m.yaml         |   61 +
 .../devicetree/bindings/vendor-prefixes.yaml  |    2 +
 MAINTAINERS                                   |    7 +
 sound/soc/codecs/Kconfig                      |   14 +
 sound/soc/codecs/Makefile                     |    2 +
 sound/soc/codecs/es9039q2m.c                  | 1259 +++++++++++++++++
 6 files changed, 1345 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/sound/ess,es9039q2m.yaml
 create mode 100644 sound/soc/codecs/es9039q2m.c

-- 
2.34.1


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology
  2026-08-20  6:26 [PATCH 0/4] ASoC: add ESS Technology ES9039Q2M codec driver Karl Asseily
@ 2026-08-20  6:26 ` Karl Asseily
  2026-08-20  6:30   ` Krzysztof Kozlowski
  2026-08-20  6:26 ` [PATCH 2/4] ASoC: dt-bindings: add ESS Technology ES9039Q2M Karl Asseily
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 8+ messages in thread
From: Karl Asseily @ 2026-08-20  6:26 UTC (permalink / raw)
  To: broonie, lgirdwood, robh, krzk+dt, conor+dt, perex, tiwai
  Cc: linux-sound, devicetree, linux-kernel, Karl Asseily

Add the vendor prefix for ESS Technology, Inc., maker of the SABRE family
of audio DACs.

Signed-off-by: Karl Asseily <karl@asseily.com>
---
 Documentation/devicetree/bindings/vendor-prefixes.yaml | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
index 396044f368e..b3cc100388a 100644
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
@@ -549,6 +549,8 @@ patternProperties:
     description: Seiko Epson Corp.
   "^esp,.*":
     description: Espressif Systems Co. Ltd.
+  "^ess,.*":
+    description: ESS Technology, Inc.
   "^est,.*":
     description: ESTeem Wireless Modems
   "^eswin,.*":
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 2/4] ASoC: dt-bindings: add ESS Technology ES9039Q2M
  2026-08-20  6:26 [PATCH 0/4] ASoC: add ESS Technology ES9039Q2M codec driver Karl Asseily
  2026-08-20  6:26 ` [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology Karl Asseily
@ 2026-08-20  6:26 ` Karl Asseily
  2026-08-20  6:26 ` [PATCH 3/4] ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver Karl Asseily
  2026-08-20  6:26 ` [PATCH 4/4] MAINTAINERS: add entry for the " Karl Asseily
  3 siblings, 0 replies; 8+ messages in thread
From: Karl Asseily @ 2026-08-20  6:26 UTC (permalink / raw)
  To: broonie, lgirdwood, robh, krzk+dt, conor+dt, perex, tiwai
  Cc: linux-sound, devicetree, linux-kernel, Karl Asseily

Add a binding for the ES9039Q2M, a 32-bit two-channel audio DAC with an
asynchronous sample rate converter.

The part selects between two control personalities with its MODE pin:
hardware mode, strapped by HW0/HW1/HW2 with no control bus at all, and
software mode over I2C or SPI. This binding describes software mode over
I2C, which MODE = GND selects.

Signed-off-by: Karl Asseily <karl@asseily.com>
---
 .../bindings/sound/ess,es9039q2m.yaml         | 61 +++++++++++++++++++
 1 file changed, 61 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/sound/ess,es9039q2m.yaml

diff --git a/Documentation/devicetree/bindings/sound/ess,es9039q2m.yaml b/Documentation/devicetree/bindings/sound/ess,es9039q2m.yaml
new file mode 100644
index 00000000000..b52c4cc5304
--- /dev/null
+++ b/Documentation/devicetree/bindings/sound/ess,es9039q2m.yaml
@@ -0,0 +1,61 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/sound/ess,es9039q2m.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ESS Technology ES9039Q2M audio DAC
+
+maintainers:
+  - Karl Asseily <karl@asseily.com>
+
+description:
+  The ES9039Q2M is a 32-bit two-channel audio DAC supporting PCM to 768 kHz,
+  DSD64 to DSD1024, DoP and S/PDIF. It is controlled over I2C or SPI when the
+  MODE pin selects software mode; this binding covers the I2C interface. The
+  part contains an asynchronous sample rate converter, so the system clock need
+  not be synchronous with the audio bit or frame clocks.
+
+allOf:
+  - $ref: dai-common.yaml#
+
+properties:
+  compatible:
+    const: ess,es9039q2m
+
+  reg:
+    description:
+      I2C address, selected by the ADDR0 and ADDR1 pins - 0x48, 0x49, 0x4a or
+      0x4b for GND/GND, GND/AVDD, AVDD/GND and AVDD/AVDD respectively.
+    enum: [ 0x48, 0x49, 0x4a, 0x4b ]
+
+  clocks:
+    maxItems: 1
+
+  clock-names:
+    const: mclk
+
+  "#sound-dai-cells":
+    const: 0
+
+required:
+  - compatible
+  - reg
+  - "#sound-dai-cells"
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        audio-codec@48 {
+            compatible = "ess,es9039q2m";
+            reg = <0x48>;
+            #sound-dai-cells = <0>;
+            clocks = <&dac_mclk>;
+            clock-names = "mclk";
+        };
+    };
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 3/4] ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver
  2026-08-20  6:26 [PATCH 0/4] ASoC: add ESS Technology ES9039Q2M codec driver Karl Asseily
  2026-08-20  6:26 ` [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology Karl Asseily
  2026-08-20  6:26 ` [PATCH 2/4] ASoC: dt-bindings: add ESS Technology ES9039Q2M Karl Asseily
@ 2026-08-20  6:26 ` Karl Asseily
  2026-08-20  6:38   ` sashiko-bot
  2026-08-20  6:26 ` [PATCH 4/4] MAINTAINERS: add entry for the " Karl Asseily
  3 siblings, 1 reply; 8+ messages in thread
From: Karl Asseily @ 2026-08-20  6:26 UTC (permalink / raw)
  To: broonie, lgirdwood, robh, krzk+dt, conor+dt, perex, tiwai
  Cc: linux-sound, devicetree, linux-kernel, Karl Asseily

The ES9039Q2M is a 32-bit two-channel audio DAC with an asynchronous
sample rate converter. It selects between two control personalities with
its MODE pin: hardware mode, strapped by HW0/HW1/HW2 with no control bus
at all, and software mode over I2C or SPI. This driver implements
software mode over I2C, which MODE = GND selects.

Three properties of the part shape the driver:

 - The ASRC in front of the DAC means MCLK need not be synchronous with
   BCLK or LRCK, so a board can feed it a fixed oscillator and never
   touch the clock again. In that asynchronous mode the part requires
   MCLK >= 130 x Fs, so the driver installs a startup constraint that
   derives the maximum rate from the MCLK it was given rather than
   advertising rates the hardware cannot honour.

 - Selecting an input format takes two registers, not one. Register 57
   INPUT_SEL chooses which port to listen to; register 1 SYS MODE CONFIG
   enables the corresponding decoder, and at reset only ENABLE_TDM_DECODE
   is set. Selecting DoP without also enabling ENABLE_DOP_DECODE leaves
   the part hunting for a marker with the marker decoder switched off, so
   it finds no valid DoP and mutes. The driver sets both.

 - Several registers have non-zero reserved defaults - register 88 reads
   0xb8 at reset - so every write is read-modify-write.

DoP is detected by the part rather than announced by the host:
AUTO_INPUT_SEL is enabled and both the TDM and DoP decoders run, so a
player can simply send DoP-encoded PCM. That is the only way DoP works in
practice, because no player can select a DAC mode through a kcontrol
first, and material sent to a part that is not looking for the marker
renders as the near-silent hiss the DoP design deliberately degrades to.

The programmable FIR coefficient controls are write-only by design. The
part has a PROG_COEFF_OUT register described as "Programmable FIR
coefficient readback", but it is not a RAM read port: it returns the last
coefficient written, whatever address is selected in PROG_COEFF_ADDR.
Measured by writing two different coefficients to addresses 0 and 1 and
reading them back five ways - plain, with a settle delay, with
PROG_COEFF_EN set, with the address written twice, and with a
write-enable pulse after the address. All ten reads returned the value
written to address 1. A get() built on that register would return
something with the shape of data and none of its meaning, so there is
none.

Every control was verified against the silicon rather than against the
driver's own read-back: written through ALSA, then read from the chip
over raw I2C. That covers the eight filter shapes, the modulator
bandwidth, the IIR bandwidth, the four signed 16-bit THD compensation
coefficients at both extremes of their range, and the automute enable,
time, level and off-level fields including the neighbouring
MUTE_RAMP_TO_GND bit that shares a register with the 11-bit time field.
The coefficient upload, whose RAM cannot be read back, was verified from
the I2C bus itself.

Signed-off-by: Karl Asseily <karl@asseily.com>
---
 sound/soc/codecs/Kconfig     |   14 +
 sound/soc/codecs/Makefile    |    2 +
 sound/soc/codecs/es9039q2m.c | 1259 ++++++++++++++++++++++++++++++++++
 3 files changed, 1275 insertions(+)
 create mode 100644 sound/soc/codecs/es9039q2m.c

diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index f9a47e262a7..a5b9896f234 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -123,6 +123,7 @@ config SND_SOC_ALL_CODECS
 	imply SND_SOC_ES8328_I2C
 	imply SND_SOC_ES8375
 	imply SND_SOC_ES8389
+	imply SND_SOC_ES9039Q2M
 	imply SND_SOC_ES9356
 	imply SND_SOC_ES7134
 	imply SND_SOC_ES7241
@@ -1315,6 +1316,19 @@ config SND_SOC_ES8389
 	tristate "Everest Semi ES8389 CODEC"
 	depends on I2C
 
+config SND_SOC_ES9039Q2M
+	tristate "ESS Technology ES9039Q2M CODEC"
+	depends on I2C
+	select REGMAP_I2C
+	help
+	  Enable support for the ESS Technology ES9039Q2M, a 32-bit
+	  two-channel audio DAC with an asynchronous sample rate converter,
+	  supporting PCM, DSD, DoP and S/PDIF input. This driver covers the
+	  I2C control interface, which the MODE pin selects.
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called snd-soc-es9039q2m.
+
 config SND_SOC_ES9356
 	tristate "Everest Semi ES9356 CODEC SDW"
 	depends on SND_SOC_SDCA
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index 3d122ace75a..3c83a4a1577 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -140,6 +140,7 @@ snd-soc-es8328-i2c-y := es8328-i2c.o
 snd-soc-es8328-spi-y := es8328-spi.o
 snd-soc-es8375-y := es8375.o
 snd-soc-es8389-y := es8389.o
+snd-soc-es9039q2m-y := es9039q2m.o
 snd-soc-es9356-y := es9356.o
 snd-soc-framer-y := framer-codec.o
 snd-soc-fs-amp-lib-y := fs-amp-lib.o
@@ -584,6 +585,7 @@ obj-$(CONFIG_SND_SOC_ES8328_I2C)+= snd-soc-es8328-i2c.o
 obj-$(CONFIG_SND_SOC_ES8328_SPI)+= snd-soc-es8328-spi.o
 obj-$(CONFIG_SND_SOC_ES8375)    += snd-soc-es8375.o
 obj-$(CONFIG_SND_SOC_ES8389)    += snd-soc-es8389.o
+obj-$(CONFIG_SND_SOC_ES9039Q2M)	+= snd-soc-es9039q2m.o
 obj-$(CONFIG_SND_SOC_ES9356)    += snd-soc-es9356.o
 obj-$(CONFIG_SND_SOC_FRAMER)	+= snd-soc-framer.o
 obj-$(CONFIG_SND_SOC_FS_AMP_LIB)+= snd-soc-fs-amp-lib.o
diff --git a/sound/soc/codecs/es9039q2m.c b/sound/soc/codecs/es9039q2m.c
new file mode 100644
index 00000000000..64db8636a01
--- /dev/null
+++ b/sound/soc/codecs/es9039q2m.c
@@ -0,0 +1,1259 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * ESS Technology ES9039Q2M 32-bit 2-channel audio DAC
+ *
+ * Copyright (C) 2026 Karl Asseily <karl@asseily.com>
+ *
+ * Every register number, bit field and default in this file was taken from
+ * ES9039Q2M datasheet v0.2.3 and then verified by reading the defaults back off
+ * a live part over I2C.
+ *
+ * The part has two control personalities selected by the MODE pin: hardware
+ * mode (strapped by HW0/HW1/HW2, no bus at all) and software mode (I2C or SPI).
+ * This driver implements software mode over I2C, which MODE = GND selects.
+ *
+ * Three properties shape the driver:
+ *
+ *  - There is an ASRC in front of the DAC, so MCLK need not be synchronous with
+ *    BCLK or LRCK. A board can feed it a fixed oscillator and never touch the
+ *    clock again, which is why the part will pass audio with no register writes
+ *    at all.
+ *
+ *  - INPUT_SEL chooses PCM / DSD / DoP / S/PDIF. It does NOT choose I2S vs
+ *    left-justified - that is TDM_LJ_MODE in register 60, and there is no
+ *    right-justified mode to map onto at all.
+ *
+ *  - Several registers have non-zero reserved defaults (register 88 reads
+ *    0xb8 at reset), so every write here is read-modify-write.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/clk.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#include <sound/control.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+/* ------------------------------------------------- read/write registers ---- */
+
+#define ES9039_SYSTEM_CONFIG		0x00	/* reg 0 */
+#define   ES9039_64FS_MODE		BIT(6)
+
+/*
+ * Register 1 selects which DECODERS are running, and it is separate from
+ * INPUT_SEL in register 57, which only says which port to listen to. Both are
+ * needed: on reset only ENABLE_TDM_DECODE is set, so selecting DoP as the input
+ * while leaving bit 2 clear leaves the part hunting for a marker with the
+ * marker decoder switched off. It then finds no valid DoP and mutes - silence,
+ * DOP_VALID reading 0, and nothing anywhere saying why. Measured on B1,
+ * 2026-08-19.
+ */
+#define ES9039_SYS_MODE			0x01	/* reg 1, reset 0xb1 */
+#define   ES9039_ENABLE_TDM_DECODE	BIT(0)	/* set at reset */
+#define   ES9039_ENABLE_DSD_DECODE	BIT(1)
+#define   ES9039_ENABLE_DOP_DECODE	BIT(2)
+#define   ES9039_ENABLE_SPDIF_DECODE	BIT(3)
+#define   ES9039_SYNC_MODE		BIT(6)	/* 0 = ASYNC, which DATUM uses */
+#define   ES9039_ENABLE_DAC_CLK		BIT(7)	/* set at reset */
+#define   ES9039_DECODE_MASK		(ES9039_ENABLE_TDM_DECODE | \
+					 ES9039_ENABLE_DSD_DECODE | \
+					 ES9039_ENABLE_DOP_DECODE)
+
+#define ES9039_AUTO_FS_DETECT		0x03	/* reg 3 */
+#define   ES9039_AUTO_FS_DETECT_EN	BIT(7)
+
+#define ES9039_INPUT_SEL		0x39	/* reg 57 */
+#define   ES9039_AUTO_INPUT_SEL		BIT(0)
+#define   ES9039_INPUT_SEL_MASK		GENMASK(2, 1)
+#define     ES9039_INPUT_PCM		0x0
+#define     ES9039_INPUT_DSD		0x1
+#define     ES9039_INPUT_DOP		0x2
+#define     ES9039_INPUT_SPDIF		0x3
+#define   ES9039_PCM_MASTER_MODE	BIT(4)
+#define   ES9039_DSD_MASTER_MODE	BIT(5)
+#define   ES9039_DSD_FAULT_DETECT	BIT(6)	/* set at reset */
+
+#define ES9039_MASTER_ENC		0x3a	/* reg 58 */
+#define   ES9039_BCK_INV		BIT(6)
+
+#define ES9039_TDM_CH_NUM		0x3b	/* reg 59, slots = value + 1 */
+#define   ES9039_TDM_CH_NUM_MASK	GENMASK(4, 0)
+
+#define ES9039_TDM_CONFIG1		0x3c	/* reg 60 */
+#define   ES9039_TDM_VALID_EDGE		BIT(6)
+#define   ES9039_TDM_LJ_MODE		BIT(7)	/* 0 = standard I2S */
+
+#define ES9039_TDM_CONFIG2		0x3d	/* reg 61 */
+#define   ES9039_TDM_BIT_WIDTH_MASK	GENMASK(6, 5)
+#define     ES9039_WIDTH_32		0x0
+#define     ES9039_WIDTH_24		0x1
+#define     ES9039_WIDTH_16		0x2
+
+#define ES9039_MONITOR_CFG		0x3e	/* reg 62 */
+#define   ES9039_DISABLE_PCM_DC		BIT(3)
+#define   ES9039_ENABLE_BCK_MONITOR	BIT(4)	/* set at reset */
+#define   ES9039_ENABLE_WS_MONITOR	BIT(5)	/* set at reset */
+#define   ES9039_DISABLE_DSD_MUTE	BIT(6)
+#define   ES9039_DISABLE_DSD_DC		BIT(7)
+
+#define ES9039_VOLUME_CH1		0x4a	/* reg 74, 0x00 = 0 dB */
+#define ES9039_VOLUME_CH2		0x4b	/* reg 75, 0xff = -127.5 dB */
+#define   ES9039_VOL_MAX		0xff
+
+#define ES9039_VOL_RATE_UP		0x52	/* reg 82 */
+#define ES9039_VOL_RATE_DOWN		0x53	/* reg 83 */
+
+#define ES9039_DAC_MUTE			0x56	/* reg 86, 1 = muted */
+#define   ES9039_MUTE_CH1		BIT(0)
+#define   ES9039_MUTE_CH2		BIT(1)
+#define   ES9039_MUTE_BOTH		(ES9039_MUTE_CH1 | ES9039_MUTE_CH2)
+
+#define ES9039_DAC_INVERT		0x57	/* reg 87 */
+
+#define ES9039_FILTER_SHAPE		0x58	/* reg 88, [7:3] reset to 10111 */
+#define   ES9039_FILTER_SHAPE_MASK	GENMASK(2, 0)
+#define     ES9039_FILTER_APODIZING	1	/* linear phase apodizing fast */
+
+#define ES9039_IIR_SPDIF		0x59	/* reg 89 */
+#define   ES9039_IIR_BW_MASK		GENMASK(2, 0)
+#define   ES9039_VOLUME_HOLD		BIT(3)
+#define   ES9039_SPDIF_SEL_MASK		GENMASK(7, 4)
+
+#define ES9039_DAC_PATH			0x5a	/* reg 90 */
+#define   ES9039_BYPASS_FIR2X		BIT(0)
+#define   ES9039_BYPASS_FIR4X		BIT(1)
+#define   ES9039_BYPASS_IIR		BIT(2)
+
+#define ES9039_THD_C2			0x5b	/* regs 91-94: CH1 lo, CH2 hi */
+#define ES9039_THD_C3			0x6b	/* regs 107-110 */
+
+#define ES9039_AUTOMUTE_EN		0x7b	/* reg 123, both set at reset */
+#define ES9039_AUTOMUTE_TIME		0x7c	/* regs 124-125 */
+#define   ES9039_AUTOMUTE_TIME_MASK	GENMASK(10, 0)
+#define   ES9039_MUTE_RAMP_TO_GND	BIT(11)	/* set at reset */
+#define ES9039_AUTOMUTE_LEVEL		0x7e	/* regs 126-127 */
+#define ES9039_AUTOMUTE_OFF_LEVEL	0x80	/* regs 128-129 */
+
+#define ES9039_SOFT_RAMP		0x82	/* reg 130, valid 0..12 */
+#define   ES9039_SOFT_RAMP_MASK		GENMASK(4, 0)
+#define   ES9039_SOFT_RAMP_MAX		12
+
+#define ES9039_NSMOD			0x83	/* reg 131 */
+#define   ES9039_NSMOD_WIDE_BW_MASK	GENMASK(4, 1)
+#define     ES9039_NSMOD_DEFAULT	0x4
+#define     ES9039_NSMOD_WIDE		0xc
+
+#define ES9039_PROG_RAM_CTRL		0x87	/* reg 135 */
+#define   ES9039_PROG_COEFF_EN		BIT(0)
+#define   ES9039_PROG_COEFF_WE		BIT(1)
+
+#define ES9039_PROG_RAM_ADDR		0x89	/* reg 137 */
+#define   ES9039_PROG_ADDR_MASK		GENMASK(6, 0)
+#define   ES9039_PROG_STAGE_4X		BIT(7)
+
+#define ES9039_PROG_RAM_DATA		0x8a	/* regs 138-140, 24-bit signed */
+
+#define ES9039_LAST_RW			0x8e	/* reg 145 */
+
+/* ----------------------------------------------------- readback registers -- */
+
+#define ES9039_READBACK_BASE		0xe0	/* reg 224 */
+
+#define ES9039_CHIP_ID			0xe1	/* reg 225 */
+#define   ES9039_CHIP_ID_ES9039Q2M	0x63
+
+#define ES9039_IRQ_SOURCES		0xea	/* regs 234-235, 16-bit */
+#define   ES9039_SRC_VOL_MIN_MASK	GENMASK(1, 0)
+#define   ES9039_SRC_AUTOMUTE_MASK	GENMASK(3, 2)
+#define   ES9039_SRC_SS_RAMP_MASK	GENMASK(5, 4)
+#define   ES9039_SRC_DOP_VALID		BIT(6)
+#define   ES9039_SRC_BCK_WS_FAIL	BIT(7)
+#define   ES9039_SRC_TDM_VALID		BIT(11)
+
+#define ES9039_AUTO_FS_READ		0xef	/* reg 239 */
+#define   ES9039_FS_DIV_MASK		GENMASK(5, 0)
+#define   ES9039_FS_HALF_DIV		BIT(6)
+#define   ES9039_FS_DIV_VALID		BIT(7)
+
+#define ES9039_AUTOMUTE_READ		0xf2	/* reg 242 */
+
+#define ES9039_INPUT_STREAM_READ	0xf5	/* reg 245 */
+#define   ES9039_RD_INPUT_SEL_MASK	GENMASK(1, 0)
+#define   ES9039_RD_DOP_VALID		BIT(2)
+#define   ES9039_RD_TDM_VALID		BIT(3)
+#define   ES9039_RD_SPDIF_VALID		BIT(4)
+
+#define ES9039_MAX_REGISTER		0xfb	/* reg 251 */
+
+/* Programmable oversampling FIR: 128 taps in the 2x stage, 32 in the 4x. */
+#define ES9039_FIR2X_TAPS		128
+#define ES9039_FIR4X_TAPS		32
+#define ES9039_COEFF_BYTES		3
+
+/* ------------------------------------------------------------------ private */
+
+struct es9039q2m_priv {
+	struct regmap *regmap;
+	struct clk *mclk;
+	unsigned int mclk_rate;
+	unsigned int fmt;
+	unsigned int stream_rate;	/* last rate from hw_params */
+	unsigned int bclk_ratio;	/* bit clocks per frame, 0 = unknown */
+	bool dop_auto;		/* let the part detect DoP itself */
+};
+
+/*
+ * Multi-byte fields are little-endian across ascending register addresses:
+ * register N holds bits [7:0], N+1 holds [15:8], and so on.
+ */
+static int es9039_read_le(struct regmap *map, unsigned int reg, int n, u32 *out)
+{
+	u8 buf[4];
+	int ret, i;
+
+	ret = regmap_bulk_read(map, reg, buf, n);
+	if (ret)
+		return ret;
+
+	*out = 0;
+	for (i = 0; i < n; i++)
+		*out |= (u32)buf[i] << (8 * i);
+
+	return 0;
+}
+
+static int es9039_write_le(struct regmap *map, unsigned int reg, int n, u32 val)
+{
+	u8 buf[4];
+	int i;
+
+	for (i = 0; i < n; i++)
+		buf[i] = (val >> (8 * i)) & 0xff;
+
+	return regmap_bulk_write(map, reg, buf, n);
+}
+
+/* ------------------------------------------------------------------ regmap */
+
+static bool es9039q2m_writeable_reg(struct device *dev, unsigned int reg)
+{
+	return reg <= ES9039_LAST_RW;
+}
+
+static bool es9039q2m_readable_reg(struct device *dev, unsigned int reg)
+{
+	return reg <= ES9039_LAST_RW || reg >= ES9039_READBACK_BASE;
+}
+
+static bool es9039q2m_volatile_reg(struct device *dev, unsigned int reg)
+{
+	return reg >= ES9039_READBACK_BASE;
+}
+
+static const struct regmap_config es9039q2m_regmap = {
+	.reg_bits	= 8,
+	.val_bits	= 8,
+	.max_register	= ES9039_MAX_REGISTER,
+	.writeable_reg	= es9039q2m_writeable_reg,
+	.readable_reg	= es9039q2m_readable_reg,
+	.volatile_reg	= es9039q2m_volatile_reg,
+	.cache_type	= REGCACHE_MAPLE,
+};
+
+/* --------------------------------------------------- signed 16-bit controls */
+
+struct es9039_s16_ctl {
+	unsigned int reg;
+	unsigned int shift;	/* 0 for CH1, 16 for CH2 within the 32-bit pair */
+};
+
+static int es9039_s16_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 = S16_MIN;
+	uinfo->value.integer.max = S16_MAX;
+	return 0;
+}
+
+static int es9039_s16_get(struct snd_kcontrol *kcontrol,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	struct es9039_s16_ctl *p = (void *)kcontrol->private_value;
+	u32 v;
+	int ret;
+
+	ret = es9039_read_le(priv->regmap, p->reg + (p->shift / 8), 2, &v);
+	if (ret)
+		return ret;
+
+	ucontrol->value.integer.value[0] = (s16)v;
+	return 0;
+}
+
+static int es9039_s16_put(struct snd_kcontrol *kcontrol,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	struct es9039_s16_ctl *p = (void *)kcontrol->private_value;
+	long v = ucontrol->value.integer.value[0];
+	u32 old;
+	int ret;
+
+	if (v < S16_MIN || v > S16_MAX)
+		return -EINVAL;
+
+	ret = es9039_read_le(priv->regmap, p->reg + (p->shift / 8), 2, &old);
+	if (ret)
+		return ret;
+
+	if ((s16)old == (s16)v)
+		return 0;
+
+	ret = es9039_write_le(priv->regmap, p->reg + (p->shift / 8), 2,
+			      (u16)v);
+	return ret ? ret : 1;
+}
+
+#define ES9039_S16(xname, xreg, xshift)					\
+{									\
+	.iface	= SNDRV_CTL_ELEM_IFACE_MIXER,				\
+	.name	= xname,						\
+	.info	= es9039_s16_info,					\
+	.get	= es9039_s16_get,					\
+	.put	= es9039_s16_put,					\
+	.private_value = (unsigned long)&(struct es9039_s16_ctl)	\
+			 { .reg = xreg, .shift = xshift },		\
+}
+
+/* ------------------------------------------------- multi-register integers */
+
+struct es9039_wide_ctl {
+	unsigned int reg;
+	unsigned int bytes;
+	unsigned int mask;
+	unsigned int max;
+};
+
+static int es9039_wide_info(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	struct es9039_wide_ctl *p = (void *)kcontrol->private_value;
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = p->max;
+	return 0;
+}
+
+static int es9039_wide_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	struct es9039_wide_ctl *p = (void *)kcontrol->private_value;
+	u32 v;
+	int ret;
+
+	ret = es9039_read_le(priv->regmap, p->reg, p->bytes, &v);
+	if (ret)
+		return ret;
+
+	ucontrol->value.integer.value[0] = v & p->mask;
+	return 0;
+}
+
+static int es9039_wide_put(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	struct es9039_wide_ctl *p = (void *)kcontrol->private_value;
+	long v = ucontrol->value.integer.value[0];
+	u32 old;
+	int ret;
+
+	if (v < 0 || v > p->max)
+		return -EINVAL;
+
+	ret = es9039_read_le(priv->regmap, p->reg, p->bytes, &old);
+	if (ret)
+		return ret;
+
+	if ((old & p->mask) == (u32)v)
+		return 0;
+
+	/* Preserve the bits outside the field - reg 124 carries MUTE_RAMP. */
+	ret = es9039_write_le(priv->regmap, p->reg, p->bytes,
+			      (old & ~p->mask) | (u32)v);
+	return ret ? ret : 1;
+}
+
+#define ES9039_WIDE(xname, xreg, xbytes, xmask, xmax)			\
+{									\
+	.iface	= SNDRV_CTL_ELEM_IFACE_MIXER,				\
+	.name	= xname,						\
+	.info	= es9039_wide_info,					\
+	.get	= es9039_wide_get,					\
+	.put	= es9039_wide_put,					\
+	.private_value = (unsigned long)&(struct es9039_wide_ctl)	\
+			 { .reg = xreg, .bytes = xbytes,		\
+			   .mask = xmask, .max = xmax },		\
+}
+
+/* ------------------------------------------------ programmable FIR upload --
+ *
+ * Write-only, deliberately. The chip has a PROG_COEFF_OUT register (248-246)
+ * described only as "Programmable FIR coefficient readback", but it is not a
+ * RAM read port: it returns the LAST COEFFICIENT WRITTEN, whatever address is
+ * selected in PROG_COEFF_ADDR. Measured on B1, 2026-08-19, over raw I2C with
+ * this driver out of the path - two different coefficients written to
+ * addresses 0 and 1, then read back with five different sequences (plain, with
+ * a settle delay, with PROG_COEFF_EN set, with the address written twice, and
+ * with a WE pulse after the address). All ten reads returned the value written
+ * to address 1.
+ *
+ * A get() built on that register would return something with the shape of data
+ * and none of its meaning, so there is no get(). If ESS documents a real
+ * readback sequence, add one - tools/es9039-coeff-probe.sh in the DATUM
+ * repository is the test it has to pass.
+ */
+
+/*
+ * Per-control data rides in our own struct with the soc_bytes_ext EMBEDDED,
+ * recovered by container_of. Not in soc_bytes_ext.dobj: that field belongs to
+ * the topology subsystem and only exists under CONFIG_SND_SOC_TOPOLOGY, so a
+ * driver stashing its own data there fails to build on any config without it.
+ */
+struct es9039_fir_ctl {
+	struct soc_bytes_ext be;
+	unsigned int taps;
+	bool stage_4x;
+};
+
+static int es9039_fir_put(struct snd_kcontrol *kcontrol,
+			  const unsigned int __user *bytes, unsigned int size)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	struct soc_bytes_ext *be = (void *)kcontrol->private_value;
+	struct es9039_fir_ctl *p = container_of(be, struct es9039_fir_ctl, be);
+	u8 *buf;
+	int ret, i;
+
+	if (size != p->taps * ES9039_COEFF_BYTES)
+		return -EINVAL;
+
+	buf = memdup_user(bytes, size);
+	if (IS_ERR(buf))
+		return PTR_ERR(buf);
+
+	/*
+	 * PROG_COEFF_WE is a per-coefficient strobe, not a gate held open
+	 * across the upload. The datasheet's sequence is address, data, raise
+	 * WE, lower WE, once per coefficient. Holding it high for the whole
+	 * loop also appears to work on ES9039Q2M silicon, but "appears to
+	 * work" is not a specification.
+	 */
+	for (i = 0; i < p->taps; i++) {
+		ret = regmap_write(priv->regmap, ES9039_PROG_RAM_ADDR,
+				   (p->stage_4x ? ES9039_PROG_STAGE_4X : 0) |
+				   FIELD_PREP(ES9039_PROG_ADDR_MASK, i));
+		if (ret)
+			goto out;
+
+		ret = regmap_bulk_write(priv->regmap, ES9039_PROG_RAM_DATA,
+					&buf[i * ES9039_COEFF_BYTES],
+					ES9039_COEFF_BYTES);
+		if (ret)
+			goto out;
+
+		ret = regmap_update_bits(priv->regmap, ES9039_PROG_RAM_CTRL,
+					 ES9039_PROG_COEFF_WE,
+					 ES9039_PROG_COEFF_WE);
+		if (ret)
+			goto out;
+
+		ret = regmap_update_bits(priv->regmap, ES9039_PROG_RAM_CTRL,
+					 ES9039_PROG_COEFF_WE, 0);
+		if (ret)
+			goto out;
+	}
+
+out:
+	regmap_update_bits(priv->regmap, ES9039_PROG_RAM_CTRL,
+			   ES9039_PROG_COEFF_WE, 0);
+	kfree(buf);
+	return ret ? ret : 1;
+}
+
+static struct es9039_fir_ctl es9039_fir2x = {
+	.be = { .max = ES9039_FIR2X_TAPS * ES9039_COEFF_BYTES,
+		.put = es9039_fir_put },
+	.taps = ES9039_FIR2X_TAPS,
+};
+
+static struct es9039_fir_ctl es9039_fir4x = {
+	.be = { .max = ES9039_FIR4X_TAPS * ES9039_COEFF_BYTES,
+		.put = es9039_fir_put },
+	.taps = ES9039_FIR4X_TAPS,
+	.stage_4x = true,
+};
+
+#define ES9039_FIR(xname, xctl)						\
+{									\
+	.iface	= SNDRV_CTL_ELEM_IFACE_MIXER,				\
+	.name	= xname,						\
+	.info	= snd_soc_bytes_info_ext,				\
+	.tlv.c	= snd_soc_bytes_tlv_callback,				\
+	.access	= SNDRV_CTL_ELEM_ACCESS_TLV_WRITE |			\
+		  SNDRV_CTL_ELEM_ACCESS_TLV_CALLBACK,			\
+	.private_value = (unsigned long)&(xctl).be,			\
+}
+
+/* ------------------------------------------------------- status (read-only) */
+
+struct es9039_stat_ctl {
+	unsigned int reg;
+	unsigned int mask;
+	unsigned int max;
+};
+
+static int es9039_stat_info(struct snd_kcontrol *kcontrol,
+			    struct snd_ctl_elem_info *uinfo)
+{
+	struct es9039_stat_ctl *p = (void *)kcontrol->private_value;
+
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+	uinfo->count = 1;
+	uinfo->value.integer.min = 0;
+	uinfo->value.integer.max = p->max;
+	return 0;
+}
+
+static int es9039_stat_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	struct es9039_stat_ctl *p = (void *)kcontrol->private_value;
+	unsigned int v;
+	int ret;
+
+	ret = regmap_read(priv->regmap, p->reg, &v);
+	if (ret)
+		return ret;
+
+	ucontrol->value.integer.value[0] =
+		(v & p->mask) >> (ffs(p->mask) - 1);
+	return 0;
+}
+
+#define ES9039_STAT(xname, xreg, xmask, xmax)				\
+{									\
+	.iface	= SNDRV_CTL_ELEM_IFACE_MIXER,				\
+	.name	= xname,						\
+	.access	= SNDRV_CTL_ELEM_ACCESS_READ |				\
+		  SNDRV_CTL_ELEM_ACCESS_VOLATILE,			\
+	.info	= es9039_stat_info,					\
+	.get	= es9039_stat_get,					\
+	.private_value = (unsigned long)&(struct es9039_stat_ctl)	\
+			 { .reg = xreg, .mask = xmask, .max = xmax },	\
+}
+
+/*
+ * Detected sample rate.
+ *
+ * When the part's own rate detector has a valid ratio, use it - it is measured
+ * from the incoming frame clock and is the ground truth:
+ *
+ *   FS = Y * SYS_CLK / ((X + 1) * (128 >> 64FS_MODE))
+ *
+ * with X = IDAC_DIV_AUTO and Y = 2 when IDAC_HALF_DIV_AUTO reports a
+ * half-integer multiple.
+ *
+ * That detector is UNAVAILABLE on any board running the DAC asynchronously -
+ * register 3[7] AUTO_FS_DETECT carries the note "Cannot be used in ASYNC mode".
+ * A board feeding a free-running oscillator and letting the ASRC absorb the
+ * difference is precisely that case, and it is the preferable design, so the
+ * detector reading 0 there is expected rather than a fault. Fall back to the
+ * rate the stream was opened at, which is what a front panel wants to show.
+ * Reports 0 only when nothing is playing and the chip has no lock either.
+ */
+static int es9039_rate_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 = 0;
+	uinfo->value.integer.max = 1536000;
+	return 0;
+}
+
+static int es9039_rate_get(struct snd_kcontrol *kcontrol,
+			   struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	unsigned int fsreg, sysreg, div, y, den;
+	int ret;
+
+	ucontrol->value.integer.value[0] = priv->stream_rate;
+
+	if (!priv->mclk_rate)
+		return 0;
+
+	ret = regmap_read(priv->regmap, ES9039_AUTO_FS_READ, &fsreg);
+	if (ret)
+		return ret;
+
+	if (!(fsreg & ES9039_FS_DIV_VALID))
+		return 0;	/* async mode: keep the stream rate set above */
+
+	ret = regmap_read(priv->regmap, ES9039_SYSTEM_CONFIG, &sysreg);
+	if (ret)
+		return ret;
+
+	div = FIELD_GET(ES9039_FS_DIV_MASK, fsreg) + 1;
+	y   = (fsreg & ES9039_FS_HALF_DIV) ? 2 : 1;
+	den = div * ((sysreg & ES9039_64FS_MODE) ? 64 : 128);
+
+	ucontrol->value.integer.value[0] =
+		DIV_ROUND_CLOSEST(priv->mclk_rate * y, den);
+	return 0;
+}
+
+static const char * const es9039_stream_texts[] = {
+	"PCM", "DSD", "DoP", "S/PDIF",
+};
+
+static int es9039_stream_info(struct snd_kcontrol *kcontrol,
+			      struct snd_ctl_elem_info *uinfo)
+{
+	return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(es9039_stream_texts),
+				 es9039_stream_texts);
+}
+
+static int es9039_stream_get(struct snd_kcontrol *kcontrol,
+			     struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	unsigned int v;
+	int ret;
+
+	ret = regmap_read(priv->regmap, ES9039_INPUT_STREAM_READ, &v);
+	if (ret)
+		return ret;
+
+	ucontrol->value.enumerated.item[0] =
+		FIELD_GET(ES9039_RD_INPUT_SEL_MASK, v);
+	return 0;
+}
+
+/* ----------------------------------------------------------------- controls */
+
+static const DECLARE_TLV_DB_SCALE(es9039_vol_tlv, -12750, 50, 1);
+
+static const char * const es9039_filter_texts[] = {
+	"Minimum Phase",
+	"Linear Phase Apodizing Fast Roll-Off",
+	"Linear Phase Fast Roll-Off",
+	"Linear Phase Fast Roll-Off Low Ripple",
+	"Linear Phase Slow Roll-Off",
+	"Minimum Phase Fast Roll-Off",
+	"Minimum Phase Slow Roll-Off",
+	"Minimum Phase Slow Roll-Off Low Dispersion",
+};
+
+static SOC_ENUM_SINGLE_DECL(es9039_filter_enum, ES9039_FILTER_SHAPE, 0,
+			    es9039_filter_texts);
+
+/* IIR_BW is a multiple of the datapath bandwidth, not a frequency. */
+static const char * const es9039_iir_texts[] = {
+	"Reserved", "BW x8", "BW x4", "BW x2", "BW", "BW /2", "BW /4", "BW /8",
+};
+
+static SOC_ENUM_SINGLE_DECL(es9039_iir_enum, ES9039_IIR_SPDIF, 0,
+			    es9039_iir_texts);
+
+static const char * const es9039_nsmod_texts[] = {
+	"Default", "Wide Bandwidth",
+};
+
+static const unsigned int es9039_nsmod_values[] = {
+	ES9039_NSMOD_DEFAULT, ES9039_NSMOD_WIDE,
+};
+
+static SOC_VALUE_ENUM_SINGLE_DECL(es9039_nsmod_enum, ES9039_NSMOD, 1,
+				  GENMASK(3, 0), es9039_nsmod_texts,
+				  es9039_nsmod_values);
+
+static int es9039_dop_get(struct snd_kcontrol *kcontrol,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+
+	ucontrol->value.integer.value[0] = priv->dop_auto;
+	return 0;
+}
+
+static int es9039_dop_put(struct snd_kcontrol *kcontrol,
+			  struct snd_ctl_elem_value *ucontrol)
+{
+	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
+	bool on = !!ucontrol->value.integer.value[0];
+	int ret;
+
+	if (on == priv->dop_auto)
+		return 0;
+
+	priv->dop_auto = on;
+
+	ret = regmap_update_bits(priv->regmap, ES9039_INPUT_SEL,
+				 ES9039_AUTO_INPUT_SEL,
+				 on ? ES9039_AUTO_INPUT_SEL : 0);
+	if (ret)
+		return ret;
+
+	return 1;
+}
+
+static const struct snd_kcontrol_new es9039q2m_controls[] = {
+	/* --- level --- */
+	SOC_DOUBLE_R_TLV("Master Playback Volume",
+			 ES9039_VOLUME_CH1, ES9039_VOLUME_CH2,
+			 0, ES9039_VOL_MAX, 1, es9039_vol_tlv),
+	SOC_DOUBLE("Master Playback Switch", ES9039_DAC_MUTE, 0, 1, 1, 1),
+	SOC_DOUBLE("DAC Invert Switch", ES9039_DAC_INVERT, 0, 1, 1, 0),
+	SOC_SINGLE("Volume Ramp Up Rate", ES9039_VOL_RATE_UP, 0, 255, 0),
+	SOC_SINGLE("Volume Ramp Down Rate", ES9039_VOL_RATE_DOWN, 0, 255, 0),
+	SOC_SINGLE("Soft Ramp Time", ES9039_SOFT_RAMP, 0,
+		   ES9039_SOFT_RAMP_MAX, 0),
+
+	/* --- reconstruction filter --- */
+	SOC_ENUM("Filter Shape", es9039_filter_enum),
+	SOC_ENUM("IIR Bandwidth", es9039_iir_enum),
+	SOC_ENUM("Modulator Bandwidth", es9039_nsmod_enum),
+	SOC_SINGLE("IIR Filter Bypass Switch", ES9039_DAC_PATH, 2, 1, 0),
+	SOC_SINGLE("FIR 2x Bypass Switch", ES9039_DAC_PATH, 0, 1, 0),
+	SOC_SINGLE("FIR 4x Bypass Switch", ES9039_DAC_PATH, 1, 1, 0),
+	SOC_SINGLE("Custom FIR Switch", ES9039_PROG_RAM_CTRL, 0, 1, 0),
+	ES9039_FIR("FIR 2x Coefficients", es9039_fir2x),
+	ES9039_FIR("FIR 4x Coefficients", es9039_fir4x),
+
+	/*
+	 * Not "... Volume". ALSA reserves that suffix for gain controls
+	 * carrying a dB scale, and these are signed correction coefficients
+	 * for the second and third harmonic - they have no dB meaning and no
+	 * TLV. Useful values come from measuring a given board's distortion on
+	 * an analyser and solving for them; zero, the reset value, is the only
+	 * honest default until someone has.
+	 */
+	/* --- distortion compensation --- */
+	ES9039_S16("THD Compensation C2 CH1", ES9039_THD_C2, 0),
+	ES9039_S16("THD Compensation C2 CH2", ES9039_THD_C2, 16),
+	ES9039_S16("THD Compensation C3 CH1", ES9039_THD_C3, 0),
+	ES9039_S16("THD Compensation C3 CH2", ES9039_THD_C3, 16),
+
+	/* --- automute --- */
+	SOC_DOUBLE("Automute Switch", ES9039_AUTOMUTE_EN, 0, 1, 1, 0),
+	ES9039_WIDE("Automute Time", ES9039_AUTOMUTE_TIME, 2,
+		    ES9039_AUTOMUTE_TIME_MASK, 2047),
+	ES9039_WIDE("Automute Level", ES9039_AUTOMUTE_LEVEL, 2, 0xffff, 65535),
+	ES9039_WIDE("Automute Off Level", ES9039_AUTOMUTE_OFF_LEVEL, 2,
+		    0xffff, 65535),
+	SOC_SINGLE("Mute Ramp To Ground Switch", ES9039_AUTOMUTE_TIME + 1,
+		   3, 1, 0),
+	SOC_SINGLE("DSD DC Automute Switch", ES9039_MONITOR_CFG, 7, 1, 1),
+	SOC_SINGLE("DSD Mute Pattern Switch", ES9039_MONITOR_CFG, 6, 1, 1),
+	SOC_SINGLE("PCM DC Automute Switch", ES9039_MONITOR_CFG, 3, 1, 1),
+
+	/* --- stream --- */
+	/*
+	 * On by default. DoP is designed to be detected, not announced: the
+	 * player just sends it and a DoP-aware DAC notices the marker, which
+	 * is why a DAC that does not notice plays it as near-silence rather
+	 * than noise. Roon and every other player rely on that, and none of
+	 * them can reach into ALSA to flip a mode first.
+	 *
+	 * Left switchable because automatic detection is a pattern match, and
+	 * anyone worried about PCM material that happens to look like DoP can
+	 * turn it off and get strictly PCM.
+	 */
+	SOC_SINGLE_BOOL_EXT("DoP Auto Detect Switch", 0,
+			    es9039_dop_get, es9039_dop_put),
+
+	/* --- status, read-only --- */
+	ES9039_STAT("Automute Active CH1", ES9039_AUTOMUTE_READ, BIT(0), 1),
+	ES9039_STAT("Automute Active CH2", ES9039_AUTOMUTE_READ, BIT(1), 1),
+	ES9039_STAT("DoP Valid", ES9039_INPUT_STREAM_READ,
+		    ES9039_RD_DOP_VALID, 1),
+	ES9039_STAT("TDM Data Valid", ES9039_INPUT_STREAM_READ,
+		    ES9039_RD_TDM_VALID, 1),
+	ES9039_STAT("SPDIF Valid", ES9039_INPUT_STREAM_READ,
+		    ES9039_RD_SPDIF_VALID, 1),
+	ES9039_STAT("Clock Fault", ES9039_IRQ_SOURCES + 1, BIT(7), 1),
+	{
+		.iface	= SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name	= "Detected Sample Rate",
+		.access	= SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info	= es9039_rate_info,
+		.get	= es9039_rate_get,
+	},
+	{
+		.iface	= SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name	= "Detected Input Format",
+		.access	= SNDRV_CTL_ELEM_ACCESS_READ |
+			  SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+		.info	= es9039_stream_info,
+		.get	= es9039_stream_get,
+	},
+};
+
+/* --------------------------------------------------------------------- DAPM */
+
+static const struct snd_soc_dapm_widget es9039q2m_widgets[] = {
+	SND_SOC_DAPM_DAC("DAC", NULL, SND_SOC_NOPM, 0, 0),
+	SND_SOC_DAPM_OUTPUT("AOUTL"),
+	SND_SOC_DAPM_OUTPUT("AOUTR"),
+};
+
+static const struct snd_soc_dapm_route es9039q2m_routes[] = {
+	{ "DAC",   NULL, "Playback" },
+	{ "AOUTL", NULL, "DAC" },
+	{ "AOUTR", NULL, "DAC" },
+};
+
+/* ---------------------------------------------------------------------- DAI */
+
+/*
+ * In asynchronous mode - a fixed oscillator with no relationship to the incoming
+ * frame clock, which is how any board using the ASRC properly is wired - the
+ * datasheet requires MCLK >= 130 * FS (hardware mode table, ASYNC rows). Cap the
+ * rate accordingly so a machine driver cannot open a stream the clock cannot
+ * legally carry. With a 24.576 MHz oscillator that ceiling is 189 kHz, which
+ * means 176.4 kHz is available and 192 kHz is not.
+ */
+#define ES9039_ASYNC_MIN_MCLK_FS	130
+
+static int es9039q2m_startup(struct snd_pcm_substream *substream,
+			     struct snd_soc_dai *dai)
+{
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(dai->component);
+	unsigned int max_rate;
+
+	if (!priv->mclk_rate)
+		return 0;
+
+	max_rate = priv->mclk_rate / ES9039_ASYNC_MIN_MCLK_FS;
+
+	return snd_pcm_hw_constraint_minmax(substream->runtime,
+					    SNDRV_PCM_HW_PARAM_RATE,
+					    8000, max_rate);
+}
+
+static int es9039q2m_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
+{
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(dai->component);
+	unsigned int cfg1 = 0, enc = 0;
+	int ret;
+
+	/*
+	 * The part can generate BCLK/WS (PCM_MASTER_MODE), but on a board fed a
+	 * fixed oscillator that means deriving them from MCLK and throwing away
+	 * the ASRC's entire purpose. Consumer only, and say so.
+	 */
+	if ((fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) != SND_SOC_DAIFMT_CBC_CFC)
+		return -EINVAL;
+
+	switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
+	case SND_SOC_DAIFMT_I2S:
+		break;
+	case SND_SOC_DAIFMT_LEFT_J:
+		cfg1 |= ES9039_TDM_LJ_MODE;
+		break;
+	default:
+		/* Register 60 offers I2S or LJ. There is no RJ mode. */
+		return -EINVAL;
+	}
+
+	switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
+	case SND_SOC_DAIFMT_NB_NF:
+		break;
+	case SND_SOC_DAIFMT_IB_NF:
+		enc |= ES9039_BCK_INV;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	ret = regmap_update_bits(priv->regmap, ES9039_TDM_CONFIG1,
+				 ES9039_TDM_LJ_MODE, cfg1);
+	if (ret)
+		return ret;
+
+	ret = regmap_update_bits(priv->regmap, ES9039_MASTER_ENC,
+				 ES9039_BCK_INV, enc);
+	if (ret)
+		return ret;
+
+	priv->fmt = fmt;
+	return 0;
+}
+
+/*
+ * TDM_BIT_WIDTH describes the SLOT width on the wire, not the sample size. Those
+ * are routinely different: 16-bit samples are usually carried left-justified in
+ * 32-bit slots. Getting this wrong misaligns the channel boundaries and the
+ * result is one channel, or noise.
+ *
+ * ASoC does not hand the codec the bit clock ratio unless a machine driver sets
+ * it, so take it when offered and otherwise assume 32-bit slots - by far the
+ * most common arrangement, and what the RK3588 I2S does unconditionally
+ * (rockchip_i2s.c sets bclk_ratio = 64 at probe and never varies it with
+ * format).
+ */
+static int es9039q2m_set_bclk_ratio(struct snd_soc_dai *dai, unsigned int ratio)
+{
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(dai->component);
+
+	priv->bclk_ratio = ratio;
+	return 0;
+}
+
+static int es9039q2m_hw_params(struct snd_pcm_substream *substream,
+			       struct snd_pcm_hw_params *params,
+			       struct snd_soc_dai *dai)
+{
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(dai->component);
+	unsigned int input_sel, decode, width, slot_bits, isel;
+	bool auto_sel;
+	int ret;
+
+	priv->stream_rate = params_rate(params);
+
+	switch (params_format(params)) {
+	case SNDRV_PCM_FORMAT_DSD_U8:
+	case SNDRV_PCM_FORMAT_DSD_U16_LE:
+	case SNDRV_PCM_FORMAT_DSD_U32_LE:
+		/*
+		 * Forced, not auto-detected: the datasheet requires DSD data on
+		 * DATA1 and DATA2 for AUTO_INPUT_SEL to identify it, which a
+		 * two-channel I2S link does not provide.
+		 */
+		input_sel = ES9039_INPUT_DSD;
+		decode = ES9039_ENABLE_DSD_DECODE;
+		auto_sel = false;
+		break;
+	case SNDRV_PCM_FORMAT_S16_LE:
+	case SNDRV_PCM_FORMAT_S24_LE:
+	case SNDRV_PCM_FORMAT_S24_3LE:
+	case SNDRV_PCM_FORMAT_S32_LE:
+		/*
+		 * DoP arrives inside ordinary PCM frames and is indistinguishable
+		 * from PCM until the part finds the marker, so both decoders run
+		 * and AUTO_INPUT_SEL picks between them. INPUT_SEL is programmed
+		 * anyway as the fallback the part uses when auto-detection is
+		 * switched off.
+		 */
+		input_sel = ES9039_INPUT_PCM;
+		decode = ES9039_ENABLE_TDM_DECODE |
+			 (priv->dop_auto ? ES9039_ENABLE_DOP_DECODE : 0);
+		auto_sel = priv->dop_auto;
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	slot_bits = priv->bclk_ratio ?
+		    priv->bclk_ratio / params_channels(params) : 32;
+
+	switch (slot_bits) {
+	case 16:
+		width = ES9039_WIDTH_16;
+		break;
+	case 24:
+		width = ES9039_WIDTH_24;
+		break;
+	case 32:
+		width = ES9039_WIDTH_32;
+		break;
+	default:
+		dev_err(dai->dev, "unsupported slot width %u\n", slot_bits);
+		return -EINVAL;
+	}
+
+	/*
+	 * AUTO_INPUT_SEL belongs in the value as well as the mask. It was in
+	 * the mask alone, so every hw_params quietly cleared it and undid what
+	 * the component probe had set - which is why enabling auto-detection
+	 * by hand mid-stream worked while enabling it in probe() did not.
+	 * INPUT_SEL is still programmed underneath: it is what the part falls
+	 * back to when auto-detection is switched off.
+	 */
+	isel = FIELD_PREP(ES9039_INPUT_SEL_MASK, input_sel);
+	if (auto_sel)
+		isel |= ES9039_AUTO_INPUT_SEL;
+
+	ret = regmap_update_bits(priv->regmap, ES9039_INPUT_SEL,
+				 ES9039_AUTO_INPUT_SEL | ES9039_INPUT_SEL_MASK,
+				 isel);
+	if (ret)
+		return ret;
+
+	ret = regmap_update_bits(priv->regmap, ES9039_TDM_CONFIG2,
+				 ES9039_TDM_BIT_WIDTH_MASK,
+				 FIELD_PREP(ES9039_TDM_BIT_WIDTH_MASK, width));
+	if (ret)
+		return ret;
+
+	ret = regmap_update_bits(priv->regmap, ES9039_TDM_CH_NUM,
+				 ES9039_TDM_CH_NUM_MASK,
+				 params_channels(params) - 1);
+	if (ret)
+		return ret;
+
+	/*
+	 * Above 705.6 kHz there is not enough MCLK for the normal oversampling
+	 * chain, so the part runs at 64FS and forces a minimum phase filter
+	 * regardless of FILTER_SHAPE.
+	 */
+	ret = regmap_update_bits(priv->regmap, ES9039_SYSTEM_CONFIG,
+				 ES9039_64FS_MODE,
+				 params_rate(params) > 705600 ?
+					ES9039_64FS_MODE : 0);
+	if (ret)
+		return ret;
+
+	/* Switch the right decoder on for this stream, and the others off. */
+	return regmap_update_bits(priv->regmap, ES9039_SYS_MODE,
+				  ES9039_DECODE_MASK, decode);
+}
+
+static int es9039q2m_mute_stream(struct snd_soc_dai *dai, int mute, int dir)
+{
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(dai->component);
+
+	return regmap_update_bits(priv->regmap, ES9039_DAC_MUTE,
+				  ES9039_MUTE_BOTH, mute ? ES9039_MUTE_BOTH : 0);
+}
+
+static const struct snd_soc_dai_ops es9039q2m_dai_ops = {
+	.startup	 = es9039q2m_startup,
+	.set_fmt	 = es9039q2m_set_fmt,
+	.set_bclk_ratio	 = es9039q2m_set_bclk_ratio,
+	.hw_params	 = es9039q2m_hw_params,
+	.mute_stream	 = es9039q2m_mute_stream,
+	.no_capture_mute = 1,
+};
+
+#define ES9039_FORMATS	(SNDRV_PCM_FMTBIT_S16_LE     | \
+			 SNDRV_PCM_FMTBIT_S24_LE     | \
+			 SNDRV_PCM_FMTBIT_S24_3LE    | \
+			 SNDRV_PCM_FMTBIT_S32_LE     | \
+			 SNDRV_PCM_FMTBIT_DSD_U8     | \
+			 SNDRV_PCM_FMTBIT_DSD_U16_LE | \
+			 SNDRV_PCM_FMTBIT_DSD_U32_LE)
+
+static struct snd_soc_dai_driver es9039q2m_dai = {
+	.name = "es9039q2m-hifi",
+	.playback = {
+		.stream_name	= "Playback",
+		.channels_min	= 2,
+		.channels_max	= 2,
+		.rates		= SNDRV_PCM_RATE_8000_768000,
+		.formats	= ES9039_FORMATS,
+	},
+	.ops = &es9039q2m_dai_ops,
+};
+
+/* ---------------------------------------------------------------- component */
+
+static int es9039q2m_component_probe(struct snd_soc_component *component)
+{
+	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(component);
+	int ret;
+
+	/*
+	 * Come up muted. The volume registers default to 0 dB, and an unmuted
+	 * DAC arriving into an already-powered analogue stage is how you get a
+	 * thump. ASoC unmutes via mute_stream() when a stream starts.
+	 */
+	ret = regmap_update_bits(priv->regmap, ES9039_DAC_MUTE,
+				 ES9039_MUTE_BOTH, ES9039_MUTE_BOTH);
+	if (ret)
+		return ret;
+
+	/*
+	 * VOLUME_HOLD makes both channel volumes latch together, so a stereo
+	 * change can never momentarily skew the image.
+	 */
+	ret = regmap_update_bits(priv->regmap, ES9039_IIR_SPDIF,
+				 ES9039_VOLUME_HOLD, 0);
+	if (ret)
+		return ret;
+
+	/*
+	 * Let the part identify DoP for itself. Register 57[0] AUTO_INPUT_SEL
+	 * makes it choose between PCM and DoP from the data, which is the only
+	 * way DoP can work in practice: players send DoP-encoded PCM and expect
+	 * the DAC to notice, and none of them can flip an ALSA control first.
+	 * Without this the part is told "PCM", never looks for the marker, and
+	 * renders a DoP stream as the near-silent hiss the DoP marker design
+	 * deliberately degrades to. Verified on hardware by enabling this
+	 * mid-stream and watching reg 245 flip from PCM to DoP with DOP_VALID
+	 * set. tools/datum-dop-test.sh in the DATUM repository is the standing
+	 * test: it plays DoP with nothing flipped and expects the part to
+	 * identify it unaided.
+	 *
+	 * The datasheet's "data must be provided on the DATA2 pin" applies to
+	 * identifying DSD, whose two channels arrive on separate data lines.
+	 * DoP is ordinary stereo I2S on one line and detects correctly without
+	 * it, which the same measurement establishes.
+	 */
+	priv->dop_auto = true;
+	ret = regmap_update_bits(priv->regmap, ES9039_INPUT_SEL,
+				 ES9039_AUTO_INPUT_SEL, ES9039_AUTO_INPUT_SEL);
+	if (ret)
+		return ret;
+
+	/*
+	 * Board defaults, applied once at probe so the part is deterministic
+	 * from cold instead of inheriting whatever its reset value happens to
+	 * be. Both stay user-settable through their kcontrols; these are
+	 * defaults, not policy.
+	 *
+	 * Reconstruction filter: linear phase apodizing fast roll-off. It keeps
+	 * the sharp cut and flat passband of the plain fast linear-phase filter
+	 * while suppressing pre-ringing, and an apodizing response also
+	 * suppresses pre-ringing already baked into the source material by the
+	 * recording chain - which none of the other seven addresses. The cost
+	 * is a little stopband rejection right at the band edge, well above
+	 * where it can matter. Chosen this way because a blind A/B listening
+	 * test on DATUM found no audible difference between any of the eight,
+	 * so the tie is broken on theory rather than on preference.
+	 *
+	 * Modulator: wide bandwidth, which is ESS's own recommendation. It
+	 * moves the modulator's noise further out of band and improves
+	 * linearity at high frequencies.
+	 */
+	ret = regmap_update_bits(priv->regmap, ES9039_FILTER_SHAPE,
+				 ES9039_FILTER_SHAPE_MASK,
+				 ES9039_FILTER_APODIZING);
+	if (ret)
+		return ret;
+
+	ret = regmap_update_bits(priv->regmap, ES9039_NSMOD,
+				 ES9039_NSMOD_WIDE_BW_MASK,
+				 FIELD_PREP(ES9039_NSMOD_WIDE_BW_MASK,
+					    ES9039_NSMOD_WIDE));
+	if (ret)
+		return ret;
+
+	/* Let the part work out the incoming rate; the ASRC does the rest. */
+	return regmap_update_bits(priv->regmap, ES9039_AUTO_FS_DETECT,
+				  ES9039_AUTO_FS_DETECT_EN,
+				  ES9039_AUTO_FS_DETECT_EN);
+}
+
+static const struct snd_soc_component_driver es9039q2m_component = {
+	.probe			= es9039q2m_component_probe,
+	.controls		= es9039q2m_controls,
+	.num_controls		= ARRAY_SIZE(es9039q2m_controls),
+	.dapm_widgets		= es9039q2m_widgets,
+	.num_dapm_widgets	= ARRAY_SIZE(es9039q2m_widgets),
+	.dapm_routes		= es9039q2m_routes,
+	.num_dapm_routes	= ARRAY_SIZE(es9039q2m_routes),
+	.idle_bias_on		= 1,
+	.endianness		= 1,
+};
+
+/* --------------------------------------------------------------------- I2C */
+
+static int es9039q2m_i2c_probe(struct i2c_client *i2c)
+{
+	struct device *dev = &i2c->dev;
+	struct es9039q2m_priv *priv;
+	unsigned int id;
+	int ret;
+
+	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return -ENOMEM;
+
+	priv->regmap = devm_regmap_init_i2c(i2c, &es9039q2m_regmap);
+	if (IS_ERR(priv->regmap))
+		return dev_err_probe(dev, PTR_ERR(priv->regmap),
+				     "failed to init regmap\n");
+
+	priv->mclk = devm_clk_get_optional_enabled(dev, "mclk");
+	if (IS_ERR(priv->mclk))
+		return dev_err_probe(dev, PTR_ERR(priv->mclk),
+				     "failed to get mclk\n");
+
+	if (priv->mclk) {
+		priv->mclk_rate = clk_get_rate(priv->mclk);
+		if (priv->mclk_rate > 50000000)
+			return dev_err_probe(dev, -EINVAL,
+					     "mclk %u Hz exceeds the 50 MHz maximum\n",
+					     priv->mclk_rate);
+	}
+
+	i2c_set_clientdata(i2c, priv);
+
+	ret = regmap_read(priv->regmap, ES9039_CHIP_ID, &id);
+	if (ret)
+		return dev_err_probe(dev, ret, "no response at 0x%02x\n",
+				     i2c->addr);
+
+	if (id != ES9039_CHIP_ID_ES9039Q2M)
+		return dev_err_probe(dev, -ENODEV,
+				     "unexpected chip id 0x%02x, want 0x%02x\n",
+				     id, ES9039_CHIP_ID_ES9039Q2M);
+
+	dev_info(dev, "ES9039Q2M at 0x%02x, mclk %u Hz\n",
+		 i2c->addr, priv->mclk_rate);
+
+	return devm_snd_soc_register_component(dev, &es9039q2m_component,
+					       &es9039q2m_dai, 1);
+}
+
+static const struct of_device_id es9039q2m_of_match[] = {
+	{ .compatible = "ess,es9039q2m" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, es9039q2m_of_match);
+
+static const struct i2c_device_id es9039q2m_i2c_id[] = {
+	{ "es9039q2m" },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, es9039q2m_i2c_id);
+
+static struct i2c_driver es9039q2m_i2c_driver = {
+	.driver = {
+		.name		= "es9039q2m",
+		.of_match_table	= es9039q2m_of_match,
+	},
+	.probe		= es9039q2m_i2c_probe,
+	.id_table	= es9039q2m_i2c_id,
+};
+module_i2c_driver(es9039q2m_i2c_driver);
+
+MODULE_DESCRIPTION("ASoC ES9039Q2M driver");
+MODULE_AUTHOR("Karl Asseily <karl@asseily.com>");
+MODULE_LICENSE("GPL");
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH 4/4] MAINTAINERS: add entry for the ES9039Q2M codec driver
  2026-08-20  6:26 [PATCH 0/4] ASoC: add ESS Technology ES9039Q2M codec driver Karl Asseily
                   ` (2 preceding siblings ...)
  2026-08-20  6:26 ` [PATCH 3/4] ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver Karl Asseily
@ 2026-08-20  6:26 ` Karl Asseily
  3 siblings, 0 replies; 8+ messages in thread
From: Karl Asseily @ 2026-08-20  6:26 UTC (permalink / raw)
  To: broonie, lgirdwood, robh, krzk+dt, conor+dt, perex, tiwai
  Cc: linux-sound, devicetree, linux-kernel, Karl Asseily

Add myself as maintainer of the ES9039Q2M codec driver and its binding.

Signed-off-by: Karl Asseily <karl@asseily.com>
---
 MAINTAINERS | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 8014b9f8253..7014285399c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9625,6 +9625,13 @@ S:	Maintained
 F:	include/linux/errseq.h
 F:	lib/errseq.c
 
+ES9039Q2M AUDIO CODEC DRIVER
+M:	Karl Asseily <karl@asseily.com>
+L:	linux-sound@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/sound/ess,es9039q2m.yaml
+F:	sound/soc/codecs/es9039q2m.c
+
 ESD CAN NETWORK DRIVERS
 M:	Stefan Mätje <stefan.maetje@esd.eu>
 R:	socketcan@esd.eu
-- 
2.34.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology
  2026-08-20  6:26 ` [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology Karl Asseily
@ 2026-08-20  6:30   ` Krzysztof Kozlowski
  2026-08-20  6:48     ` Karl Asseily
  0 siblings, 1 reply; 8+ messages in thread
From: Krzysztof Kozlowski @ 2026-08-20  6:30 UTC (permalink / raw)
  To: Karl Asseily, broonie, lgirdwood, robh, krzk+dt, conor+dt, perex,
	tiwai
  Cc: linux-sound, devicetree, linux-kernel

On 20/08/2026 08:26, Karl Asseily wrote:
> Add the vendor prefix for ESS Technology, Inc., maker of the SABRE family
> of audio DACs.
> 
> Signed-off-by: Karl Asseily <karl@asseily.com>

Website is esstech.com, so your prefix should be esstech. Unless the
website is ess.com?


Best regards,
Krzysztof

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH 3/4] ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver
  2026-08-20  6:26 ` [PATCH 3/4] ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver Karl Asseily
@ 2026-08-20  6:38   ` sashiko-bot
  0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-08-20  6:38 UTC (permalink / raw)
  To: Karl Asseily; +Cc: devicetree, robh, conor+dt

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] es9039_dop_put() corrupts hardware state when toggled mid-stream and races with hw_params().
- [Medium] 'Clock Fault' status control queries the wrong register offset.
- [Medium] Probe initialization disables VOLUME_HOLD contrary to its documented intent.
--

--- Patch [3]: [PATCH 3/4] ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver ---
commit bc64110211ab9eddd7d7d8ac88b78bebe86562d6
Author: Karl Asseily <karl@asseily.com>

ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver

This commit introduces the ASoC driver for the ESS Technology ES9039Q2M codec. 
It implements the software control mode over I2C, supporting PCM, DSD, DoP, 
and S/PDIF inputs along with an asynchronous sample rate converter.

> diff --git a/sound/soc/codecs/es9039q2m.c b/sound/soc/codecs/es9039q2m.c
> new file mode 100644
> --- /dev/null
> +++ b/sound/soc/codecs/es9039q2m.c
[ ... ]
> +static int es9039_dop_put(struct snd_kcontrol *kcontrol,
> +			  struct snd_ctl_elem_value *ucontrol)
> +{
> +	struct snd_soc_component *c = snd_kcontrol_chip(kcontrol);
> +	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(c);
> +	bool on = !!ucontrol->value.integer.value[0];
> +	int ret;
> +
> +	if (on == priv->dop_auto)
> +		return 0;
> +
> +	priv->dop_auto = on;
> +
> +	ret = regmap_update_bits(priv->regmap, ES9039_INPUT_SEL,
> +				 ES9039_AUTO_INPUT_SEL,
> +				 on ? ES9039_AUTO_INPUT_SEL : 0);

[Severity: Medium]
Will toggling this control mid-stream corrupt the hardware state?

If toggled on, it sets ES9039_AUTO_INPUT_SEL here but doesn't update
ES9039_SYS_MODE to set ES9039_ENABLE_DOP_DECODE, since that was only evaluated
in es9039q2m_hw_params() when the stream started.

The commit message notes that selecting DoP without enabling the marker
decoder leaves the part hunting and causes it to mute. Is there a need to
synchronize the decoder state in ES9039_SYS_MODE when this control is toggled?

[ ... ]
> +	/* --- status, read-only --- */
> +	ES9039_STAT("Automute Active CH1", ES9039_AUTOMUTE_READ, BIT(0), 1),
> +	ES9039_STAT("Automute Active CH2", ES9039_AUTOMUTE_READ, BIT(1), 1),
> +	ES9039_STAT("DoP Valid", ES9039_INPUT_STREAM_READ,
> +		    ES9039_RD_DOP_VALID, 1),
> +	ES9039_STAT("TDM Data Valid", ES9039_INPUT_STREAM_READ,
> +		    ES9039_RD_TDM_VALID, 1),
> +	ES9039_STAT("SPDIF Valid", ES9039_INPUT_STREAM_READ,
> +		    ES9039_RD_SPDIF_VALID, 1),
> +	ES9039_STAT("Clock Fault", ES9039_IRQ_SOURCES + 1, BIT(7), 1),

[Severity: Medium]
Is this querying the wrong register offset for the clock fault bit?

ES9039_SRC_BCK_WS_FAIL is defined as BIT(7) of the 16-bit word at
ES9039_IRQ_SOURCES. By adding 1 to the register address and still using
BIT(7) as the mask, won't this check bit 15 of the 16-bit word instead of
bit 7?

[ ... ]
> +static int es9039q2m_component_probe(struct snd_soc_component *component)
> +{
> +	struct es9039q2m_priv *priv = snd_soc_component_get_drvdata(component);
> +	int ret;
> +
> +	/*
> +	 * Come up muted. The volume registers default to 0 dB, and an unmuted
> +	 * DAC arriving into an already-powered analogue stage is how you get a
> +	 * thump. ASoC unmutes via mute_stream() when a stream starts.
> +	 */
> +	ret = regmap_update_bits(priv->regmap, ES9039_DAC_MUTE,
> +				 ES9039_MUTE_BOTH, ES9039_MUTE_BOTH);
> +	if (ret)
> +		return ret;
> +
> +	/*
> +	 * VOLUME_HOLD makes both channel volumes latch together, so a stereo
> +	 * change can never momentarily skew the image.
> +	 */
> +	ret = regmap_update_bits(priv->regmap, ES9039_IIR_SPDIF,
> +				 ES9039_VOLUME_HOLD, 0);

[Severity: Medium]
Is this intended to disable the volume hold feature?

The comment explicitly mentions that VOLUME_HOLD makes the channel volumes
latch together, but passing 0 disables it. Should this pass
ES9039_VOLUME_HOLD instead of 0 to enable the feature?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260820062626.39218-1-karl@asseily.com?part=3

^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology
  2026-08-20  6:30   ` Krzysztof Kozlowski
@ 2026-08-20  6:48     ` Karl Asseily
  0 siblings, 0 replies; 8+ messages in thread
From: Karl Asseily @ 2026-08-20  6:48 UTC (permalink / raw)
  To: linux-sound; +Cc: devicetree, linux-kernel

You're right — esstech.com. I'll switch the prefix to esstech and rename the
binding accordingly in v2.

Thanks,
Karl

On Thu, Aug 20, 2026 at 9:30 AM Krzysztof Kozlowski <krzk@kernel.org> wrote:
>
> On 20/08/2026 08:26, Karl Asseily wrote:
> > Add the vendor prefix for ESS Technology, Inc., maker of the SABRE family
> > of audio DACs.
> >
> > Signed-off-by: Karl Asseily <karl@asseily.com>
>
> Website is esstech.com, so your prefix should be esstech. Unless the
> website is ess.com?
>
>
> Best regards,
> Krzysztof

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-08-20  6:48 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-20  6:26 [PATCH 0/4] ASoC: add ESS Technology ES9039Q2M codec driver Karl Asseily
2026-08-20  6:26 ` [PATCH 1/4] dt-bindings: vendor-prefixes: add ESS Technology Karl Asseily
2026-08-20  6:30   ` Krzysztof Kozlowski
2026-08-20  6:48     ` Karl Asseily
2026-08-20  6:26 ` [PATCH 2/4] ASoC: dt-bindings: add ESS Technology ES9039Q2M Karl Asseily
2026-08-20  6:26 ` [PATCH 3/4] ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver Karl Asseily
2026-08-20  6:38   ` sashiko-bot
2026-08-20  6:26 ` [PATCH 4/4] MAINTAINERS: add entry for the " Karl Asseily

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.