From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-ot1-f44.google.com (mail-ot1-f44.google.com [209.85.210.44]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 873C78479 for ; Wed, 18 Mar 2026 00:11:06 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.210.44 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1773792671; cv=none; b=AfVQpZqokQzq8zkQe+F/B+GmINVFFVvvfPWxxFcbAbmXimBzFgWvKX1WgfMvQt8aP1gOCgDTUAZKw7tiCudWMVG57xD4ecGPirGAf1yfDhnoHY+pMBMvQmC+qmq2HSWoyri13NtdciZW+10M0yFwmk2fMWNWhfJ53ypNtsN+WPo= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1773792671; c=relaxed/simple; bh=AhzJJXfV8POUU2Tv0O3UzEjDR/9VzgmYsKjQrvc7rYM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=Zu2bQKB3o67XPemP1qLD8sq+sz/PDzVUzUG2ZKVNQJaqKC8B8V79TTSMDb13Qjq0zuE5q6pUX+yUYUnZNMK82RJLHdaw/w2jzcxv6e9aED+1uSh+y7bkp7Pl7Z4oizDIN9HFTbTkyo2WaMPIZXWPicf2hKVVcWbVK/hm5Yn/m60= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=Gx3ioEFv; arc=none smtp.client-ip=209.85.210.44 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="Gx3ioEFv" Received: by mail-ot1-f44.google.com with SMTP id 46e09a7af769-7d74c1157a4so6223726a34.1 for ; Tue, 17 Mar 2026 17:11:06 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1773792665; x=1774397465; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=8O8PFHLy9T8L+PUtDLqFmT5AcBmK2nYCZyhGjkwN6bs=; b=Gx3ioEFv2BeSA1Ko/qnSn4om8o/JAr6ev/sf9kn6TRYijaz9K5FC06PqD7L5KF6by8 7cwmtY6wEZEcPqIGJWcdjEnexIU4eea9H7IrQrjABLiJm5H3DYljcXBZobwMrEMnVOoJ B/uM35zEbMTRZ/01OWn7VlaMd0Cqy/tBG+g2trMqwaP7ggTu7hs6gpldyCcFQTyAT6+S FbrtJ5dCrOs+70fqwdWPb1faVzyw+PVeUugGEYPDcKzV+/tlFZO0Gw/SoqMnyL64RsBx s9n1ilw8lG5oVIQo/uIZJ6H2Rc/nEgT1tQMI0WFRHDoRELtteCY9XoMYiGxdQR4ccIXc 8yww== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1773792665; x=1774397465; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=8O8PFHLy9T8L+PUtDLqFmT5AcBmK2nYCZyhGjkwN6bs=; b=BEvalgxbi3Z1HeH2jnhBN940/D2iITN0iwLmVd1S3jJKLqUq/7ZVjPFVqarRbMWWdM ekOVb7PK00zwNeESKAhEfgSkvNTYuSiTWuyZhKJc2u4Wkrmsr6tyCpv/udQ8TDqWulQn kXDYmBvK7tLwLJdN43xRsRL/FBBICDZCChulQMgM7DWn/4FuiqKav58j+dbEkSZyK4cz yO2qvyroYisv18EP898nMGxeIorCveTltj2wzP5CX7hT9CBcO3fkWSZx4wrHWh35qgAX ljarpBnCa2LaTBed2HiOgm4QcpwLeC3gs276uYOTjIl9ZShYpQmRcmpFF5hZ5BCzOIYw qh/Q== X-Gm-Message-State: AOJu0YyjPrucrnNW5Omdu3Q/3ENW+e2SA7HHNRh1xL4jIQ4+Bgg8zdH+ xUBL2ZW0aA4TxQX8T0HzMBmtoXSXT7+YeP2HxBH4uJgi+pHpWETOrljqjBO2OFPc X-Gm-Gg: ATEYQzyaxAGDT7qmbPY/WueEYv0NX3ukCvC38vmUM/8JRAmjNEzGx0VonFf2eBvQmmk plGhMuxYYaMhWGNDfbLVM9Tdrg8WQsdQT9rG7yRzkto8cHoLjUGGPm3zEiX7J9dJ5HaXeDx7gij kSbgGOY4mBA9mWnvmbF1k2tgUoi8CQorAvaNNxqpvic0ztJZwf/yuMDr2xBEQyb22wBl266Q6BM WUpF9yk7u3CDzKy0VLPDjz4r1YUIgjndhZlbCO4o3cLRmj1iRzP+YeXomRvFIhIGWej9MEXjhCL o+VVChG/san92F2OiZJ5KSLyZ5sW4dqqhKVwbBs6gy6JpDU3apyUzddEad+X/I7g5OtR9fXcz6R huHP04epCWOzb78qGuXBSc21OyZVht9wvl+jGfKZEqtm/1XXdLWscug2NI4M4nXqRxVtMv4tjUn Ck1V/FGylTu7EsogT5X8xOP2lLZ9Uo/Dir30HWRVcKEsJKnJj7k0JIDANAg1rFzP2Pm4zqBE1Z X-Received: by 2002:a05:6830:4117:b0:7d7:4cf3:76c0 with SMTP id 46e09a7af769-7d7ca5b40e9mr1046853a34.14.1773792663892; Tue, 17 Mar 2026 17:11:03 -0700 (PDT) Received: from father (76-224-4-192.lightspeed.clmboh.sbcglobal.net. [76.224.4.192]) by smtp.gmail.com with ESMTPSA id 46e09a7af769-7d7c9b39e11sm897413a34.18.2026.03.17.17.11.03 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 17 Mar 2026 17:11:03 -0700 (PDT) From: Ben Hoff To: linux-media@vger.kernel.org Cc: Mauro Carvalho Chehab , Hans Verkuil , linux-kernel@vger.kernel.org, Ben Hoff Subject: [PATCH v2 1/2] media: pci: add AVMatrix HWS capture driver Date: Tue, 17 Mar 2026 20:10:53 -0400 Message-ID: <20260318001056.465071-2-hoff.benjamin.k@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260318001056.465071-1-hoff.benjamin.k@gmail.com> References: <20260112022420.390854-1-hoff.benjamin.k@gmail.com> <20260318001056.465071-1-hoff.benjamin.k@gmail.com> Precedence: bulk X-Mailing-List: linux-media@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an in-tree AVMatrix HWS PCIe capture driver. The driver supports up to four HDMI inputs and exposes the video capture path through V4L2 with vb2-dma-contig streaming, DV timings, and per-input controls. Audio support is intentionally omitted from this submission. This driver is derived from a GPL out-of-tree driver. The baseline rework used for comparison is available at: https://github.com/benhoff/hws/tree/baseline A vendor driver bundle is available at: https://www.acasis.com/pages/acasis-product-drivers The vendor is not involved in this upstreaming effort. This in-tree version folds in the review-driven cleanup needed for a v2 posting: - keep scratch DMA allocation on a single probe-owned path - avoid double-freeing V4L2 control handlers on register unwind - turn live mode changes into explicit SOURCE_CHANGE renegotiation - report frame intervals and DV power-present status Build-tested with: make -C /home/hoff/swdev/linux O=/tmp/hws-build M=drivers/media/pci/hws W=1 KBUILD_MODPOST_WARN=1 modules Signed-off-by: Ben Hoff --- drivers/media/pci/Kconfig | 1 + drivers/media/pci/Makefile | 1 + drivers/media/pci/hws/Kconfig | 12 + drivers/media/pci/hws/Makefile | 4 + drivers/media/pci/hws/hws.h | 176 +++ drivers/media/pci/hws/hws_irq.c | 271 +++++ drivers/media/pci/hws/hws_irq.h | 10 + drivers/media/pci/hws/hws_pci.c | 864 +++++++++++++ drivers/media/pci/hws/hws_reg.h | 144 +++ drivers/media/pci/hws/hws_v4l2_ioctl.c | 778 ++++++++++++ drivers/media/pci/hws/hws_v4l2_ioctl.h | 43 + drivers/media/pci/hws/hws_video.c | 1546 ++++++++++++++++++++++++ drivers/media/pci/hws/hws_video.h | 29 + 13 files changed, 3879 insertions(+) create mode 100644 drivers/media/pci/hws/Kconfig create mode 100644 drivers/media/pci/hws/Makefile create mode 100644 drivers/media/pci/hws/hws.h create mode 100644 drivers/media/pci/hws/hws_irq.c create mode 100644 drivers/media/pci/hws/hws_irq.h create mode 100644 drivers/media/pci/hws/hws_pci.c create mode 100644 drivers/media/pci/hws/hws_reg.h create mode 100644 drivers/media/pci/hws/hws_v4l2_ioctl.c create mode 100644 drivers/media/pci/hws/hws_v4l2_ioctl.h create mode 100644 drivers/media/pci/hws/hws_video.c create mode 100644 drivers/media/pci/hws/hws_video.h diff --git a/drivers/media/pci/Kconfig b/drivers/media/pci/Kconfig index eebb16c58f3d..bfdb200f85a3 100644 --- a/drivers/media/pci/Kconfig +++ b/drivers/media/pci/Kconfig @@ -13,6 +13,7 @@ if MEDIA_PCI_SUPPORT if MEDIA_CAMERA_SUPPORT comment "Media capture support" +source "drivers/media/pci/hws/Kconfig" source "drivers/media/pci/mgb4/Kconfig" source "drivers/media/pci/solo6x10/Kconfig" source "drivers/media/pci/tw5864/Kconfig" diff --git a/drivers/media/pci/Makefile b/drivers/media/pci/Makefile index 02763ad88511..c4508b6723a9 100644 --- a/drivers/media/pci/Makefile +++ b/drivers/media/pci/Makefile @@ -29,6 +29,7 @@ obj-$(CONFIG_VIDEO_CX23885) += cx23885/ obj-$(CONFIG_VIDEO_CX25821) += cx25821/ obj-$(CONFIG_VIDEO_CX88) += cx88/ obj-$(CONFIG_VIDEO_DT3155) += dt3155/ +obj-$(CONFIG_VIDEO_HWS) += hws/ obj-$(CONFIG_VIDEO_IVTV) += ivtv/ obj-$(CONFIG_VIDEO_MGB4) += mgb4/ obj-$(CONFIG_VIDEO_SAA7134) += saa7134/ diff --git a/drivers/media/pci/hws/Kconfig b/drivers/media/pci/hws/Kconfig new file mode 100644 index 000000000000..b606d5ffadef --- /dev/null +++ b/drivers/media/pci/hws/Kconfig @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-only +config VIDEO_HWS + tristate "AVMatrix HWS capture driver" + depends on VIDEO_DEV && PCI + select VIDEOBUF2_DMA_CONTIG + help + This is a Video4Linux2 driver for AVMatrix HWS PCIe capture cards. + It provides a PCIe capture interface with V4L2 streaming, DV timings, + and per-input controls for the supported HWS boards. + + To compile this driver as a module, choose M here: the module will + be called hws. diff --git a/drivers/media/pci/hws/Makefile b/drivers/media/pci/hws/Makefile new file mode 100644 index 000000000000..a66aebd348e5 --- /dev/null +++ b/drivers/media/pci/hws/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 +hws-objs := hws_pci.o hws_irq.o hws_video.o hws_v4l2_ioctl.o + +obj-$(CONFIG_VIDEO_HWS) += hws.o diff --git a/drivers/media/pci/hws/hws.h b/drivers/media/pci/hws/hws.h new file mode 100644 index 000000000000..097f6937b231 --- /dev/null +++ b/drivers/media/pci/hws/hws.h @@ -0,0 +1,176 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef HWS_PCIE_H +#define HWS_PCIE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "hws_reg.h" + +struct hwsmem_param { + u32 index; + u32 type; + u32 status; +}; + +struct hws_pix_state { + u32 width; + u32 height; + u32 fourcc; /* V4L2_PIX_FMT_* (YUYV only here) */ + u32 bytesperline; /* stride */ + u32 sizeimage; /* full frame */ + enum v4l2_field field; /* V4L2_FIELD_NONE or INTERLACED */ + enum v4l2_colorspace colorspace; /* e.g., REC709 */ + enum v4l2_ycbcr_encoding ycbcr_enc; /* V4L2_YCBCR_ENC_DEFAULT */ + enum v4l2_quantization quantization; /* V4L2_QUANTIZATION_LIM_RANGE */ + enum v4l2_xfer_func xfer_func; /* V4L2_XFER_FUNC_DEFAULT */ + bool interlaced; /* cached hardware state */ + u32 half_size; /* optional: if your HW needs it */ +}; + +#define UNSET (-1U) + +struct hws_pcie_dev; +struct hws_adapter; +struct hws_video; + +struct hwsvideo_buffer { + struct vb2_v4l2_buffer vb; + struct list_head list; + int slot; /* for two-buffer approach */ +}; + +struct hws_video { + /* ───── linkage ───── */ + struct hws_pcie_dev *parent; /* parent device */ + struct video_device *video_device; + + struct vb2_queue buffer_queue; + struct list_head capture_queue; + struct hwsvideo_buffer *active; + struct hwsvideo_buffer *next_prepared; + + /* ───── locking ───── */ + struct mutex state_lock; /* primary state */ + spinlock_t irq_lock; /* ISR-side */ + + /* ───── indices ───── */ + int channel_index; + + /* ───── colour controls ───── */ + int current_brightness; + int current_contrast; + int current_saturation; + int current_hue; + + /* ───── V4L2 controls ───── */ + struct v4l2_ctrl_handler control_handler; + struct v4l2_ctrl *hotplug_detect_control; + struct v4l2_ctrl *ctrl_brightness; + struct v4l2_ctrl *ctrl_contrast; + struct v4l2_ctrl *ctrl_saturation; + struct v4l2_ctrl *ctrl_hue; + /* ───── capture queue status ───── */ + struct hws_pix_state pix; + struct v4l2_dv_timings cur_dv_timings; /* last configured DV timings */ + u32 current_fps; /* Hz, derived from mode or HW rate reg */ + u32 alloc_sizeimage; + + /* ───── per-channel capture state ───── */ + bool cap_active; + bool stop_requested; + u8 last_buf_half_toggle; + bool half_seen; + atomic_t sequence_number; + u32 queued_count; + + /* ───── timeout and error handling ───── */ + u32 timeout_count; + u32 error_count; + + bool window_valid; + u32 last_dma_hi; + u32 last_dma_page; + u32 last_pci_addr; + u32 last_half16; + + /* ───── misc counters ───── */ + int signal_loss_cnt; +}; + +static inline void hws_set_current_dv_timings(struct hws_video *vid, + u32 width, u32 height, + bool interlaced) +{ + if (!vid) + return; + + vid->cur_dv_timings = (struct v4l2_dv_timings) { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = width, + .height = height, + .interlaced = interlaced, + }, + }; +} + +struct hws_scratch_dma { + void *cpu; + dma_addr_t dma; + size_t size; +}; + +struct hws_pcie_dev { + /* ───── core objects ───── */ + struct pci_dev *pdev; + struct hws_video video[MAX_VID_CHANNELS]; + + /* ───── BAR & workqueues ───── */ + void __iomem *bar0_base; + + /* ───── device identity / capabilities ───── */ + u16 vendor_id; + u16 device_id; + u16 device_ver; + u16 hw_ver; + u32 sub_ver; + u32 port_id; + // TriState, used in `set_video_format_size` + u32 support_yv12; + u32 max_hw_video_buf_sz; + u8 max_channels; + u8 cur_max_video_ch; + bool start_run; + + bool buf_allocated; + + /* ───── V4L2 framework objects ───── */ + struct v4l2_device v4l2_device; + + /* ───── kernel thread ───── */ + struct task_struct *main_task; + struct hws_scratch_dma scratch_vid[MAX_VID_CHANNELS]; + + bool suspended; + int irq; + + /* ───── error flags ───── */ + int pci_lost; + +}; + +#endif diff --git a/drivers/media/pci/hws/hws_irq.c b/drivers/media/pci/hws/hws_irq.c new file mode 100644 index 000000000000..11f7dfde0eff --- /dev/null +++ b/drivers/media/pci/hws/hws_irq.c @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "hws_irq.h" +#include "hws_reg.h" +#include "hws_video.h" +#include "hws.h" + +#define MAX_INT_LOOPS 100 + +static bool hws_toggle_debug; +module_param_named(toggle_debug, hws_toggle_debug, bool, 0644); +MODULE_PARM_DESC(toggle_debug, + "Read toggle registers in IRQ handler for debug logging"); + +static int hws_arm_next(struct hws_pcie_dev *hws, u32 ch) +{ + struct hws_video *v = &hws->video[ch]; + unsigned long flags; + struct hwsvideo_buffer *buf; + + dev_dbg(&hws->pdev->dev, + "arm_next(ch=%u): stop=%d cap=%d queued=%d\n", + ch, READ_ONCE(v->stop_requested), READ_ONCE(v->cap_active), + !list_empty(&v->capture_queue)); + + if (unlikely(READ_ONCE(hws->suspended))) { + dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): suspended\n", ch); + return -EBUSY; + } + + if (unlikely(READ_ONCE(v->stop_requested) || !READ_ONCE(v->cap_active))) { + dev_dbg(&hws->pdev->dev, + "arm_next(ch=%u): stop=%d cap=%d -> cancel\n", ch, + v->stop_requested, v->cap_active); + return -ECANCELED; + } + + spin_lock_irqsave(&v->irq_lock, flags); + if (list_empty(&v->capture_queue)) { + spin_unlock_irqrestore(&v->irq_lock, flags); + dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): queue empty\n", ch); + return -EAGAIN; + } + + buf = list_first_entry(&v->capture_queue, struct hwsvideo_buffer, list); + list_del_init(&buf->list); /* keep buffer safe for later cleanup */ + if (v->queued_count) + v->queued_count--; + v->active = buf; + spin_unlock_irqrestore(&v->irq_lock, flags); + dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): picked buffer %p\n", ch, + buf); + + /* Publish descriptor(s) before doorbell/MMIO kicks. */ + wmb(); + + /* Avoid MMIO during suspend */ + if (unlikely(READ_ONCE(hws->suspended))) { + unsigned long f; + + dev_dbg(&hws->pdev->dev, + "arm_next(ch=%u): suspended after pick\n", ch); + spin_lock_irqsave(&v->irq_lock, f); + if (v->active) { + list_add(&buf->list, &v->capture_queue); + v->queued_count++; + v->active = NULL; + } + spin_unlock_irqrestore(&v->irq_lock, f); + return -EBUSY; + } + + /* Also program the DMA address register directly */ + { + dma_addr_t dma_addr = + vb2_dma_contig_plane_dma_addr(&buf->vb.vb2_buf, 0); + hws_program_dma_for_addr(hws, ch, dma_addr); + iowrite32(lower_32_bits(dma_addr), + hws->bar0_base + HWS_REG_DMA_ADDR(ch)); + } + + dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): programmed buffer %p\n", ch, + buf); + spin_lock_irqsave(&v->irq_lock, flags); + hws_prime_next_locked(v); + spin_unlock_irqrestore(&v->irq_lock, flags); + return 0; +} + +static void hws_video_handle_vdone(struct hws_video *v) +{ + struct hws_pcie_dev *hws = v->parent; + unsigned int ch = v->channel_index; + struct hwsvideo_buffer *done; + unsigned long flags; + bool promoted = false; + + dev_dbg(&hws->pdev->dev, + "bh_video(ch=%u): stop=%d cap=%d active=%p\n", + ch, READ_ONCE(v->stop_requested), READ_ONCE(v->cap_active), + v->active); + + int ret; + + dev_dbg(&hws->pdev->dev, + "bh_video(ch=%u): entry stop=%d cap=%d\n", ch, + v->stop_requested, v->cap_active); + if (unlikely(READ_ONCE(hws->suspended))) + return; + + if (unlikely(READ_ONCE(v->stop_requested) || !READ_ONCE(v->cap_active))) + return; + + spin_lock_irqsave(&v->irq_lock, flags); + done = v->active; + if (done && v->next_prepared) { + v->active = v->next_prepared; + v->next_prepared = NULL; + promoted = true; + } + spin_unlock_irqrestore(&v->irq_lock, flags); + + /* 1) Complete the buffer the HW just finished (if any) */ + if (done) { + struct vb2_v4l2_buffer *vb2v = &done->vb; + size_t expected = v->pix.sizeimage; + size_t plane_size = vb2_plane_size(&vb2v->vb2_buf, 0); + + if (unlikely(expected > plane_size)) { + dev_warn_ratelimited(&hws->pdev->dev, + "bh_video(ch=%u): sizeimage %zu > plane %zu, dropping seq=%u\n", + ch, expected, plane_size, + (u32)atomic_read(&v->sequence_number) + 1); + vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_ERROR); + goto arm_next; + } + vb2_set_plane_payload(&vb2v->vb2_buf, 0, expected); + + dma_rmb(); /* device writes visible before userspace sees it */ + + vb2v->sequence = (u32)atomic_inc_return(&v->sequence_number); + vb2v->vb2_buf.timestamp = ktime_get_ns(); + dev_dbg(&hws->pdev->dev, + "bh_video(ch=%u): DONE buf=%p seq=%u half_seen=%d toggle=%u\n", + ch, done, vb2v->sequence, v->half_seen, + v->last_buf_half_toggle); + + if (!promoted) + v->active = NULL; /* channel no longer owns this buffer */ + vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_DONE); + } + + if (unlikely(READ_ONCE(hws->suspended))) + return; + + if (promoted) { + dev_dbg(&hws->pdev->dev, + "bh_video(ch=%u): promoted pre-armed buffer active=%p\n", + ch, v->active); + spin_lock_irqsave(&v->irq_lock, flags); + hws_prime_next_locked(v); + spin_unlock_irqrestore(&v->irq_lock, flags); + return; + } + +arm_next: + /* 2) Immediately arm the next queued buffer (if present) */ + ret = hws_arm_next(hws, ch); + if (ret == -EAGAIN) { + dev_dbg(&hws->pdev->dev, + "bh_video(ch=%u): no queued buffer to arm\n", ch); + return; + } + dev_dbg(&hws->pdev->dev, + "bh_video(ch=%u): armed next buffer, active=%p\n", ch, + v->active); + /* On success the engine now points at v->active’s DMA address */ +} + +irqreturn_t hws_irq_handler(int irq, void *info) +{ + struct hws_pcie_dev *pdx = info; + u32 int_state; + + dev_dbg(&pdx->pdev->dev, "irq: entry\n"); + if (likely(pdx->bar0_base)) { + dev_dbg(&pdx->pdev->dev, + "irq: INT_EN=0x%08x INT_STATUS=0x%08x\n", + readl(pdx->bar0_base + INT_EN_REG_BASE), + readl(pdx->bar0_base + HWS_REG_INT_STATUS)); + } + + /* Fast path: if suspended, quietly ack and exit */ + if (unlikely(READ_ONCE(pdx->suspended))) { + int_state = readl_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS); + if (int_state) { + writel(int_state, pdx->bar0_base + HWS_REG_INT_STATUS); + (void)readl_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS); + } + return int_state ? IRQ_HANDLED : IRQ_NONE; + } + // u32 sys_status = readl(pdx->bar0_base + HWS_REG_SYS_STATUS); + + int_state = readl_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS); + if (!int_state || int_state == 0xFFFFFFFF) { + dev_dbg(&pdx->pdev->dev, + "irq: spurious or device-gone int_state=0x%08x\n", + int_state); + return IRQ_NONE; + } + dev_dbg(&pdx->pdev->dev, "irq: entry INT_STATUS=0x%08x\n", int_state); + + /* Loop until all pending bits are serviced (max 100 iterations) */ + for (u32 cnt = 0; int_state && cnt < MAX_INT_LOOPS; ++cnt) { + for (unsigned int ch = 0; ch < pdx->cur_max_video_ch; ++ch) { + u32 vbit = HWS_INT_VDONE_BIT(ch); + + if (!(int_state & vbit)) + continue; + + if (likely(READ_ONCE(pdx->video[ch].cap_active) && + !READ_ONCE(pdx->video[ch].stop_requested))) { + if (unlikely(hws_toggle_debug)) { + u32 toggle = + readl_relaxed(pdx->bar0_base + + HWS_REG_VBUF_TOGGLE(ch)) & 0x01; + WRITE_ONCE(pdx->video[ch].last_buf_half_toggle, + toggle); + } + dma_rmb(); + WRITE_ONCE(pdx->video[ch].half_seen, true); + dev_dbg(&pdx->pdev->dev, + "irq: VDONE ch=%u toggle=%u handling inline (cap=%d)\n", + ch, + READ_ONCE(pdx->video[ch].last_buf_half_toggle), + READ_ONCE(pdx->video[ch].cap_active)); + hws_video_handle_vdone(&pdx->video[ch]); + } else { + dev_dbg(&pdx->pdev->dev, + "irq: VDONE ch=%u ignored (cap=%d stop=%d)\n", + ch, + READ_ONCE(pdx->video[ch].cap_active), + READ_ONCE(pdx->video[ch].stop_requested)); + } + + writel(vbit, pdx->bar0_base + HWS_REG_INT_STATUS); + (void)readl_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS); + } + + /* Re‐read in case new interrupt bits popped while processing */ + int_state = readl_relaxed(pdx->bar0_base + HWS_REG_INT_STATUS); + dev_dbg(&pdx->pdev->dev, + "irq: loop cnt=%u new INT_STATUS=0x%08x\n", cnt, + int_state); + if (cnt + 1 == MAX_INT_LOOPS) + dev_warn_ratelimited(&pdx->pdev->dev, + "IRQ storm? status=0x%08x\n", + int_state); + } + + return IRQ_HANDLED; +} diff --git a/drivers/media/pci/hws/hws_irq.h b/drivers/media/pci/hws/hws_irq.h new file mode 100644 index 000000000000..a42867aa0c46 --- /dev/null +++ b/drivers/media/pci/hws/hws_irq.h @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef HWS_INTERRUPT_H +#define HWS_INTERRUPT_H + +#include +#include "hws.h" + +irqreturn_t hws_irq_handler(int irq, void *info); + +#endif /* HWS_INTERRUPT_H */ diff --git a/drivers/media/pci/hws/hws_pci.c b/drivers/media/pci/hws/hws_pci.c new file mode 100644 index 000000000000..5f106d268b74 --- /dev/null +++ b/drivers/media/pci/hws/hws_pci.c @@ -0,0 +1,864 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "hws.h" +#include "hws_reg.h" +#include "hws_video.h" +#include "hws_irq.h" +#include "hws_v4l2_ioctl.h" + +#define DRV_NAME "hws" +#define DRV_DESC "AVMatrix HWS capture driver" +#define HWS_BUSY_POLL_DELAY_US 10 +#define HWS_BUSY_POLL_TIMEOUT_US 1000000 + +/* register layout inside HWS_REG_DEVICE_INFO */ +#define DEVINFO_VER GENMASK(7, 0) +#define DEVINFO_SUBVER GENMASK(15, 8) +#define DEVINFO_YV12 GENMASK(31, 28) +#define DEVINFO_HWKEY GENMASK(27, 24) +#define DEVINFO_PORTID GENMASK(25, 24) /* low 2 bits of HW-key */ + +#define MAKE_ENTRY(__vend, __chip, __subven, __subdev, __configptr) \ + { .vendor = (__vend), \ + .device = (__chip), \ + .subvendor = (__subven), \ + .subdevice = (__subdev), \ + .driver_data = (unsigned long)(__configptr) } + +/* + * PCI IDs for HWS family cards. + * + * The subsystem IDs are fixed at 0x8888:0x0007 for this family. Some boards + * enumerate with vendor ID 0x8888 or 0x1f33. Exact SKU names are not fully + * pinned down yet; update these comments when vendor documentation or INF + * strings are available. + */ +static const struct pci_device_id hws_pci_table[] = { + /* HWS family, SKU unknown. */ + MAKE_ENTRY(0x8888, 0x9534, 0x8888, 0x0007, NULL), + MAKE_ENTRY(0x1F33, 0x8534, 0x8888, 0x0007, NULL), + MAKE_ENTRY(0x1F33, 0x8554, 0x8888, 0x0007, NULL), + + /* HWS 2x2 HDMI family. */ + MAKE_ENTRY(0x8888, 0x8524, 0x8888, 0x0007, NULL), + /* HWS 2x2 SDI family. */ + MAKE_ENTRY(0x1F33, 0x6524, 0x8888, 0x0007, NULL), + + /* HWS X4 HDMI family. */ + MAKE_ENTRY(0x8888, 0x8504, 0x8888, 0x0007, NULL), + /* HWS X4 SDI family. */ + MAKE_ENTRY(0x8888, 0x6504, 0x8888, 0x0007, NULL), + + /* HWS family, SKU unknown. */ + MAKE_ENTRY(0x8888, 0x8532, 0x8888, 0x0007, NULL), + MAKE_ENTRY(0x8888, 0x8512, 0x8888, 0x0007, NULL), + MAKE_ENTRY(0x8888, 0x8501, 0x8888, 0x0007, NULL), + MAKE_ENTRY(0x1F33, 0x6502, 0x8888, 0x0007, NULL), + + /* HWS X4 HDMI family (alternate vendor ID). */ + MAKE_ENTRY(0x1F33, 0x8504, 0x8888, 0x0007, NULL), + /* HWS 2x2 HDMI family (alternate vendor ID). */ + MAKE_ENTRY(0x1F33, 0x8524, 0x8888, 0x0007, NULL), + + {} +}; + +static void enable_pcie_relaxed_ordering(struct pci_dev *dev) +{ + pcie_capability_set_word(dev, PCI_EXP_DEVCTL, PCI_EXP_DEVCTL_RELAX_EN); +} + +static void hws_configure_hardware_capabilities(struct hws_pcie_dev *hdev) +{ + u16 id = hdev->device_id; + + /* select per-chip channel counts */ + switch (id) { + case 0x9534: + case 0x6524: + case 0x8524: + case 0x8504: + case 0x6504: + hdev->cur_max_video_ch = 4; + break; + case 0x8532: + hdev->cur_max_video_ch = 2; + break; + case 0x8512: + case 0x6502: + hdev->cur_max_video_ch = 2; + break; + case 0x8501: + hdev->cur_max_video_ch = 1; + break; + default: + hdev->cur_max_video_ch = 4; + break; + } + + /* universal buffer capacity */ + hdev->max_hw_video_buf_sz = MAX_MM_VIDEO_SIZE; + + /* decide hardware-version and program DMA max size if needed */ + if (hdev->device_ver > 121) { + if (id == 0x8501 && hdev->device_ver == 122) { + hdev->hw_ver = 0; + } else { + hdev->hw_ver = 1; + u32 dma_max = (u32)(MAX_VIDEO_SCALER_SIZE / 16); + + writel(dma_max, hdev->bar0_base + HWS_REG_DMA_MAX_SIZE); + /* readback to flush posted MMIO write */ + (void)readl(hdev->bar0_base + HWS_REG_DMA_MAX_SIZE); + } + } else { + hdev->hw_ver = 0; + } +} + +static void hws_stop_device(struct hws_pcie_dev *hws); + +static void hws_log_lifecycle_snapshot(struct hws_pcie_dev *hws, + const char *action, + const char *phase) +{ + struct device *dev; + u32 int_en, int_status, vcap, sys_status, dec_mode; + + if (!hws || !hws->pdev) + return; + + dev = &hws->pdev->dev; + if (!hws->bar0_base) { + dev_dbg(dev, + "lifecycle:%s:%s bar0-unmapped suspended=%d start_run=%d pci_lost=%d irq=%d\n", + action, phase, READ_ONCE(hws->suspended), hws->start_run, + hws->pci_lost, hws->irq); + return; + } + + int_en = readl(hws->bar0_base + INT_EN_REG_BASE); + int_status = readl(hws->bar0_base + HWS_REG_INT_STATUS); + vcap = readl(hws->bar0_base + HWS_REG_VCAP_ENABLE); + sys_status = readl(hws->bar0_base + HWS_REG_SYS_STATUS); + dec_mode = readl(hws->bar0_base + HWS_REG_DEC_MODE); + + dev_dbg(dev, + "lifecycle:%s:%s suspended=%d start_run=%d pci_lost=%d irq=%d INT_EN=0x%08x INT_STATUS=0x%08x VCAP=0x%08x SYS=0x%08x DEC=0x%08x\n", + action, phase, READ_ONCE(hws->suspended), hws->start_run, + hws->pci_lost, hws->irq, int_en, int_status, vcap, + sys_status, dec_mode); +} + +static int read_chip_id(struct hws_pcie_dev *hdev) +{ + u32 reg; + /* mirror PCI IDs for later switches */ + hdev->device_id = hdev->pdev->device; + hdev->vendor_id = hdev->pdev->vendor; + + reg = readl(hdev->bar0_base + HWS_REG_DEVICE_INFO); + + hdev->device_ver = FIELD_GET(DEVINFO_VER, reg); + hdev->sub_ver = FIELD_GET(DEVINFO_SUBVER, reg); + 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); + + hws_configure_hardware_capabilities(hdev); + + dev_info(&hdev->pdev->dev, + "chip detected: ver=%u subver=%u port=%u yv12=%u\n", + hdev->device_ver, hdev->sub_ver, hdev->port_id, + hdev->support_yv12); + + return 0; +} + +static int main_ks_thread_handle(void *data) +{ + struct hws_pcie_dev *pdx = data; + + set_freezable(); + + while (!kthread_should_stop()) { + /* If we’re suspending, don’t touch hardware; just sleep/freeeze */ + if (READ_ONCE(pdx->suspended)) { + try_to_freeze(); + schedule_timeout_interruptible(msecs_to_jiffies(1000)); + continue; + } + + /* avoid MMIO when suspended (guarded above) */ + check_video_format(pdx); + + try_to_freeze(); /* cooperate with freezer each loop */ + + /* Sleep 1s or until signaled to wake/stop */ + schedule_timeout_interruptible(msecs_to_jiffies(1000)); + } + + dev_dbg(&pdx->pdev->dev, "%s: exiting\n", __func__); + return 0; +} + +static void hws_stop_kthread_action(void *data) +{ + struct hws_pcie_dev *hws = data; + struct task_struct *t; + u64 start_ns; + + if (!hws) + return; + + t = READ_ONCE(hws->main_task); + if (!IS_ERR_OR_NULL(t)) { + start_ns = ktime_get_mono_fast_ns(); + dev_dbg(&hws->pdev->dev, + "lifecycle:kthread-stop:begin task=%s[%d]\n", + t->comm, t->pid); + WRITE_ONCE(hws->main_task, NULL); + kthread_stop(t); + dev_dbg(&hws->pdev->dev, + "lifecycle:kthread-stop:done (%lluus)\n", + (unsigned long long) + ((ktime_get_mono_fast_ns() - start_ns) / 1000)); + } +} + +static int hws_alloc_seed_buffers(struct hws_pcie_dev *hws) +{ + int ch; + /* 64 KiB is plenty for a safe dummy; align to 64 for your HW */ + const size_t need = ALIGN(64 * 1024, 64); + + for (ch = 0; ch < hws->cur_max_video_ch; ch++) { +#if defined(CONFIG_HAS_DMA) /* normal on PCIe platforms */ + void *cpu = dma_alloc_coherent(&hws->pdev->dev, need, + &hws->scratch_vid[ch].dma, + GFP_KERNEL); +#else + void *cpu = NULL; +#endif + if (!cpu) { + dev_warn(&hws->pdev->dev, + "scratch: dma_alloc_coherent failed ch=%d\n", ch); + /* not fatal: free earlier ones and continue without seeding */ + while (--ch >= 0) { + if (hws->scratch_vid[ch].cpu) + dma_free_coherent(&hws->pdev->dev, + hws->scratch_vid[ch].size, + hws->scratch_vid[ch].cpu, + hws->scratch_vid[ch].dma); + hws->scratch_vid[ch].cpu = NULL; + hws->scratch_vid[ch].size = 0; + } + return -ENOMEM; + } + hws->scratch_vid[ch].cpu = cpu; + hws->scratch_vid[ch].size = need; + } + return 0; +} + +static void hws_free_seed_buffers(struct hws_pcie_dev *hws) +{ + int ch; + + for (ch = 0; ch < hws->cur_max_video_ch; ch++) { + if (hws->scratch_vid[ch].cpu) { + dma_free_coherent(&hws->pdev->dev, + hws->scratch_vid[ch].size, + hws->scratch_vid[ch].cpu, + hws->scratch_vid[ch].dma); + hws->scratch_vid[ch].cpu = NULL; + hws->scratch_vid[ch].size = 0; + } + } +} + +static void hws_seed_channel(struct hws_pcie_dev *hws, int ch) +{ + dma_addr_t paddr = hws->scratch_vid[ch].dma; + u32 lo = lower_32_bits(paddr); + u32 hi = upper_32_bits(paddr); + u32 pci_addr = lo & PCI_E_BAR_ADD_LOWMASK; + + lo &= PCI_E_BAR_ADD_MASK; + + /* Program 64-bit BAR remap entry for this channel (table @ 0x208 + ch * 8) */ + writel_relaxed(hi, hws->bar0_base + + PCI_ADDR_TABLE_BASE + 0x208 + ch * 8); + writel_relaxed(lo, hws->bar0_base + + PCI_ADDR_TABLE_BASE + 0x208 + ch * 8 + + PCIE_BARADDROFSIZE); + + /* Program capture engine per-channel base/half */ + writel_relaxed((ch + 1) * PCIEBAR_AXI_BASE + pci_addr, + hws->bar0_base + CVBS_IN_BUF_BASE + + ch * PCIE_BARADDROFSIZE); + + /* half size: use either the current format’s half or half of scratch */ + { + u32 half = hws->video[ch].pix.half_size ? + hws->video[ch].pix.half_size : + (u32)(hws->scratch_vid[ch].size / 2); + + writel_relaxed(half / 16, + hws->bar0_base + CVBS_IN_BUF_BASE2 + + ch * PCIE_BARADDROFSIZE); + } + + (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); /* flush posted writes */ +} + +static void hws_seed_all_channels(struct hws_pcie_dev *hws) +{ + int ch; + + for (ch = 0; ch < hws->cur_max_video_ch; ch++) { + if (hws->scratch_vid[ch].cpu) + hws_seed_channel(hws, ch); + } +} + +static void hws_irq_mask_gate(struct hws_pcie_dev *hws) +{ + writel(0x00000000, hws->bar0_base + INT_EN_REG_BASE); + (void)readl(hws->bar0_base + INT_EN_REG_BASE); +} + +static void hws_irq_unmask_gate(struct hws_pcie_dev *hws) +{ + writel(HWS_INT_EN_MASK, hws->bar0_base + INT_EN_REG_BASE); + (void)readl(hws->bar0_base + INT_EN_REG_BASE); +} + +static void hws_irq_clear_pending(struct hws_pcie_dev *hws) +{ + u32 st = readl(hws->bar0_base + HWS_REG_INT_STATUS); + + if (st) { + writel(st, hws->bar0_base + HWS_REG_INT_STATUS); /* W1C */ + (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); + } +} + +static void hws_block_hotpaths(struct hws_pcie_dev *hws) +{ + WRITE_ONCE(hws->suspended, true); + if (hws->irq >= 0) + disable_irq(hws->irq); + + if (!hws->bar0_base) + return; + + hws_irq_mask_gate(hws); + hws_irq_clear_pending(hws); +} + +static int hws_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id) +{ + struct hws_pcie_dev *hws; + int i, ret, irq; + unsigned long irqf = 0; + bool v4l2_registered = false; + + /* devres-backed device object */ + hws = devm_kzalloc(&pdev->dev, sizeof(*hws), GFP_KERNEL); + if (!hws) + return -ENOMEM; + + hws->pdev = pdev; + hws->irq = -1; + hws->suspended = false; + pci_set_drvdata(pdev, hws); + + /* 1) Enable device + bus mastering (managed) */ + ret = pcim_enable_device(pdev); + if (ret) + return dev_err_probe(&pdev->dev, ret, "pcim_enable_device\n"); + pci_set_master(pdev); + + /* 2) Map BAR0 (managed) */ + ret = pcim_iomap_regions(pdev, BIT(0), KBUILD_MODNAME); + if (ret) + return dev_err_probe(&pdev->dev, ret, "pcim_iomap_regions BAR0\n"); + hws->bar0_base = pcim_iomap_table(pdev)[0]; + + ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); + if (ret) { + dev_warn(&pdev->dev, + "64-bit DMA mask unavailable, falling back to 32-bit (%d)\n", + ret); + ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)); + if (ret) + return dev_err_probe(&pdev->dev, ret, + "No suitable DMA configuration\n"); + } else { + dev_dbg(&pdev->dev, "Using 64-bit DMA mask\n"); + } + + /* 3) Optional PCIe tuning (same as before) */ + enable_pcie_relaxed_ordering(pdev); +#ifdef CONFIG_ARCH_TI816X + pcie_set_readrq(pdev, 128); +#endif + + /* 4) Identify chip & capabilities */ + 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) */ + 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; + } + } + + /* 6) Allocate scratch DMA and seed BAR table + channel base/half (legacy SetDMAAddress) */ + ret = hws_alloc_seed_buffers(hws); + if (!ret) + hws_seed_all_channels(hws); + + /* 7) Start-run sequence (like InitVideoSys) */ + hws_init_video_sys(hws, false); + + /* A) Force legacy INTx; legacy used request_irq(pdev->irq, ..., IRQF_SHARED) */ + pci_intx(pdev, 1); + irqf = IRQF_SHARED; + irq = pdev->irq; + hws->irq = irq; + dev_info(&pdev->dev, "IRQ mode: legacy INTx (shared), irq=%d\n", irq); + + /* B) Mask the device's global/bridge gate (INT_EN_REG_BASE) */ + hws_irq_mask_gate(hws); + + /* C) Clear any sticky pending interrupt status (W1C) before we arm the line */ + hws_irq_clear_pending(hws); + + /* D) Request the legacy shared interrupt line (no vectors/MSI/MSI-X) */ + ret = devm_request_irq(&pdev->dev, irq, hws_irq_handler, irqf, + dev_name(&pdev->dev), hws); + if (ret) { + dev_err(&pdev->dev, "request_irq(%d) failed: %d\n", irq, ret); + goto err_unwind_channels; + } + + /* E) Set the global interrupt enable bit in main control register */ + { + u32 ctl_reg = readl(hws->bar0_base + HWS_REG_CTL); + + ctl_reg |= HWS_CTL_IRQ_ENABLE_BIT; + writel(ctl_reg, hws->bar0_base + HWS_REG_CTL); + (void)readl(hws->bar0_base + HWS_REG_CTL); /* flush write */ + dev_info(&pdev->dev, "Global IRQ enable bit set in control register\n"); + } + + /* F) Open the global gate just like legacy did */ + hws_irq_unmask_gate(hws); + dev_info(&pdev->dev, "INT_EN_GATE readback=0x%08x\n", + readl(hws->bar0_base + INT_EN_REG_BASE)); + + /* 11) Register V4L2 */ + ret = hws_video_register(hws); + if (ret) { + dev_err(&pdev->dev, "video_register: %d\n", ret); + goto err_unwind_channels; + } + v4l2_registered = true; + + /* 12) Background monitor thread (managed) */ + hws->main_task = kthread_run(main_ks_thread_handle, hws, "hws-mon"); + if (IS_ERR(hws->main_task)) { + ret = PTR_ERR(hws->main_task); + hws->main_task = NULL; + dev_err(&pdev->dev, "kthread_run: %d\n", ret); + goto err_unregister_va; + } + ret = devm_add_action_or_reset(&pdev->dev, hws_stop_kthread_action, hws); + if (ret) { + dev_err(&pdev->dev, "devm_add_action kthread_stop: %d\n", ret); + goto err_unregister_va; /* reset already stopped the thread */ + } + + /* 13) Final: show the line is armed */ + dev_info(&pdev->dev, "irq handler installed on irq=%d\n", irq); + return 0; + +err_unregister_va: + hws_stop_device(hws); + hws_video_unregister(hws); + hws_free_seed_buffers(hws); + return ret; +err_unwind_channels: + hws_free_seed_buffers(hws); + if (!v4l2_registered) { + while (--i >= 0) + hws_video_cleanup_channel(hws, i); + } + return ret; +} + +static int hws_check_busy(struct hws_pcie_dev *pdx) +{ + void __iomem *reg = pdx->bar0_base + HWS_REG_SYS_STATUS; + u32 val; + int ret; + + /* poll until !(val & BUSY_BIT), sleeping HWS_BUSY_POLL_DELAY_US between reads */ + ret = readl_poll_timeout(reg, val, !(val & HWS_SYS_DMA_BUSY_BIT), + HWS_BUSY_POLL_DELAY_US, + HWS_BUSY_POLL_TIMEOUT_US); + if (ret) { + dev_err(&pdx->pdev->dev, + "SYS_STATUS busy bit never cleared (0x%08x)\n", val); + return -ETIMEDOUT; + } + + return 0; +} + +static void hws_stop_dsp(struct hws_pcie_dev *hws) +{ + u32 status; + + /* Read the decoder mode/status register */ + status = readl(hws->bar0_base + HWS_REG_DEC_MODE); + dev_dbg(&hws->pdev->dev, "%s: status=0x%08x\n", __func__, status); + + /* If the device looks unplugged/stuck, bail out */ + if (status == 0xFFFFFFFF) + return; + + /* Tell the DSP to stop */ + writel(0x10, hws->bar0_base + HWS_REG_DEC_MODE); + + if (hws_check_busy(hws)) + dev_warn(&hws->pdev->dev, "DSP busy timeout on stop\n"); + /* Disable video capture engine in the DSP */ + writel(0x0, hws->bar0_base + HWS_REG_VCAP_ENABLE); +} + +/* Publish stop so ISR/BH won’t touch video buffers anymore. */ +static void hws_publish_stop_flags(struct hws_pcie_dev *hws) +{ + unsigned int i; + + for (i = 0; i < hws->cur_max_video_ch; ++i) { + struct hws_video *v = &hws->video[i]; + + WRITE_ONCE(v->cap_active, false); + WRITE_ONCE(v->stop_requested, true); + } + + smp_wmb(); /* make flags visible before we touch MMIO/queues */ +} + +/* Drain engines + ISR/BH after flags are published. */ +static void hws_drain_after_stop(struct hws_pcie_dev *hws) +{ + u32 ackmask = 0; + unsigned int i; + u64 start_ns = ktime_get_mono_fast_ns(); + + /* Mask device enables: no new DMA starts. */ + writel(0x0, hws->bar0_base + HWS_REG_VCAP_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. */ + for (i = 0; i < hws->cur_max_video_ch; ++i) + ackmask |= HWS_INT_VDONE_BIT(i); + if (ackmask) { + writel(ackmask, hws->bar0_base + HWS_REG_INT_STATUS); + (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); + } + + /* Ensure no hard IRQ is still running. */ + if (hws->irq >= 0) + synchronize_irq(hws->irq); + + dev_dbg(&hws->pdev->dev, "lifecycle:drain-after-stop:done (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); +} + +static void hws_stop_device(struct hws_pcie_dev *hws) +{ + u32 status = readl(hws->bar0_base + HWS_REG_PIPE_BASE(0)); + u64 start_ns = ktime_get_mono_fast_ns(); + bool live = status != 0xFFFFFFFF; + + dev_dbg(&hws->pdev->dev, "%s: status=0x%08x\n", __func__, status); + if (!live) { + hws->pci_lost = true; + goto out; + } + hws_log_lifecycle_snapshot(hws, "stop-device", "begin"); + + /* Make ISR/BH a no-op, then drain engines/IRQ. */ + hws_publish_stop_flags(hws); + hws_drain_after_stop(hws); + + /* 1) Stop the on-board DSP */ + hws_stop_dsp(hws); + +out: + hws->start_run = false; + if (live) + hws_log_lifecycle_snapshot(hws, "stop-device", "end"); + else + dev_dbg(&hws->pdev->dev, "lifecycle:stop-device:device-lost\n"); + dev_dbg(&hws->pdev->dev, "lifecycle:stop-device:done (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); + dev_dbg(&hws->pdev->dev, "%s: complete\n", __func__); +} + +static int hws_quiesce_for_transition(struct hws_pcie_dev *hws, + const char *action, + bool stop_thread) +{ + struct device *dev = &hws->pdev->dev; + u64 start_ns = ktime_get_mono_fast_ns(); + u64 step_ns; + int vret; + + hws_log_lifecycle_snapshot(hws, action, "begin"); + + step_ns = ktime_get_mono_fast_ns(); + hws_block_hotpaths(hws); + dev_dbg(dev, "lifecycle:%s:block-hotpaths (%lluus)\n", action, + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + hws_log_lifecycle_snapshot(hws, action, "blocked"); + + if (stop_thread) { + step_ns = ktime_get_mono_fast_ns(); + hws_stop_kthread_action(hws); + dev_dbg(dev, "lifecycle:%s:stop-kthread (%lluus)\n", action, + (unsigned long long) + ((ktime_get_mono_fast_ns() - step_ns) / 1000)); + } + + step_ns = ktime_get_mono_fast_ns(); + vret = hws_video_quiesce(hws, action); + dev_dbg(dev, "lifecycle:%s:video-quiesce ret=%d (%lluus)\n", action, + vret, + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + if (vret) + dev_warn(dev, "lifecycle:%s video quiesce returned %d\n", + action, vret); + + step_ns = ktime_get_mono_fast_ns(); + hws_stop_device(hws); + dev_dbg(dev, "lifecycle:%s:stop-device (%lluus)\n", action, + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + hws_log_lifecycle_snapshot(hws, action, "end"); + dev_dbg(dev, "lifecycle:%s:quiesce-done ret=%d (%lluus)\n", action, + vret, + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); + + return vret; +} + +static void hws_remove(struct pci_dev *pdev) +{ + struct hws_pcie_dev *hws = pci_get_drvdata(pdev); + u64 start_ns; + + if (!hws) + return; + + start_ns = ktime_get_mono_fast_ns(); + dev_info(&pdev->dev, "lifecycle:remove begin\n"); + hws_log_lifecycle_snapshot(hws, "remove", "begin"); + + /* Stop the monitor thread before tearing down V4L2/vb2 objects. */ + hws_block_hotpaths(hws); + hws_stop_kthread_action(hws); + + /* Stop hardware / capture cleanly (your helper) */ + hws_stop_device(hws); + + /* Unregister subsystems you registered */ + hws_video_unregister(hws); + + /* Release seeded DMA buffers */ + hws_free_seed_buffers(hws); + /* kthread is stopped by the devm action you added in probe */ + hws_log_lifecycle_snapshot(hws, "remove", "end"); + dev_info(&pdev->dev, "lifecycle:remove done (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); +} + +#ifdef CONFIG_PM_SLEEP +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 vret; + u64 start_ns = ktime_get_mono_fast_ns(); + u64 step_ns; + + dev_info(dev, "lifecycle:pm_suspend begin\n"); + vret = hws_quiesce_for_transition(hws, "pm_suspend", false); + + step_ns = ktime_get_mono_fast_ns(); + pci_save_state(pdev); + pci_clear_master(pdev); + pci_disable_device(pdev); + pci_set_power_state(pdev, PCI_D3hot); + dev_dbg(dev, "lifecycle:pm_suspend:pci-d3hot (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + dev_info(dev, "lifecycle:pm_suspend done ret=%d (%lluus)\n", vret, + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); + + return 0; +} + +static int hws_pm_resume(struct device *dev) +{ + struct pci_dev *pdev = to_pci_dev(dev); + struct hws_pcie_dev *hws = pci_get_drvdata(pdev); + int ret; + u64 start_ns = ktime_get_mono_fast_ns(); + u64 step_ns; + + dev_info(dev, "lifecycle:pm_resume begin\n"); + + /* Back to D0 and re-enable the function */ + step_ns = ktime_get_mono_fast_ns(); + pci_set_power_state(pdev, PCI_D0); + + ret = pci_enable_device(pdev); + if (ret) { + dev_err(dev, "pci_enable_device: %d\n", ret); + return ret; + } + pci_restore_state(pdev); + pci_set_master(pdev); + dev_dbg(dev, "lifecycle:pm_resume:pci-enable (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + + /* Reapply any PCIe tuning lost across D3 */ + enable_pcie_relaxed_ordering(pdev); + + /* Reinitialize chip-side capabilities / registers */ + step_ns = ktime_get_mono_fast_ns(); + read_chip_id(hws); + /* Re-seed BAR remaps/DMA windows and restart the capture core */ + hws_seed_all_channels(hws); + hws_init_video_sys(hws, true); + hws_irq_clear_pending(hws); + dev_dbg(dev, "lifecycle:pm_resume:chip-reinit (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + + /* IRQs can be re-enabled now that MMIO is sane */ + step_ns = ktime_get_mono_fast_ns(); + if (hws->irq >= 0) + enable_irq(hws->irq); + + WRITE_ONCE(hws->suspended, false); + dev_dbg(dev, "lifecycle:pm_resume:irq-unsuspend (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + + /* vb2: nothing mandatory; userspace will STREAMON again when ready */ + step_ns = ktime_get_mono_fast_ns(); + hws_video_pm_resume(hws); + dev_dbg(dev, "lifecycle:pm_resume:video-resume (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + hws_log_lifecycle_snapshot(hws, "pm_resume", "end"); + dev_info(dev, "lifecycle:pm_resume done (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); + + return 0; +} + +static SIMPLE_DEV_PM_OPS(hws_pm_ops, hws_pm_suspend, hws_pm_resume); +# define HWS_PM_OPS (&hws_pm_ops) +#else +# define HWS_PM_OPS NULL +#endif + +static void hws_shutdown(struct pci_dev *pdev) +{ + struct hws_pcie_dev *hws = pci_get_drvdata(pdev); + int vret = 0; + u64 start_ns = ktime_get_mono_fast_ns(); + u64 step_ns; + + if (!hws) + return; + + dev_info(&pdev->dev, "lifecycle:pci_shutdown begin\n"); + vret = hws_quiesce_for_transition(hws, "pci_shutdown", true); + + step_ns = ktime_get_mono_fast_ns(); + pci_clear_master(pdev); + dev_dbg(&pdev->dev, "lifecycle:pci_shutdown:clear-master (%lluus)\n", + (unsigned long long)((ktime_get_mono_fast_ns() - step_ns) / 1000)); + dev_info(&pdev->dev, "lifecycle:pci_shutdown done ret=%d (%lluus)\n", + vret, + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); +} + +static struct pci_driver hws_pci_driver = { + .name = KBUILD_MODNAME, + .id_table = hws_pci_table, + .probe = hws_probe, + .remove = hws_remove, + .shutdown = hws_shutdown, + .driver = { + .pm = HWS_PM_OPS, + }, +}; + +MODULE_DEVICE_TABLE(pci, hws_pci_table); + +static int __init pcie_hws_init(void) +{ + return pci_register_driver(&hws_pci_driver); +} + +static void __exit pcie_hws_exit(void) +{ + pci_unregister_driver(&hws_pci_driver); +} + +module_init(pcie_hws_init); +module_exit(pcie_hws_exit); + +MODULE_DESCRIPTION(DRV_DESC); +MODULE_AUTHOR("Ben Hoff "); +MODULE_AUTHOR("Sales "); +MODULE_LICENSE("GPL"); +MODULE_IMPORT_NS("DMA_BUF"); diff --git a/drivers/media/pci/hws/hws_reg.h b/drivers/media/pci/hws/hws_reg.h new file mode 100644 index 000000000000..224573595240 --- /dev/null +++ b/drivers/media/pci/hws/hws_reg.h @@ -0,0 +1,144 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef _HWS_PCIE_REG_H +#define _HWS_PCIE_REG_H + +#include +#include + +#define XDMA_CHANNEL_NUM_MAX (1) +#define MAX_NUM_ENGINES (XDMA_CHANNEL_NUM_MAX * 2) + +#define PCIE_BARADDROFSIZE 4u + +#define PCI_BUS_ACCESS_BASE 0x00000000U +#define INT_EN_REG_BASE (PCI_BUS_ACCESS_BASE + 0x0134U) +#define PCIEBR_EN_REG_BASE (PCI_BUS_ACCESS_BASE + 0x0148U) +#define PCIE_INT_DEC_REG_BASE (PCI_BUS_ACCESS_BASE + 0x0138U) + +#define HWS_INT_EN_MASK 0x0003FFFFU + +#define PCIEBAR_AXI_BASE 0x20000000U + +#define CTL_REG_ACC_BASE 0x0 +#define PCI_ADDR_TABLE_BASE CTL_REG_ACC_BASE + +#define CVBS_IN_BASE 0x00004000U +#define CVBS_IN_BUF_BASE (CVBS_IN_BASE + (16U * PCIE_BARADDROFSIZE)) +#define CVBS_IN_BUF_BASE2 (CVBS_IN_BASE + (50U * PCIE_BARADDROFSIZE)) + +/* 2 Mib */ +#define MAX_L_VIDEO_SIZE 0x200000U + +#define PCI_E_BAR_PAGE_SIZE 0x20000000 +#define PCI_E_BAR_ADD_MASK 0xE0000000 +#define PCI_E_BAR_ADD_LOWMASK 0x1FFFFFFF + +#define MAX_VID_CHANNELS 4 + +#define MAX_MM_VIDEO_SIZE SZ_4M + +#define MAX_VIDEO_HW_W 1920 +#define MAX_VIDEO_HW_H 1080 +#define MAX_VIDEO_SCALER_SIZE (1920U * 1080U * 2U) + +#define MIN_VAMP_BRIGHTNESS_UNITS 0 +#define MAX_VAMP_BRIGHTNESS_UNITS 0xff + +#define MIN_VAMP_CONTRAST_UNITS 0 +#define MAX_VAMP_CONTRAST_UNITS 0xff + +#define MIN_VAMP_SATURATION_UNITS 0 +#define MAX_VAMP_SATURATION_UNITS 0xff + +#define MIN_VAMP_HUE_UNITS 0 +#define MAX_VAMP_HUE_UNITS 0xff + +#define HWS_BRIGHTNESS_DEFAULT 0x80 +#define HWS_CONTRAST_DEFAULT 0x80 +#define HWS_SATURATION_DEFAULT 0x80 +#define HWS_HUE_DEFAULT 0x00 + +/* Core/global status. */ +#define HWS_REG_SYS_STATUS (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE) +/* bit3: DMA busy, bit2: int, ... */ + +#define HWS_SYS_DMA_BUSY_BIT BIT(3) /* 0x08 = DMA busy flag */ + +#define HWS_REG_DEC_MODE (CVBS_IN_BASE + 0 * PCIE_BARADDROFSIZE) +/* Main control register */ +#define HWS_REG_CTL (CVBS_IN_BASE + 4 * PCIE_BARADDROFSIZE) +#define HWS_CTL_IRQ_ENABLE_BIT BIT(0) /* Global interrupt enable bit */ +/* Write 0x00 to fully reset decoder, + * set bit 31=1 to "start run", + * low byte=0x13 selects YUYV/BT.709/etc, + * in ReadChipId() we also write 0x00 and 0x10 here for chip-ID sequencing. + */ + +/* per-pipe base: 0x4000, stride 0x800 ------------------------------------ */ +#define HWS_REG_PIPE_BASE(n) (CVBS_IN_BASE + ((n) * 0x800)) +#define HWS_REG_HPD(n) (HWS_REG_PIPE_BASE(n) + 0x14) /* +5 V & HPD */ + +/* handy bit masks */ +#define HWS_HPD_BIT BIT(0) /* hot-plug detect */ +#define HWS_5V_BIT BIT(3) /* cable +5-volt */ + +/* Per-channel done flags. */ +#define HWS_REG_INT_STATUS (CVBS_IN_BASE + 1 * PCIE_BARADDROFSIZE) +#define HWS_SYS_BUSY_BIT BIT(2) /* matches old 0x04 test */ + +/* Capture enable switches. */ +/* bit0-3: CH0-CH3 video enable */ +#define HWS_REG_VCAP_ENABLE (CVBS_IN_BASE + 2 * PCIE_BARADDROFSIZE) +/* bits0-3: signal present, bits8-11: interlace */ +#define HWS_REG_ACTIVE_STATUS (CVBS_IN_BASE + 5 * PCIE_BARADDROFSIZE) +/* bits0-3: HDCP detected */ +#define HWS_REG_HDCP_STATUS (CVBS_IN_BASE + 8 * PCIE_BARADDROFSIZE) +#define HWS_REG_DMA_MAX_SIZE (CVBS_IN_BASE + 9 * PCIE_BARADDROFSIZE) + +/* Buffer addresses (written once during init/reset). */ +/* Base of host-visible buffer. */ +#define HWS_REG_VBUF1_ADDR (CVBS_IN_BASE + 25 * PCIE_BARADDROFSIZE) +/* Per-channel DMA address. */ +#define HWS_REG_DMA_ADDR(ch) (CVBS_IN_BASE + (26 + (ch)) * PCIE_BARADDROFSIZE) + +/* Per-channel live buffer toggles (read-only). */ +#define HWS_REG_VBUF_TOGGLE(ch) (CVBS_IN_BASE + (32 + (ch)) * PCIE_BARADDROFSIZE) +/* + * Returns 0 or 1 = which half of the video ring the DMA engine is + * currently filling for channel *ch* (0-3). + */ + +/* Per-interrupt bits (video 0-3). */ +#define HWS_INT_VDONE_BIT(ch) BIT(ch) /* 0x01,0x02,0x04,0x08 */ + +#define HWS_REG_INT_ACK (CVBS_IN_BASE + 0x4000 + 1 * PCIE_BARADDROFSIZE) + +/* 16-bit W | 16-bit H. */ +#define HWS_REG_IN_RES(ch) (CVBS_IN_BASE + (90 + (ch) * 2) * PCIE_BARADDROFSIZE) +/* B|C|H|S packed bytes. */ +#define HWS_REG_BCHS(ch) (CVBS_IN_BASE + (91 + (ch) * 2) * PCIE_BARADDROFSIZE) + +/* Input fps. */ +#define HWS_REG_FRAME_RATE(ch) (CVBS_IN_BASE + (110 + (ch)) * PCIE_BARADDROFSIZE) +/* Programmed out W|H. */ +#define HWS_REG_OUT_RES(ch) (CVBS_IN_BASE + (120 + (ch)) * PCIE_BARADDROFSIZE) +/* Programmed out fps. */ +#define HWS_REG_OUT_FRAME_RATE(ch) (CVBS_IN_BASE + (130 + (ch)) * PCIE_BARADDROFSIZE) + +/* Device version/port ID/subversion register. */ +#define HWS_REG_DEVICE_INFO (CVBS_IN_BASE + 88 * PCIE_BARADDROFSIZE) +/* + * Reading this 32-bit word returns: + * bits 7:0 = "device version" + * bits 15:8 = "device sub-version" + * bits 23:24 = "HW key / port ID" etc. + * bits 31:28 = "support YV12" flags + */ + +/* Convenience aliases for individual channels. */ +#define HWS_REG_VBUF_TOGGLE_CH0 HWS_REG_VBUF_TOGGLE(0) +#define HWS_REG_VBUF_TOGGLE_CH1 HWS_REG_VBUF_TOGGLE(1) +#define HWS_REG_VBUF_TOGGLE_CH2 HWS_REG_VBUF_TOGGLE(2) +#define HWS_REG_VBUF_TOGGLE_CH3 HWS_REG_VBUF_TOGGLE(3) + +#endif /* _HWS_PCIE_REG_H */ diff --git a/drivers/media/pci/hws/hws_v4l2_ioctl.c b/drivers/media/pci/hws/hws_v4l2_ioctl.c new file mode 100644 index 000000000000..a9a7597f76e1 --- /dev/null +++ b/drivers/media/pci/hws/hws_v4l2_ioctl.c @@ -0,0 +1,778 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "hws.h" +#include "hws_reg.h" +#include "hws_video.h" +#include "hws_v4l2_ioctl.h" + +struct hws_dv_mode { + struct v4l2_dv_timings timings; + u32 refresh_hz; +}; + +static const struct hws_dv_mode * +hws_find_dv_by_wh(u32 w, u32 h, bool interlaced); + +static const struct hws_dv_mode hws_dv_modes[] = { + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1920, + .height = 1080, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1280, + .height = 720, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 720, + .height = 480, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 720, + .height = 576, + .interlaced = 0, + }, + }, + 50, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 800, + .height = 600, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 640, + .height = 480, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1024, + .height = 768, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1280, + .height = 768, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1280, + .height = 800, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1280, + .height = 1024, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1360, + .height = 768, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1440, + .height = 900, + .interlaced = 0, + }, + }, + 60, + }, + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1680, + .height = 1050, + .interlaced = 0, + }, + }, + 60, + }, + /* Portrait */ + { + { + .type = V4L2_DV_BT_656_1120, + .bt = { + .width = 1080, + .height = 1920, + .interlaced = 0, + }, + }, + 60, + }, +}; + +static const size_t hws_dv_modes_cnt = ARRAY_SIZE(hws_dv_modes); + +/* YUYV: 16 bpp; align to 64 as you did elsewhere */ +static inline u32 hws_calc_bpl_yuyv(u32 w) { return ALIGN(w * 2, 64); } +static inline u32 hws_calc_size_yuyv(u32 w, u32 h) { return hws_calc_bpl_yuyv(w) * h; } +static inline u32 hws_calc_half_size(u32 sizeimage) +{ + return sizeimage / 2; +} + +static inline void hws_hw_write_bchs(struct hws_pcie_dev *hws, unsigned int ch, + u8 br, u8 co, u8 hu, u8 sa) +{ + u32 packed = (sa << 24) | (hu << 16) | (co << 8) | br; + + if (!hws || !hws->bar0_base || ch >= hws->max_channels) + return; + writel_relaxed(packed, hws->bar0_base + HWS_REG_BCHS(ch)); + (void)readl(hws->bar0_base + HWS_REG_BCHS(ch)); /* post write */ +} + +/* Helper: find a supported DV mode by W/H + interlace flag */ +static const struct hws_dv_mode * +hws_match_supported_dv(const struct v4l2_dv_timings *req) +{ + const struct v4l2_bt_timings *bt; + + if (!req || req->type != V4L2_DV_BT_656_1120) + return NULL; + + bt = &req->bt; + return hws_find_dv_by_wh(bt->width, bt->height, !!bt->interlaced); +} + +/* Helper: find a supported DV mode by W/H + interlace flag */ +static const struct hws_dv_mode * +hws_find_dv_by_wh(u32 w, u32 h, bool interlaced) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(hws_dv_modes); i++) { + const struct hws_dv_mode *t = &hws_dv_modes[i]; + const struct v4l2_bt_timings *bt = &t->timings.bt; + + if (t->timings.type != V4L2_DV_BT_656_1120) + continue; + + if (bt->width == w && bt->height == h && + !!bt->interlaced == interlaced) + return t; + } + return NULL; +} + +static bool hws_get_live_dv_geometry(struct hws_video *vid, + u32 *w, u32 *h, bool *interlaced) +{ + struct hws_pcie_dev *pdx; + u32 reg; + + if (!vid) + return false; + + pdx = vid->parent; + if (!pdx || !pdx->bar0_base) + return false; + + reg = readl(pdx->bar0_base + HWS_REG_IN_RES(vid->channel_index)); + if (!reg || reg == 0xFFFFFFFF) + return false; + + if (w) + *w = reg & 0xFFFF; + if (h) + *h = (reg >> 16) & 0xFFFF; + if (interlaced) { + reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS); + *interlaced = !!(reg & BIT(8 + vid->channel_index)); + } + return true; +} + +static u32 hws_pick_fps_from_mode(u32 w, u32 h, bool interlaced) +{ + const struct hws_dv_mode *m = hws_find_dv_by_wh(w, h, interlaced); + + if (m && m->refresh_hz) + return m->refresh_hz; + /* Fallback to a sane default */ + return 60; +} + +/* Query the *current detected* DV timings on the input. + * If you have a real hardware detector, call it here; otherwise we + * derive from the cached pix state and map to the closest supported DV mode. + */ +int hws_vidioc_query_dv_timings(struct file *file, void *fh, + struct v4l2_dv_timings *timings) +{ + struct hws_video *vid = video_drvdata(file); + const struct hws_dv_mode *m; + u32 w, h; + bool interlace; + + if (!timings) + return -EINVAL; + + w = vid->pix.width; + h = vid->pix.height; + interlace = vid->pix.interlaced; + (void)hws_get_live_dv_geometry(vid, &w, &h, &interlace); + /* Map current (live if available, otherwise cached) WxH/interlace + * to one of our supported modes. + */ + m = hws_find_dv_by_wh(w, h, !!interlace); + if (!m) + return -ENOLINK; + + *timings = m->timings; + vid->cur_dv_timings = m->timings; + vid->current_fps = m->refresh_hz; + return 0; +} + +/* Enumerate the Nth supported DV timings from our static table. */ +int hws_vidioc_enum_dv_timings(struct file *file, void *fh, + struct v4l2_enum_dv_timings *edv) +{ + if (!edv) + return -EINVAL; + + if (edv->pad) + return -EINVAL; + + if (edv->index >= hws_dv_modes_cnt) + return -EINVAL; + + edv->timings = hws_dv_modes[edv->index].timings; + return 0; +} + +/* Get the *currently configured* DV timings. */ +int hws_vidioc_g_dv_timings(struct file *file, void *fh, + struct v4l2_dv_timings *timings) +{ + struct hws_video *vid = video_drvdata(file); + + if (!timings) + return -EINVAL; + + *timings = vid->cur_dv_timings; + return 0; +} + +static inline void hws_set_colorimetry_state(struct hws_pix_state *p) +{ + bool sd = p->height <= 576; + + p->colorspace = sd ? V4L2_COLORSPACE_SMPTE170M : V4L2_COLORSPACE_REC709; + p->ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT; + p->quantization = V4L2_QUANTIZATION_FULL_RANGE; + p->xfer_func = V4L2_XFER_FUNC_DEFAULT; +} + +/* Set DV timings: must match one of our supported modes. + * If buffers are queued and this implies a size change, we reject with -EBUSY. + * Otherwise we update pix state and (optionally) reprogram the HW. + */ +int hws_vidioc_s_dv_timings(struct file *file, void *fh, + struct v4l2_dv_timings *timings) +{ + struct hws_video *vid = video_drvdata(file); + const struct hws_dv_mode *m; + const struct v4l2_bt_timings *bt; + u32 new_w, new_h; + bool interlaced; + int ret = 0; + unsigned long was_busy; + + if (!timings) + return -EINVAL; + + m = hws_match_supported_dv(timings); + if (!m) + return -EINVAL; + + bt = &m->timings.bt; + if (bt->interlaced) + return -EINVAL; /* only progressive modes are advertised */ + new_w = bt->width; + new_h = bt->height; + interlaced = false; + + lockdep_assert_held(&vid->state_lock); + + /* If vb2 has active buffers and size would change, reject. */ + was_busy = vb2_is_busy(&vid->buffer_queue); + if (was_busy && + (new_w != vid->pix.width || new_h != vid->pix.height || + interlaced != vid->pix.interlaced)) { + ret = -EBUSY; + return ret; + } + + /* Update software pixel state (and recalc sizes) */ + vid->pix.width = new_w; + vid->pix.height = new_h; + vid->pix.field = interlaced ? V4L2_FIELD_INTERLACED + : V4L2_FIELD_NONE; + vid->pix.interlaced = interlaced; + vid->pix.fourcc = V4L2_PIX_FMT_YUYV; + + hws_set_colorimetry_state(&vid->pix); + + /* Recompute stride/sizeimage/half_size using your helper */ + vid->pix.bytesperline = hws_calc_bpl_yuyv(new_w); + vid->pix.sizeimage = hws_calc_size_yuyv(new_w, new_h); + vid->pix.half_size = hws_calc_half_size(vid->pix.sizeimage); + vid->cur_dv_timings = m->timings; + vid->current_fps = m->refresh_hz; + if (!was_busy) + vid->alloc_sizeimage = vid->pix.sizeimage; + return ret; +} + +/* Report DV timings capability: advertise BT.656/1120 with + * the min/max WxH derived from our table and basic progressive support. + */ +int hws_vidioc_dv_timings_cap(struct file *file, void *fh, + struct v4l2_dv_timings_cap *cap) +{ + u32 min_w = ~0U, min_h = ~0U; + u32 max_w = 0, max_h = 0; + size_t i, n = 0; + + if (!cap) + return -EINVAL; + + memset(cap, 0, sizeof(*cap)); + cap->type = V4L2_DV_BT_656_1120; + + for (i = 0; i < ARRAY_SIZE(hws_dv_modes); i++) { + const struct v4l2_bt_timings *bt = &hws_dv_modes[i].timings.bt; + + if (hws_dv_modes[i].timings.type != V4L2_DV_BT_656_1120) + continue; + n++; + + if (bt->width < min_w) + min_w = bt->width; + if (bt->height < min_h) + min_h = bt->height; + if (bt->width > max_w) + max_w = bt->width; + if (bt->height > max_h) + max_h = bt->height; + } + + /* If the table was empty, fail gracefully. */ + if (!n || min_w == U32_MAX) + return -ENODATA; + + cap->bt.min_width = min_w; + cap->bt.max_width = max_w; + cap->bt.min_height = min_h; + cap->bt.max_height = max_h; + + /* We support both CEA-861- and VESA-style modes in the list. */ + cap->bt.standards = + V4L2_DV_BT_STD_CEA861 | V4L2_DV_BT_STD_DMT | V4L2_DV_BT_STD_CVT; + + /* Progressive only, unless your table includes interlaced entries. */ + cap->bt.capabilities = V4L2_DV_BT_CAP_PROGRESSIVE; + + /* Leave pixelclock/porch limits unconstrained (0) for now. */ + return 0; +} + +static int hws_s_ctrl(struct v4l2_ctrl *ctrl) +{ + struct hws_video *vid = + container_of(ctrl->handler, struct hws_video, control_handler); + struct hws_pcie_dev *pdx = vid->parent; + bool program = false; + + switch (ctrl->id) { + case V4L2_CID_BRIGHTNESS: + vid->current_brightness = ctrl->val; + program = true; + break; + case V4L2_CID_CONTRAST: + vid->current_contrast = ctrl->val; + program = true; + break; + case V4L2_CID_SATURATION: + vid->current_saturation = ctrl->val; + program = true; + break; + case V4L2_CID_HUE: + vid->current_hue = ctrl->val; + program = true; + break; + default: + return -EINVAL; + } + + if (program) { + hws_hw_write_bchs(pdx, vid->channel_index, + (u8)vid->current_brightness, + (u8)vid->current_contrast, + (u8)vid->current_hue, + (u8)vid->current_saturation); + } + return 0; +} + +const struct v4l2_ctrl_ops hws_ctrl_ops = { + .s_ctrl = hws_s_ctrl, +}; + +int hws_vidioc_querycap(struct file *file, void *priv, struct v4l2_capability *cap) +{ + struct hws_video *vid = video_drvdata(file); + struct hws_pcie_dev *pdev = vid->parent; + int vi_index = vid->channel_index + 1; /* keep it simple */ + + strscpy(cap->driver, KBUILD_MODNAME, sizeof(cap->driver)); + snprintf(cap->card, sizeof(cap->card), + "AVMatrix HWS Capture %d", vi_index); + snprintf(cap->bus_info, sizeof(cap->bus_info), "PCI:%s", dev_name(&pdev->pdev->dev)); + + cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; + cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS; + return 0; +} + +int hws_vidioc_enum_fmt_vid_cap(struct file *file, void *priv_fh, struct v4l2_fmtdesc *f) +{ + if (f->index != 0) + return -EINVAL; /* only one format */ + + f->pixelformat = V4L2_PIX_FMT_YUYV; + return 0; +} + +int hws_vidioc_enum_frameintervals(struct file *file, void *fh, + struct v4l2_frmivalenum *fival) +{ + const struct hws_dv_mode *mode; + + if (fival->index) + return -EINVAL; + + if (fival->pixel_format != V4L2_PIX_FMT_YUYV) + return -EINVAL; + + mode = hws_find_dv_by_wh(fival->width, fival->height, false); + if (!mode) + return -EINVAL; + + fival->type = V4L2_FRMIVAL_TYPE_DISCRETE; + fival->discrete.numerator = 1; + fival->discrete.denominator = mode->refresh_hz ?: 60; + + return 0; +} + +int hws_vidioc_g_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *fmt) +{ + struct hws_video *vid = video_drvdata(file); + + fmt->fmt.pix.width = vid->pix.width; + fmt->fmt.pix.height = vid->pix.height; + fmt->fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; + fmt->fmt.pix.field = vid->pix.field; + fmt->fmt.pix.bytesperline = vid->pix.bytesperline; + fmt->fmt.pix.sizeimage = vid->pix.sizeimage; + fmt->fmt.pix.colorspace = vid->pix.colorspace; + fmt->fmt.pix.ycbcr_enc = vid->pix.ycbcr_enc; + fmt->fmt.pix.quantization = vid->pix.quantization; + fmt->fmt.pix.xfer_func = vid->pix.xfer_func; + return 0; +} + +static inline void hws_set_colorimetry_fmt(struct v4l2_pix_format *p) +{ + bool sd = p->height <= 576; + + p->colorspace = sd ? V4L2_COLORSPACE_SMPTE170M : V4L2_COLORSPACE_REC709; + p->ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT; + p->quantization = V4L2_QUANTIZATION_FULL_RANGE; + p->xfer_func = V4L2_XFER_FUNC_DEFAULT; +} + +int hws_vidioc_try_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *f) +{ + struct hws_video *vid = file ? video_drvdata(file) : NULL; + struct hws_pcie_dev *pdev = vid ? vid->parent : NULL; + struct v4l2_pix_format *pix = &f->fmt.pix; + u32 req_w = pix->width, req_h = pix->height; + u32 w, h, min_bpl, bpl; + size_t size; /* wider than u32 for overflow check */ + size_t max_frame = pdev ? pdev->max_hw_video_buf_sz : MAX_MM_VIDEO_SIZE; + + /* Only YUYV */ + pix->pixelformat = V4L2_PIX_FMT_YUYV; + + /* Defaults then clamp */ + w = (req_w ? req_w : 640); + h = (req_h ? req_h : 480); + if (w > MAX_VIDEO_HW_W) + w = MAX_VIDEO_HW_W; + if (h > MAX_VIDEO_HW_H) + h = MAX_VIDEO_HW_H; + if (!w) + w = 640; /* hard fallback in case macros are odd */ + if (!h) + h = 480; + + /* Field policy */ + pix->field = V4L2_FIELD_NONE; + + /* Stride policy for packed 16bpp, 64B align */ + min_bpl = ALIGN(w * 2, 64); + + /* Bound requested bpl to something sane, then align */ + bpl = pix->bytesperline; + if (bpl < min_bpl) { + bpl = min_bpl; + } else { + /* Cap at 16x width to avoid silly values that overflow sizeimage */ + u32 max_bpl = ALIGN(w * 2 * 16, 64); + + if (bpl > max_bpl) + bpl = max_bpl; + bpl = ALIGN(bpl, 64); + } + if (h && max_frame) { + size_t max_bpl_hw = max_frame / h; + + if (max_bpl_hw < min_bpl) + return -ERANGE; + max_bpl_hw = rounddown(max_bpl_hw, 64); + if (!max_bpl_hw) + return -ERANGE; + if (bpl > max_bpl_hw) { + if (pdev) + dev_dbg(&pdev->pdev->dev, + "try_fmt: clamp bpl %u -> %zu due to hw buf cap %zu\n", + bpl, max_bpl_hw, max_frame); + bpl = (u32)max_bpl_hw; + } + } + size = (size_t)bpl * (size_t)h; + if (size > max_frame) + return -ERANGE; + + pix->width = w; + pix->height = h; + pix->bytesperline = bpl; + pix->sizeimage = (u32)size; /* logical size, not page-aligned */ + + hws_set_colorimetry_fmt(pix); + if (pdev) + dev_dbg(&pdev->pdev->dev, + "try_fmt: w=%u h=%u bpl=%u size=%u field=%u\n", + pix->width, pix->height, pix->bytesperline, + pix->sizeimage, pix->field); + return 0; +} + +int hws_vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f) +{ + struct hws_video *vid = video_drvdata(file); + int ret; + + if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE) + return -EINVAL; + + /* Normalize the request */ + ret = hws_vidioc_try_fmt_vid_cap(file, priv, f); + if (ret) + return ret; + + /* Don’t allow size changes while buffers are queued */ + if (vb2_is_busy(&vid->buffer_queue)) { + if (f->fmt.pix.width != vid->pix.width || + f->fmt.pix.height != vid->pix.height || + f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV) { + return -EBUSY; + } + } + + /* Apply to driver state */ + vid->pix.width = f->fmt.pix.width; + vid->pix.height = f->fmt.pix.height; + vid->pix.fourcc = V4L2_PIX_FMT_YUYV; + vid->pix.field = f->fmt.pix.field; + vid->pix.colorspace = f->fmt.pix.colorspace; + vid->pix.ycbcr_enc = f->fmt.pix.ycbcr_enc; + vid->pix.quantization = f->fmt.pix.quantization; + vid->pix.xfer_func = f->fmt.pix.xfer_func; + + /* Update sizes (use helper if you prefer strict alignment math) */ + vid->pix.bytesperline = f->fmt.pix.bytesperline; /* aligned */ + vid->pix.sizeimage = f->fmt.pix.sizeimage; /* logical */ + vid->pix.half_size = hws_calc_half_size(vid->pix.sizeimage); + vid->pix.interlaced = false; + hws_set_current_dv_timings(vid, vid->pix.width, vid->pix.height, + vid->pix.interlaced); + vid->current_fps = hws_pick_fps_from_mode(vid->pix.width, + vid->pix.height, + vid->pix.interlaced); + /* Or: + * hws_calc_sizeimage(vid, vid->pix.width, vid->pix.height, false); + */ + + /* Refresh vb2 watermark when idle */ + if (!vb2_is_busy(&vid->buffer_queue)) + vid->alloc_sizeimage = PAGE_ALIGN(vid->pix.sizeimage); + dev_dbg(&vid->parent->pdev->dev, + "s_fmt: w=%u h=%u bpl=%u size=%u alloc=%u\n", + vid->pix.width, vid->pix.height, vid->pix.bytesperline, + vid->pix.sizeimage, vid->alloc_sizeimage); + + return 0; +} + +int hws_vidioc_g_parm(struct file *file, void *fh, struct v4l2_streamparm *param) +{ + struct hws_video *vid = video_drvdata(file); + u32 fps; + + if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE) + return -EINVAL; + + fps = vid->current_fps ? vid->current_fps : 60; + + /* Report cached frame rate; expose timeperframe capability */ + param->parm.capture.capability = V4L2_CAP_TIMEPERFRAME; + param->parm.capture.capturemode = 0; + param->parm.capture.timeperframe.numerator = 1; + param->parm.capture.timeperframe.denominator = fps; + param->parm.capture.extendedmode = 0; + param->parm.capture.readbuffers = 0; + + return 0; +} + +int hws_vidioc_enum_input(struct file *file, void *priv, + struct v4l2_input *input) +{ + if (input->index) + return -EINVAL; + input->type = V4L2_INPUT_TYPE_CAMERA; + strscpy(input->name, KBUILD_MODNAME, sizeof(input->name)); + input->capabilities = V4L2_IN_CAP_DV_TIMINGS; + input->status = 0; + + return 0; +} + +int hws_vidioc_g_input(struct file *file, void *priv, unsigned int *index) +{ + *index = 0; + return 0; +} + +int hws_vidioc_s_input(struct file *file, void *priv, unsigned int i) +{ + return i ? -EINVAL : 0; +} + +int hws_vidioc_s_parm(struct file *file, void *fh, struct v4l2_streamparm *param) +{ + struct hws_video *vid = video_drvdata(file); + struct v4l2_captureparm *cap; + u32 fps; + + if (param->type != V4L2_BUF_TYPE_VIDEO_CAPTURE) + return -EINVAL; + + cap = ¶m->parm.capture; + + fps = vid->current_fps ? vid->current_fps : 60; + cap->timeperframe.denominator = fps; + cap->timeperframe.numerator = 1; + cap->capability = V4L2_CAP_TIMEPERFRAME; + cap->capturemode = 0; + cap->extendedmode = 0; + /* readbuffers left unchanged or zero; vb2 handles queue depth */ + + return 0; +} diff --git a/drivers/media/pci/hws/hws_v4l2_ioctl.h b/drivers/media/pci/hws/hws_v4l2_ioctl.h new file mode 100644 index 000000000000..f20e6aadff67 --- /dev/null +++ b/drivers/media/pci/hws/hws_v4l2_ioctl.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef HWS_V4L2_IOCTL_H +#define HWS_V4L2_IOCTL_H + +#include + +#include +#include + +extern const struct v4l2_ctrl_ops hws_ctrl_ops; + +int hws_vidioc_querycap(struct file *file, void *priv, + struct v4l2_capability *cap); +int hws_vidioc_enum_fmt_vid_cap(struct file *file, void *priv_fh, + struct v4l2_fmtdesc *f); +int hws_vidioc_enum_frameintervals(struct file *file, void *fh, + struct v4l2_frmivalenum *fival); +int hws_vidioc_g_fmt_vid_cap(struct file *file, void *fh, + struct v4l2_format *fmt); +int hws_vidioc_try_fmt_vid_cap(struct file *file, void *fh, + struct v4l2_format *f); +int hws_vidioc_s_fmt_vid_cap(struct file *file, void *priv, + struct v4l2_format *f); +int hws_vidioc_g_parm(struct file *file, void *fh, + struct v4l2_streamparm *setfps); +int hws_vidioc_s_parm(struct file *file, void *fh, + struct v4l2_streamparm *a); +int hws_vidioc_enum_input(struct file *file, void *priv, + struct v4l2_input *i); +int hws_vidioc_g_input(struct file *file, void *priv, unsigned int *i); +int hws_vidioc_s_input(struct file *file, void *priv, unsigned int i); +int hws_vidioc_dv_timings_cap(struct file *file, void *fh, + struct v4l2_dv_timings_cap *cap); +int hws_vidioc_s_dv_timings(struct file *file, void *fh, + struct v4l2_dv_timings *timings); +int hws_vidioc_g_dv_timings(struct file *file, void *fh, + struct v4l2_dv_timings *timings); +int hws_vidioc_enum_dv_timings(struct file *file, void *fh, + struct v4l2_enum_dv_timings *edv); +int hws_vidioc_query_dv_timings(struct file *file, void *fh, + struct v4l2_dv_timings *timings); + +#endif diff --git a/drivers/media/pci/hws/hws_video.c b/drivers/media/pci/hws/hws_video.c new file mode 100644 index 000000000000..9fcf40a12ec3 --- /dev/null +++ b/drivers/media/pci/hws/hws_video.c @@ -0,0 +1,1546 @@ +// SPDX-License-Identifier: GPL-2.0-only +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "hws.h" +#include "hws_reg.h" +#include "hws_video.h" +#include "hws_irq.h" +#include "hws_v4l2_ioctl.h" + +#define HWS_REMAP_SLOT_OFF(ch) (0x208 + (ch) * 8) /* one 64-bit slot per ch */ +#define HWS_BUF_BASE_OFF(ch) (CVBS_IN_BUF_BASE + (ch) * PCIE_BARADDROFSIZE) +#define HWS_HALF_SZ_OFF(ch) (CVBS_IN_BUF_BASE2 + (ch) * PCIE_BARADDROFSIZE) + +static void update_live_resolution(struct hws_pcie_dev *pdx, unsigned int ch); +static bool hws_update_active_interlace(struct hws_pcie_dev *pdx, + unsigned int ch); +static void handle_hwv2_path(struct hws_pcie_dev *hws, unsigned int ch); +static void handle_legacy_path(struct hws_pcie_dev *hws, unsigned int ch); +static u32 hws_calc_sizeimage(struct hws_video *v, u16 w, u16 h, + bool interlaced); + +/* DMA helper functions */ +static void hws_program_dma_window(struct hws_video *vid, dma_addr_t dma); +static struct hwsvideo_buffer * +hws_take_queued_buffer_locked(struct hws_video *vid); + +#if IS_ENABLED(CONFIG_SYSFS) +static ssize_t resolution_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct video_device *vdev = to_video_device(dev); + struct hws_video *vid = video_get_drvdata(vdev); + struct hws_pcie_dev *hws; + u32 res_reg; + u16 w, h; + bool interlaced; + + if (!vid) + return -ENODEV; + + hws = vid->parent; + if (!hws || !hws->bar0_base) + return sysfs_emit(buf, "unknown\n"); + + res_reg = readl(hws->bar0_base + HWS_REG_IN_RES(vid->channel_index)); + if (!res_reg || res_reg == 0xFFFFFFFF) + return sysfs_emit(buf, "unknown\n"); + + w = res_reg & 0xFFFF; + h = (res_reg >> 16) & 0xFFFF; + + interlaced = + !!(readl(hws->bar0_base + HWS_REG_ACTIVE_STATUS) & + BIT(8 + vid->channel_index)); + + return sysfs_emit(buf, "%ux%u%s\n", w, h, interlaced ? "i" : "p"); +} +static DEVICE_ATTR_RO(resolution); + +static inline int hws_resolution_create(struct video_device *vdev) +{ + return device_create_file(&vdev->dev, &dev_attr_resolution); +} + +static inline void hws_resolution_remove(struct video_device *vdev) +{ + device_remove_file(&vdev->dev, &dev_attr_resolution); +} +#else +static inline int hws_resolution_create(struct video_device *vdev) +{ + return 0; +} + +static inline void hws_resolution_remove(struct video_device *vdev) +{ +} +#endif + +static bool dma_window_verify; +module_param_named(dma_window_verify, dma_window_verify, bool, 0644); +MODULE_PARM_DESC(dma_window_verify, + "Read back DMA window registers after programming (debug)"); + +void hws_set_dma_doorbell(struct hws_pcie_dev *hws, unsigned int ch, + dma_addr_t dma, const char *tag) +{ + iowrite32(lower_32_bits(dma), hws->bar0_base + HWS_REG_DMA_ADDR(ch)); + dev_dbg(&hws->pdev->dev, "dma_doorbell ch%u: dma=0x%llx tag=%s\n", ch, + (u64)dma, tag ? tag : ""); +} + +static void hws_program_dma_window(struct hws_video *vid, dma_addr_t dma) +{ + const u32 addr_mask = PCI_E_BAR_ADD_MASK; // 0xE0000000 + const u32 addr_low_mask = PCI_E_BAR_ADD_LOWMASK; // 0x1FFFFFFF + struct hws_pcie_dev *hws = vid->parent; + unsigned int ch = vid->channel_index; + u32 table_off = HWS_REMAP_SLOT_OFF(ch); + u32 lo = lower_32_bits(dma); + u32 hi = upper_32_bits(dma); + u32 pci_addr = lo & addr_low_mask; // low 29 bits inside 512MB window + u32 page_lo = lo & addr_mask; // bits 31..29 only (page bits) + + bool wrote = false; + + /* Remap entry only when DMA crosses into a new 512 MB page */ + if (!vid->window_valid || vid->last_dma_hi != hi || + vid->last_dma_page != page_lo) { + writel(hi, hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off); + writel(page_lo, + hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off + + PCIE_BARADDROFSIZE); + vid->last_dma_hi = hi; + vid->last_dma_page = page_lo; + wrote = true; + } + + /* Base pointer only needs low 29 bits */ + if (!vid->window_valid || vid->last_pci_addr != pci_addr) { + writel((ch + 1) * PCIEBAR_AXI_BASE + pci_addr, + hws->bar0_base + HWS_BUF_BASE_OFF(ch)); + vid->last_pci_addr = pci_addr; + wrote = true; + } + + /* Half-size only changes when resolution changes */ + if (!vid->window_valid || vid->last_half16 != vid->pix.half_size / 16) { + writel(vid->pix.half_size / 16, + hws->bar0_base + HWS_HALF_SZ_OFF(ch)); + vid->last_half16 = vid->pix.half_size / 16; + wrote = true; + } + + vid->window_valid = true; + + if (unlikely(dma_window_verify) && wrote) { + u32 r_hi = + readl(hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off); + u32 r_lo = + readl(hws->bar0_base + PCI_ADDR_TABLE_BASE + table_off + + PCIE_BARADDROFSIZE); + u32 r_base = readl(hws->bar0_base + HWS_BUF_BASE_OFF(ch)); + u32 r_half = readl(hws->bar0_base + HWS_HALF_SZ_OFF(ch)); + + dev_dbg(&hws->pdev->dev, + "ch%u remap verify: hi=0x%08x page_lo=0x%08x exp_page=0x%08x base=0x%08x exp_base=0x%08x half16B=0x%08x exp_half=0x%08x\n", + ch, r_hi, r_lo, page_lo, r_base, + (ch + 1) * PCIEBAR_AXI_BASE + pci_addr, r_half, + vid->pix.half_size / 16); + } else if (wrote) { + /* Flush posted writes before arming DMA */ + readl_relaxed(hws->bar0_base + HWS_HALF_SZ_OFF(ch)); + } +} + +static struct hwsvideo_buffer * +hws_take_queued_buffer_locked(struct hws_video *vid) +{ + struct hwsvideo_buffer *buf; + + if (!vid || list_empty(&vid->capture_queue)) + return NULL; + + buf = list_first_entry(&vid->capture_queue, + struct hwsvideo_buffer, list); + list_del_init(&buf->list); + if (vid->queued_count) + vid->queued_count--; + return buf; +} + +void hws_prime_next_locked(struct hws_video *vid) +{ + struct hws_pcie_dev *hws; + struct hwsvideo_buffer *next; + dma_addr_t dma; + + if (!vid) + return; + + hws = vid->parent; + if (!hws || !hws->bar0_base) + return; + + if (!READ_ONCE(vid->cap_active) || !vid->active || vid->next_prepared) + return; + + next = hws_take_queued_buffer_locked(vid); + if (!next) + return; + + vid->next_prepared = next; + dma = vb2_dma_contig_plane_dma_addr(&next->vb.vb2_buf, 0); + hws_program_dma_for_addr(hws, vid->channel_index, dma); + iowrite32(lower_32_bits(dma), + hws->bar0_base + HWS_REG_DMA_ADDR(vid->channel_index)); + dev_dbg(&hws->pdev->dev, + "ch%u pre-armed next buffer %p dma=0x%llx\n", + vid->channel_index, next, (u64)dma); +} + +static bool hws_force_no_signal_frame(struct hws_video *v, const char *tag) +{ + struct hws_pcie_dev *hws; + unsigned long flags; + struct hwsvideo_buffer *buf = NULL, *next = NULL; + bool have_next = false; + bool doorbell = false; + + if (!v) + return false; + hws = v->parent; + if (!hws || READ_ONCE(v->stop_requested) || !READ_ONCE(v->cap_active)) + return false; + spin_lock_irqsave(&v->irq_lock, flags); + if (v->active) { + buf = v->active; + v->active = NULL; + buf->slot = 0; + } else if (!list_empty(&v->capture_queue)) { + buf = list_first_entry(&v->capture_queue, + struct hwsvideo_buffer, list); + list_del_init(&buf->list); + if (v->queued_count) + v->queued_count--; + buf->slot = 0; + } + if (v->next_prepared) { + next = v->next_prepared; + v->next_prepared = NULL; + next->slot = 0; + v->active = next; + have_next = true; + } else if (!list_empty(&v->capture_queue)) { + next = list_first_entry(&v->capture_queue, + struct hwsvideo_buffer, list); + list_del_init(&next->list); + if (v->queued_count) + v->queued_count--; + next->slot = 0; + v->active = next; + have_next = true; + } else { + v->active = NULL; + } + spin_unlock_irqrestore(&v->irq_lock, flags); + if (!buf) + return false; + /* Complete buffer with a neutral frame so dequeuers keep running. */ + { + struct vb2_v4l2_buffer *vb2v = &buf->vb; + void *dst = vb2_plane_vaddr(&vb2v->vb2_buf, 0); + + if (dst) + memset(dst, 0x10, v->pix.sizeimage); + vb2_set_plane_payload(&vb2v->vb2_buf, 0, v->pix.sizeimage); + vb2v->sequence = (u32)atomic_inc_return(&v->sequence_number); + vb2v->vb2_buf.timestamp = ktime_get_ns(); + vb2_buffer_done(&vb2v->vb2_buf, VB2_BUF_STATE_DONE); + } + if (have_next && next) { + dma_addr_t dma = + vb2_dma_contig_plane_dma_addr(&next->vb.vb2_buf, 0); + hws_program_dma_for_addr(hws, v->channel_index, dma); + hws_set_dma_doorbell(hws, v->channel_index, dma, + tag ? tag : "nosignal_zero"); + doorbell = true; + } + if (doorbell) { + wmb(); /* ensure descriptors visible before enabling capture */ + hws_enable_video_capture(hws, v->channel_index, true); + } + return true; +} + +static int hws_ctrls_init(struct hws_video *vid) +{ + struct v4l2_ctrl_handler *hdl = &vid->control_handler; + + /* Create BCHS controls plus signal-detect status. */ + v4l2_ctrl_handler_init(hdl, 5); + + vid->ctrl_brightness = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, + V4L2_CID_BRIGHTNESS, + MIN_VAMP_BRIGHTNESS_UNITS, + MAX_VAMP_BRIGHTNESS_UNITS, 1, + HWS_BRIGHTNESS_DEFAULT); + + vid->ctrl_contrast = + v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, V4L2_CID_CONTRAST, + MIN_VAMP_CONTRAST_UNITS, MAX_VAMP_CONTRAST_UNITS, + 1, HWS_CONTRAST_DEFAULT); + + vid->ctrl_saturation = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, + V4L2_CID_SATURATION, + MIN_VAMP_SATURATION_UNITS, + MAX_VAMP_SATURATION_UNITS, 1, + HWS_SATURATION_DEFAULT); + + vid->ctrl_hue = v4l2_ctrl_new_std(hdl, &hws_ctrl_ops, V4L2_CID_HUE, + MIN_VAMP_HUE_UNITS, + MAX_VAMP_HUE_UNITS, 1, + HWS_HUE_DEFAULT); + vid->hotplug_detect_control = v4l2_ctrl_new_std(hdl, NULL, + V4L2_CID_DV_RX_POWER_PRESENT, + 0, 1, 1, 0); + if (vid->hotplug_detect_control) + vid->hotplug_detect_control->flags |= + V4L2_CTRL_FLAG_READ_ONLY; + + if (hdl->error) { + int err = hdl->error; + + v4l2_ctrl_handler_free(hdl); + return err; + } + return 0; +} + +static void hws_video_move_buf_to_done_locked(struct hwsvideo_buffer **buf, + struct list_head *done) +{ + if (!*buf) + return; + + if (list_empty(&(*buf)->list)) + list_add_tail(&(*buf)->list, done); + else + list_move_tail(&(*buf)->list, done); + + *buf = NULL; +} + +static void hws_video_collect_done_locked(struct hws_video *vid, + struct list_head *done) +{ + struct hwsvideo_buffer *buf; + + hws_video_move_buf_to_done_locked(&vid->active, done); + hws_video_move_buf_to_done_locked(&vid->next_prepared, done); + + while (!list_empty(&vid->capture_queue)) { + buf = list_first_entry(&vid->capture_queue, + struct hwsvideo_buffer, list); + list_move_tail(&buf->list, done); + } + + vid->queued_count = 0; +} + +int hws_video_init_channel(struct hws_pcie_dev *pdev, int ch) +{ + struct hws_video *vid; + + /* basic sanity */ + if (!pdev || ch < 0 || ch >= pdev->max_channels) + return -EINVAL; + + vid = &pdev->video[ch]; + + /* hard reset the per-channel struct (safe here since we init everything next) */ + memset(vid, 0, sizeof(*vid)); + + /* identity */ + vid->parent = pdev; + vid->channel_index = ch; + + /* locks & lists */ + mutex_init(&vid->state_lock); + spin_lock_init(&vid->irq_lock); + INIT_LIST_HEAD(&vid->capture_queue); + atomic_set(&vid->sequence_number, 0); + vid->active = NULL; + + /* DMA watchdog removed; retain counters for diagnostics */ + vid->timeout_count = 0; + vid->error_count = 0; + + vid->queued_count = 0; + vid->window_valid = false; + + /* default format (adjust to your HW) */ + vid->pix.width = 1920; + vid->pix.height = 1080; + vid->pix.fourcc = V4L2_PIX_FMT_YUYV; + vid->pix.bytesperline = ALIGN(vid->pix.width * 2, 64); + vid->pix.sizeimage = vid->pix.bytesperline * vid->pix.height; + vid->pix.field = V4L2_FIELD_NONE; + vid->pix.colorspace = V4L2_COLORSPACE_REC709; + vid->pix.ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT; + vid->pix.quantization = V4L2_QUANTIZATION_FULL_RANGE; + vid->pix.xfer_func = V4L2_XFER_FUNC_DEFAULT; + vid->pix.interlaced = false; + vid->pix.half_size = vid->pix.sizeimage / 2; + vid->alloc_sizeimage = vid->pix.sizeimage; + hws_set_current_dv_timings(vid, vid->pix.width, + vid->pix.height, vid->pix.interlaced); + vid->current_fps = 60; + + /* color controls default (mid-scale) */ + vid->current_brightness = 0x80; + vid->current_contrast = 0x80; + vid->current_saturation = 0x80; + vid->current_hue = 0x80; + + /* capture state */ + vid->cap_active = false; + vid->stop_requested = false; + vid->last_buf_half_toggle = 0; + vid->half_seen = false; + vid->signal_loss_cnt = 0; + + /* Create BCHS + DV power-present as modern controls */ + { + int err = hws_ctrls_init(vid); + + if (err) { + dev_err(&pdev->pdev->dev, + "v4l2 ctrl init failed on ch%d: %d\n", ch, err); + return err; + } + } + + return 0; +} + +void hws_video_cleanup_channel(struct hws_pcie_dev *pdev, int ch) +{ + struct hws_video *vid; + unsigned long flags; + struct hwsvideo_buffer *buf, *tmp; + LIST_HEAD(done); + + if (!pdev || ch < 0 || ch >= pdev->max_channels) + return; + + vid = &pdev->video[ch]; + + /* 1) Stop HW best-effort for this channel */ + hws_enable_video_capture(vid->parent, vid->channel_index, false); + + /* 2) Flip software state so IRQ/BH will be no-ops if they run */ + WRITE_ONCE(vid->stop_requested, true); + WRITE_ONCE(vid->cap_active, false); + + /* 3) Ensure the IRQ handler finished any in-flight completions */ + if (vid->parent && vid->parent->irq >= 0) + synchronize_irq(vid->parent->irq); + + /* 4) Drain SW capture queue & in-flight under lock */ + spin_lock_irqsave(&vid->irq_lock, flags); + hws_video_collect_done_locked(vid, &done); + spin_unlock_irqrestore(&vid->irq_lock, flags); + + list_for_each_entry_safe(buf, tmp, &done, list) { + list_del_init(&buf->list); + vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR); + } + + /* 5) Release VB2 queue if initialized */ + if (vid->buffer_queue.ops) + vb2_queue_release(&vid->buffer_queue); + + /* 6) Free V4L2 controls */ + v4l2_ctrl_handler_free(&vid->control_handler); + + /* 7) Unregister the video_device if we own it */ + if (vid->video_device && video_is_registered(vid->video_device)) + video_unregister_device(vid->video_device); + /* If you allocated it with video_device_alloc(), release it here: + * video_device_release(vid->video_device); + */ + vid->video_device = NULL; + + /* 8) Reset simple state (don’t memset the whole struct here) */ + mutex_destroy(&vid->state_lock); + INIT_LIST_HEAD(&vid->capture_queue); + vid->active = NULL; + vid->stop_requested = false; + vid->last_buf_half_toggle = 0; + vid->half_seen = false; + vid->signal_loss_cnt = 0; +} + +/* Convenience cast */ +static inline struct hwsvideo_buffer *to_hwsbuf(struct vb2_buffer *vb) +{ + return container_of(to_vb2_v4l2_buffer(vb), struct hwsvideo_buffer, vb); +} + +static int hws_buf_init(struct vb2_buffer *vb) +{ + struct hwsvideo_buffer *b = to_hwsbuf(vb); + + INIT_LIST_HEAD(&b->list); + return 0; +} + +static void hws_buf_finish(struct vb2_buffer *vb) +{ + /* vb2 core handles cache maintenance for dma-contig buffers */ + (void)vb; +} + +static void hws_buf_cleanup(struct vb2_buffer *vb) +{ + struct hwsvideo_buffer *b = to_hwsbuf(vb); + + if (!list_empty(&b->list)) + list_del_init(&b->list); +} + +void hws_program_dma_for_addr(struct hws_pcie_dev *hws, unsigned int ch, + dma_addr_t dma) +{ + struct hws_video *vid = &hws->video[ch]; + + hws_program_dma_window(vid, dma); +} + +void hws_enable_video_capture(struct hws_pcie_dev *hws, unsigned int chan, + bool on) +{ + u32 status; + + if (!hws || hws->pci_lost || chan >= hws->max_channels) + return; + + status = readl(hws->bar0_base + HWS_REG_VCAP_ENABLE); + status = on ? (status | BIT(chan)) : (status & ~BIT(chan)); + writel(status, hws->bar0_base + HWS_REG_VCAP_ENABLE); + (void)readl(hws->bar0_base + HWS_REG_VCAP_ENABLE); + + WRITE_ONCE(hws->video[chan].cap_active, on); + + dev_dbg(&hws->pdev->dev, "vcap %s ch%u (reg=0x%08x)\n", + on ? "ON" : "OFF", chan, status); +} + +static void hws_seed_dma_windows(struct hws_pcie_dev *hws) +{ + const u32 addr_mask = PCI_E_BAR_ADD_MASK; + const u32 addr_low_mask = PCI_E_BAR_ADD_LOWMASK; + u32 table = 0x208; /* one 64-bit entry per channel */ + unsigned int ch; + + if (!hws || !hws->bar0_base) + return; + + /* If cur_max_video_ch isn’t set yet, default to max_channels */ + if (!hws->cur_max_video_ch || hws->cur_max_video_ch > hws->max_channels) + hws->cur_max_video_ch = hws->max_channels; + + for (ch = 0; ch < hws->cur_max_video_ch; ch++, table += 8) { + /* Scratch buffers are allocated once during probe. */ + if (!hws->scratch_vid[ch].cpu) + continue; + + /* 2) Program 64-bit BAR remap entry for this channel */ + { + dma_addr_t p = hws->scratch_vid[ch].dma; + u32 lo = lower_32_bits(p) & addr_mask; + u32 hi = upper_32_bits(p); + u32 pci_addr_low = lower_32_bits(p) & addr_low_mask; + + writel_relaxed(hi, + hws->bar0_base + PCI_ADDR_TABLE_BASE + + table); + writel_relaxed(lo, + hws->bar0_base + PCI_ADDR_TABLE_BASE + + table + PCIE_BARADDROFSIZE); + + /* 3) Per-channel AXI base + PCI low */ + writel_relaxed((ch + 1) * PCIEBAR_AXI_BASE + + pci_addr_low, + hws->bar0_base + CVBS_IN_BUF_BASE + + ch * PCIE_BARADDROFSIZE); + + /* 4) Half-frame length in /16 units. + * Prefer the current channel’s computed half_size if available. + * Fall back to half of the preallocated scratch buffer. + */ + { + u32 half_bytes = hws->video[ch].pix.half_size ? + hws->video[ch].pix.half_size : + (hws->scratch_vid[ch].size / 2); + writel_relaxed(half_bytes / 16, + hws->bar0_base + + CVBS_IN_BUF_BASE2 + + ch * PCIE_BARADDROFSIZE); + } + } + } + + /* Post writes so device sees them before we move on */ + (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); +} + +static void hws_ack_all_irqs(struct hws_pcie_dev *hws) +{ + u32 st = readl(hws->bar0_base + HWS_REG_INT_STATUS); + + if (st) { + writel(st, hws->bar0_base + HWS_REG_INT_STATUS); /* W1C */ + (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); + } +} + +static void hws_open_irq_fabric(struct hws_pcie_dev *hws) +{ + /* Route all sources to vector 0 (same value you’re already using) */ + writel(0x00000000, hws->bar0_base + PCIE_INT_DEC_REG_BASE); + (void)readl(hws->bar0_base + PCIE_INT_DEC_REG_BASE); + + /* Turn on the bridge if your IP needs it */ + writel(0x00000001, hws->bar0_base + PCIEBR_EN_REG_BASE); + (void)readl(hws->bar0_base + PCIEBR_EN_REG_BASE); + + /* Open the global/bridge gate (legacy 0x3FFFF) */ + writel(HWS_INT_EN_MASK, hws->bar0_base + INT_EN_REG_BASE); + (void)readl(hws->bar0_base + INT_EN_REG_BASE); +} + +void hws_init_video_sys(struct hws_pcie_dev *hws, bool enable) +{ + int i; + + if (hws->start_run && !enable) + return; + + /* 1) reset the decoder mode register to 0 */ + writel(0x00000000, hws->bar0_base + HWS_REG_DEC_MODE); + hws_seed_dma_windows(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); + } + } + + /* 4) “Start run”: set bit31, wait a bit, then program low 24 bits */ + writel(0x80000000, hws->bar0_base + HWS_REG_DEC_MODE); + // udelay(500); + writel(0x80FFFFFF, hws->bar0_base + HWS_REG_DEC_MODE); + writel(0x13, hws->bar0_base + HWS_REG_DEC_MODE); + hws_ack_all_irqs(hws); + hws_open_irq_fabric(hws); + /* 6) record that we're now running */ + hws->start_run = true; +} + +int hws_check_card_status(struct hws_pcie_dev *hws) +{ + u32 status; + + if (!hws || !hws->bar0_base) + return -ENODEV; + + status = readl(hws->bar0_base + HWS_REG_SYS_STATUS); + + /* Common “device missing” pattern */ + if (unlikely(status == 0xFFFFFFFF)) { + hws->pci_lost = true; + dev_err(&hws->pdev->dev, "PCIe device not responding\n"); + return -ENODEV; + } + + /* If RUN/READY bit (bit0) isn’t set, (re)initialize the video core */ + if (!(status & BIT(0))) { + dev_dbg(&hws->pdev->dev, + "SYS_STATUS not ready (0x%08x), reinitializing\n", + status); + hws_init_video_sys(hws, true); + /* Optional: verify the core cleared its busy bit, if you have one */ + /* int ret = hws_check_busy(hws); */ + /* if (ret) return ret; */ + } + + return 0; +} + +void check_video_format(struct hws_pcie_dev *pdx) +{ + int i; + + for (i = 0; i < pdx->cur_max_video_ch; i++) { + if (!hws_update_active_interlace(pdx, i)) { + /* No active video; optionally feed neutral frames to keep streaming. */ + if (pdx->video[i].signal_loss_cnt == 0) + pdx->video[i].signal_loss_cnt = 1; + if (READ_ONCE(pdx->video[i].cap_active)) + hws_force_no_signal_frame(&pdx->video[i], + "monitor_nosignal"); + } else { + if (pdx->hw_ver > 0) + handle_hwv2_path(pdx, i); + else + /* Legacy path stub; see handle_legacy_path() comment. */ + handle_legacy_path(pdx, i); + + update_live_resolution(pdx, i); + pdx->video[i].signal_loss_cnt = 0; + } + } +} + +static inline void hws_write_if_diff(struct hws_pcie_dev *hws, u32 reg_off, + u32 new_val) +{ + void __iomem *addr; + u32 old; + + if (!hws || !hws->bar0_base) + return; + + addr = hws->bar0_base + reg_off; + + old = readl(addr); + /* Treat all-ones as device gone; avoid writing garbage. */ + if (unlikely(old == 0xFFFFFFFF)) { + hws->pci_lost = true; + return; + } + + if (old != new_val) { + writel(new_val, addr); + /* Post the write on some bridges / enforce ordering. */ + (void)readl(addr); + } +} + +static bool hws_update_active_interlace(struct hws_pcie_dev *pdx, + unsigned int ch) +{ + u32 reg; + bool active, interlace; + + if (ch >= pdx->cur_max_video_ch) + return false; + + reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS); + active = !!(reg & BIT(ch)); + interlace = !!(reg & BIT(8 + ch)); + + if (pdx->video[ch].hotplug_detect_control) { + v4l2_ctrl_lock(pdx->video[ch].hotplug_detect_control); + __v4l2_ctrl_s_ctrl(pdx->video[ch].hotplug_detect_control, + active); + v4l2_ctrl_unlock(pdx->video[ch].hotplug_detect_control); + } + + WRITE_ONCE(pdx->video[ch].pix.interlaced, interlace); + return active; +} + +/* Modern hardware path: keep HW registers in sync with current per-channel + * software state. Adjust the OUT_* bits below to match your HW contract. + */ +static void handle_hwv2_path(struct hws_pcie_dev *hws, unsigned int ch) +{ + struct hws_video *vid; + u32 reg, in_fps, cur_out_res, want_out_res; + + if (!hws || !hws->bar0_base || ch >= hws->max_channels) + return; + + vid = &hws->video[ch]; + + /* 1) Input frame rate (read-only; log or export via debugfs if wanted) */ + in_fps = readl(hws->bar0_base + HWS_REG_FRAME_RATE(ch)); + if (in_fps) + vid->current_fps = in_fps; + /* dev_dbg(&hws->pdev->dev, "ch%u input fps=%u\n", ch, in_fps); */ + + /* 2) Output resolution programming + * If your HW expects a separate “scaled” size, add fields to track it. + * For now, mirror the current format (fmt_curr) to OUT_RES. + */ + want_out_res = (vid->pix.height << 16) | vid->pix.width; + cur_out_res = readl(hws->bar0_base + HWS_REG_OUT_RES(ch)); + if (cur_out_res != want_out_res) + hws_write_if_diff(hws, HWS_REG_OUT_RES(ch), want_out_res); + + /* 3) Output FPS: only program if you actually track a target. + * Example heuristic (disabled by default): + * + * u32 out_fps = (vid->fmt_curr.height >= 1080) ? 60 : 30; + * hws_write_if_diff(hws, HWS_REG_OUT_FRAME_RATE(ch), out_fps); + */ + + /* 4) BCHS controls: pack from per-channel current_* fields */ + reg = readl(hws->bar0_base + HWS_REG_BCHS(ch)); + { + u8 br = reg & 0xFF; + u8 co = (reg >> 8) & 0xFF; + u8 hu = (reg >> 16) & 0xFF; + u8 sa = (reg >> 24) & 0xFF; + + if (br != vid->current_brightness || + co != vid->current_contrast || hu != vid->current_hue || + sa != vid->current_saturation) { + u32 packed = (vid->current_saturation << 24) | + (vid->current_hue << 16) | + (vid->current_contrast << 8) | + vid->current_brightness; + hws_write_if_diff(hws, HWS_REG_BCHS(ch), packed); + } + } + + /* 5) HDCP detect: read only (no cache field in your structs today) */ + reg = readl(hws->bar0_base + HWS_REG_HDCP_STATUS); + /* bool hdcp = !!(reg & BIT(ch)); // use if you later add a field/control */ +} + +static void handle_legacy_path(struct hws_pcie_dev *hws, unsigned int ch) +{ + /* + * Legacy (hw_ver == 0) expected behavior: + * - A per-channel SW FPS accumulator incremented on each VDONE. + * - A once-per-second poll mapped the count to discrete FPS: + * >55*2 => 60, >45*2 => 50, >25*2 => 30, >20*2 => 25, else 60, + * then reset the accumulator to 0. + * - The *2 factor assumed VDONE fired per-field; if legacy VDONE is + * per-frame, drop the factor. + * + * Current code keeps this path as a no-op; vid->current_fps stays at the + * default or mode-derived value. If accurate legacy FPS reporting is + * needed (V4L2 g_parm/timeperframe), reintroduce the accumulator in the + * IRQ path and perform the mapping/reset here. + * + * No-op by default. If you introduce a SW FPS accumulator, map it here. + * + * Example skeleton: + * + * u32 sw_rate = READ_ONCE(hws->sw_fps[ch]); // incremented elsewhere + * if (sw_rate > THRESHOLD) { + * u32 fps = pick_fps_from_rate(sw_rate); + * hws_write_if_diff(hws, HWS_REG_OUT_FRAME_RATE(ch), fps); + * WRITE_ONCE(hws->sw_fps[ch], 0); + * } + */ + (void)hws; + (void)ch; +} + +static void hws_video_apply_mode_change(struct hws_pcie_dev *pdx, + unsigned int ch, u16 w, u16 h, + bool interlaced) +{ + struct hws_video *v = &pdx->video[ch]; + unsigned long flags; + u32 new_size; + bool queue_busy; + struct list_head done; + struct hwsvideo_buffer *b, *tmp; + + if (!pdx || !pdx->bar0_base) + return; + if (ch >= pdx->max_channels) + return; + if (!w || !h || w > MAX_VIDEO_HW_W || + (!interlaced && h > MAX_VIDEO_HW_H) || + (interlaced && (h * 2) > MAX_VIDEO_HW_H)) + return; + + if (!mutex_trylock(&v->state_lock)) + return; + + INIT_LIST_HEAD(&done); + + WRITE_ONCE(v->stop_requested, true); + WRITE_ONCE(v->cap_active, false); + /* Publish software stop first so the IRQ completion path sees the stop + * before we touch MMIO or the lists. Pairs with READ_ONCE() checks in the + * VDONE handler and hws_arm_next() to prevent completions while modes + * change. + */ + smp_wmb(); + + hws_enable_video_capture(pdx, ch, false); + readl(pdx->bar0_base + HWS_REG_INT_STATUS); + + if (v->parent && v->parent->irq >= 0) + synchronize_irq(v->parent->irq); + + spin_lock_irqsave(&v->irq_lock, flags); + hws_video_collect_done_locked(v, &done); + spin_unlock_irqrestore(&v->irq_lock, flags); + + /* Update software pixel state */ + v->pix.width = w; + v->pix.height = h; + v->pix.interlaced = interlaced; + hws_set_current_dv_timings(v, w, h, interlaced); + /* Try to reflect the live frame rate if HW reports it; otherwise default + * to common rates (50 Hz for 576p, else 60 Hz). + */ + { + u32 fps = readl(pdx->bar0_base + HWS_REG_FRAME_RATE(ch)); + + if (fps) + v->current_fps = fps; + else + v->current_fps = (h == 576) ? 50 : 60; + } + + new_size = hws_calc_sizeimage(v, w, h, interlaced); + v->window_valid = false; + queue_busy = vb2_is_busy(&v->buffer_queue); + + /* Mode changes require userspace to renegotiate buffers and restart + * streaming. Complete every queued buffer with an error, surface a + * SOURCE_CHANGE event, and leave the queue in an error state until the + * next streamoff/reqbufs cycle. + */ + if (queue_busy) { + struct v4l2_event ev = { + .type = V4L2_EVENT_SOURCE_CHANGE, + }; + + ev.u.src_change.changes = V4L2_EVENT_SRC_CH_RESOLUTION; + v4l2_event_queue(v->video_device, &ev); + vb2_queue_error(&v->buffer_queue); + } else { + v->alloc_sizeimage = PAGE_ALIGN(new_size); + } + + /* Program HW with new resolution */ + hws_write_if_diff(pdx, HWS_REG_OUT_RES(ch), (h << 16) | w); + + /* Legacy half-buffer programming */ + writel(v->pix.half_size / 16, + pdx->bar0_base + CVBS_IN_BUF_BASE2 + ch * PCIE_BARADDROFSIZE); + (void)readl(pdx->bar0_base + CVBS_IN_BUF_BASE2 + + ch * PCIE_BARADDROFSIZE); + + /* Reset per-channel toggles/counters */ + WRITE_ONCE(v->last_buf_half_toggle, 0); + atomic_set(&v->sequence_number, 0); + + mutex_unlock(&v->state_lock); + + list_for_each_entry_safe(b, tmp, &done, list) { + list_del_init(&b->list); + vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR); + } +} + +static void update_live_resolution(struct hws_pcie_dev *pdx, unsigned int ch) +{ + u32 reg = readl(pdx->bar0_base + HWS_REG_IN_RES(ch)); + u16 res_w = reg & 0xFFFF; + u16 res_h = (reg >> 16) & 0xFFFF; + bool interlace = READ_ONCE(pdx->video[ch].pix.interlaced); + + bool within_hw = (res_w <= MAX_VIDEO_HW_W) && + ((!interlace && res_h <= MAX_VIDEO_HW_H) || + (interlace && (res_h * 2) <= MAX_VIDEO_HW_H)); + + if (!within_hw) + return; + + if (res_w != pdx->video[ch].pix.width || + res_h != pdx->video[ch].pix.height) { + hws_video_apply_mode_change(pdx, ch, res_w, res_h, interlace); + } +} + +static int hws_open(struct file *file) +{ + return v4l2_fh_open(file); +} + +static const struct v4l2_file_operations hws_fops = { + .owner = THIS_MODULE, + .open = hws_open, + .release = vb2_fop_release, + .poll = vb2_fop_poll, + .unlocked_ioctl = video_ioctl2, + .mmap = vb2_fop_mmap, +}; + +static int hws_subscribe_event(struct v4l2_fh *fh, + const struct v4l2_event_subscription *sub) +{ + switch (sub->type) { + case V4L2_EVENT_SOURCE_CHANGE: + return v4l2_src_change_event_subscribe(fh, sub); + case V4L2_EVENT_CTRL: + return v4l2_ctrl_subscribe_event(fh, sub); + default: + return -EINVAL; + } +} + +static const struct v4l2_ioctl_ops hws_ioctl_fops = { + /* Core caps/info */ + .vidioc_querycap = hws_vidioc_querycap, + + /* Pixel format: still needed to report YUYV etc. */ + .vidioc_enum_fmt_vid_cap = hws_vidioc_enum_fmt_vid_cap, + .vidioc_enum_frameintervals = hws_vidioc_enum_frameintervals, + .vidioc_g_fmt_vid_cap = hws_vidioc_g_fmt_vid_cap, + .vidioc_s_fmt_vid_cap = hws_vidioc_s_fmt_vid_cap, + .vidioc_try_fmt_vid_cap = hws_vidioc_try_fmt_vid_cap, + + /* Buffer queueing / streaming */ + .vidioc_reqbufs = vb2_ioctl_reqbufs, + .vidioc_prepare_buf = vb2_ioctl_prepare_buf, + .vidioc_create_bufs = vb2_ioctl_create_bufs, + .vidioc_querybuf = vb2_ioctl_querybuf, + .vidioc_qbuf = vb2_ioctl_qbuf, + .vidioc_dqbuf = vb2_ioctl_dqbuf, + .vidioc_expbuf = vb2_ioctl_expbuf, + .vidioc_streamon = vb2_ioctl_streamon, + .vidioc_streamoff = vb2_ioctl_streamoff, + + /* Inputs */ + .vidioc_enum_input = hws_vidioc_enum_input, + .vidioc_g_input = hws_vidioc_g_input, + .vidioc_s_input = hws_vidioc_s_input, + + /* DV timings (HDMI/DVI/VESA modes) */ + .vidioc_query_dv_timings = hws_vidioc_query_dv_timings, + .vidioc_enum_dv_timings = hws_vidioc_enum_dv_timings, + .vidioc_g_dv_timings = hws_vidioc_g_dv_timings, + .vidioc_s_dv_timings = hws_vidioc_s_dv_timings, + .vidioc_dv_timings_cap = hws_vidioc_dv_timings_cap, + + .vidioc_log_status = v4l2_ctrl_log_status, + .vidioc_subscribe_event = hws_subscribe_event, + .vidioc_unsubscribe_event = v4l2_event_unsubscribe, + .vidioc_g_parm = hws_vidioc_g_parm, + .vidioc_s_parm = hws_vidioc_s_parm, +}; + +static u32 hws_calc_sizeimage(struct hws_video *v, u16 w, u16 h, + bool interlaced) +{ + /* example for packed 16bpp (YUYV); replace with your real math/align */ + u32 lines = h; /* full frame lines for sizeimage */ + u32 bytesperline = ALIGN(w * 2, 64); + u32 sizeimage, half0; + + /* publish into pix, since we now carry these in-state */ + v->pix.bytesperline = bytesperline; + sizeimage = bytesperline * lines; + + half0 = sizeimage / 2; + + v->pix.sizeimage = sizeimage; + v->pix.half_size = half0; /* first half; second = sizeimage - half0 */ + v->pix.field = interlaced ? V4L2_FIELD_INTERLACED : V4L2_FIELD_NONE; + + return v->pix.sizeimage; +} + +static int hws_queue_setup(struct vb2_queue *q, unsigned int *num_buffers, + unsigned int *nplanes, unsigned int sizes[], + struct device *alloc_devs[]) +{ + struct hws_video *vid = q->drv_priv; + + (void)num_buffers; + (void)alloc_devs; + + if (!vid->pix.sizeimage) { + vid->pix.bytesperline = ALIGN(vid->pix.width * 2, 64); + vid->pix.sizeimage = vid->pix.bytesperline * vid->pix.height; + } + if (*nplanes) { + if (sizes[0] < vid->pix.sizeimage) + return -EINVAL; + } else { + *nplanes = 1; + sizes[0] = PAGE_ALIGN(vid->pix.sizeimage); + } + + vid->alloc_sizeimage = PAGE_ALIGN(vid->pix.sizeimage); + return 0; +} + +static int hws_buffer_prepare(struct vb2_buffer *vb) +{ + struct hws_video *vid = vb->vb2_queue->drv_priv; + struct hws_pcie_dev *hws = vid->parent; + size_t need = vid->pix.sizeimage; + dma_addr_t dma_addr; + + if (vb2_plane_size(vb, 0) < need) + return -EINVAL; + + /* Validate DMA address alignment */ + dma_addr = vb2_dma_contig_plane_dma_addr(vb, 0); + if (dma_addr & 0x3F) { /* 64-byte alignment required */ + dev_err(&hws->pdev->dev, + "Buffer DMA address 0x%llx not 64-byte aligned\n", + (unsigned long long)dma_addr); + return -EINVAL; + } + + vb2_set_plane_payload(vb, 0, need); + return 0; +} + +static void hws_buffer_queue(struct vb2_buffer *vb) +{ + struct hws_video *vid = vb->vb2_queue->drv_priv; + struct hwsvideo_buffer *buf = to_hwsbuf(vb); + struct hws_pcie_dev *hws = vid->parent; + unsigned long flags; + + dev_dbg(&hws->pdev->dev, + "buffer_queue(ch=%u): vb=%p sizeimage=%u q_active=%d\n", + vid->channel_index, vb, vid->pix.sizeimage, + READ_ONCE(vid->cap_active)); + + /* Initialize buffer slot */ + buf->slot = 0; + + spin_lock_irqsave(&vid->irq_lock, flags); + list_add_tail(&buf->list, &vid->capture_queue); + vid->queued_count++; + + /* If streaming and no in-flight buffer, prime HW immediately */ + if (READ_ONCE(vid->cap_active) && !vid->active) { + dma_addr_t dma_addr; + + dev_dbg(&hws->pdev->dev, + "buffer_queue(ch=%u): priming first vb=%p\n", + vid->channel_index, &buf->vb.vb2_buf); + list_del_init(&buf->list); + vid->queued_count--; + vid->active = buf; + + dma_addr = vb2_dma_contig_plane_dma_addr(&buf->vb.vb2_buf, 0); + hws_program_dma_for_addr(vid->parent, vid->channel_index, + dma_addr); + iowrite32(lower_32_bits(dma_addr), + hws->bar0_base + HWS_REG_DMA_ADDR(vid->channel_index)); + + wmb(); /* ensure descriptors visible before enabling capture */ + hws_enable_video_capture(hws, vid->channel_index, true); + hws_prime_next_locked(vid); + } else if (READ_ONCE(vid->cap_active) && vid->active) { + hws_prime_next_locked(vid); + } + spin_unlock_irqrestore(&vid->irq_lock, flags); +} + +static int hws_start_streaming(struct vb2_queue *q, unsigned int count) +{ + struct hws_video *v = q->drv_priv; + struct hws_pcie_dev *hws = v->parent; + struct hwsvideo_buffer *to_program = NULL; /* local copy */ + struct vb2_buffer *prog_vb2 = NULL; + unsigned long flags; + int ret; + + dev_dbg(&hws->pdev->dev, "start_streaming: ch=%u count=%u\n", + v->channel_index, count); + + ret = hws_check_card_status(hws); + if (ret) { + struct hwsvideo_buffer *b, *tmp; + unsigned long f; + LIST_HEAD(queued); + + spin_lock_irqsave(&v->irq_lock, f); + if (v->active) { + list_add_tail(&v->active->list, &queued); + v->active = NULL; + } + if (v->next_prepared) { + list_add_tail(&v->next_prepared->list, &queued); + v->next_prepared = NULL; + } + while (!list_empty(&v->capture_queue)) { + b = list_first_entry(&v->capture_queue, + struct hwsvideo_buffer, list); + list_move_tail(&b->list, &queued); + } + spin_unlock_irqrestore(&v->irq_lock, f); + + list_for_each_entry_safe(b, tmp, &queued, list) { + list_del_init(&b->list); + vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_QUEUED); + } + return ret; + } + (void)hws_update_active_interlace(hws, v->channel_index); + + lockdep_assert_held(&v->state_lock); + /* init per-stream state */ + WRITE_ONCE(v->stop_requested, false); + WRITE_ONCE(v->cap_active, true); + WRITE_ONCE(v->half_seen, false); + WRITE_ONCE(v->last_buf_half_toggle, 0); + + /* Try to prime a buffer, but it's OK if none are queued yet */ + spin_lock_irqsave(&v->irq_lock, flags); + if (!v->active && !list_empty(&v->capture_queue)) { + to_program = list_first_entry(&v->capture_queue, + struct hwsvideo_buffer, list); + list_del_init(&to_program->list); + v->queued_count--; + v->active = to_program; + prog_vb2 = &to_program->vb.vb2_buf; + dev_dbg(&hws->pdev->dev, + "start_streaming: ch=%u took buffer %p\n", + v->channel_index, to_program); + } + spin_unlock_irqrestore(&v->irq_lock, flags); + + /* Only program/enable HW if we actually have a buffer */ + if (to_program) { + if (!prog_vb2) + prog_vb2 = &to_program->vb.vb2_buf; + { + dma_addr_t dma_addr; + + dma_addr = vb2_dma_contig_plane_dma_addr(prog_vb2, 0); + hws_program_dma_for_addr(hws, v->channel_index, dma_addr); + iowrite32(lower_32_bits(dma_addr), + hws->bar0_base + + HWS_REG_DMA_ADDR(v->channel_index)); + dev_dbg(&hws->pdev->dev, + "start_streaming: ch=%u programmed buffer %p dma=0x%08x\n", + v->channel_index, to_program, + lower_32_bits(dma_addr)); + (void)readl(hws->bar0_base + HWS_REG_INT_STATUS); + } + + wmb(); /* ensure descriptors visible before enabling capture */ + hws_enable_video_capture(hws, v->channel_index, true); + { + unsigned long pf; + + spin_lock_irqsave(&v->irq_lock, pf); + hws_prime_next_locked(v); + spin_unlock_irqrestore(&v->irq_lock, pf); + } + } else { + dev_dbg(&hws->pdev->dev, + "start_streaming: ch=%u no buffer yet (will arm on QBUF)\n", + v->channel_index); + } + + return 0; +} + +static void hws_log_video_state(struct hws_video *v, const char *action, + const char *phase) +{ + struct hws_pcie_dev *hws = v->parent; + unsigned long flags; + unsigned int queued = 0; + unsigned int tracked = 0; + unsigned int seq = 0; + struct hwsvideo_buffer *b; + bool streaming = vb2_is_streaming(&v->buffer_queue); + bool cap_active; + bool stop_requested; + struct hwsvideo_buffer *active; + struct hwsvideo_buffer *next_prepared; + + spin_lock_irqsave(&v->irq_lock, flags); + list_for_each_entry(b, &v->capture_queue, list) + queued++; + cap_active = READ_ONCE(v->cap_active); + stop_requested = READ_ONCE(v->stop_requested); + active = v->active; + next_prepared = v->next_prepared; + tracked = v->queued_count; + seq = (u32)atomic_read(&v->sequence_number); + spin_unlock_irqrestore(&v->irq_lock, flags); + + dev_dbg(&hws->pdev->dev, + "video:%s:%s ch=%u streaming=%d cap=%d stop=%d active=%p next=%p queued=%u tracked=%u seq=%u\n", + action, phase, v->channel_index, streaming, cap_active, + stop_requested, active, next_prepared, queued, tracked, seq); +} + +static void hws_stop_streaming(struct vb2_queue *q) +{ + struct hws_video *v = q->drv_priv; + struct hws_pcie_dev *hws = v->parent; + unsigned long flags; + struct hwsvideo_buffer *b, *tmp; + LIST_HEAD(done); + unsigned int done_cnt = 0; + u64 start_ns = ktime_get_mono_fast_ns(); + + hws_log_video_state(v, "streamoff", "begin"); + + /* 1) Quiesce SW/HW first */ + lockdep_assert_held(&v->state_lock); + WRITE_ONCE(v->cap_active, false); + WRITE_ONCE(v->stop_requested, true); + + hws_enable_video_capture(v->parent, v->channel_index, false); + + /* 2) Collect in-flight + queued under the IRQ lock */ + spin_lock_irqsave(&v->irq_lock, flags); + hws_video_collect_done_locked(v, &done); + spin_unlock_irqrestore(&v->irq_lock, flags); + + /* 3) Complete outside the lock */ + list_for_each_entry_safe(b, tmp, &done, list) { + /* Unlink from 'done' before completing */ + list_del_init(&b->list); + vb2_buffer_done(&b->vb.vb2_buf, VB2_BUF_STATE_ERROR); + done_cnt++; + } + dev_dbg(&hws->pdev->dev, + "video:streamoff:done ch=%u completed=%u (%lluus)\n", + v->channel_index, done_cnt, + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); + hws_log_video_state(v, "streamoff", "end"); +} + +static const struct vb2_ops hwspcie_video_qops = { + .queue_setup = hws_queue_setup, + .buf_prepare = hws_buffer_prepare, + .buf_init = hws_buf_init, + .buf_finish = hws_buf_finish, + .buf_cleanup = hws_buf_cleanup, + // .buf_finish = hws_buffer_finish, + .buf_queue = hws_buffer_queue, + .start_streaming = hws_start_streaming, + .stop_streaming = hws_stop_streaming, +}; + +int hws_video_register(struct hws_pcie_dev *dev) +{ + int i, ret; + + ret = v4l2_device_register(&dev->pdev->dev, &dev->v4l2_device); + if (ret) { + dev_err(&dev->pdev->dev, "v4l2_device_register failed: %d\n", + ret); + return ret; + } + + for (i = 0; i < dev->cur_max_video_ch; i++) { + struct hws_video *ch = &dev->video[i]; + struct video_device *vdev; + struct vb2_queue *q; + + /* hws_video_init_channel() should have set: + * - ch->parent, ch->channel_index + * - locks (state_lock, irq_lock) + * - capture_queue (INIT_LIST_HEAD) + * - control_handler + controls + * - fmt_curr (width/height) + * Don’t reinitialize any of those here. + */ + + vdev = video_device_alloc(); + if (!vdev) { + dev_err(&dev->pdev->dev, + "video_device_alloc ch%u failed\n", i); + ret = -ENOMEM; + goto err_unwind; + } + ch->video_device = vdev; + + /* Basic V4L2 node setup */ + snprintf(vdev->name, sizeof(vdev->name), "%s-hdmi%u", + KBUILD_MODNAME, i); + vdev->v4l2_dev = &dev->v4l2_device; + vdev->fops = &hws_fops; /* your file_ops */ + vdev->ioctl_ops = &hws_ioctl_fops; /* your ioctl_ops */ + vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; + vdev->lock = &ch->state_lock; /* serialize file ops */ + vdev->ctrl_handler = &ch->control_handler; + vdev->vfl_dir = VFL_DIR_RX; + vdev->release = video_device_release; + if (ch->control_handler.error) { + ret = ch->control_handler.error; + goto err_unwind; + } + video_set_drvdata(vdev, ch); + + /* vb2 queue init (dma-contig) */ + q = &ch->buffer_queue; + memset(q, 0, sizeof(*q)); + q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + q->io_modes = VB2_MMAP | VB2_DMABUF; + q->drv_priv = ch; + q->buf_struct_size = sizeof(struct hwsvideo_buffer); + q->ops = &hwspcie_video_qops; /* your vb2_ops */ + q->mem_ops = &vb2_dma_contig_memops; + q->timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC; + q->lock = &ch->state_lock; + q->min_queued_buffers = 1; + q->dev = &dev->pdev->dev; + + ret = vb2_queue_init(q); + vdev->queue = q; + if (ret) { + dev_err(&dev->pdev->dev, + "vb2_queue_init ch%u failed: %d\n", i, ret); + goto err_unwind; + } + + /* Make controls live (no-op if none or already set up) */ + if (ch->control_handler.error) { + ret = ch->control_handler.error; + dev_err(&dev->pdev->dev, + "ctrl handler ch%u error: %d\n", i, ret); + goto err_unwind; + } + v4l2_ctrl_handler_setup(&ch->control_handler); + ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1); + if (ret) { + dev_err(&dev->pdev->dev, + "video_register_device ch%u failed: %d\n", i, + ret); + goto err_unwind; + } + + ret = hws_resolution_create(vdev); + if (ret) { + dev_err(&dev->pdev->dev, + "device_create_file(resolution) ch%u failed: %d\n", + i, ret); + goto err_unwind; + } + } + + return 0; + +err_unwind: + for (; i >= 0; i--) { + struct hws_video *ch = &dev->video[i]; + + if (video_is_registered(ch->video_device)) + hws_resolution_remove(ch->video_device); + if (video_is_registered(ch->video_device)) + vb2_video_unregister_device(ch->video_device); + if (ch->video_device) { + /* If not registered, we must free the alloc’d vdev ourselves */ + if (!video_is_registered(ch->video_device)) + video_device_release(ch->video_device); + ch->video_device = NULL; + } + } + v4l2_device_unregister(&dev->v4l2_device); + return ret; +} + +void hws_video_unregister(struct hws_pcie_dev *dev) +{ + int i; + + if (!dev) + return; + + for (i = 0; i < dev->cur_max_video_ch; i++) { + struct hws_video *ch = &dev->video[i]; + + if (ch->video_device) + hws_resolution_remove(ch->video_device); + if (ch->video_device) { + vb2_video_unregister_device(ch->video_device); + ch->video_device = NULL; + } + v4l2_ctrl_handler_free(&ch->control_handler); + } + v4l2_device_unregister(&dev->v4l2_device); +} + +int hws_video_quiesce(struct hws_pcie_dev *hws, const char *reason) +{ + int i, ret = 0; + u64 start_ns = ktime_get_mono_fast_ns(); + + dev_dbg(&hws->pdev->dev, "video:%s:begin channels=%u\n", reason, + hws->cur_max_video_ch); + for (i = 0; i < hws->cur_max_video_ch; i++) { + struct hws_video *vid = &hws->video[i]; + struct vb2_queue *q = &vid->buffer_queue; + u64 ch_start_ns = ktime_get_mono_fast_ns(); + bool streaming; + + if (!q || !q->ops) { + dev_dbg(&hws->pdev->dev, + "video:%s:ch=%d skipped queue-unavailable\n", + reason, i); + continue; + } + + streaming = vb2_is_streaming(q); + hws_log_video_state(vid, reason, "channel"); + if (streaming) { + /* Stop via vb2 (runs your .stop_streaming) */ + int r = vb2_streamoff(q, q->type); + + dev_dbg(&hws->pdev->dev, + "video:%s:ch=%d streamoff ret=%d (%lluus)\n", + reason, i, r, (unsigned long long) + ((ktime_get_mono_fast_ns() - ch_start_ns) / 1000)); + if (r && !ret) + ret = r; + } else { + dev_dbg(&hws->pdev->dev, + "video:%s:ch=%d idle (%lluus)\n", + reason, i, (unsigned long long) + ((ktime_get_mono_fast_ns() - ch_start_ns) / 1000)); + } + } + dev_dbg(&hws->pdev->dev, "video:%s:done ret=%d (%lluus)\n", reason, + ret, + (unsigned long long)((ktime_get_mono_fast_ns() - start_ns) / 1000)); + return ret; +} + +void hws_video_pm_resume(struct hws_pcie_dev *hws) +{ + /* Nothing mandatory to do here for vb2 — userspace will STREAMON again. + * If you track per-channel 'auto-restart' policy, re-arm it here. + */ +} diff --git a/drivers/media/pci/hws/hws_video.h b/drivers/media/pci/hws/hws_video.h new file mode 100644 index 000000000000..d02cfb2cdeb3 --- /dev/null +++ b/drivers/media/pci/hws/hws_video.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +#ifndef HWS_VIDEO_H +#define HWS_VIDEO_H + +struct hws_video; + +int hws_video_register(struct hws_pcie_dev *dev); +void hws_video_unregister(struct hws_pcie_dev *dev); +void hws_enable_video_capture(struct hws_pcie_dev *hws, + unsigned int chan, + bool on); +void hws_prime_next_locked(struct hws_video *vid); + +int hws_video_init_channel(struct hws_pcie_dev *pdev, int ch); +void hws_video_cleanup_channel(struct hws_pcie_dev *pdev, int ch); +void check_video_format(struct hws_pcie_dev *pdx); +int hws_check_card_status(struct hws_pcie_dev *hws); +void hws_init_video_sys(struct hws_pcie_dev *hws, bool enable); + +void hws_program_dma_for_addr(struct hws_pcie_dev *hws, + unsigned int ch, + dma_addr_t dma); +void hws_set_dma_doorbell(struct hws_pcie_dev *hws, unsigned int ch, + dma_addr_t dma, const char *tag); + +int hws_video_quiesce(struct hws_pcie_dev *hws, const char *reason); +void hws_video_pm_resume(struct hws_pcie_dev *hws); + +#endif // HWS_VIDEO_H -- 2.53.0