[PATCH v10 6/8] ALSA: usb-audio: add the Topping M62's port power settings

From: Mikhail Gavrilov

Date: Sun Oct 18 2026 - 16:59:53 EST


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@xxxxxxxxx>
---
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