ASoC: es9039q2m: add ESS Technology ES9039Q2M codec driver

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. The driver derives which case it is in rather
   than assuming one: the part runs synchronously when it drives the bus
   clocks itself, or when the MCLK it was given can be set to a
   multiple of the sample rate, and asynchronously only when a fixed
   oscillator feeds it as a clock consumer. The datasheet's MCLK floor
   differs between the two - 128 x Fs synchronous, 130 x Fs asynchronous
   - and where the clock is fixed a startup constraint derives the
   maximum rate from the MCLK actually present 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.

The part has four supply inputs and the datasheet asks for them in a set
order: AVDD, then VCCA about 200us later, then the two output-stage
references AVCC_DAC1 and AVCC_DAC2, with power-down exactly reversed. The
driver enables them one at a time for that reason rather than calling
regulator_bulk_enable(), which makes no ordering guarantee; the bulk API
still acquires them, where order does not matter. DVDD is not among them:
the part's 1.2V digital rail is generated on chip and that pin takes only
a decoupling capacitor. No settling time is inserted before the first
register access because the datasheet specifies none.

The datapath is off at reset and the driver turns it on at component
probe. SYSTEM_CONFIG[1] is clear when the part comes out of reset, so in
software mode nothing enables it at power-up and the analogue output
stays dead until the driver writes that bit. It is written after the mute
and never before it: bringing the datapath up into an unmuted part whose
volume registers still sit at their 0 dB reset value is the thump the
mute is there to prevent. Note when testing that a warm reboot does not
reset this part - registers and coefficient RAM both survive one, and
only removing power clears them - so the previous boot's value can hide
whether the write happened at all.

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.

System suspend gates the part's internal clock tree. Where the part is
clocked from the SoC, system suspend stops its clock as a side effect and
there is nothing for a driver to do; where the board feeds it a
free-running oscillator - which the ASRC makes an attractive design,
since MCLK then need not track BCLK or LRCK - nothing in the system can
gate that clock and the part stays fully clocked for the whole of
suspend. Clearing ENABLE_DAC_CLK is the only part of its consumption
software can reach; the analogue supplies are the board's business.
Measured on an RK3588 board with a fixed 24.576 MHz oscillator, system
suspend power fell from 1.98 W to 1.95 W and followed the register in
both directions across an A/B.

Resume mutes the part before regcache_sync(), because the sync walks
registers in ascending address order. A board that keeps the supplies up
needs nothing, but one that removes them brings the part back at reset
defaults - DAC_MODE clear, both channels unmuted, both volume registers
at 0 dB - and the sync would set DAC_MODE in register 0 and only restore
the real mute in register 86, lighting the datapath up at full scale in
between.

Resume also re-uploads the programmable FIR coefficient RAM.
regcache_sync() cannot restore it - the RAM is write-only, so nothing
caches it - but the sync does restore the filter selection, so a board
that removed the supplies would come back with the programmable filter
selected and the RAM holding whatever it powered up with. The driver
keeps a shadow of each stage it uploaded and replays it after the sync.
Stages never written have no shadow and keep the part's own defaults.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Karl Asseily <karl@asseily.com>
Link: https://patch.msgid.link/20260918182317.143223-4-karl@asseily.com
Signed-off-by: Mark Brown <broonie@kernel.org>
This commit is contained in:
Karl Asseily 2026-09-18 21:23:16 +03:00 committed by Mark Brown
parent ab1a02163f
commit 004e260a54
3 changed files with 2024 additions and 0 deletions

View File

@ -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
@ -1317,6 +1318,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

View File

@ -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
@ -586,6 +587,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

2008
sound/soc/codecs/es9039q2m.c Normal file

File diff suppressed because it is too large Load Diff