From: hoff.benjamin.k@gmail.com
To: mchehab@kernel.org, hverkuil+cisco@kernel.org
Cc: linux-kernel@vger.kernel.org, linux-media@vger.kernel.org
Subject: [PATCH v2 5/5] media: hws: add HDMI audio capture support
Date: Mon, 29 Jun 2026 12:03:04 -0400 [thread overview]
Message-ID: <20260629160304.154046-6-hoff.benjamin.k@gmail.com> (raw)
In-Reply-To: <20260629160304.154046-1-hoff.benjamin.k@gmail.com>
From: Ben Hoff <hoff.benjamin.k@gmail.com>
---
drivers/media/pci/hws/Kconfig | 3 +-
drivers/media/pci/hws/Makefile | 2 +-
drivers/media/pci/hws/hws.h | 52 ++
drivers/media/pci/hws/hws_audio.c | 1183 +++++++++++++++++++++++++++++
drivers/media/pci/hws/hws_audio.h | 23 +
drivers/media/pci/hws/hws_irq.c | 32 +
drivers/media/pci/hws/hws_pci.c | 105 ++-
drivers/media/pci/hws/hws_reg.h | 42 +-
drivers/media/pci/hws/hws_video.c | 3 +
9 files changed, 1422 insertions(+), 23 deletions(-)
create mode 100644 drivers/media/pci/hws/hws_audio.c
create mode 100644 drivers/media/pci/hws/hws_audio.h
diff --git a/drivers/media/pci/hws/Kconfig b/drivers/media/pci/hws/Kconfig
index ab0bbf49ca71..93a1b9188738 100644
--- a/drivers/media/pci/hws/Kconfig
+++ b/drivers/media/pci/hws/Kconfig
@@ -1,8 +1,9 @@
# SPDX-License-Identifier: GPL-2.0-only
config VIDEO_HWS
tristate "AVMatrix HWS PCIe capture devices"
- depends on PCI && VIDEO_DEV
+ depends on PCI && VIDEO_DEV && SND
select VIDEOBUF2_DMA_CONTIG
+ select SND_PCM
help
Enable support for AVMatrix HWS series multi-channel PCIe capture
devices that provide HDMI video capture.
diff --git a/drivers/media/pci/hws/Makefile b/drivers/media/pci/hws/Makefile
index f9c7dc4f2d8d..51aa2a3a0517 100644
--- a/drivers/media/pci/hws/Makefile
+++ b/drivers/media/pci/hws/Makefile
@@ -1,4 +1,4 @@
# SPDX-License-Identifier: GPL-2.0-only
-hws-objs := hws_pci.o hws_irq.o hws_video.o hws_v4l2_ioctl.o
+hws-objs := hws_pci.o hws_irq.o hws_video.o hws_v4l2_ioctl.o hws_audio.o
obj-$(CONFIG_VIDEO_HWS) += hws.o
diff --git a/drivers/media/pci/hws/hws.h b/drivers/media/pci/hws/hws.h
index 552f0663e5d8..47701e6b4e39 100644
--- a/drivers/media/pci/hws/hws.h
+++ b/drivers/media/pci/hws/hws.h
@@ -12,6 +12,10 @@
#include <linux/spinlock.h>
#include <linux/sizes.h>
#include <linux/atomic.h>
+#include <linux/workqueue.h>
+
+#include <sound/pcm.h>
+#include <sound/core.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-device.h>
@@ -20,6 +24,8 @@
#include "hws_reg.h"
+struct snd_pcm_substream;
+
struct hwsmem_param {
u32 index;
u32 type;
@@ -131,6 +137,46 @@ static inline void hws_set_current_dv_timings(struct hws_video *vid,
};
}
+struct hws_audio {
+ /* linkage */
+ struct hws_pcie_dev *parent;
+ int channel_index;
+
+ /* ALSA */
+ struct snd_pcm_substream *pcm_substream;
+ spinlock_t ring_lock; /* protects ring and period position fields */
+ snd_pcm_uframes_t ring_size_byframes;
+ snd_pcm_uframes_t ring_wpos_byframes;
+ snd_pcm_uframes_t period_size_byframes;
+ snd_pcm_uframes_t period_used_byframes;
+ size_t frame_bytes;
+ size_t hw_packet_bytes;
+
+ /* stream state */
+ bool cap_active;
+ bool stream_running;
+ bool stop_requested;
+ struct mutex scratch_state_lock; /* protects scratch_acquired */
+ bool scratch_acquired;
+
+ /* minimal HW packet tracking */
+ struct work_struct deliver_work;
+ spinlock_t pending_lock; /* protects packet_pending/toggle/irq timestamp */
+ bool packet_pending;
+ bool xrun_pending;
+ u8 pending_toggle;
+ u64 pending_irq_ns;
+ u8 last_period_toggle;
+ u32 irq_count;
+ u32 delivered_count;
+ u32 dropped_packets;
+
+ /* PCM format */
+ u32 output_sample_rate;
+ u16 channel_count;
+ u16 bits_per_sample;
+};
+
struct hws_scratch_dma {
void *cpu;
dma_addr_t dma;
@@ -141,10 +187,12 @@ struct hws_scratch_dma {
struct hws_pcie_dev {
/* Core objects */
struct pci_dev *pdev;
+ struct hws_audio audio[MAX_VID_CHANNELS];
struct hws_video video[MAX_VID_CHANNELS];
/* BAR and workqueues */
void __iomem *bar0_base;
+ struct workqueue_struct *audio_wq;
/* Device identity and capabilities */
u16 vendor_id;
@@ -158,14 +206,18 @@ struct hws_pcie_dev {
u32 max_hw_video_buf_sz;
u8 max_channels;
u8 cur_max_video_ch;
+ /* Independently capturable embedded audio inputs exposed as ALSA PCMs. */
u8 cur_max_audio_ch;
bool start_run;
bool buf_allocated;
+ u32 audio_pkt_size;
/* V4L2 framework objects */
struct v4l2_device v4l2_device;
+ struct snd_card *snd_card;
+
/* Kernel thread */
struct task_struct *main_task;
struct mutex scratch_lock; /* protects scratch DMA arenas and user refs */
diff --git a/drivers/media/pci/hws/hws_audio.c b/drivers/media/pci/hws/hws_audio.c
new file mode 100644
index 000000000000..674d6363a7ff
--- /dev/null
+++ b/drivers/media/pci/hws/hws_audio.c
@@ -0,0 +1,1183 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include "hws_audio.h"
+
+#include "hws.h"
+#include "hws_reg.h"
+
+#include <sound/core.h>
+#include <sound/pcm_params.h>
+#include <sound/control.h>
+#include <sound/pcm.h>
+#include <sound/rawmidi.h>
+#include <sound/initval.h>
+#include <linux/ktime.h>
+#include <linux/preempt.h>
+#include "hws_video.h"
+
+static inline void hws_audio_ack_pending(struct hws_pcie_dev *hws,
+ unsigned int ch);
+static void hws_audio_disable_capture_and_ack(struct hws_pcie_dev *hws,
+ unsigned int ch);
+static void hws_audio_clear_pending(struct hws_audio *a);
+static void hws_audio_deliver_work(struct work_struct *work);
+static void hws_audio_drain_channel_work(struct hws_audio *a);
+static void hws_audio_discard_stale_done(struct hws_pcie_dev *hws,
+ struct hws_audio *a,
+ unsigned int ch);
+
+static void hws_audio_reset_ring_state(struct hws_audio *a)
+{
+ unsigned long flags;
+
+ if (!a)
+ return;
+
+ spin_lock_irqsave(&a->ring_lock, flags);
+ a->ring_size_byframes = 0;
+ a->ring_wpos_byframes = 0;
+ a->period_size_byframes = 0;
+ a->period_used_byframes = 0;
+ a->frame_bytes = 0;
+ spin_unlock_irqrestore(&a->ring_lock, flags);
+}
+
+static void hws_audio_reset_counters(struct hws_audio *a)
+{
+ if (!a)
+ return;
+
+ WRITE_ONCE(a->last_period_toggle, 0xFF);
+ WRITE_ONCE(a->irq_count, 0);
+ WRITE_ONCE(a->delivered_count, 0);
+ WRITE_ONCE(a->dropped_packets, 0);
+}
+
+static void hws_audio_reset_runtime_state(struct hws_audio *a)
+{
+ if (!a)
+ return;
+
+ hws_audio_clear_pending(a);
+ hws_audio_reset_ring_state(a);
+ hws_audio_reset_counters(a);
+}
+
+static bool hws_audio_publish_stopped(struct hws_audio *a)
+{
+ unsigned long flags;
+ bool was_running;
+
+ if (!a)
+ return false;
+
+ spin_lock_irqsave(&a->ring_lock, flags);
+ was_running = READ_ONCE(a->stream_running) ||
+ READ_ONCE(a->cap_active);
+ WRITE_ONCE(a->stream_running, false);
+ WRITE_ONCE(a->cap_active, false);
+ WRITE_ONCE(a->stop_requested, true);
+ spin_unlock_irqrestore(&a->ring_lock, flags);
+ /*
+ * IRQ handlers test these flags before touching scratch buffers or
+ * ALSA pointers. Publish the no-stream state before ACAP is disabled
+ * and before any teardown clears pcm_substream.
+ */
+ smp_wmb();
+ return was_running;
+}
+
+static void hws_audio_quiesce_capture(struct hws_pcie_dev *hws,
+ unsigned int ch, bool sync_irq)
+{
+ struct hws_audio *a;
+
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return;
+
+ a = &hws->audio[ch];
+ hws_audio_publish_stopped(a);
+
+ hws_audio_disable_capture_and_ack(hws, ch);
+
+ if (sync_irq && hws->irq >= 0 && !in_interrupt())
+ synchronize_irq(hws->irq);
+
+ if (!in_interrupt())
+ hws_audio_drain_channel_work(a);
+
+ hws_audio_reset_runtime_state(a);
+}
+
+#define HWS_AUDIO_PACKET_BYTES MAX_DMA_AUDIO_PK_SIZE
+#define HWS_AUDIO_PERIODS_MIN 4U
+#define HWS_AUDIO_PERIODS_MAX 16U
+#define HWS_AUDIO_PERIOD_BYTES_MAX (HWS_AUDIO_PACKET_BYTES * 4U)
+#define HWS_AUDIO_BUFFER_BYTES_MAX (HWS_AUDIO_PACKET_BYTES * HWS_AUDIO_PERIODS_MAX)
+
+/*
+ * Audio DMA completes in fixed-size packets. The driver copies whole packets
+ * into ALSA's ring, so expose packet-sized period and buffer granularity.
+ */
+static const struct snd_pcm_hardware audio_pcm_hardware = {
+ .info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
+ SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_RESUME |
+ SNDRV_PCM_INFO_MMAP_VALID),
+ .formats = SNDRV_PCM_FMTBIT_S16_LE,
+ .rates = SNDRV_PCM_RATE_48000,
+ .rate_min = 48000,
+ .rate_max = 48000,
+ .channels_min = 2,
+ .channels_max = 2,
+ .buffer_bytes_max = HWS_AUDIO_BUFFER_BYTES_MAX,
+ .period_bytes_min = HWS_AUDIO_PACKET_BYTES,
+ .period_bytes_max = HWS_AUDIO_PERIOD_BYTES_MAX,
+ .periods_min = HWS_AUDIO_PERIODS_MIN,
+ .periods_max = HWS_AUDIO_PERIODS_MAX,
+};
+
+static bool hws_audio_select_buffer(struct hws_pcie_dev *hws, unsigned int ch,
+ void **cpu_base, dma_addr_t *dma_base,
+ size_t *size)
+{
+ struct hws_scratch_dma *scratch;
+
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return false;
+
+ scratch = &hws->scratch_aud[ch];
+ if (!scratch->cpu || !scratch->size)
+ return false;
+
+ if (cpu_base)
+ *cpu_base = scratch->cpu;
+ if (dma_base)
+ *dma_base = scratch->dma;
+ if (size)
+ *size = scratch->size;
+ return true;
+}
+
+static int hws_guard_audio_video_remap_page(struct hws_pcie_dev *hws,
+ unsigned int ch)
+{
+ struct hws_video *vid;
+ dma_addr_t audio_dma;
+ u32 audio_hi, audio_page;
+
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return -EINVAL;
+ if (ch >= hws->cur_max_video_ch)
+ return 0;
+
+ vid = &hws->video[ch];
+ if (!READ_ONCE(vid->cap_active) || !vid->window_valid)
+ return 0;
+
+ if (!hws_audio_select_buffer(hws, ch, NULL, &audio_dma, NULL))
+ return -ENOMEM;
+
+ audio_hi = upper_32_bits(audio_dma);
+ audio_page = lower_32_bits(audio_dma) & PCI_E_BAR_ADD_MASK;
+ if (audio_hi == vid->last_dma_hi && audio_page == vid->last_dma_page)
+ return 0;
+
+ dev_warn_ratelimited(&hws->pdev->dev,
+ "audio ch%u DMA page differs from active video remap slot; refusing shared-window conflict (audio=%pad video_hi=0x%08x video_page=0x%08x)\n",
+ ch, &audio_dma, vid->last_dma_hi,
+ vid->last_dma_page);
+ return -EBUSY;
+}
+
+static void hws_audio_program_remap_slot(struct hws_pcie_dev *hws,
+ u32 table_off, u32 hi, u32 page_lo)
+{
+ writel_relaxed(hi, hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off);
+ writel_relaxed(page_lo, hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off +
+ PCIE_BARADDROFSIZE);
+}
+
+static int hws_audio_seed_capture_buffer(struct hws_pcie_dev *hws, unsigned int ch)
+{
+ dma_addr_t dma;
+ u32 lo, hi, pci_addr;
+ u32 audio_table_off;
+
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return -EINVAL;
+
+ if (!hws_audio_select_buffer(hws, ch, NULL, &dma, NULL))
+ return -ENOMEM;
+
+ lo = lower_32_bits(dma);
+ hi = upper_32_bits(dma);
+ pci_addr = lo & PCI_E_BAR_ADD_LOWMASK;
+ lo &= PCI_E_BAR_ADD_MASK;
+ audio_table_off = HWS_AUDIO_REMAP_SLOT_OFF(ch);
+ hws_audio_program_remap_slot(hws, audio_table_off, hi, lo);
+ writel_relaxed((ch + 1u) * PCIEBAR_AXI_BASE + pci_addr,
+ hws->bar0_base + HWS_REG_AUD_DMA_ADDR(ch));
+ (void)readl(hws->bar0_base + HWS_REG_AUD_DMA_ADDR(ch));
+ return 0;
+}
+
+void hws_audio_seed_channels(struct hws_pcie_dev *hws)
+{
+ unsigned int ch;
+
+ if (!hws || !hws->bar0_base)
+ return;
+
+ for (ch = 0; ch < hws->cur_max_audio_ch; ch++) {
+ int ret;
+
+ if (!hws->scratch_aud[ch].cpu)
+ continue;
+
+ ret = hws_audio_seed_capture_buffer(hws, ch);
+ if (ret)
+ dev_warn(&hws->pdev->dev,
+ "audio seed ch%u failed ret=%d\n", ch, ret);
+ }
+}
+
+static size_t hws_audio_packet_offset(const struct hws_audio *a, u8 cur_toggle)
+{
+ size_t packet = a->hw_packet_bytes;
+
+ /*
+ * ABUF_TOGGLE reports the half the device is filling now, so the
+ * completed packet is the other half.
+ */
+ return cur_toggle ? 0 : packet;
+}
+
+static void hws_audio_clear_pending(struct hws_audio *a)
+{
+ unsigned long flags;
+
+ if (!a)
+ return;
+
+ spin_lock_irqsave(&a->pending_lock, flags);
+ a->packet_pending = false;
+ a->xrun_pending = false;
+ a->pending_irq_ns = 0;
+ spin_unlock_irqrestore(&a->pending_lock, flags);
+}
+
+static void hws_audio_report_xrun(struct hws_audio *a)
+{
+ struct hws_pcie_dev *hws;
+ struct snd_pcm_substream *ss;
+ unsigned long flags;
+ unsigned int ch;
+
+ if (!a)
+ return;
+
+ hws = a->parent;
+ ch = a->channel_index;
+ ss = READ_ONCE(a->pcm_substream);
+
+ hws_audio_publish_stopped(a);
+ hws_audio_disable_capture_and_ack(hws, ch);
+ hws_audio_clear_pending(a);
+
+ if (!ss)
+ return;
+
+ snd_pcm_stream_lock_irqsave(ss, flags);
+ if (ss->runtime && READ_ONCE(a->pcm_substream) == ss)
+ snd_pcm_stop(ss, SNDRV_PCM_STATE_XRUN);
+ snd_pcm_stream_unlock_irqrestore(ss, flags);
+}
+
+static void hws_audio_drain_channel_work(struct hws_audio *a)
+{
+ if (!a)
+ return;
+
+ if (!in_interrupt())
+ cancel_work_sync(&a->deliver_work);
+ hws_audio_clear_pending(a);
+}
+
+static int hws_audio_acquire_scratch(struct hws_audio *a)
+{
+ struct hws_pcie_dev *hws;
+ unsigned int ch;
+ int ret;
+
+ if (!a || !a->parent)
+ return -EINVAL;
+
+ mutex_lock(&a->scratch_state_lock);
+ if (READ_ONCE(a->scratch_acquired)) {
+ mutex_unlock(&a->scratch_state_lock);
+ return 0;
+ }
+
+ hws = a->parent;
+ ch = a->channel_index;
+ ret = hws_alloc_channel_scratch(hws, ch);
+ if (ret) {
+ mutex_unlock(&a->scratch_state_lock);
+ return ret;
+ }
+
+ WRITE_ONCE(a->scratch_acquired, true);
+ mutex_unlock(&a->scratch_state_lock);
+ return 0;
+}
+
+static void hws_audio_release_scratch(struct hws_audio *a)
+{
+ struct hws_pcie_dev *hws;
+ unsigned int ch;
+
+ if (!a)
+ return;
+
+ mutex_lock(&a->scratch_state_lock);
+ if (!a->scratch_acquired) {
+ mutex_unlock(&a->scratch_state_lock);
+ return;
+ }
+
+ WRITE_ONCE(a->scratch_acquired, false);
+ hws = a->parent;
+ ch = a->channel_index;
+ mutex_unlock(&a->scratch_state_lock);
+
+ if (hws)
+ hws_release_channel_scratch(hws, ch);
+}
+
+static bool hws_audio_deliver_packet(struct hws_audio *a, const void *src)
+{
+ struct snd_pcm_substream *ss;
+ struct snd_pcm_runtime *rt;
+ snd_pcm_uframes_t frames, ring_pos, ring_frames, period_frames;
+ size_t frame_bytes, packet_bytes, ring_bytes, first;
+ unsigned long flags;
+ unsigned int elapsed = 0;
+ bool delivered = false;
+ char *dst;
+
+ if (!READ_ONCE(a->stream_running) || !READ_ONCE(a->cap_active) ||
+ READ_ONCE(a->stop_requested))
+ return false;
+
+ ss = READ_ONCE(a->pcm_substream);
+ if (!ss)
+ return false;
+
+ rt = ss->runtime;
+ if (!rt || !rt->dma_area)
+ return false;
+
+ spin_lock_irqsave(&a->ring_lock, flags);
+ if (!READ_ONCE(a->stream_running) || !READ_ONCE(a->cap_active) ||
+ READ_ONCE(a->stop_requested) ||
+ READ_ONCE(a->pcm_substream) != ss) {
+ spin_unlock_irqrestore(&a->ring_lock, flags);
+ return false;
+ }
+
+ frame_bytes = a->frame_bytes;
+ packet_bytes = a->hw_packet_bytes;
+ ring_frames = a->ring_size_byframes;
+ period_frames = a->period_size_byframes;
+ if (!frame_bytes || !packet_bytes || !ring_frames || !period_frames)
+ goto out_unlock;
+ if (packet_bytes % frame_bytes)
+ goto out_unlock;
+
+ frames = packet_bytes / frame_bytes;
+ if (!frames)
+ goto out_unlock;
+
+ ring_pos = a->ring_wpos_byframes;
+ ring_bytes = ring_frames * frame_bytes;
+ dst = rt->dma_area + ring_pos * frame_bytes;
+ first = min(packet_bytes, ring_bytes - ring_pos * frame_bytes);
+ memcpy(dst, src, first);
+ if (first < packet_bytes)
+ memcpy(rt->dma_area, (const char *)src + first, packet_bytes - first);
+ delivered = true;
+
+ ring_pos += frames;
+ if (ring_pos >= ring_frames)
+ ring_pos %= ring_frames;
+ a->ring_wpos_byframes = ring_pos;
+
+ a->period_used_byframes += frames;
+ while (a->period_used_byframes >= period_frames) {
+ a->period_used_byframes -= period_frames;
+ elapsed++;
+ }
+out_unlock:
+ spin_unlock_irqrestore(&a->ring_lock, flags);
+
+ if (!READ_ONCE(a->stream_running) || !READ_ONCE(a->cap_active) ||
+ READ_ONCE(a->stop_requested))
+ return delivered;
+
+ while (elapsed--)
+ snd_pcm_period_elapsed(ss);
+ return delivered;
+}
+
+static bool hws_audio_packet_stale(struct hws_audio *a, u64 irq_ns)
+{
+ u64 packet_ns;
+ size_t frame_bytes;
+ u32 rate;
+ u64 frames;
+
+ if (!a || !irq_ns)
+ return false;
+
+ frame_bytes = READ_ONCE(a->frame_bytes);
+ rate = READ_ONCE(a->output_sample_rate);
+ if (!frame_bytes || !rate || a->hw_packet_bytes % frame_bytes)
+ return false;
+
+ frames = a->hw_packet_bytes / frame_bytes;
+ if (!frames)
+ return false;
+
+ packet_ns = div_u64(frames * NSEC_PER_SEC, rate);
+ return ktime_get_mono_fast_ns() - irq_ns >= packet_ns;
+}
+
+static void hws_audio_deliver_one_packet(struct hws_audio *a, u8 cur_toggle)
+{
+ struct hws_pcie_dev *hws;
+ unsigned int ch;
+ void *cpu;
+ size_t size;
+ size_t offset;
+
+ if (!a)
+ return;
+
+ hws = a->parent;
+ ch = a->channel_index;
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return;
+
+ if (!READ_ONCE(a->stream_running) || !READ_ONCE(a->cap_active) ||
+ READ_ONCE(a->stop_requested))
+ return;
+
+ if (!hws_audio_select_buffer(hws, ch, &cpu, NULL, &size))
+ return;
+
+ offset = hws_audio_packet_offset(a, cur_toggle);
+ if (offset + a->hw_packet_bytes > size)
+ return;
+
+ dma_rmb();
+ if (hws_audio_deliver_packet(a, (char *)cpu + offset))
+ WRITE_ONCE(a->delivered_count,
+ READ_ONCE(a->delivered_count) + 1);
+}
+
+static void hws_audio_deliver_work(struct work_struct *work)
+{
+ struct hws_audio *a = container_of(work, struct hws_audio, deliver_work);
+ unsigned long flags;
+ u64 irq_ns;
+ u8 toggle;
+
+ for (;;) {
+ spin_lock_irqsave(&a->pending_lock, flags);
+ if (a->xrun_pending) {
+ a->xrun_pending = false;
+ a->packet_pending = false;
+ a->pending_irq_ns = 0;
+ spin_unlock_irqrestore(&a->pending_lock, flags);
+ hws_audio_report_xrun(a);
+ break;
+ }
+ if (!a->packet_pending) {
+ spin_unlock_irqrestore(&a->pending_lock, flags);
+ break;
+ }
+ toggle = a->pending_toggle;
+ irq_ns = a->pending_irq_ns;
+ a->packet_pending = false;
+ a->pending_irq_ns = 0;
+ spin_unlock_irqrestore(&a->pending_lock, flags);
+
+ if (hws_audio_packet_stale(a, irq_ns)) {
+ WRITE_ONCE(a->dropped_packets,
+ READ_ONCE(a->dropped_packets) + 1);
+ hws_audio_report_xrun(a);
+ break;
+ }
+
+ hws_audio_deliver_one_packet(a, toggle);
+ }
+}
+
+void hws_audio_queue_interrupt(struct hws_pcie_dev *hws, unsigned int ch, u8 cur_toggle)
+{
+ struct workqueue_struct *wq;
+ struct hws_audio *a;
+
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return;
+
+ a = &hws->audio[ch];
+ if (!READ_ONCE(a->stream_running) || !READ_ONCE(a->cap_active) ||
+ READ_ONCE(a->stop_requested))
+ return;
+
+ wq = READ_ONCE(hws->audio_wq);
+ if (!wq) {
+ WRITE_ONCE(a->dropped_packets,
+ READ_ONCE(a->dropped_packets) + 1);
+ return;
+ }
+
+ WRITE_ONCE(a->last_period_toggle, cur_toggle);
+ spin_lock(&a->pending_lock);
+ if (a->packet_pending) {
+ WRITE_ONCE(a->dropped_packets,
+ READ_ONCE(a->dropped_packets) + 1);
+ a->xrun_pending = true;
+ }
+ a->pending_toggle = cur_toggle;
+ a->pending_irq_ns = ktime_get_mono_fast_ns();
+ a->packet_pending = true;
+ WRITE_ONCE(a->irq_count, READ_ONCE(a->irq_count) + 1);
+ spin_unlock(&a->pending_lock);
+
+ queue_work(wq, &a->deliver_work);
+}
+
+int hws_audio_init_channel(struct hws_pcie_dev *pdev, int ch)
+{
+ struct hws_audio *aud;
+
+ if (!pdev || ch < 0 || ch >= pdev->max_channels)
+ return -EINVAL;
+
+ aud = &pdev->audio[ch];
+ memset(aud, 0, sizeof(*aud)); /* ok: no embedded locks yet */
+
+ /* identity */
+ aud->parent = pdev;
+ aud->channel_index = ch;
+ spin_lock_init(&aud->ring_lock);
+ spin_lock_init(&aud->pending_lock);
+ mutex_init(&aud->scratch_state_lock);
+ INIT_WORK(&aud->deliver_work, hws_audio_deliver_work);
+
+ /* defaults */
+ aud->output_sample_rate = 48000;
+ aud->channel_count = 2;
+ aud->bits_per_sample = 16;
+ aud->hw_packet_bytes = pdev->audio_pkt_size;
+
+ /* ALSA linkage */
+ WRITE_ONCE(aud->pcm_substream, NULL);
+
+ /* stream state */
+ WRITE_ONCE(aud->cap_active, false);
+ WRITE_ONCE(aud->stream_running, false);
+ WRITE_ONCE(aud->stop_requested, false);
+ WRITE_ONCE(aud->scratch_acquired, false);
+
+ hws_audio_reset_counters(aud);
+
+ return 0;
+}
+
+void hws_audio_cleanup_channel(struct hws_pcie_dev *pdev, int ch, bool device_removal)
+{
+ struct hws_audio *aud;
+ struct snd_pcm_substream *ss;
+
+ if (!pdev || ch < 0 || ch >= pdev->cur_max_audio_ch)
+ return;
+
+ aud = &pdev->audio[ch];
+ hws_audio_quiesce_capture(pdev, ch, true);
+
+ /* If device is going away and stream was open, tell ALSA. */
+ ss = READ_ONCE(aud->pcm_substream);
+ if (device_removal && ss) {
+ unsigned long flags;
+
+ snd_pcm_stream_lock_irqsave(ss, flags);
+ if (ss->runtime)
+ snd_pcm_stop(ss, SNDRV_PCM_STATE_DISCONNECTED);
+ snd_pcm_stream_unlock_irqrestore(ss, flags);
+ WRITE_ONCE(aud->pcm_substream, NULL);
+ }
+
+ hws_audio_release_scratch(aud);
+}
+
+static inline bool hws_check_audio_capture(struct hws_pcie_dev *hws, unsigned int ch)
+{
+ u32 reg = readl(hws->bar0_base + HWS_REG_ACAP_ENABLE);
+
+ return !!(reg & BIT(ch));
+}
+
+static int hws_audio_hw_ready(struct hws_pcie_dev *hws)
+{
+ u32 status;
+
+ if (!hws || !hws->bar0_base)
+ return -ENODEV;
+
+ status = readl(hws->bar0_base + HWS_REG_SYS_STATUS);
+ if (status == 0xFFFFFFFF) {
+ hws->pci_lost = true;
+ dev_err(&hws->pdev->dev, "PCIe device not responding\n");
+ return -ENODEV;
+ }
+
+ if (!(status & BIT(0))) {
+ dev_warn_ratelimited(&hws->pdev->dev,
+ "audio start refused while device is not ready (SYS_STATUS=0x%08x)\n",
+ status);
+ return -EIO;
+ }
+
+ return 0;
+}
+
+static int hws_start_audio_capture(struct hws_pcie_dev *hws, unsigned int ch)
+{
+ struct hws_audio *a;
+ int ret;
+
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return -EINVAL;
+ a = &hws->audio[ch];
+
+ /* Already running? Re-assert HW if needed. */
+ if (READ_ONCE(a->stream_running)) {
+ if (!hws_check_audio_capture(hws, ch)) {
+ ret = hws_audio_hw_ready(hws);
+ if (ret)
+ return ret;
+ ret = hws_guard_audio_video_remap_page(hws, ch);
+ if (ret)
+ return ret;
+ ret = hws_audio_seed_capture_buffer(hws, ch);
+ if (ret)
+ return ret;
+ WRITE_ONCE(a->cap_active, false);
+ smp_wmb(); /* publish inactive before stale ADONE ack */
+ hws_audio_discard_stale_done(hws, a, ch);
+ WRITE_ONCE(a->cap_active, true);
+ smp_wmb(); /* publish active before ACAP_ENABLE */
+ hws_enable_audio_capture(hws, ch, true);
+ }
+ dev_dbg(&hws->pdev->dev, "audio ch%u already running (re-enabled)\n", ch);
+ return 0;
+ }
+
+ if (!READ_ONCE(a->scratch_acquired))
+ return -ENOMEM;
+
+ ret = hws_audio_hw_ready(hws);
+ if (ret)
+ return ret;
+
+ ret = hws_guard_audio_video_remap_page(hws, ch);
+ if (ret)
+ return ret;
+
+ ret = hws_audio_seed_capture_buffer(hws, ch);
+ if (ret)
+ return ret;
+
+ hws_audio_discard_stale_done(hws, a, ch);
+ hws_audio_reset_counters(a);
+
+ /*
+ * ADONE can fire as soon as capture is enabled. Discard any completion
+ * latched before this start, then publish the stream state before
+ * ACAP_ENABLE so the IRQ path accepts the first fresh packet.
+ */
+ WRITE_ONCE(a->stop_requested, false);
+ WRITE_ONCE(a->stream_running, true);
+ WRITE_ONCE(a->cap_active, true);
+ smp_wmb(); /* publish start state before ACAP_ENABLE */
+
+ /* Kick HW */
+ hws_enable_audio_capture(hws, ch, true);
+ return 0;
+}
+
+static inline void hws_audio_ack_pending(struct hws_pcie_dev *hws, unsigned int ch)
+{
+ u32 abit = HWS_INT_ADONE_BIT(ch);
+ u32 st;
+
+ if (!hws || !hws->bar0_base || ch >= hws->cur_max_audio_ch)
+ return;
+
+ st = readl(hws->bar0_base + HWS_REG_INT_STATUS);
+
+ if (st & abit) {
+ writel(abit, hws->bar0_base + HWS_REG_INT_ACK);
+ /* flush posted write */
+ readl(hws->bar0_base + HWS_REG_INT_STATUS);
+ }
+}
+
+static void hws_audio_discard_stale_done(struct hws_pcie_dev *hws,
+ struct hws_audio *a,
+ unsigned int ch)
+{
+ hws_audio_clear_pending(a);
+ hws_audio_ack_pending(hws, ch);
+}
+
+static void hws_audio_disable_capture_and_ack(struct hws_pcie_dev *hws,
+ unsigned int ch)
+{
+ if (!hws || !hws->bar0_base || ch >= hws->cur_max_audio_ch)
+ return;
+
+ hws_enable_audio_capture(hws, ch, false);
+ readl(hws->bar0_base + HWS_REG_INT_STATUS);
+ hws_audio_ack_pending(hws, ch);
+}
+
+static inline void hws_audio_ack_all(struct hws_pcie_dev *hws)
+{
+ u32 mask = 0;
+
+ if (!hws || !hws->bar0_base)
+ return;
+
+ for (unsigned int ch = 0; ch < hws->cur_max_audio_ch; ch++)
+ mask |= HWS_INT_ADONE_BIT(ch);
+ if (mask) {
+ writel(mask, hws->bar0_base + HWS_REG_INT_ACK);
+ readl(hws->bar0_base + HWS_REG_INT_STATUS);
+ }
+}
+
+static void hws_stop_audio_capture(struct hws_pcie_dev *hws, unsigned int ch)
+{
+ struct hws_audio *a;
+
+ if (!hws || ch >= hws->cur_max_audio_ch)
+ return;
+
+ a = &hws->audio[ch];
+ if (!READ_ONCE(a->stream_running) && !READ_ONCE(a->cap_active))
+ return;
+
+ hws_audio_publish_stopped(a);
+ hws_audio_disable_capture_and_ack(hws, ch);
+ hws_audio_clear_pending(a);
+ dev_dbg(&hws->pdev->dev, "audio capture stopped on ch %u\n", ch);
+}
+
+void hws_enable_audio_capture(struct hws_pcie_dev *hws,
+ unsigned int ch, bool enable)
+{
+ u32 reg, mask = BIT(ch);
+
+ if (!hws || ch >= hws->cur_max_audio_ch || hws->pci_lost)
+ return;
+
+ reg = readl(hws->bar0_base + HWS_REG_ACAP_ENABLE);
+ if (enable)
+ reg |= mask;
+ else
+ reg &= ~mask;
+
+ writel(reg, hws->bar0_base + HWS_REG_ACAP_ENABLE);
+
+ dev_dbg(&hws->pdev->dev, "audio capture %s ch%u, reg=0x%08x\n",
+ enable ? "enabled" : "disabled", ch, reg);
+}
+
+static snd_pcm_uframes_t hws_pcie_audio_pointer(struct snd_pcm_substream *substream)
+{
+ struct hws_audio *a = snd_pcm_substream_chip(substream);
+ snd_pcm_uframes_t pos;
+ unsigned long flags;
+
+ spin_lock_irqsave(&a->ring_lock, flags);
+ pos = a->ring_wpos_byframes;
+ spin_unlock_irqrestore(&a->ring_lock, flags);
+ return pos;
+}
+
+static int hws_pcie_audio_open(struct snd_pcm_substream *substream)
+{
+ struct hws_audio *a = snd_pcm_substream_chip(substream);
+ struct snd_pcm_runtime *rt = substream->runtime;
+ int ret;
+
+ rt->hw = audio_pcm_hardware;
+
+ ret = snd_pcm_hw_constraint_integer(rt, SNDRV_PCM_HW_PARAM_PERIODS);
+ if (ret < 0)
+ return ret;
+ ret = snd_pcm_hw_constraint_step(rt, 0, SNDRV_PCM_HW_PARAM_PERIOD_BYTES,
+ HWS_AUDIO_PACKET_BYTES);
+ if (ret < 0)
+ return ret;
+ ret = snd_pcm_hw_constraint_step(rt, 0, SNDRV_PCM_HW_PARAM_BUFFER_BYTES,
+ HWS_AUDIO_PACKET_BYTES);
+ if (ret < 0)
+ return ret;
+
+ WRITE_ONCE(a->pcm_substream, substream);
+ return 0;
+}
+
+static int hws_pcie_audio_close(struct snd_pcm_substream *substream)
+{
+ struct hws_audio *a = snd_pcm_substream_chip(substream);
+
+ hws_stop_audio_capture(a->parent, a->channel_index);
+ hws_audio_drain_channel_work(a);
+ hws_audio_reset_runtime_state(a);
+ hws_audio_release_scratch(a);
+ WRITE_ONCE(a->pcm_substream, NULL);
+ return 0;
+}
+
+static int hws_pcie_audio_hw_params(struct snd_pcm_substream *substream,
+ struct snd_pcm_hw_params *hw_params)
+{
+ struct hws_audio *a = snd_pcm_substream_chip(substream);
+ struct hws_pcie_dev *hws = a->parent;
+ int pages_changed;
+ int ret;
+
+ if (!hws)
+ return -ENODEV;
+
+ ret = hws_check_card_status(hws);
+ if (ret)
+ return ret;
+
+ pages_changed = snd_pcm_lib_malloc_pages(substream,
+ params_buffer_bytes(hw_params));
+ if (pages_changed < 0)
+ return pages_changed;
+
+ ret = hws_audio_acquire_scratch(a);
+ if (ret) {
+ snd_pcm_lib_free_pages(substream);
+ return ret;
+ }
+
+ ret = hws_guard_audio_video_remap_page(hws, a->channel_index);
+ if (ret) {
+ hws_audio_release_scratch(a);
+ snd_pcm_lib_free_pages(substream);
+ return ret;
+ }
+
+ return pages_changed;
+}
+
+static int hws_pcie_audio_hw_free(struct snd_pcm_substream *substream)
+{
+ struct hws_audio *a = snd_pcm_substream_chip(substream);
+ int ret;
+
+ hws_stop_audio_capture(a->parent, a->channel_index);
+ hws_audio_drain_channel_work(a);
+ hws_audio_reset_runtime_state(a);
+ hws_audio_release_scratch(a);
+ ret = snd_pcm_lib_free_pages(substream);
+ return ret;
+}
+
+static int hws_pcie_audio_prepare(struct snd_pcm_substream *substream)
+{
+ struct hws_audio *a = snd_pcm_substream_chip(substream);
+ struct snd_pcm_runtime *rt = substream->runtime;
+ unsigned long flags;
+ size_t frame_bytes;
+
+ frame_bytes = snd_pcm_format_physical_width(rt->format) / 8;
+ frame_bytes *= rt->channels;
+ if (!frame_bytes || a->hw_packet_bytes % frame_bytes)
+ return -EINVAL;
+
+ spin_lock_irqsave(&a->ring_lock, flags);
+ a->ring_size_byframes = rt->buffer_size;
+ a->ring_wpos_byframes = 0;
+ a->period_size_byframes = rt->period_size;
+ a->period_used_byframes = 0;
+ a->frame_bytes = frame_bytes;
+ spin_unlock_irqrestore(&a->ring_lock, flags);
+
+ hws_audio_reset_counters(a);
+ hws_audio_clear_pending(a);
+ return 0;
+}
+
+static int hws_pcie_audio_trigger(struct snd_pcm_substream *substream, int cmd)
+{
+ struct hws_audio *a = snd_pcm_substream_chip(substream);
+ struct hws_pcie_dev *hws = a->parent;
+ unsigned int ch = a->channel_index;
+
+ dev_dbg(&hws->pdev->dev, "audio trigger %d on ch %u\n", cmd, ch);
+
+ switch (cmd) {
+ case SNDRV_PCM_TRIGGER_START:
+ return hws_start_audio_capture(hws, ch);
+ case SNDRV_PCM_TRIGGER_STOP:
+ hws_stop_audio_capture(hws, ch);
+ return 0;
+ case SNDRV_PCM_TRIGGER_RESUME:
+ case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
+ return hws_start_audio_capture(hws, ch);
+ case SNDRV_PCM_TRIGGER_SUSPEND:
+ case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
+ hws_stop_audio_capture(hws, ch);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct snd_pcm_ops hws_pcie_pcm_ops = {
+ .open = hws_pcie_audio_open,
+ .close = hws_pcie_audio_close,
+ .ioctl = snd_pcm_lib_ioctl,
+ .hw_params = hws_pcie_audio_hw_params,
+ .hw_free = hws_pcie_audio_hw_free,
+ .prepare = hws_pcie_audio_prepare,
+ .trigger = hws_pcie_audio_trigger,
+ .pointer = hws_pcie_audio_pointer,
+};
+
+int hws_audio_register(struct hws_pcie_dev *hws)
+{
+ struct snd_card *card = NULL;
+ struct snd_pcm *pcm = NULL;
+ char card_id[16];
+ char card_name[64];
+ int i, ret;
+
+ if (!hws)
+ return -EINVAL;
+ if (!hws->cur_max_audio_ch)
+ return 0;
+
+ /* ---- Create a single ALSA card for this PCI function ---- */
+ snprintf(card_id, sizeof(card_id), "hws%u", hws->port_id); /* <=16 chars */
+ snprintf(card_name, sizeof(card_name), "HWS HDMI Audio %u", hws->port_id);
+
+ ret = snd_card_new(&hws->pdev->dev, -1 /* auto index */,
+ card_id, THIS_MODULE, 0, &card);
+ if (ret < 0) {
+ dev_err(&hws->pdev->dev, "snd_card_new failed: %d\n", ret);
+ return ret;
+ }
+
+ snd_card_set_dev(card, &hws->pdev->dev);
+ strscpy(card->driver, KBUILD_MODNAME, sizeof(card->driver));
+ strscpy(card->shortname, card_name, sizeof(card->shortname));
+ strscpy(card->longname, card->shortname, sizeof(card->longname));
+
+ /* ---- Create one PCM capture device per HDMI input ---- */
+ for (i = 0; i < hws->cur_max_audio_ch; i++) {
+ char pcm_name[32];
+
+ snprintf(pcm_name, sizeof(pcm_name), "HDMI In %d", i);
+
+ /* device number = i, so userspace sees hw:X,i */
+ ret = snd_pcm_new(card, pcm_name, i,
+ 0 /* playback */, 1 /* capture */, &pcm);
+ if (ret < 0) {
+ dev_err(&hws->pdev->dev, "snd_pcm_new(%d) failed: %d\n", i, ret);
+ goto error_card;
+ }
+
+ pcm->private_data = &hws->audio[i];
+ strscpy(pcm->name, pcm_name, sizeof(pcm->name));
+ snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &hws_pcie_pcm_ops);
+
+ /*
+ * snd_pcm_lib_malloc_pages() requires a valid DMA buffer type.
+ * Keep allocation dynamic at HW_PARAMS time, but advertise the
+ * maximum buffer size up front for modern ALSA.
+ */
+ ret = snd_pcm_set_managed_buffer_all(pcm,
+ SNDRV_DMA_TYPE_DEV,
+ &hws->pdev->dev,
+ 0,
+ audio_pcm_hardware.buffer_bytes_max);
+ if (ret < 0) {
+ dev_err(&hws->pdev->dev,
+ "snd_pcm_set_managed_buffer_all(%d) failed: %d\n",
+ i, ret);
+ goto error_card;
+ }
+ }
+
+ /* Register the card once all PCMs are created */
+ ret = snd_card_register(card);
+ if (ret < 0) {
+ dev_err(&hws->pdev->dev, "snd_card_register failed: %d\n", ret);
+ goto error_card;
+ }
+
+ /* Store the single card handle (optional: also mirror to each channel if you like) */
+ hws->snd_card = card;
+ dev_info(&hws->pdev->dev, "audio registration complete (%d HDMI inputs)\n",
+ hws->cur_max_audio_ch);
+ return 0;
+
+error_card:
+ /* Frees all PCMs created on it as well */
+ snd_card_free(card);
+ return ret;
+}
+
+void hws_audio_unregister(struct hws_pcie_dev *hws)
+{
+ if (!hws)
+ return;
+
+ /* Prevent new opens and mark existing streams disconnected */
+ if (hws->snd_card)
+ snd_card_disconnect(hws->snd_card);
+
+ for (unsigned int i = 0; i < hws->cur_max_audio_ch; i++) {
+ struct hws_audio *a = &hws->audio[i];
+
+ hws_audio_publish_stopped(a);
+ hws_enable_audio_capture(hws, i, false);
+ }
+
+ /* Flush ACAP disables before waiting for any running IRQ handler. */
+ if (hws->bar0_base)
+ readl(hws->bar0_base + HWS_REG_INT_STATUS);
+ if (hws->irq >= 0 && !in_interrupt())
+ synchronize_irq(hws->irq);
+
+ hws_audio_drain_work(hws);
+ hws_audio_ack_all(hws);
+
+ for (unsigned int i = 0; i < hws->cur_max_audio_ch; i++) {
+ struct hws_audio *a = &hws->audio[i];
+ struct snd_pcm_substream *ss = READ_ONCE(a->pcm_substream);
+
+ if (ss) {
+ unsigned long flags;
+
+ snd_pcm_stream_lock_irqsave(ss, flags);
+ if (ss->runtime)
+ snd_pcm_stop(ss, SNDRV_PCM_STATE_DISCONNECTED);
+ snd_pcm_stream_unlock_irqrestore(ss, flags);
+ }
+
+ WRITE_ONCE(a->pcm_substream, NULL);
+ hws_audio_reset_runtime_state(a);
+ hws_audio_release_scratch(a);
+ }
+
+ if (hws->snd_card) {
+ snd_card_free_when_closed(hws->snd_card);
+ hws->snd_card = NULL;
+ }
+
+ dev_info(&hws->pdev->dev, "audio unregistered (%u channels)\n",
+ hws->cur_max_audio_ch);
+}
+
+int hws_audio_pm_suspend_all(struct hws_pcie_dev *hws)
+{
+ struct snd_pcm *seen[ARRAY_SIZE(hws->audio)];
+ int seen_cnt = 0;
+ int i, j, ret = 0;
+
+ if (!hws || !hws->snd_card)
+ return 0;
+
+ /* Iterate audio channels and suspend each unique PCM device */
+ for (i = 0; i < hws->cur_max_audio_ch && i < ARRAY_SIZE(hws->audio); i++) {
+ struct hws_audio *a = &hws->audio[i];
+ struct snd_pcm_substream *ss = READ_ONCE(a->pcm_substream);
+ struct snd_pcm *pcm;
+ bool already = false;
+
+ if (!ss)
+ continue;
+
+ pcm = ss->pcm;
+ if (!pcm)
+ continue;
+
+ /* De-duplicate in case multiple channels share a PCM */
+ for (j = 0; j < seen_cnt; j++) {
+ if (seen[j] == pcm) {
+ already = true;
+ break;
+ }
+ }
+ if (already)
+ continue;
+
+ if (seen_cnt < ARRAY_SIZE(seen))
+ seen[seen_cnt++] = pcm;
+
+ if (!ret) {
+ int r = snd_pcm_suspend_all(pcm);
+
+ if (r)
+ ret = r; /* remember first error, keep going */
+ }
+
+ if (seen_cnt == ARRAY_SIZE(seen))
+ break; /* defensive: shouldn't happen with sane config */
+ }
+
+ return ret;
+}
+
+void hws_audio_pm_resume(struct hws_pcie_dev *hws)
+{
+ unsigned int ch;
+
+ if (!hws || !hws->bar0_base)
+ return;
+
+ for (ch = 0; ch < hws->cur_max_audio_ch && ch < MAX_VID_CHANNELS; ch++) {
+ struct hws_audio *a = &hws->audio[ch];
+
+ WRITE_ONCE(a->stream_running, false);
+ WRITE_ONCE(a->cap_active, false);
+ WRITE_ONCE(a->stop_requested, true);
+ hws_audio_reset_counters(a);
+ hws_audio_clear_pending(a);
+ }
+ hws_audio_seed_channels(hws);
+ hws_audio_ack_all(hws);
+}
+
+void hws_audio_drain_work(struct hws_pcie_dev *hws)
+{
+ unsigned int ch;
+
+ if (!hws)
+ return;
+
+ for (ch = 0; ch < hws->cur_max_audio_ch && ch < MAX_VID_CHANNELS; ch++)
+ hws_audio_drain_channel_work(&hws->audio[ch]);
+}
diff --git a/drivers/media/pci/hws/hws_audio.h b/drivers/media/pci/hws/hws_audio.h
new file mode 100644
index 000000000000..749ab2ebbf35
--- /dev/null
+++ b/drivers/media/pci/hws/hws_audio.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef HWS_AUDIO_PIPELINE_H
+#define HWS_AUDIO_PIPELINE_H
+
+#include <sound/pcm.h>
+#include "hws.h"
+
+int hws_audio_register(struct hws_pcie_dev *dev);
+void hws_audio_unregister(struct hws_pcie_dev *hws);
+void hws_audio_seed_channels(struct hws_pcie_dev *hws);
+void hws_audio_queue_interrupt(struct hws_pcie_dev *hws, unsigned int ch,
+ u8 cur_toggle);
+void hws_audio_drain_work(struct hws_pcie_dev *hws);
+void hws_enable_audio_capture(struct hws_pcie_dev *hws,
+ unsigned int ch,
+ bool enable);
+
+int hws_audio_init_channel(struct hws_pcie_dev *pdev, int ch);
+void hws_audio_cleanup_channel(struct hws_pcie_dev *pdev, int ch, bool device_removal);
+int hws_audio_pm_suspend_all(struct hws_pcie_dev *hws);
+void hws_audio_pm_resume(struct hws_pcie_dev *hws);
+
+#endif /* HWS_AUDIO_PIPELINE_H */
diff --git a/drivers/media/pci/hws/hws_irq.c b/drivers/media/pci/hws/hws_irq.c
index e18f018b15c4..c28d78884788 100644
--- a/drivers/media/pci/hws/hws_irq.c
+++ b/drivers/media/pci/hws/hws_irq.c
@@ -11,6 +11,7 @@
#include "hws_reg.h"
#include "hws_video.h"
#include "hws.h"
+#include "hws_audio.h"
#define MAX_INT_LOOPS 100
@@ -314,6 +315,36 @@ static bool hws_irq_queue_video(struct hws_pcie_dev *pdx, u32 int_state)
return wake_thread;
}
+static void hws_irq_handle_audio(struct hws_pcie_dev *pdx, u32 int_state)
+{
+ unsigned int ch;
+
+ for (ch = 0; ch < pdx->cur_max_audio_ch; ++ch) {
+ u32 abit = HWS_INT_ADONE_BIT(ch);
+ u8 cur_toggle;
+
+ if (!(int_state & abit))
+ continue;
+
+ /* Only service running streams */
+ if (!READ_ONCE(pdx->audio[ch].cap_active) ||
+ !READ_ONCE(pdx->audio[ch].stream_running) ||
+ READ_ONCE(pdx->audio[ch].stop_requested))
+ continue;
+
+ /*
+ * Baseline read ABUF_TOGGLE for every ADONE interrupt.
+ * The register reports the half the device is filling now, so
+ * the completed packet is the opposite half. Read it in the
+ * hard handler so the deferred audio work receives the edge's
+ * toggle value, not a later one.
+ */
+ cur_toggle = readl_relaxed(pdx->bar0_base +
+ HWS_REG_ABUF_TOGGLE(ch)) & 0x01;
+ hws_audio_queue_interrupt(pdx, ch, cur_toggle);
+ }
+}
+
irqreturn_t hws_irq_handler(int irq, void *info)
{
struct hws_pcie_dev *pdx = info;
@@ -351,6 +382,7 @@ irqreturn_t hws_irq_handler(int irq, void *info)
dev_dbg(&pdx->pdev->dev, "irq: entry INT_STATUS=0x%08x\n", int_state);
wake_thread = hws_irq_queue_video(pdx, int_state);
+ hws_irq_handle_audio(pdx, int_state);
hws_irq_ack_status(pdx, int_state);
return wake_thread ? IRQ_WAKE_THREAD : IRQ_HANDLED;
diff --git a/drivers/media/pci/hws/hws_pci.c b/drivers/media/pci/hws/hws_pci.c
index b042bbfae350..f83d6d494d25 100644
--- a/drivers/media/pci/hws/hws_pci.c
+++ b/drivers/media/pci/hws/hws_pci.c
@@ -19,6 +19,7 @@
#include <media/v4l2-ctrls.h>
#include "hws.h"
+#include "hws_audio.h"
#include "hws_reg.h"
#include "hws_video.h"
#include "hws_irq.h"
@@ -154,6 +155,7 @@ static void hws_configure_hardware_capabilities(struct hws_pcie_dev *hdev)
}
static void hws_stop_device(struct hws_pcie_dev *hws);
+static void hws_free_seed_buffers(struct hws_pcie_dev *hws);
static void hws_log_lifecycle_snapshot(struct hws_pcie_dev *hws,
const char *action,
@@ -187,6 +189,17 @@ static void hws_log_lifecycle_snapshot(struct hws_pcie_dev *hws,
sys_status, dec_mode);
}
+static void hws_init_probe_state(struct hws_pcie_dev *hdev)
+{
+ hdev->max_hw_video_buf_sz = MAX_MM_VIDEO_SIZE;
+ hdev->max_channels = 4;
+ hdev->buf_allocated = false;
+ hdev->main_task = NULL;
+ hdev->audio_pkt_size = MAX_DMA_AUDIO_PK_SIZE;
+ hdev->start_run = false;
+ hdev->pci_lost = 0;
+}
+
static int read_chip_id(struct hws_pcie_dev *hdev)
{
u32 reg;
@@ -201,13 +214,6 @@ static int read_chip_id(struct hws_pcie_dev *hdev)
hdev->support_yv12 = FIELD_GET(DEVINFO_YV12, reg);
hdev->port_id = FIELD_GET(DEVINFO_PORTID, reg);
- hdev->max_hw_video_buf_sz = MAX_MM_VIDEO_SIZE;
- hdev->max_channels = 4;
- hdev->buf_allocated = false;
- hdev->main_task = NULL;
- hdev->start_run = false;
- hdev->pci_lost = 0;
-
writel(0x00, hdev->bar0_base + HWS_REG_DEC_MODE);
writel(0x10, hdev->bar0_base + HWS_REG_DEC_MODE);
@@ -271,6 +277,21 @@ static void hws_stop_kthread_action(void *data)
}
}
+static void hws_destroy_audio_workqueue(struct hws_pcie_dev *hws)
+{
+ struct workqueue_struct *wq;
+
+ if (!hws)
+ return;
+
+ wq = hws->audio_wq;
+ if (!wq)
+ return;
+
+ hws->audio_wq = NULL;
+ destroy_workqueue(wq);
+}
+
static size_t hws_video_scratch_bytes(void)
{
return HWS_VIDEO_BOUNCE_SLOTS * ALIGN((size_t)MAX_VIDEO_SCALER_SIZE, 64);
@@ -516,6 +537,7 @@ static int hws_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
int i, ret, irq;
unsigned long irqf = 0;
bool v4l2_registered = false;
+ bool audio_registered = false;
/* devres-backed device object */
hws = devm_kzalloc(&pdev->dev, sizeof(*hws), GFP_KERNEL);
@@ -561,18 +583,38 @@ static int hws_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
#endif
/* 4) Identify chip & capabilities */
+ hws_init_probe_state(hws);
read_chip_id(hws);
dev_info(&pdev->dev, "Device VID=0x%04x DID=0x%04x\n",
pdev->vendor, pdev->device);
hws_init_video_sys(hws, false);
- /* 5) Init channels (video state, locks, vb2, ctrls) */
+ /* 5) Init channels (video/audio state, locks, vb2, ctrls) */
for (i = 0; i < hws->max_channels; i++) {
ret = hws_video_init_channel(hws, i);
if (ret) {
dev_err(&pdev->dev, "video channel init failed (ch=%d)\n", i);
goto err_unwind_channels;
}
+ ret = hws_audio_init_channel(hws, i);
+ if (ret) {
+ dev_err(&pdev->dev, "audio channel init failed (ch=%d)\n", i);
+ hws_video_cleanup_channel(hws, i);
+ goto err_unwind_channels;
+ }
+ }
+
+ if (hws->cur_max_audio_ch) {
+ hws->audio_wq = alloc_workqueue("hws-audio",
+ WQ_HIGHPRI | WQ_UNBOUND | WQ_MEM_RECLAIM,
+ 0);
+ if (!hws->audio_wq) {
+ ret = -ENOMEM;
+ dev_err(&pdev->dev, "audio workqueue allocation failed\n");
+ goto err_unwind_channels;
+ }
+ } else {
+ dev_info(&pdev->dev, "audio capture disabled; video-only mode\n");
}
/* 6) Start-run sequence. Scratch DMA is allocated on stream start. */
@@ -616,13 +658,20 @@ static int hws_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
dev_info(&pdev->dev, "INT_EN_GATE readback=0x%08x\n",
readl(hws->bar0_base + INT_EN_REG_BASE));
- /* 11) Register V4L2 */
+ /* 11) Register V4L2/ALSA */
ret = hws_video_register(hws);
if (ret) {
dev_err(&pdev->dev, "video_register: %d\n", ret);
goto err_unwind_channels;
}
v4l2_registered = true;
+ ret = hws_audio_register(hws);
+ if (ret) {
+ dev_err(&pdev->dev, "audio_register: %d\n", ret);
+ hws_video_unregister(hws);
+ goto err_unwind_channels;
+ }
+ audio_registered = true;
/* 12) Background monitor thread (managed) */
hws->main_task = kthread_run(main_ks_thread_handle, hws, "hws-mon");
@@ -644,15 +693,21 @@ static int hws_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
err_unregister_va:
hws_stop_device(hws);
- hws_video_unregister(hws);
+ if (audio_registered)
+ hws_audio_unregister(hws);
+ if (v4l2_registered)
+ hws_video_unregister(hws);
hws_free_seed_buffers(hws);
+ hws_destroy_audio_workqueue(hws);
return ret;
err_unwind_channels:
hws_free_seed_buffers(hws);
- if (!v4l2_registered) {
- while (--i >= 0)
+ while (--i >= 0) {
+ if (!v4l2_registered)
hws_video_cleanup_channel(hws, i);
+ hws_audio_cleanup_channel(hws, i, true);
}
+ hws_destroy_audio_workqueue(hws);
return ret;
}
@@ -696,7 +751,7 @@ static void hws_stop_dsp(struct hws_pcie_dev *hws)
writel(0x0, hws->bar0_base + HWS_REG_VCAP_ENABLE);
}
-/* Publish stop so ISR/BH will not touch video buffers anymore. */
+/* Publish stop so ISR/BH will not touch ALSA/VB2 anymore. */
static void hws_publish_stop_flags(struct hws_pcie_dev *hws)
{
unsigned int i;
@@ -708,6 +763,14 @@ static void hws_publish_stop_flags(struct hws_pcie_dev *hws)
WRITE_ONCE(v->stop_requested, true);
}
+ for (i = 0; i < hws->cur_max_audio_ch; ++i) {
+ struct hws_audio *a = &hws->audio[i];
+
+ WRITE_ONCE(a->stream_running, false);
+ WRITE_ONCE(a->cap_active, false);
+ WRITE_ONCE(a->stop_requested, true);
+ }
+
smp_wmb(); /* make flags visible before we touch MMIO/queues */
}
@@ -720,14 +783,17 @@ static void hws_drain_after_stop(struct hws_pcie_dev *hws)
/* Mask device enables: no new DMA starts. */
writel(0x0, hws->bar0_base + HWS_REG_VCAP_ENABLE);
+ writel(0x0, hws->bar0_base + HWS_REG_ACAP_ENABLE);
(void)readl(hws->bar0_base + HWS_REG_INT_STATUS); /* flush */
/* Let any in-flight DMAs finish (best-effort). */
(void)hws_check_busy(hws);
- /* Ack any latched VDONE. */
+ /* Ack any latched VDONE/ADONE. */
for (i = 0; i < hws->cur_max_video_ch; ++i)
ackmask |= HWS_INT_VDONE_BIT(i);
+ for (i = 0; i < hws->cur_max_audio_ch; ++i)
+ ackmask |= HWS_INT_ADONE_BIT(i);
if (ackmask) {
writel(ackmask, hws->bar0_base + HWS_REG_INT_STATUS);
(void)readl(hws->bar0_base + HWS_REG_INT_STATUS);
@@ -736,6 +802,7 @@ static void hws_drain_after_stop(struct hws_pcie_dev *hws)
/* Ensure no hard IRQ is still running. */
if (hws->irq >= 0)
synchronize_irq(hws->irq);
+ hws_audio_drain_work(hws);
dev_dbg(&hws->pdev->dev, "lifecycle:drain-after-stop:done (%lluus)\n",
hws_elapsed_us(start_ns));
@@ -834,8 +901,10 @@ static void hws_remove(struct pci_dev *pdev)
/* Stop hardware and capture cleanly. */
hws_stop_device(hws);
- /* Unregister V4L2 resources. */
+ /* Unregister ALSA resources before V4L2. */
+ hws_audio_unregister(hws);
hws_video_unregister(hws);
+ hws_destroy_audio_workqueue(hws);
/* Release seeded DMA buffers */
hws_free_seed_buffers(hws);
@@ -850,11 +919,16 @@ static int hws_pm_suspend(struct device *dev)
{
struct pci_dev *pdev = to_pci_dev(dev);
struct hws_pcie_dev *hws = pci_get_drvdata(pdev);
+ int aret;
int vret;
u64 start_ns = ktime_get_mono_fast_ns();
u64 step_ns;
dev_info(dev, "lifecycle:pm_suspend begin\n");
+ aret = hws_audio_pm_suspend_all(hws);
+ if (aret)
+ dev_warn(dev, "lifecycle:pm_suspend audio quiesce returned %d\n",
+ aret);
vret = hws_quiesce_for_transition(hws, "pm_suspend", false);
step_ns = ktime_get_mono_fast_ns();
@@ -903,6 +977,7 @@ static int hws_pm_resume(struct device *dev)
/* Re-seed BAR remaps/DMA windows and restart the capture core */
hws_seed_all_channels(hws);
hws_init_video_sys(hws, true);
+ hws_audio_pm_resume(hws);
hws_irq_clear_pending(hws);
dev_dbg(dev, "lifecycle:pm_resume:chip-reinit (%lluus)\n",
hws_elapsed_us(step_ns));
diff --git a/drivers/media/pci/hws/hws_reg.h b/drivers/media/pci/hws/hws_reg.h
index c8d6715fe0c2..ebf95e31993e 100644
--- a/drivers/media/pci/hws/hws_reg.h
+++ b/drivers/media/pci/hws/hws_reg.h
@@ -33,9 +33,10 @@
#define PCI_E_BAR_ADD_MASK 0xE0000000
#define PCI_E_BAR_ADD_LOWMASK 0x1FFFFFFF
+#define MAX_DMA_AUDIO_PK_SIZE (128U * 16U * 2U)
/*
* The legacy driver reserved a 10 KiB hardware capture window per audio
- * channel even though the delivered packet size is smaller. Keep that headroom
+ * channel even though the delivered packet size is 4 KiB. Keep that headroom
* for the split-buffer DMA engine.
*/
#define MAX_AUDIO_CAP_SIZE (10U * 1024U)
@@ -88,6 +89,7 @@
/* Capture enable switches. */
/* bit0-3: CH0-CH3 video enable */
#define HWS_REG_VCAP_ENABLE (CVBS_IN_BASE + 2 * PCIE_BARADDROFSIZE)
+#define HWS_REG_ACAP_ENABLE (CVBS_IN_BASE + 3 * PCIE_BARADDROFSIZE)
/* bits0-3: signal present, bits8-11: interlace */
#define HWS_REG_ACTIVE_STATUS (CVBS_IN_BASE + 5 * PCIE_BARADDROFSIZE)
/* bits0-3: HDCP detected */
@@ -95,12 +97,28 @@
#define HWS_REG_DMA_MAX_SIZE (CVBS_IN_BASE + 9 * PCIE_BARADDROFSIZE)
/*
- * Video DMA setup uses one BAR remap-table slot per capture channel. The
- * remap-table slot supplies the host DMA page, while CVBS_IN_BUF_BASE +
- * ch * 4 supplies the device-side buffer offset within that page.
+ * Buffer base registers follow the vendor/baseline layout:
+ *
+ * video base: CVBS_IN_BUF_BASE + ch * 4
+ * audio base: CVBS_IN_BUF_BASE + (8 + ch) * 4
+ *
+ * Do not add a video doorbell at CVBS_IN_BASE + (26 + ch) * 4. Those
+ * offsets alias the audio base bank for low video channel numbers.
*/
+/* Per-channel audio DMA address window. */
+#define HWS_REG_AUD_DMA_ADDR(ch) (CVBS_IN_BUF_BASE + ((8 + (ch)) * PCIE_BARADDROFSIZE))
+
#define HWS_VIDEO_REMAP_SLOT_OFF(ch) (0x208 + ((ch) * 8))
+/*
+ * BAR remap slots are selected by the high bits of the programmed device-side
+ * base address. Both video and audio program (ch + 1) * PCIEBAR_AXI_BASE, so
+ * audio shares the same remap slot as video for that channel. The audio base
+ * registers live at CVBS_IN_BUF_BASE + (8 + ch) * 4, but that is a register
+ * bank offset, not a second remap-table bank.
+ */
+#define HWS_AUDIO_REMAP_SLOT_OFF(ch) HWS_VIDEO_REMAP_SLOT_OFF(ch)
+
/* Per-channel live buffer toggles (read-only). */
#define HWS_REG_VBUF_TOGGLE(ch) (CVBS_IN_BASE + (32 + (ch)) * PCIE_BARADDROFSIZE)
/*
@@ -108,10 +126,18 @@
* currently filling for channel *ch* (0-3).
*/
-/* Per-interrupt bits (video 0-3). */
+#define HWS_REG_ABUF_TOGGLE(ch) (CVBS_IN_BASE + (40 + (ch)) * PCIE_BARADDROFSIZE)
+/*
+ * Returns 0 or 1 = which half of the audio ring the DMA engine is
+ * currently filling for channel *ch* (0-3).
+ */
+
+/* Per-interrupt bits (video 0-3, audio 0-3). */
#define HWS_INT_VDONE_BIT(ch) BIT(ch) /* 0x01,0x02,0x04,0x08 */
+#define HWS_INT_ADONE_BIT(ch) BIT(8 + (ch)) /* 0x100 .. 0x800 */
-#define HWS_REG_INT_ACK (CVBS_IN_BASE + 0x4000 + 1 * PCIE_BARADDROFSIZE)
+/* Legacy hardware clears interrupt bits by W1C on INT_STATUS. */
+#define HWS_REG_INT_ACK HWS_REG_INT_STATUS
/* 16-bit W | 16-bit H. */
#define HWS_REG_IN_RES(ch) (CVBS_IN_BASE + (90 + (ch) * 2) * PCIE_BARADDROFSIZE)
@@ -141,4 +167,8 @@
#define HWS_REG_VBUF_TOGGLE_CH2 HWS_REG_VBUF_TOGGLE(2)
#define HWS_REG_VBUF_TOGGLE_CH3 HWS_REG_VBUF_TOGGLE(3)
+#define HWS_REG_ABUF_TOGGLE_CH0 HWS_REG_ABUF_TOGGLE(0)
+#define HWS_REG_ABUF_TOGGLE_CH1 HWS_REG_ABUF_TOGGLE(1)
+#define HWS_REG_ABUF_TOGGLE_CH2 HWS_REG_ABUF_TOGGLE(2)
+#define HWS_REG_ABUF_TOGGLE_CH3 HWS_REG_ABUF_TOGGLE(3)
#endif /* _HWS_PCIE_REG_H */
diff --git a/drivers/media/pci/hws/hws_video.c b/drivers/media/pci/hws/hws_video.c
index 58bcc2e7030d..13ddd1040387 100644
--- a/drivers/media/pci/hws/hws_video.c
+++ b/drivers/media/pci/hws/hws_video.c
@@ -24,6 +24,7 @@
#include "hws.h"
#include "hws_reg.h"
#include "hws_video.h"
+#include "hws_audio.h"
#include "hws_irq.h"
#include "hws_v4l2_ioctl.h"
@@ -781,12 +782,14 @@ void hws_init_video_sys(struct hws_pcie_dev *hws, bool enable)
/* 1) reset the decoder mode register to 0 */
writel(0x00000000, hws->bar0_base + HWS_REG_DEC_MODE);
hws_seed_dma_windows(hws);
+ hws_audio_seed_channels(hws);
/* 3) on a full reset, clear all per-channel status and indices */
if (!enable) {
for (i = 0; i < hws->max_channels; i++) {
/* helpers to arm/disable capture engines */
hws_enable_video_capture(hws, i, false);
+ hws_enable_audio_capture(hws, i, false);
}
}
--
2.54.0
prev parent reply other threads:[~2026-06-29 16:03 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-29 16:02 [PATCH v2 0/5] media: hws: add HDMI audio capture support hoff.benjamin.k
2026-06-29 16:03 ` [PATCH v2 1/5] media: hws: program video DMA through remap windows hoff.benjamin.k
2026-06-29 16:03 ` [PATCH v2 2/5] media: hws: add shared scratch DMA arena hoff.benjamin.k
2026-06-29 16:03 ` [PATCH v2 3/5] media: hws: add video bounce path for shared remap windows hoff.benjamin.k
2026-06-29 16:03 ` [PATCH v2 4/5] media: hws: harden video DMA queue ownership hoff.benjamin.k
2026-06-29 16:03 ` hoff.benjamin.k [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260629160304.154046-6-hoff.benjamin.k@gmail.com \
--to=hoff.benjamin.k@gmail.com \
--cc=hverkuil+cisco@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-media@vger.kernel.org \
--cc=mchehab@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox