mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [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; 6+ 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] 6+ messages in thread

end of thread, other threads:[~2026-10-01 15:20 UTC | newest]

Thread overview: 6+ 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
2026-10-01 15:20         ` Takashi Iwai

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®