* [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework
@ 2026-10-18 12:48 Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 1/8] HID: topping: transport for the M62's vendor controls Mikhail Gavrilov
` (7 more replies)
0 siblings, 8 replies; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
The Topping M62 and E2x2 OTG keep part of their mixer behind a vendor
protocol on a HID interface, outside the USB Audio Class: on the M62 the
analogue input gains, the output volumes, the MUTEs beside them and the
output and loopback routing, on the E2x2 OTG the input gains, the
faders, the routing and the output switches. On Linux they can only be
set by hand on the front panel where it has a knob for them, and not at
all where it has none. An E2x2 OTG whose headphone, TRS and AUX outputs
were switched off by the vendor's application stays silent on Linux, and
so does an M62 input or output that application left muted. The M62's
battery level, which the card reports, shows nowhere, and an M62 whose
USB-C port that application has set not to charge from the computer runs
its battery down, with nothing on Linux to set it back.
This series makes them ordinary ALSA controls on the card snd-usb-audio
already creates. 1/8 is a HID driver that carries the protocol, and 2/8
adds the M62's controls to its mixer quirk, which is also the component
master the HID driver binds to; these are v8's two patches, 1/8
unchanged and with Jiri Kosina's Acked-by, 2/8 with the changes below.
3/8 and 4/8 add the E2x2 OTG, which speaks the same frame; 5/8 offers
the M62's battery level as a power supply, 6/8 the power settings of
its two USB ports as controls, 7/8 the M62's MUTEs as switches and 8/8
the E2x2 OTG's input MUTEs. 1/8, 3/8 and 5/8 are HID, the other five
ALSA: the ALSA patches build on the header 1/8 adds and need nothing
else from the HID side.
Changes since v8
(https://lore.kernel.org/all/20261003184217.34671-1-mikhail.v.gavrilov@gmail.com/):
- 3/8 and 4/8 are new: the E2x2 OTG, 152a:8756. It takes the same
frames without a subscription or a keep-alive and gets 21 controls:
three digital input gains, six faders, the headphone, TRS and AUX
output switches, the headphone gain switch, the two MON switches and
six source selectors. The control tables become per model, chosen by
USB ID, with the per-card state sized from the model, and the HID
device table names the interface each card carries the protocol on:
another card on the same protocol is then its tables and its entries;
- 2/8: the M62's four loopback returns get a source selector and a
gain each. The card reports neither, so they start where the vendor
application puts them; the gain is written as that application
writes it, Q25 in whole decibels from -89 to +12 dB;
- 2/8: "Mic-1" and "Mic-2" become "Mic1" and "Mic2", as every other
control name in the tree has it;
- 2/8: every input gain is written at the first bind too, at 0 dB,
where it changes nothing. The card reports a gain only for an input
with something plugged into it, and never BT's or OTG IN's, so a
gain it had not reported showed 0, which alsactl stored and wrote
back at the next connect, muting AUX, BT and OTG IN. A resume writes
the gains again with everything else;
- 2/8: the M62's operating mode is read from bcdDevice. Live Streaming
Mode goes with Pro Audio. In Mobile Mode, where the card records one
mix of all its inputs and part of the address map means something
else, the loopback returns and the selectors, which that mode has
not, are left out, and the Playback 1/2 knob and the Recording fader
are offered instead, written at 0 dB at the first bind;
- 5/8 is new: the M62's battery level as a power supply of device
scope, registered once the card has reported a level. A card in
Mobile Mode announces its level only inside a session, so the driver
now opens one at bind and at resume, with the handshake 0x11/0x01
the vendor application sends, and like that application waits for
the card's answer before it subscribes or writes anything;
- 6/8 is new: the M62's USB-C and OTG port power settings as
enumerated controls with the vendor application's items, written at
the first bind, the USB-C port to Charge and the OTG port to Off,
and kept up to date from the card's announce, which leaves them out
now and then;
- 7/8 is new: the MUTE beside each of the M62's levels, as a switch
named after it. The card releases HP's MUTE in about half the writes
of HP's volume made under it, and a sound server writes such a
volume whenever it switches back to a device it has saved as muted,
so a level set while its switch is off is kept and written when the
switch is turned on;
- 8/8 is new: the E2x2 OTG's input MUTEs, which that card keeps as the
input gain's 0, as switches named after the gains.
The selectors keep "... Source". Named "... Playback Route" or
"... Capture Route", a selector would join the volume of the same name
in alsa-lib's simple mixer, which then reads and writes only the
selector.
Tested
Fedora, 7.3.0-rc7-24a19b953e2a plus this series, with KASAN (generic),
lockdep and UBSAN, an M62 (firmware V87.05.45.48.27, bcdDevice 3.27 in
Pro Audio Mode, 1.45 in Mobile Mode) and an E2x2 OTG (firmware V1.10,
bcdDevice 1.10). Both loaded modules were matched against the installed
files by build ID.
- fifty cycles of hid-topping unload and load with both cards
connected and PipeWire running: a hundred binds, two per cycle, and
every mixer control of both cards the same before and after;
- five audio-side rebinds of each card with the HID driver left bound,
all three snd-usb-audio interfaces unbound and the control interface
bound again: ten binds, every control back;
- "alsactl restore" exits 0 on both cards;
- suspend to RAM and back: the controls of both cards unchanged, and
afterwards the M62's headphone knob and an E2x2 OTG MON button each
reported to their own card;
- the cable pulled out twice on each card during playback while a loop
wrote a loopback gain: each time the card bound again once plugged
back; after the last plug the control took the loop's writes again
and read the last one written, and neither driver logged an error;
- the M62 switched to Mobile Mode and back: the driver's twenty
controls in Mobile Mode, the first bind writing every one of them,
and alsactl's restore the stored gains 0.11 s later;
- the M62's port power at eighteen first binds with the HID driver
loaded after the card, out of reach of alsactl's restore: on a card
left on Off and Discharge the bind wrote Charge and Off, and Off and
Discharge written right after the bind, as a restore of those values
would, left the card on the bus for the 10 s it was watched; every
value of the two the card announced afterwards was the one last
written;
- the M62 answered each of the 74 handshakes captured in these runs
10 to 314 ms after it, and nothing else was sent to it in between;
- the M62's Loopback 1/2, with a 1 kHz tone at -20 dBFS peak on
Playback 1/2 and all sixteen capture columns recorded: its source
set to Playback 3/4 turned the two Loopback 1/2 columns into digital
zero and set back brought the tone back; its gain at -20 and +12 dB
moved them by -20.00 and +12.00 dB;
- each M62 input's switch turned off took the input to digital zero,
in its own capture columns in Pro Audio Mode and in the recording in
Mobile Mode, IN 1 and IN 2 there once the card's reverb had died
away, and turned on brought it back;
- each E2x2 OTG input's switch turned off silenced the card's meter of
that input; a gain set while it was off was not written, and the
switch turned on wrote it and the meter came back;
- no WARNING, BUG, KASAN, UBSAN, lockdep report or call trace in the
kernel log over these runs.
The M62's battery shows as a power supply, and in UPower, 2.6 to 2.8
seconds after the card is plugged in, and 3.5 to 3.7 seconds after in
Mobile Mode.
With PipeWire and the M62's UCM profile
(https://github.com/alsa-project/alsa-ucm-conf/pull/826), the M62's
headphone level follows the desktop volume and the front-panel knob
both ways; with the E2x2 OTG's profile, to be sent after that pull
request, the desktop volume moves the E2x2 OTG's Master.
Not tested:
- the reload cycles with stock alsa-lib: this machine carries the
remap fix;
- hibernation, and with it .reset_resume;
- kmemleak, compiled in here but disabled at boot;
- a card whose battery has run down;
- a big-endian host.
The plain E2x2, 152a:8752, is left out: it needs its own channel names
and a test by someone who has one.
Mikhail Gavrilov (8):
HID: topping: transport for the M62's vendor controls
ALSA: usb-audio: add the Topping M62's vendor controls
HID: topping: add the E2x2 OTG
ALSA: usb-audio: add the Topping E2x2 OTG's vendor controls
HID: topping: report the M62's battery level
ALSA: usb-audio: add the Topping M62's port power settings
ALSA: usb-audio: add the Topping M62's mutes
ALSA: usb-audio: add the Topping E2x2 OTG's input mutes
MAINTAINERS | 10 +
drivers/hid/Kconfig | 22 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 4 +
drivers/hid/hid-quirks.c | 4 +
drivers/hid/hid-topping.c | 686 ++++++++++++++++++
include/sound/topping.h | 57 ++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 6 +
sound/usb/mixer_topping.c | 1452 +++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
11 files changed, 2250 insertions(+)
create mode 100644 drivers/hid/hid-topping.c
create mode 100644 include/sound/topping.h
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
base-commit: f6e8d5ebde5ceb7d3c0eef6e9e4d82862b503ade
--
2.43.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH v9 1/8] HID: topping: transport for the M62's vendor controls
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
2026-10-18 13:13 ` sashiko-bot
2026-10-18 12:48 ` [PATCH v9 2/8] ALSA: usb-audio: add the Topping " Mikhail Gavrilov
` (6 subsequent siblings)
7 siblings, 1 reply; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov, Jiri Kosina
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.
This driver is the transport and owns no controls. The controls belong
on the sound card that plays the audio, and they have to outlive this
driver's binding: a module reload that took them off the card would
renumber them, while a stored alsactl state and a sound server holding
the card both key on what was there. So they live in snd-usb-audio,
added in the following patch, and the two drivers meet through the
component framework with the audio side as the master. At bind this
driver fills in the struct topping_component it is handed -- the
header is new here, in the shape of struct drm_audio_component -- with
the one operation the controls need, sending a frame, and from then on
passes every valid frame the card reports to the audio side, which
decides what each one means. At unbind it tells the audio side first,
so that nothing is inside the operation when it goes. Without the audio
half this driver binds, subscribes and passes nothing on, 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.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
Acked-by: Jiri Kosina <jkosina@suse.com>
---
drivers/hid/Kconfig | 20 ++
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-quirks.c | 3 +
drivers/hid/hid-topping.c | 498 ++++++++++++++++++++++++++++++++++++++
include/sound/topping.h | 57 +++++
6 files changed, 582 insertions(+)
create mode 100644 drivers/hid/hid-topping.c
create mode 100644 include/sound/topping.h
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c43e824..fecbbaa 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1284,6 +1284,26 @@ config HID_TIVO
help
Say Y if you have a TiVo Slide Bluetooth remote control.
+config HID_TOPPING
+ tristate "Topping M62 vendor controls"
+ depends on USB_HID
+ depends on SND_USB_AUDIO
+ select CRC16
+ help
+ The vendor protocol behind 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 the card's HID interface, so a
+ driver is needed for them to work at all.
+
+ The controls themselves belong to snd-usb-audio, on the sound
+ card it makes for the same device; this driver carries them
+ to the card, so both are needed. The card works without this
+ one; its vendor controls simply never appear.
+
+ To compile this driver as a module, choose M here: the module
+ will be called hid-topping.
+
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 cb7f16d..1af487c 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1477,6 +1477,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 96a36c5..bca22bb 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -790,6 +790,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)
+ { 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.c b/drivers/hid/hid-topping.c
new file mode 100644
index 0000000..a8854b2
--- /dev/null
+++ b/drivers/hid/hid-topping.c
@@ -0,0 +1,498 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Vendor protocol 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, its output volumes
+ * and its output source selectors behind a vendor protocol on a
+ * HID-class interface, and exposes none of them through UAC.
+ *
+ * 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 what it reports at all.
+ *
+ * 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 ARE NOT THIS DRIVER'S. They live in snd-usb-audio, on
+ * the card that plays the audio, and outlive any binding of this
+ * driver. This driver is the transport: it registers a component, the
+ * M62 mixer quirk is the master, and at bind this driver fills in the
+ * struct topping_component it is handed with the one operation the
+ * controls need -- send a frame -- and from then on passes on every
+ * frame the card reports.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/crc16.h>
+#include <linux/hid.h>
+#include <linux/module.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/topping.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
+
+struct topping_hid {
+ struct hid_device *hdev;
+ struct usb_interface *intf; /* for runtime PM */
+
+ /*
+ * The audio side, while it has this driver bound, and NULL
+ * otherwise. Frames arrive before it is there and after it has
+ * gone, so everything that passes one on reads this under lock.
+ */
+ struct topping_component *comp;
+ spinlock_t lock; /* guards comp */
+
+ struct delayed_work keepalive;
+};
+
+/* ------------------------------------------------------------------ */
+/* 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 the audio side 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.
+ * If 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_hid *th, 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(th->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(th->hdev, "write %02x/%02x failed: %d\n",
+ target, prop, err);
+ return err;
+}
+
+/*
+ * The one operation the audio side is given. It wakes the device
+ * first; the resume that can run on this very thread as a result only
+ * schedules the audio side's write-back, so it takes no lock a caller
+ * here may be holding.
+ *
+ * The write-back after a resume comes through here too, close behind
+ * the resume itself, and usbhid allocates its URB with GFP_KERNEL --
+ * hence the NOIO scope round the send, for the reason given there.
+ */
+static int topping_write(struct device *dev, u8 target, u8 prop, s32 value)
+{
+ struct topping_hid *th = hid_get_drvdata(to_hid_device(dev));
+ unsigned int noio;
+ int err;
+
+ if (usb_autopm_get_interface(th->intf) < 0)
+ return -EIO;
+
+ noio = memalloc_noio_save();
+ err = topping_send(th, target, prop, value);
+ memalloc_noio_restore(noio);
+
+ usb_autopm_put_interface(th->intf);
+ return err;
+}
+
+static const struct topping_ops topping_hid_ops = {
+ .write = topping_write,
+};
+
+/*
+ * 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 check of the frame. Which frames
+ * mean anything is the audio side's business, so every valid one is
+ * passed on, the meters included.
+ *
+ * Runs in the interrupt handler's context, which is why the audio side
+ * is reached under a spinlock.
+ */
+static int topping_raw_event(struct hid_device *hdev,
+ struct hid_report *report, u8 *data, int size)
+{
+ struct topping_hid *th = hid_get_drvdata(hdev);
+
+ 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;
+
+ /*
+ * Under the lock that topping_unbind() takes to clear th->comp,
+ * so a frame cannot reach an audio side that has already been
+ * told this driver is going.
+ */
+ guard(spinlock_irqsave)(&th->lock);
+ if (th->comp)
+ th->comp->audio_ops->report(th->comp, data[5], data[6],
+ get_unaligned_be32(data + 7));
+ return 0;
+}
+
+static void topping_keepalive(struct work_struct *work)
+{
+ struct topping_hid *th = container_of(work, struct topping_hid,
+ keepalive.work);
+ int err;
+
+ err = topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ if (err == -ENODEV || err == -ESHUTDOWN)
+ return; /* the device has gone; nothing to renew */
+
+ schedule_delayed_work(&th->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+}
+
+/* ------------------------------------------------------------------ */
+/* component */
+/* ------------------------------------------------------------------ */
+
+static int topping_bind(struct device *dev, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct topping_hid *th = hid_get_drvdata(hdev);
+ struct topping_component *comp = master_data;
+
+ comp->dev = &hdev->dev;
+ comp->ops = &topping_hid_ops;
+
+ /*
+ * Published only now: until this store every frame stops here,
+ * and from it on every frame reaches the audio side, which keeps
+ * what it is told whether or not its controls exist yet.
+ */
+ scoped_guard(spinlock_irqsave, &th->lock)
+ th->comp = comp;
+
+ /*
+ * Subscribe and ask for the state HERE rather than at probe:
+ * before this point there is nobody to pass the answer to. 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() on its own.
+ */
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&th->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ return 0;
+}
+
+static void topping_unbind(struct device *dev, struct device *master,
+ void *master_data)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct topping_hid *th = hid_get_drvdata(hdev);
+ struct topping_component *comp;
+
+ /*
+ * Taken away under the lock, so that once this scope ends no
+ * frame and no resume can reach the audio side. Clearing it 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 structure to clear is the one this driver was handed
+ * at bind, and it is right here.
+ */
+ scoped_guard(spinlock_irqsave, &th->lock) {
+ comp = th->comp;
+ th->comp = NULL;
+ }
+
+ if (!comp)
+ return;
+
+ cancel_delayed_work_sync(&th->keepalive);
+
+ /*
+ * The audio side stops writing before the operation goes: once
+ * this returns, nothing is inside topping_write() on its behalf
+ * and nothing will enter it.
+ */
+ comp->audio_ops->unbound(comp);
+ comp->ops = NULL;
+ comp->dev = NULL;
+}
+
+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_hid *th;
+ int err;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ th = devm_kzalloc(&hdev->dev, sizeof(*th), GFP_KERNEL);
+ if (!th)
+ return -ENOMEM;
+
+ th->hdev = hdev;
+ th->intf = to_usb_interface(hdev->dev.parent);
+ if (th->intf->cur_altsetting->desc.bInterfaceNumber !=
+ M62_VENDOR_IFNUM)
+ return -ENODEV;
+
+ spin_lock_init(&th->lock);
+ INIT_DELAYED_WORK(&th->keepalive, topping_keepalive);
+ hid_set_drvdata(hdev, th);
+
+ 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 as
+ * controls -- 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;
+
+ /*
+ * 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_hid *th = 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 th, so nothing may outlive
+ * it.
+ */
+ cancel_delayed_work_sync(&th->keepalive);
+
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static int topping_suspend(struct hid_device *hdev, pm_message_t message)
+{
+ struct topping_hid *th = hid_get_drvdata(hdev);
+
+ cancel_delayed_work_sync(&th->keepalive);
+ return 0;
+}
+
+static int topping_resume(struct hid_device *hdev)
+{
+ struct topping_hid *th = 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, &th->lock)
+ if (!th->comp)
+ return 0;
+
+ /*
+ * 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 what may have gone stale while the panel was
+ * reachable and this driver was not.
+ */
+ noio = memalloc_noio_save();
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ memalloc_noio_restore(noio);
+
+ /*
+ * Tested again under the lock topping_unbind() takes to clear it,
+ * so an unbind racing this function can neither be told of a
+ * resume after it has gone nor leave work behind that nothing
+ * will cancel. The audio side only schedules its write-back
+ * from here; the writes themselves come later, through
+ * topping_write().
+ */
+ scoped_guard(spinlock_irqsave, &th->lock) {
+ if (th->comp) {
+ th->comp->audio_ops->resumed(th->comp);
+ schedule_delayed_work(&th->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ }
+ }
+
+ 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",
+ .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 vendor control protocol");
+MODULE_AUTHOR("Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>");
+MODULE_LICENSE("GPL");
diff --git a/include/sound/topping.h b/include/sound/topping.h
new file mode 100644
index 0000000..c3ca02f
--- /dev/null
+++ b/include/sound/topping.h
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Topping vendor controls -- what hid-topping and the M62 mixer quirk in
+ * snd-usb-audio hand each other at component bind.
+ *
+ * The audio side owns this structure and the controls. The HID driver,
+ * which speaks the card's vendor protocol, fills in @dev and @ops when it
+ * binds and clears them when it unbinds, and passes on what the card
+ * reports through @audio_ops. The shape is that of struct
+ * drm_audio_component, which joins HD-audio to the graphics drivers.
+ */
+#ifndef __SOUND_TOPPING_H
+#define __SOUND_TOPPING_H
+
+#include <linux/types.h>
+
+struct device;
+struct topping_component;
+
+/**
+ * struct topping_ops - what the HID driver offers
+ * @write: send one frame to the card, waking it first if it is
+ * runtime-suspended. May sleep. Returns 0 or a negative error.
+ */
+struct topping_ops {
+ int (*write)(struct device *dev, u8 target, u8 prop, s32 value);
+};
+
+/**
+ * struct topping_audio_ops - what the audio side asks to be told
+ * @report: a valid frame arrived from the card. Called in atomic context
+ * for every frame, the meters included.
+ * @resumed: the card was resumed and subscribed again. Called in atomic
+ * context.
+ * @unbound: the HID driver is unbinding. @ops goes away when this
+ * returns, so nothing may be inside it by then. May sleep.
+ */
+struct topping_audio_ops {
+ void (*report)(struct topping_component *comp, u8 target, u8 prop,
+ s32 value);
+ void (*resumed)(struct topping_component *comp);
+ void (*unbound)(struct topping_component *comp);
+};
+
+/**
+ * struct topping_component - where the two drivers meet
+ * @dev: the HID device, while it is bound
+ * @ops: set by the HID driver at bind, cleared at unbind
+ * @audio_ops: set by the audio side before the first bind
+ */
+struct topping_component {
+ struct device *dev;
+ const struct topping_ops *ops;
+ const struct topping_audio_ops *audio_ops;
+};
+
+#endif /* __SOUND_TOPPING_H */
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v9 2/8] ALSA: usb-audio: add the Topping M62's vendor controls
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 1/8] HID: topping: transport for the M62's vendor controls Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 3/8] HID: topping: add the E2x2 OTG Mikhail Gavrilov
` (5 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
The M62's analogue input gains, output volumes and output source
selectors are reached over a vendor protocol on the card's HID
interface, which hid-topping, added in the previous patch, speaks.
This adds the controls themselves, on the sound card that plays the
audio, and the component master that joins them to that driver.
Seventeen controls in Pro Audio Mode: five input gains (IN 1, IN 2,
AUX, BT, OTG IN), two output volumes (HP, OTG OUT), the two output
source selectors, and a source selector and a gain for each of the four
loopback returns. 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. A selector answers on one target of a pair -- the second
of an output's, the first of a loopback return's -- and a loopback
return's gain is written to both, as Q25 in whole decibels from -89
to +12 dB, the values the vendor application writes for each step. The
mixer matrix, the mutes, the input power and the EQ remain reachable
only through the HID driver's hidraw node.
The M62 has three operating modes, chosen on its own panel, and keeps
one USB ID in all three; the high byte of bcdDevice tells them apart:
01xx Mobile, 02xx Live Streaming, 03xx Pro Audio. Live Streaming goes
with Pro Audio: the application's connect push in each writes the
inputs, the loopback returns and the outputs at the same targets and on
the same scales, and Live Streaming only adds two mixes and an effects
bus that no control here reaches. In Mobile Mode the card is a two-in,
two-out device for a phone. It mixes all of its inputs into one stereo
recording, which is the whole of its capture: a microphone on IN 1, a
phone on Bluetooth and a source on AUX are all in it at once. The
host's playback is not in the recording; the headphones carry it with
every input. The card has no output selectors and no loopback returns
in that mode, and the vendor application shows none and writes none.
Part of the address map means something else in Mobile Mode. The
application writes whole decibels there to property 03 of 0x51..0x54,
where Pro Audio Mode takes Q25, and 0x51/0x52 is the level of the
recording itself: on a card in Mobile Mode, writing to it the 0 dB in
Q25 that Loopback 1/2 Capture Volume starts at raised the recording
by 9 dB, from -87.7 to -78.5 dBFS of input noise. The selectors have
nothing to select. So a row that belongs to Mobile Mode alone, or to
the other two alone, says which, and the mode is taken from bcdDevice
when the card appears; a change of mode re-enumerates the card. The
loopback returns and the six selectors are Pro Audio Mode's. The input
gains and the headphone and OTG volumes are in both: the application's
Mobile Mode screen shows them on the scales these controls use, IN 1
and IN 2 at +26 dB written as 26, AUX at 0.0 dB as 81, BT and OTG IN
at -6.0 dB as 87, the headphone output at -50 dB as 21 and the OTG
output at 0 dB as 99. Mobile Mode gets the two levels it has on the
targets of two of the loopback returns, as the application writes them:
- PCM Playback Volume, the Playback 1/2 knob: the level of the host's
playback in the headphones, -90 to +12 dB on 0x53/0x54;
- Recording Capture Volume, the Recording fader: the level of the
recording, -90 to 0 dB on 0x51/0x52.
The controls live as long as the card. They are created at the first
bind of the HID driver and stay when it unbinds, unchanged and without
an event: a value written meanwhile is kept, and the next bind writes
back every value. They are not even marked inactive, because alsa-lib's
simple mixer handles an INFO event by removing the element and adding it
again, and a sound server would see the same churn as a removal. So
reloading either driver changes nothing a holder of the card can see --
the controls keep their numids, a stored alsactl state keeps matching
them, a mixer application keeps its elements -- and no control is left
showing a value the card does not hold.
The card's state cannot be read. The card reports what a hand does to
its hardware, every turn of a front-panel knob, and when a session opens
it announces the gains of the inputs that have something plugged in --
of no other input, and never BT's or OTG IN's. A source selector has no
front-panel control and is never reported, nor are Mobile Mode's two
levels, and an output volume is not reported until its knob turns.
Topping were asked for a command that reads the selection and declined
to provide one or to commit to adding one. A host that cannot read these
values has to set them, or it inherits whatever the last host, or a hand
on the panel, left in a battery-powered card, so the first bind writes
all of them and from then on the cache is the truth, which the card's
own reports keep up to date. The gains are no exception: left unwritten,
a gain the card has not reported shows 0, alsactl stores that 0, and its
restore at the next connect writes it back, which mutes AUX, BT and
OTG IN.
The values are the ones the rest of snd-usb-audio would choose rather
than the vendor's. For a volume whose GET_CUR fails, init_cur_mix_raw()
sets the minimum, and the level a user hears is left to userspace --
alsactl and the sound server, which run after the card appears. So the
analogue headphone stage starts at its quiet end; every input gain
starts at 0 dB and the digital stream to a phone at unity, where they
change nothing; and each output listens to the host's main stream,
Playback 1/2, with the mixer out of the path. The loopback returns have
no such default to borrow, so they start where the vendor application
puts them -- on Playback 1/2, Playback 3/4, BT and OTG IN -- at unity
gain. In Mobile Mode, which has no selector to take the mixer out of the
path, the Playback 1/2 knob and the Recording fader start at 0 dB, where
they change nothing: the knob passes the playback's level on to the
headphones as Pro Audio Mode's outputs do, and the fader is at its top.
After a resume the same values are written again from the cache, in case
the card came up on its own defaults, or was changed on its panel, while
the host slept.
The master's context is reached through devres on the audio control
interface rather than through 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. The context itself is
reference-counted, held by the mixer until it disconnects and by each
control until the card frees it.
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 | 10 +
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 991 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
5 files changed, 1014 insertions(+)
create mode 100644 sound/usb/mixer_topping.c
create mode 100644 sound/usb/mixer_topping.h
diff --git a/MAINTAINERS b/MAINTAINERS
index a5c7dbd..75fdd2b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -27630,6 +27630,16 @@ S: Maintained
W: https://tomoyo.sourceforge.net/
F: security/tomoyo/
+TOPPING AUDIO INTERFACE 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.c
+F: include/sound/topping.h
+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 2e842dc..5b511b5 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -15,6 +15,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 01210cc..78d1966 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 "mixer_evo.h"
@@ -4544,6 +4545,10 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_evo_controls_create(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..cd848a3
--- /dev/null
+++ b/sound/usb/mixer_topping.c
@@ -0,0 +1,991 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Topping M62 -- the card's vendor controls.
+ *
+ * The M62's analogue input gains, output volumes, source selectors and
+ * loopback gains are not described by the USB Audio Class. They are
+ * reached over a vendor protocol on the card's HID interface, which
+ * hid-topping speaks. This file owns the controls; that driver is
+ * the transport. The two meet through the component framework, with
+ * this side as the master, in the same shape as HD-audio and the
+ * graphics drivers in sound/hda/core/component.c: the master owns a
+ * struct topping_component, and the HID driver fills in its one
+ * operation when it binds and clears it when it unbinds.
+ *
+ * THE CONTROLS LIVE AS LONG AS THE CARD. They are created at the first
+ * bind of the HID driver and not removed when it unbinds: a value
+ * written meanwhile is kept, and the next bind writes everything back.
+ * Nothing about them changes in between, not even their active flag --
+ * alsa-lib's simple mixer handles an INFO event by removing the element
+ * and adding it again, which is exactly the churn a sound server must
+ * not see. So reloading either driver changes nothing that anyone
+ * holding the card can see: the controls keep their numids, a stored
+ * alsactl state keeps matching them, and a mixer application keeps its
+ * elements.
+ *
+ * THE CARD'S STATE CANNOT BE READ. The card reports what a hand does to
+ * its hardware, every turn of a front-panel knob, and when a session
+ * opens it announces the gains of the inputs that have something
+ * plugged in; nothing answers a request for a value. A host that does
+ * not set the card inherits whatever the last host, or a hand on the
+ * panel, left in a battery-powered card. So the first bind writes
+ * everything, and from then on the cache is the truth, which the card's
+ * own reports keep up to date: each output listens to the host's main
+ * stream, Playback 1/2, with the mixer out of the path, or at 0 dB
+ * through it where there is no way around it, as on an M62 in Mobile
+ * Mode; every input gain starts at 0 dB, where it changes nothing; the
+ * analogue headphone stage starts at its quiet end; and the digital
+ * stream to a phone starts at unity. That follows the usual Linux
+ * division -- the kernel puts the hardware into a defined state, and the
+ * level a user hears is set from userspace, by alsactl's restore or init
+ * and by the sound server, both of which run after the card appears.
+ */
+
+#include <linux/bits.h>
+#include <linux/build_bug.h>
+#include <linux/cleanup.h>
+#include <linux/component.h>
+#include <linux/device.h>
+#include <linux/kref.h>
+#include <linux/mutex.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/usb.h>
+#include <linux/workqueue.h>
+
+#include <sound/control.h>
+#include <sound/core.h>
+#include <sound/tlv.h>
+#include <sound/topping.h>
+
+#include "usbaudio.h"
+#include "mixer.h"
+#include "helper.h"
+#include "mixer_topping.h"
+
+/* ------------------------------------------------------------------ */
+/* what the controls are */
+/* ------------------------------------------------------------------ */
+
+/*
+ * 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, what the
+ * first bind writes, 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;
+ int first; /* written at the first bind */
+ const unsigned int *tlv;
+ const s32 *map; /* what each step writes, or NULL: the step */
+ u8 modes; /* the modes that have it, or 0 for all */
+};
+
+/*
+ * THE M62 HAS THREE OPERATING MODES, chosen on its panel, and tells
+ * which in the high byte of bcdDevice: 01xx Mobile, 02xx Live Streaming,
+ * 03xx Pro Audio. Live Streaming and Pro Audio keep everything these
+ * controls reach at the same targets; Live Streaming only adds two mixes
+ * and an effects bus, which none of them reaches. Mobile Mode is another
+ * mixer behind the same targets: the card mixes all of its inputs into
+ * one stereo recording, which is the whole of its capture, and has no
+ * output selectors and no loopback returns. A row that belongs to
+ * Mobile Mode alone, or to the other two alone, says which.
+ */
+#define TOPPING_MOBILE BIT(0)
+#define TOPPING_PRO BIT(1)
+
+/*
+ * A loopback return's gain, in whole decibels: step 0 is off and step
+ * n is n - 90 dB, up to +12 dB. These are the values the vendor
+ * application writes for each step, Q25 with 0 dB at 1 << 25.
+ */
+static const s32 topping_m62_lb_gain[] = {
+ 0, 1190, 1335, 1498, 1681, 1886,
+ 2117, 2375, 2665, 2990, 3355, 3764,
+ 4224, 4739, 5318, 5966, 6694, 7511,
+ 8428, 9456, 10610, 11905, 13358, 14988,
+ 16817, 18869, 21171, 23754, 26653, 29905,
+ 33554, 37648, 42242, 47396, 53180, 59669,
+ 66949, 75119, 84284, 94569, 106108, 119055,
+ 133582, 149882, 168170, 188690, 211714, 237547,
+ 266532, 299054, 335544, 376486, 422425, 473968,
+ 531801, 596691, 669498, 751190, 842849, 945692,
+ 1061084, 1190556, 1335825, 1498821, 1681705, 1886904,
+ 2117141, 2375471, 2665323, 2990541, 3355443, 3764869,
+ 4224252, 4739689, 5318019, 5966915, 6694989, 7511901,
+ 8428492, 9456923, 10610843, 11905561, 13358259, 14988214,
+ 16817052, 18869043, 21171415, 23754718, 26653232, 29905418,
+ 33554432, 37648691, 42242527, 47396894, 53180190, 59669155,
+ 66949893, 75119016, 84284922, 94569238, 106108430, 119055617,
+ 133582599,
+};
+
+static_assert(ARRAY_SIZE(topping_m62_lb_gain) == 103);
+
+static const DECLARE_TLV_DB_SCALE(topping_tlv_lb, -9000, 100, 1);
+
+static const struct topping_ctl_desc topping_m62_ctls[] = {
+ { "Mic1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
+ 0, topping_tlv_gain }, /* 0 dB */
+ { "Mic2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
+ 0, topping_tlv_gain }, /* 0 dB */
+ { "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
+ 81, topping_tlv_out_9 }, /* 0 dB */
+ { "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
+ 99, topping_tlv_out_0 }, /* 0 dB */
+ { "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
+ 99, topping_tlv_out_0 }, /* 0 dB */
+ { "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
+ 0, topping_tlv_out_9 }, /* the quiet end */
+ { "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
+ 99, topping_tlv_out_0 }, /* 0 dB, unity */
+ /*
+ * A loopback return's gain is written to both targets of its pair
+ * and never reported. It starts at 0 dB, unity, as the vendor
+ * application sets it.
+ */
+ { "Loopback 1/2 Capture Volume", 0x51, 0x52, 0x03, 0, 102,
+ 90, topping_tlv_lb, topping_m62_lb_gain, .modes = TOPPING_PRO },
+ { "Loopback 3/4 Capture Volume", 0x53, 0x54, 0x03, 0, 102,
+ 90, topping_tlv_lb, topping_m62_lb_gain, .modes = TOPPING_PRO },
+ { "Loopback 5/6 Capture Volume", 0x55, 0x56, 0x03, 0, 102,
+ 90, topping_tlv_lb, topping_m62_lb_gain, .modes = TOPPING_PRO },
+ { "Loopback 7/8 Capture Volume", 0x57, 0x58, 0x03, 0, 102,
+ 90, topping_tlv_lb, topping_m62_lb_gain, .modes = TOPPING_PRO },
+ /*
+ * In Mobile Mode two of those pairs carry the vendor application's
+ * Playback 1/2 knob, the level of the host's playback in the
+ * headphones, which carry it with every input while the recording
+ * does not, and its Recording fader, the level of the recording.
+ * Both are whole decibels, -90 to +12 and -90 to 0, where Pro Audio
+ * Mode takes Q25 on the same targets. Neither is reported, so the
+ * first bind writes 0 dB: the knob then passes the playback's level
+ * on unchanged, as Pro Audio Mode's outputs do with the mixer out of
+ * the path, and the fader is at its top.
+ */
+ { "PCM Playback Volume", 0x53, 0x54, 0x03, -90, 12,
+ 0, topping_tlv_lb, .modes = TOPPING_MOBILE },
+ { "Recording Capture Volume", 0x51, 0x52, 0x03, -90, 0,
+ 0, topping_tlv_lb, .modes = TOPPING_MOBILE },
+};
+
+#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.
+ */
+static const char * const topping_m62_sources[] = {
+ "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_m62_source_value[] = {
+ 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+};
+
+static_assert(ARRAY_SIZE(topping_m62_sources) ==
+ ARRAY_SIZE(topping_m62_source_value));
+
+/* items, not card values */
+#define TOPPING_M62_BT 7
+#define TOPPING_M62_OTG_IN 8
+#define TOPPING_M62_PLAYBACK_1_2 9
+#define TOPPING_M62_PLAYBACK_3_4 10
+
+struct topping_enum_desc {
+ const char *name;
+ u8 target;
+ u8 prop;
+ unsigned int first; /* the item written at the first bind */
+ u8 modes; /* the modes that have it, or 0 for all */
+};
+
+/*
+ * A selector answers on ONE target of a pair, unlike a volume or a
+ * mute, which must be written to both: the second target of an
+ * output's pair, the first of a loopback return's. It is never
+ * reported, so it is always written at the first bind. The outputs
+ * start on the host's main stream, the loopback returns where the
+ * vendor application puts them.
+ *
+ * The names end in "Source", not "Route": alsa-lib's simple mixer joins
+ * a "... Playback Route" or "... Capture Route" enumeration to the
+ * volume of the same name and then reads and writes only the
+ * enumeration, which would take the volume away from every mixer
+ * built on it.
+ */
+static const struct topping_enum_desc topping_m62_enums[] = {
+ { "Headphone Playback Source", 0x64, 0x02,
+ TOPPING_M62_PLAYBACK_1_2, TOPPING_PRO },
+ { "OTG Playback Source", 0x62, 0x02,
+ TOPPING_M62_PLAYBACK_1_2, TOPPING_PRO },
+ { "Loopback 1/2 Capture Source", 0x51, 0x02,
+ TOPPING_M62_PLAYBACK_1_2, TOPPING_PRO },
+ { "Loopback 3/4 Capture Source", 0x53, 0x02,
+ TOPPING_M62_PLAYBACK_3_4, TOPPING_PRO },
+ { "Loopback 5/6 Capture Source", 0x55, 0x02,
+ TOPPING_M62_BT, TOPPING_PRO },
+ { "Loopback 7/8 Capture Source", 0x57, 0x02,
+ TOPPING_M62_OTG_IN, TOPPING_PRO },
+};
+
+#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
+#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+
+/*
+ * One per card, and as long-lived as the card's controls: each control
+ * holds a reference, and so does the mixer until it disconnects. The
+ * controls are freed with the card, which can be well after the
+ * disconnect, and nothing here may go before them.
+ */
+struct topping_mixer {
+ struct kref ref;
+ struct device *dev; /* the audio control interface */
+ struct snd_card *card;
+ struct usb_mixer_interface *mixer;
+
+ u8 mode; /* TOPPING_MOBILE or TOPPING_PRO */
+ struct topping_component comp;
+ struct work_struct resume_work;
+
+ /*
+ * One writer at a time, end to end, and @bound with it: a write
+ * reaches the card through comp.ops only under this lock and only
+ * while @bound is set, and the HID driver clears comp.ops only
+ * after @bound has been cleared under it.
+ */
+ struct mutex lock;
+ bool bound;
+ bool created; /* the controls exist */
+
+ spinlock_t val_lock; /* the cache and kctl[] against reports */
+ int val[TOPPING_NUM_CTLS];
+ unsigned int sel[TOPPING_NUM_ENUMS];
+
+ /* the volume controls first, then the selectors */
+ struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+};
+
+static void topping_mixer_release(struct kref *ref)
+{
+ struct topping_mixer *tm = container_of(ref, struct topping_mixer,
+ ref);
+
+ mutex_destroy(&tm->lock);
+ kfree(tm);
+}
+
+/* ------------------------------------------------------------------ */
+/* writing to the card */
+/* ------------------------------------------------------------------ */
+
+static int topping_write(struct topping_mixer *tm, u8 target, u8 prop,
+ s32 value)
+{
+ lockdep_assert_held(&tm->lock);
+
+ return tm->comp.ops->write(tm->comp.dev, target, prop, value);
+}
+
+/* whether a row for @modes exists on this card */
+static bool topping_here(const struct topping_mixer *tm, u8 modes)
+{
+ return !modes || (modes & tm->mode);
+}
+
+static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
+{
+ const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ s32 wire = d->map ? d->map[value] : value;
+ int err;
+
+ err = topping_write(tm, d->target, d->prop, wire);
+ if (!err && d->target_pair) {
+ /*
+ * The device announces only one of a pair, so the other
+ * would drift away unheard.
+ */
+ err = topping_write(tm, d->target_pair, d->prop, wire);
+ }
+ return err;
+}
+
+static int topping_write_sel(struct topping_mixer *tm, int idx,
+ unsigned int item)
+{
+ const struct topping_enum_desc *d = &topping_m62_enums[idx];
+
+ return topping_write(tm, d->target, d->prop,
+ topping_m62_source_value[item]);
+}
+
+/*
+ * Brings the card into line with the cache: at the first bind, from the
+ * values the cache was born with; at a later bind, with what was set
+ * while this side was alone, which is still only in the cache; and after
+ * a resume, in case the card came up on its own defaults or was changed
+ * on its panel while the host slept.
+ *
+ * Errors are not passed on. The HID driver logs the ones that mean
+ * something, and a card that cannot be written now is written again by
+ * the next bind or resume.
+ */
+static void topping_sync(struct topping_mixer *tm)
+{
+ int i, value;
+
+ lockdep_assert_held(&tm->lock);
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ if (!topping_here(tm, topping_m62_ctls[i].modes))
+ continue;
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ value = tm->val[i];
+ topping_write_ctl(tm, i, value);
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!topping_here(tm, topping_m62_enums[i].modes))
+ continue;
+ topping_write_sel(tm, i, tm->sel[i]);
+ }
+}
+
+static void topping_resume_work(struct work_struct *work)
+{
+ struct topping_mixer *tm = container_of(work, struct topping_mixer,
+ resume_work);
+
+ guard(mutex)(&tm->lock);
+ if (tm->bound)
+ topping_sync(tm);
+}
+
+/* ------------------------------------------------------------------ */
+/* what the HID driver tells this side */
+/* ------------------------------------------------------------------ */
+
+/* -1 when this frame is not one of ours */
+static int topping_index_of(const struct topping_mixer *tm, 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 &&
+ topping_here(tm, topping_m62_ctls[i].modes))
+ return i;
+ return -1;
+}
+
+/*
+ * Every frame the card sends, meters included, in the HID driver's
+ * interrupt context -- hence the spinlock.
+ *
+ * The notify happens under the same lock that guards the cache, not
+ * after it. snd_ctl_notify() dereferences the id it is given, and the
+ * control's own private_free clears kctl[] under this lock. It is safe
+ * from here: it takes read_lock_irqsave and allocates with GFP_ATOMIC.
+ */
+static void topping_report(struct topping_component *comp, u8 target,
+ u8 prop, s32 value)
+{
+ struct topping_mixer *tm = container_of(comp, struct topping_mixer,
+ comp);
+ int idx;
+
+ idx = topping_index_of(tm, target, prop);
+ if (idx < 0)
+ return; /* a meter, or something unnamed */
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return;
+
+ guard(spinlock_irqsave)(&tm->val_lock);
+
+ if (tm->val[idx] == value)
+ return;
+ tm->val[idx] = value;
+
+ if (tm->kctl[idx])
+ snd_ctl_notify(tm->card, SNDRV_CTL_EVENT_MASK_VALUE,
+ &tm->kctl[idx]->id);
+}
+
+/* Atomic context, so the writing is left to a worker. */
+static void topping_resumed(struct topping_component *comp)
+{
+ struct topping_mixer *tm = container_of(comp, struct topping_mixer,
+ comp);
+
+ schedule_work(&tm->resume_work);
+}
+
+/*
+ * The HID driver is going and takes its operation with it. Clearing
+ * @bound under the lock waits out a write already inside it, and turns
+ * every later one into a cache update that the next bind writes out.
+ */
+static void topping_unbound(struct topping_component *comp)
+{
+ struct topping_mixer *tm = container_of(comp, struct topping_mixer,
+ comp);
+
+ guard(mutex)(&tm->lock);
+ tm->bound = false;
+}
+
+static const struct topping_audio_ops topping_mixer_ops = {
+ .report = topping_report,
+ .resumed = topping_resumed,
+ .unbound = topping_unbound,
+};
+
+/* ------------------------------------------------------------------ */
+/* 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_mixer *tm = snd_kcontrol_chip(kctl);
+
+ guard(spinlock_irqsave)(&tm->val_lock);
+ ucontrol->value.integer.value[0] = tm->val[kctl->private_value];
+ return 0;
+}
+
+static int topping_ctl_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_mixer *tm = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value;
+ int value, prev, err;
+
+ value = ucontrol->value.integer.value[0];
+ if (value < topping_m62_ctls[idx].min ||
+ value > topping_m62_ctls[idx].max)
+ return -EINVAL;
+
+ /*
+ * Held from the comparison to the end of the write, so that two
+ * writers cannot reach the device in one order and the cache in
+ * the other. The wake inside the write is safe under it: the
+ * resume it may run on this thread only schedules work.
+ */
+ guard(mutex)(&tm->lock);
+
+ /*
+ * The cache takes the new value BEFORE the write, not after. A
+ * hand on the front panel during the write produces a report the
+ * cache stores; updating afterwards would throw that away and
+ * leave 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->val_lock) {
+ if (tm->val[idx] == value)
+ return 0;
+ prev = tm->val[idx];
+ tm->val[idx] = value;
+ }
+
+ /* kept, and written when the HID driver binds again */
+ if (!tm->bound)
+ return 1;
+
+ err = topping_write_ctl(tm, idx, value);
+ if (err < 0) {
+ /* put back what was there, unless the device has spoken */
+ scoped_guard(spinlock_irqsave, &tm->val_lock) {
+ if (tm->val[idx] == value)
+ tm->val[idx] = 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,
+};
+
+/* ------------------------------------------------------------------ */
+/* 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_m62_sources),
+ topping_m62_sources);
+}
+
+static int topping_sel_get(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_mixer *tm = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value - TOPPING_NUM_CTLS;
+
+ guard(spinlock_irqsave)(&tm->val_lock);
+ ucontrol->value.enumerated.item[0] = tm->sel[idx];
+ return 0;
+}
+
+static int topping_sel_put(struct snd_kcontrol *kctl,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct topping_mixer *tm = snd_kcontrol_chip(kctl);
+ int idx = kctl->private_value - TOPPING_NUM_CTLS;
+ unsigned int item, prev;
+ int err;
+
+ item = ucontrol->value.enumerated.item[0];
+ if (item >= ARRAY_SIZE(topping_m62_sources))
+ return -EINVAL;
+
+ guard(mutex)(&tm->lock);
+
+ /*
+ * No report ever moves a selector, so the cache can be compared
+ * and changed outside the spinlock; the spinlock is still taken
+ * for the reader's sake.
+ */
+ prev = tm->sel[idx];
+ if (prev == item)
+ return 0;
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ tm->sel[idx] = item;
+
+ if (!tm->bound)
+ return 1;
+
+ err = topping_write_sel(tm, idx, item);
+ if (err < 0) {
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ tm->sel[idx] = prev;
+ return err;
+ }
+
+ 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,
+};
+
+/* ------------------------------------------------------------------ */
+/* creating the controls */
+/* ------------------------------------------------------------------ */
+
+/* @kctl->private_value is the slot: volumes first, then selectors */
+static void topping_kctl_free(struct snd_kcontrol *kctl)
+{
+ struct topping_mixer *tm = snd_kcontrol_chip(kctl);
+
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ tm->kctl[kctl->private_value] = NULL;
+
+ kref_put(&tm->ref, topping_mixer_release);
+}
+
+static int topping_add_kctl(struct topping_mixer *tm,
+ const struct snd_kcontrol_new *tmpl,
+ const char *name, int slot,
+ const unsigned int *tlv)
+{
+ struct snd_kcontrol *k;
+ int err;
+
+ k = snd_ctl_new1(tmpl, tm);
+ if (!k)
+ return -ENOMEM;
+
+ k->private_value = slot;
+ k->tlv.p = tlv;
+ strscpy(k->id.name, name, sizeof(k->id.name));
+ kref_get(&tm->ref);
+ k->private_free = topping_kctl_free;
+
+ err = snd_ctl_add(tm->card, k);
+ if (err < 0)
+ return err; /* freed, and the reference with it */
+
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ tm->kctl[slot] = k;
+ return 0;
+}
+
+static int topping_create_kctls(struct topping_mixer *tm)
+{
+ int i, err;
+
+ for (i = 0; i < TOPPING_NUM_CTLS; i++) {
+ if (!topping_here(tm, topping_m62_ctls[i].modes))
+ continue;
+ err = topping_add_kctl(tm, &topping_ctl,
+ topping_m62_ctls[i].name, i,
+ topping_m62_ctls[i].tlv);
+ if (err < 0)
+ goto err_remove;
+ }
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
+ if (!topping_here(tm, topping_m62_enums[i].modes))
+ continue;
+ err = topping_add_kctl(tm, &topping_sel,
+ topping_m62_enums[i].name,
+ TOPPING_NUM_CTLS + i, NULL);
+ if (err < 0)
+ goto err_remove;
+ }
+ return 0;
+
+err_remove:
+ for (i = 0; i < TOPPING_NUM_KCTLS; i++)
+ if (tm->kctl[i])
+ snd_ctl_remove(tm->card, tm->kctl[i]);
+ return err;
+}
+
+/* ------------------------------------------------------------------ */
+/* component master */
+/* ------------------------------------------------------------------ */
+
+/*
+ * Where the callbacks find this card's context. 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_res {
+ struct topping_mixer *tm;
+};
+
+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_mixer *topping_get_master(struct device *dev)
+{
+ struct topping_master_res *res;
+
+ res = devres_find(dev, topping_master_release, NULL, NULL);
+ return res ? res->tm : 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_mixer *tm = topping_get_master(dev);
+ bool first;
+ int err;
+
+ /*
+ * 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;
+
+ err = component_bind_all(dev, &tm->comp);
+ if (err)
+ return err;
+
+ /*
+ * Created with our lock not held: snd_ctl_add() takes
+ * controls_rwsem for writing, and a put holding it for reading
+ * can be waiting for our lock.
+ */
+ first = !tm->created;
+ if (first) {
+ err = topping_create_kctls(tm);
+ if (err < 0) {
+ component_unbind_all(dev, NULL);
+ return err;
+ }
+ tm->created = true;
+ }
+
+ guard(mutex)(&tm->lock);
+ tm->bound = true;
+ topping_sync(tm);
+ return 0;
+}
+
+/*
+ * 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.
+ *
+ * Nor is anything needed from it: the HID driver kept the structure it
+ * was handed at bind, and tells this side through it that it is going.
+ */
+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 this context.
+ *
+ * The mixer's reference is dropped last. The controls hold theirs
+ * until the card frees them.
+ */
+static void topping_master_teardown(void *data)
+{
+ struct topping_mixer *tm = data;
+
+ component_master_del(tm->dev, &topping_master_ops);
+ cancel_work_sync(&tm->resume_work);
+
+ if (tm->mixer) {
+ tm->mixer->private_data = NULL;
+ tm->mixer->private_free = NULL;
+ }
+
+ kref_put(&tm->ref, topping_mixer_release);
+}
+
+static void topping_private_free(struct usb_mixer_interface *mixer)
+{
+ struct topping_mixer *tm = mixer->private_data;
+ struct device *dev;
+
+ 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 and may drop the last
+ * reference, so the device is taken first.
+ */
+ dev = tm->dev;
+ devm_release_action(dev, topping_master_teardown, tm);
+ devres_destroy(dev, topping_master_release, NULL, NULL);
+}
+
+static struct topping_mixer *topping_mixer_new(struct usb_mixer_interface *mixer,
+ struct device *dev, u8 mode)
+{
+ struct topping_mixer *tm;
+ int i;
+
+ tm = kzalloc_obj(*tm);
+ if (!tm)
+ return NULL;
+
+ kref_init(&tm->ref); /* the mixer's */
+ tm->mode = mode;
+ tm->dev = dev;
+ tm->card = mixer->chip->card;
+ tm->mixer = mixer;
+ tm->comp.audio_ops = &topping_mixer_ops;
+ INIT_WORK(&tm->resume_work, topping_resume_work);
+ mutex_init(&tm->lock);
+ spin_lock_init(&tm->val_lock);
+
+ /* what the cache is born with is what the first bind writes */
+ for (i = 0; i < TOPPING_NUM_CTLS; i++)
+ tm->val[i] = topping_m62_ctls[i].first;
+ for (i = 0; i < TOPPING_NUM_ENUMS; i++)
+ tm->sel[i] = topping_m62_enums[i].first;
+
+ return tm;
+}
+
+int snd_topping_init(struct usb_mixer_interface *mixer)
+{
+ struct snd_usb_audio *chip = mixer->chip;
+ struct component_match *match = NULL;
+ struct topping_master_res *res;
+ struct usb_interface *intf;
+ struct topping_mixer *tm;
+ struct device *dev;
+ u8 mode = TOPPING_PRO;
+ int err;
+
+ if (le16_to_cpu(chip->dev->descriptor.bcdDevice) >> 8 == 0x01)
+ mode = TOPPING_MOBILE;
+
+ /*
+ * 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 = topping_mixer_new(mixer, dev, mode);
+ if (!tm)
+ return -ENOMEM;
+
+ res = devres_alloc(topping_master_release, sizeof(*res), GFP_KERNEL);
+ if (!res) {
+ kref_put(&tm->ref, topping_mixer_release);
+ return -ENOMEM;
+ }
+ res->tm = tm;
+ devres_add(dev, res);
+
+ 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 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);
+ cancel_work_sync(&tm->resume_work);
+ kref_put(&tm->ref, topping_mixer_release);
+ 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 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 */
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v9 3/8] HID: topping: add the E2x2 OTG
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 1/8] HID: topping: transport for the M62's vendor controls Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 2/8] ALSA: usb-audio: add the Topping " Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 4/8] ALSA: usb-audio: add the Topping E2x2 OTG's vendor controls Mikhail Gavrilov
` (4 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
The Topping E2x2 OTG (152a:8756) puts its digital input gains, its
faders, its source selectors and its output switches behind the same
vendor protocol as the M62, in the same frames and on the same
interface, so the same transport serves it.
One thing differs. The E2x2 OTG reports without being subscribed, and
its own vendor application never sends it 0x11/0x24 or 0x11/0x26. The
subscription, the announce request and the keepalive are now reserved
for a card marked as wanting them, which is the M62.
The device table now says, for each card, which interface carries the
protocol as well as whether the card wants the subscription. It is
interface 4 on both; a card that has it elsewhere is one more entry
rather than a change to the probe.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 13 ++++----
drivers/hid/hid-ids.h | 1 +
drivers/hid/hid-quirks.c | 1 +
drivers/hid/hid-topping.c | 63 +++++++++++++++++++++++++--------------
4 files changed, 50 insertions(+), 28 deletions(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index fecbbaa..9367a71 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1285,16 +1285,17 @@ config HID_TIVO
Say Y if you have a TiVo Slide Bluetooth remote control.
config HID_TOPPING
- tristate "Topping M62 vendor controls"
+ tristate "Topping M62 and E2x2 OTG vendor controls"
depends on USB_HID
depends on SND_USB_AUDIO
select CRC16
help
- The vendor protocol behind 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 the card's HID interface, so a
- driver is needed for them to work at all.
+ The vendor protocol behind the input gains, output volumes and
+ source selectors of the Topping M62 and E2x2 OTG USB audio
+ interfaces, and the output switches of the E2x2 OTG. These are
+ not described by the USB Audio Class and are reached over the
+ card's HID interface, so a driver is needed for them to work
+ at all.
The controls themselves belong to snd-usb-audio, on the sound
card it makes for the same device; this driver carries them
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 1af487c..cc114e4 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1478,6 +1478,7 @@
#define USB_DEVICE_ID_TIVO_SLIDE_PRO 0x1203
#define USB_VENDOR_ID_TOPPING 0x152a
+#define USB_DEVICE_ID_TOPPING_E2X2_OTG 0x8756
#define USB_DEVICE_ID_TOPPING_M62 0x875c
#define USB_VENDOR_ID_TOPRE 0x0853
diff --git a/drivers/hid/hid-quirks.c b/drivers/hid/hid-quirks.c
index bca22bb..5872952 100644
--- a/drivers/hid/hid-quirks.c
+++ b/drivers/hid/hid-quirks.c
@@ -791,6 +791,7 @@ static const struct hid_device_id hid_have_special_driver[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TIVO, USB_DEVICE_ID_TIVO_SLIDE_PRO) },
#endif
#if IS_ENABLED(CONFIG_HID_TOPPING)
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_E2X2_OTG) },
{ HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
#endif
#if IS_ENABLED(CONFIG_HID_TOPSEED)
diff --git a/drivers/hid/hid-topping.c b/drivers/hid/hid-topping.c
index a8854b2..7b46c4c 100644
--- a/drivers/hid/hid-topping.c
+++ b/drivers/hid/hid-topping.c
@@ -6,7 +6,10 @@
*
* The M62 (152a:875c) puts its analogue input gains, its output volumes
* and its output source selectors behind a vendor protocol on a
- * HID-class interface, and exposes none of them through UAC.
+ * HID-class interface, and exposes none of them through UAC. The E2x2
+ * OTG (152a:8756) does the same with its digital input gains, its
+ * faders, its source selectors and its output switches, on the same
+ * interface and in the same frames.
*
* The protocol was read off the vendor application's traffic. Frames
* are fifteen bytes:
@@ -22,10 +25,12 @@
* 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 what it reports at all.
+ * The M62 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 what it reports at all. The E2x2 OTG
+ * reports without being asked, and its own vendor application never
+ * sends it either of the two, so neither does this driver.
*
* The control pipe is not an option: GET_REPORT and SET_REPORT both
* stall with EPIPE for every report type, so the interrupt endpoints
@@ -37,12 +42,13 @@
* THE CONTROLS ARE NOT THIS DRIVER'S. They live in snd-usb-audio, on
* the card that plays the audio, and outlive any binding of this
* driver. This driver is the transport: it registers a component, the
- * M62 mixer quirk is the master, and at bind this driver fills in the
+ * card's mixer quirk is the master, and at bind this driver fills in the
* struct topping_component it is handed with the one operation the
* controls need -- send a frame -- and from then on passes on every
* frame the card reports.
*/
+#include <linux/bits.h>
#include <linux/cleanup.h>
#include <linux/component.h>
#include <linux/crc16.h>
@@ -63,11 +69,14 @@
#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.
+ * hid_device_id.driver_data: in the low byte, the interface that carries
+ * the control protocol and the only one this driver takes, and above it
+ * what the card wants. Both cards present two non-audio interfaces:
+ * interface 3 is Application Specific / DFU and is never touched here,
+ * and interface 4 carries the protocol.
*/
-#define M62_VENDOR_IFNUM 4
+#define TOPPING_IFNUM GENMASK(7, 0)
+#define TOPPING_SUBSCRIBE BIT(8) /* wants 0x11/0x24 and its renewal */
/* device-scope properties */
#define TOPPING_TT_DEVICE 0x11
@@ -95,6 +104,7 @@ struct topping_hid {
struct topping_component *comp;
spinlock_t lock; /* guards comp */
+ bool subscribe; /* TOPPING_SUBSCRIBE */
struct delayed_work keepalive;
};
@@ -282,10 +292,12 @@ static int topping_bind(struct device *dev, struct device *master,
* connected input about 5 s later -- so nothing here waits for
* them: each value arrives through .raw_event() on its own.
*/
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
- schedule_delayed_work(&th->keepalive,
- msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ if (th->subscribe) {
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ schedule_delayed_work(&th->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ }
return 0;
}
@@ -352,8 +364,9 @@ static int topping_probe(struct hid_device *hdev,
th->hdev = hdev;
th->intf = to_usb_interface(hdev->dev.parent);
if (th->intf->cur_altsetting->desc.bInterfaceNumber !=
- M62_VENDOR_IFNUM)
+ (id->driver_data & TOPPING_IFNUM))
return -ENODEV;
+ th->subscribe = id->driver_data & TOPPING_SUBSCRIBE;
spin_lock_init(&th->lock);
INIT_DELAYED_WORK(&th->keepalive, topping_keepalive);
@@ -451,10 +464,12 @@ static int topping_resume(struct hid_device *hdev)
* state refreshes what may have gone stale while the panel was
* reachable and this driver was not.
*/
- noio = memalloc_noio_save();
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
- memalloc_noio_restore(noio);
+ if (th->subscribe) {
+ noio = memalloc_noio_save();
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ memalloc_noio_restore(noio);
+ }
/*
* Tested again under the lock topping_unbind() takes to clear it,
@@ -467,8 +482,9 @@ static int topping_resume(struct hid_device *hdev)
scoped_guard(spinlock_irqsave, &th->lock) {
if (th->comp) {
th->comp->audio_ops->resumed(th->comp);
- schedule_delayed_work(&th->keepalive,
- msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
+ if (th->subscribe)
+ schedule_delayed_work(&th->keepalive,
+ msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
}
}
@@ -476,7 +492,10 @@ static int topping_resume(struct hid_device *hdev)
}
static const struct hid_device_id topping_devices[] = {
- { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_E2X2_OTG),
+ .driver_data = 4 },
+ { HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62),
+ .driver_data = 4 | TOPPING_SUBSCRIBE },
{ }
};
MODULE_DEVICE_TABLE(hid, topping_devices);
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v9 4/8] ALSA: usb-audio: add the Topping E2x2 OTG's vendor controls
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
` (2 preceding siblings ...)
2026-10-18 12:48 ` [PATCH v9 3/8] HID: topping: add the E2x2 OTG Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 5/8] HID: topping: report the M62's battery level Mikhail Gavrilov
` (3 subsequent siblings)
7 siblings, 0 replies; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
The E2x2 OTG keeps its input gains, its faders, its output and loopback
source selectors and its output switches behind the vendor protocol
hid-topping speaks, and reports none of them back. A Linux host cannot
set them at all, and inherits whatever the last host left in the card,
which saves its own state across power cycles: with the three output
switches left off by the vendor application, the card plays nothing
and nothing on Linux can turn it back on.
The control tables become per model, chosen by USB ID. A model is all
this file knows of a card: its rows, the properties written at every
bind without a control, and whether bcdDevice tells its mode, as the
M62's does. The per-card state is sized from the model, so a card with
more controls is only a longer table. A row may now be an on/off
switch, and a selector names its own list of sources. The E2x2 OTG
takes its gains and faders as Q25, through a table of what each step
writes, as the M62's loopback gains do.
Every control is written at the first bind, as for the M62: the gains at
+0 dB, the faders at 0 dB, the outputs on Playback 1/2, each loopback on
Playback 3/4, 5/6 and 7/8, the output switches on and the headphone
amplifier's extra gain off. Four properties of unknown meaning get the
values the vendor application always writes.
48V, INST and the monitor mix knob are left to the front panel. MON
is not, although it has buttons there and the card reports them:
after every power-on the card mixes as though monitoring were on, so
the monitor mix knob sits in the playback path -- at its leftmost
position nothing is heard -- until MON on both inputs has been
switched off. The vendor application writes both at every connect,
and so does this driver at every bind, starting from off. A MON off
written to IN 2 while IN 1 monitors also silences the monitor mix
until a MON on is written, so after either MON is written the other
is written again if it is on.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_quirks.c | 1 +
sound/usb/mixer_topping.c | 407 +++++++++++++++++++++++++++++++-------
2 files changed, 332 insertions(+), 76 deletions(-)
diff --git a/sound/usb/mixer_quirks.c b/sound/usb/mixer_quirks.c
index 78d1966..fe68975 100644
--- a/sound/usb/mixer_quirks.c
+++ b/sound/usb/mixer_quirks.c
@@ -4545,6 +4545,7 @@ int snd_usb_mixer_apply_create_quirk(struct usb_mixer_interface *mixer)
err = snd_evo_controls_create(mixer);
break;
+ case USB_ID(0x152a, 0x8756): /* Topping E2x2 OTG */
case USB_ID(0x152a, 0x875c): /* Topping M62 */
err = snd_topping_init(mixer);
break;
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index cd848a3..725db57 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -1,16 +1,17 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
- * Topping M62 -- the card's vendor controls.
+ * Topping M62 and E2x2 OTG -- the cards' vendor controls.
*
* The M62's analogue input gains, output volumes, source selectors and
- * loopback gains are not described by the USB Audio Class. They are
- * reached over a vendor protocol on the card's HID interface, which
- * hid-topping speaks. This file owns the controls; that driver is
- * the transport. The two meet through the component framework, with
- * this side as the master, in the same shape as HD-audio and the
- * graphics drivers in sound/hda/core/component.c: the master owns a
- * struct topping_component, and the HID driver fills in its one
- * operation when it binds and clears it when it unbinds.
+ * loopback gains, and the E2x2 OTG's digital input gains, faders,
+ * source selectors and output switches, are not described by the USB
+ * Audio Class. They are reached over a vendor protocol on the card's
+ * HID interface, which hid-topping speaks. This file owns the
+ * controls; that driver is the transport. The two meet through the
+ * component framework, with this side as the master, in the same shape
+ * as HD-audio and the graphics drivers in sound/hda/core/component.c:
+ * the master owns a struct topping_component, and the HID driver fills
+ * in its one operation when it binds and clears it when it unbinds.
*
* THE CONTROLS LIVE AS LONG AS THE CARD. They are created at the first
* bind of the HID driver and not removed when it unbinds: a value
@@ -107,8 +108,10 @@ struct topping_ctl_desc {
u8 prop;
int min, max;
int first; /* written at the first bind */
- const unsigned int *tlv;
+ const unsigned int *tlv; /* NULL for a switch */
const s32 *map; /* what each step writes, or NULL: the step */
+ bool sw; /* an on/off switch, not a level */
+ u8 resend; /* written again after this one if on, or 0 */
u8 modes; /* the modes that have it, or 0 for all */
};
@@ -201,8 +204,6 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
0, topping_tlv_lb, .modes = TOPPING_MOBILE },
};
-#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,
@@ -228,11 +229,24 @@ static_assert(ARRAY_SIZE(topping_m62_sources) ==
#define TOPPING_M62_PLAYBACK_1_2 9
#define TOPPING_M62_PLAYBACK_3_4 10
+/* the items of a selector, and what the card wants for each */
+struct topping_sources {
+ const char * const *items;
+ const u8 *values;
+ unsigned int num;
+};
+
+static const struct topping_sources topping_m62_src = {
+ topping_m62_sources, topping_m62_source_value,
+ ARRAY_SIZE(topping_m62_sources),
+};
+
struct topping_enum_desc {
const char *name;
u8 target;
u8 prop;
unsigned int first; /* the item written at the first bind */
+ const struct topping_sources *src;
u8 modes; /* the modes that have it, or 0 for all */
};
@@ -252,21 +266,210 @@ struct topping_enum_desc {
*/
static const struct topping_enum_desc topping_m62_enums[] = {
{ "Headphone Playback Source", 0x64, 0x02,
- TOPPING_M62_PLAYBACK_1_2, TOPPING_PRO },
+ TOPPING_M62_PLAYBACK_1_2, &topping_m62_src, TOPPING_PRO },
{ "OTG Playback Source", 0x62, 0x02,
- TOPPING_M62_PLAYBACK_1_2, TOPPING_PRO },
+ TOPPING_M62_PLAYBACK_1_2, &topping_m62_src, TOPPING_PRO },
{ "Loopback 1/2 Capture Source", 0x51, 0x02,
- TOPPING_M62_PLAYBACK_1_2, TOPPING_PRO },
+ TOPPING_M62_PLAYBACK_1_2, &topping_m62_src, TOPPING_PRO },
{ "Loopback 3/4 Capture Source", 0x53, 0x02,
- TOPPING_M62_PLAYBACK_3_4, TOPPING_PRO },
+ TOPPING_M62_PLAYBACK_3_4, &topping_m62_src, TOPPING_PRO },
{ "Loopback 5/6 Capture Source", 0x55, 0x02,
- TOPPING_M62_BT, TOPPING_PRO },
+ TOPPING_M62_BT, &topping_m62_src, TOPPING_PRO },
{ "Loopback 7/8 Capture Source", 0x57, 0x02,
- TOPPING_M62_OTG_IN, TOPPING_PRO },
+ TOPPING_M62_OTG_IN, &topping_m62_src, TOPPING_PRO },
+};
+
+/*
+ * THE E2X2 OTG REPORTS LITTLE OF WHAT IS SHOWN HERE. Its gains, faders,
+ * selectors and output switches are only ever written, by its vendor
+ * application as by this driver, so every one of them is written at
+ * the first bind and the cache is the truth from then on. None of them
+ * has a control on the front panel, so another host can leave any of
+ * them anywhere -- every output switched off, say -- and this side has
+ * to be able to put it back. What the panel owns is left alone: the
+ * 48V and INST buttons, and the monitor mix knob, which the card
+ * reports and does not take from the host anyway. MON is the
+ * exception, for the reason given at its rows.
+ *
+ * The card keeps its own state, saving a change about five seconds
+ * after it is made, so nothing is written that does not have to be: a
+ * put that leaves the cache as it was writes nothing, and only a bind
+ * or a resume writes everything.
+ *
+ * Input gain is digital, +0 to +20 dB in whole decibels, and these are
+ * the values the vendor application writes for each step. The Mobile
+ * input's gain is the one on its left channel, 0x22; it moves both.
+ */
+static const s32 topping_e2x2_gain[] = {
+ 33554432, 37648680, 42242512, 47396908, 53180188, 59669144,
+ 66949880, 75119016, 84284904, 94569240, 106108440, 119055624,
+ 133582616, 149882144, 168170512, 188690432, 211714144, 237547200,
+ 266532320, 299054176, 335544320,
+};
+
+static_assert(ARRAY_SIZE(topping_e2x2_gain) == 21);
+
+/* A fader in whole decibels: step 0 is off, step n is n - 81 dB. */
+static const s32 topping_e2x2_fader[] = {
+ 0, 3355, 3765, 4224, 4740, 5318,
+ 5967, 6695, 7512, 8428, 9457, 10611,
+ 11906, 13358, 14988, 16817, 18869, 21171,
+ 23755, 26653, 29905, 33554, 37649, 42243,
+ 47397, 53180, 59669, 66950, 75119, 84285,
+ 94569, 106108, 119056, 133583, 149882, 168171,
+ 188690, 211714, 237547, 266532, 299054, 335544,
+ 376487, 422425, 473969, 531802, 596692, 669499,
+ 751190, 842849, 945692, 1061084, 1190556, 1335826,
+ 1498821, 1681705, 1886904, 2117142, 2375472, 2665323,
+ 2990542, 3355443, 3764869, 4224253, 4739689, 5318019,
+ 5966916, 6694989, 7511902, 8428492, 9456924, 10610843,
+ 11905562, 13358260, 14988214, 16817053, 18869044, 21171415,
+ 23754719, 26653233, 29905419, 33554432,
+};
+
+static_assert(ARRAY_SIZE(topping_e2x2_fader) == 82);
+
+static const DECLARE_TLV_DB_SCALE(topping_tlv_fader, -8100, 100, 1);
+
+static const struct topping_ctl_desc topping_e2x2_ctls[] = {
+ { "Mic1 Digital Capture Volume", 0x21, 0, 0x05, 0, 20,
+ 0, topping_tlv_gain, topping_e2x2_gain },
+ { "Mic2 Digital Capture Volume", 0x23, 0, 0x05, 0, 20,
+ 0, topping_tlv_gain, topping_e2x2_gain },
+ { "OTG Capture Volume", 0x22, 0, 0x05, 0, 20,
+ 0, topping_tlv_gain, topping_e2x2_gain },
+ /*
+ * Output 1+2's fader as "Master": the sound server's stock
+ * profile takes a control by that name for the volume of the
+ * output, so the desktop's own slider moves this fader, and there
+ * is no second level left somewhere out of sight.
+ */
+ { "Master Playback Volume", 0x31, 0x32, 0x03, 0, 81,
+ 81, topping_tlv_fader, topping_e2x2_fader },
+ { "OTG Playback Volume", 0x33, 0x34, 0x03, 0, 81,
+ 81, topping_tlv_fader, topping_e2x2_fader },
+ { "SPDIF Playback Volume", 0x5a, 0x5b, 0x03, 0, 81,
+ 81, topping_tlv_fader, topping_e2x2_fader },
+ { "Loopback 1+2 Capture Volume", 0x51, 0x52, 0x03, 0, 81,
+ 81, topping_tlv_fader, topping_e2x2_fader },
+ { "Loopback 3+4 Capture Volume", 0x53, 0x54, 0x03, 0, 81,
+ 81, topping_tlv_fader, topping_e2x2_fader },
+ { "Loopback 5+6 Capture Volume", 0x55, 0x56, 0x03, 0, 81,
+ 81, topping_tlv_fader, topping_e2x2_fader },
+ /*
+ * Not "Headphone Playback Switch": the sound server's stock
+ * profile would take a control by that name for the mute of the
+ * whole output, and muting would then silence the headphones
+ * alone while TRS and AUX play on.
+ */
+ { "Output 1+2 Headphone Playback Switch", 0x37, 0, 0x01, 0, 1, 1,
+ .sw = true },
+ { "Output 1+2 TRS Playback Switch", 0x37, 0, 0x03, 0, 1, 1,
+ .sw = true },
+ { "Output 1+2 AUX Playback Switch", 0x37, 0, 0x05, 0, 1, 1,
+ .sw = true },
+ { "Output 1+2 Headphone Gain Playback Switch", 0x35, 0, 0x02, 0, 1, 0,
+ .sw = true },
+ /*
+ * MON has buttons on the front panel, and the card reports them,
+ * but it is written here all the same. After every power-on the
+ * card mixes as though monitoring were on, whatever the buttons
+ * show: the monitor mix knob stays in the playback path, passing
+ * all of it at its rightmost position and none at its leftmost,
+ * until MON on IN 1 and on IN 2 have both been switched off, from
+ * the panel or by a write. The vendor application writes both at
+ * every connect, and this driver does the same at every bind.
+ *
+ * Written from the host, MON off on IN 2 also silences the monitor
+ * mix while IN 1 still monitors, until the knob is turned or a MON
+ * on is written. So after either is written, the other is written
+ * again if it is on: a MON on is the cure, and a second MON off
+ * would be the very write that does the harm.
+ */
+ { "Mic1 Monitor Playback Switch", 0x21, 0, 0x01, 0, 1, 0,
+ .sw = true, .resend = 0x23 },
+ { "Mic2 Monitor Playback Switch", 0x23, 0, 0x01, 0, 1, 0,
+ .sw = true, .resend = 0x21 },
+};
+
+/* The vendor application's numbering, in the order of its menu. */
+static const char * const topping_e2x2_sources[] = {
+ "Mix A", "Mix B", "Mix C", "Mix D", "IN 1", "IN 2", "IN 1+2",
+ "Mobile IN", "Playback 1/2", "Playback 3/4", "Playback 5/6",
+ "Playback 7/8",
+};
+
+static const u8 topping_e2x2_source_value[] = {
+ 11, 12, 13, 14, 1, 3, 5, 2, 7, 8, 9, 10,
+};
+
+static_assert(ARRAY_SIZE(topping_e2x2_sources) ==
+ ARRAY_SIZE(topping_e2x2_source_value));
+
+static const struct topping_sources topping_e2x2_src = {
+ topping_e2x2_sources, topping_e2x2_source_value,
+ ARRAY_SIZE(topping_e2x2_sources),
+};
+
+/*
+ * The outputs listen to the host's main stream, and each loopback to
+ * one of the others, as the vendor application sets them: nothing is
+ * recorded from a loopback until someone chooses what.
+ */
+static const struct topping_enum_desc topping_e2x2_enums[] = {
+ { "Output 1+2 Playback Source", 0x35, 0x01, 8, &topping_e2x2_src },
+ { "OTG Playback Source", 0x36, 0x01, 8, &topping_e2x2_src },
+ { "SPDIF Playback Source", 0x5c, 0x01, 8, &topping_e2x2_src },
+ { "Loopback 1+2 Capture Source", 0x57, 0x01, 9, &topping_e2x2_src },
+ { "Loopback 3+4 Capture Source", 0x58, 0x01, 10, &topping_e2x2_src },
+ { "Loopback 5+6 Capture Source", 0x59, 0x01, 11, &topping_e2x2_src },
};
-#define TOPPING_NUM_ENUMS ARRAY_SIZE(topping_m62_enums)
-#define TOPPING_NUM_KCTLS (TOPPING_NUM_CTLS + TOPPING_NUM_ENUMS)
+/* written at every bind and after every resume, with no control */
+struct topping_fixed {
+ u8 target;
+ u8 prop;
+ s32 value;
+};
+
+/*
+ * What the vendor application writes, always the same, to properties
+ * whose meaning is not known; written here so that the card is in the
+ * state that application leaves it in whatever another host did.
+ */
+static const struct topping_fixed topping_e2x2_fixed[] = {
+ { 0x36, 0x02, 0 },
+ { 0x37, 0x02, 1 },
+ { 0x37, 0x04, 1 },
+ { 0x37, 0x06, 1 },
+};
+
+struct topping_model {
+ const struct topping_ctl_desc *ctls;
+ unsigned int num_ctls;
+ const struct topping_enum_desc *enums;
+ unsigned int num_enums;
+ const struct topping_fixed *fixed;
+ unsigned int num_fixed;
+ bool bcd_mode; /* tells its mode in bcdDevice, as the M62 does */
+};
+
+static const struct topping_model topping_m62 = {
+ .ctls = topping_m62_ctls,
+ .num_ctls = ARRAY_SIZE(topping_m62_ctls),
+ .enums = topping_m62_enums,
+ .num_enums = ARRAY_SIZE(topping_m62_enums),
+ .bcd_mode = true,
+};
+
+static const struct topping_model topping_e2x2_otg = {
+ .ctls = topping_e2x2_ctls,
+ .num_ctls = ARRAY_SIZE(topping_e2x2_ctls),
+ .enums = topping_e2x2_enums,
+ .num_enums = ARRAY_SIZE(topping_e2x2_enums),
+ .fixed = topping_e2x2_fixed,
+ .num_fixed = ARRAY_SIZE(topping_e2x2_fixed),
+};
/*
* One per card, and as long-lived as the card's controls: each control
@@ -280,6 +483,7 @@ struct topping_mixer {
struct snd_card *card;
struct usb_mixer_interface *mixer;
+ const struct topping_model *model;
u8 mode; /* TOPPING_MOBILE or TOPPING_PRO */
struct topping_component comp;
struct work_struct resume_work;
@@ -295,11 +499,11 @@ struct topping_mixer {
bool created; /* the controls exist */
spinlock_t val_lock; /* the cache and kctl[] against reports */
- int val[TOPPING_NUM_CTLS];
- unsigned int sel[TOPPING_NUM_ENUMS];
+ int *val; /* one per row of the model's ctls */
+ unsigned int *sel; /* one per row of its enums */
- /* the volume controls first, then the selectors */
- struct snd_kcontrol *kctl[TOPPING_NUM_KCTLS];
+ /* the volume controls and switches first, then the selectors */
+ struct snd_kcontrol **kctl;
};
static void topping_mixer_release(struct kref *ref)
@@ -307,6 +511,9 @@ static void topping_mixer_release(struct kref *ref)
struct topping_mixer *tm = container_of(ref, struct topping_mixer,
ref);
+ kfree(tm->kctl);
+ kfree(tm->sel);
+ kfree(tm->val);
mutex_destroy(&tm->lock);
kfree(tm);
}
@@ -329,11 +536,25 @@ static bool topping_here(const struct topping_mixer *tm, u8 modes)
return !modes || (modes & tm->mode);
}
+/* -1 when this frame is not one of ours */
+static int topping_index_of(const struct topping_mixer *tm, u8 target,
+ u8 prop)
+{
+ const struct topping_model *m = tm->model;
+ int i;
+
+ for (i = 0; i < m->num_ctls; i++)
+ if (m->ctls[i].target == target && m->ctls[i].prop == prop &&
+ topping_here(tm, m->ctls[i].modes))
+ return i;
+ return -1;
+}
+
static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
{
- const struct topping_ctl_desc *d = &topping_m62_ctls[idx];
+ const struct topping_ctl_desc *d = &tm->model->ctls[idx];
s32 wire = d->map ? d->map[value] : value;
- int err;
+ int err, other, on;
err = topping_write(tm, d->target, d->prop, wire);
if (!err && d->target_pair) {
@@ -343,16 +564,22 @@ static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
*/
err = topping_write(tm, d->target_pair, d->prop, wire);
}
+ if (!err && d->resend) {
+ other = topping_index_of(tm, d->resend, d->prop);
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ on = tm->val[other];
+ if (on)
+ err = topping_write(tm, d->resend, d->prop, on);
+ }
return err;
}
static int topping_write_sel(struct topping_mixer *tm, int idx,
unsigned int item)
{
- const struct topping_enum_desc *d = &topping_m62_enums[idx];
+ const struct topping_enum_desc *d = &tm->model->enums[idx];
- return topping_write(tm, d->target, d->prop,
- topping_m62_source_value[item]);
+ return topping_write(tm, d->target, d->prop, d->src->values[item]);
}
/*
@@ -368,22 +595,26 @@ static int topping_write_sel(struct topping_mixer *tm, int idx,
*/
static void topping_sync(struct topping_mixer *tm)
{
+ const struct topping_model *m = tm->model;
int i, value;
lockdep_assert_held(&tm->lock);
- for (i = 0; i < TOPPING_NUM_CTLS; i++) {
- if (!topping_here(tm, topping_m62_ctls[i].modes))
+ for (i = 0; i < m->num_ctls; i++) {
+ if (!topping_here(tm, m->ctls[i].modes))
continue;
scoped_guard(spinlock_irqsave, &tm->val_lock)
value = tm->val[i];
topping_write_ctl(tm, i, value);
}
- for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
- if (!topping_here(tm, topping_m62_enums[i].modes))
+ for (i = 0; i < m->num_enums; i++) {
+ if (!topping_here(tm, m->enums[i].modes))
continue;
topping_write_sel(tm, i, tm->sel[i]);
}
+ for (i = 0; i < m->num_fixed; i++)
+ topping_write(tm, m->fixed[i].target, m->fixed[i].prop,
+ m->fixed[i].value);
}
static void topping_resume_work(struct work_struct *work)
@@ -400,20 +631,6 @@ static void topping_resume_work(struct work_struct *work)
/* what the HID driver tells this side */
/* ------------------------------------------------------------------ */
-/* -1 when this frame is not one of ours */
-static int topping_index_of(const struct topping_mixer *tm, 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 &&
- topping_here(tm, topping_m62_ctls[i].modes))
- return i;
- return -1;
-}
-
/*
* Every frame the card sends, meters included, in the HID driver's
* interrupt context -- hence the spinlock.
@@ -433,8 +650,8 @@ static void topping_report(struct topping_component *comp, u8 target,
idx = topping_index_of(tm, target, prop);
if (idx < 0)
return; /* a meter, or something unnamed */
- if (value < topping_m62_ctls[idx].min ||
- value > topping_m62_ctls[idx].max)
+ if (value < tm->model->ctls[idx].min ||
+ value > tm->model->ctls[idx].max)
return;
guard(spinlock_irqsave)(&tm->val_lock);
@@ -484,10 +701,12 @@ static const struct topping_audio_ops topping_mixer_ops = {
static int topping_ctl_info(struct snd_kcontrol *kctl,
struct snd_ctl_elem_info *uinfo)
{
+ struct topping_mixer *tm = snd_kcontrol_chip(kctl);
const struct topping_ctl_desc *d;
- d = &topping_m62_ctls[kctl->private_value];
- uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ d = &tm->model->ctls[kctl->private_value];
+ uinfo->type = d->sw ? SNDRV_CTL_ELEM_TYPE_BOOLEAN :
+ SNDRV_CTL_ELEM_TYPE_INTEGER;
uinfo->count = 1;
uinfo->value.integer.min = d->min;
uinfo->value.integer.max = d->max;
@@ -513,8 +732,8 @@ static int topping_ctl_put(struct snd_kcontrol *kctl,
int value, prev, err;
value = ucontrol->value.integer.value[0];
- if (value < topping_m62_ctls[idx].min ||
- value > topping_m62_ctls[idx].max)
+ if (value < tm->model->ctls[idx].min ||
+ value > tm->model->ctls[idx].max)
return -EINVAL;
/*
@@ -566,6 +785,15 @@ static const struct snd_kcontrol_new topping_ctl = {
.put = topping_ctl_put,
};
+/* the same, with no scale to read */
+static const struct snd_kcontrol_new topping_sw = {
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
+ .info = topping_ctl_info,
+ .get = topping_ctl_get,
+ .put = topping_ctl_put,
+};
+
/* ------------------------------------------------------------------ */
/* the source selectors */
/* ------------------------------------------------------------------ */
@@ -573,15 +801,18 @@ static const struct snd_kcontrol_new topping_ctl = {
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_m62_sources),
- topping_m62_sources);
+ struct topping_mixer *tm = snd_kcontrol_chip(kctl);
+ const struct topping_enum_desc *d;
+
+ d = &tm->model->enums[kctl->private_value - tm->model->num_ctls];
+ return snd_ctl_enum_info(uinfo, 1, d->src->num, d->src->items);
}
static int topping_sel_get(struct snd_kcontrol *kctl,
struct snd_ctl_elem_value *ucontrol)
{
struct topping_mixer *tm = snd_kcontrol_chip(kctl);
- int idx = kctl->private_value - TOPPING_NUM_CTLS;
+ int idx = kctl->private_value - tm->model->num_ctls;
guard(spinlock_irqsave)(&tm->val_lock);
ucontrol->value.enumerated.item[0] = tm->sel[idx];
@@ -592,12 +823,12 @@ static int topping_sel_put(struct snd_kcontrol *kctl,
struct snd_ctl_elem_value *ucontrol)
{
struct topping_mixer *tm = snd_kcontrol_chip(kctl);
- int idx = kctl->private_value - TOPPING_NUM_CTLS;
+ int idx = kctl->private_value - tm->model->num_ctls;
unsigned int item, prev;
int err;
item = ucontrol->value.enumerated.item[0];
- if (item >= ARRAY_SIZE(topping_m62_sources))
+ if (item >= tm->model->enums[idx].src->num)
return -EINVAL;
guard(mutex)(&tm->lock);
@@ -678,30 +909,30 @@ static int topping_add_kctl(struct topping_mixer *tm,
static int topping_create_kctls(struct topping_mixer *tm)
{
+ const struct topping_model *m = tm->model;
int i, err;
- for (i = 0; i < TOPPING_NUM_CTLS; i++) {
- if (!topping_here(tm, topping_m62_ctls[i].modes))
+ for (i = 0; i < m->num_ctls; i++) {
+ if (!topping_here(tm, m->ctls[i].modes))
continue;
- err = topping_add_kctl(tm, &topping_ctl,
- topping_m62_ctls[i].name, i,
- topping_m62_ctls[i].tlv);
+ err = topping_add_kctl(tm, m->ctls[i].sw ? &topping_sw :
+ &topping_ctl,
+ m->ctls[i].name, i, m->ctls[i].tlv);
if (err < 0)
goto err_remove;
}
- for (i = 0; i < TOPPING_NUM_ENUMS; i++) {
- if (!topping_here(tm, topping_m62_enums[i].modes))
+ for (i = 0; i < m->num_enums; i++) {
+ if (!topping_here(tm, m->enums[i].modes))
continue;
- err = topping_add_kctl(tm, &topping_sel,
- topping_m62_enums[i].name,
- TOPPING_NUM_CTLS + i, NULL);
+ err = topping_add_kctl(tm, &topping_sel, m->enums[i].name,
+ m->num_ctls + i, NULL);
if (err < 0)
goto err_remove;
}
return 0;
err_remove:
- for (i = 0; i < TOPPING_NUM_KCTLS; i++)
+ for (i = 0; i < m->num_ctls + m->num_enums; i++)
if (tm->kctl[i])
snd_ctl_remove(tm->card, tm->kctl[i]);
return err;
@@ -875,7 +1106,9 @@ static void topping_private_free(struct usb_mixer_interface *mixer)
}
static struct topping_mixer *topping_mixer_new(struct usb_mixer_interface *mixer,
- struct device *dev, u8 mode)
+ struct device *dev,
+ const struct topping_model *m,
+ u8 mode)
{
struct topping_mixer *tm;
int i;
@@ -885,6 +1118,7 @@ static struct topping_mixer *topping_mixer_new(struct usb_mixer_interface *mixer
return NULL;
kref_init(&tm->ref); /* the mixer's */
+ tm->model = m;
tm->mode = mode;
tm->dev = dev;
tm->card = mixer->chip->card;
@@ -894,11 +1128,19 @@ static struct topping_mixer *topping_mixer_new(struct usb_mixer_interface *mixer
mutex_init(&tm->lock);
spin_lock_init(&tm->val_lock);
+ tm->val = kzalloc_objs(*tm->val, m->num_ctls);
+ tm->sel = kzalloc_objs(*tm->sel, m->num_enums);
+ tm->kctl = kzalloc_objs(*tm->kctl, m->num_ctls + m->num_enums);
+ if (!tm->val || !tm->sel || !tm->kctl) {
+ kref_put(&tm->ref, topping_mixer_release);
+ return NULL;
+ }
+
/* what the cache is born with is what the first bind writes */
- for (i = 0; i < TOPPING_NUM_CTLS; i++)
- tm->val[i] = topping_m62_ctls[i].first;
- for (i = 0; i < TOPPING_NUM_ENUMS; i++)
- tm->sel[i] = topping_m62_enums[i].first;
+ for (i = 0; i < m->num_ctls; i++)
+ tm->val[i] = m->ctls[i].first;
+ for (i = 0; i < m->num_enums; i++)
+ tm->sel[i] = m->enums[i].first;
return tm;
}
@@ -907,6 +1149,7 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
{
struct snd_usb_audio *chip = mixer->chip;
struct component_match *match = NULL;
+ const struct topping_model *model;
struct topping_master_res *res;
struct usb_interface *intf;
struct topping_mixer *tm;
@@ -914,7 +1157,19 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
u8 mode = TOPPING_PRO;
int err;
- if (le16_to_cpu(chip->dev->descriptor.bcdDevice) >> 8 == 0x01)
+ switch (chip->usb_id) {
+ case USB_ID(0x152a, 0x875c):
+ model = &topping_m62;
+ break;
+ case USB_ID(0x152a, 0x8756):
+ model = &topping_e2x2_otg;
+ break;
+ default:
+ return -ENODEV;
+ }
+
+ if (model->bcd_mode &&
+ le16_to_cpu(chip->dev->descriptor.bcdDevice) >> 8 == 0x01)
mode = TOPPING_MOBILE;
/*
@@ -931,7 +1186,7 @@ int snd_topping_init(struct usb_mixer_interface *mixer)
return -ENODEV;
dev = &intf->dev;
- tm = topping_mixer_new(mixer, dev, mode);
+ tm = topping_mixer_new(mixer, dev, model, mode);
if (!tm)
return -ENOMEM;
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v9 5/8] HID: topping: report the M62's battery level
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
` (3 preceding siblings ...)
2026-10-18 12:48 ` [PATCH v9 4/8] ALSA: usb-audio: add the Topping E2x2 OTG's vendor controls Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
2026-10-18 13:10 ` sashiko-bot
2026-10-18 12:48 ` [PATCH v9 6/8] ALSA: usb-audio: add the Topping M62's port power settings Mikhail Gavrilov
` (2 subsequent siblings)
7 siblings, 1 reply; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
The M62 runs on its own battery, and a card connected to a computer can
still run down: whether the host charges it depends on a setting of the
card's USB-C port, and nothing on a Linux desktop shows its level.
The card reports the level itself, at 0x11/0x18 in percent: within
seconds of the subscription and again at every change. Offer it as a
power supply of device scope, so that it is never taken for the
system's own battery. The supply is registered only once the first
level has arrived: UPower decides when a device supply appears whether
it is a battery, by reading its capacity, and ignores for good one that
cannot answer yet. Whether it is charging the card does not report, so
the status is unknown.
In Mobile Mode the card announces nothing to a plain subscription, and
its first level came 20 to 30 seconds after it appeared. Inside a
session it announces at once, so the driver now opens one at bind and
at resume as the vendor application does, with the handshake 0x11/0x01
before the subscription and the request to announce: the supply then
appears about 2 seconds after a card is plugged in, in either mode. The
dump of DSP blocks the card adds inside a session carries nothing this
driver exposes, and is ignored.
The card answers the handshake with its mode, 8 to 851 ms after it
here, the latest while it was still sending an announce asked for just
before, and the vendor application sends nothing until that answer has
come; its next frame follows the answer by 4 to 6 ms. In a first bind
that wrote straight after the handshake, the one write that reached the
card 2 ms before the answer was not applied, while the rest were. So
the subscription, the request to announce and everything the audio side
writes after a bind or a resume wait for the answer, or for 3.5 s, a
little longer than an announce lasts, from a card that gives none.
The E2x2 OTG has no battery and gets no supply.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
drivers/hid/Kconfig | 3 +-
drivers/hid/hid-topping.c | 195 +++++++++++++++++++++++++++++++++++---
2 files changed, 184 insertions(+), 14 deletions(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 9367a71..fad88f4 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1289,13 +1289,14 @@ config HID_TOPPING
depends on USB_HID
depends on SND_USB_AUDIO
select CRC16
+ select POWER_SUPPLY
help
The vendor protocol behind the input gains, output volumes and
source selectors of the Topping M62 and E2x2 OTG USB audio
interfaces, and the output switches of the E2x2 OTG. These are
not described by the USB Audio Class and are reached over the
card's HID interface, so a driver is needed for them to work
- at all.
+ at all. The M62's battery level is reported as a power supply.
The controls themselves belong to snd-usb-audio, on the sound
card it makes for the same device; this driver carries them
diff --git a/drivers/hid/hid-topping.c b/drivers/hid/hid-topping.c
index 7b46c4c..f9e05ce 100644
--- a/drivers/hid/hid-topping.c
+++ b/drivers/hid/hid-topping.c
@@ -28,9 +28,15 @@
* The M62 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 what it reports at all. The E2x2 OTG
- * reports without being asked, and its own vendor application never
- * sends it either of the two, so neither does this driver.
+ * makes the device announce what it reports at all -- in Mobile Mode
+ * only inside a session, so a third goes first: 0x11/0x01, the
+ * handshake the vendor application opens with. The device answers it
+ * with its mode, and inside the session adds a dump of its DSP blocks
+ * that nothing here uses; until it has answered, the application sends
+ * it nothing more, and neither does this driver. The E2x2 OTG reports
+ * without being asked, and its own vendor application never subscribes
+ * to it or asks it to announce, so this driver sends it none of the
+ * three.
*
* The control pipe is not an option: GET_REPORT and SET_REPORT both
* stall with EPIPE for every report type, so the interrupt endpoints
@@ -46,14 +52,23 @@
* struct topping_component it is handed with the one operation the
* controls need -- send a frame -- and from then on passes on every
* frame the card reports.
+ *
+ * THE M62'S BATTERY IS. The card reports its charge in percent at
+ * 0x11/0x18, and this driver offers it as a power supply of device
+ * scope, registered once the first level has arrived. Whether the card
+ * is charging it does not say. Like every report the level comes only
+ * to a subscriber, so the supply appears after the card's audio side
+ * has bound.
*/
#include <linux/bits.h>
#include <linux/cleanup.h>
+#include <linux/completion.h>
#include <linux/component.h>
#include <linux/crc16.h>
#include <linux/hid.h>
#include <linux/module.h>
+#include <linux/power_supply.h>
#include <linux/sched/mm.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
@@ -77,9 +92,12 @@
*/
#define TOPPING_IFNUM GENMASK(7, 0)
#define TOPPING_SUBSCRIBE BIT(8) /* wants 0x11/0x24 and its renewal */
+#define TOPPING_BATTERY BIT(9) /* reports its charge at 0x11/0x18 */
/* device-scope properties */
#define TOPPING_TT_DEVICE 0x11
+#define TOPPING_PP_HANDSHAKE 0x01
+#define TOPPING_PP_BATTERY 0x18
#define TOPPING_PP_SUBSCRIBE 0x24
#define TOPPING_PP_ANNOUNCE 0x26
@@ -92,6 +110,18 @@
*/
#define TOPPING_KEEPALIVE_MS 2000
+/*
+ * NOTHING FOLLOWS THE HANDSHAKE UNTIL IT IS ANSWERED. The card answers
+ * 0x11/0x01 8 to 851 ms after it here, the latest while it was still
+ * sending an announce asked for just before, and the vendor application
+ * sends nothing in between: its next frame follows the answer by 4 to
+ * 6 ms. A write that reached the card 2 ms before the answer was once
+ * not applied, while the rest of the same bind's writes were. An
+ * announce lasts 3.1 to 3.3 s from its request, so a card that has not
+ * answered by this is not waited for any longer.
+ */
+#define TOPPING_ANSWER_MS 3500
+
struct topping_hid {
struct hid_device *hdev;
struct usb_interface *intf; /* for runtime PM */
@@ -106,6 +136,18 @@ struct topping_hid {
bool subscribe; /* TOPPING_SUBSCRIBE */
struct delayed_work keepalive;
+ struct completion answered; /* the card's answer to 0x11/0x01 */
+
+ /*
+ * The charge in percent, or -1 until the card has said; written
+ * by .raw_event() alone. psy stays NULL until battery_work has
+ * registered the supply, and on a card without a battery.
+ */
+ bool has_battery; /* TOPPING_BATTERY */
+ int battery;
+ struct power_supply *psy;
+ struct power_supply_desc psy_desc;
+ struct work_struct battery_work;
};
/* ------------------------------------------------------------------ */
@@ -237,6 +279,25 @@ static int topping_raw_event(struct hid_device *hdev,
if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
return 0;
+ if (data[5] == TOPPING_TT_DEVICE && data[6] == TOPPING_PP_HANDSHAKE)
+ complete(&th->answered);
+
+ if (th->has_battery && data[5] == TOPPING_TT_DEVICE &&
+ data[6] == TOPPING_PP_BATTERY) {
+ int charge = clamp_t(s32, get_unaligned_be32(data + 7), 0, 100);
+ /* pairs with the release in topping_battery_work() */
+ struct power_supply *psy = smp_load_acquire(&th->psy);
+
+ /* the card repeats an unchanged level now and then */
+ if (charge != th->battery) {
+ WRITE_ONCE(th->battery, charge);
+ if (psy)
+ power_supply_changed(psy);
+ else
+ schedule_work(&th->battery_work);
+ }
+ }
+
/*
* Under the lock that topping_unbind() takes to clear th->comp,
* so a frame cannot reach an audio side that has already been
@@ -249,6 +310,23 @@ static int topping_raw_event(struct hid_device *hdev,
return 0;
}
+/*
+ * Open a session as the vendor application does on connecting, less
+ * its push of the whole workspace: the handshake, and once the card has
+ * answered it, the subscription and the request to announce. What the
+ * audio side writes comes after this returns, at a bind and at a resume
+ * alike, so that waits for the answer too.
+ */
+static void topping_open_session(struct topping_hid *th)
+{
+ reinit_completion(&th->answered);
+ if (!topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_HANDSHAKE, 1))
+ wait_for_completion_timeout(&th->answered,
+ msecs_to_jiffies(TOPPING_ANSWER_MS));
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
+ topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+}
+
static void topping_keepalive(struct work_struct *work)
{
struct topping_hid *th = container_of(work, struct topping_hid,
@@ -263,6 +341,89 @@ static void topping_keepalive(struct work_struct *work)
msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
}
+/* ------------------------------------------------------------------ */
+/* battery */
+/* ------------------------------------------------------------------ */
+
+static const enum power_supply_property topping_battery_props[] = {
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_SCOPE,
+ POWER_SUPPLY_PROP_MODEL_NAME,
+};
+
+static int topping_battery_get(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct topping_hid *th = power_supply_get_drvdata(psy);
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_STATUS:
+ /* the card does not report whether it is charging */
+ val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ val->intval = READ_ONCE(th->battery);
+ break;
+ case POWER_SUPPLY_PROP_SCOPE:
+ /* the card's own battery, never the system's */
+ val->intval = POWER_SUPPLY_SCOPE_DEVICE;
+ break;
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = th->hdev->name;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return 0;
+}
+
+/*
+ * Registered only once the card has reported a level. A supply that
+ * cannot yet answer for its capacity is not a battery to UPower, which
+ * decides that once, when the supply appears, and ignores it from then
+ * on.
+ */
+static void topping_battery_work(struct work_struct *work)
+{
+ struct topping_hid *th = container_of(work, struct topping_hid,
+ battery_work);
+ struct hid_device *hdev = th->hdev;
+ struct power_supply_config cfg = { .drv_data = th };
+ struct power_supply *psy;
+ int err;
+
+ if (th->psy)
+ return;
+
+ th->psy_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
+ "hid-%s-battery",
+ dev_name(&hdev->dev));
+ if (!th->psy_desc.name)
+ return;
+ th->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY;
+ th->psy_desc.properties = topping_battery_props;
+ th->psy_desc.num_properties = ARRAY_SIZE(topping_battery_props);
+ th->psy_desc.get_property = topping_battery_get;
+
+ /*
+ * devm on the HID device: released after .remove(), which has
+ * stopped .raw_event() and this work by then.
+ */
+ psy = devm_power_supply_register(&hdev->dev, &th->psy_desc, &cfg);
+ if (IS_ERR(psy)) {
+ hid_err(hdev, "battery: %ld\n", PTR_ERR(psy));
+ return;
+ }
+ err = power_supply_powers(psy, &hdev->dev);
+ if (err)
+ hid_warn(hdev, "battery: powers link: %d\n", err);
+
+ /* a supply seen by .raw_event() is a registered one */
+ smp_store_release(&th->psy, psy);
+}
+
/* ------------------------------------------------------------------ */
/* component */
/* ------------------------------------------------------------------ */
@@ -286,15 +447,16 @@ static int topping_bind(struct device *dev, struct device *master,
th->comp = comp;
/*
- * Subscribe and ask for the state HERE rather than at probe:
- * before this point there is nobody to pass the answer to. 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() on its own.
+ * Open the session and ask for the state HERE rather than at
+ * probe: before this point there is nobody to pass the answer to.
+ * 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() on its own. Only
+ * the answer to the handshake is waited for, inside
+ * topping_open_session(), before the audio side writes anything.
*/
if (th->subscribe) {
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_open_session(th);
schedule_delayed_work(&th->keepalive,
msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
}
@@ -367,9 +529,13 @@ static int topping_probe(struct hid_device *hdev,
(id->driver_data & TOPPING_IFNUM))
return -ENODEV;
th->subscribe = id->driver_data & TOPPING_SUBSCRIBE;
+ th->has_battery = id->driver_data & TOPPING_BATTERY;
+ th->battery = -1;
spin_lock_init(&th->lock);
INIT_DELAYED_WORK(&th->keepalive, topping_keepalive);
+ init_completion(&th->answered);
+ INIT_WORK(&th->battery_work, topping_battery_work);
hid_set_drvdata(hdev, th);
err = hid_parse(hdev);
@@ -409,6 +575,7 @@ err_close:
hid_hw_close(hdev);
err_stop:
hid_hw_stop(hdev);
+ cancel_work_sync(&th->battery_work);
return err;
}
@@ -430,6 +597,9 @@ static void topping_remove(struct hid_device *hdev)
hid_hw_close(hdev);
hid_hw_stop(hdev);
+
+ /* After the reports have stopped, so that nothing queues it again. */
+ cancel_work_sync(&th->battery_work);
}
static int topping_suspend(struct hid_device *hdev, pm_message_t message)
@@ -466,8 +636,7 @@ static int topping_resume(struct hid_device *hdev)
*/
if (th->subscribe) {
noio = memalloc_noio_save();
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
- topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
+ topping_open_session(th);
memalloc_noio_restore(noio);
}
@@ -495,7 +664,7 @@ static const struct hid_device_id topping_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_E2X2_OTG),
.driver_data = 4 },
{ HID_USB_DEVICE(USB_VENDOR_ID_TOPPING, USB_DEVICE_ID_TOPPING_M62),
- .driver_data = 4 | TOPPING_SUBSCRIBE },
+ .driver_data = 4 | TOPPING_SUBSCRIBE | TOPPING_BATTERY },
{ }
};
MODULE_DEVICE_TABLE(hid, topping_devices);
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v9 6/8] ALSA: usb-audio: add the Topping M62's port power settings
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
` (4 preceding siblings ...)
2026-10-18 12:48 ` [PATCH v9 5/8] HID: topping: report the M62's battery level Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 7/8] ALSA: usb-audio: add the Topping M62's mutes Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 8/8] ALSA: usb-audio: add the Topping E2x2 OTG's input mutes Mikhail Gavrilov
7 siblings, 0 replies; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
An M62 whose USB-C port the vendor's application has set to Off or
Discharge does not charge from the computer it is connected to, and
runs its battery down; nothing on Linux shows that setting or changes
it, nor the power of the OTG port. Both are kept in the card, and the
application offers them under Device.
Offer both as enumerated controls with the application's items: "USB-C
Port Power" (Charge, Discharge, Off) and "OTG Port Power" (Discharge,
Off), at 0x11/0x1c and 0x11/0x1b, in every mode.
Like every other value here, both are written at the first bind: the
USB-C port to Charge, so that a card left on Off or Discharge by another
host takes power from this one again, and the OTG port to Off, which
leaves a phone on it to its own battery rather than the card's.
alsactl's restore then puts back what the user chose. The card reports
both settings in its announce, and a report updates the control, as for
the volumes; but it sends each of the two once in an announce and leaves
one or both out of some announces, 13 of 120 in one run here, so the
announce cannot stand in for the write. The vendor application reads
them from the announce and never pushes them; a host that is to charge
the card cannot leave that to an announce that may not come.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 143 ++++++++++++++++++++++++++++----------
1 file changed, 108 insertions(+), 35 deletions(-)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 725db57..854ff0a 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -2,16 +2,17 @@
/*
* Topping M62 and E2x2 OTG -- the cards' vendor controls.
*
- * The M62's analogue input gains, output volumes, source selectors and
- * loopback gains, and the E2x2 OTG's digital input gains, faders,
- * source selectors and output switches, are not described by the USB
- * Audio Class. They are reached over a vendor protocol on the card's
- * HID interface, which hid-topping speaks. This file owns the
- * controls; that driver is the transport. The two meet through the
- * component framework, with this side as the master, in the same shape
- * as HD-audio and the graphics drivers in sound/hda/core/component.c:
- * the master owns a struct topping_component, and the HID driver fills
- * in its one operation when it binds and clears it when it unbinds.
+ * The M62's analogue input gains, output volumes, source selectors,
+ * loopback gains and port power settings, and the E2x2 OTG's digital
+ * input gains, faders, source selectors and output switches, are not
+ * described by the USB Audio Class. They are reached over a vendor
+ * protocol on the card's HID interface, which hid-topping speaks.
+ * This file owns the controls; that driver is the transport.
+ * The two meet through the component framework, with this side
+ * as the master, in the same shape as HD-audio and the graphics
+ * drivers in sound/hda/core/component.c: the master owns a struct
+ * topping_component, and the HID driver fills in its one operation
+ * when it binds and clears it when it unbinds.
*
* THE CONTROLS LIVE AS LONG AS THE CARD. They are created at the first
* bind of the HID driver and not removed when it unbinds: a value
@@ -27,19 +28,22 @@
* THE CARD'S STATE CANNOT BE READ. The card reports what a hand does to
* its hardware, every turn of a front-panel knob, and when a session
* opens it announces the gains of the inputs that have something
- * plugged in; nothing answers a request for a value. A host that does
- * not set the card inherits whatever the last host, or a hand on the
- * panel, left in a battery-powered card. So the first bind writes
+ * plugged in and the M62's port power settings, though an announce can
+ * leave those out; nothing answers a request for a value. A host that
+ * does not set the card inherits whatever the last host, or a hand on
+ * the panel, left in a battery-powered card. So the first bind writes
* everything, and from then on the cache is the truth, which the card's
* own reports keep up to date: each output listens to the host's main
* stream, Playback 1/2, with the mixer out of the path, or at 0 dB
* through it where there is no way around it, as on an M62 in Mobile
* Mode; every input gain starts at 0 dB, where it changes nothing; the
- * analogue headphone stage starts at its quiet end; and the digital
- * stream to a phone starts at unity. That follows the usual Linux
- * division -- the kernel puts the hardware into a defined state, and the
- * level a user hears is set from userspace, by alsactl's restore or init
- * and by the sound server, both of which run after the card appears.
+ * analogue headphone stage starts at its quiet end; the digital stream
+ * to a phone starts at unity; and the M62's USB-C port charges the card,
+ * while its OTG port leaves a phone to its own battery. That follows
+ * the usual Linux division -- the kernel puts the hardware into a
+ * defined state, and the level a user hears is set from userspace, by
+ * alsactl's restore or init and by the sound server, both of which run
+ * after the card appears.
*/
#include <linux/bits.h>
@@ -250,14 +254,43 @@ struct topping_enum_desc {
u8 modes; /* the modes that have it, or 0 for all */
};
+/* the power settings of the two ports, as the vendor application has them */
+static const char * const topping_m62_usbc_power[] = {
+ "Charge", "Discharge", "Off",
+};
+
+static const u8 topping_m62_usbc_power_value[] = { 1, 2, 3 };
+
+static const struct topping_sources topping_m62_usbc = {
+ topping_m62_usbc_power, topping_m62_usbc_power_value,
+ ARRAY_SIZE(topping_m62_usbc_power),
+};
+
+static const char * const topping_m62_otg_power[] = { "Discharge", "Off" };
+
+static const u8 topping_m62_otg_power_value[] = { 1, 0 };
+
+static const struct topping_sources topping_m62_otg = {
+ topping_m62_otg_power, topping_m62_otg_power_value,
+ ARRAY_SIZE(topping_m62_otg_power),
+};
+
/*
- * A selector answers on ONE target of a pair, unlike a volume or a
- * mute, which must be written to both: the second target of an
+ * A source selector answers on ONE target of a pair, unlike a volume
+ * or a mute, which must be written to both: the second target of an
* output's pair, the first of a loopback return's. It is never
* reported, so it is always written at the first bind. The outputs
* start on the host's main stream, the loopback returns where the
* vendor application puts them.
*
+ * The power settings of the USB-C port the card is connected by and of
+ * its OTG port are in the card's announce, but not in every one, and
+ * another host can leave them anywhere: set to Off or Discharge, the
+ * USB-C port does not charge the card from this computer, and the card
+ * runs its battery down. So they are written like everything else, the
+ * USB-C port to Charge and the OTG port to Off, which leaves a phone on
+ * that port to its own battery rather than the card's.
+ *
* The names end in "Source", not "Route": alsa-lib's simple mixer joins
* a "... Playback Route" or "... Capture Route" enumeration to the
* volume of the same name and then reads and writes only the
@@ -277,6 +310,8 @@ static const struct topping_enum_desc topping_m62_enums[] = {
TOPPING_M62_BT, &topping_m62_src, TOPPING_PRO },
{ "Loopback 7/8 Capture Source", 0x57, 0x02,
TOPPING_M62_OTG_IN, &topping_m62_src, TOPPING_PRO },
+ { "USB-C Port Power", 0x11, 0x1c, 0, &topping_m62_usbc }, /* Charge */
+ { "OTG Port Power", 0x11, 0x1b, 1, &topping_m62_otg }, /* Off */
};
/*
@@ -610,7 +645,9 @@ static void topping_sync(struct topping_mixer *tm)
for (i = 0; i < m->num_enums; i++) {
if (!topping_here(tm, m->enums[i].modes))
continue;
- topping_write_sel(tm, i, tm->sel[i]);
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ value = tm->sel[i];
+ topping_write_sel(tm, i, value);
}
for (i = 0; i < m->num_fixed; i++)
topping_write(tm, m->fixed[i].target, m->fixed[i].prop,
@@ -640,6 +677,41 @@ static void topping_resume_work(struct work_struct *work)
* control's own private_free clears kctl[] under this lock. It is safe
* from here: it takes read_lock_irqsave and allocates with GFP_ATOMIC.
*/
+static void topping_report_sel(struct topping_mixer *tm, u8 target,
+ u8 prop, s32 value)
+{
+ const struct topping_model *m = tm->model;
+ const struct topping_enum_desc *d;
+ struct snd_kcontrol *kctl;
+ unsigned int item;
+ int i;
+
+ for (i = 0; i < m->num_enums; i++) {
+ d = &m->enums[i];
+ if (d->target == target && d->prop == prop &&
+ topping_here(tm, d->modes))
+ break;
+ }
+ if (i == m->num_enums)
+ return; /* a meter, or something unnamed */
+
+ for (item = 0; item < d->src->num; item++)
+ if (d->src->values[item] == value)
+ break;
+ if (item == d->src->num)
+ return;
+
+ guard(spinlock_irqsave)(&tm->val_lock);
+
+ if (tm->sel[i] == item)
+ return;
+ tm->sel[i] = item;
+
+ kctl = tm->kctl[m->num_ctls + i];
+ if (kctl)
+ snd_ctl_notify(tm->card, SNDRV_CTL_EVENT_MASK_VALUE, &kctl->id);
+}
+
static void topping_report(struct topping_component *comp, u8 target,
u8 prop, s32 value)
{
@@ -648,8 +720,10 @@ static void topping_report(struct topping_component *comp, u8 target,
int idx;
idx = topping_index_of(tm, target, prop);
- if (idx < 0)
- return; /* a meter, or something unnamed */
+ if (idx < 0) {
+ topping_report_sel(tm, target, prop, value);
+ return;
+ }
if (value < tm->model->ctls[idx].min ||
value > tm->model->ctls[idx].max)
return;
@@ -795,7 +869,7 @@ static const struct snd_kcontrol_new topping_sw = {
};
/* ------------------------------------------------------------------ */
-/* the source selectors */
+/* the selectors: sources, and the M62's port power */
/* ------------------------------------------------------------------ */
static int topping_sel_info(struct snd_kcontrol *kctl,
@@ -833,24 +907,23 @@ static int topping_sel_put(struct snd_kcontrol *kctl,
guard(mutex)(&tm->lock);
- /*
- * No report ever moves a selector, so the cache can be compared
- * and changed outside the spinlock; the spinlock is still taken
- * for the reader's sake.
- */
- prev = tm->sel[idx];
- if (prev == item)
- return 0;
- scoped_guard(spinlock_irqsave, &tm->val_lock)
+ /* as for a volume: the cache first, and a report has the last word */
+ scoped_guard(spinlock_irqsave, &tm->val_lock) {
+ if (tm->sel[idx] == item)
+ return 0;
+ prev = tm->sel[idx];
tm->sel[idx] = item;
+ }
if (!tm->bound)
return 1;
err = topping_write_sel(tm, idx, item);
if (err < 0) {
- scoped_guard(spinlock_irqsave, &tm->val_lock)
- tm->sel[idx] = prev;
+ scoped_guard(spinlock_irqsave, &tm->val_lock) {
+ if (tm->sel[idx] == item)
+ tm->sel[idx] = prev;
+ }
return err;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v9 7/8] ALSA: usb-audio: add the Topping M62's mutes
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
` (5 preceding siblings ...)
2026-10-18 12:48 ` [PATCH v9 6/8] ALSA: usb-audio: add the Topping M62's port power settings Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 8/8] ALSA: usb-audio: add the Topping E2x2 OTG's input mutes Mikhail Gavrilov
7 siblings, 0 replies; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
M Control Center shows a MUTE beside each level these controls offer,
and the card keeps it: a MUTE left on by another host silences that
input or output here, with nothing on Linux to show it or to clear it.
Offer each as a switch named after its level, so that alsa-lib's simple
mixer joins the two and a sound server mutes through the card: the
inputs' MUTEs at property 05 of their targets, the outputs' at property
04 of both targets of their pair, and in Mobile Mode those of the
Playback 1/2 knob and the Recording fader at property 04 of theirs. The
card takes 1 for muted, so each switch is the inverse, on when the
sound passes. Each was pressed in the application and captured, in
both modes. The application writes them all off at each connect, and
so does the first bind here. The card reports the outputs' in its
announce, as 0x64/04 and 0x62/04, and that report has the last word.
The card also releases a MUTE by itself. In Pro Audio and Live Streaming
Mode, HP's volume written from the host while HP was muted released the
MUTE in 29 writes of 54, by HP's sound through a cable to AUX, and the
card reported each release at 0x64/04; a MUTE written right after the
volume did not hold in 7 probes of 9. No other level written from the
host released its MUTE. A sound server writes such a volume as a matter
of course: PipeWire, switching back to a device it has saved as muted,
writes the MUTE and then the volume 4 ms later, and the card released
HP's MUTE after 11 such switches of 20, for about two seconds before
PipeWire wrote the MUTE again. So a level set while its switch is off is
only kept, and the switch writes it on its way on, just before the
MUTE 0, as M Control Center writes a level turned under a MUTE before
releasing it. A bind and a resume, which write a level and then its
MUTE, leave a muted level to its switch the same way.
In Live Streaming Mode a level turned one step on the panel released its
port's MUTE in 257 turns of 260. The card reported every release of HP,
AUX and OTG OUT and most of IN 1's and IN 2's, and the switches follow
those reports rather than guess: a release left unreported stays shown
as muted, as M Control Center shows it. A level kept under a MUTE the
card has released, or one the card has reported, is dropped rather than
written over the card's own.
The MUTE on each of Pro Audio Mode's loopback returns has no property of
its own: the application writes the return's gain to off and, when
released, back to its level, and a return carrying a tone stayed as it
was with 1 written to 04 or 05 of its targets. Off is already the
bottom step of that volume control, so the returns get no switch.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 106 +++++++++++++++++++++++++++++++++++---
1 file changed, 98 insertions(+), 8 deletions(-)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 854ff0a..5fd14b3 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -46,6 +46,7 @@
* after the card appears.
*/
+#include <linux/bitmap.h>
#include <linux/bits.h>
#include <linux/build_bug.h>
#include <linux/cleanup.h>
@@ -115,6 +116,7 @@ struct topping_ctl_desc {
const unsigned int *tlv; /* NULL for a switch */
const s32 *map; /* what each step writes, or NULL: the step */
bool sw; /* an on/off switch, not a level */
+ bool mute; /* a MUTE, 1 when on: the switch is its inverse */
u8 resend; /* written again after this one if on, or 0 */
u8 modes; /* the modes that have it, or 0 for all */
};
@@ -163,21 +165,51 @@ static_assert(ARRAY_SIZE(topping_m62_lb_gain) == 103);
static const DECLARE_TLV_DB_SCALE(topping_tlv_lb, -9000, 100, 1);
+/*
+ * Each level has the MUTE the vendor application shows beside it, as a
+ * switch of the same name: property 05 for an input and 04 for an
+ * output, where 1 is muted. The application writes all of them with
+ * the sound on at every connect, and so does the first bind here; the
+ * card reports the outputs' in its announce, and that report has the
+ * last word. The card also releases a MUTE by itself: HP's in about
+ * half the writes of HP's volume made under it, and any port's when its
+ * level is turned on the panel. So a level set while its switch is off
+ * is only kept, and written on the switch's way on, as the application
+ * writes a level turned under a MUTE before releasing it. The card
+ * reports most of its own releases, and the switch follows the report;
+ * a release left unreported is not guessed at. A loopback return's
+ * MUTE has no property: the application writes the gain to off and
+ * back, and off is the bottom step of the return's volume here.
+ */
static const struct topping_ctl_desc topping_m62_ctls[] = {
{ "Mic1 Analog Capture Volume", 0x21, 0, 0x04, 0, 88,
0, topping_tlv_gain }, /* 0 dB */
+ { "Mic1 Analog Capture Switch", 0x21, 0, 0x05, 0, 1, 1,
+ .sw = true, .mute = true },
{ "Mic2 Analog Capture Volume", 0x22, 0, 0x04, 0, 88,
0, topping_tlv_gain }, /* 0 dB */
+ { "Mic2 Analog Capture Switch", 0x22, 0, 0x05, 0, 1, 1,
+ .sw = true, .mute = true },
{ "Aux Capture Volume", 0x23, 0, 0x04, 0, 99,
81, topping_tlv_out_9 }, /* 0 dB */
+ { "Aux Capture Switch", 0x23, 0, 0x05, 0, 1, 1,
+ .sw = true, .mute = true },
{ "Bluetooth Capture Volume", 0x25, 0, 0x04, 0, 99,
99, topping_tlv_out_0 }, /* 0 dB */
+ { "Bluetooth Capture Switch", 0x25, 0, 0x05, 0, 1, 1,
+ .sw = true, .mute = true },
{ "OTG Capture Volume", 0x27, 0, 0x04, 0, 99,
99, topping_tlv_out_0 }, /* 0 dB */
+ { "OTG Capture Switch", 0x27, 0, 0x05, 0, 1, 1,
+ .sw = true, .mute = true },
{ "Headphone Playback Volume", 0x64, 0x63, 0x03, 0, 99,
0, topping_tlv_out_9 }, /* the quiet end */
+ { "Headphone Playback Switch", 0x64, 0x63, 0x04, 0, 1, 1,
+ .sw = true, .mute = true },
{ "OTG Playback Volume", 0x62, 0x61, 0x03, 0, 99,
99, topping_tlv_out_0 }, /* 0 dB, unity */
+ { "OTG Playback Switch", 0x62, 0x61, 0x04, 0, 1, 1,
+ .sw = true, .mute = true },
/*
* A loopback return's gain is written to both targets of its pair
* and never reported. It starts at 0 dB, unity, as the vendor
@@ -197,15 +229,20 @@ static const struct topping_ctl_desc topping_m62_ctls[] = {
* headphones, which carry it with every input while the recording
* does not, and its Recording fader, the level of the recording.
* Both are whole decibels, -90 to +12 and -90 to 0, where Pro Audio
- * Mode takes Q25 on the same targets. Neither is reported, so the
- * first bind writes 0 dB: the knob then passes the playback's level
- * on unchanged, as Pro Audio Mode's outputs do with the mixer out of
- * the path, and the fader is at its top.
+ * Mode takes Q25 on the same targets. Neither is reported, nor is
+ * either MUTE, so the first bind writes 0 dB and the sound on: the
+ * knob then passes the playback's level on unchanged, as Pro Audio
+ * Mode's outputs do with the mixer out of the path, and the fader is
+ * at its top.
*/
{ "PCM Playback Volume", 0x53, 0x54, 0x03, -90, 12,
0, topping_tlv_lb, .modes = TOPPING_MOBILE },
+ { "PCM Playback Switch", 0x53, 0x54, 0x04, 0, 1, 1,
+ .sw = true, .mute = true, .modes = TOPPING_MOBILE },
{ "Recording Capture Volume", 0x51, 0x52, 0x03, -90, 0,
0, topping_tlv_lb, .modes = TOPPING_MOBILE },
+ { "Recording Capture Switch", 0x51, 0x52, 0x04, 0, 1, 1,
+ .sw = true, .mute = true, .modes = TOPPING_MOBILE },
};
/*
@@ -535,6 +572,7 @@ struct topping_mixer {
spinlock_t val_lock; /* the cache and kctl[] against reports */
int *val; /* one per row of the model's ctls */
+ unsigned long *kept; /* levels set under their MUTE, unwritten */
unsigned int *sel; /* one per row of its enums */
/* the volume controls and switches first, then the selectors */
@@ -548,6 +586,7 @@ static void topping_mixer_release(struct kref *ref)
kfree(tm->kctl);
kfree(tm->sel);
+ bitmap_free(tm->kept);
kfree(tm->val);
mutex_destroy(&tm->lock);
kfree(tm);
@@ -585,12 +624,45 @@ static int topping_index_of(const struct topping_mixer *tm, u8 target,
return -1;
}
+/* whether the row after @idx is the MUTE of the level in it */
+static bool topping_muted(const struct topping_model *m, int idx)
+{
+ return idx + 1 < m->num_ctls && m->ctls[idx + 1].mute;
+}
+
static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
{
- const struct topping_ctl_desc *d = &tm->model->ctls[idx];
- s32 wire = d->map ? d->map[value] : value;
+ const struct topping_model *m = tm->model;
+ const struct topping_ctl_desc *d = &m->ctls[idx];
+ s32 wire = d->mute ? !value : d->map ? d->map[value] : value;
+ bool kept;
int err, other, on;
+ /*
+ * A level set while its switch is off is only kept: written under
+ * the MUTE, it may be taken for a release. The switch writes it on
+ * its way on, before the MUTE 0, so that the sound comes back at it.
+ */
+ if (topping_muted(m, idx)) {
+ scoped_guard(spinlock_irqsave, &tm->val_lock) {
+ on = tm->val[idx + 1];
+ if (!on)
+ __set_bit(idx, tm->kept);
+ }
+ if (!on)
+ return 0;
+ } else if (d->mute && value) {
+ scoped_guard(spinlock_irqsave, &tm->val_lock) {
+ kept = test_bit(idx - 1, tm->kept);
+ other = tm->val[idx - 1];
+ }
+ if (kept) {
+ err = topping_write_ctl(tm, idx - 1, other);
+ if (err)
+ return err;
+ }
+ }
+
err = topping_write(tm, d->target, d->prop, wire);
if (!err && d->target_pair) {
/*
@@ -599,6 +671,10 @@ static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
*/
err = topping_write(tm, d->target_pair, d->prop, wire);
}
+ if (!err && topping_muted(m, idx)) {
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ __clear_bit(idx, tm->kept);
+ }
if (!err && d->resend) {
other = topping_index_of(tm, d->resend, d->prop);
scoped_guard(spinlock_irqsave, &tm->val_lock)
@@ -622,7 +698,8 @@ static int topping_write_sel(struct topping_mixer *tm, int idx,
* values the cache was born with; at a later bind, with what was set
* while this side was alone, which is still only in the cache; and after
* a resume, in case the card came up on its own defaults or was changed
- * on its panel while the host slept.
+ * on its panel while the host slept. A level whose switch is off is
+ * kept for the switch, as a put keeps it.
*
* Errors are not passed on. The HID driver logs the ones that mean
* something, and a card that cannot be written now is written again by
@@ -724,12 +801,24 @@ static void topping_report(struct topping_component *comp, u8 target,
topping_report_sel(tm, target, prop, value);
return;
}
+ if (tm->model->ctls[idx].mute)
+ value = !value;
if (value < tm->model->ctls[idx].min ||
value > tm->model->ctls[idx].max)
return;
guard(spinlock_irqsave)(&tm->val_lock);
+ /*
+ * The card's word, on a level or on the release of its MUTE, drops
+ * a level kept under that MUTE, rather than have it written over
+ * the card's later.
+ */
+ if (topping_muted(tm->model, idx))
+ __clear_bit(idx, tm->kept);
+ else if (tm->model->ctls[idx].mute && value)
+ __clear_bit(idx - 1, tm->kept);
+
if (tm->val[idx] == value)
return;
tm->val[idx] = value;
@@ -1202,9 +1291,10 @@ static struct topping_mixer *topping_mixer_new(struct usb_mixer_interface *mixer
spin_lock_init(&tm->val_lock);
tm->val = kzalloc_objs(*tm->val, m->num_ctls);
+ tm->kept = bitmap_zalloc(m->num_ctls, GFP_KERNEL);
tm->sel = kzalloc_objs(*tm->sel, m->num_enums);
tm->kctl = kzalloc_objs(*tm->kctl, m->num_ctls + m->num_enums);
- if (!tm->val || !tm->sel || !tm->kctl) {
+ if (!tm->val || !tm->kept || !tm->sel || !tm->kctl) {
kref_put(&tm->ref, topping_mixer_release);
return NULL;
}
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* [PATCH v9 8/8] ALSA: usb-audio: add the Topping E2x2 OTG's input mutes
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
` (6 preceding siblings ...)
2026-10-18 12:48 ` [PATCH v9 7/8] ALSA: usb-audio: add the Topping M62's mutes Mikhail Gavrilov
@ 2026-10-18 12:48 ` Mikhail Gavrilov
7 siblings, 0 replies; 11+ messages in thread
From: Mikhail Gavrilov @ 2026-10-18 12:48 UTC (permalink / raw)
To: jikos, bentiss, tiwai
Cc: tiwai, perex, linux-input, linux-sound, linux-kernel,
Mikhail Gavrilov
TOPPING Professional Control Center shows a MUTE beside each input of
the E2x2 OTG, and on Linux an input could not be muted on the card: the
gain controls start at +0 dB, as the application's knobs do, and its
MUTE is a value below them.
The card keeps no MUTE of its own. The application writes the input's
gain, property 05, to 0, writes 0 again at every step of the knob while
the MUTE is lit, and on release writes back the gain it kept; nothing
else is written either way, and the Mobile input's MUTE writes 0x22
alone, as its gain does, which silences both of its channels. The card
acts on the 0 at once: an input's meter, which follows the gain,
read -96.0 within 82 ms of the MUTE and came back within 62 ms of the
release. The gain itself never reaches 0, so 0 is the MUTE and anything
above it the gain.
Offer each input's MUTE as a switch named after its gain, so that
alsa-lib's simple mixer joins the two and a sound server mutes through
the card, as on the M62. The switch and the gain write that one
property: 0 while the switch is off, the gain otherwise, and a gain set
while the switch is off is only kept, as the application keeps it. The
first bind writes the switches on, so a MUTE another host left on the
card is released there, as writing the gains already released it.
An off step at the bottom of each gain would have been simpler, but the
step above it is +0 dB: the input would go from silence to its full
level in one step, and a sound server that follows the scale in
decibels would find nothing between the two.
The faders get no switch. Their MUTE, on Mobile OUT, S/PDIF OUT and
each loopback, writes the same 0 to both targets of the pair, which is
already a fader's bottom step, and the steps above it start at -80 dB.
Output 1+2 has no MUTE in the application at all, only its three output
buttons, which are switches here already.
Signed-off-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
---
sound/usb/mixer_topping.c | 43 +++++++++++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/sound/usb/mixer_topping.c b/sound/usb/mixer_topping.c
index 5fd14b3..6e1f60d 100644
--- a/sound/usb/mixer_topping.c
+++ b/sound/usb/mixer_topping.c
@@ -117,6 +117,7 @@ struct topping_ctl_desc {
const s32 *map; /* what each step writes, or NULL: the step */
bool sw; /* an on/off switch, not a level */
bool mute; /* a MUTE, 1 when on: the switch is its inverse */
+ bool zero_mute; /* the MUTE of the level above, as its 0 */
u8 resend; /* written again after this one if on, or 0 */
u8 modes; /* the modes that have it, or 0 for all */
};
@@ -404,12 +405,28 @@ static_assert(ARRAY_SIZE(topping_e2x2_fader) == 82);
static const DECLARE_TLV_DB_SCALE(topping_tlv_fader, -8100, 100, 1);
static const struct topping_ctl_desc topping_e2x2_ctls[] = {
+ /*
+ * An input's MUTE has no property of its own. The vendor
+ * application writes the gain to 0, writes 0 again at every step of
+ * the knob while the MUTE is lit, and on release writes back the
+ * gain it kept; the gain itself never reaches 0. So each gain has
+ * its MUTE after it, as a switch of the same name, and the two write
+ * that one property: 0 while the switch is off, the gain otherwise,
+ * and a gain set while the switch is off is only kept. A fader's
+ * MUTE is the same 0, which is already the fader's bottom step.
+ */
{ "Mic1 Digital Capture Volume", 0x21, 0, 0x05, 0, 20,
0, topping_tlv_gain, topping_e2x2_gain },
+ { "Mic1 Digital Capture Switch", 0x21, 0, 0x05, 0, 1, 1,
+ .sw = true, .zero_mute = true },
{ "Mic2 Digital Capture Volume", 0x23, 0, 0x05, 0, 20,
0, topping_tlv_gain, topping_e2x2_gain },
+ { "Mic2 Digital Capture Switch", 0x23, 0, 0x05, 0, 1, 1,
+ .sw = true, .zero_mute = true },
{ "OTG Capture Volume", 0x22, 0, 0x05, 0, 20,
0, topping_tlv_gain, topping_e2x2_gain },
+ { "OTG Capture Switch", 0x22, 0, 0x05, 0, 1, 1,
+ .sw = true, .zero_mute = true },
/*
* Output 1+2's fader as "Master": the sound server's stock
* profile takes a control by that name for the volume of the
@@ -630,10 +647,17 @@ static bool topping_muted(const struct topping_model *m, int idx)
return idx + 1 < m->num_ctls && m->ctls[idx + 1].mute;
}
+/* the same, for a MUTE written as the level's 0 */
+static bool topping_zero_muted(const struct topping_model *m, int idx)
+{
+ return idx + 1 < m->num_ctls && m->ctls[idx + 1].zero_mute;
+}
+
static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
{
const struct topping_model *m = tm->model;
const struct topping_ctl_desc *d = &m->ctls[idx];
+ const struct topping_ctl_desc *lvl;
s32 wire = d->mute ? !value : d->map ? d->map[value] : value;
bool kept;
int err, other, on;
@@ -663,6 +687,23 @@ static int topping_write_ctl(struct topping_mixer *tm, int idx, int value)
}
}
+ /*
+ * A zero_mute switch and the level above it write one property: 0
+ * while the switch is off, the level otherwise. A level set while
+ * its switch is off is only kept here as well.
+ */
+ if (d->zero_mute) {
+ lvl = &m->ctls[idx - 1];
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ other = tm->val[idx - 1];
+ wire = !value ? 0 : lvl->map ? lvl->map[other] : other;
+ } else if (topping_zero_muted(m, idx)) {
+ scoped_guard(spinlock_irqsave, &tm->val_lock)
+ on = tm->val[idx + 1];
+ if (!on)
+ return 0;
+ }
+
err = topping_write(tm, d->target, d->prop, wire);
if (!err && d->target_pair) {
/*
@@ -715,6 +756,8 @@ static void topping_sync(struct topping_mixer *tm)
for (i = 0; i < m->num_ctls; i++) {
if (!topping_here(tm, m->ctls[i].modes))
continue;
+ if (topping_zero_muted(m, i))
+ continue; /* its switch writes it */
scoped_guard(spinlock_irqsave, &tm->val_lock)
value = tm->val[i];
topping_write_ctl(tm, i, value);
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread
* Re: [PATCH v9 5/8] HID: topping: report the M62's battery level
2026-10-18 12:48 ` [PATCH v9 5/8] HID: topping: report the M62's battery level Mikhail Gavrilov
@ 2026-10-18 13:10 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-18 13:10 UTC (permalink / raw)
To: Mikhail Gavrilov; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] HID: topping: devres group race in topping_battery_work() causes UAF
- [Medium] HID: topping: missing runtime PM get in topping_bind() breaks session open
--
commit 6adaaec1c5316acae80d87f07a530efd9db3ae46
Author: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
HID: topping: report the M62's battery level
This patch adds support for reporting the Topping M62's battery level via
a device-scoped power supply registered once the first battery report
arrives. It also sends a session handshake (0x11/0x01) and waits for the
device's reply at bind and resume so that the M62 announces its state in
Mobile Mode.
> diff --git a/drivers/hid/hid-topping.c b/drivers/hid/hid-topping.c
> index 7b46c4cc41745..f9e05ceafad1f 100644
> --- a/drivers/hid/hid-topping.c
> +++ b/drivers/hid/hid-topping.c
[ ... ]
> @@ -237,6 +279,25 @@ static int topping_raw_event(struct hid_device *hdev,
> if (get_unaligned_be16(data + 11) != crc16(0xffff, data + 2, 9))
> return 0;
>
> + if (data[5] == TOPPING_TT_DEVICE && data[6] == TOPPING_PP_HANDSHAKE)
> + complete(&th->answered);
> +
> + if (th->has_battery && data[5] == TOPPING_TT_DEVICE &&
> + data[6] == TOPPING_PP_BATTERY) {
> + int charge = clamp_t(s32, get_unaligned_be32(data + 7), 0, 100);
> + /* pairs with the release in topping_battery_work() */
> + struct power_supply *psy = smp_load_acquire(&th->psy);
> +
> + /* the card repeats an unchanged level now and then */
> + if (charge != th->battery) {
> + WRITE_ONCE(th->battery, charge);
> + if (psy)
> + power_supply_changed(psy);
> + else
> + schedule_work(&th->battery_work);
> + }
> + }
[ ... ]
> @@ -263,6 +341,89 @@ static void topping_keepalive(struct work_struct *work)
> msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
> }
>
> +/* ------------------------------------------------------------------ */
> +/* battery */
> +/* ------------------------------------------------------------------ */
[ ... ]
> +static void topping_battery_work(struct work_struct *work)
> +{
> + struct topping_hid *th = container_of(work, struct topping_hid,
> + battery_work);
> + struct hid_device *hdev = th->hdev;
> + struct power_supply_config cfg = { .drv_data = th };
> + struct power_supply *psy;
> + int err;
> +
> + if (th->psy)
> + return;
> +
> + th->psy_desc.name = devm_kasprintf(&hdev->dev, GFP_KERNEL,
> + "hid-%s-battery",
> + dev_name(&hdev->dev));
> + if (!th->psy_desc.name)
> + return;
> + th->psy_desc.type = POWER_SUPPLY_TYPE_BATTERY;
> + th->psy_desc.properties = topping_battery_props;
> + th->psy_desc.num_properties = ARRAY_SIZE(topping_battery_props);
> + th->psy_desc.get_property = topping_battery_get;
> +
> + /*
> + * devm on the HID device: released after .remove(), which has
> + * stopped .raw_event() and this work by then.
> + */
> + psy = devm_power_supply_register(&hdev->dev, &th->psy_desc, &cfg);
> + if (IS_ERR(psy)) {
> + hid_err(hdev, "battery: %ld\n", PTR_ERR(psy));
> + return;
> + }
> + err = power_supply_powers(psy, &hdev->dev);
> + if (err)
> + hid_warn(hdev, "battery: powers link: %d\n", err);
> +
> + /* a supply seen by .raw_event() is a registered one */
> + smp_store_release(&th->psy, psy);
> +}
[Severity: High]
Can calling devm_kasprintf() and devm_power_supply_register() on &hdev->dev
from topping_battery_work() race with component_bind() and cause a
use-after-free on th->psy or th->psy_desc.name?
When the aggregate device binds, component_bind() wraps the call to
topping_bind() in a devres group on component->dev (&hdev->dev):
drivers/base/component.c:component_bind() {
if (!devres_open_group(component->dev, component, GFP_KERNEL)) {
...
}
ret = component->ops->bind(component->dev, adev->parent, data);
if (!ret) {
component->bound = true;
devres_close_group(component->dev, NULL);
...
}
}
Inside topping_bind(), topping_open_session() sends TOPPING_PP_HANDSHAKE,
waits up to 3.5 seconds, and then sends TOPPING_PP_SUBSCRIBE and
TOPPING_PP_ANNOUNCE while incoming reports are already enabled via
hid_device_io_start().
If a battery report (0x11/0x18) arrives in topping_raw_event() before
component_bind() reaches devres_close_group(&hdev->dev, NULL) (either
after TOPPING_PP_SUBSCRIBE while TOPPING_PP_ANNOUNCE is still in flight, or
during wait_for_completion_timeout() if the card's 2-second subscription
was still active from a prior bind), topping_raw_event() schedules
th->battery_work.
If topping_battery_work() executes while the component's devres group is
still open on &hdev->dev, devm_kasprintf() and devm_power_supply_register()
are captured into the component's devres group instead of the HID driver's
probe group.
Doesn't that cause two problems on unbind or removal?
1. If the audio master unbinds (for example, snd-usb-audio is unbound or
topping_master_bind() fails), component_unbind() calls
devres_release_group(&hdev->dev, component), which unregisters and frees
th->psy (and/or th->psy_desc.name) while hid-topping remains bound and
th->psy remains non-NULL. Subsequent battery frames in
topping_raw_event() will then pass the freed th->psy pointer to
power_supply_changed(psy).
2. In topping_remove(), component_del() runs before hid_hw_close(),
hid_hw_stop(), and cancel_work_sync(&th->battery_work), which would
release the captured power supply while topping_raw_event() or
topping_battery_work() can still be running.
> @@ -286,15 +447,16 @@ static int topping_bind(struct device *dev, struct device *master,
> th->comp = comp;
>
> /*
> - * Subscribe and ask for the state HERE rather than at probe:
> - * before this point there is nobody to pass the answer to. 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() on its own.
> + * Open the session and ask for the state HERE rather than at
> + * probe: before this point there is nobody to pass the answer to.
> + * 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() on its own. Only
> + * the answer to the handshake is waited for, inside
> + * topping_open_session(), before the audio side writes anything.
> */
> if (th->subscribe) {
> - topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
> - topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
> + topping_open_session(th);
> schedule_delayed_work(&th->keepalive,
> msecs_to_jiffies(TOPPING_KEEPALIVE_MS));
> }
[Severity: Medium]
Should topping_bind() hold a runtime PM reference via
usb_autopm_get_interface(th->intf) across topping_open_session()?
In topping_probe(), hid_hw_open() calls usbhid_open(), which starts the IN
URB and drops its autopm reference before returning, leaving th->intf's
runtime PM usage count at 0. As noted in the comment above topping_send(),
waking the interface is the caller's responsibility (which topping_write()
does with usb_autopm_get_interface(th->intf)).
Because topping_bind() does not call usb_autopm_get_interface(th->intf):
1. If th->intf has already runtime-suspended before topping_bind() runs
(for instance, if hid-topping probed before snd-usb-audio and
autosuspended because no reports arrive before subscription, or if the
audio driver is unbound and rebound), usbhid->urbin has been stopped by
hid_suspend(). When topping_open_session() sends TOPPING_PP_HANDSHAKE
and waits in wait_for_completion_timeout(&th->answered, ...), the reply
cannot be received on the stopped IN URB, causing a 3.5-second timeout
and dropped session frames.
2. Even if th->intf is active when topping_bind() starts, waiting up to
3500 ms in topping_open_session() exceeds the default 2000 ms USB
autosuspend delay. If th->intf runtime-suspends during the wait,
topping_suspend() runs before topping_bind() calls
schedule_delayed_work(&th->keepalive, ...), leaving th->keepalive
scheduled on a suspended interface; if th->intf then resumes before
topping_open_session() finishes, topping_resume() sees th->comp != NULL
and calls topping_open_session() concurrently on the shared
th->answered completion.
[ ... ]
> @@ -466,8 +636,7 @@ static int topping_resume(struct hid_device *hdev)
> */
> if (th->subscribe) {
> noio = memalloc_noio_save();
> - topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_SUBSCRIBE, 1);
> - topping_send(th, TOPPING_TT_DEVICE, TOPPING_PP_ANNOUNCE, 1);
> + topping_open_session(th);
> memalloc_noio_restore(noio);
> }
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261018124838.129349-1-mikhail.v.gavrilov@gmail.com?part=5
^ permalink raw reply [flat|nested] 11+ messages in thread
* Re: [PATCH v9 1/8] HID: topping: transport for the M62's vendor controls
2026-10-18 12:48 ` [PATCH v9 1/8] HID: topping: transport for the M62's vendor controls Mikhail Gavrilov
@ 2026-10-18 13:13 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-18 13:13 UTC (permalink / raw)
To: Mikhail Gavrilov; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] HID: topping: missing pm_ptr() on topping_driver PM callbacks
--
commit cefe32a6b81c6b7ed6128d273a5c5c6a50f7896a
Author: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
HID: topping: transport for the M62's vendor controls
This commit adds a HID transport driver for the Topping M62 USB audio
interface's vendor control protocol, connecting to snd-usb-audio via the
component framework.
> diff --git a/drivers/hid/hid-topping.c b/drivers/hid/hid-topping.c
> new file mode 100644
> index 0000000000000..a8854b2d15306
> --- /dev/null
> +++ b/drivers/hid/hid-topping.c
> @@ -0,0 +1,498 @@
[ ... ]
> +static struct hid_driver topping_driver = {
> + .name = "topping",
> + .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);
[Severity: Low]
In drivers/hid/hid-core.c, hid_driver_suspend(), hid_driver_resume(), and
hid_driver_reset_resume() are only built when CONFIG_PM is enabled:
drivers/hid/hid-core.c:
#ifdef CONFIG_PM
int hid_driver_suspend(struct hid_device *hdev, pm_message_t state)
{
if (hdev->driver && hdev->driver->suspend)
return hdev->driver->suspend(hdev, state);
return 0;
}
Should .suspend, .resume, and .reset_resume in topping_driver be wrapped
with pm_ptr() so that topping_suspend() and topping_resume() can be
eliminated by the compiler when CONFIG_PM is disabled?
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261018124838.129349-1-mikhail.v.gavrilov@gmail.com?part=1
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-18 13:13 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-18 12:48 [PATCH v9 0/8] the Topping M62's and E2x2 OTG's vendor controls, on the component framework Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 1/8] HID: topping: transport for the M62's vendor controls Mikhail Gavrilov
2026-10-18 13:13 ` sashiko-bot
2026-10-18 12:48 ` [PATCH v9 2/8] ALSA: usb-audio: add the Topping " Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 3/8] HID: topping: add the E2x2 OTG Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 4/8] ALSA: usb-audio: add the Topping E2x2 OTG's vendor controls Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 5/8] HID: topping: report the M62's battery level Mikhail Gavrilov
2026-10-18 13:10 ` sashiko-bot
2026-10-18 12:48 ` [PATCH v9 6/8] ALSA: usb-audio: add the Topping M62's port power settings Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 7/8] ALSA: usb-audio: add the Topping M62's mutes Mikhail Gavrilov
2026-10-18 12:48 ` [PATCH v9 8/8] ALSA: usb-audio: add the Topping E2x2 OTG's input mutes Mikhail Gavrilov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox