mirror of
https://github.com/torvalds/linux.git
synced 2026-07-27 09:36:22 +02:00
media: pci: add AVMatrix HWS capture driver
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 patch also adds the MAINTAINERS entry for the new driver. This driver is derived from a GPL out-of-tree driver. Changes since v6: - v6 accidently contained legacy history, resubmitting with latest - remove an unused mode-change label reported by W=1 Changes since v5: - keep queue_setup() and alloc_sizeimage on the logical sizeimage value - drop the dead queue_setup() fallback that rebuilt pix.sizeimage on the fly - clarify that hws_calc_sizeimage() models the packed-YUYV-only path - add Assisted-by attribution for Codex Changes since v4: - replace plain 64-bit elapsed-time divisions in debug logging with div_u64() so i386 module builds do not emit __udivdi3 references Changes since v3: - fold the MAINTAINERS update into this patch so per-patch CI sees the new file pattern - wrap the validation text for checkpatch Changes since v2: - keep scratch DMA allocation on a single probe-owned path - avoid double-freeing V4L2 control handlers on register unwind - drop the extra per-node resolution sysfs ABI - turn live geometry changes into explicit SOURCE_CHANGE renegotiation - report live DV timings and reject attempts to retime a live source - stop advertising RESOLUTION source changes for fps-only updates - keep live fps state across harmless S_FMT restarts - stop exposing an unvalidated DV RX power-present signal - clean the imported sources for checkpatch and W=1 builds Validation: - build-tested with W=1 against a local kernel build tree - compiled the driver with ARCH=i386 allmodconfig and verified the resulting hws_pci.o, hws_video.o, and hws.o do not reference __udivdi3 - v4l2-compliance 1.33.0-5459 from v4l-utils commit 4a0d2c3b4f523406cb9a6f4c541ef14f72f19f3d on /dev/video2: 48 tests succeeded, 0 failed, 1 warning DV_RX_POWER_PRESENT is intentionally left unsupported in this revision because current hardware evidence does not expose a validated receiver-side power-detect signal distinct from active video presence. Reported-by: kernel test robot <lkp@intel.com> Closes: https://lore.kernel.org/oe-kbuild-all/202604020522.z22eZuW8-lkp@intel.com/ Assisted-by: Codex:gpt-5.5 Signed-off-by: Ben Hoff <hoff.benjamin.k@gmail.com> Signed-off-by: Hans Verkuil <hverkuil+cisco@kernel.org>
This commit is contained in:
parent
2c7fe9db11
commit
ba07fd2f57
|
|
@ -4317,6 +4317,12 @@ S: Maintained
|
|||
F: Documentation/devicetree/bindings/iio/adc/avia-hx711.yaml
|
||||
F: drivers/iio/adc/hx711.c
|
||||
|
||||
AVMATRIX HWS CAPTURE DRIVER
|
||||
M: Ben Hoff <hoff.benjamin.k@gmail.com>
|
||||
L: linux-media@vger.kernel.org
|
||||
S: Maintained
|
||||
F: drivers/media/pci/hws/
|
||||
|
||||
AWINIC AW99706 WLED BACKLIGHT DRIVER
|
||||
M: Junjie Cao <caojunjie650@gmail.com>
|
||||
S: Maintained
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
11
drivers/media/pci/hws/Kconfig
Normal file
11
drivers/media/pci/hws/Kconfig
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
config VIDEO_HWS
|
||||
tristate "AVMatrix HWS PCIe capture devices"
|
||||
depends on PCI && VIDEO_DEV
|
||||
select VIDEOBUF2_DMA_CONTIG
|
||||
help
|
||||
Enable support for AVMatrix HWS series multi-channel PCIe capture
|
||||
devices that provide HDMI video capture.
|
||||
|
||||
To compile this driver as a module, choose M here: the module
|
||||
will be named hws.
|
||||
4
drivers/media/pci/hws/Makefile
Normal file
4
drivers/media/pci/hws/Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
hws-objs := hws_pci.o hws_irq.o hws_video.o hws_v4l2_ioctl.o
|
||||
|
||||
obj-$(CONFIG_VIDEO_HWS) += hws.o
|
||||
173
drivers/media/pci/hws/hws.h
Normal file
173
drivers/media/pci/hws/hws.h
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||
#ifndef HWS_PCIE_H
|
||||
#define HWS_PCIE_H
|
||||
|
||||
#include <linux/types.h>
|
||||
#include <linux/compiler.h>
|
||||
#include <linux/dma-mapping.h>
|
||||
#include <linux/kthread.h>
|
||||
#include <linux/pci.h>
|
||||
#include <linux/list.h>
|
||||
#include <linux/spinlock.h>
|
||||
#include <linux/sizes.h>
|
||||
#include <linux/atomic.h>
|
||||
|
||||
#include <media/v4l2-ctrls.h>
|
||||
#include <media/v4l2-device.h>
|
||||
#include <media/v4l2-dv-timings.h>
|
||||
#include <media/videobuf2-dma-sg.h>
|
||||
|
||||
#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; /* hardware half-frame size */
|
||||
};
|
||||
|
||||
#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;
|
||||
};
|
||||
|
||||
struct hws_video {
|
||||
/* Linkage */
|
||||
struct hws_pcie_dev *parent;
|
||||
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;
|
||||
spinlock_t irq_lock; /* Protects capture_queue and active buffers. */
|
||||
|
||||
/* Indices */
|
||||
int channel_index;
|
||||
|
||||
/* Color controls */
|
||||
int current_brightness;
|
||||
int current_contrast;
|
||||
int current_saturation;
|
||||
int current_hue;
|
||||
|
||||
/* V4L2 controls */
|
||||
struct v4l2_ctrl_handler control_handler;
|
||||
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/notified DV timings */
|
||||
u32 current_fps; /* Hz, updated by mode changes, not by read-only queries */
|
||||
|
||||
/* 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 and workqueues */
|
||||
void __iomem *bar0_base;
|
||||
|
||||
/* Device identity and capabilities */
|
||||
u16 vendor_id;
|
||||
u16 device_id;
|
||||
u16 device_ver;
|
||||
u16 hw_ver;
|
||||
u32 sub_ver;
|
||||
u32 port_id;
|
||||
/* Tri-state support flag used by 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
|
||||
269
drivers/media/pci/hws/hws_irq.c
Normal file
269
drivers/media/pci/hws/hws_irq.c
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
#include <linux/compiler.h>
|
||||
#include <linux/moduleparam.h>
|
||||
#include <linux/io.h>
|
||||
#include <linux/dma-mapping.h>
|
||||
#include <linux/interrupt.h>
|
||||
#include <linux/minmax.h>
|
||||
#include <linux/string.h>
|
||||
|
||||
#include <media/videobuf2-dma-contig.h>
|
||||
|
||||
#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 (READ_ONCE(hws->suspended)) {
|
||||
dev_dbg(&hws->pdev->dev, "arm_next(ch=%u): suspended\n", ch);
|
||||
return -EBUSY;
|
||||
}
|
||||
|
||||
if (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 (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 (READ_ONCE(hws->suspended))
|
||||
return;
|
||||
|
||||
if (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 (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 (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 (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 (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;
|
||||
}
|
||||
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 (READ_ONCE(pdx->video[ch].cap_active) &&
|
||||
!READ_ONCE(pdx->video[ch].stop_requested)) {
|
||||
if (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;
|
||||
}
|
||||
10
drivers/media/pci/hws/hws_irq.h
Normal file
10
drivers/media/pci/hws/hws_irq.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||
#ifndef HWS_INTERRUPT_H
|
||||
#define HWS_INTERRUPT_H
|
||||
|
||||
#include <linux/pci.h>
|
||||
#include "hws.h"
|
||||
|
||||
irqreturn_t hws_irq_handler(int irq, void *info);
|
||||
|
||||
#endif /* HWS_INTERRUPT_H */
|
||||
865
drivers/media/pci/hws/hws_pci.c
Normal file
865
drivers/media/pci/hws/hws_pci.c
Normal file
|
|
@ -0,0 +1,865 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
#include <linux/pci.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/iopoll.h>
|
||||
#include <linux/bitfield.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/init.h>
|
||||
#include <linux/kthread.h>
|
||||
#include <linux/interrupt.h>
|
||||
#include <linux/dma-mapping.h>
|
||||
#include <linux/err.h>
|
||||
#include <linux/ktime.h>
|
||||
#include <linux/math64.h>
|
||||
#include <linux/pm.h>
|
||||
#include <linux/freezer.h>
|
||||
#include <linux/pci_regs.h>
|
||||
|
||||
#include <media/v4l2-ctrls.h>
|
||||
|
||||
#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 HWS_BUSY_POLL_DELAY_US 10
|
||||
#define HWS_BUSY_POLL_TIMEOUT_US 1000000
|
||||
|
||||
static unsigned long long hws_elapsed_us(u64 start_ns)
|
||||
{
|
||||
return div_u64(ktime_get_mono_fast_ns() - start_ns, 1000);
|
||||
}
|
||||
|
||||
/* 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/freeze. */
|
||||
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",
|
||||
hws_elapsed_us(start_ns));
|
||||
}
|
||||
}
|
||||
|
||||
static int hws_alloc_seed_buffers(struct hws_pcie_dev *hws)
|
||||
{
|
||||
int ch;
|
||||
/* 64 KiB is plenty for a safe dummy; hardware needs 64-byte alignment. */
|
||||
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) Apply optional PCIe tuning. */
|
||||
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. */
|
||||
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 will not 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",
|
||||
hws_elapsed_us(start_ns));
|
||||
}
|
||||
|
||||
static void hws_stop_device(struct hws_pcie_dev *hws)
|
||||
{
|
||||
u32 status = readl(hws->bar0_base + HWS_REG_SYS_STATUS);
|
||||
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",
|
||||
hws_elapsed_us(start_ns));
|
||||
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,
|
||||
hws_elapsed_us(step_ns));
|
||||
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,
|
||||
hws_elapsed_us(step_ns));
|
||||
}
|
||||
|
||||
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, hws_elapsed_us(step_ns));
|
||||
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,
|
||||
hws_elapsed_us(step_ns));
|
||||
hws_log_lifecycle_snapshot(hws, action, "end");
|
||||
dev_dbg(dev, "lifecycle:%s:quiesce-done ret=%d (%lluus)\n", action,
|
||||
vret, hws_elapsed_us(start_ns));
|
||||
|
||||
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 and capture cleanly. */
|
||||
hws_stop_device(hws);
|
||||
|
||||
/* Unregister V4L2 resources. */
|
||||
hws_video_unregister(hws);
|
||||
|
||||
/* Release seeded DMA buffers */
|
||||
hws_free_seed_buffers(hws);
|
||||
/* kthread is stopped by the devm action registered in probe. */
|
||||
hws_log_lifecycle_snapshot(hws, "remove", "end");
|
||||
dev_info(&pdev->dev, "lifecycle:remove done (%lluus)\n",
|
||||
hws_elapsed_us(start_ns));
|
||||
}
|
||||
|
||||
#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",
|
||||
hws_elapsed_us(step_ns));
|
||||
dev_info(dev, "lifecycle:pm_suspend done ret=%d (%lluus)\n", vret,
|
||||
hws_elapsed_us(start_ns));
|
||||
|
||||
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",
|
||||
hws_elapsed_us(step_ns));
|
||||
|
||||
/* 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",
|
||||
hws_elapsed_us(step_ns));
|
||||
|
||||
/* 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",
|
||||
hws_elapsed_us(step_ns));
|
||||
|
||||
/* 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",
|
||||
hws_elapsed_us(step_ns));
|
||||
hws_log_lifecycle_snapshot(hws, "pm_resume", "end");
|
||||
dev_info(dev, "lifecycle:pm_resume done (%lluus)\n",
|
||||
hws_elapsed_us(start_ns));
|
||||
|
||||
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",
|
||||
hws_elapsed_us(step_ns));
|
||||
dev_info(&pdev->dev, "lifecycle:pci_shutdown done ret=%d (%lluus)\n",
|
||||
vret, hws_elapsed_us(start_ns));
|
||||
}
|
||||
|
||||
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_NAME);
|
||||
MODULE_AUTHOR("Ben Hoff <hoff.benjamin.k@gmail.com>");
|
||||
MODULE_AUTHOR("Sales <sales@avmatrix.com>");
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_IMPORT_NS("DMA_BUF");
|
||||
136
drivers/media/pci/hws/hws_reg.h
Normal file
136
drivers/media/pci/hws/hws_reg.h
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||
#ifndef _HWS_PCIE_REG_H
|
||||
#define _HWS_PCIE_REG_H
|
||||
|
||||
#include <linux/bits.h>
|
||||
#include <linux/sizes.h>
|
||||
|
||||
#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-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 */
|
||||
919
drivers/media/pci/hws/hws_v4l2_ioctl.c
Normal file
919
drivers/media/pci/hws/hws_v4l2_ioctl.c
Normal file
|
|
@ -0,0 +1,919 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/string.h>
|
||||
#include <linux/pci.h>
|
||||
#include <linux/errno.h>
|
||||
#include <linux/io.h>
|
||||
#include <linux/math64.h>
|
||||
|
||||
#include <media/v4l2-ioctl.h>
|
||||
#include <media/v4l2-dev.h>
|
||||
#include <media/v4l2-dv-timings.h>
|
||||
#include <media/videobuf2-core.h>
|
||||
#include <media/videobuf2-v4l2.h>
|
||||
|
||||
#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_find_dv_by_wh_fps(u32 w, u32 h, bool interlaced, u32 fps);
|
||||
static u32 hws_get_live_fps(struct hws_video *vid);
|
||||
static u32 hws_input_status(struct hws_video *vid);
|
||||
static int hws_fill_dv_timings(u32 w, u32 h, bool interlace, u32 fps,
|
||||
struct v4l2_dv_timings *timings);
|
||||
|
||||
static const struct hws_dv_mode hws_dv_modes[] = {
|
||||
{
|
||||
{
|
||||
.type = V4L2_DV_BT_656_1120,
|
||||
.bt = {
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
.hfrontporch = 88,
|
||||
.hsync = 44,
|
||||
.hbackporch = 148,
|
||||
.vfrontporch = 4,
|
||||
.vsync = 5,
|
||||
.vbackporch = 36,
|
||||
.pixelclock = 148500000,
|
||||
.polarities = V4L2_DV_VSYNC_POS_POL |
|
||||
V4L2_DV_HSYNC_POS_POL,
|
||||
.interlaced = 0,
|
||||
},
|
||||
},
|
||||
60,
|
||||
},
|
||||
{
|
||||
{
|
||||
.type = V4L2_DV_BT_656_1120,
|
||||
.bt = {
|
||||
.width = 1920,
|
||||
.height = 1080,
|
||||
.hfrontporch = 88,
|
||||
.hsync = 44,
|
||||
.hbackporch = 148,
|
||||
.vfrontporch = 4,
|
||||
.vsync = 5,
|
||||
.vbackporch = 36,
|
||||
.pixelclock = 74250000,
|
||||
.polarities = V4L2_DV_VSYNC_POS_POL |
|
||||
V4L2_DV_HSYNC_POS_POL,
|
||||
.interlaced = 0,
|
||||
},
|
||||
},
|
||||
30,
|
||||
},
|
||||
{
|
||||
{
|
||||
.type = V4L2_DV_BT_656_1120,
|
||||
.bt = {
|
||||
.width = 1280,
|
||||
.height = 720,
|
||||
.hfrontporch = 110,
|
||||
.hsync = 40,
|
||||
.hbackporch = 220,
|
||||
.vfrontporch = 5,
|
||||
.vsync = 5,
|
||||
.vbackporch = 20,
|
||||
.pixelclock = 74250000,
|
||||
.polarities = V4L2_DV_VSYNC_POS_POL |
|
||||
V4L2_DV_HSYNC_POS_POL,
|
||||
.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;
|
||||
u32 fps;
|
||||
|
||||
if (!req || req->type != V4L2_DV_BT_656_1120)
|
||||
return NULL;
|
||||
|
||||
bt = &req->bt;
|
||||
fps = 0;
|
||||
if (bt->pixelclock) {
|
||||
u32 total_w = bt->width + bt->hfrontporch + bt->hsync +
|
||||
bt->hbackporch;
|
||||
u32 total_h = bt->height + bt->vfrontporch + bt->vsync +
|
||||
bt->vbackporch;
|
||||
|
||||
if (total_w && total_h)
|
||||
fps = DIV_ROUND_CLOSEST_ULL((u64)bt->pixelclock,
|
||||
(u64)total_w * total_h);
|
||||
}
|
||||
if (fps) {
|
||||
const struct hws_dv_mode *exact =
|
||||
hws_find_dv_by_wh_fps(bt->width, bt->height,
|
||||
!!bt->interlaced, fps);
|
||||
if (exact)
|
||||
return exact;
|
||||
}
|
||||
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 const struct hws_dv_mode *
|
||||
hws_find_dv_by_wh_fps(u32 w, u32 h, bool interlaced, u32 fps)
|
||||
{
|
||||
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 &&
|
||||
t->refresh_hz == fps)
|
||||
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_get_live_fps(struct hws_video *vid)
|
||||
{
|
||||
struct hws_pcie_dev *pdx;
|
||||
u32 fps;
|
||||
|
||||
if (!vid)
|
||||
return 0;
|
||||
|
||||
pdx = vid->parent;
|
||||
if (!pdx || !pdx->bar0_base)
|
||||
return 0;
|
||||
|
||||
fps = readl(pdx->bar0_base + HWS_REG_FRAME_RATE(vid->channel_index));
|
||||
if (!fps || fps == 0xFFFFFFFF || fps > 240)
|
||||
return 0;
|
||||
|
||||
return fps;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static int hws_fill_dv_timings(u32 w, u32 h, bool interlace, u32 fps,
|
||||
struct v4l2_dv_timings *timings)
|
||||
{
|
||||
const struct hws_dv_mode *m;
|
||||
|
||||
m = fps ? hws_find_dv_by_wh_fps(w, h, interlace, fps) : NULL;
|
||||
if (!m)
|
||||
m = hws_find_dv_by_wh(w, h, interlace);
|
||||
if (!m)
|
||||
return -ENOLINK;
|
||||
|
||||
*timings = m->timings;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static u32 hws_input_status(struct hws_video *vid)
|
||||
{
|
||||
struct hws_pcie_dev *pdx;
|
||||
u32 reg;
|
||||
|
||||
if (!vid)
|
||||
return V4L2_IN_ST_NO_SIGNAL;
|
||||
|
||||
pdx = vid->parent;
|
||||
if (!pdx || !pdx->bar0_base)
|
||||
return V4L2_IN_ST_NO_SIGNAL;
|
||||
|
||||
reg = readl(pdx->bar0_base + HWS_REG_ACTIVE_STATUS);
|
||||
if (reg == 0xffffffff)
|
||||
return V4L2_IN_ST_NO_SIGNAL;
|
||||
|
||||
return (reg & BIT(vid->channel_index)) ? 0 : V4L2_IN_ST_NO_SIGNAL;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
u32 w, h;
|
||||
u32 fps;
|
||||
bool interlace;
|
||||
|
||||
if (!timings)
|
||||
return -EINVAL;
|
||||
|
||||
w = vid->pix.width;
|
||||
h = vid->pix.height;
|
||||
interlace = vid->pix.interlaced;
|
||||
hws_get_live_dv_geometry(vid, &w, &h, &interlace);
|
||||
fps = hws_get_live_fps(vid);
|
||||
if (!fps)
|
||||
fps = vid->current_fps ? vid->current_fps :
|
||||
hws_pick_fps_from_mode(w, h, interlace);
|
||||
|
||||
return hws_fill_dv_timings(w, h, interlace, fps, timings);
|
||||
}
|
||||
|
||||
/* 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)
|
||||
{
|
||||
struct hws_video *vid = video_drvdata(file);
|
||||
const struct hws_dv_mode *m;
|
||||
u32 w, h;
|
||||
u32 fps;
|
||||
bool interlace;
|
||||
|
||||
if (!edv)
|
||||
return -EINVAL;
|
||||
|
||||
if (edv->pad)
|
||||
return -EINVAL;
|
||||
|
||||
w = 0;
|
||||
h = 0;
|
||||
interlace = false;
|
||||
if (hws_get_live_dv_geometry(vid, &w, &h, &interlace)) {
|
||||
fps = hws_get_live_fps(vid);
|
||||
if (!fps)
|
||||
fps = vid->current_fps ? vid->current_fps :
|
||||
hws_pick_fps_from_mode(w, h, interlace);
|
||||
m = fps ? hws_find_dv_by_wh_fps(w, h, interlace, fps) : NULL;
|
||||
if (!m)
|
||||
m = hws_find_dv_by_wh(w, h, interlace);
|
||||
if (m) {
|
||||
if (edv->index)
|
||||
return -EINVAL;
|
||||
edv->timings = m->timings;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
u32 w, h;
|
||||
u32 fps;
|
||||
bool interlace;
|
||||
|
||||
if (!timings)
|
||||
return -EINVAL;
|
||||
|
||||
w = vid->pix.width;
|
||||
h = vid->pix.height;
|
||||
interlace = vid->pix.interlaced;
|
||||
if (hws_get_live_dv_geometry(vid, &w, &h, &interlace)) {
|
||||
fps = hws_get_live_fps(vid);
|
||||
if (!fps)
|
||||
fps = vid->current_fps ? vid->current_fps :
|
||||
hws_pick_fps_from_mode(w, h, interlace);
|
||||
return hws_fill_dv_timings(w, h, interlace, fps, timings);
|
||||
}
|
||||
|
||||
*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;
|
||||
u32 live_w, live_h;
|
||||
u32 live_fps;
|
||||
bool live_interlaced;
|
||||
bool live_present;
|
||||
|
||||
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);
|
||||
live_present = hws_get_live_dv_geometry(vid, &live_w, &live_h,
|
||||
&live_interlaced);
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* When a live input signal is present, the receiver owns the timing.
|
||||
* Allow setting the already-active timings so v4l2-compliance can
|
||||
* round-trip them, but reject attempts to retime the live source.
|
||||
*/
|
||||
if (live_present) {
|
||||
live_fps = hws_get_live_fps(vid);
|
||||
if (!live_fps)
|
||||
live_fps = vid->current_fps ? vid->current_fps :
|
||||
hws_pick_fps_from_mode(live_w, live_h,
|
||||
live_interlaced);
|
||||
if (live_w == new_w && live_h == new_h &&
|
||||
live_interlaced == interlaced &&
|
||||
m->refresh_hz == live_fps)
|
||||
return 0;
|
||||
return -EBUSY;
|
||||
}
|
||||
|
||||
/* 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, and half_size. */
|
||||
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;
|
||||
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;
|
||||
|
||||
/* Only progressive modes are advertised. */
|
||||
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);
|
||||
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);
|
||||
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_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 buffer layout 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.bytesperline != vid->pix.bytesperline)
|
||||
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 negotiated buffer sizes. */
|
||||
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;
|
||||
/* S_FMT negotiates buffer layout only. Keep detector-owned DV timing
|
||||
* state unchanged so a harmless restart cannot clobber the live FPS.
|
||||
*/
|
||||
/* Or:
|
||||
* hws_calc_sizeimage(vid, vid->pix.width, vid->pix.height, false);
|
||||
*/
|
||||
|
||||
dev_dbg(&vid->parent->pdev->dev,
|
||||
"s_fmt: w=%u h=%u bpl=%u size=%u\n",
|
||||
vid->pix.width, vid->pix.height, vid->pix.bytesperline,
|
||||
vid->pix.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 = hws_get_live_fps(vid);
|
||||
if (!fps)
|
||||
fps = vid->current_fps ? vid->current_fps : 60;
|
||||
|
||||
/* HDMI receivers report the detected frame period, they don't set it. */
|
||||
param->parm.capture.capability = 0;
|
||||
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)
|
||||
{
|
||||
struct hws_video *vid = video_drvdata(file);
|
||||
|
||||
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 = hws_input_status(vid);
|
||||
|
||||
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;
|
||||
}
|
||||
36
drivers/media/pci/hws/hws_v4l2_ioctl.h
Normal file
36
drivers/media/pci/hws/hws_v4l2_ioctl.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0-only */
|
||||
#ifndef HWS_V4L2_IOCTL_H
|
||||
#define HWS_V4L2_IOCTL_H
|
||||
|
||||
#include <media/v4l2-ctrls.h>
|
||||
#include <linux/fs.h>
|
||||
|
||||
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_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_g_std(struct file *file, void *priv, v4l2_std_id *tvnorms);
|
||||
int hws_vidioc_s_std(struct file *file, void *priv, v4l2_std_id tvnorms);
|
||||
int hws_vidioc_g_parm(struct file *file, void *fh, struct v4l2_streamparm *setfps);
|
||||
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_g_ctrl(struct file *file, void *fh, struct v4l2_control *a);
|
||||
int hws_vidioc_s_ctrl(struct file *file, void *fh, struct v4l2_control *a);
|
||||
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_queryctrl(struct file *file, void *fh, struct v4l2_queryctrl *a);
|
||||
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);
|
||||
int hws_vidioc_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f);
|
||||
|
||||
#endif
|
||||
1490
drivers/media/pci/hws/hws_video.c
Normal file
1490
drivers/media/pci/hws/hws_video.c
Normal file
File diff suppressed because it is too large
Load Diff
29
drivers/media/pci/hws/hws_video.h
Normal file
29
drivers/media/pci/hws/hws_video.h
Normal file
|
|
@ -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
|
||||
Loading…
Reference in New Issue
Block a user