Re: [RFC 0/2] Two ways to reach the Topping M62's analogue gains
From: Takashi Iwai
Date: Sun Aug 23 2026 - 04:51:52 EST
On Thu, 20 Aug 2026 17:13:27 +0200,
Mikhail Gavrilov wrote:
>
> You asked for PoCs of both roads and a comparison of the actual code
> rather than of arguments. Here are both. They are alternatives, not a
> series: each is written against mainline 98f21c54f995 on its own, and
> either can be applied alone.
>
> 1/2 ALSA: usb-audio: a mixer quirk that claims the HID interface
> 2/2 HID: topping: a HID driver that registers a card of its own
>
> Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two
> CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have
> been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for
> reading, for unsolicited notification from the front panel, and for
> writing.
>
> What the device is
> ==================
>
> The M62 keeps its two microphone preamp gains, its AUX and Bluetooth
> input volumes and its headphone and OTG output volumes behind a vendor
> protocol on a HID-class interface, and exposes none of them through
> UAC. What UAC does expose on the capture side is a digital trim after
> the converter, which cannot buy signal-to-noise: a noise-floor ladder
> against the card shows the converter's own floor rising with the
> signal. So on Linux today the one knob worth setting is the one that
> cannot be reached, and a measurement application has to begin by asking
> a human to touch the front panel.
>
> The protocol is fifteen-byte frames -- start magic, a constant, a
> target, a property, a signed 32-bit big-endian value, CRC-16/MODBUS
> over the middle stored big-endian, end magic. Rebuilding all 2619
> captured frames from that description reproduces them byte for byte.
> The device says nothing until it is subscribed; one write starts the
> stream and a second makes it announce its whole state, after which
> every change arrives unasked, including a front panel press.
>
> The control pipe is not an option: GET_REPORT and SET_REPORT stall with
> EPIPE for every report type, so the interrupt endpoints on the HID
> interface are the only route.
>
> What is identical in both
> =========================
>
> The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...)
> is CRC-16/MODBUS, so no private table), and the control table. A knob
> is a row of
>
> { name, target, paired target, property, min, max, TLV }
>
> so adding one is adding a row. Six rows today. The outputs come in
> pairs because the device answers on only one target of each pair and
> the other would drift away unheard.
>
> Where they differ
> =================
>
> 1/2 claims the HID interface for snd-usb-audio and puts the elements on
> the card the device already has. The cost is two-sided: an entry in
> hid_ignore_list to keep usbhid off the interface, and one new helper in
> sound/usb/card.c, because usb_audio_driver is static there and a quirk
> cannot claim an interface without it. That helper is the only change in
> 1/2 outside the new file and its dispatch. Nothing is lost by taking
> the interface: the report descriptor is a Generic Desktop application
> collection with eight unnamed usages, sixteen bytes in and out and no
> report ID, so hid-generic can only build an input device for a mouse
> that does not exist -- which is what it does today.
>
> 2/2 binds as a HID driver, and the protocol half is if anything smaller
> there: usbhid owns the endpoints, so hid_hw_output_report replaces a
> hand-built interrupt URB out, raw_event replaces the one in, and no
> interface has to be claimed. It needs nothing in sound/usb.
>
> But these are mixer controls for an audio device, and the audio
> device's card belongs to snd-usb-audio. A HID driver cannot put an
> element there. There is no interface for it, and inventing one means
> exporting from sound/usb both a lookup from struct usb_device to the
> card and an add-element call, and then answering, for a single device,
> what happens when the two drivers probe in either order and when either
> disconnects first, given that the element would live in one module and
> its private data in another.
>
> So 2/2 does what a HID driver can do alone: it registers a card of its
> own. That works, and the cost is visible from userspace rather than
> theoretical:
>
> $ cat /proc/asound/cards
> 0 [ToppingCtl ]: Topping - Topping M62 control
> ...
> 4 [M62 ]: USB-Audio - M62
>
> $ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33
> amixer: Cannot find the given element from control sysdefault:4
>
> One device, two cards; the gains on a card with no PCM beside them; and
> anything that looks for a device's mixer next to its streams --
> alsamixer -c, UCM profiles, PipeWire's device model -- does not find
> them there.
>
> Against my own preference, two honest notes. The phantom input device
> 2/2 leaves at boot (hid-generic binds first, the specific driver being
> a module outside the initramfs) is a packaging artefact, not a property
> of that road. And 1/2's claim helper is new API surface in sound/usb,
> small as it is.
>
> Field results
> =============
>
> With 1/2: the interface belongs to snd-usb-audio while a neighbouring
> device's HID interface still belongs to usbhid, so the ignore entry is
> precise. Values arrive by themselves -- the headphone volume came up at
> 51 while the zero-initialised cache would have said 0. One front panel
> press produces exactly one control event. A write reaches the hardware:
> the device reports the written value back, and its meters answer.
>
> With 2/2: the same, on its own card.
>
> One device fact worth recording: a written gain takes effect at once,
> but when the device commits it to non-volatile memory is the firmware's
> business, and a value written and then torn off the bus can come back
> as the older one. Nothing in either driver depends on that -- neither
> treats itself as the source of truth, both ask the device -- but it is
> easy to mistake for a driver bug while testing.
>
> Where I come out
> ================
>
> The knobs belong on the card the device already has, and 2/2 cannot put
> them there without a new cross-subsystem interface built for one
> device. 1/2's cost is one static-variable problem solved by one helper
> in the file that owns it. So I would take 1/2, which is also your gut
> feeling -- but the comparison is what you asked for, and either patch
> stands alone if you read it the other way.
>
> Not covered by either: the OTG input's gain. It has no front panel
> control and therefore never announced itself in any capture, so its
> property is unknown. It is one row when it is known.
Thanks! I didn't expect such complete patches when I asked for PoC,
and you've done much better than I thought :)
Honestly speaking, both look well acceptable. From the pure kernel
POV, the 2nd patch is simpler, but as you pointed out, we'll need
another stuff to combining two sound cards. It'd be likely a special
UCM profile, but this can be a bit hackish.
So, from the usability POV, the first patch would be "easier", and if
I have to choose, my gut feeling is to pick the first one. But again,
I have no strong opinion, and both look good in general. I'd like to
hear from others, too.
About the code, there are a few things to be improved. The spinlock
could be done better with guard(). A temporary buffer could be
handled with __free(), too.
thanks,
Takashi