* snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c)
@ 2026-08-12 17:10 Mikhail Gavrilov
2026-08-13 7:24 ` Takashi Iwai
0 siblings, 1 reply; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-12 17:10 UTC (permalink / raw)
To: linux-sound; +Cc: tiwai, perex, g, jikos, bentiss, linux-input
Hello,
I would like to add ALSA mixer controls for the analogue input gain of a
USB audio interface whose control channel is a vendor-specific HID
interface, and I would like to agree on the shape before writing code,
because it crosses into drivers/hid.
The device is a Topping Professional M62, USB 152a:875c. In its
multichannel modes it presents 10 playback and 16 capture channels on
interfaces 1 and 2, a DFU interface, and interface 4 of class 3 (HID)
with a vendor-defined usage page, one 16-byte Input report and one
16-byte Output report, and no report IDs.
The problem: the microphone preamplifier gain, 0..88 dB per the
specification, is not reachable through ALSA. The card does expose a
'Mic Capture Volume', but a gain ladder measured in silence shows that
control to be a digital trim after the converter. The recorded noise
floor is flat at about -172 dBFS at the bottom of its range, far below
any converter's own noise floor, so what is being measured there is the
sample word running out of bits; above that the floor rises with unity
slope, i.e. one fixed analogue noise being divided down. The analogue
stage is reachable only over the HID interface, which is what the
vendor's own application uses.
I have the protocol. It was reverse engineered from captures of the
vendor application's traffic, the same way sound/usb/mixer_scarlett2.c
describes in its header. Frames are 15 bytes:
22 33 | 20 01 01 | target | property | s32 big endian | CRC | 66 77
with CRC-16/MODBUS over bytes 2..10, stored big endian. Rebuilding
every frame of a capture from the decoded fields reproduces all 2619 of
them byte for byte. Inbound reports are that frame plus one pad byte.
The device stays silent until the host sends a subscription frame,
after which it reports every state change including front-panel button
presses, and it answers a "report your state" frame with a full dump.
The analogue gain of each microphone input is a single property carrying
whole decibels, 0..88, so a plain TLV_DB_SCALE fits it.
The constraint, and my question. This device accepts nothing on the
control pipe: SET_REPORT and GET_REPORT both stall with EPIPE, for
report types Output, Input and Feature alike. So the pattern used by
snd_soundblaster_e1_switch_update() in sound/usb/mixer_quirks.c, which
sends HID_REQ_SET_REPORT through snd_usb_ctl_msg(), is not available
here. The only usable transport is the interrupt endpoints of interface
4, which usbhid binds.
Would it be acceptable for a mixer quirk in sound/usb to own that
interface? Concretely: an entry in hid_ignore_list so that usbhid stays
away, the quirk claiming interface 4, an interrupt IN URB whose
completion handler parses the vendor frame, updates cached values and
calls snd_ctl_notify(), and usb_interrupt_msg() in the put callbacks.
The notification half looks like what snd_usb_mixer_status_create()
already does for the audio control interface's status endpoint. Or would
you prefer a different layout for this?
A first patch would be deliberately minimal: two controls for the
analogue gain of the two microphone inputs, with a dB TLV, and nothing
else. The line-level inputs and the outputs use index scales with a
piecewise taper, which I have measured but would rather submit
separately.
For context, an ALSA UCM configuration for the same card is already
proposed as alsa-project/alsa-ucm-conf#826. That is what would designate
the new control as the capture volume, so that userspace moves the
hardware gain instead of the digital trim.
I can post the full protocol notes and the captures if that would be
useful.
--
Thanks,
Mikhail Gavrilov
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c)
2026-08-12 17:10 snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c) Mikhail Gavrilov
@ 2026-08-13 7:24 ` Takashi Iwai
2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
0 siblings, 1 reply; 53+ messages in thread
From: Takashi Iwai @ 2026-08-13 7:24 UTC (permalink / raw)
To: Mikhail Gavrilov
Cc: linux-sound, tiwai, perex, g, jikos, bentiss, linux-input
On Wed, 12 Aug 2026 19:10:34 +0200,
Mikhail Gavrilov wrote:
>
> Hello,
>
> I would like to add ALSA mixer controls for the analogue input gain of a
> USB audio interface whose control channel is a vendor-specific HID
> interface, and I would like to agree on the shape before writing code,
> because it crosses into drivers/hid.
>
> The device is a Topping Professional M62, USB 152a:875c. In its
> multichannel modes it presents 10 playback and 16 capture channels on
> interfaces 1 and 2, a DFU interface, and interface 4 of class 3 (HID)
> with a vendor-defined usage page, one 16-byte Input report and one
> 16-byte Output report, and no report IDs.
>
> The problem: the microphone preamplifier gain, 0..88 dB per the
> specification, is not reachable through ALSA. The card does expose a
> 'Mic Capture Volume', but a gain ladder measured in silence shows that
> control to be a digital trim after the converter. The recorded noise
> floor is flat at about -172 dBFS at the bottom of its range, far below
> any converter's own noise floor, so what is being measured there is the
> sample word running out of bits; above that the floor rises with unity
> slope, i.e. one fixed analogue noise being divided down. The analogue
> stage is reachable only over the HID interface, which is what the
> vendor's own application uses.
>
> I have the protocol. It was reverse engineered from captures of the
> vendor application's traffic, the same way sound/usb/mixer_scarlett2.c
> describes in its header. Frames are 15 bytes:
>
> 22 33 | 20 01 01 | target | property | s32 big endian | CRC | 66 77
>
> with CRC-16/MODBUS over bytes 2..10, stored big endian. Rebuilding
> every frame of a capture from the decoded fields reproduces all 2619 of
> them byte for byte. Inbound reports are that frame plus one pad byte.
> The device stays silent until the host sends a subscription frame,
> after which it reports every state change including front-panel button
> presses, and it answers a "report your state" frame with a full dump.
> The analogue gain of each microphone input is a single property carrying
> whole decibels, 0..88, so a plain TLV_DB_SCALE fits it.
>
> The constraint, and my question. This device accepts nothing on the
> control pipe: SET_REPORT and GET_REPORT both stall with EPIPE, for
> report types Output, Input and Feature alike. So the pattern used by
> snd_soundblaster_e1_switch_update() in sound/usb/mixer_quirks.c, which
> sends HID_REQ_SET_REPORT through snd_usb_ctl_msg(), is not available
> here. The only usable transport is the interrupt endpoints of interface
> 4, which usbhid binds.
>
> Would it be acceptable for a mixer quirk in sound/usb to own that
> interface? Concretely: an entry in hid_ignore_list so that usbhid stays
> away, the quirk claiming interface 4, an interrupt IN URB whose
> completion handler parses the vendor frame, updates cached values and
> calls snd_ctl_notify(), and usb_interrupt_msg() in the put callbacks.
> The notification half looks like what snd_usb_mixer_status_create()
> already does for the audio control interface's status endpoint. Or would
> you prefer a different layout for this?
>
> A first patch would be deliberately minimal: two controls for the
> analogue gain of the two microphone inputs, with a dB TLV, and nothing
> else. The line-level inputs and the outputs use index scales with a
> piecewise taper, which I have measured but would rather submit
> separately.
>
> For context, an ALSA UCM configuration for the same card is already
> proposed as alsa-project/alsa-ucm-conf#826. That is what would designate
> the new control as the capture volume, so that userspace moves the
> hardware gain instead of the digital trim.
>
> I can post the full protocol notes and the captures if that would be
> useful.
I believe we can judge better with the comparison of the actual code.
You can try implementing PoC's for both usb-audio mixer quirk and a
HID driver, then compare which would fit better. If either of them
looks significantly harder, you don't fulfill the implementation, of
course.
My gut feeling is that we can take it as a mixer quirk, but it really
depends on the complexity.
thanks,
Takashi
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC 0/2] Two ways to reach the Topping M62's analogue gains
2026-08-13 7:24 ` Takashi Iwai
@ 2026-08-20 15:13 ` Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (4 more replies)
0 siblings, 5 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-20 15:13 UTC (permalink / raw)
To: tiwai, jikos, bentiss; +Cc: perex, linux-sound, linux-input, linux-kernel
You asked for PoCs of both roads and a comparison of the actual code
rather than of arguments. Here are both. They are alternatives, not a
series: each is written against mainline 98f21c54f995 on its own, and
either can be applied alone.
1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface
2/2 HID: topping: a HID driver that registers a card of its own
Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two
CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have
been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for
reading, for unsolicited notification from the front panel, and for
writing.
What the device is
==================
The M62 keeps its two microphone preamp gains, its AUX and Bluetooth
input volumes and its headphone and OTG output volumes behind a vendor
protocol on a HID-class interface, and exposes none of them through
UAC. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: a noise-floor ladder
against the card shows the converter's own floor rising with the
signal. So on Linux today the one knob worth setting is the one that
cannot be reached, and a measurement application has to begin by asking
a human to touch the front panel.
The protocol is fifteen-byte frames -- start magic, a constant, a
target, a property, a signed 32-bit big-endian value, CRC-16/MODBUS
over the middle stored big-endian, end magic. Rebuilding all 2619
captured frames from that description reproduces them byte for byte.
The device says nothing until it is subscribed; one write starts the
stream and a second makes it announce its whole state, after which
every change arrives unasked, including a front panel press.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route.
What is identical in both
=========================
The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...)
is CRC-16/MODBUS, so no private table), and the control table. A knob
is a row of
{ name, target, paired target, property, min, max, TLV }
so adding one is adding a row. Six rows today. The outputs come in
pairs because the device answers on only one target of each pair and
the other would drift away unheard.
Where they differ
=================
1/2 claims the HID interface for snd-usb-audio and puts the elements on
the card the device already has. The cost is two-sided: an entry in
hid_ignore_list to keep usbhid off the interface, and one new helper in
sound/usb/card.c, because usb_audio_driver is static there and a quirk
cannot claim an interface without it. That helper is the only change in
1/2 outside the new file and its dispatch. Nothing is lost by taking
the interface: the report descriptor is a Generic Desktop application
collection with eight unnamed usages, sixteen bytes in and out and no
report ID, so hid-generic can only build an input device for a mouse
that does not exist -- which is what it does today.
2/2 binds as a HID driver, and the protocol half is if anything smaller
there: usbhid owns the endpoints, so hid_hw_output_report replaces a
hand-built interrupt URB out, raw_event replaces the one in, and no
interface has to be claimed. It needs nothing in sound/usb.
But these are mixer controls for an audio device, and the audio
device's card belongs to snd-usb-audio. A HID driver cannot put an
element there. There is no interface for it, and inventing one means
exporting from sound/usb both a lookup from struct usb_device to the
card and an add-element call, and then answering, for a single device,
what happens when the two drivers probe in either order and when either
disconnects first, given that the element would live in one module and
its private data in another.
So 2/2 does what a HID driver can do alone: it registers a card of its
own. That works, and the cost is visible from userspace rather than
theoretical:
$ cat /proc/asound/cards
0 [ToppingCtl ]: Topping - Topping M62 control
...
4 [M62 ]: USB-Audio - M62
$ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33
amixer: Cannot find the given element from control sysdefault:4
One device, two cards; the gains on a card with no PCM beside them; and
anything that looks for a device's mixer next to its streams --
alsamixer -c, UCM profiles, PipeWire's device model -- does not find
them there.
Against my own preference, two honest notes. The phantom input device
2/2 leaves at boot (hid-generic binds first, the specific driver being
a module outside the initramfs) is a packaging artefact, not a property
of that road. And 1/2's claim helper is new API surface in sound/usb,
small as it is.
Field results
=============
With 1/2: the interface belongs to snd-usb-audio while a neighbouring
device's HID interface still belongs to usbhid, so the ignore entry is
precise. Values arrive by themselves -- the headphone volume came up at
51 while the zero-initialised cache would have said 0. One front panel
press produces exactly one control event. A write reaches the hardware:
the device reports the written value back, and its meters answer.
With 2/2: the same, on its own card.
One device fact worth recording: a written gain takes effect at once,
but when the device commits it to non-volatile memory is the firmware's
business, and a value written and then torn off the bus can come back
as the older one. Nothing in either driver depends on that -- neither
treats itself as the source of truth, both ask the device -- but it is
easy to mistake for a driver bug while testing.
Where I come out
================
The knobs belong on the card the device already has, and 2/2 cannot put
them there without a new cross-subsystem interface built for one
device. 1/2's cost is one static-variable problem solved by one helper
in the file that owns it. So I would take 1/2, which is also your gut
feeling -- but the comparison is what you asked for, and either patch
stands alone if you read it the other way.
Not covered by either: the OTG input's gain. It has no front panel
control and therefore never announced itself in any capture, so its
property is unknown. It is one row when it is known.
Mikhail
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
@ 2026-08-20 15:13 ` Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 2/2] HID: topping: driver for the M62's vendor control channel Mikhail Gavrilov
` (3 subsequent siblings)
4 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-20 15:13 UTC (permalink / raw)
To: tiwai, jikos, bentiss
Cc: perex, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Not included: OTG IN, which has no front panel control and therefore
never announced itself in any capture, so its property is unknown. It
is one row when it is known.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 14 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 495 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 3 +
9 files changed, 536 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index d2b7ed2..41ec6b7 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27349,6 +27349,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf58..092b2a9 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d..3c156d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a..151b481 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e4..e99b934 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,20 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592..10f3302 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 0000000..0c89a51
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,495 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for. OTG IN is missing on purpose: it has no front panel control,
+ * so it never announced itself in any capture, and its property is not
+ * known. It is one row when it is.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ u8 *buf;
+ int err, actual;
+
+ buf = kzalloc(TOPPING_EP_BUF, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ kfree(buf);
+ if (err < 0)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ unsigned long flags;
+ int idx, value, err;
+
+ if (urb->status)
+ return; /* resubmitted below only when running */
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ spin_lock_irqsave(&tm->lock, flags);
+ if (tm->val[idx] == value) {
+ spin_unlock_irqrestore(&tm->lock, flags);
+ goto resubmit;
+ }
+ tm->val[idx] = value;
+ spin_unlock_irqrestore(&tm->lock, flags);
+
+ if (tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ unsigned long flags;
+
+ spin_lock_irqsave(&tm->lock, flags);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ spin_unlock_irqrestore(&tm->lock, flags);
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ unsigned long flags;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ spin_lock_irqsave(&tm->lock, flags);
+ if (tm->val[elem->control] == value) {
+ spin_unlock_irqrestore(&tm->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&tm->lock, flags);
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ spin_lock_irqsave(&tm->lock, flags);
+ tm->val[elem->control] = value;
+ spin_unlock_irqrestore(&tm->lock, flags);
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ return 0;
+
+fail:
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 0000000..15e16b5
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d..4f9770a 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,9 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.43.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC 2/2] HID: topping: driver for the M62's vendor control channel
2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-20 15:13 ` Mikhail Gavrilov
2026-08-21 11:23 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
` (2 subsequent siblings)
4 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-20 15:13 UTC (permalink / raw)
To: tiwai, jikos, bentiss
Cc: perex, linux-sound, linux-input, linux-kernel, Mikhail Gavrilov
The alternative to doing this as a snd-usb-audio mixer quirk, written
so the two can be compared rather than argued about.
The device and the protocol are the same: fifteen-byte frames carrying
a target, a property and a signed value, CRC-16/MODBUS over the middle,
one write to subscribe and one to make the device announce its state,
after which every change arrives unasked, front panel presses included.
That part is if anything smaller here than in the quirk -- usbhid owns
the endpoints, so hid_hw_output_report replaces a hand-built interrupt
URB out and raw_event replaces the one in, and the interface needs no
claiming because this driver is what binds to it. The control table is
identical, so adding a knob is still adding a row.
WHERE THIS ROAD RUNS OUT is the reason to write it. These are mixer
controls for an audio device, and the audio device's card belongs to
snd-usb-audio. A HID driver cannot put an element there: there is no
interface for it, and inventing one means exporting from sound/usb both
a lookup from struct usb_device to the card and an add-element call --
and then answering, for a single device, what happens when the two
drivers probe in either order, and when either disconnects first, given
that the element would live in one module and its private data in
another.
So this driver does what a HID driver can do alone: it registers a card
of its own. That works, and the cost is visible from userspace rather
than theoretical -- one physical device appears as two cards, the gains
land on a card with no PCM beside them, and anything that looks for a
device's mixer next to its streams (alsamixer -c, UCM profiles,
PipeWire's device model) does not find them there.
hid_hw_start is called with no connect mask on purpose. The report
descriptor is a Generic Desktop application collection with eight
unnamed usages and no report ID, so hid-generic would build an input
device for a mouse that does not exist.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/Kconfig | 13 ++
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-topping.c | 382 ++++++++++++++++++++++++++++++++++++++
5 files changed, 405 insertions(+)
create mode 100644 drivers/hid/hid-topping.c
diff --git a/MAINTAINERS b/MAINTAINERS
index d2b7ed2..ec302fa 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27349,6 +27349,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 HID CONTROL DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-topping.c
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11..712390a 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1283,6 +1283,19 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING
+ tristate "Topping M62 vendor control channel"
+ depends on USB_HID
+ depends on SND
+ help
+ Say Y here if you have a Topping M62 audio interface and want
+ its analogue input gains and output volumes as ALSA mixer
+ controls. The device keeps them behind a vendor protocol on a
+ HID interface and exposes none of them through USB audio.
+
+ Note that the controls appear on a card of this driver's own,
+ not on the card snd-usb-audio creates for the same device.
+
config HID_TOPSEED
tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b..6e1f8fc 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
+obj-$(CONFIG_HID_TOPPING) += hid-topping.o
obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o
obj-$(CONFIG_HID_TOPRE) += hid-topre.o
obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf58..092b2a9 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-topping.c b/drivers/hid/hid-topping.c
new file mode 100644
index 0000000..9d58bce
--- /dev/null
+++ b/drivers/hid/hid-topping.c
@@ -0,0 +1,382 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for the Topping M62's vendor control channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The same device and the same protocol as the snd-usb-audio mixer
+ * quirk this is meant to be compared against; only the road differs.
+ * Here the vendor channel is taken as what it claims to be -- a HID
+ * interface -- and the driver rides usbhid instead of claiming the
+ * interface for the audio driver.
+ *
+ * The protocol: fifteen-byte frames,
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * a target TT, a property PP of it, CRC-16/MODBUS over bytes 2..10
+ * stored big-endian. Reports arriving are the same plus a pad byte.
+ * One write of 0x11/0x24 subscribes, one of 0x11/0x26 makes the device
+ * announce its whole state, and every later change arrives unasked,
+ * including a front panel press.
+ *
+ * WHERE THIS ROAD RUNS OUT, which is the point of writing it: these
+ * are mixer controls for an audio device, and the audio device's card
+ * belongs to snd-usb-audio. A HID driver cannot put a control there.
+ * There is no interface for it, and inventing one means exporting from
+ * sound/usb both a way to find the card behind a struct usb_device and
+ * a way to add an element to it -- and then answering, for a single
+ * device, what happens when the two drivers probe in either order and
+ * when either disconnects first, since the control's private data
+ * would live in this module while the element lives in that card.
+ *
+ * So this driver does what a HID driver CAN do by itself: it makes a
+ * card of its own. That works, and it is exactly the wart to weigh --
+ * one physical device shows up twice in userspace, the gains land on a
+ * card that has no PCM, and anything that looks for a device's mixer
+ * beside its streams (alsamixer -c, UCM, PipeWire) does not find them
+ * there.
+ */
+
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/tlv.h>
+
+#include "hid-ids.h"
+
+#define TOPPING_FRAME_LEN 15
+#define TOPPING_REPORT_LEN 16
+
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/* identical to the quirk's: a knob is a row */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target;
+ u8 target_pair;
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls)
+
+struct topping_hid {
+ struct hid_device *hdev;
+ struct snd_card *card;
+ spinlock_t lock; /* guards val[] against raw_event */
+ int val[TOPPING_NUM_CTLS];
+ struct snd_kcontrol *kctl[TOPPING_NUM_CTLS];
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ put_unaligned_be16(crc16(0xffff, f + 2, 9), f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+static int topping_send(struct topping_hid *th, u8 target, u8 prop,
+ s32 value)
+{
+ u8 *buf;
+ int err;
+
+ buf = kzalloc(TOPPING_FRAME_LEN, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+ topping_build(buf, target, prop, value);
+ err = hid_hw_output_report(th->hdev, buf, TOPPING_FRAME_LEN);
+ kfree(buf);
+ if (err < 0)
+ hid_err(th->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+static int topping_index_of(u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ if (topping_m62_ctls[i].target == target &&
+ topping_m62_ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *f, int size)
+{
+ struct topping_hid *th = hid_get_drvdata(hdev);
+ unsigned long flags;
+ int idx, value;
+
+ if (size < TOPPING_FRAME_LEN)
+ return 0;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ return 0;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ return 0;
+
+ idx = topping_index_of(f[5], f[6]);
+ if (idx < 0)
+ return 0;
+
+ value = get_unaligned_be32(f + 7);
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return 0;
+
+ spin_lock_irqsave(&th->lock, flags);
+ if (th->val[idx] == value) {
+ spin_unlock_irqrestore(&th->lock, flags);
+ return 0;
+ }
+ th->val[idx] = value;
+ spin_unlock_irqrestore(&th->lock, flags);
+
+ if (th->kctl[idx])
+ snd_ctl_notify(th->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &th->kctl[idx]->id);
+ return 0;
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ int idx = kctl->private_value;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = topping_m62_ctls[idx].min;
+ uinfo->value.integer.max = topping_m62_ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_hid *th = snd_kcontrol_chip(kctl);
+ unsigned long flags;
+
+ spin_lock_irqsave(&th->lock, flags);
+ ucontrol->value.integer.value[0] = th->val[kctl->private_value];
+ spin_unlock_irqrestore(&th->lock, flags);
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_hid *th = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ unsigned long flags;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ spin_lock_irqsave(&th->lock, flags);
+ if (th->val[idx] == value) {
+ spin_unlock_irqrestore(&th->lock, flags);
+ return 0;
+ }
+ spin_unlock_irqrestore(&th->lock, flags);
+
+ err = topping_send(th, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ err = topping_send(th, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ spin_lock_irqsave(&th->lock, flags);
+ th->val[idx] = value;
+ spin_unlock_irqrestore(&th->lock, flags);
+ return 1;
+}
+
+static int topping_add_ctls(struct topping_hid *th)
+{
+ struct snd_kcontrol_new tmpl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+ };
+ struct snd_kcontrol *kctl;
+ int i, err;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ tmpl.name = topping_m62_ctls[i].name;
+ tmpl.private_value = i;
+ tmpl.tlv.p = topping_m62_ctls[i].tlv;
+ kctl = snd_ctl_new1(&tmpl, th);
+ if (!kctl)
+ return -ENOMEM;
+ err = snd_ctl_add(th->card, kctl);
+ if (err < 0)
+ return err;
+ th->kctl[i] = kctl;
+ }
+ return 0;
+}
+
+static int topping_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct topping_hid *th;
+ int err;
+
+ th = devm_kzalloc(&hdev->dev, sizeof(*th), GFP_KERNEL);
+ if (!th)
+ return -ENOMEM;
+ th->hdev = hdev;
+ spin_lock_init(&th->lock);
+ hid_set_drvdata(hdev, th);
+
+ err = hid_parse(hdev);
+ if (err)
+ return err;
+
+ /*
+ * No connect mask: the report descriptor describes a Generic
+ * Desktop application collection with eight unnamed usages, so
+ * letting hid-generic have it would create an input device for a
+ * mouse that does not exist.
+ */
+ err = hid_hw_start(hdev, 0);
+ if (err)
+ return err;
+
+ err = hid_hw_open(hdev);
+ if (err)
+ goto stop;
+
+ /*
+ * A CARD OF ITS OWN, because the device's real card belongs to
+ * snd-usb-audio and nothing lets an outside module add an
+ * element to it. This is the cost of the HID road, and it is
+ * visible from userspace: two cards for one device.
+ */
+ err = snd_card_new(&hdev->dev, SNDRV_DEFAULT_IDX1, "ToppingCtl",
+ THIS_MODULE, 0, &th->card);
+ if (err < 0)
+ goto close;
+
+ strscpy(th->card->driver, "Topping", sizeof(th->card->driver));
+ strscpy(th->card->shortname, "Topping M62 control",
+ sizeof(th->card->shortname));
+ strscpy(th->card->longname, "Topping M62 vendor control channel",
+ sizeof(th->card->longname));
+
+ err = topping_add_ctls(th);
+ if (err < 0)
+ goto free_card;
+
+ err = snd_card_register(th->card);
+ if (err < 0)
+ goto free_card;
+
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ return 0;
+
+free_card:
+ snd_card_free(th->card);
+close:
+ hid_hw_close(hdev);
+stop:
+ hid_hw_stop(hdev);
+ return err;
+}
+
+static void topping_remove(struct hid_device *hdev)
+{
+ struct topping_hid *th = hid_get_drvdata(hdev);
+
+ /*
+ * The card first: its elements call into this module, and the
+ * hid device under them is about to stop answering.
+ */
+ if (th->card)
+ snd_card_free(th->card);
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id topping_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, topping_devices);
+
+static struct hid_driver topping_driver = {
+ .name = "topping",
+ .id_table = topping_devices,
+ .probe = topping_probe,
+ .remove = topping_remove,
+ .raw_event = topping_raw_event,
+};
+module_hid_driver(topping_driver);
+
+MODULE_DESCRIPTION("HID driver for the Topping M62 vendor control channel");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* Re: [RFC 0/2] Two ways to reach the Topping M62's analogue gains
2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 2/2] HID: topping: driver for the M62's vendor control channel Mikhail Gavrilov
@ 2026-08-21 11:23 ` Mikhail Gavrilov
2026-08-23 8:50 ` Takashi Iwai
2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
4 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-21 11:23 UTC (permalink / raw)
To: tiwai, jikos, bentiss; +Cc: perex, linux-sound, linux-input, linux-kernel
On Thu, Aug 20, 2026 at 8:13 PM Mikhail Gavrilov
<mikhail.v.gavrilov@gmail.com> wrote:
>
> You asked for PoCs of both roads and a comparison of the actual code
> rather than of arguments. Here are both. They are alternatives, not a
> series: each is written against mainline 98f21c54f995 on its own, and
> either can be applied alone.
>
> 1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface
> 2/2 HID: topping: a HID driver that registers a card of its own
>
> Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two
> CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have
> been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for
> reading, for unsolicited notification from the front panel, and for
> writing.
>
> What the device is
> ==================
>
> The M62 keeps its two microphone preamp gains, its AUX and Bluetooth
> input volumes and its headphone and OTG output volumes behind a vendor
> protocol on a HID-class interface, and exposes none of them through
> UAC. What UAC does expose on the capture side is a digital trim after
> the converter, which cannot buy signal-to-noise: a noise-floor ladder
> against the card shows the converter's own floor rising with the
> signal. So on Linux today the one knob worth setting is the one that
> cannot be reached, and a measurement application has to begin by asking
> a human to touch the front panel.
>
> The protocol is fifteen-byte frames -- start magic, a constant, a
> target, a property, a signed 32-bit big-endian value, CRC-16/MODBUS
> over the middle stored big-endian, end magic. Rebuilding all 2619
> captured frames from that description reproduces them byte for byte.
> The device says nothing until it is subscribed; one write starts the
> stream and a second makes it announce its whole state, after which
> every change arrives unasked, including a front panel press.
>
> The control pipe is not an option: GET_REPORT and SET_REPORT stall with
> EPIPE for every report type, so the interrupt endpoints on the HID
> interface are the only route.
>
> What is identical in both
> =========================
>
> The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...)
> is CRC-16/MODBUS, so no private table), and the control table. A knob
> is a row of
>
> { name, target, paired target, property, min, max, TLV }
>
> so adding one is adding a row. Six rows today. The outputs come in
> pairs because the device answers on only one target of each pair and
> the other would drift away unheard.
>
> Where they differ
> =================
>
> 1/2 claims the HID interface for snd-usb-audio and puts the elements on
> the card the device already has. The cost is two-sided: an entry in
> hid_ignore_list to keep usbhid off the interface, and one new helper in
> sound/usb/card.c, because usb_audio_driver is static there and a quirk
> cannot claim an interface without it. That helper is the only change in
> 1/2 outside the new file and its dispatch. Nothing is lost by taking
> the interface: the report descriptor is a Generic Desktop application
> collection with eight unnamed usages, sixteen bytes in and out and no
> report ID, so hid-generic can only build an input device for a mouse
> that does not exist -- which is what it does today.
>
> 2/2 binds as a HID driver, and the protocol half is if anything smaller
> there: usbhid owns the endpoints, so hid_hw_output_report replaces a
> hand-built interrupt URB out, raw_event replaces the one in, and no
> interface has to be claimed. It needs nothing in sound/usb.
>
> But these are mixer controls for an audio device, and the audio
> device's card belongs to snd-usb-audio. A HID driver cannot put an
> element there. There is no interface for it, and inventing one means
> exporting from sound/usb both a lookup from struct usb_device to the
> card and an add-element call, and then answering, for a single device,
> what happens when the two drivers probe in either order and when either
> disconnects first, given that the element would live in one module and
> its private data in another.
>
> So 2/2 does what a HID driver can do alone: it registers a card of its
> own. That works, and the cost is visible from userspace rather than
> theoretical:
>
> $ cat /proc/asound/cards
> 0 [ToppingCtl ]: Topping - Topping M62 control
> ...
> 4 [M62 ]: USB-Audio - M62
>
> $ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33
> amixer: Cannot find the given element from control sysdefault:4
>
> One device, two cards; the gains on a card with no PCM beside them; and
> anything that looks for a device's mixer next to its streams --
> alsamixer -c, UCM profiles, PipeWire's device model -- does not find
> them there.
>
> Against my own preference, two honest notes. The phantom input device
> 2/2 leaves at boot (hid-generic binds first, the specific driver being
> a module outside the initramfs) is a packaging artefact, not a property
> of that road. And 1/2's claim helper is new API surface in sound/usb,
> small as it is.
>
> Field results
> =============
>
> With 1/2: the interface belongs to snd-usb-audio while a neighbouring
> device's HID interface still belongs to usbhid, so the ignore entry is
> precise. Values arrive by themselves -- the headphone volume came up at
> 51 while the zero-initialised cache would have said 0. One front panel
> press produces exactly one control event. A write reaches the hardware:
> the device reports the written value back, and its meters answer.
>
> With 2/2: the same, on its own card.
>
> One device fact worth recording: a written gain takes effect at once,
> but when the device commits it to non-volatile memory is the firmware's
> business, and a value written and then torn off the bus can come back
> as the older one. Nothing in either driver depends on that -- neither
> treats itself as the source of truth, both ask the device -- but it is
> easy to mistake for a driver bug while testing.
>
> Where I come out
> ================
>
> The knobs belong on the card the device already has, and 2/2 cannot put
> them there without a new cross-subsystem interface built for one
> device. 1/2's cost is one static-variable problem solved by one helper
> in the file that owns it. So I would take 1/2, which is also your gut
> feeling -- but the comparison is what you asked for, and either patch
> stands alone if you read it the other way.
>
> Not covered by either: the OTG input's gain. It has no front panel
> control and therefore never announced itself in any capture, so its
> property is unknown. It is one row when it is known.
>
> Mikhail
Three things I should have said in the cover letter, one of which
argues against the conclusion I drew there.
The quirk's hid_ignore_list entry means no hidraw node is created for
this device at all. Topping ship a control application for Windows and
macOS and not for Linux; there is none today, and the driver should not
block one if it appears. The HID road leaves that channel open and the
quirk road closes it. I weighed the two roads by where the mixer
controls can live and did not weigh this, and it belongs on the scale.
The one gap the cover letter named is closed. The OTG input's gain is
target 0x27, property 0x04, on the same taper family as Bluetooth --
read out of a capture of the vendor application moving it, with the
indices it dwelt on matching the decibels it displayed. Both patches
carry the row here; I have not resent them for one line, and it will be
in whichever version goes forward.
And the protocol turns out to need more than a write path, which bears
on the complexity you asked me to compare. The vendor application
repeats its subscribe every two seconds, so a driver has to keep that
up or the device stops reporting; the device has two memories, and a
separate command commits the live state to the one that survives a
power cycle; and the source selectors -- which mix or bus each output
listens to -- can be written but never read: the device does not
announce them, and the vendor application does not ask, because on
connect it pushes its whole workspace rather than reading anything.
None of that favours either road, since both pay it identically, but it
is a fair bit more than a table of gains, and I would rather you saw it
before deciding.
I have not resent the patches. Both roads work on the hardware; the
question is still which one you would rather carry.
--
Thanks,
Mikhail Gavrilov.
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: [RFC 0/2] Two ways to reach the Topping M62's analogue gains
2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
` (2 preceding siblings ...)
2026-08-21 11:23 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
@ 2026-08-23 8:50 ` Takashi Iwai
2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
4 siblings, 0 replies; 53+ messages in thread
From: Takashi Iwai @ 2026-08-23 8:50 UTC (permalink / raw)
To: Mikhail Gavrilov
Cc: tiwai, jikos, bentiss, perex, linux-sound, linux-input,
linux-kernel
On Thu, 20 Aug 2026 17:13:27 +0200,
Mikhail Gavrilov wrote:
>
> You asked for PoCs of both roads and a comparison of the actual code
> rather than of arguments. Here are both. They are alternatives, not a
> series: each is written against mainline 98f21c54f995 on its own, and
> either can be applied alone.
>
> 1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface
> 2/2 HID: topping: a HID driver that registers a card of its own
>
> Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two
> CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have
> been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for
> reading, for unsolicited notification from the front panel, and for
> writing.
>
> What the device is
> ==================
>
> The M62 keeps its two microphone preamp gains, its AUX and Bluetooth
> input volumes and its headphone and OTG output volumes behind a vendor
> protocol on a HID-class interface, and exposes none of them through
> UAC. What UAC does expose on the capture side is a digital trim after
> the converter, which cannot buy signal-to-noise: a noise-floor ladder
> against the card shows the converter's own floor rising with the
> signal. So on Linux today the one knob worth setting is the one that
> cannot be reached, and a measurement application has to begin by asking
> a human to touch the front panel.
>
> The protocol is fifteen-byte frames -- start magic, a constant, a
> target, a property, a signed 32-bit big-endian value, CRC-16/MODBUS
> over the middle stored big-endian, end magic. Rebuilding all 2619
> captured frames from that description reproduces them byte for byte.
> The device says nothing until it is subscribed; one write starts the
> stream and a second makes it announce its whole state, after which
> every change arrives unasked, including a front panel press.
>
> The control pipe is not an option: GET_REPORT and SET_REPORT stall with
> EPIPE for every report type, so the interrupt endpoints on the HID
> interface are the only route.
>
> What is identical in both
> =========================
>
> The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...)
> is CRC-16/MODBUS, so no private table), and the control table. A knob
> is a row of
>
> { name, target, paired target, property, min, max, TLV }
>
> so adding one is adding a row. Six rows today. The outputs come in
> pairs because the device answers on only one target of each pair and
> the other would drift away unheard.
>
> Where they differ
> =================
>
> 1/2 claims the HID interface for snd-usb-audio and puts the elements on
> the card the device already has. The cost is two-sided: an entry in
> hid_ignore_list to keep usbhid off the interface, and one new helper in
> sound/usb/card.c, because usb_audio_driver is static there and a quirk
> cannot claim an interface without it. That helper is the only change in
> 1/2 outside the new file and its dispatch. Nothing is lost by taking
> the interface: the report descriptor is a Generic Desktop application
> collection with eight unnamed usages, sixteen bytes in and out and no
> report ID, so hid-generic can only build an input device for a mouse
> that does not exist -- which is what it does today.
>
> 2/2 binds as a HID driver, and the protocol half is if anything smaller
> there: usbhid owns the endpoints, so hid_hw_output_report replaces a
> hand-built interrupt URB out, raw_event replaces the one in, and no
> interface has to be claimed. It needs nothing in sound/usb.
>
> But these are mixer controls for an audio device, and the audio
> device's card belongs to snd-usb-audio. A HID driver cannot put an
> element there. There is no interface for it, and inventing one means
> exporting from sound/usb both a lookup from struct usb_device to the
> card and an add-element call, and then answering, for a single device,
> what happens when the two drivers probe in either order and when either
> disconnects first, given that the element would live in one module and
> its private data in another.
>
> So 2/2 does what a HID driver can do alone: it registers a card of its
> own. That works, and the cost is visible from userspace rather than
> theoretical:
>
> $ cat /proc/asound/cards
> 0 [ToppingCtl ]: Topping - Topping M62 control
> ...
> 4 [M62 ]: USB-Audio - M62
>
> $ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33
> amixer: Cannot find the given element from control sysdefault:4
>
> One device, two cards; the gains on a card with no PCM beside them; and
> anything that looks for a device's mixer next to its streams --
> alsamixer -c, UCM profiles, PipeWire's device model -- does not find
> them there.
>
> Against my own preference, two honest notes. The phantom input device
> 2/2 leaves at boot (hid-generic binds first, the specific driver being
> a module outside the initramfs) is a packaging artefact, not a property
> of that road. And 1/2's claim helper is new API surface in sound/usb,
> small as it is.
>
> Field results
> =============
>
> With 1/2: the interface belongs to snd-usb-audio while a neighbouring
> device's HID interface still belongs to usbhid, so the ignore entry is
> precise. Values arrive by themselves -- the headphone volume came up at
> 51 while the zero-initialised cache would have said 0. One front panel
> press produces exactly one control event. A write reaches the hardware:
> the device reports the written value back, and its meters answer.
>
> With 2/2: the same, on its own card.
>
> One device fact worth recording: a written gain takes effect at once,
> but when the device commits it to non-volatile memory is the firmware's
> business, and a value written and then torn off the bus can come back
> as the older one. Nothing in either driver depends on that -- neither
> treats itself as the source of truth, both ask the device -- but it is
> easy to mistake for a driver bug while testing.
>
> Where I come out
> ================
>
> The knobs belong on the card the device already has, and 2/2 cannot put
> them there without a new cross-subsystem interface built for one
> device. 1/2's cost is one static-variable problem solved by one helper
> in the file that owns it. So I would take 1/2, which is also your gut
> feeling -- but the comparison is what you asked for, and either patch
> stands alone if you read it the other way.
>
> Not covered by either: the OTG input's gain. It has no front panel
> control and therefore never announced itself in any capture, so its
> property is unknown. It is one row when it is known.
Thanks! I didn't expect such complete patches when I asked for PoC,
and you've done much better than I thought :)
Honestly speaking, both look well acceptable. From the pure kernel
POV, the 2nd patch is simpler, but as you pointed out, we'll need
another stuff to combining two sound cards. It'd be likely a special
UCM profile, but this can be a bit hackish.
So, from the usability POV, the first patch would be "easier", and if
I have to choose, my gut feeling is to pick the first one. But again,
I have no strong opinion, and both look good in general. I'd like to
hear from others, too.
About the code, there are a few things to be improved. The spinlock
could be done better with guard(). A temporary buffer could be
handled with __free(), too.
thanks,
Takashi
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
` (3 preceding siblings ...)
2026-08-23 8:50 ` Takashi Iwai
@ 2026-08-23 14:22 ` Mikhail Gavrilov
2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (2 more replies)
4 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 14:22 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
v2, and the road is the mixer quirk. The HID driver is dropped from
this posting; it is in the RFC thread if anyone wants to argue for it.
The cost of this road is accepted knowingly: with the hid_ignore_list
entry there is no hidraw node, so a future Linux control application
would have no channel of its own to the card.
Changes since the RFC:
- guard() and __free() as you asked. The spinlock is guard() where a
whole function holds it and scoped_guard() elsewhere; one goto that
would have jumped out of a guarded scope is gone, replaced by a
flag, since the cleanup runs either way but the jump reads like a
trap.
- The OTG input's gain is in. It was the one gap the RFC cover letter
named: it has no front panel control, so it never announced itself.
A capture of the vendor application moving it names it target 0x27
on the same taper family as Bluetooth, and the indices it dwelt on
match the decibels it displayed.
- The subscription is renewed every two seconds. It lapses: a
listener that subscribed once got the meters and the identification
block and then very little, while one that kept repeating got the
gains too, about five seconds in. The vendor application does the
same.
- New in 2/2: the outputs' source selectors, as enumerated controls.
A correction to my own follow-up, which said the HID road leaves a
hidraw node open for a future vendor application while the quirk closes
it. Half of that is wrong: the HID driver as posted calls
hid_hw_start(hdev, 0), which creates no hidraw either. It would take
one word to fix there and cannot be fixed on this road at all, so the
comparison stands, but the archive should not carry a claim the code
did not support.
Which raises a form neither posting covered: snd-usb-audio could
register the hid_driver itself. usbhid stays the transport, so hidraw
survives and no hid_ignore_list entry is needed; the controls still
land on the card the device already has, because it is all one module
holding the mixer pointer; and the claim helper in card.c goes away.
The cost is that snd-usb-audio would depend on the HID core, and I find
no precedent for that direction -- the reverse exists, hid-prodikeys
registers a card of its own. The probe-order and disconnect questions
do not disappear, but they stay inside one module. I mention it rather
than implement it: you have picked a road, and I would rather ask
whether this is a better one than send a fourth variant unasked.
About 2/2 and one thing in it I am not comfortable with. Each output
listens to one source chosen inside the card -- a mix, an input, or one
playback bus straight from USB -- and the device NEVER reports that
choice. Not to this driver, and not to the vendor's own application,
which pushes its whole workspace on connect rather than reading
anything. So the control can be written but not read, and the item list
starts with "Unknown", which is what it shows until a hand has chosen;
selecting it is refused. If there is a convention for this that I have
missed, I would rather use it.
Both patches are on mainline 98f21c54f995 and have been exercised on
the hardware: values arrive by themselves after probe, a front panel
knob still reaches the driver ten minutes later, and a write reaches
the analogue stage -- recording one source at gain 30 and at gain 60
differs by 29.7 dB against the 30.0 dB the taper table predicts, which
also confirms the decoded scale. For 2/2, the audible test: point an
output away from the bus being played and it goes silent, point it back
and the sound returns. Tested on a KASAN and lockdep kernel, including
unplug while a stream was running; no reports.
Mikhail Gavrilov (2):
ALSA: usb-audio: expose the Topping M62's analogue gains as mixer
controls
ALSA: usb-audio: let the M62's outputs say what they listen to
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 14 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 636 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 3 +
9 files changed, 677 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-08-23 14:22 ` Mikhail Gavrilov
2026-08-23 14:22 ` [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 14:22 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Seven rows. OTG IN was the one gap when this was first posted -- it has
no front panel control, so it never announced itself and its property
was unknown; a capture of the vendor application moving it named it as
target 0x27, and its taper is the same family as Bluetooth, confirmed
by the indices the application dwelt on matching the decibels it
displayed.
The subscription lapses, so it is renewed: the vendor application
repeats the same subscribe every two seconds for as long as it runs,
and a device that hears nothing stops reporting. A listener that
subscribed once got the meters and the identification block and then
very little; one that kept repeating got the gains too, about five
seconds in. Nothing in the frame says "keep alive" and nothing
acknowledges it -- it is the subscribe again -- so a plain periodic
write does it. Two seconds is what the vendor uses; the device
presumably tolerates longer, but there is no reason to find the edge.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 14 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 511 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 3 +
9 files changed, 552 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index c31a32a2c748..86447da07a6d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27435,6 +27435,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..3c156d1420d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e491779..e99b9341086f 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,20 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..3405f28dec66
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,511 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/cleanup.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ struct delayed_work keepalive;
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_KERNEL);
+ int err, actual;
+
+ if (!buf)
+ return -ENOMEM;
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ if (err < 0)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ int idx, value, err;
+ bool changed;
+
+ if (urb->status)
+ return; /* resubmitted below only when running */
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ changed = false;
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[idx] != value) {
+ tm->val[idx] = value;
+ changed = true;
+ }
+ }
+
+ if (changed && tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ keepalive.work);
+
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(spinlock_irqsave)(&tm->lock);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ if (tm->val[elem->control] == value)
+ return 0;
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ tm->val[elem->control] = value;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+ INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+fail:
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d7ad25..4f9770a5db69 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,9 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-23 14:22 ` Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 14:22 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
Each output on this card has a source selector: it can take one of the
three internal mixes, or any input, or any playback bus straight from
USB. Which it is decides everything downstream -- point the headphones
at a playback bus and the card's mixer leaves the path entirely, along
with every question about what is summed into it.
That matters more here than it would elsewhere. There is no control
panel for the M62 on Linux, so a user who never runs the vendor's
application on another machine has no way to see or change this, and
inherits whatever the card was last told. One enumerated control per
output gives them the whole choice, and it does so without exposing the
sixty-cell mixer matrix, which without a graphical representation would
confuse far more than it helps.
The item list has "Unknown" first, and it is deliberate rather than
tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not
to the vendor's own application, which on connect pushes its entire
workspace to the card rather than asking it anything. So the current
setting cannot be learned at probe, and saying so is the only honest
thing a control can do until a hand has chosen. Selecting "Unknown" is
refused, since it is a report and not a choice.
The numbering the card uses has a hole where 4 and 5 would be, so the
item index and the value written are kept as separate tables rather
than one being computed from the other.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 125 ++++++++++++++++++++++++++++++++++++++
1 file changed, 125 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 3405f28dec66..029f54865e22 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -125,6 +125,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
topping_tlv_out_0 },
};
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+
struct topping_mixer {
struct usb_mixer_interface *mixer;
struct usb_interface *iface;
@@ -139,6 +178,7 @@ struct topping_mixer {
spinlock_t lock; /* guards val[] against the URB */
int *val;
struct snd_kcontrol **kctl;
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
};
static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
@@ -312,6 +352,58 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
return 1;
}
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ ucontrol->value.enumerated.item[0] = tm->sel[elem->control];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ const struct topping_enum_desc *d;
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+ if (!item)
+ return -EINVAL; /* "Unknown" is a report, not a choice */
+ if (tm->sel[elem->control] == item)
+ return 0;
+
+ d = &topping_m62_enums[elem->control];
+ err = topping_send(tm, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ tm->sel[elem->control] = item;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
static const struct snd_kcontrol_new topping_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
@@ -354,6 +446,34 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx)
return 0;
}
+static int topping_add_sel(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_sel, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ strscpy(kctl->id.name, topping_m62_enums[idx].name,
+ sizeof(kctl->id.name));
+
+ return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
static void topping_private_free(struct usb_mixer_interface *mixer)
{
struct topping_mixer *tm = mixer->private_data;
@@ -479,6 +599,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
if (err < 0)
return err; /* private_free cleans up */
}
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_sel(tm, i);
+ if (err < 0)
+ return err;
+ }
err = usb_submit_urb(tm->urb, GFP_KERNEL);
if (err < 0) {
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-23 14:22 ` [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
@ 2026-08-23 19:48 ` Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 19:48 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
v3 answers the automated review of v2. Four of its five findings were
real, and one of them was a bug a user would have met on every boot.
- The enum refused "Unknown", which is the value it reports until a
hand has chosen. alsactl stores and restores that value like any
other, so the refusal failed a restore of the driver's own report
-- observed here as "Cannot write control ... Invalid argument"
from alsactl. Writing "Unknown" is now a quiet no-op rather than an
error, since it is a report and not a choice either way.
- The hardware is reached under snd_usb_lock_shutdown(), the way the
rest of this directory reaches it. Without it nothing made the
teardown wait for a control callback already in flight.
- Suspend and resume are handled rather than survived: the URB does
not outlive a system sleep, so notifications stopped for good after
the first one. The resume path resubmits, subscribes again and asks
for the state, which also refreshes a cache that may have gone
stale while the panel was reachable and the driver was not.
- The claimed interface is released, on the error path and at
teardown, so unbind and bind again works instead of failing at the
claim. That needed a release helper beside snd_usb_claim_iface(),
for the same reason the claim needed one.
- A mutex spans each write from the comparison to the cache update.
The review called two writers reaching the device in one order and
the cache in the other a race, and it is one, though a narrow one.
Nothing else changed since v2; the questions in that cover letter about
the third form of the driver and about a control that can be written
but not read still stand.
Tested on the hardware as before: values arrive by themselves after
probe, a front panel knob still reaches the driver ten minutes later, a
write reaches the analogue stage -- one source recorded at gain 30 and
at gain 60 differs by 29.7 dB against the 30.0 dB the taper table
predicts -- and for 2/2 the audible test, where pointing an output away
from the bus being played silences it and pointing it back returns the
sound. On a KASAN and lockdep kernel, including unplug while a stream
was running, and now also across a suspend and resume cycle; no
reports.
Mikhail Gavrilov (2):
ALSA: usb-audio: expose the Topping M62's analogue gains as mixer
controls
ALSA: usb-audio: let the M62's outputs say what they listen to
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 709 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 756 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-08-23 19:48 ` Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 19:48 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Seven rows. OTG IN was the one gap when this was first posted -- it has
no front panel control, so it never announced itself and its property
was unknown; a capture of the vendor application moving it named it as
target 0x27, and its taper is the same family as Bluetooth, confirmed
by the indices the application dwelt on matching the decibels it
displayed.
The subscription lapses, so it is renewed: the vendor application
repeats the same subscribe every two seconds for as long as it runs,
and a device that hears nothing stops reporting. A listener that
subscribed once got the meters and the identification block and then
very little; one that kept repeating got the gains too, about five
seconds in. Nothing in the frame says "keep alive" and nothing
acknowledges it -- it is the subscribe again -- so a plain periodic
write does it. Two seconds is what the vendor uses; the device
presumably tolerates longer, but there is no reason to find the edge.
The device is reached under the shutdown lock, the way the rest of this
directory reaches hardware: the teardown waits for everyone holding it
before the card is taken apart, so nothing here can be talking to a
device that has gone. A mutex spans each write from the comparison to
the cache update, so two writers cannot arrive at the device in one
order and at the cache in the other.
Suspend and resume are handled rather than survived. The URB does not
outlive a system sleep, and a device that has heard nothing for a while
stops reporting anyway, so the resume path resubmits, subscribes again
and asks for the state -- which also refreshes a cache that may have
gone stale while the panel was reachable and this driver was not.
The claimed interface is given back, on the error path and at teardown
alike, so that unbinding and binding again works instead of failing at
the claim.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 577 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 624 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index c31a32a2c748..86447da07a6d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27435,6 +27435,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..3c156d1420d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e491779..191391822092 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,25 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
+void snd_usb_release_iface(struct usb_interface *iface)
+{
+ usb_driver_release_interface(&usb_audio_driver, iface);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..2f2365b9b476
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,577 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/cleanup.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ bool claimed; /* iface is ours to give back */
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_KERNEL);
+ int err, actual;
+
+ if (!buf)
+ return -ENOMEM;
+
+ /*
+ * The shutdown lock is what makes a write safe against disconnect:
+ * the teardown waits for everyone holding it before the card goes
+ * away, so the device and this mixer are alive for as long as it
+ * is held.
+ */
+ CLASS(snd_usb_lock, pm)(tm->mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ if (err < 0)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ int idx, value, err;
+ bool changed;
+
+ if (urb->status)
+ return; /* resubmitted below only when running */
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ changed = false;
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[idx] != value) {
+ tm->val[idx] = value;
+ changed = true;
+ }
+ }
+
+ if (changed && tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ keepalive.work);
+
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(spinlock_irqsave)(&tm->lock);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&tm->write_lock);
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ if (tm->val[elem->control] == value)
+ return 0;
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ tm->val[elem->control] = value;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_suspend(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb)
+ usb_kill_urb(tm->urb);
+}
+
+static int topping_resume(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+ int err;
+
+ if (!tm)
+ return 0;
+
+ if (tm->urb) {
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0)
+ return err;
+ }
+
+ /*
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ if (tm->claimed)
+ snd_usb_release_iface(tm->iface);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+ mutex_init(&tm->write_lock);
+ INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+ tm->claimed = true;
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+ mixer->private_suspend = topping_suspend;
+ mixer->private_resume = topping_resume;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+fail:
+ if (tm->claimed)
+ snd_usb_release_iface(iface);
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d7ad25..16b8c6a8031c 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,10 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+void snd_usb_release_iface(struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-23 19:48 ` Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 19:48 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
Each output on this card has a source selector: it can take one of the
three internal mixes, or any input, or any playback bus straight from
USB. Which it is decides everything downstream -- point the headphones
at a playback bus and the card's mixer leaves the path entirely, along
with every question about what is summed into it.
That matters more here than it would elsewhere. There is no control
panel for the M62 on Linux, so a user who never runs the vendor's
application on another machine has no way to see or change this, and
inherits whatever the card was last told. One enumerated control per
output gives them the whole choice, and it does so without exposing the
sixty-cell mixer matrix, which without a graphical representation would
confuse far more than it helps.
The item list has "Unknown" first, and it is deliberate rather than
tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not
to the vendor's own application, which on connect pushes its entire
workspace to the card rather than asking it anything. So the current
setting cannot be learned at probe, and saying so is the only honest
thing a control can do until a hand has chosen. Selecting "Unknown" is
refused, since it is a report and not a choice.
The numbering the card uses has a hole where 4 and 5 would be, so the
item index and the value written are kept as separate tables rather
than one being computed from the other.
Writing "Unknown" changes nothing and says so quietly. It is what the
control reports until a hand has chosen, and alsactl stores and
restores it like any other value, so refusing it would fail a restore
of the driver's own report -- once at every boot, and again whenever a
saved state is put back over a chosen one.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 132 ++++++++++++++++++++++++++++++++++++++
1 file changed, 132 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 2f2365b9b476..8bf5fe483ccf 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -126,6 +126,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
topping_tlv_out_0 },
};
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+
struct topping_mixer {
struct usb_mixer_interface *mixer;
struct usb_interface *iface;
@@ -142,6 +181,7 @@ struct topping_mixer {
spinlock_t lock; /* guards val[] against the URB */
int *val;
struct snd_kcontrol **kctl;
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
};
static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
@@ -332,6 +372,66 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
return 1;
}
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(mutex)(&tm->write_lock);
+ ucontrol->value.enumerated.item[0] = tm->sel[elem->control];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ const struct topping_enum_desc *d;
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ guard(mutex)(&tm->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has chosen,
+ * and alsactl stores and restores it like any other value. It is
+ * not a choice, so writing it changes nothing -- quietly, rather
+ * than failing a restore of the driver's own report.
+ */
+ if (!item || tm->sel[elem->control] == item)
+ return 0;
+
+ d = &topping_m62_enums[elem->control];
+ err = topping_send(tm, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ tm->sel[elem->control] = item;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
static const struct snd_kcontrol_new topping_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
@@ -374,6 +474,33 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx)
return 0;
}
+static int topping_add_sel(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_sel, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ strscpy(kctl->id.name, topping_m62_enums[idx].name,
+ sizeof(kctl->id.name));
+
+ return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
static void topping_suspend(struct usb_mixer_interface *mixer)
{
struct topping_mixer *tm = mixer->private_data;
@@ -543,6 +670,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
if (err < 0)
return err; /* private_free cleans up */
}
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_sel(tm, i);
+ if (err < 0)
+ return err;
+ }
err = usb_submit_urb(tm->urb, GFP_KERNEL);
if (err < 0) {
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
@ 2026-08-23 22:29 ` Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 22:29 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
v4 answers the automated review of v3. One of its three findings led to
a change; the other two do not hold, and I would rather say why here
than leave the question hanging for a human reader of the thread.
The change is in 2/2. The driver marks its elements USB_MIXER_BESPOKEN,
which opts them out of the mixer core's restore_mixer_value(), so
restoring across a suspend became this driver's own business. For the
gains that is covered -- the device announces them and the cache
re-syncs by itself -- but a source selector is never reported by the
device at all, so nothing covered it: if the card had come up on its
own defaults while the host slept, the driver's idea of the selector
would have been silently wrong, and writing the remembered value back
would have looked like no change and done nothing. The resume path now
writes the chosen value again.
Said plainly, because it matters for how you weigh it: I did not
observe that. This card has a battery, and with the headphone selector
on Playback 3/4 and music playing into that bus, a full suspend and
wake brought the sound straight back untouched. The change closes a gap
this driver opened by opting out of the core's restore, not a fault
seen on the hardware.
The two findings I am not acting on both concern a control callback
using a freed private structure during disconnect. The ALSA core
already excludes it. snd_ctl_elem_read() and snd_ctl_elem_write() take
snd_power_ref_and_wait(card) around the callback;
snd_card_disconnect() ends with snd_power_sync_ref(card), which waits
until every such reference is dropped; and in usb-audio's disconnect,
snd_card_disconnect() runs before usb_audio_disconnect_components()
reaches snd_usb_mixer_disconnect() and hence this driver's
private_free(). So no callback can be inside the driver when the
structure is freed, and none can enter afterwards --
snd_power_ref_and_wait() sees card->shutdown and returns -ENODEV. The
observation that the controls themselves live until snd_card_free() is
correct; the access path to them is closed earlier.
Nothing else changed since v3. The questions in the v2 cover letter --
about a third form in which snd-usb-audio would register the hid_driver
itself, and about a control that can be written but not read -- still
stand.
Tested on the hardware: values arrive by themselves after probe, a
front panel knob still reaches the driver ten minutes later and after a
suspend and resume cycle, a write reaches the analogue stage (one
source recorded at gain 30 and at gain 60 differs by 30.4 dB against
the 30.0 dB the taper table predicts), the audible selector test
passes, unbind and bind again works, and alsactl stores and restores
these controls without complaint. On a KASAN and lockdep kernel,
including unplug while a stream was running; no reports.
Mikhail Gavrilov (2):
ALSA: usb-audio: expose the Topping M62's analogue gains as mixer
controls
ALSA: usb-audio: let the M62's outputs say what they listen to
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 734 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 781 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-08-23 22:29 ` Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 22:29 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Seven rows. OTG IN was the one gap when this was first posted -- it has
no front panel control, so it never announced itself and its property
was unknown; a capture of the vendor application moving it named it as
target 0x27, and its taper is the same family as Bluetooth, confirmed
by the indices the application dwelt on matching the decibels it
displayed.
The subscription lapses, so it is renewed: the vendor application
repeats the same subscribe every two seconds for as long as it runs,
and a device that hears nothing stops reporting. A listener that
subscribed once got the meters and the identification block and then
very little; one that kept repeating got the gains too, about five
seconds in. Nothing in the frame says "keep alive" and nothing
acknowledges it -- it is the subscribe again -- so a plain periodic
write does it. Two seconds is what the vendor uses; the device
presumably tolerates longer, but there is no reason to find the edge.
The device is reached under the shutdown lock, the way the rest of this
directory reaches hardware: the teardown waits for everyone holding it
before the card is taken apart, so nothing here can be talking to a
device that has gone. A mutex spans each write from the comparison to
the cache update, so two writers cannot arrive at the device in one
order and at the cache in the other.
Suspend and resume are handled rather than survived. The URB does not
outlive a system sleep, and a device that has heard nothing for a while
stops reporting anyway, so the resume path resubmits, subscribes again
and asks for the state -- which also refreshes a cache that may have
gone stale while the panel was reachable and this driver was not.
The claimed interface is given back, on the error path and at teardown
alike, so that unbinding and binding again works instead of failing at
the claim.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 577 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 624 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index c31a32a2c748..86447da07a6d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27435,6 +27435,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..3c156d1420d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e491779..191391822092 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,25 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
+void snd_usb_release_iface(struct usb_interface *iface)
+{
+ usb_driver_release_interface(&usb_audio_driver, iface);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..2f2365b9b476
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,577 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/cleanup.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ bool claimed; /* iface is ours to give back */
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_KERNEL);
+ int err, actual;
+
+ if (!buf)
+ return -ENOMEM;
+
+ /*
+ * The shutdown lock is what makes a write safe against disconnect:
+ * the teardown waits for everyone holding it before the card goes
+ * away, so the device and this mixer are alive for as long as it
+ * is held.
+ */
+ CLASS(snd_usb_lock, pm)(tm->mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ if (err < 0)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ int idx, value, err;
+ bool changed;
+
+ if (urb->status)
+ return; /* resubmitted below only when running */
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ changed = false;
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[idx] != value) {
+ tm->val[idx] = value;
+ changed = true;
+ }
+ }
+
+ if (changed && tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ keepalive.work);
+
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(spinlock_irqsave)(&tm->lock);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&tm->write_lock);
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ if (tm->val[elem->control] == value)
+ return 0;
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ tm->val[elem->control] = value;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_suspend(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb)
+ usb_kill_urb(tm->urb);
+}
+
+static int topping_resume(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+ int err;
+
+ if (!tm)
+ return 0;
+
+ if (tm->urb) {
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0)
+ return err;
+ }
+
+ /*
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ if (tm->claimed)
+ snd_usb_release_iface(tm->iface);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+ mutex_init(&tm->write_lock);
+ INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+ tm->claimed = true;
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+ mixer->private_suspend = topping_suspend;
+ mixer->private_resume = topping_resume;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+fail:
+ if (tm->claimed)
+ snd_usb_release_iface(iface);
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d7ad25..16b8c6a8031c 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,10 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+void snd_usb_release_iface(struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-23 22:29 ` Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-23 22:29 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
Each output on this card has a source selector: it can take one of the
three internal mixes, or any input, or any playback bus straight from
USB. Which it is decides everything downstream -- point the headphones
at a playback bus and the card's mixer leaves the path entirely, along
with every question about what is summed into it.
That matters more here than it would elsewhere. There is no control
panel for the M62 on Linux, so a user who never runs the vendor's
application on another machine has no way to see or change this, and
inherits whatever the card was last told. One enumerated control per
output gives them the whole choice, and it does so without exposing the
sixty-cell mixer matrix, which without a graphical representation would
confuse far more than it helps.
The item list has "Unknown" first, and it is deliberate rather than
tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not
to the vendor's own application, which on connect pushes its entire
workspace to the card rather than asking it anything. So the current
setting cannot be learned at probe, and saying so is the only honest
thing a control can do until a hand has chosen. Selecting "Unknown" is
refused, since it is a report and not a choice.
The numbering the card uses has a hole where 4 and 5 would be, so the
item index and the value written are kept as separate tables rather
than one being computed from the other.
Writing "Unknown" changes nothing and says so quietly. It is what the
control reports until a hand has chosen, and alsactl stores and
restores it like any other value, so refusing it would fail a restore
of the driver's own report -- once at every boot, and again whenever a
saved state is put back over a chosen one.
The choice is written again on resume. The gains need no such help --
the device announces them and the cache re-syncs by itself -- but a
selector is never reported, so if the card came up on its own defaults
while the host slept, the driver's idea of it would be silently wrong
and writing the remembered value back would look like no change at all.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 157 ++++++++++++++++++++++++++++++++++++++
1 file changed, 157 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 2f2365b9b476..e71ea70ec4b0 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -126,6 +126,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
topping_tlv_out_0 },
};
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+
struct topping_mixer {
struct usb_mixer_interface *mixer;
struct usb_interface *iface;
@@ -142,6 +181,7 @@ struct topping_mixer {
spinlock_t lock; /* guards val[] against the URB */
int *val;
struct snd_kcontrol **kctl;
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
};
static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
@@ -332,6 +372,66 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
return 1;
}
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(mutex)(&tm->write_lock);
+ ucontrol->value.enumerated.item[0] = tm->sel[elem->control];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ const struct topping_enum_desc *d;
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ guard(mutex)(&tm->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has chosen,
+ * and alsactl stores and restores it like any other value. It is
+ * not a choice, so writing it changes nothing -- quietly, rather
+ * than failing a restore of the driver's own report.
+ */
+ if (!item || tm->sel[elem->control] == item)
+ return 0;
+
+ d = &topping_m62_enums[elem->control];
+ err = topping_send(tm, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ tm->sel[elem->control] = item;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
static const struct snd_kcontrol_new topping_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
@@ -374,6 +474,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx)
return 0;
}
+static int topping_add_sel(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_sel, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ strscpy(kctl->id.name, topping_m62_enums[idx].name,
+ sizeof(kctl->id.name));
+
+ return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again
+ * -- which is what the mixer core does for every control that is not
+ * marked as the driver's own to handle.
+ */
+static void topping_restore_sel(struct topping_mixer *tm)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&tm->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!tm->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(tm, d->target, d->prop,
+ topping_source_value[tm->sel[i]]);
+ }
+}
+
static void topping_suspend(struct usb_mixer_interface *mixer)
{
struct topping_mixer *tm = mixer->private_data;
@@ -407,6 +558,7 @@ static int topping_resume(struct usb_mixer_interface *mixer)
*/
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(tm);
schedule_delayed_work(&tm->keepalive,
msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
return 0;
@@ -543,6 +695,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
if (err < 0)
return err; /* private_free cleans up */
}
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_sel(tm, i);
+ if (err < 0)
+ return err;
+ }
err = usb_submit_urb(tm->urb, GFP_KERNEL);
if (err < 0) {
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
@ 2026-08-24 20:13 ` Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-24 20:13 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
v5 takes three more points from the automated review, all in 1/2, and
declines two that are a repeat of v3's.
Taken:
- Allocation on the resume path now asks for no I/O. topping_send()
is reached from topping_resume(), where reclaim can wait on a block
device that has not woken yet, and the URB submitted there wanted
the same treatment. This is what snd_usb_mixer_activate() does a
few hundred lines away in mixer.c, so it is the file's own habit
rather than a novelty.
- A resubmit that fails with -EPERM is no longer reported. That is a
URB usb_kill_urb() has already refused, which is the ordinary sound
of an unplug rather than a fault. The line has been wrong since v1;
nobody noticed until now.
Declined, with the same reasoning the v4 cover letter gave, since the
finding came back unchanged: a control callback cannot dereference a
freed private structure during disconnect. snd_ctl_elem_read() and
snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the
callback; snd_card_disconnect() ends with snd_power_sync_ref(card),
which waits until every such reference is dropped; and in usb-audio's
disconnect, snd_card_disconnect() runs before
usb_audio_disconnect_components() reaches snd_usb_mixer_disconnect()
and hence this driver's private_free(). No callback can be inside the
driver when the structure is freed, and none can enter afterwards --
snd_power_ref_and_wait() sees card->shutdown and returns -ENODEV.
Nothing else changed since v4. 2/2 is unchanged. The questions from the
v2 cover letter still stand: whether snd-usb-audio registering the
hid_driver itself would be a better shape than either road posted, and
whether there is a convention for a control that can be written but not
read.
Tested on the hardware as before, including a suspend and resume cycle
and unbind and bind again, on a KASAN and lockdep kernel; no reports.
Mikhail Gavrilov (2):
ALSA: usb-audio: expose the Topping M62's analogue gains as mixer
controls
ALSA: usb-audio: let the M62's outputs say what they listen to
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 744 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 791 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: 2709dd5ae32f0828f386327c76bba9f39f63a1c6
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-08-24 20:13 ` Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-24 20:13 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Seven rows. OTG IN was the one gap when this was first posted -- it has
no front panel control, so it never announced itself and its property
was unknown; a capture of the vendor application moving it named it as
target 0x27, and its taper is the same family as Bluetooth, confirmed
by the indices the application dwelt on matching the decibels it
displayed.
The subscription lapses, so it is renewed: the vendor application
repeats the same subscribe every two seconds for as long as it runs,
and a device that hears nothing stops reporting. A listener that
subscribed once got the meters and the identification block and then
very little; one that kept repeating got the gains too, about five
seconds in. Nothing in the frame says "keep alive" and nothing
acknowledges it -- it is the subscribe again -- so a plain periodic
write does it. Two seconds is what the vendor uses; the device
presumably tolerates longer, but there is no reason to find the edge.
The device is reached under the shutdown lock, the way the rest of this
directory reaches hardware: the teardown waits for everyone holding it
before the card is taken apart, so nothing here can be talking to a
device that has gone. A mutex spans each write from the comparison to
the cache update, so two writers cannot arrive at the device in one
order and at the cache in the other.
Suspend and resume are handled rather than survived. The URB does not
outlive a system sleep, and a device that has heard nothing for a while
stops reporting anyway, so the resume path resubmits, subscribes again
and asks for the state -- which also refreshes a cache that may have
gone stale while the panel was reachable and this driver was not.
The claimed interface is given back, on the error path and at teardown
alike, so that unbinding and binding again works instead of failing at
the claim.
Allocation on the resume path asks for no I/O, the way the mixer core
does beside it, since reclaim there can wait on a block device that has
not woken yet; and a URB that usb_kill_urb() has already refused is not
reported as a failure, since -EPERM at that point is the ordinary sound
of an unplug.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 587 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 634 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index c31a32a2c748..86447da07a6d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27435,6 +27435,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..3c156d1420d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e491779..191391822092 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,25 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
+void snd_usb_release_iface(struct usb_interface *iface)
+{
+ usb_driver_release_interface(&usb_audio_driver, iface);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..2f42b2633626
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,587 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/cleanup.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ bool claimed; /* iface is ours to give back */
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet. The frame is fifteen bytes; nothing is lost by
+ * asking for it without I/O.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_NOIO);
+ int err, actual;
+
+ if (!buf)
+ return -ENOMEM;
+
+ /*
+ * The shutdown lock is what makes a write safe against disconnect:
+ * the teardown waits for everyone holding it before the card goes
+ * away, so the device and this mixer are alive for as long as it
+ * is held.
+ */
+ CLASS(snd_usb_lock, pm)(tm->mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ if (err < 0)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ int idx, value, err;
+ bool changed;
+
+ if (urb->status)
+ return; /* resubmitted below only when running */
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ changed = false;
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[idx] != value) {
+ tm->val[idx] = value;
+ changed = true;
+ }
+ }
+
+ if (changed && tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ /*
+ * ENODEV, ESHUTDOWN and EPERM are the sound of an unplug -- the
+ * last being a URB that usb_kill_urb() has already refused.
+ */
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN && err != -EPERM)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ keepalive.work);
+
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(spinlock_irqsave)(&tm->lock);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&tm->write_lock);
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ if (tm->val[elem->control] == value)
+ return 0;
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ tm->val[elem->control] = value;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_suspend(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb)
+ usb_kill_urb(tm->urb);
+}
+
+static int topping_resume(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+ int err;
+
+ if (!tm)
+ return 0;
+
+ if (tm->urb) {
+ err = usb_submit_urb(tm->urb, GFP_NOIO);
+ if (err < 0)
+ return err;
+ }
+
+ /*
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ if (tm->claimed)
+ snd_usb_release_iface(tm->iface);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+ mutex_init(&tm->write_lock);
+ INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+ tm->claimed = true;
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+ mixer->private_suspend = topping_suspend;
+ mixer->private_resume = topping_resume;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+fail:
+ if (tm->claimed)
+ snd_usb_release_iface(iface);
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d7ad25..16b8c6a8031c 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,10 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+void snd_usb_release_iface(struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-24 20:13 ` Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-24 20:13 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
Each output on this card has a source selector: it can take one of the
three internal mixes, or any input, or any playback bus straight from
USB. Which it is decides everything downstream -- point the headphones
at a playback bus and the card's mixer leaves the path entirely, along
with every question about what is summed into it.
That matters more here than it would elsewhere. There is no control
panel for the M62 on Linux, so a user who never runs the vendor's
application on another machine has no way to see or change this, and
inherits whatever the card was last told. One enumerated control per
output gives them the whole choice, and it does so without exposing the
sixty-cell mixer matrix, which without a graphical representation would
confuse far more than it helps.
The item list has "Unknown" first, and it is deliberate rather than
tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not
to the vendor's own application, which on connect pushes its entire
workspace to the card rather than asking it anything. So the current
setting cannot be learned at probe, and saying so is the only honest
thing a control can do until a hand has chosen. Selecting "Unknown" is
refused, since it is a report and not a choice.
The numbering the card uses has a hole where 4 and 5 would be, so the
item index and the value written are kept as separate tables rather
than one being computed from the other.
Writing "Unknown" changes nothing and says so quietly. It is what the
control reports until a hand has chosen, and alsactl stores and
restores it like any other value, so refusing it would fail a restore
of the driver's own report -- once at every boot, and again whenever a
saved state is put back over a chosen one.
The choice is written again on resume. The gains need no such help --
the device announces them and the cache re-syncs by itself -- but a
selector is never reported, so if the card came up on its own defaults
while the host slept, the driver's idea of it would be silently wrong
and writing the remembered value back would look like no change at all.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 157 ++++++++++++++++++++++++++++++++++++++
1 file changed, 157 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 2f42b2633626..1743b8ee7fac 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -126,6 +126,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
topping_tlv_out_0 },
};
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+
struct topping_mixer {
struct usb_mixer_interface *mixer;
struct usb_interface *iface;
@@ -142,6 +181,7 @@ struct topping_mixer {
spinlock_t lock; /* guards val[] against the URB */
int *val;
struct snd_kcontrol **kctl;
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
};
static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
@@ -342,6 +382,66 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
return 1;
}
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(mutex)(&tm->write_lock);
+ ucontrol->value.enumerated.item[0] = tm->sel[elem->control];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ const struct topping_enum_desc *d;
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ guard(mutex)(&tm->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has chosen,
+ * and alsactl stores and restores it like any other value. It is
+ * not a choice, so writing it changes nothing -- quietly, rather
+ * than failing a restore of the driver's own report.
+ */
+ if (!item || tm->sel[elem->control] == item)
+ return 0;
+
+ d = &topping_m62_enums[elem->control];
+ err = topping_send(tm, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ tm->sel[elem->control] = item;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
static const struct snd_kcontrol_new topping_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
@@ -384,6 +484,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx)
return 0;
}
+static int topping_add_sel(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_sel, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ strscpy(kctl->id.name, topping_m62_enums[idx].name,
+ sizeof(kctl->id.name));
+
+ return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again
+ * -- which is what the mixer core does for every control that is not
+ * marked as the driver's own to handle.
+ */
+static void topping_restore_sel(struct topping_mixer *tm)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&tm->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!tm->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(tm, d->target, d->prop,
+ topping_source_value[tm->sel[i]]);
+ }
+}
+
static void topping_suspend(struct usb_mixer_interface *mixer)
{
struct topping_mixer *tm = mixer->private_data;
@@ -417,6 +568,7 @@ static int topping_resume(struct usb_mixer_interface *mixer)
*/
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(tm);
schedule_delayed_work(&tm->keepalive,
msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
return 0;
@@ -553,6 +705,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
if (err < 0)
return err; /* private_free cleans up */
}
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_sel(tm, i);
+ if (err < 0)
+ return err;
+ }
err = usb_submit_urb(tm->urb, GFP_KERNEL);
if (err < 0) {
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
@ 2026-08-24 22:31 ` Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-24 22:31 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
v6 takes four more points from the automated review and declines two.
The first of the four is a deadlock, and it is worth saying how it got
there, because neither change that made it was wrong on its own.
v3 added a mutex around each write, so two writers could not reach the
device in one order and the cache in the other. v4 added a resume-time
write of the source selectors, since the device never reports them and
nothing else would restore them. Together they close a loop: a write on
a runtime-suspended device takes the mutex, calls into the device, and
waking it runs this driver's own resume callback on the same thread --
which takes the same mutex, held by the caller. The order is now the
other way round: the device is woken outside the lock, so a wake that
runs the resume callback finds nothing held.
The other three:
- A URB that completes with an error is resubmitted unless the error
means the URB or the device is gone. Bus noise gives -EPROTO and
-EILSEQ, and stopping on those left the card silent until it was
replugged. This is what snd_usb_mixer_status_complete() does a few
hundred lines away.
- The resume path now forbids I/O reclaim for everything under it,
not only for the frame buffer: usb_interrupt_msg() allocates a URB
of its own with GFP_KERNEL, so a polite flag on our allocation
settles nothing by itself.
- The claimed interface is held with a reference. Claiming does not
keep it alive, and this driver hands the pointer back to the core
when the card goes away.
Declined, for the third time and with the same reasoning the v4 cover
letter gave: a control callback cannot dereference a freed private
structure during disconnect. snd_ctl_elem_read() and
snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the
callback; snd_card_disconnect() ends with snd_power_sync_ref(card),
which waits until every such reference is dropped; and in usb-audio's
disconnect, snd_card_disconnect() runs before
usb_audio_disconnect_components() reaches this driver's private_free().
There is a second reason not to do it anyway: taking the shutdown lock
in a get would wake a runtime-suspended device in order to read a
number this driver already has in memory.
The path was exercised. It needs the card in runtime suspend at the
moment a control is written, which does not happen by itself here: the
driver's own keepalive writes every two seconds and the default
autosuspend delay is also two thousand milliseconds, so the timer never
expires. With that delay set to zero the card suspends between
keepalives, and a control write then returns at once with the value
set. I did not go back to v5 to watch it hang.
Tested on the hardware as before: values arrive by themselves after
probe, a front panel knob reaches the driver ten minutes later and
after a suspend and resume cycle, a write reaches the analogue stage
(one source recorded at gain 30 and at gain 60 differs by 30.4 dB
against the 30.0 dB the taper table predicts), the audible selector
test passes, unbind and bind again works, and alsactl stores and
restores these controls without complaint. On a KASAN and lockdep
kernel; no reports.
The questions from the v2 cover letter still stand: whether
snd-usb-audio registering the hid_driver itself would be a better shape
than either road posted, and whether there is a convention for a
control that can be written but not read.
Mikhail Gavrilov (2):
ALSA: usb-audio: expose the Topping M62's analogue gains as mixer
controls
ALSA: usb-audio: let the M62's outputs say what they listen to
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 794 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 841 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: 47096fc3d064a07c0842f748b99ebf01be120f2b
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-08-24 22:31 ` Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-24 22:31 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Seven rows. OTG IN was the one gap when this was first posted -- it has
no front panel control, so it never announced itself and its property
was unknown; a capture of the vendor application moving it named it as
target 0x27, and its taper is the same family as Bluetooth, confirmed
by the indices the application dwelt on matching the decibels it
displayed.
The subscription lapses, so it is renewed: the vendor application
repeats the same subscribe every two seconds for as long as it runs,
and a device that hears nothing stops reporting. A listener that
subscribed once got the meters and the identification block and then
very little; one that kept repeating got the gains too, about five
seconds in. Nothing in the frame says "keep alive" and nothing
acknowledges it -- it is the subscribe again -- so a plain periodic
write does it. Two seconds is what the vendor uses; the device
presumably tolerates longer, but there is no reason to find the edge.
The device is reached under the shutdown lock, the way the rest of this
directory reaches hardware: the teardown waits for everyone holding it
before the card is taken apart, so nothing here can be talking to a
device that has gone. A mutex spans each write from the comparison to
the cache update, so two writers cannot arrive at the device in one
order and at the cache in the other.
Suspend and resume are handled rather than survived. The URB does not
outlive a system sleep, and a device that has heard nothing for a while
stops reporting anyway, so the resume path resubmits, subscribes again
and asks for the state -- which also refreshes a cache that may have
gone stale while the panel was reachable and this driver was not.
The claimed interface is given back, on the error path and at teardown
alike, so that unbinding and binding again works instead of failing at
the claim.
Allocation on the resume path asks for no I/O, the way the mixer core
does beside it, since reclaim there can wait on a block device that has
not woken yet; and a URB that usb_kill_urb() has already refused is not
reported as a failure, since -EPERM at that point is the ordinary sound
of an unplug.
The device is woken before the write lock is taken, not after. Waking it
can run the resume callback on the calling thread, and that callback
writes to the device too; entering the lock first would meet it already
held, by the same thread, with nothing able to release it.
Bus noise does not end the listening. A frame that arrives with -EPROTO
or -EILSEQ is dropped and the URB goes back; only the three statuses
that mean the URB or the device is gone stop it, which is what the
mixer's own status handler does next door.
The resume path forbids I/O reclaim for everything under it, not just
for the frame buffer: usb_interrupt_msg() allocates a URB of its own
with GFP_KERNEL, so a polite flag on our own allocation settles
nothing.
And the claimed interface is held with a reference of its own. Claiming
does not keep it alive -- on unplug the core deletes and puts every
interface -- while this driver hands the pointer back to the core when
the card goes away.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 632 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
9 files changed, 679 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 8d9648737e92..5179f0bd5699 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27469,6 +27469,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..3c156d1420d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e491779..191391822092 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,25 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
+void snd_usb_release_iface(struct usb_interface *iface)
+{
+ usb_driver_release_interface(&usb_audio_driver, iface);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..f8ddeafb0222
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,632 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/cleanup.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ bool claimed; /* iface is ours to give back */
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet. The frame is fifteen bytes; nothing is lost by
+ * asking for it without I/O.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_NOIO);
+ int err, actual;
+
+ if (!buf)
+ return -ENOMEM;
+
+ /*
+ * The shutdown lock is what makes a write safe against disconnect:
+ * the teardown waits for everyone holding it before the card goes
+ * away, so the device and this mixer are alive for as long as it
+ * is held.
+ */
+ CLASS(snd_usb_lock, pm)(tm->mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ if (err < 0)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ int idx, value, err;
+ bool changed;
+
+ /*
+ * Bus noise gives -EPROTO and -EILSEQ, and those are not the end
+ * of anything: the frame is dropped and the listening goes on.
+ * Only the three that mean the URB or the device is gone stop it,
+ * which is what the mixer's own status handler does next door.
+ */
+ if (urb->status)
+ goto resubmit;
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ changed = false;
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[idx] != value) {
+ tm->val[idx] = value;
+ changed = true;
+ }
+ }
+
+ if (changed && tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ if (urb->status == -ENOENT || urb->status == -ECONNRESET ||
+ urb->status == -ESHUTDOWN)
+ return;
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ /*
+ * ENODEV, ESHUTDOWN and EPERM are the sound of an unplug -- the
+ * last being a URB that usb_kill_urb() has already refused.
+ */
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN && err != -EPERM)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ keepalive.work);
+
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(spinlock_irqsave)(&tm->lock);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run the
+ * resume callback on this very thread, and that callback takes
+ * write_lock to write the selectors back; taking write_lock first
+ * would meet it already held, by us. So the device is woken
+ * first and the lock is entered with nothing left to run under
+ * it. topping_send() takes the same wake lock again, which is
+ * refcounted and cheap.
+ */
+ CLASS(snd_usb_lock, pm)(mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&tm->write_lock);
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ if (tm->val[elem->control] == value)
+ return 0;
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ tm->val[elem->control] = value;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_suspend(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb)
+ usb_kill_urb(tm->urb);
+}
+
+static int topping_resume(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+ unsigned int noio;
+ int err;
+
+ if (!tm)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usb_interrupt_msg()
+ * allocates a URB of its own with GFP_KERNEL, so asking for the
+ * frame buffer politely is not enough, and reclaim here can wait
+ * on a block device that has not woken yet.
+ */
+ noio = memalloc_noio_save();
+
+ if (tm->urb) {
+ err = usb_submit_urb(tm->urb, GFP_NOIO);
+ if (err < 0) {
+ memalloc_noio_restore(noio);
+ return err;
+ }
+ }
+
+ /*
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ if (tm->claimed) {
+ snd_usb_release_iface(tm->iface);
+ usb_put_intf(tm->iface);
+ }
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+ mutex_init(&tm->write_lock);
+ INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+ tm->claimed = true;
+ /*
+ * Claiming does not keep the interface alive. On unplug the core
+ * deletes and puts every interface, and this pointer is handed
+ * back to it later, so it holds a reference of its own until then.
+ */
+ usb_get_intf(iface);
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+ mixer->private_suspend = topping_suspend;
+ mixer->private_resume = topping_resume;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+fail:
+ if (tm->claimed) {
+ snd_usb_release_iface(iface);
+ usb_put_intf(iface);
+ }
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d7ad25..16b8c6a8031c 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,10 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+void snd_usb_release_iface(struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-24 22:31 ` Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-24 22:31 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
Each output on this card has a source selector: it can take one of the
three internal mixes, or any input, or any playback bus straight from
USB. Which it is decides everything downstream -- point the headphones
at a playback bus and the card's mixer leaves the path entirely, along
with every question about what is summed into it.
That matters more here than it would elsewhere. There is no control
panel for the M62 on Linux, so a user who never runs the vendor's
application on another machine has no way to see or change this, and
inherits whatever the card was last told. One enumerated control per
output gives them the whole choice, and it does so without exposing the
sixty-cell mixer matrix, which without a graphical representation would
confuse far more than it helps.
The item list has "Unknown" first, and it is deliberate rather than
tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not
to the vendor's own application, which on connect pushes its entire
workspace to the card rather than asking it anything. So the current
setting cannot be learned at probe, and saying so is the only honest
thing a control can do until a hand has chosen. Selecting "Unknown" is
refused, since it is a report and not a choice.
The numbering the card uses has a hole where 4 and 5 would be, so the
item index and the value written are kept as separate tables rather
than one being computed from the other.
Writing "Unknown" changes nothing and says so quietly. It is what the
control reports until a hand has chosen, and alsactl stores and
restores it like any other value, so refusing it would fail a restore
of the driver's own report -- once at every boot, and again whenever a
saved state is put back over a chosen one.
The choice is written again on resume. The gains need no such help --
the device announces them and the cache re-syncs by itself -- but a
selector is never reported, so if the card came up on its own defaults
while the host slept, the driver's idea of it would be silently wrong
and writing the remembered value back would look like no change at all.
This write wakes the device before taking the lock for the same reason
the gain controls do: the wake can run the resume callback on this
thread, and that callback writes the selectors back under the same
lock.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 162 ++++++++++++++++++++++++++++++++++++++
1 file changed, 162 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index f8ddeafb0222..df0dbe212c40 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -127,6 +127,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
topping_tlv_out_0 },
};
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+
struct topping_mixer {
struct usb_mixer_interface *mixer;
struct usb_interface *iface;
@@ -143,6 +182,7 @@ struct topping_mixer {
spinlock_t lock; /* guards val[] against the URB */
int *val;
struct snd_kcontrol **kctl;
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
};
static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
@@ -365,6 +405,71 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
return 1;
}
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(mutex)(&tm->write_lock);
+ ucontrol->value.enumerated.item[0] = tm->sel[elem->control];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ const struct topping_enum_desc *d;
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ CLASS(snd_usb_lock, pm)(elem->head.mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+
+ guard(mutex)(&tm->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has chosen,
+ * and alsactl stores and restores it like any other value. It is
+ * not a choice, so writing it changes nothing -- quietly, rather
+ * than failing a restore of the driver's own report.
+ */
+ if (!item || tm->sel[elem->control] == item)
+ return 0;
+
+ d = &topping_m62_enums[elem->control];
+ err = topping_send(tm, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ tm->sel[elem->control] = item;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
static const struct snd_kcontrol_new topping_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
@@ -407,6 +512,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx)
return 0;
}
+static int topping_add_sel(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_sel, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ strscpy(kctl->id.name, topping_m62_enums[idx].name,
+ sizeof(kctl->id.name));
+
+ return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again
+ * -- which is what the mixer core does for every control that is not
+ * marked as the driver's own to handle.
+ */
+static void topping_restore_sel(struct topping_mixer *tm)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&tm->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!tm->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(tm, d->target, d->prop,
+ topping_source_value[tm->sel[i]]);
+ }
+}
+
static void topping_suspend(struct usb_mixer_interface *mixer)
{
struct topping_mixer *tm = mixer->private_data;
@@ -451,6 +607,7 @@ static int topping_resume(struct usb_mixer_interface *mixer)
*/
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(tm);
schedule_delayed_work(&tm->keepalive,
msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
memalloc_noio_restore(noio);
@@ -596,6 +753,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
if (err < 0)
return err; /* private_free cleans up */
}
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_sel(tm, i);
+ if (err < 0)
+ return err;
+ }
err = usb_submit_urb(tm->urb, GFP_KERNEL);
if (err < 0) {
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
@ 2026-08-25 8:56 ` Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-25 8:56 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
v7 takes two more points from the automated review of v6, both in 1/2.
The first is a build one: SND_USB_AUDIO now selects CRC16. The frames
this driver reads are checked with crc16() and nothing else in the
directory pulled it in, so a kernel built with CONFIG_CRC16 off would
have failed to link.
The second is another deadlock, and again of our own making. The
keepalive woke the device before writing; a runtime suspend arriving at
the same moment reached this driver's suspend callback, which waits in
cancel_delayed_work_sync() for the worker -- while the worker waited in
the PM core for that same suspend to finish. Waking is now the caller's
business rather than the frame writer's: a write asked for by a hand
wakes what is asleep, the keepalive and the resume path do not. The
first because a sleeping device has no subscription worth renewing --
resume renews it -- and the second because it is the resume.
That also fixes something nobody had reported yet: with a write every
two seconds and a default autosuspend delay of the same two seconds,
the card could never reach runtime suspend at all. It can now.
The rest of this letter is v6's, since nothing else changed.
v6 took four points from the automated review and declined two. The
first of the four is a deadlock, and it is worth saying how it got
there, because neither change that made it was wrong on its own.
v3 added a mutex around each write, so two writers could not reach the
device in one order and the cache in the other. v4 added a resume-time
write of the source selectors, since the device never reports them and
nothing else would restore them. Together they close a loop: a write on
a runtime-suspended device takes the mutex, calls into the device, and
waking it runs this driver's own resume callback on the same thread --
which takes the same mutex, held by the caller. The order is now the
other way round: the device is woken outside the lock, so a wake that
runs the resume callback finds nothing held.
The other three:
- A URB that completes with an error is resubmitted unless the error
means the URB or the device is gone. Bus noise gives -EPROTO and
-EILSEQ, and stopping on those left the card silent until it was
replugged. This is what snd_usb_mixer_status_complete() does a few
hundred lines away.
- The resume path now forbids I/O reclaim for everything under it,
not only for the frame buffer: usb_interrupt_msg() allocates a URB
of its own with GFP_KERNEL, so a polite flag on our allocation
settles nothing by itself.
- The claimed interface is held with a reference. Claiming does not
keep it alive, and this driver hands the pointer back to the core
when the card goes away.
Declined, for the third time and with the same reasoning the v4 cover
letter gave: a control callback cannot dereference a freed private
structure during disconnect. snd_ctl_elem_read() and
snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the
callback; snd_card_disconnect() ends with snd_power_sync_ref(card),
which waits until every such reference is dropped; and in usb-audio's
disconnect, snd_card_disconnect() runs before
usb_audio_disconnect_components() reaches this driver's private_free().
There is a second reason not to do it anyway: taking the shutdown lock
in a get would wake a runtime-suspended device in order to read a
number this driver already has in memory.
The path was exercised. It needs the card in runtime suspend at the
moment a control is written, which does not happen by itself here: the
driver's own keepalive writes every two seconds and the default
autosuspend delay is also two thousand milliseconds, so the timer never
expires. With that delay set to zero the card suspends between
keepalives, and a control write then returns at once with the value
set. I did not go back to v5 to watch it hang.
Tested on the hardware as before: values arrive by themselves after
probe, a front panel knob reaches the driver ten minutes later and
after a suspend and resume cycle, a write reaches the analogue stage
(one source recorded at gain 30 and at gain 60 differs by 30.4 dB
against the 30.0 dB the taper table predicts), the audible selector
test passes, unbind and bind again works, and alsactl stores and
restores these controls without complaint. On a KASAN and lockdep
kernel; no reports.
The questions from the v2 cover letter still stand: whether
snd-usb-audio registering the hid_driver itself would be a better shape
than either road posted, and whether there is a convention for a
control that can be written but not read.
Mikhail Gavrilov (2):
ALSA: usb-audio: expose the Topping M62's analogue gains as mixer
controls
ALSA: usb-audio: let the M62's outputs say what they listen to
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Kconfig | 1 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 798 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
10 files changed, 846 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: 66498c75b4f8017f62d720d9b59675bdf3abce91
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-08-25 8:56 ` Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-25 8:56 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Seven rows. OTG IN was the one gap when this was first posted -- it has
no front panel control, so it never announced itself and its property
was unknown; a capture of the vendor application moving it named it as
target 0x27, and its taper is the same family as Bluetooth, confirmed
by the indices the application dwelt on matching the decibels it
displayed.
The subscription lapses, so it is renewed: the vendor application
repeats the same subscribe every two seconds for as long as it runs,
and a device that hears nothing stops reporting. A listener that
subscribed once got the meters and the identification block and then
very little; one that kept repeating got the gains too, about five
seconds in. Nothing in the frame says "keep alive" and nothing
acknowledges it -- it is the subscribe again -- so a plain periodic
write does it. Two seconds is what the vendor uses; the device
presumably tolerates longer, but there is no reason to find the edge.
The device is reached under the shutdown lock, the way the rest of this
directory reaches hardware: the teardown waits for everyone holding it
before the card is taken apart, so nothing here can be talking to a
device that has gone. A mutex spans each write from the comparison to
the cache update, so two writers cannot arrive at the device in one
order and at the cache in the other.
Suspend and resume are handled rather than survived. The URB does not
outlive a system sleep, and a device that has heard nothing for a while
stops reporting anyway, so the resume path resubmits, subscribes again
and asks for the state -- which also refreshes a cache that may have
gone stale while the panel was reachable and this driver was not.
The claimed interface is given back, on the error path and at teardown
alike, so that unbinding and binding again works instead of failing at
the claim.
Allocation on the resume path asks for no I/O, the way the mixer core
does beside it, since reclaim there can wait on a block device that has
not woken yet; and a URB that usb_kill_urb() has already refused is not
reported as a failure, since -EPERM at that point is the ordinary sound
of an unplug.
The device is woken before the write lock is taken, not after. Waking it
can run the resume callback on the calling thread, and that callback
writes to the device too; entering the lock first would meet it already
held, by the same thread, with nothing able to release it.
Waking is the caller's business, not the frame writer's. A write asked
for by a hand wakes what is asleep; the keepalive and the resume path
do not, the first because a sleeping device has no subscription worth
renewing -- resume renews it -- and the second because it is the
resume. When the keepalive woke the device itself, a runtime suspend
arriving at the same moment waited in cancel_delayed_work_sync() for a
worker that was in turn waiting for that suspend to finish. It also
means the card can now reach runtime suspend at all, which a write
every two seconds had prevented.
Bus noise does not end the listening. A frame that arrives with -EPROTO
or -EILSEQ is dropped and the URB goes back; only the three statuses
that mean the URB or the device is gone stop it, which is what the
mixer's own status handler does next door.
The resume path forbids I/O reclaim for everything under it, not just
for the frame buffer: usb_interrupt_msg() allocates a URB of its own
with GFP_KERNEL, so a polite flag on our own allocation settles
nothing.
And the claimed interface is held with a reference of its own. Claiming
does not keep it alive -- on unplug the core deletes and puts every
interface -- while this driver hands the pointer back to the core when
the card goes away.
SND_USB_AUDIO selects CRC16, since the frames are checked with it and
nothing else in the directory pulled it in.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Kconfig | 1 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 636 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
10 files changed, 684 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 549df316f487..f5b0bd36be22 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27497,6 +27497,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..3c156d1420d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Kconfig b/sound/usb/Kconfig
index b4588915efa1..5af29599e494 100644
--- a/sound/usb/Kconfig
+++ b/sound/usb/Kconfig
@@ -17,6 +17,7 @@ config SND_USB_AUDIO
select SND_PCM
select SND_UMP if SND_USB_AUDIO_MIDI_V2
select BITREVERSE
+ select CRC16
select SND_USB_AUDIO_USE_MEDIA_CONTROLLER if MEDIA_CONTROLLER && (MEDIA_SUPPORT=y || MEDIA_SUPPORT=SND_USB_AUDIO)
help
Say Y here to include support for USB audio and USB MIDI
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e491779..191391822092 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,25 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
+void snd_usb_release_iface(struct usb_interface *iface)
+{
+ usb_driver_release_interface(&usb_audio_driver, iface);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..06cea753046a
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,636 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/cleanup.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ bool claimed; /* iface is ours to give back */
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes the shutdown lock
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has
+ * no subscription worth renewing -- resume renews it -- and the
+ * second because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ */
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet. The frame is fifteen bytes; nothing is lost by
+ * asking for it without I/O.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_NOIO);
+ int err, actual;
+
+ if (!buf)
+ return -ENOMEM;
+
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ if (err < 0)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ int idx, value, err;
+ bool changed;
+
+ /*
+ * Bus noise gives -EPROTO and -EILSEQ, and those are not the end
+ * of anything: the frame is dropped and the listening goes on.
+ * Only the three that mean the URB or the device is gone stop it,
+ * which is what the mixer's own status handler does next door.
+ */
+ if (urb->status)
+ goto resubmit;
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ changed = false;
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[idx] != value) {
+ tm->val[idx] = value;
+ changed = true;
+ }
+ }
+
+ if (changed && tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ if (urb->status == -ENOENT || urb->status == -ECONNRESET ||
+ urb->status == -ESHUTDOWN)
+ return;
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ /*
+ * ENODEV, ESHUTDOWN and EPERM are the sound of an unplug -- the
+ * last being a URB that usb_kill_urb() has already refused.
+ */
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN && err != -EPERM)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ keepalive.work);
+
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(spinlock_irqsave)(&tm->lock);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run the
+ * resume callback on this very thread, and that callback takes
+ * write_lock to write the selectors back; taking write_lock first
+ * would meet it already held, by us. So the device is woken
+ * first and the lock is entered with nothing left to run under
+ * it. This is also the wake topping_send() relies on, since it
+ * does none of its own.
+ */
+ CLASS(snd_usb_lock, pm)(mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&tm->write_lock);
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ if (tm->val[elem->control] == value)
+ return 0;
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (err < 0)
+ return err;
+ if (d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ if (err < 0)
+ return err;
+ }
+
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ tm->val[elem->control] = value;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_suspend(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb)
+ usb_kill_urb(tm->urb);
+}
+
+static int topping_resume(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+ unsigned int noio;
+ int err;
+
+ if (!tm)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usb_interrupt_msg()
+ * allocates a URB of its own with GFP_KERNEL, so asking for the
+ * frame buffer politely is not enough, and reclaim here can wait
+ * on a block device that has not woken yet.
+ */
+ noio = memalloc_noio_save();
+
+ if (tm->urb) {
+ err = usb_submit_urb(tm->urb, GFP_NOIO);
+ if (err < 0) {
+ memalloc_noio_restore(noio);
+ return err;
+ }
+ }
+
+ /*
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ if (tm->claimed) {
+ snd_usb_release_iface(tm->iface);
+ usb_put_intf(tm->iface);
+ }
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+ mutex_init(&tm->write_lock);
+ INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+ tm->claimed = true;
+ /*
+ * Claiming does not keep the interface alive. On unplug the core
+ * deletes and puts every interface, and this pointer is handed
+ * back to it later, so it holds a reference of its own until then.
+ */
+ usb_get_intf(iface);
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+ mixer->private_suspend = topping_suspend;
+ mixer->private_resume = topping_resume;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+fail:
+ if (tm->claimed) {
+ snd_usb_release_iface(iface);
+ usb_put_intf(iface);
+ }
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d7ad25..16b8c6a8031c 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,10 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+void snd_usb_release_iface(struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-25 8:56 ` Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-25 8:56 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
Each output on this card has a source selector: it can take one of the
three internal mixes, or any input, or any playback bus straight from
USB. Which it is decides everything downstream -- point the headphones
at a playback bus and the card's mixer leaves the path entirely, along
with every question about what is summed into it.
That matters more here than it would elsewhere. There is no control
panel for the M62 on Linux, so a user who never runs the vendor's
application on another machine has no way to see or change this, and
inherits whatever the card was last told. One enumerated control per
output gives them the whole choice, and it does so without exposing the
sixty-cell mixer matrix, which without a graphical representation would
confuse far more than it helps.
The item list has "Unknown" first, and it is deliberate rather than
tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not
to the vendor's own application, which on connect pushes its entire
workspace to the card rather than asking it anything. So the current
setting cannot be learned at probe, and saying so is the only honest
thing a control can do until a hand has chosen. Selecting "Unknown" is
refused, since it is a report and not a choice.
The numbering the card uses has a hole where 4 and 5 would be, so the
item index and the value written are kept as separate tables rather
than one being computed from the other.
Writing "Unknown" changes nothing and says so quietly. It is what the
control reports until a hand has chosen, and alsactl stores and
restores it like any other value, so refusing it would fail a restore
of the driver's own report -- once at every boot, and again whenever a
saved state is put back over a chosen one.
The choice is written again on resume. The gains need no such help --
the device announces them and the cache re-syncs by itself -- but a
selector is never reported, so if the card came up on its own defaults
while the host slept, the driver's idea of it would be silently wrong
and writing the remembered value back would look like no change at all.
This write wakes the device before taking the lock for the same reason
the gain controls do: the wake can run the resume callback on this
thread, and that callback writes the selectors back under the same
lock.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 162 ++++++++++++++++++++++++++++++++++++++
1 file changed, 162 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 06cea753046a..79ba1c5fdcdf 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -127,6 +127,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
topping_tlv_out_0 },
};
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+
struct topping_mixer {
struct usb_mixer_interface *mixer;
struct usb_interface *iface;
@@ -143,6 +182,7 @@ struct topping_mixer {
spinlock_t lock; /* guards val[] against the URB */
int *val;
struct snd_kcontrol **kctl;
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
};
static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
@@ -369,6 +409,71 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
return 1;
}
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(mutex)(&tm->write_lock);
+ ucontrol->value.enumerated.item[0] = tm->sel[elem->control];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ const struct topping_enum_desc *d;
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ CLASS(snd_usb_lock, pm)(elem->head.mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+
+ guard(mutex)(&tm->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has chosen,
+ * and alsactl stores and restores it like any other value. It is
+ * not a choice, so writing it changes nothing -- quietly, rather
+ * than failing a restore of the driver's own report.
+ */
+ if (!item || tm->sel[elem->control] == item)
+ return 0;
+
+ d = &topping_m62_enums[elem->control];
+ err = topping_send(tm, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ tm->sel[elem->control] = item;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
static const struct snd_kcontrol_new topping_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
@@ -411,6 +516,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx)
return 0;
}
+static int topping_add_sel(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_sel, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ strscpy(kctl->id.name, topping_m62_enums[idx].name,
+ sizeof(kctl->id.name));
+
+ return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again
+ * -- which is what the mixer core does for every control that is not
+ * marked as the driver's own to handle.
+ */
+static void topping_restore_sel(struct topping_mixer *tm)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&tm->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!tm->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(tm, d->target, d->prop,
+ topping_source_value[tm->sel[i]]);
+ }
+}
+
static void topping_suspend(struct usb_mixer_interface *mixer)
{
struct topping_mixer *tm = mixer->private_data;
@@ -455,6 +611,7 @@ static int topping_resume(struct usb_mixer_interface *mixer)
*/
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(tm);
schedule_delayed_work(&tm->keepalive,
msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
memalloc_noio_restore(noio);
@@ -600,6 +757,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
if (err < 0)
return err; /* private_free cleans up */
}
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_sel(tm, i);
+ if (err < 0)
+ return err;
+ }
err = usb_submit_urb(tm->urb, GFP_KERNEL);
if (err < 0) {
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
@ 2026-08-25 11:12 ` Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-25 11:12 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
v8 takes two more points from the automated review of v7, both in 1/2.
The cache now takes a new value before the write reaches the device
rather than after. The lock cannot be held across a send, and a hand on
the front panel during that window produces a notification the URB
handler stores; updating afterwards threw that away and left the driver
claiming a value the device had already moved away from. Written first,
the device's own report is simply the last word. A failed write puts
the old value back unless the device has spoken meanwhile.
And the keepalive stops renewing a subscription nobody is listening to:
a write that fails because the device has gone ends the cycle rather
than scheduling another two seconds later, and those two errors are no
longer logged, being an unplug rather than a fault. The review asked
whether the worker could reschedule forever after an unplug; it could
not, since the mixer's private_free() runs during disconnect and
cancels it, but the shape it pointed at was worth straightening anyway.
The selector control has no such race and is left alone: the device
never reports a selector, so nothing but this driver's own writes ever
touches that cache, and they are serialised by the mutex.
v7 took two points from the review of v6, both in 1/2.
The first is a build one: SND_USB_AUDIO now selects CRC16. The frames
this driver reads are checked with crc16() and nothing else in the
directory pulled it in, so a kernel built with CONFIG_CRC16 off would
have failed to link.
The second is another deadlock, and again of our own making. The
keepalive woke the device before writing; a runtime suspend arriving at
the same moment reached this driver's suspend callback, which waits in
cancel_delayed_work_sync() for the worker -- while the worker waited in
the PM core for that same suspend to finish. Waking is now the caller's
business rather than the frame writer's: a write asked for by a hand
wakes what is asleep, the keepalive and the resume path do not. The
first because a sleeping device has no subscription worth renewing --
resume renews it -- and the second because it is the resume.
That also fixes something nobody had reported yet: with a write every
two seconds and a default autosuspend delay of the same two seconds,
the card could never reach runtime suspend at all. It can now.
The rest of this letter is v6's, since nothing else changed.
v6 took four points from the automated review and declined two. The
first of the four is a deadlock, and it is worth saying how it got
there, because neither change that made it was wrong on its own.
v3 added a mutex around each write, so two writers could not reach the
device in one order and the cache in the other. v4 added a resume-time
write of the source selectors, since the device never reports them and
nothing else would restore them. Together they close a loop: a write on
a runtime-suspended device takes the mutex, calls into the device, and
waking it runs this driver's own resume callback on the same thread --
which takes the same mutex, held by the caller. The order is now the
other way round: the device is woken outside the lock, so a wake that
runs the resume callback finds nothing held.
The other three:
- A URB that completes with an error is resubmitted unless the error
means the URB or the device is gone. Bus noise gives -EPROTO and
-EILSEQ, and stopping on those left the card silent until it was
replugged. This is what snd_usb_mixer_status_complete() does a few
hundred lines away.
- The resume path now forbids I/O reclaim for everything under it,
not only for the frame buffer: usb_interrupt_msg() allocates a URB
of its own with GFP_KERNEL, so a polite flag on our allocation
settles nothing by itself.
- The claimed interface is held with a reference. Claiming does not
keep it alive, and this driver hands the pointer back to the core
when the card goes away.
Declined, for the third time and with the same reasoning the v4 cover
letter gave: a control callback cannot dereference a freed private
structure during disconnect. snd_ctl_elem_read() and
snd_ctl_elem_write() take snd_power_ref_and_wait(card) around the
callback; snd_card_disconnect() ends with snd_power_sync_ref(card),
which waits until every such reference is dropped; and in usb-audio's
disconnect, snd_card_disconnect() runs before
usb_audio_disconnect_components() reaches this driver's private_free().
There is a second reason not to do it anyway: taking the shutdown lock
in a get would wake a runtime-suspended device in order to read a
number this driver already has in memory.
The path was exercised. It needs the card in runtime suspend at the
moment a control is written, which does not happen by itself here: the
driver's own keepalive writes every two seconds and the default
autosuspend delay is also two thousand milliseconds, so the timer never
expires. With that delay set to zero the card suspends between
keepalives, and a control write then returns at once with the value
set. I did not go back to v5 to watch it hang.
Tested on the hardware as before: values arrive by themselves after
probe, a front panel knob reaches the driver ten minutes later and
after a suspend and resume cycle, a write reaches the analogue stage
(one source recorded at gain 30 and at gain 60 differs by 30.4 dB
against the 30.0 dB the taper table predicts), the audible selector
test passes, unbind and bind again works, and alsactl stores and
restores these controls without complaint. On a KASAN and lockdep
kernel; no reports.
The questions from the v2 cover letter still stand: whether
snd-usb-audio registering the hid_driver itself would be a better shape
than either road posted, and whether there is a convention for a
control that can be written but not read.
Mikhail Gavrilov (2):
ALSA: usb-audio: expose the Topping M62's analogue gains as mixer
controls
ALSA: usb-audio: let the M62's outputs say what they listen to
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Kconfig | 1 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 816 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
10 files changed, 864 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: 66498c75b4f8017f62d720d9b59675bdf3abce91
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls
2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-08-25 11:12 ` Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-26 18:06 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-25 11:12 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
The M62 (152a:875c) keeps its analogue input gains and its output
volumes behind a vendor protocol on a HID-class interface, and exposes
none of them through UAC. What UAC does offer on the capture side is a
digital trim after the converter, which cannot buy signal-to-noise:
raising it lifts the converter's own floor along with the signal. A
noise-floor ladder against the card shows exactly that, so on Linux
today the one knob worth setting is the one that cannot be reached, and
a measurement has to begin by asking a human to touch the front panel.
The protocol was read off the vendor application's traffic, the way
mixer_scarlett2.c describes reading Focusrite's. Frames are fifteen
bytes -- start magic, a constant, a target, a property, a signed 32-bit
big-endian value, CRC-16/MODBUS over the middle stored big-endian, end
magic -- and rebuilding all 2619 captured frames from that description
reproduces them byte for byte. The device says nothing until it is
subscribed; one write starts the stream, a second makes it announce its
whole state, after which every change arrives unsolicited, including a
front panel press. So the controls are populated by asking rather than
by caching what was written, which matters here because the vendor
application on another host pushes its own cached state onto the card
on connect.
The control pipe cannot carry this: GET_REPORT and SET_REPORT stall
with EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route and this driver has to own that interface.
hid_ignore_list keeps usbhid away. Nothing is lost by that: the report
descriptor the device offers is a fig leaf -- a Generic Desktop
application collection, eight unnamed usages, sixteen bytes in and out,
no report ID -- so hid-generic can only make a nonexistent mouse of it.
The controls are a table: a name, the target and property that carry
the knob, the second target that must be written in step with it, the
range and the scale. Adding a knob is adding a row. Six rows here --
the two microphone preamps in whole decibels, AUX and Bluetooth on the
input side, headphone and OTG on the output side -- and the outputs
come in pairs because the device answers on only one of each pair and
the other would drift away unheard.
The two volume tapers are measured, not guessed: index 0 is mute, index
99 the maximum, the step is 0.5 dB above -10 dB and 1 dB below it, and
the family that must cover 97 dB in 98 steps takes 2 dB below -52 dB as
well. Both express as DB_RANGE. The microphone preamps are ordinary
1 dB steps from 0 to 88.
One thing a mixer quirk cannot do for itself: usb_audio_driver is
private to card.c, so claiming an interface the audio class knows
nothing about needs a helper there. snd_usb_claim_iface() is that
helper, and it is the only change outside the new file and its
dispatch.
Seven rows. OTG IN was the one gap when this was first posted -- it has
no front panel control, so it never announced itself and its property
was unknown; a capture of the vendor application moving it named it as
target 0x27, and its taper is the same family as Bluetooth, confirmed
by the indices the application dwelt on matching the decibels it
displayed.
The subscription lapses, so it is renewed: the vendor application
repeats the same subscribe every two seconds for as long as it runs,
and a device that hears nothing stops reporting. A listener that
subscribed once got the meters and the identification block and then
very little; one that kept repeating got the gains too, about five
seconds in. Nothing in the frame says "keep alive" and nothing
acknowledges it -- it is the subscribe again -- so a plain periodic
write does it. Two seconds is what the vendor uses; the device
presumably tolerates longer, but there is no reason to find the edge.
The device is reached under the shutdown lock, the way the rest of this
directory reaches hardware: the teardown waits for everyone holding it
before the card is taken apart, so nothing here can be talking to a
device that has gone. A mutex spans each write from the comparison to
the cache update, so two writers cannot arrive at the device in one
order and at the cache in the other.
Suspend and resume are handled rather than survived. The URB does not
outlive a system sleep, and a device that has heard nothing for a while
stops reporting anyway, so the resume path resubmits, subscribes again
and asks for the state -- which also refreshes a cache that may have
gone stale while the panel was reachable and this driver was not.
The claimed interface is given back, on the error path and at teardown
alike, so that unbinding and binding again works instead of failing at
the claim.
Allocation on the resume path asks for no I/O, the way the mixer core
does beside it, since reclaim there can wait on a block device that has
not woken yet; and a URB that usb_kill_urb() has already refused is not
reported as a failure, since -EPERM at that point is the ordinary sound
of an unplug.
The device is woken before the write lock is taken, not after. Waking it
can run the resume callback on the calling thread, and that callback
writes to the device too; entering the lock first would meet it already
held, by the same thread, with nothing able to release it.
Waking is the caller's business, not the frame writer's. A write asked
for by a hand wakes what is asleep; the keepalive and the resume path
do not, the first because a sleeping device has no subscription worth
renewing -- resume renews it -- and the second because it is the
resume. When the keepalive woke the device itself, a runtime suspend
arriving at the same moment waited in cancel_delayed_work_sync() for a
worker that was in turn waiting for that suspend to finish. It also
means the card can now reach runtime suspend at all, which a write
every two seconds had prevented.
Bus noise does not end the listening. A frame that arrives with -EPROTO
or -EILSEQ is dropped and the URB goes back; only the three statuses
that mean the URB or the device is gone stop it, which is what the
mixer's own status handler does next door.
The resume path forbids I/O reclaim for everything under it, not just
for the frame buffer: usb_interrupt_msg() allocates a URB of its own
with GFP_KERNEL, so a polite flag on our own allocation settles
nothing.
And the claimed interface is held with a reference of its own. Claiming
does not keep it alive -- on unplug the core deletes and puts every
interface -- while this driver hands the pointer back to the core when
the card goes away.
SND_USB_AUDIO selects CRC16, since the frames are checked with it and
nothing else in the directory pulled it in.
The cache takes a new value before the write reaches the device rather
than after. The lock cannot be held across a send, and a hand on the
front panel during that window produces a notification the URB handler
stores; updating afterwards would throw that away and leave the driver
claiming a value the device had already moved away from. Written first,
the device's own report is simply the last word. A failed write puts
the old value back, unless the device has spoken in the meantime.
The keepalive stops renewing a subscription nobody is listening to: a
write that fails because the device has gone ends the cycle instead of
scheduling another two seconds later. Those two errors are also no
longer logged, being an unplug rather than a fault.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 2 +
sound/usb/Kconfig | 1 +
sound/usb/Makefile | 1 +
sound/usb/card.c | 19 ++
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 654 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
sound/usb/usbaudio.h | 4 +
10 files changed, 702 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 549df316f487..f5b0bd36be22 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27497,6 +27497,12 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 MIXER DRIVER
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+S: Maintained
+F: sound/usb/mixer_topping.*
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..3c156d1420d1 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -981,6 +981,8 @@ static const struct hid_device_id hid_ignore_list[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_WTP) },
{ HID_USB_DEVICE(USB_VENDOR_ID_SYNAPTICS, USB_DEVICE_ID_SYNAPTICS_DPAD) },
#endif
+ /* the M62's vendor control channel, driven by snd-usb-audio */
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
{ HID_USB_DEVICE(USB_VENDOR_ID_YEALINK, USB_DEVICE_ID_YEALINK_P1K_P4K_B2K) },
{ HID_USB_DEVICE(USB_VENDOR_ID_QUANTA, USB_DEVICE_ID_QUANTA_HP_5MP_CAMERA_5473) },
{ }
diff --git a/sound/usb/Kconfig b/sound/usb/Kconfig
index b4588915efa1..5af29599e494 100644
--- a/sound/usb/Kconfig
+++ b/sound/usb/Kconfig
@@ -17,6 +17,7 @@ config SND_USB_AUDIO
select SND_PCM
select SND_UMP if SND_USB_AUDIO_MIDI_V2
select BITREVERSE
+ select CRC16
select SND_USB_AUDIO_USE_MEDIA_CONTROLLER if MEDIA_CONTROLLER && (MEDIA_SUPPORT=y || MEDIA_SUPPORT=SND_USB_AUDIO)
help
Say Y here to include support for USB audio and USB MIDI
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/card.c b/sound/usb/card.c
index 24112e491779..191391822092 100644
--- a/sound/usb/card.c
+++ b/sound/usb/card.c
@@ -325,6 +325,25 @@ static int snd_usb_create_stream(struct snd_usb_audio *chip, int ctrlif, int int
return 0;
}
+/*
+ * Claim an interface of this device for snd-usb-audio.
+ *
+ * A mixer quirk may need an interface the audio class knows nothing
+ * about -- a vendor control channel that happens to wear the HID class,
+ * for instance -- and cannot claim it itself, because usb_audio_driver
+ * is private to this file.
+ */
+int snd_usb_claim_iface(struct snd_usb_audio *chip, struct usb_interface *iface)
+{
+ return usb_driver_claim_interface(&usb_audio_driver, iface,
+ USB_AUDIO_IFACE_UNUSED);
+}
+
+void snd_usb_release_iface(struct usb_interface *iface)
+{
+ usb_driver_release_interface(&usb_audio_driver, iface);
+}
+
/*
* parse audio control descriptor and create pcm/midi streams
*/
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..933a8bbe57b9
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,654 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Mixer controls for Topping interfaces behind a vendor HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * Note that the control pipe is not an option here: GET_REPORT and
+ * SET_REPORT both stall with EPIPE for every report type, so the
+ * interrupt endpoints on the HID interface are the only route and this
+ * driver has to own that interface. hid_ignore_list keeps usbhid off
+ * it; the report descriptor it would bind to describes nothing anyway
+ * (a Generic Desktop application collection with eight unnamed usages
+ * and no report ID), so no HID functionality is lost.
+ */
+
+#include <linux/crc16.h>
+#include <linux/unaligned.h>
+#include <linux/init.h>
+#include <linux/cleanup.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/usb.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "mixer_topping.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+#define TOPPING_EP_BUF 64 /* the endpoints' packet size */
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+struct topping_mixer {
+ struct usb_mixer_interface *mixer;
+ struct usb_interface *iface;
+ bool claimed; /* iface is ours to give back */
+ const struct topping_ctl_desc *ctls;
+ int num_ctls;
+ struct urb *urb;
+ u8 *inbuf;
+ dma_addr_t inbuf_dma;
+ unsigned int pipe_in, pipe_out;
+ int interval;
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against the URB */
+ int *val;
+ struct snd_kcontrol **kctl;
+};
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes the shutdown lock
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has
+ * no subscription worth renewing -- resume renews it -- and the
+ * second because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ */
+static int topping_send(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet. The frame is fifteen bytes; nothing is lost by
+ * asking for it without I/O.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_EP_BUF, GFP_NOIO);
+ int err, actual;
+
+ if (!buf)
+ return -ENOMEM;
+
+ topping_build(buf, target, prop, value);
+ err = usb_interrupt_msg(tm->mixer->chip->dev, tm->pipe_out,
+ buf, TOPPING_FRAME_LEN, &actual, 1000);
+ /* ENODEV and ESHUTDOWN are an unplug, not a fault worth a line */
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(struct topping_mixer *tm, u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < tm->num_ctls; i++)
+ if (tm->ctls[i].target == target && tm->ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+static void topping_urb_complete(struct urb *urb)
+{
+ struct topping_mixer *tm = urb->context;
+ const u8 *f = urb->transfer_buffer;
+ int idx, value, err;
+ bool changed;
+
+ /*
+ * Bus noise gives -EPROTO and -EILSEQ, and those are not the end
+ * of anything: the frame is dropped and the listening goes on.
+ * Only the three that mean the URB or the device is gone stop it,
+ * which is what the mixer's own status handler does next door.
+ */
+ if (urb->status)
+ goto resubmit;
+ if (urb->actual_length < TOPPING_FRAME_LEN)
+ goto resubmit;
+ if (f[0] != 0x22 || f[1] != 0x33 || f[13] != 0x66 || f[14] != 0x77)
+ goto resubmit;
+ if (get_unaligned_be16(f + 11) != crc16(0xffff, f + 2, 9))
+ goto resubmit;
+
+ idx = topping_index_of(tm, f[5], f[6]);
+ if (idx < 0)
+ goto resubmit; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(f + 7);
+ if (value < tm->ctls[idx].min || value > tm->ctls[idx].max)
+ goto resubmit;
+
+ changed = false;
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[idx] != value) {
+ tm->val[idx] = value;
+ changed = true;
+ }
+ }
+
+ if (changed && tm->kctl[idx])
+ snd_ctl_notify(tm->mixer->chip->card,
+ SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+
+resubmit:
+ if (urb->status == -ENOENT || urb->status == -ECONNRESET ||
+ urb->status == -ESHUTDOWN)
+ return;
+ err = usb_submit_urb(urb, GFP_ATOMIC);
+ /*
+ * ENODEV, ESHUTDOWN and EPERM are the sound of an unplug -- the
+ * last being a URB that usb_kill_urb() has already refused.
+ */
+ if (err < 0 && err != -ENODEV && err != -ESHUTDOWN && err != -EPERM)
+ usb_audio_err(tm->mixer->chip,
+ "Topping: cannot resubmit: %d\n", err);
+}
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ keepalive.work);
+ int err;
+
+ err = topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; there is nothing to renew */
+
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ int idx = elem->control;
+
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = tm->ctls[idx].min;
+ uinfo->value.integer.max = tm->ctls[idx].max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(spinlock_irqsave)(&tm->lock);
+ ucontrol->value.integer.value[0] = tm->val[elem->control];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct usb_mixer_interface *mixer = elem->head.mixer;
+ struct topping_mixer *tm = mixer->private_data;
+ const struct topping_ctl_desc *d = &tm->ctls[elem->control];
+ int value, prev, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < d->min || value > d->max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run the
+ * resume callback on this very thread, and that callback takes
+ * write_lock to write the selectors back; taking write_lock first
+ * would meet it already held, by us. So the device is woken
+ * first and the lock is entered with nothing left to run under
+ * it. This is also the wake topping_send() relies on, since it
+ * does none of its own.
+ */
+ CLASS(snd_usb_lock, pm)(mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&tm->write_lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after. The
+ * lock cannot be held across a send, and a hand on the front
+ * panel during that window produces a notification the URB
+ * handler stores; updating afterwards would throw that away and
+ * leave the driver claiming a value the device had already moved
+ * away from. Written first, the device's own report is simply
+ * the last word, which is the right bias.
+ */
+ scoped_guard(spinlock_irqsave, &tm->lock) {
+ if (tm->val[elem->control] == value)
+ return 0;
+ prev = tm->val[elem->control];
+ tm->val[elem->control] = value;
+ }
+
+ err = topping_send(tm, d->target, d->prop, value);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(tm, d->target_pair, d->prop, value);
+ }
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &tm->lock)
+ if (tm->val[elem->control] == value)
+ tm->val[elem->control] = prev;
+ return err;
+ }
+
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+static int topping_add_ctl(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+ int err;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_ctl, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ kctl->tlv.p = tm->ctls[idx].tlv;
+ strscpy(kctl->id.name, tm->ctls[idx].name, sizeof(kctl->id.name));
+
+ err = snd_usb_mixer_add_control(&elem->head, kctl);
+ if (err < 0)
+ return err;
+
+ tm->kctl[idx] = kctl;
+ return 0;
+}
+
+static void topping_suspend(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb)
+ usb_kill_urb(tm->urb);
+}
+
+static int topping_resume(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+ unsigned int noio;
+ int err;
+
+ if (!tm)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usb_interrupt_msg()
+ * allocates a URB of its own with GFP_KERNEL, so asking for the
+ * frame buffer politely is not enough, and reclaim here can wait
+ * on a block device that has not woken yet.
+ */
+ noio = memalloc_noio_save();
+
+ if (tm->urb) {
+ err = usb_submit_urb(tm->urb, GFP_NOIO);
+ if (err < 0) {
+ memalloc_noio_restore(noio);
+ return err;
+ }
+ }
+
+ /*
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+ cancel_delayed_work_sync(&tm->keepalive);
+ if (tm->urb) {
+ usb_kill_urb(tm->urb);
+ usb_free_coherent(mixer->chip->dev, TOPPING_EP_BUF,
+ tm->inbuf, tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ }
+ if (tm->claimed) {
+ snd_usb_release_iface(tm->iface);
+ usb_put_intf(tm->iface);
+ }
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ mixer->private_data = NULL;
+}
+
+/* the HID interface, by class rather than by a number in a comment */
+static struct usb_interface *topping_find_iface(struct snd_usb_audio *chip,
+ int *ep_in, int *ep_out,
+ int *interval)
+{
+ struct usb_device *dev = chip->dev;
+ struct usb_host_interface *alts;
+ struct usb_interface *iface;
+ int i, e;
+
+ for (i = 0; i < 256; i++) {
+ iface = usb_ifnum_to_if(dev, i);
+ if (!iface)
+ continue;
+ alts = &iface->altsetting[0];
+ if (alts->desc.bInterfaceClass != USB_CLASS_HID)
+ continue;
+ *ep_in = *ep_out = 0;
+ for (e = 0; e < alts->desc.bNumEndpoints; e++) {
+ struct usb_endpoint_descriptor *ep;
+
+ ep = &alts->endpoint[e].desc;
+ if (!usb_endpoint_xfer_int(ep))
+ continue;
+ if (usb_endpoint_dir_in(ep)) {
+ *ep_in = usb_endpoint_num(ep);
+ *interval = ep->bInterval;
+ } else {
+ *ep_out = usb_endpoint_num(ep);
+ }
+ }
+ if (*ep_in && *ep_out)
+ return iface;
+ }
+ return NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct usb_interface *iface;
+ struct topping_mixer *tm;
+ int ep_in = 0, ep_out = 0, interval = 5;
+ int i, err;
+
+ iface = topping_find_iface(chip, &ep_in, &ep_out, &interval);
+ if (!iface) {
+ usb_audio_err(chip, "Topping: no vendor HID interface\n");
+ return 0; /* not fatal: the card still plays */
+ }
+ if (usb_interface_claimed(iface)) {
+ usb_audio_err(chip,
+ "Topping: the HID interface is already claimed\n");
+ return 0;
+ }
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return -ENOMEM;
+
+ tm->mixer = mixer;
+ tm->iface = iface;
+ tm->ctls = topping_m62_ctls;
+ tm->num_ctls = ARRAY_SIZE(topping_m62_ctls);
+ tm->pipe_in = usb_rcvintpipe(chip->dev, ep_in);
+ tm->pipe_out = usb_sndintpipe(chip->dev, ep_out);
+ tm->interval = interval;
+ spin_lock_init(&tm->lock);
+ mutex_init(&tm->write_lock);
+ INIT_DELAYED_WORK(&tm->keepalive, topping_keepalive);
+
+ tm->val = kcalloc(tm->num_ctls, sizeof(*tm->val), GFP_KERNEL);
+ tm->kctl = kcalloc(tm->num_ctls, sizeof(*tm->kctl), GFP_KERNEL);
+ if (!tm->val || !tm->kctl) {
+ err = -ENOMEM;
+ goto fail;
+ }
+
+ err = snd_usb_claim_iface(chip, iface);
+ if (err < 0)
+ goto fail;
+ tm->claimed = true;
+ /*
+ * Claiming does not keep the interface alive. On unplug the core
+ * deletes and puts every interface, and this pointer is handed
+ * back to it later, so it holds a reference of its own until then.
+ */
+ usb_get_intf(iface);
+
+ tm->urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!tm->urb) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ tm->inbuf = usb_alloc_coherent(chip->dev, TOPPING_EP_BUF, GFP_KERNEL,
+ &tm->inbuf_dma);
+ if (!tm->inbuf) {
+ err = -ENOMEM;
+ goto fail;
+ }
+ usb_fill_int_urb(tm->urb, chip->dev, tm->pipe_in,
+ tm->inbuf, TOPPING_EP_BUF,
+ topping_urb_complete, tm, tm->interval);
+ tm->urb->transfer_dma = tm->inbuf_dma;
+ tm->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+ mixer->private_suspend = topping_suspend;
+ mixer->private_resume = topping_resume;
+
+ for (i = 0; i < tm->num_ctls; i++) {
+ err = topping_add_ctl(tm, i);
+ if (err < 0)
+ return err; /* private_free cleans up */
+ }
+
+ err = usb_submit_urb(tm->urb, GFP_KERNEL);
+ if (err < 0) {
+ usb_audio_err(chip, "Topping: cannot listen: %d\n", err);
+ return err;
+ }
+
+ /*
+ * Subscribe, then ask for the state. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through the URB
+ * and notifies its own control.
+ */
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&tm->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+fail:
+ if (tm->claimed) {
+ snd_usb_release_iface(iface);
+ usb_put_intf(iface);
+ }
+ if (tm->inbuf)
+ usb_free_coherent(chip->dev, TOPPING_EP_BUF, tm->inbuf,
+ tm->inbuf_dma);
+ usb_free_urb(tm->urb);
+ kfree(tm->val);
+ kfree(tm->kctl);
+ kfree(tm);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
diff --git a/sound/usb/usbaudio.h b/sound/usb/usbaudio.h
index c49709d7ad25..16b8c6a8031c 100644
--- a/sound/usb/usbaudio.h
+++ b/sound/usb/usbaudio.h
@@ -80,6 +80,10 @@ struct snd_usb_audio {
#define USB_AUDIO_IFACE_UNUSED ((void *)-1L)
+int snd_usb_claim_iface(struct snd_usb_audio *chip,
+ struct usb_interface *iface);
+void snd_usb_release_iface(struct usb_interface *iface);
+
#define usb_audio_err(chip, fmt, args...) \
dev_err(&(chip)->dev->dev, fmt, ##args)
#define usb_audio_err_ratelimited(chip, fmt, args...) \
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to
2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
@ 2026-08-25 11:12 ` Mikhail Gavrilov
2026-08-26 18:06 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-25 11:12 UTC (permalink / raw)
To: tiwai
Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel,
Mikhail Gavrilov
Each output on this card has a source selector: it can take one of the
three internal mixes, or any input, or any playback bus straight from
USB. Which it is decides everything downstream -- point the headphones
at a playback bus and the card's mixer leaves the path entirely, along
with every question about what is summed into it.
That matters more here than it would elsewhere. There is no control
panel for the M62 on Linux, so a user who never runs the vendor's
application on another machine has no way to see or change this, and
inherits whatever the card was last told. One enumerated control per
output gives them the whole choice, and it does so without exposing the
sixty-cell mixer matrix, which without a graphical representation would
confuse far more than it helps.
The item list has "Unknown" first, and it is deliberate rather than
tidy: THE DEVICE NEVER REPORTS A SELECTOR. Not to this driver, and not
to the vendor's own application, which on connect pushes its entire
workspace to the card rather than asking it anything. So the current
setting cannot be learned at probe, and saying so is the only honest
thing a control can do until a hand has chosen. Selecting "Unknown" is
refused, since it is a report and not a choice.
The numbering the card uses has a hole where 4 and 5 would be, so the
item index and the value written are kept as separate tables rather
than one being computed from the other.
Writing "Unknown" changes nothing and says so quietly. It is what the
control reports until a hand has chosen, and alsactl stores and
restores it like any other value, so refusing it would fail a restore
of the driver's own report -- once at every boot, and again whenever a
saved state is put back over a chosen one.
The choice is written again on resume. The gains need no such help --
the device announces them and the cache re-syncs by itself -- but a
selector is never reported, so if the card came up on its own defaults
while the host slept, the driver's idea of it would be silently wrong
and writing the remembered value back would look like no change at all.
This write wakes the device before taking the lock for the same reason
the gain controls do: the wake can run the resume callback on this
thread, and that callback writes the selectors back under the same
lock.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 162 ++++++++++++++++++++++++++++++++++++++
1 file changed, 162 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 933a8bbe57b9..7d6ba7257807 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -127,6 +127,45 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
topping_tlv_out_0 },
};
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+
struct topping_mixer {
struct usb_mixer_interface *mixer;
struct usb_interface *iface;
@@ -143,6 +182,7 @@ struct topping_mixer {
spinlock_t lock; /* guards val[] against the URB */
int *val;
struct snd_kcontrol **kctl;
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
};
static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
@@ -387,6 +427,71 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
return 1;
}
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+
+ guard(mutex)(&tm->write_lock);
+ ucontrol->value.enumerated.item[0] = tm->sel[elem->control];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct usb_mixer_elem_info *elem = kctl->private_data;
+ struct topping_mixer *tm = elem->head.mixer->private_data;
+ const struct topping_enum_desc *d;
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ CLASS(snd_usb_lock, pm)(elem->head.mixer->chip);
+ if (pm.err < 0)
+ return -EIO;
+
+ guard(mutex)(&tm->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has chosen,
+ * and alsactl stores and restores it like any other value. It is
+ * not a choice, so writing it changes nothing -- quietly, rather
+ * than failing a restore of the driver's own report.
+ */
+ if (!item || tm->sel[elem->control] == item)
+ return 0;
+
+ d = &topping_m62_enums[elem->control];
+ err = topping_send(tm, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ tm->sel[elem->control] = item;
+ return 1;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
static const struct snd_kcontrol_new topping_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
@@ -429,6 +534,57 @@ static int topping_add_ctl(struct topping_mixer *tm, int idx)
return 0;
}
+static int topping_add_sel(struct topping_mixer *tm, int idx)
+{
+ struct usb_mixer_elem_info *elem;
+ struct snd_kcontrol *kctl;
+
+ elem = kzalloc_obj(*elem);
+ if (!elem)
+ return -ENOMEM;
+
+ elem->head.mixer = tm->mixer;
+ elem->head.id = 0;
+ elem->control = idx;
+ elem->channels = 1;
+ elem->val_type = USB_MIXER_BESPOKEN;
+
+ kctl = snd_ctl_new1(&topping_sel, elem);
+ if (!kctl) {
+ kfree(elem);
+ return -ENOMEM;
+ }
+ kctl->private_free = snd_usb_mixer_elem_free;
+ strscpy(kctl->id.name, topping_m62_enums[idx].name,
+ sizeof(kctl->id.name));
+
+ return snd_usb_mixer_add_control(&elem->head, kctl);
+}
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again
+ * -- which is what the mixer core does for every control that is not
+ * marked as the driver's own to handle.
+ */
+static void topping_restore_sel(struct topping_mixer *tm)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&tm->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!tm->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(tm, d->target, d->prop,
+ topping_source_value[tm->sel[i]]);
+ }
+}
+
static void topping_suspend(struct usb_mixer_interface *mixer)
{
struct topping_mixer *tm = mixer->private_data;
@@ -473,6 +629,7 @@ static int topping_resume(struct usb_mixer_interface *mixer)
*/
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
topping_send(tm, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(tm);
schedule_delayed_work(&tm->keepalive,
msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
memalloc_noio_restore(noio);
@@ -618,6 +775,11 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
if (err < 0)
return err; /* private_free cleans up */
}
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_sel(tm, i);
+ if (err < 0)
+ return err;
+ }
err = usb_submit_urb(tm->urb, GFP_KERNEL);
if (err < 0) {
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
@ 2026-08-26 18:06 ` Mikhail Gavrilov
2026-09-03 8:30 ` Takashi Iwai
2 siblings, 1 reply; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-08-26 18:06 UTC (permalink / raw)
To: tiwai; +Cc: perex, jikos, bentiss, linux-sound, linux-input, linux-kernel
Found on my own bench while chasing something else, and I would rather
bring it than have it found for me.
The problem, as a user could see it: after unbinding the vendor
interface by hand, the driver keeps writing to the card. Reads stop --
the control freezes on its last announced value -- while writes go on
reaching the hardware, and the two-second keepalive presumably goes on
with them, into an interface the driver no longer owns.
# echo 3-1.3:1.4 > /sys/bus/usb/drivers/snd-usb-audio/unbind
# amixer -c M62 cget name='Mic-1 Analog Capture Volume' # frozen
# amixer -c M62 cset name='Mic-1 Analog Capture Volume' 50
... and the gain really moves on the card.
The sound card itself survives the unbind untouched, which is right and
is the good half of the result.
My reading of why, and I would be glad to be corrected on it. The quirk
claims that interface with usb_driver_claim_interface() and marks it
USB_AUDIO_IFACE_UNUSED, so usb_audio_disconnect() returns at its first
line and nothing ever tells the quirk to wind down. Meanwhile
usb_interrupt_msg() takes a struct usb_device and an endpoint address,
not an interface, so losing the claim costs the driver nothing on the
write path; the read path dies only because usbcore kills the URBs on
the interface being unbound.
So the claim is what keeps usbhid away, not what grants the right to
write, and the two are easy to conflate -- I had conflated them.
How reachable this is: only by hand from sysfs. A plain unplug takes
the whole device, and there disconnect runs on the audio interfaces and
the quirk is freed with the mixer. I have not found a path that reaches
it in ordinary use.
How I plan to solve it, and this is where I need your word, because the
change is in card.c rather than in my own file. An interface the quirk
claimed is marked exactly like one nobody wanted, and those two are
different things: the first has a driver behind it that should be told
when it goes away. The shapes I can see are
(a) let the quirk register a small teardown callback at claim time
and have usb_audio_disconnect() run it before the early return,
(b) give the claimed-and-used case its own sentinel instead of
USB_AUDIO_IFACE_UNUSED, so disconnect can tell them apart,
(c) leave card.c alone and have the quirk take a usb_device
reference plus its own notifier, which keeps the fix inside
sound/usb/mixer_topping.c at the cost of a second path watching
the same event.
I lean to (b) as the smallest honest change, but this is your file and
the sentinel is your convention.
Whichever you prefer, I would rather send it as a follow-up once the
current series lands than fold it into v9: it is a separate defect,
it touches a path shared by every quirk, and stirring it into a series
under review would make both harder to read.
Mikhail
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-08-26 18:06 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
@ 2026-09-03 8:30 ` Takashi Iwai
2026-09-03 9:35 ` Mikhail Gavrilov
0 siblings, 1 reply; 53+ messages in thread
From: Takashi Iwai @ 2026-09-03 8:30 UTC (permalink / raw)
To: Mikhail Gavrilov
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel
On Wed, 26 Aug 2026 20:06:07 +0200,
Mikhail Gavrilov wrote:
>
> Found on my own bench while chasing something else, and I would rather
> bring it than have it found for me.
>
> The problem, as a user could see it: after unbinding the vendor
> interface by hand, the driver keeps writing to the card. Reads stop --
> the control freezes on its last announced value -- while writes go on
> reaching the hardware, and the two-second keepalive presumably goes on
> with them, into an interface the driver no longer owns.
>
> # echo 3-1.3:1.4 > /sys/bus/usb/drivers/snd-usb-audio/unbind
> # amixer -c M62 cget name='Mic-1 Analog Capture Volume' # frozen
> # amixer -c M62 cset name='Mic-1 Analog Capture Volume' 50
> ... and the gain really moves on the card.
>
> The sound card itself survives the unbind untouched, which is right and
> is the good half of the result.
>
> My reading of why, and I would be glad to be corrected on it. The quirk
> claims that interface with usb_driver_claim_interface() and marks it
> USB_AUDIO_IFACE_UNUSED, so usb_audio_disconnect() returns at its first
> line and nothing ever tells the quirk to wind down. Meanwhile
> usb_interrupt_msg() takes a struct usb_device and an endpoint address,
> not an interface, so losing the claim costs the driver nothing on the
> write path; the read path dies only because usbcore kills the URBs on
> the interface being unbound.
>
> So the claim is what keeps usbhid away, not what grants the right to
> write, and the two are easy to conflate -- I had conflated them.
>
> How reachable this is: only by hand from sysfs. A plain unplug takes
> the whole device, and there disconnect runs on the audio interfaces and
> the quirk is freed with the mixer. I have not found a path that reaches
> it in ordinary use.
>
> How I plan to solve it, and this is where I need your word, because the
> change is in card.c rather than in my own file. An interface the quirk
> claimed is marked exactly like one nobody wanted, and those two are
> different things: the first has a driver behind it that should be told
> when it goes away. The shapes I can see are
>
> (a) let the quirk register a small teardown callback at claim time
> and have usb_audio_disconnect() run it before the early return,
>
> (b) give the claimed-and-used case its own sentinel instead of
> USB_AUDIO_IFACE_UNUSED, so disconnect can tell them apart,
>
> (c) leave card.c alone and have the quirk take a usb_device
> reference plus its own notifier, which keeps the fix inside
> sound/usb/mixer_topping.c at the cost of a second path watching
> the same event.
>
> I lean to (b) as the smallest honest change, but this is your file and
> the sentinel is your convention.
>
> Whichever you prefer, I would rather send it as a follow-up once the
> current series lands than fold it into v9: it is a separate defect,
> it touches a path shared by every quirk, and stirring it into a series
> under review would make both harder to read.
Thinking more on this, I see another possibility. Namely, create an
individual HID driver like your previous plan 2, but instead of
creating an own snd_card object, use the component framework
(include/linux/component.h) for binding between the audio and the HID
drivers.
That is, in USB-audio side, you'll have a mixer quirk to add the
component master. In the HID driver, you'll register a component to
be bound. In the bind callback, the snd_card object is passed from
the USB-audio component master, and you can instantiate the mixer
kcontrols there.
At disconnection of USB-audio, the mixer quirk calls the component
unbind, and the HID component unbind callback frees the created
kcontrols in turn. If the HID is unbound first, it just unregister
the component (and frees the kcontrols). If the HID is re-bound, it
just re-adds the kcontrols at its bind callback again.
The component match might be a bit tricky, but it should be certainly
feasible.
thanks,
Takashi
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-09-03 8:30 ` Takashi Iwai
@ 2026-09-03 9:35 ` Mikhail Gavrilov
2026-09-03 10:02 ` Takashi Iwai
0 siblings, 1 reply; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-03 9:35 UTC (permalink / raw)
To: tiwai; +Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel
On Thu, 03 Sep 2026 10:30:45 +0200, Takashi Iwai wrote:
>
> Thinking more on this, I see another possibility. Namely, create an
> individual HID driver like your previous plan 2, but instead of
> creating an own snd_card object, use the component framework
> (include/linux/component.h) for binding between the audio and the HID
> drivers.
Thank you -- and this answers more than the mail it replies to. The v2
cover letter asked whether snd-usb-audio registering the hid_driver
itself would be a better shape than either road posted, and the question
has been repeated in every letter since. It is answered now, and the
answer is neither of the two roads I had drawn.
I will rebuild the series in this shape. What follows is the plan and
the three things I could not settle by reading, so that they are asked
before the code is written rather than after.
What the shape becomes. A new drivers/hid/hid-topping-m62.c owns the
vendor interface the ordinary way and speaks the protocol; it registers
a component in probe. sound/usb/mixer_topping.c keeps only
snd_topping_init(), which allocates a small context in devres on the
audio control interface, adds one match and registers the master. The
master's bind calls component_bind_all() with the snd_card; the HID
side creates the kcontrols there and drops them in unbind. I took
sound/hda/core/component.c as the model, including devres_find() keyed
on the release function to recover the master's context, since drvdata
on a usb_interface is snd-usb-audio's own.
What that deletes. snd_usb_claim_iface() and snd_usb_release_iface()
in card.c go, and with them the only change this series made outside
its own files; usb_driver_claim_interface(), the interface reference,
the "claimed" bookkeeping and the search for the HID interface by class
go with them; and the hid_ignore_list entry goes, because the device
now has a driver of its own. The defect I wrote to you about two weeks
ago goes too: with no claim there is no interface marked
USB_AUDIO_IFACE_UNUSED, so none of the three shapes I offered is needed
and card.c is not touched at all.
Now the three questions.
1. The match.
Neither helper fits. component_compare_dev() compares device pointers
and the audio side has no pointer to the HID device; component_compare_
dev_name() would need "0003:152A:875C.000X", whose instance counter is
not predictable. is_usb_interface() would have made a tidy predicate
but it lives in drivers/usb/core/usb.h, which is private to usbcore.
What I plan instead is a test of descent alone. The HID device sits
two levels below the USB device -- hid_device, usb_interface,
usb_device -- so the master passes &chip->dev->dev as compare_data and
the compare function is
return dev->parent && dev->parent->parent == data;
Which interface it is stays the HID driver's business: it returns
-ENODEV for anything but the vendor interface, so it registers a
component for that one and no other. That keeps sound/usb free of both
HID symbols and any opinion about this card's interface numbering, and
the function is only ever called against devices that registered with
component_add(), so it does not have to defend itself against the wider
device tree.
Is that acceptable, or would you rather the audio side knew which
interface it was looking for?
2. When the controls appear.
try_to_bring_up_aggregate_device() reports an incomplete set as "not
ready" and returns 0, not -EPROBE_DEFER, so a card whose HID module is
absent comes up with no vendor controls and nothing said about it. And
mixer quirks run inside snd_usb_create_mixer(), before
snd_card_register(), so even when both halves are present the controls
are added to a card that is already registered, arriving as add events
some time after the card itself.
Neither is wrong, but both are visible from userspace: a restore can
race the controls into existence, and a missing module looks like a
card that simply has no gains. Would you want a MODULE_SOFTDEP on the
audio side, or is late arrival the expected behaviour for this pattern?
3. Remote wakeup, which is the one I have no good answer to.
usbhid arms every device it opens: usbhid_open() sets
intf->needs_remote_wakeup = 1, and so does usbhid_start() on the
HID_QUIRK_ALWAYS_POLL path, so there is no way to receive input reports
from usbhid without asking for it. This card does not offer it --
bmAttributes is 0xc0, and there is no power/wakeup under its sysfs
node, so device_can_wakeup() is false -- and usb_suspend_both() then
refuses autosuspend for the whole device:
if (w && !device_can_wakeup(&udev->dev))
return -EOPNOTSUPP;
That would take back what v7 fixed. It is worth saying that the
driver's own two-second keepalive already keeps the card awake at the
default autosuspend delay, so the loss is structural rather than
observable today -- but structural is worse.
This driver genuinely does not need remote wakeup, and its own resume
path is the proof: it subscribes again and asks the card for its whole
state, so a knob turned while the host slept is picked up on the way
back. So the smallest thing that works is to clear the flag after
opening, with a comment saying why. The field is not private --
cdc-acm and usbnet both set it directly -- but clearing it from outside
usbhid is unusual enough that I would rather ask than post it.
It also decides a question I was going to raise separately. I had
meant to keep HID_CONNECT_HIDRAW, because a hidraw node is how the
protocol was read in the first place and how the parts this driver does
not expose stay reachable. But a hidraw open calls hid_hw_open() again
and sets the flag back, so clearing it only holds under
HID_CONNECT_DRIVER. If you prefer the flag left alone, hidraw can
stay; if you prefer runtime suspend kept, it cannot.
Would you rather see this as a driver-local clear, as
HID_CONNECT_DRIVER plus the clear, or as something usbhid ought to
offer to drivers that resynchronise on resume?
Whatever you decide on these, the next posting will be the new shape
rather than a v9 of this one, and it will cross into drivers/hid, so I
will send it to both lists. I will mark v8 superseded in patchwork
once it goes out.
--
Mikhail
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-09-03 9:35 ` Mikhail Gavrilov
@ 2026-09-03 10:02 ` Takashi Iwai
2026-09-03 10:19 ` Mikhail Gavrilov
2026-09-04 0:18 ` Mikhail Gavrilov
0 siblings, 2 replies; 53+ messages in thread
From: Takashi Iwai @ 2026-09-03 10:02 UTC (permalink / raw)
To: Mikhail Gavrilov
Cc: tiwai, tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel
On Thu, 03 Sep 2026 11:35:10 +0200,
Mikhail Gavrilov wrote:
>
> On Thu, 03 Sep 2026 10:30:45 +0200, Takashi Iwai wrote:
> >
> > Thinking more on this, I see another possibility. Namely, create an
> > individual HID driver like your previous plan 2, but instead of
> > creating an own snd_card object, use the component framework
> > (include/linux/component.h) for binding between the audio and the HID
> > drivers.
>
> Thank you -- and this answers more than the mail it replies to. The v2
> cover letter asked whether snd-usb-audio registering the hid_driver
> itself would be a better shape than either road posted, and the question
> has been repeated in every letter since. It is answered now, and the
> answer is neither of the two roads I had drawn.
>
> I will rebuild the series in this shape. What follows is the plan and
> the three things I could not settle by reading, so that they are asked
> before the code is written rather than after.
>
> What the shape becomes. A new drivers/hid/hid-topping-m62.c owns the
> vendor interface the ordinary way and speaks the protocol; it registers
> a component in probe. sound/usb/mixer_topping.c keeps only
> snd_topping_init(), which allocates a small context in devres on the
> audio control interface, adds one match and registers the master. The
> master's bind calls component_bind_all() with the snd_card; the HID
> side creates the kcontrols there and drops them in unbind. I took
> sound/hda/core/component.c as the model, including devres_find() keyed
> on the release function to recover the master's context, since drvdata
> on a usb_interface is snd-usb-audio's own.
>
> What that deletes. snd_usb_claim_iface() and snd_usb_release_iface()
> in card.c go, and with them the only change this series made outside
> its own files; usb_driver_claim_interface(), the interface reference,
> the "claimed" bookkeeping and the search for the HID interface by class
> go with them; and the hid_ignore_list entry goes, because the device
> now has a driver of its own. The defect I wrote to you about two weeks
> ago goes too: with no claim there is no interface marked
> USB_AUDIO_IFACE_UNUSED, so none of the three shapes I offered is needed
> and card.c is not touched at all.
>
> Now the three questions.
>
> 1. The match.
>
> Neither helper fits. component_compare_dev() compares device pointers
> and the audio side has no pointer to the HID device; component_compare_
> dev_name() would need "0003:152A:875C.000X", whose instance counter is
> not predictable. is_usb_interface() would have made a tidy predicate
> but it lives in drivers/usb/core/usb.h, which is private to usbcore.
>
> What I plan instead is a test of descent alone. The HID device sits
> two levels below the USB device -- hid_device, usb_interface,
> usb_device -- so the master passes &chip->dev->dev as compare_data and
> the compare function is
>
> return dev->parent && dev->parent->parent == data;
>
> Which interface it is stays the HID driver's business: it returns
> -ENODEV for anything but the vendor interface, so it registers a
> component for that one and no other. That keeps sound/usb free of both
> HID symbols and any opinion about this card's interface numbering, and
> the function is only ever called against devices that registered with
> component_add(), so it does not have to defend itself against the wider
> device tree.
>
> Is that acceptable, or would you rather the audio side knew which
> interface it was looking for?
It's along my rough idea, too. We can simply compare the common
parent USB device in the match function.
> 2. When the controls appear.
>
> try_to_bring_up_aggregate_device() reports an incomplete set as "not
> ready" and returns 0, not -EPROBE_DEFER, so a card whose HID module is
> absent comes up with no vendor controls and nothing said about it. And
> mixer quirks run inside snd_usb_create_mixer(), before
> snd_card_register(), so even when both halves are present the controls
> are added to a card that is already registered, arriving as add events
> some time after the card itself.
>
> Neither is wrong, but both are visible from userspace: a restore can
> race the controls into existence, and a missing module looks like a
> card that simply has no gains. Would you want a MODULE_SOFTDEP on the
> audio side, or is late arrival the expected behaviour for this pattern?
I'm afraid that the softdep is problematic because it'd bring this
always no matter which device is used. In the case of USB-audio, the
state restoration is always racy per design of multiple USB interfaces
(the probe happens multiple times and the instances are added at each
probe).
> 3. Remote wakeup, which is the one I have no good answer to.
>
> usbhid arms every device it opens: usbhid_open() sets
> intf->needs_remote_wakeup = 1, and so does usbhid_start() on the
> HID_QUIRK_ALWAYS_POLL path, so there is no way to receive input reports
> from usbhid without asking for it. This card does not offer it --
> bmAttributes is 0xc0, and there is no power/wakeup under its sysfs
> node, so device_can_wakeup() is false -- and usb_suspend_both() then
> refuses autosuspend for the whole device:
>
> if (w && !device_can_wakeup(&udev->dev))
> return -EOPNOTSUPP;
>
> That would take back what v7 fixed. It is worth saying that the
> driver's own two-second keepalive already keeps the card awake at the
> default autosuspend delay, so the loss is structural rather than
> observable today -- but structural is worse.
>
> This driver genuinely does not need remote wakeup, and its own resume
> path is the proof: it subscribes again and asks the card for its whole
> state, so a knob turned while the host slept is picked up on the way
> back. So the smallest thing that works is to clear the flag after
> opening, with a comment saying why. The field is not private --
> cdc-acm and usbnet both set it directly -- but clearing it from outside
> usbhid is unusual enough that I would rather ask than post it.
>
> It also decides a question I was going to raise separately. I had
> meant to keep HID_CONNECT_HIDRAW, because a hidraw node is how the
> protocol was read in the first place and how the parts this driver does
> not expose stay reachable. But a hidraw open calls hid_hw_open() again
> and sets the flag back, so clearing it only holds under
> HID_CONNECT_DRIVER. If you prefer the flag left alone, hidraw can
> stay; if you prefer runtime suspend kept, it cannot.
>
> Would you rather see this as a driver-local clear, as
> HID_CONNECT_DRIVER plus the clear, or as something usbhid ought to
> offer to drivers that resynchronise on resume?
I think it's rather a question to HID people...
> Whatever you decide on these, the next posting will be the new shape
> rather than a v9 of this one, and it will cross into drivers/hid, so I
> will send it to both lists. I will mark v8 superseded in patchwork
> once it goes out.
Well, let's experiment the component stuff a bit. The v8 doesn't look
too bad, and the disconnection notification handling can be added
easily there, too. Although I think the component is cleaner from the
design POV, the actual behavior might be problematic in practice (such
as the mixer state restore delay as you described) -- or the
implementation might become too complex than wished. Let's see.
thanks,
Takashi
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-09-03 10:02 ` Takashi Iwai
@ 2026-09-03 10:19 ` Mikhail Gavrilov
2026-09-04 0:18 ` Mikhail Gavrilov
1 sibling, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-03 10:19 UTC (permalink / raw)
To: jikos, bentiss
Cc: tiwai, tiwai, perex, linux-input, linux-sound, linux-kernel
On Thu, 03 Sep 2026 12:02:57 +0200, Takashi Iwai wrote:
>
> > Would you rather see this as a driver-local clear, as
> > HID_CONNECT_DRIVER plus the clear, or as something usbhid ought to
> > offer to drivers that resynchronise on resume?
>
> I think it's rather a question to HID people...
Jiri, Benjamin -- putting it to you then, with enough context to answer
it without reading the rest of the thread. The wider design question,
whether this belongs in a HID driver at all, is settled with Takashi
upstream in this thread; what follows is narrower than that.
The device is a Topping M62 USB audio interface, 152a:875c. Its
analogue gains, output volumes and source selectors are not described
by the USB Audio Class; they are reached over a vendor protocol on a
HID-class interface whose report descriptor is a fig leaf -- a Generic
Desktop application collection, eight usages stretched over sixteen
unnamed bytes in and out, no report ID -- so hid-generic can only make
a nonexistent pointer of it. The plan under discussion is a small HID
driver that speaks that protocol and hands the values to snd-usb-audio
as mixer controls. It wants raw input reports and nothing else: no
input device, no hiddev.
The trouble is that asking usbhid for input reports arms the device for
remote wakeup, and there is no way to ask for one without the other.
usbhid_open() sets
usbhid->intf->needs_remote_wakeup = 1;
and usbhid_start() sets the same on the HID_QUIRK_ALWAYS_POLL path, so
both roads to hid_start_in() go through it.
This card does not offer remote wakeup: bmAttributes is 0xc0, and
there is no power/wakeup attribute under its sysfs node, so
device_can_wakeup() is false. usb_suspend_both() then refuses
autosuspend, and not merely for that interface:
if (w && !device_can_wakeup(&udev->dev)) {
dev_dbg(&udev->dev, "remote wakeup needed for autosuspend\n");
return -EOPNOTSUPP;
}
So binding this driver would forbid runtime suspend to the whole
device, including its audio interfaces. An earlier revision of this
series had to fix runtime suspend once already, and I would rather not
hand it back.
What makes this feel like the wrong flag rather than an unlucky device
is that the driver does not depend on the device to wake anything. The
card stops reporting to a host it has not heard from, so the driver's
resume path subscribes again and asks for the whole state; a knob
turned on the front panel while the host slept is picked up on the way
back, by asking, not by being told. A device-initiated wakeup would be
of no use to it.
Three shapes, and I do not know which you would want:
(a) the driver clears intf->needs_remote_wakeup after hid_hw_open(),
with a comment saying why. The field is not private -- cdc-acm
and usbnet both set it directly -- but clearing it from outside
usbhid is unusual enough that I did not want to just post it;
(b) the same, plus HID_CONNECT_DRIVER instead of HID_CONNECT_HIDRAW.
A hidraw open calls hid_hw_open() again and sets the flag back,
so (a) only holds if userspace cannot open the device. That is a
real cost here: the hidraw node is how this protocol was read in
the first place, and how the parts the driver does not expose --
the mixer matrix, the mutes, the EQ -- stay reachable at all;
(c) something in usbhid, so that a driver which resynchronises on
resume can say so once and have both roads to hid_start_in()
honour it. That would cover any driver in this position rather
than this one.
Or a fourth I have not seen: is there an existing way to take raw input
reports from usbhid without arming the device?
I lean to (c) as the honest fix and (a) as what I can post today, but
this is your subsystem and I would rather ask than guess.
--
Mikhail
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-09-03 10:02 ` Takashi Iwai
2026-09-03 10:19 ` Mikhail Gavrilov
@ 2026-09-04 0:18 ` Mikhail Gavrilov
2026-09-04 7:05 ` Mikhail Gavrilov
1 sibling, 1 reply; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 0:18 UTC (permalink / raw)
To: tiwai; +Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel
On Thu, 03 Sep 2026 12:02:57 +0200, Takashi Iwai wrote:
>
> Well, let's experiment the component stuff a bit.
I did, and it works. But while testing it I measured something that
undercuts the premise of the whole series, so that comes first and the
component question is at the bottom, where it now belongs.
**The card cannot be read. Not by this driver, and not by the vendor's
own application.**
Every version of this series has rested on one sentence, which is in
the v8 commit message: that a single write of 0x11/0x26 makes the
device announce its whole state, and that this is how the controls are
populated without caching what we wrote. That is wrong, and I am sorry
it stood for eight postings.
Six captures, on two operating systems.
Four with usbmon here: a cold start with the card powered down first,
an ordinary replug, a module unload and reload with the card left in
place, and one with every state restorer disabled -- alsa-restore and
alsa-state masked, the 90-alsa-restore udev rule symlinked to
/dev/null, wireplumber and pipewire stopped, asound.state moved aside.
That last one: thirty-one seconds, seventeen frames out, 2589 in,
every incoming frame a level meter. The 0x11/0x26 write elicited
nothing at all.
Two of the vendor's application on macOS. Its connect sequence is:
0.000 OUT 0x11/0x01 = 1 handshake, device answers 3
0.034 OUT 0x11/0x20 = 1 opens a bulk transaction
... 149 writes gains, mixer, EQ, routing
1.070 OUT 0x11/0x20 = 0 closes it
1.072 OUT 0x11/0x26 = 1
1.074 OUT 0x11/0x24 = 1 subscribe, then every ~2 s
All 162 outgoing frames are writes. There is not one read. The
application never asks the card anything: it opens a transaction and
pushes its own stored workspace in, including every one of the nine
values this series exposes. And 0x11/0x26 goes out *after* the upload,
so whatever it is, it is not a request for state. I had it backwards.
The second macOS capture was taken deliberately. I set Mic-1 to 70
from the front panel here and confirmed it through the control; the
Mac had 50 stored. Forty milliseconds after connecting, the
application wrote 0x21/0x04 = 50 and the panel followed. That write is
the only mention of 0x21/0x04 anywhere in the capture.
So the plausible values in my earlier testing were the driver's own
writes coming back. topping_ctl_write() updates the cache before
sending, and alsactl restored the stored state within about 80 ms of
the bind on every plug. My asound.state held "value 28" for Mic-1, and
28 is what the control read for two days. Reload the module -- no
card-add uevent, so no restorer runs -- and it reads 0 while the panel
is plainly not at zero.
This is what the vendor told me in August: there is no read command.
They were describing their hardware accurately.
**How this differs from an ordinary card.**
An HDA codec keeps its amp values in registers and
snd_hda_codec_amp_read() asks for them; a UAC device answers GET_CUR.
Both drivers then cache, which is why a mixer control looks like plain
software state from the outside. The M62 remembers too -- better than
either, since it has a battery and survives being unplugged -- but
there is no way to ask it. The cache has nothing to start from, so it
starts from zero.
That is not merely inaccurate. The output volumes use a TLV whose
index 0 is DB_GAIN_MUTE and whose index 99 is the top of the scale, so
the driver can report "muted" for a card sitting at maximum. The input
gains are no better in kind: this card is bought for measurement, and
a preamplifier reading 28 while the hardware is at 70 makes every
number taken through it wrong, quietly and later.
The two source selectors are worse than either, because a wrong one is
silent rather than loud. If the headphone output is pointed at Mix A
and the host plays into Playback 1/2, nothing comes out and nothing
says why: the card is playing correctly, from a source no one is
feeding. No log line, no error, no clue short of reading the panel.
The vendor application writes 0x64/0x02 on connect for exactly this
reason.
**What I think the answer is.**
There is no "do nothing" option: publishing a control at all makes
alsactl store and restore it, so the driver imposes state on the card
whether it means to or not.
While looking for a convention I found that usb-audio already has one
for this, in mixer.c:
/* forcibly initialize the current mixer value; if GET_CUR fails,
* set to the minimum as default
*/
static void init_cur_mix_raw(...)
{
err = snd_usb_get_cur_mix_value(cval, ch, idx, &val);
if (!err)
return;
...
snd_usb_set_cur_mix_value(cval, ch, idx, cval->min);
}
If a device will not answer GET_CUR -- and there is a get_cur_broken
flag and a check_mixer_get_cur() that detects it -- the mixer writes
the minimum and caches that. It does not display an unverified number
and it does not make the control write-only. The reported value is
true because the driver made it true, and safe because the minimum is
the safe end.
That is what I would like to do here: write the minimum to all seven
gains and volumes at bind, and cache it. alsactl restore then puts the
user's stored values over the top a moment later, exactly as it does
for any other card. A hand-set panel value is lost on connect, which
is a real cost -- but the vendor's own application already destroys it,
with its stored value rather than the minimum.
The selectors do not fit that shape, since their minimum is the
"Unknown" item and writing it is a no-op. Either the driver picks a
sensible default source at bind, and takes over routing the user may
have set deliberately, or it leaves them alone and the card can be
silent for reasons nothing on the host can explain. I do not have a
good answer and would rather be told.
So: is init_cur_mix_raw()'s behaviour the right precedent to follow
for a vendor protocol that has no read at all, rather than only a
broken GET_CUR? And what would you do about the selectors?
**The component experiment**, briefly. It works. The master goes up
from snd_topping_init() inside snd_usb_create_mixer(), so on an
ordinary plug the bind is synchronous and the controls exist before
try_to_register_card() -- the same ordering as v8, and the restore
timing worry does not bite there. From HID probe to component bind:
54 to 57 ms across five replugs. Only a module reload onto a live card
adds controls after registration, and there a desktop mixer does not
follow the renumbered elements until wireplumber is restarted.
I will hold the prototype until the above is settled, because the
answer decides what these controls are.
--
Mikhail
^ permalink raw reply [flat|nested] 53+ messages in thread
* Re: [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls
2026-09-04 0:18 ` Mikhail Gavrilov
@ 2026-09-04 7:05 ` Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
0 siblings, 1 reply; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 7:05 UTC (permalink / raw)
To: tiwai; +Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel
On Fri, 04 Sep 2026 05:18:18 +0500, I wrote:
>
> **The card cannot be read. Not by this driver, and not by the vendor's
> own application.**
That is wrong, and I would rather correct it before it is answered.
The card does report its state. It reports rather a lot of it. What I
had was two blind spots that happened to hide the same thing.
**What I missed.** In four of the six captures the state restorers on
this machine wrote to the card about 90 ms after the bind, so the
report that arrives at 5.2 s described their write and was
indistinguishable from silence. In the one capture with the restorers
disabled, nothing was plugged into any input -- and the card reports
the gain only of an input whose jack is present, so there was nothing
for it to report. Each blind spot alone would have been enough.
Repeating that capture with a coupler in IN 1 and every restorer
masked: the card sent 0x21/0x04 = 50 at 5.17 s, which is exactly what
I had set by hand on the front panel. Twenty-two outgoing frames in
the whole run, all of them 0x11/0x24 or 0x11/0x26. Nothing was
written; that value came from the card.
Then I turned the knobs by hand, still with nothing writing:
- Mic-1, 50 up to 53 and back to 50: every step reported, seventeen
frames of 0x21/0x04;
- the headphone volume, likewise: five frames of 0x64/0x03, 32, 33,
33, 32, 31.
So the corrected picture:
announced at connect when a hand moves it
five input gains yes, if the jack is yes
present, ~5.2 s after
subscribe
two output volumes no yes
two selectors no never
The model that fits: **the card reports what a hand does to the
hardware, not what its settings are.** Jack states, mutes, battery and
the gain of a connected input are all physical facts it knows by
itself. An output volume is a setting, so it is not in the connect
report -- but its knob is on the front panel, so turning it is an
event and that is reported. A source selector has no front-panel
control at all, which is why nothing about it ever arrives: there is
no event to report, not a missing command.
That last point is worth stating plainly because it retires a question
I have been asking since v2. The "Unknown" first item on the two
selectors is not a compromise; it is the accurate description of a
control the host can set and can never learn. If another host set it,
this one cannot find out, and no firmware change short of adding a
query would alter that.
**I withdraw the proposal at the end of my last mail.** Writing the
minimum to the gains at bind, after init_cur_mix_raw(), would destroy
a value the card is about to report 5.2 s later. It was the right
shape for a device that cannot be read and the wrong shape for this
one.
What the measurement suggests instead is that the driver's problem is
a race it need not enter. alsactl restores at 90 ms; the truth arrives
at 5.2 s; the driver loses by a factor of fifty and publishes an
invented number in between. But the component road you suggested can
simply not publish yet: hold component_add() until the first report
lands, and the controls appear late, once, with the values the panel
actually has. Nothing restores over them because they do not exist to
be restored. A mixer quirk cannot express that; the component split
can, which is an argument for it I did not have this morning.
The output volumes stay unknown until someone touches them, and the
selectors stay Unknown until someone writes them. That is honest
incompleteness rather than invention, and it is as good as this
hardware allows.
What stands from the last mail: the vendor application still issues
162 frames on connect and not one read; 0x11/0x26 still goes out after
its bulk push rather than before; a value set by hand still does not
survive being plugged into another host, since MCC overwrote my 70
with its stored 50 forty milliseconds after connecting. What does not
stand is the conclusion I drew from those, and the recommendation.
Sorry for the noise. I would rather send this than have you answer the
version I got wrong.
--
Mikhail
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC PATCH 0/2] the Topping M62's vendor controls, on the component framework
2026-09-04 7:05 ` Mikhail Gavrilov
@ 2026-09-04 11:26 ` Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
` (2 more replies)
0 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 11:26 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
This is the experiment Takashi suggested in the v8 thread: an
individual HID driver joined to snd-usb-audio through the component
framework, instead of a mixer quirk that claims the HID interface for
itself. It is posted as an RFC rather than as v9 because v8 is not
withdrawn -- it remains the alternative, and the choice between the two
is the question this series exists to answer.
I said this morning that I would hold this until the questions in that
thread were answered. I am sending it anyway, because a patch gets a
better answer than a question does, and because the last of those
questions answered itself: the correction I sent a few hours ago
proposed deferring the controls until the card reports, and working out
how that lands in code showed why it cannot -- which is in the section
below. The only question still genuinely open is the HID one, and it
touches one line.
What the shape buys, measured rather than argued:
- card.c is not touched at all. The v8 series had to add
snd_usb_claim_iface() and snd_usb_release_iface() there, and had a
defect I reported separately: an interface claimed and marked
USB_AUDIO_IFACE_UNUSED never gets told to wind down, because
usb_audio_disconnect() returns at its first line. With no claim
there is no such interface and the defect has nothing to attach
to. Watching the wire with usbmon: unbind the audio interface and
the keepalive stops. In v8 it kept going.
- the hid_ignore_list entry goes away, so hidraw stays available.
That matters because five of the card's functions -- the mixer
matrix, the mutes, the loopback routing, the input power and the
EQ -- are reachable only through the vendor protocol, and v8 made
them unreachable from userspace as the price of the quirk.
- two M62s on one host bind to their own cards. The match is by
descent from the shared USB device, and it holds: gains set on one
do not appear on the other, and unplugging one leaves the other's
controls in place. The quirk road could not be tested for this at
all.
On the timing worry from that thread: it does not bite on an ordinary
plug. The master goes up from snd_topping_init() inside
snd_usb_create_mixer(), so the bind is synchronous and the controls
exist before try_to_register_card() -- the same ordering as v8. From
HID probe to component bind, 54 to 57 ms across five replugs. Only a
module reload onto a live card adds controls after registration, and
there a desktop mixer does not follow the renumbered elements until
wireplumber is restarted. That is a real wart and I have not found a
way around it that is worth the code.
What the card does and does not report
--------------------------------------
I got this wrong twice in the v8 thread and would rather state it
plainly here, since the design follows from it.
After a subscribe the card reports itself in two waves: jacks at about
0.9 s, then at about 5.2 s the jacks again plus the output mutes plus
the gain of every input whose jack is present. Every turn of a
front-panel knob is reported as it happens, gains and output volumes
alike. What is never reported is a source selector, because the card
reports events and a selector has no front-panel control, so no event
can exist; and an output volume before anyone has touched it, because
it is a setting rather than a physical fact.
So five of the nine controls have a source of truth on the card, two
have one only after a hand moves them, and two never do.
That looked at first like an argument for deferring the controls until
the first report lands, so that a value read at connect would be the
panel's rather than something restored over it. I have not done that,
and here is why: alsactl restores once, at card add, and does not
come back for elements that appear later. Deferring would therefore
trade an accurate value for five controls against no restore at all
for nine, and would wait forever for an input with nothing plugged
into it -- which on a five-input card is the normal case.
The controls are published at bind, as in v8, and the value at
connect is the restored one.
Open question, not blocking the review
--------------------------------------
hid_hw_open() sets intf->needs_remote_wakeup, and usbhid offers no way
to take input reports without it. This card does not advertise remote
wakeup (bmAttributes 0xc0, no power/wakeup node), so that forbids
runtime suspend to the whole device -- undoing something an earlier
revision of the v8 series had to fix. The driver clears the flag after
opening, with a comment saying so, because it resynchronises on resume
and has no use for a device-initiated wakeup. Jiri and Benjamin have
been asked whether that is acceptable or whether usbhid should offer
something; the question is in the same thread and unanswered. If the
answer is that the clear must go, HID_CONNECT_DRIVER replaces
HID_CONNECT_HIDRAW and the hidraw node goes with it.
Tested
------
Fedora, 7.2.0-rc1 plus this series, KASAN, PROVE_LOCKING, UBSAN and
kmemleak enabled. One M62, firmware V87.05.45.48.27, and a second of
the same for the two-card test.
All nine controls read and written from amixer and from the front
panel, values cross-checked against the panel. Module load and unload
against a live card, audio-side unbind and rebind, unplug while
playing and while writing in a loop, and fifty cycles of module
load/unload with kmemleak scanned afterwards: no splat, no leak.
System suspend and resume, with the selectors written back on resume
as they must be. Two cards on one host.
Not tested: hibernate and the reset_resume path; a card whose battery
has run down; anything on a big-endian host.
Ordering
--------
The HID driver comes first. Between the two patches it binds, speaks
to the card and creates no controls, which is harmless; the reverse
order leaves a master that never matches, which is equally harmless
but leaves hid-generic making a bogus input device out of the
descriptor in the meantime.
Mikhail Gavrilov (2):
HID: topping-m62: driver for the M62's vendor controls
ALSA: usb-audio: bind the Topping M62's vendor controls
MAINTAINERS | 9 +
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 855 ++++++++++++++++++++++++++++++++++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 173 +++++++
sound/usb/mixer_topping.h | 7 +
10 files changed, 1076 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC PATCH 1/2] HID: topping-m62: driver for the M62's vendor controls
2026-09-04 11:26 ` [RFC PATCH 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
@ 2026-09-04 11:26 ` Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 11:26 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The Topping M62 is a USB audio interface whose analogue input gains,
output volumes and output source selectors are not described by the USB
Audio Class. They live behind a vendor protocol on a HID-class
interface, spoken by Topping's M Control Center, which has no Linux
build. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: raising it lifts the
converter's own floor with the signal. So on Linux the only gain worth
setting is unreachable, and a measurement application has to ask a
human to set it by hand on the front panel.
The protocol was read off the vendor application's traffic. Frames are
fifteen bytes:
22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
with TT a target, PP a property of that target, and the checksum
CRC-16/MODBUS over bytes 2..10, most significant byte first. Reports
from the device are the same frame plus one pad byte. The vendor
application sends 00 00 in place of the checksum and the device accepts
it, so the device does not verify what it receives; this driver signs
its writes anyway and validates what it reads.
The card is silent until subscribed. One write of 0x11/0x24 starts the
notification stream, and it lapses unless repeated: the vendor
application sends it every two seconds and so does this driver. After
that the card reports what a hand does to its hardware -- jack states,
mutes, battery, and every turn of a front-panel knob.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints are the only
route. The report descriptor describes nothing worth having -- a
Generic Desktop application collection, eight usages stretched over
sixteen unnamed bytes in and out, no report ID -- so hid-generic makes
an input device with an ABS_MISC axis out of it and nothing else. Hence
the hid_have_special_driver entry, and HID_CONNECT_HIDRAW here with no
input device.
The controls belong on the sound card rather than on a card of this
driver's own, so this driver creates none. It registers a component;
the M62 mixer quirk in snd-usb-audio is the master and hands over its
struct snd_card at bind time. Everything created here is dropped again
at unbind, whichever half goes away first, which is what makes the two
drivers independent of each other's disconnect. The audio half is the
following patch; without it this driver binds, speaks to the card and
creates nothing, which is harmless.
Interface 3 is Application Specific / DFU and is never touched: a stray
write there can leave the card unusable. Only interface 4 grows a
component.
Nine controls: five input gains (IN 1, IN 2, AUX, BT, OTG IN), two
output volumes (HP, OTG OUT) and the two output source selectors. The
outputs come in pairs and the device announces only the second of each,
so both are written and the second is the one listened for. The mixer
matrix, the mutes, the loopback routing, the input power and the EQ
remain reachable only through the hidraw node.
The two selectors carry an extra first item, "Unknown", and start
there. The card reports what a hand does to its hardware rather than
what its settings are, and a selector has no front-panel control, so
there is no event for it to report. A host that did not write the
selector cannot learn where the output points, and no command would
help, because there is nothing for one to be built on.
That matters more than a missing readback usually would, because the
selector decides whether the card makes a sound at all. It is
independent of which PCM device the host is playing into: a host can
be feeding Playback 1/2 while the headphone output listens to Mix C,
in which case the card is playing correctly from a source nobody is
feeding, and nothing on the host says why. The card is battery powered
and runs without a host at all, so whatever the last application to
touch it left behind is what a fresh Linux system inherits.
Without the extra item the driver would have to name a source it has
not read, and the first alsactl store would turn that invention into a
setting the user never made. With it, "Unknown" is stored and restored
like any other value until a hand chooses something, and writing it
back is accepted as a no-op rather than failing a restore of the
driver's own report.
Putting the card into a known state is a UCM profile's job rather than
this driver's: a profile knows what the user is trying to do, and a
driver does not. "Unknown" is what lets a profile do that without
harm, since it distinguishes "nobody has chosen" from "the user chose
Mix B" and can leave the second alone. No profile exists yet.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 855 ++++++++++++++++++++++++++++++++++
5 files changed, 881 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..e29f018fefd2 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1283,6 +1283,25 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING_M62
+ tristate "Topping M62 vendor controls"
+ depends on USB_HID
+ depends on SND_USB_AUDIO
+ select CRC16
+ help
+ Support for the analogue input gains, output volumes and
+ output source selectors of the Topping M62 USB audio
+ interface. These are not described by the USB Audio Class
+ and are reached over a vendor protocol on the card's HID
+ interface, so a driver is needed for them to appear at all.
+
+ The controls are created on the sound card that snd-usb-audio
+ makes for the same device, so both drivers are needed. The
+ card works without this one; it simply has no gain controls.
+
+ To compile this driver as a module, choose M here: the module
+ will be called hid-topping-m62.
+
config HID_TOPSEED
tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245ee..9f854686b702 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
+obj-$(CONFIG_HID_TOPPING_M62) += hid-topping-m62.o
obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o
obj-$(CONFIG_HID_TOPRE) += hid-topre.o
obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..60ca5712cb58 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -787,6 +787,9 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE_PRO) },
#endif
+#if IS_ENABLED(CONFIG_HID_TOPPING_M62)
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+#endif
#if IS_ENABLED(CONFIG_HID_TOPSEED)
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE_2) },
diff --git a/drivers/hid/hid-topping-m62.c b/drivers/hid/hid-topping-m62.c
new file mode 100644
index 000000000000..0ef407ffb46b
--- /dev/null
+++ b/drivers/hid/hid-topping-m62.c
@@ -0,0 +1,855 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Vendor controls for Topping interfaces behind a HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * The control pipe is not an option: GET_REPORT and SET_REPORT both
+ * stall with EPIPE for every report type, so the interrupt endpoints
+ * are the only route. The report descriptor describes nothing worth
+ * having -- a Generic Desktop application collection, eight usages
+ * stretched over sixteen unnamed bytes in and out, no report ID -- so
+ * this driver takes HID_CONNECT_HIDRAW and no input device.
+ *
+ * THE CONTROLS BELONG ON THE SOUND CARD, so this driver creates no
+ * card of its own. It registers a component; the M62 mixer quirk in
+ * snd-usb-audio is the master and hands over its struct snd_card at
+ * bind time. Everything created here is taken off again at unbind,
+ * whichever half goes away first, which is what makes the two drivers
+ * independent of each other's disconnect.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "hid-ids.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+
+/*
+ * The M62 presents two non-audio interfaces. Interface 3 is
+ * Application Specific / DFU and is never touched here. Interface 4
+ * carries the control protocol and is the only one this driver takes.
+ */
+#define M62_VENDOR_IFNUM 4
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls)
+
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+
+struct topping_m62 {
+ struct hid_device *hdev;
+ struct usb_interface *intf; /* for runtime PM */
+
+ /*
+ * NULL until the audio side binds and NULL again after it
+ * unbinds. Frames arrive before the audio half is there, so
+ * anything that reports to userspace reads this under lock.
+ */
+ struct snd_card *card;
+
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against .raw_event */
+
+ int val[TOPPING_NUM_CTLS];
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
+
+ /* the volume controls first, then the selectors */
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+};
+
+/* ------------------------------------------------------------------ */
+/* the wire */
+/* ------------------------------------------------------------------ */
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes a runtime PM reference
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has no
+ * subscription worth renewing -- resume renews it -- and the second
+ * because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ *
+ * usbhid drops a leading zero byte, taking it for a report ID this
+ * device does not use, and sends the rest on the interrupt OUT
+ * endpoint. So the fifteen bytes that reach the card are the frame
+ * and nothing else.
+ */
+static int topping_send(struct topping_m62 *m62, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_REPORT_LEN, GFP_NOIO);
+ int err;
+
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = 0; /* the report ID usbhid will drop */
+ topping_build(buf + 1, target, prop, value);
+
+ err = hid_hw_output_report(m62->hdev, buf, TOPPING_FRAME_LEN + 1);
+ if (err >= 0)
+ return 0;
+ /* ENODEV and ESHUTDOWN are an unplug, not a fault worth a line */
+ if (err != -ENODEV && err != -ESHUTDOWN)
+ hid_err(m62->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ if (topping_m62_ctls[i].target == target &&
+ topping_m62_ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+/*
+ * What was the URB completion handler, minus everything usbhid now
+ * owns: there is no resubmit here and no bus-noise status to sort
+ * through. What is left is the decode.
+ *
+ * Runs in the interrupt handler's context, which is why val[] is
+ * behind a spinlock rather than the mutex.
+ */
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_kcontrol *kctl = NULL;
+ struct snd_card *card;
+ int idx, value;
+
+ if (size < TOPPING_FRAME_LEN)
+ return 0;
+ if (data[0] != 0x22 || data[1] != 0x33 ||
+ data[13] != 0x66 || data[14] != 0x77)
+ return 0;
+ if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
+ return 0;
+
+ idx = topping_index_of(data[5], data[6]);
+ if (idx < 0)
+ return 0; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(data + 7);
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return 0;
+
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ card = m62->card;
+ if (m62->val[idx] != value) {
+ m62->val[idx] = value;
+ kctl = m62->kctl[idx];
+ }
+ }
+
+ /*
+ * A frame that arrived just as the audio side was unbinding can
+ * notify an id that has already gone. snd_ctl_notify() takes a
+ * copy of the id and touches nothing that unbind frees, so such
+ * an event is merely wasted.
+ */
+ if (card && kctl)
+ snd_ctl_notify(card, SNDRV_CTL_EVENT_MASK_VALUE, &kctl->id);
+
+ return 0;
+}
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_m62 *m62 = container_of(work, struct topping_m62,
+ keepalive.work);
+ int err;
+
+ err = topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; nothing to renew */
+
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+/* ------------------------------------------------------------------ */
+/* the volume controls */
+/* ------------------------------------------------------------------ */
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ const struct topping_ctl_desc *d;
+
+ d = &topping_m62_ctls[kctl->private_value];
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = d->min;
+ uinfo->value.integer.max = d->max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(spinlock_irqsave)(&m62->lock);
+ ucontrol->value.integer.value[0] = m62->val[kctl->private_value];
+ return 0;
+}
+
+/*
+ * Split out so that the runtime PM reference has one place to be
+ * dropped. Entered with the device awake and nothing else held.
+ */
+static int topping_ctl_write(struct topping_m62 *m62, int idx, int value)
+{
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ int prev, err;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after.
+ * The lock cannot be held across a send, and a hand on the front
+ * panel during that window produces a notification .raw_event
+ * stores; updating afterwards would throw that away and leave
+ * the driver claiming a value the device had already moved away
+ * from. Written first, the device's own report is simply the
+ * last word, which is the right bias.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ if (m62->val[idx] == value)
+ return 0;
+ prev = m62->val[idx];
+ m62->val[idx] = value;
+ }
+
+ err = topping_send(m62, d->target, d->prop, value);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(m62, d->target_pair, d->prop, value);
+ }
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->val[idx] == value)
+ m62->val[idx] = prev;
+ return err;
+ }
+
+ return 1;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run
+ * this driver's own resume callback on this very thread, and
+ * that callback takes write_lock to write the selectors back;
+ * taking write_lock first would meet it already held, by us.
+ *
+ * There is no guard against disconnect here and none is needed:
+ * snd_ctl_remove() in topping_unbind() takes controls_rwsem for
+ * writing, and no control callback can be inside it.
+ */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_ctl_write(m62, idx, value);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+/* ------------------------------------------------------------------ */
+/* the source selectors */
+/* ------------------------------------------------------------------ */
+
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(mutex)(&m62->write_lock);
+ ucontrol->value.enumerated.item[0] = m62->sel[kctl->private_value];
+ return 0;
+}
+
+static int topping_sel_write(struct topping_m62 *m62, int idx,
+ unsigned int item)
+{
+ const struct topping_enum_desc *d = &topping_m62_enums[idx];
+ int err;
+
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has
+ * chosen, and alsactl stores and restores it like any other
+ * value. It is not a choice, so writing it changes nothing --
+ * quietly, rather than failing a restore of the driver's own
+ * report.
+ */
+ if (!item || m62->sel[idx] == item)
+ return 0;
+
+ err = topping_send(m62, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ m62->sel[idx] = item;
+ return 1;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_sel_write(m62, kctl->private_value, item);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again.
+ */
+static void topping_restore_sel(struct topping_m62 *m62)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&m62->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!m62->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(m62, d->target, d->prop,
+ topping_source_value[m62->sel[i]]);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* creating and dropping the controls */
+/* ------------------------------------------------------------------ */
+
+/*
+ * v8 carried the index in a usb_mixer_elem_info, because that is what
+ * snd_usb_mixer_add_control() wants. Off the mixer there is nothing
+ * to satisfy: private_data is this driver and private_value is the
+ * index, so the per-control allocation goes away with its private_free.
+ */
+static int topping_add_kctl(struct topping_m62 *m62,
+ const struct snd_kcontrol_new *tmpl,
+ const char *name, int idx, int slot,
+ const unsigned int *tlv)
+{
+ struct snd_kcontrol *kctl;
+ int err;
+
+ kctl = snd_ctl_new1(tmpl, m62);
+ if (!kctl)
+ return -ENOMEM;
+
+ kctl->private_value = idx;
+ kctl->tlv.p = tlv;
+ strscpy(kctl->id.name, name, sizeof(kctl->id.name));
+
+ err = snd_ctl_add(m62->card, kctl);
+ if (err < 0)
+ return err; /* snd_ctl_add() freed it */
+
+ m62->kctl[slot] = kctl;
+ return 0;
+}
+
+static void topping_drop_kctls(struct topping_m62 *m62, struct snd_card *card)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_KCTLS; i++) {
+ snd_ctl_remove(card, m62->kctl[i]);
+ m62->kctl[i] = NULL;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* component */
+/* ------------------------------------------------------------------ */
+
+static int topping_bind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_card *card = master_data;
+ int i, err;
+
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = card;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ err = topping_add_kctl(m62, &topping_ctl,
+ topping_m62_ctls[i].name, i, i,
+ topping_m62_ctls[i].tlv);
+ if (err < 0)
+ goto err_drop;
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_kctl(m62, &topping_sel,
+ topping_m62_enums[i].name, i,
+ TOPPING_NUM_CTLS + i, NULL);
+ if (err < 0)
+ goto err_drop;
+ }
+
+ /*
+ * Subscribe and ask for the state HERE rather than at probe:
+ * before this point every announced value would land in the
+ * cache with no control to notify. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through
+ * .raw_event and notifies its own control.
+ */
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+err_drop:
+ topping_drop_kctls(m62, card);
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = NULL;
+ return err;
+}
+
+static void topping_unbind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_card *card = master_data;
+
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ /*
+ * Stop reporting before the controls go, so that a frame in
+ * flight cannot look up a control this function is removing.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = NULL;
+
+ topping_drop_kctls(m62, card);
+}
+
+static const struct component_ops topping_component_ops = {
+ .bind = topping_bind,
+ .unbind = topping_unbind,
+};
+
+/* ------------------------------------------------------------------ */
+/* HID */
+/* ------------------------------------------------------------------ */
+
+static int topping_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct topping_m62 *m62;
+ int err;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ m62 = devm_kzalloc(&hdev->dev, sizeof(*m62), GFP_KERNEL);
+ if (!m62)
+ return -ENOMEM;
+
+ m62->hdev = hdev;
+ m62->intf = to_usb_interface(hdev->dev.parent);
+ if (m62->intf->cur_altsetting->desc.bInterfaceNumber !=
+ M62_VENDOR_IFNUM)
+ return -ENODEV;
+
+ spin_lock_init(&m62->lock);
+ INIT_DELAYED_WORK(&m62->keepalive, topping_keepalive);
+ hid_set_drvdata(hdev, m62);
+
+ err = devm_mutex_init(&hdev->dev, &m62->write_lock);
+ if (err)
+ return err;
+
+ err = hid_parse(hdev);
+ if (err)
+ return err;
+
+ /*
+ * HIDRAW and no input device. The descriptor would only make a
+ * nonexistent pointer, while a hidraw node is how this protocol
+ * was read in the first place and how the parts not exposed here
+ * -- the mixer matrix, the mutes, the EQ -- stay reachable.
+ */
+ err = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (err)
+ return err;
+
+ err = hid_hw_open(hdev);
+ if (err)
+ goto err_stop;
+
+ /*
+ * XXX awaiting the HID maintainers' word. usbhid arms every
+ * device it opens for remote wakeup, and this card does not
+ * offer it, which forbids runtime suspend to the whole device.
+ * Nothing here needs it: the resume path subscribes again and
+ * asks the card for its whole state, so a knob turned while the
+ * host slept is picked up by asking rather than by being told.
+ *
+ * This clear does not survive a hidraw open, which calls
+ * hid_hw_open() again -- so if it stays, HID_CONNECT_DRIVER has
+ * to replace HID_CONNECT_HIDRAW above.
+ */
+ m62->intf->needs_remote_wakeup = 0;
+
+ /*
+ * Reports are dropped for the whole of probe unless this is called,
+ * and component_add() below can bind synchronously when the audio
+ * side is already there -- which subscribes, and the device answers
+ * at once. Without this the identification wave is thrown away.
+ */
+ hid_device_io_start(hdev);
+
+ err = component_add(&hdev->dev, &topping_component_ops);
+ if (err)
+ goto err_close;
+
+ return 0;
+
+err_close:
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+ return err;
+}
+
+static void topping_remove(struct hid_device *hdev)
+{
+ /* Runs topping_unbind() first if the audio side is bound. */
+ component_del(&hdev->dev, &topping_component_ops);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static int topping_suspend(struct hid_device *hdev, pm_message_t message)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ return 0;
+}
+
+static int topping_resume(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ unsigned int noio;
+
+ /*
+ * Nothing to report to yet, and nothing the card needs told.
+ * The next bind does the subscribing.
+ */
+ if (!READ_ONCE(m62->card))
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usbhid's own
+ * usb_interrupt_msg() allocates a URB with GFP_KERNEL, so asking
+ * for the frame buffer politely is not enough, and reclaim here
+ * can wait on a block device that has not woken yet.
+ *
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ noio = memalloc_noio_save();
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(m62);
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static const struct hid_device_id topping_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, topping_devices);
+
+static struct hid_driver topping_driver = {
+ .name = "topping-m62",
+ .id_table = topping_devices,
+ .probe = topping_probe,
+ .remove = topping_remove,
+ .raw_event = topping_raw_event,
+ .suspend = topping_suspend,
+ .resume = topping_resume,
+ .reset_resume = topping_resume,
+};
+module_hid_driver(topping_driver);
+
+MODULE_DESCRIPTION("Topping M62 vendor controls");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH 2/2] ALSA: usb-audio: bind the Topping M62's vendor controls
2026-09-04 11:26 ` [RFC PATCH 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
@ 2026-09-04 11:26 ` Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 11:26 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The M62's vendor controls are driven by hid-topping-m62, added in the
previous patch, which speaks a vendor protocol on the card's HID
interface. Those controls belong on the sound card that plays the
audio, not on a card of the HID driver's own.
This adds the other half of that: a component master, registered from
the M62's mixer quirk, which hands its struct snd_card to the HID
driver at bind time and takes the controls away again at unbind. The
lifetime rules are the component framework's, which is the point --
neither driver has to be told about the other's disconnect, and neither
has to guess at the other's state. The same shape binds HD-audio to the
graphics drivers in sound/hda/core/component.c, with sound as the
master there too.
The master's context lives in devres on the audio control interface
rather than in drvdata, which on a usb_interface belongs to
snd-usb-audio itself; devres_find(), keyed on the release function,
gives it back inside the callbacks, which are handed nothing but a
struct device *. It hangs off the control interface rather than off the
USB device because component_match_add() allocates the match list with
devm: on the interface that is released at unbind, while on the
usb_device it would live until the device itself was released and a
rebind would stack a second list on top.
Neither component_compare_dev() nor component_compare_dev_name() fits:
the audio side has no pointer to the HID device, and the HID device's
name carries an instance counter that is not predictable. The match is
therefore one of descent -- the HID device sits two levels below the
USB device -- and which interface it is stays the HID driver's
business, since it registers a component for the vendor interface and
for no other. That keeps sound/usb free of HID symbols and of any
opinion about this card's interface numbering.
component_master_add_with_match() returns 0 with the aggregate merely
pending when the HID driver is absent, so the card comes up either way
and grows the vendor controls if and when the other half appears.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 9 ++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 ++
sound/usb/mixer_topping.c | 173 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 ++
5 files changed, 195 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 627595e245f3..b49560efa1c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27593,6 +27593,15 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 VENDOR CONTROLS
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-topping-m62.c
+F: sound/usb/mixer_topping.c
+F: sound/usb/mixer_topping.h
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..922b0f61cbd4
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Topping M62 -- component master for the card's vendor controls.
+ *
+ * The M62's analogue gains, output volumes and source selectors are not
+ * described by the USB Audio Class. They are reached over a vendor
+ * protocol on the card's HID interface, which hid-topping-m62 speaks.
+ *
+ * This file speaks none of that protocol. It publishes the sound card to
+ * whoever drives the vendor interface, so that the controls are created on
+ * the card that plays the audio rather than on a card of their own, and are
+ * torn down when either side goes away. The lifetime rules are the
+ * component framework's, which is the point: neither driver has to guess at
+ * the other's state, and neither has to be told about the other's
+ * disconnect.
+ *
+ * The same shape binds HD-audio to the graphics drivers in
+ * sound/hda/core/component.c, with sound as the master there too.
+ */
+
+#include <linux/component.h>
+#include <linux/device.h>
+#include <linux/usb.h>
+
+#include <sound/core.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_topping.h"
+
+/*
+ * What the master hands the component at bind time. It lives in devres on
+ * the audio control interface rather than in drvdata, because drvdata on a
+ * usb_interface belongs to snd-usb-audio itself. devres_find(), keyed on
+ * the release function, gives it back inside the callbacks, which are
+ * handed nothing but a struct device *.
+ */
+struct topping_master {
+ struct device *dev; /* the audio control interface */
+ struct snd_card *card;
+};
+
+static void topping_master_release(struct device *dev, void *res)
+{
+ /* The devres allocation is the storage; nothing else to drop. */
+}
+
+static struct topping_master *topping_get_master(struct device *dev)
+{
+ return devres_find(dev, topping_master_release, NULL, NULL);
+}
+
+/*
+ * Which of the registered components is ours.
+ *
+ * This is only ever called against devices that have registered with
+ * component_add(), so it does not have to defend itself against the whole
+ * device tree. What it does have to do is tell this card's vendor
+ * function apart from a second M62 on another port.
+ *
+ * The HID device sits two levels below the USB device:
+ *
+ * hid_device -> usb_interface -> usb_device
+ *
+ * WHICH interface it is, is the HID driver's business: it registers a
+ * component for the vendor interface and for nothing else. So the test
+ * here is one of descent alone and needs no HID symbols in sound/usb --
+ * which also keeps this file free of any opinion about the M62's
+ * interface numbering.
+ */
+static int topping_match_component(struct device *dev, void *data)
+{
+ return dev->parent && dev->parent->parent == data;
+}
+
+static int topping_master_bind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return -EINVAL;
+
+ return component_bind_all(dev, tm->card);
+}
+
+static void topping_master_unbind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return;
+
+ component_unbind_all(dev, tm->card);
+}
+
+static const struct component_master_ops topping_master_ops = {
+ .bind = topping_master_bind,
+ .unbind = topping_master_unbind,
+};
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_master *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+
+ /*
+ * Reached from snd_usb_mixer_disconnect(), on an unplug and on an
+ * unbind of the audio interface alike. component_master_del() runs
+ * topping_master_unbind() on the way, so the HID side has taken its
+ * kcontrols off this card before the card is taken apart.
+ */
+ component_master_del(tm->dev, &topping_master_ops);
+ devres_destroy(tm->dev, topping_master_release, NULL, NULL);
+ mixer->private_data = NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct component_match *match = NULL;
+ struct usb_interface *intf;
+ struct topping_master *tm;
+ struct device *dev;
+ int err;
+
+ /*
+ * The master hangs off the audio control interface rather than off
+ * the USB device: component_match_add() allocates the match list
+ * with devm, and on an interface that is released when the interface
+ * is unbound. On the usb_device it would live until the device
+ * itself was released, and a rebind would stack a second list on top
+ * of the first.
+ */
+ intf = usb_ifnum_to_if(chip->dev,
+ get_iface_desc(mixer->hostif)->bInterfaceNumber);
+ if (!intf)
+ return -ENODEV;
+ dev = &intf->dev;
+
+ tm = devres_alloc(topping_master_release, sizeof(*tm), GFP_KERNEL);
+ if (!tm)
+ return -ENOMEM;
+ tm->dev = dev;
+ tm->card = chip->card;
+ devres_add(dev, tm);
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ component_match_add(dev, &match, topping_match_component,
+ &chip->dev->dev);
+
+ /*
+ * This returns 0 with the aggregate merely pending when
+ * hid-topping-m62 has not registered its component yet:
+ * try_to_bring_up_aggregate_device() reports an incomplete set as
+ * "not ready", not as an error. So the card comes up either way and
+ * grows the vendor controls if and when the other half appears.
+ */
+ err = component_master_add_with_match(dev, &topping_master_ops, match);
+ if (err < 0) {
+ mixer->private_data = NULL;
+ mixer->private_free = NULL;
+ devres_destroy(dev, topping_master_release, NULL, NULL);
+ usb_audio_err(chip, "Topping: no component master: %d\n", err);
+ return err;
+ }
+
+ return 0;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v2 0/2] the Topping M62's vendor controls, on the component framework
2026-09-04 11:26 ` [RFC PATCH 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
@ 2026-09-04 14:11 ` Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 14:11 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
This is the experiment Takashi suggested in the v8 thread: an
individual HID driver joined to snd-usb-audio through the component
framework, instead of a mixer quirk that claims the HID interface for
itself. It is posted as an RFC rather than as v9 because v8 is not
withdrawn -- it remains the alternative, and the choice between the two
is the question this series exists to answer.
I said this morning that I would hold this until the questions in that
thread were answered. I am sending it anyway, because a patch gets a
better answer than a question does, and because the last of those
questions answered itself: the correction I sent a few hours ago
proposed deferring the controls until the card reports, and working out
how that lands in code showed why it cannot -- which is in the section
below. The only question still genuinely open is the HID one, and it
touches one line.
Changes since the RFC, all three from the Sashiko bot's review of it:
- .raw_event() read the control pointer under the lock and used it
after releasing it. snd_ctl_notify() dereferences the id it is
given -- it compares id->numid and copies the structure into the
event -- so an unbind racing between the two could free the
control first, and the card pointer was stale by then as well.
The notify now happens under the same lock; it is safe there,
taking read_lock_irqsave and allocating with GFP_ATOMIC, and
usb-audio's own mixer already notifies from a URB completion.
- the keepalive could outlive the driver. topping_unbind() cancelled
the work before clearing m62->card, so a resume in that window
could see a live card and schedule it again, and topping_remove()
never cancelled at all -- devm would free the driver data with the
timer still armed. The clear now comes first, resume tests and
schedules under the same lock, and remove() cancels
unconditionally.
- the third finding, that topping_drop_kctls() passes NULL to
snd_ctl_remove(), is not a defect. That function returns 0 on a
NULL control as its first statement and its kerneldoc says so;
the loop is deliberately unconditional so that a partly built set
unwinds through one path.
No other changes: the sound side is untouched.
- topping_send() logged at err level for -EPROTO, so pulling the
cable out of a card that was being written to produced eight
complaints about a device that was already leaving. EPROTO and
EILSEQ join ENODEV and ESHUTDOWN in the quiet list. The keepalive
still gives up only on the latter two: EPROTO can be a transient
bus error, and stopping on it would leave the subscription dead
until the next replug.
What the shape buys, measured rather than argued:
- card.c is not touched at all. The v8 series had to add
snd_usb_claim_iface() and snd_usb_release_iface() there, and had a
defect I reported separately: an interface claimed and marked
USB_AUDIO_IFACE_UNUSED never gets told to wind down, because
usb_audio_disconnect() returns at its first line. With no claim
there is no such interface and the defect has nothing to attach
to. Watching the wire with usbmon: unbind the audio interface and
the keepalive stops. In v8 it kept going.
- the hid_ignore_list entry goes away, so hidraw stays available.
That matters because five of the card's functions -- the mixer
matrix, the mutes, the loopback routing, the input power and the
EQ -- are reachable only through the vendor protocol, and v8 made
them unreachable from userspace as the price of the quirk.
- two M62s on one host bind to their own cards. The match is by
descent from the shared USB device, and it holds: gains set on one
do not appear on the other, and unplugging one leaves the other's
controls in place. The quirk road could not be tested for this at
all.
On the timing worry from that thread: it does not bite on an ordinary
plug. The master goes up from snd_topping_init() inside
snd_usb_create_mixer(), so the bind is synchronous and the controls
exist before try_to_register_card() -- the same ordering as v8. From
HID probe to component bind, 54 to 57 ms across five replugs. Only a
module reload onto a live card adds controls after registration, and
there a desktop mixer does not follow the renumbered elements until
wireplumber is restarted. That is a real wart and I have not found a
way around it that is worth the code.
What the card does and does not report
I got this wrong twice in the v8 thread and would rather state it
plainly here, since the design follows from it.
After a subscribe the card reports itself in two waves: jacks at about
0.9 s, then at about 5.2 s the jacks again plus the output mutes plus
the gain of every input whose jack is present. Every turn of a
front-panel knob is reported as it happens, gains and output volumes
alike. What is never reported is a source selector, because the card
reports events and a selector has no front-panel control, so no event
can exist; and an output volume before anyone has touched it, because
it is a setting rather than a physical fact.
So five of the nine controls have a source of truth on the card, two
have one only after a hand moves them, and two never do.
That looked at first like an argument for deferring the controls until
the first report lands, so that a value read at connect would be the
panel's rather than something restored over it. I have not done that,
and here is why: alsactl restores once, at card add, and does not
come back for elements that appear later. Deferring would therefore
trade an accurate value for five controls against no restore at all
for nine, and would wait forever for an input with nothing plugged
into it -- which on a five-input card is the normal case.
The controls are published at bind, as in v8, and the value at
connect is the restored one.
Open question, not blocking the review
hid_hw_open() sets intf->needs_remote_wakeup, and usbhid offers no way
to take input reports without it. This card does not advertise remote
wakeup (bmAttributes 0xc0, no power/wakeup node), so that forbids
runtime suspend to the whole device -- undoing something an earlier
revision of the v8 series had to fix. The driver clears the flag after
opening, with a comment saying so, because it resynchronises on resume
and has no use for a device-initiated wakeup. Jiri and Benjamin have
been asked whether that is acceptable or whether usbhid should offer
something; the question is in the same thread and unanswered. If the
answer is that the clear must go, HID_CONNECT_DRIVER replaces
HID_CONNECT_HIDRAW and the hidraw node goes with it.
Tested
Fedora, 7.3.0-rc1-bc35965f6940 plus this series, with KASAN,
PROVE_LOCKING and UBSAN enabled. One M62, firmware V87.05.45.48.27,
and a second of the same for the two-card test. Everything below was
run after the fixes listed above, not before them.
All nine controls read and written from amixer and from the front
panel, values cross-checked against the panel.
One hundred cycles of module load and unload against a live card,
which is the path the fixes touch: the controls come and go while the
meter stream keeps arriving, so .raw_event() runs against a set of
controls that is being taken apart. No splat and no lockdep complaint,
including on the lock order the first fix introduces -- m62->lock is
now held across snd_ctl_notify(), which takes the card's
controls_rwlock inside it.
An audio-side unbind and rebind, exercising topping_unbind() through
the master rather than through the HID driver's own remove.
The cable pulled out of a card while a control was being written in a
loop. That produces a run of failed writes and then the disconnect,
and after the last fix it does so without a line of complaint each.
System suspend and resume: the subscription survives it -- turning a
front-panel knob afterwards still moves the control -- and the
selectors are written back on the way out, as they must be since the
card never reports them.
Two cards on one host, on different ports of the same hub: the
component match is by descent from the shared USB device and it holds
-- dmesg shows snd-usb-audio 3-1.3:1.0 binding one HID device and
3-1.4.2:1.0 the other, each card carries its own nine controls, gains
set from one front panel move only that card's controls, and
unplugging one leaves the other's in place.
Not tested:
- hibernation, and with it .reset_resume, which shares
topping_resume() with .resume. This machine is not set up for it:
there is no resume= on the command line and zram takes swap
priority, so the attempt logs "PM: Image not found (code -16)" and
powers off instead of saving an image. That is the same on a stock
kernel and has nothing to do with this series, but it does mean
the path is unexercised;
- kmemleak, compiled in on this system but disabled at boot;
- a card whose battery has run down;
- anything on a big-endian host.
Ordering
The HID driver comes first. Between the two patches it binds, speaks
to the card and creates no controls, which is harmless; the reverse
order leaves a master that never matches, which is equally harmless
but leaves hid-generic making a bogus input device out of the
descriptor in the meantime.
Mikhail Gavrilov (2):
HID: topping-m62: driver for the M62's vendor controls
ALSA: usb-audio: bind the Topping M62's vendor controls
MAINTAINERS | 9 +
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 889 ++++++++++++++++++++++++++++++++++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 173 +++++++
sound/usb/mixer_topping.h | 7 +
10 files changed, 1110 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC PATCH v2 1/2] HID: topping-m62: driver for the M62's vendor controls
2026-09-04 14:11 ` [RFC PATCH v2 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
@ 2026-09-04 14:11 ` Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 14:42 ` [RFC PATCH v3 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 14:11 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The Topping M62 is a USB audio interface whose analogue input gains,
output volumes and output source selectors are not described by the USB
Audio Class. They live behind a vendor protocol on a HID-class
interface, spoken by Topping's M Control Center, which has no Linux
build. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: raising it lifts the
converter's own floor with the signal. So on Linux the only gain worth
setting is unreachable, and a measurement application has to ask a
human to set it by hand on the front panel.
The protocol was read off the vendor application's traffic. Frames are
fifteen bytes:
22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
with TT a target, PP a property of that target, and the checksum
CRC-16/MODBUS over bytes 2..10, most significant byte first. Reports
from the device are the same frame plus one pad byte. The vendor
application sends 00 00 in place of the checksum and the device accepts
it, so the device does not verify what it receives; this driver signs
its writes anyway and validates what it reads.
The card is silent until subscribed. One write of 0x11/0x24 starts the
notification stream, and it lapses unless repeated: the vendor
application sends it every two seconds and so does this driver. After
that the card reports what a hand does to its hardware -- jack states,
mutes, battery, and every turn of a front-panel knob.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints are the only
route. The report descriptor describes nothing worth having -- a
Generic Desktop application collection, eight usages stretched over
sixteen unnamed bytes in and out, no report ID -- so hid-generic makes
an input device with an ABS_MISC axis out of it and nothing else. Hence
the hid_have_special_driver entry, and HID_CONNECT_HIDRAW here with no
input device.
The controls belong on the sound card rather than on a card of this
driver's own, so this driver creates none. It registers a component;
the M62 mixer quirk in snd-usb-audio is the master and hands over its
struct snd_card at bind time. Everything created here is dropped again
at unbind, whichever half goes away first, which is what makes the two
drivers independent of each other's disconnect. The audio half is the
following patch; without it this driver binds, speaks to the card and
creates nothing, which is harmless.
Interface 3 is Application Specific / DFU and is never touched: a stray
write there can leave the card unusable. Only interface 4 grows a
component.
Nine controls: five input gains (IN 1, IN 2, AUX, BT, OTG IN), two
output volumes (HP, OTG OUT) and the two output source selectors. The
outputs come in pairs and the device announces only the second of each,
so both are written and the second is the one listened for. The mixer
matrix, the mutes, the loopback routing, the input power and the EQ
remain reachable only through the hidraw node.
The two selectors carry an extra first item, "Unknown", and start
there. The card reports what a hand does to its hardware rather than
what its settings are, and a selector has no front-panel control, so
there is no event for it to report. A host that did not write the
selector cannot learn where the output points, and no command would
help, because there is nothing for one to be built on.
That matters more than a missing readback usually would, because the
selector decides whether the card makes a sound at all. It is
independent of which PCM device the host is playing into: a host can
be feeding Playback 1/2 while the headphone output listens to Mix C,
in which case the card is playing correctly from a source nobody is
feeding, and nothing on the host says why. The card is battery powered
and runs without a host at all, so whatever the last application to
touch it left behind is what a fresh Linux system inherits.
Without the extra item the driver would have to name a source it has
not read, and the first alsactl store would turn that invention into a
setting the user never made. With it, "Unknown" is stored and restored
like any other value until a hand chooses something, and writing it
back is accepted as a no-op rather than failing a restore of the
driver's own report.
Putting the card into a known state is a UCM profile's job rather than
this driver's: a profile knows what the user is trying to do, and a
driver does not. "Unknown" is what lets a profile do that without
harm, since it distinguishes "nobody has chosen" from "the user chose
Mix B" and can leave the second alone. No profile exists yet.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 889 ++++++++++++++++++++++++++++++++++
5 files changed, 915 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..e29f018fefd2 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1283,6 +1283,25 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING_M62
+ tristate "Topping M62 vendor controls"
+ depends on USB_HID
+ depends on SND_USB_AUDIO
+ select CRC16
+ help
+ Support for the analogue input gains, output volumes and
+ output source selectors of the Topping M62 USB audio
+ interface. These are not described by the USB Audio Class
+ and are reached over a vendor protocol on the card's HID
+ interface, so a driver is needed for them to appear at all.
+
+ The controls are created on the sound card that snd-usb-audio
+ makes for the same device, so both drivers are needed. The
+ card works without this one; it simply has no gain controls.
+
+ To compile this driver as a module, choose M here: the module
+ will be called hid-topping-m62.
+
config HID_TOPSEED
tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245ee..9f854686b702 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
+obj-$(CONFIG_HID_TOPPING_M62) += hid-topping-m62.o
obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o
obj-$(CONFIG_HID_TOPRE) += hid-topre.o
obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..60ca5712cb58 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -787,6 +787,9 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE_PRO) },
#endif
+#if IS_ENABLED(CONFIG_HID_TOPPING_M62)
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+#endif
#if IS_ENABLED(CONFIG_HID_TOPSEED)
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE_2) },
diff --git a/drivers/hid/hid-topping-m62.c b/drivers/hid/hid-topping-m62.c
new file mode 100644
index 000000000000..66c037e2b868
--- /dev/null
+++ b/drivers/hid/hid-topping-m62.c
@@ -0,0 +1,889 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Vendor controls for Topping interfaces behind a HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * The control pipe is not an option: GET_REPORT and SET_REPORT both
+ * stall with EPIPE for every report type, so the interrupt endpoints
+ * are the only route. The report descriptor describes nothing worth
+ * having -- a Generic Desktop application collection, eight usages
+ * stretched over sixteen unnamed bytes in and out, no report ID -- so
+ * this driver takes HID_CONNECT_HIDRAW and no input device.
+ *
+ * THE CONTROLS BELONG ON THE SOUND CARD, so this driver creates no
+ * card of its own. It registers a component; the M62 mixer quirk in
+ * snd-usb-audio is the master and hands over its struct snd_card at
+ * bind time. Everything created here is taken off again at unbind,
+ * whichever half goes away first, which is what makes the two drivers
+ * independent of each other's disconnect.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "hid-ids.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+
+/*
+ * The M62 presents two non-audio interfaces. Interface 3 is
+ * Application Specific / DFU and is never touched here. Interface 4
+ * carries the control protocol and is the only one this driver takes.
+ */
+#define M62_VENDOR_IFNUM 4
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls)
+
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+
+struct topping_m62 {
+ struct hid_device *hdev;
+ struct usb_interface *intf; /* for runtime PM */
+
+ /*
+ * NULL until the audio side binds and NULL again after it
+ * unbinds. Frames arrive before the audio half is there, so
+ * anything that reports to userspace reads this under lock.
+ */
+ struct snd_card *card;
+
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against .raw_event */
+
+ int val[TOPPING_NUM_CTLS];
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
+
+ /* the volume controls first, then the selectors */
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+};
+
+/* ------------------------------------------------------------------ */
+/* the wire */
+/* ------------------------------------------------------------------ */
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes a runtime PM reference
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has no
+ * subscription worth renewing -- resume renews it -- and the second
+ * because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ *
+ * usbhid drops a leading zero byte, taking it for a report ID this
+ * device does not use, and sends the rest on the interrupt OUT
+ * endpoint. So the fifteen bytes that reach the card are the frame
+ * and nothing else.
+ */
+static int topping_send(struct topping_m62 *m62, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_REPORT_LEN, GFP_NOIO);
+ int err;
+
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = 0; /* the report ID usbhid will drop */
+ topping_build(buf + 1, target, prop, value);
+
+ err = hid_hw_output_report(m62->hdev, buf, TOPPING_FRAME_LEN + 1);
+ if (err >= 0)
+ return 0;
+
+ /*
+ * A cable pulled out of a running card produces one of these per
+ * write until the disconnect arrives, and none of them says
+ * anything about this driver: ENODEV and ESHUTDOWN are the
+ * device already gone, EPROTO and EILSEQ the bus falling apart
+ * on the way there. Anything else is worth a line.
+ */
+ if (err != -ENODEV && err != -ESHUTDOWN &&
+ err != -EPROTO && err != -EILSEQ)
+ hid_err(m62->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ if (topping_m62_ctls[i].target == target &&
+ topping_m62_ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+/*
+ * What was the URB completion handler, minus everything usbhid now
+ * owns: there is no resubmit here and no bus-noise status to sort
+ * through. What is left is the decode.
+ *
+ * Runs in the interrupt handler's context, which is why val[] is
+ * behind a spinlock rather than the mutex.
+ */
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ int idx, value;
+
+ if (size < TOPPING_FRAME_LEN)
+ return 0;
+ if (data[0] != 0x22 || data[1] != 0x33 ||
+ data[13] != 0x66 || data[14] != 0x77)
+ return 0;
+ if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
+ return 0;
+
+ idx = topping_index_of(data[5], data[6]);
+ if (idx < 0)
+ return 0; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(data + 7);
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return 0;
+
+ /*
+ * The notify happens under the same lock that guards the cache,
+ * not after it. snd_ctl_notify() dereferences the id it is
+ * given -- it compares id->numid and copies the structure into
+ * the event -- so a concurrent topping_unbind() releasing the
+ * lock's other side could free the control in between, and the
+ * card pointer would be stale by then too. It is safe from
+ * here: it takes read_lock_irqsave and allocates with GFP_ATOMIC.
+ */
+ guard(spinlock_irqsave)(&m62->lock);
+
+ if (m62->val[idx] == value)
+ return 0;
+ m62->val[idx] = value;
+
+ if (m62->card && m62->kctl[idx])
+ snd_ctl_notify(m62->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &m62->kctl[idx]->id);
+
+ return 0;
+}
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_m62 *m62 = container_of(work, struct topping_m62,
+ keepalive.work);
+ int err;
+
+ err = topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; nothing to renew */
+
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+/* ------------------------------------------------------------------ */
+/* the volume controls */
+/* ------------------------------------------------------------------ */
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ const struct topping_ctl_desc *d;
+
+ d = &topping_m62_ctls[kctl->private_value];
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = d->min;
+ uinfo->value.integer.max = d->max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(spinlock_irqsave)(&m62->lock);
+ ucontrol->value.integer.value[0] = m62->val[kctl->private_value];
+ return 0;
+}
+
+/*
+ * Split out so that the runtime PM reference has one place to be
+ * dropped. Entered with the device awake and nothing else held.
+ */
+static int topping_ctl_write(struct topping_m62 *m62, int idx, int value)
+{
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ int prev, err;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after.
+ * The lock cannot be held across a send, and a hand on the front
+ * panel during that window produces a notification .raw_event
+ * stores; updating afterwards would throw that away and leave
+ * the driver claiming a value the device had already moved away
+ * from. Written first, the device's own report is simply the
+ * last word, which is the right bias.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ if (m62->val[idx] == value)
+ return 0;
+ prev = m62->val[idx];
+ m62->val[idx] = value;
+ }
+
+ err = topping_send(m62, d->target, d->prop, value);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(m62, d->target_pair, d->prop, value);
+ }
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->val[idx] == value)
+ m62->val[idx] = prev;
+ return err;
+ }
+
+ return 1;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run
+ * this driver's own resume callback on this very thread, and
+ * that callback takes write_lock to write the selectors back;
+ * taking write_lock first would meet it already held, by us.
+ *
+ * There is no guard against disconnect here and none is needed:
+ * snd_ctl_remove() in topping_unbind() takes controls_rwsem for
+ * writing, and no control callback can be inside it.
+ */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_ctl_write(m62, idx, value);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+/* ------------------------------------------------------------------ */
+/* the source selectors */
+/* ------------------------------------------------------------------ */
+
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(mutex)(&m62->write_lock);
+ ucontrol->value.enumerated.item[0] = m62->sel[kctl->private_value];
+ return 0;
+}
+
+static int topping_sel_write(struct topping_m62 *m62, int idx,
+ unsigned int item)
+{
+ const struct topping_enum_desc *d = &topping_m62_enums[idx];
+ int err;
+
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has
+ * chosen, and alsactl stores and restores it like any other
+ * value. It is not a choice, so writing it changes nothing --
+ * quietly, rather than failing a restore of the driver's own
+ * report.
+ */
+ if (!item || m62->sel[idx] == item)
+ return 0;
+
+ err = topping_send(m62, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ m62->sel[idx] = item;
+ return 1;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_sel_write(m62, kctl->private_value, item);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again.
+ */
+static void topping_restore_sel(struct topping_m62 *m62)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&m62->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!m62->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(m62, d->target, d->prop,
+ topping_source_value[m62->sel[i]]);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* creating and dropping the controls */
+/* ------------------------------------------------------------------ */
+
+/*
+ * v8 carried the index in a usb_mixer_elem_info, because that is what
+ * snd_usb_mixer_add_control() wants. Off the mixer there is nothing
+ * to satisfy: private_data is this driver and private_value is the
+ * index, so the per-control allocation goes away with its private_free.
+ */
+static int topping_add_kctl(struct topping_m62 *m62,
+ const struct snd_kcontrol_new *tmpl,
+ const char *name, int idx, int slot,
+ const unsigned int *tlv)
+{
+ struct snd_kcontrol *kctl;
+ int err;
+
+ kctl = snd_ctl_new1(tmpl, m62);
+ if (!kctl)
+ return -ENOMEM;
+
+ kctl->private_value = idx;
+ kctl->tlv.p = tlv;
+ strscpy(kctl->id.name, name, sizeof(kctl->id.name));
+
+ err = snd_ctl_add(m62->card, kctl);
+ if (err < 0)
+ return err; /* snd_ctl_add() freed it */
+
+ m62->kctl[slot] = kctl;
+ return 0;
+}
+
+static void topping_drop_kctls(struct topping_m62 *m62, struct snd_card *card)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_KCTLS; i++) {
+ snd_ctl_remove(card, m62->kctl[i]);
+ m62->kctl[i] = NULL;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* component */
+/* ------------------------------------------------------------------ */
+
+static int topping_bind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_card *card = master_data;
+ int i, err;
+
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = card;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ err = topping_add_kctl(m62, &topping_ctl,
+ topping_m62_ctls[i].name, i, i,
+ topping_m62_ctls[i].tlv);
+ if (err < 0)
+ goto err_drop;
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_kctl(m62, &topping_sel,
+ topping_m62_enums[i].name, i,
+ TOPPING_NUM_CTLS + i, NULL);
+ if (err < 0)
+ goto err_drop;
+ }
+
+ /*
+ * Subscribe and ask for the state HERE rather than at probe:
+ * before this point every announced value would land in the
+ * cache with no control to notify. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through
+ * .raw_event and notifies its own control.
+ */
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+err_drop:
+ topping_drop_kctls(m62, card);
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = NULL;
+ return err;
+}
+
+static void topping_unbind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_card *card = master_data;
+
+ /*
+ * Clear the card first, then cancel. m62->card is what
+ * topping_resume() tests before it restarts the keepalive, and
+ * it tests it under this lock, so once this store is visible no
+ * resume can schedule the work again and the cancel below is
+ * final. Clearing it also stops .raw_event() from looking up a
+ * control this function is about to remove.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = NULL;
+
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ topping_drop_kctls(m62, card);
+}
+
+static const struct component_ops topping_component_ops = {
+ .bind = topping_bind,
+ .unbind = topping_unbind,
+};
+
+/* ------------------------------------------------------------------ */
+/* HID */
+/* ------------------------------------------------------------------ */
+
+static int topping_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct topping_m62 *m62;
+ int err;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ m62 = devm_kzalloc(&hdev->dev, sizeof(*m62), GFP_KERNEL);
+ if (!m62)
+ return -ENOMEM;
+
+ m62->hdev = hdev;
+ m62->intf = to_usb_interface(hdev->dev.parent);
+ if (m62->intf->cur_altsetting->desc.bInterfaceNumber !=
+ M62_VENDOR_IFNUM)
+ return -ENODEV;
+
+ spin_lock_init(&m62->lock);
+ INIT_DELAYED_WORK(&m62->keepalive, topping_keepalive);
+ hid_set_drvdata(hdev, m62);
+
+ err = devm_mutex_init(&hdev->dev, &m62->write_lock);
+ if (err)
+ return err;
+
+ err = hid_parse(hdev);
+ if (err)
+ return err;
+
+ /*
+ * HIDRAW and no input device. The descriptor would only make a
+ * nonexistent pointer, while a hidraw node is how this protocol
+ * was read in the first place and how the parts not exposed here
+ * -- the mixer matrix, the mutes, the EQ -- stay reachable.
+ */
+ err = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (err)
+ return err;
+
+ err = hid_hw_open(hdev);
+ if (err)
+ goto err_stop;
+
+ /*
+ * XXX awaiting the HID maintainers' word. usbhid arms every
+ * device it opens for remote wakeup, and this card does not
+ * offer it, which forbids runtime suspend to the whole device.
+ * Nothing here needs it: the resume path subscribes again and
+ * asks the card for its whole state, so a knob turned while the
+ * host slept is picked up by asking rather than by being told.
+ *
+ * This clear does not survive a hidraw open, which calls
+ * hid_hw_open() again -- so if it stays, HID_CONNECT_DRIVER has
+ * to replace HID_CONNECT_HIDRAW above.
+ */
+ m62->intf->needs_remote_wakeup = 0;
+
+ /*
+ * Reports are dropped for the whole of probe unless this is called,
+ * and component_add() below can bind synchronously when the audio
+ * side is already there -- which subscribes, and the device answers
+ * at once. Without this the identification wave is thrown away.
+ */
+ hid_device_io_start(hdev);
+
+ err = component_add(&hdev->dev, &topping_component_ops);
+ if (err)
+ goto err_close;
+
+ return 0;
+
+err_close:
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+ return err;
+}
+
+static void topping_remove(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ /* Runs topping_unbind() first if the audio side is bound. */
+ component_del(&hdev->dev, &topping_component_ops);
+
+ /*
+ * Unconditionally, and after component_del(): if the audio side
+ * had already unbound, the cancel there has been and gone, and
+ * a resume in between could have restarted the work. This is
+ * the last point before devm frees m62, so nothing may outlive
+ * it.
+ */
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static int topping_suspend(struct hid_device *hdev, pm_message_t message)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ return 0;
+}
+
+static int topping_resume(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ unsigned int noio;
+
+ /*
+ * Nothing to report to yet, and nothing the card needs told:
+ * the next bind does the subscribing.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (!m62->card)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usbhid's own
+ * usb_interrupt_msg() allocates a URB with GFP_KERNEL, so asking
+ * for the frame buffer politely is not enough, and reclaim here
+ * can wait on a block device that has not woken yet.
+ *
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ noio = memalloc_noio_save();
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(m62);
+
+ /*
+ * Restart the keepalive only if the audio side is still bound.
+ * Testing m62->card again, under the lock that topping_unbind()
+ * takes to clear it, is what keeps an unbind racing this
+ * function from leaving work behind that nothing will cancel.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->card)
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static const struct hid_device_id topping_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, topping_devices);
+
+static struct hid_driver topping_driver = {
+ .name = "topping-m62",
+ .id_table = topping_devices,
+ .probe = topping_probe,
+ .remove = topping_remove,
+ .raw_event = topping_raw_event,
+ .suspend = topping_suspend,
+ .resume = topping_resume,
+ .reset_resume = topping_resume,
+};
+module_hid_driver(topping_driver);
+
+MODULE_DESCRIPTION("Topping M62 vendor controls");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v2 2/2] ALSA: usb-audio: bind the Topping M62's vendor controls
2026-09-04 14:11 ` [RFC PATCH v2 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
@ 2026-09-04 14:11 ` Mikhail Gavrilov
2026-09-04 14:42 ` [RFC PATCH v3 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 14:11 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The M62's vendor controls are driven by hid-topping-m62, added in the
previous patch, which speaks a vendor protocol on the card's HID
interface. Those controls belong on the sound card that plays the
audio, not on a card of the HID driver's own.
This adds the other half of that: a component master, registered from
the M62's mixer quirk, which hands its struct snd_card to the HID
driver at bind time and takes the controls away again at unbind. The
lifetime rules are the component framework's, which is the point --
neither driver has to be told about the other's disconnect, and neither
has to guess at the other's state. The same shape binds HD-audio to the
graphics drivers in sound/hda/core/component.c, with sound as the
master there too.
The master's context lives in devres on the audio control interface
rather than in drvdata, which on a usb_interface belongs to
snd-usb-audio itself; devres_find(), keyed on the release function,
gives it back inside the callbacks, which are handed nothing but a
struct device *. It hangs off the control interface rather than off the
USB device because component_match_add() allocates the match list with
devm: on the interface that is released at unbind, while on the
usb_device it would live until the device itself was released and a
rebind would stack a second list on top.
Neither component_compare_dev() nor component_compare_dev_name() fits:
the audio side has no pointer to the HID device, and the HID device's
name carries an instance counter that is not predictable. The match is
therefore one of descent -- the HID device sits two levels below the
USB device -- and which interface it is stays the HID driver's
business, since it registers a component for the vendor interface and
for no other. That keeps sound/usb free of HID symbols and of any
opinion about this card's interface numbering.
component_master_add_with_match() returns 0 with the aggregate merely
pending when the HID driver is absent, so the card comes up either way
and grows the vendor controls if and when the other half appears.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 9 ++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 ++
sound/usb/mixer_topping.c | 173 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 ++
5 files changed, 195 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 627595e245f3..b49560efa1c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27593,6 +27593,15 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 VENDOR CONTROLS
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-topping-m62.c
+F: sound/usb/mixer_topping.c
+F: sound/usb/mixer_topping.h
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..922b0f61cbd4
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,173 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Topping M62 -- component master for the card's vendor controls.
+ *
+ * The M62's analogue gains, output volumes and source selectors are not
+ * described by the USB Audio Class. They are reached over a vendor
+ * protocol on the card's HID interface, which hid-topping-m62 speaks.
+ *
+ * This file speaks none of that protocol. It publishes the sound card to
+ * whoever drives the vendor interface, so that the controls are created on
+ * the card that plays the audio rather than on a card of their own, and are
+ * torn down when either side goes away. The lifetime rules are the
+ * component framework's, which is the point: neither driver has to guess at
+ * the other's state, and neither has to be told about the other's
+ * disconnect.
+ *
+ * The same shape binds HD-audio to the graphics drivers in
+ * sound/hda/core/component.c, with sound as the master there too.
+ */
+
+#include <linux/component.h>
+#include <linux/device.h>
+#include <linux/usb.h>
+
+#include <sound/core.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_topping.h"
+
+/*
+ * What the master hands the component at bind time. It lives in devres on
+ * the audio control interface rather than in drvdata, because drvdata on a
+ * usb_interface belongs to snd-usb-audio itself. devres_find(), keyed on
+ * the release function, gives it back inside the callbacks, which are
+ * handed nothing but a struct device *.
+ */
+struct topping_master {
+ struct device *dev; /* the audio control interface */
+ struct snd_card *card;
+};
+
+static void topping_master_release(struct device *dev, void *res)
+{
+ /* The devres allocation is the storage; nothing else to drop. */
+}
+
+static struct topping_master *topping_get_master(struct device *dev)
+{
+ return devres_find(dev, topping_master_release, NULL, NULL);
+}
+
+/*
+ * Which of the registered components is ours.
+ *
+ * This is only ever called against devices that have registered with
+ * component_add(), so it does not have to defend itself against the whole
+ * device tree. What it does have to do is tell this card's vendor
+ * function apart from a second M62 on another port.
+ *
+ * The HID device sits two levels below the USB device:
+ *
+ * hid_device -> usb_interface -> usb_device
+ *
+ * WHICH interface it is, is the HID driver's business: it registers a
+ * component for the vendor interface and for nothing else. So the test
+ * here is one of descent alone and needs no HID symbols in sound/usb --
+ * which also keeps this file free of any opinion about the M62's
+ * interface numbering.
+ */
+static int topping_match_component(struct device *dev, void *data)
+{
+ return dev->parent && dev->parent->parent == data;
+}
+
+static int topping_master_bind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return -EINVAL;
+
+ return component_bind_all(dev, tm->card);
+}
+
+static void topping_master_unbind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return;
+
+ component_unbind_all(dev, tm->card);
+}
+
+static const struct component_master_ops topping_master_ops = {
+ .bind = topping_master_bind,
+ .unbind = topping_master_unbind,
+};
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_master *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+
+ /*
+ * Reached from snd_usb_mixer_disconnect(), on an unplug and on an
+ * unbind of the audio interface alike. component_master_del() runs
+ * topping_master_unbind() on the way, so the HID side has taken its
+ * kcontrols off this card before the card is taken apart.
+ */
+ component_master_del(tm->dev, &topping_master_ops);
+ devres_destroy(tm->dev, topping_master_release, NULL, NULL);
+ mixer->private_data = NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct component_match *match = NULL;
+ struct usb_interface *intf;
+ struct topping_master *tm;
+ struct device *dev;
+ int err;
+
+ /*
+ * The master hangs off the audio control interface rather than off
+ * the USB device: component_match_add() allocates the match list
+ * with devm, and on an interface that is released when the interface
+ * is unbound. On the usb_device it would live until the device
+ * itself was released, and a rebind would stack a second list on top
+ * of the first.
+ */
+ intf = usb_ifnum_to_if(chip->dev,
+ get_iface_desc(mixer->hostif)->bInterfaceNumber);
+ if (!intf)
+ return -ENODEV;
+ dev = &intf->dev;
+
+ tm = devres_alloc(topping_master_release, sizeof(*tm), GFP_KERNEL);
+ if (!tm)
+ return -ENOMEM;
+ tm->dev = dev;
+ tm->card = chip->card;
+ devres_add(dev, tm);
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ component_match_add(dev, &match, topping_match_component,
+ &chip->dev->dev);
+
+ /*
+ * This returns 0 with the aggregate merely pending when
+ * hid-topping-m62 has not registered its component yet:
+ * try_to_bring_up_aggregate_device() reports an incomplete set as
+ * "not ready", not as an error. So the card comes up either way and
+ * grows the vendor controls if and when the other half appears.
+ */
+ err = component_master_add_with_match(dev, &topping_master_ops, match);
+ if (err < 0) {
+ mixer->private_data = NULL;
+ mixer->private_free = NULL;
+ devres_destroy(dev, topping_master_release, NULL, NULL);
+ usb_audio_err(chip, "Topping: no component master: %d\n", err);
+ return err;
+ }
+
+ return 0;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v3 0/2] the Topping M62's vendor controls, on the component framework
2026-09-04 14:11 ` [RFC PATCH v2 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
@ 2026-09-04 14:42 ` Mikhail Gavrilov
2026-09-04 14:42 ` [RFC PATCH v3 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 14:42 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
This is the experiment Takashi suggested in the v8 thread: an
individual HID driver joined to snd-usb-audio through the component
framework, instead of a mixer quirk that claims the HID interface for
itself. It is posted as an RFC rather than as v9 because v8 is not
withdrawn -- it remains the alternative, and the choice between the two
is the question this series exists to answer.
I said this morning that I would hold this until the questions in that
thread were answered. I am sending it anyway, because a patch gets a
better answer than a question does, and because the last of those
questions answered itself: the correction I sent a few hours ago
proposed deferring the controls until the card reports, and working out
how that lands in code showed why it cannot -- which is in the section
below. The only question still genuinely open is the HID one, and it
touches one line.
Changes since v2:
- topping_bind()'s error path dropped the controls before clearing
m62->card, so a report arriving while a half-built set was being
unwound could still find a control that snd_ctl_remove() had just
freed. It is the race v2 fixed in topping_unbind() and left here.
err_drop now clears the card under the lock first, as unbind does.
- snd_topping_init() passed component_match_add()'s result straight
to component_master_add_with_match(). The former reports a failed
allocation by storing ERR_PTR(-ENOMEM) in the match pointer rather
than by returning, and the latter reads match->num without an
IS_ERR() check, so an allocation failure would have dereferenced
an error pointer. There is now a check, and the two failure exits
share one label.
What the shape buys, measured rather than argued:
- card.c is not touched at all. The v8 series had to add
snd_usb_claim_iface() and snd_usb_release_iface() there, and had a
defect I reported separately: an interface claimed and marked
USB_AUDIO_IFACE_UNUSED never gets told to wind down, because
usb_audio_disconnect() returns at its first line. With no claim
there is no such interface and the defect has nothing to attach
to. Watching the wire with usbmon: unbind the audio interface and
the keepalive stops. In v8 it kept going.
- the hid_ignore_list entry goes away, so hidraw stays available.
That matters because five of the card's functions -- the mixer
matrix, the mutes, the loopback routing, the input power and the
EQ -- are reachable only through the vendor protocol, and v8 made
them unreachable from userspace as the price of the quirk.
- two M62s on one host bind to their own cards. The match is by
descent from the shared USB device, and it holds: gains set on one
do not appear on the other, and unplugging one leaves the other's
controls in place. The quirk road could not be tested for this at
all.
On the timing worry from that thread: it does not bite on an ordinary
plug. The master goes up from snd_topping_init() inside
snd_usb_create_mixer(), so the bind is synchronous and the controls
exist before try_to_register_card() -- the same ordering as v8. From
HID probe to component bind, 54 to 57 ms across five replugs. Only a
module reload onto a live card adds controls after registration, and
there a desktop mixer does not follow the renumbered elements until
wireplumber is restarted. That is a real wart and I have not found a
way around it that is worth the code.
What the card does and does not report
I got this wrong twice in the v8 thread and would rather state it
plainly here, since the design follows from it.
After a subscribe the card reports itself in two waves: jacks at about
0.9 s, then at about 5.2 s the jacks again plus the output mutes plus
the gain of every input whose jack is present. Every turn of a
front-panel knob is reported as it happens, gains and output volumes
alike. What is never reported is a source selector, because the card
reports events and a selector has no front-panel control, so no event
can exist; and an output volume before anyone has touched it, because
it is a setting rather than a physical fact.
So five of the nine controls have a source of truth on the card, two
have one only after a hand moves them, and two never do.
That looked at first like an argument for deferring the controls until
the first report lands, so that a value read at connect would be the
panel's rather than something restored over it. I have not done that,
and here is why: alsactl restores once, at card add, and does not
come back for elements that appear later. Deferring would therefore
trade an accurate value for five controls against no restore at all
for nine, and would wait forever for an input with nothing plugged
into it -- which on a five-input card is the normal case.
The controls are published at bind, as in v8, and the value at
connect is the restored one.
Open question, not blocking the review
hid_hw_open() sets intf->needs_remote_wakeup, and usbhid offers no way
to take input reports without it. This card does not advertise remote
wakeup (bmAttributes 0xc0, no power/wakeup node), so that forbids
runtime suspend to the whole device -- undoing something an earlier
revision of the v8 series had to fix. The driver clears the flag after
opening, with a comment saying so, because it resynchronises on resume
and has no use for a device-initiated wakeup. Jiri and Benjamin have
been asked whether that is acceptable or whether usbhid should offer
something; the question is in the same thread and unanswered. If the
answer is that the clear must go, HID_CONNECT_DRIVER replaces
HID_CONNECT_HIDRAW and the hidraw node goes with it.
Tested
Fedora, 7.3.0-rc1-bc35965f6940 plus this series, with KASAN,
PROVE_LOCKING and UBSAN enabled. One M62, firmware V87.05.45.48.27,
and a second of the same for the two-card test. Everything below was
run after the fixes listed above, not before them.
All nine controls read and written from amixer and from the front
panel, values cross-checked against the panel.
One hundred cycles of module load and unload against a live card,
which is the path the fixes touch: the controls come and go while the
meter stream keeps arriving, so .raw_event() runs against a set of
controls that is being taken apart. No splat and no lockdep complaint,
including on the lock order the first fix introduces -- m62->lock is
now held across snd_ctl_notify(), which takes the card's
controls_rwlock inside it.
An audio-side unbind and rebind, exercising topping_unbind() through
the master rather than through the HID driver's own remove.
The cable pulled out of a card while a control was being written in a
loop. That produces a run of failed writes and then the disconnect,
and after the last fix it does so without a line of complaint each.
System suspend and resume: the subscription survives it -- turning a
front-panel knob afterwards still moves the control -- and the
selectors are written back on the way out, as they must be since the
card never reports them.
Two cards on one host, on different ports of the same hub: the
component match is by descent from the shared USB device and it holds
-- dmesg shows snd-usb-audio 3-1.3:1.0 binding one HID device and
3-1.4.2:1.0 the other, each card carries its own nine controls, gains
set from one front panel move only that card's controls, and
unplugging one leaves the other's in place.
Not tested:
- hibernation, and with it .reset_resume, which shares
topping_resume() with .resume. This machine is not set up for it:
there is no resume= on the command line and zram takes swap
priority, so the attempt logs "PM: Image not found (code -16)" and
powers off instead of saving an image. That is the same on a stock
kernel and has nothing to do with this series, but it does mean
the path is unexercised;
- kmemleak, compiled in on this system but disabled at boot;
- a card whose battery has run down;
- anything on a big-endian host.
Ordering
The HID driver comes first. Between the two patches it binds, speaks
to the card and creates no controls, which is harmless; the reverse
order leaves a master that never matches, which is equally harmless
but leaves hid-generic making a bogus input device out of the
descriptor in the meantime.
Mikhail Gavrilov (2):
HID: topping-m62: driver for the M62's vendor controls
ALSA: usb-audio: bind the Topping M62's vendor controls
MAINTAINERS | 9 +
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 898 ++++++++++++++++++++++++++++++++++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 186 +++++++
sound/usb/mixer_topping.h | 7 +
10 files changed, 1132 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC PATCH v3 1/2] HID: topping-m62: driver for the M62's vendor controls
2026-09-04 14:42 ` [RFC PATCH v3 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
@ 2026-09-04 14:42 ` Mikhail Gavrilov
2026-09-04 14:43 ` [RFC PATCH v3 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 14:42 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The Topping M62 is a USB audio interface whose analogue input gains,
output volumes and output source selectors are not described by the USB
Audio Class. They live behind a vendor protocol on a HID-class
interface, spoken by Topping's M Control Center, which has no Linux
build. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: raising it lifts the
converter's own floor with the signal. So on Linux the only gain worth
setting is unreachable, and a measurement application has to ask a
human to set it by hand on the front panel.
The protocol was read off the vendor application's traffic. Frames are
fifteen bytes:
22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
with TT a target, PP a property of that target, and the checksum
CRC-16/MODBUS over bytes 2..10, most significant byte first. Reports
from the device are the same frame plus one pad byte. The vendor
application sends 00 00 in place of the checksum and the device accepts
it, so the device does not verify what it receives; this driver signs
its writes anyway and validates what it reads.
The card is silent until subscribed. One write of 0x11/0x24 starts the
notification stream, and it lapses unless repeated: the vendor
application sends it every two seconds and so does this driver. After
that the card reports what a hand does to its hardware -- jack states,
mutes, battery, and every turn of a front-panel knob.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints are the only
route. The report descriptor describes nothing worth having -- a
Generic Desktop application collection, eight usages stretched over
sixteen unnamed bytes in and out, no report ID -- so hid-generic makes
an input device with an ABS_MISC axis out of it and nothing else. Hence
the hid_have_special_driver entry, and HID_CONNECT_HIDRAW here with no
input device.
The controls belong on the sound card rather than on a card of this
driver's own, so this driver creates none. It registers a component;
the M62 mixer quirk in snd-usb-audio is the master and hands over its
struct snd_card at bind time. Everything created here is dropped again
at unbind, whichever half goes away first, which is what makes the two
drivers independent of each other's disconnect. The audio half is the
following patch; without it this driver binds, speaks to the card and
creates nothing, which is harmless.
Interface 3 is Application Specific / DFU and is never touched: a stray
write there can leave the card unusable. Only interface 4 grows a
component.
Nine controls: five input gains (IN 1, IN 2, AUX, BT, OTG IN), two
output volumes (HP, OTG OUT) and the two output source selectors. The
outputs come in pairs and the device announces only the second of each,
so both are written and the second is the one listened for. The mixer
matrix, the mutes, the loopback routing, the input power and the EQ
remain reachable only through the hidraw node.
The two selectors carry an extra first item, "Unknown", and start
there. The card reports what a hand does to its hardware rather than
what its settings are, and a selector has no front-panel control, so
there is no event for it to report. A host that did not write the
selector cannot learn where the output points, and no command would
help, because there is nothing for one to be built on.
That matters more than a missing readback usually would, because the
selector decides whether the card makes a sound at all. It is
independent of which PCM device the host is playing into: a host can
be feeding Playback 1/2 while the headphone output listens to Mix C,
in which case the card is playing correctly from a source nobody is
feeding, and nothing on the host says why. The card is battery powered
and runs without a host at all, so whatever the last application to
touch it left behind is what a fresh Linux system inherits.
Without the extra item the driver would have to name a source it has
not read, and the first alsactl store would turn that invention into a
setting the user never made. With it, "Unknown" is stored and restored
like any other value until a hand chooses something, and writing it
back is accepted as a no-op rather than failing a restore of the
driver's own report.
Putting the card into a known state is a UCM profile's job rather than
this driver's: a profile knows what the user is trying to do, and a
driver does not. "Unknown" is what lets a profile do that without
harm, since it distinguishes "nobody has chosen" from "the user chose
Mix B" and can leave the second alone. No profile exists yet.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 898 ++++++++++++++++++++++++++++++++++
5 files changed, 924 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..e29f018fefd2 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1283,6 +1283,25 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING_M62
+ tristate "Topping M62 vendor controls"
+ depends on USB_HID
+ depends on SND_USB_AUDIO
+ select CRC16
+ help
+ Support for the analogue input gains, output volumes and
+ output source selectors of the Topping M62 USB audio
+ interface. These are not described by the USB Audio Class
+ and are reached over a vendor protocol on the card's HID
+ interface, so a driver is needed for them to appear at all.
+
+ The controls are created on the sound card that snd-usb-audio
+ makes for the same device, so both drivers are needed. The
+ card works without this one; it simply has no gain controls.
+
+ To compile this driver as a module, choose M here: the module
+ will be called hid-topping-m62.
+
config HID_TOPSEED
tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245ee..9f854686b702 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
+obj-$(CONFIG_HID_TOPPING_M62) += hid-topping-m62.o
obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o
obj-$(CONFIG_HID_TOPRE) += hid-topre.o
obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..60ca5712cb58 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -787,6 +787,9 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE_PRO) },
#endif
+#if IS_ENABLED(CONFIG_HID_TOPPING_M62)
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+#endif
#if IS_ENABLED(CONFIG_HID_TOPSEED)
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE_2) },
diff --git a/drivers/hid/hid-topping-m62.c b/drivers/hid/hid-topping-m62.c
new file mode 100644
index 000000000000..3c1b8e7626f6
--- /dev/null
+++ b/drivers/hid/hid-topping-m62.c
@@ -0,0 +1,898 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Vendor controls for Topping interfaces behind a HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * The control pipe is not an option: GET_REPORT and SET_REPORT both
+ * stall with EPIPE for every report type, so the interrupt endpoints
+ * are the only route. The report descriptor describes nothing worth
+ * having -- a Generic Desktop application collection, eight usages
+ * stretched over sixteen unnamed bytes in and out, no report ID -- so
+ * this driver takes HID_CONNECT_HIDRAW and no input device.
+ *
+ * THE CONTROLS BELONG ON THE SOUND CARD, so this driver creates no
+ * card of its own. It registers a component; the M62 mixer quirk in
+ * snd-usb-audio is the master and hands over its struct snd_card at
+ * bind time. Everything created here is taken off again at unbind,
+ * whichever half goes away first, which is what makes the two drivers
+ * independent of each other's disconnect.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "hid-ids.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+
+/*
+ * The M62 presents two non-audio interfaces. Interface 3 is
+ * Application Specific / DFU and is never touched here. Interface 4
+ * carries the control protocol and is the only one this driver takes.
+ */
+#define M62_VENDOR_IFNUM 4
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls)
+
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+
+struct topping_m62 {
+ struct hid_device *hdev;
+ struct usb_interface *intf; /* for runtime PM */
+
+ /*
+ * NULL until the audio side binds and NULL again after it
+ * unbinds. Frames arrive before the audio half is there, so
+ * anything that reports to userspace reads this under lock.
+ */
+ struct snd_card *card;
+
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against .raw_event */
+
+ int val[TOPPING_NUM_CTLS];
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
+
+ /* the volume controls first, then the selectors */
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+};
+
+/* ------------------------------------------------------------------ */
+/* the wire */
+/* ------------------------------------------------------------------ */
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes a runtime PM reference
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has no
+ * subscription worth renewing -- resume renews it -- and the second
+ * because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ *
+ * usbhid drops a leading zero byte, taking it for a report ID this
+ * device does not use, and sends the rest on the interrupt OUT
+ * endpoint. So the fifteen bytes that reach the card are the frame
+ * and nothing else.
+ */
+static int topping_send(struct topping_m62 *m62, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_REPORT_LEN, GFP_NOIO);
+ int err;
+
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = 0; /* the report ID usbhid will drop */
+ topping_build(buf + 1, target, prop, value);
+
+ err = hid_hw_output_report(m62->hdev, buf, TOPPING_FRAME_LEN + 1);
+ if (err >= 0)
+ return 0;
+
+ /*
+ * A cable pulled out of a running card produces one of these per
+ * write until the disconnect arrives, and none of them says
+ * anything about this driver: ENODEV and ESHUTDOWN are the
+ * device already gone, EPROTO and EILSEQ the bus falling apart
+ * on the way there. Anything else is worth a line.
+ */
+ if (err != -ENODEV && err != -ESHUTDOWN &&
+ err != -EPROTO && err != -EILSEQ)
+ hid_err(m62->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ if (topping_m62_ctls[i].target == target &&
+ topping_m62_ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+/*
+ * What was the URB completion handler, minus everything usbhid now
+ * owns: there is no resubmit here and no bus-noise status to sort
+ * through. What is left is the decode.
+ *
+ * Runs in the interrupt handler's context, which is why val[] is
+ * behind a spinlock rather than the mutex.
+ */
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ int idx, value;
+
+ if (size < TOPPING_FRAME_LEN)
+ return 0;
+ if (data[0] != 0x22 || data[1] != 0x33 ||
+ data[13] != 0x66 || data[14] != 0x77)
+ return 0;
+ if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
+ return 0;
+
+ idx = topping_index_of(data[5], data[6]);
+ if (idx < 0)
+ return 0; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(data + 7);
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return 0;
+
+ /*
+ * The notify happens under the same lock that guards the cache,
+ * not after it. snd_ctl_notify() dereferences the id it is
+ * given -- it compares id->numid and copies the structure into
+ * the event -- so a concurrent topping_unbind() releasing the
+ * lock's other side could free the control in between, and the
+ * card pointer would be stale by then too. It is safe from
+ * here: it takes read_lock_irqsave and allocates with GFP_ATOMIC.
+ */
+ guard(spinlock_irqsave)(&m62->lock);
+
+ if (m62->val[idx] == value)
+ return 0;
+ m62->val[idx] = value;
+
+ if (m62->card && m62->kctl[idx])
+ snd_ctl_notify(m62->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &m62->kctl[idx]->id);
+
+ return 0;
+}
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_m62 *m62 = container_of(work, struct topping_m62,
+ keepalive.work);
+ int err;
+
+ err = topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; nothing to renew */
+
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+/* ------------------------------------------------------------------ */
+/* the volume controls */
+/* ------------------------------------------------------------------ */
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ const struct topping_ctl_desc *d;
+
+ d = &topping_m62_ctls[kctl->private_value];
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = d->min;
+ uinfo->value.integer.max = d->max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(spinlock_irqsave)(&m62->lock);
+ ucontrol->value.integer.value[0] = m62->val[kctl->private_value];
+ return 0;
+}
+
+/*
+ * Split out so that the runtime PM reference has one place to be
+ * dropped. Entered with the device awake and nothing else held.
+ */
+static int topping_ctl_write(struct topping_m62 *m62, int idx, int value)
+{
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ int prev, err;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after.
+ * The lock cannot be held across a send, and a hand on the front
+ * panel during that window produces a notification .raw_event
+ * stores; updating afterwards would throw that away and leave
+ * the driver claiming a value the device had already moved away
+ * from. Written first, the device's own report is simply the
+ * last word, which is the right bias.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ if (m62->val[idx] == value)
+ return 0;
+ prev = m62->val[idx];
+ m62->val[idx] = value;
+ }
+
+ err = topping_send(m62, d->target, d->prop, value);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(m62, d->target_pair, d->prop, value);
+ }
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->val[idx] == value)
+ m62->val[idx] = prev;
+ return err;
+ }
+
+ return 1;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run
+ * this driver's own resume callback on this very thread, and
+ * that callback takes write_lock to write the selectors back;
+ * taking write_lock first would meet it already held, by us.
+ *
+ * There is no guard against disconnect here and none is needed:
+ * snd_ctl_remove() in topping_unbind() takes controls_rwsem for
+ * writing, and no control callback can be inside it.
+ */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_ctl_write(m62, idx, value);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+/* ------------------------------------------------------------------ */
+/* the source selectors */
+/* ------------------------------------------------------------------ */
+
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(mutex)(&m62->write_lock);
+ ucontrol->value.enumerated.item[0] = m62->sel[kctl->private_value];
+ return 0;
+}
+
+static int topping_sel_write(struct topping_m62 *m62, int idx,
+ unsigned int item)
+{
+ const struct topping_enum_desc *d = &topping_m62_enums[idx];
+ int err;
+
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has
+ * chosen, and alsactl stores and restores it like any other
+ * value. It is not a choice, so writing it changes nothing --
+ * quietly, rather than failing a restore of the driver's own
+ * report.
+ */
+ if (!item || m62->sel[idx] == item)
+ return 0;
+
+ err = topping_send(m62, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ m62->sel[idx] = item;
+ return 1;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_sel_write(m62, kctl->private_value, item);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again.
+ */
+static void topping_restore_sel(struct topping_m62 *m62)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&m62->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!m62->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(m62, d->target, d->prop,
+ topping_source_value[m62->sel[i]]);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* creating and dropping the controls */
+/* ------------------------------------------------------------------ */
+
+/*
+ * v8 carried the index in a usb_mixer_elem_info, because that is what
+ * snd_usb_mixer_add_control() wants. Off the mixer there is nothing
+ * to satisfy: private_data is this driver and private_value is the
+ * index, so the per-control allocation goes away with its private_free.
+ */
+static int topping_add_kctl(struct topping_m62 *m62,
+ const struct snd_kcontrol_new *tmpl,
+ const char *name, int idx, int slot,
+ const unsigned int *tlv)
+{
+ struct snd_kcontrol *kctl;
+ int err;
+
+ kctl = snd_ctl_new1(tmpl, m62);
+ if (!kctl)
+ return -ENOMEM;
+
+ kctl->private_value = idx;
+ kctl->tlv.p = tlv;
+ strscpy(kctl->id.name, name, sizeof(kctl->id.name));
+
+ err = snd_ctl_add(m62->card, kctl);
+ if (err < 0)
+ return err; /* snd_ctl_add() freed it */
+
+ m62->kctl[slot] = kctl;
+ return 0;
+}
+
+static void topping_drop_kctls(struct topping_m62 *m62, struct snd_card *card)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_KCTLS; i++) {
+ snd_ctl_remove(card, m62->kctl[i]);
+ m62->kctl[i] = NULL;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* component */
+/* ------------------------------------------------------------------ */
+
+static int topping_bind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_card *card = master_data;
+ int i, err;
+
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = card;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ err = topping_add_kctl(m62, &topping_ctl,
+ topping_m62_ctls[i].name, i, i,
+ topping_m62_ctls[i].tlv);
+ if (err < 0)
+ goto err_drop;
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_kctl(m62, &topping_sel,
+ topping_m62_enums[i].name, i,
+ TOPPING_NUM_CTLS + i, NULL);
+ if (err < 0)
+ goto err_drop;
+ }
+
+ /*
+ * Subscribe and ask for the state HERE rather than at probe:
+ * before this point every announced value would land in the
+ * cache with no control to notify. The device answers in two
+ * waves -- identification at once, the gains about 3.7 s later,
+ * which is the same delay a phantom rail takes to settle -- so
+ * nothing here waits for them: each value lands through
+ * .raw_event and notifies its own control.
+ */
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+
+err_drop:
+ /*
+ * Same order as topping_unbind(), and for the same reason:
+ * .raw_event() can run throughout, since hid_device_io_start()
+ * was called before component_add(), and it looks at
+ * m62->kctl[] whenever m62->card is set. Clearing the card
+ * first is what stops it from reaching a control this path is
+ * about to free.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = NULL;
+
+ topping_drop_kctls(m62, card);
+ return err;
+}
+
+static void topping_unbind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_card *card = master_data;
+
+ /*
+ * Clear the card first, then cancel. m62->card is what
+ * topping_resume() tests before it restarts the keepalive, and
+ * it tests it under this lock, so once this store is visible no
+ * resume can schedule the work again and the cancel below is
+ * final. Clearing it also stops .raw_event() from looking up a
+ * control this function is about to remove.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ m62->card = NULL;
+
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ topping_drop_kctls(m62, card);
+}
+
+static const struct component_ops topping_component_ops = {
+ .bind = topping_bind,
+ .unbind = topping_unbind,
+};
+
+/* ------------------------------------------------------------------ */
+/* HID */
+/* ------------------------------------------------------------------ */
+
+static int topping_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct topping_m62 *m62;
+ int err;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ m62 = devm_kzalloc(&hdev->dev, sizeof(*m62), GFP_KERNEL);
+ if (!m62)
+ return -ENOMEM;
+
+ m62->hdev = hdev;
+ m62->intf = to_usb_interface(hdev->dev.parent);
+ if (m62->intf->cur_altsetting->desc.bInterfaceNumber !=
+ M62_VENDOR_IFNUM)
+ return -ENODEV;
+
+ spin_lock_init(&m62->lock);
+ INIT_DELAYED_WORK(&m62->keepalive, topping_keepalive);
+ hid_set_drvdata(hdev, m62);
+
+ err = devm_mutex_init(&hdev->dev, &m62->write_lock);
+ if (err)
+ return err;
+
+ err = hid_parse(hdev);
+ if (err)
+ return err;
+
+ /*
+ * HIDRAW and no input device. The descriptor would only make a
+ * nonexistent pointer, while a hidraw node is how this protocol
+ * was read in the first place and how the parts not exposed here
+ * -- the mixer matrix, the mutes, the EQ -- stay reachable.
+ */
+ err = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (err)
+ return err;
+
+ err = hid_hw_open(hdev);
+ if (err)
+ goto err_stop;
+
+ /*
+ * XXX awaiting the HID maintainers' word. usbhid arms every
+ * device it opens for remote wakeup, and this card does not
+ * offer it, which forbids runtime suspend to the whole device.
+ * Nothing here needs it: the resume path subscribes again and
+ * asks the card for its whole state, so a knob turned while the
+ * host slept is picked up by asking rather than by being told.
+ *
+ * This clear does not survive a hidraw open, which calls
+ * hid_hw_open() again -- so if it stays, HID_CONNECT_DRIVER has
+ * to replace HID_CONNECT_HIDRAW above.
+ */
+ m62->intf->needs_remote_wakeup = 0;
+
+ /*
+ * Reports are dropped for the whole of probe unless this is called,
+ * and component_add() below can bind synchronously when the audio
+ * side is already there -- which subscribes, and the device answers
+ * at once. Without this the identification wave is thrown away.
+ */
+ hid_device_io_start(hdev);
+
+ err = component_add(&hdev->dev, &topping_component_ops);
+ if (err)
+ goto err_close;
+
+ return 0;
+
+err_close:
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+ return err;
+}
+
+static void topping_remove(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ /* Runs topping_unbind() first if the audio side is bound. */
+ component_del(&hdev->dev, &topping_component_ops);
+
+ /*
+ * Unconditionally, and after component_del(): if the audio side
+ * had already unbound, the cancel there has been and gone, and
+ * a resume in between could have restarted the work. This is
+ * the last point before devm frees m62, so nothing may outlive
+ * it.
+ */
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static int topping_suspend(struct hid_device *hdev, pm_message_t message)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ return 0;
+}
+
+static int topping_resume(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ unsigned int noio;
+
+ /*
+ * Nothing to report to yet, and nothing the card needs told:
+ * the next bind does the subscribing.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (!m62->card)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usbhid's own
+ * usb_interrupt_msg() allocates a URB with GFP_KERNEL, so asking
+ * for the frame buffer politely is not enough, and reclaim here
+ * can wait on a block device that has not woken yet.
+ *
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ noio = memalloc_noio_save();
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(m62);
+
+ /*
+ * Restart the keepalive only if the audio side is still bound.
+ * Testing m62->card again, under the lock that topping_unbind()
+ * takes to clear it, is what keeps an unbind racing this
+ * function from leaving work behind that nothing will cancel.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->card)
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static const struct hid_device_id topping_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, topping_devices);
+
+static struct hid_driver topping_driver = {
+ .name = "topping-m62",
+ .id_table = topping_devices,
+ .probe = topping_probe,
+ .remove = topping_remove,
+ .raw_event = topping_raw_event,
+ .suspend = topping_suspend,
+ .resume = topping_resume,
+ .reset_resume = topping_resume,
+};
+module_hid_driver(topping_driver);
+
+MODULE_DESCRIPTION("Topping M62 vendor controls");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v3 2/2] ALSA: usb-audio: bind the Topping M62's vendor controls
2026-09-04 14:42 ` [RFC PATCH v3 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 14:42 ` [RFC PATCH v3 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
@ 2026-09-04 14:43 ` Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 14:43 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The M62's vendor controls are driven by hid-topping-m62, added in the
previous patch, which speaks a vendor protocol on the card's HID
interface. Those controls belong on the sound card that plays the
audio, not on a card of the HID driver's own.
This adds the other half of that: a component master, registered from
the M62's mixer quirk, which hands its struct snd_card to the HID
driver at bind time and takes the controls away again at unbind. The
lifetime rules are the component framework's, which is the point --
neither driver has to be told about the other's disconnect, and neither
has to guess at the other's state. The same shape binds HD-audio to the
graphics drivers in sound/hda/core/component.c, with sound as the
master there too.
The master's context lives in devres on the audio control interface
rather than in drvdata, which on a usb_interface belongs to
snd-usb-audio itself; devres_find(), keyed on the release function,
gives it back inside the callbacks, which are handed nothing but a
struct device *. It hangs off the control interface rather than off the
USB device because component_match_add() allocates the match list with
devm: on the interface that is released at unbind, while on the
usb_device it would live until the device itself was released and a
rebind would stack a second list on top.
Neither component_compare_dev() nor component_compare_dev_name() fits:
the audio side has no pointer to the HID device, and the HID device's
name carries an instance counter that is not predictable. The match is
therefore one of descent -- the HID device sits two levels below the
USB device -- and which interface it is stays the HID driver's
business, since it registers a component for the vendor interface and
for no other. That keeps sound/usb free of HID symbols and of any
opinion about this card's interface numbering.
component_master_add_with_match() returns 0 with the aggregate merely
pending when the HID driver is absent, so the card comes up either way
and grows the vendor controls if and when the other half appears.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 9 ++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 186 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 ++
5 files changed, 208 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 627595e245f3..b49560efa1c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27593,6 +27593,15 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 VENDOR CONTROLS
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-topping-m62.c
+F: sound/usb/mixer_topping.c
+F: sound/usb/mixer_topping.h
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..d97d895a4493
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,186 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Topping M62 -- component master for the card's vendor controls.
+ *
+ * The M62's analogue gains, output volumes and source selectors are not
+ * described by the USB Audio Class. They are reached over a vendor
+ * protocol on the card's HID interface, which hid-topping-m62 speaks.
+ *
+ * This file speaks none of that protocol. It publishes the sound card to
+ * whoever drives the vendor interface, so that the controls are created on
+ * the card that plays the audio rather than on a card of their own, and are
+ * torn down when either side goes away. The lifetime rules are the
+ * component framework's, which is the point: neither driver has to guess at
+ * the other's state, and neither has to be told about the other's
+ * disconnect.
+ *
+ * The same shape binds HD-audio to the graphics drivers in
+ * sound/hda/core/component.c, with sound as the master there too.
+ */
+
+#include <linux/component.h>
+#include <linux/device.h>
+#include <linux/usb.h>
+
+#include <sound/core.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_topping.h"
+
+/*
+ * What the master hands the component at bind time. It lives in devres on
+ * the audio control interface rather than in drvdata, because drvdata on a
+ * usb_interface belongs to snd-usb-audio itself. devres_find(), keyed on
+ * the release function, gives it back inside the callbacks, which are
+ * handed nothing but a struct device *.
+ */
+struct topping_master {
+ struct device *dev; /* the audio control interface */
+ struct snd_card *card;
+};
+
+static void topping_master_release(struct device *dev, void *res)
+{
+ /* The devres allocation is the storage; nothing else to drop. */
+}
+
+static struct topping_master *topping_get_master(struct device *dev)
+{
+ return devres_find(dev, topping_master_release, NULL, NULL);
+}
+
+/*
+ * Which of the registered components is ours.
+ *
+ * This is only ever called against devices that have registered with
+ * component_add(), so it does not have to defend itself against the whole
+ * device tree. What it does have to do is tell this card's vendor
+ * function apart from a second M62 on another port.
+ *
+ * The HID device sits two levels below the USB device:
+ *
+ * hid_device -> usb_interface -> usb_device
+ *
+ * WHICH interface it is, is the HID driver's business: it registers a
+ * component for the vendor interface and for nothing else. So the test
+ * here is one of descent alone and needs no HID symbols in sound/usb --
+ * which also keeps this file free of any opinion about the M62's
+ * interface numbering.
+ */
+static int topping_match_component(struct device *dev, void *data)
+{
+ return dev->parent && dev->parent->parent == data;
+}
+
+static int topping_master_bind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return -EINVAL;
+
+ return component_bind_all(dev, tm->card);
+}
+
+static void topping_master_unbind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return;
+
+ component_unbind_all(dev, tm->card);
+}
+
+static const struct component_master_ops topping_master_ops = {
+ .bind = topping_master_bind,
+ .unbind = topping_master_unbind,
+};
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_master *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+
+ /*
+ * Reached from snd_usb_mixer_disconnect(), on an unplug and on an
+ * unbind of the audio interface alike. component_master_del() runs
+ * topping_master_unbind() on the way, so the HID side has taken its
+ * kcontrols off this card before the card is taken apart.
+ */
+ component_master_del(tm->dev, &topping_master_ops);
+ devres_destroy(tm->dev, topping_master_release, NULL, NULL);
+ mixer->private_data = NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct component_match *match = NULL;
+ struct usb_interface *intf;
+ struct topping_master *tm;
+ struct device *dev;
+ int err;
+
+ /*
+ * The master hangs off the audio control interface rather than off
+ * the USB device: component_match_add() allocates the match list
+ * with devm, and on an interface that is released when the interface
+ * is unbound. On the usb_device it would live until the device
+ * itself was released, and a rebind would stack a second list on top
+ * of the first.
+ */
+ intf = usb_ifnum_to_if(chip->dev,
+ get_iface_desc(mixer->hostif)->bInterfaceNumber);
+ if (!intf)
+ return -ENODEV;
+ dev = &intf->dev;
+
+ tm = devres_alloc(topping_master_release, sizeof(*tm), GFP_KERNEL);
+ if (!tm)
+ return -ENOMEM;
+ tm->dev = dev;
+ tm->card = chip->card;
+ devres_add(dev, tm);
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ component_match_add(dev, &match, topping_match_component,
+ &chip->dev->dev);
+
+ /*
+ * component_match_add() reports a failed allocation by storing
+ * an error pointer rather than by returning, and
+ * component_master_add_with_match() dereferences what it is
+ * given without looking.
+ */
+ if (IS_ERR(match)) {
+ err = PTR_ERR(match);
+ goto err_free;
+ }
+
+ /*
+ * This returns 0 with the aggregate merely pending when
+ * hid-topping-m62 has not registered its component yet:
+ * try_to_bring_up_aggregate_device() reports an incomplete set as
+ * "not ready", not as an error. So the card comes up either way and
+ * grows the vendor controls if and when the other half appears.
+ */
+ err = component_master_add_with_match(dev, &topping_master_ops, match);
+ if (err < 0)
+ goto err_free;
+
+ return 0;
+
+err_free:
+ mixer->private_data = NULL;
+ mixer->private_free = NULL;
+ devres_destroy(dev, topping_master_release, NULL, NULL);
+ usb_audio_err(chip, "Topping: no component master: %d\n", err);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v4 0/2] the Topping M62's vendor controls, on the component framework
2026-09-04 14:42 ` [RFC PATCH v3 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 14:42 ` [RFC PATCH v3 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 14:43 ` [RFC PATCH v3 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
@ 2026-09-04 15:30 ` Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 15:30 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
This is the experiment Takashi suggested in the v8 thread: an
individual HID driver joined to snd-usb-audio through the component
framework, instead of a mixer quirk that claims the HID interface for
itself. It is posted as an RFC rather than as v9 because v8 is not
withdrawn -- it remains the alternative, and the choice between the two
is the question this series exists to answer.
I said this morning that I would hold this until the questions in that
thread were answered. I am sending it anyway, because a patch gets a
better answer than a question does, and because the last of those
questions answered itself: the correction I sent a few hours ago
proposed deferring the controls until the card reports, and working out
how that lands in code showed why it cannot -- which is in the section
below. The only question still genuinely open is the HID one, and it
touches one line.
Changes since v3:
- m62->kctl[] was written without the lock that .raw_event() takes to
read it. The object behind the pointer was safe by accident --
snd_ctl_add() takes controls_rwsem, whose release orders the
initialisation before the pointer store -- but the asymmetry itself
is a data race and reads as one.
Rather than paper over it with a barrier, the whole set is now
built into a local array, where no interrupt can reach it, and
published in one store under the lock together with m62->card.
That makes m62->card the single flag: while it is NULL,
.raw_event() never looks at m62->kctl[] at all. topping_unbind()
is its mirror, taking the set away under the lock and only then
calling snd_ctl_remove(), which sleeps and cannot run from in
there.
- with that, topping_bind() no longer needs a goto, which also
settles the second finding: cleanup.h asks that "goto" and
scope-based cleanup are never mixed in one function, and the error
path now returns normally. topping_add_kctl() and
topping_drop_kctls() work on the array they are handed and no
longer touch m62 at all.
- the master's devres context could outlive its usefulness the other
way round. usb_audio_probe() deliberately leaves the card and its
mixer list alone when a probe fails on an interface after an
earlier one succeeded, so a mixer created by that failed probe
stays on chip->mixer_list while the interface's devres -- and with
it this master's context -- is released. A later disconnect then
reached topping_private_free() with a private_data pointing at
freed storage.
topping_master_release() was empty, since the normal road goes
through devres_destroy() and never calls it. It now does the work
that road needs: the context carries a back pointer to its mixer,
and the release takes the master down and unhooks private_data and
private_free, so nothing is left pointing at it.
What the shape buys, measured rather than argued:
- card.c is not touched at all. The v8 series had to add
snd_usb_claim_iface() and snd_usb_release_iface() there, and had a
defect I reported separately: an interface claimed and marked
USB_AUDIO_IFACE_UNUSED never gets told to wind down, because
usb_audio_disconnect() returns at its first line. With no claim
there is no such interface and the defect has nothing to attach
to. Watching the wire with usbmon: unbind the audio interface and
the keepalive stops. In v8 it kept going.
- the hid_ignore_list entry goes away, so hidraw stays available.
That matters because five of the card's functions -- the mixer
matrix, the mutes, the loopback routing, the input power and the
EQ -- are reachable only through the vendor protocol, and v8 made
them unreachable from userspace as the price of the quirk.
- two M62s on one host bind to their own cards. The match is by
descent from the shared USB device, and it holds: gains set on one
do not appear on the other, and unplugging one leaves the other's
controls in place. The quirk road could not be tested for this at
all.
On the timing worry from that thread: it does not bite on an ordinary
plug. The master goes up from snd_topping_init() inside
snd_usb_create_mixer(), so the bind is synchronous and the controls
exist before try_to_register_card() -- the same ordering as v8. From
HID probe to component bind, 54 to 57 ms across five replugs. Only a
module reload onto a live card adds controls after registration, and
there a desktop mixer does not follow the renumbered elements until
wireplumber is restarted. That is a real wart and I have not found a
way around it that is worth the code.
What the card does and does not report
I got this wrong twice in the v8 thread and would rather state it
plainly here, since the design follows from it.
After a subscribe the card reports itself in two waves: jacks at about
0.9 s, then at about 5.2 s the jacks again plus the output mutes plus
the gain of every input whose jack is present. Every turn of a
front-panel knob is reported as it happens, gains and output volumes
alike. What is never reported is a source selector, because the card
reports events and a selector has no front-panel control, so no event
can exist; and an output volume before anyone has touched it, because
it is a setting rather than a physical fact.
So five of the nine controls have a source of truth on the card, two
have one only after a hand moves them, and two never do.
That looked at first like an argument for deferring the controls until
the first report lands, so that a value read at connect would be the
panel's rather than something restored over it. I have not done that,
and here is why: alsactl restores once, at card add, and does not
come back for elements that appear later. Deferring would therefore
trade an accurate value for five controls against no restore at all
for nine, and would wait forever for an input with nothing plugged
into it -- which on a five-input card is the normal case.
The controls are published at bind, as in v8, and the value at
connect is the restored one.
Open question, not blocking the review
hid_hw_open() sets intf->needs_remote_wakeup, and usbhid offers no way
to take input reports without it. This card does not advertise remote
wakeup (bmAttributes 0xc0, no power/wakeup node), so that forbids
runtime suspend to the whole device -- undoing something an earlier
revision of the v8 series had to fix. The driver clears the flag after
opening, with a comment saying so, because it resynchronises on resume
and has no use for a device-initiated wakeup. Jiri and Benjamin have
been asked whether that is acceptable or whether usbhid should offer
something; the question is in the same thread and unanswered. If the
answer is that the clear must go, HID_CONNECT_DRIVER replaces
HID_CONNECT_HIDRAW and the hidraw node goes with it.
Tested
Fedora, 7.3.0-rc1-bc35965f6940 plus this series, with KASAN,
PROVE_LOCKING and UBSAN enabled. One M62, firmware V87.05.45.48.27,
and a second of the same for the two-card test. Everything below was
run after the fixes listed above, not before them.
All nine controls read and written from amixer and from the front
panel, values cross-checked against the panel.
One hundred cycles of module load and unload against a live card,
which is the path the fixes touch: the controls come and go while the
meter stream keeps arriving, so .raw_event() runs against a set of
controls that is being taken apart. No splat and no lockdep complaint,
including on the lock order the first fix introduces -- m62->lock is
now held across snd_ctl_notify(), which takes the card's
controls_rwlock inside it.
An audio-side unbind and rebind, exercising topping_unbind() through
the master rather than through the HID driver's own remove.
The cable pulled out of a card while a control was being written in a
loop. That produces a run of failed writes and then the disconnect,
and after the last fix it does so without a line of complaint each.
System suspend and resume: the subscription survives it -- turning a
front-panel knob afterwards still moves the control -- and the
selectors are written back on the way out, as they must be since the
card never reports them.
Two cards on one host, on different ports of the same hub: the
component match is by descent from the shared USB device and it holds
-- dmesg shows snd-usb-audio 3-1.3:1.0 binding one HID device and
3-1.4.2:1.0 the other, each card carries its own nine controls, gains
set from one front panel move only that card's controls, and
unplugging one leaves the other's in place.
Not tested:
- hibernation, and with it .reset_resume, which shares
topping_resume() with .resume. This machine is not set up for it:
there is no resume= on the command line and zram takes swap
priority, so the attempt logs "PM: Image not found (code -16)" and
powers off instead of saving an image. That is the same on a stock
kernel and has nothing to do with this series, but it does mean
the path is unexercised;
- kmemleak, compiled in on this system but disabled at boot;
- a card whose battery has run down;
- anything on a big-endian host.
Ordering
The HID driver comes first. Between the two patches it binds, speaks
to the card and creates no controls, which is harmless; the reverse
order leaves a master that never matches, which is equally harmless
but leaves hid-generic making a bogus input device out of the
descriptor in the meantime.
Mikhail Gavrilov (2):
HID: topping-m62: driver for the M62's vendor controls
ALSA: usb-audio: bind the Topping M62's vendor controls
MAINTAINERS | 9 +
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 916 ++++++++++++++++++++++++++++++++++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 204 ++++++++
sound/usb/mixer_topping.h | 7 +
10 files changed, 1168 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC PATCH v4 1/2] HID: topping-m62: driver for the M62's vendor controls
2026-09-04 15:30 ` [RFC PATCH v4 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
@ 2026-09-04 15:30 ` Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 15:30 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The Topping M62 is a USB audio interface whose analogue input gains,
output volumes and output source selectors are not described by the USB
Audio Class. They live behind a vendor protocol on a HID-class
interface, spoken by Topping's M Control Center, which has no Linux
build. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: raising it lifts the
converter's own floor with the signal. So on Linux the only gain worth
setting is unreachable, and a measurement application has to ask a
human to set it by hand on the front panel.
The protocol was read off the vendor application's traffic. Frames are
fifteen bytes:
22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
with TT a target, PP a property of that target, and the checksum
CRC-16/MODBUS over bytes 2..10, most significant byte first. Reports
from the device are the same frame plus one pad byte. The vendor
application sends 00 00 in place of the checksum and the device accepts
it, so the device does not verify what it receives; this driver signs
its writes anyway and validates what it reads.
The card is silent until subscribed. One write of 0x11/0x24 starts the
notification stream, and it lapses unless repeated: the vendor
application sends it every two seconds and so does this driver. After
that the card reports what a hand does to its hardware -- jack states,
mutes, battery, and every turn of a front-panel knob.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints are the only
route. The report descriptor describes nothing worth having -- a
Generic Desktop application collection, eight usages stretched over
sixteen unnamed bytes in and out, no report ID -- so hid-generic makes
an input device with an ABS_MISC axis out of it and nothing else. Hence
the hid_have_special_driver entry, and HID_CONNECT_HIDRAW here with no
input device.
The controls belong on the sound card rather than on a card of this
driver's own, so this driver creates none. It registers a component;
the M62 mixer quirk in snd-usb-audio is the master and hands over its
struct snd_card at bind time. Everything created here is dropped again
at unbind, whichever half goes away first, which is what makes the two
drivers independent of each other's disconnect. The audio half is the
following patch; without it this driver binds, speaks to the card and
creates nothing, which is harmless.
Interface 3 is Application Specific / DFU and is never touched: a stray
write there can leave the card unusable. Only interface 4 grows a
component.
Nine controls: five input gains (IN 1, IN 2, AUX, BT, OTG IN), two
output volumes (HP, OTG OUT) and the two output source selectors. The
outputs come in pairs and the device announces only the second of each,
so both are written and the second is the one listened for. The mixer
matrix, the mutes, the loopback routing, the input power and the EQ
remain reachable only through the hidraw node.
The two selectors carry an extra first item, "Unknown", and start
there. The card reports what a hand does to its hardware rather than
what its settings are, and a selector has no front-panel control, so
there is no event for it to report. A host that did not write the
selector cannot learn where the output points, and no command would
help, because there is nothing for one to be built on.
That matters more than a missing readback usually would, because the
selector decides whether the card makes a sound at all. It is
independent of which PCM device the host is playing into: a host can
be feeding Playback 1/2 while the headphone output listens to Mix C,
in which case the card is playing correctly from a source nobody is
feeding, and nothing on the host says why. The card is battery powered
and runs without a host at all, so whatever the last application to
touch it left behind is what a fresh Linux system inherits.
Without the extra item the driver would have to name a source it has
not read, and the first alsactl store would turn that invention into a
setting the user never made. With it, "Unknown" is stored and restored
like any other value until a hand chooses something, and writing it
back is accepted as a no-op rather than failing a restore of the
driver's own report.
Putting the card into a known state is a UCM profile's job rather than
this driver's: a profile knows what the user is trying to do, and a
driver does not. "Unknown" is what lets a profile do that without
harm, since it distinguishes "nobody has chosen" from "the user chose
Mix B" and can leave the second alone. No profile exists yet.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 916 ++++++++++++++++++++++++++++++++++
5 files changed, 942 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..e29f018fefd2 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1283,6 +1283,25 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING_M62
+ tristate "Topping M62 vendor controls"
+ depends on USB_HID
+ depends on SND_USB_AUDIO
+ select CRC16
+ help
+ Support for the analogue input gains, output volumes and
+ output source selectors of the Topping M62 USB audio
+ interface. These are not described by the USB Audio Class
+ and are reached over a vendor protocol on the card's HID
+ interface, so a driver is needed for them to appear at all.
+
+ The controls are created on the sound card that snd-usb-audio
+ makes for the same device, so both drivers are needed. The
+ card works without this one; it simply has no gain controls.
+
+ To compile this driver as a module, choose M here: the module
+ will be called hid-topping-m62.
+
config HID_TOPSEED
tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245ee..9f854686b702 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
+obj-$(CONFIG_HID_TOPPING_M62) += hid-topping-m62.o
obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o
obj-$(CONFIG_HID_TOPRE) += hid-topre.o
obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..60ca5712cb58 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -787,6 +787,9 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE_PRO) },
#endif
+#if IS_ENABLED(CONFIG_HID_TOPPING_M62)
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+#endif
#if IS_ENABLED(CONFIG_HID_TOPSEED)
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE_2) },
diff --git a/drivers/hid/hid-topping-m62.c b/drivers/hid/hid-topping-m62.c
new file mode 100644
index 000000000000..c691f9ae861d
--- /dev/null
+++ b/drivers/hid/hid-topping-m62.c
@@ -0,0 +1,916 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Vendor controls for Topping interfaces behind a HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * The control pipe is not an option: GET_REPORT and SET_REPORT both
+ * stall with EPIPE for every report type, so the interrupt endpoints
+ * are the only route. The report descriptor describes nothing worth
+ * having -- a Generic Desktop application collection, eight usages
+ * stretched over sixteen unnamed bytes in and out, no report ID -- so
+ * this driver takes HID_CONNECT_HIDRAW and no input device.
+ *
+ * THE CONTROLS BELONG ON THE SOUND CARD, so this driver creates no
+ * card of its own. It registers a component; the M62 mixer quirk in
+ * snd-usb-audio is the master and hands over its struct snd_card at
+ * bind time. Everything created here is taken off again at unbind,
+ * whichever half goes away first, which is what makes the two drivers
+ * independent of each other's disconnect.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "hid-ids.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+
+/*
+ * The M62 presents two non-audio interfaces. Interface 3 is
+ * Application Specific / DFU and is never touched here. Interface 4
+ * carries the control protocol and is the only one this driver takes.
+ */
+#define M62_VENDOR_IFNUM 4
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls)
+
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+
+struct topping_m62 {
+ struct hid_device *hdev;
+ struct usb_interface *intf; /* for runtime PM */
+
+ /*
+ * NULL until the audio side binds and NULL again after it
+ * unbinds. Frames arrive before the audio half is there, so
+ * anything that reports to userspace reads this under lock.
+ */
+ struct snd_card *card;
+
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against .raw_event */
+
+ int val[TOPPING_NUM_CTLS];
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
+
+ /* the volume controls first, then the selectors */
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+};
+
+/* ------------------------------------------------------------------ */
+/* the wire */
+/* ------------------------------------------------------------------ */
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes a runtime PM reference
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has no
+ * subscription worth renewing -- resume renews it -- and the second
+ * because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ *
+ * usbhid drops a leading zero byte, taking it for a report ID this
+ * device does not use, and sends the rest on the interrupt OUT
+ * endpoint. So the fifteen bytes that reach the card are the frame
+ * and nothing else.
+ */
+static int topping_send(struct topping_m62 *m62, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_REPORT_LEN, GFP_NOIO);
+ int err;
+
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = 0; /* the report ID usbhid will drop */
+ topping_build(buf + 1, target, prop, value);
+
+ err = hid_hw_output_report(m62->hdev, buf, TOPPING_FRAME_LEN + 1);
+ if (err >= 0)
+ return 0;
+
+ /*
+ * A cable pulled out of a running card produces one of these per
+ * write until the disconnect arrives, and none of them says
+ * anything about this driver: ENODEV and ESHUTDOWN are the
+ * device already gone, EPROTO and EILSEQ the bus falling apart
+ * on the way there. Anything else is worth a line.
+ */
+ if (err != -ENODEV && err != -ESHUTDOWN &&
+ err != -EPROTO && err != -EILSEQ)
+ hid_err(m62->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ if (topping_m62_ctls[i].target == target &&
+ topping_m62_ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+/*
+ * What was the URB completion handler, minus everything usbhid now
+ * owns: there is no resubmit here and no bus-noise status to sort
+ * through. What is left is the decode.
+ *
+ * Runs in the interrupt handler's context, which is why val[] is
+ * behind a spinlock rather than the mutex.
+ */
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ int idx, value;
+
+ if (size < TOPPING_FRAME_LEN)
+ return 0;
+ if (data[0] != 0x22 || data[1] != 0x33 ||
+ data[13] != 0x66 || data[14] != 0x77)
+ return 0;
+ if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
+ return 0;
+
+ idx = topping_index_of(data[5], data[6]);
+ if (idx < 0)
+ return 0; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(data + 7);
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return 0;
+
+ /*
+ * The notify happens under the same lock that guards the cache,
+ * not after it. snd_ctl_notify() dereferences the id it is
+ * given -- it compares id->numid and copies the structure into
+ * the event -- so a concurrent topping_unbind() releasing the
+ * lock's other side could free the control in between, and the
+ * card pointer would be stale by then too. It is safe from
+ * here: it takes read_lock_irqsave and allocates with GFP_ATOMIC.
+ */
+ guard(spinlock_irqsave)(&m62->lock);
+
+ if (m62->val[idx] == value)
+ return 0;
+ m62->val[idx] = value;
+
+ if (m62->card && m62->kctl[idx])
+ snd_ctl_notify(m62->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &m62->kctl[idx]->id);
+
+ return 0;
+}
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_m62 *m62 = container_of(work, struct topping_m62,
+ keepalive.work);
+ int err;
+
+ err = topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; nothing to renew */
+
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+/* ------------------------------------------------------------------ */
+/* the volume controls */
+/* ------------------------------------------------------------------ */
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ const struct topping_ctl_desc *d;
+
+ d = &topping_m62_ctls[kctl->private_value];
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = d->min;
+ uinfo->value.integer.max = d->max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(spinlock_irqsave)(&m62->lock);
+ ucontrol->value.integer.value[0] = m62->val[kctl->private_value];
+ return 0;
+}
+
+/*
+ * Split out so that the runtime PM reference has one place to be
+ * dropped. Entered with the device awake and nothing else held.
+ */
+static int topping_ctl_write(struct topping_m62 *m62, int idx, int value)
+{
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ int prev, err;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after.
+ * The lock cannot be held across a send, and a hand on the front
+ * panel during that window produces a notification .raw_event
+ * stores; updating afterwards would throw that away and leave
+ * the driver claiming a value the device had already moved away
+ * from. Written first, the device's own report is simply the
+ * last word, which is the right bias.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ if (m62->val[idx] == value)
+ return 0;
+ prev = m62->val[idx];
+ m62->val[idx] = value;
+ }
+
+ err = topping_send(m62, d->target, d->prop, value);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(m62, d->target_pair, d->prop, value);
+ }
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->val[idx] == value)
+ m62->val[idx] = prev;
+ return err;
+ }
+
+ return 1;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run
+ * this driver's own resume callback on this very thread, and
+ * that callback takes write_lock to write the selectors back;
+ * taking write_lock first would meet it already held, by us.
+ *
+ * There is no guard against disconnect here and none is needed:
+ * snd_ctl_remove() in topping_unbind() takes controls_rwsem for
+ * writing, and no control callback can be inside it.
+ */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_ctl_write(m62, idx, value);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+/* ------------------------------------------------------------------ */
+/* the source selectors */
+/* ------------------------------------------------------------------ */
+
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(mutex)(&m62->write_lock);
+ ucontrol->value.enumerated.item[0] = m62->sel[kctl->private_value];
+ return 0;
+}
+
+static int topping_sel_write(struct topping_m62 *m62, int idx,
+ unsigned int item)
+{
+ const struct topping_enum_desc *d = &topping_m62_enums[idx];
+ int err;
+
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has
+ * chosen, and alsactl stores and restores it like any other
+ * value. It is not a choice, so writing it changes nothing --
+ * quietly, rather than failing a restore of the driver's own
+ * report.
+ */
+ if (!item || m62->sel[idx] == item)
+ return 0;
+
+ err = topping_send(m62, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ m62->sel[idx] = item;
+ return 1;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_sel_write(m62, kctl->private_value, item);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again.
+ */
+static void topping_restore_sel(struct topping_m62 *m62)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&m62->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!m62->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(m62, d->target, d->prop,
+ topping_source_value[m62->sel[i]]);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* creating and dropping the controls */
+/* ------------------------------------------------------------------ */
+
+/*
+ * v8 carried the index in a usb_mixer_elem_info, because that is what
+ * snd_usb_mixer_add_control() wants. Off the mixer there is nothing
+ * to satisfy: private_data is this driver and private_value is the
+ * index, so the per-control allocation goes away with its private_free.
+ */
+/*
+ * Builds into the caller's array rather than into m62->kctl[], which
+ * is only ever touched under m62->lock: an interrupt can arrive at any
+ * point here, since hid_device_io_start() ran before component_add().
+ * Nothing this function makes is visible to .raw_event() until the
+ * whole set is published, below.
+ */
+static int topping_add_kctl(struct topping_m62 *m62, struct snd_card *card,
+ struct snd_kcontrol **kctl,
+ const struct snd_kcontrol_new *tmpl,
+ const char *name, int idx, int slot,
+ const unsigned int *tlv)
+{
+ struct snd_kcontrol *k;
+ int err;
+
+ k = snd_ctl_new1(tmpl, m62);
+ if (!k)
+ return -ENOMEM;
+
+ k->private_value = idx;
+ k->tlv.p = tlv;
+ strscpy(k->id.name, name, sizeof(k->id.name));
+
+ err = snd_ctl_add(card, k);
+ if (err < 0)
+ return err; /* snd_ctl_add() freed it */
+
+ kctl[slot] = k;
+ return 0;
+}
+
+static void topping_drop_kctls(struct snd_card *card,
+ struct snd_kcontrol **kctl)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_KCTLS; i++)
+ snd_ctl_remove(card, kctl[i]);
+}
+
+/* ------------------------------------------------------------------ */
+/* component */
+/* ------------------------------------------------------------------ */
+
+static int topping_build_kctls(struct topping_m62 *m62, struct snd_card *card,
+ struct snd_kcontrol **kctl)
+{
+ int i, err;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ err = topping_add_kctl(m62, card, kctl, &topping_ctl,
+ topping_m62_ctls[i].name, i, i,
+ topping_m62_ctls[i].tlv);
+ if (err < 0)
+ return err;
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_kctl(m62, card, kctl, &topping_sel,
+ topping_m62_enums[i].name, i,
+ TOPPING_NUM_CTLS + i, NULL);
+ if (err < 0)
+ return err;
+ }
+ return 0;
+}
+
+static int topping_bind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS] = { };
+ struct snd_card *card = master_data;
+ int err;
+
+ err = topping_build_kctls(m62, card, kctl);
+ if (err < 0) {
+ topping_drop_kctls(card, kctl);
+ return err;
+ }
+
+ /*
+ * One publication, and m62->card is the flag for it: until this
+ * store, .raw_event() sees a NULL card and never looks at
+ * m62->kctl[] at all, so the set above was built where no
+ * interrupt could reach it. Both fields are written here under
+ * the lock that every reader takes.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ memcpy(m62->kctl, kctl, sizeof(m62->kctl));
+ m62->card = card;
+ }
+
+ /*
+ * Subscribe and ask for the state HERE rather than at probe:
+ * before this point every announced value would land in the
+ * cache with no control to notify. The card answers in two
+ * waves -- the jacks at once, the gain of a connected input
+ * about 5 s later -- so nothing here waits for them: each value
+ * arrives through .raw_event() and notifies its own control.
+ */
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+}
+
+static void topping_unbind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+ struct snd_card *card = master_data;
+
+ /*
+ * The mirror of the publication above: take the whole set away
+ * under the lock, so that once this scope ends no reader can
+ * reach a control, and only then remove them -- snd_ctl_remove()
+ * sleeps and cannot be called from in here.
+ *
+ * Clearing the card also stops topping_resume() from restarting
+ * the keepalive, which makes the cancel below final.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ memcpy(kctl, m62->kctl, sizeof(kctl));
+ memset(m62->kctl, 0, sizeof(m62->kctl));
+ m62->card = NULL;
+ }
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ topping_drop_kctls(card, kctl);
+}
+
+static const struct component_ops topping_component_ops = {
+ .bind = topping_bind,
+ .unbind = topping_unbind,
+};
+
+/* ------------------------------------------------------------------ */
+/* HID */
+/* ------------------------------------------------------------------ */
+
+static int topping_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct topping_m62 *m62;
+ int err;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ m62 = devm_kzalloc(&hdev->dev, sizeof(*m62), GFP_KERNEL);
+ if (!m62)
+ return -ENOMEM;
+
+ m62->hdev = hdev;
+ m62->intf = to_usb_interface(hdev->dev.parent);
+ if (m62->intf->cur_altsetting->desc.bInterfaceNumber !=
+ M62_VENDOR_IFNUM)
+ return -ENODEV;
+
+ spin_lock_init(&m62->lock);
+ INIT_DELAYED_WORK(&m62->keepalive, topping_keepalive);
+ hid_set_drvdata(hdev, m62);
+
+ err = devm_mutex_init(&hdev->dev, &m62->write_lock);
+ if (err)
+ return err;
+
+ err = hid_parse(hdev);
+ if (err)
+ return err;
+
+ /*
+ * HIDRAW and no input device. The descriptor would only make a
+ * nonexistent pointer, while a hidraw node is how this protocol
+ * was read in the first place and how the parts not exposed here
+ * -- the mixer matrix, the mutes, the EQ -- stay reachable.
+ */
+ err = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (err)
+ return err;
+
+ err = hid_hw_open(hdev);
+ if (err)
+ goto err_stop;
+
+ /*
+ * XXX awaiting the HID maintainers' word. usbhid arms every
+ * device it opens for remote wakeup, and this card does not
+ * offer it, which forbids runtime suspend to the whole device.
+ * Nothing here needs it: the resume path subscribes again and
+ * asks the card for its whole state, so a knob turned while the
+ * host slept is picked up by asking rather than by being told.
+ *
+ * This clear does not survive a hidraw open, which calls
+ * hid_hw_open() again -- so if it stays, HID_CONNECT_DRIVER has
+ * to replace HID_CONNECT_HIDRAW above.
+ */
+ m62->intf->needs_remote_wakeup = 0;
+
+ /*
+ * Reports are dropped for the whole of probe unless this is called,
+ * and component_add() below can bind synchronously when the audio
+ * side is already there -- which subscribes, and the device answers
+ * at once. Without this the identification wave is thrown away.
+ */
+ hid_device_io_start(hdev);
+
+ err = component_add(&hdev->dev, &topping_component_ops);
+ if (err)
+ goto err_close;
+
+ return 0;
+
+err_close:
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+ return err;
+}
+
+static void topping_remove(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ /* Runs topping_unbind() first if the audio side is bound. */
+ component_del(&hdev->dev, &topping_component_ops);
+
+ /*
+ * Unconditionally, and after component_del(): if the audio side
+ * had already unbound, the cancel there has been and gone, and
+ * a resume in between could have restarted the work. This is
+ * the last point before devm frees m62, so nothing may outlive
+ * it.
+ */
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static int topping_suspend(struct hid_device *hdev, pm_message_t message)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ return 0;
+}
+
+static int topping_resume(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ unsigned int noio;
+
+ /*
+ * Nothing to report to yet, and nothing the card needs told:
+ * the next bind does the subscribing.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (!m62->card)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usbhid's own
+ * usb_interrupt_msg() allocates a URB with GFP_KERNEL, so asking
+ * for the frame buffer politely is not enough, and reclaim here
+ * can wait on a block device that has not woken yet.
+ *
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ noio = memalloc_noio_save();
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(m62);
+
+ /*
+ * Restart the keepalive only if the audio side is still bound.
+ * Testing m62->card again, under the lock that topping_unbind()
+ * takes to clear it, is what keeps an unbind racing this
+ * function from leaving work behind that nothing will cancel.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->card)
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static const struct hid_device_id topping_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, topping_devices);
+
+static struct hid_driver topping_driver = {
+ .name = "topping-m62",
+ .id_table = topping_devices,
+ .probe = topping_probe,
+ .remove = topping_remove,
+ .raw_event = topping_raw_event,
+ .suspend = topping_suspend,
+ .resume = topping_resume,
+ .reset_resume = topping_resume,
+};
+module_hid_driver(topping_driver);
+
+MODULE_DESCRIPTION("Topping M62 vendor controls");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v4 2/2] ALSA: usb-audio: bind the Topping M62's vendor controls
2026-09-04 15:30 ` [RFC PATCH v4 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
@ 2026-09-04 15:30 ` Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 15:30 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The M62's vendor controls are driven by hid-topping-m62, added in the
previous patch, which speaks a vendor protocol on the card's HID
interface. Those controls belong on the sound card that plays the
audio, not on a card of the HID driver's own.
This adds the other half of that: a component master, registered from
the M62's mixer quirk, which hands its struct snd_card to the HID
driver at bind time and takes the controls away again at unbind. The
lifetime rules are the component framework's, which is the point --
neither driver has to be told about the other's disconnect, and neither
has to guess at the other's state. The same shape binds HD-audio to the
graphics drivers in sound/hda/core/component.c, with sound as the
master there too.
The master's context lives in devres on the audio control interface
rather than in drvdata, which on a usb_interface belongs to
snd-usb-audio itself; devres_find(), keyed on the release function,
gives it back inside the callbacks, which are handed nothing but a
struct device *. It hangs off the control interface rather than off the
USB device because component_match_add() allocates the match list with
devm: on the interface that is released at unbind, while on the
usb_device it would live until the device itself was released and a
rebind would stack a second list on top.
Neither component_compare_dev() nor component_compare_dev_name() fits:
the audio side has no pointer to the HID device, and the HID device's
name carries an instance counter that is not predictable. The match is
therefore one of descent -- the HID device sits two levels below the
USB device -- and which interface it is stays the HID driver's
business, since it registers a component for the vendor interface and
for no other. That keeps sound/usb free of HID symbols and of any
opinion about this card's interface numbering.
component_master_add_with_match() returns 0 with the aggregate merely
pending when the HID driver is absent, so the card comes up either way
and grows the vendor controls if and when the other half appears.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 9 ++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 204 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 ++
5 files changed, 226 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 627595e245f3..b49560efa1c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27593,6 +27593,15 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 VENDOR CONTROLS
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-topping-m62.c
+F: sound/usb/mixer_topping.c
+F: sound/usb/mixer_topping.h
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..15217286842a
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Topping M62 -- component master for the card's vendor controls.
+ *
+ * The M62's analogue gains, output volumes and source selectors are not
+ * described by the USB Audio Class. They are reached over a vendor
+ * protocol on the card's HID interface, which hid-topping-m62 speaks.
+ *
+ * This file speaks none of that protocol. It publishes the sound card to
+ * whoever drives the vendor interface, so that the controls are created on
+ * the card that plays the audio rather than on a card of their own, and are
+ * torn down when either side goes away. The lifetime rules are the
+ * component framework's, which is the point: neither driver has to guess at
+ * the other's state, and neither has to be told about the other's
+ * disconnect.
+ *
+ * The same shape binds HD-audio to the graphics drivers in
+ * sound/hda/core/component.c, with sound as the master there too.
+ */
+
+#include <linux/component.h>
+#include <linux/device.h>
+#include <linux/usb.h>
+
+#include <sound/core.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_topping.h"
+
+/*
+ * What the master hands the component at bind time. It lives in devres on
+ * the audio control interface rather than in drvdata, because drvdata on a
+ * usb_interface belongs to snd-usb-audio itself. devres_find(), keyed on
+ * the release function, gives it back inside the callbacks, which are
+ * handed nothing but a struct device *.
+ */
+struct topping_master {
+ struct device *dev; /* the audio control interface */
+ struct snd_card *card;
+ struct usb_mixer_interface *mixer;
+};
+
+static const struct component_master_ops topping_master_ops;
+
+/*
+ * Ordinarily the master is taken down by topping_private_free() below,
+ * which calls devres_destroy() and so never reaches here. This runs on
+ * the other road: a probe that got as far as creating this mixer and
+ * then failed. usb_audio_probe() leaves the card and its mixer list
+ * alone in that case, as long as some earlier interface had succeeded,
+ * so the mixer outlives the interface whose devres this is -- and with
+ * it a private_data pointing at storage about to be freed. Take the
+ * master down and unhook ourselves before that can happen.
+ */
+static void topping_master_release(struct device *dev, void *res)
+{
+ struct topping_master *tm = res;
+
+ component_master_del(tm->dev, &topping_master_ops);
+ tm->mixer->private_data = NULL;
+ tm->mixer->private_free = NULL;
+}
+
+static struct topping_master *topping_get_master(struct device *dev)
+{
+ return devres_find(dev, topping_master_release, NULL, NULL);
+}
+
+/*
+ * Which of the registered components is ours.
+ *
+ * This is only ever called against devices that have registered with
+ * component_add(), so it does not have to defend itself against the whole
+ * device tree. What it does have to do is tell this card's vendor
+ * function apart from a second M62 on another port.
+ *
+ * The HID device sits two levels below the USB device:
+ *
+ * hid_device -> usb_interface -> usb_device
+ *
+ * WHICH interface it is, is the HID driver's business: it registers a
+ * component for the vendor interface and for nothing else. So the test
+ * here is one of descent alone and needs no HID symbols in sound/usb --
+ * which also keeps this file free of any opinion about the M62's
+ * interface numbering.
+ */
+static int topping_match_component(struct device *dev, void *data)
+{
+ return dev->parent && dev->parent->parent == data;
+}
+
+static int topping_master_bind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return -EINVAL;
+
+ return component_bind_all(dev, tm->card);
+}
+
+static void topping_master_unbind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return;
+
+ component_unbind_all(dev, tm->card);
+}
+
+static const struct component_master_ops topping_master_ops = {
+ .bind = topping_master_bind,
+ .unbind = topping_master_unbind,
+};
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_master *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+
+ /*
+ * Reached from snd_usb_mixer_disconnect(), on an unplug and on an
+ * unbind of the audio interface alike. component_master_del() runs
+ * topping_master_unbind() on the way, so the HID side has taken its
+ * kcontrols off this card before the card is taken apart.
+ */
+ component_master_del(tm->dev, &topping_master_ops);
+ devres_destroy(tm->dev, topping_master_release, NULL, NULL);
+ mixer->private_data = NULL;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct component_match *match = NULL;
+ struct usb_interface *intf;
+ struct topping_master *tm;
+ struct device *dev;
+ int err;
+
+ /*
+ * The master hangs off the audio control interface rather than off
+ * the USB device: component_match_add() allocates the match list
+ * with devm, and on an interface that is released when the interface
+ * is unbound. On the usb_device it would live until the device
+ * itself was released, and a rebind would stack a second list on top
+ * of the first.
+ */
+ intf = usb_ifnum_to_if(chip->dev,
+ get_iface_desc(mixer->hostif)->bInterfaceNumber);
+ if (!intf)
+ return -ENODEV;
+ dev = &intf->dev;
+
+ tm = devres_alloc(topping_master_release, sizeof(*tm), GFP_KERNEL);
+ if (!tm)
+ return -ENOMEM;
+ tm->dev = dev;
+ tm->card = chip->card;
+ tm->mixer = mixer;
+ devres_add(dev, tm);
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ component_match_add(dev, &match, topping_match_component,
+ &chip->dev->dev);
+
+ /*
+ * component_match_add() reports a failed allocation by storing
+ * an error pointer rather than by returning, and
+ * component_master_add_with_match() dereferences what it is
+ * given without looking.
+ */
+ if (IS_ERR(match)) {
+ err = PTR_ERR(match);
+ goto err_free;
+ }
+
+ /*
+ * This returns 0 with the aggregate merely pending when
+ * hid-topping-m62 has not registered its component yet:
+ * try_to_bring_up_aggregate_device() reports an incomplete set as
+ * "not ready", not as an error. So the card comes up either way and
+ * grows the vendor controls if and when the other half appears.
+ */
+ err = component_master_add_with_match(dev, &topping_master_ops, match);
+ if (err < 0)
+ goto err_free;
+
+ return 0;
+
+err_free:
+ mixer->private_data = NULL;
+ mixer->private_free = NULL;
+ devres_destroy(dev, topping_master_release, NULL, NULL);
+ usb_audio_err(chip, "Topping: no component master: %d\n", err);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v5 0/2] the Topping M62's vendor controls, on the component framework
2026-09-04 15:30 ` [RFC PATCH v4 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
@ 2026-09-04 16:22 ` Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
` (2 more replies)
2 siblings, 3 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 16:22 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
This is the experiment Takashi suggested in the v8 thread: an
individual HID driver joined to snd-usb-audio through the component
framework, instead of a mixer quirk that claims the HID interface for
itself. It is posted as an RFC rather than as v9 because v8 is not
withdrawn -- it remains the alternative, and the choice between the two
is the question this series exists to answer.
I said this morning that I would hold this until the questions in that
thread were answered. I am sending it anyway, because a patch gets a
better answer than a question does, and because the last of those
questions answered itself: the correction I sent a few hours ago
proposed deferring the controls until the card reports, and working out
how that lands in code showed why it cannot -- which is in the section
below. The only question still genuinely open is the HID one, and it
touches one line.
Changes since v4:
- the master's teardown could not work from the path it was written
for. topping_master_release() called component_master_del(), which
calls topping_master_unbind(), which looked its own context up with
devres_find() -- and devres_release_all() moves every node off the
device before it calls a single release, so that lookup always came
back empty. The WARN_ON fired and the unbind was skipped, leaving
the component bound to a master that no longer existed.
Worse, the skip was load-bearing: component_unbind_all() walks the
match array, and component_match_add() had put that array in devres
after the context, so devres releases it first. Had the unbind gone
ahead it would have walked freed memory.
Two changes. topping_master_unbind() no longer looks anything up:
it passes NULL as the component data, and the HID side takes its
controls off the card it was handed at bind, which it still has.
And the teardown moved out of the release into its own devres
action, registered after component_match_add(), so that it unwinds
before the match array rather than after it.
topping_master_release() is storage again, and the ordering that
v4's release was reaching for is now expressed by where the action
is registered rather than by hoping.
What the shape buys, measured rather than argued:
- card.c is not touched at all. The v8 series had to add
snd_usb_claim_iface() and snd_usb_release_iface() there, and had a
defect I reported separately: an interface claimed and marked
USB_AUDIO_IFACE_UNUSED never gets told to wind down, because
usb_audio_disconnect() returns at its first line. With no claim
there is no such interface and the defect has nothing to attach
to. Watching the wire with usbmon: unbind the audio interface and
the keepalive stops. In v8 it kept going.
- the hid_ignore_list entry goes away, so hidraw stays available.
That matters because five of the card's functions -- the mixer
matrix, the mutes, the loopback routing, the input power and the
EQ -- are reachable only through the vendor protocol, and v8 made
them unreachable from userspace as the price of the quirk.
- two M62s on one host bind to their own cards. The match is by
descent from the shared USB device, and it holds: gains set on one
do not appear on the other, and unplugging one leaves the other's
controls in place. The quirk road could not be tested for this at
all.
On the timing worry from that thread: it does not bite on an ordinary
plug. The master goes up from snd_topping_init() inside
snd_usb_create_mixer(), so the bind is synchronous and the controls
exist before try_to_register_card() -- the same ordering as v8. From
HID probe to component bind, 54 to 57 ms across five replugs. Only a
module reload onto a live card adds controls after registration, and
there a desktop mixer does not follow the renumbered elements until
wireplumber is restarted. That is a real wart and I have not found a
way around it that is worth the code.
What the card does and does not report
I got this wrong twice in the v8 thread and would rather state it
plainly here, since the design follows from it.
After a subscribe the card reports itself in two waves: jacks at about
0.9 s, then at about 5.2 s the jacks again plus the output mutes plus
the gain of every input whose jack is present. Every turn of a
front-panel knob is reported as it happens, gains and output volumes
alike. What is never reported is a source selector, because the card
reports events and a selector has no front-panel control, so no event
can exist; and an output volume before anyone has touched it, because
it is a setting rather than a physical fact.
So five of the nine controls have a source of truth on the card, two
have one only after a hand moves them, and two never do.
That looked at first like an argument for deferring the controls until
the first report lands, so that a value read at connect would be the
panel's rather than something restored over it. I have not done that,
and here is why: alsactl restores once, at card add, and does not
come back for elements that appear later. Deferring would therefore
trade an accurate value for five controls against no restore at all
for nine, and would wait forever for an input with nothing plugged
into it -- which on a five-input card is the normal case.
The controls are published at bind, as in v8, and the value at
connect is the restored one.
Open question, not blocking the review
hid_hw_open() sets intf->needs_remote_wakeup, and usbhid offers no way
to take input reports without it. This card does not advertise remote
wakeup (bmAttributes 0xc0, no power/wakeup node), so that forbids
runtime suspend to the whole device -- undoing something an earlier
revision of the v8 series had to fix. The driver clears the flag after
opening, with a comment saying so, because it resynchronises on resume
and has no use for a device-initiated wakeup. Jiri and Benjamin have
been asked whether that is acceptable or whether usbhid should offer
something; the question is in the same thread and unanswered. If the
answer is that the clear must go, HID_CONNECT_DRIVER replaces
HID_CONNECT_HIDRAW and the hidraw node goes with it.
Tested
Fedora, 7.3.0-rc1-bc35965f6940 plus this series, with KASAN,
PROVE_LOCKING and UBSAN enabled. One M62, firmware V87.05.45.48.27,
and a second of the same for the two-card test. Everything below was
run after the fixes listed above, not before them.
All nine controls read and written from amixer and from the front
panel, values cross-checked against the panel.
One hundred cycles of module load and unload against a live card,
which is the path the fixes touch: the controls come and go while the
meter stream keeps arriving, so .raw_event() runs against a set of
controls that is being taken apart. No splat and no lockdep complaint,
including on the lock order the first fix introduces -- m62->lock is
now held across snd_ctl_notify(), which takes the card's
controls_rwlock inside it.
An audio-side unbind and rebind, exercising topping_unbind() through
the master rather than through the HID driver's own remove.
The cable pulled out of a card while a control was being written in a
loop. That produces a run of failed writes and then the disconnect,
and after the last fix it does so without a line of complaint each.
System suspend and resume: the subscription survives it -- turning a
front-panel knob afterwards still moves the control -- and the
selectors are written back on the way out, as they must be since the
card never reports them.
Two cards on one host, on different ports of the same hub: the
component match is by descent from the shared USB device and it holds
-- dmesg shows snd-usb-audio 3-1.3:1.0 binding one HID device and
3-1.4.2:1.0 the other, each card carries its own nine controls, gains
set from one front panel move only that card's controls, and
unplugging one leaves the other's in place.
Not tested:
- hibernation, and with it .reset_resume, which shares
topping_resume() with .resume. This machine is not set up for it:
there is no resume= on the command line and zram takes swap
priority, so the attempt logs "PM: Image not found (code -16)" and
powers off instead of saving an image. That is the same on a stock
kernel and has nothing to do with this series, but it does mean
the path is unexercised;
- kmemleak, compiled in on this system but disabled at boot;
- a card whose battery has run down;
- anything on a big-endian host.
Ordering
The HID driver comes first. Between the two patches it binds, speaks
to the card and creates no controls, which is harmless; the reverse
order leaves a master that never matches, which is equally harmless
but leaves hid-generic making a bogus input device out of the
descriptor in the meantime.
Mikhail Gavrilov (2):
HID: topping-m62: driver for the M62's vendor controls
ALSA: usb-audio: bind the Topping M62's vendor controls
MAINTAINERS | 9 +
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 925 ++++++++++++++++++++++++++++++++++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 227 +++++++++
sound/usb/mixer_topping.h | 7 +
10 files changed, 1200 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC PATCH v5 1/2] HID: topping-m62: driver for the M62's vendor controls
2026-09-04 16:22 ` [RFC PATCH v5 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
@ 2026-09-04 16:22 ` Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 16:22 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The Topping M62 is a USB audio interface whose analogue input gains,
output volumes and output source selectors are not described by the USB
Audio Class. They live behind a vendor protocol on a HID-class
interface, spoken by Topping's M Control Center, which has no Linux
build. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: raising it lifts the
converter's own floor with the signal. So on Linux the only gain worth
setting is unreachable, and a measurement application has to ask a
human to set it by hand on the front panel.
The protocol was read off the vendor application's traffic. Frames are
fifteen bytes:
22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
with TT a target, PP a property of that target, and the checksum
CRC-16/MODBUS over bytes 2..10, most significant byte first. Reports
from the device are the same frame plus one pad byte. The vendor
application sends 00 00 in place of the checksum and the device accepts
it, so the device does not verify what it receives; this driver signs
its writes anyway and validates what it reads.
The card is silent until subscribed. One write of 0x11/0x24 starts the
notification stream, and it lapses unless repeated: the vendor
application sends it every two seconds and so does this driver. After
that the card reports what a hand does to its hardware -- jack states,
mutes, battery, and every turn of a front-panel knob.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints are the only
route. The report descriptor describes nothing worth having -- a
Generic Desktop application collection, eight usages stretched over
sixteen unnamed bytes in and out, no report ID -- so hid-generic makes
an input device with an ABS_MISC axis out of it and nothing else. Hence
the hid_have_special_driver entry, and HID_CONNECT_HIDRAW here with no
input device.
The controls belong on the sound card rather than on a card of this
driver's own, so this driver creates none. It registers a component;
the M62 mixer quirk in snd-usb-audio is the master and hands over its
struct snd_card at bind time. Everything created here is dropped again
at unbind, whichever half goes away first, which is what makes the two
drivers independent of each other's disconnect. The audio half is the
following patch; without it this driver binds, speaks to the card and
creates nothing, which is harmless.
Interface 3 is Application Specific / DFU and is never touched: a stray
write there can leave the card unusable. Only interface 4 grows a
component.
Nine controls: five input gains (IN 1, IN 2, AUX, BT, OTG IN), two
output volumes (HP, OTG OUT) and the two output source selectors. The
outputs come in pairs and the device announces only the second of each,
so both are written and the second is the one listened for. The mixer
matrix, the mutes, the loopback routing, the input power and the EQ
remain reachable only through the hidraw node.
The two selectors carry an extra first item, "Unknown", and start
there. The card reports what a hand does to its hardware rather than
what its settings are, and a selector has no front-panel control, so
there is no event for it to report. A host that did not write the
selector cannot learn where the output points, and no command would
help, because there is nothing for one to be built on.
That matters more than a missing readback usually would, because the
selector decides whether the card makes a sound at all. It is
independent of which PCM device the host is playing into: a host can
be feeding Playback 1/2 while the headphone output listens to Mix C,
in which case the card is playing correctly from a source nobody is
feeding, and nothing on the host says why. The card is battery powered
and runs without a host at all, so whatever the last application to
touch it left behind is what a fresh Linux system inherits.
Without the extra item the driver would have to name a source it has
not read, and the first alsactl store would turn that invention into a
setting the user never made. With it, "Unknown" is stored and restored
like any other value until a hand chooses something, and writing it
back is accepted as a no-op rather than failing a restore of the
driver's own report.
Putting the card into a known state is a UCM profile's job rather than
this driver's: a profile knows what the user is trying to do, and a
driver does not. "Unknown" is what lets a profile do that without
harm, since it distinguishes "nobody has chosen" from "the user chose
Mix B" and can leave the second alone. No profile exists yet.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 925 ++++++++++++++++++++++++++++++++++
5 files changed, 951 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..e29f018fefd2 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1283,6 +1283,25 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING_M62
+ tristate "Topping M62 vendor controls"
+ depends on USB_HID
+ depends on SND_USB_AUDIO
+ select CRC16
+ help
+ Support for the analogue input gains, output volumes and
+ output source selectors of the Topping M62 USB audio
+ interface. These are not described by the USB Audio Class
+ and are reached over a vendor protocol on the card's HID
+ interface, so a driver is needed for them to appear at all.
+
+ The controls are created on the sound card that snd-usb-audio
+ makes for the same device, so both drivers are needed. The
+ card works without this one; it simply has no gain controls.
+
+ To compile this driver as a module, choose M here: the module
+ will be called hid-topping-m62.
+
config HID_TOPSEED
tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245ee..9f854686b702 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
+obj-$(CONFIG_HID_TOPPING_M62) += hid-topping-m62.o
obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o
obj-$(CONFIG_HID_TOPRE) += hid-topre.o
obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..60ca5712cb58 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -787,6 +787,9 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE_PRO) },
#endif
+#if IS_ENABLED(CONFIG_HID_TOPPING_M62)
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+#endif
#if IS_ENABLED(CONFIG_HID_TOPSEED)
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE_2) },
diff --git a/drivers/hid/hid-topping-m62.c b/drivers/hid/hid-topping-m62.c
new file mode 100644
index 000000000000..69980935e031
--- /dev/null
+++ b/drivers/hid/hid-topping-m62.c
@@ -0,0 +1,925 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Vendor controls for Topping interfaces behind a HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * The control pipe is not an option: GET_REPORT and SET_REPORT both
+ * stall with EPIPE for every report type, so the interrupt endpoints
+ * are the only route. The report descriptor describes nothing worth
+ * having -- a Generic Desktop application collection, eight usages
+ * stretched over sixteen unnamed bytes in and out, no report ID -- so
+ * this driver takes HID_CONNECT_HIDRAW and no input device.
+ *
+ * THE CONTROLS BELONG ON THE SOUND CARD, so this driver creates no
+ * card of its own. It registers a component; the M62 mixer quirk in
+ * snd-usb-audio is the master and hands over its struct snd_card at
+ * bind time. Everything created here is taken off again at unbind,
+ * whichever half goes away first, which is what makes the two drivers
+ * independent of each other's disconnect.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "hid-ids.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+
+/*
+ * The M62 presents two non-audio interfaces. Interface 3 is
+ * Application Specific / DFU and is never touched here. Interface 4
+ * carries the control protocol and is the only one this driver takes.
+ */
+#define M62_VENDOR_IFNUM 4
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls)
+
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+
+struct topping_m62 {
+ struct hid_device *hdev;
+ struct usb_interface *intf; /* for runtime PM */
+
+ /*
+ * NULL until the audio side binds and NULL again after it
+ * unbinds. Frames arrive before the audio half is there, so
+ * anything that reports to userspace reads this under lock.
+ */
+ struct snd_card *card;
+
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against .raw_event */
+
+ int val[TOPPING_NUM_CTLS];
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
+
+ /* the volume controls first, then the selectors */
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+};
+
+/* ------------------------------------------------------------------ */
+/* the wire */
+/* ------------------------------------------------------------------ */
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes a runtime PM reference
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has no
+ * subscription worth renewing -- resume renews it -- and the second
+ * because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ *
+ * usbhid drops a leading zero byte, taking it for a report ID this
+ * device does not use, and sends the rest on the interrupt OUT
+ * endpoint. So the fifteen bytes that reach the card are the frame
+ * and nothing else.
+ */
+static int topping_send(struct topping_m62 *m62, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_REPORT_LEN, GFP_NOIO);
+ int err;
+
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = 0; /* the report ID usbhid will drop */
+ topping_build(buf + 1, target, prop, value);
+
+ err = hid_hw_output_report(m62->hdev, buf, TOPPING_FRAME_LEN + 1);
+ if (err >= 0)
+ return 0;
+
+ /*
+ * A cable pulled out of a running card produces one of these per
+ * write until the disconnect arrives, and none of them says
+ * anything about this driver: ENODEV and ESHUTDOWN are the
+ * device already gone, EPROTO and EILSEQ the bus falling apart
+ * on the way there. Anything else is worth a line.
+ */
+ if (err != -ENODEV && err != -ESHUTDOWN &&
+ err != -EPROTO && err != -EILSEQ)
+ hid_err(m62->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ if (topping_m62_ctls[i].target == target &&
+ topping_m62_ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+/*
+ * What was the URB completion handler, minus everything usbhid now
+ * owns: there is no resubmit here and no bus-noise status to sort
+ * through. What is left is the decode.
+ *
+ * Runs in the interrupt handler's context, which is why val[] is
+ * behind a spinlock rather than the mutex.
+ */
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ int idx, value;
+
+ if (size < TOPPING_FRAME_LEN)
+ return 0;
+ if (data[0] != 0x22 || data[1] != 0x33 ||
+ data[13] != 0x66 || data[14] != 0x77)
+ return 0;
+ if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
+ return 0;
+
+ idx = topping_index_of(data[5], data[6]);
+ if (idx < 0)
+ return 0; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(data + 7);
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return 0;
+
+ /*
+ * The notify happens under the same lock that guards the cache,
+ * not after it. snd_ctl_notify() dereferences the id it is
+ * given -- it compares id->numid and copies the structure into
+ * the event -- so a concurrent topping_unbind() releasing the
+ * lock's other side could free the control in between, and the
+ * card pointer would be stale by then too. It is safe from
+ * here: it takes read_lock_irqsave and allocates with GFP_ATOMIC.
+ */
+ guard(spinlock_irqsave)(&m62->lock);
+
+ if (m62->val[idx] == value)
+ return 0;
+ m62->val[idx] = value;
+
+ if (m62->card && m62->kctl[idx])
+ snd_ctl_notify(m62->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &m62->kctl[idx]->id);
+
+ return 0;
+}
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_m62 *m62 = container_of(work, struct topping_m62,
+ keepalive.work);
+ int err;
+
+ err = topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; nothing to renew */
+
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+/* ------------------------------------------------------------------ */
+/* the volume controls */
+/* ------------------------------------------------------------------ */
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ const struct topping_ctl_desc *d;
+
+ d = &topping_m62_ctls[kctl->private_value];
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = d->min;
+ uinfo->value.integer.max = d->max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(spinlock_irqsave)(&m62->lock);
+ ucontrol->value.integer.value[0] = m62->val[kctl->private_value];
+ return 0;
+}
+
+/*
+ * Split out so that the runtime PM reference has one place to be
+ * dropped. Entered with the device awake and nothing else held.
+ */
+static int topping_ctl_write(struct topping_m62 *m62, int idx, int value)
+{
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ int prev, err;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after.
+ * The lock cannot be held across a send, and a hand on the front
+ * panel during that window produces a notification .raw_event
+ * stores; updating afterwards would throw that away and leave
+ * the driver claiming a value the device had already moved away
+ * from. Written first, the device's own report is simply the
+ * last word, which is the right bias.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ if (m62->val[idx] == value)
+ return 0;
+ prev = m62->val[idx];
+ m62->val[idx] = value;
+ }
+
+ err = topping_send(m62, d->target, d->prop, value);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(m62, d->target_pair, d->prop, value);
+ }
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->val[idx] == value)
+ m62->val[idx] = prev;
+ return err;
+ }
+
+ return 1;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run
+ * this driver's own resume callback on this very thread, and
+ * that callback takes write_lock to write the selectors back;
+ * taking write_lock first would meet it already held, by us.
+ *
+ * There is no guard against disconnect here and none is needed:
+ * snd_ctl_remove() in topping_unbind() takes controls_rwsem for
+ * writing, and no control callback can be inside it.
+ */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_ctl_write(m62, idx, value);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+/* ------------------------------------------------------------------ */
+/* the source selectors */
+/* ------------------------------------------------------------------ */
+
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(mutex)(&m62->write_lock);
+ ucontrol->value.enumerated.item[0] = m62->sel[kctl->private_value];
+ return 0;
+}
+
+static int topping_sel_write(struct topping_m62 *m62, int idx,
+ unsigned int item)
+{
+ const struct topping_enum_desc *d = &topping_m62_enums[idx];
+ int err;
+
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has
+ * chosen, and alsactl stores and restores it like any other
+ * value. It is not a choice, so writing it changes nothing --
+ * quietly, rather than failing a restore of the driver's own
+ * report.
+ */
+ if (!item || m62->sel[idx] == item)
+ return 0;
+
+ err = topping_send(m62, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ m62->sel[idx] = item;
+ return 1;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_sel_write(m62, kctl->private_value, item);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again.
+ */
+static void topping_restore_sel(struct topping_m62 *m62)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&m62->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!m62->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(m62, d->target, d->prop,
+ topping_source_value[m62->sel[i]]);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* creating and dropping the controls */
+/* ------------------------------------------------------------------ */
+
+/*
+ * v8 carried the index in a usb_mixer_elem_info, because that is what
+ * snd_usb_mixer_add_control() wants. Off the mixer there is nothing
+ * to satisfy: private_data is this driver and private_value is the
+ * index, so the per-control allocation goes away with its private_free.
+ */
+/*
+ * Builds into the caller's array rather than into m62->kctl[], which
+ * is only ever touched under m62->lock: an interrupt can arrive at any
+ * point here, since hid_device_io_start() ran before component_add().
+ * Nothing this function makes is visible to .raw_event() until the
+ * whole set is published, below.
+ */
+static int topping_add_kctl(struct topping_m62 *m62, struct snd_card *card,
+ struct snd_kcontrol **kctl,
+ const struct snd_kcontrol_new *tmpl,
+ const char *name, int idx, int slot,
+ const unsigned int *tlv)
+{
+ struct snd_kcontrol *k;
+ int err;
+
+ k = snd_ctl_new1(tmpl, m62);
+ if (!k)
+ return -ENOMEM;
+
+ k->private_value = idx;
+ k->tlv.p = tlv;
+ strscpy(k->id.name, name, sizeof(k->id.name));
+
+ err = snd_ctl_add(card, k);
+ if (err < 0)
+ return err; /* snd_ctl_add() freed it */
+
+ kctl[slot] = k;
+ return 0;
+}
+
+static void topping_drop_kctls(struct snd_card *card,
+ struct snd_kcontrol **kctl)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_KCTLS; i++)
+ snd_ctl_remove(card, kctl[i]);
+}
+
+/* ------------------------------------------------------------------ */
+/* component */
+/* ------------------------------------------------------------------ */
+
+static int topping_build_kctls(struct topping_m62 *m62, struct snd_card *card,
+ struct snd_kcontrol **kctl)
+{
+ int i, err;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ err = topping_add_kctl(m62, card, kctl, &topping_ctl,
+ topping_m62_ctls[i].name, i, i,
+ topping_m62_ctls[i].tlv);
+ if (err < 0)
+ return err;
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_kctl(m62, card, kctl, &topping_sel,
+ topping_m62_enums[i].name, i,
+ TOPPING_NUM_CTLS + i, NULL);
+ if (err < 0)
+ return err;
+ }
+ return 0;
+}
+
+static int topping_bind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS] = { };
+ struct snd_card *card = master_data;
+ int err;
+
+ err = topping_build_kctls(m62, card, kctl);
+ if (err < 0) {
+ topping_drop_kctls(card, kctl);
+ return err;
+ }
+
+ /*
+ * One publication, and m62->card is the flag for it: until this
+ * store, .raw_event() sees a NULL card and never looks at
+ * m62->kctl[] at all, so the set above was built where no
+ * interrupt could reach it. Both fields are written here under
+ * the lock that every reader takes.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ memcpy(m62->kctl, kctl, sizeof(m62->kctl));
+ m62->card = card;
+ }
+
+ /*
+ * Subscribe and ask for the state HERE rather than at probe:
+ * before this point every announced value would land in the
+ * cache with no control to notify. The card answers in two
+ * waves -- the jacks at once, the gain of a connected input
+ * about 5 s later -- so nothing here waits for them: each value
+ * arrives through .raw_event() and notifies its own control.
+ */
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+}
+
+static void topping_unbind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+ struct snd_card *card;
+
+ /*
+ * The mirror of the publication above: take the whole set away
+ * under the lock, so that once this scope ends no reader can
+ * reach a control, and only then remove them -- snd_ctl_remove()
+ * sleeps and cannot be called from in here.
+ *
+ * Clearing the card also stops topping_resume() from restarting
+ * the keepalive, which makes the cancel below final.
+ *
+ * @master_data is deliberately unused. The audio side cannot look
+ * its own context up while its devres is unwinding, so it passes
+ * NULL; the card to take the controls off is the one this driver
+ * was handed at bind, and it is right here.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ card = m62->card;
+ memcpy(kctl, m62->kctl, sizeof(kctl));
+ memset(m62->kctl, 0, sizeof(m62->kctl));
+ m62->card = NULL;
+ }
+
+ if (!card)
+ return;
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ topping_drop_kctls(card, kctl);
+}
+
+static const struct component_ops topping_component_ops = {
+ .bind = topping_bind,
+ .unbind = topping_unbind,
+};
+
+/* ------------------------------------------------------------------ */
+/* HID */
+/* ------------------------------------------------------------------ */
+
+static int topping_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct topping_m62 *m62;
+ int err;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ m62 = devm_kzalloc(&hdev->dev, sizeof(*m62), GFP_KERNEL);
+ if (!m62)
+ return -ENOMEM;
+
+ m62->hdev = hdev;
+ m62->intf = to_usb_interface(hdev->dev.parent);
+ if (m62->intf->cur_altsetting->desc.bInterfaceNumber !=
+ M62_VENDOR_IFNUM)
+ return -ENODEV;
+
+ spin_lock_init(&m62->lock);
+ INIT_DELAYED_WORK(&m62->keepalive, topping_keepalive);
+ hid_set_drvdata(hdev, m62);
+
+ err = devm_mutex_init(&hdev->dev, &m62->write_lock);
+ if (err)
+ return err;
+
+ err = hid_parse(hdev);
+ if (err)
+ return err;
+
+ /*
+ * HIDRAW and no input device. The descriptor would only make a
+ * nonexistent pointer, while a hidraw node is how this protocol
+ * was read in the first place and how the parts not exposed here
+ * -- the mixer matrix, the mutes, the EQ -- stay reachable.
+ */
+ err = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (err)
+ return err;
+
+ err = hid_hw_open(hdev);
+ if (err)
+ goto err_stop;
+
+ /*
+ * XXX awaiting the HID maintainers' word. usbhid arms every
+ * device it opens for remote wakeup, and this card does not
+ * offer it, which forbids runtime suspend to the whole device.
+ * Nothing here needs it: the resume path subscribes again and
+ * asks the card for its whole state, so a knob turned while the
+ * host slept is picked up by asking rather than by being told.
+ *
+ * This clear does not survive a hidraw open, which calls
+ * hid_hw_open() again -- so if it stays, HID_CONNECT_DRIVER has
+ * to replace HID_CONNECT_HIDRAW above.
+ */
+ m62->intf->needs_remote_wakeup = 0;
+
+ /*
+ * Reports are dropped for the whole of probe unless this is called,
+ * and component_add() below can bind synchronously when the audio
+ * side is already there -- which subscribes, and the device answers
+ * at once. Without this the identification wave is thrown away.
+ */
+ hid_device_io_start(hdev);
+
+ err = component_add(&hdev->dev, &topping_component_ops);
+ if (err)
+ goto err_close;
+
+ return 0;
+
+err_close:
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+ return err;
+}
+
+static void topping_remove(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ /* Runs topping_unbind() first if the audio side is bound. */
+ component_del(&hdev->dev, &topping_component_ops);
+
+ /*
+ * Unconditionally, and after component_del(): if the audio side
+ * had already unbound, the cancel there has been and gone, and
+ * a resume in between could have restarted the work. This is
+ * the last point before devm frees m62, so nothing may outlive
+ * it.
+ */
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static int topping_suspend(struct hid_device *hdev, pm_message_t message)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ return 0;
+}
+
+static int topping_resume(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ unsigned int noio;
+
+ /*
+ * Nothing to report to yet, and nothing the card needs told:
+ * the next bind does the subscribing.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (!m62->card)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usbhid's own
+ * usb_interrupt_msg() allocates a URB with GFP_KERNEL, so asking
+ * for the frame buffer politely is not enough, and reclaim here
+ * can wait on a block device that has not woken yet.
+ *
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ noio = memalloc_noio_save();
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(m62);
+
+ /*
+ * Restart the keepalive only if the audio side is still bound.
+ * Testing m62->card again, under the lock that topping_unbind()
+ * takes to clear it, is what keeps an unbind racing this
+ * function from leaving work behind that nothing will cancel.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->card)
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static const struct hid_device_id topping_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, topping_devices);
+
+static struct hid_driver topping_driver = {
+ .name = "topping-m62",
+ .id_table = topping_devices,
+ .probe = topping_probe,
+ .remove = topping_remove,
+ .raw_event = topping_raw_event,
+ .suspend = topping_suspend,
+ .resume = topping_resume,
+ .reset_resume = topping_resume,
+};
+module_hid_driver(topping_driver);
+
+MODULE_DESCRIPTION("Topping M62 vendor controls");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v5 2/2] ALSA: usb-audio: bind the Topping M62's vendor controls
2026-09-04 16:22 ` [RFC PATCH v5 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
@ 2026-09-04 16:22 ` Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2 siblings, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 16:22 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The M62's vendor controls are driven by hid-topping-m62, added in the
previous patch, which speaks a vendor protocol on the card's HID
interface. Those controls belong on the sound card that plays the
audio, not on a card of the HID driver's own.
This adds the other half of that: a component master, registered from
the M62's mixer quirk, which hands its struct snd_card to the HID
driver at bind time and takes the controls away again at unbind. The
lifetime rules are the component framework's, which is the point --
neither driver has to be told about the other's disconnect, and neither
has to guess at the other's state. The same shape binds HD-audio to the
graphics drivers in sound/hda/core/component.c, with sound as the
master there too.
The master's context lives in devres on the audio control interface
rather than in drvdata, which on a usb_interface belongs to
snd-usb-audio itself; devres_find(), keyed on the release function,
gives it back inside the callbacks, which are handed nothing but a
struct device *. It hangs off the control interface rather than off the
USB device because component_match_add() allocates the match list with
devm: on the interface that is released at unbind, while on the
usb_device it would live until the device itself was released and a
rebind would stack a second list on top.
Neither component_compare_dev() nor component_compare_dev_name() fits:
the audio side has no pointer to the HID device, and the HID device's
name carries an instance counter that is not predictable. The match is
therefore one of descent -- the HID device sits two levels below the
USB device -- and which interface it is stays the HID driver's
business, since it registers a component for the vendor interface and
for no other. That keeps sound/usb free of HID symbols and of any
opinion about this card's interface numbering.
component_master_add_with_match() returns 0 with the aggregate merely
pending when the HID driver is absent, so the card comes up either way
and grows the vendor controls if and when the other half appears.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 9 ++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 227 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 ++
5 files changed, 249 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 627595e245f3..b49560efa1c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27593,6 +27593,15 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 VENDOR CONTROLS
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-topping-m62.c
+F: sound/usb/mixer_topping.c
+F: sound/usb/mixer_topping.h
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..722f98f97d0b
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Topping M62 -- component master for the card's vendor controls.
+ *
+ * The M62's analogue gains, output volumes and source selectors are not
+ * described by the USB Audio Class. They are reached over a vendor
+ * protocol on the card's HID interface, which hid-topping-m62 speaks.
+ *
+ * This file speaks none of that protocol. It publishes the sound card to
+ * whoever drives the vendor interface, so that the controls are created on
+ * the card that plays the audio rather than on a card of their own, and are
+ * torn down when either side goes away. The lifetime rules are the
+ * component framework's, which is the point: neither driver has to guess at
+ * the other's state, and neither has to be told about the other's
+ * disconnect.
+ *
+ * The same shape binds HD-audio to the graphics drivers in
+ * sound/hda/core/component.c, with sound as the master there too.
+ */
+
+#include <linux/component.h>
+#include <linux/device.h>
+#include <linux/usb.h>
+
+#include <sound/core.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_topping.h"
+
+/*
+ * What the master hands the component at bind time. It lives in devres on
+ * the audio control interface rather than in drvdata, because drvdata on a
+ * usb_interface belongs to snd-usb-audio itself. devres_find(), keyed on
+ * the release function, gives it back inside the callbacks, which are
+ * handed nothing but a struct device *.
+ */
+struct topping_master {
+ struct device *dev; /* the audio control interface */
+ struct snd_card *card;
+ struct usb_mixer_interface *mixer;
+};
+
+static void topping_master_release(struct device *dev, void *res)
+{
+ /*
+ * Storage only. Taking the master down is a separate devres
+ * action registered after the match array, so that it runs
+ * before it -- see topping_master_teardown() below.
+ */
+}
+
+static struct topping_master *topping_get_master(struct device *dev)
+{
+ return devres_find(dev, topping_master_release, NULL, NULL);
+}
+
+/*
+ * Which of the registered components is ours.
+ *
+ * This is only ever called against devices that have registered with
+ * component_add(), so it does not have to defend itself against the whole
+ * device tree. What it does have to do is tell this card's vendor
+ * function apart from a second M62 on another port.
+ *
+ * The HID device sits two levels below the USB device:
+ *
+ * hid_device -> usb_interface -> usb_device
+ *
+ * WHICH interface it is, is the HID driver's business: it registers a
+ * component for the vendor interface and for nothing else. So the test
+ * here is one of descent alone and needs no HID symbols in sound/usb --
+ * which also keeps this file free of any opinion about the M62's
+ * interface numbering.
+ */
+static int topping_match_component(struct device *dev, void *data)
+{
+ return dev->parent && dev->parent->parent == data;
+}
+
+static int topping_master_bind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ if (WARN_ON(!tm))
+ return -EINVAL;
+
+ return component_bind_all(dev, tm->card);
+}
+
+/*
+ * No topping_get_master() here, deliberately. This can run while the
+ * interface's devres is unwinding, and devres_release_all() moves every
+ * node off the device before it calls a single release, so the lookup
+ * would come back empty and the unbind would be skipped altogether.
+ *
+ * The component does not need the card from us in any case: it kept the
+ * one it was handed at bind, and takes its controls off that.
+ */
+static void topping_master_unbind(struct device *dev)
+{
+ component_unbind_all(dev, NULL);
+}
+
+static const struct component_master_ops topping_master_ops = {
+ .bind = topping_master_bind,
+ .unbind = topping_master_unbind,
+};
+
+/*
+ * Registered as a devres action AFTER component_match_add(), because
+ * devres unwinds in reverse: this then runs before the match array is
+ * freed, and component_unbind_all() walks that array.
+ *
+ * Two roads reach it. Normally topping_private_free() calls it through
+ * devm_release_action(). The other is a probe that got as far as
+ * creating this mixer and then failed: usb_audio_probe() leaves the
+ * card and its mixer list alone in that case, as long as an earlier
+ * interface had succeeded, so the mixer would outlive the interface
+ * whose devres this is. Unhooking it here is what keeps a later
+ * disconnect from reaching freed storage.
+ */
+static void topping_master_teardown(void *data)
+{
+ struct topping_master *tm = data;
+
+ component_master_del(tm->dev, &topping_master_ops);
+
+ if (tm->mixer) {
+ tm->mixer->private_data = NULL;
+ tm->mixer->private_free = NULL;
+ }
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_master *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+
+ /*
+ * Reached from snd_usb_mixer_disconnect(), on an unplug and on an
+ * unbind of the audio interface alike. The action clears
+ * mixer->private_data on its way through, so nothing is left
+ * pointing at the storage dropped below.
+ */
+ devm_release_action(tm->dev, topping_master_teardown, tm);
+ devres_destroy(tm->dev, topping_master_release, NULL, NULL);
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct component_match *match = NULL;
+ struct usb_interface *intf;
+ struct topping_master *tm;
+ struct device *dev;
+ int err;
+
+ /*
+ * The master hangs off the audio control interface rather than off
+ * the USB device: component_match_add() allocates the match list
+ * with devm, and on an interface that is released when the interface
+ * is unbound. On the usb_device it would live until the device
+ * itself was released, and a rebind would stack a second list on top
+ * of the first.
+ */
+ intf = usb_ifnum_to_if(chip->dev,
+ get_iface_desc(mixer->hostif)->bInterfaceNumber);
+ if (!intf)
+ return -ENODEV;
+ dev = &intf->dev;
+
+ tm = devres_alloc(topping_master_release, sizeof(*tm), GFP_KERNEL);
+ if (!tm)
+ return -ENOMEM;
+ tm->dev = dev;
+ tm->card = chip->card;
+ tm->mixer = mixer;
+ devres_add(dev, tm);
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ component_match_add(dev, &match, topping_match_component,
+ &chip->dev->dev);
+
+ /*
+ * component_match_add() reports a failed allocation by storing
+ * an error pointer rather than by returning, and
+ * component_master_add_with_match() dereferences what it is
+ * given without looking.
+ */
+ if (IS_ERR(match)) {
+ err = PTR_ERR(match);
+ goto err_free;
+ }
+
+ /*
+ * This returns 0 with the aggregate merely pending when
+ * hid-topping-m62 has not registered its component yet:
+ * try_to_bring_up_aggregate_device() reports an incomplete set as
+ * "not ready", not as an error. So the card comes up either way and
+ * grows the vendor controls if and when the other half appears.
+ */
+ err = component_master_add_with_match(dev, &topping_master_ops, match);
+ if (err < 0)
+ goto err_free;
+
+ /* Last, so that devres releases it first -- see the teardown. */
+ err = devm_add_action(dev, topping_master_teardown, tm);
+ if (err < 0) {
+ component_master_del(dev, &topping_master_ops);
+ goto err_free;
+ }
+
+ return 0;
+
+err_free:
+ mixer->private_data = NULL;
+ mixer->private_free = NULL;
+ devres_destroy(dev, topping_master_release, NULL, NULL);
+ usb_audio_err(chip, "Topping: no component master: %d\n", err);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v6 0/2] the Topping M62's vendor controls, on the component framework
2026-09-04 16:22 ` [RFC PATCH v5 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
@ 2026-09-04 16:58 ` Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2 siblings, 2 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 16:58 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
This is the experiment Takashi suggested in the v8 thread: an
individual HID driver joined to snd-usb-audio through the component
framework, instead of a mixer quirk that claims the HID interface for
itself. It is posted as an RFC rather than as v9 because v8 is not
withdrawn -- it remains the alternative, and the choice between the two
is the question this series exists to answer.
I said this morning that I would hold this until the questions in that
thread were answered. I am sending it anyway, because a patch gets a
better answer than a question does, and because the last of those
questions answered itself: the correction I sent a few hours ago
proposed deferring the controls until the card reports, and working out
how that lands in code showed why it cannot -- which is in the section
below. The only question still genuinely open is the HID one, and it
touches one line.
Changes since v5:
- topping_master_bind() warned when it could not find its context,
treating that as impossible. It is not. devres_release_all() moves
every node off the device before it calls a single release, so
between a failed probe starting to unwind and the teardown action
running, the context is gone while the aggregate is still
registered -- and a HID component arriving in that window takes
component_mutex first and reaches the bind. The window is narrow
but it is legitimate, and on a kernel with panic_on_warn the WARN
would take the machine down for it.
It now returns -EPROBE_DEFER instead, which is what the situation
actually is: the master is going away, the component came at the
wrong moment, and it should come back. The framework does not log
that return, and the HID probe is retried once the audio side is
back.
The bot described the window as opening in devm_release_action(),
which unlinks only the action's own node and leaves the context in
place; the ordinary teardown road is therefore safe. The window is
real on the other road, which is what this fixes.
What the shape buys, measured rather than argued:
- card.c is not touched at all. The v8 series had to add
snd_usb_claim_iface() and snd_usb_release_iface() there, and had a
defect I reported separately: an interface claimed and marked
USB_AUDIO_IFACE_UNUSED never gets told to wind down, because
usb_audio_disconnect() returns at its first line. With no claim
there is no such interface and the defect has nothing to attach
to. Watching the wire with usbmon: unbind the audio interface and
the keepalive stops. In v8 it kept going.
- the hid_ignore_list entry goes away, so hidraw stays available.
That matters because five of the card's functions -- the mixer
matrix, the mutes, the loopback routing, the input power and the
EQ -- are reachable only through the vendor protocol, and v8 made
them unreachable from userspace as the price of the quirk.
- two M62s on one host bind to their own cards. The match is by
descent from the shared USB device, and it holds: gains set on one
do not appear on the other, and unplugging one leaves the other's
controls in place. The quirk road could not be tested for this at
all.
On the timing worry from that thread: it does not bite on an ordinary
plug. The master goes up from snd_topping_init() inside
snd_usb_create_mixer(), so the bind is synchronous and the controls
exist before try_to_register_card() -- the same ordering as v8. From
HID probe to component bind, 54 to 57 ms across five replugs. Only a
module reload onto a live card adds controls after registration, and
there a desktop mixer does not follow the renumbered elements until
wireplumber is restarted. That is a real wart and I have not found a
way around it that is worth the code.
What the card does and does not report
I got this wrong twice in the v8 thread and would rather state it
plainly here, since the design follows from it.
After a subscribe the card reports itself in two waves: jacks at about
0.9 s, then at about 5.2 s the jacks again plus the output mutes plus
the gain of every input whose jack is present. Every turn of a
front-panel knob is reported as it happens, gains and output volumes
alike. What is never reported is a source selector, because the card
reports events and a selector has no front-panel control, so no event
can exist; and an output volume before anyone has touched it, because
it is a setting rather than a physical fact.
So five of the nine controls have a source of truth on the card, two
have one only after a hand moves them, and two never do.
That looked at first like an argument for deferring the controls until
the first report lands, so that a value read at connect would be the
panel's rather than something restored over it. I have not done that,
and here is why: alsactl restores once, at card add, and does not
come back for elements that appear later. Deferring would therefore
trade an accurate value for five controls against no restore at all
for nine, and would wait forever for an input with nothing plugged
into it -- which on a five-input card is the normal case.
The controls are published at bind, as in v8, and the value at
connect is the restored one.
Open question, not blocking the review
hid_hw_open() sets intf->needs_remote_wakeup, and usbhid offers no way
to take input reports without it. This card does not advertise remote
wakeup (bmAttributes 0xc0, no power/wakeup node), so that forbids
runtime suspend to the whole device -- undoing something an earlier
revision of the v8 series had to fix. The driver clears the flag after
opening, with a comment saying so, because it resynchronises on resume
and has no use for a device-initiated wakeup. Jiri and Benjamin have
been asked whether that is acceptable or whether usbhid should offer
something; the question is in the same thread and unanswered. If the
answer is that the clear must go, HID_CONNECT_DRIVER replaces
HID_CONNECT_HIDRAW and the hidraw node goes with it.
Tested
Fedora, 7.3.0-rc1-bc35965f6940 plus this series, with KASAN,
PROVE_LOCKING and UBSAN enabled. One M62, firmware V87.05.45.48.27,
and a second of the same for the two-card test. Everything below was
run after the fixes listed above, not before them.
All nine controls read and written from amixer and from the front
panel, values cross-checked against the panel.
One hundred cycles of module load and unload against a live card,
which is the path the fixes touch: the controls come and go while the
meter stream keeps arriving, so .raw_event() runs against a set of
controls that is being taken apart. No splat and no lockdep complaint,
including on the lock order the first fix introduces -- m62->lock is
now held across snd_ctl_notify(), which takes the card's
controls_rwlock inside it.
An audio-side unbind and rebind, exercising topping_unbind() through
the master rather than through the HID driver's own remove.
The cable pulled out of a card while a control was being written in a
loop. That produces a run of failed writes and then the disconnect,
and after the last fix it does so without a line of complaint each.
System suspend and resume: the subscription survives it -- turning a
front-panel knob afterwards still moves the control -- and the
selectors are written back on the way out, as they must be since the
card never reports them.
Two cards on one host, on different ports of the same hub: the
component match is by descent from the shared USB device and it holds
-- dmesg shows snd-usb-audio 3-1.3:1.0 binding one HID device and
3-1.4.2:1.0 the other, each card carries its own nine controls, gains
set from one front panel move only that card's controls, and
unplugging one leaves the other's in place.
Not tested:
- hibernation, and with it .reset_resume, which shares
topping_resume() with .resume. This machine is not set up for it:
there is no resume= on the command line and zram takes swap
priority, so the attempt logs "PM: Image not found (code -16)" and
powers off instead of saving an image. That is the same on a stock
kernel and has nothing to do with this series, but it does mean
the path is unexercised;
- kmemleak, compiled in on this system but disabled at boot;
- a card whose battery has run down;
- anything on a big-endian host.
Ordering
The HID driver comes first. Between the two patches it binds, speaks
to the card and creates no controls, which is harmless; the reverse
order leaves a master that never matches, which is equally harmless
but leaves hid-generic making a bogus input device out of the
descriptor in the meantime.
Mikhail Gavrilov (2):
HID: topping-m62: driver for the M62's vendor controls
ALSA: usb-audio: bind the Topping M62's vendor controls
MAINTAINERS | 9 +
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 925 ++++++++++++++++++++++++++++++++++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 236 +++++++++
sound/usb/mixer_topping.h | 7 +
10 files changed, 1209 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
--
2.55.0
^ permalink raw reply [flat|nested] 53+ messages in thread
* [RFC PATCH v6 1/2] HID: topping-m62: driver for the M62's vendor controls
2026-09-04 16:58 ` [RFC PATCH v6 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
@ 2026-09-04 16:58 ` Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
1 sibling, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 16:58 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The Topping M62 is a USB audio interface whose analogue input gains,
output volumes and output source selectors are not described by the USB
Audio Class. They live behind a vendor protocol on a HID-class
interface, spoken by Topping's M Control Center, which has no Linux
build. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: raising it lifts the
converter's own floor with the signal. So on Linux the only gain worth
setting is unreachable, and a measurement application has to ask a
human to set it by hand on the front panel.
The protocol was read off the vendor application's traffic. Frames are
fifteen bytes:
22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
with TT a target, PP a property of that target, and the checksum
CRC-16/MODBUS over bytes 2..10, most significant byte first. Reports
from the device are the same frame plus one pad byte. The vendor
application sends 00 00 in place of the checksum and the device accepts
it, so the device does not verify what it receives; this driver signs
its writes anyway and validates what it reads.
The card is silent until subscribed. One write of 0x11/0x24 starts the
notification stream, and it lapses unless repeated: the vendor
application sends it every two seconds and so does this driver. After
that the card reports what a hand does to its hardware -- jack states,
mutes, battery, and every turn of a front-panel knob.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints are the only
route. The report descriptor describes nothing worth having -- a
Generic Desktop application collection, eight usages stretched over
sixteen unnamed bytes in and out, no report ID -- so hid-generic makes
an input device with an ABS_MISC axis out of it and nothing else. Hence
the hid_have_special_driver entry, and HID_CONNECT_HIDRAW here with no
input device.
The controls belong on the sound card rather than on a card of this
driver's own, so this driver creates none. It registers a component;
the M62 mixer quirk in snd-usb-audio is the master and hands over its
struct snd_card at bind time. Everything created here is dropped again
at unbind, whichever half goes away first, which is what makes the two
drivers independent of each other's disconnect. The audio half is the
following patch; without it this driver binds, speaks to the card and
creates nothing, which is harmless.
Interface 3 is Application Specific / DFU and is never touched: a stray
write there can leave the card unusable. Only interface 4 grows a
component.
Nine controls: five input gains (IN 1, IN 2, AUX, BT, OTG IN), two
output volumes (HP, OTG OUT) and the two output source selectors. The
outputs come in pairs and the device announces only the second of each,
so both are written and the second is the one listened for. The mixer
matrix, the mutes, the loopback routing, the input power and the EQ
remain reachable only through the hidraw node.
The two selectors carry an extra first item, "Unknown", and start
there. The card reports what a hand does to its hardware rather than
what its settings are, and a selector has no front-panel control, so
there is no event for it to report. A host that did not write the
selector cannot learn where the output points, and no command would
help, because there is nothing for one to be built on.
That matters more than a missing readback usually would, because the
selector decides whether the card makes a sound at all. It is
independent of which PCM device the host is playing into: a host can
be feeding Playback 1/2 while the headphone output listens to Mix C,
in which case the card is playing correctly from a source nobody is
feeding, and nothing on the host says why. The card is battery powered
and runs without a host at all, so whatever the last application to
touch it left behind is what a fresh Linux system inherits.
Without the extra item the driver would have to name a source it has
not read, and the first alsactl store would turn that invention into a
setting the user never made. With it, "Unknown" is stored and restored
like any other value until a hand chooses something, and writing it
back is accepted as a no-op rather than failing a restore of the
driver's own report.
Putting the card into a known state is a UCM profile's job rather than
this driver's: a profile knows what the user is trying to do, and a
driver does not. "Unknown" is what lets a profile do that without
harm, since it distinguishes "nobody has chosen" from "the user chose
Mix B" and can leave the second alone. No profile exists yet.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 19 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping-m62.c | 925 ++++++++++++++++++++++++++++++++++
5 files changed, 951 insertions(+)
create mode 100644 drivers/hid/hid-topping-m62.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index aa7fa11a0197..e29f018fefd2 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1283,6 +1283,25 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING_M62
+ tristate "Topping M62 vendor controls"
+ depends on USB_HID
+ depends on SND_USB_AUDIO
+ select CRC16
+ help
+ Support for the analogue input gains, output volumes and
+ output source selectors of the Topping M62 USB audio
+ interface. These are not described by the USB Audio Class
+ and are reached over a vendor protocol on the card's HID
+ interface, so a driver is needed for them to appear at all.
+
+ The controls are created on the sound card that snd-usb-audio
+ makes for the same device, so both drivers are needed. The
+ card works without this one; it simply has no gain controls.
+
+ To compile this driver as a module, choose M here: the module
+ will be called hid-topping-m62.
+
config HID_TOPSEED
tristate "TopSeed Cyberlink, BTC Emprex, Conceptronic remote control support"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 48a863b245ee..9f854686b702 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -141,6 +141,7 @@ obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
+obj-$(CONFIG_HID_TOPPING_M62) += hid-topping-m62.o
obj-$(CONFIG_HID_TOPSEED) += hid-topseed.o
obj-$(CONFIG_HID_TOPRE) += hid-topre.o
obj-$(CONFIG_HID_TWINHAN) += hid-twinhan.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 341bf587863b..092b2a942b4c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1470,6 +1470,9 @@
#define USB_DEVICE_ID_TIVO_SLIDE 0x1201
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
+#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_M62 0x875c
+
#define USB_VENDOR_ID_TOPRE 0x0853
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_108 0x0148
#define USB_DEVICE_ID_TOPRE_REALFORCE_R2_87 0x0146
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index 8a0b51d47040..60ca5712cb58 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -787,6 +787,9 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE_PRO) },
#endif
+#if IS_ENABLED(CONFIG_HID_TOPPING_M62)
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+#endif
#if IS_ENABLED(CONFIG_HID_TOPSEED)
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE) },
{ HID_USB_DEVICE(USB_VENDOR_ID_BTC, USB_DEVICE_ID_BTC_EMPREX_REMOTE_2) },
diff --git a/drivers/hid/hid-topping-m62.c b/drivers/hid/hid-topping-m62.c
new file mode 100644
index 000000000000..69980935e031
--- /dev/null
+++ b/drivers/hid/hid-topping-m62.c
@@ -0,0 +1,925 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Vendor controls for Topping interfaces behind a HID channel
+ *
+ * Copyright (c) 2026 Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+ *
+ * The M62 (152a:875c) puts its analogue input gains and its output
+ * volumes behind a vendor protocol on a HID-class interface, and
+ * exposes nothing of them through UAC. What UAC does expose on the
+ * capture side is a digital trim AFTER the converter, which cannot buy
+ * signal-to-noise: raising it lifts the converter's own floor with the
+ * signal. So the only knob worth automating is unreachable, and a
+ * measurement application on Linux has to ask a human to set it by
+ * hand on the front panel.
+ *
+ * The protocol was read off the vendor application's traffic. Frames
+ * are fifteen bytes:
+ *
+ * 22 33 | 20 01 01 | TT | PP | s32 value BE | CRC16 BE | 66 77
+ *
+ * with TT a target (an input, an output, or the device itself), PP a
+ * property of that target, and the checksum CRC-16/MODBUS over bytes
+ * 2..10 stored most significant byte first. Reports arriving from the
+ * device are the same frame plus one trailing pad byte; an idle poll
+ * returns sixteen zeroes. The vendor application sends 00 00 in place
+ * of the checksum and the device accepts it, so the device evidently
+ * does not verify what it receives -- this driver signs its writes
+ * anyway, and validates what it reads.
+ *
+ * The device says nothing until it is subscribed: one write of
+ * 0x11/0x24 starts the notification stream, after which every change,
+ * including a front panel button, arrives unsolicited. A second
+ * write, 0x11/0x26, makes the device announce its whole state, which
+ * is how the controls are populated without caching what we wrote.
+ *
+ * The control pipe is not an option: GET_REPORT and SET_REPORT both
+ * stall with EPIPE for every report type, so the interrupt endpoints
+ * are the only route. The report descriptor describes nothing worth
+ * having -- a Generic Desktop application collection, eight usages
+ * stretched over sixteen unnamed bytes in and out, no report ID -- so
+ * this driver takes HID_CONNECT_HIDRAW and no input device.
+ *
+ * THE CONTROLS BELONG ON THE SOUND CARD, so this driver creates no
+ * card of its own. It registers a component; the M62 mixer quirk in
+ * snd-usb-audio is the master and hands over its struct snd_card at
+ * bind time. Everything created here is taken off again at unbind,
+ * whichever half goes away first, which is what makes the two drivers
+ * independent of each other's disconnect.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sched/mm.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/unaligned.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+
+#include "hid-ids.h"
+
+#define TOPPING_FRAME_LEN 15 /* what we send */
+#define TOPPING_REPORT_LEN 16 /* what arrives, one pad byte more */
+
+/*
+ * The M62 presents two non-audio interfaces. Interface 3 is
+ * Application Specific / DFU and is never touched here. Interface 4
+ * carries the control protocol and is the only one this driver takes.
+ */
+#define M62_VENDOR_IFNUM 4
+
+/* device-scope properties */
+#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_SUBSCRIBE 0x24
+#define TOPPING_PP_ANNOUNCE 0x26
+
+/*
+ * THE SUBSCRIPTION LAPSES. The vendor application repeats 0x11/0x24
+ * every two seconds for as long as it is running, and a device that
+ * hears nothing stops reporting -- which is why a listener that
+ * subscribed once saw the meters and not much else. Nothing in the
+ * frame says "keep alive"; it is simply the same subscribe again.
+ */
+#define TOPPING_KEEPALIVE_MS 2000
+
+/*
+ * The two volume tapers, measured against the vendor application's own
+ * readout: index 0 is always mute, index 99 always the maximum, the
+ * step is 0.5 dB above -10 dB and 1 dB below it, and the family that
+ * has to cover 97 dB in 98 steps takes 2 dB below -52 dB as well.
+ */
+static const DECLARE_TLV_DB_SCALE(topping_tlv_gain, 0, 100, 0);
+
+static const unsigned int topping_tlv_out_9[] = {
+ TLV_DB_RANGE_HEAD(4),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 19, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 200, 0),
+ 20, 61, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-5100, 100, 0),
+ 62, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+static const unsigned int topping_tlv_out_0[] = {
+ TLV_DB_RANGE_HEAD(3),
+ 0, 0, SNDRV_CTL_TLVD_DB_SCALE_ITEM(SNDRV_CTL_TLVD_DB_GAIN_MUTE, 0, 1),
+ 1, 79, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-8800, 100, 0),
+ 80, 99, SNDRV_CTL_TLVD_DB_SCALE_ITEM(-950, 50, 0),
+};
+
+/*
+ * One row per knob. A row is the whole description of a control: what
+ * to call it, which target and property carry it, the second target
+ * that has to be written in step with the first, the range, and the
+ * scale. Adding a knob is adding a row.
+ *
+ * The outputs come in pairs and the device announces only the second
+ * of each pair, so both are written and the second is the one listened
+ * for.
+ */
+struct topping_ctl_desc {
+ const char *name;
+ u8 target; /* the target that reports */
+ u8 target_pair; /* written too, or 0 */
+ u8 prop;
+ int min, max;
+ const unsigned int *tlv;
+};
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic-1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Mic-2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ topping_tlv_gain },
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ topping_tlv_out_9 },
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ topping_tlv_out_0 },
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ topping_tlv_out_9 },
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ topping_tlv_out_0 },
+};
+
+#define TOPPING_NUM_CTLS ARRAY_SIZE(topping_m62_ctls)
+
+/*
+ * WHAT AN OUTPUT CAN LISTEN TO. The same numbering serves the outputs
+ * and the loopback returns, and it has a hole where 4 and 5 would be,
+ * so the index of a control item is not the value the card wants and
+ * the two are kept side by side.
+ *
+ * "Unknown" is first and is not a choice: the device NEVER reports a
+ * selector, not to us and not to the vendor's own application, which
+ * pushes its whole workspace on connect rather than asking. So a
+ * driver cannot learn where an output is pointing, and the only honest
+ * thing it can show until a hand has chosen is that it does not know.
+ */
+static const char * const topping_sources[] = {
+ "Unknown", "Mix A", "Mix B", "Mix C", "IN 1", "IN 2", "IN 1+2",
+ "AUX", "BT", "OTG IN", "Playback 1/2", "Playback 3/4",
+ "Playback 5/6", "Playback 7/8", "Playback 9/10",
+};
+
+static const u8 topping_source_value[] = {
+ 0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+};
+
+/*
+ * The selector answers on ONE target of an output's pair, unlike the
+ * volume and the mute which must be written to both.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02 },
+ { "OTG Playback Source", 0x62, 0x02 },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+
+struct topping_m62 {
+ struct hid_device *hdev;
+ struct usb_interface *intf; /* for runtime PM */
+
+ /*
+ * NULL until the audio side binds and NULL again after it
+ * unbinds. Frames arrive before the audio half is there, so
+ * anything that reports to userspace reads this under lock.
+ */
+ struct snd_card *card;
+
+ struct delayed_work keepalive;
+ struct mutex write_lock; /* one writer at a time, end to end */
+ spinlock_t lock; /* guards val[] against .raw_event */
+
+ int val[TOPPING_NUM_CTLS];
+ int sel[TOPPING_NUM_ENUMS]; /* what a hand chose, or 0 */
+
+ /* the volume controls first, then the selectors */
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+};
+
+/* ------------------------------------------------------------------ */
+/* the wire */
+/* ------------------------------------------------------------------ */
+
+static void topping_build(u8 *f, u8 target, u8 prop, s32 value)
+{
+ u16 crc;
+
+ f[0] = 0x22;
+ f[1] = 0x33;
+ f[2] = 0x20;
+ f[3] = 0x01;
+ f[4] = 0x01;
+ f[5] = target;
+ f[6] = prop;
+ put_unaligned_be32(value, f + 7);
+ crc = crc16(0xffff, f + 2, 9);
+ put_unaligned_be16(crc, f + 11);
+ f[13] = 0x66;
+ f[14] = 0x77;
+}
+
+/*
+ * The frame goes out as it is; waking the device is the CALLER's
+ * business. A write asked for by a hand takes a runtime PM reference
+ * first, which wakes what is asleep. The keepalive and the resume
+ * path deliberately do not: the first because a sleeping device has no
+ * subscription worth renewing -- resume renews it -- and the second
+ * because it IS the resume.
+ *
+ * That division is also what keeps the keepalive out of a deadlock.
+ * When it woke the device itself, a runtime suspend arriving at the
+ * same moment would wait in cancel_delayed_work_sync() for a worker
+ * that was in turn waiting for that suspend to finish.
+ *
+ * usbhid drops a leading zero byte, taking it for a report ID this
+ * device does not use, and sends the rest on the interrupt OUT
+ * endpoint. So the fifteen bytes that reach the card are the frame
+ * and nothing else.
+ */
+static int topping_send(struct topping_m62 *m62, u8 target, u8 prop,
+ s32 value)
+{
+ /*
+ * NOIO rather than KERNEL: this is called from the resume path
+ * too, where reclaim can wait on a block device that has not
+ * woken yet.
+ */
+ u8 *buf __free(kfree) = kzalloc(TOPPING_REPORT_LEN, GFP_NOIO);
+ int err;
+
+ if (!buf)
+ return -ENOMEM;
+
+ buf[0] = 0; /* the report ID usbhid will drop */
+ topping_build(buf + 1, target, prop, value);
+
+ err = hid_hw_output_report(m62->hdev, buf, TOPPING_FRAME_LEN + 1);
+ if (err >= 0)
+ return 0;
+
+ /*
+ * A cable pulled out of a running card produces one of these per
+ * write until the disconnect arrives, and none of them says
+ * anything about this driver: ENODEV and ESHUTDOWN are the
+ * device already gone, EPROTO and EILSEQ the bus falling apart
+ * on the way there. Anything else is worth a line.
+ */
+ if (err != -ENODEV && err != -ESHUTDOWN &&
+ err != -EPROTO && err != -EILSEQ)
+ hid_err(m62->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(u8 target, u8 prop)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ if (topping_m62_ctls[i].target == target &&
+ topping_m62_ctls[i].prop == prop)
+ return i;
+ return -1;
+}
+
+/*
+ * What was the URB completion handler, minus everything usbhid now
+ * owns: there is no resubmit here and no bus-noise status to sort
+ * through. What is left is the decode.
+ *
+ * Runs in the interrupt handler's context, which is why val[] is
+ * behind a spinlock rather than the mutex.
+ */
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ int idx, value;
+
+ if (size < TOPPING_FRAME_LEN)
+ return 0;
+ if (data[0] != 0x22 || data[1] != 0x33 ||
+ data[13] != 0x66 || data[14] != 0x77)
+ return 0;
+ if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
+ return 0;
+
+ idx = topping_index_of(data[5], data[6]);
+ if (idx < 0)
+ return 0; /* a meter, or something unnamed */
+
+ value = get_unaligned_be32(data + 7);
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return 0;
+
+ /*
+ * The notify happens under the same lock that guards the cache,
+ * not after it. snd_ctl_notify() dereferences the id it is
+ * given -- it compares id->numid and copies the structure into
+ * the event -- so a concurrent topping_unbind() releasing the
+ * lock's other side could free the control in between, and the
+ * card pointer would be stale by then too. It is safe from
+ * here: it takes read_lock_irqsave and allocates with GFP_ATOMIC.
+ */
+ guard(spinlock_irqsave)(&m62->lock);
+
+ if (m62->val[idx] == value)
+ return 0;
+ m62->val[idx] = value;
+
+ if (m62->card && m62->kctl[idx])
+ snd_ctl_notify(m62->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &m62->kctl[idx]->id);
+
+ return 0;
+}
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_m62 *m62 = container_of(work, struct topping_m62,
+ keepalive.work);
+ int err;
+
+ err = topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; nothing to renew */
+
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+/* ------------------------------------------------------------------ */
+/* the volume controls */
+/* ------------------------------------------------------------------ */
+
+static int topping_ctl_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ const struct topping_ctl_desc *d;
+
+ d = &topping_m62_ctls[kctl->private_value];
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = d->min;
+ uinfo->value.integer.max = d->max;
+ uinfo->value.integer.step = 1;
+ return 0;
+}
+
+static int topping_ctl_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(spinlock_irqsave)(&m62->lock);
+ ucontrol->value.integer.value[0] = m62->val[kctl->private_value];
+ return 0;
+}
+
+/*
+ * Split out so that the runtime PM reference has one place to be
+ * dropped. Entered with the device awake and nothing else held.
+ */
+static int topping_ctl_write(struct topping_m62 *m62, int idx, int value)
+{
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ int prev, err;
+
+ /*
+ * Held from the comparison to the cache update, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other.
+ */
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after.
+ * The lock cannot be held across a send, and a hand on the front
+ * panel during that window produces a notification .raw_event
+ * stores; updating afterwards would throw that away and leave
+ * the driver claiming a value the device had already moved away
+ * from. Written first, the device's own report is simply the
+ * last word, which is the right bias.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ if (m62->val[idx] == value)
+ return 0;
+ prev = m62->val[idx];
+ m62->val[idx] = value;
+ }
+
+ err = topping_send(m62, d->target, d->prop, value);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_send(m62, d->target_pair, d->prop, value);
+ }
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->val[idx] == value)
+ m62->val[idx] = prev;
+ return err;
+ }
+
+ return 1;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ int value, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return -EINVAL;
+
+ /*
+ * THE ORDER OF THESE TWO MATTERS. Waking the device can run
+ * this driver's own resume callback on this very thread, and
+ * that callback takes write_lock to write the selectors back;
+ * taking write_lock first would meet it already held, by us.
+ *
+ * There is no guard against disconnect here and none is needed:
+ * snd_ctl_remove() in topping_unbind() takes controls_rwsem for
+ * writing, and no control callback can be inside it.
+ */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_ctl_write(m62, idx, value);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_ctl = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_TLV_READ,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
+/* ------------------------------------------------------------------ */
+/* the source selectors */
+/* ------------------------------------------------------------------ */
+
+static int topping_sel_info(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_info *uinfo)
+{
+ return snd_ctl_enum_info(uinfo, 1, ARRAY_SIZE(topping_sources),
+ topping_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+
+ guard(mutex)(&m62->write_lock);
+ ucontrol->value.enumerated.item[0] = m62->sel[kctl->private_value];
+ return 0;
+}
+
+static int topping_sel_write(struct topping_m62 *m62, int idx,
+ unsigned int item)
+{
+ const struct topping_enum_desc *d = &topping_m62_enums[idx];
+ int err;
+
+ guard(mutex)(&m62->write_lock);
+
+ /*
+ * "Unknown" is what this control reports until a hand has
+ * chosen, and alsactl stores and restores it like any other
+ * value. It is not a choice, so writing it changes nothing --
+ * quietly, rather than failing a restore of the driver's own
+ * report.
+ */
+ if (!item || m62->sel[idx] == item)
+ return 0;
+
+ err = topping_send(m62, d->target, d->prop,
+ topping_source_value[item]);
+ if (err < 0)
+ return err;
+
+ m62->sel[idx] = item;
+ return 1;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_m62 *m62 = snd_kcontrol_chip(kctl);
+ unsigned int item;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_sources))
+ return -EINVAL;
+
+ /* the wake before the lock, for the reason given in _ctl_put */
+ if (usb_autopm_get_interface(m62->intf) < 0)
+ return -EIO;
+
+ err = topping_sel_write(m62, kctl->private_value, item);
+
+ usb_autopm_put_interface(m62->intf);
+ return err;
+}
+
+static const struct snd_kcontrol_new topping_sel = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_sel_info,
+ .get = topping_sel_get,
+ .put = topping_sel_put,
+};
+
+/*
+ * The gains come back by themselves, since the device announces them,
+ * but a selector is never reported: if the card came up on its own
+ * defaults while the host slept, this driver's idea of it would be
+ * silently wrong, and writing the remembered value would then look
+ * like no change at all. So the choice a hand made is written again.
+ */
+static void topping_restore_sel(struct topping_m62 *m62)
+{
+ const struct topping_enum_desc *d;
+ int i;
+
+ guard(mutex)(&m62->write_lock);
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!m62->sel[i])
+ continue; /* nothing was ever chosen */
+ d = &topping_m62_enums[i];
+ topping_send(m62, d->target, d->prop,
+ topping_source_value[m62->sel[i]]);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* creating and dropping the controls */
+/* ------------------------------------------------------------------ */
+
+/*
+ * v8 carried the index in a usb_mixer_elem_info, because that is what
+ * snd_usb_mixer_add_control() wants. Off the mixer there is nothing
+ * to satisfy: private_data is this driver and private_value is the
+ * index, so the per-control allocation goes away with its private_free.
+ */
+/*
+ * Builds into the caller's array rather than into m62->kctl[], which
+ * is only ever touched under m62->lock: an interrupt can arrive at any
+ * point here, since hid_device_io_start() ran before component_add().
+ * Nothing this function makes is visible to .raw_event() until the
+ * whole set is published, below.
+ */
+static int topping_add_kctl(struct topping_m62 *m62, struct snd_card *card,
+ struct snd_kcontrol **kctl,
+ const struct snd_kcontrol_new *tmpl,
+ const char *name, int idx, int slot,
+ const unsigned int *tlv)
+{
+ struct snd_kcontrol *k;
+ int err;
+
+ k = snd_ctl_new1(tmpl, m62);
+ if (!k)
+ return -ENOMEM;
+
+ k->private_value = idx;
+ k->tlv.p = tlv;
+ strscpy(k->id.name, name, sizeof(k->id.name));
+
+ err = snd_ctl_add(card, k);
+ if (err < 0)
+ return err; /* snd_ctl_add() freed it */
+
+ kctl[slot] = k;
+ return 0;
+}
+
+static void topping_drop_kctls(struct snd_card *card,
+ struct snd_kcontrol **kctl)
+{
+ int i;
+
+ for (i = 0; i < TOPPING_NUM_KCTLS; i++)
+ snd_ctl_remove(card, kctl[i]);
+}
+
+/* ------------------------------------------------------------------ */
+/* component */
+/* ------------------------------------------------------------------ */
+
+static int topping_build_kctls(struct topping_m62 *m62, struct snd_card *card,
+ struct snd_kcontrol **kctl)
+{
+ int i, err;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ err = topping_add_kctl(m62, card, kctl, &topping_ctl,
+ topping_m62_ctls[i].name, i, i,
+ topping_m62_ctls[i].tlv);
+ if (err < 0)
+ return err;
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ err = topping_add_kctl(m62, card, kctl, &topping_sel,
+ topping_m62_enums[i].name, i,
+ TOPPING_NUM_CTLS + i, NULL);
+ if (err < 0)
+ return err;
+ }
+ return 0;
+}
+
+static int topping_bind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS] = { };
+ struct snd_card *card = master_data;
+ int err;
+
+ err = topping_build_kctls(m62, card, kctl);
+ if (err < 0) {
+ topping_drop_kctls(card, kctl);
+ return err;
+ }
+
+ /*
+ * One publication, and m62->card is the flag for it: until this
+ * store, .raw_event() sees a NULL card and never looks at
+ * m62->kctl[] at all, so the set above was built where no
+ * interrupt could reach it. Both fields are written here under
+ * the lock that every reader takes.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ memcpy(m62->kctl, kctl, sizeof(m62->kctl));
+ m62->card = card;
+ }
+
+ /*
+ * Subscribe and ask for the state HERE rather than at probe:
+ * before this point every announced value would land in the
+ * cache with no control to notify. The card answers in two
+ * waves -- the jacks at once, the gain of a connected input
+ * about 5 s later -- so nothing here waits for them: each value
+ * arrives through .raw_event() and notifies its own control.
+ */
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+}
+
+static void topping_unbind(struct device *comp, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(comp);
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+ struct snd_card *card;
+
+ /*
+ * The mirror of the publication above: take the whole set away
+ * under the lock, so that once this scope ends no reader can
+ * reach a control, and only then remove them -- snd_ctl_remove()
+ * sleeps and cannot be called from in here.
+ *
+ * Clearing the card also stops topping_resume() from restarting
+ * the keepalive, which makes the cancel below final.
+ *
+ * @master_data is deliberately unused. The audio side cannot look
+ * its own context up while its devres is unwinding, so it passes
+ * NULL; the card to take the controls off is the one this driver
+ * was handed at bind, and it is right here.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock) {
+ card = m62->card;
+ memcpy(kctl, m62->kctl, sizeof(kctl));
+ memset(m62->kctl, 0, sizeof(m62->kctl));
+ m62->card = NULL;
+ }
+
+ if (!card)
+ return;
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ topping_drop_kctls(card, kctl);
+}
+
+static const struct component_ops topping_component_ops = {
+ .bind = topping_bind,
+ .unbind = topping_unbind,
+};
+
+/* ------------------------------------------------------------------ */
+/* HID */
+/* ------------------------------------------------------------------ */
+
+static int topping_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ struct topping_m62 *m62;
+ int err;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ m62 = devm_kzalloc(&hdev->dev, sizeof(*m62), GFP_KERNEL);
+ if (!m62)
+ return -ENOMEM;
+
+ m62->hdev = hdev;
+ m62->intf = to_usb_interface(hdev->dev.parent);
+ if (m62->intf->cur_altsetting->desc.bInterfaceNumber !=
+ M62_VENDOR_IFNUM)
+ return -ENODEV;
+
+ spin_lock_init(&m62->lock);
+ INIT_DELAYED_WORK(&m62->keepalive, topping_keepalive);
+ hid_set_drvdata(hdev, m62);
+
+ err = devm_mutex_init(&hdev->dev, &m62->write_lock);
+ if (err)
+ return err;
+
+ err = hid_parse(hdev);
+ if (err)
+ return err;
+
+ /*
+ * HIDRAW and no input device. The descriptor would only make a
+ * nonexistent pointer, while a hidraw node is how this protocol
+ * was read in the first place and how the parts not exposed here
+ * -- the mixer matrix, the mutes, the EQ -- stay reachable.
+ */
+ err = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (err)
+ return err;
+
+ err = hid_hw_open(hdev);
+ if (err)
+ goto err_stop;
+
+ /*
+ * XXX awaiting the HID maintainers' word. usbhid arms every
+ * device it opens for remote wakeup, and this card does not
+ * offer it, which forbids runtime suspend to the whole device.
+ * Nothing here needs it: the resume path subscribes again and
+ * asks the card for its whole state, so a knob turned while the
+ * host slept is picked up by asking rather than by being told.
+ *
+ * This clear does not survive a hidraw open, which calls
+ * hid_hw_open() again -- so if it stays, HID_CONNECT_DRIVER has
+ * to replace HID_CONNECT_HIDRAW above.
+ */
+ m62->intf->needs_remote_wakeup = 0;
+
+ /*
+ * Reports are dropped for the whole of probe unless this is called,
+ * and component_add() below can bind synchronously when the audio
+ * side is already there -- which subscribes, and the device answers
+ * at once. Without this the identification wave is thrown away.
+ */
+ hid_device_io_start(hdev);
+
+ err = component_add(&hdev->dev, &topping_component_ops);
+ if (err)
+ goto err_close;
+
+ return 0;
+
+err_close:
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+ return err;
+}
+
+static void topping_remove(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ /* Runs topping_unbind() first if the audio side is bound. */
+ component_del(&hdev->dev, &topping_component_ops);
+
+ /*
+ * Unconditionally, and after component_del(): if the audio side
+ * had already unbound, the cancel there has been and gone, and
+ * a resume in between could have restarted the work. This is
+ * the last point before devm frees m62, so nothing may outlive
+ * it.
+ */
+ cancel_delayed_work_sync(&m62->keepalive);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static int topping_suspend(struct hid_device *hdev, pm_message_t message)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+
+ cancel_delayed_work_sync(&m62->keepalive);
+ return 0;
+}
+
+static int topping_resume(struct hid_device *hdev)
+{
+ struct topping_m62 *m62 = hid_get_drvdata(hdev);
+ unsigned int noio;
+
+ /*
+ * Nothing to report to yet, and nothing the card needs told:
+ * the next bind does the subscribing.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (!m62->card)
+ return 0;
+
+ /*
+ * Everything below runs without I/O reclaim: usbhid's own
+ * usb_interrupt_msg() allocates a URB with GFP_KERNEL, so asking
+ * for the frame buffer politely is not enough, and reclaim here
+ * can wait on a block device that has not woken yet.
+ *
+ * Subscribing again is not a formality: the device stops
+ * reporting to a host it has not heard from, and asking for the
+ * state refreshes a cache that may have gone stale while the
+ * panel was reachable and this driver was not.
+ */
+ noio = memalloc_noio_save();
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(m62, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_restore_sel(m62);
+
+ /*
+ * Restart the keepalive only if the audio side is still bound.
+ * Testing m62->card again, under the lock that topping_unbind()
+ * takes to clear it, is what keeps an unbind racing this
+ * function from leaving work behind that nothing will cancel.
+ */
+ scoped_guard(spinlock_irqsave, &m62->lock)
+ if (m62->card)
+ schedule_delayed_work(&m62->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+
+ memalloc_noio_restore(noio);
+ return 0;
+}
+
+static const struct hid_device_id topping_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, topping_devices);
+
+static struct hid_driver topping_driver = {
+ .name = "topping-m62",
+ .id_table = topping_devices,
+ .probe = topping_probe,
+ .remove = topping_remove,
+ .raw_event = topping_raw_event,
+ .suspend = topping_suspend,
+ .resume = topping_resume,
+ .reset_resume = topping_resume,
+};
+module_hid_driver(topping_driver);
+
+MODULE_DESCRIPTION("Topping M62 vendor controls");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
* [RFC PATCH v6 2/2] ALSA: usb-audio: bind the Topping M62's vendor controls
2026-09-04 16:58 ` [RFC PATCH v6 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
@ 2026-09-04 16:58 ` Mikhail Gavrilov
1 sibling, 0 replies; 53+ messages in thread
From: Mikhail Gavrilov @ 2026-09-04 16:58 UTC (permalink / raw)
To: tiwai
Cc: tiwai, perex, jikos, bentiss, linux-sound, linux-input,
linux-kernel, Mikhail Gavrilov
The M62's vendor controls are driven by hid-topping-m62, added in the
previous patch, which speaks a vendor protocol on the card's HID
interface. Those controls belong on the sound card that plays the
audio, not on a card of the HID driver's own.
This adds the other half of that: a component master, registered from
the M62's mixer quirk, which hands its struct snd_card to the HID
driver at bind time and takes the controls away again at unbind. The
lifetime rules are the component framework's, which is the point --
neither driver has to be told about the other's disconnect, and neither
has to guess at the other's state. The same shape binds HD-audio to the
graphics drivers in sound/hda/core/component.c, with sound as the
master there too.
The master's context lives in devres on the audio control interface
rather than in drvdata, which on a usb_interface belongs to
snd-usb-audio itself; devres_find(), keyed on the release function,
gives it back inside the callbacks, which are handed nothing but a
struct device *. It hangs off the control interface rather than off the
USB device because component_match_add() allocates the match list with
devm: on the interface that is released at unbind, while on the
usb_device it would live until the device itself was released and a
rebind would stack a second list on top.
Neither component_compare_dev() nor component_compare_dev_name() fits:
the audio side has no pointer to the HID device, and the HID device's
name carries an instance counter that is not predictable. The match is
therefore one of descent -- the HID device sits two levels below the
USB device -- and which interface it is stays the HID driver's
business, since it registers a component for the vendor interface and
for no other. That keeps sound/usb free of HID symbols and of any
opinion about this card's interface numbering.
component_master_add_with_match() returns 0 with the aggregate merely
pending when the HID driver is absent, so the card comes up either way
and grows the vendor controls if and when the other half appears.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
MAINTAINERS | 9 ++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 236 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 ++
5 files changed, 258 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 627595e245f3..b49560efa1c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27593,6 +27593,15 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING M62 VENDOR CONTROLS
+M: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
+L: linux-sound@vger.kernel.org
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-topping-m62.c
+F: sound/usb/mixer_topping.c
+F: sound/usb/mixer_topping.h
+
TOPSTAR LAPTOP EXTRAS DRIVER
M: Herton Ronaldo Krzesinski <herton@canonical.com>
L: platform-driver-x86@vger.kernel.org
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73..151b481df795 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -14,6 +14,7 @@ snd-usb-audio-y := card.o \
mixer_quirks.o \
mixer_scarlett.o \
mixer_scarlett2.o \
+ mixer_topping.o \
mixer_us16x08.o \
mixer_s1810c.o \
pcm.o \
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index a1f5592cc5d5..10f33026cdff 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -36,6 +36,7 @@
#include "mixer_quirks.h"
#include "mixer_scarlett.h"
#include "mixer_scarlett2.h"
+#include "mixer_topping.h"
#include "mixer_us16x08.h"
#include "mixer_s1810c.h"
#include "helper.h"
@@ -4531,6 +4532,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_fcp_init(mixer);
break;
+ case USB_ID(0x152a, 0x875c): /* Topping M62 */
+ err = snd_topping_init(mixer);
+ break;
+
case USB_ID(0x041e, 0x323b): /* Creative Sound Blaster E1 */
err = snd_soundblaster_e1_switch_create(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
new file mode 100644
index 000000000000..5557bc41f05f
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Topping M62 -- component master for the card's vendor controls.
+ *
+ * The M62's analogue gains, output volumes and source selectors are not
+ * described by the USB Audio Class. They are reached over a vendor
+ * protocol on the card's HID interface, which hid-topping-m62 speaks.
+ *
+ * This file speaks none of that protocol. It publishes the sound card to
+ * whoever drives the vendor interface, so that the controls are created on
+ * the card that plays the audio rather than on a card of their own, and are
+ * torn down when either side goes away. The lifetime rules are the
+ * component framework's, which is the point: neither driver has to guess at
+ * the other's state, and neither has to be told about the other's
+ * disconnect.
+ *
+ * The same shape binds HD-audio to the graphics drivers in
+ * sound/hda/core/component.c, with sound as the master there too.
+ */
+
+#include <linux/component.h>
+#include <linux/device.h>
+#include <linux/usb.h>
+
+#include <sound/core.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_topping.h"
+
+/*
+ * What the master hands the component at bind time. It lives in devres on
+ * the audio control interface rather than in drvdata, because drvdata on a
+ * usb_interface belongs to snd-usb-audio itself. devres_find(), keyed on
+ * the release function, gives it back inside the callbacks, which are
+ * handed nothing but a struct device *.
+ */
+struct topping_master {
+ struct device *dev; /* the audio control interface */
+ struct snd_card *card;
+ struct usb_mixer_interface *mixer;
+};
+
+static void topping_master_release(struct device *dev, void *res)
+{
+ /*
+ * Storage only. Taking the master down is a separate devres
+ * action registered after the match array, so that it runs
+ * before it -- see topping_master_teardown() below.
+ */
+}
+
+static struct topping_master *topping_get_master(struct device *dev)
+{
+ return devres_find(dev, topping_master_release, NULL, NULL);
+}
+
+/*
+ * Which of the registered components is ours.
+ *
+ * This is only ever called against devices that have registered with
+ * component_add(), so it does not have to defend itself against the whole
+ * device tree. What it does have to do is tell this card's vendor
+ * function apart from a second M62 on another port.
+ *
+ * The HID device sits two levels below the USB device:
+ *
+ * hid_device -> usb_interface -> usb_device
+ *
+ * WHICH interface it is, is the HID driver's business: it registers a
+ * component for the vendor interface and for nothing else. So the test
+ * here is one of descent alone and needs no HID symbols in sound/usb --
+ * which also keeps this file free of any opinion about the M62's
+ * interface numbering.
+ */
+static int topping_match_component(struct device *dev, void *data)
+{
+ return dev->parent && dev->parent->parent == data;
+}
+
+static int topping_master_bind(struct device *dev)
+{
+ struct topping_master *tm = topping_get_master(dev);
+
+ /*
+ * Not a bug, and deliberately not a WARN. devres_release_all()
+ * moves every node off the device before it calls a single
+ * release, so between a failed probe beginning to unwind and the
+ * teardown action running, the context is already gone while the
+ * aggregate is still registered. A component that arrives in
+ * that window has simply come at the wrong moment; EPROBE_DEFER
+ * says so, and the framework does not log it.
+ */
+ if (!tm)
+ return -EPROBE_DEFER;
+
+ return component_bind_all(dev, tm->card);
+}
+
+/*
+ * No topping_get_master() here, deliberately. This can run while the
+ * interface's devres is unwinding, and devres_release_all() moves every
+ * node off the device before it calls a single release, so the lookup
+ * would come back empty and the unbind would be skipped altogether.
+ *
+ * The component does not need the card from us in any case: it kept the
+ * one it was handed at bind, and takes its controls off that.
+ */
+static void topping_master_unbind(struct device *dev)
+{
+ component_unbind_all(dev, NULL);
+}
+
+static const struct component_master_ops topping_master_ops = {
+ .bind = topping_master_bind,
+ .unbind = topping_master_unbind,
+};
+
+/*
+ * Registered as a devres action AFTER component_match_add(), because
+ * devres unwinds in reverse: this then runs before the match array is
+ * freed, and component_unbind_all() walks that array.
+ *
+ * Two roads reach it. Normally topping_private_free() calls it through
+ * devm_release_action(). The other is a probe that got as far as
+ * creating this mixer and then failed: usb_audio_probe() leaves the
+ * card and its mixer list alone in that case, as long as an earlier
+ * interface had succeeded, so the mixer would outlive the interface
+ * whose devres this is. Unhooking it here is what keeps a later
+ * disconnect from reaching freed storage.
+ */
+static void topping_master_teardown(void *data)
+{
+ struct topping_master *tm = data;
+
+ component_master_del(tm->dev, &topping_master_ops);
+
+ if (tm->mixer) {
+ tm->mixer->private_data = NULL;
+ tm->mixer->private_free = NULL;
+ }
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_master *tm = mixer->private_data;
+
+ if (!tm)
+ return;
+
+ /*
+ * Reached from snd_usb_mixer_disconnect(), on an unplug and on an
+ * unbind of the audio interface alike. The action clears
+ * mixer->private_data on its way through, so nothing is left
+ * pointing at the storage dropped below.
+ */
+ devm_release_action(tm->dev, topping_master_teardown, tm);
+ devres_destroy(tm->dev, topping_master_release, NULL, NULL);
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct component_match *match = NULL;
+ struct usb_interface *intf;
+ struct topping_master *tm;
+ struct device *dev;
+ int err;
+
+ /*
+ * The master hangs off the audio control interface rather than off
+ * the USB device: component_match_add() allocates the match list
+ * with devm, and on an interface that is released when the interface
+ * is unbound. On the usb_device it would live until the device
+ * itself was released, and a rebind would stack a second list on top
+ * of the first.
+ */
+ intf = usb_ifnum_to_if(chip->dev,
+ get_iface_desc(mixer->hostif)->bInterfaceNumber);
+ if (!intf)
+ return -ENODEV;
+ dev = &intf->dev;
+
+ tm = devres_alloc(topping_master_release, sizeof(*tm), GFP_KERNEL);
+ if (!tm)
+ return -ENOMEM;
+ tm->dev = dev;
+ tm->card = chip->card;
+ tm->mixer = mixer;
+ devres_add(dev, tm);
+
+ mixer->private_data = tm;
+ mixer->private_free = topping_private_free;
+
+ component_match_add(dev, &match, topping_match_component,
+ &chip->dev->dev);
+
+ /*
+ * component_match_add() reports a failed allocation by storing
+ * an error pointer rather than by returning, and
+ * component_master_add_with_match() dereferences what it is
+ * given without looking.
+ */
+ if (IS_ERR(match)) {
+ err = PTR_ERR(match);
+ goto err_free;
+ }
+
+ /*
+ * This returns 0 with the aggregate merely pending when
+ * hid-topping-m62 has not registered its component yet:
+ * try_to_bring_up_aggregate_device() reports an incomplete set as
+ * "not ready", not as an error. So the card comes up either way and
+ * grows the vendor controls if and when the other half appears.
+ */
+ err = component_master_add_with_match(dev, &topping_master_ops, match);
+ if (err < 0)
+ goto err_free;
+
+ /* Last, so that devres releases it first -- see the teardown. */
+ err = devm_add_action(dev, topping_master_teardown, tm);
+ if (err < 0) {
+ component_master_del(dev, &topping_master_ops);
+ goto err_free;
+ }
+
+ return 0;
+
+err_free:
+ mixer->private_data = NULL;
+ mixer->private_free = NULL;
+ devres_destroy(dev, topping_master_release, NULL, NULL);
+ usb_audio_err(chip, "Topping: no component master: %d\n", err);
+ return err;
+}
diff --git a/sound/usb/mixer_topping.h b/sound/usb/mixer_topping.h
new file mode 100644
index 000000000000..15e16b509eb9
--- /dev/null
+++ b/sound/usb/mixer_topping.h
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#ifndef __USB_MIXER_TOPPING_H
+#define __USB_MIXER_TOPPING_H
+
+int snd_topping_init(struct usb_mixer_interface *mixer);
+
+#endif /* __USB_MIXER_TOPPING_H */
--
2.55.0
^ permalink raw reply related [flat|nested] 53+ messages in thread
end of thread, other threads:[~2026-09-04 16:59 UTC | newest]
Thread overview: 53+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-12 17:10 snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c) Mikhail Gavrilov
2026-08-13 7:24 ` Takashi Iwai
2026-08-20 15:13 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 2/2] HID: topping: driver for the M62's vendor control channel Mikhail Gavrilov
2026-08-21 11:23 ` [RFC 0/2] Two ways to reach the Topping M62's analogue gains Mikhail Gavrilov
2026-08-23 8:50 ` Takashi Iwai
2026-08-23 14:22 ` [PATCH v2 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 14:22 ` [PATCH v2 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-23 14:22 ` [PATCH v2 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-23 19:48 ` [PATCH v3 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-23 22:29 ` [PATCH v4 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-24 20:13 ` [PATCH v5 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-24 22:31 ` [PATCH v6 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-25 8:56 ` [PATCH v7 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-25 11:12 ` [PATCH v8 2/2] ALSA: usb-audio: let the M62's outputs say what they listen to Mikhail Gavrilov
2026-08-26 18:06 ` [PATCH v8 0/2] ALSA: usb-audio: the Topping M62's vendor controls Mikhail Gavrilov
2026-09-03 8:30 ` Takashi Iwai
2026-09-03 9:35 ` Mikhail Gavrilov
2026-09-03 10:02 ` Takashi Iwai
2026-09-03 10:19 ` Mikhail Gavrilov
2026-09-04 0:18 ` Mikhail Gavrilov
2026-09-04 7:05 ` Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 11:26 ` [RFC PATCH 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 14:11 ` [RFC PATCH v2 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 14:42 ` [RFC PATCH v3 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 14:42 ` [RFC PATCH v3 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 14:43 ` [RFC PATCH v3 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 15:30 ` [RFC PATCH v4 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 16:22 ` [RFC PATCH v5 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 0/2] the Topping M62's vendor controls, on the component framework Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 1/2] HID: topping-m62: driver for the M62's vendor controls Mikhail Gavrilov
2026-09-04 16:58 ` [RFC PATCH v6 2/2] ALSA: usb-audio: bind the Topping " Mikhail Gavrilov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox