Re: [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support

From: dchristensen8

Date: Wed Sep 30 2026 - 13:21:12 EST


Thats genius. I reworked it with an ALSA byte control instead of sysfs and rolled in all your code cleanups for v2.

I set up two controls using IFACE_CARD and volatile:
-AE-5 LED Strip": 300 bytes of RGB
-AE-5 LED Strip Count: integer for setting the active strip length

Attached the v2 patch file. If you'd prefer anything named or laid out differently, just let me know.

Thanks,
Devin





On Wednesday, September 30th, 2026 at 9:46 AM, Takashi Iwai <tiwai@xxxxxxx> wrote:

> On Tue, 29 Sep 2026 16:22:07 +0200,
> dchristensen8 wrote:
> >
> > Thanks for the quick review!
> >
> > I went with sysfs mainly because of the streaming nature of WS2812 strips (using an atomic DMA frame):
> >
> > -The LED subsystem has no strip or pixel-buffer concept, individual led_classdevs would hit sysfs with thousands of writes/sec at 30-60 FPS (and leds-class.rst advises against it for addressable arrays anyway).
> > -An ALSA kcontrol could hold raw bytes, but pushing frames at 60 Hz spams control change events to PipeWire/PulseAudio.
> >
> > I initially went with text attributes (ae5_strip_leds/ae5_strip_num_leds) so basic shell scripts could just echo colors into it, but I know comma-separated lists bend the "one value per file" rule. Would you prefer a bin_attribute for raw RGB bytes instead? or keeping both?
> >
> > All your code points make total sense too. I have no objections.
> >
> > Let me know your thoughts on the sysfs format.
>
> On the second though, why not simply to use an ALSA control element
> with bytes data? It can hold up to 512 bytes, so 100 of 24bit RGB
> strips can fit.
> You can declare the element as volatile (and with IFACE_CARD), so that
> pipewire or alsactl won't be bothered by the changes.
>
>
> thanks,
>
> Takashi
> From 69aec5e8836f8874e16663cd364ac5887ad85645 Mon Sep 17 00:00:00 2001
From: Devin Christensen <dchristensen8@xxxxxxxxx>
Date: Wed, 30 Sep 2026 10:09:17 -0400
Subject: [PATCH v2] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED
strip support

The Sound BlasterX AE-5 and AE-5 Plus expose a 3-pin 5V addressable RGB
header driven directly by the CA0113 PCIe-to-HD-Audio bridge. Rather
than using the CA0132 DSP or MMIO GPIO, the CA0113 contains an
autonomous HDA link sniffer that snoops a designated HDA DMA playback
stream, extracts the 24-bit GRB color payload, and serializes it as
800 kHz single-wire NRZ pulses to the strip (WS2812B protocol).

Expose the strip through volatile card control elements:

"AE-5 LED Strip" (RW, BYTES): 300-byte RGB payload representing
up to 100 addressable LEDs (3 bytes per LED).
"AE-5 LED Strip Count" (RW, INTEGER): Active LED count (1..100).

The controls are marked with IFACE_CARD and SNDRV_CTL_ELEM_ACCESS_VOLATILE
so changes do not trigger unnecessary audio daemon wakeups or state
restoration by alsactl.

Writes allocate an idle HDA playback stream, format a 32 KB DMA buffer
populated with repeating WS2812 frames (preamble, audio words per LED,
and reset gap), configure the CA0113 BAR2 serializers and stream-tag
pin mux, and pulse the stream long enough for the reset latch to fire.
Access is serialized with a per-spec mutex, and the controls are only
registered on QUIRK_AE5 devices.

Signed-off-by: Devin Christensen <dchristensen8@xxxxxxxxx>
---
v1 -> v2:
- Replaced sysfs attributes with ALSA control elements as suggested by Takashi:
* "AE-5 LED Strip" (300-byte RW BYTES control for up to 100 RGB LEDs)
* "AE-5 LED Strip Count" (RW INTEGER control for active strip length 1..100)
* Marked with IFACE_CARD and SNDRV_CTL_ELEM_ACCESS_VOLATILE to prevent
spurious wakeups in PipeWire/PulseAudio and avoid state restoration in alsactl.
* Dropped sysfs attributes and Documentation/ABI file.
- Cleaned up ae5_strip_encode_24() using BIT() macro logic.
- Converted ae5_strip_find_stream() to guard(spinlock_irqsave)(&bus->reg_lock).
- Added lockdep_assert_held(&spec->ae5_strip_mutex) in ae5_strip_send_frame().
- Replaced magic numbers with named constants (AE5_STRIP_DMA_SIZE, etc.).
- Collapsed the 4-channel CA0113 BAR2 serializer register writes into loops
with stride 0x40.

sound/hda/codecs/ca0132.c | 373 +++++++++++++++++++++++++++++++++++++-
1 file changed, 372 insertions(+), 1 deletion(-)

diff --git a/sound/hda/codecs/ca0132.c b/sound/hda/codecs/ca0132.c
index 0f8fb62f0..c6eaef5d5 100644
--- a/sound/hda/codecs/ca0132.c
+++ b/sound/hda/codecs/ca0132.c
@@ -35,6 +35,8 @@
#include <sound/tlv.h>
#endif

+#include <linux/dma-mapping.h>
+
#define FLOAT_ZERO 0x00000000
#define FLOAT_ONE 0x3f800000
#define FLOAT_TWO 0x40000000
@@ -1060,6 +1062,8 @@ enum dsp_download_state {
* CA0132 specific
*/

+#define AE5_STRIP_MAX_LEDS 100
+
struct ca0132_spec {
struct hda_gen_spec gen;

@@ -1162,6 +1166,11 @@ struct ca0132_spec {
* Renames PlayEnhancement and CrystalVoice too.
*/
bool use_alt_controls;
+
+ /* AE-5 WS2812 strip control */
+ struct mutex ae5_strip_mutex;
+ u32 ae5_strip_cur_colors[AE5_STRIP_MAX_LEDS];
+ int ae5_strip_cur_num_leds;
};

/*
@@ -6846,6 +6855,8 @@ static int ca0132_alt_add_mic_boost_enum(struct hda_codec *codec)

}

+static int ae5_add_strip_controls(struct hda_codec *codec);
+
/*
* Add headphone gain enumerated control for the AE-5. This switches between
* three modes, low, medium, and high. When non-headphone outputs are selected,
@@ -7149,6 +7160,10 @@ static int ca0132_build_controls(struct hda_codec *codec)

switch (ca0132_quirk(spec)) {
case QUIRK_AE5:
+ err = ae5_add_strip_controls(codec);
+ if (err < 0)
+ return err;
+ fallthrough;
case QUIRK_AE7:
err = ae5_add_headphone_gain_enum(codec);
if (err < 0)
@@ -8316,8 +8331,361 @@ static void sbz_setup_defaults(struct hda_codec *codec)
}

/*
- * Setup default parameters for the Sound BlasterX AE-5 DSP.
+ * Sound BlasterX AE-5 External WS2812B RGB LED Strip Transport
*/
+#define AE5_STRIP_DMA_SIZE 0x8000
+#define AE5_STRIP_TOTAL_WORDS (AE5_STRIP_DMA_SIZE / sizeof(u32))
+#define AE5_STRIP_PREAMBLE_WORDS 5
+#define AE5_STRIP_WORDS_PER_LED 4
+#define AE5_STRIP_RESET_GAP_WORDS 83
+
+#define CA0113_SERIALIZER_BASE(spec, ch) ((spec)->mem_base + 0x42c + (ch) * 0x40)
+#define CA0113_SERIALIZER_CLK(spec, ch) ((spec)->mem_base + 0x43c + (ch) * 0x40)
+#define CA0113_SERIALIZER_OUT(spec, ch) ((spec)->mem_base + 0x454 + (ch) * 0x40)
+
+static void ae5_strip_encode_24(u32 u, u32 *words)
+{
+ /*
+ * 24-bit pixel -> 4 x 32-bit audio words (6 bits per word).
+ * Hardware audio serializer transmits MSB first (bits 31 down to 0).
+ * For word i (i8 = 23, 17, 11, 5), bit_idx 0..5 (MSB to LSB within the word)
+ * is placed at n = 29 - bit_idx * 4:
+ * Bit 0 -> 0b100 << n (375ns high, 1042ns low): BIT(n + 2)
+ * Bit 1 -> 0b111 << n (1083ns high, 333ns low): BIT(n + 2) | BIT(n + 1) | BIT(n)
+ */
+ int i8 = 23;
+ int i, bit_idx;
+
+ for (i = 0; i < 4; i++) {
+ u32 out = 0;
+
+ for (bit_idx = 0; bit_idx < 6; bit_idx++) {
+ int bit = (u >> (i8 - bit_idx)) & 1;
+ int n = 29 - bit_idx * 4;
+
+ out |= (bit ? (BIT(2) | BIT(1) | BIT(0)) : BIT(2)) << n;
+ }
+ words[i] = out;
+ i8 -= 6;
+ }
+}
+
+static struct hdac_stream *ae5_strip_find_stream(struct hda_codec *codec)
+{
+ struct ca0132_spec *spec = codec->spec;
+ struct hdac_bus *bus = &codec->bus->core;
+ struct hdac_stream *s, *dsp = NULL, *first = NULL;
+
+ guard(spinlock_irqsave)(&bus->reg_lock);
+ list_for_each_entry(s, &bus->stream_list, list) {
+ if (s->direction != SNDRV_PCM_STREAM_PLAYBACK)
+ continue;
+ if (s->running || s->locked)
+ continue;
+ if (!first)
+ first = s;
+ /* The DSP DMA consumes the DSP-loader stream (tag in dsp_stream_id). */
+ if (spec->dsp_stream_id && s->stream_tag == spec->dsp_stream_id)
+ dsp = s;
+ }
+
+ return dsp ? dsp : first;
+}
+
+/*
+ * Caller must hold spec->ae5_strip_mutex.
+ */
+static int ae5_strip_send_frame(struct hda_codec *codec, const u32 *grb_colors, int num_leds)
+{
+ struct ca0132_spec *spec = codec->spec;
+ struct snd_dma_buffer dmab;
+ struct hdac_stream *hstr = NULL;
+ unsigned int format, stream_tag;
+ u32 enc_words[4];
+ u32 *dst;
+ int word_idx = 0;
+ int frame_words;
+ int led, w, i;
+ int retries = 20; /* ~100ms max */
+ u8 orig_sd_ctl = 0;
+
+ if (!spec || !spec->mem_base)
+ return -ENODEV;
+ if (num_leds <= 0 || num_leds > AE5_STRIP_MAX_LEDS)
+ return -EINVAL;
+
+ lockdep_assert_held(&spec->ae5_strip_mutex);
+
+ snd_hda_power_up(codec);
+
+ /*
+ * 44.1kHz, 24-bit, 2-channel format (0x4031) matches Windows CtxHda
+ * HDAUDIO_STREAM_FORMAT.
+ */
+ format = snd_hdac_stream_format(2, 24, 44100);
+
+ for (;;) {
+ hstr = ae5_strip_find_stream(codec);
+ if (!hstr) {
+ if (--retries <= 0) {
+ codec_warn(codec, "AE5 strip: no free azx stream after retries\n");
+ snd_hda_power_down(codec);
+ return -EBUSY;
+ }
+ usleep_range(5000, 10000);
+ continue;
+ }
+
+ stream_tag = snd_hdac_dsp_prepare(hstr, format, AE5_STRIP_DMA_SIZE, &dmab);
+ if ((int)stream_tag > 0)
+ break;
+
+ /* Prepare failed (e.g. -EBUSY due to race with playback starting); retry */
+ if (--retries <= 0) {
+ codec_warn(codec, "AE5 strip: snd_hdac_dsp_prepare failed %d\n",
+ (int)stream_tag);
+ snd_hda_power_down(codec);
+ return (int)stream_tag;
+ }
+ usleep_range(5000, 10000);
+ }
+
+ /* Prevent interrupt storms during free-running LED stream, saving original control bits */
+ if (hstr->sd_addr) {
+ orig_sd_ctl = readb(hstr->sd_addr);
+ writeb(orig_sd_ctl & ~0x1c, hstr->sd_addr); /* Clear IOCE, FEIE, DEIE */
+ }
+
+ /* 1. Populate the 32KB DMA buffer with repeating WS2812 frames + reset gaps */
+ memset(dmab.area, 0, AE5_STRIP_DMA_SIZE);
+ dst = (u32 *)dmab.area;
+
+ /* Words per frame: preamble + (num_leds * words_per_led) + reset gap */
+ frame_words = AE5_STRIP_PREAMBLE_WORDS + (num_leds * AE5_STRIP_WORDS_PER_LED) +
+ AE5_STRIP_RESET_GAP_WORDS;
+
+ while (word_idx + frame_words <= AE5_STRIP_TOTAL_WORDS) {
+ /* Preamble per Windows CtxHda FUN_00042944 */
+ for (w = 0; w < AE5_STRIP_PREAMBLE_WORDS; w++) {
+ *dst++ = 0;
+ word_idx++;
+ }
+ /* LEDs */
+ for (led = 0; led < num_leds; led++) {
+ ae5_strip_encode_24(grb_colors[led], enc_words);
+ for (w = 0; w < AE5_STRIP_WORDS_PER_LED; w++) {
+ *dst++ = enc_words[w];
+ word_idx++;
+ }
+ }
+ /* Zero reset gap (> 1.8 ms; WS2812 requires > 50 us low) */
+ for (w = 0; w < AE5_STRIP_RESET_GAP_WORDS; w++) {
+ *dst++ = 0;
+ word_idx++;
+ }
+ }
+ while (word_idx < AE5_STRIP_TOTAL_WORDS) {
+ *dst++ = 0;
+ word_idx++;
+ }
+ /* Ensure DMA buffer writes are committed before triggering the HDA stream */
+ wmb();
+
+ /* 2. Configure CA0113 BAR2 hardware registers */
+ /* Power up clocks (CtxHdb 0x141b7 / 0x14210) */
+ writel(readl(spec->mem_base + 0xc04) | 0x7, spec->mem_base + 0xc04);
+
+ /* Base serializer initialization (CtxHdb 0x14ba4) */
+ writel(readl(spec->mem_base + 0x400) | 1, spec->mem_base + 0x400);
+ for (i = 0; i < 4; i++) {
+ writel(readl(CA0113_SERIALIZER_BASE(spec, i)) & ~1,
+ CA0113_SERIALIZER_BASE(spec, i));
+ writel(0, CA0113_SERIALIZER_CLK(spec, i));
+ }
+ writel(readl(spec->mem_base + 0x408) | 1, spec->mem_base + 0x408);
+ writel(readl(spec->mem_base + 0x40c) | 1, spec->mem_base + 0x40c);
+ writel((readl(spec->mem_base + 0x410) & ~0xb) | 0x14, spec->mem_base + 0x410);
+
+ /* Configure serializers 0..3 clocking (CtxHdb 0x14a6c) */
+ for (i = 0; i < 4; i++)
+ writel(readl(CA0113_SERIALIZER_CLK(spec, i)) | 0x33,
+ CA0113_SERIALIZER_CLK(spec, i));
+
+ /* BAR2 + 0x100 format & channel enables:
+ * - base 0x70f
+ * - bit 28 = 1 (24-bit audio format via CtxHdb 0x14f58)
+ * - bits 29..31 = 1 (44.1 kHz sample rate via CtxHdb 0x14fa4)
+ * - bits 4..7 = 0xf (stage 1 enables)
+ * - bits 12..15 = 0xf (stage 2 enables)
+ * => 0x3000ff8f
+ */
+ writel((readl(spec->mem_base + 0x100) & ~0xe000ffff) | 0x3000ff8f, spec->mem_base + 0x100);
+
+ /* Enable serializer output drivers for pins 0..3 (CtxHdb 0x14e94) */
+ for (i = 0; i < 4; i++)
+ writel(readl(CA0113_SERIALIZER_OUT(spec, i)) | 1,
+ CA0113_SERIALIZER_OUT(spec, i));
+
+ /* Route all channels 0..3 to stream_tag (CtxHdb 0x1502c) */
+ writel((stream_tag << 28) | (stream_tag << 20) | (stream_tag << 12) | (stream_tag << 4),
+ spec->mem_base + 0x104);
+
+ /* Enable CA0113 MMIO GPIO 0 & 1 */
+ ca0113_mmio_gpio_set(codec, 0, true);
+ ca0113_mmio_gpio_set(codec, 1, true);
+
+ codec_dbg(codec, "AE5 strip: send_frame: num_leds=%d stream_tag=%u (0x%08x)\n",
+ num_leds, stream_tag, readl(spec->mem_base + 0x104));
+
+ /* 3. Trigger stream transmission on HDA link */
+ snd_hdac_dsp_trigger(hstr, true);
+ /*
+ * WS2812 latches on the >50us reset gap after one frame. The 32KB buffer holds ~64
+ * repeating frame+reset cycles; at ~1.4ms/frame a 15ms run transmits ~10 complete
+ * cycles - more than enough to latch reliably. Keep it short so rapid sysfs writes
+ * (OpenRGB effects/theme changes) are not serialized behind a 1s blocking sleep.
+ */
+ usleep_range(15000, 20000);
+ snd_hdac_dsp_trigger(hstr, false);
+ codec_dbg(codec, "AE5 strip: trigger complete\n");
+
+ /* Teardown: clear 0x104 pin mux back to 0 */
+ writel(0x00000000, spec->mem_base + 0x104);
+
+ /* Restore descriptor control register */
+ if (hstr->sd_addr)
+ writeb(orig_sd_ctl, hstr->sd_addr);
+
+ snd_hdac_dsp_cleanup(hstr, &dmab);
+ snd_hda_power_down(codec);
+
+ return 0;
+}
+
+static int ae5_strip_ctl_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_BYTES;
+ uinfo->count = AE5_STRIP_MAX_LEDS * 3;
+ return 0;
+}
+
+static int ae5_strip_ctl_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct hda_codec *codec = snd_kcontrol_chip(kcontrol);
+ struct ca0132_spec *spec = codec->spec;
+ int i;
+
+ guard(mutex)(&spec->ae5_strip_mutex);
+ for (i = 0; i < spec->ae5_strip_cur_num_leds; i++) {
+ u32 grb = spec->ae5_strip_cur_colors[i];
+
+ ucontrol->value.bytes.data[i * 3 + 0] = (grb >> 8) & 0xff;
+ ucontrol->value.bytes.data[i * 3 + 1] = (grb >> 16) & 0xff;
+ ucontrol->value.bytes.data[i * 3 + 2] = grb & 0xff;
+ }
+ if (spec->ae5_strip_cur_num_leds < AE5_STRIP_MAX_LEDS)
+ memset(&ucontrol->value.bytes.data[spec->ae5_strip_cur_num_leds * 3], 0,
+ (AE5_STRIP_MAX_LEDS - spec->ae5_strip_cur_num_leds) * 3);
+ return 0;
+}
+
+static int ae5_strip_ctl_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct hda_codec *codec = snd_kcontrol_chip(kcontrol);
+ struct ca0132_spec *spec = codec->spec;
+ u32 colors[AE5_STRIP_MAX_LEDS];
+ int i, num_leds;
+
+ guard(mutex)(&spec->ae5_strip_mutex);
+ num_leds = spec->ae5_strip_cur_num_leds;
+ for (i = 0; i < num_leds; i++) {
+ u8 r = ucontrol->value.bytes.data[i * 3 + 0];
+ u8 g = ucontrol->value.bytes.data[i * 3 + 1];
+ u8 b = ucontrol->value.bytes.data[i * 3 + 2];
+
+ colors[i] = ((u32)g << 16) | ((u32)r << 8) | (u32)b;
+ }
+ memcpy(spec->ae5_strip_cur_colors, colors, sizeof(u32) * num_leds);
+ ae5_strip_send_frame(codec, colors, num_leds);
+ return 0;
+}
+
+static int ae5_strip_num_leds_info(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_info *uinfo)
+{
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
+ uinfo->count = 1;
+ uinfo->value.integer.min = 1;
+ uinfo->value.integer.max = AE5_STRIP_MAX_LEDS;
+ return 0;
+}
+
+static int ae5_strip_num_leds_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct hda_codec *codec = snd_kcontrol_chip(kcontrol);
+ struct ca0132_spec *spec = codec->spec;
+
+ guard(mutex)(&spec->ae5_strip_mutex);
+ ucontrol->value.integer.value[0] = spec->ae5_strip_cur_num_leds;
+ return 0;
+}
+
+static int ae5_strip_num_leds_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ struct hda_codec *codec = snd_kcontrol_chip(kcontrol);
+ struct ca0132_spec *spec = codec->spec;
+ int val = ucontrol->value.integer.value[0];
+
+ if (val < 1 || val > AE5_STRIP_MAX_LEDS)
+ return -EINVAL;
+
+ guard(mutex)(&spec->ae5_strip_mutex);
+ if (spec->ae5_strip_cur_num_leds == val)
+ return 0;
+ spec->ae5_strip_cur_num_leds = val;
+ ae5_strip_send_frame(codec, spec->ae5_strip_cur_colors, val);
+ return 0;
+}
+
+static const struct snd_kcontrol_new ae5_strip_ctls[] = {
+ {
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .name = "AE-5 LED Strip",
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = ae5_strip_ctl_info,
+ .get = ae5_strip_ctl_get,
+ .put = ae5_strip_ctl_put,
+ },
+ {
+ .iface = SNDRV_CTL_ELEM_IFACE_CARD,
+ .name = "AE-5 LED Strip Count",
+ .access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
+ SNDRV_CTL_ELEM_ACCESS_VOLATILE,
+ .info = ae5_strip_num_leds_info,
+ .get = ae5_strip_num_leds_get,
+ .put = ae5_strip_num_leds_put,
+ },
+};
+
+static int ae5_add_strip_controls(struct hda_codec *codec)
+{
+ int i, err;
+
+ for (i = 0; i < ARRAY_SIZE(ae5_strip_ctls); i++) {
+ err = snd_hda_ctl_add(codec, 0,
+ snd_ctl_new1(&ae5_strip_ctls[i], codec));
+ if (err < 0)
+ return err;
+ }
+ return 0;
+}
+
static void ae5_setup_defaults(struct hda_codec *codec)
{
struct ca0132_spec *spec = codec->spec;
@@ -8741,6 +9109,8 @@ static void ca0132_init_chip(struct hda_codec *codec)
unsigned int on;

mutex_init(&spec->chipio_mutex);
+ mutex_init(&spec->ae5_strip_mutex);
+ spec->ae5_strip_cur_num_leds = 10;

/*
* The Windows driver always does this upon startup, which seems to
@@ -9627,6 +9997,7 @@ static void ca0132_free(struct hda_codec *codec)
if (spec->mem_base)
pci_iounmap(codec->bus->pci, spec->mem_base);
#endif
+ mutex_destroy(&spec->ae5_strip_mutex);
kfree(spec->spec_init_verbs);
kfree(codec->spec);
codec->spec = NULL;
--
2.55.0