Thread (39 messages) flat view 39 messages, 3 authors, 8h ago

Re: [RFC 0/2] Two ways to reach the Topping M62's analogue gains

From: Mikhail Gavrilov <hidden>
Date: 2026-08-21 11:23:34
Also in: linux-sound, lkml

On Thu, Aug 20, 2026 at 8:13 PM Mikhail Gavrilov
[off-list ref] wrote:
You asked for PoCs of both roads and a comparison of the actual code
rather than of arguments. Here are both. They are alternatives, not a
series: each is written against mainline 98f21c54f995 on its own, and
either can be applied alone.

  1/2  ALSA: usb-audio: a mixer quirk that claims the HID interface
  2/2  HID: topping: a HID driver that registers a card of its own

Both build clean (checkpatch --strict: 0 errors, 0 warnings; the two
CamelCase CHECKs in 1/2 are bNumEndpoints and bInterval) and both have
been exercised on the device -- 152a:875c, bcdDevice 3.27 -- for
reading, for unsolicited notification from the front panel, and for
writing.

What the device is
==================

The M62 keeps its two microphone preamp gains, its AUX and Bluetooth
input volumes and its headphone and OTG output volumes behind a vendor
protocol on a HID-class interface, and exposes none of them through
UAC. What UAC does expose on the capture side is a digital trim after
the converter, which cannot buy signal-to-noise: a noise-floor ladder
against the card shows the converter's own floor rising with the
signal. So on Linux today the one knob worth setting is the one that
cannot be reached, and a measurement application has to begin by asking
a human to touch the front panel.

The protocol is fifteen-byte frames -- start magic, a constant, a
target, a property, a signed 32-bit big-endian value, CRC-16/MODBUS
over the middle stored big-endian, end magic. Rebuilding all 2619
captured frames from that description reproduces them byte for byte.
The device says nothing until it is subscribed; one write starts the
stream and a second makes it announce its whole state, after which
every change arrives unasked, including a front panel press.

The control pipe is not an option: GET_REPORT and SET_REPORT stall with
EPIPE for every report type, so the interrupt endpoints on the HID
interface are the only route.

What is identical in both
=========================

The frame builder, the parser, the CRC (the kernel's crc16(0xffff, ...)
is CRC-16/MODBUS, so no private table), and the control table. A knob
is a row of

        { name, target, paired target, property, min, max, TLV }

so adding one is adding a row. Six rows today. The outputs come in
pairs because the device answers on only one target of each pair and
the other would drift away unheard.

Where they differ
=================

1/2 claims the HID interface for snd-usb-audio and puts the elements on
the card the device already has. The cost is two-sided: an entry in
hid_ignore_list to keep usbhid off the interface, and one new helper in
sound/usb/card.c, because usb_audio_driver is static there and a quirk
cannot claim an interface without it. That helper is the only change in
1/2 outside the new file and its dispatch. Nothing is lost by taking
the interface: the report descriptor is a Generic Desktop application
collection with eight unnamed usages, sixteen bytes in and out and no
report ID, so hid-generic can only build an input device for a mouse
that does not exist -- which is what it does today.

2/2 binds as a HID driver, and the protocol half is if anything smaller
there: usbhid owns the endpoints, so hid_hw_output_report replaces a
hand-built interrupt URB out, raw_event replaces the one in, and no
interface has to be claimed. It needs nothing in sound/usb.

But these are mixer controls for an audio device, and the audio
device's card belongs to snd-usb-audio. A HID driver cannot put an
element there. There is no interface for it, and inventing one means
exporting from sound/usb both a lookup from struct usb_device to the
card and an add-element call, and then answering, for a single device,
what happens when the two drivers probe in either order and when either
disconnects first, given that the element would live in one module and
its private data in another.

So 2/2 does what a HID driver can do alone: it registers a card of its
own. That works, and the cost is visible from userspace rather than
theoretical:

        $ cat /proc/asound/cards
         0 [ToppingCtl     ]: Topping - Topping M62 control
         ...
         4 [M62            ]: USB-Audio - M62

        $ amixer -c M62 cset name='Mic-1 Analog Capture Volume' 33
        amixer: Cannot find the given element from control sysdefault:4

One device, two cards; the gains on a card with no PCM beside them; and
anything that looks for a device's mixer next to its streams --
alsamixer -c, UCM profiles, PipeWire's device model -- does not find
them there.

Against my own preference, two honest notes. The phantom input device
2/2 leaves at boot (hid-generic binds first, the specific driver being
a module outside the initramfs) is a packaging artefact, not a property
of that road. And 1/2's claim helper is new API surface in sound/usb,
small as it is.

Field results
=============

With 1/2: the interface belongs to snd-usb-audio while a neighbouring
device's HID interface still belongs to usbhid, so the ignore entry is
precise. Values arrive by themselves -- the headphone volume came up at
51 while the zero-initialised cache would have said 0. One front panel
press produces exactly one control event. A write reaches the hardware:
the device reports the written value back, and its meters answer.

With 2/2: the same, on its own card.

One device fact worth recording: a written gain takes effect at once,
but when the device commits it to non-volatile memory is the firmware's
business, and a value written and then torn off the bus can come back
as the older one. Nothing in either driver depends on that -- neither
treats itself as the source of truth, both ask the device -- but it is
easy to mistake for a driver bug while testing.

Where I come out
================

The knobs belong on the card the device already has, and 2/2 cannot put
them there without a new cross-subsystem interface built for one
device. 1/2's cost is one static-variable problem solved by one helper
in the file that owns it. So I would take 1/2, which is also your gut
feeling -- but the comparison is what you asked for, and either patch
stands alone if you read it the other way.

Not covered by either: the OTG input's gain. It has no front panel
control and therefore never announced itself in any capture, so its
property is unknown. It is one row when it is known.

Mikhail
Three things I should have said in the cover letter, one of which
argues against the conclusion I drew there.

The quirk's hid_ignore_list entry means no hidraw node is created for
this device at all. Topping ship a control application for Windows and
macOS and not for Linux; there is none today, and the driver should not
block one if it appears. The HID road leaves that channel open and the
quirk road closes it. I weighed the two roads by where the mixer
controls can live and did not weigh this, and it belongs on the scale.

The one gap the cover letter named is closed. The OTG input's gain is
target 0x27, property 0x04, on the same taper family as Bluetooth --
read out of a capture of the vendor application moving it, with the
indices it dwelt on matching the decibels it displayed. Both patches
carry the row here; I have not resent them for one line, and it will be
in whichever version goes forward.

And the protocol turns out to need more than a write path, which bears
on the complexity you asked me to compare. The vendor application
repeats its subscribe every two seconds, so a driver has to keep that
up or the device stops reporting; the device has two memories, and a
separate command commits the live state to the one that survives a
power cycle; and the source selectors -- which mix or bus each output
listens to -- can be written but never read: the device does not
announce them, and the vendor application does not ask, because on
connect it pushes its whole workspace rather than reading anything.
None of that favours either road, since both pay it identically, but it
is a fair bit more than a table of gains, and I would rather you saw it
before deciding.

I have not resent the patches. Both roads work on the hardware; the
question is still which one you would rather carry.

-- 
Thanks,
Mikhail Gavrilov.
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help