* [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support
@ 2026-09-28 17:26 dchristensen8
2026-09-29 13:10 ` Takashi Iwai
0 siblings, 1 reply; 5+ messages in thread
From: dchristensen8 @ 2026-09-28 17:26 UTC (permalink / raw)
To: Jaroslav Kysela, Takashi Iwai; +Cc: linux-sound, alsa-devel, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 2454 bytes --]
Hi Takashi, Jaroslav,
Please find attached a patch adding support for the external addressable RGB LED strip header on the Creative Sound BlasterX AE-5 and AE-5 Plus.
The patch is attached as a .patch file.
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 a sysfs interface on the codec device:
ae5_strip_leds: read current colors write a comma-deparated #RRGGBB or R,G,B list (a single color replicates across all active LEDs)
ae5_strip_num_leds: get or set the active LED count (1..100)
Writes allocate an idle HDA playback stream, format a 32 KB DMA buffer populated with repeating WS2812 frames (5-word zero preamble, 4 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 attributes are only registered on QUIRK_AE5 devices.
Document the new sysfs attributes in documentation/ABI/testing.
Signed-off-by: Devin Christensen <dchristensen8@proton.me>
Testing & Subsystem Context:
- Verified on physical Sound BlasterX AE-5 hardware.
- Tested continuous 60 FPS streaming and dynamic strip resizing (10 -> 7 -> 10 LEDs)
via the OpenRGB effects engine (MR: https://gitlab.com/CalcProgrammer1/OpenRGB/-/merge_requests/3658).
- Verified concurrent audio playback and LED updates without dropped samples or xruns.
- Built clean against tiwai/sound for-next (commit 72107104d4f5).
- Passes scripts/checkpatch.pl with 0 errors and 0 checks.
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-ALSA-hda-ca0132-Add-Sound-BlasterX-AE-5-external-RGB.patch --]
[-- Type: text/x-patch; name=0001-ALSA-hda-ca0132-Add-Sound-BlasterX-AE-5-external-RGB.patch, Size: 18185 bytes --]
From b7ccf89f34211856c6406d944949a0140681c3c0 Mon Sep 17 00:00:00 2001
From: Devin Christensen <dchristensen8@proton.me>
Date: Mon, 28 Sep 2026 12:04:40 -0400
Subject: [PATCH] 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 a sysfs interface on the codec device:
ae5_strip_leds read current colors; write a comma-separated
#RRGGBB or R,G,B list (a single color replicates
across all active LEDs)
ae5_strip_num_leds get or set the active LED count (1..100)
Writes allocate an idle HDA playback stream, format a 32 KB DMA buffer
populated with repeating WS2812 frames (5-word zero preamble, 4 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 attributes
are only registered on QUIRK_AE5 devices.
Document the new sysfs attributes in Documentation/ABI/testing.
Signed-off-by: Devin Christensen <dchristensen8@proton.me>
---
Testing & Subsystem Context:
- Verified on physical Sound BlasterX AE-5 hardware.
- Tested continuous 60 FPS streaming and dynamic strip resizing (10 -> 7 -> 10 LEDs)
via the OpenRGB effects engine (MR: https://gitlab.com/CalcProgrammer1/OpenRGB/-/merge_requests/3658).
- Verified concurrent audio playback and LED updates without dropped samples or xruns.
- Built clean against tiwai/sound for-next (commit 72107104d4f5).
.../ABI/testing/sysfs-devices-sound-ae5 | 30 ++
sound/hda/codecs/ca0132.c | 401 +++++++++++++++++-
2 files changed, 430 insertions(+), 1 deletion(-)
create mode 100644 Documentation/ABI/testing/sysfs-devices-sound-ae5
diff --git a/Documentation/ABI/testing/sysfs-devices-sound-ae5 b/Documentation/ABI/testing/sysfs-devices-sound-ae5
new file mode 100644
index 000000000..69f523a35
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-devices-sound-ae5
@@ -0,0 +1,30 @@
+What: /sys/bus/hdaudio/devices/<device>/ae5_strip_leds
+Date: September 2026
+KernelVersion: 7.3
+Contact: Devin Christensen <dchristensen8@proton.me>
+Description:
+ (RW) Sets or reads the RGB color configuration for the external
+ addressable RGB LED strip header on Creative Sound BlasterX
+ AE-5 and AE-5 Plus cards (QUIRK_AE5).
+
+ Writing accepts a comma-separated list of colors in either
+ hexadecimal "#RRGGBB" or decimal "R,G,B" format. If a single
+ color is provided, it is replicated across all currently active
+ LEDs. Supplying fewer colors than active LEDs updates the
+ provided range while preserving existing downstream colors.
+ Supplying more colors automatically expands the active strip
+ count up to a maximum of 100 LEDs.
+
+ Reading returns a comma-separated list of the current colors in
+ "#RRGGBB" format terminated by a newline.
+
+What: /sys/bus/hdaudio/devices/<device>/ae5_strip_num_leds
+Date: September 2026
+KernelVersion: 7.3
+Contact: Devin Christensen <dchristensen8@proton.me>
+Description:
+ (RW) Gets or sets the number of active addressable LEDs on the
+ external strip header (integer range: 1 to 100).
+
+ Writing updates the strip length and transmits the current color
+ buffer for the new count. Reading returns the current LED count.
diff --git a/sound/hda/codecs/ca0132.c b/sound/hda/codecs/ca0132.c
index 0f8fb62f0..464b48874 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;
};
/*
@@ -8316,8 +8325,386 @@ 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
*/
+static void ae5_strip_encode_24(u32 u, u32 *words)
+{
+ /* Bit-for-bit exact encoding matching CtxHda.sys FUN_00042944:
+ * 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 (bits 29, 25, 21, 17, 13, 9):
+ * out |= (bit << n) | ((bit | 2) << (n + 1))
+ * Bit 0 -> 0b010 (375ns high, 1042ns low)
+ * Bit 1 -> 0b111 (1083ns high, 333ns low)
+ */
+ int i8 = 23;
+ int i;
+
+ for (i = 0; i < 4; i++) {
+ u32 out = 0;
+ int bit_idx;
+
+ for (bit_idx = 0; bit_idx < 6; bit_idx++) {
+ int bit = (u >> (i8 - bit_idx)) & 1;
+ int n = 29 - bit_idx * 4;
+
+ out |= (u32)(bit << n) | ((u32)(bit | 2) << (n + 1));
+ }
+ 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;
+ unsigned long flags;
+
+ spin_lock_irqsave(&bus->reg_lock, flags);
+ 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;
+ }
+ spin_unlock_irqrestore(&bus->reg_lock, flags);
+
+ 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 total_words = 0x8000 / sizeof(u32); /* 8192 words (32KB) */
+ int word_idx = 0;
+ int frame_words;
+ int led, w;
+ 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;
+
+ 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, 0x8000, &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, 0x8000);
+ dst = (u32 *)dmab.area;
+
+ /* Words per frame: 5 words zero preamble + num_leds * 4 words + 83 words reset gap */
+ frame_words = 5 + (num_leds * 4) + 83;
+
+ while (word_idx + frame_words <= total_words) {
+ /* 5-word zero preamble per Windows CtxHda FUN_00042944 */
+ for (w = 0; w < 5; 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 < 4; w++) {
+ *dst++ = enc_words[w];
+ word_idx++;
+ }
+ }
+ /* 83 words (> 1.8 ms) zero reset gap (WS2812 requires > 50 us low) */
+ for (w = 0; w < 83; w++) {
+ *dst++ = 0;
+ word_idx++;
+ }
+ }
+ while (word_idx < 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);
+ writel(readl(spec->mem_base + 0x42c) & ~1, spec->mem_base + 0x42c);
+ writel(readl(spec->mem_base + 0x46c) & ~1, spec->mem_base + 0x46c);
+ writel(readl(spec->mem_base + 0x4ac) & ~1, spec->mem_base + 0x4ac);
+ writel(readl(spec->mem_base + 0x4ec) & ~1, spec->mem_base + 0x4ec);
+ writel(0, spec->mem_base + 0x43c);
+ writel(0, spec->mem_base + 0x47c);
+ writel(0, spec->mem_base + 0x4bc);
+ writel(0, spec->mem_base + 0x4fc);
+ 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) */
+ writel(readl(spec->mem_base + 0x43c) | 0x33, spec->mem_base + 0x43c);
+ writel(readl(spec->mem_base + 0x47c) | 0x33, spec->mem_base + 0x47c);
+ writel(readl(spec->mem_base + 0x4bc) | 0x33, spec->mem_base + 0x4bc);
+ writel(readl(spec->mem_base + 0x4fc) | 0x33, spec->mem_base + 0x4fc);
+
+ /* 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):
+ * Channel 0 is 0x454, Channel 1 is 0x494, Channel 2 is 0x4d4, Channel 3 is 0x514
+ */
+ writel(readl(spec->mem_base + 0x454) | 1, spec->mem_base + 0x454);
+ writel(readl(spec->mem_base + 0x494) | 1, spec->mem_base + 0x494);
+ writel(readl(spec->mem_base + 0x4d4) | 1, spec->mem_base + 0x4d4);
+ writel(readl(spec->mem_base + 0x514) | 1, spec->mem_base + 0x514);
+
+ /* 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 ssize_t ae5_strip_leds_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hdac_device *hdev = container_of(dev, struct hdac_device, dev);
+ struct hda_codec *codec = container_of(hdev, struct hda_codec, core);
+ struct ca0132_spec *spec = codec->spec;
+ int i, len = 0;
+
+ mutex_lock(&spec->ae5_strip_mutex);
+ for (i = 0; i < spec->ae5_strip_cur_num_leds && len < PAGE_SIZE - 16; i++) {
+ u32 grb = spec->ae5_strip_cur_colors[i];
+ u8 g = (grb >> 16) & 0xff;
+ u8 r = (grb >> 8) & 0xff;
+ u8 b = grb & 0xff;
+
+ len += scnprintf(buf + len, PAGE_SIZE - len, "%s#%02x%02x%02x",
+ i == 0 ? "" : ",", r, g, b);
+ }
+ mutex_unlock(&spec->ae5_strip_mutex);
+
+ len += scnprintf(buf + len, PAGE_SIZE - len, "\n");
+ return len;
+}
+
+static ssize_t ae5_strip_leds_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hdac_device *hdev = container_of(dev, struct hdac_device, dev);
+ struct hda_codec *codec = container_of(hdev, struct hda_codec, core);
+ struct ca0132_spec *spec = codec->spec;
+ u32 colors[AE5_STRIP_MAX_LEDS];
+ int num_leds = 0;
+ const char *p = buf;
+ int err;
+
+ codec_dbg(codec, "AE5 strip: leds_store received %zu bytes: '%.*s'\n",
+ count, (int)min_t(size_t, count, 64), buf);
+
+ while (*p && *p != '\n' && num_leds < AE5_STRIP_MAX_LEDS) {
+ unsigned int r = 0, g = 0, b = 0;
+ int n = 0;
+
+ while (*p == ' ' || *p == ',' || *p == '\t')
+ p++;
+ if (!*p || *p == '\n')
+ break;
+ if (*p == '#')
+ p++;
+
+ if (sscanf(p, "%2x%2x%2x%n", &r, &g, &b, &n) == 3 && n == 6) {
+ colors[num_leds++] = ((g & 0xff) << 16) | ((r & 0xff) << 8) | (b & 0xff);
+ p += n;
+ } else if (sscanf(p, "%u,%u,%u%n", &r, &g, &b, &n) == 3) {
+ colors[num_leds++] = ((min(g, 255U) & 0xff) << 16) |
+ ((min(r, 255U) & 0xff) << 8) |
+ (min(b, 255U) & 0xff);
+ p += n;
+ } else {
+ break;
+ }
+ }
+
+ if (num_leds == 0)
+ return -EINVAL;
+
+ mutex_lock(&spec->ae5_strip_mutex);
+
+ /* If 1 color is supplied, replicate it across all configured LEDs */
+ if (num_leds == 1) {
+ int i;
+
+ for (i = 1; i < spec->ae5_strip_cur_num_leds; i++)
+ colors[i] = colors[0];
+ num_leds = spec->ae5_strip_cur_num_leds;
+ } else if (num_leds < spec->ae5_strip_cur_num_leds) {
+ /* Partial update: keep existing colors for downstream LEDs and send full frame */
+ int i;
+
+ for (i = num_leds; i < spec->ae5_strip_cur_num_leds; i++)
+ colors[i] = spec->ae5_strip_cur_colors[i];
+ num_leds = spec->ae5_strip_cur_num_leds;
+ } else {
+ /* More colors supplied: expand strip count */
+ spec->ae5_strip_cur_num_leds = num_leds;
+ }
+
+ memcpy(spec->ae5_strip_cur_colors, colors, sizeof(u32) * num_leds);
+
+ err = ae5_strip_send_frame(codec, colors, num_leds);
+
+ mutex_unlock(&spec->ae5_strip_mutex);
+
+ if (err < 0)
+ return err;
+
+ return count;
+}
+static DEVICE_ATTR_RW(ae5_strip_leds);
+
+static ssize_t ae5_strip_num_leds_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hdac_device *hdev = container_of(dev, struct hdac_device, dev);
+ struct hda_codec *codec = container_of(hdev, struct hda_codec, core);
+ struct ca0132_spec *spec = codec->spec;
+ int val;
+
+ mutex_lock(&spec->ae5_strip_mutex);
+ val = spec->ae5_strip_cur_num_leds;
+ mutex_unlock(&spec->ae5_strip_mutex);
+
+ return scnprintf(buf, PAGE_SIZE, "%d\n", val);
+}
+
+static ssize_t ae5_strip_num_leds_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hdac_device *hdev = container_of(dev, struct hdac_device, dev);
+ struct hda_codec *codec = container_of(hdev, struct hda_codec, core);
+ struct ca0132_spec *spec = codec->spec;
+ unsigned int n;
+ int err;
+
+ if (kstrtouint(buf, 0, &n) || n == 0 || n > AE5_STRIP_MAX_LEDS)
+ return -EINVAL;
+
+ mutex_lock(&spec->ae5_strip_mutex);
+ spec->ae5_strip_cur_num_leds = n;
+ err = ae5_strip_send_frame(codec, spec->ae5_strip_cur_colors, n);
+ mutex_unlock(&spec->ae5_strip_mutex);
+
+ if (err < 0)
+ return err;
+
+ return count;
+}
+static DEVICE_ATTR_RW(ae5_strip_num_leds);
+
+static struct attribute *ae5_strip_attrs[] = {
+ &dev_attr_ae5_strip_leds.attr,
+ &dev_attr_ae5_strip_num_leds.attr,
+ NULL,
+};
+
+static const struct attribute_group ae5_strip_attr_group = {
+ .attrs = ae5_strip_attrs,
+};
+
static void ae5_setup_defaults(struct hda_codec *codec)
{
struct ca0132_spec *spec = codec->spec;
@@ -8741,6 +9128,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 +10016,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;
@@ -9956,6 +10346,9 @@ static void ca0132_codec_remove(struct hda_codec *codec)
{
struct ca0132_spec *spec = codec->spec;
+ if (ca0132_quirk(spec) == QUIRK_AE5)
+ sysfs_remove_group(&codec->core.dev.kobj, &ae5_strip_attr_group);
+
switch (ca0132_quirk(spec)) {
case QUIRK_GENERIC:
snd_hda_gen_remove(codec);
@@ -10085,6 +10478,12 @@ static int ca0132_codec_probe(struct hda_codec *codec,
ca0132_setup_unsol(codec);
+ if (ca0132_quirk(spec) == QUIRK_AE5) {
+ err = sysfs_create_group(&codec->core.dev.kobj, &ae5_strip_attr_group);
+ if (err)
+ codec_warn(codec, "AE5 strip: failed to create sysfs group: %d\n", err);
+ }
+
return 0;
error:
--
2.55.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support
2026-09-28 17:26 [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support dchristensen8
@ 2026-09-29 13:10 ` Takashi Iwai
2026-09-29 14:22 ` dchristensen8
0 siblings, 1 reply; 5+ messages in thread
From: Takashi Iwai @ 2026-09-29 13:10 UTC (permalink / raw)
To: dchristensen8
Cc: Jaroslav Kysela, Takashi Iwai, linux-sound, alsa-devel, linux-kernel
On Mon, 28 Sep 2026 19:26:05 +0200,
dchristensen8 wrote:
>
> Hi Takashi, Jaroslav,
>
> Please find attached a patch adding support for the external addressable RGB LED strip header on the Creative Sound BlasterX AE-5 and AE-5 Plus.
>
> The patch is attached as a .patch file.
>
> 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 a sysfs interface on the codec device:
> ae5_strip_leds: read current colors write a comma-deparated #RRGGBB or R,G,B list (a single color replicates across all active LEDs)
>
> ae5_strip_num_leds: get or set the active LED count (1..100)
>
> Writes allocate an idle HDA playback stream, format a 32 KB DMA buffer populated with repeating WS2812 frames (5-word zero preamble, 4 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 attributes are only registered on QUIRK_AE5 devices.
>
> Document the new sysfs attributes in documentation/ABI/testing.
>
> Signed-off-by: Devin Christensen <dchristensen8@proton.me>
> Testing & Subsystem Context:
> - Verified on physical Sound BlasterX AE-5 hardware.
> - Tested continuous 60 FPS streaming and dynamic strip resizing (10 -> 7 -> 10 LEDs)
> via the OpenRGB effects engine (MR: https://gitlab.com/CalcProgrammer1/OpenRGB/-/merge_requests/3658).
> - Verified concurrent audio playback and LED updates without dropped samples or xruns.
> - Built clean against tiwai/sound for-next (commit 72107104d4f5).
> - Passes scripts/checkpatch.pl with 0 errors and 0 checks.
Thanks for the patch.
It's an interesting stuff (rather surprising that they (ab)use
HD-audio for such an interface :)
Through a quick glance, most of the code looks OK.
So the question is rather the interface -- is the sysfs the best
choice for this kind of display controls? The implementation can be
refactored at any time, but the interface must be set solid, so that's
the most important question to be clarified.
About the code:
> /*
> - * Setup default parameters for the Sound BlasterX AE-5 DSP.
> + * Sound BlasterX AE-5 External WS2812B RGB LED Strip Transport
> */
> +static void ae5_strip_encode_24(u32 u, u32 *words)
> +{
> + /* Bit-for-bit exact encoding matching CtxHda.sys FUN_00042944:
> + * 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 (bits 29, 25, 21, 17, 13, 9):
> + * out |= (bit << n) | ((bit | 2) << (n + 1))
> + * Bit 0 -> 0b010 (375ns high, 1042ns low)
> + * Bit 1 -> 0b111 (1083ns high, 333ns low)
> + */
> + int i8 = 23;
> + int i;
> +
> + for (i = 0; i < 4; i++) {
> + u32 out = 0;
> + int bit_idx;
> +
> + for (bit_idx = 0; bit_idx < 6; bit_idx++) {
> + int bit = (u >> (i8 - bit_idx)) & 1;
> + int n = 29 - bit_idx * 4;
> +
> + out |= (u32)(bit << n) | ((u32)(bit | 2) << (n + 1));
> + }
> + words[i] = out;
> + i8 -= 6;
> + }
I guess some BIT() macro or such could help in a bit cleaner form.
> +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;
> + unsigned long flags;
> +
> + spin_lock_irqsave(&bus->reg_lock, flags);
> + 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;
> + }
> + spin_unlock_irqrestore(&bus->reg_lock, flags);
Try to use guard() instead of manual lock/unlock.
> +/*
> + * Caller must hold spec->ae5_strip_mutex.
> + */
You can put a lockdep assert to assure that in the actual code, too.
> +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 total_words = 0x8000 / sizeof(u32); /* 8192 words (32KB) */
It should be defined as a constant instead?
> + int word_idx = 0;
> + int frame_words;
> + int led, w;
> + 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;
> +
> + 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, 0x8000, &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, 0x8000);
> + dst = (u32 *)dmab.area;
> +
> + /* Words per frame: 5 words zero preamble + num_leds * 4 words + 83 words reset gap */
> + frame_words = 5 + (num_leds * 4) + 83;
> +
> + while (word_idx + frame_words <= total_words) {
> + /* 5-word zero preamble per Windows CtxHda FUN_00042944 */
> + for (w = 0; w < 5; 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 < 4; w++) {
> + *dst++ = enc_words[w];
> + word_idx++;
> + }
> + }
> + /* 83 words (> 1.8 ms) zero reset gap (WS2812 requires > 50 us low) */
> + for (w = 0; w < 83; w++) {
Here, too. Let's avoid magic numbers.
> + *dst++ = 0;
> + word_idx++;
> + }
> + }
> + while (word_idx < 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);
> + writel(readl(spec->mem_base + 0x42c) & ~1, spec->mem_base + 0x42c);
> + writel(readl(spec->mem_base + 0x46c) & ~1, spec->mem_base + 0x46c);
> + writel(readl(spec->mem_base + 0x4ac) & ~1, spec->mem_base + 0x4ac);
> + writel(readl(spec->mem_base + 0x4ec) & ~1, spec->mem_base + 0x4ec);
> + writel(0, spec->mem_base + 0x43c);
> + writel(0, spec->mem_base + 0x47c);
> + writel(0, spec->mem_base + 0x4bc);
> + writel(0, spec->mem_base + 0x4fc);
> + 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) */
> + writel(readl(spec->mem_base + 0x43c) | 0x33, spec->mem_base + 0x43c);
> + writel(readl(spec->mem_base + 0x47c) | 0x33, spec->mem_base + 0x47c);
> + writel(readl(spec->mem_base + 0x4bc) | 0x33, spec->mem_base + 0x4bc);
> + writel(readl(spec->mem_base + 0x4fc) | 0x33, spec->mem_base + 0x4fc);
Some helpers or macros look deserving.
I stop at this point, as we'd need to define the interface at first.
thanks,
Takashi
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support
2026-09-29 13:10 ` Takashi Iwai
@ 2026-09-29 14:22 ` dchristensen8
2026-09-30 13:45 ` Takashi Iwai
0 siblings, 1 reply; 5+ messages in thread
From: dchristensen8 @ 2026-09-29 14:22 UTC (permalink / raw)
To: Takashi Iwai
Cc: Jaroslav Kysela, Takashi Iwai, linux-sound, alsa-devel, linux-kernel
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.
Thanks,
Devin
On Tuesday, September 29th, 2026 at 9:10 AM, Takashi Iwai <tiwai@suse.de> wrote:
> On Mon, 28 Sep 2026 19:26:05 +0200,
> dchristensen8 wrote:
> >
> > Hi Takashi, Jaroslav,
> >
> > Please find attached a patch adding support for the external addressable RGB LED strip header on the Creative Sound BlasterX AE-5 and AE-5 Plus.
> >
> > The patch is attached as a .patch file.
> >
> > 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 a sysfs interface on the codec device:
> > ae5_strip_leds: read current colors write a comma-deparated #RRGGBB or R,G,B list (a single color replicates across all active LEDs)
> >
> > ae5_strip_num_leds: get or set the active LED count (1..100)
> >
> > Writes allocate an idle HDA playback stream, format a 32 KB DMA buffer populated with repeating WS2812 frames (5-word zero preamble, 4 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 attributes are only registered on QUIRK_AE5 devices.
> >
> > Document the new sysfs attributes in documentation/ABI/testing.
> >
> > Signed-off-by: Devin Christensen <dchristensen8@proton.me>
> > Testing & Subsystem Context:
> > - Verified on physical Sound BlasterX AE-5 hardware.
> > - Tested continuous 60 FPS streaming and dynamic strip resizing (10 -> 7 -> 10 LEDs)
> > via the OpenRGB effects engine (MR: https://gitlab.com/CalcProgrammer1/OpenRGB/-/merge_requests/3658).
> > - Verified concurrent audio playback and LED updates without dropped samples or xruns.
> > - Built clean against tiwai/sound for-next (commit 72107104d4f5).
> > - Passes scripts/checkpatch.pl with 0 errors and 0 checks.
>
> Thanks for the patch.
>
> It's an interesting stuff (rather surprising that they (ab)use
> HD-audio for such an interface :)
>
> Through a quick glance, most of the code looks OK.
>
> So the question is rather the interface -- is the sysfs the best
> choice for this kind of display controls? The implementation can be
> refactored at any time, but the interface must be set solid, so that's
> the most important question to be clarified.
>
> About the code:
>
> > /*
> > - * Setup default parameters for the Sound BlasterX AE-5 DSP.
> > + * Sound BlasterX AE-5 External WS2812B RGB LED Strip Transport
> > */
> > +static void ae5_strip_encode_24(u32 u, u32 *words)
> > +{
> > + /* Bit-for-bit exact encoding matching CtxHda.sys FUN_00042944:
> > + * 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 (bits 29, 25, 21, 17, 13, 9):
> > + * out |= (bit << n) | ((bit | 2) << (n + 1))
> > + * Bit 0 -> 0b010 (375ns high, 1042ns low)
> > + * Bit 1 -> 0b111 (1083ns high, 333ns low)
> > + */
> > + int i8 = 23;
> > + int i;
> > +
> > + for (i = 0; i < 4; i++) {
> > + u32 out = 0;
> > + int bit_idx;
> > +
> > + for (bit_idx = 0; bit_idx < 6; bit_idx++) {
> > + int bit = (u >> (i8 - bit_idx)) & 1;
> > + int n = 29 - bit_idx * 4;
> > +
> > + out |= (u32)(bit << n) | ((u32)(bit | 2) << (n + 1));
> > + }
> > + words[i] = out;
> > + i8 -= 6;
> > + }
>
> I guess some BIT() macro or such could help in a bit cleaner form.
>
>
> > +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;
> > + unsigned long flags;
> > +
> > + spin_lock_irqsave(&bus->reg_lock, flags);
> > + 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;
> > + }
> > + spin_unlock_irqrestore(&bus->reg_lock, flags);
>
> Try to use guard() instead of manual lock/unlock.
>
>
> > +/*
> > + * Caller must hold spec->ae5_strip_mutex.
> > + */
>
> You can put a lockdep assert to assure that in the actual code, too.
>
> > +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 total_words = 0x8000 / sizeof(u32); /* 8192 words (32KB) */
>
> It should be defined as a constant instead?
>
> > + int word_idx = 0;
> > + int frame_words;
> > + int led, w;
> > + 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;
> > +
> > + 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, 0x8000, &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, 0x8000);
> > + dst = (u32 *)dmab.area;
> > +
> > + /* Words per frame: 5 words zero preamble + num_leds * 4 words + 83 words reset gap */
> > + frame_words = 5 + (num_leds * 4) + 83;
> > +
> > + while (word_idx + frame_words <= total_words) {
> > + /* 5-word zero preamble per Windows CtxHda FUN_00042944 */
> > + for (w = 0; w < 5; 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 < 4; w++) {
> > + *dst++ = enc_words[w];
> > + word_idx++;
> > + }
> > + }
> > + /* 83 words (> 1.8 ms) zero reset gap (WS2812 requires > 50 us low) */
> > + for (w = 0; w < 83; w++) {
>
> Here, too. Let's avoid magic numbers.
>
> > + *dst++ = 0;
> > + word_idx++;
> > + }
> > + }
> > + while (word_idx < 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);
> > + writel(readl(spec->mem_base + 0x42c) & ~1, spec->mem_base + 0x42c);
> > + writel(readl(spec->mem_base + 0x46c) & ~1, spec->mem_base + 0x46c);
> > + writel(readl(spec->mem_base + 0x4ac) & ~1, spec->mem_base + 0x4ac);
> > + writel(readl(spec->mem_base + 0x4ec) & ~1, spec->mem_base + 0x4ec);
> > + writel(0, spec->mem_base + 0x43c);
> > + writel(0, spec->mem_base + 0x47c);
> > + writel(0, spec->mem_base + 0x4bc);
> > + writel(0, spec->mem_base + 0x4fc);
> > + 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) */
> > + writel(readl(spec->mem_base + 0x43c) | 0x33, spec->mem_base + 0x43c);
> > + writel(readl(spec->mem_base + 0x47c) | 0x33, spec->mem_base + 0x47c);
> > + writel(readl(spec->mem_base + 0x4bc) | 0x33, spec->mem_base + 0x4bc);
> > + writel(readl(spec->mem_base + 0x4fc) | 0x33, spec->mem_base + 0x4fc);
>
> Some helpers or macros look deserving.
>
> I stop at this point, as we'd need to define the interface at first.
>
>
> thanks,
>
> Takashi
>
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support
2026-09-29 14:22 ` dchristensen8
@ 2026-09-30 13:45 ` Takashi Iwai
2026-09-30 16:09 ` dchristensen8
0 siblings, 1 reply; 5+ messages in thread
From: Takashi Iwai @ 2026-09-30 13:45 UTC (permalink / raw)
To: dchristensen8
Cc: Jaroslav Kysela, Takashi Iwai, linux-sound, alsa-devel, linux-kernel
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
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support
2026-09-30 13:45 ` Takashi Iwai
@ 2026-09-30 16:09 ` dchristensen8
0 siblings, 0 replies; 5+ messages in thread
From: dchristensen8 @ 2026-09-30 16:09 UTC (permalink / raw)
To: Takashi Iwai
Cc: Jaroslav Kysela, Takashi Iwai, linux-sound, alsa-devel, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 1826 bytes --]
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@suse.de> 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
>
[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: v2-0001-ALSA-hda-ca0132-Add-Sound-BlasterX-AE-5-external-.patch --]
[-- Type: text/x-patch; name=v2-0001-ALSA-hda-ca0132-Add-Sound-BlasterX-AE-5-external-.patch, Size: 16037 bytes --]
From 69aec5e8836f8874e16663cd364ac5887ad85645 Mon Sep 17 00:00:00 2001
From: Devin Christensen <dchristensen8@proton.me>
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@proton.me>
---
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
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-30 16:10 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-28 17:26 [PATCH] ALSA: hda/ca0132: Add Sound BlasterX AE-5 external RGB LED strip support dchristensen8
2026-09-29 13:10 ` Takashi Iwai
2026-09-29 14:22 ` dchristensen8
2026-09-30 13:45 ` Takashi Iwai
2026-09-30 16:09 ` dchristensen8
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®