[PATCH v8 0/2] the Topping M62's vendor controls, on the component framework
From: Mikhail Gavrilov
Date: Sat Oct 03 2026 - 14:42:46 EST
The Topping M62 keeps its analogue input gains, output volumes and
output source selectors behind a vendor protocol on a HID interface,
outside the USB Audio Class. On Linux they can only be set by hand on
the front panel; the one capture gain snd-usb-audio exposes is a
digital trim after the converter.
This series makes them ordinary ALSA controls on the card
snd-usb-audio already creates. 1/2 is a HID driver that carries the
protocol. 2/2 adds the controls to the M62's mixer quirk, which is
also the component master the HID driver binds to.
Two things change since v7.
The controls now live as long as the card. In v7 the HID driver
created them on the card it was handed at bind and removed them at
unbind, so reloading it was visible to everything holding the card:
the controls came back with new numids, a stored alsactl state no
longer matched them -- and restore then fell back to its generic init,
which sets any "Headphone Playback Volume" to -20 dB, here the
analogue headphone stage -- a sound server kept the elements it had
already enumerated, and a volume the card was still holding read as
zero. Now the controls are snd-usb-audio's and the HID driver is the
transport: it fills in an ops structure the master owns, in the shape
of struct drm_audio_component (new: include/sound/topping.h), and
passes on what the card reports. Unbinding it changes nothing about
the controls, not even their active flag, since alsa-lib's simple
mixer handles an INFO event by removing the element and adding it
again -- the very churn this is meant to spare a sound server. A value
written meanwhile is kept, and the next bind writes it back. Controls
are no longer removed from a live card either, which is what set off
the alsa-lib heap corruption described in the v7 cover letter.
The selectors no longer have an "Unknown" item. The card never reports
a source selector, and reports an output volume only when its knob
turns. Topping were asked for a command that reads the selection and
declined to provide one or to commit to adding one. So the driver now
sets what it cannot read: at the first bind each output listens to
Playback 1/2, the analogue headphone volume starts at its quiet end --
what init_cur_mix_raw() does for a volume it cannot read -- and the
OTG stream at unity. alsactl and the sound server override these as
soon as the card appears, and after a resume they are written again
from the cache.
On the tree question: 2/2 includes the header 1/2 adds, so it builds
only on top of 1/2; at run time neither needs the other. 1/2 alone
binds, subscribes and passes nothing on; 2/2 alone registers a master
that never matches.
Changes since v7
(https://lore.kernel.org/all/20260930213322.32454-1-mikhail.v.gavrilov@xxxxxxxxx/):
- the controls, their table, tapers and cache moved from
hid-topping-m62 into sound/usb/mixer_topping.c; the HID driver
keeps the frame, the subscription and the power management, and
hands the audio side every valid frame;
- include/sound/topping.h is the interface between the two;
- the HID driver is now hid-topping, CONFIG_HID_TOPPING: captures of
Topping's own application driving their E2x2 OTG show the same
frame, and a module name is hard to change once it has been
released;
- the controls survive the HID driver's unbind without an event, and
a write made while it is unbound reaches the card at the next bind;
- the "Unknown" item is gone; what the card does not report is
written at the first bind and after a resume;
- a gain the card has not reported and nobody has set is left alone
at a rebind rather than written as the zero the cache holds.
Sashiko's question on v7 2/2, whether topping_private_free() can race
the devres unwind of a failed probe, applies to this version
unchanged, and so does the answer: both run under the usb_device lock
(https://lore.kernel.org/all/20260930224954.121402-1-mikhail.v.gavrilov@xxxxxxxxx/).
Tested
Fedora, 7.3.0-rc5-e767a4ea70a3 plus this series, with KASAN (generic),
lockdep and UBSAN; M62 firmware V87.05.45.48.27, bcdDevice 3.27. Both
loaded modules, hid-topping and snd-usb-audio, were matched against
the installed files by build ID, and the new source files in the tree
they were built from against these patches. Two M62s were connected
throughout, the second on other firmware, bcdDevice 1.45; the runs
below address the first unless they say otherwise.
A reboot with both connected: hid-topping bound the two at 11.6 and
11.7 s, and the components were bound at 27.3 and 34.0 s from inside
snd-usb-audio's probe, each card to the HID device of its own unit, so
the nine controls existed before either card was registered -- numids
11 to 19 on the first card, 12 to 20 on the second, whose USB Audio
Class part has one control more; the first card's headphone selector
with fourteen items, on Playback 1/2.
Module unload and load against the live cards with PipeWire running:
"amixer -c M62 contents" was identical before and after, and "wpctl
get-volume @DEFAULT_AUDIO_SINK@" read 0.29 before and after.
Fifty further cycles of module unload and load, one second apart, with
PipeWire running on an alsa-lib that carries the remap fix: a hundred
binds, two per cycle; WirePlumber was never restarted by systemd, and
nothing on the system dumped core.
With the card's headphone volume at 20, "Headphone Playback Volume"
set to 30 while hid-topping was unloaded read 30 after the module was
loaded again, and one step of the front-panel knob then brought the
card's own report of 31: the new bind had written 30 to the card.
"alsactl restore M62" exits 0, with no fallback to the generic init,
before and after a system suspend to RAM. Around a second suspend, one
step of a card's headphone knob moved that card's "Headphone Playback
Volume" by one and left the other card's alone -- before the suspend
on the first card, after it on both.
The first card's cable pulled out while audio was playing: one failed
URB submission from snd-usb-audio, the disconnect, and nothing else;
plugged back in, the HID driver and the component bound again within
a second. The second card was not disturbed.
The cable pulled out again while a loop wrote 30 and 31 into
"Headphone Playback Volume" back to back: five writes during the pull
returned EPROTO to amixer, then the card was gone. The log held the
disconnect and nothing else, and both bound again within a second of
the plug going back in. The second card's cable pulled out once as
well: the disconnect alone, and both bound again within a second of
the plug going back in.
No WARNING, BUG, KASAN, UBSAN, lockdep report or call trace in the
kernel log of the whole boot.
Not re-run for v8, after passing on v7: Mobile Mode without a UCM
profile.
Not tested:
- the reload cycles with stock alsa-lib: this machine carries the
remap fix;
- the HID driver binding after the card has registered, so that the
restorers run before the controls exist: on every boot here the HID
driver bound first;
- the write-back after a resume of what the card does not report,
which changes nothing a control shows and was not listened for;
- an audio-side unbind and rebind with the HID driver left bound;
- hibernation, and with it .reset_resume, which shares
topping_resume() with .resume: this machine powers off instead of
saving an image, for reasons unrelated to this series;
- kmemleak, compiled in here but disabled at boot;
- a card whose battery has run down;
- a big-endian host.
Mikhail Gavrilov (2):
HID: topping: transport for the M62's vendor controls
ALSA: usb-audio: add the Topping M62's vendor controls
MAINTAINERS | 10 +
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 +++
sound/usb/Makefile | 1 +
sound/usb/mixer_quirks.c | 5 +
sound/usb/mixer_topping.c | 902 ++++++++++++++++++++++++++++++++++++++
sound/usb/mixer_topping.h | 7 +
11 files changed, 1507 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
--
2.43.0