From: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
To: tiwai@suse.com, jikos@kernel.org, bentiss@kernel.org
Cc: perex@perex.cz, linux-sound@vger.kernel.org,
linux-input@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [RFC 0/2] Two ways to reach the Topping M62's analogue gains
Date: Thu, 20 Aug 2026 20:13:27 +0500 [thread overview]
Message-ID: <20260820151329.18332-1-mikhail.v.gavrilov@gmail.com> (raw)
In-Reply-To: <87y0eaxz39.wl-tiwai@suse.de>
You asked for PoCs of both roads and a comparison of the actual code
rather than of arguments. Here are both. They are alternatives, not a
series: each is written against mainline 98f21c54f995 on its own, and
either can be applied alone.
1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface
2/2 HID: topping: a HID driver that registers a card of its own
Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two
CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have
been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for
reading, for unsolicited notification from the front panel, and for
writing.
What the device is
==================
The M62 keeps its two microphone preamp gains, its AUX and Bluetooth
input volumes and its headphone and OTG output volumes behind a vendor
protocol on a HID-class interface, and exposes none of them through
UAC. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: a noise-floor ladder
against the card shows the converter's own floor rising with the
signal. So on Linux today the one knob worth setting is the one that
cannot be reached, and a measurement application has to begin by asking
a human to touch the front panel.
The protocol is fifteen-byte frames -- start magic, a constant, a
target, a property, a signed 32-bit big-endian value, CRC-16/MODBUS
over the middle stored big-endian, end magic. Rebuilding all 2619
captured frames from that description reproduces them byte for byte.
The device says nothing until it is subscribed; one write starts the
stream and a second makes it announce its whole state, after which
every change arrives unasked, including a front panel press.
The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route.
What is identical in both
=========================
The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...)
is CRC-16/MODBUS, so no private table), and the control table. A knob
is a row of
{ name, target, paired target, property, min, max, TLV }
so adding one is adding a row. Six rows today. The outputs come in
pairs because the device answers on only one target of each pair and
the other would drift away unheard.
Where they differ
=================
1/2 claims the HID interface for snd-usb-audio and puts the elements on
the card the device already has. The cost is two-sided: an entry in
hid_ignore_list to keep usbhid off the interface, and one new helper in
sound/usb/card.c, because usb_audio_driver is static there and a quirk
cannot claim an interface without it. That helper is the only change in
1/2 outside the new file and its dispatch. Nothing is lost by taking
the interface: the report descriptor is a Generic Desktop application
collection with eight unnamed usages, sixteen bytes in and out and no
report ID, so hid-generic can only build an input device for a mouse
that does not exist -- which is what it does today.
2/2 binds as a HID driver, and the protocol half is if anything smaller
there: usbhid owns the endpoints, so hid_hw_output_report replaces a
hand-built interrupt URB out, raw_event replaces the one in, and no
interface has to be claimed. It needs nothing in sound/usb.
But these are mixer controls for an audio device, and the audio
device's card belongs to snd-usb-audio. A HID driver cannot put an
element there. There is no interface for it, and inventing one means
exporting from sound/usb both a lookup from struct usb_device to the
card and an add-element call, and then answering, for a single device,
what happens when the two drivers probe in either order and when either
disconnects first, given that the element would live in one module and
its private data in another.
So 2/2 does what a HID driver can do alone: it registers a card of its
own. That works, and the cost is visible from userspace rather than
theoretical:
$ cat /proc/asound/cards
0 [ToppingCtl ]: Topping - Topping M62 control
...
4 [M62 ]: USB-Audio - M62
$ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33
amixer: Cannot find the given element from control sysdefault:4
One device, two cards; the gains on a card with no PCM beside them; and
anything that looks for a device's mixer next to its streams --
alsamixer -c, UCM profiles, PipeWire's device model -- does not find
them there.
Against my own preference, two honest notes. The phantom input device
2/2 leaves at boot (hid-generic binds first, the specific driver being
a module outside the initramfs) is a packaging artefact, not a property
of that road. And 1/2's claim helper is new API surface in sound/usb,
small as it is.
Field results
=============
With 1/2: the interface belongs to snd-usb-audio while a neighbouring
device's HID interface still belongs to usbhid, so the ignore entry is
precise. Values arrive by themselves -- the headphone volume came up at
51 while the zero-initialised cache would have said 0. One front panel
press produces exactly one control event. A write reaches the hardware:
the device reports the written value back, and its meters answer.
With 2/2: the same, on its own card.
One device fact worth recording: a written gain takes effect at once,
but when the device commits it to non-volatile memory is the firmware's
business, and a value written and then torn off the bus can come back
as the older one. Nothing in either driver depends on that -- neither
treats itself as the source of truth, both ask the device -- but it is
easy to mistake for a driver bug while testing.
Where I come out
================
The knobs belong on the card the device already has, and 2/2 cannot put
them there without a new cross-subsystem interface built for one
device. 1/2's cost is one static-variable problem solved by one helper
in the file that owns it. So I would take 1/2, which is also your gut
feeling -- but the comparison is what you asked for, and either patch
stands alone if you read it the other way.
Not covered by either: the OTG input's gain. It has no front panel
control and therefore never announced itself in any capture, so its
property is unknown. It is one row when it is known.
Mikhail
next prev parent reply other threads:[~2026-08-20 15:13 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-12 17:10 snd-usb-audio: exposing a vendor HID control channel as mixer controls (Topping M62, 152a:875c) Mikhail Gavrilov
2026-08-13 7:24 ` Takashi Iwai
2026-08-20 15:13 ` Mikhail Gavrilov [this message]
2026-08-20 15:13 ` [RFC 1/2] ALSA: usb-audio: expose the Topping M62's analogue gains as mixer controls Mikhail Gavrilov
2026-08-20 15:13 ` [RFC 2/2] HID: topping: driver for the M62's vendor control channel Mikhail Gavrilov
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260820151329.18332-1-mikhail.v.gavrilov@gmail.com \
--to=mikhail.v.gavrilov@gmail.com \
--cc=bentiss@kernel.org \
--cc=jikos@kernel.org \
--cc=linux-input@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-sound@vger.kernel.org \
--cc=perex@perex.cz \
--cc=tiwai@suse.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox